All incidents

SimpleSwap Reserve Drain on Polygon

Share
Jun 02, 2023 03:02 UTCAttackLoss: 30,395.08 USDT, 11,640 NSTPending manual check1 exploit txWindow: Atomic
Estimated Impact
30,395.08 USDT, 11,640 NST
Label
Attack
Exploit Tx
1
Addresses
1
Attack Window
Atomic
Jun 02, 2023 03:02 UTC → Jun 02, 2023 03:02 UTC

Exploit Transactions

TX 1Polygon
0xa1f2377fc6c24d7cd9ca084cafec29e5d5c8442a10aae4e7e304a4fbf548be6d
Jun 02, 2023 03:02 UTCExplorer

Victim Addresses

0x9d101e71064971165cd801e39c6b07234b65aa88Polygon

Loss Breakdown

30,395.08USDT
11,640NST

Similar Incidents

Root Cause Analysis

SimpleSwap Reserve Drain on Polygon

1. Incident Overview TL;DR

At Polygon block 43430815, transaction 0xa1f2377fc6c24d7cd9ca084cafec29e5d5c8442a10aae4e7e304a4fbf548be6d used Balancer's public flash-loan entrypoint to exploit the unverified SimpleSwap contract at 0x9d101e71064971165cd801e39c6b07234b65aa88. The attacker borrowed 40,000 USDT, bought NST through SimpleSwap, sold the NST back, and then used the victim-created USDT allowance to pull the victim's remaining USDT reserve with a standard transferFrom.

The root cause is a duplicated payout entitlement in both public swap directions. Each vulnerable selector first approves the caller for the output amount and then transfers the same output amount directly. That leaves a live allowance against protocol reserves after settlement, so the caller can spend the same reserve balance a second time. The victim lost its full 30,395.083207 USDT reserve and 11,640.0000 NST through round-trip fee leakage. The adversary cluster's changed-value holdings increased from 3146.1114970822351124633675 USD to 32149.291097623876427289165621 USD, for a deterministic net increase of 29003.179600541641314825798121 USD after gas.

2. Key Background

SimpleSwap is an unverified Polygon contract, but its runtime strings and bytecode-reconstruction artifacts identify it as a fixed-price USDT/NST swap venue. The collected analysis shows the following storage layout for the live runtime:

  • slot 1: NST token 0x83ee54ccf462255ea3ec56fa8de6797d679276e7
  • slot 2: USDT token 0xc2132d05d31c914a87c6611c10748aeb04b58e8f
  • slot 5 low byte: sell-enabled flag

At the end of block 43430814, before the exploit transaction entered the next block, the public state already satisfied every exploit precondition:

  • SimpleSwap held 30,395.083207 USDT.
  • SimpleSwap held 1,697,049.1685 NST.
  • the sell-enabled flag in storage slot 5 was 0x01.

Two verified token implementations are relevant to the exploit mechanics:

  • Polygon USDT (0xc2132d05d31c914a87c6611c10748aeb04b58e8f) uses 6 decimals and standard ERC20 approve, transfer, and transferFrom semantics.
  • NST (0x83ee54ccf462255ea3ec56fa8de6797d679276e7) uses 4 decimals and standard ERC20 allowance semantics.

Balancer Vault at 0xba12222222228d8ba445958a75a0704d566bf2c8 supplied the flash liquidity. The victim's swap logic also routes a fixed 3% fee to 0xbb5a92c69355dd75480e66db8d07cea4443cbea1, which is why the round-trip leaves the victim short both USDT and NST.

3. Vulnerability Analysis & Root Cause Summary

This is an ATTACK-class ACT exploit against victim reserve accounting. The critical invariant is simple: for any completed swap, the caller must receive exactly one claim on amountOut, and no residual allowance should remain that lets the caller spend the same reserve balance again. SimpleSwap violates that invariant in both public swap selectors.

The victim's buy selector 0x6e41592c computes NST output from USDT input, pulls USDT from the caller, then executes NST.approve(caller, amountOut) followed by NST.transfer(caller, amountOut). The sell selector 0x7cd0599b mirrors the same pattern in the opposite direction: it pulls NST from the caller, then executes USDT.approve(caller, amountOut) followed by USDT.transfer(caller, amountOut). In both cases the contract grants an allowance for the full payout and also transfers the full payout immediately, so the allowance becomes a second entitlement rather than a bookkeeping artifact.

The reconstructed victim logic is summarized below.

Victim bytecode reconstruction

Selector 0x6e41592c:
- USDT.transferFrom(caller, victim, 97% of input)
- USDT.transferFrom(caller, feeRecipient, 3% of input)
- NST.approve(caller, amountOut)
- NST.transfer(caller, amountOut)

Selector 0x7cd0599b:
- NST.transferFrom(caller, victim, 97% of input)
- NST.transferFrom(caller, feeRecipient, 3% of input)
- USDT.approve(caller, amountOut)
- USDT.transfer(caller, amountOut)

The decisive breakpoint is the approve-then-transfer pair on the payout asset. Once the direct transfer completes, the live allowance still exists and can be consumed immediately with a standard ERC20 transferFrom.

4. Detailed Root Cause Analysis

