Yearn $9M Theft Attack Analysis
- 核心观点:Yearn协议遭多阶段组合攻击损失900万美元。
- 关键要素:
- 利用闪电贷撬动初始攻击资金。
- 组合利用精度丢失等三个核心漏洞。
- 最终实现无限铸造LP代币并掏空资金池。
- 市场影响:凸显DeFi协议组合漏洞风险,促进行业安全升级。
- 时效性标注:中期影响
Foreword
On December 1, 2025, the Yearn protocol suffered a sophisticated, multi-stage hack that resulted in the loss of approximately $9 million in assets. The attack was not a single exploit; rather, the attackers leveraged funds through flash loans, exploited multiple logical flaws in the protocol to gradually manipulate the liquidity pool, ultimately achieving a near-infinite minting of yETH-related LP tokens and depleting the liquidity pool.
This attack used flash loans as initial funding leverage and broke through the protocol's defenses layer by layer through multiple steps. The core attack process can be divided into four key stages: funding preparation, state manipulation, unlimited minting, and profit-taking. Each stage is interconnected and precisely exploits logical vulnerabilities in the protocol design.
Attack on Tencent : https://etherscan.io/tx/0x53fe7ef190c34d810c50fb66f0fc65a1ceedc10309cf4b4013d64042a0331156
Technical Analysis
First, we borrow wstETH, rETH, WETH and 0xa35b_ETHx, rETH, wstETH, cbETH from Balancer and Aave respectively through two flash loans.


In the flash loan callback function, the borrowed ETH is deposited into Tornado.Cash: 100 ETH for mixing with 1100 ETH. Then, the Tornado.Cash: 100 ETHwithdraw function is used to withdraw 100 ETH to the malicious contract 0x3e8e7533dcf69c698Cf806C3DB22f7f10B9B0b97 and trigger its fallback function.


In the fallack function, we can see that the last exchange in the image below is the same as the value of remove_liquidity in the next step. It can be inferred that the previous steps were all to exchange the assets obtained from the flash loan into a large number of yETH weighted stableswap pool LP tokens through operations such as exchange, in preparation for subsequent attacks.

At this point, the core attack process officially begins.
1. First, all LP tokens obtained from the above exchange are destroyed and converted into 8 underlying assets in the pool according to the share allocation through the remove_liquidity function of the yETH weighted stableswap pool.
The logic of the remove_liquidity function can be understood as follows: assuming there are a total of 10,000 LP tokens in the pool, and you destroy 416.37 LP tokens, your proportion is: 416.37 / 10,000 = 4.16%.
Then, for each asset, assuming there are 1,000 wstETH (virtual balance prev_vb) in the pool, the virtual balance you can withdraw is: dvb = 1,000 * 416.37 / 10,000 = 41.637 wstETH, which is then divided by the exchange rate to convert to the actual number of tokens.
2. Secondly, by repeatedly calling `add_liquidity`, a one-sided pool is injected. There are a total of 8 `_amounts` parameters, each corresponding to the amount of different assets to be injected. It was observed that in the previous loops, index3[rETH token], index6[wOETH token], and index7[mETH token] were all entered as 0, meaning that these 3 tokens were not added each time liquidity was added.

By injecting unilateral assets and withdrawing tokens from the pool in the manner described above, the quantity gap between rETH, w0 ETH, mETH, and other tokens in the pool is artificially widened.
3. Next, a huge amount of rETH tokens are injected unilaterally, followed by the crucial step of removing_liquidity, but with _amount=0.

Why can 0 tokens be withdrawn? This is due to the internal implementation of remove_liquidity .

The remove_liquidity function did not perform short-circuit processing for 0-value transactions; it still executed the complete vb_prod calculation loop, and calculated and updated the global packed_pool_vb state based on the token quantity difference in the artificially created pool mentioned above.
4. Then call the update_rates function to update only the pool ratio of index6[wOETH], and finally call remove_liquidity to extract tokens from the pool. At this point, the amount of W0 ETH in the pool is almost gone.

5. Similarly, squeeze the share of index6[w0 ETH] and index7[mETH] in the pool in a similar way. Note that after updating index6 for the first two times, remove_liquidity is used to withdraw tokens, while index7[mETH] has only been updated and has not yet been withdrawn.

By adding massive amounts of tokens to the single-sided pool and continuously extracting tokens from all pools, the ratio of W0 ETH to mETH in the pool has now reached almost zero.
At this point, a new malicious contract 0xADbE952eBB9b3e247261d2E3b96835f00f721f8E is created, and all tokens are transferred to this contract. Note that the LP tokens obtained by unilaterally adding rETH in the previous step were not converted into the underlying tokens, but were also transferred to the new malicious contract.

