-
Notifications
You must be signed in to change notification settings - Fork 27
feat(token): native shielded token supply extension #638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
989538d
feat(token): native shielded token supply extension
0xisk ec1804e
feat(token): expose supply ledger keys unprefixed
0xisk 5770f4a
feat(token): make totalSupply clamp to zero
0xisk 5f47727
test(token): fuzz both supply flavors in one file
0xisk e00ef37
fix(token): give supply _addBurned guards distinct assert messages
0xisk 2dcd26e
docs(token): warn that mint/burn accounting pairing is security-critical
0xisk a159446
Merge branch 'main' into feat/native-shielded-token-supply-accounting
0xisk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
145 changes: 145 additions & 0 deletions
145
contracts/src/token/extensions/NativeShieldedTokenFamilySupply.compact
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| // OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenFamilySupply.compact) | ||
|
|
||
| pragma language_version >= 0.23.0; | ||
|
|
||
| /** | ||
| * @module NativeShieldedTokenFamilySupply | ||
| * @description Optional standalone extension that adds per-domain on-chain | ||
| * supply accounting to a `NativeShieldedTokenFamily`. It exposes `_addMinted` / | ||
| * `_addBurned` building blocks plus the `totalMinted` / `totalBurned` / | ||
| * `totalSupply` getters, each keyed by domain. It imports no token module. | ||
| * | ||
| * It is a thin wrapper over `NativeShieldedTokenSupplyCore`, forwarding the | ||
| * caller's per-call `domain`, so the accounting logic and guarantees live in | ||
| * the shared core. | ||
| * | ||
| * @notice Pairs with `NativeShieldedTokenFamily`. The consuming contract | ||
| * composes the pieces, calling the accounting block alongside the matching | ||
| * token op: | ||
| * | ||
| * export circuit mint( | ||
| * domain: Bytes<32>, | ||
| * recipient: Either<ZswapCoinPublicKey, ContractAddress>, | ||
| * amount: Uint<64>, | ||
| * nonce: Bytes<32> | ||
| * ): ShieldedCoinInfo { | ||
| * const coin = Family__mint(domain, recipient, amount, nonce); | ||
| * Supply__addMinted(domain, amount); | ||
| * return coin; | ||
| * } | ||
| * | ||
| * export circuit burn( | ||
| * domain: Bytes<32>, | ||
| * coin: ShieldedCoinInfo, | ||
| * amount: Uint<128>, | ||
| * refundTo: Either<ZswapCoinPublicKey, ContractAddress> | ||
| * ): Maybe<ShieldedCoinInfo> { | ||
| * const refund = Family__burn(domain, coin, amount, refundTo); | ||
| * Supply__addBurned(domain, amount); | ||
| * return refund; | ||
| * } | ||
| * | ||
| * @notice Privacy trade-off (read before composing): composing this extension | ||
| * makes contract-mediated BURN amounts PUBLIC. The bare family's `_burn` / | ||
| * `_burnFromSelf` are amount-private (their coin operations emit only | ||
| * commitments and nullifiers, never the value; the `disclose()` wrappers they | ||
| * require are compiler permission markers, not disclosure sinks). The | ||
| * `_addBurned` write here is what puts the burned amount into the public | ||
| * transcript. Compose this only when on-chain auditability is worth more than | ||
| * burn-amount privacy. | ||
| * | ||
| * @dev Mint amounts are already public via the protocol's `shieldedMints` | ||
| * effect, so `_addMinted` adds accounting, not disclosure. See | ||
| * `NativeShieldedTokenSupplyCore` for the exact / lower-bound / upper-bound | ||
| * guarantees, the per-domain `burned <= minted` invariant, and the mint-side | ||
| * drift caveat. | ||
| * | ||
| * @warning Pair every mint with `_addMinted` and every burn with `_addBurned` on | ||
| * every path, under the matching domain; mis-wiring is a security-critical, | ||
| * undetectable error (e.g. an undercounted mint makes `_addBurned` reject | ||
| * legitimate burns). See `NativeShieldedTokenSupplyCore`. | ||
| */ | ||
| module NativeShieldedTokenFamilySupply { | ||
| import CompactStandardLibrary; | ||
| import "./NativeShieldedTokenSupplyCore" prefix Core_; | ||
|
|
||
| // Circuits use the `Core_` prefix above. The named import/re-export below is | ||
| // only for the core supply ledger keys, so the implementing contract's ledger | ||
| // keys read as `_totalMinted` / `_totalBurned` rather than | ||
| // `NativeShieldedTokenSupplyCore__totalMinted`. The circuit names are | ||
| // intentionally NOT imported unprefixed, to avoid potential clashes. | ||
| import { _totalMinted, _totalBurned } from "./NativeShieldedTokenSupplyCore"; | ||
| export { _totalMinted, _totalBurned }; | ||
|
|
||
| /** | ||
| * @description Adds `amount` to the exact minted total for `domain`. Call | ||
| * once per successful `NativeShieldedTokenFamily._mint`. | ||
| * | ||
| * @circuitInfo k=10, rows=653 | ||
| * | ||
| * @param {Bytes<32>} domain - 32-byte token discriminator. | ||
| * @param {Uint<64>} amount - The minted amount. | ||
| * @return {[]} - Empty tuple. | ||
| */ | ||
| export circuit _addMinted(domain: Bytes<32>, amount: Uint<64>): [] { | ||
| Core__addMinted(domain, amount); | ||
| } | ||
|
|
||
| /** | ||
| * @description Adds `amount` to the contract-mediated burned total for | ||
| * `domain`. Call once per successful `NativeShieldedTokenFamily._burn` / | ||
| * `_burnFromSelf`. | ||
| * @dev Enforces per-domain `burned <= minted`; an unpaired, duplicate, or | ||
| * wrong-domain burn reverts. | ||
| * | ||
| * @circuitInfo k=10, rows=755 | ||
| * | ||
| * @param {Bytes<32>} domain - 32-byte token discriminator. | ||
| * @param {Uint<128>} amount - The burned amount. | ||
| * @return {[]} - Empty tuple. | ||
| */ | ||
| export circuit _addBurned(domain: Bytes<32>, amount: Uint<128>): [] { | ||
| Core__addBurned(domain, amount); | ||
| } | ||
|
|
||
| /** | ||
| * @description Returns the exact amount ever minted for `domain`. | ||
| * | ||
| * @circuitInfo k=9, rows=344 | ||
| * | ||
| * @param {Bytes<32>} domain - 32-byte token discriminator. | ||
| * @return {Uint<128>} - The total amount minted. | ||
| */ | ||
| export circuit totalMinted(domain: Bytes<32>): Uint<128> { | ||
| return Core_totalMinted(domain); | ||
| } | ||
|
|
||
| /** | ||
| * @description Returns the contract-mediated amount burned for `domain`. | ||
| * @notice This is a lower bound: coins sent directly to the burn address | ||
| * without going through the contract are not counted. | ||
| * | ||
| * @circuitInfo k=9, rows=341 | ||
| * | ||
| * @param {Bytes<32>} domain - 32-byte token discriminator. | ||
| * @return {Uint<128>} - The total amount burned through the contract. | ||
| */ | ||
| export circuit totalBurned(domain: Bytes<32>): Uint<128> { | ||
| return Core_totalBurned(domain); | ||
| } | ||
|
|
||
| /** | ||
| * @description Returns `totalMinted(domain) - totalBurned(domain)`. | ||
| * @notice This is an UPPER BOUND on circulating supply, not an exact value: | ||
| * burns that bypass the contract are invisible. See the module notes. | ||
| * | ||
| * @circuitInfo k=9, rows=439 | ||
| * | ||
| * @param {Bytes<32>} domain - 32-byte token discriminator. | ||
| * @return {Uint<128>} - The upper bound on tokens in existence. | ||
| */ | ||
| export circuit totalSupply(domain: Bytes<32>): Uint<128> { | ||
| return Core_totalSupply(domain); | ||
| } | ||
| } |
142 changes: 142 additions & 0 deletions
142
contracts/src/token/extensions/NativeShieldedTokenSupply.compact
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| // OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenSupply.compact) | ||
|
|
||
| pragma language_version >= 0.23.0; | ||
|
|
||
| /** | ||
| * @module NativeShieldedTokenSupply | ||
| * @description Optional standalone extension that adds on-chain supply | ||
| * accounting to a `NativeShieldedToken`. It exposes scalar `_addMinted` / | ||
| * `_addBurned` building blocks plus the `totalMinted` / `totalBurned` / | ||
| * `totalSupply` getters. It imports no token module. | ||
| * | ||
| * It is a thin wrapper over `NativeShieldedTokenSupplyCore`, forwarding a | ||
| * single fixed key (the paired contract has exactly one token type), so the | ||
| * accounting logic and guarantees live in the shared core. | ||
| * | ||
| * @notice Pairs with `NativeShieldedToken`. The consuming contract composes the | ||
| * pieces, calling the accounting block alongside the matching token op: | ||
| * | ||
| * export circuit mint( | ||
| * recipient: Either<ZswapCoinPublicKey, ContractAddress>, | ||
| * amount: Uint<64>, | ||
| * nonce: Bytes<32> | ||
| * ): ShieldedCoinInfo { | ||
| * const coin = Token__mint(recipient, amount, nonce); | ||
| * Supply__addMinted(amount); | ||
| * return coin; | ||
| * } | ||
| * | ||
| * export circuit burn( | ||
| * coin: ShieldedCoinInfo, | ||
| * amount: Uint<128>, | ||
| * refundTo: Either<ZswapCoinPublicKey, ContractAddress> | ||
| * ): Maybe<ShieldedCoinInfo> { | ||
| * const refund = Token__burn(coin, amount, refundTo); | ||
| * Supply__addBurned(amount); | ||
| * return refund; | ||
| * } | ||
| * | ||
| * @notice Privacy trade-off (read before composing): composing this extension | ||
| * makes contract-mediated BURN amounts PUBLIC. The bare token's `_burn` / | ||
| * `_burnFromSelf` are amount-private (their coin operations emit only | ||
| * commitments and nullifiers, never the value; the `disclose()` wrappers they | ||
| * require are compiler permission markers, not disclosure sinks). The | ||
| * `_addBurned` write here is what puts the burned amount into the public | ||
| * transcript. Compose this only when on-chain auditability is worth more than | ||
| * burn-amount privacy. | ||
| * | ||
| * @dev Mint amounts are already public via the protocol's `shieldedMints` | ||
| * effect, so `_addMinted` adds accounting, not disclosure. See | ||
| * `NativeShieldedTokenSupplyCore` for the exact / lower-bound / upper-bound | ||
| * guarantees, the `burned <= minted` invariant, and the mint-side drift caveat. | ||
| * | ||
| * @warning Pair every mint with `_addMinted` and every burn with `_addBurned` on | ||
| * every path; mis-wiring is a security-critical, undetectable error (e.g. an | ||
| * undercounted mint makes `_addBurned` reject legitimate burns). See | ||
| * `NativeShieldedTokenSupplyCore`. | ||
| */ | ||
| module NativeShieldedTokenSupply { | ||
| import CompactStandardLibrary; | ||
| import "./NativeShieldedTokenSupplyCore" prefix Core_; | ||
|
|
||
| // Circuits use the `Core_` prefix above. The named import/re-export below is | ||
| // only for the core supply ledger keys, so the implementing contract's ledger | ||
| // keys read as `_totalMinted` / `_totalBurned` rather than | ||
| // `NativeShieldedTokenSupplyCore__totalMinted`. The circuit names are | ||
| // intentionally NOT imported unprefixed, to avoid potential clashes. | ||
| import { _totalMinted, _totalBurned } from "./NativeShieldedTokenSupplyCore"; | ||
| export { _totalMinted, _totalBurned }; | ||
|
|
||
| /** | ||
| * @description The single fixed domain key. The paired `NativeShieldedToken` | ||
| * has exactly one token type, so all accounting lives under one key. | ||
| */ | ||
| circuit key(): Bytes<32> { | ||
| return default<Bytes<32>>; | ||
| } | ||
|
|
||
| /** | ||
| * @description Adds `amount` to the exact minted total. Call once per | ||
| * successful `NativeShieldedToken._mint`. | ||
| * | ||
| * @circuitInfo k=9, rows=375 | ||
| * | ||
| * @param {Uint<64>} amount - The minted amount. | ||
| * @return {[]} - Empty tuple. | ||
| */ | ||
| export circuit _addMinted(amount: Uint<64>): [] { | ||
| Core__addMinted(key(), amount); | ||
| } | ||
|
|
||
| /** | ||
| * @description Adds `amount` to the contract-mediated burned total. Call once | ||
| * per successful `NativeShieldedToken._burn` / `_burnFromSelf`. | ||
| * @dev Enforces `burned <= minted`; an unpaired or duplicate burn reverts. | ||
| * | ||
| * @circuitInfo k=9, rows=477 | ||
| * | ||
| * @param {Uint<128>} amount - The burned amount. | ||
| * @return {[]} - Empty tuple. | ||
| */ | ||
| export circuit _addBurned(amount: Uint<128>): [] { | ||
| Core__addBurned(key(), amount); | ||
| } | ||
|
|
||
| /** | ||
| * @description Returns the exact amount ever minted. | ||
| * | ||
| * @circuitInfo k=7, rows=65 | ||
| * | ||
| * @return {Uint<128>} - The total amount minted. | ||
| */ | ||
| export circuit totalMinted(): Uint<128> { | ||
| return Core_totalMinted(key()); | ||
| } | ||
|
|
||
| /** | ||
| * @description Returns the contract-mediated amount burned. | ||
| * @notice This is a lower bound: coins sent directly to the burn address | ||
| * without going through the contract are not counted. | ||
| * | ||
| * @circuitInfo k=7, rows=62 | ||
| * | ||
| * @return {Uint<128>} - The total amount burned through the contract. | ||
| */ | ||
| export circuit totalBurned(): Uint<128> { | ||
| return Core_totalBurned(key()); | ||
| } | ||
|
|
||
| /** | ||
| * @description Returns `totalMinted() - totalBurned()`. | ||
| * @notice This is an UPPER BOUND on circulating supply, not an exact value: | ||
| * burns that bypass the contract are invisible. See the module notes. | ||
| * | ||
| * @circuitInfo k=9, rows=160 | ||
| * | ||
| * @return {Uint<128>} - The upper bound on tokens in existence. | ||
| */ | ||
| export circuit totalSupply(): Uint<128> { | ||
| return Core_totalSupply(key()); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.