🌿 Intermediate ⏱️ 35 min

Inheritance & Interfaces

Inheritance is how Solidity contracts reuse code. OpenZeppelin's entire security library — Ownable, ReentrancyGuard, Pausable, AccessControl — is delivered through inheritance. But inheritance also introduces subtle hazards: storage collisions in proxy upgrades, unimplemented functions deployed by mistake, and constructor logic that doesn't run in upgradeable contracts. This lesson covers the complete inheritance system with full security implications.

📖 Why Inheritance Matters for Security

When you inherit from a contract, you inherit its state variables, functions, and storage layout. Changing inheritance order in an upgradeable contract shifts every storage slot, potentially corrupting all stored data. The Nomad bridge hack ($190M) and several Compound governance incidents trace back to inheritance-related storage layout issues in proxy deployments.

Single and Multiple Inheritance

🧬 Inheritance Syntax
// Single inheritance contract Animal { string public name; function speak() external virtual returns (string memory) { return "..."; } } contract Dog is Animal { function speak() external override returns (string memory) { return "Woof"; } } // Multiple inheritance — order matters (C3 linearization) contract A { function hello() public virtual returns (string memory) { return "A"; } } contract B is A { function hello() public virtual override returns (string memory) { return "B"; } } contract C is A { function hello() public virtual override returns (string memory) { return "C"; } } // C3 linearization: most derived first in `is` list contract D is B, C { function hello() public override(B, C) returns (string memory) { return super.hello(); // calls C.hello() — rightmost in linearization } // MRO for D: D → C → B → A }

virtual and override Keywords

