Skip to content

refactor(token): CFT supply extension and integration composition - #9

Closed
0xisk wants to merge 6 commits into
andrew-fleming:add-cft-2from
OpenZeppelin:refactor/cft-supply-preset
Closed

refactor(token): CFT supply extension and integration composition#9
0xisk wants to merge 6 commits into
andrew-fleming:add-cft-2from
OpenZeppelin:refactor/cft-supply-preset

Conversation

@0xisk

@0xisk 0xisk commented Jul 20, 2026

Copy link
Copy Markdown

Types of changes

What types of changes does your code introduce to OpenZeppelin Midnight Contracts?
Put an x in the boxes that apply

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Update (if none of the other choices apply)

Fixes: N/A. Layout proposal for OpenZeppelin#653 (Confidential Fungible Token, tracking OpenZeppelin#569), targeting add-cft-2 so it can merge into that PR directly.

What this proposes

Restructure the CFT layout so the shipped library is one token module plus one optional supply extension, with the assembled token living in the integration test suite:

src/token/ConfidentialFungibleToken.compact                   (renamed from ConfidentialFungibleTokenCore;
                                                               gains _mint/_burn/_burnFrom building blocks)
src/token/extensions/ConfidentialFungibleTokenSupply.compact  (supply tracker: _totalSupply,
                                                               _addSupply/_subSupply, totalSupply())
test/integration/_mocks/ConfidentialFungibleTokenPublicSupply.compact
                                                              (TEST-ONLY top-level contract composing both)
test/integration/fixtures/confidentialFungibleTokenPublicSupply.ts
test/integration/specs/confidentialFungibleToken/*.spec.ts    (7 case specs)

Commit by commit:

  1. refactor(token): drop Core suffix from the CFT module — the NST family keeps a Core because two wrappers share it; CFT has one flavor, so the module consumers import is just ConfidentialFungibleToken.
  2. feat(token): add _mint/_burn/_burnFrom building blocks — thin intent-named aliases over _credit/_debit/_spendEscrow (which transfer/_move keep using internally). No supply accounting; a composing contract gates them and pairs them with the tracker.
  3. feat(token): add ConfidentialFungibleTokenSupply extension — mirrors NativeShieldedTokenSupply: imports no token module, exposes the scalar accounting blocks plus the getter; the original error strings are preserved.
  4. refactor(test): compose CFT public supply in the integration suite — the assembled token (former ConfidentialFungibleTokenPublicSupply module) integrates two production pieces, so it moves out of src/ and becomes a top-level TEST-ONLY contract driven directly through its artifact; the unit mock is deleted and the simulator becomes an integration fixture with shared helpers.
  5. test(integration): add the CFT composition spec suite — the former single test file split into per-case specs (metadata, mint, burn, burnFrom, supplyConservation), following the specs/<topic>/<case>.spec.ts layout, with new coverage for the conservation invariant (sum(balances) == totalSupply: only paired mint/burn/burnFrom may move the total; transfer, approve/transferFrom, _move, and sweep never do).
  6. test(integration): add CFT privacy and concurrency specs
    • privacy.spec asserts observer-side properties from the public ledger only: balances/pending/escrows stay ciphertexts, the approve cap never appears in clear, equal amounts are unlinkable, transfer reveals the counterparty graph but no amount, mint/burn disclose amounts through the supply delta by design, and hostile wallets (wrong EK, overstated plaintext or allowance) fail the witness-binding checks.
    • concurrency.spec documents the same-block limitation from the module header by proving its cause deterministically: with the witness seed pinned, an identical credit replayed on a changed recipient pre-state yields a different transcript (two transactions proven against one pre-state conflict), credits to distinct recipients touch disjoint cells, and sweep contests the same pending cell an incoming credit writes.

Testing notes

  • Every touched contract compiles (compact compile --skip-zk, artifacts verified), tsc --noEmit and biome ci are clean.
  • The spec suite runs under yarn test:integration.
  • Note: moving the composed suite out of src/** removes it from the live-test matrix (the unit-live project only globs src/**/*.test.ts).

