Solidity supports multiple inheritance and polymorphism. There are of course many similarities with other high-level languages, but there are also particularities and gotchas you should be aware of when you are using inheritance in Solidity.

Let’s get started right away with some code examples. We will first look at the basics, then I will show you some more advanced concepts and at the end of the article I’ll provide you with a quick summary of the most important rules and concepts about inheritance inn Solidity.

The code samples are very simple. They probably would not make any sense in a real-world contract and they are only used to demonstrate the various concepts around inheritance in Solidity.

I recommend, you copy/paste the code into Remix (https://remix.ethereum.org) and play around with it, so you get a better understanding on the various techniques and concepts presented here.

Basic inheritance in Solidity:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract C1 {
    function f1() private pure returns(uint) {
        return 11;
    }

    function f2() public pure virtual returns(uint) {
        return f1() + 1;
    }

    function f3() public pure virtual returns(uint) {
        return 13;
    }
}

contract C2 is C1 {
    function f3() public pure virtual override returns(uint) {
        return 23;
    }    
}

Here, we have a base contract C1 that defines 2 public and 1 private function. And, we have contract C2 that inherits from C1 (using the “is” keyword). In our derived contract C2, we get access to all non-private members of C1 (functions that are labeled as public, external or internal). This means, we can’t call the private function f1() in our contract C2.

By the way, I’m using double digit integers for the return values of the functions. The first digit indicates the name of the contract – so, for all functions inn C1, I will use 1 as the first digit. The second digit indicates the name of the function – for example, for f3() I will use the digit 3. So, the return value for f3() that is defined in contract C1 will be 13 and so on.

This makes things much easier when we test our contracts, especially when the inheritance hierarchies are getting more complicated. Using this simple trick, we know immediately which function in which contract gets called.

Ok, let’s continue with our inheritance example… In a derived contract, we can also modify the implementations of the functions we inherit from our base contract – in other words, we can override inherited functions. n our example, we are overriding f3() to return 23 instead of 13.

However, this is only possible, if the function in the base contract is labeled with the “virtual” keyword. When we want to change the implementation of such a function in our inheriting contract C2, we have to use the “override” keyword on that function.

If we want other contracts that may inherit from our contract C2 also allow to override that function, we also need to specify the “virtual” keyword. Basically, “virtual” means: an inheriting contract is allowed to override this function. “override” means: we are changing the implementation of  this function.

Give it a try in Remix, deploy C2 and execute f3() – you will get the value 23 and not 13. Execute f2() and you will get 12 (the value from our base contract C1).

Multiple inheritance:

Let’s add the following contracts and functions to our example:

contract C3 {
    function f2() public pure virtual returns(uint) {
        return 32;
    }    
}

contract C4 is C2, C3 {   
    function f2() public pure override(C1, C3) returns(uint) {
         //return 42;
        //return  super.f2();
        return C1.f2();
    }

    function callF3() public pure returns(uint) {
        return f3(); // 23   
    } 

    function callSuperF1() public pure returns(uint) {
        return super.f3(); // 23       
    }
}

C3 is just another base contract, but it contains a function (f2) with the same signature as C1. C4 inherits from C2 (which in turn inherits from C1) and C3. Now, we have a problem, because our new contract C4 inherits the function f2() from two different contracts: C1 (through C2) AND C3.

We have no choice, we need to override f2() in C4, otherwise our contract won’t compile. And, because we are overriding a function that is defined in more than one base contracts, we also need to specify those contracts with the override keyword => override (C1, C3)

In the overridden function f2, we could just return a new value => return 42;

We could call f2() on a contract instance (C1 or C3 in our example) => return C1.f2();

Or, we could use the “super” keyword to call the function one level higher up in the inheritance hierarchy => super.f2();

Inheritance hierarchy:

Now, let’s take a quick look at the remaining functions. In callF3(), we simply call the function f3(), without specifying a specific contract instance. C4 inherits from C2, which inherits from C1. But, f3() is implemented in C1 (return 13) AND in C2 (returns 23).

So, what will happen? The inheritance hierarchy will be searched from the most derived contract to the most base contract and the first match of f3() will be executed.

In our example (C4 is C3, C3…), C3 is the most derived contract, but there is no F3() on C3. Next in the hierarchy is C2 and yes, C2 has an implementation of f3() and that’s the one that will be executed (returns 23). Next in the inheritance hierarchy would be C1, but as we already got a match on C2, there is no need to search any deeper in the hierarchy.

The function callSuperF1() uses the super keyword. We already briefly discussed this case above – super calls the function one level higher up in the inheritance hierarchy. In our case, that’s f3() defined on C2. So, if we are calling super.f3() or f3(), we are getting the same result.

The order of the defined contracts:

The order of the inheriting contracts after the “is” keyword is important. In Solidity, the first contract is considered to be the most base contract and the last contract after the “is” keyword is the most derived contract.

So, writing: C4 is C2, C3… is not the same as C4 is C3, C2…

Replace C4 with the following code:

contract C4 is C2, C3 {   
    function f2() public pure override(C1, C3) returns(uint) {
        return super.f2(); //32
    }
}

Deploy C4 and call f2(). What do you get? C3 is the most derived contract (because it is the last contract listed after the “is” keyword) and in the inheritance hierarchy, we always search from most derived to least derived. So, f3() will be called on C3, which returns 32

Now, replace C4 with the following code:

contract C4 is C3, C2 {   
    function f2() public pure override(C1, C3) returns(uint) {
        return super.f2(); //12
    }
}

Deploy C4 and call f2(). In this example, C2 is the most derived contract. But, there is no implementation of f2() on C2. However, C2 inherits from C1, which is next contract in the inheritance hierarchy that contains an implementation of f2() and that’s the function that will be called (returns 12).

Constructors and inheritance:

If the base contract uses a constructor with one or more arguments to set the initial value(s) of state variable(s), the derived contracts also need to specify all those arguments. There are 2 possibilities to set the arguments of the base contract:

  • Either specify the arguments directly when you derive from the base contract – in brackets, next to the name of the base contract.
  • Or, specify the base contract with the list of the required arguments after the “constructor” keyword of the derived contract

The code sample below demonstrates those 2 possibilities:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract C1 {
    uint public myValue;

    constructor(uint initialValue) {
        myValue = initialValue;
    }    
}

contract C2 is C1(5) {
    // code for C2...       
}

contract C3 is C1 {
    constructor(uint otherValue) C1(otherValue + 1) {        
    } 

    // code for C3...
}

Deploy C2 (which inherits the public state variable “myValue” from C1) and then call the getter function for “myValue”. What do you get? Of course, the number 5, because that’s what you specified as constructor argument for C1.

Deploy C3 (don’t forget to provide a value for the argument “otherValue”) and call again “myValue”. This time you are getting the number you specified when you deployed C3 + 1.

You can use the first option, if the constructor argument is a constant. The second way has to be used if the constructor argument(s) of the base contract depend on those of the derived contract.

Constructors and more complex inheritance hierarchies:

No matter, in which order you are listing the constructors of the contracts from which you are inheriting from, they will always be executed in the linearized order from the most base-like contract to the most derived contract.

Let’s look at some examples:

contract C is A, B {

constructor() A() B() {} …

The constructors are executed in the following order: A => B => C , because A is the most base and C is the most derived contract.

contract C is  B, A {

constructor() A() B() {} …

The constructors are executed in the following order: B => A => C , because now, B is the most base-like contract (B is the first contract listed after the “is” keyword).

Here is a quick summary of the most important rules about inheritance in Solidity:
  • You can declare a state variable in a derived contract only if no non-private state variable with the same name has been declared in any of its base contracts.
  • If you want to allow an inheriting contract to override a function, you need to specify the “virtual” keyword on that function. If you want to override a function of a contract you are inheriting from, you need to use the “override” keyword on that function.
  • When you override an external function, you are allowed to change the visibility modifier to public. Also, the mutability modifier can be changed from pure to view and from non-payable to either view or pure.
  • All base contracts that define the same function and have not yet been overridden by another base contract need to specified on that function in the derived contract after the “override” keyword.
  • If the constructor of a base contract has argument(s), they need to be provided with the derived contract. You can either specify the arguments directly on the contract name you are deriving from in the inheritance list or you can use a “constructor-modifier” for the base contract next to the constructor keyword of the derived contract.
  • When you use multiple inheritance, the order in which the base contracts are listed is important. The contracts have to be listed in the order from the most base-like to the most derived contract.
  • When a function is called that is defined in several base contracts, the given contracts are searched from right to left (from the most derived to the most base), until a first match of that function is found.
  • The constructors of the contracts in the inheritance hierarchy are always executed in the linearized order from most base-like to most derived contract, regardless of the order in which those constructors are listed after the inheriting contract’s constructor.
  • None of the following pairs of functions, modifiers and events (including inherited ones) in a contract can have the same name:
    • A function and a modifier
    • A function and an event
    • An event and a modifier