The exploit bootstraps from USDT alone because the bug exists in both swap directions. First, the attacker borrows 40,000 USDT from Balancer with zero flash-loan fee. The attacker helper then calls 0x6e41592c(40_000e6) on SimpleSwap. That buy leg transfers 38,800 USDT to the victim, 1,200 USDT to the fee recipient, returns 388,000.0000 NST to the attacker, and also leaves a redundant NST allowance from the victim to the attacker helper.

Second, the helper calls 0x7cd0599b(388_000.0000 NST). That sell leg transfers 376,360.0000 NST-equivalent input to the victim and 11,640.0000 NST to the fee recipient, then pays out 37,636 USDT to the helper and simultaneously leaves a fresh 37,636 USDT allowance from the victim to the helper. After this direct payout, the victim still holds 31,559.083207 USDT.

The seed trace shows the exploit transition clearly:

Observed execution from the seed trace

... 0x9D101E71064971165Cd801E39c6B07234B65aa88::7cd0599b(...)
... UChildERC20::approve(0x3BB7..., 37636000000)
... UChildERC20::transfer(0x3BB7..., 37636000000)
... UChildERC20::transferFrom(
      0x9D101E71064971165Cd801E39c6B07234B65aa88,
      0x3BB7a0f2fe88ABA35408C64F588345481490Fe93,
      31559083207
    )

That final transferFrom is not a separate vulnerability; it is the direct realization of the duplicated entitlement created by the sell path. The allowance granted by the victim exceeds the victim's remaining USDT balance, so the attacker can drain the full residual reserve in the same transaction. The helper then repays the 40,000 USDT flash loan and transfers the remaining 29,195.083207 USDT profit to 0xb867099768d5d58c090be8db803b83f1aaeb9eeb.

The ACT conditions are fully public and deterministic:

  • the sell-enabled flag must already be on;
  • the victim must hold USDT and NST reserves;
  • the attacker needs temporary capital in one reserve asset;
  • no privileged role, hidden calldata, or private key is required.

Because Balancer flash liquidity is public and both vulnerable selectors are public, any unprivileged actor observing the same block-43430814 pre-state could have executed the same strategy.

5. Adversary Flow Analysis

The adversary cluster contains three observable roles:

  • sender EOA 0xcb3585f3e09f0238a3f61838502590a23f15bb5b
  • helper contract 0x3bb7a0f2fe88aba35408c64f588345481490fe93
  • profit-recipient EOA 0xb867099768d5d58c090be8db803b83f1aaeb9eeb

The end-to-end flow in the seed transaction is:

  1. The sender EOA calls the helper contract.
  2. The helper contract requests a Balancer flash loan of 40,000 USDT.
  3. The helper calls SimpleSwap buy selector 0x6e41592c and receives 388,000.0000 NST plus a redundant NST allowance.
  4. The helper immediately calls SimpleSwap sell selector 0x7cd0599b, receives 37,636 USDT plus a redundant 37,636 USDT allowance.
  5. The helper observes that the victim still holds 31,559.083207 USDT and spends that residual balance with USDT.transferFrom(victim, helper, 31,559.083207).
  6. The helper repays Balancer.
  7. The helper transfers 29,195.083207 USDT profit to the profit-recipient EOA.

The trace and balance-diff artifacts also confirm the economic side effects:

  • the victim's USDT balance moves from 30,395.083207 to 0;
  • the fee recipient gains 1,200 USDT and 11,640.0000 NST;
  • the profit-recipient EOA gains 29,195.083207 USDT;
  • the sender EOA pays 213.003995229819871461 MATIC in gas, equal to 191.903606458358685174201879 USD using the stated Chainlink MATIC/USD price.

6. Impact & Losses

The victim contract lost two reserve assets:

  • USDT: 30395083207 raw units (30,395.083207 USDT, decimal = 6)
  • NST: 116400000 raw units (11,640.0000 NST, decimal = 4)

USDT was drained to zero. NST was not fully emptied, but the round-trip fee mechanism still extracted 11,640.0000 NST from the victim over the course of the exploit. The direct profit recipient gained 29,195.083207 USDT, while the remaining difference between gross victim loss and profit is explained by protocol fee transfers and gas.

Using the changed-value adversary cluster defined in the root-cause artifact, the cluster held 3146.1114970822351124633675 USD before execution and 32149.291097623876427289165621 USD after execution, for a deterministic net increase of 29003.179600541641314825798121 USD.

7. References

  • Seed exploit transaction: 0xa1f2377fc6c24d7cd9ca084cafec29e5d5c8442a10aae4e7e304a4fbf548be6d
  • Victim contract: 0x9d101e71064971165cd801e39c6b07234b65aa88
  • Balancer Vault: 0xba12222222228d8ba445958a75a0704d566bf2c8
  • USDT token: 0xc2132d05d31c914a87c6611c10748aeb04b58e8f
  • NST token: 0x83ee54ccf462255ea3ec56fa8de6797d679276e7
  • Fee recipient: 0xbb5a92c69355dd75480e66db8d07cea4443cbea1
  • Public evidence used for validation:
    • seed trace for the full call path and final transferFrom drain
    • seed balance diff for victim losses, profit transfer, and gas-paying EOA deltas
    • victim bytecode reconstruction notes for the duplicated approve plus transfer payout logic
    • verified USDT and NST source code for ERC20 behavior and token-decimal interpretation