One of the most interesting features of the recent Solidity 0.6 release is the improvements around inheritance to make it more explicit when functions can be overriden (the
virtual keyword), or that a given function definition is intended to be an override of another (the
override keyword). The lack of this feature was in fact reported in our audit of the compiler in the issue No mechanism to prevent functions from being overridden.
This now enables library authors to design abstractions that can be audited in isolation, and that guarantee a certain behavior even when combined with other contracts through inheritance.¹ We think this is important because it simplifies the task of auditors by restricting what they have to look for in an audit.
OpenZeppelin Contracts doesn’t work in Solidity 0.6 yet, and before we make the upgrade we have to decide how we will adopt this new feature. The main question is: should we allow users to override the functions we provide?
This is a very tough question, because on one hand we want to guarantee a certain behavior of our contracts, but on the other hand our users want to extend that behavior in many different ways, and often in ways that deviate significantly from the specification we may have had in mind. The new override controls allow us to constrain or forbid those kinds of extensions. The upside of that is that we can audit the contracts in the library and more strongly guarantee their correctness and security. However, by doing this we close the doors to the more creative extensions.
We could try to stay in a middle ground where some things are extensible and others are more closed down, but since the Ethereum ecosystem is so young and is constantly in flux, it’s impossible for us to predict what will be the relevant points of extensibility.
Because of all this we’ve decided to make almost everything virtual. One of the exceptions is view functions (getters) none of which will be virtual. Keeping getters closed down means that we retain some guarantees that the data that a contract reads from its private state variables is the same data that is exposed to the outside world. We think this will lead to a higher degree of internal consistency.
The other exception is modifiers, mostly because you can’t use
super to delegate to the base implementation. This touches on an important point, which is that not delegating to the base implementation of a virtual function will almost certainly be a mistake, so you should keep that in mind.
Even though everything will be virtual, users of the library will have to use the
override keyword and make that intent explicit. This makes it easy to identify places where behavior may diverge from that of vanilla OpenZeppelin Contracts. By using override, though, you are potentially violating ERC standards or introducing bugs, so it should be done sparingly, with a lot of care, and with an auditor’s approval.
An important factor in this decision was making these rules simple, so that it doesn’t add too much friction for maintainers and contributors.
We wanted to share our thought process on this with you. Let us know what you think!
- Unless a contract in the inheritance tree uses assembly, although auditing that assembly block by itself one would be able to assess whether it is potentially interfering with another contract.