Introduction to Solidity : Inheritance

For the last article of this series, we will take a look at inheritance and others concepts like polymorphism, functions overriding and modifiers overriding.

Inheritance

Inheritance is a way to create a new contract based on an existing one. The new contract will inherit all the functions and variables of the parent contract that are not private. These functions and variables can't accessed externally via this.

Solidity supports inheritance but also polymorphism. This means that a function call (internal and external) always executes the function of the same name (and parameter types) in the most derived contract in the inheritance hierarchy. This has to be explicitly enabled on each function in the hierarchy using the virtual and override keywords. We will see the use of these keywords in the next section.

It is possible to call functions further up in the inheritance hierarchy internally by explicitly specifying the contract using ContractName.functionName() or using super.functionName() if you want to call the function one level higher up in the flattened inheritance hierarchy.

Let's see this with an exemple :

pragma solidity ^0.8.0;

contract Animal {
    function makeSound() external virtual returns (string memory) {
        return "Animal sound";
    }
}

contract Dog is Animal {
    function makeSound() external override returns (string memory) {
        return "Bark";
    }

    function makeAnimalSound() external returns (string memory) {
        return super.makeSound();
    }
}

In this exemple, we have a contract Animal that has a function makeSound that returns a string. We also have a contract Dog that inherits from Animal and overrides the makeSound function. We can see that the makeSound function is marked as virtual in the Animal contract and as override in the Dog contract. This is mandatory to be able to override a function in a child contract.

So when we call the makeSound function in the Dog contract, we will get the string "Bark" as a result. But when we call the makeAnimalSound function in the Dog contract, we will get the string "Animal sound" as a result. This is because we are calling the makeSound function of the Animal contract. They keyword super is used to call the function one level higher up in the flattened inheritance hierarchy.

To make your contract inherit from another contract, you have to use the is keyword. You can inherit from multiple contracts by separating them with a comma.

contract Dog is Animal, Pet {
    // ...
}

You can't declare state variables x in a derived contract if x is already declared in a base contract.

If you want the function you are overriding to be also overridable in a child contract, you have can mark it as virtualtoo like this :

contract Animal {
    function makeSound() external virtual returns (string memory) {
        return "Animal sound";
    }
}

contract Dog is Animal {
    function makeSound() external virtual override returns (string memory) {
        return "Bark";
    }
}

contract Poodle is Dog {
    function makeSound() external override(Dog, Animal) returns (string memory) {
        return "Bark bark";
    }
}

It's also possible to create a contract with the abstract keyword. Abstract Contract are only used to provide an interface that is know to the compiler but not implemented.

Here's an exemple :

pragma solidity ^0.8.0;

abstract contract Config {
    function lookup(uint id) public virtual returns (address adr);
}

contract Named {
    Config public config;

    constructor() {
        config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
    }

    function lookup(uint id) external returns (address adr) {
        return config.lookup(id);
    }
}

In this example, the Named contract has a public config variable that holds an instance of the Config contract. The lookup() function in Named simply calls the lookup() function on the config variable.

This allows the Named contract to use the lookup() function from the Config contract, even though Config is an abstract contract and cannot be instantiated directly.

So, to summarize, while you cannot use the abstract keyword to define the type of a variable directly, you can use an abstract contract to define the type of a contract variable, as shown in this example.

Abstract contracts can also serves as a base contract that others contracts can inherit from and implement its abstract functions. Abstract functions are required to be implemented by any derived contracts before they can be deployed on the blockchain.

If we take the first exemple of this article, we can make the Animal contract abstract like this :

pragma solidity ^0.8.0;

contract Animal {
    function makeSound() external virtual returns (string memory)
}

contract Dog is Animal {
    function makeSound() external override returns (string memory) {
        return "Bark";
    }
}

This allow us to create a Dog contract that inherits from the Animal contract and which need to implement the makeSound function. This is a way to force the implementation of a function in a child contract.

And finally if the constructor of a base contract takes arguments it could be provided in two ways :

  • In the header of the derived contract
  • In the constructor of the derived contract
pragma solidity ^0.8.0;

// Define a base contract with a constructor that takes an argument
contract Base {
    uint public value;

    constructor(uint _value) {
        value = _value;
    }
}

// Define a derived contract that declares the constructor arguments in its header
contract Derived is Base(42) {
}

// Define a derived contract that calls the constructor of the base contract with an argument
contract Derived is Base {
    constructor(uint _value) Base(_value) {
    }
}

Functions overriding

Like we have seen previously in the inheritance section, it is possible to override a function in a child contract. To do this the base function must be marked as virtual and the child function must be marked as override.

The overriding function may only change the visibility of the overridden function from external to public. The mutability may be changed to a more strict one following the order: nonpayable can be overridden by view and pure. view can be overridden by pure. payable is an exception and cannot be changed to any other mutability.

For multiple inheritance, the most derived base contracts that define the same function must be specified explicitly after the override keyword. In other words, you have to specify all base contracts that define the same function and have not yet been overridden by another base contract (on some path through the inheritance graph). Additionally, if a contract inherits the same function from multiple (unrelated) bases, it has to explicitly override it:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract Base1
{
    function foo() virtual public {}
}

contract Base2
{
    function foo() virtual public {}
}

contract Inherited is Base1, Base2
{
    // Derives from multiple bases defining foo(), so we must explicitly
    // override it
    function foo() public override(Base1, Base2) {}
}

If you do not mark a function that overrides as virtual, derived contracts can no longer change the behaviour of that function.

Functions with the private visibility cannot be virtual.

Finally a public state variable can override a an external function if parameters and return types of the function match the getter function of the state variable.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract Base
{
    function foo() virtual external returns (uint) { return 7; }
}

contract Inherited is Base
{
    uint public override foo;
}

Modifier overriding

Like functions, modifiers can also be overridden in a child contract. To do this the base modifier must be marked as virtual and the child modifier must be marked as override.

Conclusion

That it's for this article about inheritance in Solidity and for this introducing you to Solidity language. I hope you enjoyed it and that you learned something new. If you want to learn more about Solidity, I invite you to read the Solidity documentation.