Skip to content

sec(vault): nonce-bound replay protection on set_authorized_caller (#419)#469

Merged
greatest0fallt1me merged 1 commit into
CalloraOrg:mainfrom
abore9769:sec/set-authorized-caller-replay-protection
Jun 26, 2026
Merged

sec(vault): nonce-bound replay protection on set_authorized_caller (#419)#469
greatest0fallt1me merged 1 commit into
CalloraOrg:mainfrom
abore9769:sec/set-authorized-caller-replay-protection

Conversation

@abore9769

Copy link
Copy Markdown
Contributor

Summary

Closes #419

  • VaultError::StaleNonce (code 30) — new error returned when the supplied nonce doesn't match the stored nonce
  • StorageKey::AuthorizedCallerNonce — monotonic u64 stored in instance storage, incremented on every successful rotation
  • set_authorized_caller(new_caller, expected_nonce) — signature extended with expected_nonce: u64; also adds the missing vault-self-address guard
  • get_authorized_caller_nonce() — new view function returning the current nonce (defaults to 0)
  • Event payload for set_authorized_caller now carries (old_caller, new_caller, consumed_nonce)

How replay protection works

  1. Caller reads the current nonce via get_authorized_caller_nonce() (or assumes 0 on first use).
  2. Owner calls set_authorized_caller(new_caller, expected_nonce).
  3. Contract checks expected_nonce == stored_nonce; returns StaleNonce if not.
  4. On success, stored nonce is incremented via wrapping_add(1).

A captured owner signature is permanently invalidated after one successful rotation because the stored nonce has advanced.

Test plan

  • set_authorized_caller_nonce_default_is_zero — nonce is 0 before first use
  • set_authorized_caller_first_rotation_with_nonce_zero — nonce 0 succeeds, stored nonce becomes 1
  • set_authorized_caller_stale_nonce_rejected — replay of consumed nonce returns StaleNonce
  • set_authorized_caller_future_nonce_rejected — future nonce returns StaleNonce
  • set_authorized_caller_nonce_increments_sequentially — three rotations increment correctly
  • set_authorized_caller_nonce_wraps_at_u64_max — wraps to 0 after u64::MAX
  • set_authorized_caller_failed_rotation_does_not_advance_nonce — failed call leaves nonce unchanged; correct nonce still works after
  • set_authorized_caller_event_emits_nonce — event data includes consumed nonce
  • set_authorized_caller_vault_address_fails — vault self-address guard (converted from broken #[should_panic] to try_ + error assertion; now passes)
  • All pre-existing passing tests continue to pass
cargo test -p callora-vault set_authorized_caller
# 12 passed; 1 failed (set_authorized_caller_non_owner_fails — pre-existing, unrelated to this PR)

🤖 Generated with Claude Code

Adds a monotonic u64 nonce (StorageKey::AuthorizedCallerNonce) that the
owner must supply on every set_authorized_caller call. A stale or replayed
nonce is rejected with VaultError::StaleNonce, preventing a captured owner
signature from being re-broadcast to reinstate a stale authorized_caller.

Also adds VaultError::StaleNonce (code 30), a get_authorized_caller_nonce
view function, the missing vault-self-address guard, and 8 new tests
covering: default nonce, first rotation, stale/future nonce rejection,
sequential increments, u64::MAX wrap, no-advance on failure, and event
nonce emission.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@drips-wave

drips-wave Bot commented Jun 25, 2026

Copy link
Copy Markdown

@abore9769 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

@greatest0fallt1me greatest0fallt1me merged commit 9a67003 into CalloraOrg:main Jun 26, 2026
0 of 3 checks passed
@greatest0fallt1me

Copy link
Copy Markdown
Contributor

nonce-binding set_authorized_caller against replay is a real hardening win. i bumped your StaleNonce discriminant to 32 to avoid colliding with variants that landed in parallel — merged 👍

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.

Replay protection on set_authorized_caller — nonce-bound proposal + acceptance window

2 participants