All incidents

Legacy Payer Stale-Allowance Drain

Share
Nov 12, 2023 18:50 UTCAttackLoss: 366,058.04 BSC-USDPending manual check1 exploit txWindow: Atomic
Estimated Impact
366,058.04 BSC-USD
Label
Attack
Exploit Tx
1
Addresses
2
Attack Window
Atomic
Nov 12, 2023 18:50 UTC → Nov 12, 2023 18:50 UTC

Exploit Transactions

TX 1BSC
0x3dcb26a1f49eb4d02ca29960b4833bfb2e83d7b5d9591aed1204168944c8c9b3
Nov 12, 2023 18:50 UTCExplorer

Victim Addresses

0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7BSC
0x19a23DdAA47396335894229E0439D3D187D89eC9BSC

Loss Breakdown

366,058.04BSC-USD

Similar Incidents

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.

  1. 0x69e068Eb917115ed103278B812Ec7541f021CEa0 deploys helper 0x3918e0D26B41134c006e8D2d7e3206a53B006108 in tx 0xd63ad591fda133130ddc4bfbd4ecc6b5ae20e9b3bedf54f57f811f0f1dd2cb68.
  2. In tx 0x3dcb26a1f49eb4d02ca29960b4833bfb2e83d7b5d9591aed1204168944c8c9b3, the EOA calls the helper’s exploit entrypoint.
  3. The helper flash-borrows exactly the victim’s balance from Pancake pair 0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE.
  4. The helper approves the legacy payer and calls ac3994ec(...) with attacker-controlled fields and the public metadata word.
  5. The legacy payer consumes attacker-supplied funds, stores phase-1 state, and leaves the future source address unbound.
  6. The helper immediately calls 1270d364(...), this time providing victim wallet 0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7 as the source account.
  7. Because USDT still honors the victim’s allowance to the legacy payer, USDT.transferFrom(victim, helper, amount) succeeds.
  8. 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(...) and 1270d364(...), 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: 366058040206325661577467 BSC-USD
  • Flash-pair fee gain: 1101478556287840506252 BSC-USD
  • Attacker end-of-tx balance: 364956561650037821071215 BSC-USD
  • Native gas spent by attacker EOA: 691632000000000 wei

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

  1. Seed exploit transaction: 0x3dcb26a1f49eb4d02ca29960b4833bfb2e83d7b5d9591aed1204168944c8c9b3
  2. Helper deployment transaction: 0xd63ad591fda133130ddc4bfbd4ecc6b5ae20e9b3bedf54f57f811f0f1dd2cb68
  3. Managed wallet: 0x8C2D4ed92Badb9b65f278EfB8b440F4BC995fFe7
  4. Legacy payer: 0x19a23DdAA47396335894229E0439D3D187D89eC9
  5. USDT / BSC-USD token: 0x55d398326f99059fF775485246999027B3197955
  6. Pancake USDT/WBNB pair: 0x16b9a82891338f9bA80E2D6970FddA79D1eb0daE
  7. 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