YAM Incident: Root Cause Analysis
At 08:01 AM UTC, Aug. 13, 2020, the creator of YAM, @brockjelmore, tweeted about the failure of rescuing the $750,000 yCRV tokens locked in the governance contract. Hours before that tweet, people in the Ethereum community advocated of voting to a bug-fix proposal which could have the chance to SAVE YAM!. Here we will elaborate the technical details in this blog post.
Summary
This incident was caused by the wrong calculation of totalSupply
in the first rebase. The system was designed to execute bug-fix proposals to solve the problem. Unfortunately, the proposal having enough votes cannot be executed before the second rebase since the ETA of the proposal is set to a time after the second rebase automatically. When the bug-fix proposal is effective after the second rebase, the initSupply
derived from the abnormal totalSupply
makes the proposal a defeated proposal. That’s why nothing could save YAM except a hard-fork.
Details
totalSupply & initSupply
We started the analysis from the two rebase transactions: first rebase and second rebase. As shown in Figure 2, the screenshot generated by http://oko.palkeo.com/, the totalSupply
became extremely large after the first rebase.
In the second rebase, Figure 3 shows that the wrong calculation of totalSupply
propagated to initSupply
.
As described in the medium post, totalSupply
should be divided by BASE
(10^18). That’s the one-line code which creates the abnormal totalSupply
in the first rebase.
Later on, in the second rebase, the caller of YAMToken::rebase()
, YAMRebaser::rebase()
calculates the mintAmount
based on the wrong totalSupply
. Then, afterRebase()
is invoked, which eventually mints a huge amount of YAM tokens to the governor contract and messes up initSupply
.
A short summary here: totalySupply
was messed up in the first rebase; initSupply
was messed up in the second rebase.
Why can’t we execute the bug-fix proposal before the second rebase?
The abnormal totalySupply
was soon identified by the devs such that a bug-fix proposal was proposed and queued. Even after the proposal had enough votes, no one could execute it before the second rebase. The reason is that the ETA of each proposal is set as current timestamp + 12.5 hours
by the governer contract when the proposal is queued. Specifically, the GovernorAlpha::queue()
public function allows anyone to put a proposal indexed by proposalId
into the queue for execution. One line before the underlying function, _queueOrRevert()
is invoked, the eta
of that proposal is derived by the current timestamp and the timelock.delay()
which is 12.5 hours
. It means the bug-fix proposal is NOT EFFECTIVE BEFORE THE SECOND REBASE.
Why not executing the proposal after the second rebase?
Whenever someone triggers the governor contract for executing a proposal, GovernorAlpha::execute()
checks the state of that proposal with a view function state()
.
As you can see in state()
line 330, when forVotes
is less or equal to againstVotes
, the proposal is set as a defeated proposal. This was definitely NOT THE CASE as people in the community contributed enough votes to the bug-fix proposal with the help of the SAVE YAM! campaign. Actually, the forVotes
was under the quorumVotes()
fails the rescue mission.
When the system was designed, the quorumVotes()
should be 4% of the initSupply
as shown in Figure 9. However, as mentioned earlier, the initSupply
is messed up in the second rebase. This makes quorumVotes()
returns a huge number such that it is impossible to have enough forVotes
.
After the second rebase, new proposal cannot be proposed due to the fact that GovernorAlpha::propose()
checks the proposalThreshold()
which returns 1% of the initSupply
— a huge number you cannot reach in this world.
Timeline
2020–08–14 Update
As @bantg pointed out in this tweet, someone actually cancel()
the bug-fix proposal in this transaction 31 mins after the second rebase. Since the public function GovernorAlpha::cancel()
allows anyone to cancel a proposal if the voting power of the proposer
is less than proposalThreshold()
, the keeper successfully canceled the proposal because of the huge initSupply
(after the second rebase).
About Us
PeckShield Inc. is an industry leading blockchain security company with the goal of elevating the security, privacy, and usability of current blockchain ecosystem. For any business or media inquiries (including the need for smart contract auditing), please contact us at telegram, twitter, or email.