# Incident disclosure 2025-12-01 ## Summary - On Ethereum mainnet block **23,914,086**, the yETH weighted stableswap pool[[1]](#References) was exploited[[2]](#References). - Yearn v2/v3 vaults and other Yearn products **were not affected**; The impact of this exploit was **isolated to the yETH product and its direct integrators**; - The exploitation involved a complex sequence of operations where the attacker first forced the pool’s internal solver into a divergent state, causing the product term (`Π`) to collapse to zero. This allowed the protocol to **over-mint yETH LP tokens** in the normal `add_liquidity` path. - Using this over-minted LP, the attacker drained the underlying Liquid Staking Tokens (LSTs) through repeated rounds of withdrawals, **driving the pool’s internal supply to 0**. - After emptying the stableswap pool, the attacker **re-entered the pool’s initialization branch** with dust deposits, triggering an arithmetic underflow that minted approximately **2.3544 × 10⁵⁶ yETH LP tokens**, which were then used to drain the yETH/ETH Curve pool. - A coordinated recovery of **857.49 pxETH** was performed with the assistance of the Plume and Dinero teams[[5]](#References). - yETH is *self-governed by its depositors* and not governed by Yearn or YFI. Under **YIP-72 §8: Use at Own Risk**[[7]](#References), contributors and governance are not liable for reimbursement. Any recovered assets will be redistributed to affected depositors. - Recovery efforts remain active and ongoing. --- ## Background **The yETH protocol is composed of three contracts working together:** 1. **The yETH stableswap liquidity pool**, implementing a weighted multi-asset AMM over LSTs. 2. **A simple ERC-20 token contract** representing LP ownership (`yETH`). 3. **A staking contract** (`st-yETH`) that receives all LST yield and absorbs losses by burning assets held in the staking contract when the system supply decreases. The pool tracks: - a **sum term** over virtual balances, `Σ` (sigma), - a **product term** over virtual balances, `Π` (Pi), - an internal **equilibrium supply**, `D`. To compute `D`, the pool uses an **iterative fixed-point solver**. This solver relies on preconditions regarding domain validity ($A \cdot \Sigma \ge D \cdot \Pi$) and convergence (the iteration must remain in a regime where its approximations do not diverge), preconditions that were not enforced on-chain. Additionally: - The protocol intentionally maintains **two supply notions**: the internal invariant `D`, and the ERC-20 `totalSupply`. - These differ by design to support Protocol-Owned Liquidity (POL) and migration flows. - Reconciliation is performed during rate updates (yield or slashing), minting or burning yETH to or from the `st-yETH` contract to keep accounting aligned. It was precisely this reconciliation surface, combined with the presence of POL, that the attacker exploited in Phase 2. This incident was confined to the yETH protocol and downstream integrations. **Yearn v2/v3 vaults and other Yearn strategies were never at risk.** --- ## Assets affected Token holdings in the **yETH weighted stableswap pool**[[1]](#References) at block **23,914,085** (pre-exploit): | Token | Amount | | :--- | :--- | | apxETH | 798.10 | | sfrxETH | 641.07 | | wstETH | 359.66 | | cbETH | 279.06 | | rETH | 204.30 | | ETHx | 203.17 | | mETH | 51.39 | | wOETH | 50.54 | Separately, WETH in the **yETH/WETH Curve pool**[[6]](#References) at block **23,914,085** (pre-exploit): | Token | Amount | | :--- | :--- | | WETH | 298.35 | --- ## Details of exploit The exploit proceeded in three phases, combining numerical instability with architectural choices around POL and supply reconciliation. A more detailed technical summary of the exploit is available in [[3]](#References). A runnable PoC harness demonstrating reproduction of the exploit using scripts and testing frameworks is available in [[4]](#References). ### Phase 1: Force bypass the stableswap invariant and over-mint according to altered math The attacker performed a sequence of `add_liquidity` operations designed to introduce **extreme imbalance** between the new deposit and the existing pool balances. This forced the solver into a regime it was not designed to handle. 1. **Mechanism: extremely imbalanced deposits produce a tiny initial `vb_prod`.** The `pow_up` path computes ```text vb_prod = (prev_vb / vb)^(weight) ``` for each asset. When a deposit is *orders of magnitude larger* than the existing virtual balance, `prev_vb / vb` becomes extremely small. Repeated across assets and weightings, this produces a very small initial `vb_prod`, far below values the solver was designed to operate with. 2. **Divergence and truncation driving `r` to 0.** `_calc_supply` attempts to solve for `D` via iterative approximation. With an extremely small `vb_prod`, the Newton step oscillates violently between iterations. Eventually, the term representing the relative change in supply (calculated as `sp / s` in the code) became small enough to round down to exactly 0 under integer arithmetic. This caused the product accumulator `r` to collapse to `0`. 3. **Result: `Π = 0` and invariant collapse.** Once `r = 0`, the solver stores `Π = 0` on-chain. A product term of zero destroys the weighted-stableswap invariant, effectively degenerating it into **constant-sum-like behaviour**. In this broken state, the solver returns a grossly inflated value for `D`, and the protocol, lacking domain validation, **accepts it and over-mints LP tokens** for the attacker far beyond the economic value of their deposit. (Importantly, beginning `_calc_supply` with a small `Π` is not sufficient on its own to cause this failure; the essential factor is that the solver’s Newton iteration **diverges** under extremely imbalanced deposits and eventually drives the internal update step to zero, collapsing `r` to 0.) ### Phase 2: Repeatedly exploit distorted supply to drain the pool The attacker now held over-minted LP while the pool state was internally inconsistent (`Π = 0`, `D` inflated), and while POL existed via yETH held in the `st-yETH` contract. 1. **Product repair:** A call to `remove_liquidity(0)` recomputed `Π` directly from balances, restoring a valid non-zero `Π` without correcting the inflated `D`. 2. **Staking pool burn:** A subsequent call to the permissionless `update_rates` function caused `_update_supply` to reconcile the inflated `D` by burning yETH from the `st-yETH` contract. The system interpreted the discrepancy as a massive “slashing” event and removed the excess supply entirely from the staking contract, not from the attacker. 3. **Draining the pool (repeated rounds):** The attacker then withdrew assets using `remove_liquidity`. Their LP balance was wildly oversized relative to the now-normalized `Π` and balances. By **repeating this loop several times**, the attacker withdrew nearly all LSTs, leaving the pool with `internal supply = 0` while having effectively offloaded the cost of the over-mint onto POL. ### Phase 3: Re-enter initialization path to infinite-mint LPs With the pool emptied and `prev_supply == 0`, the attacker could access a **bootstrap initialization path** intended only for the pool’s first launch. 1. **Invariant violation:** The attacker deposited a crafted “dust” configuration of virtual balances, which violated the domain requirement $A \cdot \Sigma \ge D \cdot \Pi$. 2. **Underflow:** The solver executed an operation equivalent to: ```text unsafe_sub(A * Σ, D * Π) ``` With `D * Π` greater than `A * Σ`, and `unsafe_math` enabled, the subtraction **underflowed**, wrapping to `uint256` max-range values. 3. **Result:** The resulting supply was approximately **$2.35 \times 10^{56}$**, minted to the attacker. This extreme yETH supply was then used to drain the **primary yETH/ETH Curve pool**, completing the exploit chain. --- ## Consolidated root causes 1. **Numerical instability and missing domain checks:** Under large relative deposits, the combination of the `pow_up` path and the subsequent solver iteration allowed internal variables to diverge and drove `Π` to 0, breaking the intended invariant without reverting. 2. **Asymmetric handling of `Π`:** `add_liquidity` updated the product term incrementally (making it possible to corrupt), while `remove_liquidity(0)` recomputed it from scratch (allowing a “partial repair” that left `D` inflated). 3. **POL-enabled zero-supply state:** The presence of Protocol-Owned Liquidity meant that internal supply `D` could be driven to 0 while ERC-20 balances still existed. This made the `prev_supply == 0` bootstrap branch reachable in production and allowed the underflow path to be triggered after the main pool was drained. 4. **Re-initialization risk:** The bootstrap path was not permanently disabled after initial deployment, making it possible to re-enter an initialization-only code path under adversarial conditions. 5. **Unsafe math in invariant-critical code:** The use of `unsafe_sub` in `_calc_supply` allowed a domain violation in `A * Σ - D * Π` to manifest as a catastrophic infinite-mint event instead of a revert. --- ## Remediation and architectural lessons To structurally prevent recurrence of these failure modes, future designs must enforce numerical safety in the solver, close unintended lifecycle states (like re-enterable bootstrap paths), and treat POL-driven accounting as a high-risk surface that requires tight constraints and dedicated testing. ### 1. Numerical safety & solver constraints The solver must respect the mathematical domain it relies on and fail safely if it is violated: * **Domain enforcement:** Explicitly check `A * Σ >= D * Π` before entering and during the iteration. If this fails, revert rather than attempting to solve in an invalid region. * **Detection of degenerate products:** Treat `Π = 0` (or values that can only arise from diverging iterations rather than realistic pool states) as a fatal condition for the solver and revert. Divergent iterations must not be able to silently collapse the invariant into constant-sum behaviour. * **Checked arithmetic:** Replace `unsafe_math` in invariant-critical paths with checked operations (or equivalent explicit bounds) so that underflows and overflows surface as reverts, not wrapped values. ### 2. POL interaction and lifecycle safety Using POL in combination with an internal invariant and a separate LP token supply introduces additional failure modes: * **Zero-supply reachability:** Design POL mechanisms such that `prev_supply == 0` cannot be reached in normal operation while ERC-20 balances remain, unless through an explicit, governance-controlled shutdown or migration process. * **Bootstrap gating:** Ensure that any “bootstrap” or initialization code paths are permanently gated or disabled after initial deployment so they cannot be re-entered as part of a runtime exploit chain. ### 3. Economic sanity bounds As a final safeguard against unexpected solver behaviour: * **Mint caps:** Introduce hard caps tying LP issuance to the economic value of deposits, e.g. `minted_lp <= k * deposit_value`, where `k` is a conservative multiplier. This ensures that even if a numerical issue escapes detection, any single transaction cannot mint LP grossly out of line with its inputs. ### 4. Testing depth & invariant fuzzing The specific failure mode here — large relative deposits pushing the solver into a divergent regime where internal variables eventually truncate to 0 — was not caught by standard testing. Future security work must emphasize: * **Invariant-driven fuzzing:** Property-based tests over solver inputs (balances, weights, amplification) that assert invariant relationships, not just specific outputs. * **Adversarial numerical exploration:** Targeted fuzzing of extreme and imbalanced deposit/withdraw patterns to explore edge cases in fixed-point math and solver convergence. * **Differential testing of solver paths:** Comparing the on-chain solver implementation against an off-chain high-precision or symbolic reference model to detect divergence, especially around special paths like `pow_up` and re-initialization. --- ## Timeline of events ### November 30, 2025 * **09:11:59 PM UTC — Exploit execution, 1000 ETH sent to Tornado** (block 23,914,086) * **09:34 PM UTC — Yearn internal war room convened** * **10:05 PM UTC — SEAL 911 war room engaged** ### December 01, 2025 * **08:56:23 AM UTC — Recovery execution** (857.49 pxETH recovered)[[5]](#References) * **06:17:23 PM UTC — Message sent to exploiter**[[8]](#References) ### December 05, 2025 * **04:02:47 PM UTC — Remainder of funds sent to Tornado Cash** (block 23,947,805)[[9]](#References) ### Ongoing * Continued recovery efforts and asset tracing * Continued technical analysis * Continued external security collaboration * Continued monitoring of attacker-linked flows and integrator exposures --- ## Acknowledgments Yearn contributors thank yETH auditors **ChainSecurity**[[10]](#References) for their extensive support in root cause analysis and validation of the exploit mechanics, and **SEAL 911**[[11]](#References) for their assistance with incident response coordination and asset recovery efforts. --- ## References 1. yETH Weighted Stableswap Pool Contract [https://etherscan.io/address/0xCcd04073f4BdC4510927ea9Ba350875C3c65BF81](https://etherscan.io/address/0xCcd04073f4BdC4510927ea9Ba350875C3c65BF81) 2. Exploit Transaction [https://app.blocksec.com/explorer/tx/eth/0x53fe7ef190c34d810c50fb66f0fc65a1ceedc10309cf4b4013d64042a0331156](https://app.blocksec.com/explorer/tx/eth/0x53fe7ef190c34d810c50fb66f0fc65a1ceedc10309cf4b4013d64042a0331156) 3. yETH Exploit Summary (PDF) [https://github.com/0xkorin/yETH-exploit-summary/blob/master/summary.pdf](https://github.com/0xkorin/yETH-exploit-summary/blob/master/summary.pdf) 4. PoC Exploit Replication (testing harness) [https://github.com/johnnyonline/yETH-hack](https://github.com/johnnyonline/yETH-hack) 5. pxETH Recovery Transaction [https://etherscan.io/tx/0x0e83bb95bb9d05fb81213b2fad11c01ea671796752e8770b09935f7052691c35](https://etherscan.io/tx/0x0e83bb95bb9d05fb81213b2fad11c01ea671796752e8770b09935f7052691c35) 6. Curve yETH/WETH pool [https://etherscan.io/address/0x69ACcb968B19a53790f43e57558F5E443A91aF22](https://etherscan.io/address/0x69ACcb968B19a53790f43e57558F5E443A91aF22) 7. YIP-72 Governance Reference — Use at Own Risk [https://gov.yearn.fi/t/yip-72-launch-yeth/13158#p-33602-h-8-use-at-own-risk-37](https://gov.yearn.fi/t/yip-72-launch-yeth/13158#p-33602-h-8-use-at-own-risk-37) 8. Message sent to exploiter [https://etherscan.io/tx/0x919dc0d056953e7449e7e6e4044fc991d0feebab195e9239750d584fd8179607](https://etherscan.io/tx/0x919dc0d056953e7449e7e6e4044fc991d0feebab195e9239750d584fd8179607) 9. Remainder of exploited funds sent to Tornado Cash [https://etherscan.io/tx/0xd1fda150209775777dfd1046feb1aa08a13ee4a8b317823d9aabd96d53d1be61](https://etherscan.io/tx/0xd1fda150209775777dfd1046feb1aa08a13ee4a8b317823d9aabd96d53d1be61) 10. ChainSecurity [https://chainsecurity.com](https://chainsecurity.com) 11. Security Alliance Seal 911 [https://securityalliance.org/seal-911](https://securityalliance.org/seal-911)