Skip to content

fix(vault): compute share<>asset conversions in a 256-bit intermediate - #38

Open
alejoamiras wants to merge 6 commits into
mainfrom
stack/closeout-vault-overflow
Open

alejoamiras wants to merge 6 commits into
mainfrom
stack/closeout-vault-overflow

Conversation

@alejoamiras

@alejoamiras alejoamiras commented Aug 19, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes F-004 from the audit. The vault's share<>asset conversions (a * b / denominator) overflowed u128 on large but valid inputs. Noir range-checks, so it reverts instead of wrapping - a vault that gets there can't deposit or withdraw anymore and everyone's funds are stuck.

  • New conversion.nr with a mul_div that widens to noir-bignum's U256 (same lib escrow already uses), divides there and narrows back, asserting the result fits. (2^128-1)^2 fits in 256 bits, so the product is always exact.
  • Rounding is unchanged: same floor(p/d) + (round_up && p%d != 0), so every input the old code accepted gives the same result. The 188 existing vault tests still pass, plus 7 new ones on the edges (products that used to overflow, max operands, rounding both ways, zero denominator).
  • A zero denominator now reverts explicitly - bignum's unconstrained path would otherwise return garbage. Unreachable from the vault, but mul_div is safe on its own.
  • README drops the overflow from the known issues. The ARC-403 hook reentrancy (F-001/F-002) is still there and still open.

The bigger Vault pushes its class publish to 63,776 DA gas, over the local network's per-tx cap (55,836 with the default 3s blocks; mainnet's 6s blocks admit ~117k). So CI now runs on mainnet geometry:

  • JS tests set SEQ_BLOCK_DURATION_MS=6000. The job is inlined because run-tests.yml has no knob for it, so the check is now JS Tests instead of checks / JS Tests.
  • Benchmarks pin aztec-benchmark v6.0.0-rc.1 (feat!: Aztec v6 (@aztec-labs), block-duration-ms input, SHA-pinned actions aztec-benchmark#3, which adds block-duration-ms), and the devDep moves to 6.0.0-rc.1 - same commit, @aztec-labs peers.
  • The bump-aztec-version skill now notes the benchmark peers, pins and DA cap.

Rebased on v6 main.

🤖 Generated with Claude Code

@alejoamiras
alejoamiras force-pushed the stack/closeout-vault-overflow branch from 09aac15 to 5f6f39f Compare August 19, 2026 16:16
@alejoamiras
alejoamiras force-pushed the stack/closeout-nft-notehash branch from d17b64d to 252c352 Compare August 19, 2026 16:16
@alejoamiras
alejoamiras force-pushed the stack/closeout-vault-overflow branch from 5f6f39f to b22f58e Compare August 19, 2026 19:07
@alejoamiras alejoamiras changed the title fix(vault): compute share<>asset conversions in a 512-bit intermediate fix(vault): compute share<>asset conversions in a 256-bit intermediate Aug 19, 2026
Comment thread .github/workflows/main-tests.yml Fixed
@alejoamiras
alejoamiras force-pushed the stack/closeout-nft-notehash branch from 252c352 to c6a4513 Compare September 28, 2026 14:00
@alejoamiras
alejoamiras force-pushed the stack/closeout-vault-overflow branch from b22f58e to b96059e Compare September 28, 2026 14:00
@alejoamiras
alejoamiras removed this pull request from stack #39 September 28, 2026 14:01
@alejoamiras
alejoamiras force-pushed the stack/closeout-vault-overflow branch from b96059e to 3433a70 Compare September 29, 2026 15:19
@alejoamiras
alejoamiras changed the base branch from stack/closeout-nft-notehash to main September 29, 2026 15:19
alejoamiras and others added 2 commits September 30, 2026 13:04
The conversions compute `a * b / denominator`. Done natively in u128 the
intermediate product overflows for large-but-legitimate inputs; Noir
range-checks u128, so the transaction reverts rather than wrapping. That
is an availability bug (audit F-004): a vault whose totals reach the
range can no longer be deposited to or withdrawn from, permanently
locking every participant's funds. Both sites carried TODOs.

New `conversion.nr` module with a `mul_div` primitive that widens both
operands to noir-bignum's U256 (the same library the escrow's key
derivation already uses), multiplies and divides there, then narrows the
quotient back to u128, asserting it fits. U256 arithmetic is modulo
2^256 and the largest possible product, (2^128-1)^2, is 2^129-1 short of
that modulus, so the product is always exact — no modular wraparound.

