The nitty-gritty of Ethereum and Solidity: Inheritance.

Alberto Molina
Coinmonks
6 min readNov 18, 2023

--

In Solidity, smart contracts can inherit from one or multiple other smart contracts. This common technique in Object-Oriented Programming (OOP) languages is highly valuable for extending and reusing functionality.

However, it is crucial to comprehend how the Solidity compiler handles smart contract inheritance to prevent unexpected behavior. Understanding this process is essential for addressing the potentially troublesome ‘Linearization of inheritance graph impossible’ error.

Note that, regardless of the number of parent contracts a child contract may have, only one smart contract, encompassing all aggregated functionality, is deployed. While this might seem obvious, it is crucial to remember that Ethereum smart contracts are limited to 24KB. This limitation implies that heavy inheritance might not be feasible.

Simple inheritance

Let’s start with the simplest form of inheritance: single or simple inheritance. In this case, contracts inherit from just one parent contract, creating a straightforward and linear inheritance hierarchy.

Polymorphism

Solidity supports two types of polymorphism :

  • Overloading: In Solidity, two methods can share the same name, provided they have distinct input parameter data types. From the perspective of the Solidity compiler, these two methods are effectively assigned different identifiers. This is because a method’s identifier is its selector, represented by the first 4 bytes of its signature’s hash (calculated using keccak256). The method’s signature comprises its name and the data types of its input parameters.
contract Contract{
// signature : keccak256("method(uint256)")
function method(uint256 _input) external pure returns(uint256 output_){
output_ = 3 * _input;
}

// signature : keccak256("method(uint256,uint256)")
function method(uint256 _input, uint256 _factor) external pure returns(uint256 output_){
output_ = _factor * _input;
}
}
  • Overriding: In Solidity, it is possible for multiple methods to share the same name and input parameters, as long as they override each other. When a function is called, it will always execute the function with the same name in the most derived contract within the inheritance hierarchy. To replace methods marked with the ‘virtual’ keyword in its parent contracts, a derived contract must use the ‘override’ keyword. Additionally, the derived contract has the flexibility to invoke methods from its parents using either ‘super.functionName()’ (when calling the contract immediately above it in the inheritance hierarchy) or ‘contractName.functionName()’ (when calling a contract further up the inheritance hierarchy) as long as those methods are NOT external.
contract ParentContract_1 {

function method(uint256 _input) public virtual pure returns(uint256 output_){
output_ = 2 * _input;
}

}

contract ParentContract_2 is ParentContract_1{

function otherMethod(uint256 _input, uint256 _factor) public virtual pure returns(uint256 output_){
output_ = 3 * _factor * _input;
}
}

contract ChildContract is ParentContract_2{
// calls ParentContract_1 method
function method(uint256 _input) public override pure returns(uint256 output_){
output_ = 4 * ParentContract_1.method(_input);
}
// calls ParentContract_2 otherMethod
function otherMethod(uint256 _input, uint256 _factor) public override pure returns(uint256 output_){
output_ = 5 * super.otherMethod(_input, _factor);
}
}

Functions

When overriding functions, the visibility can only be modified from ‘external’ to ‘public,’ and the mutability can only be changed from ‘view’ to ‘pure.’ It’s important to note that ‘private’ functions cannot be overridden because they are not inherited by derived contracts; these functions are intended to be used only within the contract that defines them.

Additionally, if a contract inherits from an ‘abstract’ contract (one that does not implement all its methods), the inheriting contract must also be marked as ‘abstract’ unless it provides implementations for all the unimplemented methods.

Modifiers

Modifiers can also be overridden just like functions, however they cannot be overloaded. Overridden modifiers must be marked as “virtual” as overriding modifiers must be marked as “override”.

contract ParentContract_1 {

modifier mod(uint256 _input) virtual {
if(_input > 10) revert("More than 10");
_;
}

}

contract ParentContract_2 is ParentContract_1{

modifier mod_2(uint256 _input, uint256 _factor)virtual {
if(_factor * _input > 20) revert("More than 20");
_;
}
}

contract ChildContract is ParentContract_2{
modifier mod(uint256 _input) override {
if(_input > 100) revert("More than 100");
_;
}

modifier mod_2(uint256 _input, uint256 _factor)override {
if(_factor * _input > 200) revert("More than 200");
_;
}

}

Events

Unlike modifiers, events cannot be overridden, as it wouldn’t make sense given that events do not contain code. However, events can be overloaded instead.

