Skip to content

feat: public and private supply#325

Open
zkfrov wants to merge 3 commits intodevfrom
feat/public-and-private-supply
Open

feat: public and private supply#325
zkfrov wants to merge 3 commits intodevfrom
feat/public-and-private-supply

Conversation

@zkfrov
Copy link
Copy Markdown
Contributor

@zkfrov zkfrov commented Apr 6, 2026

🤖 Linear

Closes AZT-XXX

Description

  • Add public_supply storage field to the token contract, tracking how much of the total supply lives in public balances
  • Derive private_supply as total_supply - public_supply
  • Add public_supply() and private_supply() view functions
  • Rename increase_public_balance_internal / decrease_public_balance_internal to increase_public_balance_and_supply_internal / decrease_public_balance_and_supply_internal
  • Add assert_supply test helper (Noir + JS) that validates public, private, and total supply in a single call
  • Add supply assertions to all success tests across every transfer, mint, and burn flow

Motivation

Tracking the public/private supply split gives other contracts an on-chain oracle for the token's "visibility distribution" — a property unique to Aztec. This enables use cases like dynamic risk parameters in lending markets, liquidity visibility for DEXes, governance quorum calculations, compliance circuit breakers, and more.

Design decisions

  • Track total_supply + public_supply (not private_supply): Every change to public supply already involves a public execution, making it the natural value to update directly. total_supply is already needed for mint overflow checks.
  • Separate _increase_public_supply / _decrease_public_supply internal functions: Consistent with the existing pattern for _increase_total_supply / _decrease_total_supply. Trades a small function dispatch overhead for cleaner reuse across 6 call sites.

@github-actions

This comment has been minimized.

