waveygist

Ajna cbETH/WETH Two-Loan Liquidity Drain

Field Value
Date (UTC) 2026-08-28 15:58–2026-08-29 10:09
Network Ethereum
Vulnerability Cross-loan debt socialization through transient LUP manipulation
Loss $119,322.33 (48.9072 WETH: 44.5660 realized and 4.3412 pending exposure)
Attacker EOA (0x6F2..27e)
Victim / Loss Bearer Ajna cbETH/WETH lenders; primary bucket-4151 lender account (0x708..ce0) held 99.66% of affected LP
Key Transactions First setup (0x8a8..64b), first extraction (0x12d..4f5), first settlement (0xc4f..44d), second setup (0x1e7..fa8), second extraction (0xc06..7a8)

Summary

The attacker drained the Ajna cbETH/WETH pool (0xad2..178) by opening two linked loans against one shared liquidity curve: a nearly uncollateralized loan at a transient high LUP and a market-value loan that consumed the honest lenders' WETH. The attacker used its own high-price bucket to buy and recover the healthy loan's cbETH, leaving the dust-collateral loan for Ajna's settlement path to charge against lender deposits. Two cycles produced 48.7985 WETH-equivalent in attacker proceeds; 44.5660 WETH of lender loss is realized and another 4.3412 WETH remained exposed at the latest sampled block.

Root Cause

  • Ajna assumes that checking the borrower being modified against the post-borrow LUP is sufficient, but debt and liquidity are pooled across all borrowers. An attacker can first establish a loan whose threshold price is supported only by a transient, self-supplied high-price bucket, then open a second individually healthy loan that pushes LUP below the first loan's threshold price and consumes honest liquidity. The first loan becomes bad debt while the attacker preserves the economically valuable collateral in the second loan, so lenders—not the attacker—absorb the loss.

Exploit path

Verified BorrowerActions.drawDebt calculates the pool-wide post-action LUP but tests only the borrower receiving that draw:

result_.poolDebt = Maths.wmul(result_.t0PoolDebt, poolState_.inflator);
result_.newLup   = Deposits.getLup(deposits_, result_.poolDebt);

if (!_isCollateralized(
    vars.borrowerDebt,
    borrower.collateral,
    result_.newLup,
    poolState_.poolType
)) revert BorrowerUnderCollateralized();
  • The check should also reject a draw when the new LUP falls below the existing highest threshold price. Instead, the second loan passed because it was itself collateralized at the lower LUP, even though that LUP made the first loan immediately liquidatable.
  • In the first setup, a 105 WETH flash loan funded swaps and a 49.3437 WETH deposit at bucket 2000. The orchestrator borrowed 49.3191 WETH against 0.0011 cbETH; its debt including fee was 49.3437 WETH and its threshold price was 46,847 WETH/cbETH. A linked borrower then borrowed another 49.3191 WETH against 48.1284 cbETH at a 1.0268 WETH/cbETH threshold price. Pool deposits doubled to 98.6877 WETH while physical WETH fell to 1.0365 WETH.
  • The decoded setup trace binds those state changes to one callback: call paths 0.13.3.16, 0.13.3.17, 0.13.3.19, 0.13.3.22, and 0.13.3.23 respectively add the bucket-2000 deposit, draw the first loan, open the linked loan, and kick both auctions.

Verified TakerActions._rewardBucketTake then lets any selected bucket repay an auctioned borrower by replacing that bucket's quote-token claim with the borrower's collateral:

Deposits.unscaledRemove(
    deposits_, bucketIndex_, vars.unscaledQuoteTokenAmount
);
bucket.collateral += vars.collateralAmount;
  • In the first extraction (0x12d..4f5), the attacker applied its bucket-2000 deposit to the healthy borrower's auction, removed and sold 4.3760 cbETH across two swaps, repaid the healthy loan, and pulled the remaining 43.7514 cbETH. The pool was left with 49.3624 WETH of debt against only 0.0011 cbETH in the dust loan.
  • Verified SettlerActions.settlePoolDebt exhausts auction collateral and reserves, then _forgiveBadDebt removes deposits from the highest-price remaining buckets. The first settlement (0xc4f..44d) reduced lender deposit claims from 49.3440 WETH to 4.7780 WETH, realizing the 44.5660 WETH writeoff.

Steps

  1. The attacker funded the EOA with 7.0077 ETH and deployed the orchestrator and linked borrower contracts.
  2. A 105 WETH Balancer flash loan bought cbETH and funded a 49.3437 WETH Ajna deposit at high-price bucket 2000.
  3. The orchestrator borrowed 49.3191 WETH against 0.0011 cbETH while the transient bucket kept that loan collateralized.
  4. The linked borrower borrowed another 49.3191 WETH against 48.1284 cbETH, lowering LUP and consuming the honest bucket-4151 liquidity.
  5. The orchestrator kicked both loans after the second draw made the dust loan insolvent.
  6. After the auction price decayed, bucketTake converted the attacker's bucket-2000 WETH claim into the healthy borrower's cbETH.
  7. The attacker repaid the healthy loan, pulled its remaining cbETH, and left only the dust loan for the lender-funded settlement path.
  8. Settlement removed 44.5660 WETH from lender deposit claims to forgive the first cycle's bad debt.
  9. The attacker repeated the same paired-loan sequence against the remaining 4.7780 WETH, leaving 4.3412 WETH of additional cash shortfall at the latest sampled block.

