Calculated from recorded token losses using historical USD prices at the incident time.
0xe9c4d4f095c7943a9ef5ec01afd1385d011855a1BSC0x6ac860ae21993d65b790a95cfc1a3a4b42dd0ce3BSC0xd061a395190581cb677b5ffff1dc38448d4976c8BSC0x42717781d93197247907f82482ae1d35d7bc101bBSC0x4f31fa980a675570939b737ebdde0471a4be40ebBSCOn BSC (chainid 56), the H2O token contract at 0xe9c4d4f095c7943a9ef5ec01afd1385d011855a1 implements a helper-token reward mechanism that can be repeatedly triggered by the H2O/USDT PancakeSwap pair 0x42717781d93197247907f82482ae1d35d7bc101b. An unprivileged EOA, 0x8842dd26fd301c74afc4df12e9cdabd9db107d1e, deployed a custom helper contract 0x03ca8b574dd4250576f7bccc5707e6214e8c6e0d, used a 100,000 USDT flash loan from PancakeV3Pool 0x4f31fa980a675570939b737ebdde0471a4be40eb to enter the H2O/USDT pool, and abusively drove H2O’s _calulate reward logic to transfer large amounts of H2O from the token contract’s own balance into the pair and the helper. The helper then swapped H2O back to USDT inside exploit transaction 0x994abe7906a4a955c103071221e5eaa734a30dccdcdaac63496ece2b698a0fc3, concentrating 22,688.210530453207954293 USDT on the helper contract before cashing out to the EOA via bfbaa190(USDT) in transaction 0x33cfa1257d85bcd77206438d5d7efdf22e6cea3c306fce646fa9aa7594b964ec.
The root cause is a protocol bug in H2O’s transfer() and _calulate() functions: they implement an unbounded, pseudo-random helper-token reward mechanic that can be triggered an arbitrary number of times within a single transaction by a DEX pair. This allows an attacker-controlled contract to repeatedly receive reward transfers from the token contract’s own balance (address(this)), with no per-address or per-transaction cap, enabling a flash-loan-assisted drain of H2O-backed USDT liquidity from the H2O/USDT pool.
0x994abe7906a4a955c103071221e5eaa734a30dccdcdaac63496ece2b698a0fc30xd97694e02eb94f48887308a945a7e58b62bd6f20b28aaaf2978090e5535f3a8e0x33cfa1257d85bcd77206438d5d7efdf22e6cea3c306fce646fa9aa7594b964ec0xa0688f0f02aad1ecdbdf653c407ac9c5757a99444ee9beab7b3a60eecc59e1ae0xe57e600cf8909a37833855005e5b2491414650b863329e13275806f940d17a7fprotocol_bugThe ACT opportunity is evaluated at block height 47454937 on BSC (chainid 56), immediately before exploit transaction 0x994abe7906a4a955c103071221e5eaa734a30dccdcdaac63496ece2b698a0fc3. The pre-state σ_B is the publicly reconstructible BSC state before this tx, including balances and allowances for:
0xe9c4d4f095c7943a9ef5ec01afd1385d011855a10x42717781d93197247907f82482ae1d35d7bc101b0x4f31fa980a675570939b737ebdde0471a4be40eb0x03ca8b574dd4250576f7bccc5707e6214e8c6e0d0x6AC860AE21993d65b790a95Cfc1A3A4b42dd0ce3 and 0xD061A395190581cb677b5FfFF1dc38448D4976c80x55d398326f99059ff775485246999027b31979550x8842dd26fd301c74afc4df12e9cdabd9db107d1eThis state is reconstructed and cross-checked using:
0x33cfa125... and 0xa0688f0f....The ACT sequence b consists of three adversary-crafted transactions on BSC:
0x994abe7906a4a955c103071221e5eaa734a30dccdcdaac63496ece2b698a0fc3
0x4f31fa98..., swaps USDT into H2O on pair 0x42717781..., repeatedly triggers H2O._calulate via pair-initiated transfers to move H2O from the token contract into the pair and helper, then exits the flash loan with a net 22,688.210530453207954293 USDT increase on helper 0x03ca8b57..., while paying a 50 USDT flash-loan fee to the pool holder.0x33cfa1257d85bcd77206438d5d7efdf22e6cea3c306fce646fa9aa7594b964ec
0x8842dd26... calls helper.bfbaa190(USDT), which reads USDT.balanceOf(address(this)) on 0x03ca8b57... and transfers that entire USDT balance to msg.sender. Prestate-based ERC20 balance diffs show helper USDT delta -22,770.891449589918819435 and EOA USDT delta +22,770.891449589918819435.0xa0688f0f02aad1ecdbdf653c407ac9c5757a99444ee9beab7b3a60eecc59e1ae
bfbaa190(USDT) after the helper’s USDT balance is already zero; prestate-based ERC20 diffs show zero USDT movement for both the helper and the EOA, so only gas is spent.The exploit predicate is expressed as a profit opportunity in USDT:
0x55d398326f99059ff775485246999027b3197955).0x8842dd26fd301c74afc4df12e9cdabd9db107d1e.+22,688.210530453207954293.0x994abe79...: -0.004001803 BNB (EOA).0x33cfa125...: -0.0000518 BNB (EOA).0xa0688f0f...: -0.0000339 BNB (EOA).-0.004087503 BNB.Using the on-chain BNB/USDT price at block 47454937:
{
"pair_address": "0x16b9a82891338f9ba80e2d6970fdda79d1eb0dae",
"token0": "0x55d398326f99059ff775485246999027b3197955",
"token1": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c",
"reserve0": "11578991082411373539449813",
"reserve1": "19952960489260653037466",
"price_bnb_in_usdt": 580.3144394859886
}
Snippet: On-chain PancakeSwap v2 WBNB/USDT reserves and derived BNB price in USDT at the exploit block.
The BNB gas expenditure corresponds to approximately 2.372037012342296860 USDT. Combining the gross USDT gain and gas-equivalent cost yields a net adversary profit of:
22,685.838493440865657433 USDT.This satisfies the ACT profit predicate in a single reference asset, computed entirely from prestate-based balance diffs and on-chain DEX pricing data.
H2O is a BEP20-like token on BSC with symbol H2O and total supply 200,000,000 * 10**18. The contract introduces two helper tokens:
_o2: 0x6AC860AE21993d65b790a95Cfc1A3A4b42dd0ce3_h2: 0xD061A395190581cb677b5FfFF1dc38448D4976c8The H2O/USDT trading pair is created on deployment using PancakeSwap’s factory, and the contract’s own balance (address(this)) holds half of the initial H2O supply, effectively acting as a treasury.
Whenever the H2O/USDT pair 0x42717781... calls H2O.transfer(to, amount), H2O’s transfer implementation invokes _calulate(to, amount) to run helper-token-based reward logic. The _calulate function:
h2obalance = balanceOf(address(this)), the remaining H2O held by the token contract itself.rate in {1, 2, 3, 4, 5} depending on h2obalance buckets.random = getRandomOnchain() % 2, where getRandomOnchain() uses keccak256(block.timestamp, msg.sender, blockhash(block.number-1)).amount * rate / 100 helper tokens to the transfer recipient on _h2 or _o2 depending on random.h2balance = IBEP20(_h2).balanceOf(to) and o2balance = IBEP20(_o2).balanceOf(to); when they exceed thresholds, burns some helper tokens from the recipient and transfers H2O from the token contract’s own balance (address(this)) to the recipient, capped only by the current h2obalance.Critically, there is no per-recipient or per-transaction cap on how many times this reward branch can execute within a single transaction. A DEX pair, acting as msg.sender, can repeatedly call transfer(to=helper) in one flash-loan callback, causing _calulate to fire many times for the same helper address and drain a large amount of H2O from the token contract.
The adversary’s helper contract 0x03ca8b57... orchestrates the attack:
0x4f31fa98....0x10ED43C718714eb63d5aA57B78B54704E256024E into the H2O/USDT pair._calulate(to=helper) and fuels further reward emissions.bfbaa190(USDT) that moves all USDT held by the helper contract to msg.sender.function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = msg.sender;
_spendAllowance(owner, to, 0);
_transfer(owner, to, amount);
if (pair == msg.sender) {
_calulate(to, amount);
}
return true;
}
function _calulate(address to, uint256 amount) internal {
uint256 h2obalance = balanceOf(address(this));
uint256 rate = 0;
if (h2obalance <= 20_000_000 * 10**18) {
rate = 1;
} else if (h2obalance <= 40_000_000 * 10**18) {
rate = 2;
} else if (h2obalance <= 60_000_000 * 10**18) {
rate = 3;
} else if (h2obalance <= 80_000_000 * 10**18) {
rate = 4;
} else if (h2obalance <= 100_000_000 * 10**18) {
rate = 5;
}
uint256 random = getRandomOnchain() % 2;
if (random == 1) {
IBEP20(_h2).mint(to, amount * rate / 100);
} else if (random == 0) {
IBEP20(_o2).mint(to, amount * rate / 100);
}
uint256 h2balance = IBEP20(_h2).balanceOf(to);
uint256 o2balance = IBEP20(_o2).balanceOf(to);
if (h2balance >= 10 * 10**18 && o2balance >= 5 * 10**18) {
if (h2balance / 2 >= o2balance) {
IBEP20(_o2).burn(to, o2balance);
IBEP20(_h2).burn(to, o2balance * 2);
uint256 amountto = o2balance;
if (amountto >= h2obalance) {
amountto = h2obalance;
}
_transfer(address(this), to, amountto);
} else if (h2balance / 2 < o2balance) {
IBEP20(_o2).burn(to, h2balance / 2);
IBEP20(_h2).burn(to, h2balance);
uint256 amountto = h2balance / 2;
if (amountto >= h2obalance) {
amountto = h2obalance;
}
_transfer(address(this), to, amountto);
}
}
}
Snippet: H2O token’s transfer and _calulate logic, showing how pair-initiated transfers can repeatedly move H2O from the token contract’s own balance to an attacker-controlled recipient.
H2O’s helper-token reward logic allows an unbounded number of _calulate() invocations on the same recipient within a single transaction. When driven by a DEX pair, this logic can be abused to transfer large amounts of H2O from the token contract’s own balance to an attacker-controlled address. The mechanism relies on helper tokens and pseudo-randomness from getRandomOnchain, but it lacks any per-address or per-transaction cap or invariant that prevents the contract’s treasury from being drained during a single flash-loan cycle.
From the H2O source:
transfer() calls _transfer(owner, to, amount) and, if msg.sender equals the H2O/USDT pair, calls _calulate(to, amount)._calulate() mints helper tokens _h2 or _o2 to the recipient based on a pseudo-random bit and the transfer amount. It then uses helper-token balances as thresholds to decide when to transfer H2O from the contract’s treasury to the recipient via _transfer(address(this), to, amountto).amountto is bounded only by the current h2obalance and o2/h2 balances; there is no global limit on total H2O that can be emitted to a single recipient in one transaction.An adversary can therefore:
_calulate() calls for the same helper address.address(this) into the helper and the pair in a single exploit tx.The vulnerability is aggravated by:
block.timestamp, blockhash) that can be influenced by transaction ordering and miner/validator behavior.0xe9c4d4f095c7943a9ef5ec01afd1385d011855a1, specifically:
transfer(address,address,uint256)_calulate(address,uint256)0x6AC860AE21993d65b790a95Cfc1A3A4b42dd0ce3 and 0xD061A395190581cb677b5FfFF1dc38448D4976c8, whose mint and burn functions act as counters inside _calulate.0x03ca8b574dd4250576f7bccc5707e6214e8c6e0d, whose ecf8ecb0 function orchestrates flash loans, swaps, and looping transfers that drive _calulate, and whose bfbaa190 function enables USDT cash-out.For the exploit to succeed, the following conditions hold at σ_B:
0x42717781... that routes H2O transfers with msg.sender == pair and to == helper.address(this)) holds sufficient H2O to support large outgoing reward transfers without immediately reaching zero.0x4f31fa98... and use PancakeRouter’s swapExactTokensForTokensSupportingFeeOnTransferTokens to enter/exit the H2O/USDT pool._calulate(to=helper) multiple times in one transaction._calulate, and no restriction preventing the helper contract or the pair from being the primary reward beneficiary._calulate + getRandomOnchain) without enforced caps.The adversary’s strategy is to:
_calulate reward logic via pair-initiated transfers to an attacker-controlled helper contract.bfbaa190(USDT) to transfer all USDT from the helper to the EOA, and optionally repeat cash-out when no USDT remains (incurring only gas).This lifecycle is entirely permissionless and uses public DEX, flash-loan, and token interfaces; no privileged roles or non-standard assumptions are required.
Identified adversary cluster:
EOA 0x8842dd26fd301c74afc4df12e9cdabd9db107d1e (BSC, chainid 56)
0x03ca8b57... (via tx 0xe57e600c...).0x33cfa125....Helper contract 0x03ca8b574dd4250576f7bccc5707e6214e8c6e0d (BSC, chainid 56)
0x8842dd26....ecf8ecb0 exploit logic, holding intermediate USDT profit after tx 0x994abe79....bfbaa190(USDT) in tx 0x33cfa125....Victim candidates:
0xe9c4d4f095c7943a9ef5ec01afd1385d011855a1 (verified source).0x42717781d93197247907f82482ae1d35d7bc101b.0x4f31fa980a675570939b737ebdde0471a4be40eb.function Unresolved_bfbaa190(uint256 arg0, address arg1) public payable {
// ...
(bool success, bytes memory ret0) = address(arg1).Unresolved_70a08231(address(this)); // balanceOf(this)
// ...
(bool success, bytes memory ret1) = address(arg1).Unresolved_a9059cbb(msg.sender); // transfer(msg.sender, balance)
// ...
}
Snippet: Decompiled helper-contract bfbaa190 function (selector 0xbfbaa190), reading the helper’s full token balance and transferring it to msg.sender, used to cash out USDT to the adversary EOA.
Adversary deployment and setup
0xe57e600cf8909a37833855005e5b2491414650b863329e13275806f940d17a7f (block 47454856, mechanism deploy).0x8842dd26... deploys helper contract 0x03ca8b57..., configuring references to:
0x55d398326f99059fF775485246999027B31979550x4f31fa980a675570939b737ebdde0471a4be40eb0x10ED43C7...0xe9c4d4f09...0x6AC860AE... and 0xD061A395....Exploit execution via flash loan and helper-token rewards
Tx: 0x994abe7906a4a955c103071221e5eaa734a30dccdcdaac63496ece2b698a0fc3 (block 47454937, mechanism flashloan+swap).
Inside this tx, helper contract 0x03ca8b57...:
0x4f31fa98....0x10ED43C7... into pair 0x42717781....H2O._calulate(to=helper) and causes H2O transfers from address(this) to the helper and the pair.22,688.210530453207954293 more USDT than it started with, while the pair has lost 22,738.210530453207954293 USDT and gained 83,280,442,437,787,583,376,702,261 H2O; the flash-loan pool holder receives a 50 USDT fee.Supporting ERC20 balance diffs (exploit tx prestate):
{
"erc20_balance_deltas": [
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x03ca8b574dd4250576f7bccc5707e6214e8c6e0d",
"delta": "22688210530453207954293"
},
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x42717781d93197247907f82482ae1d35d7bc101b",
"delta": "-22738210530453207954293"
},
{
"token": "0xe9c4d4f095c7943a9ef5ec01afd1385d011855a1",
"holder": "0x42717781d93197247907f82482ae1d35d7bc101b",
"delta": "83280442437787583376702261"
},
{
"token": "0xe9c4d4f095c7943a9ef5ec01afd1385d011855a1",
"holder": "0xe9c4d4f095c7943a9ef5ec01afd1385d011855a1",
"delta": "-83280442437787583376702261"
}
]
}
Snippet: Prestate-based ERC20 balance deltas for exploit tx 0x994abe79..., showing USDT and H2O movements between the helper, pair, and token contract.
Txs:
0x33cfa1257d85bcd77206438d5d7efdf22e6cea3c306fce646fa9aa7594b964ec (block 47454972, mechanism transfer).0xa0688f0f02aad1ecdbdf653c407ac9c5757a99444ee9beab7b3a60eecc59e1ae (block 47455274, mechanism transfer).In 0x33cfa125..., EOA 0x8842dd26... calls bfbaa190(USDT):
{
"erc20_balance_deltas": [
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x8842dd26fd301c74afc4df12e9cdabd9db107d1e",
"delta": "22770891449589918819435"
},
{
"token": "0x55d398326f99059ff775485246999027b3197955",
"holder": "0x03ca8b574dd4250576f7bccc5707e6214e8c6e0d",
"delta": "-22770891449589918819435"
}
]
}
0xa0688f0f..., the EOA repeats bfbaa190(USDT); prestate-based ERC20 balance diffs show zero USDT delta for both the helper and EOA, confirming no additional USDT is moved and only gas is spent.22,688.210530453207954293 USDT.22,685.838493440865657433 USDT.From the exploit tx prestate-based balance diffs:
H2O/USDT pair 0x42717781... loses 22,738.210530453207954293 USDT.
22,688.210530453207954293 USDT becomes adversary profit held by helper contract 0x03ca8b57... before cash-out.H2O balances:
0xe9c4d4f09... loses 83,280,442,437,787,583,376,702,261 H2O from its own treasury balance (address(this)).Consequences:
{0x8842dd26..., 0x03ca8b57...} realizes a fee-adjusted net profit of approximately 22,685.8385 USDT, fully supported by prestate-based on-chain balance diffs and DEX pricing.For completeness, the following BSC transactions are relevant to this incident:
0x3b0891a4eb65d916bb0069c69a51d9ff165bf69f83358e37523d0c275f2739bd — related (failed/earlier attempt).0x729c502a7dfd5332a9bdbcacec97137899ecc82c17d0797b9686a7f9f6005cb7 — related (smaller-scale attempt without durable profit).0x994abe7906a4a955c103071221e5eaa734a30dccdcdaac63496ece2b698a0fc3 — attacker-crafted exploit aggregation tx.0xd97694e02eb94f48887308a945a7e58b62bd6f20b28aaaf2978090e5535f3a8e — related (failed exploit attempt).0xe57e600cf8909a37833855005e5b2491414650b863329e13275806f940d17a7f — related (helper contract deployment).0x33cfa1257d85bcd77206438d5d7efdf22e6cea3c306fce646fa9aa7594b964ec — attacker-crafted USDT cash-out to EOA.0xa0688f0f02aad1ecdbdf653c407ac9c5757a99444ee9beab7b3a60eecc59e1ae — attacker-crafted noop cash-out attempt (gas only).Contract.sol source (verified on-chain contract source).0x03ca8b57... decompiled source and ABI (Heimdall decompilation).0x6AC860AE... and 0xD061A395... decompiled sources and ABIs (Heimdall decompilation).0x994abe79... cast trace and prestate-based balance diff, used to reconstruct the flash-loan exploit flow and exact ERC20 deltas.0x33cfa125... and 0xa0688f0f... prestate-based balance diffs, used to reconstruct USDT movements between helper and EOA.