Skip to content

docs: correct ARC-403 mint semantics and disclose known trust boundaries - #24

Open
alejoamiras wants to merge 1 commit into
mainfrom
docs/audit-followups
Open

docs: correct ARC-403 mint semantics and disclose known trust boundaries#24
alejoamiras wants to merge 1 commit into
mainfrom
docs/audit-followups

Conversation

@alejoamiras

Copy link
Copy Markdown
Collaborator

Documentation follow-ups from a security audit of the Noir contracts. No contract logic changes — comments and READMEs only.

1. token/main.nr — remove a comment that contradicts the code

Both ARC-403 hook wrappers documented from as "(zero address for mints)". No mint path calls either hook, so the parenthetical described the spec's parameter semantics rather than this contract's behaviour.

It was load-bearing in the wrong direction: it was the strongest single reason two independent reviews classified the deliberate design as an authorization bypass (CWE-862). Replaced with a pointer to the README.

2. token/README — state what unhooked mints imply

The README already documented that mints are unhooked and why. Added the consequence for integrators: an authorization contract is not a universal kill switch. Pausing it stops transfers and burns while the minter can still issue supply — so a compromised minter key cannot be contained by the hook, minter being a single immutable address where the hook can encode multisig/timelock/pause.

Also records the open ARC-403 question: hooking mints (with from = zero) would enable pause and supply-cap policies but still not recipient screening, since to is deliberately not forwarded. That's a spec decision for the forum draft, not a patch — flagging it here rather than changing behaviour unilaterally.

3. multitoken/README — new "Commitment trust model" section

initialize_transfer_commitment binds the recipient, randomness and completer, but deliberately not token_id or amount — the completer supplies both at completion. That is documented intent and is genuinely useful when the value depends on public state.

The gap was that nothing said which patterns it is unsafe for. A commitment is not a payment guarantee: a malicious completer can satisfy it with a negligible amount of an arbitrary id, including one they created. Safe when the completer is already trusted for the value; unsafe for escrow, marketplace and handoff flows. Noted that this is a wider surface than Token, where the token is implicit and only the amount is completer-chosen.

Same kill-switch caveat as (2) added, since MultiToken carries the identical mints-unhooked design.

4. vault/README + root README — disclose the reentrancy exposure

The existing warning disclosed the conversion overflow and the privacy limitations, but not the reentrancy exposure, which understated the risk profile.

The vault's protection against reentrancy is the order of its operations (assets in before shares minted, shares burned before assets paid out), which assumes a token transfer is indivisible. It isn't: when the asset token has an ARC-403 auth_contract, that contract runs during the transfer, before balances move, and can observe the vault in exactly the intermediate state the ordering exists to exclude. Reading the share price there yields a value no completed operation produces.

The warning now advises wrapping only assets whose get_auth_contract() is zero, or one whose authorization contract is fully trusted. Vaults over unhooked tokens are unaffected.

Validation

aztec-nargo fmt --check clean · aztec compile OK. No test changes (nothing behavioural changed).

Note

The .nr formatting fix for main's failing Format check lives in #23 and is deliberately not duplicated here.

🤖 Generated with Claude Code

Follow-ups from a security audit of the Noir contracts. Documentation
only — no contract logic changes.

- token/main.nr: the two ARC-403 hook wrappers documented `from` as
  "(zero address for mints)", describing the spec's parameter semantics
  rather than this contract's behaviour: no mint path calls either hook.
  The comment was the single strongest reason two independent reviews
  mistook the deliberate design for an authorization bypass. Replaced
  with a pointer to the README.
- token/README: state the consequence of mints being unhooked — an
  authorization contract is not a universal kill switch, so a
  compromised minter cannot be contained by pausing the hook. Notes the
  open ARC-403 question (hooking mints would enable pause and supply
  caps but still not recipient screening, since `to` is not forwarded).
- multitoken/README: same kill-switch caveat, plus a new "Commitment
  trust model" section. `initialize_transfer_commitment` binds the
  recipient and completer but deliberately not `token_id`/`amount`,
  which the completer supplies later. That is documented intent and is
  safe when the completer is already trusted for the value, but it makes
  a commitment unsuitable as a payment guarantee in escrow or handoff
  patterns — a wider surface than Token, where only the amount is free.
- vault/README + root README: the existing warning disclosed the
  conversion overflow and privacy limits but not the reentrancy exposure.
  The vault's protection is the order of its operations, which assumes a
  token transfer is indivisible; when the asset token has an ARC-403
  hook, that hook runs mid-transfer and can observe the vault in the
  intermediate state the ordering exists to prevent. Advise wrapping only
  assets with no hook, or a fully trusted one.

Validated: aztec-nargo fmt --check clean, aztec compile OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alejoamiras
alejoamiras force-pushed the docs/audit-followups branch from 8496fd5 to d24dacd Compare August 18, 2026 15:43
@github-actions

Copy link
Copy Markdown

Benchmark Comparison

CPU Cores RAM Arch
INTEL(R) XEON(R) PLATINUM 8573C 4 16 GiB x64

Contract: escrow

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
(partial) withdraw 485,780 485,780 3,744 3,744 587,800 587,800 13,838 12,361 -1,477 (-10.7%)
withdraw 330,054 330,054 832 832 499,700 499,700 11,178 10,019 -1,159 (-10.4%)
withdraw_nft 378,231 378,231 1,440 1,440 527,400 527,400 12,313 10,943 -1,370 (-11.1%)

Contract: logic

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
get_escrow 390,336 390,336 192 192 456,000 456,000 12,439 11,211 -1,228 (-9.9%)
secret_key_to_public_keys 387,695 387,695 192 192 456,000 456,000 12,459 11,162 -1,297 (-10.4%)
share_escrow 304,211 304,211 1,952 1,952 520,700 520,700 10,952 9,820 -1,132 (-10.3%)

Contract: multitoken

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
burn_private 260,232 260,232 832 832 499,700 499,700 10,162 9,090 -1,072 (-10.5%)
burn_public 223,800 223,800 416 416 638,292 638,292 9,758 8,654 -1,104 (-11.3%)
initialize_transfer_commitment 225,054 225,054 768 768 474,500 474,500 9,690 8,636 -1,054 (-10.9%)
mint_to_private 297,795 297,795 1,408 1,408 511,400 511,400 10,932 9,794 -1,138 (-10.4%)
mint_to_public 223,800 223,800 416 416 637,800 637,800 9,657 8,795 -862 (-8.9%)
transfer_private_to_commitment 263,571 263,571 1,024 1,024 511,400 511,400 10,233 9,079 -1,154 (-11.3%)
transfer_private_to_private 336,124 336,124 2,048 2,048 555,100 555,100 11,350 10,267 -1,083 (-9.5%)
transfer_private_to_public 306,785 306,785 1,056 1,056 714,677 714,677 10,953 9,828 -1,125 (-10.3%)
transfer_public_to_commitment 223,800 223,800 640 640 663,516 663,516 9,772 8,746 -1,026 (-10.5%)
transfer_public_to_private 298,556 298,556 1,024 1,024 683,958 683,958 10,807 9,772 -1,035 (-9.6%)
transfer_public_to_public 223,800 223,800 480 480 673,505 673,505 9,690 8,687 -1,003 (-10.4%)

Contract: nft

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
burn_private 275,232 275,232 416 416 661,052 661,052 10,545 9,510 -1,035 (-9.8%)
burn_public 223,800 223,800 448 448 670,034 670,034 9,681 8,782 -899 (-9.3%)
mint_to_private 341,272 341,272 1,600 1,600 735,342 735,342 11,468 10,322 -1,146 (-10.0%)
mint_to_public 223,800 223,800 448 448 670,718 670,718 9,684 8,687 -997 (-10.3%)
transfer_private_to_private 249,433 249,433 832 832 499,700 499,700 9,934 8,904 -1,030 (-10.4%)
transfer_private_to_public 275,261 275,261 416 416 659,258 659,258 10,665 9,438 -1,227 (-11.5%)
transfer_public_to_private 292,557 292,557 992 992 683,268 683,268 10,774 9,607 -1,167 (-10.8%)
transfer_public_to_public 223,800 223,800 384 384 633,285 633,285 9,661 8,705 -956 (-9.9%)

