- Due to an oversight in the migration process, the previous GUSD strategy[1] attached to the GUSD vault[2] ended up incorrectly being affected by calls to the crvGUSD vault[3].
- This led to incorrect share price calculations for both vaults that resulted in losses of 11,435.95 GUSD for vault depositors.
- Affected depositors have been compensated from Yearn's Operations Fund for the full amounts.
- As a result of this incident, Yearn developers are reviewing the process for migrating between strategies.
- On 2020-10-26, funds were withdrawn from strategy[1] and deposits into strategy for GUSD vault[2] were disabled as a result of a decimal issue in the strategy[4].
- On 2020-12-07, the crvGUSD vault[3] and strategy[5] were deployed.
- On 2020-12-22, the crvGUSD vault called
earn
for the first time[6], incorrectly altering the share price of the GUSD vault. Because the previous GUSD strategy was not revoked properly, it led to incorrect calculations of GUSD vault share prices. From here on out, eachearn
orwithdrawAll
call on crvGUSD altered share price of yGUSD.
The root cause was more than one active strategy using the same Curve Gauge. All veCRV boosts are handled by the CurveYCRVVoter[6] -- сontract that is whitelisted by Curve, and each pool has only one corresponding gauge. Therefore, if more than one strategy uses the same gauge, this causes issues because strategies will see balances from other strategies as their own.
Each earn()
and withdrawAll()
call on the crvGUSD Vault incorrectly affected share price of the GUSD Vault.
This occured because:
getPricePerFullShare()
on yGUSD callsbalance()
.balance()
callsbalanceOf()
from the v1 Vault Controller[7].- The Controller in turn calls
balanceOf()
on the GUSD strategy, StrategyCurveGUSDProxy[8]. - The strategy's
balanceOf
calls and addsbalanceOfPoolInWant()
andbalanceOfWant()
. balanceOfPoolInWant()
callsbalanceOfPool()
.- The strategy's
balanceOfPool()
callsbalanceOf()
in the StrategyProxy contract[9]. - StrategyProxy's
balanceOf()
calls the gauge balance for CurveYCRVVoter[10], and stakes LP tokens for Curve pools in the GUSD Gauge[11] to earn CRV emissions.
Both the GUSD and crvGUSD Vaults deposit their LP tokens in the same gauge from the same address. Therefore, the yGUSD vault saw the LP tokens deposited by crvGUSD and thought those were its own, rapidly increasing the share price because no new yGUSD tokens were minted.
When the last user drained the old GUSD strategy[12], their withdrawAll()
call also incorrectly removed 0.00000000000158595 GUSD/3Crv from the Curve gauge.
This is the difference between the balance
and totalSupply
values for the new GUSD/3Crv vault. As getPricePerFullShare = balance / totalSupply
, this results in a share value <1.
When a user withdraws, the vault checks to see if there are enough free funds to withdraw directly from the vault. If not, it pulls funds from the strategy, in this cause withdrawing from the GUSD Curve gauge. Additionally, this withdrawal value was much smaller than intended due to previously mentioned decimal issues.
Since the balance
value was incorrectly skewed by the crvGUSD vault, this led to the following:
- GUSD Vault calculates
r = shares * balance / totalSupply = 2422549
- Compares against
b = token.balanceOf(address(this)) = 836599
, which was the GUSD sitting free in the vault - As
b < r
, it told the vault to pull funds from the strategy, equal tor - b = 1585950
, so the strategy pulled 0.00000000000158595 gusd3CRV from the gauge and burned it.
Strategy contract GUSDRescue
[13] was deployed as a mock strategy for yGUSD to use. This strategy hardcoded balanceOf
and withdrawAll()
, with a mock deposit()
to rescue the locked GUSD in yGUSD vault [14].
- Jan 3, 2021: Yearn developers notice an issue with GUSD share price and begin investigation.
- Jan 13: The remainder of funds in the vault is transferred to the executive multi-sig, and GUSD strategy is replaced [13], [14].
- Jan 15: All affected vault depositors are compensated[15].
- Jan 16: This disclosure is published.
- StrategyCurveGUSDProxy
- GUSD vault
- crvGUSD Vault
- GUSD Strategy Decimals Bug
- StrategyCurveGUSDVoterProxy
- First crvGUSD earn call
- Vulnerability disclosure 2020-10-30
- CurveYCRVVoter
- v1 Vault Controller
- StrategyProxy
- GUSD gauge
- Transaction to drain old GUSD strat
- StrategyGUSDRescue
- Transaction rescuing funds and attaching new GUSD strategy
- Transaction compensating all outstanding yGUSD holders