Inheritance
Solidity supports single + multiple inheritance for contracts. The most common pattern is composing battle-tested base contracts (OpenZeppelin’s Ownable, ERC20, Pausable) instead of writing them from scratch — but the linearisation rules and the constructor order are subtle and have caused real exploits.
is, virtual/override, C3 linearisation
EXAMPLE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// 1) Single inheritance
contract Base {
uint public x;
constructor(uint _x) { x = _x; }
function describe() public view virtual returns (string memory) {
return 'Base';
}
}
contract Child is Base {
constructor() Base(42) { }
function describe() public pure override returns (string memory) {
return 'Child';
}
}
// 2) virtual + override (required in modern Solidity)
// • The base function must be marked 'virtual' to allow overriding
// • The child function must be marked 'override'
// • If the child wants to be further overridden, it ALSO declares 'virtual'
contract Grand is Child { } // implicitly inherits Child.describe(); cannot override again
// 3) Multiple inheritance + diamond problem
interface A { function foo() external view returns (uint); }
interface B { function foo() external view returns (uint); }
contract Impl is A, B {
function foo() public pure override(A, B) returns (uint) {
return 1;
}
}
// You MUST list every base that declares the function in 'override(...)'.
// 4) C3 linearisation — the order of bases matters
contract X { function role() public pure virtual returns (string memory) { return 'X'; } }
contract Y is X { function role() public pure virtual override returns (string memory) { return 'Y'; } }
contract Z is X { function role() public pure virtual override returns (string memory) { return 'Z'; } }
// 'most base to most derived' rule: list parents from most general to most specific.
// The order in 'is Y, Z' determines which override wins — last listed has precedence.
contract YZ is Y, Z {
function role() public pure override(Y, Z) returns (string memory) {
return Z.role(); // pick explicitly
}
}
// Many exploits have come from authors not realising which contract's logic 'super' resolves to.
// 5) Constructors run base-to-derived, in C3 order
contract WithCtor is Y, Z {
constructor() Y() Z() { } // order of base() calls is independent of inheritance order
}
// Surprises: a base contract's logic can run BEFORE your child contract's storage is initialised.
// Use Initializable patterns + initializer functions instead of constructors for upgradeable contracts.
// 6) super — call the next contract up the chain
contract A1 {
event Run(string from);
function run() public virtual { emit Run('A1'); }
}
contract B1 is A1 {
function run() public virtual override { super.run(); emit Run('B1'); }
}
contract C1 is B1 {
function run() public override { super.run(); emit Run('C1'); }
}
// C1.run() emits A1, B1, C1 in that order.
// 7) Abstract contracts
abstract contract Stoppable {
bool public stopped;
modifier whenLive() { require(!stopped, 'stopped'); _; }
function _stop() internal virtual { stopped = true; }
}
contract Counter is Stoppable {
uint public n;
function inc() external whenLive { unchecked { n++; } }
function stop() external { _stop(); }
}
// 8) Interfaces — pure abstraction (no state, no logic)
interface IFlashReceiver {
function onFlashLoan(address initiator, address token, uint amount, uint fee, bytes calldata data)
external returns (bytes32);
}
contract MyReceiver is IFlashReceiver {
function onFlashLoan(address, address, uint, uint, bytes calldata) external pure returns (bytes32) {
return keccak256('IFlashReceiver.onFlashLoan');
}
}
// 9) Composing OpenZeppelin contracts — the everyday pattern
import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts/utils/Pausable.sol';
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
contract MyToken is ERC20, Ownable, Pausable, ReentrancyGuard {
constructor() ERC20('My Token', 'MTK') Ownable(msg.sender) { }
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
function _update(address from, address to, uint value) internal override whenNotPaused {
super._update(from, to, value);
}
}
// 10) Library inheritance — using a library is composition, not inheritance
library Math {
function max(uint a, uint b) internal pure returns (uint) { return a >= b ? a : b; }
}
contract UseLib {
using Math for uint;
function pick(uint a, uint b) external pure returns (uint) { return a.max(b); }
}
// 11) Upgradeable contracts — initializer functions, not constructors
import '@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol';
import '@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol';
contract MyUpgradeable is Initializable, OwnableUpgradeable {
function initialize(address initialOwner) public initializer {
__Ownable_init(initialOwner);
}
}
// Constructors can't run when a contract is behind a proxy, so use the
// initializer pattern. Use OpenZeppelin's Upgrades plugin to verify storage layout safety.
// 12) Function modifiers — inherited and composable
contract OnlyAfter {
uint public unlockAt;
constructor(uint t) { unlockAt = t; }
modifier afterUnlock { require(block.timestamp >= unlockAt, 'too early'); _; }
}
contract Vault is OnlyAfter {
constructor(uint t) OnlyAfter(t) { }
function withdraw() external afterUnlock { /* … */ }
}
// 13) Inheritance vs composition
// • Inheritance: share storage, modifiers, base implementations (Ownable, AccessControl)
// • Composition: deploy a separate contract and store its address; call it via interface
// • Libraries: stateless utility functions, deployed once and linked
// Prefer composition for things you'd swap independently. Reach for inheritance for canonical patterns.
// 14) Security flags from auditors
// • Diamond inheritance with state-mutating logic in multiple parents — confirm storage layout
// • super.* call in an override that ALSO does access checks — guard ordering matters
// • Mixing initializer-style upgradeable with constructor-style — storage clashes on upgrade
// • Multiple inheritance with the same private variable name — last writer wins in storage layout
// • Modifier inherited from a child of two different bases with the same modifier name — pick one
// 15) Common bugs
// • Forgetting virtual on a base function you intend to override → compile error
// • Forgetting override on the child → compile error
// • Wrong order in 'is A, B' → C3 picks unintended impl when calling super.*
// • Base constructor side effects relying on derived state not yet initialised → exploits
// • Adding storage to a base in a future upgrade → shifts storage slots in proxy patterns
// • Inheriting from too many base contracts → code paths nobody mentally tracks → bugs land in audit
Why it matters
Compose battle-tested base contracts — OpenZeppelin’s Ownable, Pausable, ReentrancyGuard, ERC20 — rather than rolling your own. Pay attention to the order in is A, B: C3 linearisation decides which override super hits, and the difference between “works” and “burned in an audit” is often that one comma.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
abstract contract Owned {
address public owner;
constructor() { owner = msg.sender; }
modifier onlyOwner() { require(msg.sender == owner); _; }
}
contract Token is Owned { /* … */ }
Try it Yourself »
Discussion
Loading…