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.
0x21d001e8b07da2e3facb3e2d636bbaef43ba3c978bd84810368840b7d57c50680x9e208bed81b7072fa75af8f9eaca42ced3ec8154bb04f5b948c2a9455125d1360x46234ba81f3a5ba6571383233df0f9ea5fe60a3a327537be1f5fec447bced6930x95969906ca735c9d44e8a44b5b7791b4dacaddf70fbdfbda40ccd3f8a9fd4920artifacts/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.
clearing_house::calculate_taker_feesclearing_house::process_fill_takeraccount::add_integrator_configclearing_house::deallocate_collateralaccount::withdraw_collateralThe 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 });
}
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:
max_taker_fee = 0.integrator_taker_fee.calculate_taker_fees accepts it because negative <= 0.process_fill_taker computes filled_value - (taker_fee + negative_integrator_fee).deallocate_free_collateral moves that synthetic collateral back from the clearing house into the taker account balance.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.
For each profitable exploit transaction, the attacker followed the same pattern:
interface::create_and_return_account.100 USDC into the maker-side account only.max_taker_fee = 0.clearing_house::create_integrator_info(attacker_address, negative_fee) and start a session for the taker-side account.place_market_order, causing calculate_taker_fees to accept the negative fee.The first profitable transaction demonstrates the pattern clearly:
| Signal | Value |
|---|---|
| Digest | 531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u |
| Maker deposit | 100.000000 USDC into account 1201 |
| Taker config | max_taker_fee = 0 on account 1202 |
| Filled taker order fee | raw integrator_taker_fees encodes a negative signed fixed-point value |
| Paid integrator fees event | raw fees field carries the same negative signed value for the attacker integrator address |
| Withdrawal | 261,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.
These 11 profitable transactions match the public “11 transactions in ~36 minutes” summary:
| UTC time | Digest | Net sender delta (USDC) |
|---|---|---|
| 2026-04-29T08:55:50.037Z | 531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u | 261,652.224099 |
| 2026-04-29T08:55:54.621Z | FQALUkZD1NnY9cDYoah2Zus6Z4TSp6voj1k2XKuA863Y | 199,564.413377 |
| 2026-04-29T09:06:40.373Z | u4zE7XBnPKxETVPBfF7XtKYd694qEHa6QTx1LDHwujT | 71,202.339922 |
| 2026-04-29T09:11:40.312Z | 2s3ybJFhwzzLSMTsugGSAN9b1SgjaL1uc8yPaqVbJ1wz | 69,416.507849 |
| 2026-04-29T09:13:30.486Z | 9UJLFirEqNuXEPkVZQL1MfNHZTfHy2zDH99MyoSb7k61 | 75,354.800010 |
| 2026-04-29T09:14:47.208Z | CKfb8nYzTA9XWSs6A3PpaCT6HqQFp48nBLBPwEV4EGPC | 96,912.047700 |
| 2026-04-29T09:15:27.136Z | 7P8TFYuvACtoPG79SpsBhucEiJ7MvmdVj11DomLjgrXu | 69,538.670010 |
| 2026-04-29T09:18:13.659Z | BqWYBVHnx8USdbKH24BNJD1gB7WxZWGeGz4WqMVmEFMn | 70,955.786777 |
| 2026-04-29T09:18:52.818Z | CSaHz1ABSeUbaLv7zdqFkH4ibLxEBxPWqYTBmXyCXVXB | 68,239.650407 |
| 2026-04-29T09:19:05.836Z | 7gR7xpvy3GiPMUyB62R5a4j5yw7tx1NnubcVdf4535xg | 77,480.596967 |
| 2026-04-29T09:31:49.685Z | 4pGQdfFG96Ghqj1xqkaeeAgMQCpttivdkgSRUGc6wVD8 | 79,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.
| Metric | Amount |
|---|---|
Gross WithdrewCollateral across 11 profitable drains | 1,141,027.483185 USDC |
| Maker-side seed deposits across 11 profitable drains | 1,100.000000 USDC |
| Net sender gain across 11 profitable drains | 1,139,927.483185 USDC |
| Net sender gain across all 17 perps-path transactions | 1,139,652.059864 USDC |
| Losing exploit attempts | -275.423321 USDC |
| Profitable exploit count | 11 |
| Total perps-path interaction count | 17 |
| Profitable drain window | 35m59.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.930.789478 USDC and 3,443.28852349 SUI.This means the exploit and immediate fan-out can be separated cleanly:
artifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/tx_primary.jsonartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/attacker_ledger.mdartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/attacker_transaction_summary.jsonartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/analysis_summary.jsonartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/core_package_object.jsonartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/interface_package_object.jsonartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/ifixed_package_object.jsonartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/clearing_house.disasm.moveartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/account.disasm.moveartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/position.disasm.moveartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/constants.disasm.moveartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/ifixed.disasm.moveartifacts/analysis_531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u/clearing_house_object_current.jsonhttps://suivision.xyz/account/0x1a65086c85114c1a3f8dc74140115c6e18438d48d33a21fd112311561112d41e?tab=Activityhttps://suivision.xyz/txblock/531W14qrdyoD8tZrA34CzSU8pe4Dz1bNEAEcq2mC7E7u