Audius
Last updated
Last updated
Audius is a fully decentralized music platform. It was hacked due to improper proxy contract usage. The hacker re-initialised multiple contracts of the project to tamper with malicious parameters.
Governance Contract https://etherscan.io/address/0x4deca517d6817b6510798b7328f2314d3003abacâĻ (Proxy) https://etherscan.io/address/0x35dd16dfa4ea1522c29ddd087e8f076cad0ae5e8âĻ (Impl)
Staking Contract https://etherscan.io/address/0xe6d97b2099f142513be7a2a068be040656ae4591âĻ (Proxy) https://etherscan.io/address/0xea10fd3536fce6a5d40d55c790b96df33b26702fâĻ (Impl) DelegateManagerV2 Contract https://etherscan.io/address/0xf24aeab628493f82742db68596b532ab8a141057âĻ
Hackerâs EOA
https://etherscan.io/address/0xa0c7bd318d69424603cbf91e9969870f21b8ab4câĻ
One of hacker's helper contracts
https://etherscan.io/address/0xbdbb5945f252bc3466a319cdcc3ee8056bf2e569
Tamper with vote parameters by re-initialisation
Submit malicious proposal
Tamper with vote weight by re-initialisation
Vote
Execute proposal
Call initilize()
of Governance Contract to tamper with vote params:
Set votePeriod = 3
and Delay = 0
, so that only three blocks were needed to finish voting. Set _votingQuorumPercent = 1%
, meaning that only 1% of the total staked token will meet the quorum.
Submit malicious proposal(id=85) to Governance: Call submitProposal()
with _functionSignature = transfer(address,uint256)
, address = attacker
, amount = 18,564,497,819,999,999,999,735,541
, _targetContractRegistryKey= 307800..00
(resolve as token contract in registry).
_quorumMet() will check whether the vote has met the quorum. If not then no result will come out. The attacker needed to increase its own weight to pass this check.
In DelegateManagerV2 call initilize()
, set himself as governanceAddress
, who has the power to delegatestake. Call delegatestake()
, as we can see thereâs no limitation on _amount, so the hacker wrote a very large number. Only vote yes to meet the quorum.
Call submitVote() to vote for proposal 85. Block height was 15201796 at that time.
Call evaluateProposalOutcome()
in Governance to settle proposal 85. Block height was 15201799. Settlement time window reached, quorum met, and only him voted for yes, so the proposal passed. Function inside the proposal will be called, i.e. transfer token to the attacker.
The reason why the hacker could re-initialise an initialised contract is improper usage of the proxy architecture.
A proxy architecture can be simply divided into two contracts: proxy(Proxy) and implementation (Impl). In fact, there are some other auxiliaries, more than two contracts.
When a user interacts with the contract, the Proxy contract doesn't "know" what functions and variables are there in the Impl. Calling functions in Impl is actually using Delegatecall from Proxy to Impl.
Delegatecall's main feature: code of callee is copied and run in the caller. I.e., the results of the computation, eg state variables, are all stored in the Proxy. And these variables are not indexed by name but by storage slot, so there is a problem of storage conflict.
In Proxy, a variable is used to record the address of the Impl contract, which has already conflicted in the above figure.
To solve this, EIP-1967 defined a storage slot location IMPLEMENTATION_SLOT = keccak256 ('eip1967.proxy.implementation' )) - 1
, and stores the impl address in this slot.
IMPLEMENTATION_SLOT is a constant, and it won't occupy storage slot. Then, use solidity assembly to write the variable into the specified slot.
Then there's the collision is eliminated.
There were a customized variable proxyAdmin
in its multiple proxy contracts, which caused the Initalized
and Initializing
variables in Impl that marked the initialization state to conflict with the proxyAdmin
storage.
We ran a test on Rinkeby and found that both initialized
and initializing
was compromised(equals True) since deployment, as address
type can cover multiple bool
types(uint 8) with non-0 value.
With initializing == true
, the modifier always allow initialisation.
In our test, if we remove the customized variable proxyAdmin
, no re-initialisation was allowed. So the conclusion is clear.
This attack happened due to re-initialisation was doable, which was caused by the improper understanding of proxy architecture.
Developers should read OpenZeppelin Proxy Docs before using its proxy module. Any customisation should be made with caution. The standard method instead of a customised variable should be used to manage access control. If you have to use a customised variable for special purposes, it should be implemented as EIP1967's storage guide.
Proxy | Impl | Note | |
---|---|---|---|
Proxy | Impl | Note | |
---|---|---|---|
Status
Exploited
Type
Proxy
Date
Jul 24, 2021
Source
Direct Loss
$6M
Project Repo
Slot 0
address impl
<- bool var1
Collision!
Slot 1
<- bool var2
Slot 2
<- uint256 var3
Slot 3
Slot 0
<- bool var1
Slot 1
<- bool var2
Slot 2
<- uint256 var3
Slot 3
...
Slot Custom
address impl
No collison!