How we plan to adopt Solidity 0.6 virtual functions

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!


  1. 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.
6 Likes

Totally agree and love how it’s written. Creativity is the biggest value of the future!

2 Likes

OpenZeppelin Contracts were migrated to Solidity 0.6 in OpenZeppelin Contracts v3.0 Beta

Give the Beta a try and let us know what you think!

Not allowing overrides on view function will make some use cases impossible that I worked on in the past but I guess the old WET design rule of copy it all and modify what you need will need to do there instead of the DRY concept of only overriding what actually needs changes. I’ve started to drift in that direction more and more recently anyhow as specific overrides can result in messy bytecode anyhow due to potentially some variables being in the resulting contract but unused and stuff like that.

2 Likes

Welcome to the forum @KaiRo!

I would love to see some of the uses cases you mention so we can understand if we need to revisit this rule.

1 Like

For example, for the Mobilio ERC20 token (see https://etherscan.io/address/0xbe59434473c50021b30686b6d34cdd0b1b4f6198#code line 610 ff.), we did an override of totalSupply() and balanceOf() to make it appear as if new tokens were minted into a “bank” account by the second over the minting period (100 years), actually syncing that with state variables and events only when that account makes actual transfers.
For Crypto stamp (code at https://etherscan.io/address/0x7e789E2dd1340971De0A9bca35b14AC0939Aa330#code ) - as I mentioned in a GitHub discussion - we did an override for tokenURI() (see line 921) to get the style of meta data URI we wanted. That specific style is now possible in OpenZeppelin 3.0, but who knows if someone else will want a URI style that is not supported with this.
That’s two examples I know right off the top of my head where we needed to override view functions in the past and we may need similar things again in the future.

1 Like

Thanks for sharing! Those are very interesting and legitimate use cases in my opinion.

We’ve decided to review this in the next release cycle (Contracts 3.1) since it’s a backwards compatible change and we want to focus on getting 3.0 released as soon as possible. If anyone else has other use cases which need virtual view functions, please keep them coming. :slight_smile:

1 Like

FWIW, I’ve just run into another case: We want to be able to externally finance the gas for transfers of tokens in a certain ERC-721 contract, and figured we could have a second contract that verifies somehow that it’s a valid request and then call the transfer of the token. For that, this other contracts needs to have “approval for all” on tokens of all owners, which we can achieve decently by overriding isApprovedForAll() and always returning true for that contract address (and otherwise delegates to its super).
I also had conversations in the past where regulators required a “backdoor” to be able to disown owners who were involved in illegal action (let’s leave out for the moment how much that undermines ideas of the blockchain), and I a similar isApprovedForAll() override could be used for that.

is there a way to override functions in an inherited MyContractV2 from MyContract if the function in MyContract has not specified a virtual keyword?


contract MyContract is ERC20Upgradeable {     

using MyLib.StateV1 for MyTypes.State;

MyTypes.State state;

function balanceOf(address account)
        public
        view
        override(ERC20Upgradeable)
        returns (uint256)
    {
        return state.balanceOf(account);
    }

 }
contract MyContractV2 is MyContract {     

using MyLib.StateV2 for MyTypes.State;

function balanceOf(address account)
        public
        view
        override(ERC20Upgradeable, MyContract)
        returns (uint256)
    {
        return state.balanceOf(account); // new logic from MyLib.StateV2
    }

 }

TypeError: Trying to override non-virtual function. Did you forget to add "virtual"?

No it's not possible.

1 Like