September 15, 2026

When Cleanup Creates a Liveness Risk

An Order-Book Liveness Vulnerability in Sui Perpetuals

Editor:

Andrei

Some of the most instructive smart contract findings do not begin with one clearly problematic line of code. They emerge when several individually reasonable behaviors interact unexpectedly.

During Certora's audit of Aftermath Finance Perpetuals, we identified a critical order-book liveness vulnerability in the protocol's Sui perpetuals implementation. Aftermath mitigated the issue before publication. This article describes the vulnerable pre-fix implementation, the reasoning that revealed its full impact, and the layered remediation Aftermath introduced.

Aftermath Perpetuals is an on-chain perpetual-futures exchange on Sui. Traders take long or short exposure against USD-denominated collateral, and orders are matched through a fully on-chain central limit order book rather than an AMM. Because matching, settlement, and stale-order cleanup all execute within Move transactions, the gas and progress properties of those operations are part of the protocol's security model.

Matching Deferred Cleanup Until the End

When a taker encountered an expired maker order, the matching engine marked the stale order for cancellation and removed it from the trade. This is an intuitive design: normal trading also performs maintenance, reducing the need for a separate cleanup transaction.

Both limit and market takers traversed the opposite side of the order book. For each maker, the engine called process_fill_maker, subtracted the matched amount from the taker's remaining size, and stopped once that size reached zero.

while (orders_remain) { let (taker_size_matched, order_fully_filled, ...) = process_fill_maker(..., size, ...); size = size - taker_size_matched; if (size == 0) { break }; }; if (last_order_id_matched > 0) { map::batch_drop(map, last_order_id_matched, include_last_order_id); };

This structure appears naturally bounded by size: every fill reduces the taker's remaining quantity, so a finite taker order should require only a finite number of makers.

That hidden assumption here is that every visited maker makes progress toward filling the taker. Expired orders violate that assumption.

The Zero-Progress State

When process_fill_maker found an expired maker, it forced the amount eligible to trade to zero. That behavior protected the maker from an execution after the order's stated expiry and marked the order for removal.

However, a zero fill left the taker's remaining size unchanged:

remaining_size(next) = remaining_size(current) - 0

This structure appears naturally bounded by size: every fill reduces the taker's remaining quantity, so a finite taker order should require only a finite number of makers.

That hidden assumption here is that every visited maker makes progress toward filling the taker. Expired orders violate that assumption.

The Zero-Progress State

When process_fill_maker found an expired maker, it forced the amount eligible to trade to zero. That behavior protected the maker from an execution after the order's stated expiry and marked the order for removal.

However, a zero fill left the taker's remaining size unchanged:

remaining_size(next) = remaining_size(current) - 0

The cursor advanced to the next maker, but the quantity intended to bound the loop did not decrease.

If N expired orders were resting at the front of the book, a taker transaction had to inspect all N of them, even when the taker wanted to trade only a small amount. Matching work therefore became O(N), controlled by the number of stale makers rather than the taker's order size.

This was the first important link in the vulnerability: protective expiry handling created a valid execution state in which the matching loop performed work without making fill progress.

Why Deferred Cleanup Made the State Persistent

An expensive loop does not necessarily create a persistent denial of service. If each transaction removes part of a stale prefix before reaching its work limit, repeated calls can eventually restore the book.

In the vulnerable implementation, the inspected prefix was removed with a single map::batch_drop only after traversal finished. That final operation also performed traversal and tree updates, so the full scan and structural deletion both had to fit within one atomic transaction:

scan maker 1 scan maker 2 ... scan maker N batch-drop the inspected prefix commit transaction

If the transaction exhausted its gas while scanning maker k, it never reached batch_drop. Sui's atomic execution reverted the intermediate changes, including the attempted cancellation of expired makers, and restored the same prefix.

Once the prefix exceeded the work that a transaction could complete, matching-based cleanup could no longer make durable forward progress. A subsequent crossing taker encountered the same orders and repeated the same work. The security lesson is broader than this implementation: cleanup that saves progress only after an unbounded operation can become a liveness risk under finite transaction limits.

How the Stale Prefix Could Be Constructed

Several protocol properties made the state practical to create in the pre-fix implementation.

First, account creation was permissionless. Although each account could hold at most 100 pending orders, an attacker could distribute orders across Sybil accounts.

Second, ordinary order cancellation required the creator's authority. Other users could not directly cancel the attacker's expired orders, so the matching path was responsible for encountering and removing them.

Third, the posting and matching checks disagreed at the timestamp boundary:

// Posting accepted equality. assert!(expiration_timestamp_ms >= timestamp_ms, ...); // Matching treated equality as expired. if (timestamp_ms >= maker_expiration_timestamp_ms) { allowed_maker_size = 0; };

An order with an expiration equal to the current timestamp passed the posting check but was considered expired when matching ran. In other words, the order could be placed already expired, without an accumulation delay.