Rounding is unchanged: both old and new return
floor(p/d) + (round_up && p%d != 0). For every input the old code
accepted the results are identical; the widening only extends the domain
that succeeds. This matters because the vault's economic safety depends
on rounding always favouring the vault — a shift in either direction
would leak value between the vault and its depositors.

`mul_div` also rejects a zero denominator explicitly: noir-bignum's
constrained udiv_mod fails on it, but its unconstrained path assumes
non-zero and would return a meaningless witness. Unreachable from the
vault (denominators are total_assets+1 and total_supply+vault_offset
with vault_offset >= 1) but the helper is now safe in isolation.

Extracting mul_div into its own module is what makes the overflow
boundary testable at all: a real vault cannot be driven to a 2^128
supply in a test, but the primitive can be called directly at its edges.

Also adds `ensureVaultContractClassPublished` to the JS test utils.
Publishing just the class is what the vault tests actually need and is
substantially cheaper in DA gas than deploying a throwaway Vault to get
the class published as a side effect. It is idempotent — publication
emits a nullifier keyed on the class id, so a second publish of the same
class is rejected with "Existing nullifier"; the helper checks
registration state first, the same way DeployMethod does.

Validated: vault_contract 195 Noir tests (188 pre-existing all still
green — the strongest evidence rounding did not shift — plus 7 new
covering limb round-trips, rounding direction both ways, products that
previously overflowed, max operands, and the two revert guards).
aztec compile OK. Codex adversarial review: correct, rounding invariance
proven algebraically, no value-leak path; its zero-denominator hardening
is applied.

Note: the vault README still describes the overflow as a known issue.
That warning block is rewritten in PR #24 (unmerged); leaving it there
avoids a three-way conflict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`aztec start --local-network` defaults to 3s blocks. With 72s slots that
packs 21 blocks into a checkpoint, and the per-tx DA admission limit is
`ceil(daBudget / blocks * 1.5)` = 55,882 DA gas. Mainnet runs 6s blocks
(10 blocks per checkpoint) and admits 117,668.

The protocol picks that 1.5 multiplier deliberately, and says so in
aztec stdlib `gas/tx_gas_limits.ts`: it is set "so the largest tx we want
to support — a maximal contract class registration (~97k DA gas) — fits
a single block under v5 mainnet geometry (72s slots, 6s blocks -> 10
blocks per checkpoint)". The default local geometry therefore advertises
a limit well below the ~97k the protocol guarantees for exactly this
kind of transaction, and rejects contracts that are valid on the network
we ship to.

Publishing the Vault contract class costs ~64k DA gas. That is fine on
mainnet and inside the protocol's stated envelope, but over the local
55,882 cap. Note this is not specific to any one change: main's Vault
already sits at ~54k, i.e. 97% of the local ceiling, so essentially any
growth in that contract trips it.

Reusable workflows do not inherit the caller's `env`, and
aztec-ci-actions' run-tests.yml exposes no knob for this, so the JS job
is inlined here (`run-js-tests: false` on the reusable call) purely to
own the environment. It still calls the same pinned `setup-aztec` and
`js-tests` composite actions, so behaviour is otherwise unchanged.

NOTE FOR REVIEWERS: this renames the check from "checks / JS Tests" to
"JS Tests". Any branch protection rule naming the old check needs
updating, or it will block merges waiting on a check that no longer runs.

The better long-term fix is upstream — either the local network should
default to mainnet geometry, or run-tests.yml should expose the knob.
Worth raising with the Aztec CI folks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Vault benchmark's setup publishes the Vault contract class (63,776 DA
gas). The local network's default 3s blocks cap a tx at 55,836, and
aztec-benchmark's reusable workflows had no way to change that.

