This is a lower bound: only assets with reliable historical USD prices are counted, so the actual loss may be higher.
0xa1f2377fc6c24d7cd9ca084cafec29e5d5c8442a10aae4e7e304a4fbf548be6d0x9d101e71064971165cd801e39c6b07234b65aa88PolygonAt 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.
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:
1: NST token 0x83ee54ccf462255ea3ec56fa8de6797d679276e72: USDT token 0xc2132d05d31c914a87c6611c10748aeb04b58e8f5 low byte: sell-enabled flagAt 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.5 was 0x01.Two verified token implementations are relevant to the exploit mechanics:
0xc2132d05d31c914a87c6611c10748aeb04b58e8f) uses 6 decimals and standard ERC20 approve, transfer, and transferFrom semantics.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.
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.
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:
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.
The adversary cluster contains three observable roles:
0xcb3585f3e09f0238a3f61838502590a23f15bb5b0x3bb7a0f2fe88aba35408c64f588345481490fe930xb867099768d5d58c090be8db803b83f1aaeb9eebThe end-to-end flow in the seed transaction is:
40,000 USDT.SimpleSwap buy selector 0x6e41592c and receives 388,000.0000 NST plus a redundant NST allowance.SimpleSwap sell selector 0x7cd0599b, receives 37,636 USDT plus a redundant 37,636 USDT allowance.31,559.083207 USDT and spends that residual balance with USDT.transferFrom(victim, helper, 31,559.083207).29,195.083207 USDT profit to the profit-recipient EOA.The trace and balance-diff artifacts also confirm the economic side effects:
30,395.083207 to 0;1,200 USDT and 11,640.0000 NST;29,195.083207 USDT;213.003995229819871461 MATIC in gas, equal to 191.903606458358685174201879 USD using the stated Chainlink MATIC/USD price.The victim contract lost two reserve assets:
30395083207 raw units (30,395.083207 USDT, decimal = 6)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.
0xa1f2377fc6c24d7cd9ca084cafec29e5d5c8442a10aae4e7e304a4fbf548be6d0x9d101e71064971165cd801e39c6b07234b65aa880xba12222222228d8ba445958a75a0704d566bf2c80xc2132d05d31c914a87c6611c10748aeb04b58e8f0x83ee54ccf462255ea3ec56fa8de6797d679276e70xbb5a92c69355dd75480e66db8d07cea4443cbea1transferFrom drainapprove plus transfer payout logic