Inheritance in Solidity
Solidity lets contracts inherit from other contracts. The child contract gets all the public and internal functions and state variables of the parent. This lets you split logic across multiple focused contracts and reuse it across your codebase without copying code.
This post covers the four core mechanisms: is, virtual, override, and super. It also covers multiple inheritance, how Solidity resolves conflicts between parents, and a few mistakes worth knowing about before they bite you.
The is Keyword
is establishes the inheritance relationship. Write contract Child is Parent and the child contract inherits everything that is not private.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Animal {
string public name;
function makeSound() public pure virtual returns (string memory) {
return "Some sound";
}
}
contract Dog is Animal {
function makeSound() public pure override returns (string memory) {
return "Woof!";
}
}
Dog gets name from Animal for free. It replaces makeSound with its own version.
virtual and override
These two keywords always work together.
virtual marks a function in the parent as replaceable. Without it, child contracts cannot override the function. Solidity will throw a compiler error if they try.
override marks a function in the child as a replacement for a parent's virtual function. Without it, the compiler will also error. This is intentional. Both sides must opt in.
contract Base {
// Child contracts can replace this
function calculate() public pure virtual returns (uint) {
return 5;
}
// Child contracts cannot replace this
function fixedFunction() public pure returns (uint) {
return 10;
}
}
contract Derived is Base {
function calculate() public pure override returns (uint) {
return 99;
}
}
If you write a function in the child that matches a parent function's name but forget override, Solidity errors. This prevents accidental shadowing.
The super Keyword
super calls the function from the parent contract. Use it when you want to extend the parent's behavior rather than fully replace it.
contract Counter {
uint public count;
function increment() public virtual {
count += 1;
}
}
contract DoubleCounter is Counter {
function increment() public override {
super.increment(); // runs parent logic
super.increment(); // runs it again
}
}
Without super, calling increment() in the child would only run the child's logic. The parent's logic would be skipped entirely.
Multiple Inheritance
A contract can inherit from more than one parent.
contract A {
function foo() public pure virtual returns (string memory) {
return "A";
}
}
contract B {
function bar() public pure virtual returns (string memory) {
return "B";
}
}
contract C is A, B {
function getValues() public pure returns (string memory, string memory) {
return (foo(), bar());
}
}
C has access to both foo from A and bar from B.
The situation gets more complex when two parents define the same function name.
contract X {
function getValue() public pure virtual returns (uint) {
return 1;
}
}
contract Y {
function getValue() public pure virtual returns (uint) {
return 2;
}
}
contract Z is X, Y {
// Must override explicitly, listing both parents
function getValue() public pure override(X, Y) returns (uint) {
return 3;
}
}
When a function exists in multiple parents, you must list all of them inside override(...). If you do not override at all, the compiler refuses to compile because it cannot decide which parent's version to use.
Inheritance Order and C3 Linearization
Solidity resolves multiple inheritance using C3 linearization. The practical rule: list parent contracts from most general to most specific, left to right. Solidity processes them right to left internally.
contract Base1 {
function greet() public pure virtual returns (string memory) {
return "Hello from Base1";
}
}
contract Base2 {
function greet() public pure virtual returns (string memory) {
return "Hello from Base2";
}
}
contract Derived is Base1, Base2 {
function greet() public pure override(Base1, Base2) returns (string memory) {
return "Hello from Derived";
}
}
This matters most when using super in a chain of contracts. Solidity will walk the chain in linearized order.
contract A {
event Log(string message);
function process() public virtual {
emit Log("A processed");
}
}
contract B is A {
function process() public virtual override {
emit Log("B processed");
super.process();
}
}
contract C is A {
function process() public virtual override {
emit Log("C processed");
super.process();
}
}
contract D is B, C {
function process() public override(B, C) {
super.process();
}
}
When D calls super.process(), the linearized order is D, C, B, A. So C.process() runs first, then B.process(), then A.process(). Each call to super passes control to the next in that chain.
The linearization order is deterministic and computed at compile time. Solidity will reject inheritance hierarchies that cannot be linearized cleanly.
A Practical Example
Here is a pattern you will see often in real contracts: a Token that is both ownable and pausable.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function transferOwnership(address newOwner) public virtual onlyOwner {
owner = newOwner;
}
}
contract Pausable {
bool public paused;
function pause() public virtual {
paused = true;
}
function unpause() public virtual {
paused = false;
}
}
contract Token is Ownable, Pausable {
mapping(address => uint) public balances;
function transfer(address to, uint amount) public {
require(!paused, "Contract is paused");
balances[msg.sender] -= amount;
balances[to] += amount;
}
// Override to restrict to owner only
function pause() public override onlyOwner {
super.pause();
}
function unpause() public override onlyOwner {
super.unpause();
}
}
Token inherits owner, onlyOwner, and transferOwnership from Ownable. It inherits paused, pause, and unpause from Pausable. It then narrows the pause functions with the onlyOwner modifier, and uses super to call the original implementations.
Common Mistakes
State variable shadowing. You cannot declare a state variable in a child with the same name as one in a parent. The compiler will error.
contract Parent {
uint public value = 10;
}
contract Child is Parent {
// Compiler error: cannot shadow parent's state variable
// uint public value = 20;
}
Constructor arguments. Parent constructors run before child constructors. If a parent requires constructor arguments, you must pass them in the child.
contract Parent {
uint public value;
constructor(uint _value) {
value = _value;
}
}
// Pass the argument directly in the inheritance list
contract Child is Parent(100) {
constructor() {
// runs after Parent constructor
}
}
Visibility. You can only override public or external functions. private functions are not visible to child contracts at all. internal functions are visible and can be called, but they cannot be marked virtual and overridden unless the parent explicitly allows it.
Inheritance in Solidity gives you a clean way to separate concerns across contracts and reuse logic without duplication. The four keywords (is, virtual, override, super) each serve a distinct role, and understanding how they interact with multiple inheritance and linearization will save you from confusing compiler errors when contracts get more complex.