All incidents

SwapX Stale-Allowance Drain

Share
Feb 27, 2023 07:32 UTCAttackLoss: 9,886,961.36 LZPending manual check1 exploit txWindow: Atomic
Estimated Impact
9,886,961.36 LZ
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Feb 27, 2023 07:32 UTC → Feb 27, 2023 07:32 UTC

Exploit Transactions

TX 1BSC
0xaee8ef10ac816834cd7026ec34f35bdde568191fe2fa67724fcf2739e48c3cae
Feb 27, 2023 07:32 UTCExplorer

Victim Addresses

0xdad254728a37d1e80c21afae688c64d0383cc307BSC

Loss Breakdown

9,886,961.36LZ

Similar Incidents

Root Cause Analysis

SwapX Stale-Allowance Drain

1. Incident Overview TL;DR

On BNB Chain block 26024420, EOA 0x7d192fa3a48c307100c3e663050291fff786aa1f sent tx 0xaee8ef10ac816834cd7026ec34f35bdde568191fe2fa67724fcf2739e48c3cae to helper contract 0x1c2b102f22c08694eee5b1f45e7973b6eaca3e92. That helper then called unverified SwapX contract 0x6d8981847eb3cc2234179d0f0e72f6b6b2421a01 with victim EOA 0xdad254728a37d1e80c21afae688c64d0383cc307 as the externally supplied token owner. SwapX spent the victim's pre-existing LZ allowance, sold the victim's entire 9886961355188035733617076 LZ position into thin Biswap pair 0xdb821bb482cfdae5d3b1a48eead8d2f74678d593, and returned only 6978443256059505388 BUSD to the victim. The same transaction then bought back almost the full manipulated Biswap LZ inventory for 50 BUSD and sold it into Pancake pair 0x2d518fdcc1c8e89b1abc6ed73b887e12e61f06de for 87911041488100790619279 BUSD.

The root cause is an authorization failure in SwapX selector 0x4f1f05bc(address[],uint256,uint256,uint256[],address). The runtime path trusts a calldata-supplied owner address instead of binding spending to msg.sender or to a fresh signature, so any stale ERC20 approval to SwapX can be converted into a permissionless arbitrary-from sale.

2. Key Background

Two background facts make this incident understandable:

  • ERC20 approvals are persistent capabilities. If a router accepts an arbitrary owner address in calldata and later calls transferFrom(owner, ...) without authenticating the caller, any stale approval becomes publicly spendable.
  • This incident used two different LZ/BUSD venues. Biswap pair 0xdb821bb482cfdae5d3b1a48eead8d2f74678d593 was thin before the exploit, with only 44800091627815976903 LZ against 6978474940356518127 BUSD. Pancake pair 0x2d518fdcc1c8e89b1abc6ed73b887e12e61f06de was much deeper, with 657041612962649340390689 LZ and 93767820911362903584313 BUSD.
  • The victim was an externally owned address, not a protocol-controlled helper. Incident-block state snapshots show no runtime code at 0xdad254728a37d1e80c21afae688c64d0383cc307, while helper contract 0x1c2b102f22c08694eee5b1f45e7973b6eaca3e92 had runtime code and storage slot 0 set to the attacking EOA.

These conditions meant a public stale approval could be monetized immediately through deterministic cross-pool arbitrage.

3. Vulnerability Analysis & Root Cause Summary

SwapX is unverified, so the decisive evidence comes from its incident-block runtime bytecode and the seed transaction trace. The selector table in the runtime disassembly exposes 0x4f1f05bc(address[],uint256,uint256,uint256[],address) as a public entrypoint. The bytecode route for that selector decodes the final address argument from calldata and jumps into execution without traversing the owner-gated CALLER checks that exist elsewhere in the contract. The live trace then shows the concrete consequence: msg.sender at SwapX was helper contract 0x1c2b102f22c08694eee5b1f45e7973b6eaca3e92, but SwapX still executed LZToken.transferFrom(0xdad254728a37d1e80c21afae688c64d0383cc307, 0xdb821bb482cfdae5d3b1a48eead8d2f74678d593, 9886961355188035733617076). That means the owner address came from calldata rather than from caller identity.

