Problem Statement
The #[authorize_once] macro currently enforces nonce == 0 when from == msg_sender, via helpers.nr#L34-L35:
if (!from.eq(self.msg_sender())) {
assert_current_call_valid_authwit_public(self.context, from);
} else {
assert(nonce == 0, "Invalid authwit nonce. When 'from' and 'msg_sender' are the same, 'nonce' must be zero");
}
This is fine for leaf contracts where the nonce is consumed by the contract itself. But it breaks for wrapper contracts that need to forward the user's nonce to an inner contract's authwit-protected function — the user is forced to pass nonce = 0, so every call with the same parameters produces the same authwit hash. Since authwits are consumed on first use, only the first call succeeds and subsequent identical calls revert. The nonce is the only knob the caller has to disambiguate otherwise-identical inner calls.
This behavior is the same in v4.2.0 and v5 nightly (next branch) as of today.
Proposed Solution
When from == msg_sender, the macro should skip the authwit check without imposing any constraint on the nonce value. This matches the behavior of the previous assert_current_call_valid_authwit / assert_current_call_valid_authwit_public helpers.
The injected check would simplify to:
if (!from.eq(self.msg_sender())) {
assert_current_call_valid_authwit_public(self.context, from);
}
// else: no check, nonce can be any value (caller decides what it means)
The macro should only handle the auth check. What the contract does with the nonce beyond that should be left to the contract author.
Example Use Case
A vault that calls a token's authwit-protected transfer_public_to_public on the user's behalf:
#[authorize_once("from", "nonce")]
#[external("public")]
fn deposit_public_to_public(from: AztecAddress, to: AztecAddress, assets: u128, nonce: Field) {
// ... compute shares ...
// Forward to token. Here msg_sender = vault, from = user → token runs authwit check.
// The nonce is part of the authwit hash and must be unique per call for replay protection.
Token::at(asset_token).transfer_public_to_public(from, self.address, assets, nonce);
Token::at(shares_token).mint_to_public(to, shares);
}
When alice calls deposit_public_to_public(alice, bob, 100, 1) with a public authwit set for transfer_public_to_public(alice, vault, 100, 1), the macro panics: alice's own call has from == msg_sender, but nonce = 1.
If she changes to nonce = 0, the inner authwit hash collapses to a single value across every deposit she makes. After the first call consumes the authwit, every subsequent deposit reverts.
This pattern exists in our aztec-standards Vault contract — we currently use #[authorize_once] for the Token and NFT contracts but had to revert the Vault to manual _validate_from_private / _validate_from_public calls because of this constraint.
Alternative Solutions
-
Manual auth (status quo workaround) — keep the old assert_current_call_valid_authwit_public calls in wrapper contracts and only use #[authorize_once] in leaf contracts. This is what we ended up doing, but it means the macro can't be applied uniformly across a contract suite.
-
Allow opt-out via attribute syntax — e.g. #[authorize_once("from", "nonce", forwardable)] that suppresses the nonce == 0 check. Less elegant than just dropping the check, but preserves the current default behavior for those who rely on it.
-
Two-nonce wrapper signature — add a separate token_nonce parameter that the wrapper forwards to the inner call, while keeping nonce = 0 for the macro. We prototyped this approach but ended up moving away from it, since it adds an extra Field to every wrapper function just to work around the macro.
Additional Context
- Verified the same behavior on
v4.2.0 and next (today's nightly).
- Real-world example from our codebase showing the mixed adoption pattern: aztec-standards —
#[authorize_once] adopted in token/NFT, manually rolled back in vault.
Problem Statement
The
#[authorize_once]macro currently enforcesnonce == 0whenfrom == msg_sender, via helpers.nr#L34-L35:This is fine for leaf contracts where the nonce is consumed by the contract itself. But it breaks for wrapper contracts that need to forward the user's nonce to an inner contract's authwit-protected function — the user is forced to pass
nonce = 0, so every call with the same parameters produces the same authwit hash. Since authwits are consumed on first use, only the first call succeeds and subsequent identical calls revert. The nonce is the only knob the caller has to disambiguate otherwise-identical inner calls.This behavior is the same in v4.2.0 and v5 nightly (
nextbranch) as of today.Proposed Solution
When
from == msg_sender, the macro should skip the authwit check without imposing any constraint on the nonce value. This matches the behavior of the previousassert_current_call_valid_authwit/assert_current_call_valid_authwit_publichelpers.The injected check would simplify to:
The macro should only handle the auth check. What the contract does with the nonce beyond that should be left to the contract author.
Example Use Case
A vault that calls a token's authwit-protected
transfer_public_to_publicon the user's behalf:When alice calls
deposit_public_to_public(alice, bob, 100, 1)with a public authwit set fortransfer_public_to_public(alice, vault, 100, 1), the macro panics: alice's own call hasfrom == msg_sender, butnonce = 1.If she changes to
nonce = 0, the inner authwit hash collapses to a single value across every deposit she makes. After the first call consumes the authwit, every subsequent deposit reverts.This pattern exists in our aztec-standards Vault contract — we currently use
#[authorize_once]for the Token and NFT contracts but had to revert the Vault to manual_validate_from_private/_validate_from_publiccalls because of this constraint.Alternative Solutions
Manual auth (status quo workaround) — keep the old
assert_current_call_valid_authwit_publiccalls in wrapper contracts and only use#[authorize_once]in leaf contracts. This is what we ended up doing, but it means the macro can't be applied uniformly across a contract suite.Allow opt-out via attribute syntax — e.g.
#[authorize_once("from", "nonce", forwardable)]that suppresses thenonce == 0check. Less elegant than just dropping the check, but preserves the current default behavior for those who rely on it.Two-nonce wrapper signature — add a separate
token_nonceparameter that the wrapper forwards to the inner call, while keepingnonce = 0for the macro. We prototyped this approach but ended up moving away from it, since it adds an extraFieldto every wrapper function just to work around the macro.Additional Context
v4.2.0andnext(today's nightly).#[authorize_once]adopted in token/NFT, manually rolled back in vault.