PR Checklist

Further comments

  • Why supply-as-extension: the base stays supply-neutral (conserving surface + intent-named blocks), matching the NST precedent (extensions/NativeShieldedTokenSupply) and leaving room for a confidential-supply sibling (ElGamal cell + auditor viewing key) without touching the base.
  • Why the assembled token is test-only: an ungated mint is a faucet, so shipping it as an importable "batteries-included" module invites unsafe deployments. As an integration mock the DANGER surface disappears, and the composition still gets a full spec. Whether the library should also ship a gated deployable preset (e.g. mint behind Ownable) is worth discussing.

0xisk added 6 commits July 20, 2026 15:02
Rename ConfidentialFungibleTokenCore to ConfidentialFungibleToken: it
is the token module consumers import, not an internal shared core (the
NST family keeps a Core because two wrappers share it; CFT has one).
Mock, simulator, and test files follow the same rename.
Expose the supply-changing halves under their intent names: _mint
delegates to _credit, _burn to _debit, _burnFrom to _spendEscrow. They
perform no supply accounting; a composing contract pairs them with the
supply tracker extension when it wants a tracked totalSupply, and gates
them per its issuance policy. Docs updated to frame the module as
supply-neutral rather than supply-free.
Standalone public supply tracker mirroring NativeShieldedTokenSupply:
an _totalSupply cell with _addSupply/_subSupply building blocks and the
totalSupply getter, importing no token module. A consuming contract
calls the accounting block alongside the matching token op (_mint,
_burn, _burnFrom); the assembled pairing ships as the
ConfidentialFungibleTokenPublicSupply preset.
The assembled token (module surface + supply accounting) is a
composition of two production pieces, so it moves out of src/ and into
the integration suite as a top-level TEST-ONLY contract
(test/integration/_mocks/ConfidentialFungibleTokenPublicSupply). It
pairs every supply-changing op with its accounting block: mint =
_addSupply + _mint, burn = _burn + _subSupply, burnFrom = _burnFrom +
_subSupply.

The unit mock is superseded by driving the composed artifact directly;
the simulator moves to the integration fixtures with shared spec
helpers (users, deploy, funding) added.
Split the former single supply test file into per-case specs under
specs/confidentialFungibleToken/ (metadata, mint, burn, burnFrom,
supplyConservation), following the specs/<topic>/<case>.spec.ts layout.

New coverage beyond the original suite:

* supply conservation across transfer, approve/transferFrom, _move,
  and sweep (only paired mint/burn/burnFrom may move totalSupply)
* multi-user end-to-end mint -> transfer -> sweep -> burn flow
* escrow burn-down to zero and the exhaustion failure
* burnFrom with no escrow
* burn pushes no memo (debits are memo-less)
* registration and metadata wiring smoke tests
privacy.spec observes only the public ledger: balances, pending, and
escrows stay ciphertexts; the approve cap never appears in clear;
transfer leaks the counterparty graph (memo growth) but no amount;
mint/burn disclose their amounts through the totalSupply delta by
design; hostile wallets (wrong EK, overstated plaintext or allowance)
fail the witness-binding checks.

concurrency.spec documents the same-block limitation from the module
header by proving its cause deterministically: with the witness seed
pinned, an identical credit replayed on top of a changed recipient
pre-state yields a different transcript (so two same-pre-state
transactions conflict), credits to distinct recipients touch disjoint
cells (no conflict), and sweep contests the same pending cell an
incoming credit writes.
@0xisk

0xisk commented Jul 20, 2026

Copy link
Copy Markdown
Author

Superseded by #10, which keeps the refactor plus the composed integration contract and defers the spec suite to a follow-up PR.

@0xisk 0xisk closed this Jul 20, 2026
@0xisk
0xisk deleted the refactor/cft-supply-preset branch July 20, 2026 14:09
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