The previous attack operation updated index7[mETH] in update_rates, but the tokens that were not withdrawn are withdrawn here by calling remove_liquidity. At this time, the share of index6[w0 ETH] in the pool is very small, and the share of index7[mETH] is even smaller.

At this point, the token ratio in the pool was severely imbalanced. The attacker called add_liquidity again to add liquidity, acquiring a huge amount of LP tokens in the ratio of [1, 1, 1, 1, 1, 1, 1, 9].

At this point, the attackers have obtained a large amount of LP tokens. They then use methods such as exchange and redeem to profit and repay the flash loan fees.

Attack Review
This attack was a complex, multi-stage combined attack in which the attackers exploited three core vulnerabilities in the pool.vy contract: Precision Loss, Yield Stripping, and Zero Supply Initialization.
Phase One: Creating Extreme Imbalance
- Operation: The attacker repeatedly calls add_liquidity, but deliberately avoids index 3 (rETH), index 6 (wOETH), and index 7 (mETH).
- Objective: To artificially create an imbalance in the asset ratio within the pool.
- Key step: Then inject a massive amount of rETH unilaterally.
- Consequences: This drastically widens the quantitative gap between rETH and other assets (especially wOETH and mETH), creating the mathematical conditions for loss of precision.
Phase Two: Triggering and Locking the Error
- Operation: Call remove_liquidity(_amount=0).
- principle:
`remove_liquidity` does not perform short-circuiting for amounts of 0.
Even without transferring funds, the contract still executes the complete calculation loop of vb_prod.
Under extreme weight imbalances, the _pow_down function produces a significant down-rounding error.
The contract writes the smaller error value of vb_prod into the global state packed_pool_vb.
- Essentially, this is a "zero-cost" state attack, where the attacker successfully alters the pool's book value without incurring any cost.
Phase Three: Revenue Stripping and Market Share Extraction
- operate:
update_rates([6]) (updates the wOETH exchange rate).
remove_liquidity (Remove Assets).
update_rates([7]) (updates the mETH exchange rate).
- principle:
`update_rates` will trigger `_update_supply`. Because `vb_prod` was previously maliciously suppressed, the system mistakenly judged that the pool value had shrunk, and thus destroyed the LP tokens held by the staking contract to balance the accounts.
Attackers exploited remove_liquidity to arbitrage before and after exchange rate updates, gradually draining the pool of wOETH and mETH.
- Result: A large number of staking contracts were destroyed, the attacker's share of LPs was passively increased, and the pool's Total Supply was pushed to 0.
Phase Four: Zero Supply, Unlimited Minting
- Pre-existing state: After the above operations, the pool has been emptied, the Total Supply is close to 0, and the wOETH and mETH balances are extremely low.
- Operation: add_liquidity, parameter is _amounts=[1, 1, 1, 1, 1, 1, 1, 9].
- principle:
When prev_supply ≈ 0, the iterative formula for _calc_supply fails when dealing with extremely small values (1 wei, 9 wei).
The contract incorrectly calculated an astronomical number of LP tokens to be minted.
- Result: The attacker obtained 235,443 ... yETH LP tokens out of thin air.
Summarize
The Yearn attack exposed multiple shortcomings in DeFi protocols regarding logic verification in edge scenarios, precision control in numerical calculations, and risk prevention against multiple vulnerability combinations. The attacker's attack pattern, using flash loans as a tool, vulnerability combination exploitation as the core, and fund obfuscation as a cover, highlights the current trend of professionalization and complexity in DeFi attacks. Key lessons learned from this attack include: First, protocols need to strengthen logic verification for edge scenarios such as "zero amount" and "extreme imbalance" to avoid state tampering risks due to lack of short-circuit handling; second, precision loss under extreme ratios needs to be addressed in numerical calculations, and the calculation logic of key functions such as `_pow_down` needs to be optimized. The Balancer protocol previously experienced a security incident due to precision loss, serving as a cautionary tale; third, a multi-dimensional risk monitoring system should be established to issue warnings for suspicious operations such as high-frequency one-sided liquidity injections and abnormal exchange rate updates. For the entire DeFi industry, this incident once again proves that protocol security requires not only fixing individual vulnerabilities but also preventing attacks that combine multiple vulnerabilities from a full-process perspective, while strengthening the tracking and interception of attackers' fund flows to improve the overall security protection capabilities of the industry.