Economics

  • Attacker realized proceeds: $118,889.11, calculated as 55.7537 WETH-equivalent ending inventory minus 7.0077 ETH/WETH seed capital = 48.7985 WETH-equivalent. The ending inventory was 0.1552 ETH + 6.2263 WETH + 43.3724 cbETH, valuing cbETH at 1.1383 WETH.
  • Victim gross loss: $119,322.33, calculated as the 44.5660 WETH realized writeoff ($108,745.76 at the first-settlement price) plus the 4.3412 WETH pending cash shortfall ($10,576.57 at the second-cycle price) = 48.9072 WETH.
  • Attack cost: approximately $264.84, or 0.1087 WETH, inferred as victim gross loss minus attacker proceeds. This is the net intrinsic friction from swaps, protocol fees, and value left behind; gas of 0.0525 ETH is tracked separately and excluded.
  • Temporary capital required: 105 WETH for the first setup plus 4 WETH and 10 WETH for extraction/settlement flash loans, all repaid in their respective transactions; the attacker also supplied 7.0077 ETH of persistent seed capital.
  • Value chain: honest lender WETH → attacker-controlled paired loans → healthy-loan cbETH recovery → attacker inventory; the dust loan → bad-debt forgiveness → lender writeoff.

Impact

  • Assets lost: 44.5660 WETH was realized as a lender-deposit writeoff; 4.3412 WETH remained a live cash shortfall, for 48.9072 WETH ($119,322.33) total realized loss plus pending exposure.
  • Affected contracts: the Ajna cbETH/WETH ERC20Pool (0xad2..178) and its bucket-4151 LPs.
  • Blast radius: the attacker EOA's complete history from blocks 25,854,570 through 25,860,219 shows two executions of this mechanism against this pool. No claim is made here about other Ajna pools.
  • Loss attribution: the primary bucket-4151 lender account (0x708..ce0) held 49.9986 of 50.1689 LP, or 99.66%, before the attack and is the dominant loss bearer; smaller LP holders absorb the remainder. The account's off-chain owner is not established.
  • Current exposure: at block 25,860,219 the pool reported 4.7780 WETH of deposit claims but held only 0.4368 WETH, while the second dust borrower remained in auction with about 4.7793 WETH debt and 0.0001 cbETH collateral.
  • Numerical check: victim gross loss and pending exposure $119,322.33 | attacker realized proceeds $118,889.11 | inferred intrinsic friction $264.84 | valuation/timing difference $168.38.
  • Gaps: the second dust auction had not settled at the latest sampled block, so its 4.3412 WETH is pending exposure rather than a finalized writeoff. Attacker identity and any off-chain coordination remain unknown.

Next Steps

  • Immediate:
    • Pause new borrowing in the affected pool and warn lenders that settling the second dust auction can crystallize the remaining 4.3412 WETH exposure.
    • Monitor the attacker cluster, paired high/low-threshold loans, same-block high-price deposits, and immediate dual kicks across Ajna pools.
  • Follow-up:
    • Reject any new debt draw when its post-action LUP falls below the highest threshold price of an existing loan; this blocks the second draw from deliberately making the first loan insolvent.
    • Add adversarial tests for two attacker-controlled borrowers sharing one liquidity curve, including addQuoteToken → drawDebt → drawDebt → kick → bucketTake → settle sequences.
    • Assess whether bucket-take accounting needs borrower/bucket isolation or delayed eligibility when newly supplied LP is used against another loan controlled by the same actor.
    • Enumerate other ERC20Pool deployments for reachable liquidity and historical paired-loan signatures before restoring unrestricted borrowing.

Reference

Address Label Role Description
0x6F2..27e Attacker EOA Funder and beneficiary Deployed both orchestrators and retained proceeds
0xad2..178 Ajna ERC20Pool Victim pool cbETH collateral / WETH quote pool
0x80A..D47 First orchestrator Dust borrower and controller Created paired loans and coordinated first cycle
0x02D..F5f First linked borrower Healthy borrower Held market-value cbETH and returned recovered collateral
0xF0D..8Ce Second orchestrator Dust borrower and controller Repeated the mechanism against residual liquidity
0x1F5..cb3 Second linked borrower Healthy borrower Held market-value cbETH in the second cycle
0x708..ce0 Primary bucket-4151 lender account Dominant loss bearer Held 99.66% of affected LP before attack
0xBe9..704 cbETH Collateral token Recovered from healthy loans and retained or sold
0xC02..Cc2 WETH Quote token Borrowed liquidity and loss denomination
Tx Hash Action Notes
0x5ec..34b Interest update Prepared pool clock before first setup
0x8a8..64b First setup High-price deposit, paired borrows, dual kicks
0x12d..4f5 First extraction Bucket take, healthy-loan repayment, cbETH recovery
0xc4f..44d First settlement 44.5660 WETH lender writeoff
0x503..49d Proceeds sweep Moved assets to second orchestrator
0x1e7..fa8 Second setup Repeated paired-loan construction
0xc06..7a8 Second extraction Recovered healthy collateral; dust auction remained

Official context: Ajna liquidation FAQ and Ajna audits repository.

Open questions — Who controls the primary lender account? Has the second dust auction settled after block 25,860,219? Are other Ajna pools presently reachable by the same paired-loan sequence?

Confidence: High for the mechanism, first-cycle realized loss, attacker cluster, and current sampled exposure; Medium for the final second-cycle loss because its auction remained unsettled.

Run Info

Field Value
Author OpenAI Codex
Started 2026-08-29T00:00:00Z
Completed 2026-08-29T10:09:55Z
Runtime 10h09m55s