Legacy Payer Stale-Allowance Drain
Exploit Transactions
0x3dcb26a1f49eb4d02ca29960b4833bfb2e83d7b5d9591aed1204168944c8c9b3Victim Addresses
0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7BSC0x19a23DdAA47396335894229E0439D3D187D89eC9BSCLoss Breakdown
Similar Incidents
SwapX Stale-Allowance Drain
43%FiberRouter Allowance Reuse Drain
36%MetaPoint Public Approval Drain
35%BSC WBNB allowance drain from unsafe spender approvals
34%EEECOIN Public Helper LP Drain
33%STRAC Callback Drain
32%Root Cause Analysis
Legacy Payer Stale-Allowance Drain
1. Incident Overview TL;DR
On 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.
2. Key Background
Two unverified BNB Chain contracts define the incident:
- Managed wallet
0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7 - Legacy payer
0x19a23DdAA47396335894229E0439D3D187D89eC9
The 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.
3. Vulnerability Analysis & Root Cause Summary
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.
4. Detailed Root Cause Analysis
4.1 Victim-side code behavior
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:
- internal payer already changed away from the legacy payer,
- internal
records(USDT, legacy_payer)already cleared, - token-side approval to the legacy payer still live on USDT.
4.2 Legacy payer breakpoint
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.
4.3 Adversary helper behavior
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.
4.4 Profit predicate
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
5. Adversary Flow Analysis
The adversary flow is a single-transaction flash-swap-assisted allowance drain.
0x69e068Eb917115ed103278B812Ec7541f021CEa0deploys helper0x3918e0D26B41134c006e8D2d7e3206a53B006108in tx0xd63ad591fda133130ddc4bfbd4ecc6b5ae20e9b3bedf54f57f811f0f1dd2cb68.- In tx
0x3dcb26a1f49eb4d02ca29960b4833bfb2e83d7b5d9591aed1204168944c8c9b3, the EOA calls the helper’s exploit entrypoint. - The helper flash-borrows exactly the victim’s balance from Pancake pair
0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE. - The helper approves the legacy payer and calls
ac3994ec(...)with attacker-controlled fields and the public metadata word. - The legacy payer consumes attacker-supplied funds, stores phase-1 state, and leaves the future source address unbound.
- The helper immediately calls
1270d364(...), this time providing victim wallet0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7as the source account. - Because USDT still honors the victim’s allowance to the legacy payer,
USDT.transferFrom(victim, helper, amount)succeeds. - The helper repays the pair with fee, transfers the remaining USDT profit to the attacker EOA, and self-destructs.
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:
- a victim must hold the token and approve the legacy payer,
- public flash liquidity must be available to seed phase 1,
- the attacker must replay matching order fields across
ac3994ec(...)and1270d364(...), changing only the source wallet in the second call.
6. Impact & Losses
The impact is a full drain of the victim managed wallet’s USDT balance in a single adversary-crafted transaction.
- Victim wallet loss:
366058040206325661577467BSC-USD - Flash-pair fee gain:
1101478556287840506252BSC-USD - Attacker end-of-tx balance:
364956561650037821071215BSC-USD - Native gas spent by attacker EOA:
691632000000000wei
The 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.
7. References
- Seed exploit transaction:
0x3dcb26a1f49eb4d02ca29960b4833bfb2e83d7b5d9591aed1204168944c8c9b3 - Helper deployment transaction:
0xd63ad591fda133130ddc4bfbd4ecc6b5ae20e9b3bedf54f57f811f0f1dd2cb68 - Managed wallet:
0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7 - Legacy payer:
0x19a23DdAA47396335894229E0439D3D187D89eC9 - USDT / BSC-USD token:
0x55d398326f99059fF775485246999027B3197955 - Pancake USDT/WBNB pair:
0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE - Collected artifacts used for validation:
- exploit transaction metadata
- verbose trace for the seed transaction
- pre/post balance diff for the seed transaction
- persisted RPC observations for block
33435892 - bytecode analysis of the managed wallet, legacy payer, and attacker helper
- deterministic fee computation artifact
- fork-based PoC execution log