Pin both workflows to AztecProtocol/aztec-benchmark#3, which adds a
block-duration-ms input, and pass 6000 (mainnet's 6s blocks: 117,624).
This is a pre-release commit; swap it for the v6.0.0-rc.1 tag commit
before merging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

This comment has been minimized.

879fa93 is the squash commit of aztec-benchmark#3 that v6.0.0-rc.1 tags.
Its pr-benchmark.yml and update-baseline.yml are byte-identical to the
d623993 head they were verified at; only publish.yml differs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

This comment has been minimized.

alejoamiras and others added 2 commits October 1, 2026 17:56
5.0.1 declared @aztec/aztec.js and @aztec/wallets peers (<6) and typed
its Benchmark base class against them, so against this repo's
@aztec-labs/* v6 packages the peers were unmet and the benchmark files
carried 33 type errors (unseen: tsx does not type-check). 6.0.0-rc.1 peers
@aztec-labs/* >=6.0.0-0 <7 and the benchmark files type-check clean.

Its gitHead and SLSA provenance name 879fa93, the commit both benchmark
workflows are pinned to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The benchmark gotcha described 5.0.1's v5 peers as current. 6.0.0-rc.1
is the first @aztec-labs release. A peer mismatch fails nothing (yarn
warns, tsconfig skips benchmarks/, tsx doesn't type-check), so document
the explicit typecheck. Record that the devDependency and both workflow
pins move together, and the local-network DA cap that the Vault class
publication hits without 6s blocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Benchmark Comparison

CPU Cores RAM Arch
AMD EPYC 9V74 80-Core Processor 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 354,645 354,645 3,744 3,744 587,800 587,800 9,161 9,114 -47 (-0.5%)
⚪ withdraw 270,326 270,326 832 832 499,700 499,700 7,636 7,624 -12 (-0.2%)
⚪ withdraw_nft 266,680 266,680 1,440 1,440 527,400 527,400 7,899 7,879 -20 (-0.3%)

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 298,866 298,866 192 192 456,000 456,000 8,425 8,362 -63 (-0.7%)
⚪ secret_key_to_public_keys 296,331 296,331 192 192 456,000 456,000 8,394 8,336 -58 (-0.7%)
⚪ share_escrow 245,424 245,424 1,952 1,952 520,700 520,700 7,494 7,384 -110 (-1.5%)

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 222,468 222,468 832 832 499,700 499,700 6,981 6,943 -38 (-0.5%)
⚪ burn_public 185,539 185,539 416 416 638,286 638,286 6,485 6,459 -26 (-0.4%)
⚪ initialize_transfer_commitment 188,484 188,484 768 768 474,500 474,500 6,505 6,468 -37 (-0.6%)
⚪ mint_to_private 239,217 239,217 1,408 1,408 511,400 511,400 7,307 7,281 -26 (-0.4%)
⚪ mint_to_public 185,539 185,539 416 416 637,794 637,794 6,469 6,452 -17 (-0.3%)
⚪ transfer_private_to_commitment 225,577 225,577 1,024 1,024 511,400 511,400 6,952 6,956 +4 (+0.1%)
⚪ transfer_private_to_private 278,917 278,917 2,048 2,048 555,100 555,100 7,767 7,723 -44 (-0.6%)
⚪ transfer_private_to_public 254,364 254,364 1,056 1,056 714,671 714,671 7,498 7,492 -6 (-0.1%)
⚪ transfer_public_to_commitment 185,539 185,539 640 640 663,507 663,507 6,474 6,483 +9 (+0.1%)
⚪ transfer_public_to_private 246,797 246,797 1,024 1,024 683,952 683,952 7,418 7,387 -31 (-0.4%)
⚪ transfer_public_to_public 185,539 185,539 480 480 673,499 673,499 6,503 6,492 -11 (-0.2%)

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 223,523 223,523 416 416 661,046 661,046 7,153 7,128 -25 (-0.3%)
⚪ burn_public 185,539 185,539 448 448 670,028 670,028 6,468 6,440 -28 (-0.4%)
⚪ mint_to_private 268,061 268,061 1,600 1,600 735,336 735,336 7,837 7,825 -12 (-0.2%)
⚪ mint_to_public 185,539 185,539 448 448 670,712 670,712 6,501 6,476 -25 (-0.4%)
⚪ transfer_private_to_private 211,981 211,981 832 832 499,700 499,700 6,779 6,749 -30 (-0.4%)
⚪ transfer_private_to_public 223,550 223,550 416 416 659,252 659,252 7,137 7,114 -23 (-0.3%)
⚪ transfer_public_to_private 240,913 240,913 992 992 683,262 683,262 7,338 7,309 -29 (-0.4%)
⚪ transfer_public_to_public 185,539 185,539 384 384 633,279 633,279 6,463 6,445 -18 (-0.3%)

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 250,779 250,779 1,024 1,024 713,951 713,951 7,503 7,482 -21 (-0.3%)
⚪ burn_public 185,539 185,539 448 448 672,626 672,626 6,553 6,479 -74 (-1.1%)
⚪ initialize_transfer_commitment 188,484 188,484 768 768 474,500 474,500 6,527 6,505 -22 (-0.3%)
⚪ mint_to_private 281,953 281,953 2,144 2,144 738,064 738,064 7,996 7,993 -3 (-0.0%)
⚪ mint_to_public 185,539 185,539 448 448 672,221 672,221 6,502 6,499 -3 (-0.0%)
⚪ transfer_private_to_commitment 222,045 222,045 992 992 511,400 511,400 6,943 6,932 -11 (-0.2%)
⚪ transfer_private_to_private 286,325 286,325 2,592 2,592 557,600 557,600 7,863 7,840 -23 (-0.3%)
⚪ transfer_private_to_public 250,831 250,831 1,024 1,024 714,017 714,017 7,525 7,516 -9 (-0.1%)
⚪ transfer_private_to_public_with_commitment 254,753 254,753 1,600 1,600 747,317 747,317 7,518 7,518
⚪ transfer_public_to_commitment 185,539 185,539 576 576 662,520 662,520 6,476 6,487 +11 (+0.2%)
⚪ transfer_public_to_private 244,762 244,762 992 992 683,298 683,298 7,400 7,408 +8 (+0.1%)
⚪ transfer_public_to_public 185,539 185,539 448 448 672,542 672,542 6,479 6,460 -19 (-0.3%)

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 362,767 362,767 1,312 1,312 878,227 1,080,775 +202,548 (+23.1%) 9,326 9,306 -20 (-0.2%)
🔴 deposit_private_to_private_exact 471,194 471,194 1,888 1,888 915,295 1,117,843 +202,548 (+22.1%) 11,665 11,653 -12 (-0.1%)
🔴 deposit_private_to_public 308,983 308,983 768 768 862,252 1,064,800 +202,548 (+23.5%) 8,458 8,420 -38 (-0.4%)
🔴 deposit_public_to_private 298,960 298,960 1,984 1,984 965,024 1,167,572 +202,548 (+21.0%) 8,367 8,330 -37 (-0.4%)
🔴 deposit_public_to_private_exact 301,691 301,691 1,952 1,952 949,448 1,151,996 +202,548 (+21.3%) 8,359 8,391 +32 (+0.4%)
🔴 deposit_public_to_public 185,539 185,539 832 832 897,542 1,100,090 +202,548 (+22.6%) 6,508 6,486 -22 (-0.3%)
🔴 issue_private_to_private_exact 471,194 471,194 1,888 1,888 915,955 1,118,119 +202,164 (+22.1%) 11,154 11,102 -52 (-0.5%)
🔴 issue_private_to_public_exact 339,595 339,595 1,344 1,344 899,977 1,102,141 +202,164 (+22.5%) 9,039 9,022 -17 (-0.2%)
🔴 issue_public_to_private 275,652 275,652 1,376 1,376 921,640 1,123,804 +202,164 (+21.9%) 7,906 7,898 -8 (-0.1%)
🔴 issue_public_to_public 185,539 185,539 832 832 898,232 1,100,396 +202,164 (+22.5%) 6,491 6,474 -17 (-0.3%)
🔴 redeem_private_to_private_exact 474,161 474,161 1,888 1,888 915,802 1,118,350 +202,548 (+22.1%) 11,245 11,140 -105 (-0.9%)
🔴 redeem_private_to_public 308,930 308,930 768 768 862,696 1,065,244 +202,548 (+23.5%) 8,487 8,444 -43 (-0.5%)
🔴 redeem_public_to_private_exact 304,711 304,711 1,952 1,952 949,769 1,152,317 +202,548 (+21.3%) 8,456 8,345 -111 (-1.3%)
🔴 redeem_public_to_public 185,539 185,539 832 832 898,064 1,100,612 +202,548 (+22.6%) 6,495 6,454 -41 (-0.6%)
🔴 withdraw_private_to_private 365,734 365,734 1,312 1,312 878,437 1,080,601 +202,164 (+23.0%) 9,357 9,352 -5 (-0.1%)
🔴 withdraw_private_to_private_exact 474,161 474,161 1,888 1,888 915,748 1,117,912 +202,164 (+22.1%) 11,180 11,184 +4 (+0.0%)
🔴 withdraw_private_to_public_exact 339,542 339,542 1,344 1,344 900,211 1,102,375 +202,164 (+22.5%) 9,027 9,072 +45 (+0.5%)
🔴 withdraw_public_to_private 314,923 314,923 2,528 2,528 967,335 1,169,499 +202,164 (+20.9%) 8,532 8,602 +70 (+0.8%)
🔴 withdraw_public_to_public 185,539 185,539 832 832 898,367 1,100,531 +202,164 (+22.5%) 6,467 6,454 -13 (-0.2%)

@alejoamiras
alejoamiras marked this pull request as ready for review October 1, 2026 18:46

This branch has not been deployed

No deployments
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.

2 participants