Audius Governance Takeover Post-Mortem 7/23/22 | Audius Blog
Details provided:
- Audius claimed there was a bug in their implementation of the Upgradeable smart contracts pattern, implemented with OpenZeppelin
- The attacker was able to make repeated calls to contracts that are meant to be called once (initializers) which allow them to set state variables.
Contract Structure
Storage Layout of Audius
| slot | Admin (Implementation) | Proxy |
|------------|------------------------|--------------------------------|
| 0 | proxyAdmin | initialized, initializing (OZ) | <------ Storage Collision
| 1 | | isInitialized (V2) |
| ... | | |
| [0x3...bc] | implementation | |
-
OZ Initializable has 2 boolean values packed into storage slot 0 (2/32 bytes):
- Initialized
- Initializing
-
Audius has a governance contract that stores the address
proxyAdmin
at storage slot 0. It also acts as a Proxy, so will cause collisions with the implementation contract in_delegate
-
Recall how a call to an OZ upgradeable contract works:
- Call to Proxy
- Function not recognised, passed to fallback
- Custom fallback implements custom delegate call
- Delegate call to implementation (address stored at
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
) - Execute call within context of implementation contract using Proxy for storage
The problem, therefore, was storing information about the Admin in the Proxy contract.
- The check requiring
initializing
always passes, because initially initializing is set to true - The
isTopLevelCall
always returnsfalse
becauseinitialized
istrue
and so!initalized
isfalse
initalizing
is never set tofalse
- Attacker can therefore call the
initialize
function, in, sayStaking.sol
and set the governance address to what they want
Prevention:
The Proxy should contain no data unless it has a very specific place in storage you know will not be overwritten.
Example in OZ:
- We can safely store the Admin in the
ERC1967Proxy
because it inherits fromERC1967Upgrade
which reserves storage slots for key variables, such as the admin of the Proxy.
bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;
bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
- If you define a separate
ProxyAdmin()
contract, you can use standard storage (onlyOwner, etc) inside this new ProxyAdmin contract because it’s not related to the storage of the Proxy.- Set the admin of the proxy contract as the ProxyAdmin, at a predefined storage slot