Skip to content

feat: make multi-sig releases safe against signature replay and dupli…#464

Open
Faromzy wants to merge 1 commit into
StayLitCodes:mainfrom
Faromzy:feat/multisig-signature-safety
Open

feat: make multi-sig releases safe against signature replay and dupli…#464
Faromzy wants to merge 1 commit into
StayLitCodes:mainfrom
Faromzy:feat/multisig-signature-safety

Conversation

@Faromzy

@Faromzy Faromzy commented Jun 30, 2026

Copy link
Copy Markdown

Multi-sig Signature Safety: Prevent Replay and Duplication

Summary

Hardens the multi-signature milestone release mechanism so collected signatures are scoped
strictly to a single release window. Signatures can no longer be replayed across milestones or
survive terminal state transitions.

───────────────────────────────────────────────────────────────────────────────────────────────

What Changed

New error variants

  • DuplicateSigner (32) — returned when the same address calls collect_signature twice on the
    same escrow
  • InvalidRequiredSignatures (33) — returned when configure_multisig is called with
    required_signatures = 0 or > 10

New constant

  • MAX_REQUIRED_SIGNATURES = 10 — hard upper bound on the signature threshold

New event

  • SignatureResetEvent { escrow_id, reason, timestamp } — emitted every time
    collected_signatures is cleared, with reason set to "milestone", "dispute", "cancel", or
    "expired" so off-chain listeners always know why the slate was wiped

Behaviour changes

┌───────────────────┬────────┬───────┐
│ Location │ Before │ After │
├────────────────────┼────────────────────────────────────┼──────────────────────────────┤
│ collect_signature │ Duplicate silently returned Ok(()) │ Returns Err(DuplicateSigner) │
│ │ Ok(()) │ Err(DuplicateSigner) │
├───────────────────────────┼─────────────────────────────────┼────────────────────────────┤
│ configure_multisig │ Accepted any u32 including 0 │ Rejects 0 and values > 10 │
├───────────────────────────┼───────────────────────────────┼──────────────────────────────┤
│ release_pending_milestone │ Signatures persisted after │ Cleared immediately after │
│ │ release │ release │
├───────────────────────────┼───────────────────────────────┼──────────────────────────────┤
│ raise_dispute │ Signatures persisted │ Cleared, SignatureReset │
│ │ │ emitted │
├───────────────────────────┼───────────────────────────────┼──────────────────────────────┤
│ cancel_escrow │ Signatures persisted │ Cleared, SignatureReset │
│ │ │ emitted │
├───────────────────────────┼───────────────────────────────┼──────────────────────────────┤
│ refund_expired │ Signatures persisted │ Cleared, SignatureReset │
│ │ │ emitted │
└───────────────────────────┴───────────────────────────────┴──────────────────────────────┘

───────────────────────────────────────────────────────────────────────────────────────────────

Tests Added (13 new)

  • test_collect_signature_duplicate_signer_is_rejected
  • test_collect_signature_different_signers_are_all_accepted
  • test_signatures_are_cleared_after_milestone_release — proves replay attempt on milestone N+1
    fails with UnauthorizedAccess after milestone N is released
  • test_signature_reset_event_emitted_on_milestone_release
  • test_signatures_cleared_on_dispute_raise
  • test_signatures_cleared_on_cancel_active
  • test_signatures_cleared_on_refund_expired
  • test_configure_multisig_rejects_zero_required_signatures
  • test_configure_multisig_rejects_required_signatures_above_max
  • test_configure_multisig_accepts_min_required_signatures
  • test_configure_multisig_accepts_max_required_signatures
  • test_release_milestone_exact_threshold_requires_multisig — confirms amount == threshold does
    not trigger multi-sig (condition is strictly >)
  • test_multi_sig_below_threshold_partial_signatures_still_work

Tests Updated (2)

  • test_raise_dispute_happy_path — updated to find DisputeRaised by topic since SignatureReset
    now follows it
  • test_full_lifecycle_event_summaries_are_accurate — same, for MilestoneReleased and
    DeliveryConfirmed

Result: 121/121 tests pass.

───────────────────────────────────────────────────────────────────────────────────────────────

Security Impact

┌─────────────────────────────────────┬────────┐
│ Threat │ Status │
├──────────────────────────────────────────────────────┼─────────────────────────────────┤
│ Signer counted twice in same window │ Blocked — DuplicateSigner error │
├──────────────────────────────────────────────────────┼─────────────────────────────────┤
│ Signatures from milestone N reused for milestone N+1 │ Blocked — cleared on release │
├──────────────────────────────────────────────────────┼────────────────────────────────────┤
├─────────────────────────────────────────────────────┼─────────────────────────────────────┤
│ Partially-collected signatures surviving a dispute │ Blocked — cleared on raise_dispute │
├─────────────────────────────────────────────────────┼─────────────────────────────────────┤
├───────────────────────────────────────────────┼───────────────────────────────────────────┤
│ Stale signatures surviving cancellation or │ Blocked — cleared on cancel/expired │
│ expiry │ │
├───────────────────────────────────────────────┼───────────────────────────────────────────┤
│ Zero-threshold bypass (required_signatures = │ Blocked — InvalidRequiredSignatures error │
│ 0) │ │
└───────────────────────────────────────────────┴───────────────────────────────────────────┘
closes #211

…cation

- Add DuplicateSigner error: collect_signature now returns an explicit
  error instead of silently succeeding when the same address signs twice
- Add InvalidRequiredSignatures error: configure_multisig rejects
  required_signatures == 0 or > MAX_REQUIRED_SIGNATURES (10)
- Add MAX_REQUIRED_SIGNATURES constant (10) as an upper bound cap
- Clear collected_signatures after every milestone release so signatures
  from one release window cannot be replayed on the next milestone
- Clear collected_signatures on raise_dispute, cancel_escrow, and
  refund_expired so stale signatures never survive state transitions
- Add SignatureResetEvent emitted on each clear with a reason field:
  'milestone', 'dispute', 'cancel', or 'expired'
- Add 13 new tests covering duplicate rejection, replay prevention,
  reset semantics per state transition, and bounds validation
- Update test_raise_dispute_happy_path and
  test_full_lifecycle_event_summaries_are_accurate to account for the
  new trailing SignatureReset events

All 121 tests pass.
@drips-wave

drips-wave Bot commented Jun 30, 2026

Copy link
Copy Markdown

@Faromzy Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

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.

[CONTRACT] Multi-sig release hardening (signatures lifecycle + replay safety)

1 participant