Early rate$2,400 of senior audit time for $500. Early members keep the rate as it climbs.$2,400 of senior audit time for $500See how →
F-2026-0015·design-flaw

Cancel-all is scoped per settlement contract while the badge is shared, and the deploy script arms both markets by default

Fixednfterc-1155marketplace
TL;DR

The cancel-all epoch is stored per settlement contract while the badge is shared, and the deploy script enables both markets, so cancelling everything through one market leaves orders live on the other.

Severity
LOW
Impact
LOW
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
LOW
02Section · Description

Description

nonces is per-settlement-contract storage:

solidity
/// @notice Per-user order-cancellation epoch; bumping voids all open orders.
mapping(address maker => uint256) public nonces;

while the badge, and a maker's understanding of "cancel everything I have open", are shared across every settlement contract whitelisted for that badge. incrementNonce() on one market performs no write the other market's _validateOrder can observe, so a maker who cancels through the app — which pins to the dollar market — has cancelled nothing on the euro book, after being told the cancellation succeeded.

Signature replay across the two markets is genuinely closed: the EIP-712 domain binds verifyingContract, and _validateOrder independently requires order.quote to equal the market's immutable quoteCurrency. Cancellation is the property that does not carry across.

This is not reachable on the frozen deployment, where the euro market is de-authorized — its badge settlementClassMask is 0 and its RELAYER_ROLE is revoked. What is reported is that the deployment script arms both whenever the euro market is configured. EURC_ADDRESS is optional and the script says so, but when it is set — as it was for the frozen deployment — the script whitelists the second market with CLASS_MASK_ORDINARY and grants it RELAYER_ROLE in the same run. The de-authorized state is therefore a manual post-deploy step that nothing in the code reproduces, and a fresh deployment carrying an EURC_ADDRESS has two live books and one cancel-all that covers one of them.

03Section · Impact

Impact

A maker who cancels everything through the application has cancelled nothing on the sibling market, after being told the cancellation succeeded. Their orders there remain fillable at their signed prices until expiry. Signature replay across the two markets is separately closed; it is cancellation that does not carry across.

04Section · Recommendation

Recommendation

Two independent changes; the second is worth doing whether or not the first is.

Share the epoch through the contract both markets already trust. The badge is the common dependency:

diff
+mapping(address maker => uint256) public orderEpoch;
+event OrderEpochIncremented(address indexed maker, uint256 newEpoch);
+
+function incrementOrderEpoch(uint256 newEpoch) external {
+ if (newEpoch <= orderEpoch[msg.sender]) revert EpochNotAdvancing(newEpoch);
+ orderEpoch[msg.sender] = newEpoch;
+ emit OrderEpochIncremented(msg.sender, newEpoch);
+}

on PlakxioBadge, with PlakxioSettlement._validateOrder comparing order.nonce against badge.orderEpoch(order.maker) instead of local storage, and incrementNonce computing the jump as it does today and forwarding it. One bump then voids a maker's orders on every market settling that badge.

Deploy the second market de-authorized. Have the script skip the whitelist and the RELAYER_ROLE grant for any market beyond the primary, so arming it is an explicit later action rather than the default that must be manually undone.

05Section · Resolution

Resolution

The second market is removed from the deployment scripts rather than defaulted off, so a fresh deployment can no longer bring up a book that cancel-all does not cover. Verified at audit-v2.

06Section · Affected files

Affected files

  • src/PlakxioSettlement.sol#L180-L181 and script/DeployBase.s.sol at commit 5c38893
Status
Fixed
F-2026-0015