.
@AftermathFi on
#SUI was reported being attacked several hours ago, with direct losses of about $1.14M. According to the team, only Aftermath Perps was affected, while the exploit was caused by the protocol incorrectly allowing negative builder fees.
Based on our analysis of the on-chain disassembled Move bytecode, the underlying implementation issue was a semantic mismatch: builder fees were expected to be user-approved, non-negative values, but were validated through a signed fixed-point comparison over a u256 interface. In the disassembled calculate_taker_fees path, the critical check was:
// Builder fee is only checked against an upper bound.
// Missing invariant: fee must also be non-negative.
assert!(
ifixed::less_than_eq(
v5.taker_fee,
account::get_integrator_max_taker_fee(
account::get_integrator_config(arg1, v5.integrator_address)
)
),
errors::invalid_integrator_taker_fee()
);
Semantically, both values were expected to represent non-negative fee rates. However, ifixed::less_than_eq() performs a signed comparison. This means that once the attacker set max_taker_fee = 0, they could pass a value such as 2^256 - 10^16, which is interpreted under signed semantics as a negative fee, i.e. -10^16. Since -10^16 <= 0 holds, the check passed.
public fun create_integrator_info(arg0: address, arg1: u256): Option<IntegratorInfo> {
let v0 = IntegratorInfo {
integrator_address : arg0,
taker_fee : arg1,
};
option::some<IntegratorInfo>(v0)
}
The exploit path was further exposed because create_integrator_info() was publicly callable and did not enforce any permission or fee-bound validation on the supplied taker_fee.
let (v7, v8, v9) = calculate_taker_fees(...);
// v6 = taker PnL
// v7 = normal taker fee
// v8 = builder fee
//
// Intended effect:
// collateral = pnl - taker_fee - builder_fee
//
// If v8 is negative, subtracting it turns it into a positive credit.
position::add_to_collateral_usd(
arg0,
ifixed::sub(v6, ifixed::add(v7, v8)),
arg2
);
As a result, the negative builder fee was not merely accepted; it was transformed into a direct positive collateral credit during taker settlement. The attacker then deallocated that inflated free collateral back into the account balance and withdrew real USDC from the protocol.
Some thoughts:
1) This was not just a fee bypass: the negative builder fee was converted into positive collateral during settlement.
2) The exploit was permissionless: the attacker could self-configure the taker-side cap and inject the negative fee through a public path.
3) The actual loss was realized through the normal deallocate-and-withdraw flow, meaning the inflated collateral became real withdrawable USDC from the vault.