Wrong gap counting algorithm is used in upgradable contracts #252
Labels
bug
Something isn't working
downgraded by judge
Judge downgraded the risk level of this issue
duplicate-315
QA (Quality Assurance)
Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax
Lines of code
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/AssetRegistry.sol#L184
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/BasketHandler.sol#L738
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/Distributor.sol#L190
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/RToken.sol#L845
https://github.com/reserve-protocol/protocol/blob/df7ecadc2bae74244ace5e8b39e94bc992903158/contracts/p1/StRSR.sol#L839
Vulnerability details
Impact
The developer used a wrong algorithm to count the state variables of struct types.
This isn't an accident, so it's likely that contracts won't work in a future upgrade, or even that most contracts wll be in a wrong state and can't be upgraded again, leaving all assets locked.
Proof of Concept
The following table lists the gap information of all upgradable contracts (the code used to print this table can be found at the end of this section):
There are 5 contracts that calculate the gap size incorrectly.
Let's analyze how these gap calculation errors are caused one by one:
_erc20s
config
andbasket
, missed_targetNames
and_newBasket
From the above, we can find that only one slot is counted for any struct type state variable.
This counting algorithm is wrong, a state variables of struct type needs to be expanded to count the slots.
In a proxy contract upgrade, if the slot calculation of a sub-contract is wrong, the state of the upgraded contract may be confused due to the storage slots shift, which may cause the contracts not work or work abnormally, and even may never be able to upgrade again.
For example, suppose that a future update replaces the ContextUpgradeable with a custom one that modifies the ContextUpgradeable as follows:
This update will cause serious problems after upgrading, almost all the contracts will work abnormally and never be able to updatable again, such as: AssetRegistry, BasketHandler, Broker, Distributor, Furnace, RToken, StRSR, RevenueTrader, etc.
This is because all these contracts inherit ContextUpgradeable through Component.
The modification of ContextUpgradeable changes its head slot size from 50 to 51, which causes all the state variables of its subcontracts to be shifted by 1 slot.
As a result, all the contracts will get into a state mess and not work properly.
To make matters worse, the shift makes the variable Component.main, which is required for upgrading, point to a wrong address. All calls to
main
will fail.All assets will be locked up in the contracts:
RToken.redeem()
will revert because the its modifiernotFrozen
involves themain
:StRSR.unstake()
will revert because its modifiernotPausedOrFrozen
involves themain
:Also, there is no way to fix this problem because all upgrades will revert by the modifier
governance
atComponent._authorizeUpgrade()
for the same reason:Code used to print the gap table:
Tools Used
VS Code
Recommended Mitigation Steps
1. Code style
It is recommended to declare the gap rule, correct gap counting algorithm and emphasize the importance of correctness in the docs(such as solidity-style.md#storage-gaps).
It does not matter whether the head slot size (including the gap) of a contract is 50 or not. The important thing is that the calculation must be correct, and the head slot size must remain the same before and after each update.
2. Code modification
If this new version will be deployed directly instead of upgrading the old version, it is recommended that all gaps be re-calculated to meet the sum-50-slots rule.
Otherwise, it is recommended to add a
uint256[x] __ignore_extra_gap;
to the end of the wrong-gap contracts, wherex
is the head total size of the contract minus 50.In this way, the number of lots occupied by regular variables and
__gaps
is 50 again for each contract, which can make future maintenance more convenient and safer.3. Inspection tool
Consider developing a tool/task to veriry the gap size each time a contract is modified.
The head slot size before and after each update should be guaranteed to remain the same.
The text was updated successfully, but these errors were encountered: