Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Rename the contract-compilation scripts and Turbo tasks from `compact` / `compact:*` to `compile` / `compile:*`, and the Biome scripts from `fmt-and-lint` / `fmt-and-lint:*` to `lint` / `lint:*`. (#680)
- Rename the native shielded token supply extensions to `NativeShieldedTokenPublicSupply` / `NativeShieldedTokenFamilyPublicSupply` (and the shared `NativeShieldedTokenPublicSupplyCore`), making explicit that they track supply on-chain and matching the `ConfidentialFungibleTokenPublicSupply` naming. (#710)

## 0.3.0-alpha (2026-06-30)

Expand Down
2 changes: 1 addition & 1 deletion contracts/src/token/NativeShieldedToken.compact
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pragma language_version >= 0.23.0;
* recipient-public.
*
* @notice Supply accounting is opt-in: this module tracks NO supply totals.
* Compose the optional `NativeShieldedTokenSupply` extension only if you need an
* Compose the optional `NativeShieldedTokenPublicSupply` extension only if you need an
* on-chain `totalSupply()`; see it for the privacy trade-off it introduces (it
* makes contract-mediated burn amounts public).
*
Expand Down
2 changes: 1 addition & 1 deletion contracts/src/token/NativeShieldedTokenFamily.compact
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pragma language_version >= 0.23.0;
* output, at the cost of making those mints recipient-public.
*
* @notice Supply accounting is opt-in: this module tracks NO supply totals.
* Compose the optional `NativeShieldedTokenFamilySupply` extension only if you
* Compose the optional `NativeShieldedTokenFamilyPublicSupply` extension only if you
* need an on-chain `totalSupply(domain)`; see it for the privacy trade-off it
* introduces (it makes contract-mediated burn amounts public).
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenFamilySupply.compact)
// OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenFamilyPublicSupply.compact)

pragma language_version >= 0.23.0;

/**
* @module NativeShieldedTokenFamilySupply
* @module NativeShieldedTokenFamilyPublicSupply
* @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
* It is a thin wrapper over `NativeShieldedTokenPublicSupplyCore`, forwarding the
* caller's per-call `domain`, so the accounting logic and guarantees live in
* the shared core.
*
Expand Down Expand Up @@ -51,25 +51,25 @@ pragma language_version >= 0.23.0;
*
* @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
* `NativeShieldedTokenPublicSupplyCore` 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`.
* legitimate burns). See `NativeShieldedTokenPublicSupplyCore`.
*/
module NativeShieldedTokenFamilySupply {
module NativeShieldedTokenFamilyPublicSupply {
import CompactStandardLibrary;
import "./NativeShieldedTokenSupplyCore" prefix Core_;
import "./NativeShieldedTokenPublicSupplyCore" 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
// `NativeShieldedTokenPublicSupplyCore__totalMinted`. The circuit names are
// intentionally NOT imported unprefixed, to avoid potential clashes.
import { _totalMinted, _totalBurned } from "./NativeShieldedTokenSupplyCore";
import { _totalMinted, _totalBurned } from "./NativeShieldedTokenPublicSupplyCore";
export { _totalMinted, _totalBurned };

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenSupply.compact)
// OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenPublicSupply.compact)

pragma language_version >= 0.23.0;

/**
* @module NativeShieldedTokenSupply
* @module NativeShieldedTokenPublicSupply
* @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
* It is a thin wrapper over `NativeShieldedTokenPublicSupplyCore`, forwarding a
* single fixed key (the paired contract has exactly one token type), so the
* accounting logic and guarantees live in the shared core.
*
Expand Down Expand Up @@ -48,24 +48,24 @@ pragma language_version >= 0.23.0;
*
* @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
* `NativeShieldedTokenPublicSupplyCore` 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`.
* `NativeShieldedTokenPublicSupplyCore`.
*/
module NativeShieldedTokenSupply {
module NativeShieldedTokenPublicSupply {
import CompactStandardLibrary;
import "./NativeShieldedTokenSupplyCore" prefix Core_;
import "./NativeShieldedTokenPublicSupplyCore" 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
// `NativeShieldedTokenPublicSupplyCore__totalMinted`. The circuit names are
// intentionally NOT imported unprefixed, to avoid potential clashes.
import { _totalMinted, _totalBurned } from "./NativeShieldedTokenSupplyCore";
import { _totalMinted, _totalBurned } from "./NativeShieldedTokenPublicSupplyCore";
export { _totalMinted, _totalBurned };

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenSupplyCore.compact)
// OpenZeppelin Compact Contracts v0.2.0 (token/extensions/NativeShieldedTokenPublicSupplyCore.compact)

pragma language_version >= 0.23.0;

/**
* @module NativeShieldedTokenSupplyCore
* @module NativeShieldedTokenPublicSupplyCore
* @description Shared, domain-keyed core of the optional supply-accounting
* extension. It owns the per-domain minted/burned maps and exposes the
* `_addMinted` / `_addBurned` building blocks plus the `totalMinted` /
Expand All @@ -13,12 +13,12 @@ pragma language_version >= 0.23.0;
* This module is NOT meant to be composed directly. It is the de-duplicated
* base behind the two user-facing flavors:
*
* - `NativeShieldedTokenSupply` — pairs with `NativeShieldedToken`; forwards a
* - `NativeShieldedTokenPublicSupply` — pairs with `NativeShieldedToken`; forwards a
* single fixed key (the contract has one token type).
* - `NativeShieldedTokenFamilySupply` — pairs with `NativeShieldedTokenFamily`;
* - `NativeShieldedTokenFamilyPublicSupply` — pairs with `NativeShieldedTokenFamily`;
* forwards the caller's per-call `domain`.
*
* @dev Assertion messages use the generic `NativeShieldedTokenSupply:` prefix
* @dev Assertion messages use the generic `NativeShieldedTokenPublicSupply:` prefix
* so they read sensibly from either flavor. There is no initialization: an
* absent domain key reads as 0, and the getters do not gate on the paired token
* module's init flag (a counter reading 0 before init is correct), so init
Expand Down Expand Up @@ -51,7 +51,7 @@ pragma language_version >= 0.23.0;
* overstates circulating supply.
* Call the matching accounting block on every mint and burn path, same domain.
*/
module NativeShieldedTokenSupplyCore {
module NativeShieldedTokenPublicSupplyCore {
import CompactStandardLibrary;
import "../../utils/Utils" prefix Utils_;

Expand Down Expand Up @@ -79,7 +79,7 @@ module NativeShieldedTokenSupplyCore {
*/
export circuit _addMinted(domain: Bytes<32>, amount: Uint<64>): [] {
const current = totalMinted(domain);
assert(Utils_UINT128_MAX() - current >= amount, "NativeShieldedTokenSupply: arithmetic overflow");
assert(Utils_UINT128_MAX() - current >= amount, "NativeShieldedTokenPublicSupply: arithmetic overflow");
_totalMinted.insert(disclose(domain), disclose((current + amount) as Uint<128>));
}

Expand All @@ -103,8 +103,8 @@ module NativeShieldedTokenSupplyCore {
// First guards the subtraction below from underflowing on an already-corrupt
// state (stored burned past minted); the second is the reachable per-call
// check that this burn does not push the domain past its minted total.
assert(minted >= current, "NativeShieldedTokenSupply: burned total exceeds minted");
assert(minted - current >= amount, "NativeShieldedTokenSupply: burn exceeds available supply");
assert(minted >= current, "NativeShieldedTokenPublicSupply: burned total exceeds minted");
assert(minted - current >= amount, "NativeShieldedTokenPublicSupply: burn exceeds available supply");
_totalBurned.insert(disclose(domain), disclose((current + amount) as Uint<128>));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { NativeShieldedTokenFamilySupplySimulator } from '../simulators/NativeShieldedTokenFamilySupplySimulator.js';
import { NativeShieldedTokenFamilyPublicSupplySimulator } from '../simulators/NativeShieldedTokenFamilyPublicSupplySimulator.js';

const b32 = (label: string): Uint8Array => {
const u = new Uint8Array(32);
Expand All @@ -13,11 +13,11 @@ const DOMAIN_B = b32('domain-B');
// The per-domain supply extension imports no token module, so it is unit-tested
// by driving its accounting blocks and getters directly.

let supply: NativeShieldedTokenFamilySupplySimulator;
let supply: NativeShieldedTokenFamilyPublicSupplySimulator;

describe('NativeShieldedTokenFamilySupply (extension)', () => {
describe('NativeShieldedTokenFamilyPublicSupply (extension)', () => {
beforeEach(async () => {
supply = await NativeShieldedTokenFamilySupplySimulator.create();
supply = await NativeShieldedTokenFamilyPublicSupplySimulator.create();
});

describe('initial state', () => {
Expand Down Expand Up @@ -65,14 +65,14 @@ describe('NativeShieldedTokenFamilySupply (extension)', () => {
await supply._addMinted(DOMAIN_A, 1_000n);
// DOMAIN_B was never minted; burning under it must revert.
await expect(supply._addBurned(DOMAIN_B, 1n)).rejects.toThrow(
'NativeShieldedTokenSupply: burn exceeds available supply',
'NativeShieldedTokenPublicSupply: burn exceeds available supply',
);
});

it('should revert a burn that exceeds the domain minted total', async () => {
await supply._addMinted(DOMAIN_A, 1_000n);
await expect(supply._addBurned(DOMAIN_A, 1_001n)).rejects.toThrow(
'NativeShieldedTokenSupply: burn exceeds available supply',
'NativeShieldedTokenPublicSupply: burn exceeds available supply',
);
});

Expand All @@ -82,7 +82,7 @@ describe('NativeShieldedTokenFamilySupply (extension)', () => {
// available-supply check) is what fires.
await supply.unsafeSetBurned(DOMAIN_A, 1_500n);
await expect(supply._addBurned(DOMAIN_A, 1n)).rejects.toThrow(
'NativeShieldedTokenSupply: burned total exceeds minted',
'NativeShieldedTokenPublicSupply: burned total exceeds minted',
);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import fc from 'fast-check';
import { describe, expect, it } from 'vitest';
import { NativeShieldedTokenFamilySupplySimulator } from '../simulators/NativeShieldedTokenFamilySupplySimulator.js';
import { NativeShieldedTokenSupplySimulator } from '../simulators/NativeShieldedTokenSupplySimulator.js';
import { NativeShieldedTokenFamilyPublicSupplySimulator } from '../simulators/NativeShieldedTokenFamilyPublicSupplySimulator.js';
import { NativeShieldedTokenPublicSupplySimulator } from '../simulators/NativeShieldedTokenPublicSupplySimulator.js';

// Property-based invariant fuzzing over random mint/burn op sequences for the
// supply extension. Both flavors are covered in one file: the scalar
// `NativeShieldedTokenSupply` and the per-domain `NativeShieldedTokenFamilySupply`.
// Both delegate to the shared `NativeShieldedTokenSupplyCore`, so the core
// `NativeShieldedTokenPublicSupply` and the per-domain `NativeShieldedTokenFamilyPublicSupply`.
// Both delegate to the shared `NativeShieldedTokenPublicSupplyCore`, so the core
// accounting is exercised transitively through each.
//
// Conservation (burned <= minted) is respected by construction: each op burns
Expand All @@ -24,13 +24,14 @@ const opArb = fc.record({
burnPct: fc.integer({ min: 0, max: 100 }),
});

describe('NativeShieldedTokenSupply(Core/Family) — property: supply invariants under random op sequences', () => {
describe('NativeShieldedTokenPublicSupply(Core/Family) — property: supply invariants under random op sequences', () => {
it('should keep totalMinted exact, totalSupply = minted - burned, and burned <= minted (scalar)', async () => {
await fc.assert(
fc.asyncProperty(
fc.array(opArb, { minLength: 1, maxLength: 6 }),
async (ops) => {
const supply = await NativeShieldedTokenSupplySimulator.create();
const supply =
await NativeShieldedTokenPublicSupplySimulator.create();
let expectedMinted = 0n;
let expectedBurned = 0n;

Expand Down Expand Up @@ -71,7 +72,7 @@ describe('NativeShieldedTokenSupply(Core/Family) — property: supply invariants
),
async (ops) => {
const supply =
await NativeShieldedTokenFamilySupplySimulator.create();
await NativeShieldedTokenFamilyPublicSupplySimulator.create();
const expectedMinted = DOMAINS.map(() => 0n);
const expectedBurned = DOMAINS.map(() => 0n);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { NativeShieldedTokenSupplySimulator } from '../simulators/NativeShieldedTokenSupplySimulator.js';
import { NativeShieldedTokenPublicSupplySimulator } from '../simulators/NativeShieldedTokenPublicSupplySimulator.js';

// The supply extension imports no token module, so it is unit-tested by driving
// its accounting blocks (`_addMinted` / `_addBurned`) and getters directly.

let supply: NativeShieldedTokenSupplySimulator;
let supply: NativeShieldedTokenPublicSupplySimulator;

describe('NativeShieldedTokenSupply (extension)', () => {
describe('NativeShieldedTokenPublicSupply (extension)', () => {
beforeEach(async () => {
supply = await NativeShieldedTokenSupplySimulator.create();
supply = await NativeShieldedTokenPublicSupplySimulator.create();
});

describe('initial state', () => {
Expand Down Expand Up @@ -37,22 +37,22 @@ describe('NativeShieldedTokenSupply (extension)', () => {

it('should revert an unpaired burn (burned exceeds minted from zero)', async () => {
await expect(supply._addBurned(1n)).rejects.toThrow(
'NativeShieldedTokenSupply: burn exceeds available supply',
'NativeShieldedTokenPublicSupply: burn exceeds available supply',
);
});

it('should revert a burn that exceeds the minted total', async () => {
await supply._addMinted(1_000n);
await expect(supply._addBurned(1_001n)).rejects.toThrow(
'NativeShieldedTokenSupply: burn exceeds available supply',
'NativeShieldedTokenPublicSupply: burn exceeds available supply',
);
});

it('should revert a duplicate burn that pushes past minted', async () => {
await supply._addMinted(1_000n);
await supply._addBurned(600n);
await expect(supply._addBurned(600n)).rejects.toThrow(
'NativeShieldedTokenSupply: burn exceeds available supply',
'NativeShieldedTokenPublicSupply: burn exceeds available supply',
);
// The rejected burn left the total unchanged.
expect(await supply.totalBurned()).toBe(600n);
Expand All @@ -64,7 +64,7 @@ describe('NativeShieldedTokenSupply (extension)', () => {
// first guard (not the available-supply check) is what fires.
await supply.unsafeSetBurned(1_500n);
await expect(supply._addBurned(1n)).rejects.toThrow(
'NativeShieldedTokenSupply: burned total exceeds minted',
'NativeShieldedTokenPublicSupply: burned total exceeds minted',
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,37 @@ pragma language_version >= 0.23.0;

import CompactStandardLibrary;

import "../../extensions/NativeShieldedTokenFamilySupply" prefix NativeShieldedTokenFamilySupply_;
import "../../extensions/NativeShieldedTokenFamilyPublicSupply" prefix NativeShieldedTokenFamilyPublicSupply_;

export { ZswapCoinPublicKey, ContractAddress, Either, Maybe };

// Surface the core supply ledger state unprefixed so tests can read it via
// `getPublicState()` and assert the per-domain accounting getters return it verbatim.
import { _totalMinted, _totalBurned } from "../../extensions/NativeShieldedTokenFamilySupply";
import { _totalMinted, _totalBurned } from "../../extensions/NativeShieldedTokenFamilyPublicSupply";
export { _totalMinted, _totalBurned };

// Unit mock for the NativeShieldedTokenFamilySupply extension in isolation. The
// Unit mock for the NativeShieldedTokenFamilyPublicSupply extension in isolation. The
// extension imports no token module, so the per-domain accounting blocks and
// getters are driven directly here. The extension needs no initialization.

export circuit _addMinted(domain: Bytes<32>, amount: Uint<64>): [] {
NativeShieldedTokenFamilySupply__addMinted(domain, amount);
NativeShieldedTokenFamilyPublicSupply__addMinted(domain, amount);
}

export circuit _addBurned(domain: Bytes<32>, amount: Uint<128>): [] {
NativeShieldedTokenFamilySupply__addBurned(domain, amount);
NativeShieldedTokenFamilyPublicSupply__addBurned(domain, amount);
}

export circuit totalMinted(domain: Bytes<32>): Uint<128> {
return NativeShieldedTokenFamilySupply_totalMinted(domain);
return NativeShieldedTokenFamilyPublicSupply_totalMinted(domain);
}

export circuit totalBurned(domain: Bytes<32>): Uint<128> {
return NativeShieldedTokenFamilySupply_totalBurned(domain);
return NativeShieldedTokenFamilyPublicSupply_totalBurned(domain);
}

export circuit totalSupply(domain: Bytes<32>): Uint<128> {
return NativeShieldedTokenFamilySupply_totalSupply(domain);
return NativeShieldedTokenFamilyPublicSupply_totalSupply(domain);
}

// Test-only: force `_totalBurned(domain)` past `_totalMinted(domain)` to reach
Expand Down
Loading