The Solidity team releases a major breaking version of the language roughly every year. 0.6 was released in December 2019 and contains around 30 breaking changes amongst many improvements. This post outlines the upgrade process for the smart contracts that underpin the Argent Wallet from Solidity version 0.5 to 0.6 (or, more precisely, from 0.5.4 to 0.6.12, the latest available minor version at the time of writing).
In addition, the post examines the approach for supporting gradual migration, that is to partially migrate contracts while leaving others on older major versions. In total, over 4,300 lines of code in 31 contracts were migrated to 0.6, integrated with solc 0.5 based infrastructure contracts and Ethereum subsystems. Although the argent-contracts repo uses etherlime for compilation and testing, I will be drawing parallels to truffle as well because of its popularity.
Start with a well tested solution
It's best practice to maintain as close to 100% branch code coverage as possible on your contracts to ensure no regression bugs are introduced with the upgrade refactoring. Both truffle and etherlime provide code coverage functionality based on solidity-coverage tool. Additionally, we run the slither static analyzer on contracts.
Determine contract upgrade groups
Ideally, a solution is upgraded end-to-end, however, that's often not possible or practical. We chose to migrate the Wallet contracts to 0.6 while preserving most of the infrastructure contracts on version 0.5. With such a staged migration approach, we first analyzed the contracts structure and dependencies to determine whether different contract groups are sufficiently isolated to allow them to be compiled independently. slither comes with a set of printers to help with analysis of usages, namely the inheritance-graph and call-graph printers, see https://github.com/crytic/slither/wiki/Printer-documentation. Below is an extract of the wallet modules inheritance model generated with slither . --print inheritance-graph
.
Currently, neither etherlime nor truffle allow more granular compile targets than a folder, and selective compilation behaviour is not supported. Essentially we have to segregate contracts that target different solc versions in distinct folders to allow compilation using either framework.
The final contract groups and compile target versions are as follows:
pathsolccontracts/infrastructure_0.50.5.4contracts/infrastructure0.6.12contracts/modules0.6.12contracts/wallet0.6.12contracts-legacy/v1.3.00.5.4contracts-legacy/v1.6.00.5.4contracts-test0.6.12lib0.5.4
Note that with truffle you can achieve some selective compilation via regex, e.g. it's possible to exclude contracts from compilation via --contracts_directory
option: truffle compile --contracts_directory 'lib/dappsys/[!note][!stop][!proxy][!thing][!token]*.sol'
Integration with 0.5 contracts
With the gradual upgrade approach after the separation of contracts, which we did above, if there are still unresolved cross solc version dependencies between contracts you will get errors similar to this:
{ Error: /argent-contracts/contracts/infrastructure/base/Managed.sol:17:1: ParserError: Source file requires different compiler version (current compiler is 0.6.12+commit.0bbfe453.Emscripten.clang) - note that nightly builds are considered to be strictly less than the released version
pragma solidity ^0.5.4;
^---------------------^
This overlapping dependencies problem can be solved by either setting the contract to compile with wider version range e.g. pragma solidity >=0.5.4 <0.7.0;
or, if that is not possible (because of breaking changes), abstracting the called functions to an interface which we can set to the wider version range.
For example, we abstracted IModuleRegistry
from ModuleRegistry
and imported that new interface in WalletFactory
instead of the full ModuleRegistry contract:
Another obstacle is the problem with external library imports, which don't allow parallel version installations, e.g. openzeppelin-solidity
and git submodules. There's no graceful solution to this and our design allowed us to upgrade the openzeppelin-solidity
while keeping the dappsys
contracts on its original pragma that allowed compilation with both 0.5 and 0.6.
Once solidity versions no longer clashed we continued to fix the 0.6.12 breaking changes following the documentation.
Tooling support
Throughout the migration work the following blockers and improvements were logged in dependent frameworks we use:
IDE
Using Visual Studio Code with solidity extension when working on large scale refactoring like upgrades wasn't optimal. Switching contracts to a major version with 30 breaking changes potentially raises tens if not hundreds of compiler errors and warnings we have to work through. Since these are normally addressed by type, it will be easier for the reporter to allow us to group it that way.
https://github.com/microsoft/vscode/issues/98819
Test client
ganache-cli was able to successfully run all contract interactions post upgrade. Only issue was found https://github.com/trufflesuite/ganache-cli/issues/759
Solidity linter
We migrated to solhint since ethlint (a.k.a. solium) provided no support for the 0.6 syntax https://github.com/duaraghav8/Ethlint/issues/281
Compile and test framework
There are still gaps in supporting parallel compilation with different solidity versions.
- more granular compile targets
https://github.com/LimeChain/etherlime/issues/325
https://github.com/trufflesuite/truffle/issues/2021#issuecomment-654051052
- support multiple contracts with the same name
This has been an issue for over two years now but the problem is exacerbated with upgrades as the need to support legacy contracts means we have to effectively rename contracts to compile correctly, which becomes tedious to manage. See https://github.com/trufflesuite/truffle/issues/1087 as part of the ongoing rewrite here https://github.com/trufflesuite/truffle/issues/1718
slither provides a printer for detection of reused contract names slither . --ignore-compile --detect name-reused
which helps to detect such instances.
Additionally slither itself does not yet fully support 0.6. https://github.com/crytic/slither/issues/405
Solidity
I was surprised by how easy it was switching to the version, helped by the Breaking changes documentation and the compiler messages that provided many fix suggestions. The only issue I bumped into was with the new override
keyword which is required when implementing a function from a parent interface. See discussion here https://github.com/ethereum/solidity/issues/8281
Finally, for those using the DelegateProxy (a.k.a. EtherRouter or Proxy) upgrade pattern, since the base Proxy in the Argent Wallet is non-upgradable and designed with minimal code (~20 lines of code) essentially we end up running 0.6 contracts on top of a 0.5 Proxy. Since there are no breaking changes to storage design, this functions correctly.
Summary
Our work on the upgrade for the Argent Wallet can be found here https://github.com/argentlabs/argent-contracts/pull/114. Overall the upgrade was made easier by the Solidity compiler and documentation; it was made harder because tooling support was lacking on both 0.6 features and support for the mix of solidity versions.
Note that while this work was ongoing, Solidity 0.7 was released (in July 2020). However, due to the limited tooling support for that we decided to target 0.6. The work for upgrading to 0.7 and the related tooling and integration issues with the wallet are tracked here.
As always in our process, our contracts were audited by leading independent firms. For more information on this, you can find the audits at: https://github.com/argentlabs/argent-contracts/tree/master/audit