Pre-auditThree days of senior audit time for $1,500, credited in full toward your Zealynx audit.3 audit days, credited toward your auditSee how →
M-03·

Revoking a delegated order signer only pauses it: registering the same key again revives every unexpired order it signed

Acknowledgedrwarfqoracles
TL;DR

A maker in cold custody lets a hot key sign its quotes with registerAllowedOrderSigner(key, true).

Severity
LOW
Method
MManual review
CAT.
02Section · Affected files

Affected files

src/octarine-rfq/OctarineRFQ.sol#L278-L282 src/octarine-rfq/OctarineRFQ.sol#L292-L304 src/octarine-rfq/interfaces/IOctarineRFQ.sol#L254-L257

03Section · Description

Description

A maker in cold custody lets a hot key sign its quotes with registerAllowedOrderSigner(key, true). Every place the protocol describes revoking that key says it voids the key's outstanding orders:

  • IOctarineRFQ.sol L256: "Revocation is immediate and invalidates every outstanding order that signer produced."
  • IOctarineRFQ.sol L112-L113: revoking a delegated signer "remains the bulk kill-switch for one key's whole book."
  • src/octarine-rfq/README.md, assumption 3: "revoking that signer voids every order it produced."
  • src/octarine-rfq/DESIGN.md: "revocation invalidates every outstanding order that signer produced, which is the intended kill-switch."

The code does not void anything. A delegated signature is checked against a live flag at fill time (_verifySignature, L301):

solidity
&& (recovered == signer || (allowDelegate && orderSigners[signer][recovered]))

registerAllowedOrderSigner only writes that flag (L280):

solidity
orderSigners[msg.sender][signer] = allowed;

Revoking sets it to false. Nothing records that the key was revoked, and an order carries no signer generation. Registering the same key again sets the flag back to true, and every order that key signed before the revocation becomes fillable again, unless it has expired or its salt was cancelled.

A kill-switch invites switching back on. A maker that pulls its book this way, for an issuer event, a pricing review or an infrastructure incident, and later re-enables its quoting key to resume, revives the whole old book at the old rates. A taker can watch for OrderSignerRegistered(maker, key, true) and fill in the next block. The taker sends the fill itself, or anyone holding both signatures does when sender is unset, which is how the app sends fills. Orders from the app carry expiries of 24 hours to one month, so the window in which a re-enable revives old orders is long.

Vulnerable Scenario:

  1. A maker's hot key signs a 10.00 bps/day quote for 1,000 RWA; the taker signs its request.
  2. The maker revokes the key. The order now reverts with InvalidSignature.
  3. Hours later the maker re-enables the same key and quotes 15.00 bps/day.
  4. The taker fills the old order. The maker pays 970 USDC for a lot its live book prices at 955 USDC.
04Section · Impact

Impact

The maker trades on orders it retracted, at terms it no longer offers, across every unexpired order the key signed. Each order is bounded by its own maxMakerAmount and takerAmount. Those were signed under the old conditions, though, so they do not bound the gap between the old terms and the maker's current view. In the PoC the gap is 15 USDC per 1,000 RWA (1.5%). If the maker pulled its book because of an issuer event the oracle has not priced yet, and re-enables the key before the oracle catches up, the loss is the NAV gap, up to each order's cap.

05Section · Recommendations

Recommendations

Option A (recommended): make revocation permanent for that key.

diff
@@ -67,6 +67,7 @@ contract OctarineRFQ is IOctarineRFQ, AccessControl, ReentrancyGuard, Pausable,
mapping(bytes32 orderHash => uint128) public rfqFilled;
mapping(bytes32 requestHash => uint128) public requestFilled;
mapping(address maker => mapping(address signer => bool)) public orderSigners;
+ mapping(address maker => mapping(address signer => bool)) public orderSignerRevoked;
mapping(address signer => mapping(uint256 salt => bool)) public saltCancelled;
/*//////////////////////////////////////////////////////////////
@@ -277,6 +278,8 @@ contract OctarineRFQ is IOctarineRFQ, AccessControl, ReentrancyGuard, Pausable,
/// @inheritdoc IOctarineRFQ
function registerAllowedOrderSigner(address signer, bool allowed) external {
if (signer == address(0)) revert ZeroAddress();
+ if (allowed && orderSignerRevoked[msg.sender][signer]) revert Unauthorized();
+ if (!allowed) orderSignerRevoked[msg.sender][signer] = true;
orderSigners[msg.sender][signer] = allowed;
emit OrderSignerRegistered(msg.sender, signer, allowed);
}

The maker rotates to a fresh key instead of re-enabling the old one. The fresh key fills normally (lifecycle test above). Option A costs one storage write per revocation, and a key revoked by mistake cannot come back, so the maker registers a new one. The full test suite passes with this change (706 passed, 1 skipped).

Option B: if pause-and-resume is the intended behaviour, say so in the four places above. Tell makers to cancelSalt their outstanding salts, or wait past the longest expiry they accepted, before re-enabling a key.

06Section · Remediation

Remediation

Acknowledged, and regraded to Low (Medium at delivery). Re-enabling a key still revives its orders, by design. In 70b34f4 the client applied Option B. All four places now describe revocation as a pause, not a cancellation:

  • the registerAllowedOrderSigner natspec;
  • the RFQOrder natspec;
  • README assumption 3;
  • DESIGN.md.

They also tell makers to cancel outstanding salts or wait out the longest expiry before re-enabling a key, and to register a new key to retire one for good. A maker that follows the documentation is no longer exposed, which is why the finding is now graded Low.

A runnable Foundry proof of concept for this finding is in the corresponding issue on the project tracker.

07Section · Report provenance

Report provenance

Source: the final report dated 28 October 2026. Original finding identifiers are retained, including M-03 and M-04, which were regraded Low. Final dispositions and residual conditions are preserved in this write-up. Read the full report.

M-03