The broken invariant is straightforward: a swap router may only spend tokens from address owner when msg.sender == owner or when the call carries an authorization primitive that explicitly binds owner, the spender, and the swap parameters. SwapX violated that invariant on the 0x4f1f05bc -> 0x0304 -> 0x08a2 route. The attacker helper does not change the ACT classification, because its own bytecode only orchestrates public calls to SwapX, ERC20 contracts, Biswap, and Pancake. Any unprivileged adversary could deploy an equivalent wrapper and reproduce the same sequence.

The exploit required four conditions, all satisfied at block 26024420: the victim already held LZ and had approved SwapX, the approved token traded on a thin venue that could be distorted by a forced sale, a deeper exit venue existed, and the attacker could fund the counter-trade with 50 BUSD. The violated security principles were caller-bound authorization, rejecting arbitrary user-supplied owner addresses for spending, and preventing persistent approvals from becoming public execution rights.

4. Detailed Root Cause Analysis

The incident-block state before the exploit already contained everything the attacker needed. Victim EOA 0xdad254728a37d1e80c21afae688c64d0383cc307 held 9886961355188035733617076 LZ and had approved SwapX for 999998300000000000000000000 LZ. Helper contract 0x1c2b102f22c08694eee5b1f45e7973b6eaca3e92 held 50 BUSD and had storage slot 0 set to the attacking EOA. Biswap and Pancake both had live LZ/BUSD liquidity, but Biswap was shallow enough to be manipulated by a forced sale.

Incident trace excerpt:

0x6D8981847Eb3cc2234179d0F0e72F6b6b2421a01::4f1f05bc(...)
├─ LZToken::transferFrom(
│    0xdad254728A37D1E80C21AFae688C64d0383cc307,
│    0xDb821BB482cfDae5D3B1A48EeaD8d2F74678D593,
│    9886961355188035733617076
│  )
├─ 0xDb821BB482cfDae5D3B1A48EeaD8d2F74678D593::swap(
│    0,
│    6978443256059505388,
│    0xdad254728A37D1E80C21AFae688C64d0383cc307,
│    0x
│  )
└─ emit Swap(
     sender: 0x6D8981847Eb3cc2234179d0F0e72F6b6b2421a01,
     amount0In: 9886961355188035733617076,
     amount1Out: 6978443256059505388,
     to: 0xdad254728A37D1E80C21AFae688C64d0383cc307
   )

That trace is the on-chain proof of arbitrary-from spending: the caller at SwapX was the helper, but the tokens came from the victim EOA. The victim had no interaction inside the exploit transaction and provided no fresh signature. The forced sale moved Biswap from 44800091627815976903 LZ / 6978474940356518127 BUSD to 9887006155279663549593979 LZ / 31684297012739 BUSD, collapsing the local LZ price and leaving the victim with only dust-priced BUSD proceeds.

Incident-block helper bytecode findings:

- main routine 0xa558c55d enforces slot0 == CALLER
- the routine then only performs public calls:
  - SwapX selector 0x4f1f05bc
  - ERC20 balanceOf / allowance / approve
  - Biswap getAmountsOut / swapExactTokensForTokens
  - Pancake getAmountsOut / swapExactTokensForTokens

This matters because it rules out a privileged attacker-side dependency. The helper was owner-controlled by 0x7d192fa3a48c307100c3e663050291fff786aa1f, but it only automated public protocol calls. The exploit opportunity therefore remained permissionless at strategy level.

Incident-block SwapX bytecode findings:

- selector table exposes 0x4f1f05bc(address[],uint256,uint256,uint256[],address)
- offsets 0x008b-0x015e decode calldata, including the final owner address argument
- the route jumps to 0x0304 and then into execution at 0x08a2
- explicit CALLER equality checks exist only in separate admin paths around 0x0578-0x07f5

Those bytecode facts explain why the trace behaved the way it did. SwapX reasoned about a caller-supplied owner address rather than msg.sender, so a stale approval became enough to liquidate the victim's tokens on demand. Once the authorization bug forced the sale, the rest of the exploit was deterministic market plumbing: buy manipulated inventory cheaply on Biswap, then sell it at a much better price on PancakeSwap.

5. Adversary Flow Analysis

The adversary cluster consisted of:

  • EOA 0x7d192fa3a48c307100c3e663050291fff786aa1f, which paid gas for the seed transaction and owned helper contract 0x1c2b102f22c08694eee5b1f45e7973b6eaca3e92.
  • Helper contract 0x1c2b102f22c08694eee5b1f45e7973b6eaca3e92, which executed the public on-chain call sequence and received the BUSD profit.

The victim was externally owned LZ holder 0xdad254728a37d1e80c21afae688c64d0383cc307, an unverified EOA that had previously approved SwapX.

The adversary flow inside tx 0xaee8ef10ac816834cd7026ec34f35bdde568191fe2fa67724fcf2739e48c3cae was:

  1. The attacking EOA called its helper contract with SwapX and the LZ/BUSD path.
  2. The helper called SwapX selector 0x4f1f05bc, supplying the victim EOA as the token owner.
  3. SwapX spent the victim's entire LZ balance via transferFrom and sold it into thin Biswap pair 0xdb821bb482cfdae5d3b1a48eead8d2f74678d593.
  4. After the forced sale collapsed Biswap's BUSD side to 31684297012739, the helper approved Biswap router 0x3a6d8cA21D1CF76F653A67577FA0D27453350dD8 and spent 50 BUSD to buy 9886999877471233034310454 LZ.
  5. The helper then approved Pancake router 0x10ED43C718714eb63d5aA57B78B54704E256024E and sold those LZ into Pancake pair 0x2d518fdcc1c8e89b1abc6ed73b887e12e61f06de for 87911041488100790619279 BUSD.

The decision point in the exploit was not timing around private access or privileged sequencing; it was simply recognizing that SwapX would honor a stale approval from an arbitrary calldata-supplied owner and that Biswap was shallow enough for profitable manipulation.

6. Impact & Losses

The direct victim loss was:

  • 9886961355188035733617076 LZ from victim EOA 0xdad254728a37d1e80c21afae688c64d0383cc307 while receiving only 6978443256059505388 BUSD in return.

The attacker-side financial outcome was:

  • helper contract balance moved from 50 BUSD to 87911.041488100790619279 BUSD,
  • gross BUSD gain inside the helper was 87861041488100790619279 wei-denominated BUSD units,
  • gas cost was 0.001841135 BNB, valued at 0.562046080112859953 BUSD,
  • net profit remained 87860.479442020677759326 BUSD.

The incident also removed 87911041488100790619279 BUSD from PancakeSwap liquidity in the same transaction. In root-cause terms, this was an ATTACK category ACT exploit: a permissionless caller converted a public stale approval into a forced sale and immediate cross-pool extraction.

7. References

  1. Seed transaction 0xaee8ef10ac816834cd7026ec34f35bdde568191fe2fa67724fcf2739e48c3cae on BNB Chain block 26024420.
  2. Seed transaction metadata: /workspace/session/artifacts/collector/seed/56/0xaee8ef10ac816834cd7026ec34f35bdde568191fe2fa67724fcf2739e48c3cae/metadata.json
  3. Seed transaction trace: /workspace/session/artifacts/collector/seed/56/0xaee8ef10ac816834cd7026ec34f35bdde568191fe2fa67724fcf2739e48c3cae/trace.cast.log
  4. Seed transaction balance diff: /workspace/session/artifacts/collector/seed/56/0xaee8ef10ac816834cd7026ec34f35bdde568191fe2fa67724fcf2739e48c3cae/balance_diff.json
  5. LZToken verified source: /workspace/session/artifacts/collector/seed/56/0x3b78458981eb7260d1f781cb8be2caac7027dbe2/src/Contract.sol
  6. BiswapRouter02 verified source: https://api.etherscan.io/v2/api?chainid=56&module=contract&action=getsourcecode&address=0x3a6d8ca21d1cf76f653a67577fa0d27453350dd8
  7. Incident-block helper bytecode findings: /workspace/session/artifacts/auditor/iter_1/bytecode_findings.txt
  8. Incident-block helper disassembly: /workspace/session/artifacts/auditor/iter_1/helper_26024420.disasm.txt
  9. SwapX runtime code and disassembly: /workspace/session/artifacts/auditor/iter_1/swapx.code and /workspace/session/artifacts/auditor/iter_1/swapx.disasm.txt
  10. Supporting state snapshot: /workspace/session/artifacts/auditor/iter_1/supporting_state.txt