# Ajna Finance: self-controlled liquidation drains cbETH pool via bucketTake

Attacker controlling both borrower and liquidator in Ajna's oracle-free Dutch auction manipulated internal bucket prices to seize ~48 cbETH for ~3.5 WETH, draining ~$124.8K from one pool.

- Payout: Real-world hack
- Severity: critical
- Program: AjnaFinance
- Credited: DeFiHackLabs
- Disclosed: 2026-08-28
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-08/AjnaFinance_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260828-ajnafinance
- Tags: domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:source-review vuln:business-logic vuln:liquidation vuln:oracle-manipulation

**Target:** Ajna ERC20Pool (cbETH/WETH) liquidation auction, bucketTake/take/repayDebt
**Root cause:** Ajna is permissionless and oracle-free: auction and bucket prices derive solely from pool internal state, so an attacker controlling both the over-leveraged borrower and the taker/liquidator can shape the prices both sides interact at and extract more collateral value than quote paid, socializing loss onto other lenders.
**Steps:**
1. Fork mainnet at block 25854887 (parent of exploit tx 0x12dfde...).
2. Attacker-controlled borrower 0x02D329 sits in active Dutch auction: debt 49.36 WETH, collateral 48.13 cbETH; Fenwick bucket 2000 holds ~49.34 WETH of attacker deposit.
3. Taker calls `bucketTake(borrower, false, 2000)` to consume bucket quote, repay most debt at manipulated bucket price, award taker bucket LP (debt 49.36 -> 3.47 WETH).
4. Taker calls `removeCollateral(max, 2000)` to pull awarded cbETH.
5. Taker calls `take(borrower, max, taker, "")` to clear residual 3.47 WETH debt and settle auction.
6. As borrower, call `repayDebt(borrower, 0, freedCollateral, taker, 7388)` to pull ~46.5 cbETH freed collateral to taker.
7. Net: taker holds ~48.13 cbETH for ~3.47 WETH paid (~44.65 ETH-equivalent, ~$124.8K).
**How found:** Attacker recognized Ajna's oracle-free design means auction/bucket prices come only from pool internal state with no external feed to contradict them; controlling both the borrower and taker sides of a liquidation lets them set the prices those sides trade at. Verified against on-chain trace and Ajna's verified ERC20Pool source, confirming bucketTake/take/removeCollateral/repayDebt are plain permissionless functions and the validate(...) staticcall is the attacker contract's own tx.origin check, not an Ajna gate.
**Exploitation:** Attacker pre-funded an over-leveraged borrower position and its own quote deposit in bucket 2000, let the position enter liquidation, then as the taker used bucketTake to repay debt at the manipulated internal bucket price, removed the awarded collateral, settled residual debt with take, and as the borrower pulled the freed collateral out via repayDebt. On-chain the take was funded by a Balancer flash loan plus cbETH->WETH swap (economically neutral); net ~43.75 cbETH + ~1.5 WETH extracted. Same bug hit 7 Ajna ERC20 pools same day (~$775K aggregate).
**Escalation:** Same bug class replicated across 7 Ajna ERC20 pools in one campaign: syrupUSDC $173.7K, wstETH $159.8K, rETH $127.4K+$15.6K, cbETH $124.8K+$12.1K, WBTC $101.8K, WETH/USDC $42.0K, sDAI $18.0K (~$775K total).
**Insight:** In oracle-free lending protocols where auction prices derive purely from internal pool state, check whether one actor can be both borrower and liquidator/taker; if so, the internal price is attacker-chosen and the liquidation can be made value-extractive against other lenders. Trace which public functions (bucketTake, take, repayDebt) let the same actor move collateral and quote on both sides.
