Cancellation has no on-chain expression, so an order cancelled in the app stays fillable at its stale price until expiry
Cancelling an order in the app has no on-chain effect, so the order remains fillable at its stale signed price until it expires.
Description
_validateOrder gates an order on its quote, expiry, nonce epoch, replay flag and signature:
if (order.quote != currentQuote) revert QuoteCurrencyMismatch(order.quote, currentQuote);digest = orderDigest(order);if (block.timestamp > order.expiry) revert OrderExpired(digest, order.expiry);if (order.nonce != nonces[order.maker]) revert StaleNonce(digest, order.nonce, nonces[order.maker]);if (settledOrders[digest]) revert OrderAlreadySettled(digest);
settledOrders[digest] is written only by a completed fill in _finalize, and the only
maker-callable revocation is incrementNonce, which is all-or-nothing by construction — its
own comment describes individual cancellation as off-chain and itself as the backstop.
There is therefore no on-chain expression of a single cancelled order. A maker who cancels one
order in the app has performed no state change; their signature remains fully valid until
expiry. They will not use the backstop, because it destroys every other order they hold.
This sits against the stated model of the off-chain layer, which is that admission is a convenience and not a boundary because the contract re-checks everything. Cancellation is the one thing the contract does not re-check, so for this property the off-chain book is the only enforcement.
Submission requires RELAYER_ROLE, so an honest relayer following its own records will not
present a cancelled order. What the contract does not have is any way to refuse one if the
relayer's records are wrong, its key is compromised, or a stale cache is replayed — and the
relayer can also supply the counterparty, since SelfTrade only compares the two makers'
addresses.
Vulnerable Scenario:
- A maker signs an ask at 10.00 USDC with a 30-day expiry. The relayer's infrastructure stores it.
- The maker cancels it in the app.
nonces[maker]is unchanged;settledOrders[digest]isfalse; the expiry is weeks away. - The badge appreciates. The signature is still on-chain-valid at the stale price.
- The order is submitted against any bid at 10.00 and settles. Every check in
_validateOrderpasses.
Impact
Every order a maker believes they have cancelled remains fillable at its signed price until it expires. The maker loses the difference between that price and the badge's current value, and their only defence is to void their entire open book — which requires them to be online, and which the design assumes they are not. The counterparty and the treasury receive what the maker lost.
Recommendation
Add a per-order revocation that reuses the existing single-fill mapping, so _validateOrder
needs no new check and the epoch bump becomes the rare escape hatch it is documented as:
+error OrderCancelled(bytes32 orderHash);++/// @notice Voids a single order. The on-chain counterpart of an app-side cancel.+function cancelOrder(PlakxioOrderTypes.Order calldata order) external {+ if (msg.sender != order.maker) revert InvalidSignature(orderDigest(order), order.maker);+ settledOrders[orderDigest(order)] = true;+ emit OrderCancelledEvent(orderDigest(order), msg.sender);+}
Reusing settledOrders means a cancelled order surfaces as OrderAlreadySettled to any
existing consumer, which is already a terminal state they handle. If distinguishing the two
matters off-chain, use a separate cancelledOrders mapping and its own error instead — the
extra SLOAD in _validateOrder is the only cost.
Users pay no gas in this system, so route the call the same way incrementNonce is routed.
Resolution
On-chain cancellation now exists: cancelOrder and a batch cancelOrders, both permissionless to
submit with the maker's signature as the authority, alongside a maximum order lifetime. Keeping
submission permissionless is the right trade — role-gating it would let a compromised relayer key
destroy every open order, and gasless makers cannot send transactions at all. Verified at
audit-v2.
Affected files
src/PlakxioSettlement.sol#L605-L618andsrc/PlakxioSettlement.sol#L284-L305at commit5c38893