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.


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.


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.

Figure 2: State Changes in the First Rebase

In the second rebase, Figure 3 shows that the wrong calculation of totalSupply propagated to initSupply.

Figure 3: State Changes in the Second Rebase

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.

Figure 4: YAMToken::rebase() Creates an Abnormal totalSupply

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.

Figure 5: YAMRebaser::rebase() Propagates the Wrong totalSuppy to 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.

Figure 6: GovernorAlpha::queue() Sets the ETA of the Proposal

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().

Figure 7: GovernorAlpha::execute() Reverts b/c of the State of the Proposal

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.

Figure 8: GovernorAlpha::state() Returns ProposalState.Defeated b/c of quorumVotes()

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.

Figure 9: GovernorAlpha::quorumVotes() Returns a Huge Number b/c of the Abnormal initSupply

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.


Figure 10: Timeline of SAVE YAM!

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.

A Blockchain Security Company (https://peckshield.com)