On Sui mainnet on April 29, 2026, attacker 0x1a65086c85114c1a3f8dc74140115c6e18438d48d33a21fd112311561112d41e exploited AftermathFi Perpetuals by supplying a negative integrator taker fee to the clearing house fee path. The bug let the attacker turn fees into a collateral credit, deallocate that collateral back into an account balance, and then withdraw real USDC. On-chain evidence shows 11 profitable exploit transactions over 35m59.648s, matching Blockaid’s public summary. Across those 11 drains, the sender realized 1,139,927.483185 USDC of net USDC gain, while the protocol emitted 1,141,027.483185 USDC of WithdrewCollateral events and the attacker seeded only 1,100 USDC of maker-side deposits. Including three flat attempts and three losing attempts on the same exploit path, the sender’s net gain from perps interactions is 1,139,652.059864 USDC.

The exploit did not require protocol-level privileged access. The attacker repeatedly created fresh accounts, deposited only 100 USDC into the maker-side account, set the taker-side account’s integrator fee cap to 0, then passed a negative fee through clearing_house::create_integrator_info. Because the fee validation only enforced an upper bound and never enforced 0 <= fee, the negative signed fixed-point value passed validation. The downstream accounting subtracted this negative fee from the taker position’s collateral delta, which increased collateral instead of decreasing it. The attacker then deallocated free collateral from the clearing house back into the account balance and withdrew that balance as USDC.

Root Cause

Vulnerable Package

  • Core package: 0x21d001e8b07da2e3facb3e2d636bbaef43ba3c978bd84810368840b7d57c5068
  • Interface wrapper used by the exploit transactions: 0x9e208bed81b7072fa75af8f9eaca42ced3ec8154bb04f5b948c2a9455125d136
  • Signed fixed-point helper package: 0x46234ba81f3a5ba6571383233df0f9ea5fe60a3a327537be1f5fec447bced693
  • USDC clearing house object: 0x95969906ca735c9d44e8a44b5b7791b4dacaddf70fbdfbda40ccd3f8a9fd4920
  • Source type: on-chain Move bytecode disassembly preserved in artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/

At the time of collection, the current clearing house object snapshot reports paused: true, which is consistent with an emergency response after exploitation.

Critical Exploit Path Functions

  • clearing_house::calculate_taker_fees
  • clearing_house::process_fill_taker
  • account::add_integrator_config
  • clearing_house::deallocate_collateral
  • account::withdraw_collateral

Vulnerable Logic

The critical validation appears in clearing_house::calculate_taker_fees:

// clearing_house.disasm.move
// Conceptual reconstruction from on-chain disassembly

integrator_cfg = account::get_integrator_config(account, integrator_address);
max_fee = account::get_integrator_max_taker_fee(integrator_cfg);
assert!(ifixed::less_than_eq(integrator_info.taker_fee, max_fee), invalid_integrator_taker_fee());
integrator_fee = integrator_info.taker_fee;
return total_taker_fee * quote_delta, integrator_fee * quote_delta, some(integrator_address);

The collateral update happens in process_fill_taker:

// clearing_house.disasm.move
// Conceptual reconstruction from on-chain disassembly

(taker_fee_total, integrator_fee_total, integrator_addr) =
    calculate_taker_fees(position, account, quote_filled, base_fee, gas_fee, integrator_info);

collateral_delta = filled_value - (taker_fee_total + integrator_fee_total);
position::add_to_collateral_usd(position, collateral_delta, price_scale);

The account configuration path lets a user set a per-integrator ceiling:

// account.disasm.move
public(friend) add_integrator_config<T>(account: &mut Account<T>, integrator: address, max_taker_fee: u256) {
    dynamic_field::add(&mut account.id, keys::integrator_config(integrator), IntegratorConfig { max_taker_fee });
}

Why It’s Vulnerable

The validation only checks integrator_taker_fee <= max_taker_fee. In the exploit path, the attacker set max_taker_fee = 0 on the taker account, then passed a negative signed fixed-point fee. Because the fee type is represented as signed fixed-point inside u256, any negative value is less than or equal to zero, so the check passes.

The disassembly of ifixed::less_than_eq confirms the comparison is signed-aware, not raw unsigned:

public less_than_eq(a: u256, b: u256): bool {
    return (a ^ SIGN_BIT) <= (b ^ SIGN_BIT);
}

The disassembly of ifixed::to_balance also aborts on negative values, which confirms the type is intentionally signed and that negative fee values are first-class values, not decoding artifacts:

public to_balance(value: u256, scale: u256): u64 {
    if (value >= SIGN_BIT) abort 0;
    return (value / scale) as u64;
}

That signed behavior is exactly why the exploit works:

  1. The attacker sets max_taker_fee = 0.
  2. The attacker passes a negative integrator_taker_fee.
  3. calculate_taker_fees accepts it because negative <= 0.
  4. process_fill_taker computes filled_value - (taker_fee + negative_integrator_fee).
  5. Subtracting a negative fee increases collateral instead of reducing it.
  6. deallocate_free_collateral moves that synthetic collateral back from the clearing house into the taker account balance.
  7. withdraw_collateral turns that account balance into real USDC.

The missing condition is a lower-bound check. The code should have enforced 0 <= integrator_taker_fee <= max_taker_fee, but it enforced only the upper bound.

Attack Execution

High-Level Flow