Finally, many minimum-value asks could be placed at the lowest valid ask price. A bid crossing that price had to traverse the stale prefix before reaching usable liquidity. Sharing one price prevented a crossing taker from splitting cleanup with a more selective limit price, while zero fills meant that reducing the taker size did not reduce the number of makers inspected.

One way this state could be constructed was:

  1. Create and minimally fund enough accounts.
  2. Place many minimum-value orders on one side of the book at the best available price.
  3. Set each expiration equal to the current timestamp.

The impact applies to any user:

Any limit or market taker crossing the affected side encounters the stale prefix. The traversal exhausts the transaction gas budget, so it reverts without removing any stale orders, leaving the same stale prefix in place. Constructing the prefix on both sides could prevent both opening and closing through order-book matching.

Test Results and Practical Implications

Our instrumented test confirmed the core mechanism: each expired maker returned zero taker fill, left the taker's remaining size unchanged, and added work to the same transaction. Across the tested sizes, the total work grew linearly with the number of stale makers encountered.

These measurements establish the scaling behavior but should not be read as a precise production threshold. Move unit-test gas accounting differs from Sui's on-chain execution model, while deployment-specific gas schedules, market configuration, storage and placement costs, and operational response all affect where traversal would fail.

The practical security conclusion is qualitative: an attacker-controlled stale prefix could eventually require more computation than a single transaction could complete. Because cleanup occurred only after traversal, reaching that point caused retries to repeat the same work without making durable progress. The liveness risk therefore did not depend on any particular order-count or capital-cost estimate.

Impact on Traders and Vault Operations

The vulnerability did not directly transfer user assets to an attacker. Its primary impact was order-book liveness and the temporary lock of capital committed to positions or vault operations that depended on matching.

The affected paths included:

  • Taker limit and market orders crossing the affected side.
  • Position reductions and exits requiring order-book liquidity.
  • Stop-loss and TWAP execution paths that eventually placed limit or market orders.
  • Market-making-vault force withdrawals that attempted to unwind positions through place_market_order.

This distinction matters: free or idle collateral did not depend on crossing the order book, and liquidation used a separate mark-price settlement path. Liquidations therefore remained available for undercollateralized positions even when voluntary matching was unavailable.

That separation also created a potential secondary consequence. A trader unable to execute a reduction, stop-loss, or TWAP exit could remain exposed while prices, funding, and margin requirements changed. If adverse market movement later made the position undercollateralized, it could still be liquidated and incur the corresponding fee.

Aftermath's Layered Remediation

Aftermath addressed the vulnerability with controls covering both prevention and recovery. Aftermath's response went beyond patching the immediate timestamp edge case. The team adopted a defense-in-depth approach that addressed the full liveness failure mode at multiple layers: preventing the easiest construction path, bounding cleanup work so transactions could commit progress, and adding a dedicated recovery mechanism. This was a strong engineering response because it reduced reliance on any single safeguard while preserving a practical path to restore normal operation.

First, new orders must expire strictly after the current timestamp. This closes the boundary mismatch that allowed an order to be posted already expired.

Second, limit-order matching now maintains a cumulative count of maker visits that produce zero fill and stops after a bounded number. Reaching the bound completes successfully instead of exhausting the transaction budget, allowing the inspected stale prefix to be removed and the transaction to commit.

Each successful cleanup transaction therefore makes durable progress, and the next transaction resumes from a shorter prefix. An immediate-or-cancel limit order can serve as a permissionless bounded cleanup mechanism.

Third, Aftermath added try_cancel_stale_orders function, a dedicated recovery path for authorized maintenance operators. An operator can submit an account ID and a transaction-sized batch of order IDs. The function removes expired orders as well as reduce-only orders that can no longer reduce the account's current position. This provides targeted, incremental cleanup even when ordinary trading is paused or cannot conveniently reach the stale entries.

Together, these changes prevent the zero-wait construction, bound matching work, and provide an explicit operational recovery path. The order-book data structure also bounds branch and leaf sizes so that individual tree operations remain within practical compute limits.

Key Takeaways for Auditors and Developers

  1. Security properties emerge from composition. Permissionless account creation, ownership-gated cancellation, timestamp equality, zero-fill expiry handling, deferred deletion, and atomic rollback each appear manageable in isolation. Their interaction created the critical liveness condition. Reviews should model how controls and edge cases compose across an entire transaction path.
  2. Explicitly bound attacker-controlled traversal. Advancing a cursor does not guarantee that the business state is progressing. Auditors should identify zero-progress branches and directly cap the number of entries that a transaction can visit.
  3. On-chain cleanup should make bounded, durable progress. With finite gas and atomic rollback, maintenance paths should commit manageable batches rather than depend on completing work in one transaction. Prevention and recovery controls are strongest when designed together.

This case illustrates the value of collaborative security review: Certora identified how several subtle behaviors combined into a critical liveness issue, Aftermath implemented layered mitigations, and the resulting lessons apply broadly to on-chain order books and other stateful protocols.

Get every blog post delivered

Certora Logo
logologo
Terms of UsePrivacy Policy