💻 virtual, override, and super
contract Base { uint256 public fee = 100; // virtual: this function CAN be overridden by children function calculateFee(uint256 amount) public view virtual returns (uint256) { return amount * fee / 10000; } } contract Child is Base { // override: MUST specify if this replaces a virtual function function calculateFee(uint256 amount) public view override returns (uint256) { // super.calculateFee() calls Base.calculateFee() uint256 baseFee = super.calculateFee(amount); return baseFee + 50; // adds a flat surcharge } } // Solidity 0.8 REQUIRES override — without it, the compiler errors // In Solidity 0.7 and below, shadowing a parent function without // override was a silent bug — the parent function was replaced without notice // functions NOT marked virtual CANNOT be overridden: function lockedFunction() external { // no 'virtual' — final // Child cannot override this }

Abstract Contracts vs Interfaces

FeatureInterfaceAbstract Contract
State variablesNot allowedAllowed
ConstructorNot allowedAllowed
Implemented functionsNot allowed (all external)Allowed (mix of implemented and abstract)
InheritanceCan inherit interfaces onlyCan inherit contracts and interfaces
Deployable?NoNo (unless all functions implemented)
Primary use caseERC20, ERC721, ERC165 ABI definitionsShared logic with extension points
Modifier accessNot allowedAllowed
📋 Interface vs Abstract Contract Examples
// INTERFACE: only declarations, all external interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); event Transfer(address indexed from, address indexed to, uint256 value); } // ABSTRACT CONTRACT: can have state, implementation, and abstract functions abstract contract BaseToken is IERC20 { mapping(address => uint256) internal _balances; // state variable // Implemented function function balanceOf(address a) external view override returns (uint256) { return _balances[a]; } // Abstract function — child MUST implement function _beforeTransfer(address from, address to, uint256 amount) internal virtual; }

OpenZeppelin Inheritance Patterns

🛡️ OpenZeppelin Pattern — Composing Safety Modules
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/security/Pausable.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; // Multiple inheritance — order matters for storage layout! contract MyToken is ERC20, Ownable, Pausable, ReentrancyGuard { constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) { _mint(msg.sender, 1_000_000e18); } // whenNotPaused modifier from Pausable function transfer(address to, uint256 amount) public override whenNotPaused returns (bool) { return super.transfer(to, amount); } // onlyOwner modifier from Ownable function pause() external onlyOwner { _pause(); } function unpause() external onlyOwner { _unpause(); } }

Storage Layout Inheritance — Critical for Proxies

In an upgradeable proxy pattern, the proxy contract and the implementation contract share the same storage. The storage layout is determined by inheritance order. Adding or reordering inherited contracts in an upgrade breaks all existing data:

💣 Storage Collision via Wrong Inheritance Order
// Version 1 — storage layout: // slot 0: _status (ReentrancyGuard) // slot 1: _owner (Ownable) // slot 2: _balances (ERC20 mapping) contract TokenV1 is ReentrancyGuard, Ownable, ERC20 { } // Version 2 — WRONG UPGRADE: changed inheritance order! // slot 0: _owner (Ownable) ← was _status before — collision! // slot 1: _status (ReentrancyGuard) ← was _owner before // slot 2: _balances (ERC20 mapping) ← OK contract TokenV2_BROKEN is Ownable, ReentrancyGuard, ERC20 { } // Result: _owner slot now reads garbage, reentrancy guard reads wrong slot // Safe practice: use OZ Upgradeable variants + storage gap pattern // Each upgradeable contract reserves slots with: // uint256[50] private __gap; // reserve 50 slots for future variables // Tool: compare storage layouts before upgrading // forge inspect TokenV1 storage-layout // forge inspect TokenV2 storage-layout

Security Implications

🚨 Constructors Don't Run in Proxy Patterns

When a proxy delegates to an implementation, the implementation's constructor has already run — but it ran against the implementation's own storage, not the proxy's storage. If you inherit from Ownable and set owner in the constructor, the proxy's owner storage slot is never set. Always use initializer functions (not constructors) for upgradeable contracts, and use OZ's Initializable base with initializer modifier to prevent double-initialization.

⚠️ Accidentally Deploying an Abstract Contract

If a contract has unimplemented functions (i.e., it should be abstract) but is missing the abstract keyword, Solidity 0.8 will catch this as a compile error. However, in older versions (0.6, 0.7), you could deploy a contract with unimplemented functions — calling those functions would revert unexpectedly at runtime. Always verify all inherited virtual functions are either implemented or the contract is properly marked abstract.

⚠️ selfdestruct Inherited from a Parent

If any contract in your inheritance chain contains a selfdestruct call — even in an internal function — any child contract inherits that capability. An attacker who can call the function containing selfdestruct can permanently destroy the contract and send all its ETH to an arbitrary address. Search for selfdestruct and opcode 0xff across all inherited contracts during an audit.

✏️ Try It — ERC165 Interface Compliance Check
// ERC165 lets contracts declare which interfaces they implement interface IERC165 { function supportsInterface(bytes4 interfaceId) external view returns (bool); } // OZ's ERC721 implements this — you can query on-chain: bytes4 ERC721_INTERFACE_ID = 0x80ac58cd; bool isNFT = IERC165(tokenAddress).supportsInterface(ERC721_INTERFACE_ID); // Implement in your contract: function supportsInterface(bytes4 id) public view virtual override returns (bool) { return id == type(IERC721).interfaceId || id == type(IERC165).interfaceId || super.supportsInterface(id); } // Query with cast: // cast call <nftAddress> "supportsInterface(bytes4)" 0x80ac58cd

Key Takeaways

  • Multiple inheritance is resolved using C3 linearization — the rightmost parent in the is list is the "most derived" and has priority in super calls.
  • Functions must be marked virtual to allow overriding, and override is required in Solidity 0.8 when replacing a parent function — preventing silent shadowing bugs.
  • Interfaces contain only external function declarations and events; abstract contracts can have state, implemented functions, and abstract functions that children must implement.
  • Storage layout is determined by inheritance order — changing that order in a proxy upgrade corrupts all stored data. Use forge inspect storage-layout to compare layouts before deploying upgrades.
  • Constructors in inherited contracts do not run in proxy deployments — use OZ's Initializable and write explicit initialize() functions instead.
  • Audit all inherited contracts for selfdestruct — a single inherited kill switch can permanently destroy a protocol.