Calculated from recorded token losses using historical USD prices at the incident time.
0x3dcb26a1f49eb4d02ca29960b4833bfb2e83d7b5d9591aed1204168944c8c9b30x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7BSC0x19a23DdAA47396335894229E0439D3D187D89eC9BSCOn BNB Chain, transaction 0x3dcb26a1f49eb4d02ca29960b4833bfb2e83d7b5d9591aed1204168944c8c9b3 drained the full 366058040206325661577467 BSC-USD balance from managed wallet 0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7. The attacker EOA 0x69e068Eb917115ed103278B812Ec7541f021CEa0 called helper contract 0x3918e0D26B41134c006e8D2d7e3206a53B006108, which flash-borrowed USDT from Pancake pair 0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE, seeded state in legacy payer 0x19a23DdAA47396335894229E0439D3D187D89eC9, reused that state to pull funds from the victim wallet, repaid the pair, and retained 364956561650037821071215 BSC-USD.
The root cause is an authorization-binding failure in the legacy payer’s public ac3994ec(...) and 1270d364(...) flow, combined with a stale wallet approval. The managed wallet was originally configured to trust the legacy payer and later rotated to a new payer, but wallet bytecode analysis shows setPayer(address) only rewrites the configured payer slot and does not revoke ERC20 approvals. The legacy payer then persists phase-1 state without binding the future token-owner/source account, so 1270d364(...) can later substitute any address that still approved the payer on USDT.
Two unverified BNB Chain contracts define the incident:
0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe70x19a23DdAA47396335894229E0439D3D187D89eC9The wallet was deployed with constructor arguments that explicitly linked it to the legacy payer and bot manager:
constructor args:
payer = 0x19a23DdAA47396335894229E0439D3D187D89eC9
botManager = 0x3ebf963D94Bccfb5A91Ea96ec3d2Fc7593723b19
By block 33435892, on-chain state already showed that the wallet had switched its configured payer to 0x7942829d5975daf7AE3542ceD29f4Da4ED38B91b, and records(USDT, legacy_payer) was (0, 0, 0), but the USDT allowance to the legacy payer was still 2^256 - 1. Independent RPC validation confirms that exact stale-approval condition:
owner() -> 0xDeE322Df76D9361FAB825d14B77E385d9baC082D
payer() -> 0x7942829d5975daf7AE3542ceD29f4Da4ED38B91b
records(USDT, legacy_payer) -> (0, 0, 0)
USDT.allowance(wallet, legacy_payer) -> 115792089237316195423570985008687907853269984665640564039457584007913129639935
USDT.balanceOf(wallet) -> 366058040206325661577467
The wallet-side bytecode analysis explains why this stale approval persisted: selector 0xd55e6975 (setPayer(address)) performs an owner check and rewrites storage slot 5, but it does not call USDT approve or revoke any prior allowance. That leaves token-side approval enforcement entirely to the USDT contract, independent of the wallet’s internal bookkeeping.
This is an ATTACK-category ACT exploit against a public spender flow, not a privileged compromise. The relevant invariant is straightforward: a spend executor must bind authorization state to the exact token-owner/source account that may later be debited. The legacy payer violates that invariant.
In ac3994ec(...), the payer accepts attacker-controlled order fields, transfers attacker-supplied tokens through a shared transferFrom helper, and writes phase-1 state into the storage root starting at 0xa8cf6bc00c84d03b3515c1955f095aa6652a0b6680186f022e1ad3a8d665ca95. That persisted state records the phase and helper/recipient address, but it does not store the future source wallet whose tokens will later be pulled. In 1270d364(...), the payer reuses the same storage root and passes a fresh runtime source address into the same transfer helper, which becomes USDT.transferFrom(victim, recipient, amount).
The managed wallet’s stale approval is the enabling condition, but the root cause sits in the payer’s authorization model. The wallet had already rotated away from the legacy payer, yet token approval remained active because the wallet never revoked it. Once an attacker seeded matching phase-1 state with their own flash-borrowed funds, the legacy payer’s public execution path allowed the attacker to substitute the victim wallet as the debit source during phase 2.
The exploit is permissionless and deterministic. The attacker helper computes the required metadata word from public block data only, ((block.timestamp + 1) << 96) | (chainid << 64), then uses public Pancake liquidity, public legacy-payer selectors, and the victim’s publicly visible stale USDT approval to realize profit.
Bytecode analysis of the managed wallet identifies the relevant selector map:
0x123119cd -> payer()
0x50a3238b -> botManager()
0xe563037e -> balancer()
0x853ea853 -> records(address,address)
0x99374642 -> roles(address)
0xd55e6975 -> setPayer(address)
The critical wallet function is setPayer(address). The disassembly summary in the auditor artifact shows that its implementation is limited to:
owner check
SLOAD slot 5
mask/update payer field
SSTORE slot 5
There is no ERC20 interaction and no approval cleanup. That is why pre-state can simultaneously show:
records(USDT, legacy_payer) already cleared,The payer bytecode exposes the two public selectors used by the incident:
0xac3994ec(uint256,uint256,uint256,uint256,address,uint256,uint256,address)
0x1270d364(uint256,uint256,uint256,uint256,address,uint256,uint256,address,address,uint256)
The auditor’s contract-code analysis shows both paths dispatch into the same internal ERC20 transferFrom helper. The exploit-relevant behavior is:
ac3994ec(...):
- transferFrom(attacker_helper, attacker_helper, amount)
- derive storage root from attacker-controlled metadata
- write phase 1 + helper/recipient into 0xa8cf...95/96
- do not store the future source wallet
1270d364(...):
- reuse the same storage root
- pass runtime source_arg and recipient_arg into the same transfer helper
- execute token.transferFrom(source_arg, recipient_arg, amount)
- increment phase from 1 to 2
The collected seed trace shows the exact transition:
0x19a23...9ec9::ac3994ec(...)
BEP20USDT::transferFrom(0x3918...6108, 0x3918...6108, 366058040206325661577467)
@ 0xa8cf...ca95: 0 -> 0x...01 3918e0d26b41134c006e8d2d7e3206a53b006108
0x19a23...9ec9::1270d364(...)
BEP20USDT::transferFrom(0x8C2D...fFe7, 0x3918...6108, 366058040206325661577467)
@ 0xa8cf...ca95: 0x...01 3918e0d26b41134c006e8d2d7e3206a53b006108
-> 0x...02 3918e0d26b41134c006e8d2d7e3206a53b006108
That trace is the code-level breakpoint in practice: phase-1 state is seeded using attacker funds, but the victim wallet is introduced only in the later transferFrom call.
The helper deployment transaction 0xd63ad591fda133130ddc4bfbd4ecc6b5ae20e9b3bedf54f57f811f0f1dd2cb68 embeds constructor argument 0x69e068eb917115ed103278b812ec7541f021cea0, the final profit recipient. Bytecode analysis of the helper shows it exposes:
0x53d0489c(address,address) -> exploit entrypoint
0xdf8de3e7(address) -> token-claim function
0xb603cd80() -> self-destruct/cleanup path
Most importantly, the helper computes the metadata value with public information only:
((block.timestamp + 1) << 96) | (chainid << 64)
No privileged signer, private key, or hidden victim state is required. The exploit is therefore a reproducible ACT opportunity as long as the stale approval and sufficient public flash liquidity exist.
The attacker paid 691632000000000 wei in gas. Using the pre-state Pancake USDT/WBNB reserves
reserve_usdt_raw = 14933047931656531245040988
reserve_wbnb_raw = 60136752053422081954155
the fee converts deterministically to:
fee_in_reference_asset_raw = native_fee_wei * reserve_usdt_raw / reserve_wbnb_raw
= 171744789241236470
The resulting BSC-USD profit values are:
value_before = 0
value_after = 364956561650037821071215
value_delta = 364956389905248579834745
The adversary flow is a single-transaction flash-swap-assisted allowance drain.
0x69e068Eb917115ed103278B812Ec7541f021CEa0 deploys helper 0x3918e0D26B41134c006e8D2d7e3206a53B006108 in tx 0xd63ad591fda133130ddc4bfbd4ecc6b5ae20e9b3bedf54f57f811f0f1dd2cb68.0x3dcb26a1f49eb4d02ca29960b4833bfb2e83d7b5d9591aed1204168944c8c9b3, the EOA calls the helper’s exploit entrypoint.0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE.ac3994ec(...) with attacker-controlled fields and the public metadata word.1270d364(...), this time providing victim wallet 0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7 as the source account.USDT.transferFrom(victim, helper, amount) succeeds.The trace excerpt below captures the exploit core:
flash_pair::swap(366058040206325661577467, 0, helper, ...)
0x19a23...9ec9::ac3994ec(...)
BEP20USDT::transferFrom(helper, helper, 366058040206325661577467)
0x19a23...9ec9::1270d364(...)
BEP20USDT::transferFrom(victim_wallet, helper, 366058040206325661577467)
BEP20USDT::transfer(flash_pair, 367159518762613502083719)
BEP20USDT::transfer(attacker_eoa, 364956561650037821071215)
The ACT conditions are also concrete:
ac3994ec(...) and 1270d364(...), changing only the source wallet in the second call.The impact is a full drain of the victim managed wallet’s USDT balance in a single adversary-crafted transaction.
366058040206325661577467 BSC-USD1101478556287840506252 BSC-USD364956561650037821071215 BSC-USD691632000000000 weiThe collected balance diff records the loss and profit directly:
{
"victim_wallet_delta": "-366058040206325661577467",
"attacker_eoa_delta": "364956561650037821071215",
"flash_pair_delta": "1101478556287840506252"
}
The affected parties are the wallet owner controlling 0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7, the legacy payer integration 0x19a23DdAA47396335894229E0439D3D187D89eC9, and any system that assumed rotating the configured payer also retired token-side approval risk.
0x3dcb26a1f49eb4d02ca29960b4833bfb2e83d7b5d9591aed1204168944c8c9b30xd63ad591fda133130ddc4bfbd4ecc6b5ae20e9b3bedf54f57f811f0f1dd2cb680x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe70x19a23DdAA47396335894229E0439D3D187D89eC90x55d398326f99059fF775485246999027B31979550x16b9a82891338f9bA80E2D6970FddA79D1eb0daE33435892