contract ParentContract_1 {
event E_1(uint256);
}

contract ChildContract is ParentContract_1{
event E_1(uint256, bool);
}

Constructors

The constructor is an optional function within a contract that executes during the contract’s deployment and is subsequently removed from the deployed bytecode.

When derived contracts inherit from contracts that have constructors, the execution of these constructors follows the inheritance hierarchy, beginning with the eldest parent and concluding with the most derived child.

If a parent contract defines a constructor with input parameters, all derived contracts must either provide those parameters or declare the contract as abstract.

There are two ways a derived contract can execute its parent constructor method:

  • When defining the inheritance. Input parameters must be defined at compile time, which means they can not be provided when deploying the contract.
  • When defining its own constructor. Input parameters can be provided at deployment time.
contract ParentContract_1 {
uint256 public i;

constructor(uint256 _i) {
i = _i;
}

}

// parent constructor invoked when defining the inheritance.
contract ParentContract_2 is ParentContract_1(1){
uint256 public j;

constructor(uint256 _j) {
j = _j;
}
}

contract ChildContract is ParentContract_2{
uint256 public p;

// parent constructor invoked when defining the constructor method
constructor(uint256 _p) ParentContract_2(2 * _p){
p = _p;
}
}

Storage variables

Storage variables are inherited from parent to child contracts. The Solidity compiler begins assigning storage slots from the contract furthest up in the inheritance hierarchy to the most derived contract.

It’s crucial to note that derived contracts cannot use variable names that have already been utilized by parent contracts, and can only access storage variables from their parent contracts as long as these are not defined as “private”.

contract ParentContract_1 {
uint256 private i;
uint256 public j;
}

/**
ChildContract storage slot will be:
- slot 0: i
- slot 1: j
- slot 2: p
**/
contract ChildContract is ParentContract_1{
uint256 public p;

// these methods will not compile because they are accessing a private variable from its parent contract
function set_I(uint256 _i) public {
i = _i;
}

function get_I() public view returns (uint256){
return i;
}

// these methods will compile
function set_J(uint256 _j) public {
j = _j;
}

function get_J() public view returns (uint256){
return j;
}
}

Multiple inheritance

Up to this point, our focus has been on simple inheritance; however, Solidity allows a contract to inherit from multiple parents. When this occurs, the Solidity compiler must establish an inheritance hierarchy. Essentially, it transforms the multiple inheritance into a simple inheritance and then applies all the concepts we have discussed earlier. This transformation is crucial to avoid the “diamond problem,” where a method is inherited from two or more different parents, resulting in an unsolvable ambiguity.

The process of defining the inheritance hierarchy is known as “linearization.” It involves determining the order in which parent contracts must be positioned. Linearization can be quite intricate and, in some cases, even impossible. In such instances, the compiler will fail and return the infamous “Linearization of inheritance graph impossible” error.

Linearization

Solidity employs the ‘C3 Linearization’ algorithm to establish the inheritance hierarchy (for more info you can check this wikipedia page). The sequence in which parent classes are specified after the ‘is’ keyword is crucial. As a general rule of thumb, list parent classes from the ‘most base-like’ to the ‘most derived’.

contract A {}
contract B {}
contract C {}

/**
This two derived contract might not be identical
since their inheritance hierarchy will differ
**/

// C overrides B, which overrides A
contract D is A, B, C {}

// A overrides C, which overrides B
contract E is B, C, A {}

When a method defined in multiple parent contracts is called, the search among the specified bases occurs from right to left, stopping at the first match.

If a derived contract overrides a method present in multiple parent contracts, it must specify the names of the parent contracts in the ‘override’ clause.

contract A {
function method() virtual public{}
}
contract B {
function method()virtual public{}
}

contract C is A, B {
function method()override(A, B) public{}
}

Something that might be tricky is the way constructors are executed in the case of multiple inheritance. Solidity permits constructors from parent contracts to be written in any order; however, the execution will consistently follow the inheritance hierarchy.

contract A {
constructor() {}
}

contract B {
constructor() {}
}

// Constructors are executed in the following order:
// 1 - A
// 2 - B
// 3 - C
contract C is A, B {
constructor() A() B() {}
}

// Constructors are executed in the following order:
// 1 - B
// 2 - A
// 3 - D
contract D is B, A {
constructor() B() A() {}
}

// Constructors are still executed in the following order:
// 1 - B
// 2 - A
// 3 - E
contract E is B, A {
constructor() A() B() {}
}

--

--