SimpleSwap Reserve Drain on Polygon
Exploit Transactions
0xa1f2377fc6c24d7cd9ca084cafec29e5d5c8442a10aae4e7e304a4fbf548be6dVictim Addresses
0x9d101e71064971165cd801e39c6b07234b65aa88PolygonLoss Breakdown
Similar Incidents
Polygon Uninitialized Clone Wallet Takeover and TEL Drain
35%Telcoin Wallet Reinitialization Drain
32%LibertiVault Reentrant Share Inflation
30%LunaFi VLFI Reward Replay
29%Midas LP Oracle Read-Only Reentrancy via Curve stMATIC/WPOL
28%0VIX ovGHST Oracle Inflation
28%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 token0x83ee54ccf462255ea3ec56fa8de6797d679276e7 - slot
2: USDT token0xc2132d05d31c914a87c6611c10748aeb04b58e8f - slot
5low 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:
SimpleSwapheld30,395.083207USDT.SimpleSwapheld1,697,049.1685NST.- the sell-enabled flag in storage slot
5was0x01.
Two verified token implementations are relevant to the exploit mechanics:
- Polygon USDT (
0xc2132d05d31c914a87c6611c10748aeb04b58e8f) uses6decimals and standard ERC20approve,transfer, andtransferFromsemantics. - NST (
0x83ee54ccf462255ea3ec56fa8de6797d679276e7) uses4decimals 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:
- The sender EOA calls the helper contract.
- The helper contract requests a Balancer flash loan of
40,000USDT. - The helper calls
SimpleSwapbuy selector0x6e41592cand receives388,000.0000NST plus a redundant NST allowance. - The helper immediately calls
SimpleSwapsell selector0x7cd0599b, receives37,636USDT plus a redundant37,636USDT allowance. - The helper observes that the victim still holds
31,559.083207USDT and spends that residual balance withUSDT.transferFrom(victim, helper, 31,559.083207). - The helper repays Balancer.
- The helper transfers
29,195.083207USDT 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.083207to0; - the fee recipient gains
1,200USDT and11,640.0000NST; - the profit-recipient EOA gains
29,195.083207USDT; - the sender EOA pays
213.003995229819871461MATIC in gas, equal to191.903606458358685174201879USD using the stated Chainlink MATIC/USD price.
6. Impact & Losses
The victim contract lost two reserve assets:
- USDT:
30395083207raw units (30,395.083207USDT,decimal = 6) - NST:
116400000raw units (11,640.0000NST,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
transferFromdrain - seed balance diff for victim losses, profit transfer, and gas-paying EOA deltas
- victim bytecode reconstruction notes for the duplicated
approveplustransferpayout logic - verified USDT and NST source code for ERC20 behavior and token-decimal interpretation
- seed trace for the full call path and final