For each profitable exploit transaction, the attacker followed the same pattern:

  1. Create two fresh accounts through interface::create_and_return_account.
  2. Create positions for both accounts.
  3. Deposit 100 USDC into the maker-side account only.
  4. Create an integrator vault for the attacker address.
  5. Add integrator config to the taker-side account with max_taker_fee = 0.
  6. Start a session for the maker-side account and place a limit order.
  7. Call clearing_house::create_integrator_info(attacker_address, negative_fee) and start a session for the taker-side account.
  8. Fill the order with place_market_order, causing calculate_taker_fees to accept the negative fee.
  9. End the session, deallocate free collateral, and withdraw collateral from the taker-side account.
  10. Share the clearing house and consume share policies to clean up the transient accounts.

The first profitable transaction demonstrates the pattern clearly:

SignalValue
Digest531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u
Maker deposit100.000000 USDC into account 1201
Taker configmax_taker_fee = 0 on account 1202
Filled taker order feeraw integrator_taker_fees encodes a negative signed fixed-point value
Paid integrator fees eventraw fees field carries the same negative signed value for the attacker integrator address
Withdrawal261,752.224099 USDC from account 1202
Net sender change+261,652.224099 USDC

The maker-side deposit was only 100 USDC, but the paired taker-side account withdrew 261,752.224099 USDC in the same PTB. That delta cannot be explained by trading PnL alone; it is synthetic collateral minted by subtracting a negative fee.

Profitable Drain Transactions

These 11 profitable transactions match the public “11 transactions in ~36 minutes” summary:

UTC timeDigestNet sender delta (USDC)
2026-04-29T08:55:50.037Z531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u261,652.224099
2026-04-29T08:55:54.621ZFQALUkZD1NnY9cDYoah2Zus6Z4TSp6voj1k2XKuA863Y199,564.413377
2026-04-29T09:06:40.373Zu4zE7XBnPKxETVPBfF7XtKYd694qEHa6QTx1LDHwujT71,202.339922
2026-04-29T09:11:40.312Z2s3ybJFhwzzLSMTsugGSAN9b1SgjaL1uc8yPaqVbJ1wz69,416.507849
2026-04-29T09:13:30.486Z9UJLFirEqNuXEPkVZQL1MfNHZTfHy2zDH99MyoSb7k6175,354.800010
2026-04-29T09:14:47.208ZCKfb8nYzTA9XWSs6A3PpaCT6HqQFp48nBLBPwEV4EGPC96,912.047700
2026-04-29T09:15:27.136Z7P8TFYuvACtoPG79SpsBhucEiJ7MvmdVj11DomLjgrXu69,538.670010
2026-04-29T09:18:13.659ZBqWYBVHnx8USdbKH24BNJD1gB7WxZWGeGz4WqMVmEFMn70,955.786777
2026-04-29T09:18:52.818ZCSaHz1ABSeUbaLv7zdqFkH4ibLxEBxPWqYTBmXyCXVXB68,239.650407
2026-04-29T09:19:05.836Z7gR7xpvy3GiPMUyB62R5a4j5yw7tx1NnubcVdf4535xg77,480.596967
2026-04-29T09:31:49.685Z4pGQdfFG96Ghqj1xqkaeeAgMQCpttivdkgSRUGc6wVD879,610.446067

Across those 11 transactions, WithdrewCollateral events total 1,141,027.483185 USDC, funded by only 1,100 USDC of maker-side seed deposits, for a profitable-subset net sender gain of 1,139,927.483185 USDC. The full sender history also contains six additional perps-path transactions in the same window: three flat attempts with zero USDC gain and three losing attempts totaling -275.423321 USDC. Those extra attempts explain why raw sender history shows 17 exploit-path transactions while the public alert mentioned 11 drains.

Financial Impact

MetricAmount
Gross WithdrewCollateral across 11 profitable drains1,141,027.483185 USDC
Maker-side seed deposits across 11 profitable drains1,100.000000 USDC
Net sender gain across 11 profitable drains1,139,927.483185 USDC
Net sender gain across all 17 perps-path transactions1,139,652.059864 USDC
Losing exploit attempts-275.423321 USDC
Profitable exploit count11
Total perps-path interaction count17
Profitable drain window35m59.648s

Post-exploit cashout behavior is also visible on-chain:

  • 939,000 USDC was transferred out through 19 direct transfer-only transactions to fresh addresses.
  • 200,000 USDC was swapped into 213,338.343466958 SUI across four router transactions.
  • 210,000 SUI was then forwarded to four recipient addresses across five transfers.
  • At the time of collection, the tracked attacker address still held 930.789478 USDC and 3,443.28852349 SUI.

This means the exploit and immediate fan-out can be separated cleanly:

  • Protocol drain: the perps transactions produced the USDC gain.
  • Cashout and fan-out: the later transfer-only and router transactions redistributed those proceeds.

Evidence

  • Primary exploit artifact: artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/tx_primary.json
  • Full sender ledger: artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/attacker_ledger.md
  • Parsed sender summary: artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/attacker_transaction_summary.json
  • Summary totals: artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/analysis_summary.json
  • Core package object: artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/core_package_object.json
  • Interface package object: artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/interface_package_object.json
  • Signed fixed-point package object: artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/ifixed_package_object.json
  • Relevant disassemblies:
    • artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/clearing_house.disasm.move
    • artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/account.disasm.move
    • artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/position.disasm.move
    • artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/constants.disasm.move
    • artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/ifixed.disasm.move
  • Current paused clearing house snapshot: artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/clearing_house_object_current.json
  • Attacker activity: https://suivision.xyz/account/0x1a65086c85114c1a3f8dc74140115c6e18438d48d33a21fd112311561112d41e?tab=Activity
  • Primary exploit transaction: https://suivision.xyz/txblock/531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u