Skip to content

fix: F-2026-18191 | [Dual Defense] Byzantine TSS Coordinator Can Freeze Outbound Withdrawals via Gap Nonce - #306

Merged
Aman035 merged 3 commits into
audit-fixesfrom
F-2026-18191
Aug 18, 2026
Merged

fix: F-2026-18191 | [Dual Defense] Byzantine TSS Coordinator Can Freeze Outbound Withdrawals via Gap Nonce#306
Aman035 merged 3 commits into
audit-fixesfrom
F-2026-18191

Conversation

@Aman035

@Aman035 Aman035 commented Aug 18, 2026

Copy link
Copy Markdown
Member

F-2026-18191 Gap-nonce freeze by a byzantine coordinator

Problem

  • Verify checked only req.Nonce < finalizedNonce. No upper bound.
  • The follower rebuilds the hash with the coordinator's nonce, so any unused nonce passes.
  • A far-future nonce never mines, so the resolver never votes failure and the outbound freezes with its PRC20 already burned.

What I did

  • Added checkNonceInRange and nonceBounds, used by verifyOutboundSigningRequest and verifyFundMigrationSigningRequest.
  • Accepted range is now finalized <= assigned <= pending + 2*PerChainCap.
  • Kept finalized as the lower bound, since it is consensus stable and followers do not diverge on it.
  • Failed pending lookup falls back to finalized, and the ceiling base is clamped up to finalized, so a stale pending read cannot widen the window.
  • Left the existing fail-open on a failed finalized lookup unchanged.

Why the ceiling uses pending, not finalized

  • GetInFlightSignEvents counts only IN_PROGRESS and SIGNED. A BROADCASTED event stops counting toward PerChainCap but still holds a mempool nonce.
  • So during congestion the coordinator signs another PerChainCap per poll and pending - finalized grows without bound.
  • Any fixed gap above finalized would reject honest coordinators during a stall. Pending tracks congestion on its own and is real chain state an attacker cannot inflate.

Tests

  • TestCheckNonceInRange: accepts equal to finalized, within cap, at the gap limit, congestion (pending = finalized + 500), and nonce 0 on SVM. Rejects below finalized, one past the limit, finalized + 2^32 when congested, and a stale ceiling base below finalized.
  • TestNonceBounds: pending above finalized becomes the base; pending failure and pending below finalized fall back to finalized; finalized failure errors so the caller skips.

Deliberately not added

  • Re-signing a stuck outbound with a fresh nonce. That leaves two valid signatures live for the same outbound, and if the original later lands we have a double spend that is hard to prevent today.
  • Admin revert for a stuck outbound. UVs vote the outcome and let each outbound finish its lifecycle. An admin breaking that cycle from outside adds a second authority that can contradict the voting model.
  • A client side timeout that votes the outbound failed. That is a heuristic in per node code, so validators on different versions would vote differently.

With the gap nonce rejected at verify, none of these recovery paths are needed for this vector.

@Aman035 Aman035 changed the title fix: bound coordinator-assigned nonce to block gap-nonce outbound freeze (F-2026-18191) fix: F-2026-18191 | [Dual Defense] Byzantine TSS Coordinator Can Freeze Outbound Withdrawals via Gap Nonce Aug 18, 2026
@Aman035
Aman035 merged commit 49de107 into audit-fixes Aug 18, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant