← Back to Blog

The Delete-Then-Validate Bug — How return vs abort Silently Corrupts State in Move

A real bug from Aptos core's trading engine that permanently deleted orders on an "error" path. The root cause? A return where there should have been an abort. This pattern applies to both Aptos and Sui Move.

✍ 0xTheBlackPanther 📅 22nd February 2026 ⏱ 5 min read 🏷 Move, Aptos, Sui, Security

The Core Lesson

In Move, abort and return behave very differently when state has been mutated. Most developers treat return as a safe "exit on error" — it's not. This confusion is a real bug class, and it showed up in Aptos core's order book.

✅ abort / assert! Transaction fails.
ALL state changes rolled back.
Nothing persists on-chain.

This is your rollback.
❌ return Function exits normally.
ALL mutations committed.
Transaction succeeds on-chain.

This is NOT a safety net.

If you mutate state and then return on a failure path, that mutation is permanent. The function exits, the transaction completes, and the corrupted state lives on-chain forever.

The Bug

This was found in place_bulk_order in Aptos's experimental order book module during an audit by OtterSec (finding OS-ADP-ADV-05, Medium severity). Here's the flow:

// bulk_order_book.move — simplified public fun place_bulk_order(
  self: &mut BulkOrderBook,
  ...
) : BulkOrderPlaceResponse {

  // Step 1: Get account and new sequence number
  let account = get_account_from_order_request(&order_req);
  let new_seq = get_sequence_number_from_order_request(&order_req);

  // Step 2: REMOVE the existing order immediately
  let order_option = self.orders.remove_or_none(&account); // ⚠️ STATE MUTATED

  if (order_option.is_some()) {
    let old_order = order_option.destroy_some();
    let existing_seq = get_sequence_number_from_bulk_order(&old_order);

    // Step 3: Validate AFTER deletion
    if (new_seq <= existing_seq) {
      return new_bulk_order_place_response_rejection( // ❌ return, not abort!
        utf8(b"Invalid sequence number")
      );
    };
    ...
  }
  ...
}

The sequence:

What happens step by step: 1. Existing order is removed from self.orders — state is now mutated
2. Sequence number validation runs — finds it's invalid
3. Function hits return with a rejection response
4. Transaction completes successfully — the deletion is committed
5. The old order is gone. The new order was never placed. Order book is now inconsistent.

The order is permanently deleted. No new order replaces it. The order book is now in a state that can cause aborts in subsequent operations. All because return was used where abort should have been.

The Fix

Two approaches, both valid:

FIX 1: VALIDATE BEFORE MUTATING Check the sequence number before removing the order. If invalid, return early — no state was touched yet, so return is safe.
FIX 2: ABORT ON FAILURE Keep the current order, but replace return with abort. The transaction reverts and the original order survives.

The Aptos team resolved this in PR #17959.

This Applies to Sui Move Too

This isn't an Aptos-specific bug. The abort vs return semantics are baked into the Move VM itself. Both Aptos and Sui inherit this behavior.

The difference is how state mutation looks on each chain:

Aptos Move Uses the global storage model — move_to, move_from, borrow_global_mut.
Mutations happen directly on global resources. A return after mutation = committed.
Sui Move Uses the object-centric model — objects passed as &mut parameters.
Mutations happen on objects via mutable references. Same rule: return after mutation = committed.

The surface looks different, but the underlying principle is identical. If you audit Sui Move, look for functions that take &mut object references, mutate them, and then use return instead of abort on error paths.

The Audit Pattern

What to grep for: Any function that mutates state (removes, inserts, updates) before a conditional check that uses return instead of abort on failure. That's your bug. Every time.

  1. Treat every return after a mutation as suspicious. In Move, return commits all changes. It's a normal function exit, not a rollback.
  2. Validate before you mutate. The safest pattern is: check first, mutate second. If validation fails, no state was touched.
  3. Use abort when state is already mutated. If you've already changed state and hit an error condition, abort is your only real rollback mechanism.
  4. This applies to both Aptos and Sui Move. The Move VM handles abort and return the same way on both chains. The object model differs, but the bug class is identical.

In Move, return is a commitment — not a safety net. Audit accordingly. 🔍

Finding: OS-ADP-ADV-05 — Inconsistent Order Book State Handling
Severity: Medium
Codebase: Aptos Core — Experimental Order Book
Auditor: OtterSec
Fix: PR #17959
Applies to: Aptos Move + Sui Move
Follow: @thepantherplus