Interfaces and Inheritance

B is A

As we have seen in previous sections, Solidity is an object-oriented programming language which allow us to create classes (contracts) and objects (deployments of contracts). However, object-oriented programming offers much more than this. For example, one of the features that OOP provides is inheritance; classes whose logic can be derived from other classes.

Solidity, in alignment with OOP, also allows us to utilize inheritance with respect to contracts. Consider a situation where we have two contracts A and B, and we want for B to inherit the state/logic of A. Below is the syntax of how we would do that in Solidity:

contract A {}

contract B is A {}

To demonstrate the inheriting property of smart contracts, consider the following code:

contract A {

    uint age = 21;
    
    function getName() public pure returns(string memory) {
        return "A";
    }

}

contract B is A {

    function getAge() public view returns(uint) {
        return age;
    }

}

In contract A, we have the state variable age = 21 and the function getName, which returns the name of the contract of the contract. When we create B, B inherits the function getName. Furthermore, B also inherits the state variable age, which is made evident by the function getAge.

Overriding Functions

Although inheritance allows us to extract common logic to a single smart contract, there are instances where we might want to define our own logic for a function rather than using the implementation provided by the parent function.

To override a function, our first intuition might be as follows:

contract A {

    function getName() public pure virtual returns(string memory) {
        return "John";
    }

}

contract B is A {

    function getName() public view returns(string memory) {
        return "Bob";
    }

}

However, if you attempt to compile this the code above, you will get a very angry message from the compiler telling you that you need to declare getName to be override-able. And indeed, this is what we're forgetting:

contract A {

    function getName() public pure virtual returns(string memory) {
        return "John";
    }

}

contract B is A {

    function getName() public view override returns(string memory) {
        return "Bob";
    }

}

Examining the code above on a line-by-line basis:

  • Line 3: we mark getName in the parent contract as virtual: this tells the compiler that children contracts can override the logic of getName

  • Line 11: we mark getName in the child contract as override: this tells the compiler that we are overriding the logic defined in the parent contract with our own

The super Keyword

There exists instances where, although we want to rely on the logic of a parent contract, we also want to extend the functionality of a function without having to rewrite code. The following example makes this dilemma clear:

Everyone who works at company A is an employee by default. Furthermore, everyone at company A receives a salary of $100,000. However, the CEO, in addition to already being an employee by default, also receives a bonus of $25,000.

In the example above, if we represent employees and the CEO as contracts, the associated code is as follows:

contract Employee {

    uint salary;

    function setSalary() public virtual {
        salary = 100000;
    }

}

contract CEO is Employee {

    uint bonus;

    function setSalary() public override {
        salary = 100000;
        bonus = 25000;
    }

}

Notice that in line 16, we are copying the same code as in line 6 (i.e. we are duplicating code). Rather than duplicating code, which can be a source of bugs later on, we can utilize the super keyword to reuse the logic of the parent implementation.

contract Employee {

    uint salary;

    function setSalary() public virtual {
        salary = 100000;
    }

}

contract CEO is Employee {

    uint bonus;

    function setSalary() public override {
        super.setSalary();
        bonus = 25000;
    }

}

In line 16, we are calling the setSalary which pertains to the parent contract (i.e. we are setting the salary of the CEO). Afterwords, in line 17, we are setting the bonus of the CEO to be equal to 25000.

Overloading Functions

Consider the following code:

contract A {

    function returnArg(string memory name) public pure returns(string memory) {
        return name;
    }

}

contract B is A {

    function returnArg(uint num) public pure returns(uint) {
        return num;
    }

}

Both contract A, B contain the function returnArg which returns the argument passed in. However, A's implementation of returnArg deals with strings while B's implementation of returnArg deals with uints. With our current understanding of functions and inheritance, this shouldn't compile, right?

It turns out, however, that this is completely valid Solidity code. The reason for this is that although both implementations of returnArg are different, the returnArg found in A is actually not the same function as returnArg in B.

To understand why this is the case, we must introduce the idea of function selectors - the ID of a function. Although we will dive much deeper into this when we discuss the EVM, what you need to know is that when compiled, each function name is associated with an number. How is this number generated? The ID of a function is dependent on the type function parameters and the ordering of the parameters. As an example, the following demonstrates this idea firsthand:

returnArg(uint age) => i
returnArg(uint year) => i
returnarg(int age) => j
returnArg(uint age, string memory name) => k
returnArg(string memory name, uint age) => m

As it might have become evident, there is no need to override returnArg because returnArg in contract A has a different function selector than returnArg in contract B. In this scenario, we are overloading returnArg with two different implementations.

Interfaces

In object-oriented programming languages, a way in which to hide implementation details from users of a class is via the use of interfaces. Likewise, in Solidity, we can also utilize interfaces to communicate to users the functionality of any inheriting smart contract. The syntax of a contract interface is as follows:

interface interfaceName {}

To motivate our understanding of interfaces, consider the following smart contract:

contract Miner {

    bool isWorking;
    
    function clockIn() public {
        isWorking = true;
    }
    
    function clockOut() public {
        isWorking = false;
    }
    
    function mine() public {}

}

If we only wanted to understand the behavior of the Miner contract rather than the implementation details, we could write an interface for the Miner contract:

interface IMiner {

    function clockIn() external {}
    
    function clockOut() external {}
    
    function mine() external {}

}

There's a couple of things to note from this example:

  • Why are all functions marked as external? Recall that external functions can only be called by other accounts. Therefore, by marking all functions in an interface as external, we are signaling that the functions a contract implements via an interface are meant to be called by other accounts.

  • Although all functions are marked in an interface as external, contracts that implement an interface can change the visibility of said functions as public. However, they cannot be changed to private or internal.

Abstract Contracts

In our final section on inheritance, we cover the concept of abstract contracts - contracts where there is at least one function that is missing implementation details.

Consider the following scenario where we wish to implement a Mathematician contract; although all chefs are required to remember a specific equation (in this case, the average of two numbers), they are also required to remember their own specific equation. Already, it is obvious that there will be some overlapping logic (i.e. all chefs will have a function to compute the average of two numbers), there will also be mutually exclusive logic in each mathematician contract.

interface IMathematician {

    function computeAverage(int x, int y) external {}
    
    function computeSpecialEquation() external {}

}

Although the IMathematician interface tells us about the behavior of a mathematician, it still does not allow us to implement the overlapping logic that all mathematicians have. In this case, it would be best to use an abstract contract. The syntax for an abstract contract is as follows:

abstract contract abstractContractName {}

The code below shows how we can use abstract contracts to create our Mathematician contract:

abstract contract Mathematician {
    function computeAverage(int x, int y) public {
        return (x + y) / 2;
    }
    
    function computeSpecialEquation() public {}
}

Last updated