Inheritance in Solidity demystified with analysis of depth-first and breadth-first resolution of contracts
Solidity is the most used programming language for writing Smart Contracts for the Ethereum Blockchain. In this article, we will explore one important feature of the Solidity language and that is inheritance.
Solidity’s inheritance is inspired from Python’s and uses C3 Linearization, also called Method Resolution Order (MRO) for the resolution of Base Classes (Contracts in Solidity). Our topic of discussion for this article is going to be about how the Base Smart Contracts are resolved in a particular order by the C3 Linearization algorithm to get the final Smart Contract. The understanding of the C3 Linearization algorithm is not required to understand inheritance and is out of the scope of this article.
We will layout our Smart Contracts in a Diamond based inheritance graph to get a better picture of the resolution of Base Contracts.
We have four contracts in the above file, namely A, B, C and D. The contract D inherits from B and C, and the contract B inherits from contract A. The resolution of base contracts for the derived contract D happens from right to left. The first contract to be resolved after D is B, then A is resolved and then finally C is resolved. The order of resolution for contracts can be described as:
D->B->A->C
We have read in the official documentation of Solidity about the depth-first approach that is used to resolve a base contract and we are going to verify it by deploying the contract D in Remix and check the logs of event Event
as it is emitted for each contract when it’s included in the inheritance graph.
The above screenshot shows output of logs in Remix when we deploy the contract D. The logs in output are in an order such that the last log represents the first emitted event and the first log represents the last emitted event. If we look at `args` parameter of the logs we can see that the constructors of contracts were executed in the order of D-> B-> A-> C. This allows us to verify the depth-first approach used in resolution of contract B when the next contract chosen for resolution is A and not C.
What if contract C also inherited from contract A and we come up with Diamond shaped inheritance, would the order of resolution of base contracts be the same?
The above file has same contracts and the only difference is that now the contract C also inherits from contract A and we have a Diamond shaped inheritance graph. Now, if we deploy the contract D and look at the logs in snapshot below:
The order of resolution for the contracts is D-> B-> C-> A. We can see from the logs in the above snapshot that the resolution of contract B does not involve the resolution of contract A and algorithm resolves contract C right after it resolves contract B, skipping contract A. This is because the contract A also appear later in the inheritance chain as part of another contract’s base. It almost looks like a breadth-first approach. The gotcha here is that only the last occurrence of a contract in inheritance chain is chosen for resolution and all the other are discarded. When resolving a contract (contract B), the algorithm looks at base contract (contract A) and checks if this contract is a base of another contract that exists later in the inheritance chain, if so (contract C), it is discarded and the resolution happens at the last occurrence of the contract (contract A).
We also need to take into account that the inheritance should be valid for C3 Linearization algorithm. If we inherit contract D from contracts A and B such that contract D is B, A {}
and try to compile the contract D, we will not be successful as this inheritance is invalid for C3 Linearization. We are essentially asking contract A to override contract B, which in-turn overrides contract A and hence the inheritance graph is not possible.