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.
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.
ALL state changes rolled back.
Nothing persists on-chain.
This is your rollback.
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:
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:
self.orders — state is now mutated2. Sequence number validation runs — finds it's invalid
3. Function hits
return with a rejection response4. 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:
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:
move_to, move_from, borrow_global_mut.Mutations happen directly on global resources. A
return after mutation = committed.
&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.
- Treat every
returnafter a mutation as suspicious. In Move,returncommits all changes. It's a normal function exit, not a rollback. - Validate before you mutate. The safest pattern is: check first, mutate second. If validation fails, no state was touched.
- Use
abortwhen state is already mutated. If you've already changed state and hit an error condition,abortis your only real rollback mechanism. - This applies to both Aptos and Sui Move. The Move VM handles
abortandreturnthe 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. 🔍
Severity: Medium
Codebase: Aptos Core — Experimental Order Book
Auditor: OtterSec
Fix: PR #17959
Applies to: Aptos Move + Sui Move
Follow: @thepantherplus