Contract: token

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
burn_private 303,054 303,054 1,024 1,024 713,957 713,957 10,915 9,851 -1,064 (-9.7%)
burn_public 223,800 223,800 448 448 672,632 672,632 9,641 8,639 -1,002 (-10.4%)
initialize_transfer_commitment 225,054 225,054 768 768 474,500 474,500 9,656 8,657 -999 (-10.3%)
mint_to_private 355,507 355,507 2,144 2,144 738,070 738,070 11,648 10,547 -1,101 (-9.5%)
mint_to_public 223,800 223,800 448 448 672,227 672,227 9,639 8,941 -698 (-7.2%)
transfer_private_to_commitment 259,896 259,896 992 992 511,400 511,400 10,090 9,045 -1,045 (-10.4%)
transfer_private_to_private 343,699 343,699 2,592 2,592 557,600 557,600 11,476 10,345 -1,131 (-9.9%)
transfer_private_to_public 303,110 303,110 1,024 1,024 714,023 714,023 10,803 9,853 -950 (-8.8%)
transfer_private_to_public_with_commitment 307,280 307,280 1,600 1,600 747,323 747,323 10,961 9,847 -1,114 (-10.2%)
transfer_public_to_commitment 223,800 223,800 576 576 662,529 662,529 9,696 8,644 -1,052 (-10.8%)
transfer_public_to_private 296,520 296,520 992 992 683,304 683,304 10,819 9,759 -1,060 (-9.8%)
transfer_public_to_public 223,800 223,800 448 448 672,548 672,548 9,621 8,687 -934 (-9.7%)

Contract: vault

🚦 Function Gates DA Gas L2 Gas Proving Time (ms)
Base PR Diff Base PR Diff Base PR Diff Base PR Diff
deposit_private_to_private 507,983 507,983 1,312 1,312 878,248 878,248 14,177 12,720 -1,457 (-10.3%)
deposit_private_to_private_exact 611,708 611,708 1,888 1,888 915,316 915,316 16,094 14,394 -1,700 (-10.6%)
deposit_private_to_public 435,487 435,487 768 768 862,273 862,273 13,152 11,803 -1,349 (-10.3%)
deposit_public_to_private 424,931 424,931 1,984 1,984 965,045 965,045 13,015 11,693 -1,322 (-10.2%)
deposit_public_to_private_exact 424,942 424,942 1,952 1,952 949,469 949,469 12,965 11,640 -1,325 (-10.2%)
deposit_public_to_public 223,800 223,800 832 832 897,563 897,563 9,783 8,697 -1,086 (-11.1%)
issue_private_to_private_exact 611,708 611,708 1,888 1,888 915,976 915,976 16,048 14,420 -1,628 (-10.1%)
issue_private_to_public_exact 484,222 484,222 1,344 1,344 899,998 899,998 13,959 12,445 -1,514 (-10.8%)
issue_public_to_private 346,383 346,383 1,376 1,376 921,661 921,661 11,569 10,359 -1,210 (-10.5%)
issue_public_to_public 223,800 223,800 832 832 898,253 898,253 9,819 8,715 -1,104 (-11.2%)
redeem_private_to_private_exact 614,712 614,712 1,888 1,888 915,823 915,823 16,111 14,411 -1,700 (-10.6%)
redeem_private_to_public 435,430 435,430 768 768 862,717 862,717 13,154 11,800 -1,354 (-10.3%)
redeem_public_to_private_exact 428,003 428,003 1,952 1,952 949,790 949,790 13,037 11,668 -1,369 (-10.5%)
redeem_public_to_public 223,800 223,800 832 832 898,085 898,085 9,707 8,721 -986 (-10.2%)
withdraw_private_to_private 510,987 510,987 1,312 1,312 878,458 878,458 14,232 12,756 -1,476 (-10.4%)
withdraw_private_to_private_exact 614,712 614,712 1,888 1,888 915,769 915,769 16,122 14,490 -1,632 (-10.1%)
withdraw_private_to_public_exact 484,165 484,165 1,344 1,344 900,232 900,232 13,983 12,440 -1,543 (-11.0%)
withdraw_public_to_private 441,260 441,260 2,528 2,528 967,356 967,356 13,216 11,901 -1,315 (-10.0%)
withdraw_public_to_public 223,800 223,800 832 832 898,388 898,388 9,743 8,653 -1,090 (-11.2%)

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