Move compares structs lexicographically by field declaration order. If you use a struct as a key in an ordered map, the first field dominates sorting — not the field you think matters. A real bug from Decibel Exchange's perpetual futures DEX on Aptos.
In Move, when structs are used as keys in ordered data structures like BigOrderedMap, they're compared lexicographically by field declaration order. Not by any specific field. Not by whichever field seems most "important." The comparison starts at the first field and only moves to the next if there's a tie.
This is the same logic as string comparison — "apple" comes before "banana" because 'a' < 'b' at the first character. Move does this with struct fields.
The implication: If your struct key has three fields and you need the map sorted by the third field, you're out of luck. The first field dominates. The third field is only consulted when the first two are equal.
This showed up in Decibel Exchange — a perpetual futures DEX on Aptos — during a security assessment by OtterSec (finding OS-DBX-ADV-07, Medium severity). The trading engine uses a TP/SL (take-profit/stop-loss) tracker that indexes orders by price, so it can quickly find which orders to trigger when the mark price moves.
The code uses BigOrderedMap with a struct key called PriceIndexKey. The logic assumes that borrow_front and borrow_back return the orders with the lowest and highest trigger_price — enabling fast lookups and early termination.
The problem? trigger_price wasn't the first field.
The functions take_ready_price_move_up_orders and take_ready_price_move_down_orders use borrow_front and borrow_back on the ordered map to peek at the next triggerable order. They assume price ordering, so they can bail early when the mark price no longer satisfies the trigger condition.
account first, not trigger_priceborrow_front returns the entry with the lowest account address, not the lowest trigger priceThe result: TP/SL orders that should trigger don't, and orders that shouldn't trigger might. The entire price-based indexing assumption collapses because the map was never sorted by price in the first place.
Move trigger_price to the first field in the struct. That's it. The BigOrderedMap will now sort by price first, with account and order_id as tiebreakers — which is exactly the intended behavior.
Resolved in commit 487eb77.
This isn't specific to trading engines. Any Move code that uses ordered data structures with composite struct keys is vulnerable to this exact class of bug. The pattern is:
This is particularly dangerous because there's no compiler warning, no runtime error, and no abort. The map works perfectly — it's just sorted by the wrong criteria. Everything looks fine until you start getting incorrect results from queries that assume a specific ordering.
Why this is easy to miss: In languages like Rust, you'd implement Ord explicitly and control exactly how comparison works. In Move, struct comparison is implicit and determined entirely by field declaration order. There's no way to override it. The struct layout is the comparison logic.
borrow_front / borrow_back assumptions. Any code that peeks at the front or back of an ordered map is assuming something about sort order. Verify that assumption against the actual struct layout.In Move, struct field order isn't cosmetic — it's behavioral. Read every struct definition like it's a sorting specification. 🔍