xorsal
xorsal previously approved these changes Apr 6, 2026
#[external("public")]
#[view]
fn private_supply() -> u128 {
self.storage.total_supply.read() - self.storage.public_supply.read()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enters the list of invariants (that we never wrote) we need to hold,

total_supply must be always >= public_supply

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yess, we should add the invariant list!

#[external("public")]
#[only_self]
fn increase_public_balance_internal(to: AztecAddress, amount: u128) {
fn increase_public_balance_and_supply_internal(to: AztecAddress, amount: u128) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking out loud...
This makes me wonder if it should be increase_public_balance_and_supply_internal or increase_public_balance_and_public_supply_internal,
I think the latter is better but it's too verbose IMO.

Copy link
Copy Markdown
Contributor Author

@zkfrov zkfrov Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree ser, will use it tho, as we have supply and public_supply now -> 2068577

@zkfrov zkfrov marked this pull request as ready for review April 7, 2026 08:54
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add public and private supply tracking to token contract

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add public_supply storage field tracking public token distribution
• Implement public_supply() and private_supply() view functions
• Rename internal functions to include supply updates in their names
• Add comprehensive supply assertions across all token transfer, mint, and burn tests
Diagram
flowchart LR
  A["Token Storage"] -->|adds| B["public_supply field"]
  C["View Functions"] -->|adds| D["public_supply()"]
  C -->|adds| E["private_supply()"]
  F["Internal Functions"] -->|renamed| G["increase_public_balance_and_supply_internal"]
  F -->|renamed| H["decrease_public_balance_and_supply_internal"]
  I["Tests"] -->|adds| J["assertSupply helper"]
  I -->|updates| K["All transfer/mint/burn tests"]
Loading

Grey Divider

File Changes

1. src/token_contract/src/main.nr ✨ Enhancement +48/-9

Add public supply storage and view functions

src/token_contract/src/main.nr


2. src/token_contract/src/test/utils.nr 🧪 Tests +18/-0

Add assert_supply test helper function

src/token_contract/src/test/utils.nr


3. src/ts/test/utils.ts 🧪 Tests +15/-0

Add assertSupply test helper for TypeScript

src/ts/test/utils.ts


View more (13)
4. src/ts/test/token.test.ts 🧪 Tests +9/-3

Add supply assertions to token tests

src/ts/test/token.test.ts


5. src/token_contract/src/test/burn_private.nr 🧪 Tests +2/-0

Add supply assertions to private burn tests

src/token_contract/src/test/burn_private.nr


6. src/token_contract/src/test/burn_public.nr 🧪 Tests +4/-2

Add supply assertions to public burn tests

src/token_contract/src/test/burn_public.nr


7. src/token_contract/src/test/mint_to_commitment.nr 🧪 Tests +1/-3

Add supply assertions to commitment mint tests

src/token_contract/src/test/mint_to_commitment.nr


8. src/token_contract/src/test/mint_to_private.nr 🧪 Tests +1/-3

Add supply assertions to private mint tests

src/token_contract/src/test/mint_to_private.nr


9. src/token_contract/src/test/mint_to_public.nr 🧪 Tests +1/-3

Add supply assertions to public mint tests

src/token_contract/src/test/mint_to_public.nr


10. src/token_contract/src/test/transfer_private_to_commitment.nr 🧪 Tests +2/-0

Add supply assertions to private commitment transfers

src/token_contract/src/test/transfer_private_to_commitment.nr


11. src/token_contract/src/test/transfer_private_to_private.nr 🧪 Tests +3/-0

Add supply assertions to private transfers

src/token_contract/src/test/transfer_private_to_private.nr


12. src/token_contract/src/test/transfer_private_to_public.nr 🧪 Tests +18/-0

Add supply assertions to private-to-public transfers

src/token_contract/src/test/transfer_private_to_public.nr


13. src/token_contract/src/test/transfer_private_to_public_with_commitment.nr 🧪 Tests +3/-0

Add supply assertions to commitment-based transfers

src/token_contract/src/test/transfer_private_to_public_with_commitment.nr


14. src/token_contract/src/test/transfer_public_to_commitment.nr 🧪 Tests +2/-0

Add supply assertions to public commitment transfers

src/token_contract/src/test/transfer_public_to_commitment.nr


15. src/token_contract/src/test/transfer_public_to_private.nr 🧪 Tests +13/-1

Add supply assertions to public-to-private transfers

src/token_contract/src/test/transfer_public_to_private.nr


16. src/token_contract/src/test/transfer_public_to_public.nr 🧪 Tests +3/-0

Add supply assertions to public transfers

src/token_contract/src/test/transfer_public_to_public.nr


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Apr 7, 2026

Code Review by Qodo

🐞 Bugs (1)   📘 Rule violations (0)   📎 Requirement gaps (0)   🎨 UX Issues (0)
🐞\ ☼ Reliability (1)

Grey Divider


Remediation recommended

1. private_supply underflow revert 🐞
Description
private_supply() computes total_supply - public_supply without checking the invariant, so if
public_supply ever exceeds total_supply the view will revert with an overflow. Because this is
intended as an on-chain oracle, an unexpected revert can break downstream contract integrations and
off-chain callers even if the rest of the token continues functioning.
Code

src/token_contract/src/main.nr[R341-347]

+    /// @notice Returns the private total supply of the token
+    /// @return The private total supply of the token
+    #[external("public")]
+    #[view]
+    fn private_supply() -> u128 {
+        self.storage.total_supply.read() - self.storage.public_supply.read()
+    }
Evidence
The new private_supply() view performs a direct u128 subtraction with no guard, which will
underflow/revert when public_supply > total_supply. Since public_supply is maintained via
separate writes in _increase_public_supply/_decrease_public_supply (and is updated in different
call paths than _increase_total_supply/_decrease_total_supply), the critical invariant is not
enforced at the read site, so any future logic bug (or unexpected state corruption) would turn this
view into a reverting oracle.

src/token_contract/src/main.nr[333-347]
src/token_contract/src/main.nr[575-605]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`private_supply()` currently returns `total_supply - public_supply` directly. In u128 arithmetic this will underflow and revert if `public_supply > total_supply`, which makes the new supply oracle brittle.

### Issue Context
- `public_supply` is updated independently from `total_supply` in multiple flows.
- Even if current flows are correct, a future regression would cause `private_supply()` to revert with a generic overflow error.

### Fix Focus Areas
- src/token_contract/src/main.nr[333-347]
- src/token_contract/src/main.nr[575-605]

### Suggested change
- In `private_supply()`, read both values into locals and add `assert(total >= public, "public_supply exceeds total_supply")` before subtracting.
- (Optional but stronger) Add invariant checks in `_increase_public_supply` / `_decrease_public_supply`:
 - After increasing, assert `public_supply <= total_supply`.
 - Before decreasing, assert `public_supply >= amount` (to fail with a clearer message than an overflow).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.

Grey Divider

Previous review results

Review updated until commit 2068577

Results up to commit cf79ee4


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review Grey Divider Grey Divider

Qodo Logo

cubic-dev-ai[bot]
cubic-dev-ai bot previously approved these changes Apr 7, 2026
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 16 files

@zkfrov zkfrov dismissed stale reviews from cubic-dev-ai[bot] and xorsal via 2068577 April 7, 2026 09:43
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Apr 7, 2026

Persistent review updated to latest commit 2068577

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

Benchmark Comparison

CPU Cores RAM Arch
AMD EPYC 7763 64-Core Processor 16 63 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 579,408 579,408 1,312 1,312 495,400 495,400 7,169 7,131 -38 (-0.5%)
withdraw 579,408 579,408 736 736 483,700 483,700 7,140 7,164 +24 (+0.3%)
withdraw_nft 536,373 536,373 736 736 483,700 483,700 6,933 7,009 +76 (+1.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 520,851 520,851 128 128 456,000 456,000 6,854 6,907 +53 (+0.8%)
secret_key_to_public_keys 515,935 515,935 128 128 456,000 456,000 6,819 6,848 +29 (+0.4%)
share_escrow 420,067 420,067 704 704 474,500 474,500 6,006 6,010 +4 (+0.1%)

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 455,161 455,161 416 416 671,675 671,675 6,360 6,295 -65 (-1.0%)
burn_public 340,915 340,915 448 448 691,760 691,760 5,360 5,329 -31 (-0.6%)
mint_to_private 472,217 472,217 960 960 662,581 662,581 6,428 6,463 +35 (+0.5%)
mint_to_public 340,915 340,915 448 448 694,916 694,916 5,368 5,375 +7 (+0.1%)
transfer_private_to_private 427,956 427,956 736 736 483,700 483,700 6,112 6,046 -66 (-1.1%)
transfer_private_to_public 455,237 455,237 416 416 666,473 666,473 6,323 6,285 -38 (-0.6%)
transfer_public_to_private 468,858 468,858 960 960 662,665 662,665 6,381 6,373 -8 (-0.1%)
transfer_public_to_public 340,915 340,915 384 384 648,357 648,357 5,379 5,419 +40 (+0.7%)

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 486,861 486,861 992 992 687,411 687,600 +189 (+0.0%) 6,489 6,496 +7 (+0.1%)
🔴 burn_public 340,915 340,915 448 512 +64 (+14.3%) 681,599 716,746 +35,147 (+5.2%) 5,340 5,385 +45 (+0.8%)
initialize_transfer_commitment 403,443 403,443 704 704 474,500 474,500 5,898 5,920 +22 (+0.4%)
mint_to_private 473,137 473,137 960 960 656,449 656,638 +189 (+0.0%) 6,466 6,409 -57 (-0.9%)
🔴 mint_to_public 340,915 340,915 448 512 +64 (+14.3%) 690,521 725,668 +35,147 (+5.1%) 5,364 5,350 -14 (-0.3%)
transfer_private_to_commitment 453,824 453,824 896 896 495,400 495,400 6,229 6,184 -45 (-0.7%)
transfer_private_to_private 470,985 470,985 1,312 1,312 495,400 495,400 6,274 6,317 +43 (+0.7%)
🔴 transfer_private_to_public 486,937 486,937 992 1,056 +64 (+6.5%) 690,330 725,315 +34,985 (+5.1%) 6,532 6,521 -11 (-0.2%)
🔴 transfer_private_to_public_with_commitment 497,197 497,197 1,568 1,632 +64 (+4.1%) 723,630 758,615 +34,985 (+4.8%) 6,568 6,533 -35 (-0.5%)
🔴 transfer_public_to_commitment 340,915 340,915 576 640 +64 (+11.1%) 683,157 718,142 +34,985 (+5.1%) 5,399 5,338 -61 (-1.1%)
🔴 transfer_public_to_private 469,793 469,793 960 1,024 +64 (+6.7%) 659,611 694,596 +34,985 (+5.3%) 6,394 6,426 +32 (+0.5%)
transfer_public_to_public 340,915 340,915 448 448 684,065 684,092 +27 (+0.0%) 5,340 5,428 +88 (+1.6%)

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 904,856 904,856 1,280 1,344 +64 (+5.0%) 894,014 929,242 +35,228 (+3.9%) 10,084 9,993 -91 (-0.9%)
🔴 deposit_private_to_private_exact 1,017,026 1,017,026 1,856 1,920 +64 (+3.4%) 936,548 971,776 +35,228 (+3.8%) 10,943 10,888 -55 (-0.5%)
🔴 deposit_private_to_public 718,982 718,982 768 896 +128 (+16.7%) 928,555 998,741 +70,186 (+7.6%) 8,480 8,478 -2 (-0.0%)
deposit_public_to_private 589,551 589,551 1,344 1,344 959,628 960,342 +714 (+0.1%) 7,365 7,360 -5 (-0.1%)
deposit_public_to_private_exact 748,093 748,093 1,920 1,920 1,002,324 1,003,038 +714 (+0.1%) 8,742 8,680 -62 (-0.7%)
🔴 deposit_public_to_public 340,915 340,915 832 896 +64 (+7.7%) 1,000,133 1,035,805 +35,672 (+3.6%) 5,346 5,384 +38 (+0.7%)
🔴 issue_private_to_private_exact 1,017,026 1,017,026 1,856 1,920 +64 (+3.4%) 936,824 972,052 +35,228 (+3.8%) 10,911 10,895 -16 (-0.1%)
🔴 issue_private_to_public_exact 831,162 831,162 1,344 1,472 +128 (+9.5%) 971,590 1,041,776 +70,186 (+7.2%) 9,391 9,365 -26 (-0.3%)
issue_public_to_private 639,893 639,893 1,344 1,344 968,462 969,176 +714 (+0.1%) 7,859 7,820 -39 (-0.5%)
🔴 issue_public_to_public 340,915 340,915 832 896 +64 (+7.7%) 1,000,436 1,036,108 +35,672 (+3.6%) 5,348 5,374 +26 (+0.5%)
🔴 redeem_private_to_private_exact 1,013,679 1,013,679 1,856 1,920 +64 (+3.4%) 937,055 972,283 +35,228 (+3.8%) 10,890 10,825 -65 (-0.6%)
redeem_private_to_public 718,905 718,905 768 768 919,369 919,639 +270 (+0.0%) 8,410 8,401 -9 (-0.1%)
🔴 redeem_public_to_private_exact 698,526 698,526 1,920 2,048 +128 (+6.7%) 999,453 1,069,639 +70,186 (+7.0%) 8,292 8,204 -88 (-1.1%)
🔴 redeem_public_to_public 340,915 340,915 832 896 +64 (+7.7%) 987,833 1,023,061 +35,228 (+3.6%) 5,346 5,318 -28 (-0.5%)
🔴 withdraw_private_to_private 901,509 901,509 1,280 1,344 +64 (+5.0%) 893,840 929,068 +35,228 (+3.9%) 9,985 9,952 -33 (-0.3%)
🔴 withdraw_private_to_private_exact 1,013,679 1,013,679 1,856 1,920 +64 (+3.4%) 936,617 971,845 +35,228 (+3.8%) 10,897 10,827 -70 (-0.6%)
withdraw_private_to_public_exact 831,084 831,084 1,344 1,344 962,194 962,464 +270 (+0.0%) 9,369 9,310 -59 (-0.6%)
🔴 withdraw_public_to_private 586,272 586,272 1,344 1,472 +128 (+9.5%) 955,866 1,026,052 +70,186 (+7.3%) 7,338 7,334 -4 (-0.1%)
🔴 withdraw_public_to_public 340,915 340,915 832 896 +64 (+7.7%) 987,752 1,022,980 +35,228 (+3.6%) 5,362 5,375 +13 (+0.2%)

Comment on lines +81 to +82
transfer_private_to_public_amount,
mint_amount - transfer_private_to_public_amount,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does transfer_private_to_public_amount change supply? 👀

Comment on lines +131 to +136
utils::assert_supply(
env,
token_contract_address,
transfer_amount,
total_amount - transfer_amount,
);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not following why the transfer amount changes supply, will keep on reading and come back

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, i understand now, hm idk about the need for the feature tbh, but it's true that's an onchain untracked metric on current design, wonder what's the use case of querying that

@wei3erHase
Copy link
Copy Markdown
Member

like: gate-count remains constant because is just public logic and storage, +5% doesn't seem prohibitive, tho we need to analyze and understand the actual mainnet costs
dont like: don't see real use case

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants