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.
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
// 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
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
| Feature | Interface | Abstract Contract |
|---|---|---|
| State variables | Not allowed | Allowed |
| Constructor | Not allowed | Allowed |
| Implemented functions | Not allowed (all external) | Allowed (mix of implemented and abstract) |
| Inheritance | Can inherit interfaces only | Can inherit contracts and interfaces |
| Deployable? | No | No (unless all functions implemented) |
| Primary use case | ERC20, ERC721, ERC165 ABI definitions | Shared logic with extension points |
| Modifier access | Not allowed | Allowed |
// 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
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:
// 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-layoutSecurity Implications
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.
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.
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.
// 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)" 0x80ac58cdKey Takeaways
- Multiple inheritance is resolved using C3 linearization — the rightmost parent in the
islist is the "most derived" and has priority insupercalls. - Functions must be marked
virtualto allow overriding, andoverrideis 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-layoutto compare layouts before deploying upgrades. - Constructors in inherited contracts do not run in proxy deployments — use OZ's
Initializableand write explicitinitialize()functions instead. - Audit all inherited contracts for
selfdestruct— a single inherited kill switch can permanently destroy a protocol.