iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…