-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feat(contracts)/upgradability #1214
Merged
Merged
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
jaybuidl
force-pushed
the
feat/upgradability
branch
from
September 25, 2023 17:56
3b0bc0d
to
c69aeb7
Compare
✅ Deploy Preview for kleros-v2 ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
…he deploy scripts
jaybuidl
force-pushed
the
feat/upgradability
branch
from
September 25, 2023 17:57
c69aeb7
to
487fc5b
Compare
…e arithmetics in deploy scripts
jaybuidl
force-pushed
the
feat/upgradability
branch
from
September 26, 2023 12:39
54d2e0d
to
4467927
Compare
jaybuidl
force-pushed
the
feat/upgradability
branch
from
September 26, 2023 13:02
7e48b3a
to
c5104cb
Compare
Code Climate has analyzed commit acb94e9 and detected 19 issues on this pull request. Here's the issue category breakdown:
View more on Code Climate. |
Kudos, SonarCloud Quality Gate passed! 0 Bugs No Coverage information |
Iterations on @zmalatrax's work
|
jaybuidl
approved these changes
Sep 26, 2023
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR follows the #1136 PR which introduced a UUPS Proxy.
A few changes to the upgradability mechanism were made. The SortitionModule and the KlerosCore have been made upgradeable. This is the first step before having the whole Kleros-v2 stack upgradeable. It can be considered WIP as the KlerosCore might need some design changes.
Upgrade Mechanism
Instead of relying on a boolean
initialized
which only allows initializing the proxy storage once, the upgradeable contracts inherit from Initializable.sol, copied from OpenZeppelin.It introduces two modifiers:
initializer
andreinitializer(uint8 version)
, and the function_disableInitializers()
.initializer
acts the same asreinitializer(1)
.This contract allows re-initializing the proxy storage, if needed, by using the
reinitializer(uint8 version)
with a higher version than the previous one. It is quite useful when adding new modules requiring initialization.The version increment can be superior to 1, which allows initializing parent contracts in between if needed. OpenZeppelin has some docs about it here. The version cap is 255, a contract whose version is 255 cannot be re-initialized.
This is what
_disableInitializers()
do, bumping the initialize version to 255. Calling it in the constructor of the implementation contract will disable the initialization, preventing potential attackers from taking over the implementation contract (see self-destruct attacks..)KlerosCore Upgradability
The KlerosCore contract is heavy: the deployed bytecode weighs 22.220 kiB in the dev branch (sizes are taken from hardhat-contract-sizer, running
yarn hardhat size-contracts
). Making it upgradeable (transferring the constructor to aninitialize()
function and inheriting from two abstract contracts UUPSProxiable and Initializable adds 2.183 kiB to the deployed bytecode. Without reducing the contract size, the upgradeable version of KlerosCore is above 24 kiB, making it impossible to deploy on mainnet.This is why an optimization of the KlerosCore size is needed.
Optimization
With different optimizations (errors instead of require statements, private constants over unused public constants, caching calls to storage in a variable if there are more than 3 calls to it (empirical value based on tests, I haven't compared the ops but it should add up), removing cache variables if called less than 3 times (low impact, can even be considered negative as it reduces the readability) the size of KlerosCore have been reduced by almost 1 kiB.
Here is a recap table of the measured sizes of KlerosCore:
Fig.1.1: KlerosCore size comparison
Here is a table giving measured changes of specific changes made to KlerosCore when optimizing it:
Replace modifieronlyByGovernor
by a private view functionEDIT: reverted for code readability, as long as we can afford it
courts[_courtID]
in storage variablecourt
inchangeCourtParameters()
disputeKit
in DisputeKitJump case fromappeal()
court
ingetTimesPerPeriod()
rate
inchangeCurrencyRates()
Fig.1.2: Details of KlerosCore optimization
As the details show, removing cached variables when they are called less than 3 times has a minimal impact. In the case of
appeal()
the variable caches an extremely long path which is of great help for readability purposes. However, for the other two cases, caching a variable has small changes to readability. Even if pretty small, I'd take those changes into account.Replacing a modifier by a private function is kind of desperate but, as the modifier is called many times, it has the biggest impact. It allows having 0.518 kiB free rather than 0.212 kiB. But even with 0.5 kiB, adding new functionality is rather limited.
This is why splitting the contracts in (at least) two should be the most considered option.
Splitting KlerosCore
Due to its size and the difficulty of reducing it through optimization, it is better to split up KlerosCore.
The split design choices must be well thought out and semantically correct. I haven't had time to think much about it. The functions specified in
IArbitratorV2
could be split into one contract, as well as the governance-related functions.Upgrade Scripts
Proxy Options
To deploy the upgradeable contracts through a custom UUPS proxy, changes to the deployment scripts had to be made, defining the proxy options and how hardhat-deploy should behave when upgrading the proxy. The previous PR #1136's part on the deployment script is outdated as the upgrading mechanism doesn't work due to missing flags.
Here is an updated version:
Setting the
checkProxyAdmin
andcheckABIConflict
flags to false allows not checking if there is a deployed Admin contract or a selector conflict between the proxy functions and the implementation functions, which is only necessary for Transparent proxies.The
onUpgrade
function is defined. This specifies the post-upgrade function, the function to be called once the upgrade is done. There usually is no need to call a post-upgrade function unless the proxy storage needs to be initialized again. However, if noupdateMethod
is defined (i.e.onUpgrade
field is not defined), hardhat-deploy will try callingupgradeTo()
which is not defined in UUPSProxiable.sol (as in the new versions of OpenZeppelin's proxy).Calling a public variable getter is a workaround to bypass this limitation.
I have tweaked the hardhat-deploy package to allow specifying a custom upgrade function (which function with which arguments to be called when an upgrade needs to be done). For more details, you can check this issue and PR that I've proposed to hardhat-deploy, but no feedback has been given yet... and therefore not merged.
For testing purposes, a package has been published. It allows specifying the upgrade function to be used when upgrading through the new ProxyOptions field
upgradeFunction
:Upgrading Multiple contracts
Selective Upgrades
The current deployment scripts deploy multiple contracts at once. When we want to upgrade a specific contract, using these scripts would re-deploy non-upgradeable contracts, which are not to be used by the upgraded contracts (unless the proxy storage is also updated). Therefore a script for upgrades only must be written.
However, we should be able to select which contract to upgrade rather than having hardhat-deploy trying to upgrade every upgradeable contract in one script.
To do so, every upgradeable contract should have its
XX-upgrade-contract-name.ts
script with at least two tags:["Upgrade", "ContractName"]
Prior to v0.11.37, hardhat-deploy doesn't allow executing deployment scripts that intersect the given tags:
yarn hardhat deploy --network localhost --tags Upgrade,KlerosCore
will try running all scripts with the tag "Upgrade" or "KlerosCore".The version v0.11.37 introduces the flag
--tags-require-all
which will look for the intersection of the given tags:yarn hardhat deploy --network localhost --tags Upgrade,KlerosCore --tags-require-all
will try running all scripts with the tags "Upgrade" and "KlerosCore".It allows a more selective choice of deployments with proper tags (not having a unique tag for the sole purpose of running it alone). However, it doesn't support combining AND & OR operations with tags.
But what if we want to upgrade multiple contracts in a specific order?
Upgrading contracts in chains
To upgrade multiple contracts, the scripts should have a common tag/group of tags (e.g. "Upgrade" for every upgrade script, ["Upgrade", "Arbitration"] to upgrade all arbitration contracts...).
Then, the scripts should be ordered with two digits at the beginning of the filename. For example, upgrading the KlerosCore before the SortitionModule would be:
00-upgrade-kleros-core.ts
and01-upgrade-sortition-module.ts
.NOTE on upgradeable contracts importing another upgradeable contract
The SortitionModule imports KlerosCore to have its interface and interact with it. As per the Solidity docs, the deployed bytecode includes an extra 33 bytes which are metadata. These metadata are the ipfs hash of the compiler version, the sources used, the ABI and NatSpec documentation.
More details can be found in this short article.
This metadata includes the sources used, so it is based on the whole source code of the contract, which includes the imported contract source code.
When modifying an imported contract (e.g. KlerosCore), it will modify the deployed bytecode of the contract importing the modified contract (e.g. SortitionModule). As a result, if we perform an upgrade of the KlerosCore, running all upgrade scripts thinking that only the KlerosCore will be upgraded (only changes made), hardhat-deploy will also upgrade the SortitionModule due to this metadata changes. If overlooked, it results in a loss of funds (and block size).
The metadata can be excluded from the bytecode with the flag
--no-cbor-metadata
when compiling. Metadata are used when verifying the source code, they are crucial for transparency towards users.Importing an interface instead of the whole contract to make external calls to it should be considered.
Tests
Tests covering the UUPS proxy have been written, but they don't include the Initializable contract in it. It should be done though.
The arbitration/index.ts#DisputeKitClassic has been updated, replacing the SortitionModule & KlerosCore factories with a new deployment, specific to the test.
No tests including the upgrade scripts were written, it has been tested empirically.
yarn start-local
yarn deploy-local
, performing an upgrade while doing so (but ineffective as it re-deploy many contracts which aren't used)OR
yarn hardhat deploy --network localhost --tags Upgrade
yarn hardhat deploy --network localhost --tags Upgrade,SortitionModule --tags-require-all
yarn hardhat deploy --network localhost --tags Upgrade,KlerosCore --tags-require-all
Some yarn scripts should be added to the package.json to easily launch upgrades.
Hardhat tests for the upgrading through hardhat-deploy should also be written. This can be done by making use of
deployments.fixture
. Broadly, more tests should be written, to have a better coverage of v2 contracts.