From 5c85c497ed0b7b26da76cd0ad3115105c0324b16 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 25 Oct 2026 13:19:19 -0300 Subject: [PATCH 01/10] bump to 0.3.0 --- content/contracts-compact/access-control.mdx | 48 +- .../contracts-compact/api/access-control.mdx | 94 +- .../api/confidential-fungible-token.mdx | 668 +++++++ content/contracts-compact/api/crypto.mdx | 445 +++++ .../contracts-compact/api/fungible-token.mdx | 73 +- content/contracts-compact/api/multi-token.mdx | 70 +- content/contracts-compact/api/multisig.mdx | 1120 +++++++++++ .../api/native-shielded-token.mdx | 1716 +++++++++++++++++ .../api/non-fungible-token.mdx | 138 +- content/contracts-compact/api/ownable.mdx | 183 +- content/contracts-compact/api/security.mdx | 211 +- .../api/shielded-access-control.mdx | 203 +- content/contracts-compact/api/utils.mdx | 104 +- .../confidential-fungible-token.mdx | 407 ++++ content/contracts-compact/crypto.mdx | 261 +++ content/contracts-compact/extensibility.mdx | 10 +- content/contracts-compact/fungible-token.mdx | 46 + content/contracts-compact/index.mdx | 11 +- content/contracts-compact/latest-version.js | 2 +- content/contracts-compact/multi-token.mdx | 45 + content/contracts-compact/multisig.mdx | 355 ++++ .../native-shielded-token.mdx | 374 ++++ .../contracts-compact/non-fungible-token.mdx | 46 + content/contracts-compact/ownable.mdx | 93 +- content/contracts-compact/security.mdx | 191 +- .../shielded-access-control.mdx | 55 +- content/contracts-compact/utils.mdx | 89 +- content/contracts-compact/utils/constants.js | 4 +- src/navigation/midnight.json | 40 + 29 files changed, 6604 insertions(+), 498 deletions(-) create mode 100644 content/contracts-compact/api/confidential-fungible-token.mdx create mode 100644 content/contracts-compact/api/crypto.mdx create mode 100644 content/contracts-compact/api/multisig.mdx create mode 100644 content/contracts-compact/api/native-shielded-token.mdx create mode 100644 content/contracts-compact/confidential-fungible-token.mdx create mode 100644 content/contracts-compact/crypto.mdx create mode 100644 content/contracts-compact/multisig.mdx create mode 100644 content/contracts-compact/native-shielded-token.mdx diff --git a/content/contracts-compact/access-control.mdx b/content/contracts-compact/access-control.mdx index 846282ee..d80d342c 100644 --- a/content/contracts-compact/access-control.mdx +++ b/content/contracts-compact/access-control.mdx @@ -8,17 +8,18 @@ title: AccessControl [FungibleToken]: ./fungible-token.mdx [Ownable]: ./ownable.mdx -[Initializable]: ./security#initializable +[Utils]: ./utils.mdx [AccessControl]: api/access-control [ShieldedAccessControl]: ./shielded-access-control.mdx [assertOnlyRole]: api/access-control#AccessControl-assertOnlyRole +[hasRole]: api/access-control#AccessControl-hasRole [grantRole]: api/access-control#AccessControl-grantRole [_grantRole]: api/access-control#AccessControl-_grantRole [_unsafeGrantRole]: api/access-control#AccessControl-_unsafeGrantRole [revokeRole]: api/access-control#AccessControl-revokeRole [_setRoleAdmin]: api/access-control#AccessControl-_setRoleAdmin -[computeAccountId]: api/access-control#AccessControl-computeAccountId +[computeAccountId]: api/utils#Utils-computeAccountId This module provides a role-based access control mechanism, where roles can be used to represent a set of permissions providing the flexibility to create different levels of account authorization. @@ -50,13 +51,36 @@ some users are regular users, some may be supervisors or managers, and a few wil AccessControl uses a witness-derived identity scheme. Each caller proves knowledge of a secret key by injecting it via the `wit_AccessControlSK` witness. The module computes an account identifier -as `persistentHash(secretKey)`, which is a commitment that hides the key while providing a stable, +as `H(secretKey)` (a `persistentHash` of the key), which is a commitment that hides the key while providing a stable, pseudonymous on-chain identity. -The same secret key produces the same identity across all contracts (analogous to Solidity's `msg.sender`). +What is public and what is private: + +* **On the ledger**: `_operatorRoles`, a public map from role identifier to account identifier to a Boolean, + and `_adminRoles`. Every grant, revoke, and role check discloses the role identifier and the account identifier it touches, + so an observer can see which identifiers hold which roles. +* **Held privately by each account**: the 32-byte `secretKey`, supplied through `wit_AccessControlSK` at proving time. + The key never reaches the ledger; only `H(secretKey)` does. + +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + +The same secret key produces the same identity across all contracts. Users who desire cross-contract unlinkability can use different secret keys per contract at the wallet layer. -To derive an account identifier off-chain (e.g. before passing it to a grant operation), use [computeAccountId]. +To derive an account identifier off-chain (e.g. before passing it to a grant operation), +use [computeAccountId] from the [Utils] module: + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +const accountId = Utils_computeAccountId(secretKey); +``` + +`computeAccountId` is a pure circuit, so it can be evaluated off-chain (for example through a contract simulator), +or recomputed in TypeScript as a `persistentHash` over a `Vector<1, Bytes<32>>` containing the key +(see the [Ownable] guide for a runtime snippet). ### Using `AccessControl` @@ -164,7 +188,7 @@ an internal circuit that is useful when programmatically assigning roles (such a But what if we later want to grant the 'minter' role to additional accounts? By default, **accounts with a role cannot grant it or revoke it from other accounts**: -all having a role does is making the `hasRole` check pass. +all having a role does is making the [hasRole] check pass. To grant and revoke roles dynamically, you will need help from the _role's admin_. Every role has an associated admin role, @@ -212,7 +236,7 @@ constructor( BURNER_ROLE = persistentHash>(pad(32, "BURNER_ROLE")); // Grant the default admin role to the provided account identifier. // This account will be able to grant and revoke any roles. - // The account identifier should be derived off-chain using computeAccountId(secretKey). + // The account identifier should be derived off-chain using Utils_computeAccountId(secretKey). AccessControl__grantRole(AccessControl_DEFAULT_ADMIN_ROLE(), defaultAdmin); } @@ -232,8 +256,10 @@ Note that, unlike the previous examples, no accounts are granted the 'minter' or However, because those roles' admin role is the default admin role, and _that_ role was granted to `defaultAdmin`, that same account can call [grantRole] to give minting or burning permission, and [revokeRole] to remove it. -The `defaultAdmin` account identifier should be derived off-chain using `AccessControl_computeAccountId(secretKey)`, +The `defaultAdmin` account identifier should be derived off-chain using [computeAccountId] from the [Utils] module +(`Utils_computeAccountId(secretKey)`), where `secretKey` is a 32-byte cryptographically secure random value held by the deployer. +Pass it as `left, ContractAddress>(accountId)`. Dynamic role allocation is often a desirable property, for example in systems where trust in a participant may vary over time. @@ -241,11 +267,11 @@ It can also be used to support use cases such as KYC, where the list of role-bearers may not be known up-front, or may be prohibitively expensive to include in a single transaction. -### Experimental features +### Unsafe circuits -This module offers an experimental circuit that allows access control permissions to be granted to contract addresses [_unsafeGrantRole]. +This module offers a circuit that allows access control permissions to be granted to contract addresses, [_unsafeGrantRole]. -Note that the circuit name is very explicit ("unsafe") with this experimental circuit. +Note that the circuit name is very explicit ("unsafe"). Until contract-to-contract calls are supported, there is no direct way for a contract to call permissioned circuits of other contracts or grant/revoke role permissions. diff --git a/content/contracts-compact/api/access-control.mdx b/content/contracts-compact/api/access-control.mdx index 1686289b..d44359c6 100644 --- a/content/contracts-compact/api/access-control.mdx +++ b/content/contracts-compact/api/access-control.mdx @@ -49,7 +49,7 @@ More complex role relationships can be created by using [_setRoleAdmin](#AccessC ## AccessControl [toc] [#AccessControl] ```ts @@ -69,6 +69,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; id="AccessControl-_operatorRoles" > Mapping from a role identifier -> account -> its permissions. + Account keys are stored canonicalized (the inactive branch of the `Either` is zeroed). #### _adminRoles [toc] [#AccessControl-_adminRoles] @@ -84,6 +85,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; ### Witnesses [toc] [#AccessControl-Witnesses] ### Witnesses [!toc] [#AccessControl-Witnesses] +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + #### wit_AccessControlSK [toc] [#AccessControl-wit_AccessControlSK] #### [!toc] [#AccessControl-wit_AccessControlSK] - Returns the caller's secret key used in deriving the account identifier. + Returns the caller's secret key used in deriving the account identifier `H(secretKey)`. + The key never reaches the ledger; only the derived identifier is used as a map key. The same key produces the same account identifier across all contracts. Users who desire cross-contract unlinkability should use different keys per contract. @@ -104,7 +109,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; #### [!toc] [#AccessControl-DEFAULT_ADMIN_ROLE] The default admin role for all roles. Returns zero bytes (`default>`). @@ -121,19 +126,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; modulePrefix="AccessControl" > Returns `true` if `account` has been granted `roleId`. - + `account` is canonicalized before the lookup. -#### _hasRole [toc] [#AccessControl-_hasRole] -#### [!toc] [#AccessControl-_hasRole] - - Returns `true` if `account` has been granted `roleId`. - Internal variant that assumes `account` has already been canonicalized by the caller. - External consumers should use `hasRole` which canonicalizes the input automatically. + Discloses: `roleId` and the canonicalized `account` (map lookup keys). #### assertOnlyRole [toc] [#AccessControl-assertOnlyRole] @@ -145,7 +140,10 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; modulePrefix="AccessControl" > Reverts if the caller cannot prove ownership of `roleId`. - The caller's identity is derived from the `wit_AccessControlSK` witness as `persistentHash(secretKey)`. + The caller's identity is derived from the `wit_AccessControlSK` witness as `H(secretKey)`, + the same derivation as [`Utils.computeAccountId`](./utils#Utils-computeAccountId). + + Discloses: `roleId` and the caller's account identifier (map lookup keys). Requirements: @@ -157,11 +155,13 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; Reverts if `account` is missing `roleId`. + Discloses: `roleId` and the canonicalized `account` (map lookup keys). + Requirements: * `account` must have `roleId`. @@ -179,6 +179,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; See [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole). To change a role's admin use [_setRoleAdmin](#AccessControl-_setRoleAdmin). + + Discloses: `roleId`. #### grantRole [toc] [#AccessControl-grantRole] @@ -196,6 +198,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; This restriction prevents permanently disabling access to a circuit. + Discloses: `roleId`, the caller's account identifier, and the canonicalized `account`, which is written into `_operatorRoles`. + Requirements: * `account` must not be a ContractAddress. @@ -212,6 +216,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; > Revokes `roleId` from `account`. + Discloses: `roleId`, the caller's account identifier, and the canonicalized `account`. + Requirements: * The caller must have `roleId`'s admin role. @@ -233,7 +239,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; if they are compromised (such as when a trusted device is misplaced). The caller's identity is derived from the `wit_AccessControlSK` witness as - `persistentHash(secretKey)`. The `callerConfirmation` parameter must match + `H(secretKey)`. The `callerConfirmation` parameter must match the caller's computed account identifier to prevent accidental renunciation. @@ -241,6 +247,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; We may revisit this in future if this feature is made available in Compact. + Discloses: `roleId` and `callerConfirmation`. + Requirements: * The caller's computed account identifier must match `callerConfirmation`. @@ -256,6 +264,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; modulePrefix="AccessControl" > Sets `adminRole` as `roleId`'s admin role. + Internal circuit without access restriction. + + Discloses: `roleId` and `adminRole`, written to `_adminRoles`. #### _grantRole [toc] [#AccessControl-_grantRole] @@ -266,7 +277,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; complexity="k=11, rows=1143" modulePrefix="AccessControl" > - Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. + Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted + (`false` when `account` already holds it). Internal circuit without access restriction. @@ -275,6 +287,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; This restriction prevents permanently disabling access to a circuit. + Discloses: `roleId` and the canonicalized `account`, written into `_operatorRoles`. + Requirements: * `account` must not be a ContractAddress. @@ -289,12 +303,15 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; modulePrefix="AccessControl" > Unsafe variant of [_grantRole](#AccessControl-_grantRole). + It does not check whether `account` is a ContractAddress. Granting roles to contract addresses is considered unsafe because contract-to-contract calls are not currently supported. Granting a role to a smart contract may render a circuit permanently inaccessible. Once contract-to-contract calls are supported, this circuit may be deprecated. + + Discloses: `roleId` and the canonicalized `account`, written into `_operatorRoles`. #### _revokeRole [toc] [#AccessControl-_revokeRole] @@ -305,43 +322,10 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; complexity="k=11, rows=1073" modulePrefix="AccessControl" > - Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. + Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked + (`false` when `account` did not hold it). Internal circuit without access restriction. - - -#### _computeAccountId [toc] [#AccessControl-_computeAccountId] -#### [!toc] [#AccessControl-_computeAccountId] - - Computes the caller's account identifier from the `wit_AccessControlSK` witness. - - ID Derivation: `accountId = persistentHash(secretKey)` - - The result is a 32-byte commitment that uniquely identifies the caller. - -#### computeAccountId [toc] [#AccessControl-computeAccountId] -#### [!toc] [#AccessControl-computeAccountId] - - Computes an account identifier without on-chain state, allowing a user to derive - their identity commitment before submitting it in a grant or revoke operation. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. - - ID Derivation: `accountId = persistentHash(secretKey)` - - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the privacy guarantees of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss. - A lost key cannot be recovered. - + Discloses: `roleId` and the canonicalized `account`. diff --git a/content/contracts-compact/api/confidential-fungible-token.mdx b/content/contracts-compact/api/confidential-fungible-token.mdx new file mode 100644 index 00000000..6924632a --- /dev/null +++ b/content/contracts-compact/api/confidential-fungible-token.mdx @@ -0,0 +1,668 @@ +--- +title: ConfidentialFungibleToken API +--- + +This module provides the full ConfidentialFungibleToken module API. + + + For an overview of the module, read the [ConfidentialFungibleToken guide](../confidential-fungible-token). + The `ElGamal_Ciphertext` and `EcdhMask_Ciphertext` types used below are documented in the [Crypto API](./crypto). + + +The page covers the following modules: + +- [ConfidentialFungibleToken](#ConfidentialFungibleToken): the token with ElGamal-encrypted balances. +- [ConfidentialFungibleTokenPublicSupply](#ConfidentialFungibleTokenPublicSupply): an extension that tracks a public total supply. + +## ConfidentialFungibleToken [toc] [#ConfidentialFungibleToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibleToken"; +``` + +--- + +### Types [toc] [#ConfidentialFungibleToken-Types] +### Types [!toc] [#ConfidentialFungibleToken-Types] + +#### EscrowEntry [toc] [#ConfidentialFungibleToken-EscrowEntry] +#### [!toc] [#ConfidentialFungibleToken-EscrowEntry] + + The encrypted allowance an owner granted a spender. `spenderCt` and `ownerCt` are two + [ElGamal ciphertexts](./crypto#ElGamal-Ciphertext) of the same remaining amount, one under each party's key. + `ownerMemo` is an [EcdhMask ciphertext](./crypto#EcdhMask-Ciphertext) that lets the owner read the current + remaining allowance directly with its encryption secret; it is set to the cap on `approve` and refreshed to the + reduced remainder on every spend. There is no public cap. + + +### Ledger [toc] [#ConfidentialFungibleToken-Ledger] +### Ledger [!toc] [#ConfidentialFungibleToken-Ledger] + +#### _balances [toc] [#ConfidentialFungibleToken-_balances] +#### [!toc] [#ConfidentialFungibleToken-_balances] + + Mapping from account identifiers to their spendable balance ciphertext. Debits draw from this cell. + + +#### _pending [toc] [#ConfidentialFungibleToken-_pending] +#### [!toc] [#ConfidentialFungibleToken-_pending] + + Mapping from account identifiers to their pending balance ciphertext, where incoming credits land. + Only the owner moves pending value into `_balances`, with [sweep](#ConfidentialFungibleToken-sweep). + + +#### _encryptionKeys [toc] [#ConfidentialFungibleToken-_encryptionKeys] +#### [!toc] [#ConfidentialFungibleToken-_encryptionKeys] + + Mapping from account identifiers to their ElGamal public key, set by [register](#ConfidentialFungibleToken-register). + Membership in this map is what "registered" means throughout the module. + + +#### _memos [toc] [#ConfidentialFungibleToken-_memos] +#### [!toc] [#ConfidentialFungibleToken-_memos] + + Mapping from account identifiers to their memo list. Every credit pushes one memo carrying the credited amount, + encrypted to the recipient's key, to the front of the list. Memos record credits only; debits leave none. + + +#### _escrow [toc] [#ConfidentialFungibleToken-_escrow] +#### [!toc] [#ConfidentialFungibleToken-_escrow] + + Mapping from owner accounts to spender accounts to their [EscrowEntry](#ConfidentialFungibleToken-EscrowEntry). + + +#### _escrowSpendEpochs [toc] [#ConfidentialFungibleToken-_escrowSpendEpochs] +#### [!toc] [#ConfidentialFungibleToken-_escrowSpendEpochs] + + Mapping from owner accounts to spender accounts to the number of times that escrow has been spent from. + The counter is folded into the randomness of each spend and never resets, so the per-pair spend count is + public and permanent. + + +#### _creditEpochs [toc] [#ConfidentialFungibleToken-_creditEpochs] +#### [!toc] [#ConfidentialFungibleToken-_creditEpochs] + + Mapping from account identifiers to the lifetime number of credits received. The counter is folded into the + randomness of each credit and never resets (it is not cleared by [clearMemos](#ConfidentialFungibleToken-clearMemos)), + so an account's credit count is public and permanent. + + +#### _isInitialized [toc] [#ConfidentialFungibleToken-_isInitialized] +#### [!toc] [#ConfidentialFungibleToken-_isInitialized] + + Boolean indicating if initialized. + + +#### _name [toc] [#ConfidentialFungibleToken-_name] +#### [!toc] [#ConfidentialFungibleToken-_name] + + The immutable token name. + + +#### _symbol [toc] [#ConfidentialFungibleToken-_symbol] +#### [!toc] [#ConfidentialFungibleToken-_symbol] + + The immutable token symbol. + + +#### _decimals [toc] [#ConfidentialFungibleToken-_decimals] +#### [!toc] [#ConfidentialFungibleToken-_decimals] + + The immutable token decimals. + + +### Witnesses [toc] [#ConfidentialFungibleToken-Witnesses] +### Witnesses [!toc] [#ConfidentialFungibleToken-Witnesses] + +The TypeScript witnesses in the repository are test-only and are not published; a consumer authors its own. + +#### wit_ConfidentialTokenSK [toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenSK] +#### [!toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenSK] + + Returns the caller's account secret. Verified in-circuit: `accountId = H(SK)` is used for every ledger lookup, + so a wrong secret resolves to a different account and the call fails. + + The same secret produces the same account identifier across all contracts. + + +#### wit_ConfidentialTokenEK [toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenEK] +#### [!toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenEK] + + Returns the caller's ElGamal encryption secret. Verified in-circuit by re-deriving the public key and comparing it + against the key stored at registration. May share key material with `wit_ConfidentialTokenSK`; the two derivations + are domain-separated. + + +#### wit_PlaintextBalance [toc] [#ConfidentialFungibleToken-wit_PlaintextBalance] +#### [!toc] [#ConfidentialFungibleToken-wit_PlaintextBalance] + + Returns the wallet's cached plaintext for `ct`. The ciphertext is passed explicitly so the wallet can route by + ciphertext identity rather than by call order. Verified in-circuit by `ElGamal_assertDecryptsTo`; a wrong value + fails the proof. The claim is `Uint<248>` so a balance accumulated past the per-operation bound stays spendable. + + +#### wit_RandomnessSeed [toc] [#ConfidentialFungibleToken-wit_RandomnessSeed] +#### [!toc] [#ConfidentialFungibleToken-wit_RandomnessSeed] + + Returns the 32-byte seed from which all per-operation randomness and every memo ephemeral is derived in-circuit. + Not verified in-circuit. + + + The seed must be fresh, high-entropy, and secret on every invocation. The derivation is deterministic and public, + so anyone who knows a seed can read the amounts it masked, and reusing a seed across two operations leaks the + difference between their amounts. On `transferFrom` and `_burnFrom` the seed is the spender's but the values it + protects are the owner's. + + + +### Circuits [toc] [#ConfidentialFungibleToken-Circuits] +### Circuits [!toc] [#ConfidentialFungibleToken-Circuits] + +#### MAX_TRANSFER_VALUE [toc] [#ConfidentialFungibleToken-MAX_TRANSFER_VALUE] +#### [!toc] [#ConfidentialFungibleToken-MAX_TRANSFER_VALUE] + + Returns the maximum value accepted by a single transfer, mint, burn, or approve: the `Uint<128>` maximum. + + +#### initialize [toc] [#ConfidentialFungibleToken-initialize] +#### [!toc] [#ConfidentialFungibleToken-initialize] + + Initializes the contract by setting the name, symbol, and decimals. + + This MUST be called in the implementing contract's constructor. + Failure to do so can lead to an irreparable contract. + + Requirements: + + * Contract is not initialized. + + +#### balanceOf [toc] [#ConfidentialFungibleToken-balanceOf] +#### [!toc] [#ConfidentialFungibleToken-balanceOf] + + Returns `account`'s spendable balance ciphertext, or a well-formed encryption of zero if the account is unregistered. + The amount is encrypted; only the account holder can decrypt it. + + Requirements: + + * Contract is initialized. + + +#### pendingOf [toc] [#ConfidentialFungibleToken-pendingOf] +#### [!toc] [#ConfidentialFungibleToken-pendingOf] + + Returns `account`'s pending balance ciphertext (incoming credits not yet swept), or a well-formed encryption of zero + if the account is unregistered. An account's total is `balanceOf` plus `pendingOf`. + + Requirements: + + * Contract is initialized. + + +#### allowance [toc] [#ConfidentialFungibleToken-allowance] +#### [!toc] [#ConfidentialFungibleToken-allowance] + + Returns the [EscrowEntry](#ConfidentialFungibleToken-EscrowEntry) that `owner` granted `spender`. + When no allowance exists, returns a well-formed entry whose ciphertexts encrypt zero, so the result is always safe + to prove against and to decrypt. "No allowance" and "an allowance of zero" read the same; a caller that must + distinguish them reads `_escrow` membership off chain. The getter grants nothing: the spend path gates on escrow + membership independently. + + +#### isRegistered [toc] [#ConfidentialFungibleToken-isRegistered] +#### [!toc] [#ConfidentialFungibleToken-isRegistered] + + Returns whether `account` has registered an encryption key. + + +#### register [toc] [#ConfidentialFungibleToken-register] +#### [!toc] [#ConfidentialFungibleToken-register] + + Registers the caller: derives their ElGamal public key from `wit_ConfidentialTokenEK`, stores it, and initializes + their spendable and pending balances to an encryption of zero. Registration is a prerequisite for sending or receiving. + Discloses the caller's `accountId` and public key. + + Returns the caller's `accountId`, so a composing contract can gate on who may register. + + Requirements: + + * Contract is initialized. + * The caller is not already registered. + + +#### _mint [toc] [#ConfidentialFungibleToken-_mint] +#### [!toc] [#ConfidentialFungibleToken-_mint] + + Credits `value` to `account`: homomorphically adds it to the account's pending ciphertext, pushes a memo carrying + `value` encrypted to the recipient's key, and increments the recipient's credit counter. Performs no supply accounting; + pair it with a supply tracker such as [_addSupply](#ConfidentialFungibleTokenPublicSupply-_addSupply). + Discloses `account`, the new pending ciphertext, and the memo; the amount is not disclosed. + + + A building block for a gated `mint` wrapper. Never re-export it from a deployed contract: a raw `_mint` is a + permissionless mint. + + + Requirements: + + * Contract is initialized. + * `value` is at most `MAX_TRANSFER_VALUE()`. + * `account` is not the zero account. + * `account` is registered. + + +#### _burn [toc] [#ConfidentialFungibleToken-_burn] +#### [!toc] [#ConfidentialFungibleToken-_burn] + + Debits `value` from the caller's spendable balance: the caller proves the balance plaintext, and the ciphertext is + replaced by a homomorphic subtraction of `value`. Performs no supply accounting; pair it with + [_subSupply](#ConfidentialFungibleTokenPublicSupply-_subSupply). Discloses the caller's `accountId` and the new + balance ciphertext; the amount is not disclosed. + + Returns the caller's `accountId` for caller-side gating. + + + A building block for a `burn` wrapper. Never re-export it raw from a deployed contract. + + + Requirements: + + * Contract is initialized. + * `value` is at most `MAX_TRANSFER_VALUE()`. + * The caller is registered and holds the encryption secret registered for their account. + * The caller's spendable balance is at least `value`. + + +#### transfer [toc] [#ConfidentialFungibleToken-transfer] +#### [!toc] [#ConfidentialFungibleToken-transfer] + + Moves `value` from the caller to `to`: debits the caller's spendable balance, credits `to`'s pending balance, and + pushes a memo to `to`. Discloses the (sender, recipient) pair, both new ciphertexts, and the memo; the amount is not + disclosed. + + Returns the caller's (sender's) `accountId` for caller-side gating. + + Requirements: + + * Contract is initialized. + * `value` is at most `MAX_TRANSFER_VALUE()`. + * The caller is registered, holds their encryption secret, and has a spendable balance of at least `value`. + * `to` is not the caller. + * `to` is not the zero account and is registered. + + +#### _move [toc] [#ConfidentialFungibleToken-_move] +#### [!toc] [#ConfidentialFungibleToken-_move] + + The conserving value-movement primitive: debits the caller and credits `to` by the same `value`, net zero, and never + changes supply. Equivalent to [transfer](#ConfidentialFungibleToken-transfer) without the self-transfer guard, so a + self-move (spendable into the caller's own pending) is permitted; add a `to != caller` check in the wrapper to reject it. + Discloses the same values as `transfer`. + + Returns the caller's `accountId` for caller-side gating. + + Requirements: + + * Contract is initialized. + * `value` is at most `MAX_TRANSFER_VALUE()`. + * The caller is registered, holds their encryption secret, and has a spendable balance of at least `value`. + * `to` is not the zero account and is registered. + + +#### sweep [toc] [#ConfidentialFungibleToken-sweep] +#### [!toc] [#ConfidentialFungibleToken-sweep] + + Moves the caller's pending balance into their spendable balance: homomorphically adds the two ciphertexts and resets + pending to an encryption of zero. No plaintext is needed. This is the only path from pending into spendable, and only + the owner can take it. Discloses the caller's `accountId` and the two new ciphertexts. + + Returns the caller's `accountId` for caller-side gating. + + Requirements: + + * Contract is initialized. + * The caller is registered. + * The caller holds the encryption secret registered for their account. + + +#### approve [toc] [#ConfidentialFungibleToken-approve] +#### [!toc] [#ConfidentialFungibleToken-approve] + + Sets `spender`'s allowance over the caller's balance to `value` via an escrow: any prior escrow for `spender` is + first refunded to the caller's spendable balance (a homomorphic add, no plaintext needed), then the caller's balance + is debited by `value` and an [EscrowEntry](#ConfidentialFungibleToken-EscrowEntry) holding two encryptions of `value` + plus an owner-readable memo of the remaining allowance is stored. Allowances replace rather than stack; `value` of + zero revokes. Discloses the (owner, spender) pair, the owner's new balance ciphertext, and the entry; the cap is not + disclosed. + + Returns the caller's (approver's) `accountId` for caller-side gating. + + Requirements: + + * Contract is initialized. + * `value` is at most `MAX_TRANSFER_VALUE()`. + * `spender` is not the zero account, is not the caller, and is registered. + * The caller is registered, holds their encryption secret, and (after any prior escrow is refunded) has a spendable + balance of at least `value`. + + +#### transferFrom [toc] [#ConfidentialFungibleToken-transferFrom] +#### [!toc] [#ConfidentialFungibleToken-transferFrom] + + Spends `value` from the escrow that `fromAddress` granted the caller and credits it to `to`. The caller proves the + plaintext of the spender copy, both escrow copies are reduced by `value`, the entry's `ownerMemo` is refreshed with the + new remaining amount, the (owner, spender) spend counter is incremented, and `to` is credited with a memo. Discloses + the owner, spender, and recipient ids, the new escrow entry, the recipient's new pending ciphertext, and the memo; + the amount is not disclosed. + + Returns the caller's (spender's) `accountId` for caller-side gating. + + Requirements: + + * Contract is initialized. + * `value` is at most `MAX_TRANSFER_VALUE()`. + * `to` is not the zero account and is registered. + * An escrow from `fromAddress` to the caller exists, the caller holds their encryption secret, and the escrow holds + at least `value`. + + +#### clearMemos [toc] [#ConfidentialFungibleToken-clearMemos] +#### [!toc] [#ConfidentialFungibleToken-clearMemos] + + Empties the caller's memo list. `expectedEpoch` is the caller's view of its `_creditEpochs` value when it read the + list; the call is rejected if a credit has landed since, so a memo the caller has not seen is never discarded. + + + Destructive. Spending requires proving the balance plaintext, which a wallet reconstructs from its memos or its + cache. Clear only memos already folded into a durable, backed-up cache; otherwise the funds become unspendable. + + + Requirements: + + * Contract is initialized. + * The caller is registered. + * The caller holds the encryption secret registered for their account. + * The caller has a memo list. + * `expectedEpoch` equals the caller's current credit epoch. + + +#### computeAccountId [toc] [#ConfidentialFungibleToken-computeAccountId] +#### [!toc] [#ConfidentialFungibleToken-computeAccountId] + + Derives the public `accountId` from a secret key as `H(sk)`, so a wallet can compute its own account identifier + off chain before transacting. Delegates to `Utils_computeAccountId`; the identifier is global, and the same key + material yields the same id in every module that derives it. + + + The `sk` parameter is a sensitive secret. Never log or persist it in plaintext, generate it from cryptographically + secure randomness, and treat key loss as identity loss. + + + +#### _burnFrom [toc] [#ConfidentialFungibleToken-_burnFrom] +#### [!toc] [#ConfidentialFungibleToken-_burnFrom] + + Consumes `value` from the escrow that `fromAddress` granted the caller, with no recipient credit: the same escrow + spend as [transferFrom](#ConfidentialFungibleToken-transferFrom) (both copies reduced, `ownerMemo` refreshed, spend + counter incremented) and nothing else. Performs no supply accounting; pair it with + [_subSupply](#ConfidentialFungibleTokenPublicSupply-_subSupply). Discloses the owner and spender ids and the new + escrow entry; the amount is not disclosed. + + Returns the caller's (spender's) `accountId` for caller-side gating. + + + A building block for a `burnFrom` wrapper. Never re-export it raw from a deployed contract: an approved spender + could destroy escrowed value with no supply decrement. + + + Requirements: + + * Contract is initialized. + * `value` is at most `MAX_TRANSFER_VALUE()`. + * An escrow from `fromAddress` to the caller exists, the caller holds their encryption secret, and the escrow holds + at least `value`. + + +--- + +## ConfidentialFungibleTokenPublicSupply [toc] [#ConfidentialFungibleTokenPublicSupply] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/ConfidentialFungibleTokenPublicSupply"; +``` + +Optional standalone extension that adds public supply accounting to a ConfidentialFungibleToken. It imports no token +module and needs no initialization. "Public" supply means the total, and therefore each mint and burn amount, is +disclosed on chain through the delta; balances stay confidential. The composing contract pairs each accounting block +with the matching token operation. + +--- + +### Ledger [toc] [#ConfidentialFungibleTokenPublicSupply-Ledger] +### Ledger [!toc] [#ConfidentialFungibleTokenPublicSupply-Ledger] + +#### _totalSupply [toc] [#ConfidentialFungibleTokenPublicSupply-_totalSupply] +#### [!toc] [#ConfidentialFungibleTokenPublicSupply-_totalSupply] + + The public circulating supply. + + +### Witnesses [toc] [#ConfidentialFungibleTokenPublicSupply-Witnesses] +### Witnesses [!toc] [#ConfidentialFungibleTokenPublicSupply-Witnesses] + +None. + +### Circuits [toc] [#ConfidentialFungibleTokenPublicSupply-Circuits] +### Circuits [!toc] [#ConfidentialFungibleTokenPublicSupply-Circuits] + +#### _addSupply [toc] [#ConfidentialFungibleTokenPublicSupply-_addSupply] +#### [!toc] [#ConfidentialFungibleTokenPublicSupply-_addSupply] + + Increases the total supply by `value`. Call alongside the token module's + [_mint](#ConfidentialFungibleToken-_mint). The added amount is disclosed through the supply delta. + + Requirements: + + * `_totalSupply + value` does not overflow `Uint<128>`. + + +#### _subSupply [toc] [#ConfidentialFungibleTokenPublicSupply-_subSupply] +#### [!toc] [#ConfidentialFungibleTokenPublicSupply-_subSupply] + + Decreases the total supply by `value`. Call alongside the token module's + [_burn](#ConfidentialFungibleToken-_burn) or [_burnFrom](#ConfidentialFungibleToken-_burnFrom). + The removed amount is disclosed through the supply delta. + + Requirements: + + * `_totalSupply` is at least `value`. + + +#### totalSupply [toc] [#ConfidentialFungibleTokenPublicSupply-totalSupply] +#### [!toc] [#ConfidentialFungibleTokenPublicSupply-totalSupply] + + Returns the public total token supply. + diff --git a/content/contracts-compact/api/crypto.mdx b/content/contracts-compact/api/crypto.mdx new file mode 100644 index 00000000..3ec32444 --- /dev/null +++ b/content/contracts-compact/api/crypto.mdx @@ -0,0 +1,445 @@ +--- +title: Crypto API +--- + +This page provides the full API for the Crypto modules. + + + For an overview of the modules, read the [Crypto guide](../crypto). + + +The page covers the following modules: + +- [ElGamal](#ElGamal): exponential ElGamal encryption over Jubjub with homomorphic addition and re-randomization. +- [EcdhMask](#EcdhMask): an ECDH-derived mask for encrypting a value to a recipient's public key. + +## ElGamal [toc] [#ElGamal] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/crypto/ElGamal"; +``` + +--- + +### Types [toc] [#ElGamal-Types] +### Types [!toc] [#ElGamal-Types] + +#### Ciphertext [toc] [#ElGamal-Ciphertext] +#### [!toc] [#ElGamal-Ciphertext] + + A single-receiver ElGamal ciphertext over Jubjub: `c1 = g^r` and `c2 = pk^r * m`. + In the lifted form the message point is `m = g^v` for a plaintext value `v`. + + +### Ledger [toc] [#ElGamal-Ledger] +### Ledger [!toc] [#ElGamal-Ledger] + +None. + +### Witnesses [toc] [#ElGamal-Witnesses] +### Witnesses [!toc] [#ElGamal-Witnesses] + +None. + +### Circuits [toc] [#ElGamal-Circuits] +### Circuits [!toc] [#ElGamal-Circuits] + +#### secretToScalar [toc] [#ElGamal-secretToScalar] +#### [!toc] [#ElGamal-secretToScalar] + + Maps a 32-byte secret to a valid Jubjub scalar, computed as + `degradeToTransient(H("ElGamal:secretToScalar" || secret))` with the tag padded to 32 bytes. + The output is always below the Jubjub subgroup order, so it is safe to pass to `ecMulGenerator` and `ecMul`. + The fixed tag keeps this derivation distinct from both `expandRandomness` and any account identifier computed as `H(secret)`. + + +#### derivePk [toc] [#ElGamal-derivePk] +#### [!toc] [#ElGamal-derivePk] + + Derives the public key `pk = g^secretToScalar(ek)` from the 32-byte encryption secret `ek`. + + +#### expandRandomness [toc] [#ElGamal-expandRandomness] +#### [!toc] [#ElGamal-expandRandomness] + + Deterministically expands a 32-byte seed into a Jubjub scalar tagged by a distinguishing string, + computed as `degradeToTransient(H(seed || tag))`. + One secret seed can yield many distinct randomness values within a single circuit invocation, one per tag. + Distinct tags always produce distinct outputs; the caller is responsible for using distinct tags + and for supplying a fresh, secret seed on every invocation. + + +#### encryptZero [toc] [#ElGamal-encryptZero] +#### [!toc] [#ElGamal-encryptZero] + + Returns the identity ciphertext `Enc(0)`, where both components are the curve identity point. + Useful for initializing a fresh slot. + + This is a fixed, deterministic value, not a hiding encryption: a slot still holding it is publicly recognizable as never written. + Rerandomize it, or credit it with fresh randomness, before treating it as private. + + +#### encryptPoint [toc] [#ElGamal-encryptPoint] +#### [!toc] [#ElGamal-encryptPoint] + + Encrypts an arbitrary message point `m` under `pk` with randomness `r`, producing `(g^r, pk^r * m)`. + This is the general (non-lifted) encryption; `encrypt` is the lifted special case with `m = g^value`. + Every encryption path in the module goes through this circuit, so its requirements apply to all of them. + + `m` and `pk` are constrained into the prime-order subgroup by the runtime, so no in-circuit membership check is performed. + Decrypting `m` is only feasible when it is a known or small-exponent point; the caller owns that concern. + `r` must be a valid Jubjub scalar (below the subgroup order), secret, and fresh per ciphertext; + the circuit cannot check any of these. + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### encrypt [toc] [#ElGamal-encrypt] +#### [!toc] [#ElGamal-encrypt] + + Produces a fresh ciphertext of `value` under `pk` with randomness `r`, lifting `value` to the point `g^value`: + the result is `(g^r, pk^r * g^value)`. + The caller is responsible for keeping `value` within a range a discrete-log search can recover. + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### negate [toc] [#ElGamal-negate] +#### [!toc] [#ElGamal-negate] + + Negates a ciphertext componentwise: if `ct` encrypts `v`, the result encrypts `-v` under the same key + (with negated randomness). Each point is negated by scalar multiplication with the subgroup order minus one. + This is the building block `sub` rests on. + + +#### add [toc] [#ElGamal-add] +#### [!toc] [#ElGamal-add] + + Homomorphically adds two ciphertexts componentwise: `Enc(a) + Enc(b)` decrypts to `a + b`. + The randomness of the result is the sum of the inputs' randomness, so the result is a valid ciphertext under the same key. + + Both inputs must be encrypted under the same public key; combining ciphertexts under different keys yields + a ciphertext no single key can open. + + +#### sub [toc] [#ElGamal-sub] +#### [!toc] [#ElGamal-sub] + + Homomorphically subtracts two ciphertexts: `Enc(a) - Enc(b)` decrypts to `a - b`. Implemented as `add(a, negate(b))`. + + Both inputs must be encrypted under the same public key. + The caller must ensure the underlying plaintext subtraction does not underflow; this circuit cannot check it. + + +#### scalarMul [toc] [#ElGamal-scalarMul] +#### [!toc] [#ElGamal-scalarMul] + + Homomorphically multiplies the plaintext of `ct` by a public scalar `k`: if `ct` encrypts `v`, + the result encrypts `k * v` under the same key. Both components are scaled, so the randomness becomes `k * r`. + Together with `add` this expresses linear combinations, for example `Enc(3a + 5b)` as `add(scalarMul(ca, 3), scalarMul(cb, 5))`. + + `k` must be a valid Jubjub scalar (below the subgroup order); larger values fault `ecMul`. + The caller remains responsible for keeping `k * v` within the discrete-log bound. + + +#### addEncrypted [toc] [#ElGamal-addEncrypted] +#### [!toc] [#ElGamal-addEncrypted] + + Homomorphically adds `value` to the plaintext encrypted by `old`, producing a fresh ciphertext under `pk` + with new randomness `r`. Equivalent to `add(old, encrypt(pk, value, r))`. + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### subEncrypted [toc] [#ElGamal-subEncrypted] +#### [!toc] [#ElGamal-subEncrypted] + + Homomorphically subtracts `value` from the plaintext encrypted by `old`, producing a fresh ciphertext under `pk` + with new randomness `r`. Equivalent to `sub(old, encrypt(pk, value, r))`. + + The caller must ensure the underlying plaintext subtraction does not underflow; this circuit cannot check it. + Callers verify sufficiency by reading the plaintext through their own witness and asserting it with + `assertDecryptsTo` before calling. + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### rerandomize [toc] [#ElGamal-rerandomize] +#### [!toc] [#ElGamal-rerandomize] + + Returns a fresh ciphertext of the same plaintext as `ct` under the same key `pk`, using new randomness `r`. + The output is unlinkable to the input yet decrypts identically. Equivalent to homomorphically adding an encryption of zero. + + A `pk` that is not the key `ct` was encrypted under silently corrupts the plaintext; the circuit cannot detect this. + Pass fresh randomness and the matching `pk`. + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### assertKeyPair [toc] [#ElGamal-assertKeyPair] +#### [!toc] [#ElGamal-assertKeyPair] + + Asserts that `ek` is the secret for `pk`, i.e. `derivePk(ek) == pk`. + Lets a caller prove ownership of a public key without touching any ciphertext or plaintext. + + Requirements: + + * `derivePk(ek) == pk`. + + +#### assertDecryptsToPoint [toc] [#ElGamal-assertDecryptsToPoint] +#### [!toc] [#ElGamal-assertDecryptsToPoint] + + Asserts that `ek` is the secret for `pk` and that `ct` decrypts under `(pk, ek)` to the message point `m`, + computed as `c2 - ek * c1`. This is the general (non-lifted) verification; + `assertDecryptsTo` is the lifted special case with `m = g^claimedValue`. + + Requirements: + + * `ek` is the secret for `pk`. + * `ct` decrypts under `(pk, ek)` to `m`. + + +#### assertDecryptsTo [toc] [#ElGamal-assertDecryptsTo] +#### [!toc] [#ElGamal-assertDecryptsTo] + + Asserts that `ek` is the secret for `pk` and that `ct` decrypts under `(pk, ek)` to `claimedValue`. + This is the in-circuit verification path an importing contract uses to bind its witness-supplied secret and cached plaintext + to publicly stored state: a wrong `ek` fails the key check, a wrong plaintext fails the decryption check. + + `claimedValue` is the widest `Uint` Compact offers, so this parameter never bounds a caller; a narrower value declared + at the call site costs proportionally less. The claim domain must stay below the Jubjub subgroup order, since two claims + congruent modulo that order are indistinguishable; `Uint<248>` satisfies this. + + Requirements: + + * `ek` is the secret for `pk`. + * `ct` decrypts under `(pk, ek)` to `claimedValue`. + + +--- + +## EcdhMask [toc] [#EcdhMask] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/crypto/EcdhMask"; +``` + +--- + +### Types [toc] [#EcdhMask-Types] +### Types [!toc] [#EcdhMask-Types] + +#### Ciphertext [toc] [#EcdhMask-Ciphertext] +#### [!toc] [#EcdhMask-Ciphertext] + + An ECDH one-time-pad ciphertext: the ephemeral public key `g^e` and the field-masked value `value + kdf(pk^e, domain)`. + + +### Ledger [toc] [#EcdhMask-Ledger] +### Ledger [!toc] [#EcdhMask-Ledger] + +None. + +### Witnesses [toc] [#EcdhMask-Witnesses] +### Witnesses [!toc] [#EcdhMask-Witnesses] + +None. + +### Circuits [toc] [#EcdhMask-Circuits] +### Circuits [!toc] [#EcdhMask-Circuits] + +#### kdf [toc] [#EcdhMask-kdf] +#### [!toc] [#EcdhMask-kdf] + + Derives the field mask from the ECDH shared secret point, domain-separated by the caller-supplied `domain` tag: + `degradeToTransient(H(H(sShared) || domain))`, where `H` is `persistentHash`. + `sShared` is `pk^e` for the sender and `ephemeralPk^ek` for the recipient, which are equal by construction. + The consumer chooses `domain` and must use the same value on encrypt and decrypt. + + +#### encrypt [toc] [#EcdhMask-encrypt] +#### [!toc] [#EcdhMask-encrypt] + + Encrypts `value` to `recipientPk` under the ephemeral scalar `e`, returning + `{ ephemeralPk: g^e, ct: value + kdf(recipientPk^e, domain) }`. + Any `Uint<128>` value is accepted; there is no discrete-log bound. + + `e` is a one-time pad key: it must be a valid Jubjub scalar (below the subgroup order), secret, + and fresh for every ciphertext. Reusing `e` to the same recipient repeats the mask and exposes both values; + a predictable `e` lets anyone recompute the mask from the public `recipientPk`. + The pad is not authenticated. + + Requirements: + + * `recipientPk` is not the identity point. + * The ephemeral point `g^e` is not the identity (this rejects `e = 0`). + + +#### decrypt [toc] [#EcdhMask-decrypt] +#### [!toc] [#EcdhMask-decrypt] + + Recovers the value from a ciphertext using the recipient's secret scalar: computes `sShared = ephemeralPk^ekScalar` + and returns `ct - kdf(sShared, domain)`. The result equals the original `value` as a `Field`. + `ekScalar` is the scalar form of the recipient's secret, as produced by ElGamal's `secretToScalar`, + and `domain` must match the tag used on encrypt. + + Intended for wallets and off-chain code: reading a received value needs no proof. + diff --git a/content/contracts-compact/api/fungible-token.mdx b/content/contracts-compact/api/fungible-token.mdx index b458cd81..3739bb5b 100644 --- a/content/contracts-compact/api/fungible-token.mdx +++ b/content/contracts-compact/api/fungible-token.mdx @@ -11,7 +11,7 @@ This module provides the full FungibleToken module API. ## FungibleToken [toc] [#FungibleToken] ```ts @@ -23,6 +23,17 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; ### Ledger [toc] [#FungibleToken-Ledger] ### Ledger [!toc] [#FungibleToken-Ledger] +#### _isInitialized [toc] [#FungibleToken-_isInitialized] +#### [!toc] [#FungibleToken-_isInitialized] + + Initialization flag, owned by this module. Set to `true` by [initialize](#FungibleToken-initialize); + every other circuit asserts it is set. + + #### _balances [toc] [#FungibleToken-_balances] #### [!toc] [#FungibleToken-_balances] Returns the caller's secret key used in deriving the account identifier. + The key stays private; only `persistentHash(secretKey)` reaches the ledger. The same key produces the same account identifier across all contracts. Users who desire cross-contract unlinkability should use different keys per contract. @@ -102,17 +114,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; ### Circuits [toc] [#FungibleToken-Circuits] ### Circuits [!toc] [#FungibleToken-Circuits] -#### ZERO [toc] [#FungibleToken-ZERO] -#### [!toc] [#FungibleToken-ZERO] - - Returns a canonical zero `Either` value for `Bytes<32>` and `ContractAddress`. - Returns the left variant (`Bytes<32>`) to avoid misleading contract-to-contract error messages. - - #### initialize [toc] [#FungibleToken-initialize] #### [!toc] [#FungibleToken-initialize] - Initializes the contract by setting the name, symbol, and decimals. + Initializes the contract by setting the name, symbol, and decimals, and marks the module initialized. This MUST be called in the implementing contract's constructor. Failure to do so can lead to an irreparable contract. @@ -220,6 +221,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" > Moves a `value` amount of tokens from the caller's account to `to`. + The caller's identity is derived from the `wit_FungibleTokenSK` witness as `persistentHash(secretKey)`. Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. @@ -268,6 +270,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; > Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` through [transferFrom](#FungibleToken-transferFrom). This value changes when [approve](#FungibleToken-approve) or [transferFrom](#FungibleToken-transferFrom) are called. + Returns zero when no allowance entry exists. Requirements: @@ -283,6 +286,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" > Sets a `value` amount of tokens as allowance of `spender` over the caller's tokens. + The caller's identity is derived from the `wit_FungibleTokenSK` witness as `persistentHash(secretKey)`. Requirements: @@ -305,6 +309,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; > Moves `value` tokens from `fromAddress` to `to` using the allowance mechanism. `value` is then deducted from the caller's allowance. + The caller's identity is derived from the `wit_FungibleTokenSK` witness as `persistentHash(secretKey)`. Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. @@ -533,10 +538,14 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Updates `owner`'s allowance for `spender` based on spent `value`. Does not update the allowance value in case of infinite allowance. + A missing allowance entry is treated as a zero allowance, mirroring [allowance](#FungibleToken-allowance). + A zero `value` is a no-op: no allowance is read and no entry is written. + Whether `value` is zero is disclosed. + Requirements: * Contract is initialized. - * `spender` must have at least an allowance of `value` from `owner`. + * If `value` is non-zero, `spender` must have an allowance of at least `value` from `owner`. #### _computeAccountId [toc] [#FungibleToken-_computeAccountId] @@ -546,41 +555,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; kind="internal" modulePrefix="FungibleToken" > - Computes the caller's account identifier from the `wit_FungibleTokenSK` witness. + Computes the caller's account identifier from the `wit_FungibleTokenSK` witness + by delegating to [Utils computeAccountId](./utils#Utils-computeAccountId). ID Derivation: `accountId = persistentHash(secretKey)` The result is a 32-byte commitment that uniquely identifies the caller. - - -#### computeAccountId [toc] [#FungibleToken-computeAccountId] -#### [!toc] [#FungibleToken-computeAccountId] - - Computes an account identifier without on-chain state, allowing a user to derive - their identity commitment before submitting a token operation. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. - - ID Derivation: `accountId = persistentHash(secretKey)` - - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the security of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss. - A lost key cannot be recovered. - - - -#### _isTargetZero [toc] [#FungibleToken-_isTargetZero] -#### [!toc] [#FungibleToken-_isTargetZero] - - Returns `true` if `target`'s active branch (as indicated by `is_left`) holds the zero value. + The same derivation is available off-chain through `Utils_computeAccountId`. diff --git a/content/contracts-compact/api/multi-token.mdx b/content/contracts-compact/api/multi-token.mdx index 5444b95b..48ab1ef3 100644 --- a/content/contracts-compact/api/multi-token.mdx +++ b/content/contracts-compact/api/multi-token.mdx @@ -11,11 +11,11 @@ This module provides the full MultiToken module API. ## MultiToken [toc] [#MultiToken] ```ts -import "./node_modules/@openzeppelin/token/src/MultiToken"; +import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; ``` --- @@ -23,6 +23,17 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; ### Ledger [toc] [#MultiToken-Ledger] ### Ledger [!toc] [#MultiToken-Ledger] +#### _isInitialized [toc] [#MultiToken-_isInitialized] +#### [!toc] [#MultiToken-_isInitialized] + + Initialization flag, owned by this module. Set to `true` by [initialize](#MultiToken-initialize); + every other circuit asserts it is set. + + #### _balances [toc] [#MultiToken-_balances] #### [!toc] [#MultiToken-_balances] Returns the caller's secret key used in deriving the account identifier. + The key stays private; only `persistentHash(secretKey)` reaches the ledger. The same key produces the same account identifier across all contracts. Users who desire cross-contract unlinkability should use different keys per contract. @@ -72,18 +84,6 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; ### Circuits [toc] [#MultiToken-Circuits] ### Circuits [!toc] [#MultiToken-Circuits] -#### ZERO [toc] [#MultiToken-ZERO] -#### [!toc] [#MultiToken-ZERO] - - Returns a canonical zero `Either` value (left variant with zero `Bytes<32>`). - Used as the zero value for mint/burn operations in `_update`, where a zero - `fromAddress` signals a mint and a zero `to` signals a burn. - - #### initialize [toc] [#MultiToken-initialize] #### [!toc] [#MultiToken-initialize] - Initializes the contract by setting the base URI for all tokens. + Initializes the contract by setting the base URI for all tokens, and marks the module initialized. This MUST be called in the implementing contract's constructor. Failure to do so can lead to an irreparable contract. @@ -132,6 +132,7 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; stackParams="true" > Returns the amount of `id` tokens owned by `account`. + Returns zero when `id` has never been minted or `account` has no entry for it. Requirements: @@ -257,6 +258,10 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; Transfers a value amount of tokens of type `id` from `fromAddress` to `to`. This circuit will mint (or burn) if `fromAddress` (or `to`) is zero. + A zero `value` writes nothing: the sender's balance is still checked, but no balance entry is created or updated, + so a zero-value transfer of an `id` that has never been minted does not revert. + Whether `value` is zero is disclosed. + Requirements: * Contract is initialized. @@ -451,40 +456,11 @@ import "./node_modules/@openzeppelin/token/src/MultiToken"; kind="internal" modulePrefix="MultiToken" > - Computes the caller's account identifier from the `wit_MultiTokenSK` witness. + Computes the caller's account identifier from the `wit_MultiTokenSK` witness + by delegating to [Utils computeAccountId](./utils#Utils-computeAccountId). ID Derivation: `accountId = persistentHash(secretKey)` The result is a 32-byte commitment that uniquely identifies the caller. - - -#### computeAccountId [toc] [#MultiToken-computeAccountId] -#### [!toc] [#MultiToken-computeAccountId] - - Computes an account identifier without on-chain state, allowing a user to derive - their identity commitment before submitting it in a token operation. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. - - ID Derivation: `accountId = persistentHash(secretKey)` - - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the security of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss — a lost key cannot be recovered. - - - -#### _isTargetZero [toc] [#MultiToken-_isTargetZero] -#### [!toc] [#MultiToken-_isTargetZero] - - Returns `true` if `target`'s active branch (as indicated by `is_left`) holds the zero value. + The same derivation is available off-chain through `Utils_computeAccountId`. diff --git a/content/contracts-compact/api/multisig.mdx b/content/contracts-compact/api/multisig.mdx new file mode 100644 index 00000000..4126ac40 --- /dev/null +++ b/content/contracts-compact/api/multisig.mdx @@ -0,0 +1,1120 @@ +--- +title: Multisig API +--- + +This page provides the full API for the Multisig modules. + + + For an overview of the modules, read the [Multisig guide](../multisig). + + +The page covers the following modules: + +- [Signer](#Signer): the registry of who may approve and how many approvals are required, generic over the signer identity type. +- [ProposalManager](#ProposalManager): the lifecycle of transfer proposals, independent of the token kind. +- [ShieldedTreasury](#ShieldedTreasury): holds one shielded coin per token color on the ledger and sends from it. +- [ShieldedTreasuryStateless](#ShieldedTreasuryStateless): receives and sends shielded coins without keeping any ledger state. +- [UnshieldedTreasury](#UnshieldedTreasury): holds unshielded tokens with a tracked per-color balance. +- [ForwarderPrivate](#ForwarderPrivate): forwards shielded deposits to a parent stored as a commitment. +- [ForwarderShielded](#ForwarderShielded): forwards shielded deposits to a public parent in the same transaction. +- [ForwarderUnshielded](#ForwarderUnshielded): forwards unshielded deposits to a public parent in the same transaction. + +## Signer [toc] [#Signer] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/Signer">; +``` + +`Signer` is generic over the signer identity type `T`, chosen at import time (for example `Bytes<32>`, `JubjubPoint`, or `Either`). +The module never resolves the caller; the consuming contract passes an identity in. + +--- + +### Ledger [toc] [#Signer-Ledger] +### Ledger [!toc] [#Signer-Ledger] + +#### _signers [toc] [#Signer-_signers] +#### [!toc] [#Signer-_signers] + + The set of registered signer identities. + + +#### _signerCount [toc] [#Signer-_signerCount] +#### [!toc] [#Signer-_signerCount] + + The number of registered signers (at most 255). + + +#### _threshold [toc] [#Signer-_threshold] +#### [!toc] [#Signer-_threshold] + + The minimum number of approvals required. Zero until it is set. + + +#### _isInitialized [toc] [#Signer-_isInitialized] +#### [!toc] [#Signer-_isInitialized] + + Records whether [initialize](#Signer-initialize) has run. It gates re-entry into `initialize` and nothing else; no other circuit consults it. + + +### Witnesses [toc] [#Signer-Witnesses] +### Witnesses [!toc] [#Signer-Witnesses] + +None. + +### Circuits [toc] [#Signer-Circuits] +### Circuits [!toc] [#Signer-Circuits] + +#### initialize [toc] [#Signer-initialize] +#### [!toc] [#Signer-initialize] + + Registers each entry of `signers` and sets the threshold to `thresh`. + If used, it MUST only be called from the contract's constructor; a contract may instead configure signers through its own gated flow with [_addSigner](#Signer-_addSigner) and [_setThreshold](#Signer-_setThreshold). + The signer identities and the threshold are disclosed to the ledger. + + Requirements: + + * Contract is not initialized. + * `thresh` is not zero. + * `thresh` does not exceed the number of `signers`. + * `signers` contains no duplicates. + + +#### assertSigner [toc] [#Signer-assertSigner] +#### [!toc] [#Signer-assertSigner] + + Asserts that `caller` is a registered signer. Does not check initialization: an empty registry rejects every caller. + `caller` is disclosed as the key of the set lookup. + + Requirements: + + * `caller` is a member of the signer registry. + + +#### assertThresholdMet [toc] [#Signer-assertThresholdMet] +#### [!toc] [#Signer-assertThresholdMet] + + Asserts that `approvalCount` meets the threshold. + The count is supplied by the consuming contract and compared against the threshold only; the registry is never consulted, + so the contract's approval accounting must itself be gated by [assertSigner](#Signer-assertSigner) and count distinct signers. + The count is not bounded by `_signerCount`, so approvals collected before a signer was removed still count. + + Requirements: + + * The threshold is set (not zero). + * `approvalCount` is greater than or equal to the threshold. + + +#### getSignerCount [toc] [#Signer-getSignerCount] +#### [!toc] [#Signer-getSignerCount] + + Returns the number of registered signers. Returns zero when none have been registered. + + +#### getThreshold [toc] [#Signer-getThreshold] +#### [!toc] [#Signer-getThreshold] + + Returns the approval threshold. Returns zero when it has not been set. + + +#### isSigner [toc] [#Signer-isSigner] +#### [!toc] [#Signer-isSigner] + + Returns true if `account` is a registered signer. `account` is disclosed as the key of the set lookup. + + +#### _addSigner [toc] [#Signer-_addSigner] +#### [!toc] [#Signer-_addSigner] + + Adds `signer` to the registry and increments the signer count. `signer` is disclosed. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * `signer` is not already a registered signer. + + +#### _removeSigner [toc] [#Signer-_removeSigner] +#### [!toc] [#Signer-_removeSigner] + + Removes `signer` from the registry and decrements the signer count. `signer` is disclosed. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * `signer` is a registered signer. + * The signer count after removal is greater than or equal to the threshold. + + +#### _changeThreshold [toc] [#Signer-_changeThreshold] +#### [!toc] [#Signer-_changeThreshold] + + Updates the approval threshold, checking it against the current signer count. `newThreshold` is disclosed. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * `newThreshold` is not zero. + * `newThreshold` does not exceed the current signer count. + + +#### _setThreshold [toc] [#Signer-_setThreshold] +#### [!toc] [#Signer-_setThreshold] + + Sets the approval threshold without checking it against the signer count. `newThreshold` is disclosed. + Intended for construction or custom setup flows where signers are not yet registered. + A threshold above the signer count is permitted mid-setup and is not self-enforcing: + the contract must not gate operations on [assertThresholdMet](#Signer-assertThresholdMet) while setup is incomplete. + Use [_changeThreshold](#Signer-_changeThreshold) for operational changes. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * `newThreshold` is not zero. + + +--- + +## ProposalManager [toc] [#ProposalManager] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ProposalManager"; +``` + +--- + +### Types [toc] [#ProposalManager-Types] +### Types [!toc] [#ProposalManager-Types] + +#### ProposalStatus [toc] [#ProposalManager-ProposalStatus] +#### [!toc] [#ProposalManager-ProposalStatus] + + Proposal lifecycle states. `Inactive` is the zero value and is never stored: + [_createProposal](#ProposalManager-_createProposal) writes `Active`, and every view asserts the proposal exists before reading. + + +#### RecipientKind [toc] [#ProposalManager-RecipientKind] +#### [!toc] [#ProposalManager-RecipientKind] + + Says how a recipient's 32 address bytes are to be read: as a Zswap coin public key, a user address, or a contract address. + `Contract` is accepted by both [toShieldedRecipient](#ProposalManager-toShieldedRecipient) and [toUnshieldedRecipient](#ProposalManager-toUnshieldedRecipient), + so the tag alone does not determine the rail. + + +#### Recipient [toc] [#ProposalManager-Recipient] +#### [!toc] [#ProposalManager-Recipient] + + A typed recipient. Construct it with the recipient helper circuits rather than directly. + + +#### Proposal [toc] [#ProposalManager-Proposal] +#### [!toc] [#ProposalManager-Proposal] + + A transfer proposal: recipient, token color, amount, and lifecycle status. + + +### Ledger [toc] [#ProposalManager-Ledger] +### Ledger [!toc] [#ProposalManager-Ledger] + +#### _nextProposalId [toc] [#ProposalManager-_nextProposalId] +#### [!toc] [#ProposalManager-_nextProposalId] + + Counter of proposals created. Its value is the id of the most recent proposal. + + +#### _proposals [toc] [#ProposalManager-_proposals] +#### [!toc] [#ProposalManager-_proposals] + + Mapping from proposal id to proposal. Proposals are public: every recipient address, color, amount, and status is readable off-chain. + + +### Witnesses [toc] [#ProposalManager-Witnesses] +### Witnesses [!toc] [#ProposalManager-Witnesses] + +None. + +### Circuits [toc] [#ProposalManager-Circuits] +### Circuits [!toc] [#ProposalManager-Circuits] + +#### shieldedUserRecipient [toc] [#ProposalManager-shieldedUserRecipient] +#### [!toc] [#ProposalManager-shieldedUserRecipient] + + Constructs a `ShieldedUser` recipient from a coin public key. + + +#### unshieldedUserRecipient [toc] [#ProposalManager-unshieldedUserRecipient] +#### [!toc] [#ProposalManager-unshieldedUserRecipient] + + Constructs an `UnshieldedUser` recipient from a user address. + + +#### contractRecipient [toc] [#ProposalManager-contractRecipient] +#### [!toc] [#ProposalManager-contractRecipient] + + Constructs a `Contract` recipient from a contract address. + + +#### toShieldedRecipient [toc] [#ProposalManager-toShieldedRecipient] +#### [!toc] [#ProposalManager-toShieldedRecipient] + + Converts a recipient into the form a shielded send accepts: `left` for a `ShieldedUser`, `right` for a `Contract`. + + Requirements: + + * `r.kind` is `ShieldedUser` or `Contract`. + + +#### toUnshieldedRecipient [toc] [#ProposalManager-toUnshieldedRecipient] +#### [!toc] [#ProposalManager-toUnshieldedRecipient] + + Converts a recipient into the form an unshielded send accepts: `left` for a `Contract`, `right` for an `UnshieldedUser`. + + Requirements: + + * `r.kind` is `UnshieldedUser` or `Contract`. + + +#### assertProposalExists [toc] [#ProposalManager-assertProposalExists] +#### [!toc] [#ProposalManager-assertProposalExists] + + Asserts that a proposal with `id` has been created. `id` is disclosed. + + Requirements: + + * A proposal with `id` exists. + + +#### assertProposalActive [toc] [#ProposalManager-assertProposalActive] +#### [!toc] [#ProposalManager-assertProposalActive] + + Asserts that a proposal with `id` exists and is `Active`. `id` is disclosed. + + Requirements: + + * A proposal with `id` exists. + * Its status is `Active`. + + +#### _createProposal [toc] [#ProposalManager-_createProposal] +#### [!toc] [#ProposalManager-_createProposal] + + Increments the proposal counter, stores a new `Active` proposal under the new id, and returns the id. + The whole proposal (recipient, color, amount, status) is disclosed to the ledger. + The counter is incremented and read back as the map key, so concurrent creations against the same state conflict. + + The recipient is not validated beyond being well-typed: a kind the execution rail cannot accept creates a proposal that stays `Active` until cancelled, + and an unspendable address is accepted. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * `amount` is greater than zero. + + +#### _cancelProposal [toc] [#ProposalManager-_cancelProposal] +#### [!toc] [#ProposalManager-_cancelProposal] + + Sets the proposal's status to `Cancelled`. `id` is disclosed. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * The proposal exists and is `Active`. + + +#### _markExecuted [toc] [#ProposalManager-_markExecuted] +#### [!toc] [#ProposalManager-_markExecuted] + + Sets the proposal's status to `Executed`. `id` is disclosed. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * The proposal exists and is `Active`. + + +#### getProposal [toc] [#ProposalManager-getProposal] +#### [!toc] [#ProposalManager-getProposal] + + Returns the full proposal. `id` is disclosed. + + Requirements: + + * A proposal with `id` exists. + + +#### getProposalRecipient [toc] [#ProposalManager-getProposalRecipient] +#### [!toc] [#ProposalManager-getProposalRecipient] + + Returns the proposal's recipient. `id` is disclosed. + + Requirements: + + * A proposal with `id` exists. + + +#### getProposalAmount [toc] [#ProposalManager-getProposalAmount] +#### [!toc] [#ProposalManager-getProposalAmount] + + Returns the proposal's amount. `id` is disclosed. + + Requirements: + + * A proposal with `id` exists. + + +#### getProposalColor [toc] [#ProposalManager-getProposalColor] +#### [!toc] [#ProposalManager-getProposalColor] + + Returns the proposal's token color. `id` is disclosed. + + Requirements: + + * A proposal with `id` exists. + + +#### getProposalStatus [toc] [#ProposalManager-getProposalStatus] +#### [!toc] [#ProposalManager-getProposalStatus] + + Returns the proposal's status. `id` is disclosed. + + Requirements: + + * A proposal with `id` exists. + + +--- + +## ShieldedTreasury [toc] [#ShieldedTreasury] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasury"; +``` + +--- + +### Ledger [toc] [#ShieldedTreasury-Ledger] +### Ledger [!toc] [#ShieldedTreasury-Ledger] + +#### _coins [toc] [#ShieldedTreasury-_coins] +#### [!toc] [#ShieldedTreasury-_coins] + + One stored coin per token color. Every coin held (nonce, color, value, and Merkle index) is readable off-chain in plaintext. + + +#### _received [toc] [#ShieldedTreasury-_received] +#### [!toc] [#ShieldedTreasury-_received] + + Cumulative value received per token color. + + +#### _sent [toc] [#ShieldedTreasury-_sent] +#### [!toc] [#ShieldedTreasury-_sent] + + Cumulative value sent per token color. + + +### Witnesses [toc] [#ShieldedTreasury-Witnesses] +### Witnesses [!toc] [#ShieldedTreasury-Witnesses] + +None. + +### Circuits [toc] [#ShieldedTreasury-Circuits] +### Circuits [!toc] [#ShieldedTreasury-Circuits] + +#### _deposit [toc] [#ShieldedTreasury-_deposit] +#### [!toc] [#ShieldedTreasury-_deposit] + + Claims `coin` with `receiveShielded`, then merges it into the stored coin of the same color (`mergeCoinImmediate`) or inserts it when the color is new, + and adds its value to the received total. + The coin and the updated total are disclosed to the ledger. + Zero-value deposits are permitted: on a new color the zero coin occupies that color's slot until a later deposit merges into it. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * The received total for `coin.color` plus `coin.value` does not overflow `Uint<128>`. + + +#### _send [toc] [#ShieldedTreasury-_send] +#### [!toc] [#ShieldedTreasury-_send] + + Looks up the stored coin for `color`, sends `amount` of it to `recipient` with `sendShielded`, and adds `amount` to the sent total. + Any change is routed back to the contract as a self-owned output and retained in `_coins` for the next spend; + when the send leaves no change, the color's entry is removed. + The color, recipient, amount, and retained change coin are disclosed to the ledger. + + The returned `ShieldedSendResult` carries the sent and change coins in plaintext; returning it from an entry point, writing it to the ledger, + or passing it to another contract discloses them. + The change coin belongs to the treasury and must not be spent again by the caller. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * A coin of `color` exists in the treasury. + * The stored coin's value is greater than or equal to `amount`. + * The sent total for `color` plus `amount` does not overflow `Uint<128>`. + + +#### getTokenBalance [toc] [#ShieldedTreasury-getTokenBalance] +#### [!toc] [#ShieldedTreasury-getTokenBalance] + + Returns the value of the stored coin for `color`, or zero when none is held. `color` is disclosed. + + +#### getReceivedTotal [toc] [#ShieldedTreasury-getReceivedTotal] +#### [!toc] [#ShieldedTreasury-getReceivedTotal] + + Returns the cumulative value received for `color`, or zero. `color` is disclosed. + + +#### getSentTotal [toc] [#ShieldedTreasury-getSentTotal] +#### [!toc] [#ShieldedTreasury-getSentTotal] + + Returns the cumulative value sent for `color`, or zero. `color` is disclosed. + + +#### getReceivedMinusSent [toc] [#ShieldedTreasury-getReceivedMinusSent] +#### [!toc] [#ShieldedTreasury-getReceivedMinusSent] + + Returns the received total minus the sent total for `color`, which equals [getTokenBalance](#ShieldedTreasury-getTokenBalance) when all value moved through `_deposit` and `_send`. + The subtraction is unguarded: a consumer that writes `_coins` directly can make it fail as a range violation. + + +--- + +## ShieldedTreasuryStateless [toc] [#ShieldedTreasuryStateless] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasuryStateless"; +``` + +--- + +### Ledger [toc] [#ShieldedTreasuryStateless-Ledger] +### Ledger [!toc] [#ShieldedTreasuryStateless-Ledger] + +None. The module keeps no coin map, balances, or totals; the operator tracks coins off-chain. + +### Witnesses [toc] [#ShieldedTreasuryStateless-Witnesses] +### Witnesses [!toc] [#ShieldedTreasuryStateless-Witnesses] + +None. + +### Circuits [toc] [#ShieldedTreasuryStateless-Circuits] +### Circuits [!toc] [#ShieldedTreasuryStateless-Circuits] + +#### _deposit [toc] [#ShieldedTreasuryStateless-_deposit] +#### [!toc] [#ShieldedTreasuryStateless-_deposit] + + Claims `coin` with `receiveShielded`. Nothing is written to the ledger; the coin is disclosed to the protocol. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + +#### _send [toc] [#ShieldedTreasuryStateless-_send] +#### [!toc] [#ShieldedTreasuryStateless-_send] + + Sends `amount` of the caller-supplied `coin` to `recipient` with `sendShielded` and returns the result untouched. + The coin, recipient, and amount are disclosed. + Any change is routed back to the contract as a self-owned output and returned in `result.change` as a live, spendable coin: + keep it off-chain as the `coin` of a later spend, or route it onward in the same transaction with `sendImmediateShielded`. + The change must never be spent again and still treated as held. + + The returned `ShieldedSendResult` holds the sent and change coins in plaintext. + Consuming it in-circuit is safe; returning it from an entry point, writing it to the ledger, or passing it to another contract publishes those coins, + and the compiler does not require a `disclose` at that point. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * `coin.value` is greater than or equal to `amount` (enforced by `sendShielded`). + + +--- + +## UnshieldedTreasury [toc] [#UnshieldedTreasury] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/UnshieldedTreasury"; +``` + +--- + +### Ledger [toc] [#UnshieldedTreasury-Ledger] +### Ledger [!toc] [#UnshieldedTreasury-Ledger] + +#### _balances [toc] [#UnshieldedTreasury-_balances] +#### [!toc] [#UnshieldedTreasury-_balances] + + Tracked balance per token color. It is the authority for the module's sufficiency and overflow guards and stays in step with the protocol balance only while every credit and debit goes through `_deposit` and `_send`. + + +### Witnesses [toc] [#UnshieldedTreasury-Witnesses] +### Witnesses [!toc] [#UnshieldedTreasury-Witnesses] + +None. + +### Circuits [toc] [#UnshieldedTreasury-Circuits] +### Circuits [!toc] [#UnshieldedTreasury-Circuits] + +#### _deposit [toc] [#UnshieldedTreasury-_deposit] +#### [!toc] [#UnshieldedTreasury-_deposit] + + Receives `amount` of `color` with `receiveUnshielded` and adds it to the tracked balance. + The color, amount, and new balance are disclosed. + Zero-value deposits are permitted and create a zero-valued balance entry on a new color. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * The tracked balance for `color` plus `amount` does not overflow `Uint<128>`. + + +#### _send [toc] [#UnshieldedTreasury-_send] +#### [!toc] [#UnshieldedTreasury-_send] + + Subtracts `amount` from the tracked balance and sends it to `recipient` with `sendUnshielded`. + The color, amount, recipient, and new balance are disclosed. + Zero-amount sends are permitted. + Recipients are not validated: an unspendable recipient permanently removes the funds, + and a send to the contract's own address zeroes the tracked balance while the funds stay held at the protocol level. + + Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + + Requirements: + + * The tracked balance for `color` is greater than or equal to `amount`. + + +#### getTokenBalance [toc] [#UnshieldedTreasury-getTokenBalance] +#### [!toc] [#UnshieldedTreasury-getTokenBalance] + + Returns the tracked balance for `color`, or zero. `color` is disclosed. + + +--- + +## ForwarderPrivate [toc] [#ForwarderPrivate] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderPrivate"; +``` + +--- + +### Ledger [toc] [#ForwarderPrivate-Ledger] +### Ledger [!toc] [#ForwarderPrivate-Ledger] + +#### _parentCommitment [toc] [#ForwarderPrivate-_parentCommitment] +#### [!toc] [#ForwarderPrivate-_parentCommitment] + + Commitment to the `(parent, opSecret)` pair, `H(pad(32, "ForwarderPrivate:commitment") || parent || opSecret)`. Immutable after initialization. + + +#### _isInitialized [toc] [#ForwarderPrivate-_isInitialized] +#### [!toc] [#ForwarderPrivate-_isInitialized] + + Boolean indicating if initialized. + + +### Witnesses [toc] [#ForwarderPrivate-Witnesses] +### Witnesses [!toc] [#ForwarderPrivate-Witnesses] + +None. + +### Circuits [toc] [#ForwarderPrivate-Circuits] +### Circuits [!toc] [#ForwarderPrivate-Circuits] + +#### initialize [toc] [#ForwarderPrivate-initialize] +#### [!toc] [#ForwarderPrivate-initialize] + + Stores the parent commitment, computed off-chain with [_calculateParentCommitment](#ForwarderPrivate-_calculateParentCommitment). The commitment is disclosed. + + Requirements: + + * Contract is not initialized. + * `parentCommitment` is not the zero bytes. + + +#### _deposit [toc] [#ForwarderPrivate-_deposit] +#### [!toc] [#ForwarderPrivate-_deposit] + + Claims `coin` with `receiveShielded`. Nothing is written to the ledger; the coin dwells at the contract address until drained. The coin is disclosed to the protocol. + + Requirements: + + * Contract is initialized. + + +#### _drain [toc] [#ForwarderPrivate-_drain] +#### [!toc] [#ForwarderPrivate-_drain] + + Sends `value` of a previously deposited `coin` to `parent` with `sendShielded`, after checking that `(parent, opSecret)` hashes to the stored commitment. + The coin and value are disclosed; the parent is a coin public key, which is encrypted inside the Zswap output and does not appear on the public transcript. + `opSecret` never leaves the circuit. + Any change is routed back to the contract and returned untouched in `result.change`, to leave dwelling for a future drain or to route onward with `sendImmediateShielded`. + The change must never be spent again and still treated as held. + + Knowledge of the preimage is the only authorization; losing the operational secret makes every coin at the contract permanently inaccessible. + + Requirements: + + * Contract is initialized. + * `parent` is not the zero key. + * `_calculateParentCommitment(parent.bytes, opSecret)` equals `_parentCommitment`. + * `coin.value` is greater than or equal to `value` (enforced by `sendShielded`). + + +#### _calculateParentCommitment [toc] [#ForwarderPrivate-_calculateParentCommitment] +#### [!toc] [#ForwarderPrivate-_calculateParentCommitment] + + Returns `H(pad(32, "ForwarderPrivate:commitment") || parentAddr || opSecret)` using `persistentHash`. + Used off-chain by the deployer to compute the constructor argument and inside [_drain](#ForwarderPrivate-_drain) for the preimage check. Callable without initialization. + + +--- + +## ForwarderShielded [toc] [#ForwarderShielded] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderShielded"; +``` + +--- + +### Ledger [toc] [#ForwarderShielded-Ledger] +### Ledger [!toc] [#ForwarderShielded-Ledger] + +#### _parent [toc] [#ForwarderShielded-_parent] +#### [!toc] [#ForwarderShielded-_parent] + + The parent recipient. Stored as a generic `Either` for forward compatibility; `initialize` only ever writes the `left` (coin public key) arm. + + +#### _isInitialized [toc] [#ForwarderShielded-_isInitialized] +#### [!toc] [#ForwarderShielded-_isInitialized] + + Boolean indicating if initialized. + + +### Witnesses [toc] [#ForwarderShielded-Witnesses] +### Witnesses [!toc] [#ForwarderShielded-Witnesses] + +None. + +### Circuits [toc] [#ForwarderShielded-Circuits] +### Circuits [!toc] [#ForwarderShielded-Circuits] + +#### initialize [toc] [#ForwarderShielded-initialize] +#### [!toc] [#ForwarderShielded-initialize] + + Stores `parent` in the `left` arm of `_parent`. The parent is disclosed. + Beyond the zero-key check, the caller must ensure `parent` can spend forwarded coins; an unspendable parent strands every deposit. + + Requirements: + + * Contract is not initialized. + * `parent` is not the zero key. + + +#### _deposit [toc] [#ForwarderShielded-_deposit] +#### [!toc] [#ForwarderShielded-_deposit] + + Claims `coin` with `receiveShielded` and immediately re-sends its full value to `_parent` with `sendImmediateShielded`. The coin is disclosed. + + Requirements: + + * Contract is initialized. + + +--- + +## ForwarderUnshielded [toc] [#ForwarderUnshielded] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderUnshielded"; +``` + +--- + +### Ledger [toc] [#ForwarderUnshielded-Ledger] +### Ledger [!toc] [#ForwarderUnshielded-Ledger] + +#### _parent [toc] [#ForwarderUnshielded-_parent] +#### [!toc] [#ForwarderUnshielded-_parent] + + The parent recipient. Stored as a generic `Either` for forward compatibility; `initialize` only ever writes the `right` (user address) arm. + + +#### _isInitialized [toc] [#ForwarderUnshielded-_isInitialized] +#### [!toc] [#ForwarderUnshielded-_isInitialized] + + Boolean indicating if initialized. + + +### Witnesses [toc] [#ForwarderUnshielded-Witnesses] +### Witnesses [!toc] [#ForwarderUnshielded-Witnesses] + +None. + +### Circuits [toc] [#ForwarderUnshielded-Circuits] +### Circuits [!toc] [#ForwarderUnshielded-Circuits] + +#### initialize [toc] [#ForwarderUnshielded-initialize] +#### [!toc] [#ForwarderUnshielded-initialize] + + Stores `parent` in the `right` arm of `_parent`. The parent is disclosed. + Beyond the zero-address check, the caller must ensure `parent` can spend forwarded amounts; an unspendable parent strands every deposit. + + Requirements: + + * Contract is not initialized. + * `parent` is not the zero address. + + +#### _deposit [toc] [#ForwarderUnshielded-_deposit] +#### [!toc] [#ForwarderUnshielded-_deposit] + + Receives `amount` of `color` with `receiveUnshielded` and immediately forwards it to `_parent` with `sendUnshielded`. + Depositor, recipient, color, and amount all appear on the public transcript. + + Requirements: + + * Contract is initialized. + + diff --git a/content/contracts-compact/api/native-shielded-token.mdx b/content/contracts-compact/api/native-shielded-token.mdx new file mode 100644 index 00000000..1a17734b --- /dev/null +++ b/content/contracts-compact/api/native-shielded-token.mdx @@ -0,0 +1,1716 @@ +--- +title: NativeShieldedToken API +--- + +This module provides the full NativeShieldedToken module API: the two token flavors, their shared core, +the supply and derived-nonce extensions, and the owner-gated preset. + + + For an overview of the modules, read the [NativeShieldedToken guide](../native-shielded-token). + + +The page covers the following modules: + +- **Token modules** + - [NativeShieldedToken](#NativeShieldedToken): a single token type issued and burned by the contract. + - [NativeShieldedTokenFamily](#NativeShieldedTokenFamily): many token types, keyed by domain, under one contract. + - [NativeShieldedTokenCore](#NativeShieldedTokenCore): the shared core behind the two flavors; not composed directly. +- **Extensions** + - [NativeShieldedTokenPublicSupply](#NativeShieldedTokenPublicSupply): on-chain minted and burned totals for a `NativeShieldedToken`. + - [NativeShieldedTokenFamilyPublicSupply](#NativeShieldedTokenFamilyPublicSupply): per-domain on-chain totals for a `NativeShieldedTokenFamily`. + - [NativeShieldedTokenPublicSupplyCore](#NativeShieldedTokenPublicSupplyCore): the shared core behind the two supply extensions; not composed directly. + - [NativeShieldedTokenDerivedNonce](#NativeShieldedTokenDerivedNonce): mint coin nonces derived from a counter. +- **Preset** + - [ZOwnableNativeShieldedToken](#ZOwnableNativeShieldedToken): a single token whose `ZOwnablePK` owner mints and burns, with a public minted total. + +These modules export no structs or enums of their own. Coins are the standard library's `ShieldedCoinInfo` +(a `nonce`, `color`, and `value`) and `QualifiedShieldedCoinInfo` (the same plus the Merkle-tree index `mt_index` +of a coin the contract holds). + +## NativeShieldedToken [toc] [#NativeShieldedToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedToken"; +``` + +--- + +### Ledger [toc] [#NativeShieldedToken-Ledger] +### Ledger [!toc] [#NativeShieldedToken-Ledger] + +#### _domain [toc] [#NativeShieldedToken-_domain] +#### [!toc] [#NativeShieldedToken-_domain] + + Domain separator fixed at construction. Together with the contract address it determines this token's color. + + +#### _name [toc] [#NativeShieldedToken-_name] +#### [!toc] [#NativeShieldedToken-_name] + + The immutable token name. Re-exported from `NativeShieldedTokenCore`. + + +#### _symbol [toc] [#NativeShieldedToken-_symbol] +#### [!toc] [#NativeShieldedToken-_symbol] + + The immutable token symbol. Re-exported from `NativeShieldedTokenCore`. + + +#### _decimals [toc] [#NativeShieldedToken-_decimals] +#### [!toc] [#NativeShieldedToken-_decimals] + + The immutable token decimals. Re-exported from `NativeShieldedTokenCore`. + + +#### _isInitialized [toc] [#NativeShieldedToken-_isInitialized] +#### [!toc] [#NativeShieldedToken-_isInitialized] + + Boolean indicating if initialized. Re-exported from `NativeShieldedTokenCore`. + + +### Witnesses [toc] [#NativeShieldedToken-Witnesses] +### Witnesses [!toc] [#NativeShieldedToken-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedToken-Circuits] +### Circuits [!toc] [#NativeShieldedToken-Circuits] + +#### initialize [toc] [#NativeShieldedToken-initialize] +#### [!toc] [#NativeShieldedToken-initialize] + + Initializes the token's domain and metadata. + + This MUST be called in the implementing contract's constructor, and it must be the only token operation + performed there. Failure to call it can lead to an irreparable contract. Perform mints, burns, and `tokenColor` + only in post-deployment circuits. + + Discloses `domainSep`, `name_`, `symbol_`, and `decimals_` to the ledger. + + Requirements: + + * Contract is not initialized. + + +#### isInitialized [toc] [#NativeShieldedToken-isInitialized] +#### [!toc] [#NativeShieldedToken-isInitialized] + + Returns whether the contract has been initialized. Safe to call before `initialize`. + + +#### name [toc] [#NativeShieldedToken-name] +#### [!toc] [#NativeShieldedToken-name] + + Returns the token name. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#NativeShieldedToken-symbol] +#### [!toc] [#NativeShieldedToken-symbol] + + Returns the token symbol. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#NativeShieldedToken-decimals] +#### [!toc] [#NativeShieldedToken-decimals] + + Returns the number of decimals used to get the user representation. + This is a display convention only; the protocol operates on integer values. + + Requirements: + + * Contract is initialized. + + +#### tokenColor [toc] [#NativeShieldedToken-tokenColor] +#### [!toc] [#NativeShieldedToken-tokenColor] + + Returns this token's coin color, `tokenType(_domain, kernel.self())`, computed at call time. + + Requirements: + + * Contract is initialized. + + +#### _mint [toc] [#NativeShieldedToken-_mint] +#### [!toc] [#NativeShieldedToken-_mint] + + Mints `amount` of the token to `recipient` using the caller-supplied `nonce`, and returns the new coin's info. + + The caller is responsible for nonce uniqueness: reusing a nonce for the same (value, recipient) produces a + duplicate commitment, which the ledger rejects. With a secret, random nonce the mint is recipient-private; + for mints without a caller-managed nonce, compose `NativeShieldedTokenDerivedNonce` and pass its `_deriveNonce()` output. + + The returned coin info is the only copy available to the recipient, so callers should deliver it out of band; + wallets cannot detect contract-minted coins by scanning the chain. + + Puts the coin color, the amount, and the coin commitment on the ledger. The recipient and the nonce are not posted. + + Requirements: + + * Contract is initialized. + * `recipient` is not the zero key. + + +#### _mintToSelf [toc] [#NativeShieldedToken-_mintToSelf] +#### [!toc] [#NativeShieldedToken-_mintToSelf] + + Mints `amount` to this contract itself and claims the coin in the same call. + Nonce uniqueness is the caller's responsibility, as in `_mint`. + + The consumer should persist the returned coin info in its own ledger state: it is the contract's holding and + is not otherwise recoverable. Spend it later with `_burnFromSelf`, once it carries a Merkle-tree index. + + Puts the coin color, the amount, and the coin commitment on the ledger. + + Requirements: + + * Contract is initialized. + + +#### _burn [toc] [#NativeShieldedToken-_burn] +#### [!toc] [#NativeShieldedToken-_burn] + + Burns `amount` from `coin`, a coin provided within the current transaction (for example paid in by the + caller's wallet), and routes the remaining change to `refundTo`. + + Returns the refund coin created for `refundTo` as `some`, or `none` if the coin was burned in full. + To burn the coin in full, pass `coin.value` as `amount`; `refundTo` is inert in that case but must still be + supplied and non-zero. For coins the contract already holds, use `_burnFromSelf` instead. + + The returned refund coin info is the only copy available to `refundTo`; callers should deliver it out of band. + + Puts the spent coin's nullifier and the output commitments on the ledger. The amount is not posted; a change + output reveals that the burn was partial. + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token color. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not the zero key (the zero key is the burn address; a zero `refundTo` would burn the change as well). + + +#### _burnFromSelf [toc] [#NativeShieldedToken-_burnFromSelf] +#### [!toc] [#NativeShieldedToken-_burnFromSelf] + + Burns `amount` from `coin`, a coin this contract already holds (a Merkle-tree entry with a valid `mt_index`). + Any change is received by the contract automatically and returned as `some`; `none` means the coin was burned in full. + + The consumer should persist the returned change coin info in its own ledger state: the change replaces `coin` + as the contract's holding, and its info is not otherwise recoverable. + + Puts the spent coin's nullifier and the output commitments on the ledger. The amount is not posted. + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token color. + * `amount` is less than or equal to `coin.value`. + + +--- + +## NativeShieldedTokenFamily [toc] [#NativeShieldedTokenFamily] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedTokenFamily"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenFamily-Ledger] +### Ledger [!toc] [#NativeShieldedTokenFamily-Ledger] + +#### _name [toc] [#NativeShieldedTokenFamily-_name] +#### [!toc] [#NativeShieldedTokenFamily-_name] + + The immutable family name shared by all token types. Re-exported from `NativeShieldedTokenCore`. + + +#### _symbol [toc] [#NativeShieldedTokenFamily-_symbol] +#### [!toc] [#NativeShieldedTokenFamily-_symbol] + + The immutable family symbol shared by all token types. Re-exported from `NativeShieldedTokenCore`. + + +#### _decimals [toc] [#NativeShieldedTokenFamily-_decimals] +#### [!toc] [#NativeShieldedTokenFamily-_decimals] + + The immutable family-wide decimals. Re-exported from `NativeShieldedTokenCore`. + + +#### _isInitialized [toc] [#NativeShieldedTokenFamily-_isInitialized] +#### [!toc] [#NativeShieldedTokenFamily-_isInitialized] + + Boolean indicating if initialized. Re-exported from `NativeShieldedTokenCore`. + + +### Witnesses [toc] [#NativeShieldedTokenFamily-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenFamily-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenFamily-Circuits] +### Circuits [!toc] [#NativeShieldedTokenFamily-Circuits] + +#### initialize [toc] [#NativeShieldedTokenFamily-initialize] +#### [!toc] [#NativeShieldedTokenFamily-initialize] + + Initializes the family metadata shared by every token type. + + This MUST be called in the implementing contract's constructor, and it must be the only token operation + performed there. Failure to call it can lead to an irreparable contract. Perform mints, burns, and `tokenColor` + only in post-deployment circuits. + + Discloses `name_`, `symbol_`, and `decimals_` to the ledger. + + Requirements: + + * Contract is not initialized. + + +#### isInitialized [toc] [#NativeShieldedTokenFamily-isInitialized] +#### [!toc] [#NativeShieldedTokenFamily-isInitialized] + + Returns whether the contract has been initialized. Safe to call before `initialize`. + + +#### name [toc] [#NativeShieldedTokenFamily-name] +#### [!toc] [#NativeShieldedTokenFamily-name] + + Returns the family name shared by all token types of this contract. + Per-type identity belongs in the consumer's own state. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#NativeShieldedTokenFamily-symbol] +#### [!toc] [#NativeShieldedTokenFamily-symbol] + + Returns the family symbol shared by all token types of this contract. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#NativeShieldedTokenFamily-decimals] +#### [!toc] [#NativeShieldedTokenFamily-decimals] + + Returns the family-wide number of decimals used to get the user representation. + Issuers with heterogeneous decimals need their own per-domain handling. + + Requirements: + + * Contract is initialized. + + +#### tokenColor [toc] [#NativeShieldedTokenFamily-tokenColor] +#### [!toc] [#NativeShieldedTokenFamily-tokenColor] + + Returns the coin color (token type) for `domain` under this contract's address, + `tokenType(domain, kernel.self())`, computed at call time. + + Discloses `domain` to the ledger. + + Requirements: + + * Contract is initialized. + + +#### _mint [toc] [#NativeShieldedTokenFamily-_mint] +#### [!toc] [#NativeShieldedTokenFamily-_mint] + + Mints `amount` of the token identified by `domain` to `recipient` using the caller-supplied `nonce`, + and returns the new coin's info. The same domain under the same contract address always produces the same color. + + The caller is responsible for nonce uniqueness: reusing a nonce for the same (domain, value, recipient) + produces a duplicate commitment, which the ledger rejects. With a secret, random nonce the mint is + recipient-private; for mints without a caller-managed nonce, compose `NativeShieldedTokenDerivedNonce` + and pass its `_deriveNonce()` output. + + The returned coin info is the only copy available to the recipient, so callers should deliver it out of band; + wallets cannot detect contract-minted coins by scanning the chain. + + Puts `domain`, the coin color, the amount, and the coin commitment on the ledger. The recipient and the nonce are not posted. + + Requirements: + + * Contract is initialized. + * `recipient` is not the zero key. + + +#### _mintToSelf [toc] [#NativeShieldedTokenFamily-_mintToSelf] +#### [!toc] [#NativeShieldedTokenFamily-_mintToSelf] + + Mints `amount` of the token identified by `domain` to this contract itself and claims the coin in the same call. + Nonce uniqueness is the caller's responsibility, as in `_mint`. + + The consumer should persist the returned coin info in its own ledger state: it is the contract's holding and + is not otherwise recoverable. Spend it later with `_burnFromSelf`, once it carries a Merkle-tree index. + + Puts `domain`, the coin color, the amount, and the coin commitment on the ledger. + + Requirements: + + * Contract is initialized. + + +#### _burn [toc] [#NativeShieldedTokenFamily-_burn] +#### [!toc] [#NativeShieldedTokenFamily-_burn] + + Burns `amount` from `coin`, a coin of the `domain` token type provided within the current transaction + (for example paid in by the caller's wallet), and routes the remaining change to `refundTo`. + + Returns the refund coin created for `refundTo` as `some`, or `none` if the coin was burned in full. + To burn the coin in full, pass `coin.value` as `amount`; `refundTo` is inert in that case but must still be + supplied and non-zero. For coins the contract already holds, use `_burnFromSelf` instead. + + The returned refund coin info is the only copy available to `refundTo`; callers should deliver it out of band. + + Puts `domain`, the spent coin's nullifier, and the output commitments on the ledger. The amount is not posted; + a change output reveals that the burn was partial. + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not the zero key (the zero key is the burn address; a zero `refundTo` would burn the change as well). + + +#### _burnFromSelf [toc] [#NativeShieldedTokenFamily-_burnFromSelf] +#### [!toc] [#NativeShieldedTokenFamily-_burnFromSelf] + + Burns `amount` from `coin`, a coin of the `domain` token type that this contract already holds + (a Merkle-tree entry with a valid `mt_index`). Any change is received by the contract automatically and + returned as `some`; `none` means the coin was burned in full. + + The consumer should persist the returned change coin info in its own ledger state: the change replaces `coin` + as the contract's holding, and its info is not otherwise recoverable. + + Puts `domain`, the spent coin's nullifier, and the output commitments on the ledger. The amount is not posted. + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. + + +--- + +## NativeShieldedTokenCore [toc] [#NativeShieldedTokenCore] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedTokenCore"; +``` + +--- + +The shared, domain-parameterized core behind `NativeShieldedToken` and `NativeShieldedTokenFamily`. +It is not meant to be composed directly: compose one of the two flavors instead. +Every token operation takes the `domain` separator as a parameter, and the assertion messages use the generic +`NativeShieldedToken:` prefix. + +### Ledger [toc] [#NativeShieldedTokenCore-Ledger] +### Ledger [!toc] [#NativeShieldedTokenCore-Ledger] + +#### _isInitialized [toc] [#NativeShieldedTokenCore-_isInitialized] +#### [!toc] [#NativeShieldedTokenCore-_isInitialized] + + Boolean indicating if initialized. + + +#### _name [toc] [#NativeShieldedTokenCore-_name] +#### [!toc] [#NativeShieldedTokenCore-_name] + + The immutable token name. + + +#### _symbol [toc] [#NativeShieldedTokenCore-_symbol] +#### [!toc] [#NativeShieldedTokenCore-_symbol] + + The immutable token symbol. + + +#### _decimals [toc] [#NativeShieldedTokenCore-_decimals] +#### [!toc] [#NativeShieldedTokenCore-_decimals] + + The immutable token decimals. + + +### Witnesses [toc] [#NativeShieldedTokenCore-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenCore-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenCore-Circuits] +### Circuits [!toc] [#NativeShieldedTokenCore-Circuits] + +#### initialize [toc] [#NativeShieldedTokenCore-initialize] +#### [!toc] [#NativeShieldedTokenCore-initialize] + + Initializes the contract-wide metadata. + + This MUST be called in the implementing contract's constructor, through the wrapping flavor's `initialize`. + Failure to do so can lead to an irreparable contract. + + Discloses `name_`, `symbol_`, and `decimals_` to the ledger. + + Requirements: + + * Contract is not initialized. + + +#### assertInitialized [toc] [#NativeShieldedTokenCore-assertInitialized] +#### [!toc] [#NativeShieldedTokenCore-assertInitialized] + + Asserts that the contract has been initialized, throwing an error if not. + + Requirements: + + * Contract is initialized. + + +#### assertNotInitialized [toc] [#NativeShieldedTokenCore-assertNotInitialized] +#### [!toc] [#NativeShieldedTokenCore-assertNotInitialized] + + Asserts that the contract has not been initialized, throwing an error if it has. + + Requirements: + + * Contract is not initialized. + + +#### isInitialized [toc] [#NativeShieldedTokenCore-isInitialized] +#### [!toc] [#NativeShieldedTokenCore-isInitialized] + + Returns whether the contract has been initialized. Safe to call before `initialize`. + + +#### name [toc] [#NativeShieldedTokenCore-name] +#### [!toc] [#NativeShieldedTokenCore-name] + + Returns the token name. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#NativeShieldedTokenCore-symbol] +#### [!toc] [#NativeShieldedTokenCore-symbol] + + Returns the token symbol. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#NativeShieldedTokenCore-decimals] +#### [!toc] [#NativeShieldedTokenCore-decimals] + + Returns the number of decimals used to get the user representation. + This is a display convention only; the protocol operates on integer values. + + Requirements: + + * Contract is initialized. + + +#### tokenColor [toc] [#NativeShieldedTokenCore-tokenColor] +#### [!toc] [#NativeShieldedTokenCore-tokenColor] + + Returns the coin color for `domain` under this contract's address, `tokenType(domain, kernel.self())`, + computed at call time. + + Discloses `domain` to the ledger. + + Requirements: + + * Contract is initialized. + + +#### _mint [toc] [#NativeShieldedTokenCore-_mint] +#### [!toc] [#NativeShieldedTokenCore-_mint] + + Mints `amount` of the token identified by `domain` to `recipient` using the caller-supplied `nonce`, + and returns the new coin's info. + + `recipient` may be any coin public key, or this contract's own address. A contract-addressed coin is + claimed with `receiveShielded` in the same call, so the consumer should persist the returned coin info: + it is the contract's holding and is not otherwise recoverable. Any other contract address is rejected. + + The caller is responsible for nonce uniqueness: reusing a nonce for the same (domain, value, recipient) + produces a duplicate commitment, which the ledger rejects. The returned coin info is the only copy available + to the recipient; wallets cannot detect contract-minted coins by scanning the chain. + + Puts `domain`, the coin color, the amount, and the coin commitment on the ledger, and discloses whether the + recipient is a contract address. + + Requirements: + + * Contract is initialized. + * `recipient` is not zero. + * `recipient`, if a contract address, is this contract's own address. + + +#### _burn [toc] [#NativeShieldedTokenCore-_burn] +#### [!toc] [#NativeShieldedTokenCore-_burn] + + Burns `amount` from `coin`, a coin of the `domain` token type provided within the current transaction, + and routes the remaining change to `refundTo`. + + Returns the refund coin created for `refundTo` as `some`, or `none` if the coin was burned in full. + To burn the coin in full, pass `coin.value` as `amount`; `refundTo` is inert in that case but must still be + supplied and non-zero. `refundTo` may be any coin public key, or this contract's own address, in which case + the refund is claimed with `receiveShielded` in the same call. For coins the contract already holds, + use `_burnFromSelf` instead. + + The returned refund coin info is the only copy available to `refundTo`; callers should deliver it out of band. + + Puts `domain`, the spent coin's nullifier, and the output commitments on the ledger, and discloses whether a + change output exists and whether `refundTo` is a contract address. The amount is not posted. + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not zero (the zero key is the burn address; a zero `refundTo` would burn the change as well). + * `refundTo`, if a contract address, is this contract's own address. + + +#### _burnFromSelf [toc] [#NativeShieldedTokenCore-_burnFromSelf] +#### [!toc] [#NativeShieldedTokenCore-_burnFromSelf] + + Burns `amount` from `coin`, a coin of the `domain` token type that this contract already holds + (a Merkle-tree entry with a valid `mt_index`). Any change is received by the contract automatically and + returned as `some`; `none` means the coin was burned in full. + + The consumer should persist the returned change coin info in its own ledger state: the change replaces `coin` + as the contract's holding, and its info is not otherwise recoverable. + + Puts `domain`, the spent coin's nullifier, and the output commitments on the ledger. The amount is not posted. + + Requirements: + + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. + + +--- + +## NativeShieldedTokenPublicSupply [toc] [#NativeShieldedTokenPublicSupply] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenPublicSupply"; +``` + +--- + +Optional extension that adds on-chain supply accounting to a `NativeShieldedToken`. +It imports no token module and needs no initialization: the consuming contract calls `_addMinted` alongside +every `_mint` / `_mintToSelf` and `_addBurned` alongside every `_burn` / `_burnFromSelf`. +All accounting lives under a single fixed key in the core maps. + +Composing this extension makes contract-mediated burn amounts public; mint amounts are already public through +the protocol's shielded-mint effect. + +### Ledger [toc] [#NativeShieldedTokenPublicSupply-Ledger] +### Ledger [!toc] [#NativeShieldedTokenPublicSupply-Ledger] + +#### _totalMinted [toc] [#NativeShieldedTokenPublicSupply-_totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupply-_totalMinted] + + Exact amount minted, stored under the single fixed key `default>`. + Re-exported from `NativeShieldedTokenPublicSupplyCore`. + + +#### _totalBurned [toc] [#NativeShieldedTokenPublicSupply-_totalBurned] +#### [!toc] [#NativeShieldedTokenPublicSupply-_totalBurned] + + Contract-mediated amount burned (a lower bound), stored under the single fixed key `default>`. + Re-exported from `NativeShieldedTokenPublicSupplyCore`. + + +### Witnesses [toc] [#NativeShieldedTokenPublicSupply-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenPublicSupply-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenPublicSupply-Circuits] +### Circuits [!toc] [#NativeShieldedTokenPublicSupply-Circuits] + +#### _addMinted [toc] [#NativeShieldedTokenPublicSupply-_addMinted] +#### [!toc] [#NativeShieldedTokenPublicSupply-_addMinted] + + Adds `amount` to the exact minted total. Call once per successful `NativeShieldedToken._mint` or `_mintToSelf`. + + Puts the new minted total on the ledger. + + Requirements: + + * The minted total plus `amount` does not overflow `Uint<128>`. + + +#### _addBurned [toc] [#NativeShieldedTokenPublicSupply-_addBurned] +#### [!toc] [#NativeShieldedTokenPublicSupply-_addBurned] + + Adds `amount` to the contract-mediated burned total. Call once per successful `NativeShieldedToken._burn` + or `_burnFromSelf`. Enforces `burned <= minted`, so an unpaired or duplicate burn reverts. + + Puts the new burned total on the ledger, which makes the burned amount public. + + Requirements: + + * The burned total does not already exceed the minted total. + * The burned total plus `amount` does not exceed the minted total. + + +#### totalMinted [toc] [#NativeShieldedTokenPublicSupply-totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupply-totalMinted] + + Returns the exact amount ever minted. Reads as 0 before the first `_addMinted`. + + +#### totalBurned [toc] [#NativeShieldedTokenPublicSupply-totalBurned] +#### [!toc] [#NativeShieldedTokenPublicSupply-totalBurned] + + Returns the contract-mediated amount burned. This is a lower bound: coins sent directly to the burn address + without going through the contract are not counted. + + +#### totalSupply [toc] [#NativeShieldedTokenPublicSupply-totalSupply] +#### [!toc] [#NativeShieldedTokenPublicSupply-totalSupply] + + Returns `totalMinted() - totalBurned()`. This is an upper bound on circulating supply, not an exact value: + burns that bypass the contract are invisible. + + +--- + +## NativeShieldedTokenFamilyPublicSupply [toc] [#NativeShieldedTokenFamilyPublicSupply] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenFamilyPublicSupply"; +``` + +--- + +Optional extension that adds per-domain on-chain supply accounting to a `NativeShieldedTokenFamily`. +It imports no token module and needs no initialization: the consuming contract calls `_addMinted` alongside +every `_mint` / `_mintToSelf` and `_addBurned` alongside every `_burn` / `_burnFromSelf`, under the matching domain. + +Composing this extension makes contract-mediated burn amounts public; mint amounts are already public through +the protocol's shielded-mint effect. + +### Ledger [toc] [#NativeShieldedTokenFamilyPublicSupply-Ledger] +### Ledger [!toc] [#NativeShieldedTokenFamilyPublicSupply-Ledger] + +#### _totalMinted [toc] [#NativeShieldedTokenFamilyPublicSupply-_totalMinted] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_totalMinted] + + Exact amount minted per domain. Re-exported from `NativeShieldedTokenPublicSupplyCore`. + + +#### _totalBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-_totalBurned] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_totalBurned] + + Contract-mediated amount burned per domain (a lower bound). Re-exported from `NativeShieldedTokenPublicSupplyCore`. + + +### Witnesses [toc] [#NativeShieldedTokenFamilyPublicSupply-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenFamilyPublicSupply-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenFamilyPublicSupply-Circuits] +### Circuits [!toc] [#NativeShieldedTokenFamilyPublicSupply-Circuits] + +#### _addMinted [toc] [#NativeShieldedTokenFamilyPublicSupply-_addMinted] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_addMinted] + + Adds `amount` to the exact minted total for `domain`. Call once per successful + `NativeShieldedTokenFamily._mint` or `_mintToSelf`. + + Puts `domain` and its new minted total on the ledger. + + Requirements: + + * The minted total for `domain` plus `amount` does not overflow `Uint<128>`. + + +#### _addBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-_addBurned] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_addBurned] + + Adds `amount` to the contract-mediated burned total for `domain`. Call once per successful + `NativeShieldedTokenFamily._burn` or `_burnFromSelf`. Enforces per-domain `burned <= minted`, + so an unpaired, duplicate, or wrong-domain burn reverts. + + Puts `domain` and its new burned total on the ledger, which makes the burned amount public. + + Requirements: + + * The burned total for `domain` does not already exceed its minted total. + * The burned total for `domain` plus `amount` does not exceed its minted total. + + +#### totalMinted [toc] [#NativeShieldedTokenFamilyPublicSupply-totalMinted] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-totalMinted] + + Returns the exact amount ever minted for `domain`. An absent domain reads as 0. + + Discloses `domain` to the ledger. + + +#### totalBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-totalBurned] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-totalBurned] + + Returns the contract-mediated amount burned for `domain`. This is a lower bound: coins sent directly to the + burn address without going through the contract are not counted. + + Discloses `domain` to the ledger. + + +#### totalSupply [toc] [#NativeShieldedTokenFamilyPublicSupply-totalSupply] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-totalSupply] + + Returns `totalMinted(domain) - totalBurned(domain)`. This is an upper bound on circulating supply, + not an exact value: burns that bypass the contract are invisible. + + Discloses `domain` to the ledger. + + +--- + +## NativeShieldedTokenPublicSupplyCore [toc] [#NativeShieldedTokenPublicSupplyCore] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenPublicSupplyCore"; +``` + +--- + +The shared, domain-keyed core behind `NativeShieldedTokenPublicSupply` and `NativeShieldedTokenFamilyPublicSupply`. +It is not meant to be composed directly: compose one of the two flavors instead. +It owns the per-domain minted and burned maps and needs no initialization; an absent domain reads as 0. +The assertion messages use the generic `NativeShieldedTokenPublicSupply:` prefix. + +### Ledger [toc] [#NativeShieldedTokenPublicSupplyCore-Ledger] +### Ledger [!toc] [#NativeShieldedTokenPublicSupplyCore-Ledger] + +#### _totalMinted [toc] [#NativeShieldedTokenPublicSupplyCore-_totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_totalMinted] + + Exact amount minted per domain. + + +#### _totalBurned [toc] [#NativeShieldedTokenPublicSupplyCore-_totalBurned] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_totalBurned] + + Contract-mediated amount burned per domain (a lower bound). + + +### Witnesses [toc] [#NativeShieldedTokenPublicSupplyCore-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenPublicSupplyCore-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenPublicSupplyCore-Circuits] +### Circuits [!toc] [#NativeShieldedTokenPublicSupplyCore-Circuits] + +#### _addMinted [toc] [#NativeShieldedTokenPublicSupplyCore-_addMinted] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_addMinted] + + Adds `amount` to the exact minted total for `domain`. Call once per successful token mint. + + Puts `domain` and its new minted total on the ledger. + + Requirements: + + * The minted total for `domain` plus `amount` does not overflow `Uint<128>`. + + +#### _addBurned [toc] [#NativeShieldedTokenPublicSupplyCore-_addBurned] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_addBurned] + + Adds `amount` to the contract-mediated burned total for `domain`. Call once per successful token burn. + Enforces per-domain `burned <= minted`, so an unpaired, duplicate, or wrong-domain burn reverts rather than + pushing `totalSupply(domain)` negative. + + Puts `domain` and its new burned total on the ledger, which makes the burned amount public. + + Requirements: + + * The burned total for `domain` does not already exceed its minted total. + * The burned total for `domain` plus `amount` does not exceed its minted total. + + +#### totalMinted [toc] [#NativeShieldedTokenPublicSupplyCore-totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-totalMinted] + + Returns the exact amount ever minted for `domain`. An absent domain reads as 0. + + Discloses `domain` to the ledger. + + +#### totalBurned [toc] [#NativeShieldedTokenPublicSupplyCore-totalBurned] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-totalBurned] + + Returns the contract-mediated amount burned for `domain`. This is a lower bound: coins sent directly to the + burn address without going through the contract are not counted. + + Discloses `domain` to the ledger. + + +#### totalSupply [toc] [#NativeShieldedTokenPublicSupplyCore-totalSupply] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-totalSupply] + + Returns `totalMinted(domain) - totalBurned(domain)`, or 0 if the burned total somehow exceeds the minted total. + This is an upper bound on circulating supply, not an exact value: burns that bypass the contract are invisible. + + Discloses `domain` to the ledger. + + +--- + +## NativeShieldedTokenDerivedNonce [toc] [#NativeShieldedTokenDerivedNonce] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenDerivedNonce"; +``` + +--- + +Optional extension that derives coin nonces for native shielded token mints from a monotonic counter, +so callers need not manage nonces themselves. It imports no token module and needs no initialization, +and pairs with either `NativeShieldedToken` or `NativeShieldedTokenFamily`. + +Derived nonces are predictable from public state, so mints that use them are recipient-public. +For recipient privacy, call the token's `_mint` with a secret, cryptographically random nonce instead. + +### Ledger [toc] [#NativeShieldedTokenDerivedNonce-Ledger] +### Ledger [!toc] [#NativeShieldedTokenDerivedNonce-Ledger] + +#### _counter [toc] [#NativeShieldedTokenDerivedNonce-_counter] +#### [!toc] [#NativeShieldedTokenDerivedNonce-_counter] + + Monotonic index feeding the nonce derivation. One counter serves the whole contract, so derived nonces never + repeat across token types. + + +### Witnesses [toc] [#NativeShieldedTokenDerivedNonce-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenDerivedNonce-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenDerivedNonce-Circuits] +### Circuits [!toc] [#NativeShieldedTokenDerivedNonce-Circuits] + +#### _deriveNonce [toc] [#NativeShieldedTokenDerivedNonce-_deriveNonce] +#### [!toc] [#NativeShieldedTokenDerivedNonce-_deriveNonce] + + Advances the counter and returns the next derived coin nonce, + `evolveNonce(_counter, pad(32, "NativeShieldedToken:nonce"))`, for use as the `nonce` argument of a token mint. + Uniqueness comes from the counter; the fixed tag keeps these nonces separate from any other `evolveNonce` + use in the implementing contract. + + A reverted mint does not advance the counter, so retrying an identical mint after a collision collides again; + a mint with different inputs advances the counter. + + Puts the incremented counter on the ledger. + + +--- + +## ZOwnableNativeShieldedToken [toc] [#ZOwnableNativeShieldedToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/presets/ZOwnableNativeShieldedToken"; +``` + +--- + +A native shielded token with a single shielded owner. Only the owner mints (to a coin public key, with a +caller-supplied nonce) and burns (a coin the owner's wallet pays into the transaction). Minted amounts are +counted on-chain; burned amounts are not, so burns stay amount-private. + +The preset composes `ZOwnablePK` (the owner commitment), `NativeShieldedToken` (one token color), and +`NativeShieldedTokenPublicSupply` (the minted total). Implementing contracts call `initialize` from their +constructor and must not import the composed modules directly. + +Holders transfer through Zswap directly. There are no roles, freeze, seize, contract-held coins, +burned total, or total supply. + +### Ledger [toc] [#ZOwnableNativeShieldedToken-Ledger] +### Ledger [!toc] [#ZOwnableNativeShieldedToken-Ledger] + +#### _ownerCommitment [toc] [#ZOwnableNativeShieldedToken-_ownerCommitment] +#### [!toc] [#ZOwnableNativeShieldedToken-_ownerCommitment] + + The current owner commitment, `H(id || instanceSalt || counter || pad(32, "ZOwnablePK:shield:"))`. + Zero means the contract is unowned. Re-exported from `ZOwnablePK`. + + +#### _counter [toc] [#ZOwnableNativeShieldedToken-_counter] +#### [!toc] [#ZOwnableNativeShieldedToken-_counter] + + The number of ownership changes. Re-exported from `ZOwnablePK`. + + +#### _instanceSalt [toc] [#ZOwnableNativeShieldedToken-_instanceSalt] +#### [!toc] [#ZOwnableNativeShieldedToken-_instanceSalt] + + Per-deployment salt for the owner commitment. Re-exported from `ZOwnablePK`. + + +#### _ownableIsInitialized [toc] [#ZOwnableNativeShieldedToken-_ownableIsInitialized] +#### [!toc] [#ZOwnableNativeShieldedToken-_ownableIsInitialized] + + Boolean indicating if the owner is initialized. This is `ZOwnablePK`'s `_isInitialized`, aliased because the + token module already owns `_isInitialized`. + + +#### _domain [toc] [#ZOwnableNativeShieldedToken-_domain] +#### [!toc] [#ZOwnableNativeShieldedToken-_domain] + + Domain separator of this contract's token color. Re-exported from `NativeShieldedToken`. + + +#### _name [toc] [#ZOwnableNativeShieldedToken-_name] +#### [!toc] [#ZOwnableNativeShieldedToken-_name] + + The immutable token name. Re-exported from `NativeShieldedToken`. + + +#### _symbol [toc] [#ZOwnableNativeShieldedToken-_symbol] +#### [!toc] [#ZOwnableNativeShieldedToken-_symbol] + + The immutable token symbol. Re-exported from `NativeShieldedToken`. + + +#### _decimals [toc] [#ZOwnableNativeShieldedToken-_decimals] +#### [!toc] [#ZOwnableNativeShieldedToken-_decimals] + + The immutable token decimals. Re-exported from `NativeShieldedToken`. + + +#### _isInitialized [toc] [#ZOwnableNativeShieldedToken-_isInitialized] +#### [!toc] [#ZOwnableNativeShieldedToken-_isInitialized] + + Boolean indicating if the token is initialized. Re-exported from `NativeShieldedToken`. + + +#### _totalMinted [toc] [#ZOwnableNativeShieldedToken-_totalMinted] +#### [!toc] [#ZOwnableNativeShieldedToken-_totalMinted] + + Exact amount minted, under the single fixed key `default>`. + Re-exported from `NativeShieldedTokenPublicSupply`. + + +#### _totalBurned [toc] [#ZOwnableNativeShieldedToken-_totalBurned] +#### [!toc] [#ZOwnableNativeShieldedToken-_totalBurned] + + Never written by this preset, so it stays empty. Re-exported from `NativeShieldedTokenPublicSupply`. + + +### Witnesses [toc] [#ZOwnableNativeShieldedToken-Witnesses] +### Witnesses [!toc] [#ZOwnableNativeShieldedToken-Witnesses] + +None declared by this module. The composed `ZOwnablePK` requires +[wit_secretNonce](./ownable#ZOwnablePK-wit_secretNonce), which every owner-gated circuit calls through +`assertOnlyOwner`. The implementing DApp supplies it; the TypeScript witnesses in the library repository are +test-only and are not published. + +### Circuits [toc] [#ZOwnableNativeShieldedToken-Circuits] +### Circuits [!toc] [#ZOwnableNativeShieldedToken-Circuits] + +#### initialize [toc] [#ZOwnableNativeShieldedToken-initialize] +#### [!toc] [#ZOwnableNativeShieldedToken-initialize] + + Sets the owner and the token's domain and metadata. + + Call from the implementing contract's constructor only. It writes sealed fields, so implementing contracts + must not expose it as a post-deploy circuit. + + `ownerId` must be `_computeOwnerId(pk, secretNonce)` for the coin public key the owner's wallet will supply + as `ownPublicKey()`. The circuit cannot check this at deploy; any other value leaves the contract with no + usable owner and no recovery. + + Puts the owner commitment, `instanceSalt`, `tokenDomain`, `name_`, `symbol_`, and `decimals_` on the ledger. + + Requirements: + + * Contract is not initialized. + * `ownerId` is not zero. + * `instanceSalt` is not zero. + + +#### mint [toc] [#ZOwnableNativeShieldedToken-mint] +#### [!toc] [#ZOwnableNativeShieldedToken-mint] + + Mints `amount` to `recipient` and adds it to the minted total. Owner only. + `nonce` must be fresh random bytes per mint. Persist the returned coin: it is the only copy. + + Puts the coin color, the amount, the coin commitment, and the new minted total on the ledger. + The recipient and the nonce are not posted. + + Requirements: + + * Caller is the owner. + * `recipient` is not the zero key. + + +#### burn [toc] [#ZOwnableNativeShieldedToken-burn] +#### [!toc] [#ZOwnableNativeShieldedToken-burn] + + Burns `amount` of `coin`, paid into the transaction by the caller's wallet, and refunds the rest to `refundTo`. + Owner only. Returns the refund coin, or `none` on a full burn. Persist the returned coin: it is the only copy. + + Puts the spent coin's nullifier and the output commitments on the ledger. The amount is not posted; + a change output reveals that the burn was partial. + + Requirements: + + * Caller is the owner. + * `coin.color` is this token's color. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not the zero key. + + +#### transferOwnership [toc] [#ZOwnableNativeShieldedToken-transferOwnership] +#### [!toc] [#ZOwnableNativeShieldedToken-transferOwnership] + + Transfers ownership to `newOwnerId` in one step. Owner only. + + `newOwnerId` must be `_computeOwnerId(pk, secretNonce)` for the coin public key the new owner's wallet will + supply as `ownPublicKey()`. The circuit cannot check this; any other value leaves the contract with no usable + owner and no recovery. + + Puts the new owner commitment and the incremented counter on the ledger. + + Requirements: + + * Caller is the owner. + * `newOwnerId` is not zero. + + +#### renounceOwnership [toc] [#ZOwnableNativeShieldedToken-renounceOwnership] +#### [!toc] [#ZOwnableNativeShieldedToken-renounceOwnership] + + Clears the owner. Owner only. Irreversible: `mint` and `burn` stop forever. + + Puts the zero owner commitment on the ledger. + + Requirements: + + * Caller is the owner. + + +#### owner [toc] [#ZOwnableNativeShieldedToken-owner] +#### [!toc] [#ZOwnableNativeShieldedToken-owner] + + Returns the current owner commitment. + + Requirements: + + * Contract is initialized. + + +#### _computeOwnerId [toc] [#ZOwnableNativeShieldedToken-_computeOwnerId] +#### [!toc] [#ZOwnableNativeShieldedToken-_computeOwnerId] + + Returns the owner id `H(pk || nonce)`. Compute it off-chain to produce the `ownerId` passed to `initialize` + and `transferOwnership`. + + Requirements: + + * `pk` is a coin public key, not a contract address. + + +#### totalMinted [toc] [#ZOwnableNativeShieldedToken-totalMinted] +#### [!toc] [#ZOwnableNativeShieldedToken-totalMinted] + + Returns the total amount ever minted. + + +#### name [toc] [#ZOwnableNativeShieldedToken-name] +#### [!toc] [#ZOwnableNativeShieldedToken-name] + + Returns the token name. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#ZOwnableNativeShieldedToken-symbol] +#### [!toc] [#ZOwnableNativeShieldedToken-symbol] + + Returns the token symbol. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#ZOwnableNativeShieldedToken-decimals] +#### [!toc] [#ZOwnableNativeShieldedToken-decimals] + + Returns the token decimals. + + Requirements: + + * Contract is initialized. + + +#### tokenColor [toc] [#ZOwnableNativeShieldedToken-tokenColor] +#### [!toc] [#ZOwnableNativeShieldedToken-tokenColor] + + Returns this contract's token color, `tokenType(tokenDomain, kernel.self())`. + + Requirements: + + * Contract is initialized. + diff --git a/content/contracts-compact/api/non-fungible-token.mdx b/content/contracts-compact/api/non-fungible-token.mdx index 4bfa6409..649c595b 100644 --- a/content/contracts-compact/api/non-fungible-token.mdx +++ b/content/contracts-compact/api/non-fungible-token.mdx @@ -11,7 +11,7 @@ This module provides the full NonFungibleToken module API. ## NonFungibleToken [toc] [#NonFungibleToken] ```ts @@ -43,6 +43,17 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; The immutable token symbol. +#### _isInitialized [toc] [#NonFungibleToken-_isInitialized] +#### [!toc] [#NonFungibleToken-_isInitialized] + + Initialization flag, owned by this module. Set to `true` by [initialize](#NonFungibleToken-initialize); + every other circuit asserts it is set. + + #### _owners [toc] [#NonFungibleToken-_owners] #### [!toc] [#NonFungibleToken-_owners] Returns the caller's secret key used in deriving the account identifier. + The key stays private; only `persistentHash(secretKey)` reaches the ledger. The same key produces the same account identifier across all contracts. Users who desire cross-contract unlinkability should use different keys per contract. @@ -112,17 +124,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; ### Circuits [toc] [#NonFungibleToken-Circuits] ### Circuits [!toc] [#NonFungibleToken-Circuits] -#### ZERO [toc] [#NonFungibleToken-ZERO] -#### [!toc] [#NonFungibleToken-ZERO] - - Returns a canonical zero `Either` value (left variant with zero `Bytes<32>`). - Used as the zero value for mint/burn operations and cleared approvals. - - #### initialize [toc] [#NonFungibleToken-initialize] #### [!toc] [#NonFungibleToken-initialize] - Initializes the contract by setting the name and symbol. + Initializes the contract by setting the name and symbol, and marks the module initialized. This MUST be called in the implementing contract's constructor. Failure to do so can lead to an irreparable contract. @@ -247,7 +248,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Gives permission to `to` to transfer `tokenId` token to another account. @@ -287,7 +288,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; approved: Boolean ) → []' kind="circuit" - complexity="k=13, rows=2936" + complexity="k=13, rows=2947" modulePrefix="NonFungibleToken" stackParams="true" > @@ -322,7 +323,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -340,7 +341,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; * `fromAddress` is not zero. * `to` is not zero. * `to` is not a ContractAddress. - * `tokenId` token must be owned by `fromAddress`. + * `tokenId` must exist and be owned by `fromAddress`. * If the caller is not `fromAddress`, it must be approved to move this token by either [approve](#NonFungibleToken-approve) or [setApprovalForAll](#NonFungibleToken-setApprovalForAll). @@ -349,12 +350,13 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Unsafe variant of [transferFrom](#NonFungibleToken-transferFrom) which allows transfers to contract addresses. The caller's identity is derived from the `wit_NonFungibleTokenSK` witness as `persistentHash(secretKey)`. + The circuit asserts that `tokenId` had an owner before the update, so a stray approval recorded for an unminted token cannot turn it into a mint. Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. @@ -367,7 +369,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; * The contract is initialized. * `fromAddress` is not zero. * `to` is not zero. - * `tokenId` token must be owned by `fromAddress`. + * `tokenId` must exist and be owned by `fromAddress`. * If the caller is not `fromAddress`, it must be approved to move this token by either [approve](#NonFungibleToken-approve) or [setApprovalForAll](#NonFungibleToken-setApprovalForAll). @@ -379,7 +381,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; complexity="k=9, rows=308" modulePrefix="NonFungibleToken" > - Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist. + Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist; returns the zero value instead. Requirements: @@ -454,12 +456,13 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; > Transfers `tokenId` from its current owner to `to`, or alternatively mints (or burns) if the current owner (or `to`) is the zero address. + Clears any single-token approval on `tokenId` when it had an owner. Returns the owner of the `tokenId` before the update. Requirements: * The contract is initialized. - * If `auth` is non 0, then this function will check that `auth` is either the owner of the token, + * If `auth` is non-zero, then this function will check that `auth` is either the owner of the token, or approved to operate on the token (by the owner). @@ -468,7 +471,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Mints `tokenId` and transfers it to `to`. @@ -491,7 +494,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Unsafe variant of [_mint](#NonFungibleToken-_mint) which allows transfers to contract addresses. @@ -514,11 +517,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Destroys `tokenId`. - The approval is cleared when the token is burned. + The approval and the token URI are cleared when the token is burned. This circuit does not check if the sender is authorized to operate on the token. Requirements: @@ -532,7 +535,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -549,7 +552,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; * The contract is initialized. * `to` is not zero. * `to` is not a ContractAddress. - * `tokenId` token must be owned by `fromAddress`. + * `tokenId` must exist and be owned by `fromAddress`. #### _unsafeTransfer [toc] [#NonFungibleToken-_unsafeTransfer] @@ -557,7 +560,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -575,7 +578,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; * Contract is initialized. * `to` is not zero. - * `tokenId` token must be owned by `fromAddress`. + * `tokenId` must exist and be owned by `fromAddress`. #### _approve [toc] [#NonFungibleToken-_approve] @@ -583,16 +586,50 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; - Approve `to` to operate on `tokenId`. + Approve `to` to operate on `tokenId`. The token must exist. + Equivalent to [_unsafeApprove](#NonFungibleToken-_unsafeApprove) with `isExistenceRequired` set to `true`. Requirements: * The contract is initialized. - * If `auth` is non 0, then this function will check that `auth` is either the owner of the token, + * `tokenId` must exist. + * If `auth` is non-zero, then this function will check that `auth` is either the owner of the token, + or approved to operate on the token (by the owner). + + +#### _unsafeApprove [toc] [#NonFungibleToken-_unsafeApprove] +#### [!toc] [#NonFungibleToken-_unsafeApprove] + + Approve `to` to operate on `tokenId`, requiring the token to exist only when `isExistenceRequired` is `true`. + The owner is read only when the existence check runs or `auth` is non-zero. + Writes the canonicalized `to` (or the zero value) to `_tokenApprovals`; `isExistenceRequired` is disclosed. + + + Passing `false` skips the check that `tokenId` was minted. The transfer path relies on approvals never + existing for unminted tokens, so a planted approval can mint a token nobody authorized or outlive a later mint. + Pass `false` only where the owner has already been read, as the internal `_update` does when clearing an approval. + + + Requirements: + + * The contract is initialized. + * `tokenId` must exist, unless `isExistenceRequired` is `false` and `auth` is zero. + * If `auth` is non-zero, then this function will check that `auth` is either the owner of the token, or approved to operate on the token (by the owner). @@ -601,7 +638,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -638,40 +675,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; kind="internal" modulePrefix="NonFungibleToken" > - Computes the caller's account identifier from the `wit_NonFungibleTokenSK` witness. + Computes the caller's account identifier from the `wit_NonFungibleTokenSK` witness + by delegating to [Utils computeAccountId](./utils#Utils-computeAccountId). ID Derivation: `accountId = persistentHash(secretKey)` The result is a 32-byte commitment that uniquely identifies the caller. - - -#### computeAccountId [toc] [#NonFungibleToken-computeAccountId] -#### [!toc] [#NonFungibleToken-computeAccountId] - - Computes an account identifier without on-chain state, allowing a user to derive - their identity commitment before submitting it in a token operation. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. - - ID Derivation: `accountId = persistentHash(secretKey)` - - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the security of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss — a lost key cannot be recovered. - - - -#### _isTargetZero [toc] [#NonFungibleToken-_isTargetZero] -#### [!toc] [#NonFungibleToken-_isTargetZero] - - Returns `true` if `target`'s active branch (as indicated by `is_left`) holds the zero value. + The same derivation is available off-chain through `Utils_computeAccountId`. diff --git a/content/contracts-compact/api/ownable.mdx b/content/contracts-compact/api/ownable.mdx index 8d4cd78d..63704e85 100644 --- a/content/contracts-compact/api/ownable.mdx +++ b/content/contracts-compact/api/ownable.mdx @@ -8,10 +8,15 @@ This module provides the full Ownable module API. For an overview of the module, read the [Ownable guide](../ownable). +The page covers the following modules: + +- [Ownable](#Ownable): a single owner identified by a public account identifier. +- [ZOwnablePK](#ZOwnablePK): a single owner identified by a shielded commitment. + ## Ownable [toc] [#Ownable] ```ts @@ -23,6 +28,17 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; ### Ledger [toc] [#Ownable-Ledger] ### Ledger [!toc] [#Ownable-Ledger] +#### _isInitialized [toc] [#Ownable-_isInitialized] +#### [!toc] [#Ownable-_isInitialized] + + Boolean indicating whether [initialize](#Ownable-initialize) has been called. + Every other circuit in the module requires it to be `true`. + + #### _owner [toc] [#Ownable-_owner] #### [!toc] [#Ownable-_owner] - Either a `Bytes<32>` account identifier or `ContractAddress` representing the owner. + Either a `Bytes<32>` account identifier or a `ContractAddress` representing the owner. + The value is stored canonicalized (the inactive branch of the `Either` is zeroed). + A `Bytes<32>` value of zero means the contract is unowned. ### Witnesses [toc] [#Ownable-Witnesses] ### Witnesses [!toc] [#Ownable-Witnesses] +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + #### wit_OwnableSK [toc] [#Ownable-wit_OwnableSK] #### [!toc] [#Ownable-wit_OwnableSK] - Returns the caller's secret key used in deriving the account identifier. + Returns the caller's secret key used in deriving the account identifier `H(secretKey)`. + The key never reaches the ledger; only the derived identifier is compared against `_owner`. The same key produces the same account identifier across all contracts. Users who desire cross-contract unlinkability should use different keys per contract. @@ -63,6 +85,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; Initializes the contract by setting the `initialOwner`. This must be called in the contract's constructor. + Discloses: the canonicalized `initialOwner`, written to `_owner`. + Requirements: * Contract is not already initialized. @@ -100,6 +124,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; This restriction prevents permanently disabling access to a circuit. + Discloses: the canonicalized `newOwner`, written to `_owner`. + Requirements: * Contract is initialized. @@ -124,6 +150,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; Once contract-to-contract calls are supported, this circuit may be deprecated. + Discloses: the canonicalized `newOwner`, written to `_owner`. + Requirements: * Contract is initialized. @@ -139,7 +167,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; complexity="k=13, rows=2364" modulePrefix="Ownable" > - Leaves the contract without an owner. + Leaves the contract without an owner by writing the zero `Bytes<32>` identifier to `_owner`. It will not be possible to call [`assertOnlyOwner`](#Ownable-assertOnlyOwner) circuits anymore. Can only be called by the current owner. @@ -159,11 +187,14 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; > Throws if called by any account other than the owner. Use this to restrict access of specific circuits to the owner. - The caller's identity is derived from the `wit_OwnableSK` witness as `persistentHash(secretKey)`. + The caller's identity is derived from the `wit_OwnableSK` witness as `H(secretKey)`, + the same derivation as [`Utils.computeAccountId`](./utils#Utils-computeAccountId), + and compared against `_owner`. Requirements: * Contract is initialized. + * `_owner` holds a `Bytes<32>` account identifier (a contract-address owner cannot authenticate). * The caller is the current contract owner. @@ -182,6 +213,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; This restriction prevents circuits from being inadvertently locked in contracts. + Discloses: the canonicalized `newOwner`, written to `_owner`. + Requirements: * Contract is initialized. @@ -197,6 +230,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; modulePrefix="Ownable" > Unsafe variant of [`_transferOwnership`](#Ownable-_transferOwnership). + This is the single write point for `_owner`: every ownership change flows through it. Ownership transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. @@ -204,53 +238,19 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; Once contract-to-contract calls are supported, this circuit may be deprecated. + Discloses: the canonicalized `newOwner`, written to `_owner`. + Requirements: * Contract is initialized. -#### _computeAccountId [toc] [#Ownable-_computeAccountId] -#### [!toc] [#Ownable-_computeAccountId] - - Computes the caller's account identifier from the `wit_OwnableSK` witness. - - ID Derivation: `accountId = persistentHash(secretKey)` - - The result is a 32-byte commitment that uniquely identifies the caller. - - -#### computeAccountId [toc] [#Ownable-computeAccountId] -#### [!toc] [#Ownable-computeAccountId] - - Computes an account identifier without on-chain state, allowing a user to derive - their identity commitment before submitting it in an ownership transfer. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. - - ID Derivation: `accountId = persistentHash(secretKey)` - - - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the security of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss. - A lost key cannot be recovered. - - - --- ## ZOwnablePK [toc] [#ZOwnablePK] ```ts @@ -259,8 +259,19 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; --- -### Ledger [toc] [#ZOwnablePK-ledger] -### Ledger [!toc] [#ZOwnablePK-ledger] +### Ledger [toc] [#ZOwnablePK-Ledger] +### Ledger [!toc] [#ZOwnablePK-Ledger] + +#### _isInitialized [toc] [#ZOwnablePK-_isInitialized] +#### [!toc] [#ZOwnablePK-_isInitialized] + + Boolean indicating whether [initialize](#ZOwnablePK-initialize) has been called. + Every other non-pure circuit in the module requires it to be `true`. + #### _ownerCommitment [toc] [#ZOwnablePK-_ownerCommitment] #### [!toc] [#ZOwnablePK-_ownerCommitment] @@ -270,7 +281,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; id="ZOwnablePK-_ownerCommitment" > Stores the current hashed commitment representing the owner. - This commitment is derived from the public identifier (e.g., `SHA256(pk, nonce)`), + This commitment is derived from the owner identifier `SHA256(pk || nonce)`, the `instanceSalt`, the transfer `counter`, and a domain separator. A commitment of `default>` (i.e. zero) indicates the contract is unowned. @@ -299,32 +310,43 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; A per-instance value provided at initialization used to namespace commitments for this contract instance. This salt prevents commitment collisions across contracts that might otherwise use the same owner identifiers or domain parameters. + Use a fresh random value per deployment: a reused salt links the same owner across deployments. It is immutable after initialization. -### Witnesses [toc] [#ZOwnablePK-witnesses] -### Witnesses [!toc] [#ZOwnablePK-witnesses] +### Witnesses [toc] [#ZOwnablePK-Witnesses] +### Witnesses [!toc] [#ZOwnablePK-Witnesses] + +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. #### wit_secretNonce [toc] [#ZOwnablePK-wit_secretNonce] #### [!toc] [#ZOwnablePK-wit_secretNonce] A private per-user nonce used in deriving the shielded owner identifier. - Combined with the user's public key as `SHA256(pk, nonce)` to produce an obfuscated, unlinkable identity commitment. - Users are encouraged to rotate this value on ownership changes. + Combined with the user's public key as `SHA256(pk || nonce)` to produce an obfuscated, + unlinkable identity commitment. Users are encouraged to rotate this value on ownership changes. + + + `ownPublicKey()` is itself a witness: it returns the caller's Zswap coin public key, and nothing proves the caller + holds the matching private key. The secret nonce is therefore the owner credential. Anyone holding the nonce and + the owner's public key can pass `assertOnlyOwner`. Generate it as 32 bytes from a CSPRNG and keep it private; + losing it prevents the owner from proving ownership or transferring it. + -### Circuits [toc] [#ZOwnablePK-circuits] -### Circuits [!toc] [#ZOwnablePK-circuits] +### Circuits [toc] [#ZOwnablePK-Circuits] +### Circuits [!toc] [#ZOwnablePK-Circuits] #### initialize [toc] [#ZOwnablePK-initialize] #### [!toc] [#ZOwnablePK-initialize] + Discloses: `instanceSalt`, written to `_instanceSalt`, and the resulting owner commitment, written to `_ownerCommitment`. + The `ownerId` itself is not disclosed. + Requirements: * Contract is not initialized. * `ownerId` is not zero. + * `instanceSalt` is not zero. #### owner [toc] [#ZOwnablePK-owner] #### [!toc] [#ZOwnablePK-owner] Returns the current commitment representing the contract owner. - The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. + The full commitment is `SHA256(SHA256(pk || nonce) || instanceSalt || counter || domain)`. Requirements: @@ -364,7 +390,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; #### transferOwnership [toc] [#ZOwnablePK-transferOwnership] #### [!toc] [#ZOwnablePK-transferOwnership] #### renounceOwnership [toc] [#ZOwnablePK-renounceOwnership] #### [!toc] [#ZOwnablePK-renounceOwnership] - Leaves the contract without an owner. + Leaves the contract without an owner by resetting `_ownerCommitment` to zero. It will not be possible to call `assertOnlyOwner` circuits anymore. Can only be called by the current owner. @@ -400,25 +428,28 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; #### assertOnlyOwner [toc] [#ZOwnablePK-assertOnlyOwner] #### [!toc] [#ZOwnablePK-assertOnlyOwner] - Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match + Throws if called by any account whose id hash `SHA256(pk || nonce)` does not match the stored owner commitment. Use this to only allow the owner to call specific circuits. + The `pk` is read from `ownPublicKey()` and the `nonce` from `wit_secretNonce`; both are witness values. + Neither reaches the ledger: the check compares the recomputed commitment against `_ownerCommitment` inside the proof. + Requirements: * Contract is initialized. - * Caller's id (`SHA256(pk, nonce)`) when used in `_computeOwnerCommitment` equals the stored `_ownerCommitment`. + * Caller's id (`SHA256(pk || nonce)`) when used in `_computeOwnerCommitment` equals the stored `_ownerCommitment`. #### _computeOwnerCommitment [toc] [#ZOwnablePK-_computeOwnerCommitment] #### [!toc] [#ZOwnablePK-_computeOwnerCommitment] ` for hashing. - Domain separator: `"ZOwnablePK:shield:"` padded to 32 bytes. @@ -442,13 +473,13 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; #### _computeOwnerId [toc] [#ZOwnablePK-_computeOwnerId] #### [!toc] [#ZOwnablePK-_computeOwnerId] Computes the unique identifier (`id`) of the owner from their public key and a secret nonce. - The ID derivation follows: `id = SHA256(pk, nonce)` + The ID derivation follows: `id = SHA256(pk || nonce)` Currently only supports `ZswapCoinPublicKey`. Contract address owners are not yet supported. @@ -462,26 +493,18 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; #### _transferOwnership [toc] [#ZOwnablePK-_transferOwnership] #### [!toc] [#ZOwnablePK-_transferOwnership] Transfers ownership to owner id `newOwnerId` without enforcing permission checks on the caller. - This is an internal function that increments the counter and updates the owner commitment. + This is an internal circuit that increments the counter and updates the owner commitment. + + Discloses: the new owner commitment, written to `_ownerCommitment`. The `newOwnerId` itself is not disclosed. Requirements: * Contract is initialized. - -#### _isTargetZero [toc] [#Ownable-_isTargetZero] -#### [!toc] [#Ownable-_isTargetZero] - - Returns `true` if `target`'s active branch (as indicated by `is_left`) holds the zero value. - diff --git a/content/contracts-compact/api/security.mdx b/content/contracts-compact/api/security.mdx index 5920fea5..981aa8ea 100644 --- a/content/contracts-compact/api/security.mdx +++ b/content/contracts-compact/api/security.mdx @@ -8,16 +8,31 @@ This package provides the API for all Security modules. For an overview of the module, read the [Security guide](../security). +The page covers the following modules: + +- [Initializable](#Initializable): a one-time initialization guard. +- [Pausable](#Pausable): an emergency stop mechanism. +- [Allowlist](#Allowlist): a permit-list keyed by account identifier. +- [Blocklist](#Blocklist): a deny-list keyed by account identifier. + ## Initializable [toc] [#Initializable] ```ts import "./node_modules/@openzeppelin/compact-contracts/security/Initializable"; ``` + + Use this module only at the top contract layer, and import it into at most one module per contract. + When two modules in the same directory both import Initializable, the compiler collapses their + `_isInitialized` flags into a single shared ledger slot + ([compact#270](https://github.com/LFDT-Minokawa/compact/issues/270)): initializing one marks the other + as initialized. The OpenZeppelin modules do not import this module; each tracks its own `_isInitialized` flag inline. + + --- ### Ledger [toc] [#Initializable-Ledger] @@ -91,7 +106,7 @@ None. ## Pausable [toc] [#Pausable] ```ts @@ -191,3 +206,195 @@ None. * Contract must be paused. + +--- + +## Allowlist [toc] [#Allowlist] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/security/Allowlist"; +``` + +A minimal permit-list keyed by a `Bytes<32>` account identifier. +The mutators `_allow` and `_disallow` are ungated: the implementing contract must protect them with its own access control +and decides where `assertAllowed` applies. +Membership is public and the approved set is publicly enumerable. + +--- + +### Ledger [toc] [#Allowlist-Ledger] +### Ledger [!toc] [#Allowlist-Ledger] + +#### _allowed [toc] [#Allowlist-_allowed] +#### [!toc] [#Allowlist-_allowed] + + Approved accounts. An absent account is not allowed. + + +### Witnesses [toc] [#Allowlist-Witnesses] +### Witnesses [!toc] [#Allowlist-Witnesses] + +None. + +### Circuits [toc] [#Allowlist-Circuits] +### Circuits [!toc] [#Allowlist-Circuits] + +#### isAllowed [toc] [#Allowlist-isAllowed] +#### [!toc] [#Allowlist-isAllowed] + + Returns whether `account` is currently allowed. + + Discloses `account` to the ledger. + + +#### assertAllowed [toc] [#Allowlist-assertAllowed] +#### [!toc] [#Allowlist-assertAllowed] + + Asserts that `account` is allowed. The implementing contract decides when this gate applies. + + Discloses `account` to the ledger. + + Requirements: + + * `account` is a member of the allowlist. + + +#### _allow [toc] [#Allowlist-_allow] +#### [!toc] [#Allowlist-_allow] + + Adds `account` to the allowlist. Ungated: the implementing contract is responsible for protecting this circuit. + + Discloses `account` to the ledger. + + +#### _disallow [toc] [#Allowlist-_disallow] +#### [!toc] [#Allowlist-_disallow] + + Removes `account` from the allowlist. Ungated: the implementing contract is responsible for protecting this circuit. + + Discloses `account` to the ledger. + + +--- + +## Blocklist [toc] [#Blocklist] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/security/Blocklist"; +``` + +A minimal deny-list keyed by a `Bytes<32>` account identifier. +The mutators `_block` and `_unblock` are ungated: the implementing contract must protect them with its own access control +and decides where `assertNotBlocked` applies. +Membership is public and the blocked set is publicly enumerable. + +--- + +### Ledger [toc] [#Blocklist-Ledger] +### Ledger [!toc] [#Blocklist-Ledger] + +#### _blocked [toc] [#Blocklist-_blocked] +#### [!toc] [#Blocklist-_blocked] + + Blocked accounts. An absent account is not blocked. + + +### Witnesses [toc] [#Blocklist-Witnesses] +### Witnesses [!toc] [#Blocklist-Witnesses] + +None. + +### Circuits [toc] [#Blocklist-Circuits] +### Circuits [!toc] [#Blocklist-Circuits] + +#### isBlocked [toc] [#Blocklist-isBlocked] +#### [!toc] [#Blocklist-isBlocked] + + Returns whether `account` is currently blocked. + + Discloses `account` to the ledger. + + +#### assertNotBlocked [toc] [#Blocklist-assertNotBlocked] +#### [!toc] [#Blocklist-assertNotBlocked] + + Asserts that `account` is not blocked. The implementing contract decides when this gate applies. + + Discloses `account` to the ledger. + + Requirements: + + * `account` is not a member of the blocklist. + + +#### _block [toc] [#Blocklist-_block] +#### [!toc] [#Blocklist-_block] + + Adds `account` to the blocklist. Ungated: the implementing contract is responsible for protecting this circuit. + + Discloses `account` to the ledger. + + +#### _unblock [toc] [#Blocklist-_unblock] +#### [!toc] [#Blocklist-_unblock] + + Removes `account` from the blocklist. Ungated: the implementing contract is responsible for protecting this circuit. + + Discloses `account` to the ledger. + diff --git a/content/contracts-compact/api/shielded-access-control.mdx b/content/contracts-compact/api/shielded-access-control.mdx index d9cf7ced..378025a6 100644 --- a/content/contracts-compact/api/shielded-access-control.mdx +++ b/content/contracts-compact/api/shielded-access-control.mdx @@ -8,14 +8,14 @@ This module provides a shielded role-based access control (RBAC) mechanism, where roles can be used to represent a set of permissions. Roles are stored as Merkle tree commitments to avoid disclosing information about role holders. Role commitments are created with the following hashing scheme, -where `‖` denotes concatenation and all values are `Bytes<32>`: +where `||` denotes concatenation and all values are `Bytes<32>`: ``` -roleCommitment := SHA256( role ‖ accountId ‖ instanceSalt ‖ commitmentDomain ) +roleCommitment := SHA256( role || accountId || instanceSalt || commitmentDomain ) -accountId := SHA256( secretKey ‖ instanceSalt ‖ accountIdDomain ) +accountId := SHA256( secretKey || instanceSalt || accountIdDomain ) -roleNullifier := SHA256( roleCommitment ‖ nullifierDomain ) +roleNullifier := SHA256( roleCommitment || nullifierDomain ) commitmentDomain := pad(32, "ShieldedAccessControl:commitment") accountIdDomain := pad(32, "ShieldedAccessControl:accountId") @@ -77,11 +77,11 @@ More complex role relationships can be created by using [_setRoleAdmin](#Shielde ## ShieldedAccessControl [toc] [#ShieldedAccessControl] ```ts -import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl" +import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl"; ``` --- @@ -106,7 +106,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="type" id="ShieldedAccessControl-RoleCommitment" > - A Merkle tree leaf committing a `(role, accountId)` pairing. Computed as `SHA256(role, accountId, instanceSalt, commitmentDomain)`. + A Merkle tree leaf committing a `(role, accountId)` pairing. Computed as `SHA256(role || accountId || instanceSalt || commitmentDomain)`. #### RoleIdentifier [toc] [#ShieldedAccessControl-RoleIdentifier] @@ -126,7 +126,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="type" id="ShieldedAccessControl-AccountIdentifier" > - A privacy-preserving identity commitment. Computed as `SHA256(secretKey, instanceSalt, accountIdDomain)`. + A privacy-preserving identity commitment. Computed as `SHA256(secretKey || instanceSalt || accountIdDomain)`. #### RoleNullifier [toc] [#ShieldedAccessControl-RoleNullifier] @@ -136,12 +136,23 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="type" id="ShieldedAccessControl-RoleNullifier" > - A one-time burn token that permanently invalidates a role commitment on revocation. Computed as `SHA256(roleCommitment, nullifierDomain)`. + A one-time burn token that permanently invalidates a role commitment on revocation. Computed as `SHA256(roleCommitment || nullifierDomain)`. ### Ledger [toc] [#ShieldedAccessControl-Ledger] ### Ledger [!toc] [#ShieldedAccessControl-Ledger] +#### _isInitialized [toc] [#ShieldedAccessControl-_isInitialized] +#### [!toc] [#ShieldedAccessControl-_isInitialized] + + Boolean indicating whether [initialize](#ShieldedAccessControl-initialize) has been called. + The role-management and role-proof circuits require it to be `true`. + + #### _operatorRoles [toc] [#ShieldedAccessControl-_operatorRoles] #### [!toc] [#ShieldedAccessControl-_operatorRoles] - A Merkle tree of role commitments stored as `SHA256(role | accountId | instanceSalt | commitmentDomain)`. - Has a fixed capacity of 2^20 leaf slots. + A Merkle tree of role commitments stored as `SHA256(role || accountId || instanceSalt || commitmentDomain)`. + Has a fixed capacity of 2^20 leaf slots, one per distinct `(role, accountId)` pairing ever granted. + Slots are not reclaimed on revocation. The tree reports occupancy via `isFull()` in circuits and `firstFree()` from TypeScript. #### _adminRoles [toc] [#ShieldedAccessControl-_adminRoles] @@ -173,6 +185,18 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont A set of nullifiers used to prove a role has been revoked. +#### _grantedRoleNullifiers [toc] [#ShieldedAccessControl-_grantedRoleNullifiers] +#### [!toc] [#ShieldedAccessControl-_grantedRoleNullifiers] + + Nullifiers of every `(role, accountId)` pairing ever granted. + Makes a repeated grant a no-op so one pairing can never consume more than one `_operatorRoles` slot, + and gates revocation so a nullifier can only be inserted into `_roleCommitmentNullifiers` for a granted pairing. + + #### _instanceSalt [toc] [#ShieldedAccessControl-_instanceSalt] #### [!toc] [#ShieldedAccessControl-_instanceSalt] ### Witnesses [toc] [#ShieldedAccessControl-Witnesses] ### Witnesses [!toc] [#ShieldedAccessControl-Witnesses] +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + #### wit_getRoleCommitmentPath [toc] [#ShieldedAccessControl-wit_getRoleCommitmentPath] #### [!toc] [#ShieldedAccessControl-wit_getRoleCommitmentPath] Returns a path to a role commitment in the `_operatorRoles` Merkle tree if one exists. Otherwise, returns an invalid path. + The consumer's witness implementation builds this path from the exported `_operatorRoles` ledger state. #### wit_secretKey [toc] [#ShieldedAccessControl-wit_secretKey] @@ -208,6 +238,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont id="ShieldedAccessControl-wit_secretKey" > Returns the user's secret key used in deriving the shielded account identifier. + The key never reaches the ledger; it is hashed with `_instanceSalt` inside the proof. The same key can be used across multiple roles within a contract instance. If a role is revoked and re-granted, a new secret key must be generated to produce a new `accountId`. @@ -231,6 +262,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont secure random number generator (e.g. `crypto.getRandomValues()`) to maintain strong privacy guarantees. + Discloses: `instanceSalt`, written to `_instanceSalt`. + Requirements: * Contract is not initialized. @@ -257,8 +290,11 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont modulePrefix="ShieldedAccessControl" > Reverts if the caller cannot provide a valid proof of ownership for `role`. + The caller's `accountId` is derived from the `wit_secretKey` witness and `_instanceSalt`; + the Merkle path comes from `wit_getRoleCommitmentPath`. - Disclosures: a Merkle tree path to a role commitment, a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. + Discloses: a Merkle tree path to a role commitment, a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. + Every call under the same role instance discloses the same nullifier, so an observer can link those calls over time. Requirements: @@ -278,7 +314,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an unauthorized caller. - Disclosures: a Merkle tree path to a role commitment, a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. + Discloses: a Merkle tree path to a role commitment, a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. + Every call under the same role instance discloses the same nullifier, so an observer can link those calls over time. Requirements: @@ -286,26 +323,6 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont * Caller must not provide a valid Merkle tree path for a different `(role, accountId)` pairing. -#### _uncheckedCanProveRole [toc] [#ShieldedAccessControl-_uncheckedCanProveRole] -#### [!toc] [#ShieldedAccessControl-_uncheckedCanProveRole] - - Returns `true` if the caller proves ownership of `role` and is not revoked. - MAY return false for a legitimately credentialed caller if the proving environment supplies an invalid Merkle path. - This circuit will never return true for an unauthorized caller. - - Disclosures: a Merkle tree path to a role commitment, a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. - - - This circuit does not perform an initialization check. - It is only meant to be used as an internal helper. - Using this circuit outside of the module may cause undefined behavior and break security guarantees. - - - #### grantRole [toc] [#ShieldedAccessControl-grantRole] #### [!toc] [#ShieldedAccessControl-grantRole] Grants `role` to `accountId` by inserting a role commitment unique to the - `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Duplicate role commitments can be issued - so long as they remain unrevoked. Once revoked, a role cannot be re-granted to the same `accountId`. A new + `(role, accountId)` pairing into the `_operatorRoles` Merkle tree and recording its nullifier in `_grantedRoleNullifiers`. + Granting an already active pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new `accountId` must be generated to be re-authorized for a revoked `role`. - Disclosures: a Merkle tree path to a role commitment, a role commitment, a nullifier, and a role identifier. + Discloses: a Merkle tree path to a role commitment, a role commitment (on the first grant only), a nullifier, and a role identifier. Requirements: @@ -341,18 +358,14 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for `role`. - - At this time, proofs of non-membership on values in the Merkle tree are not available, - so a `(role, accountId)` pairing that does not exist can still be revoked. - - - Disclosures: a Merkle tree path to a role commitment, a role commitment, a nullifier, and a role identifier. + Discloses: a Merkle tree path to a role commitment, a role commitment, a nullifier, and a role identifier. Requirements: * Contract is initialized. * Caller must prove they are an admin for `role`. * Caller must not provide a valid Merkle tree path for a different `(role, accountId)` pairing. + * The `(role, accountId)` pairing must have been granted (reverts with `role was never granted` otherwise). * The `(role, accountId)` pairing must not be already revoked. @@ -371,8 +384,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont Roles are often managed via [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole): this circuit's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). + Consumers with no need for self-revocation should not export this circuit. - Disclosures: a nullifier for the respective role commitment. + Discloses: a nullifier for the respective role commitment. Outside observers may be able to use timing and pattern analysis to weaken pseudonymity @@ -382,32 +396,11 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont Requirements: * Contract is initialized. - * The caller must provide a valid `accountId` for the role. + * `accountIdConfirmation` must equal the caller's computed `accountId`. + * The `(role, accountId)` pairing must have been granted (reverts with `role was never granted` otherwise). * The `(role, accountId)` pairing must not be already revoked. -#### _updateRole [toc] [#ShieldedAccessControl-_updateRole] -#### [!toc] [#ShieldedAccessControl-_updateRole] - - Core business logic for the grant/revoke role circuits. - Asserts that the `(role, accountId)` pairing has not already been revoked. - On success, dispatches on `updateType`: a `Grant` inserts the role commitment into `_operatorRoles`, and - a `Revoke` inserts the nullifier into `_roleCommitmentNullifiers`. - - Disclosures: a nullifier for the respective role commitment, and a role commitment (on Grant only). - - - The nullifier is disclosed via `_roleCommitmentNullifiers.member` on every call, - regardless of the update type. - This enables observers to correlate grant and revocation transactions for the same `(role, accountId)` pairing. - - - #### getRoleAdmin [toc] [#ShieldedAccessControl-getRoleAdmin] #### [!toc] [#ShieldedAccessControl-getRoleAdmin] #### _grantRole [toc] [#ShieldedAccessControl-_grantRole] @@ -434,11 +427,12 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont modulePrefix="ShieldedAccessControl" > Grants `role` to `accountId` by inserting a role commitment unique to the - `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. + `(role, accountId)` pairing into the `_operatorRoles` Merkle tree and recording its nullifier in `_grantedRoleNullifiers`. + Granting an already active pairing is a no-op. Internal circuit without access restriction. - Disclosures: a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. + Discloses: a role commitment corresponding to a `(role, accountId)` pairing (on the first grant only), and a nullifier for the respective role commitment. Exposing this circuit directly in an implementing contract would allow anyone to grant @@ -446,10 +440,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots. - Deployers should monitor slot consumption off-chain. Duplicate grants waste tree capacity - but are otherwise benign. A single nullifier invalidates all duplicate commitments. - Implementing contracts are responsible for mitigating tree exhaustion risk. + The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots, one per distinct pairing. + Slots are never reclaimed. Deployers should monitor slot consumption off-chain. Requirements: @@ -472,7 +464,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont Internal circuit without access restriction. - Disclosures: a nullifier for the respective role commitment. + Discloses: a nullifier for the respective role commitment. Exposing this circuit directly in an implementing contract would allow anyone to revoke @@ -482,6 +474,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont Requirements: * Contract is initialized. + * The `(role, accountId)` pairing must have been granted (reverts with `role was never granted` otherwise). * The `(role, accountId)` pairing must not be already revoked. @@ -495,7 +488,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont Sets `adminId` as `role`'s admin identifier. Users with valid admin identifiers may grant and revoke access to the specified `role`. Internal circuit without access restriction. - Disclosures: the role identifier and the admin identifier. + Discloses: the role identifier and the admin identifier, written to `_adminRoles`. Exposing this circuit directly in an implementing contract would allow anyone to assign @@ -507,33 +500,6 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont * Contract is initialized. -#### _validateRole [toc] [#ShieldedAccessControl-_validateRole] -#### [!toc] [#ShieldedAccessControl-_validateRole] - - Verifies whether `accountId` holds `role`. - MAY return false for a legitimately credentialed account - if the proving environment supplies an invalid Merkle path. - - Disclosures: a Merkle tree path to a role commitment, and a nullifier for the respective role commitment. - - - This circuit does not perform an initialization check. - It is only meant to be used as an internal helper. - Using this circuit outside of the module may cause undefined behavior and break security guarantees. - - - The nullifier is disclosed via `_roleCommitmentNullifiers.member` on every call. - Since this circuit is invoked by `assertOnlyRole` and `canProveRole`, - every protected operation discloses the same nullifier, - allowing observers to link all actions performed under the same role instance across time. - - - #### computeRoleCommitment [toc] [#ShieldedAccessControl-computeRoleCommitment] #### [!toc] [#ShieldedAccessControl-computeRoleCommitment] - Computes the role commitment from the given `role` and `accountId`. + Computes the role commitment from the given `role` and `accountId` using the stored `_instanceSalt`. - Role Commitment Derivation: `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)` + Role Commitment Derivation: `roleCommitment = SHA256(role || accountId || instanceSalt || commitmentDomain)` This circuit does not perform an initialization check. It is only meant to be used as @@ -557,32 +523,12 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont #### [!toc] [#ShieldedAccessControl-computeNullifier] Computes the role nullifier for a given `roleCommitment`. - Role Nullifier Derivation: `roleNullifier = SHA256(roleCommitment, nullifierDomain)` - - -#### _computeAccountId [toc] [#ShieldedAccessControl-_computeAccountId] -#### [!toc] [#ShieldedAccessControl-_computeAccountId] - - Computes the caller's account identifier from the `wit_secretKey` witness and the `_instanceSalt`. - - ID Derivation: `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)` - - The result is a 32-byte commitment that uniquely identifies the account. - - - This circuit does not perform an initialization check. - It is only meant to be used as an internal helper. - Using this circuit outside of the module may cause undefined behavior and break security guarantees. - + Role Nullifier Derivation: `roleNullifier = SHA256(roleCommitment || nullifierDomain)` #### computeAccountId [toc] [#ShieldedAccessControl-computeAccountId] @@ -595,10 +541,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont > Computes an `accountId` without on-chain state, allowing a user to derive their shielded identity commitment before submitting it in a grant or revoke operation. - This is the off-chain counterpart to the internal `_computeAccountId` and produces an identical result - given the same inputs. + It produces the same value the module derives internally from `wit_secretKey` and `_instanceSalt`. - ID Derivation: `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)` + ID Derivation: `accountId = SHA256(secretKey || instanceSalt || accountIdDomain)` The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the privacy guarantees of this system. diff --git a/content/contracts-compact/api/utils.mdx b/content/contracts-compact/api/utils.mdx index be5c264a..2df521da 100644 --- a/content/contracts-compact/api/utils.mdx +++ b/content/contracts-compact/api/utils.mdx @@ -11,7 +11,7 @@ This package provides the API for all Utils modules. ## Utils [toc] [#Utils] ```ts @@ -21,7 +21,8 @@ import "./node_modules/@openzeppelin/compact-contracts/utils/Utils"; --- - There's no easy way to get the constraints of circuits at this time so the constraints of the circuits listed below have been omitted. + The circuits below are helpers that are compiled into the circuit that calls them, + so no standalone complexity is listed for them. ### Ledger [toc] [#Utils-Ledger] @@ -41,15 +42,15 @@ None. #### [!toc] [#Utils-isKeyOrAddressZero] Returns whether `keyOrAddress` is the zero address. Midnight's burn address is represented as `left(default)` in Compact, - so we've chosen to represent the zero address as this structure as well. + so the zero address is represented as this structure as well. @@ -57,7 +58,7 @@ None. #### [!toc] [#Utils-isKeyZero] Returns whether `key` is the zero address. @@ -67,8 +68,8 @@ None. #### [!toc] [#Utils-isKeyOrAddressEqual] Returns whether `keyOrAddress` is equal to `other`. @@ -79,8 +80,8 @@ None. #### [!toc] [#Utils-isContractAddress] Returns whether `keyOrAddress` is a `ContractAddress` type. @@ -90,7 +91,7 @@ None. #### [!toc] [#Utils-emptyString] A helper function that returns the empty string: `""`. @@ -100,8 +101,87 @@ None. #### [!toc] [#Utils-canonicalize] Zeroes out the unused side of an `Either` value. Prevents crafted inputs where both `left` and `right` fields carry data from bypassing checks that only inspect the active side. + +#### zeroAccount [toc] [#Utils-zeroAccount] +#### [!toc] [#Utils-zeroAccount] + + Returns the canonical zero `Either, ContractAddress>` value: the left variant (`Bytes<32>`) set to zero. + Used as the sentinel for mint and burn operations (a zero `from` signals a mint, a zero `to` signals a burn) + and for cleared approvals. + + The left variant is returned so that `isTargetZero` compares against `default>` + and so that the value does not trigger contract-address guards. + + +#### isTargetZero [toc] [#Utils-isTargetZero] +#### [!toc] [#Utils-isTargetZero] + + Returns `true` if the active branch of `target` (as indicated by `is_left`) holds the zero value. + + +#### computeAccountId [toc] [#Utils-computeAccountId] +#### [!toc] [#Utils-computeAccountId] + + Computes an account identifier without on-chain state, allowing a user to derive + their identity commitment before submitting an operation. + + Derivation: `accountId = persistentHash(secretKey)`, with no domain-separation tag. + + The absence of a tag is deliberate. The identifier is global by design: the same key material yields the same + identity in every module deriving one, so a user who wishes to carry one identity across modules can. + Modules wanting a per-deployment, unlinkable identity should not use this circuit. + + Not for private derivation. This returns an identifier, not a secret, and the construction is untagged. + A circuit deriving a private value from a secret must use its own domain-separation tag. + + + `secretKey` is a sensitive secret. Never log or persist it in plaintext (browser devtools, application logs, + analytics pipelines, or any observable side-channel). Prefer offline storage or secure enclaves (HSMs, + air-gapped devices, encrypted vaults) over hot storage. Generate it with `crypto.getRandomValues()` or equivalent; + weak or predictable keys can be brute-forced to reveal the identity. Treat key loss as identity loss: a lost key + cannot be recovered. Avoid evaluating this circuit in untrusted environments (an unverified browser extension, + a compromised runtime, or a shared machine) where the key may be exposed to a malicious observer. + + + Requirements: + + * `secretKey` is a 32-byte cryptographically secure random value. + + +#### selfAsRecipient [toc] [#Utils-selfAsRecipient] +#### [!toc] [#Utils-selfAsRecipient] + + Returns the current contract's address as an `Either` + for use as a recipient in shielded send operations (deposits and receiving change). + + +#### UINT128_MAX [toc] [#Utils-UINT128_MAX] +#### [!toc] [#Utils-UINT128_MAX] + + The maximum value representable by a `Uint<128>`: `2^128 - 1`. + diff --git a/content/contracts-compact/confidential-fungible-token.mdx b/content/contracts-compact/confidential-fungible-token.mdx new file mode 100644 index 00000000..012b81bd --- /dev/null +++ b/content/contracts-compact/confidential-fungible-token.mdx @@ -0,0 +1,407 @@ +--- +title: ConfidentialFungibleToken +--- + +{/* links */} +[FungibleToken]: ./fungible-token.mdx +[Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern +[Ownable]: ./ownable.mdx +[crypto guide]: ./crypto +[ElGamal]: api/crypto#ElGamal +[EcdhMask]: api/crypto#EcdhMask +[ConfidentialFungibleToken API]: api/confidential-fungible-token +[EscrowEntry]: api/confidential-fungible-token#ConfidentialFungibleToken-EscrowEntry +[wit_ConfidentialTokenSK]: api/confidential-fungible-token#ConfidentialFungibleToken-wit_ConfidentialTokenSK +[wit_ConfidentialTokenEK]: api/confidential-fungible-token#ConfidentialFungibleToken-wit_ConfidentialTokenEK +[wit_PlaintextBalance]: api/confidential-fungible-token#ConfidentialFungibleToken-wit_PlaintextBalance +[wit_RandomnessSeed]: api/confidential-fungible-token#ConfidentialFungibleToken-wit_RandomnessSeed +[MAX_TRANSFER_VALUE]: api/confidential-fungible-token#ConfidentialFungibleToken-MAX_TRANSFER_VALUE +[initialize]: api/confidential-fungible-token#ConfidentialFungibleToken-initialize +[balanceOf]: api/confidential-fungible-token#ConfidentialFungibleToken-balanceOf +[pendingOf]: api/confidential-fungible-token#ConfidentialFungibleToken-pendingOf +[allowance]: api/confidential-fungible-token#ConfidentialFungibleToken-allowance +[isRegistered]: api/confidential-fungible-token#ConfidentialFungibleToken-isRegistered +[register]: api/confidential-fungible-token#ConfidentialFungibleToken-register +[_mint]: api/confidential-fungible-token#ConfidentialFungibleToken-_mint +[_burn]: api/confidential-fungible-token#ConfidentialFungibleToken-_burn +[_burnFrom]: api/confidential-fungible-token#ConfidentialFungibleToken-_burnFrom +[transfer]: api/confidential-fungible-token#ConfidentialFungibleToken-transfer +[_move]: api/confidential-fungible-token#ConfidentialFungibleToken-_move +[sweep]: api/confidential-fungible-token#ConfidentialFungibleToken-sweep +[approve]: api/confidential-fungible-token#ConfidentialFungibleToken-approve +[transferFrom]: api/confidential-fungible-token#ConfidentialFungibleToken-transferFrom +[clearMemos]: api/confidential-fungible-token#ConfidentialFungibleToken-clearMemos +[computeAccountId]: api/confidential-fungible-token#ConfidentialFungibleToken-computeAccountId +[ConfidentialFungibleTokenPublicSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply +[_addSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply-_addSupply +[_subSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply-_subSupply +[totalSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply-totalSupply + +ConfidentialFungibleToken is an account-based fungible token whose balances live on the public ledger +as [ElGamal] ciphertexts over the Jubjub curve, so no amount is ever written in the clear. +Accounts are identified the same way as in [FungibleToken], by a 32-byte `accountId` derived from a secret key, +but every value a circuit moves is encrypted under the account holder's own encryption key, +and the caller proves in zero knowledge that it knows the plaintext of the balance it spends from. +Transfers hide the amount; the (sender, recipient) pair is public. +Total-supply tracking is optional and lives in the separate [ConfidentialFungibleTokenPublicSupply] extension. + +The encryption primitives (`ElGamal` and `EcdhMask`) are documented in the [crypto guide]. +This page covers how the token composes them. + +## The account model + +### What is on the public ledger + +Every ledger field of the module is exported, so an observer can read all of the following directly from state: + +* **Encryption keys** (`_encryptionKeys`): `accountId` to the account's ElGamal public key, published by [register]. +* **Spendable balances** (`_balances`): one ElGamal ciphertext per registered account. + Debits (transfers out, approvals, burns) draw from this cell. +* **Pending balances** (`_pending`): a second ciphertext per account where incoming credits land. + Only the owner moves pending into spendable, with [sweep]. +* **Memos** (`_memos`): per account, a list of [EcdhMask] ciphertexts, one per credit received. + Each one carries the credited amount encrypted to the recipient's key; this is how a recipient learns what it was sent. +* **Escrow entries** (`_escrow`): per (owner, spender), an [EscrowEntry] holding the encrypted allowance. +* **Credit counters** (`_creditEpochs`): per account, the lifetime number of credits received. It never resets. +* **Escrow-spend counters** (`_escrowSpendEpochs`): per (owner, spender), the number of times the escrow was drawn on. It never resets. +* **Metadata** (`_name`, `_symbol`, `_decimals`) and the `_isInitialized` flag. + +The module keeps no nullifier sets; the two counters above are the only monotonic state it writes, +and both exist to keep per-operation randomness distinct (see below), not to prevent replay. + +What an observer learns from this is the set of registered `accountId`s, which accounts transact with which, +how many credits each account has received, and how often each escrow has been spent from. +What an observer does not learn is any balance, any transferred amount, or any allowance cap, +unless the deployment also tracks public supply (see [Minting, burning and supply](#minting-burning-and-supply)). + +### What the user holds privately + +Four witnesses feed the circuits. Each is listed with how (or whether) the circuit verifies it. + +* [wit_ConfidentialTokenSK] returns the **account secret**. The circuit computes `accountId = H(SK)` + and uses that id for every ledger lookup, so a wrong secret resolves to a different account and the call fails. +* [wit_ConfidentialTokenEK] returns the **encryption secret**. The circuit re-derives the ElGamal public key from it + and compares against the key stored at registration, either through the balance proof or directly on + [sweep] and [clearMemos], which have no plaintext to prove. +* [wit_PlaintextBalance] returns the wallet's **cached plaintext** for a given ciphertext. + The ciphertext is passed in as the argument so the wallet can route by ciphertext identity rather than by call order. + The circuit verifies the claim with `ElGamal_assertDecryptsTo`; a wrong value fails the proof. +* [wit_RandomnessSeed] returns a 32-byte **randomness seed** from which every per-operation random scalar + and every memo ephemeral is expanded in-circuit. Unlike the other three, this witness is **not** verified. + +The same key material may back both the account secret and the encryption secret, or both may be derived from one master secret: +the account id is an untagged hash of the SK, while the encryption scalar is a domain-separated hash of the EK, +so publishing the id does not reveal the encryption key. + + + Seed freshness is load-bearing for confidentiality on every path. The seed-to-randomness expansion is deterministic + and public, so anyone who knows a seed can strip the memo mask and read the delivered amount, and can recover a balance + or escrow amount from its public ciphertext. Reusing a seed across two operations leaks the difference between their amounts, + and the circuit does not prevent reuse. A wallet must supply a fresh, high-entropy, secret seed on every invocation. + On `transferFrom` and `burnFrom` the seed is the spender's but the ciphertexts it randomizes belong to the owner, + so a careless spender leaks a counterparty's balance, not their own. + + +The TypeScript witnesses in the repository are test-only and are not published with the package; +in particular the test seed witness returns a fixed value. A consumer authors its own witnesses, +backed by the user's wallet, and the seed witness must draw from a cryptographically secure random source. + +### Balance claims and the per-operation bound + +Every value-bearing circuit takes a `Uint<128>` amount and checks it against [MAX_TRANSFER_VALUE]. +A balance, however, is the homomorphic sum of every credit it has ever received and has no running total to bound it, +so the plaintext a wallet claims through [wit_PlaintextBalance] is a `Uint<248>`, wider than any single amount. +A balance accumulated past the per-operation bound therefore stays spendable. +Escrow claims are narrowed back to `Uint<128>`, since an allowance is set by one `approve` and only ever reduced. + +## Registration + +[register] derives the caller's ElGamal public key from the encryption secret, stores it under the caller's `accountId`, +and initializes both the spendable and the pending balance to an encryption of zero. +It discloses the `accountId` and the public key. Registration is required before an account can send **or** receive: +a credit needs the recipient's public key to encrypt under. + +Before any on-chain call, a wallet derives its own id with the pure [computeAccountId] circuit (no proof needed): + +```ts +const accountId = Token_computeAccountId(secretKey); +``` + +The id is global: the same secret produces the same `accountId` in every module that derives it this way +(`Utils_computeAccountId` is the shared implementation). + +## Transfers, pending balances and memos + +[transfer] does three things in one proof: + +1. **Debit.** The caller proves the plaintext of its spendable balance, the circuit checks it is at least `value`, + and the balance ciphertext is replaced by a homomorphic subtraction of `value` under fresh randomness. +2. **Credit.** The recipient's *pending* ciphertext is replaced by a homomorphic addition of `value`. + The recipient's credit counter is incremented and folded into the randomness, so two credits never share randomness + even if a sender reuses a seed. +3. **Memo.** An [EcdhMask] ciphertext carrying `value`, encrypted to the recipient's public key, is pushed to the front of + the recipient's memo list. + +What reaches the ledger: the sender's `accountId`, the recipient's `accountId`, two new ciphertexts and one memo. +The amount is not disclosed. Self-transfer is rejected. + +### How a recipient learns of incoming funds + +The on-chain balance ciphertext is exponential ElGamal (it encrypts `g^value`), so a wallet cannot cheaply decrypt it. +Instead the wallet scans its own `_memos` list off-chain, decrypts each new entry directly with its encryption secret +(no discrete-log search and no bounded-value table), and adds the amounts to a local plaintext cache. + +Memos record credits only. Debits (sends, approvals, burns) move value with no memo, so summing memos overcounts an account +that has ever spent. The wallet's running cache, not memo replay, is the authoritative balance, +and it is what the wallet returns from [wit_PlaintextBalance]. + +### Sweeping pending into spendable + +Credits land in `_pending`, not in `_balances`, so a third party cannot change the ciphertext an owner is about to prove against. +Without this split an attacker could send dust to invalidate a victim's in-flight spend proofs. +The owner calls [sweep] when ready: it homomorphically adds the two ciphertexts, resets pending to an encryption of zero, +and needs no plaintext (the wallet already learned the pending amounts from its memos). +Only the owner can sweep, since the circuit checks both the account secret and the encryption secret. +A wallet's total holding is `balanceOf + pendingOf`. + +### The conserving primitive + +[_move] is [transfer] without the self-transfer guard: it debits the caller and credits `to` by the same amount, net zero, +and never changes supply. It is the building block for custom value flows (fees, splits, settlement), +and it stays permissive so a composing contract can set its own policy. + +### Pruning memos + +A wallet that has folded its memos into a durable cache can call [clearMemos] to empty its list on chain. +The call takes the caller's view of its credit counter and is rejected if a credit landed in between, +so a memo the wallet has not yet seen is never discarded. + + + Clearing memos is destructive. Spending requires proving the balance plaintext, and the plaintext can only be + reconstructed from the memos or from the wallet's cache. Clear only memos already folded into a durable, backed-up cache; + otherwise the balance becomes unspendable, not merely undisplayable. + + +## Approvals and escrow + +An allowance is not a public cap. [approve] moves `value` out of the caller's spendable balance into an escrow entry +that holds two encryptions of the same amount, one under the spender's key and one under the owner's, +plus an owner-readable memo of the current remaining allowance: + +``` +EscrowEntry { spenderCt, ownerCt, ownerMemo } +``` + +Re-approving first refunds any prior escrow for that spender back to the owner's balance (a homomorphic add of `ownerCt`, +no plaintext needed) and then escrows the new amount, so allowances replace rather than stack. +Revoking is `approve(spender, 0)`. What reaches the ledger: the owner's and spender's ids, the owner's new balance ciphertext, +and the entry; the cap is not disclosed. + +[transferFrom] is called by the spender. It proves the plaintext of `spenderCt`, checks it is at least `value`, +reduces **both** escrow copies by `value`, refreshes `ownerMemo` with the new remaining amount +(computed in-circuit from the verified plaintext, so the spender cannot forge it), increments the (owner, spender) spend counter, +and credits `to` exactly as [transfer] does. [_burnFrom] is the same escrow spend with no credit. + +### What a partial spend leaves behind + +After a spender draws part of an allowance, the ledger holds a reduced `spenderCt` and `ownerCt` that still encrypt the same +remaining amount, a fresh `ownerMemo` stating that remainder, and an incremented spend counter. +The owner receives no balance memo (its spendable balance did not change) and instead decrypts `ownerMemo` +to learn how much is left in escrow. That is what lets the owner prove its post-refund balance on the next `approve`, +when the remainder flows back into `_balances`. Without the memo a partial spend would strand the escrow. + +[allowance] returns the entry, or a well-formed encrypted-zero entry when none exists. "No allowance" and "an allowance of zero" +read the same through the getter; a caller that must distinguish them reads `_escrow` membership off chain. +Granting nothing is safe because the spend path gates on escrow membership independently. + +## Minting, burning and supply + +The base module ships three supply-changing building blocks and no supply accounting: + +* [_mint] credits `value` to `account` (with a memo), exactly like the credit half of a transfer. +* [_burn] debits `value` from the caller's spendable balance. +* [_burnFrom] consumes `value` from an escrow the owner granted the caller. + +None of them touches a total. Pair them with [ConfidentialFungibleTokenPublicSupply] when the deployment tracks supply: +its `_totalSupply` ledger field is a public `Uint<128>`, and [_addSupply] / [_subSupply] disclose each mint and burn amount +through the delta on that value. Balances stay confidential; only the aggregate is visible. This is the right default +for issuers who must attest to a circulating supply. The extension imports no token module, needs no initialization, +and the pairing of each token operation with its accounting block is the composing contract's responsibility. + + + Never re-export `_mint`, `_burn`, or `_burnFrom` from a deployed contract. Compact cannot restrict a re-export, + so a raw `_mint` is a permissionless mint, a raw `_burn` a permissionless burn, and a raw `_burnFrom` destroys escrowed + value with no supply decrement. Expose only the conserving surface (`transfer`, `_move`, `approve`, `transferFrom`, `sweep`) + plus your own gated `mint` / `burn` / `burnFrom` wrappers. + + +## Gating on the caller + +A composing contract that enforces policy on the caller (freeze lists, KYC, pause) cannot read the caller's identity itself: +the caller is derived from a witness inside the module, and a second witness call in the wrapper could return a different secret. +For that reason every caller-authenticating circuit ([register], [_burn], [transfer], [_move], [approve], [transferFrom], +[sweep], [_burnFrom]) returns the `accountId` it authenticated. Gate on the returned value after the call; +an assertion that fails anywhere in a circuit invalidates the whole transaction, so a post-call check still blocks the operation. +The returned id is always the caller (sender, approver, spender, or registrant), never `to` or `fromAddress`, +which are explicit arguments a wrapper gates directly. + +Accounts are externally owned. A contract has no private state in which to hold an encryption secret, +so it could be credited but could never prove a balance or scan memos; the module therefore keys everything by a `Bytes<32>` +account id and offers no `ContractAddress` variant. A protocol that needs to hold these tokens uses an account it controls +through `approve` / `transferFrom`. + +## Usage + +The following contract composes the token with the public-supply extension and [Ownable], +pairing every supply change with its accounting block and restricting `mint` to the contract owner. +It re-exports the module's ledger so wallets can read ciphertexts and memos off chain, +and it exposes the conserving surface plus gated supply wrappers. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibleToken" + prefix Token_; +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/ConfidentialFungibleTokenPublicSupply" + prefix Supply_; +import "./node_modules/@openzeppelin/compact-contracts/crypto/ElGamal" + prefix ElGamal_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +// Types wallets need to read balances and escrow entries. +export { Token_EscrowEntry, ElGamal_Ciphertext }; + +// Surface the module's ledger so wallets can scan memos and read ciphertexts +// without a proof. +export { + Token__balances, + Token__pending, + Token__encryptionKeys, + Token__memos, + Token__escrow, + Token__creditEpochs, + Token__name, + Token__symbol, + Token__decimals, + Supply__totalSupply +}; + +constructor( + name_: Opaque<"string">, + symbol_: Opaque<"string">, + decimals_: Uint<8>, + issuer: Either, ContractAddress> +) { + Token_initialize(name_, symbol_, decimals_); + Ownable_initialize(issuer); +} + +// Views + +export circuit totalSupply(): Uint<128> { + return Supply_totalSupply(); +} + +export circuit balanceOf(account: Bytes<32>): ElGamal_Ciphertext { + return Token_balanceOf(account); +} + +export circuit pendingOf(account: Bytes<32>): ElGamal_Ciphertext { + return Token_pendingOf(account); +} + +export circuit allowance(owner: Bytes<32>, spender: Bytes<32>): Token_EscrowEntry { + return Token_allowance(owner, spender); +} + +export circuit isRegistered(account: Bytes<32>): Boolean { + return Token_isRegistered(account); +} + +export pure circuit computeAccountId(sk: Bytes<32>): Bytes<32> { + return Token_computeAccountId(sk); +} + +// Registration and wallet maintenance + +export circuit register(): Bytes<32> { + return Token_register(); +} + +export circuit sweep(): Bytes<32> { + return Token_sweep(); +} + +export circuit clearMemos(expectedEpoch: Uint<64>): [] { + Token_clearMemos(expectedEpoch); +} + +// Conserving value operations + +export circuit transfer(to: Bytes<32>, value: Uint<128>): Bytes<32> { + return Token_transfer(to, value); +} + +export circuit approve(spender: Bytes<32>, value: Uint<128>): Bytes<32> { + return Token_approve(spender, value); +} + +export circuit transferFrom( + fromAddress: Bytes<32>, + to: Bytes<32>, + value: Uint<128> +): Bytes<32> { + return Token_transferFrom(fromAddress, to, value); +} + +// Supply operations: each token building block is paired with its +// accounting block, so sum(balances) == totalSupply holds by construction. + +export circuit mint(account: Bytes<32>, value: Uint<128>): [] { + Ownable_assertOnlyOwner(); + Supply__addSupply(value); + Token__mint(account, value); +} + +export circuit burn(value: Uint<128>): Bytes<32> { + const accountId = Token__burn(value); + Supply__subSupply(value); + return accountId; +} + +export circuit burnFrom(fromAddress: Bytes<32>, value: Uint<128>): Bytes<32> { + const spenderId = Token__burnFrom(fromAddress, value); + Supply__subSupply(value); + return spenderId; +} +``` + +A few points about this layout: + +* `mint` is gated on the contract owner. `burn` and `burnFrom` need no extra gate: the module already authenticates + the caller as the account (or the approved spender) whose value is destroyed, and both return that `accountId` + for any further policy check. +* `name`, `symbol`, and `decimals` are read from the exported ledger rather than through getter circuits. +* The raw `_mint`, `_burn`, and `_burnFrom` building blocks are not re-exported; see the callout above. + +### Wallet-side flow + +A wallet integrating this contract typically: + +1. Derives `accountId` with `computeAccountId(sk)` and calls `register()` once. +2. Scans `_memos[accountId]` after each block, decrypts new entries with the encryption secret, and updates its plaintext cache. +3. Calls `sweep()` to make pending credits spendable. +4. Supplies, through its witnesses, the account secret, the encryption secret, the cached plaintext for whichever ciphertext + the circuit asks about, and a fresh seed on every call. +5. After a partial `transferFrom` against an allowance it granted, decrypts the escrow entry's `ownerMemo` to learn the remainder. + +## Known limitations + +* Two credits to the same recipient in the same block conflict, because each credit reads and advances the recipient's + credit counter. Credits to one account serialize. +* Anyone can append memos to any registered account by sending it dust, and only the account owner can prune the list. +* The value circuits are large. A deployment that exposes the full surface may exceed a network's per-transaction byte budget; + confirm deployability against the target network and expose only the operations the contract needs. +* Contract-to-contract holdings are out of scope, as described under [Gating on the caller](#gating-on-the-caller). diff --git a/content/contracts-compact/crypto.mdx b/content/contracts-compact/crypto.mdx new file mode 100644 index 00000000..d110b017 --- /dev/null +++ b/content/contracts-compact/crypto.mdx @@ -0,0 +1,261 @@ +--- +title: Crypto +--- + +{/* links */} +[ConfidentialFungibleToken guide]: ./confidential-fungible-token +[ElGamal API]: api/crypto#ElGamal +[EcdhMask API]: api/crypto#EcdhMask +[secretToScalar]: api/crypto#ElGamal-secretToScalar +[derivePk]: api/crypto#ElGamal-derivePk +[expandRandomness]: api/crypto#ElGamal-expandRandomness +[encrypt]: api/crypto#ElGamal-encrypt +[encryptPoint]: api/crypto#ElGamal-encryptPoint +[encryptZero]: api/crypto#ElGamal-encryptZero +[add]: api/crypto#ElGamal-add +[sub]: api/crypto#ElGamal-sub +[scalarMul]: api/crypto#ElGamal-scalarMul +[addEncrypted]: api/crypto#ElGamal-addEncrypted +[subEncrypted]: api/crypto#ElGamal-subEncrypted +[rerandomize]: api/crypto#ElGamal-rerandomize +[assertKeyPair]: api/crypto#ElGamal-assertKeyPair +[assertDecryptsTo]: api/crypto#ElGamal-assertDecryptsTo +[assertDecryptsToPoint]: api/crypto#ElGamal-assertDecryptsToPoint +[kdf]: api/crypto#EcdhMask-kdf +[EcdhMask.encrypt]: api/crypto#EcdhMask-encrypt +[EcdhMask.decrypt]: api/crypto#EcdhMask-decrypt + +The Crypto directory provides two stateless encryption primitives over Midnight's embedded Jubjub curve: +ElGamal, an additively homomorphic encryption of small integers, and EcdhMask, +a one-time pad derived from an ECDH key exchange that delivers a value to a recipient directly. +Both are pure modules: they hold no ledger state, declare no witnesses, +and take their keys, plaintexts, and randomness as explicit arguments. +The importing contract decides what is stored, what is disclosed, and where its secrets come from. + +The [ConfidentialFungibleToken guide] shows the two primitives working together end to end: +ElGamal keeps every balance as a ciphertext that the contract can update without decrypting it, +and EcdhMask hands each recipient the plaintext amount of an incoming transfer. + +## ElGamal + +The [ElGamal API] implements single-receiver ElGamal encryption over Jubjub. +A ciphertext is a pair of curve points `(c1, c2)` with `c1 = g^r` and `c2 = pk^r * m`, +where `g` is the Jubjub generator, `pk` is the recipient's public key, `r` is the encryption randomness, +and `m` is the message point. + +The module offers the scheme in two forms. + +* ***Exponential (lifted) form*** ([encrypt], [assertDecryptsTo]): a `Uint<128>` value `v` is encrypted as the point `g^v`. +Because the message sits in the exponent, ciphertexts combine arithmetically: +[add] turns ciphertexts of `a` and `b` into a ciphertext of `a + b`, [sub] yields `a - b`, +and [scalarMul] multiplies the plaintext by a public scalar. +[addEncrypted] and [subEncrypted] fold a known plaintext into an existing ciphertext in one call. +Recovering `v` from `g^v` is a discrete-log search, so the lifted form only works for values small enough to search; +the caller is responsible for keeping every encrypted value inside that bound. +* ***General point form*** ([encryptPoint], [assertDecryptsToPoint]): an arbitrary Jubjub point is encrypted as is. +There is no bound to respect, at the cost of decryption recovering only a point, never a scalar. + +[rerandomize] produces a fresh ciphertext of the same plaintext under the same key +(it homomorphically adds an encryption of zero), so a stored ciphertext can be made unlinkable to its previous form +without changing what it encrypts. +[encryptZero] returns the fixed pair of identity points `(O, O)`, a convenient initial value for a fresh slot. + + +`encryptZero()` is a constant, not a hiding encryption. +A slot still holding it is publicly recognizable as never written. +Credit it with fresh randomness, or rerandomize it, before treating it as private. + + +### Decryption by assertion + +No circuit returns a plaintext: a circuit cannot run the discrete-log search that recovers `v` from `g^v`. +Instead, the owner's wallet keeps the plaintext of every ciphertext it holds +and supplies it to the circuit through a witness, and the circuit calls [assertDecryptsTo] +to prove that the claimed plaintext is correct: it re-derives `pk` from the secret `ek`, +computes `c2 - ek * c1`, and asserts that the result equals `g^claimedValue`. +A wrong secret fails the key check; a wrong plaintext fails the decryption check. +[assertKeyPair] performs the key check alone, which lets a caller prove ownership of a public key +without touching any ciphertext. + +`claimedValue` is a `Uint<248>` so that a balance accumulated through many homomorphic additions +never outgrows the claim. The comparison happens on the curve, so two claims that are congruent modulo +the Jubjub subgroup order `ℓ` are indistinguishable; `Uint<248>` stays below `ℓ`, so the claim domain is sound. + +### Keys and randomness + +The secret in this scheme is a 32-byte value `ek`. Everything else is derived from it. + +* [secretToScalar] maps the secret to a Jubjub scalar: `degradeToTransient(H("ElGamal:secretToScalar" || ek))`, +where the tag is the 32-byte padded string and `H` is `persistentHash`. +`degradeToTransient` truncates the hash to 31 bytes, which guarantees a valid Jubjub scalar. +The tag is placed first so this derivation can never coincide with a call to [expandRandomness]. +It also keeps the encryption scalar unrelated to an account identifier computed as `H(secret)`, +so a wallet may derive both roles from common key material. +* [derivePk] computes the public key `pk = g^secretToScalar(ek)`. The public key is safe to publish +and is normally stored on the ledger next to the ciphertexts encrypted under it. +* [expandRandomness] derives encryption randomness from a seed: `degradeToTransient(H(seed || tag))`. +One secret seed per circuit invocation can therefore yield as many distinct scalars as the circuit needs, +one per `tag`. + +Every encryption takes its randomness `r` as a `Field` argument, and the module holds no record of which values have been used. +The caller owns three obligations that no circuit can check: + +* `r` must be a valid Jubjub scalar (strictly less than `ℓ`); larger values fault `ecMul`. +[expandRandomness] and [secretToScalar] always satisfy this. +* `r` must be secret. Anyone who learns `r` for a ciphertext computes `pk^r` and strips the mask. +* `r` must be fresh for every ciphertext. Reusing `r` under the same `pk` repeats `c1`, +and `c2 - c2'` then reveals the difference of the two plaintexts. +In practice this means the witness seed must be fresh per invocation and the tag distinct per encryption within an invocation. + +The module does reject the two inputs that would zero the mask altogether: `pk` equal to the identity point and `r = 0`. +Both are asserted in [encryptPoint], which every encryption path goes through. + +## EcdhMask + +The [EcdhMask API] is the direct-decrypt counterpart to ElGamal. Where exponential ElGamal lets a contract add ciphertexts +but makes recovery a search, EcdhMask delivers a `Uint<128>` value to whoever holds the secret behind a Jubjub public key, +with no bound on the value and no search to recover it. The construction, for a recipient key `pk = g^ek` and a fresh ephemeral scalar `e`: + +``` +E = g^e (ephemeral public key, published with the ciphertext) +S = pk^e (ECDH shared secret point) +mask = kdf(S, domain) +ct = value + mask (one-time pad in the field) +``` + +[kdf] hashes the shared point and the consumer's domain tag: `degradeToTransient(H(H(S) || domain))`, +with `H` again `persistentHash`. The consumer chooses `domain` and must use the same value on both sides. +[EcdhMask.encrypt] returns the `Ciphertext { ephemeralPk: E, ct }`; the recipient recovers `S = E^ek` and subtracts the mask with [EcdhMask.decrypt], +passing its secret as the scalar produced by ElGamal's [secretToScalar]. +`decrypt` is a pure circuit meant for wallets and off-chain code: reading a received value needs no proof, +and the on-chain path only ever calls `encrypt`. + +The same Jubjub key may serve both primitives, as the confidential token does. +The schemes share only the public key, the KDF is domain-separated and keyed by the secret point, +and neither exposes a decryption oracle. + + +The ephemeral scalar `e` is a one-time pad key. It must be unique per ciphertext AND secret. +Reusing `e` to the same recipient repeats the mask, and subtracting the two ciphertexts recovers both values. +A unique but predictable `e` (a counter, a public nonce) is just as fatal, because `pk` is public and anyone can recompute `pk^e`. +Derive `e` the way you derive ElGamal randomness: from a secret, per-invocation seed, with a distinct tag per memo. + + +The pad is malleable and the module adds no authentication. That is safe when the ciphertext is computed inside the same circuit +that commits to the value elsewhere and is immutable once written, which is the intended on-chain use. +A consumer with mutable storage or an in-transit attacker in its threat model must add its own authentication. +As with ElGamal, the weak inputs that would zero the shared secret are rejected: `encrypt` asserts that `recipientPk` is not the identity +and that the ephemeral point `g^e` is not the identity (which covers `e = 0` and any `e` congruent to zero modulo `ℓ`). + +## Jubjub facts a consumer must respect + +Both modules rely on two properties of Midnight's embedded-curve arithmetic. + +* ***Every point is constrained into the prime-order subgroup.*** The runtime assigns each `JubjubPoint` reaching a curve operation +by cofactor clearing at the constraint level, so an off-curve, low-order, or mixed-order point makes the circuit unsatisfiable. +This holds for points a caller passes as arguments or witnesses (a recipient `pk`, the message point of `encryptPoint`), +which is why the modules perform no in-circuit membership check and why the homomorphic algebra is sound. +The identity point is a valid subgroup member, so it is NOT caught by this guarantee; that is why the modules assert against it explicitly. +* ***Negation is scalar multiplication by `ℓ - 1`, never by `-1`.*** A `Field` literal `-1` is the BLS scalar modulus minus one, +which exceeds the Jubjub scalar order and faults `ecMul`. For any `P` in the subgroup, `(ℓ - 1) * P = -P`, and that identity is what +`negate`, `sub`, and the decryption check build on. Do not multiply by `ℓ` itself either: it is out of range and faults as well. +A consumer that needs to negate a point of its own should follow the same pattern. + +## Usage + +Both modules are imported like any other module; prefix them with `ElGamal_` and `EcdhMask_` to avoid circuit signature clashes. +Each module exports a `Ciphertext` struct, which the prefix turns into `ElGamal_Ciphertext` and `EcdhMask_Ciphertext`. + +The witnesses below are declared by the importing contract; this library ships no TypeScript witness implementations +(the ones in the repository are test-only), so consumers author their own. + +### An encrypted single-owner balance + +The contract stores the owner's public key and one ElGamal ciphertext on the ledger. +The owner's wallet holds the encryption secret, the current plaintext, and a fresh randomness seed per call, +and supplies them through witnesses. +`credit` takes a public `value` and adds it to the ciphertext without knowing the balance; +`debit` proves the claimed balance is correct before subtracting. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/crypto/ElGamal" + prefix ElGamal_; + +export ledger _ownerPk: JubjubPoint; +export ledger _balance: ElGamal_Ciphertext; + +// The owner's 32-byte encryption secret. +witness wit_EncryptionKey(): Bytes<32>; +// The wallet's cached plaintext for `ct`. +witness wit_Plaintext(ct: ElGamal_Ciphertext): Uint<248>; +// A fresh, secret 32-byte seed for every call. +witness wit_RandomnessSeed(): Bytes<32>; + +export circuit register(): [] { + const pk = ElGamal_derivePk(wit_EncryptionKey()); + _ownerPk = disclose(pk); + _balance = ElGamal_encryptZero(); +} + +export circuit credit(value: Uint<128>): [] { + const r = ElGamal_expandRandomness(wit_RandomnessSeed(), pad(32, "credit")); + _balance = disclose(ElGamal_addEncrypted(_balance, _ownerPk, value, r)); +} + +export circuit debit(value: Uint<128>): [] { + const ek = wit_EncryptionKey(); + const ct = _balance; + const plaintext = wit_Plaintext(ct); + // Binds the private `ek` and `plaintext` to the public `_ownerPk` and `ct`. + ElGamal_assertDecryptsTo(ct, _ownerPk, ek, plaintext); + assert(plaintext >= value, "Balance: insufficient balance"); + + const r = ElGamal_expandRandomness(wit_RandomnessSeed(), pad(32, "debit")); + _balance = disclose(ElGamal_subEncrypted(ct, _ownerPk, value, r)); +} +``` + +What reaches the ledger: the public key and each new ciphertext. The `value` argument of an exported circuit is public, +so these circuits hide the running balance, not the individual amounts; +the confidential token layers escrow and memos on top of this pattern to hide amounts as well. +The secret, the plaintext, and the seed never leave the witness, and `assertDecryptsTo` is what makes a dishonest plaintext unprovable. + +### Delivering a value with a memo + +A sender who credits a recipient's ciphertext has no way to tell the recipient how much arrived, since the recipient would +otherwise face a discrete-log search over its own balance. An EcdhMask memo solves that: the circuit encrypts the amount +to the recipient's public key and appends it to a per-recipient list the recipient's wallet reads and decrypts off chain. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/crypto/ElGamal" + prefix ElGamal_; +import "./node_modules/@openzeppelin/compact-contracts/crypto/EcdhMask" + prefix EcdhMask_; + +export ledger _recipientPks: Map, JubjubPoint>; +export ledger _memos: Map, List>; + +witness wit_RandomnessSeed(): Bytes<32>; + +export circuit notify(recipient: Bytes<32>, value: Uint<128>): [] { + const pk = _recipientPks.lookup(disclose(recipient)); + // Secret, fresh per call, distinct tag per memo within a call. + const e = ElGamal_expandRandomness(wit_RandomnessSeed(), pad(32, "memo")); + const memo = EcdhMask_encrypt(pk, value, e, pad(32, "MyApp:memo:v1")); + + if (!_memos.member(disclose(recipient))) { + _memos.insert(disclose(recipient), default>); + } + _memos.lookup(disclose(recipient)).pushFront(disclose(memo)); +} +``` + +The recipient's wallet recovers the amount with the pure circuit +`EcdhMask_decrypt(memo, ElGamal_secretToScalar(ek), pad(32, "MyApp:memo:v1"))`, using the same domain tag. +Only the memo (the ephemeral point and the masked field element) reaches the ledger. diff --git a/content/contracts-compact/extensibility.mdx b/content/contracts-compact/extensibility.mdx index 08531c39..f030f54e 100644 --- a/content/contracts-compact/extensibility.mdx +++ b/content/contracts-compact/extensibility.mdx @@ -62,9 +62,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" prefix FungibleToken_; constructor( - _name: Maybe>, - _symbol: Maybe>, - _decimals:Uint<8>, + _name: Opaque<"string">, + _symbol: Opaque<"string">, + _decimals: Uint<8>, _owner: Either, ContractAddress> ) { FungibleToken_initialize(_name, _symbol, _decimals); @@ -113,12 +113,12 @@ export circuit transfer( } export circuit transferFrom( - from: Either, ContractAddress>, + fromAddress: Either, ContractAddress>, to: Either, ContractAddress>, value: Uint<128> ): Boolean { Pausable_assertNotPaused(); - return FungibleToken_transferFrom(from, to, value); + return FungibleToken_transferFrom(fromAddress, to, value); } export circuit approve( diff --git a/content/contracts-compact/fungible-token.mdx b/content/contracts-compact/fungible-token.mdx index 645f2777..cb53b204 100644 --- a/content/contracts-compact/fungible-token.mdx +++ b/content/contracts-compact/fungible-token.mdx @@ -7,6 +7,11 @@ title: FungibleToken [EIP-20]: https://eips.ethereum.org/EIPS/eip-20 [Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern [_mint]: api/fungible-token#FungibleToken-_mint +[transfer]: api/fungible-token#FungibleToken-transfer +[approve]: api/fungible-token#FungibleToken-approve +[transferFrom]: api/fungible-token#FungibleToken-transferFrom +[wit_FungibleTokenSK]: api/fungible-token#FungibleToken-wit_FungibleTokenSK +[computeAccountId]: api/utils#Utils-computeAccountId FungibleToken is a specification for [fungible tokens], a type of token where all the units are exactly equal to each other. @@ -43,6 +48,47 @@ The `unsafe` circuits will eventually be deprecated after Compact supports contr `transfer`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. +## Identity Model + +FungibleToken uses a witness-derived identity scheme. Each caller proves knowledge of a secret key +by injecting it through the [wit_FungibleTokenSK] witness. The module computes the caller's account identifier +as `persistentHash(secretKey)`, a commitment that hides the key while providing a stable, pseudonymous on-chain identity. +That identifier is the `Bytes<32>` (left) branch of the `Either, ContractAddress>` account type +used by [transfer], [approve], and [transferFrom] to locate the caller's balance and allowances. + +What is public on the ledger: + +* `_balances`, keyed by account identifier (or contract address), and `_totalSupply`. +* `_allowances`, keyed by owner and then spender. +* `_name`, `_symbol`, and `_decimals`. + +What the caller holds privately: + +* The 32-byte `secretKey`. It never reaches the ledger; only its hash does. + +A transfer discloses the caller's account identifier, the recipient, and the amount, because the balance map is updated with those values. +An approval discloses the owner's identifier, the spender, and the allowance. + +Because the identifier is `H(secretKey)` with no per-deployment salt, the same key produces the same identity in every contract. +Users who want cross-contract unlinkability can use a different secret key per contract at the wallet layer. + +To derive an account identifier off-chain (for example, to pass as the `recipient` of a mint), use [computeAccountId] from the Utils module: + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" prefix Utils_; + +const accountId = Utils_computeAccountId(secretKey); +``` + +The TypeScript witness implementation lives with the DApp, not with the library. +The witnesses in the `compact-contracts` repository are test-only and are not published; +consumers author their own `wit_FungibleTokenSK` that returns the caller's 32-byte secret key. + + + The `secretKey` must never be logged or persisted in plaintext. Loss of the key means loss of the tokens held under that identity, + and key exposure allows impersonation. Use `crypto.getRandomValues()` or equivalent to generate keys. + + ## Usage Import the FungibleToken module into the implementing contract. diff --git a/content/contracts-compact/index.mdx b/content/contracts-compact/index.mdx index 1963a214..896cae91 100644 --- a/content/contracts-compact/index.mdx +++ b/content/contracts-compact/index.mdx @@ -159,17 +159,20 @@ In the project root, compile the contract. ```bash % compact compile MyContract.compact artifacts/MyContract -Compiling 11 circuits: +Compiling 14 circuits: circuit "allowance" (k=11, rows=1352) - circuit "approve" (k=13, rows=3075) + circuit "approve" (k=13, rows=3080) circuit "balanceOf" (k=10, rows=673) circuit "decimals" (k=6, rows=28) circuit "name" (k=6, rows=28) + circuit "owner" (k=7, rows=76) circuit "pause" (k=13, rows=2365) + circuit "renounceOwnership" (k=13, rows=2364) circuit "symbol" (k=6, rows=28) circuit "totalSupply" (k=6, rows=28) circuit "transfer" (k=13, rows=3990) - circuit "transferFrom" (k=13, rows=4972) + circuit "transferFrom" (k=13, rows=4977) + circuit "transferOwnership" (k=13, rows=2959) circuit "unpause" (k=13, rows=2362) -Overall progress [====================] 11/11 +Overall progress [====================] 14/14 ``` diff --git a/content/contracts-compact/latest-version.js b/content/contracts-compact/latest-version.js index 52662a23..32018183 100644 --- a/content/contracts-compact/latest-version.js +++ b/content/contracts-compact/latest-version.js @@ -1 +1 @@ -export const latestStable = "0.1.x"; +export const latestStable = "0.3.x"; diff --git a/content/contracts-compact/multi-token.mdx b/content/contracts-compact/multi-token.mdx index d4123f2b..9e2a4b40 100644 --- a/content/contracts-compact/multi-token.mdx +++ b/content/contracts-compact/multi-token.mdx @@ -7,6 +7,10 @@ title: MultiToken [ERC165]: https://eips.ethereum.org/EIPS/eip-165 [Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern [_mint]: ./api/multi-token#MultiToken-_mint +[setApprovalForAll]: ./api/multi-token#MultiToken-setApprovalForAll +[transferFrom]: ./api/multi-token#MultiToken-transferFrom +[wit_MultiTokenSK]: ./api/multi-token#MultiToken-wit_MultiTokenSK +[computeAccountId]: ./api/utils#Utils-computeAccountId MultiToken is a specification for contracts that manage multiple token types. This module is an approximation of [EIP-1155] written in the Compact programming language for the Midnight network. @@ -62,6 +66,47 @@ The `unsafe` circuits will eventually be deprecated after Compact supports contr `transferFrom`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. +## Identity Model + +MultiToken uses a witness-derived identity scheme. Each caller proves knowledge of a secret key +by injecting it through the [wit_MultiTokenSK] witness. The module computes the caller's account identifier +as `persistentHash(secretKey)`, a commitment that hides the key while providing a stable, pseudonymous on-chain identity. +That identifier is the `Bytes<32>` (left) branch of the `Either, ContractAddress>` account type +and is what [setApprovalForAll] and [transferFrom] check ownership and operator rights against. + +What is public on the ledger: + +* `_balances`, keyed by token id and then by account identifier (or contract address). +* `_operatorApprovals`, keyed by owner and then operator. +* `_uri`. + +What the caller holds privately: + +* The 32-byte `secretKey`. It never reaches the ledger; only its hash does. + +A transfer discloses the sender, the recipient, the token id, and the amount, because the balance map is updated with those values. +An operator approval discloses the caller's identifier, the operator, and the flag. + +Because the identifier is `H(secretKey)` with no per-deployment salt, the same key produces the same identity in every contract. +Users who want cross-contract unlinkability can use a different secret key per contract at the wallet layer. + +To derive an account identifier off-chain (for example, to pass as the `recipient` of a mint), use [computeAccountId] from the Utils module: + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" prefix Utils_; + +const accountId = Utils_computeAccountId(secretKey); +``` + +The TypeScript witness implementation lives with the DApp, not with the library. +The witnesses in the `compact-contracts` repository are test-only and are not published; +consumers author their own `wit_MultiTokenSK` that returns the caller's 32-byte secret key. + + + The `secretKey` must never be logged or persisted in plaintext. Loss of the key means loss of the tokens held under that identity, + and key exposure allows impersonation. Use `crypto.getRandomValues()` or equivalent to generate keys. + + ## Usage Import the MultiToken module into the implementing contract. diff --git a/content/contracts-compact/multisig.mdx b/content/contracts-compact/multisig.mdx new file mode 100644 index 00000000..21b1c8c6 --- /dev/null +++ b/content/contracts-compact/multisig.mdx @@ -0,0 +1,355 @@ +--- +title: Multisig +--- + +{/* links */} +[Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern +[Ownable]: ./ownable +[Signer API]: api/multisig#Signer +[initialize]: api/multisig#Signer-initialize +[assertSigner]: api/multisig#Signer-assertSigner +[assertThresholdMet]: api/multisig#Signer-assertThresholdMet +[isSigner]: api/multisig#Signer-isSigner +[_addSigner]: api/multisig#Signer-_addSigner +[_removeSigner]: api/multisig#Signer-_removeSigner +[_changeThreshold]: api/multisig#Signer-_changeThreshold +[_setThreshold]: api/multisig#Signer-_setThreshold +[ProposalManager API]: api/multisig#ProposalManager +[_createProposal]: api/multisig#ProposalManager-_createProposal +[_cancelProposal]: api/multisig#ProposalManager-_cancelProposal +[_markExecuted]: api/multisig#ProposalManager-_markExecuted +[toShieldedRecipient]: api/multisig#ProposalManager-toShieldedRecipient +[toUnshieldedRecipient]: api/multisig#ProposalManager-toUnshieldedRecipient +[ShieldedTreasury API]: api/multisig#ShieldedTreasury +[ShieldedTreasury _deposit]: api/multisig#ShieldedTreasury-_deposit +[ShieldedTreasury _send]: api/multisig#ShieldedTreasury-_send +[ShieldedTreasuryStateless API]: api/multisig#ShieldedTreasuryStateless +[ShieldedTreasuryStateless _send]: api/multisig#ShieldedTreasuryStateless-_send +[UnshieldedTreasury API]: api/multisig#UnshieldedTreasury +[UnshieldedTreasury _deposit]: api/multisig#UnshieldedTreasury-_deposit +[UnshieldedTreasury _send]: api/multisig#UnshieldedTreasury-_send +[ForwarderShielded API]: api/multisig#ForwarderShielded +[ForwarderUnshielded API]: api/multisig#ForwarderUnshielded +[ForwarderPrivate API]: api/multisig#ForwarderPrivate +[_drain]: api/multisig#ForwarderPrivate-_drain +[_calculateParentCommitment]: api/multisig#ForwarderPrivate-_calculateParentCommitment +[presets directory]: https://github.com/OpenZeppelin/compact-contracts/tree/v0.3.0/contracts/src/multisig/presets + +The multisig directory provides building blocks for M-of-N governance over funds held by a contract: +a `Signer` registry with an approval threshold, a `ProposalManager` that tracks transfer proposals, +three treasuries that hold and move shielded or unshielded tokens, +and three forwarders that pass every deposit on to a parent. +The modules are independent of one another. +A contract composes the ones it needs, following the [Module/Contract Pattern], +and supplies its own authorization policy on top. +The package also ships reference compositions of these modules; see [Presets](#presets). + +None of the multisig modules declares a witness. +Every input is a circuit parameter, so what reaches the public ledger is decided by the `disclose` calls inside each circuit. +The sections below name what each module publishes. + +## Signer + +The [Signer API] keeps the registry of who may approve and how many approvals are required. +It is generic over the signer identity type `T`, which the consuming contract chooses at import time: +`Bytes<32>` for a commitment such as the hash of a public key, +`JubjubPoint` for an aggregated key, +or `Either` for a caller identity. +The module never resolves who the caller is. +The contract passes an identity in, and Signer checks it against the registry. + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/multisig/Signer"> + prefix Signer_; +``` + +### Public ledger state + +Everything the module keeps is public: +`_signers` (the full set of registered identities), `_signerCount`, `_threshold`, and `_isInitialized`. +[isSigner], [_addSigner], and [_removeSigner] disclose the identity they are given, since it is the key of the set lookup or insertion, +and [_setThreshold] discloses the new threshold. +The signer count and threshold are `Uint<8>`, so a registry holds at most 255 signers. + +### Who may call what + +* [initialize] registers an initial signer vector and sets the threshold. + It runs once and MUST only be called from the constructor; calling it later would corrupt the signer set. + A contract may skip it entirely and configure signers through its own flow with [_addSigner] and [_setThreshold]. +* The guards [assertSigner] and [assertThresholdMet] and the views `getSignerCount`, `getThreshold`, and [isSigner] are open to anyone + and deliberately do not check initialization. + Safety comes from the state rather than from a flag: + an empty registry makes [isSigner] false for every input, so [assertSigner] rejects every caller, + and a zero threshold makes [assertThresholdMet] reject before it compares any approval count. + A contract that needs a "configured" signal beyond that tracks one itself. +* [_addSigner], [_removeSigner], [_changeThreshold], and [_setThreshold] have no access control. + The consuming contract must gate them behind its own authorization policy. + Left ungated, the first caller registers their own signer set and takes control of the contract. + +[assertThresholdMet] compares the supplied count against the threshold and nothing else; it never consults the registry. +The contract's own approval accounting must be gated by [assertSigner] and must count distinct signers. +The count is intentionally not bounded by `_signerCount`, +so approvals collected before a signer was removed still count toward a threshold the remaining signers satisfy. + +[_changeThreshold] refuses a threshold above the current signer count. +[_setThreshold] skips that check so a custom setup flow can set the threshold before any signer is registered. +A contract must not gate operations on [assertThresholdMet] while such a setup is incomplete. + +Reconfigurations that take several transactions (removing a signer, then lowering the threshold) pass through intermediate states +where the module's checks hold but the contract's intended configuration is incomplete. +Perform them atomically in a single circuit, or keep a configuration nonce that invalidates proposals created under a stale signer set. + +### Usage + +The example registers three `Bytes<32>` signer identities at deployment and lets an owner manage the registry afterwards. +The [Ownable] module authenticates the owner through its `wit_OwnableSK` witness, +which the consuming contract supplies in its own TypeScript +(the witness implementations in the library repository are test-only and are not published). + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/multisig/Signer"> + prefix Signer_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +constructor( + initialOwner: Either, ContractAddress>, + signers: Vector<3, Bytes<32>>, + thresh: Uint<8> +) { + Ownable_initialize(initialOwner); + Signer_initialize<3>(signers, thresh); +} + +export circuit addSigner(signer: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Signer__addSigner(signer); +} + +export circuit removeSigner(signer: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Signer__removeSigner(signer); +} + +export circuit changeThreshold(newThreshold: Uint<8>): [] { + Ownable_assertOnlyOwner(); + Signer__changeThreshold(newThreshold); +} + +export circuit isSigner(account: Bytes<32>): Boolean { + return Signer_isSigner(account); +} + +export circuit getThreshold(): Uint<8> { + return Signer_getThreshold(); +} +``` + +## ProposalManager + +The [ProposalManager API] tracks the lifecycle of transfer proposals independently of the token kind. +A `Proposal` records a `Recipient`, a token `color`, an `amount`, and a `ProposalStatus` (`Active`, `Executed`, or `Cancelled`; +`Inactive` is the zero value and is never stored). +A `Recipient` pairs 32 address bytes with a `RecipientKind` tag (`ShieldedUser`, `UnshieldedUser`, or `Contract`) +that says how those bytes are to be read. +Build recipients with `shieldedUserRecipient`, `unshieldedUserRecipient`, and `contractRecipient`, +and convert them at execution time with [toShieldedRecipient] (accepts `ShieldedUser` and `Contract`) +or [toUnshieldedRecipient] (accepts `UnshieldedUser` and `Contract`). +Because `Contract` is accepted by both, the tag alone does not decide the rail; +a contract composing both treasuries chooses which one a proposal executes on. + +### Public ledger state + +Proposals are public. +`_proposals` is exported ledger state, so every proposal's recipient address, color, amount, and status is readable off-chain, +and `_nextProposalId` reveals how many have been created. +This holds whichever treasury the contract composes: pairing the module with `ShieldedTreasuryStateless` still publishes +the full intent of every transfer. +The transfer is private; the plan is not. + +### Who may call what + +[_createProposal], [_cancelProposal], and [_markExecuted] have no access control and must be gated by the contract. +The guards `assertProposalExists` and `assertProposalActive`, the recipient helpers, and the getters are open. +[_createProposal] increments the counter and reads it back as the map key, +so two transactions that create proposals against the same ledger state conflict and only one of them lands. + +[_createProposal] validates nothing about the recipient beyond its type. +A kind the execution rail cannot accept (an `UnshieldedUser` recipient on a shielded treasury) +creates a proposal that stays `Active` and unexecutable until [_cancelProposal] retires it, +and a zero or otherwise unspendable address is accepted, so funds sent to it on execution are unrecoverable. + +## Treasuries + +Each treasury exposes an ungated `_deposit` and `_send` pair that the contract gates with its own policy. +They differ in what they keep on the ledger and in who handles the change of a shielded spend. + +### ShieldedTreasury + +The [ShieldedTreasury API] stores one coin per token color in `_coins`, a map from color to `QualifiedShieldedCoinInfo`, +together with cumulative `_received` and `_sent` totals per color. +[ShieldedTreasury _deposit] claims the incoming coin with `receiveShielded` +and merges it into the stored coin of the same color (`mergeCoinImmediate`), or inserts it when the color is new, +so the contract never has to choose between several UTXOs of one color at spend time. +[ShieldedTreasury _send] looks the coin up by color, checks it covers the amount, and calls `sendShielded`, +which routes any change back to the contract as a self-owned output. +The treasury retains that change: it is written back to `_coins` for the next spend, +and the color's entry is removed when the send leaves none. + +The holdings of this treasury are public. +`_coins` is exported, so every coin it holds (nonce, color, value, and Merkle index) is readable in plaintext, +as are the received and sent totals. +"Shielded" refers to the token rail: individual transfers are shielded, but what the contract holds is not. +Deposits disclose the incoming coin; sends disclose the color, recipient, and amount. + + + `_send` returns a `ShieldedSendResult` whose `sent` and `change` coins are in plaintext. + Consume it inside the circuit or drop it; returning it from an entry point, writing it to the ledger, + or passing it to another contract publishes the coin details. + The change coin already belongs to the treasury and is recorded in `_coins`: + spending it again in the same transaction double-spends the recorded coin. + + +### ShieldedTreasuryStateless + +The [ShieldedTreasuryStateless API] keeps no ledger state at all: no coin map, no balances, no totals. +`_deposit` calls `receiveShielded` and nothing else. +The operator discovers the coin's Merkle index from `ZswapOutput` events through the indexer, +builds the `QualifiedShieldedCoinInfo` off-chain, and passes it to [ShieldedTreasuryStateless _send] at spend time, +so coin data never reaches the public ledger and there is no on-chain balance to query. +Sufficiency is enforced by `sendShielded` rather than by the module, +and a double spend or a coin the contract does not own is rejected by the protocol when the transaction is applied. + +`sendShielded` routes change back to the contract, and [ShieldedTreasuryStateless _send] returns it untouched in `result.change` +as a live, spendable coin. +The contract decides what to do with it: keep it off-chain and pass it back as the `coin` of a later spend, +or route it onward to a different recipient in the same transaction with `sendImmediateShielded`. +The only rule is that the change must never be re-spent and still treated as held. +Writing it to the ledger would publish its nonce, color, and value. + +Deposits disclose the incoming coin; sends disclose the spent coin, recipient, and amount. +A coin commitment is a hash of the coin's data, so a receive can be linked to its later spend. + +### UnshieldedTreasury + +The [UnshieldedTreasury API] tracks a per-color balance in `_balances`. +[UnshieldedTreasury _deposit] calls `receiveUnshielded` and adds to the balance; +[UnshieldedTreasury _send] subtracts and calls `sendUnshielded`. +The sufficiency and overflow guards read `_balances`, not the protocol balance, +which is fixed at the start of execution and cannot see a deposit made earlier in the same transaction. +Route every unshielded credit and debit through the module: +a direct `receiveUnshielded` or `sendUnshielded` leaves `_balances` out of step and `getTokenBalance` misreports. + +Unshielded transfers are fully public: depositor, recipient, color, and amount all appear on the transcript. +Recipients are not validated, so an unspendable recipient permanently removes the funds, +and a send to the contract's own address zeroes the tracked balance while the funds stay held at the protocol level. + +### Usage + +The example composes `ShieldedTreasury` with [Ownable]: anyone may fund the treasury, only the owner may spend from it. +The send result is dropped because the change is already recorded in the treasury's coin map. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasury" + prefix Treasury_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +constructor(initialOwner: Either, ContractAddress>) { + Ownable_initialize(initialOwner); +} + +export circuit deposit(coin: ShieldedCoinInfo): [] { + Treasury__deposit(coin); +} + +export circuit withdraw( + recipient: Either, + color: Bytes<32>, + amount: Uint<128> +): [] { + Ownable_assertOnlyOwner(); + Treasury__send(recipient, color, amount); +} + +export circuit getTokenBalance(color: Bytes<32>): Uint<128> { + return Treasury_getTokenBalance(color); +} + +export circuit getReceivedTotal(color: Bytes<32>): Uint<128> { + return Treasury_getReceivedTotal(color); +} + +export circuit getSentTotal(color: Bytes<32>): Uint<128> { + return Treasury_getSentTotal(color); +} +``` + +## Forwarders + +A forwarder is bound to a single parent at deployment and passes every deposit on to it. +The modules are permissionless: the recipient is fixed, so any caller may deposit without compromising the parent. +None of them exposes a setter; a contract that needs to change the parent adds its own circuit, or deploys a new forwarder. + +### ForwarderShielded and ForwarderUnshielded + +The [ForwarderShielded API] and [ForwarderUnshielded API] forward atomically: +`_deposit` claims the incoming value (`receiveShielded` or `receiveUnshielded`) and re-sends it to the parent in the same transaction +(`sendImmediateShielded` or `sendUnshielded`). +Each keeps two ledger fields, `_parent` and `_isInitialized`, and every circuit except `initialize` asserts initialization. +The parent is stored as a generic `Either` so the ledger layout can later accept a contract address without a state migration, +but `initialize` accepts only a `ZswapCoinPublicKey` (shielded, stored in the `left` arm) +or a `UserAddress` (unshielded, stored in the `right` arm) today: +an atomic forward can only reach a recipient that needs no in-transaction claim, +and a third-party contract would never claim the output. + +`initialize` rejects only the all-zero key or address. +The deployer must set a parent that can actually spend the forwarded coins; forwarding to a key no one controls strands every deposit. +The shielded forwarder discloses the incoming coin; the unshielded forwarder discloses depositor, recipient, color, and amount. + +### ForwarderPrivate + +The [ForwarderPrivate API] hides the parent. +The ledger holds `_parentCommitment`, computed off-chain by the deployer as +`H(pad(32, "ForwarderPrivate:commitment") || parent || opSecret)` with the pure [_calculateParentCommitment], +plus `_isInitialized`. +`_deposit` claims the coin and writes nothing; coins dwell at the contract address until drained. +[_drain] takes the coin, the parent coin public key, the operational secret, and the value to send: +it checks that the pair hashes to the stored commitment, then sends the value to the parent with `sendShielded`. +The parent is a `ZswapCoinPublicKey` and never a `ContractAddress`: +a coin-public-key recipient is encrypted inside the Zswap output and never appears on the public transcript, +whereas a shielded send to a contract publishes that contract's address. +Any change is routed back to the contract and returned untouched in `result.change`, +to leave dwelling for a future drain or to route onward with `sendImmediateShielded`. + +Knowledge of the preimage is the sole authorization gate. +Two forwarders bound to the same parent with different operational secrets produce different commitments and are unlinkable on-chain. +Deposits disclose the incoming coin; drains disclose the spent coin and the value. + + + Losing the operational secret is permanent fund loss. + It is the only drain authorization, with no rotation, revocation, or recovery path: + every coin accumulated at the contract becomes inaccessible. + Back it up offline before the first deposit and treat it with the hygiene of a signing key. + + +## Presets + +The package ships six standalone contracts under its [presets directory]: +`ShieldedMultiSig`, `ShieldedMultiSigV2`, `ShieldedMultiSigV3`, and one forwarder preset per forwarder module. +They are reference compositions that show how the modules above fit together, and they are compiled and deployed as-is rather than imported. +They are not covered by the API reference. + + + The three `ShieldedMultiSig` presets do not verify real signatures. + `ShieldedMultiSigV2` and `ShieldedMultiSigV3` call a `stubVerifySignature` that accepts every signature + (to be replaced with `ecdsaVerify` and `keccak256` once those primitives are available), + and `ShieldedMultiSig` identifies the caller by `ownPublicKey()` alone. + Do not deploy them as multisigs. + diff --git a/content/contracts-compact/native-shielded-token.mdx b/content/contracts-compact/native-shielded-token.mdx new file mode 100644 index 00000000..9599346c --- /dev/null +++ b/content/contracts-compact/native-shielded-token.mdx @@ -0,0 +1,374 @@ +--- +title: NativeShieldedToken +--- + +{/* links */} +[Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern +[ZOwnablePK]: ./ownable +[wit_secretNonce]: api/ownable#ZOwnablePK-wit_secretNonce +[ZOwnablePK__computeOwnerId]: api/ownable#ZOwnablePK-_computeOwnerId +[NativeShieldedToken]: api/native-shielded-token#NativeShieldedToken +[NativeShieldedTokenFamily]: api/native-shielded-token#NativeShieldedTokenFamily +[NativeShieldedTokenCore]: api/native-shielded-token#NativeShieldedTokenCore +[tokenColor]: api/native-shielded-token#NativeShieldedToken-tokenColor +[_mint]: api/native-shielded-token#NativeShieldedToken-_mint +[_mintToSelf]: api/native-shielded-token#NativeShieldedToken-_mintToSelf +[_burn]: api/native-shielded-token#NativeShieldedToken-_burn +[_burnFromSelf]: api/native-shielded-token#NativeShieldedToken-_burnFromSelf +[Core__mint]: api/native-shielded-token#NativeShieldedTokenCore-_mint +[NativeShieldedTokenPublicSupply]: api/native-shielded-token#NativeShieldedTokenPublicSupply +[NativeShieldedTokenFamilyPublicSupply]: api/native-shielded-token#NativeShieldedTokenFamilyPublicSupply +[_addMinted]: api/native-shielded-token#NativeShieldedTokenPublicSupply-_addMinted +[_addBurned]: api/native-shielded-token#NativeShieldedTokenPublicSupply-_addBurned +[totalMinted]: api/native-shielded-token#NativeShieldedTokenPublicSupply-totalMinted +[totalBurned]: api/native-shielded-token#NativeShieldedTokenPublicSupply-totalBurned +[totalSupply]: api/native-shielded-token#NativeShieldedTokenPublicSupply-totalSupply +[NativeShieldedTokenDerivedNonce]: api/native-shielded-token#NativeShieldedTokenDerivedNonce +[_deriveNonce]: api/native-shielded-token#NativeShieldedTokenDerivedNonce-_deriveNonce +[ZOwnableNativeShieldedToken]: api/native-shielded-token#ZOwnableNativeShieldedToken +[mint]: api/native-shielded-token#ZOwnableNativeShieldedToken-mint +[burn]: api/native-shielded-token#ZOwnableNativeShieldedToken-burn + +A native shielded token is a token whose units are Midnight's own shielded coins (Zswap UTXOs) +rather than entries in a contract-owned map. +The issuing contract mints coins of its own color and burns them; +holding, spending, and transferring happen in the holder's wallet through Zswap, outside the contract. +This family of modules implements the MIP-0011 native shielded token standard for the Midnight network: +a single-token flavor ([NativeShieldedToken]), a multi-token flavor ([NativeShieldedTokenFamily]), +the shared core both are built on ([NativeShieldedTokenCore]), opt-in supply accounting, +an opt-in derived-nonce helper, and an owner-gated preset ([ZOwnableNativeShieldedToken]). + +## How a native shielded token works + +**Coins, not balances.** A unit of the token is a shielded coin with a `color`, a `value`, and a `nonce`. +The ledger stores a commitment to each coin and a nullifier once it is spent; it never stores who holds what. +There is no `balanceOf`, `allowance`, or contract-mediated `transfer`: +once a user holds a coin, the contract cannot observe or restrict its movement, +and holders move coins between wallets with ordinary Zswap transactions. + +**Color.** Every token type is identified by its coin color, `tokenType(domain, contractAddress)`, +where `domain` is a 32-byte discriminator and `contractAddress` is the issuing contract. +Because the contract address is part of the derivation, only the issuing contract can mint coins of its colors. +The modules compute the color at call time with [tokenColor] and never store it. + +**Minting.** The contract calls `mintShieldedToken`, which creates a new coin for a recipient. +The protocol caps a single mint at `Uint<64>`. + +**Burning.** The contract burns by sending value to `shieldedBurnAddress()`. +Burn amounts are `Uint<128>`, because `sendShielded` accepts that width; the asymmetry with mints is a protocol property. + +**Metadata.** `name`, `symbol`, and `decimals` are contract-wide, fixed at construction, and public. +`decimals` is a display convention only; the protocol operates on integer values. + +## What is public and what is private + +The token modules declare no witnesses; the only secrets are the ones that travel inside coins. + +Public on the ledger as state: + +* `_name`, `_symbol`, `_decimals`, and `_isInitialized` (and `_domain` for the single-token flavor). +* The supply counters, if a supply extension is composed. + +Public in every transaction: + +* Each mint's color and amount, through the protocol's shielded-mint effect. +* The coin commitments created and the nullifiers spent. +* Which circuit ran. + +Private: + +* The recipient and the nonce of a mint, provided the nonce is secret and random. + Anyone who knows the nonce can recompute the commitment for candidate recipient keys. +* The amount, the coin, and the refund recipient of a burn, as long as the observer does not know the coin's nonce. + A change output does reveal that a burn was partial. + + +Contract-initiated mints and refunds do not create coin ciphertexts, so a recipient's wallet cannot discover +those coins by scanning the chain. The `ShieldedCoinInfo` a circuit returns is the only copy of the coin: +the DApp must capture it and deliver it to the recipient (or persist it, when the contract is the recipient). +Dropping it strands the value irrecoverably. + + +## Single token or a family + +Both user-facing modules are thin wrappers over [NativeShieldedTokenCore], which owns the initialization flag, +the metadata, and the mint and burn logic. They differ only in where the domain comes from. + +* [NativeShieldedToken] issues one token type. The domain is passed to `initialize`, stored as `sealed ledger _domain`, + and forwarded to every core operation, so no circuit takes a domain parameter and a caller cannot misuse one. +* [NativeShieldedTokenFamily] issues many token types from one contract. Every circuit takes a `domain` parameter, + and `tokenType(domain, kernel.self())` discriminates the types. + Midnight has no contract-to-contract calls and no way for a contract to deploy another, + so a protocol that issues many related tokens (one liquidity-share token per pair, for instance) + must do so from a single contract. Metadata is family-wide; per-type identity belongs in the consumer's own state. + + +Pick one flavor per contract. Both wrap the same core, so importing both into one contract shares a single +initialization flag and one set of `name`, `symbol`, and `decimals`. + + +The core itself is not meant to be composed directly. Its `_mint` and `_burn` take an +`Either` recipient; the flavors narrow that to a `ZswapCoinPublicKey` +and expose [_mintToSelf] for the contract's own coins. + +## Minting and who can receive a coin + +[_mint] takes a `ZswapCoinPublicKey` recipient, a `Uint<64>` amount, and a caller-supplied `Bytes<32>` nonce, +and returns the new coin's `ShieldedCoinInfo`. The recipient must not be the zero key. + +**Recipients.** A coin can be minted to any user's coin public key, or to the issuing contract itself through [_mintToSelf], +which mints to `kernel.self()` and claims the coin with `receiveShielded` in the same call. +No other contract can receive a mint: the ledger only accepts a contract-addressed commitment when that contract claims it +in the same transaction, and no primitive can claim a receive on another contract's behalf. +The core enforces this directly ([Core__mint] rejects any `ContractAddress` other than the contract's own), +and the flavors do not expose the address arm at all. +A coin minted to the contract is the contract's holding: persist the returned coin info in your own ledger state, +and spend it later with [_burnFromSelf] once it carries a Merkle-tree index. + +**Nonces.** The caller is responsible for nonce uniqueness. Reusing a nonce for the same (domain, value, recipient) +produces a duplicate commitment, which the ledger rejects. +With a secret, cryptographically random nonce the mint is recipient-private. +If you do not want to manage nonces, compose [NativeShieldedTokenDerivedNonce] and pass its [_deriveNonce] output, +at the cost of making those mints recipient-public (see below). + +The modules include no access control. Gate `_mint` and `_mintToSelf` with an access-control module +following the [Module/Contract Pattern]; exposing them as-is lets anyone mint. + +## Burning and change + +There are two burn paths, chosen by where the coin comes from. + +[_burn] destroys value from a coin that the caller's wallet pays into the current transaction. +The contract receives the coin and spends `amount` of it to the burn address through `sendImmediateShielded`, +the required path for a coin that was created in the same transaction. +If `amount` is less than `coin.value`, the remainder is sent to `refundTo` as a new coin and returned as `some(refund)`; +pass `coin.value` as `amount` to burn the coin in full, in which case `none` is returned. +`refundTo` must always be supplied and non-zero, even on a full burn: the zero key is the burn address, +so a zero `refundTo` would silently burn the change as well. + +[_burnFromSelf] destroys value from a coin the contract already holds, identified by a `QualifiedShieldedCoinInfo` +(a coin with a valid Merkle-tree index). It spends through `sendShielded`; any change is received by the contract +automatically and returned so the consumer can persist it. The change coin replaces the original as the contract's holding, +and its info is not otherwise recoverable. + +Both paths put the spent coin's nullifier and the new output commitments on the ledger. +Neither puts the burned amount on the ledger: the `disclose()` calls in the module are compiler permission markers +for the coin primitives, not disclosure sinks. + +## Supply accounting (optional) + +The token modules track no supply totals. [NativeShieldedTokenPublicSupply] (for the single-token flavor) and +[NativeShieldedTokenFamilyPublicSupply] (for the family) add on-chain counters. +They import no token module: the consuming contract pairs every mint with [_addMinted] and every burn with [_addBurned], +under the same domain for the family. The three getters carry different guarantees: + +* [totalMinted] is exact. Every mint of this contract's colors goes through the contract. +* [totalBurned] is a lower bound. A holder can send coins to the burn address without calling the contract, and those burns are invisible. +* [totalSupply] is `totalMinted - totalBurned`, an upper bound on circulating supply. + +`_addBurned` enforces `burned <= minted`, so an unpaired, duplicate, or wrong-domain burn reverts instead of pushing +the supply negative. Nothing catches drift on the mint side: a missing `_addMinted` undercounts, after which +`_addBurned` rejects legitimate burns, and a duplicate `_addMinted` lets burns exceed what was minted. +The pairing is an invariant of the consuming contract. + + +Composing a supply extension makes contract-mediated burn amounts public: the `_addBurned` write is what puts the +burned amount into the public transcript. Mint amounts are already public through the shielded-mint effect, so +`_addMinted` adds accounting, not disclosure. Compose supply tracking only when on-chain auditability is worth more +than burn-amount privacy. + + +## Derived nonces (optional) + +[NativeShieldedTokenDerivedNonce] owns a monotonic `_counter` and exposes [_deriveNonce], which increments the counter +and returns `evolveNonce(_counter, pad(32, "NativeShieldedToken:nonce"))`. +Uniqueness comes from the counter; the fixed tag separates these nonces from any other `evolveNonce` use in the contract. +One counter serves the whole contract, so derived nonces never repeat across token types. +It needs no initialization and pairs with either flavor. + +Derived nonces are predictable from public state, so mints that use them are recipient-public: +anyone can recompute the commitment for candidate recipient keys. +They are also open to collision griefing: an actor with access to a caller-nonce mint can pre-mint a colliding commitment +and make the matching derived-nonce mint fail. Gate both mint paths at the same trust level, and do not rely on retrying: +the nonce is a pure function of the counter, and a reverted mint does not advance it, so an identical retry collides again. +A mint with different inputs advances the counter and clears the way. + +## Usage + +### An owner-gated single token + +[ZOwnableNativeShieldedToken] composes [ZOwnablePK] (a shielded owner), [NativeShieldedToken] (one color), and +[NativeShieldedTokenPublicSupply] (a public minted total). Only the owner can [mint] and [burn]; +the preset deliberately never calls `_addBurned`, so burn amounts stay private and `_totalBurned` stays empty. +The following contract is the published `ZOwnableNativeShieldedTokenExample`. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/presets/ZOwnableNativeShieldedToken" + prefix ZOwnableNativeShieldedToken_; + +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe, ShieldedCoinInfo }; + +// Surface the preset's state unprefixed so `ledger()` readers use the keys the +// modules declare. +import { + _ownerCommitment, + _counter, + _instanceSalt, + _ownableIsInitialized, + _domain, + _name, + _symbol, + _decimals, + _isInitialized, + _totalMinted, + _totalBurned, +} from "./node_modules/@openzeppelin/compact-contracts/token/presets/ZOwnableNativeShieldedToken"; +export { + _ownerCommitment, + _counter, + _instanceSalt, + _ownableIsInitialized, + _domain, + _name, + _symbol, + _decimals, + _isInitialized, + _totalMinted, + _totalBurned, +}; + +constructor( + ownerId: Bytes<32>, + instanceSalt: Bytes<32>, + tokenDomain: Bytes<32>, + name_: Opaque<"string">, + symbol_: Opaque<"string">, + decimals_: Uint<8>, +) { + ZOwnableNativeShieldedToken_initialize( + ownerId, + instanceSalt, + tokenDomain, + name_, + symbol_, + decimals_, + ); +} + +export circuit mint(recipient: ZswapCoinPublicKey, amount: Uint<64>, nonce: Bytes<32>): ShieldedCoinInfo { + return ZOwnableNativeShieldedToken_mint(recipient, amount, nonce); +} + +export circuit burn(coin: ShieldedCoinInfo, amount: Uint<64>, refundTo: ZswapCoinPublicKey): Maybe { + return ZOwnableNativeShieldedToken_burn(coin, amount, refundTo); +} + +export circuit transferOwnership(newOwnerId: Bytes<32>): [] { + ZOwnableNativeShieldedToken_transferOwnership(newOwnerId); +} + +export circuit renounceOwnership(): [] { + ZOwnableNativeShieldedToken_renounceOwnership(); +} + +export circuit owner(): Bytes<32> { + return ZOwnableNativeShieldedToken_owner(); +} + +export pure circuit _computeOwnerId(pk: Either, nonce: Bytes<32>): Bytes<32> { + return ZOwnableNativeShieldedToken__computeOwnerId(pk, nonce); +} + +export circuit totalMinted(): Uint<128> { + return ZOwnableNativeShieldedToken_totalMinted(); +} + +export circuit tokenColor(): Bytes<32> { + return ZOwnableNativeShieldedToken_tokenColor(); +} +``` + +The owner is identified by a commitment, not a key. Before deploying, compute +`ownerId = _computeOwnerId(pk, secretNonce)` off-chain, where `pk` is the coin public key the owner's wallet +will supply as `ownPublicKey()` and `secretNonce` is fresh random bytes the owner keeps private. +The contract cannot check this value at deploy time; any other value leaves the contract with no usable owner and no recovery. +Every owner transaction then proves knowledge of the pair: the wallet supplies the coin public key, and the +[wit_secretNonce] witness (inherited from [ZOwnablePK]) supplies the nonce. +The TypeScript witness implementations in the library repository are test-only and are not published; +you author the witness for your own DApp. +The proof server sees `wit_secretNonce`, so run it where the owner controls it. + +On the client side, pass fresh random bytes as `nonce` on every `mint`, and persist the `ShieldedCoinInfo` values that +`mint` and `burn` return: midnight-js encrypts an output to its recipient only when it can resolve the recipient's +encryption key (its own, or one supplied in `additionalCoinEncPublicKeyMappings`). + +`initialize` writes sealed fields, so it belongs in the constructor and must not be exposed as a post-deploy circuit. +`transferOwnership` is one step and `renounceOwnership` stops minting and burning forever. + +### A token family with supply tracking and derived nonces + +This contract issues one token type per `domain`, counts supply per domain, and lets the owner mint without +managing nonces. Burns are open to any holder, since a holder can always burn by sending to the burn address directly; +routing burns through the contract keeps `totalBurned` closer to the truth. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK" + prefix ZOwnablePK_; +import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedTokenFamily" + prefix Family_; +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenFamilyPublicSupply" + prefix Supply_; +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenDerivedNonce" + prefix Derived_; + +export { ZswapCoinPublicKey, Maybe, ShieldedCoinInfo }; + +constructor( + ownerId: Bytes<32>, + instanceSalt: Bytes<32>, + name_: Opaque<"string">, + symbol_: Opaque<"string">, + decimals_: Uint<8>, +) { + ZOwnablePK_initialize(ownerId, instanceSalt); + Family_initialize(name_, symbol_, decimals_); +} + +// Owner only. Derived nonces make these mints recipient-public. +export circuit mint(domain: Bytes<32>, recipient: ZswapCoinPublicKey, amount: Uint<64>): ShieldedCoinInfo { + ZOwnablePK_assertOnlyOwner(); + const coin = Family__mint(domain, recipient, amount, Derived__deriveNonce()); + Supply__addMinted(domain, amount); + return coin; +} + +// Any holder. `coin` is paid into the transaction by the caller's wallet; +// the change, if any, goes to `refundTo`. Publishes `amount`. +export circuit burn( + domain: Bytes<32>, + coin: ShieldedCoinInfo, + amount: Uint<128>, + refundTo: ZswapCoinPublicKey +): Maybe { + const refund = Family__burn(domain, coin, amount, refundTo); + Supply__addBurned(domain, amount); + return refund; +} + +export circuit tokenColor(domain: Bytes<32>): Bytes<32> { + return Family_tokenColor(domain); +} + +export circuit totalSupply(domain: Bytes<32>): Uint<128> { + return Supply_totalSupply(domain); +} +``` + +A `mint` and a `burn` under the same `domain` both touch that domain's supply entry, so only one of them lands per block; +operations on different domains do not conflict. diff --git a/content/contracts-compact/non-fungible-token.mdx b/content/contracts-compact/non-fungible-token.mdx index a70985ac..f6ae620b 100644 --- a/content/contracts-compact/non-fungible-token.mdx +++ b/content/contracts-compact/non-fungible-token.mdx @@ -8,6 +8,11 @@ title: NonFungibleToken [ERC-165]: https://eips.ethereum.org/EIPS/eip-165 [Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [_mint]: ./api/non-fungible-token#NonFungibleToken-_mint +[approve]: ./api/non-fungible-token#NonFungibleToken-approve +[setApprovalForAll]: ./api/non-fungible-token#NonFungibleToken-setApprovalForAll +[transferFrom]: ./api/non-fungible-token#NonFungibleToken-transferFrom +[wit_NonFungibleTokenSK]: ./api/non-fungible-token#NonFungibleToken-wit_NonFungibleTokenSK +[computeAccountId]: ./api/utils#Utils-computeAccountId NonFungibleToken is a specification for [non-fungible tokens], a type of token where all the units are unique and distinct from each other. @@ -52,6 +57,47 @@ The `unsafe` circuits will eventually be deprecated after Compact supports contr `_transfer`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. +## Identity Model + +NonFungibleToken uses a witness-derived identity scheme. Each caller proves knowledge of a secret key +by injecting it through the [wit_NonFungibleTokenSK] witness. The module computes the caller's account identifier +as `persistentHash(secretKey)`, a commitment that hides the key while providing a stable, pseudonymous on-chain identity. +That identifier is the `Bytes<32>` (left) branch of the `Either, ContractAddress>` account type +and is what [approve], [setApprovalForAll], and [transferFrom] check ownership and operator rights against. + +What is public on the ledger: + +* `_owners`, keyed by token id, and `_balances`, keyed by account identifier (or contract address). +* `_tokenApprovals`, keyed by token id, and `_operatorApprovals`, keyed by owner and then operator. +* `_tokenURIs`, `_name`, and `_symbol`. + +What the caller holds privately: + +* The 32-byte `secretKey`. It never reaches the ledger; only its hash does. + +A transfer discloses the token id, the previous owner, and the recipient, because the owner and balance maps are updated with those values. +A single-token approval discloses the token id and the approved account; an operator approval discloses the caller's identifier, the operator, and the flag. + +Because the identifier is `H(secretKey)` with no per-deployment salt, the same key produces the same identity in every contract. +Users who want cross-contract unlinkability can use a different secret key per contract at the wallet layer. + +To derive an account identifier off-chain (for example, to pass as the `recipient` of a mint), use [computeAccountId] from the Utils module: + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" prefix Utils_; + +const accountId = Utils_computeAccountId(secretKey); +``` + +The TypeScript witness implementation lives with the DApp, not with the library. +The witnesses in the `compact-contracts` repository are test-only and are not published; +consumers author their own `wit_NonFungibleTokenSK` that returns the caller's 32-byte secret key. + + + The `secretKey` must never be logged or persisted in plaintext. Loss of the key means loss of the tokens held under that identity, + and key exposure allows impersonation. Use `crypto.getRandomValues()` or equivalent to generate keys. + + ## Usage Import the NonFungibleToken module into the implementing contract. diff --git a/content/contracts-compact/ownable.mdx b/content/contracts-compact/ownable.mdx index 5deb387f..0c1f2b2c 100644 --- a/content/contracts-compact/ownable.mdx +++ b/content/contracts-compact/ownable.mdx @@ -8,7 +8,7 @@ title: Ownable [_transferOwnership]: api/ownable#Ownable-_transferOwnership [_unsafeTransferOwnership]: api/ownable#Ownable-_unsafeTransferOwnership [_unsafeUncheckedTransferOwnership]: api/ownable#Ownable-_unsafeUncheckedTransferOwnership -[computeAccountId]: api/ownable#Ownable-computeAccountId +[computeAccountId]: api/utils#Utils-computeAccountId [assertOnlyOwner]: api/ownable#Ownable-assertOnlyOwner [Ownable API]: api/ownable#Ownable @@ -16,10 +16,12 @@ title: Ownable [ZOwnablePK_transferOwnership]: api/ownable#ZOwnablePK-transferOwnership [ZOwnablePK_assertOnlyOwner]: api/ownable#ZOwnablePK-assertOnlyOwner [_computeOwnerId]: api/ownable#ZOwnablePK-_computeOwnerId +[wit_secretNonce]: api/ownable#ZOwnablePK-wit_secretNonce [ed25519]: https://ed25519.cr.yp.to/ [rfc6979]: https://datatracker.ietf.org/doc/html/rfc6979 [AccessControl]: ./access-control.mdx +[Utils]: ./utils.mdx ## Access Control Defined @@ -46,17 +48,47 @@ This can later be changed with [transferOwnership]. Ownable uses a witness-derived identity scheme. The owner proves knowledge of a secret key by injecting it via the `wit_OwnableSK` witness. The module computes an account identifier -as `persistentHash(secretKey)`, which is a commitment that hides the key while providing a stable, +as `H(secretKey)` (a `persistentHash` of the key), which is a commitment that hides the key while providing a stable, pseudonymous on-chain identity. +What is public and what is private: + +* **On the ledger**: `_owner` holds the account identifier `H(secretKey)` (or a contract address), and `_isInitialized`. +* **Held privately by the owner**: the 32-byte `secretKey`, supplied through `wit_OwnableSK` at proving time. + The key never reaches the ledger; [assertOnlyOwner] recomputes the identifier inside the proof and compares it against `_owner`. + +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + To derive an account identifier off-chain (e.g. for the `initialOwner` parameter during construction), -use [computeAccountId]: +use [computeAccountId] from the [Utils] module: ```typescript -const accountId = Ownable_computeAccountId(secretKey); +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +const accountId = Utils_computeAccountId(secretKey); +``` + +`computeAccountId` is a pure circuit, so it can be evaluated off-chain (for example through a contract simulator). +The same value can be computed directly in TypeScript with the Compact runtime: + +```typescript +import { + CompactTypeBytes, + CompactTypeVector, + persistentHash, +} from '@midnight-ntwrk/compact-runtime'; + +const computeAccountId = (secretKey: Uint8Array): Uint8Array => { + const rt_type = new CompactTypeVector(1, new CompactTypeBytes(32)); + return persistentHash(rt_type, [secretKey]); +}; ``` The `secretKey` is a 32-byte cryptographically secure random value that the owner keeps private. +Because the identifier has no per-deployment salt, the same key produces the same identity across all contracts. +Owners who want cross-contract unlinkability should use a different key per contract. The `secretKey` must never be logged or persisted in plaintext. Loss of the key means loss of ownership. @@ -72,12 +104,12 @@ In other words, ownership transfers to contract addresses are disallowed through This is because Compact currently does not support contract-to-contract calls, which means if a contract is granted ownership, the owner contract cannot directly call the protected circuit. -### Experimental Features +### Unsafe Circuits -This module offers experimental circuits that allow ownership to be granted to contract addresses +This module offers circuits that allow ownership to be granted to contract addresses ([_unsafeTransferOwnership] and [_unsafeUncheckedTransferOwnership]). -Note that the circuit names are very explicit ("unsafe") with these experimental circuits. +Note that the circuit names are very explicit ("unsafe"). Until contract-to-contract calls are supported, there is no direct way for a contract to call circuits of other contracts or transfer ownership back to a user. @@ -90,7 +122,7 @@ there is no direct way for a contract to call circuits of other contracts or tra Import the Ownable module into the implementing contract. It's recommended to prefix the module with `Ownable_` to avoid circuit signature clashes. -The `initialOwner` should be an account identifier derived off-chain using `computeAccountId(secretKey)`: +The `initialOwner` should be an account identifier derived off-chain using `Utils_computeAccountId(secretKey)`: ```ts pragma language_version >= {{compact_language_version}}; @@ -118,7 +150,7 @@ export circuit mySensitiveCircuit(): [] { ``` Contracts may expose [transferOwnership] to allow the owner to transfer ownership. -The `newOwner` should be an account identifier derived by the new owner using `computeAccountId(secretKey)`: +The `newOwner` should be an account identifier derived by the new owner using `Utils_computeAccountId(secretKey)`: ```ts export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { @@ -139,7 +171,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" /** * Set `initialOwner` as the owner of the contract. - * `initialOwner` should be derived off-chain using computeAccountId(secretKey). + * `initialOwner` should be derived off-chain using Utils_computeAccountId(secretKey). */ constructor(initialOwner: Either, ContractAddress>) { Ownable_initialize(initialOwner); @@ -155,7 +187,7 @@ export circuit owner(): Either, ContractAddress> { /** * Transfers ownership of the contract. * Can only be called by the current owner. - * `newOwner` should be derived off-chain by the new owner using computeAccountId(secretKey). + * `newOwner` should be derived off-chain by the new owner using Utils_computeAccountId(secretKey). */ export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { Ownable_transferOwnership(newOwner); @@ -202,6 +234,17 @@ instead, the contract stores only a cryptographic commitment that proves ownersh This is useful in scenarios such as private treasuries, anonymous governance, or any application where revealing the administrator's identity may compromise the system's confidentiality. +What is public and what is private: + +* **On the ledger**: `_ownerCommitment`, the transfer `_counter`, the sealed `_instanceSalt`, and `_isInitialized`. + Ownership changes disclose only the new commitment, never the owner identifier that produced it. +* **Held privately by the owner**: the secret nonce, supplied through the [wit_secretNonce] witness, + and the public key, supplied through `ownPublicKey()`. [ZOwnablePK_assertOnlyOwner] recomputes the commitment + from both inside the proof and compares it against `_ownerCommitment`. + +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + ### Commitment Scheme The `ZOwnablePK` module employs a two-layer cryptographic commitment scheme designed to provide privacy, @@ -212,7 +255,7 @@ unlinkability, and collision resistance across deployments and ownership transfe The foundation of the system is the owner identifier, computed as: ```typescript -id = SHA256(pk, nonce) +id = SHA256(pk || nonce) ``` Where `pk` is the owner's public key and `nonce` is a secret value that may be either randomly generated for maximum privacy @@ -225,14 +268,15 @@ ensuring the owner's identity remains confidential. The final ownership commitment stored on-chain is computed as: ```typescript -commitment = SHA256(id, instanceSalt, counter, pad(32, "ZOwnablePK:shield:")) +commitment = SHA256(id || instanceSalt || counter || pad(32, "ZOwnablePK:shield:")) ``` This multi-element hash provides several security properties: * `id`: The privacy-preserving owner identifier described above. * `instanceSalt`: A unique per-deployment salt that prevents commitment collisions across different contract instances, -even when the same owner and nonce are used. +even when the same owner and nonce are used. It must be nonzero, and it should be a fresh random value per deployment: +a reused salt links the same owner across deployments. * `counter`: Incremented with each ownership transfer to ensure unlinkability. Each transfer produces a completely different commitment even with the same underlying owner. * `pad(32, "ZOwnablePK:shield:")`: A domain separator padded to 32 bytes that prevents hash collisions @@ -246,6 +290,13 @@ This commitment scheme ensures that: * Observers cannot correlate past and future ownership. * Cross-contract collisions are prevented through instance-specific salting. + + `ownPublicKey()` is a witness: it returns the caller's Zswap coin public key, and nothing in the circuit proves + that the caller holds the matching private key. The secret nonce is therefore the owner credential: + anyone holding the nonce and the owner's public key can pass [ZOwnablePK_assertOnlyOwner]. + Generate the nonce as 32 bytes from a CSPRNG (or a derivation with equivalent entropy) and keep it private. + + ### Nonce Generation Strategies The choice of nonce generation strategy represents a fundamental trade-off between simplicity/security and recoverability. @@ -261,7 +312,7 @@ const ownerId = ZOwnablePK._computeOwnerId(publicKey, randomNonce); ``` This approach is easy to generate and ensures maximum unlinkability. -However, it requires secure backup of both the private key and the nonce. +However, it requires secure backup of both the public key and the nonce. **Loss of either component results in permanent, irrecoverable loss of ownership.** #### Deterministic Nonce @@ -318,18 +369,18 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK" prefix ZOwnablePK_; constructor( - initOwnerCommitment: Bytes<32>, + initOwnerId: Bytes<32>, instanceSalt: Bytes<32>, ) { - ZOwnablePK_initialize(initOwnerCommitment, instanceSalt); + ZOwnablePK_initialize(initOwnerId, instanceSalt); } export circuit owner(): Bytes<32> { return ZOwnablePK_owner(); } -export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { - return ZOwnablePK_transferOwnership(disclose(newOwnerCommitment)); +export circuit transferOwnership(newOwnerId: Bytes<32>): [] { + return ZOwnablePK_transferOwnership(disclose(newOwnerId)); } export circuit renounceOwnership(): [] { @@ -337,6 +388,8 @@ export circuit renounceOwnership(): [] { } ``` +[ZOwnablePK_initialize] rejects a zero `ownerId` and a zero `instanceSalt`. + Similar to the [Ownable](#usage) module, circuits can be protected so that only the contract owner may call them by adding `assertOnlyOwner` as the first line in the circuit body: @@ -373,7 +426,7 @@ const generateId = ( return persistentHash(rt_type, [pk, nonce]); }; -// Instance salt for the constructor +// Instance salt for the constructor (must be nonzero) const generateInstanceSalt = (): Uint8Array => { return getRandomValues(new Uint8Array(32)); } diff --git a/content/contracts-compact/security.mdx b/content/contracts-compact/security.mdx index e8419e1b..b2824704 100644 --- a/content/contracts-compact/security.mdx +++ b/content/contracts-compact/security.mdx @@ -2,6 +2,24 @@ title: Security --- +{/* links */} +[Ownable]: ./ownable +[Utils guide]: ./utils#account-identifiers +[assertPaused]: ./api/security#Pausable-assertPaused +[assertNotPaused]: ./api/security#Pausable-assertNotPaused +[Initializable API]: ./api/security#Initializable +[Allowlist API]: ./api/security#Allowlist +[Blocklist API]: ./api/security#Blocklist +[isAllowed]: ./api/security#Allowlist-isAllowed +[assertAllowed]: ./api/security#Allowlist-assertAllowed +[_allow]: ./api/security#Allowlist-_allow +[_disallow]: ./api/security#Allowlist-_disallow +[isBlocked]: ./api/security#Blocklist-isBlocked +[assertNotBlocked]: ./api/security#Blocklist-assertNotBlocked +[_block]: ./api/security#Blocklist-_block +[_unblock]: ./api/security#Blocklist-_unblock +[compact#270]: https://github.com/LFDT-Minokawa/compact/issues/270 + The following documentation provides context, reasoning, and examples of modules found in the Security directory. ## Initializable @@ -11,11 +29,21 @@ More specifically, it enables logic to be performed once and only once to set up This is useful when a constructor cannot be used, for example when there are circular dependencies at construction time. -Many modules also use the initializable pattern which ensures that implementing contracts: +The initializable pattern ensures that implementing contracts: - Don't allow circuit calls until the contract is initialized. - Can only initialize the contract once. + + Use this module only at the top contract layer, and import it into at most one module per contract. + The Compact compiler collapses a shared import's ledger state inconsistently depending on directory layout + ([compact#270]): when two modules in the same directory both import Initializable, + their `_isInitialized` flags share a single ledger slot, so initializing one marks the other as initialized + and the other can never be initialized on its own. + For this reason the OpenZeppelin modules do not import Initializable; each one tracks its own + `_isInitialized` ledger flag inline. + + ### Usage ```ts @@ -38,7 +66,7 @@ export circuit doSomethingBeforeInitialized(): [] { export circuit setFieldAfterDeployment(f: Field): [] { // Initialize so the circuit cannot be called again Initializable_initialize(); - _fieldAfterDeployment = f; + _fieldAfterDeployment = disclose(f); } export circuit checkFieldAfterDeployment(): Field { @@ -48,11 +76,9 @@ export circuit checkFieldAfterDeployment(): Field { } ``` -## Pausable +See the [Initializable API] for the full list of circuits. -[Ownable]: ./ownable -[assertPaused]: ./api/security#Pausable-assertPaused -[assertNotPaused]: ./api/security#Pausable-assertNotPaused +## Pausable The Pausable module allows contracts to implement an emergency stop mechanism. This can be useful for scenarios such as preventing trades until the end of an evaluation period @@ -101,3 +127,156 @@ export circuit whenPaused(): [] { // Do something } ``` + +## Allowlist + +The Allowlist module is a minimal, reusable permit-list keyed by a `Bytes<32>` account identifier. +It answers a single question: is this account a member? +It backs use cases such as KYC allowlists, permissioned mints, and membership gates. + +The module is a membership mechanism, not a policy. +It holds no access control and no notion of when the list is enforced: + +- The mutators [_allow] and [_disallow] are ungated building blocks. + The implementing contract must protect them with its own access control module, + in the same way a token protects `_mint` and `_burn`. +- The implementing contract decides where [assertAllowed] applies, + for example only on transfers, only on mints, or only while an "eligibility required" flag is set. +- [isAllowed] reads membership without asserting it. + +### What is public + +The approved set lives in the public ledger field `_allowed: Set>`. +Membership is publicly readable and the approved set is publicly enumerable. +Every circuit discloses the `account` it is called with, so the identifier reaches the ledger as a public input. +This is the right posture for transparent or public-graph assets. +An asset whose transaction graph is meant to stay private should not use a public allowlist, +since the published candidate set would bound its anonymity set. + +The `Bytes<32>` keys are account identifiers as derived by `Utils_computeAccountId`; +see the [Utils guide] for how to derive one off-chain. + +### Usage + +The following example is a token whose recipients must be approved by the owner before they can receive tokens. +Membership changes are gated behind [Ownable], and the gate is enforced on `transfer`: + +```ts +// PermissionedToken.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/security/Allowlist" + prefix Allowlist_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + initialOwner: Either, ContractAddress>, + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, +) { + Ownable_initialize(initialOwner); + FungibleToken_initialize(name, symbol, decimals); +} + +// Membership changes are protected by the owner. +export circuit allow(account: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Allowlist__allow(account); +} + +export circuit disallow(account: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Allowlist__disallow(account); +} + +export circuit isAllowed(account: Bytes<32>): Boolean { + return Allowlist_isAllowed(account); +} + +// The implementing contract decides where the gate applies. +export circuit transfer(to: Either, ContractAddress>, value: Uint<128>): Boolean { + assert(to.is_left, "PermissionedToken: recipient must be an account identifier"); + Allowlist_assertAllowed(to.left); + return FungibleToken_transfer(to, value); +} +``` + +See the [Allowlist API] for the full list of circuits. + +## Blocklist + +The Blocklist module is a minimal, reusable deny-list keyed by a `Bytes<32>` account identifier. +It answers a single question: is this account blocked? +It backs use cases such as sanctions lists, account freezes, and abuse denials. + +As with Allowlist, the module is a membership mechanism, not a policy: + +- The mutators [_block] and [_unblock] are ungated building blocks that the implementing contract + must protect with its own access control module. +- The implementing contract decides where [assertNotBlocked] applies. +- [isBlocked] reads membership without asserting it. + +### What is public + +The blocked set lives in the public ledger field `_blocked: Set>`. +Membership is publicly readable and the blocked set is publicly enumerable, +and every circuit discloses the `account` it is called with. +As with Allowlist, this is the right posture for transparent or public-graph assets only. + +### Usage + +The following example lets the owner freeze accounts, and refuses transfers to a frozen recipient: + +```ts +// FreezableToken.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/security/Blocklist" + prefix Blocklist_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + initialOwner: Either, ContractAddress>, + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, +) { + Ownable_initialize(initialOwner); + FungibleToken_initialize(name, symbol, decimals); +} + +// Freezing and unfreezing are protected by the owner. +export circuit block(account: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Blocklist__block(account); +} + +export circuit unblock(account: Bytes<32>): [] { + Ownable_assertOnlyOwner(); + Blocklist__unblock(account); +} + +export circuit isBlocked(account: Bytes<32>): Boolean { + return Blocklist_isBlocked(account); +} + +// The implementing contract decides where the gate applies. +export circuit transfer(to: Either, ContractAddress>, value: Uint<128>): Boolean { + assert(to.is_left, "FreezableToken: recipient must be an account identifier"); + Blocklist_assertNotBlocked(to.left); + return FungibleToken_transfer(to, value); +} +``` + +See the [Blocklist API] for the full list of circuits. diff --git a/content/contracts-compact/shielded-access-control.mdx b/content/contracts-compact/shielded-access-control.mdx index e3a3d34c..578933c0 100644 --- a/content/contracts-compact/shielded-access-control.mdx +++ b/content/contracts-compact/shielded-access-control.mdx @@ -18,6 +18,11 @@ title: ShieldedAccessControl [getRoleAdmin]: api/shielded-access-control#ShieldedAccessControl-getRoleAdmin [computeAccountId]: api/shielded-access-control#ShieldedAccessControl-computeAccountId [DEFAULT_ADMIN_ROLE]: api/shielded-access-control#ShieldedAccessControl-DEFAULT_ADMIN_ROLE +[_operatorRoles]: api/shielded-access-control#ShieldedAccessControl-_operatorRoles +[_grantedRoleNullifiers]: api/shielded-access-control#ShieldedAccessControl-_grantedRoleNullifiers +[_roleCommitmentNullifiers]: api/shielded-access-control#ShieldedAccessControl-_roleCommitmentNullifiers +[wit_secretKey]: api/shielded-access-control#ShieldedAccessControl-wit_secretKey +[wit_getRoleCommitmentPath]: api/shielded-access-control#ShieldedAccessControl-wit_getRoleCommitmentPath This module provides a **privacy-preserving** role-based access control mechanism. Unlike [AccessControl], which stores role assignments in a public map, @@ -47,26 +52,44 @@ ShieldedAccessControl is built on three cryptographic primitives: When a role is granted, a commitment is inserted into a Merkle tree: ``` -roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain) +roleCommitment = SHA256(role || accountId || instanceSalt || commitmentDomain) ``` **Account identifiers** are privacy-preserving identity commitments derived from a secret key and a per-deployment instance salt. A single secret key can be used across multiple roles within the same contract: ``` -accountId = SHA256(secretKey, instanceSalt, accountIdDomain) +accountId = SHA256(secretKey || instanceSalt || accountIdDomain) ``` **Role nullifiers** are one-time burn tokens. When a role is revoked, its nullifier is inserted into a set, permanently invalidating the corresponding commitment: ``` -roleNullifier = SHA256(roleCommitment, nullifierDomain) +roleNullifier = SHA256(roleCommitment || nullifierDomain) ``` The `instanceSalt` is a per-deployment random value that prevents commitment collisions across contracts and ensures that the same secret key produces different identities in different deployments. +### Public ledger and private witnesses + +What is on the ledger: + +- [_operatorRoles]: the Merkle tree of role commitments. Inserts expose only the internal leaf hash, not the commitment value. +- [_grantedRoleNullifiers]: the nullifier of every `(role, accountId)` pairing ever granted. +- [_roleCommitmentNullifiers]: the nullifier of every pairing that has been revoked. +- `_adminRoles`, the sealed `_instanceSalt`, and `_isInitialized`. + +What each role holder keeps private, and supplies through witnesses at proving time: + +- The `secretKey`, via [wit_secretKey]. It is hashed with `_instanceSalt` inside the proof and never reaches the ledger. +- The Merkle path to their role commitment, via [wit_getRoleCommitmentPath]. + The consumer's witness implementation builds this path from the exported `_operatorRoles` state. + +Witness implementations are authored by the consuming DApp. +The TypeScript witnesses in the repository are test-only and are not published with the package. + ## Using `ShieldedAccessControl` Usage follows the same pattern as [AccessControl] which is to define role identifiers, @@ -103,7 +126,7 @@ export circuit mint(/* ... */): [] { ``` - The `instanceSalt` must be generated using a cryptographically secure random number generator + The `instanceSalt` must be nonzero and generated using a cryptographically secure random number generator (e.g. `crypto.getRandomValues()`) prior to deployment. Weak or predictable salts weaken privacy guarantees. @@ -132,6 +155,14 @@ Only accounts that can prove they hold the admin role may call [grantRole] and [ By default, all roles are administered by [DEFAULT_ADMIN_ROLE]. Custom admin relationships can be set using [_setRoleAdmin]. +Each `(role, accountId)` pairing has exactly one lifecycle: + +- **Grant**: the first grant inserts the role commitment into the Merkle tree and records the pairing's nullifier + in [_grantedRoleNullifiers]. Granting a pairing that is already active is a no-op, so a pairing never consumes + more than one tree slot. +- **Revoke**: revoking (or renouncing) inserts the nullifier into [_roleCommitmentNullifiers]. + A pairing that was never granted cannot be revoked; the call reverts with `role was never granted`. + There is one important difference from unshielded AccessControl: **revocation is permanent for a given identity**. When a role is revoked, its nullifier is burned, and the same `(role, accountId)` pairing can never be granted again. @@ -144,7 +175,8 @@ until all roles under the old key are no longer needed. [renounceRole] allows a role holder to voluntarily give up their own role. This is useful if a trusted device is compromised and the holder wants to immediately invalidate their credentials. -The caller must confirm their `accountId` to prevent accidental renunciation. +The caller must confirm their `accountId` to prevent accidental renunciation, +and the pairing must have been granted. Consumers with no need for self-revocation should not export this circuit. ### Proving Role Ownership @@ -168,12 +200,13 @@ ShieldedAccessControl provides strong privacy guarantees, but it's important to - The same nullifier across calls, allowing them to link all actions performed under the same role instance over time (pseudonymity, not anonymity) - Correlation between a grant and its revocation (the same nullifier appears in both transactions) - Which role is being granted or revoked when the admin-checked circuits ([grantRole], [revokeRole]) are used, since those disclose the role identifier in the same transaction -- The cumulative number of role grants across all roles; per-role counts are also inferable when the admin-checked circuits are used +- The number of distinct `(role, accountId)` pairings ever granted across all roles; per-role counts are also inferable when the admin-checked circuits are used **What observers CANNOT see:** - The identity of any role holder, so long as secret keys are kept private and generated using cryptographically secure random values - The value of role commitments stored in the Merkle tree (MerkleTree inserts hash the value internally; only the internal leaf hash appears as a public input, unlike Set operations where values are directly visible) -- Users can be retroactively deanonymized if their secret key is exposed + +Users can be retroactively deanonymized if their secret key is exposed. The internal circuits ([_grantRole], [_revokeRole]) do not disclose the role identifier, offering stronger privacy @@ -183,10 +216,10 @@ ShieldedAccessControl provides strong privacy guarantees, but it's important to ## Security Considerations -- The `_operatorRoles` Merkle tree has a **fixed capacity of 2^20 leaf slots** and slots cannot be reclaimed after revocation. Monitor slot consumption off-chain and plan capacity accordingly. -- A single secret key compromise exposes **all roles** held by that user within the contract instance. Users requiring compartmentalization between roles should use separate keys per role. -- Admins can revoke `(role, accountId)` pairings that were never granted (non-membership proofs are not available for the Merkle tree). Admin trust is a fundamental assumption. Off-chain validation should confirm a pairing was actually granted before submitting a revocation. -- The SHA256 hashing function is used throughout. A migration to a ZK-friendly hashing function is planned when an implementation becomes available. +- The `_operatorRoles` Merkle tree has a **fixed capacity of 2^20 leaf slots**. Each distinct `(role, accountId)` pairing consumes one slot, re-granting an active pairing is a no-op, and slots cannot be reclaimed after revocation. Once exhausted, the contract can never grant a new role. Monitor slot consumption off-chain (`firstFree()` on the exported tree in TypeScript, `isFull()` in a circuit) and plan capacity accordingly. +- A single secret key compromise exposes **all roles** held by that user within the contract instance. Users requiring compartmentalization between roles should use separate keys per role. On compromise, coordinate with admins to revoke every role under the compromised `accountId` and re-grant under a new identity. +- A pairing can only be revoked after it was granted, so the nullifier set holds at most one entry per granted pairing. Admin trust is still a fundamental assumption: an admin can revoke any granted pairing, and revocation is irreversible. +- The SHA256 hashing function is used throughout. For the full API reference, see the [ShieldedAccessControl API]. diff --git a/content/contracts-compact/utils.mdx b/content/contracts-compact/utils.mdx index 1437ed3e..a7589eaa 100644 --- a/content/contracts-compact/utils.mdx +++ b/content/contracts-compact/utils.mdx @@ -2,9 +2,88 @@ title: Utils --- +{/* links */} +[Utils API]: ./api/utils#Utils +[computeAccountId]: ./api/utils#Utils-computeAccountId +[zeroAccount]: ./api/utils#Utils-zeroAccount +[isTargetZero]: ./api/utils#Utils-isTargetZero +[selfAsRecipient]: ./api/utils#Utils-selfAsRecipient +[Ownable]: ./ownable +[AccessControl]: ./access-control + The Utils module provides miscellaneous circuits and common utilities for Compact contract development. +Most of them are pure circuits with no ledger state, so they can be called from a circuit, +from a constructor, or off-chain through the compiled contract. + +## Account identifiers + +The [Ownable] and [AccessControl] modules, and the token modules, identify a caller by a `Bytes<32>` account identifier +rather than by a public key. +The caller proves knowledge of a 32-byte secret key through a witness, and the module derives the identifier as: + +``` +accountId = H(secretKey) +``` + +where `H` is `persistentHash`. +[computeAccountId] is the shared derivation: it is a pure circuit, so the same function that the modules +evaluate inside a proof can be evaluated ahead of time to produce the identifier a contract expects as input, +for example the `initialOwner` of an Ownable contract, the `defaultAdmin` of an AccessControl contract, +or the recipient of a token mint. -### Usage +The witnesses that supply the secret key are authored by the implementing contract's TypeScript side. +The witnesses in the library repository are test-only and are not published with the package. + +### Deriving an identifier off-chain + +Any of the following produce the same identifier for the same key: + +* Call the pure circuit through the compiled contract: `pureCircuits.computeAccountId(secretKey)` + (exposed by any contract that re-exports it as a `pure circuit`). +* Compute it directly with `@midnight-ntwrk/compact-runtime`: + +```typescript +import { + CompactTypeBytes, + CompactTypeVector, + persistentHash, +} from '@midnight-ntwrk/compact-runtime'; + +const accountIdOf = (secretKey: Uint8Array): Uint8Array => + persistentHash(new CompactTypeVector(1, new CompactTypeBytes(32)), [secretKey]); +``` + +The secret key must be a 32-byte cryptographically secure random value (`crypto.getRandomValues()` or equivalent). + + + The secret key is the identity. Never log or persist it in plaintext, keep it in offline or enclave storage + where possible, and avoid evaluating the derivation in an untrusted runtime. A lost key cannot be recovered, + and an exposed key allows impersonation. + + +### Scope of the identifier + +The derivation is deliberately untagged and global: the same secret key yields the same identifier in every +module and every contract that derives one, so a user who wants to carry a single identity across contracts can. +Users who want unlinkable identities across contracts should use a different secret key per contract. +A module that needs a per-deployment, unlinkable identity should not use this circuit. + +The output is an identifier, not a secret. Because the construction carries no domain-separation tag, +it must not be reused to derive private values: a circuit that derives a private value from a secret +must use its own domain-separation tag. + +## Zero accounts + +[zeroAccount] returns the canonical zero `Either, ContractAddress>` (the left variant set to zero), +which the token modules use as the mint and burn sentinel and for cleared approvals. +[isTargetZero] checks whether the active side of such a value is zero. + +## Contract as recipient + +[selfAsRecipient] wraps the executing contract's own address as an `Either`, +the recipient shape expected by shielded send operations, for deposits into the contract and for receiving change. + +## Usage ```typescript pragma language_version >= {{compact_language_version}}; @@ -24,4 +103,12 @@ export circuit performActionWhenEqual( // Do something else } } + +// Re-export the derivation so the identifier can be computed off-chain +// through the compiled contract. +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} ``` + +See the [Utils API] for the full list of circuits. diff --git a/content/contracts-compact/utils/constants.js b/content/contracts-compact/utils/constants.js index 27c5d08b..fb6add47 100644 --- a/content/contracts-compact/utils/constants.js +++ b/content/contracts-compact/utils/constants.js @@ -1,2 +1,2 @@ -export const COMPACT_COMPILER_VERSION = "0.29.0"; -export const COMPACT_LANGUAGE_VERSION = "0.21.0"; +export const COMPACT_COMPILER_VERSION = "0.31.0"; +export const COMPACT_LANGUAGE_VERSION = "0.23.0"; diff --git a/src/navigation/midnight.json b/src/navigation/midnight.json index 793f569e..b24a8fe9 100644 --- a/src/navigation/midnight.json +++ b/src/navigation/midnight.json @@ -73,9 +73,29 @@ "type": "page", "name": "MultiToken", "url": "/contracts-compact/multi-token" + }, + { + "type": "page", + "name": "NativeShieldedToken", + "url": "/contracts-compact/native-shielded-token" + }, + { + "type": "page", + "name": "ConfidentialFungibleToken", + "url": "/contracts-compact/confidential-fungible-token" } ] }, + { + "type": "page", + "name": "Crypto", + "url": "/contracts-compact/crypto" + }, + { + "type": "page", + "name": "Multisig", + "url": "/contracts-compact/multisig" + }, { "type": "page", "name": "Utils", @@ -92,6 +112,16 @@ "name": "AccessControl API", "url": "/contracts-compact/api/access-control" }, + { + "type": "page", + "name": "ConfidentialFungibleToken API", + "url": "/contracts-compact/api/confidential-fungible-token" + }, + { + "type": "page", + "name": "Crypto API", + "url": "/contracts-compact/api/crypto" + }, { "type": "page", "name": "FungibleToken API", @@ -102,6 +132,16 @@ "name": "MultiToken API", "url": "/contracts-compact/api/multi-token" }, + { + "type": "page", + "name": "Multisig API", + "url": "/contracts-compact/api/multisig" + }, + { + "type": "page", + "name": "NativeShieldedToken API", + "url": "/contracts-compact/api/native-shielded-token" + }, { "type": "page", "name": "NonFungibleToken API", From 204fec4a44dd9d62751453f49430588898bbe924 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 25 Oct 2026 15:53:13 -0300 Subject: [PATCH 02/10] improve api docs --- .../contracts-compact/api/access-control.mdx | 99 +-- .../api/confidential-fungible-token.mdx | 395 ++++++---- content/contracts-compact/api/crypto.mdx | 262 ++++--- .../contracts-compact/api/fungible-token.mdx | 230 +++--- content/contracts-compact/api/multi-token.mdx | 213 +++--- content/contracts-compact/api/multisig.mdx | 516 ++++++++----- .../api/native-shielded-token.mdx | 715 ++++++++---------- .../api/non-fungible-token.mdx | 293 ++++--- content/contracts-compact/api/ownable.mdx | 226 +++--- content/contracts-compact/api/security.mdx | 52 +- .../api/shielded-access-control.mdx | 259 ++++--- content/contracts-compact/api/utils.mdx | 90 ++- 12 files changed, 1735 insertions(+), 1615 deletions(-) diff --git a/content/contracts-compact/api/access-control.mdx b/content/contracts-compact/api/access-control.mdx index d44359c6..2da99115 100644 --- a/content/contracts-compact/api/access-control.mdx +++ b/content/contracts-compact/api/access-control.mdx @@ -69,7 +69,6 @@ import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; id="AccessControl-_operatorRoles" > Mapping from a role identifier -> account -> its permissions. - Account keys are stored canonicalized (the inactive branch of the `Either` is zeroed). #### _adminRoles [toc] [#AccessControl-_adminRoles] @@ -95,8 +94,7 @@ The TypeScript witnesses in the repository are test-only and are not published w kind="witness" id="AccessControl-wit_AccessControlSK" > - Returns the caller's secret key used in deriving the account identifier `H(secretKey)`. - The key never reaches the ledger; only the derived identifier is used as a map key. + Returns the caller's secret key used in deriving the account identifier. The same key produces the same account identifier across all contracts. Users who desire cross-contract unlinkability should use different keys per contract. @@ -112,9 +110,8 @@ The TypeScript witnesses in the repository are test-only and are not published w kind="pure" modulePrefix="AccessControl" > - The default admin role for all roles. Returns zero bytes (`default>`). - Only accounts with this role will be able to grant or revoke other roles - unless custom admin roles are created via [_setRoleAdmin](#AccessControl-_setRoleAdmin). + The default admin role for all roles. Only accounts with this role will be able + to grant or revoke other roles unless custom admin roles are created via `_setRoleAdmin`. #### hasRole [toc] [#AccessControl-hasRole] @@ -126,9 +123,6 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="AccessControl" > Returns `true` if `account` has been granted `roleId`. - `account` is canonicalized before the lookup. - - Discloses: `roleId` and the canonicalized `account` (map lookup keys). #### assertOnlyRole [toc] [#AccessControl-assertOnlyRole] @@ -140,14 +134,12 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="AccessControl" > Reverts if the caller cannot prove ownership of `roleId`. - The caller's identity is derived from the `wit_AccessControlSK` witness as `H(secretKey)`, - the same derivation as [`Utils.computeAccountId`](./utils#Utils-computeAccountId). - - Discloses: `roleId` and the caller's account identifier (map lookup keys). + In the case of an external (non-contract) caller, the caller’s identity is + derived from the `wit_AccessControlSK` witness as `persistentHash(secretKey)`. Requirements: - * The caller must have `roleId`. + - The caller must have `roleId`. #### _checkRole [toc] [#AccessControl-_checkRole] @@ -160,11 +152,9 @@ The TypeScript witnesses in the repository are test-only and are not published w > Reverts if `account` is missing `roleId`. - Discloses: `roleId` and the canonicalized `account` (map lookup keys). - Requirements: - * `account` must have `roleId`. + - `account` must have `roleId`. #### getRoleAdmin [toc] [#AccessControl-getRoleAdmin] @@ -175,12 +165,10 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=9, rows=373" modulePrefix="AccessControl" > - Returns the admin role that controls `roleId` or a byte array with all zero bytes if `roleId` doesn't exist. - See [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole). + Returns the admin role that controls `roleId` or + a byte array with all zero bytes if `roleId` doesn't exist. See `grantRole` and `revokeRole`. - To change a role's admin use [_setRoleAdmin](#AccessControl-_setRoleAdmin). - - Discloses: `roleId`. + To change a role's admin use `_setRoleAdmin`. #### grantRole [toc] [#AccessControl-grantRole] @@ -193,17 +181,10 @@ The TypeScript witnesses in the repository are test-only and are not published w > Grants `roleId` to `account`. - - Granting roles to contract addresses is currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents permanently disabling access to a circuit. - - - Discloses: `roleId`, the caller's account identifier, and the canonicalized `account`, which is written into `_operatorRoles`. - Requirements: - * `account` must not be a ContractAddress. - * The caller must have `roleId`'s admin role. + - `account` must not be a ContractAddress. + - The caller must have `roleId`'s admin role. #### revokeRole [toc] [#AccessControl-revokeRole] @@ -216,11 +197,9 @@ The TypeScript witnesses in the repository are test-only and are not published w > Revokes `roleId` from `account`. - Discloses: `roleId`, the caller's account identifier, and the canonicalized `account`. - Requirements: - * The caller must have `roleId`'s admin role. + - The caller must have `roleId`'s admin role. #### renounceRole [toc] [#AccessControl-renounceRole] @@ -234,25 +213,18 @@ The TypeScript witnesses in the repository are test-only and are not published w > Revokes `roleId` from the calling account. - Roles are often managed via [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole): - this circuit's purpose is to provide a mechanism for accounts to lose their privileges + Roles are often managed via `grantRole` and `revokeRole`: this circuit's + purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). The caller's identity is derived from the `wit_AccessControlSK` witness as - `H(secretKey)`. The `callerConfirmation` parameter must match + `persistentHash(secretKey)`. The `callerConfirmation` parameter must match the caller's computed account identifier to prevent accidental renunciation. - - We do not provide functionality for smart contracts to renounce roles because self-executing transactions are not supported on Midnight at this time. - We may revisit this in future if this feature is made available in Compact. - - - Discloses: `roleId` and `callerConfirmation`. - Requirements: - * The caller's computed account identifier must match `callerConfirmation`. - * The caller must not be a `ContractAddress`. + - The caller's computed account identifier must match `callerConfirmation`. + - The caller must not be a `ContractAddress`. #### _setRoleAdmin [toc] [#AccessControl-_setRoleAdmin] @@ -264,9 +236,6 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="AccessControl" > Sets `adminRole` as `roleId`'s admin role. - Internal circuit without access restriction. - - Discloses: `roleId` and `adminRole`, written to `_adminRoles`. #### _grantRole [toc] [#AccessControl-_grantRole] @@ -277,21 +246,12 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=11, rows=1143" modulePrefix="AccessControl" > - Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted - (`false` when `account` already holds it). - + Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. Internal circuit without access restriction. - - Granting roles to contract addresses is currently disallowed in this circuit until contract-to-contract interactions are supported in Compact. - This restriction prevents permanently disabling access to a circuit. - - - Discloses: `roleId` and the canonicalized `account`, written into `_operatorRoles`. - Requirements: - * `account` must not be a ContractAddress. + - `account` must not be a ContractAddress. #### _unsafeGrantRole [toc] [#AccessControl-_unsafeGrantRole] @@ -302,16 +262,11 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=11, rows=1142" modulePrefix="AccessControl" > - Unsafe variant of [_grantRole](#AccessControl-_grantRole). - It does not check whether `account` is a ContractAddress. - - - Granting roles to contract addresses is considered unsafe because contract-to-contract calls are not currently supported. - Granting a role to a smart contract may render a circuit permanently inaccessible. - Once contract-to-contract calls are supported, this circuit may be deprecated. - + Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. + Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress. - Discloses: `roleId` and the canonicalized `account`, written into `_operatorRoles`. + External smart contracts cannot call the access control contract at this time, so granting a role to a ContractAddress may + render a circuit permanently inaccessible. #### _revokeRole [toc] [#AccessControl-_revokeRole] @@ -322,10 +277,6 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=11, rows=1073" modulePrefix="AccessControl" > - Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked - (`false` when `account` did not hold it). - + Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. Internal circuit without access restriction. - - Discloses: `roleId` and the canonicalized `account`. diff --git a/content/contracts-compact/api/confidential-fungible-token.mdx b/content/contracts-compact/api/confidential-fungible-token.mdx index 6924632a..23508f77 100644 --- a/content/contracts-compact/api/confidential-fungible-token.mdx +++ b/content/contracts-compact/api/confidential-fungible-token.mdx @@ -36,11 +36,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl kind="struct" id="ConfidentialFungibleToken-EscrowEntry" > - The encrypted allowance an owner granted a spender. `spenderCt` and `ownerCt` are two - [ElGamal ciphertexts](./crypto#ElGamal-Ciphertext) of the same remaining amount, one under each party's key. - `ownerMemo` is an [EcdhMask ciphertext](./crypto#EcdhMask-Ciphertext) that lets the owner read the current - remaining allowance directly with its encryption secret; it is set to the cap on `approve` and refreshed to the - reduced remainder on every spend. There is no public cap. + Per-(owner, spender) encrypted escrow. Stored as two + ciphertexts of the same value: one to the spender, one to the owner. + No public cap. ### Ledger [toc] [#ConfidentialFungibleToken-Ledger] @@ -53,7 +51,12 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl kind="ledger" id="ConfidentialFungibleToken-_balances" > - Mapping from account identifiers to their spendable balance ciphertext. Debits draw from this cell. + Dual-balance grief fix. `_balances` is the SPENDABLE (confirmed) balance that + debits draw from; `_pending` is the incoming pool that credits land in. Only + the owner moves pending -> spendable, via `sweep()`. So a third party spamming + dust credits cannot churn a victim's spendable ciphertext and invalidate the + victim's in-flight spend proofs (a liveness grief). A wallet's total balance + is `balanceOf` (spendable) + `pendingOf` (pending). #### _pending [toc] [#ConfidentialFungibleToken-_pending] @@ -106,9 +109,16 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl kind="ledger" id="ConfidentialFungibleToken-_escrowSpendEpochs" > - Mapping from owner accounts to spender accounts to the number of times that escrow has been spent from. - The counter is folded into the randomness of each spend and never resets, so the per-pair spend count is - public and permanent. + Per-(owner, spender) escrow-spend counter, folded into the randomness + `_spendEscrow` derives so repeat spends by one spender against one owner + never share a one-time pad. It restarts for each pair, which is why both + counterparties are bound in too; see `SpendRandomnessPreimage`. + + Kept out of `EscrowEntry`: `approve` replaces that entry wholesale, so a + counter living there would restart on every re-approve. + + Note that this exposes a per-pair spend count which is already derivable + from tx history. #### _creditEpochs [toc] [#ConfidentialFungibleToken-_creditEpochs] @@ -118,9 +128,17 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl kind="ledger" id="ConfidentialFungibleToken-_creditEpochs" > - Mapping from account identifiers to the lifetime number of credits received. The counter is folded into the - randomness of each credit and never resets (it is not cleared by [clearMemos](#ConfidentialFungibleToken-clearMemos)), - so an account's credit count is public and permanent. + Per-account credit counter. `_credit` uses it as the nonce that keeps each + credit's randomness distinct, so it MUST only ever increase. No circuit + resets it. It is kept out of `_memos` for that reason: the memo list is + prunable by its owner, and a nonce that could be walked back would let a + reused seed repeat an earlier credit's randomness. The cost is that an + account's lifetime credit count is now permanently readable from state. + + `Counter` is chosen for no-reset monotonicity, not commutativity. Its + `increment` emits no pinned read, but `_credit` reads the value back to + derive the nonce, and that read pins the recipient's key. Credits to one + recipient still serialize; see the Concurrency note above. #### _isInitialized [toc] [#ConfidentialFungibleToken-_isInitialized] @@ -175,10 +193,8 @@ The TypeScript witnesses in the repository are test-only and are not published; kind="witness" id="ConfidentialFungibleToken-wit_ConfidentialTokenSK" > - Returns the caller's account secret. Verified in-circuit: `accountId = H(SK)` is used for every ledger lookup, - so a wrong secret resolves to a different account and the call fails. - - The same secret produces the same account identifier across all contracts. + Returns the caller's account secret. Verified in-circuit by + matching `persistentHash(SK)` against the public accountId. #### wit_ConfidentialTokenEK [toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenEK] @@ -188,9 +204,9 @@ The TypeScript witnesses in the repository are test-only and are not published; kind="witness" id="ConfidentialFungibleToken-wit_ConfidentialTokenEK" > - Returns the caller's ElGamal encryption secret. Verified in-circuit by re-deriving the public key and comparing it - against the key stored at registration. May share key material with `wit_ConfidentialTokenSK`; the two derivations - are domain-separated. + Returns the caller's ElGamal secret. Verified in-circuit by + `ElGamal_assertDecryptsTo`, which re-derives the pk and compares + against the stored pk. #### wit_PlaintextBalance [toc] [#ConfidentialFungibleToken-wit_PlaintextBalance] @@ -200,9 +216,18 @@ The TypeScript witnesses in the repository are test-only and are not published; kind="witness" id="ConfidentialFungibleToken-wit_PlaintextBalance" > - Returns the wallet's cached plaintext for `ct`. The ciphertext is passed explicitly so the wallet can route by - ciphertext identity rather than by call order. Verified in-circuit by `ElGamal_assertDecryptsTo`; a wrong value - fails the proof. The claim is `Uint<248>` so a balance accumulated past the per-operation bound stays spendable. + Returns the wallet's cached plaintext for the given + ciphertext. Called whenever the circuit needs to verify a ciphertext's + plaintext value (sender's main balance, escrow ciphertext during spend, + prior escrow during refund, etc.). + + The ciphertext is passed explicitly so the wallet can route to the right + cached plaintext by ciphertext identity. This avoids call-order + conventions (which would be brittle to circuit refactors) and lets the + wallet maintain a `Map` lookup. + + Verified in-circuit by `ElGamal_assertDecryptsTo`: a wrong plaintext + fails the assertion `Dec(ct, EK) == claimedValue`. #### wit_RandomnessSeed [toc] [#ConfidentialFungibleToken-wit_RandomnessSeed] @@ -212,15 +237,18 @@ The TypeScript witnesses in the repository are test-only and are not published; kind="witness" id="ConfidentialFungibleToken-wit_RandomnessSeed" > - Returns the 32-byte seed from which all per-operation randomness and every memo ephemeral is derived in-circuit. - Not verified in-circuit. + Returns a single 32-byte seed from which all per-operation + randomness (and each memo's ephemeral) is derived in-circuit. - - The seed must be fresh, high-entropy, and secret on every invocation. The derivation is deterministic and public, - so anyone who knows a seed can read the amounts it masked, and reusing a seed across two operations leaks the - difference between their amounts. On `transferFrom` and `_burnFrom` the seed is the spender's but the values it - protects are the owner's. - + Seed freshness is load-bearing for confidentiality on EVERY path (see the + module header). The derivation is deterministic and public, so an observer + who knows the seed can strip a memo's mask and read the delivered amount, and + recover balance/escrow amounts from the public ciphertexts. Seed REUSE leaks + plaintext differences and the circuit does NOT prevent it on most paths. + `_spendEscrow` is an exception (its randomness folds the spend epoch and both + counterparties); the credit path folds the recipient's `_creditEpochs` + counter as defense in depth, and that covers credits alone. The seed MUST be + fresh, high-entropy, and secret for every invocation. ### Circuits [toc] [#ConfidentialFungibleToken-Circuits] @@ -233,7 +261,16 @@ The TypeScript witnesses in the repository are test-only and are not published; kind="pure" modulePrefix="ConfidentialFungibleToken" > - Returns the maximum value accepted by a single transfer, mint, burn, or approve: the `Uint<128>` maximum. + Maximum allowed value per single transfer / mint / burn / + approve: the `Uint<128>` maximum. The prior `2^48` cap existed only to keep + wallet-side discrete-log recovery tractable; with the ECDH memo delivering + values directly (see `crypto/EcdhMask`), that constraint is gone. Values stay + well under the Jubjub scalar-field order, so homomorphic sums do not wrap. + + This equals the argument type's own maximum, so the per-op check is + redundant today. KEEP IT: it compares against a hard constant rather than + the argument type, so it still catches a later widening of the transfer + type — which is what the balance-claim width is sized against. #### initialize [toc] [#ConfidentialFungibleToken-initialize] @@ -248,14 +285,12 @@ The TypeScript witnesses in the repository are test-only and are not published; modulePrefix="ConfidentialFungibleToken" stackParams="true" > - Initializes the contract by setting the name, symbol, and decimals. - - This MUST be called in the implementing contract's constructor. - Failure to do so can lead to an irreparable contract. + Initializes the token's name, symbol, and decimals. Intended to + be called once, from the deploying contract's constructor. Requirements: - * Contract is not initialized. + - Contract is not already initialized. #### balanceOf [toc] [#ConfidentialFungibleToken-balanceOf] @@ -266,12 +301,13 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=10, rows=989" modulePrefix="ConfidentialFungibleToken" > - Returns `account`'s spendable balance ciphertext, or a well-formed encryption of zero if the account is unregistered. - The amount is encrypted; only the account holder can decrypt it. + Returns `account`'s balance ciphertext, or the default (zero) + ciphertext if the account is unregistered. The amount is encrypted; only the + account (or an authorized viewer) can decrypt it. Requirements: - * Contract is initialized. + - Contract is initialized. #### pendingOf [toc] [#ConfidentialFungibleToken-pendingOf] @@ -282,12 +318,14 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=10, rows=986" modulePrefix="ConfidentialFungibleToken" > - Returns `account`'s pending balance ciphertext (incoming credits not yet swept), or a well-formed encryption of zero - if the account is unregistered. An account's total is `balanceOf` plus `pendingOf`. + Returns `account`'s PENDING balance ciphertext (incoming credits + not yet swept into spendable), or Enc(0) if unregistered. The spendable + balance is `balanceOf`; a wallet's total is the sum of the two. See the + dual-balance note on the ledger declarations. Requirements: - * Contract is initialized. + - Contract is initialized. #### allowance [toc] [#ConfidentialFungibleToken-allowance] @@ -298,11 +336,18 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=10, rows=938" modulePrefix="ConfidentialFungibleToken" > - Returns the [EscrowEntry](#ConfidentialFungibleToken-EscrowEntry) that `owner` granted `spender`. - When no allowance exists, returns a well-formed entry whose ciphertexts encrypt zero, so the result is always safe - to prove against and to decrypt. "No allowance" and "an allowance of zero" read the same; a caller that must - distinguish them reads `_escrow` membership off chain. The getter grants nothing: the spend path gates on escrow - membership independently. + Returns the escrow entry (the encrypted allowance) that `owner` + granted `spender`. When no allowance exists, returns a well-formed + encrypted-zero entry (its encrypted amounts decrypt to 0, analogous to an + ERC-20 zero allowance) rather than `default`: the returned + Jubjub points are on-curve, so the view is safe to prove (e.g. through a + contract-to-contract call) and safe for a wallet to decrypt, whereas a zeroed + default holds off-curve points. The cap is encrypted, not public. + + "No allowance" and "an allowance of 0" both read as encrypted-zero; + a caller that must distinguish them reads `_escrow` membership off-chain. This + getter grants nothing, the spend path (`transferFrom`/`burnFrom`) gates on + `_escrow` membership independently, so a non-existent allowance is unspendable. #### isRegistered [toc] [#ConfidentialFungibleToken-isRegistered] @@ -324,16 +369,18 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=13, rows=8100" modulePrefix="ConfidentialFungibleToken" > - Registers the caller: derives their ElGamal public key from `wit_ConfidentialTokenEK`, stores it, and initializes - their spendable and pending balances to an encryption of zero. Registration is a prerequisite for sending or receiving. - Discloses the caller's `accountId` and public key. + Registers the caller: derives their encryption public key from + `wit_ConfidentialTokenEK` and initializes their balance to an encryption of + zero. Registration is a prerequisite for sending or receiving. - Returns the caller's `accountId`, so a composing contract can gate on who may register. + Returns the caller's `accountId`. A composing contract can gate on + the returned value (e.g. assert it is KYC-approved) to restrict who may + register. See the module header note on caller-identity gating. Requirements: - * Contract is initialized. - * The caller is not already registered. + - Contract is initialized. + - The caller is not already registered. #### _mint [toc] [#ConfidentialFungibleToken-_mint] @@ -344,22 +391,17 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=15, rows=27179" modulePrefix="ConfidentialFungibleToken" > - Credits `value` to `account`: homomorphically adds it to the account's pending ciphertext, pushes a memo carrying - `value` encrypted to the recipient's key, and increments the recipient's credit counter. Performs no supply accounting; - pair it with a supply tracker such as [_addSupply](#ConfidentialFungibleTokenPublicSupply-_addSupply). - Discloses `account`, the new pending ciphertext, and the memo; the amount is not disclosed. - - - A building block for a gated `mint` wrapper. Never re-export it from a deployed contract: a raw `_mint` is a - permissionless mint. - + Mints `value` to `account`: `_credit` under its intent name. + The building block for a composing contract's gated `mint`; it performs no + supply accounting. Pair it with a supply tracker (see + `extensions/ConfidentialFungibleTokenPublicSupply`) if the deployment tracks + `totalSupply`. Requirements: - * Contract is initialized. - * `value` is at most `MAX_TRANSFER_VALUE()`. - * `account` is not the zero account. - * `account` is registered. + - Contract is initialized. + - `value` is within `MAX_TRANSFER_VALUE()`. + - `account` is registered. #### _burn [toc] [#ConfidentialFungibleToken-_burn] @@ -370,23 +412,17 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=15, rows=16720" modulePrefix="ConfidentialFungibleToken" > - Debits `value` from the caller's spendable balance: the caller proves the balance plaintext, and the ciphertext is - replaced by a homomorphic subtraction of `value`. Performs no supply accounting; pair it with - [_subSupply](#ConfidentialFungibleTokenPublicSupply-_subSupply). Discloses the caller's `accountId` and the new - balance ciphertext; the amount is not disclosed. - - Returns the caller's `accountId` for caller-side gating. - - - A building block for a `burn` wrapper. Never re-export it raw from a deployed contract. - + Burns `value` from the caller's own balance: `_debit` under + its intent name. The building block for a composing contract's `burn`; it + performs no supply accounting. Pair it with a supply tracker (see + `extensions/ConfidentialFungibleTokenPublicSupply`) if the deployment tracks + `totalSupply`. Requirements: - * Contract is initialized. - * `value` is at most `MAX_TRANSFER_VALUE()`. - * The caller is registered and holds the encryption secret registered for their account. - * The caller's spendable balance is at least `value`. + - Contract is initialized. + - `value` is within `MAX_TRANSFER_VALUE()`. + - The caller is registered and has a balance of at least `value`. #### transfer [toc] [#ConfidentialFungibleToken-transfer] @@ -397,19 +433,20 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=16, rows=43701" modulePrefix="ConfidentialFungibleToken" > - Moves `value` from the caller to `to`: debits the caller's spendable balance, credits `to`'s pending balance, and - pushes a memo to `to`. Discloses the (sender, recipient) pair, both new ciphertexts, and the memo; the amount is not - disclosed. + Transfers `value` from the caller to `to`: debits the caller, + credits `to`, and pushes an encrypted memo to `to`. The amount is hidden; + the (sender, recipient) pair is public. Returns the caller's (sender's) `accountId` for caller-side gating. + The sender is derived from the same authentication `_debit` performs, so the + returned id is exactly the account whose funds moved. Requirements: - * Contract is initialized. - * `value` is at most `MAX_TRANSFER_VALUE()`. - * The caller is registered, holds their encryption secret, and has a spendable balance of at least `value`. - * `to` is not the caller. - * `to` is not the zero account and is registered. + - Contract is initialized. + - `value` is within `MAX_TRANSFER_VALUE()`. + - The caller is registered with a balance of at least `value`. + - `to` is registered and is not the caller (no self-transfer). #### _move [toc] [#ConfidentialFungibleToken-_move] @@ -420,19 +457,31 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=16, rows=43690" modulePrefix="ConfidentialFungibleToken" > - The conserving value-movement primitive: debits the caller and credits `to` by the same `value`, net zero, and never - changes supply. Equivalent to [transfer](#ConfidentialFungibleToken-transfer) without the self-transfer guard, so a - self-move (spendable into the caller's own pending) is permitted; add a `to != caller` check in the wrapper to reject it. - Discloses the same values as `transfer`. - - Returns the caller's `accountId` for caller-side gating. + The conserving value-movement primitive: debits the caller and + credits `to` by the same `value`, net zero. It NEVER changes supply, so it is + the safe surface for composition and the primitive this supply-free base + exposes. `transfer` is `_move` plus a self-transfer guard; supply-changing + operations (`mint`/`burn`) live in a separate layer outside this conserving + surface, and only they may break conservation. + + Like `transfer`, the credit lands in the recipient's pending pool + (see the dual-balance note); the caller sweeps their own incoming value. + + Self-move (`to` == caller) is PERMITTED here, unlike `transfer`, which + rejects it. A self-move is conserving and safe: it debits the caller's + spendable and credits the caller's own pending (net zero, distinct ledger + cells; the value is recovered with `sweep`). `transfer`'s self-transfer guard + is therefore a UX choice, not a safety requirement: it spares users a + self-transfer with no foreseeable benefit (a shuffle of spendable into + pending plus a self-memo). This primitive stays permissive so a composer can + set its own policy; add a `to != caller` check in the wrapper to reject a + self-move. Requirements: - * Contract is initialized. - * `value` is at most `MAX_TRANSFER_VALUE()`. - * The caller is registered, holds their encryption secret, and has a spendable balance of at least `value`. - * `to` is not the zero account and is registered. + - Contract is initialized. + - The caller is registered with a spendable balance of at least `value`. + - `to` is registered. #### sweep [toc] [#ConfidentialFungibleToken-sweep] @@ -443,17 +492,24 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=13, rows=7585" modulePrefix="ConfidentialFungibleToken" > - Moves the caller's pending balance into their spendable balance: homomorphically adds the two ciphertexts and resets - pending to an encryption of zero. No plaintext is needed. This is the only path from pending into spendable, and only - the owner can take it. Discloses the caller's `accountId` and the two new ciphertexts. + Sweeps the caller's incoming `pending` pool into their + `spendable` balance. Credits land in `pending` (see the dual-balance note); + this `sweep` is the ONLY path from pending into spendable, and only the owner + can invoke it (the caller proves BOTH witness secrets: the account secret + identifies the account, the encryption secret proves the caller can read it). + So a third party spamming credits cannot force a victim's spendable ciphertext + to change, which is what would otherwise invalidate the victim's in-flight + spend proofs (a liveness grief). - Returns the caller's `accountId` for caller-side gating. + Purely homomorphic: adds the two ciphertexts (same key) and resets + pending to Enc(0). No plaintext is needed; the wallet already learned the + pending amounts from its memos. Requirements: - * Contract is initialized. - * The caller is registered. - * The caller holds the encryption secret registered for their account. + - Contract is initialized. + - The caller is registered. + - The caller holds the encryption secret registered for their account. #### approve [toc] [#ConfidentialFungibleToken-approve] @@ -464,22 +520,27 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=16, rows=45114" modulePrefix="ConfidentialFungibleToken" > - Sets `spender`'s allowance over the caller's balance to `value` via an escrow: any prior escrow for `spender` is - first refunded to the caller's spendable balance (a homomorphic add, no plaintext needed), then the caller's balance - is debited by `value` and an [EscrowEntry](#ConfidentialFungibleToken-EscrowEntry) holding two encryptions of `value` - plus an owner-readable memo of the remaining allowance is stored. Allowances replace rather than stack; `value` of - zero revokes. Discloses the (owner, spender) pair, the owner's new balance ciphertext, and the entry; the cap is not - disclosed. + Sets `spender`'s allowance over the caller's balance to `value` + via an escrow: the caller's main balance is debited by `value`, and matching + encryptions are stored for the spender and the owner. Re-approving first + refunds any prior escrow, so allowances replace rather than stack. The cap + is not public. Returns the caller's (approver's) `accountId` for caller-side gating. + Also stores an owner-readable `ownerMemo` (ECDH one-time pad; see + `crypto/EcdhMask`) in the escrow entry, carrying the current remaining + allowance (= `value` here, refreshed to the reduced remaining on every + spend). This is what lets the owner prove the post-refund balance on a later + re-approve or revoke after a partial spend; without it a partial spend would + strand the escrow. See the `EscrowEntry` field doc. + Requirements: - * Contract is initialized. - * `value` is at most `MAX_TRANSFER_VALUE()`. - * `spender` is not the zero account, is not the caller, and is registered. - * The caller is registered, holds their encryption secret, and (after any prior escrow is refunded) has a spendable - balance of at least `value`. + - Contract is initialized. + - `value` is within `MAX_TRANSFER_VALUE()`. + - `spender` is registered, is not the zero account, and is not the caller. + - The caller is registered with a balance of at least `value`. #### transferFrom [toc] [#ConfidentialFungibleToken-transferFrom] @@ -495,21 +556,18 @@ The TypeScript witnesses in the repository are test-only and are not published; modulePrefix="ConfidentialFungibleToken" stackParams="true" > - Spends `value` from the escrow that `fromAddress` granted the caller and credits it to `to`. The caller proves the - plaintext of the spender copy, both escrow copies are reduced by `value`, the entry's `ownerMemo` is refreshed with the - new remaining amount, the (owner, spender) spend counter is incremented, and `to` is credited with a memo. Discloses - the owner, spender, and recipient ids, the new escrow entry, the recipient's new pending ciphertext, and the memo; - the amount is not disclosed. + Spends `value` from `fromAddress`'s escrow (granted to the + caller via `approve`) and credits it to `to`, reducing both escrow copies by + `value`. The amount is hidden. Returns the caller's (spender's) `accountId` for caller-side gating. Requirements: - * Contract is initialized. - * `value` is at most `MAX_TRANSFER_VALUE()`. - * `to` is not the zero account and is registered. - * An escrow from `fromAddress` to the caller exists, the caller holds their encryption secret, and the escrow holds - at least `value`. + - Contract is initialized. + - `value` is within `MAX_TRANSFER_VALUE()`. + - `to` is registered and is not the zero account. + - An escrow from `fromAddress` to the caller exists with at least `value`. #### clearMemos [toc] [#ConfidentialFungibleToken-clearMemos] @@ -520,21 +578,35 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=13, rows=7053" modulePrefix="ConfidentialFungibleToken" > - Empties the caller's memo list. `expectedEpoch` is the caller's view of its `_creditEpochs` value when it read the - list; the call is rejected if a credit has landed since, so a memo the caller has not seen is never discarded. + Clears the caller's memo list, letting a wallet that has folded + its memos into its local balance cache prune on-chain memo storage. + + `expectedEpoch` is the caller's view of its `_creditEpochs` value at the + moment it read the list. The call is rejected unless that still matches, so + a credit landing between the read and this transaction is never silently + discarded. The wallet retries against the memos it has now seen. A wallet + therefore reads its memo list and its epoch together, folds the memos into + its cache, and passes that epoch back here. - Destructive. Spending requires proving the balance plaintext, which a wallet reconstructs from its memos or its - cache. Clear only memos already folded into a durable, backed-up cache; otherwise the funds become unspendable. + Clearing memos is destructive and can permanently lock funds. The + on-chain balance ciphertext is not directly decryptable (discrete-log + bound), so a wallet reconstructs its balance by replaying memos. Clearing + them without a durable off-chain record of the balance loses the ability to + recover it; and because spending requires proving the balance plaintext, the + funds become unspendable, not merely undisplayable. Clear only memos already + folded into a durable cache/backup. The epoch check only rules out a credit + the caller has not seen; it does not make clearing safe for memos the caller + never folded in. Requirements: - * Contract is initialized. - * The caller is registered. - * The caller holds the encryption secret registered for their account. - * The caller has a memo list. - * `expectedEpoch` equals the caller's current credit epoch. + - Contract is initialized. + - The caller is registered. + - The caller holds the encryption secret registered for their account. + - The caller has a memo list. + - `expectedEpoch` equals the caller's current credit epoch. #### computeAccountId [toc] [#ConfidentialFungibleToken-computeAccountId] @@ -544,14 +616,13 @@ The TypeScript witnesses in the repository are test-only and are not published; kind="pure" modulePrefix="ConfidentialFungibleToken" > - Derives the public `accountId` from a secret key as `H(sk)`, so a wallet can compute its own account identifier - off chain before transacting. Delegates to `Utils_computeAccountId`; the identifier is global, and the same key - material yields the same id in every module that derives it. + Derives the public `accountId` from a secret key, as + `persistentHash(sk)`. Pure, so a wallet can compute its own account + identifier off-chain (no proof) before transacting. - - The `sk` parameter is a sensitive secret. Never log or persist it in plaintext, generate it from cryptographically - secure randomness, and treat key loss as identity loss. - + Delegates to `Utils.computeAccountId` rather than reimplementing the + hash. The identifier is deliberately GLOBAL. The same key material yields + the same id in every module that derives it. #### _burnFrom [toc] [#ConfidentialFungibleToken-_burnFrom] @@ -562,25 +633,16 @@ The TypeScript witnesses in the repository are test-only and are not published; complexity="k=16, rows=45376" modulePrefix="ConfidentialFungibleToken" > - Consumes `value` from the escrow that `fromAddress` granted the caller, with no recipient credit: the same escrow - spend as [transferFrom](#ConfidentialFungibleToken-transferFrom) (both copies reduced, `ownerMemo` refreshed, spend - counter incremented) and nothing else. Performs no supply accounting; pair it with - [_subSupply](#ConfidentialFungibleTokenPublicSupply-_subSupply). Discloses the owner and spender ids and the new - escrow entry; the amount is not disclosed. - - Returns the caller's (spender's) `accountId` for caller-side gating. - - - A building block for a `burnFrom` wrapper. Never re-export it raw from a deployed contract: an approved spender - could destroy escrowed value with no supply decrement. - + Burns `value` from `fromAddress` by consuming an escrow that + owner granted to the caller: `_spendEscrow` under its intent name, with no + recipient credit. The building block for a composing contract's + `burnFrom`; it performs no supply accounting (see `_burn`). Requirements: - * Contract is initialized. - * `value` is at most `MAX_TRANSFER_VALUE()`. - * An escrow from `fromAddress` to the caller exists, the caller holds their encryption secret, and the escrow holds - at least `value`. + - Contract is initialized. + - `value` is within `MAX_TRANSFER_VALUE()`. + - An escrow from `fromAddress` to the caller exists with at least `value`. --- @@ -612,7 +674,8 @@ with the matching token operation. kind="ledger" id="ConfidentialFungibleTokenPublicSupply-_totalSupply" > - The public circulating supply. + The public circulating supply. Mint and burn disclose their amounts + through the delta on this value. ### Witnesses [toc] [#ConfidentialFungibleTokenPublicSupply-Witnesses] @@ -631,12 +694,12 @@ None. complexity="k=9, rows=401" modulePrefix="ConfidentialFungibleTokenPublicSupply" > - Increases the total supply by `value`. Call alongside the token module's - [_mint](#ConfidentialFungibleToken-_mint). The added amount is disclosed through the supply delta. + Increases `totalSupply` by `value`. Call alongside the token + module's `_mint`. The added amount is disclosed via the supply delta. Requirements: - * `_totalSupply + value` does not overflow `Uint<128>`. + - `totalSupply + value` does not overflow `Uint<128>`. #### _subSupply [toc] [#ConfidentialFungibleTokenPublicSupply-_subSupply] @@ -647,13 +710,13 @@ None. complexity="k=9, rows=256" modulePrefix="ConfidentialFungibleTokenPublicSupply" > - Decreases the total supply by `value`. Call alongside the token module's - [_burn](#ConfidentialFungibleToken-_burn) or [_burnFrom](#ConfidentialFungibleToken-_burnFrom). - The removed amount is disclosed through the supply delta. + Decreases `totalSupply` by `value`. Call alongside the token + module's `_burn` or `_burnFrom`. The removed amount is disclosed via the + supply delta. Requirements: - * `_totalSupply` is at least `value`. + - `totalSupply` is at least `value`. #### totalSupply [toc] [#ConfidentialFungibleTokenPublicSupply-totalSupply] diff --git a/content/contracts-compact/api/crypto.mdx b/content/contracts-compact/api/crypto.mdx index 3ec32444..7e2299a9 100644 --- a/content/contracts-compact/api/crypto.mdx +++ b/content/contracts-compact/api/crypto.mdx @@ -35,8 +35,9 @@ import "./node_modules/@openzeppelin/compact-contracts/crypto/ElGamal"; kind="struct" id="ElGamal-Ciphertext" > - A single-receiver ElGamal ciphertext over Jubjub: `c1 = g^r` and `c2 = pk^r * m`. - In the lifted form the message point is `m = g^v` for a plaintext value `v`. + Single-receiver ElGamal ciphertext over Jubjub. + c1 = g^r + c2 = pk^r * m (lifted: m = g^v) ### Ledger [toc] [#ElGamal-Ledger] @@ -59,10 +60,27 @@ None. kind="pure" modulePrefix="ElGamal" > - Maps a 32-byte secret to a valid Jubjub scalar, computed as - `degradeToTransient(H("ElGamal:secretToScalar" || secret))` with the tag padded to 32 bytes. - The output is always below the Jubjub subgroup order, so it is safe to pass to `ecMulGenerator` and `ecMul`. - The fixed tag keeps this derivation distinct from both `expandRandomness` and any account identifier computed as `H(secret)`. + Maps a 32-byte secret to a valid Jubjub scalar. See the + module-level hash-to-scalar note for why `degradeToTransient` is required. + + Domain separation here is load-bearing, not cosmetic. This output is + the private key protecting every ciphertext held under an account. Public + account identifiers derived from `Utils.computeAccountId`, for example, + are computed as `persistentHash([secretKey])`, and that identifier + is stored in the clear as a ledger map key. Untagged, this circuit + would compute that same hash, so a secret used in both roles would + have its encryption key recoverable from a published identifier by applying + `degradeToTransient`. The tag makes the two derivations unrelated, so a + wallet MAY safely derive its account secret and its encryption secret from + common key material. + + The tag is FIRST, deliberately. `expandRandomness` hashes + `[seed, tag]` with both slots caller-supplied, so tagging this circuit as + `[secret, tag]` would make `secretToScalar(x)` identical to + `expandRandomness(x, )` reachable by passing this domain string + to the parameter that exists to receive domain strings. Leading with the + tag means reproducing this output through `expandRandomness` would require + passing the domain constant as the *seed*. Do not normalise the orderings. #### derivePk [toc] [#ElGamal-derivePk] @@ -72,7 +90,8 @@ None. kind="pure" modulePrefix="ElGamal" > - Derives the public key `pk = g^secretToScalar(ek)` from the 32-byte encryption secret `ek`. + Derives the ElGamal public key `pk = g^secretToScalar(ek)` + from a secret `ek`. #### expandRandomness [toc] [#ElGamal-expandRandomness] @@ -82,11 +101,11 @@ None. kind="pure" modulePrefix="ElGamal" > - Deterministically expands a 32-byte seed into a Jubjub scalar tagged by a distinguishing string, - computed as `degradeToTransient(H(seed || tag))`. - One secret seed can yield many distinct randomness values within a single circuit invocation, one per tag. - Distinct tags always produce distinct outputs; the caller is responsible for using distinct tags - and for supplying a fresh, secret seed on every invocation. + Deterministically expands a 32-byte seed into a Jubjub scalar + tagged by a distinguishing string, so a single witness seed can yield many + distinct randomness values within one circuit invocation. Distinct tags + always produce distinct outputs (collision resistance of the hash); the + caller is responsible for using distinct tags. #### encryptZero [toc] [#ElGamal-encryptZero] @@ -96,11 +115,13 @@ None. kind="pure" modulePrefix="ElGamal" > - Returns the identity ciphertext `Enc(0)`, where both components are the curve identity point. - Useful for initializing a fresh slot. + The identity ciphertext `Enc(0)` (both components are the + curve identity). Useful for initializing a fresh balance. - This is a fixed, deterministic value, not a hiding encryption: a slot still holding it is publicly recognizable as never written. - Rerandomize it, or credit it with fresh randomness, before treating it as private. + This is a fixed, deterministic value `(O, O)` — NOT a hiding + encryption. It is publicly recognizable, so a slot left at `encryptZero` is + distinguishable as never-written. Rerandomize (or credit with fresh `r`) + before treating it as private. #### encryptPoint [toc] [#ElGamal-encryptPoint] @@ -115,19 +136,23 @@ None. modulePrefix="ElGamal" stackParams="true" > - Encrypts an arbitrary message point `m` under `pk` with randomness `r`, producing `(g^r, pk^r * m)`. - This is the general (non-lifted) encryption; `encrypt` is the lifted special case with `m = g^value`. - Every encryption path in the module goes through this circuit, so its requirements apply to all of them. - - `m` and `pk` are constrained into the prime-order subgroup by the runtime, so no in-circuit membership check is performed. - Decrypting `m` is only feasible when it is a known or small-exponent point; the caller owns that concern. - `r` must be a valid Jubjub scalar (below the subgroup order), secret, and fresh per ciphertext; - the circuit cannot check any of these. + Encrypts an arbitrary message point `m` under `pk` with + randomness `r`, producing the ciphertext `(g^r, pk^r * m)`. This is the + general (non-lifted) ElGamal encryption; `encrypt` is the lifted special + case `encryptPoint(pk, g^value, r)`. + + `m` (like `pk`) is constrained into the prime-order subgroup by the + runtime (see the module-level subgroup note), so no in-circuit membership + check is performed. `pk` is asserted non-identity and `r` non-zero — the + two inputs that would zero the mask and expose `m` (see the weak-keys and + randomness notes). Decrypting `m` is only feasible when it is a known or + small-exponent point; the caller owns that concern (the lifted `encrypt` + bounds it via the discrete-log range). Requirements: - * `pk` is not the identity point. - * `r` is non-zero. + - `pk` is not the identity point. + - `r` is non-zero. #### encrypt [toc] [#ElGamal-encrypt] @@ -142,14 +167,15 @@ None. modulePrefix="ElGamal" stackParams="true" > - Produces a fresh ciphertext of `value` under `pk` with randomness `r`, lifting `value` to the point `g^value`: - the result is `(g^r, pk^r * g^value)`. - The caller is responsible for keeping `value` within a range a discrete-log search can recover. + Produces a fresh ciphertext of `value` under `pk` with + randomness `r`, lifting `value` to the point `g^value`. Randomness is taken + as a `Field` directly to avoid a redundant `Bytes<32> -> Field` conversion + at every call site; `expandRandomness` produces `Field` for the same reason. Requirements: - * `pk` is not the identity point. - * `r` is non-zero. + - `pk` is not the identity point. + - `r` is non-zero. #### negate [toc] [#ElGamal-negate] @@ -159,8 +185,8 @@ None. kind="pure" modulePrefix="ElGamal" > - Negates a ciphertext componentwise: if `ct` encrypts `v`, the result encrypts `-v` under the same key - (with negated randomness). Each point is negated by scalar multiplication with the subgroup order minus one. + Negates a ciphertext componentwise: if `ct` encrypts `v` then + the result encrypts `-v` under the same key (with negated randomness). This is the building block `sub` rests on. @@ -171,11 +197,15 @@ None. kind="pure" modulePrefix="ElGamal" > - Homomorphically adds two ciphertexts componentwise: `Enc(a) + Enc(b)` decrypts to `a + b`. - The randomness of the result is the sum of the inputs' randomness, so the result is a valid ciphertext under the same key. - - Both inputs must be encrypted under the same public key; combining ciphertexts under different keys yields - a ciphertext no single key can open. + Homomorphically adds two ciphertexts: `Enc(a) + Enc(b)` + decrypts to `a + b`. This is the defining additive-homomorphism primitive + and combines two independently produced ciphertexts componentwise. + + Both inputs MUST be encrypted under the same public key for the + result to decrypt under that key's secret; combining ciphertexts under + different keys yields a ciphertext no single key can open. The randomness + of the result is the sum of the inputs' randomness, so the result is a + valid fresh ciphertext under that key. #### sub [toc] [#ElGamal-sub] @@ -185,10 +215,12 @@ None. kind="pure" modulePrefix="ElGamal" > - Homomorphically subtracts two ciphertexts: `Enc(a) - Enc(b)` decrypts to `a - b`. Implemented as `add(a, negate(b))`. + Homomorphically subtracts two ciphertexts: `Enc(a) - Enc(b)` + decrypts to `a - b`. Implemented as `add(a, negate(b))`. - Both inputs must be encrypted under the same public key. - The caller must ensure the underlying plaintext subtraction does not underflow; this circuit cannot check it. + Same-key requirement as `add`. The caller MUST ensure the + underlying plaintext subtraction does not underflow; this circuit cannot + check it (see `subEncrypted`). #### scalarMul [toc] [#ElGamal-scalarMul] @@ -198,12 +230,20 @@ None. kind="pure" modulePrefix="ElGamal" > - Homomorphically multiplies the plaintext of `ct` by a public scalar `k`: if `ct` encrypts `v`, - the result encrypts `k * v` under the same key. Both components are scaled, so the randomness becomes `k * r`. - Together with `add` this expresses linear combinations, for example `Enc(3a + 5b)` as `add(scalarMul(ca, 3), scalarMul(cb, 5))`. - - `k` must be a valid Jubjub scalar (below the subgroup order); larger values fault `ecMul`. - The caller remains responsible for keeping `k * v` within the discrete-log bound. + Homomorphically multiplies the plaintext of `ct` by a public + scalar `k`: if `ct` encrypts `v` then the result encrypts `k * v` under the + same key. Both components are scaled by `k`, so the randomness scales to + `k * r` and the result is a valid fresh ciphertext under that key. + + This is the scalar-multiplication leg of the homomorphism, and the only way + to express linear combinations: a weighted sum `Enc(3a + 5b)` is + `add(scalarMul(ca, 3), scalarMul(cb, 5))`, which repeated `add` cannot do + for large weights. + + `k` MUST be a valid Jubjub scalar (less than the subgroup order); + larger values fault `ecMul` (see the module-level hash-to-scalar note). The + caller also remains responsible for the discrete-log bound — `k * v` must + stay within the range a recovery search can clear. #### addEncrypted [toc] [#ElGamal-addEncrypted] @@ -219,13 +259,15 @@ None. modulePrefix="ElGamal" stackParams="true" > - Homomorphically adds `value` to the plaintext encrypted by `old`, producing a fresh ciphertext under `pk` - with new randomness `r`. Equivalent to `add(old, encrypt(pk, value, r))`. + Homomorphically adds `value` to the plaintext encrypted by + `old`, producing a fresh ciphertext under `pk` with new randomness `r`. + Convenience over `add` for folding a known plaintext into a ciphertext: + `add(old, encrypt(pk, value, r))`. Requirements: - * `pk` is not the identity point. - * `r` is non-zero. + - `pk` is not the identity point. + - `r` is non-zero. #### subEncrypted [toc] [#ElGamal-subEncrypted] @@ -241,17 +283,18 @@ None. modulePrefix="ElGamal" stackParams="true" > - Homomorphically subtracts `value` from the plaintext encrypted by `old`, producing a fresh ciphertext under `pk` - with new randomness `r`. Equivalent to `sub(old, encrypt(pk, value, r))`. + Homomorphically subtracts `value` from the plaintext encrypted + by `old`. Convenience over `sub`: `sub(old, encrypt(pk, value, r))`. - The caller must ensure the underlying plaintext subtraction does not underflow; this circuit cannot check it. - Callers verify sufficiency by reading the plaintext through their own witness and asserting it with + The caller MUST ensure the underlying plaintext subtraction does + not underflow; this circuit cannot check it. Callers verify sufficiency by + reading the plaintext via their own witness and asserting it with `assertDecryptsTo` before calling. Requirements: - * `pk` is not the identity point. - * `r` is non-zero. + - `pk` is not the identity point. + - `r` is non-zero. #### rerandomize [toc] [#ElGamal-rerandomize] @@ -266,16 +309,22 @@ None. modulePrefix="ElGamal" stackParams="true" > - Returns a fresh ciphertext of the same plaintext as `ct` under the same key `pk`, using new randomness `r`. - The output is unlinkable to the input yet decrypts identically. Equivalent to homomorphically adding an encryption of zero. - - A `pk` that is not the key `ct` was encrypted under silently corrupts the plaintext; the circuit cannot detect this. - Pass fresh randomness and the matching `pk`. + Rerandomizes `ct`: returns a fresh ciphertext of the same + plaintext under the same key `pk`, using new randomness `r`. The output is + unlinkable to the input (it is a uniformly fresh encryption of the same + value) yet decrypts identically. Equivalent to homomorphically adding an + encryption of zero. + + `r = 0` is rejected (it would be a no-op via the `encryptPoint` + zero-randomness assert). One silent failure mode remains that the circuit + cannot catch: a `pk` that is not the key `ct` was encrypted under corrupts + the plaintext (the result no longer decrypts to the original value). Pass + fresh randomness and the matching `pk`. Requirements: - * `pk` is not the identity point. - * `r` is non-zero. + - `pk` is not the identity point. + - `r` is non-zero. #### assertKeyPair [toc] [#ElGamal-assertKeyPair] @@ -285,12 +334,13 @@ None. kind="pure" modulePrefix="ElGamal" > - Asserts that `ek` is the secret for `pk`, i.e. `derivePk(ek) == pk`. - Lets a caller prove ownership of a public key without touching any ciphertext or plaintext. + Asserts that `ek` is the secret for `pk`, i.e. + `derivePk(ek) == pk`. Lets a caller prove ownership of a public key + without touching any ciphertext or plaintext. Requirements: - * `derivePk(ek) == pk`. + - `ek` is the secret for `pk` (`derivePk(ek) == pk`). #### assertDecryptsToPoint [toc] [#ElGamal-assertDecryptsToPoint] @@ -306,14 +356,15 @@ None. modulePrefix="ElGamal" stackParams="true" > - Asserts that `ek` is the secret for `pk` and that `ct` decrypts under `(pk, ek)` to the message point `m`, - computed as `c2 - ek * c1`. This is the general (non-lifted) verification; - `assertDecryptsTo` is the lifted special case with `m = g^claimedValue`. + Asserts that `ct` decrypts under `(pk, ek)` to the message + point `m`, and that `ek` is the secret for `pk`. This is the general + (non-lifted) verification; `assertDecryptsTo` is the lifted special case + with `m = g^claimedValue`. Requirements: - * `ek` is the secret for `pk`. - * `ct` decrypts under `(pk, ek)` to `m`. + - `ek` is the secret for `pk`. + - `ct` decrypts under `(pk, ek)` to `m`. #### assertDecryptsTo [toc] [#ElGamal-assertDecryptsTo] @@ -329,18 +380,30 @@ None. modulePrefix="ElGamal" stackParams="true" > - Asserts that `ek` is the secret for `pk` and that `ct` decrypts under `(pk, ek)` to `claimedValue`. - This is the in-circuit verification path an importing contract uses to bind its witness-supplied secret and cached plaintext - to publicly stored state: a wrong `ek` fails the key check, a wrong plaintext fails the decryption check. - - `claimedValue` is the widest `Uint` Compact offers, so this parameter never bounds a caller; a narrower value declared - at the call site costs proportionally less. The claim domain must stay below the Jubjub subgroup order, since two claims - congruent modulo that order are indistinguishable; `Uint<248>` satisfies this. + Asserts that `ct` decrypts under `(pk, ek)` to `claimedValue`, + and that `ek` is the secret for `pk`. This is the in-circuit verification + path an importing module uses to bind its witness-supplied `ek` and cached + plaintext to publicly stored state: a wrong `ek` fails the key check, a + wrong plaintext fails the decryption check. Lifted special case of + `assertDecryptsToPoint` with the message point `g^claimedValue`. + + `claimedValue` is the widest `Uint` Compact offers, so this parameter + never bounds a caller. A narrower value costs proportionally fewer rows: the + range constraint belongs to where the value is declared, not to this + signature. + + + The claim domain must stay below the Jubjub subgroup order `l`: + verification compares `g^claimedValue`, so two claims congruent mod `l` are + indistinguishable. `Uint<248>` clears `l` (~2^251.86) by under 4 bits, and + that width is set by Compact's cap rather than by this bound. 251 is the + widest sound `Uint` if the cap ever rises. + Requirements: - * `ek` is the secret for `pk`. - * `ct` decrypts under `(pk, ek)` to `claimedValue`. + - `ek` is the secret for `pk`. + - `ct` decrypts under `(pk, ek)` to `claimedValue`. --- @@ -367,7 +430,8 @@ import "./node_modules/@openzeppelin/compact-contracts/crypto/EcdhMask"; kind="struct" id="EcdhMask-Ciphertext" > - An ECDH one-time-pad ciphertext: the ephemeral public key `g^e` and the field-masked value `value + kdf(pk^e, domain)`. + An ECDH one-time-pad ciphertext: the ephemeral public key and + the field-masked value. ### Ledger [toc] [#EcdhMask-Ledger] @@ -390,10 +454,15 @@ None. kind="pure" modulePrefix="EcdhMask" > - Derives the field mask from the ECDH shared secret point, domain-separated by the caller-supplied `domain` tag: - `degradeToTransient(H(H(sShared) || domain))`, where `H` is `persistentHash`. - `sShared` is `pk^e` for the sender and `ephemeralPk^ek` for the recipient, which are equal by construction. - The consumer chooses `domain` and must use the same value on encrypt and decrypt. + Derives the field mask from the ECDH shared secret point, + domain-separated by a caller-supplied `domain` tag. Hashes the point to + `Bytes<32>`, then re-hashes it with the domain tag and truncates into the + field via `degradeToTransient`. The consumer chooses `domain` (so this + module is not tied to any one protocol) and MUST use the same value on + encrypt and decrypt. + + Uses `persistentHash` deliberately: the recipient reproduces this mask + to decrypt, possibly across a platform upgrade, so it must be upgrade-stable. #### encrypt [toc] [#EcdhMask-encrypt] @@ -409,19 +478,14 @@ None. modulePrefix="EcdhMask" stackParams="true" > - Encrypts `value` to `recipientPk` under the ephemeral scalar `e`, returning - `{ ephemeralPk: g^e, ct: value + kdf(recipientPk^e, domain) }`. - Any `Uint<128>` value is accepted; there is no discrete-log bound. + Encrypts `value` to `recipientPk` under ephemeral scalar `e`. - `e` is a one-time pad key: it must be a valid Jubjub scalar (below the subgroup order), secret, - and fresh for every ciphertext. Reusing `e` to the same recipient repeats the mask and exposes both values; - a predictable `e` lets anyone recompute the mask from the public `recipientPk`. - The pad is not authenticated. + `e` MUST be fresh per call (see the module freshness precondition). Requirements: - * `recipientPk` is not the identity point. - * The ephemeral point `g^e` is not the identity (this rejects `e = 0`). + - `recipientPk` is not the identity point. + - `e` is non-zero, a valid Jubjub scalar (`< ℓ`), secret, and fresh per call. #### decrypt [toc] [#EcdhMask-decrypt] @@ -436,10 +500,6 @@ None. modulePrefix="EcdhMask" stackParams="true" > - Recovers the value from a ciphertext using the recipient's secret scalar: computes `sShared = ephemeralPk^ekScalar` - and returns `ct - kdf(sShared, domain)`. The result equals the original `value` as a `Field`. - `ekScalar` is the scalar form of the recipient's secret, as produced by ElGamal's `secretToScalar`, - and `domain` must match the tag used on encrypt. - - Intended for wallets and off-chain code: reading a received value needs no proof. + Recovers the value from a ciphertext using the recipient's + secret scalar. Pure and off-chain (no proof needed to read a received value). diff --git a/content/contracts-compact/api/fungible-token.mdx b/content/contracts-compact/api/fungible-token.mdx index 3739bb5b..c955a135 100644 --- a/content/contracts-compact/api/fungible-token.mdx +++ b/content/contracts-compact/api/fungible-token.mdx @@ -30,8 +30,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; kind="ledger" id="FungibleToken-_isInitialized" > - Initialization flag, owned by this module. Set to `true` by [initialize](#FungibleToken-initialize); - every other circuit asserts it is set. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. #### _balances [toc] [#FungibleToken-_balances] @@ -51,7 +52,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; kind="ledger" id="FungibleToken-_allowances" > - Mapping from owner accounts to spender accounts to their allowances. + Mapping from owner accounts to spender accounts and their allowances. #### _totalSupply [toc] [#FungibleToken-_totalSupply] @@ -105,7 +106,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; id="FungibleToken-wit_FungibleTokenSK" > Returns the caller's secret key used in deriving the account identifier. - The key stays private; only `persistentHash(secretKey)` reaches the ledger. The same key produces the same account identifier across all contracts. Users who desire cross-contract unlinkability should use different keys per contract. @@ -127,14 +127,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" stackParams="true" > - Initializes the contract by setting the name, symbol, and decimals, and marks the module initialized. - - This MUST be called in the implementing contract's constructor. - Failure to do so can lead to an irreparable contract. - - Requirements: - - * Contract is not initialized. + Initializes the contract by setting the name, symbol, and decimals. + This MUST be called in the implementing contract's constructor. Failure to do so + can lead to an irreparable contract. #### name [toc] [#FungibleToken-name] @@ -149,7 +144,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - * Contract is initialized. + - Contract is initialized. #### symbol [toc] [#FungibleToken-symbol] @@ -164,7 +159,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - * Contract is initialized. + - Contract is initialized. #### decimals [toc] [#FungibleToken-decimals] @@ -179,7 +174,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - * Contract is initialized. + - Contract is initialized. #### totalSupply [toc] [#FungibleToken-totalSupply] @@ -194,7 +189,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - * Contract is initialized. + - Contract is initialized. #### balanceOf [toc] [#FungibleToken-balanceOf] @@ -207,9 +202,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; > Returns the value of tokens owned by `account`. + Manually checks if `account` is a key in the map and returns 0 if it is not. + Requirements: - * Contract is initialized. + - Contract is initialized. #### transfer [toc] [#FungibleToken-transfer] @@ -221,19 +218,17 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" > Moves a `value` amount of tokens from the caller's account to `to`. - The caller's identity is derived from the `wit_FungibleTokenSK` witness as `persistentHash(secretKey)`. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: - * Contract is initialized. - * `to` is not a ContractAddress. - * `to` is not the zero address. - * The caller has a balance of at least `value`. + - Contract is initialized. + - `to` is not a ContractAddress. + - `to` is not the zero address. + - The caller has a balance of at least `value`. #### _unsafeTransfer [toc] [#FungibleToken-_unsafeTransfer] @@ -244,19 +239,19 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; complexity="k=13, rows=3982" modulePrefix="FungibleToken" > - Unsafe variant of [transfer](#FungibleToken-transfer) which allows transfers to contract addresses. + Unsafe variant of `transfer` which allows transfers to contract addresses. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. Requirements: - * Contract is initialized. - * `to` is not the zero address. - * The caller has a balance of at least `value`. + - Contract is initialized. + - `to` is not the zero address. + - The caller has a balance of at least `value`. #### allowance [toc] [#FungibleToken-allowance] @@ -268,13 +263,14 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" stackParams="true" > - Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` through [transferFrom](#FungibleToken-transferFrom). - This value changes when [approve](#FungibleToken-approve) or [transferFrom](#FungibleToken-transferFrom) are called. - Returns zero when no allowance entry exists. + Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` + through `transferFrom`. This value changes when `approve` or `transferFrom` are called. + + Manually checks if `owner` and `spender` are keys in the map and returns 0 if they are not. Requirements: - * Contract is initialized. + - Contract is initialized. #### approve [toc] [#FungibleToken-approve] @@ -286,12 +282,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" > Sets a `value` amount of tokens as allowance of `spender` over the caller's tokens. - The caller's identity is derived from the `wit_FungibleTokenSK` witness as `persistentHash(secretKey)`. Requirements: - * Contract is initialized. - * `spender` is not the zero address. + - Contract is initialized. + - `spender` is not the zero address. #### transferFrom [toc] [#FungibleToken-transferFrom] @@ -308,22 +303,20 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" > Moves `value` tokens from `fromAddress` to `to` using the allowance mechanism. - `value` is then deducted from the caller's allowance. - The caller's identity is derived from the `wit_FungibleTokenSK` witness as `persistentHash(secretKey)`. + `value` is the deducted from the caller's allowance. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: - * Contract is initialized. - * `fromAddress` is not the zero address. - * `fromAddress` must have a balance of at least `value`. - * `to` is not the zero address. - * `to` is not a ContractAddress. - * The caller has an allowance of `fromAddress`'s tokens of at least `value`. + - Contract is initialized. + - `fromAddress` is not the zero address. + - `fromAddress` must have a balance of at least `value`. + - `to` is not the zero address. + - `to` is not a ContractAddress. + - The caller has an allowance of `fromAddress`'s tokens of at least `value`. #### _unsafeTransferFrom [toc] [#FungibleToken-_unsafeTransferFrom] @@ -339,21 +332,21 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; stackParams="true" modulePrefix="FungibleToken" > - Unsafe variant of [transferFrom](#FungibleToken-transferFrom) which allows transfers to contract addresses. + Unsafe variant of `transferFrom` which allows transfers to contract addresses. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. Requirements: - * Contract is initialized. - * `fromAddress` is not the zero address. - * `fromAddress` must have a balance of at least `value`. - * `to` is not the zero address. - * The caller has an allowance of `fromAddress`'s tokens of at least `value`. + - Contract is initialized. + - `fromAddress` is not the zero address. + - `fromAddress` must have a balance of at least `value`. + - `to` is not the zero address. + - The caller has an allowance of `fromAddress`'s tokens of at least `value`. #### _transfer [toc] [#FungibleToken-_transfer] @@ -370,21 +363,20 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" > Moves a `value` amount of tokens from `fromAddress` to `to`. - This circuit is equivalent to [transfer](#FungibleToken-transfer), and can be used to e.g. - implement automatic token fees, slashing mechanisms, etc. + This circuit is equivalent to `transfer`, and can be used to + e.g. implement automatic token fees, slashing mechanisms, etc. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: - * Contract is initialized. - * `fromAddress` is not the zero address. - * `fromAddress` must have at least a balance of `value`. - * `to` must not be the zero address. - * `to` must not be a ContractAddress. + - Contract is initialized. + - `fromAddress` is not be the zero address. + - `fromAddress` must have at least a balance of `value`. + - `to` must not be the zero address. + - `to` must not be a ContractAddress. #### _unsafeUncheckedTransfer [toc] [#FungibleToken-_unsafeUncheckedTransfer] @@ -400,40 +392,19 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" stackParams="true" > - Unsafe variant of [_transfer](#FungibleToken-_transfer) which allows transfers to contract addresses. + Unsafe variant of `transferFrom` which allows transfers to contract addresses. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. Requirements: - * Contract is initialized. - * `fromAddress` is not the zero address. - * `to` is not the zero address. - - -#### _update [toc] [#FungibleToken-_update] -#### [!toc] [#FungibleToken-_update] - - Transfers a `value` amount of tokens from `fromAddress` to `to`, - or alternatively mints (or burns) if `fromAddress` (or `to`) is the zero address. - - Requirements: - - * Contract is initialized. + - Contract is initialized. + - `fromAddress` is not the zero address. + - `to` is not the zero address. #### _mint [toc] [#FungibleToken-_mint] @@ -444,19 +415,18 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; complexity="k=11, rows=1437" modulePrefix="FungibleToken" > - Creates a `value` amount of tokens and assigns them to `account`, by transferring it from the zero address. - Relies on the `_update` mechanism. + Creates a `value` amount of tokens and assigns them to `account`, + by transferring it from the zero address. Relies on the `update` mechanism. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: - * Contract is initialized. - * `account` is not a ContractAddress. - * `account` is not the zero address. + - Contract is initialized. + - `account` is not a ContractAddress. + - `account` is not the zero address. #### _unsafeMint [toc] [#FungibleToken-_unsafeMint] @@ -467,18 +437,18 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; complexity="k=11, rows=1434" modulePrefix="FungibleToken" > - Unsafe variant of [_mint](#FungibleToken-_mint) which allows transfers to contract addresses. + Unsafe variant of `_mint` which allows transfers to contract addresses. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. Requirements: - * Contract is initialized. - * `account` is not the zero address. + - Contract is initialized. + - `account` is not the zero address. #### _burn [toc] [#FungibleToken-_burn] @@ -494,9 +464,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - * Contract is initialized. - * `account` is not the zero address. - * `account` must have at least a balance of `value`. + - Contract is initialized. + - `account` is not the zero address. + - `account` must have at least a balance of `value`. #### _approve [toc] [#FungibleToken-_approve] @@ -513,13 +483,14 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; stackParams="true" > Sets `value` as the allowance of `spender` over the `owner`'s tokens. - This circuit is equivalent to `approve`, and can be used to e.g. set automatic allowances for certain subsystems, etc. + This circuit is equivalent to `approve`, and can be used to + e.g. set automatic allowances for certain subsystems, etc. Requirements: - * Contract is initialized. - * `owner` is not the zero address. - * `spender` is not the zero address. + - Contract is initialized. + - `owner` is not the zero address. + - `spender` is not the zero address. #### _spendAllowance [toc] [#FungibleToken-_spendAllowance] @@ -538,28 +509,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Updates `owner`'s allowance for `spender` based on spent `value`. Does not update the allowance value in case of infinite allowance. - A missing allowance entry is treated as a zero allowance, mirroring [allowance](#FungibleToken-allowance). - A zero `value` is a no-op: no allowance is read and no entry is written. - Whether `value` is zero is disclosed. - Requirements: - * Contract is initialized. - * If `value` is non-zero, `spender` must have an allowance of at least `value` from `owner`. + - Contract is initialized. + - `spender` must have at least an allowance of `value` from `owner`. -#### _computeAccountId [toc] [#FungibleToken-_computeAccountId] -#### [!toc] [#FungibleToken-_computeAccountId] - - Computes the caller's account identifier from the `wit_FungibleTokenSK` witness - by delegating to [Utils computeAccountId](./utils#Utils-computeAccountId). - - ID Derivation: `accountId = persistentHash(secretKey)` - - The result is a 32-byte commitment that uniquely identifies the caller. - The same derivation is available off-chain through `Utils_computeAccountId`. - diff --git a/content/contracts-compact/api/multi-token.mdx b/content/contracts-compact/api/multi-token.mdx index 48ab1ef3..be2fef61 100644 --- a/content/contracts-compact/api/multi-token.mdx +++ b/content/contracts-compact/api/multi-token.mdx @@ -30,8 +30,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; kind="ledger" id="MultiToken-_isInitialized" > - Initialization flag, owned by this module. Set to `true` by [initialize](#MultiToken-initialize); - every other circuit asserts it is set. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. #### _balances [toc] [#MultiToken-_balances] @@ -62,6 +63,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; id="MultiToken-_uri" > Base URI for computing token URIs. + Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/`id`.json ### Witnesses [toc] [#MultiToken-Witnesses] @@ -75,7 +77,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; id="MultiToken-wit_MultiTokenSK" > Returns the caller's secret key used in deriving the account identifier. - The key stays private; only `persistentHash(secretKey)` reaches the ledger. The same key produces the same account identifier across all contracts. Users who desire cross-contract unlinkability should use different keys per contract. @@ -92,14 +93,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; complexity="k=6, rows=33" modulePrefix="MultiToken" > - Initializes the contract by setting the base URI for all tokens, and marks the module initialized. - - This MUST be called in the implementing contract's constructor. - Failure to do so can lead to an irreparable contract. + Initializes the contract by setting the base URI for all tokens. Requirements: - * Contract is not initialized. + - Must be called in the contract's constructor. #### uri [toc] [#MultiToken-uri] @@ -110,13 +108,15 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; complexity="k=8, rows=176" modulePrefix="MultiToken" > - This implementation returns the same URI for **all** token types. - It relies on the token type ID substitution mechanism defined in the EIP: [ERC1155-Metadata](https://eips.ethereum.org/EIPS/eip-1155#metadata). - Clients calling this function must replace the `\{id\}` substring with the actual token type ID. + This implementation returns the same URI for *all* token types. It relies + on the token type ID substitution mechanism defined in the EIP: + https://eips.ethereum.org/EIPS/eip-1155#metadata. + Clients calling this function must replace the `\{id\}` substring with the + actual token type ID. Requirements: - * Contract is initialized. + - Contract is initialized. #### balanceOf [toc] [#MultiToken-balanceOf] @@ -132,11 +132,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; stackParams="true" > Returns the amount of `id` tokens owned by `account`. - Returns zero when `id` has never been minted or `account` has no entry for it. Requirements: - * Contract is initialized. + - Contract is initialized. #### setApprovalForAll [toc] [#MultiToken-setApprovalForAll] @@ -152,12 +151,14 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; modulePrefix="MultiToken" > Enables or disables approval for `operator` to manage all of the caller's assets. - The caller's identity is derived from the `wit_MultiTokenSK` witness as `persistentHash(secretKey)`. + + In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK` + witness as `persistentHash(secretKey)`. Requirements: - * Contract is initialized. - * `operator` is not zero. + - Contract is initialized. + - `operator` is not zero. #### isApprovedForAll [toc] [#MultiToken-isApprovedForAll] @@ -176,7 +177,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - * Contract is initialized. + - Contract is initialized. #### transferFrom [toc] [#MultiToken-transferFrom] @@ -194,21 +195,22 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; > Transfers ownership of `value` amount of `id` tokens from `fromAddress` to `to`. The caller must be `fromAddress` or approved to transfer on their behalf. - The caller's identity is derived from the `wit_MultiTokenSK` witness as `persistentHash(secretKey)`. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - + In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK` + witness as `persistentHash(secretKey)`. + + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: - * Contract is initialized. - * `to` is not a ContractAddress. - * `to` is not zero. - * `fromAddress` is not zero. - * Caller must be `fromAddress` or approved via [setApprovalForAll](#MultiToken-setApprovalForAll). - * `fromAddress` must have an `id` balance of at least `value`. + - Contract is initialized. + - `to` is not a ContractAddress. + - `to` is not zero. + - `fromAddress` is not zero. + - Caller must be `fromAddress` or approved via `setApprovalForAll`. + - `fromAddress` must have an `id` balance of at least `value`. #### _transfer [toc] [#MultiToken-_transfer] @@ -226,46 +228,20 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; modulePrefix="MultiToken" > Transfers ownership of `value` amount of `id` tokens from `fromAddress` to `to`. - Does not impose restrictions on the caller, making it suitable for composition in higher-level contract logic. + Does not impose restrictions on the caller, making it suitable for composition + in higher-level contract logic. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: - * Contract is initialized. - * `to` is not a ContractAddress. - * `to` is not zero. - * `fromAddress` is not zero. - * `fromAddress` must have an `id` balance of at least `value`. - - -#### _update [toc] [#MultiToken-_update] -#### [!toc] [#MultiToken-_update] - - Transfers a value amount of tokens of type `id` from `fromAddress` to `to`. - This circuit will mint (or burn) if `fromAddress` (or `to`) is zero. - - A zero `value` writes nothing: the sender's balance is still checked, but no balance entry is created or updated, - so a zero-value transfer of an `id` that has never been minted does not revert. - Whether `value` is zero is disclosed. - - Requirements: - - * Contract is initialized. - * If `fromAddress` is not zero, the balance of `id` of `fromAddress` must be >= `value`. + - Contract is initialized. + - `to` is not a ContractAddress. + - `to` is not zero. + - `fromAddress` is not zero. + - `fromAddress` must have an `id` balance of at least `value`. #### _unsafeTransferFrom [toc] [#MultiToken-_unsafeTransferFrom] @@ -282,23 +258,25 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; stackParams="true" modulePrefix="MultiToken" > - Unsafe variant of [transferFrom](#MultiToken-transferFrom) which allows transfers to contract addresses. + Unsafe variant of `transferFrom` which allows transfers to contract addresses. The caller must be `fromAddress` or approved to transfer on their behalf. - The caller's identity is derived from the `wit_MultiTokenSK` witness as `persistentHash(secretKey)`. + + In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK` + witness as `persistentHash(secretKey)`. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. Requirements: - * Contract is initialized. - * `to` is not zero. - * `fromAddress` is not zero. - * Caller must be `fromAddress` or approved via [setApprovalForAll](#MultiToken-setApprovalForAll). - * `fromAddress` must have an `id` balance of at least `value`. + - Contract is initialized. + - `to` is not zero. + - `fromAddress` is not zero. + - Caller must be `fromAddress` or approved via `setApprovalForAll`. + - `fromAddress` must have an `id` balance of at least `value`. #### _unsafeTransfer [toc] [#MultiToken-_unsafeTransfer] @@ -315,21 +293,22 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; stackParams="true" modulePrefix="MultiToken" > - Unsafe variant of [_transfer](#MultiToken-_transfer) which allows transfers to contract addresses. - Does not impose restrictions on the caller, making it suitable as a low-level building block for advanced contract logic. + Unsafe variant of `_transfer` which allows transfers to contract addresses. + Does not impose restrictions on the caller, making it suitable as a low-level + building block for advanced contract logic. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. Requirements: - * Contract is initialized. - * `fromAddress` is not zero. - * `to` is not zero. - * `fromAddress` must have an `id` balance of at least `value`. + - Contract is initialized. + - `fromAddress` is not zero. + - `to` is not zero. + - `fromAddress` must have an `id` balance of at least `value`. #### _setURI [toc] [#MultiToken-_setURI] @@ -340,18 +319,22 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; complexity="k=6, rows=28" modulePrefix="MultiToken" > - Sets a new URI for all token types, by relying on the token type ID substitution mechanism defined in the MultiToken standard. + Sets a new URI for all token types, by relying on the token type ID + substitution mechanism defined in the MultiToken standard. See https://eips.ethereum.org/EIPS/eip-1155#metadata. - By this mechanism, any occurrence of the `\{id\}` substring - in either the URI or any of the values in the JSON file at said URI will be replaced by clients with the token type ID. + By this mechanism, any occurrence of the `\{id\}` substring in either the + URI or any of the values in the JSON file at said URI will be replaced by + clients with the token type ID. - For example, the `https://token-cdn-domain/\{id\}.json` URI would be interpreted by clients as - `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` for token type ID 0x4cce0. + For example, the `https://token-cdn-domain/\{id\}.json` URI would be + interpreted by clients as + `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` + for token type ID 0x4cce0. Requirements: - * Contract is initialized. + - Contract is initialized. #### _mint [toc] [#MultiToken-_mint] @@ -365,16 +348,15 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; > Creates a `value` amount of tokens of type `id`, and assigns them to `to`. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. Requirements: - * Contract is initialized. - * `to` is not zero. - * `to` is not a ContractAddress. + - Contract is initialized. + - `to` is not zero. + - `to` is not a ContractAddress. #### _unsafeMint [toc] [#MultiToken-_unsafeMint] @@ -390,18 +372,18 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; stackParams="true" modulePrefix="MultiToken" > - Unsafe variant of [_mint](#MultiToken-_mint) which allows transfers to contract addresses. + Unsafe variant of `_mint` which allows transfers to contract addresses. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. Requirements: - * Contract is initialized. - * `to` is not zero. + - Contract is initialized. + - `to` is not zero. #### _burn [toc] [#MultiToken-_burn] @@ -421,9 +403,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - * Contract is initialized. - * `fromAddress` is not zero. - * `fromAddress` must have an `id` balance of at least `value`. + - Contract is initialized. + - `fromAddress` is not zero. + - `fromAddress` must have an `id` balance of at least `value`. #### _setApprovalForAll [toc] [#MultiToken-_setApprovalForAll] @@ -440,27 +422,14 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; modulePrefix="MultiToken" > Enables or disables approval for `operator` to manage all of `owner`'s assets. - This circuit does not check for access permissions but can be useful as a building block for more complex contract logic. + + This circuit does not check for access permissions but can be useful as a building block + for more complex contract logic. Requirements: - * Contract is initialized. - * `owner` is not zero. - * `operator` is not zero. + - Contract is initialized. + - `owner` is not zero. + - `operator` is not zero. -#### _computeAccountId [toc] [#MultiToken-_computeAccountId] -#### [!toc] [#MultiToken-_computeAccountId] - - Computes the caller's account identifier from the `wit_MultiTokenSK` witness - by delegating to [Utils computeAccountId](./utils#Utils-computeAccountId). - - ID Derivation: `accountId = persistentHash(secretKey)` - - The result is a 32-byte commitment that uniquely identifies the caller. - The same derivation is available off-chain through `Utils_computeAccountId`. - diff --git a/content/contracts-compact/api/multisig.mdx b/content/contracts-compact/api/multisig.mdx index 4126ac40..5bad5164 100644 --- a/content/contracts-compact/api/multisig.mdx +++ b/content/contracts-compact/api/multisig.mdx @@ -74,7 +74,9 @@ The module never resolves the caller; the consuming contract passes an identity kind="ledger" id="Signer-_isInitialized" > - Records whether [initialize](#Signer-initialize) has run. It gates re-entry into `initialize` and nothing else; no other circuit consults it. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. ### Witnesses [toc] [#Signer-Witnesses] @@ -92,16 +94,16 @@ None. kind="circuit" id="Signer-initialize" > - Registers each entry of `signers` and sets the threshold to `thresh`. - If used, it MUST only be called from the contract's constructor; a contract may instead configure signers through its own gated flow with [_addSigner](#Signer-_addSigner) and [_setThreshold](#Signer-_setThreshold). - The signer identities and the threshold are disclosed to the ledger. + Initializes the signer module with the given threshold + and an initial set of signers. + If used, it should only be called in the contract's constructor. Requirements: - * Contract is not initialized. - * `thresh` is not zero. - * `thresh` does not exceed the number of `signers`. - * `signers` contains no duplicates. + - If used, can only be called once (in the constructor). + - `thresh` must not be zero. + - `thresh` must not exceed the number of `signers`. + - `signers` must not contain duplicates. #### assertSigner [toc] [#Signer-assertSigner] @@ -111,12 +113,11 @@ None. kind="circuit" modulePrefix="Signer" > - Asserts that `caller` is a registered signer. Does not check initialization: an empty registry rejects every caller. - `caller` is disclosed as the key of the set lookup. + Asserts that the given caller is an active signer. Requirements: - * `caller` is a member of the signer registry. + - `caller` must be a member of the signers registry. #### assertThresholdMet [toc] [#Signer-assertThresholdMet] @@ -127,15 +128,24 @@ None. complexity="k=9, rows=54" modulePrefix="Signer" > - Asserts that `approvalCount` meets the threshold. - The count is supplied by the consuming contract and compared against the threshold only; the registry is never consulted, - so the contract's approval accounting must itself be gated by [assertSigner](#Signer-assertSigner) and count distinct signers. - The count is not bounded by `_signerCount`, so approvals collected before a signer was removed still count. + Asserts that the given approval count meets the threshold. + + `approvalCount` is supplied by the consuming contract and is + compared against the threshold only. This circuit never consults the signer + registry. Ensuring the count reflects distinct, currently-registered signers + is the consuming contract's responsibility, and a contract whose approval + accounting is not itself gated by `assertSigner` gets no protection here. + + The count is deliberately left unbounded by `_signerCount`: approvals + collected before a signer was removed must still count toward a threshold + the remaining signers satisfy, and bounding the count would reject those. + See the stale-signer-set guidance in the module header. Requirements: - * The threshold is set (not zero). - * `approvalCount` is greater than or equal to the threshold. + - Ledger threshold must be set (not be zero). An unconfigured module has a + zero threshold, so this rejects before any approval count is compared. + - `approvalCount` must be >= threshold. #### getSignerCount [toc] [#Signer-getSignerCount] @@ -146,7 +156,8 @@ None. complexity="k=6, rows=26" modulePrefix="Signer" > - Returns the number of registered signers. Returns zero when none have been registered. + Returns the current signer count. Returns zero when no + signers have been registered. #### getThreshold [toc] [#Signer-getThreshold] @@ -157,7 +168,8 @@ None. complexity="k=6, rows=26" modulePrefix="Signer" > - Returns the approval threshold. Returns zero when it has not been set. + Returns the approval threshold. Returns zero when the + threshold has not been set. #### isSigner [toc] [#Signer-isSigner] @@ -167,7 +179,7 @@ None. kind="circuit" modulePrefix="Signer" > - Returns true if `account` is a registered signer. `account` is disclosed as the key of the set lookup. + Returns whether the given account is an active signer. #### _addSigner [toc] [#Signer-_addSigner] @@ -177,13 +189,15 @@ None. kind="circuit" modulePrefix="Signer" > - Adds `signer` to the registry and increments the signer count. `signer` is disclosed. + Adds a new signer to the registry. - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * `signer` is not already a registered signer. + - `signer` must not already be an active signer. #### _removeSigner [toc] [#Signer-_removeSigner] @@ -193,14 +207,16 @@ None. kind="circuit" modulePrefix="Signer" > - Removes `signer` from the registry and decrements the signer count. `signer` is disclosed. + Removes a signer from the registry. - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * `signer` is a registered signer. - * The signer count after removal is greater than or equal to the threshold. + - `signer` must be an active signer. + - Removal must not drop signer count below threshold. #### _changeThreshold [toc] [#Signer-_changeThreshold] @@ -211,14 +227,16 @@ None. complexity="k=9, rows=53" modulePrefix="Signer" > - Updates the approval threshold, checking it against the current signer count. `newThreshold` is disclosed. + Updates the approval threshold. - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * `newThreshold` is not zero. - * `newThreshold` does not exceed the current signer count. + - `newThreshold` must not be zero. + - `newThreshold` must not exceed the current signer count. #### _setThreshold [toc] [#Signer-_setThreshold] @@ -229,17 +247,30 @@ None. complexity="k=6, rows=40" modulePrefix="Signer" > - Sets the approval threshold without checking it against the signer count. `newThreshold` is disclosed. - Intended for construction or custom setup flows where signers are not yet registered. - A threshold above the signer count is permitted mid-setup and is not self-enforcing: - the contract must not gate operations on [assertThresholdMet](#Signer-assertThresholdMet) while setup is incomplete. - Use [_changeThreshold](#Signer-_changeThreshold) for operational changes. + Sets the approval threshold without checking + against the current signer count. + + + This is intended for use during contract construction + or custom setup flows where signers may not yet be registered. - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Leaving `newThreshold` above the signer count is permitted and expected + mid-setup, and is not self-enforcing: `assertThresholdMet` compares + `approvalCount` against the threshold alone, so a caller supplying a matching + count satisfies it even against an empty registry. Contracts must not gate + operations on `assertThresholdMet` while setup is incomplete. Lower the + threshold with `_setThreshold`; `_changeThreshold` cannot lower one while the + signer count is zero, because that would require a zero threshold. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Use `_changeThreshold` for + operational threshold changes with signer count validation. Requirements: - * `newThreshold` is not zero. + - `newThreshold` must not be zero. --- @@ -266,8 +297,10 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ProposalManager" kind="enum" id="ProposalManager-ProposalStatus" > - Proposal lifecycle states. `Inactive` is the zero value and is never stored: - [_createProposal](#ProposalManager-_createProposal) writes `Active`, and every view asserts the proposal exists before reading. + Proposal lifecycle states. `Inactive` is the zero value and is + never stored: `_createProposal` writes `Active`, and every view asserts the + proposal exists before reading, so a stored proposal can never read + `Inactive`. #### RecipientKind [toc] [#ProposalManager-RecipientKind] @@ -340,7 +373,7 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Constructs a `ShieldedUser` recipient from a coin public key. + Constructs a shielded user recipient. #### unshieldedUserRecipient [toc] [#ProposalManager-unshieldedUserRecipient] @@ -350,7 +383,7 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Constructs an `UnshieldedUser` recipient from a user address. + Constructs an unshielded user recipient. #### contractRecipient [toc] [#ProposalManager-contractRecipient] @@ -360,7 +393,7 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Constructs a `Contract` recipient from a contract address. + Constructs a contract recipient. #### toShieldedRecipient [toc] [#ProposalManager-toShieldedRecipient] @@ -370,11 +403,12 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Converts a recipient into the form a shielded send accepts: `left` for a `ShieldedUser`, `right` for a `Contract`. + Converts a Recipient to a shielded send recipient. + Handles both ShieldedUser and Contract kinds. Requirements: - * `r.kind` is `ShieldedUser` or `Contract`. + - Recipient kind must be ShieldedUser or Contract. #### toUnshieldedRecipient [toc] [#ProposalManager-toUnshieldedRecipient] @@ -384,11 +418,12 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Converts a recipient into the form an unshielded send accepts: `left` for a `Contract`, `right` for an `UnshieldedUser`. + Converts a Recipient to an unshielded send recipient. + Handles both UnshieldedUser and Contract kinds. Requirements: - * `r.kind` is `UnshieldedUser` or `Contract`. + - Recipient kind must be UnshieldedUser or Contract. #### assertProposalExists [toc] [#ProposalManager-assertProposalExists] @@ -398,11 +433,11 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Asserts that a proposal with `id` has been created. `id` is disclosed. + Asserts that a proposal exists. Requirements: - * A proposal with `id` exists. + - Proposal with `id` must have been created. #### assertProposalActive [toc] [#ProposalManager-assertProposalActive] @@ -412,12 +447,12 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Asserts that a proposal with `id` exists and is `Active`. `id` is disclosed. + Asserts that a proposal exists and is active. Requirements: - * A proposal with `id` exists. - * Its status is `Active`. + - Proposal must exist. + - Proposal status must be Active. #### _createProposal [toc] [#ProposalManager-_createProposal] @@ -432,18 +467,30 @@ None. modulePrefix="ProposalManager" stackParams="true" > - Increments the proposal counter, stores a new `Active` proposal under the new id, and returns the id. - The whole proposal (recipient, color, amount, status) is disclosed to the ledger. - The counter is incremented and read back as the map key, so concurrent creations against the same state conflict. + Creates a new proposal. + + + The recipient is NOT validated beyond being well-typed. Two + consequences the consumer must handle: - The recipient is not validated beyond being well-typed: a kind the execution rail cannot accept creates a proposal that stays `Active` until cancelled, - and an unspendable address is accepted. + - A `kind` the execution rail cannot accept (an `UnshieldedUser` recipient + on a shielded treasury, say) creates a proposal that passes here and then + reverts in `toShieldedRecipient` at execution. It stays `Active` and + unexecutable on that rail until `_cancelProposal` retires it. + - A zero or otherwise unspendable `address` is accepted, and funds sent to + it on execution are unrecoverable. + - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + `_nextProposalId` is incremented and then read back for the map key, + making this a pinned read-modify-write. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * `amount` is greater than zero. + - `amount` must be greater than 0. #### _cancelProposal [toc] [#ProposalManager-_cancelProposal] @@ -453,13 +500,15 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Sets the proposal's status to `Cancelled`. `id` is disclosed. + Cancels a proposal. - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * The proposal exists and is `Active`. + - Proposal must be active. #### _markExecuted [toc] [#ProposalManager-_markExecuted] @@ -469,13 +518,15 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Sets the proposal's status to `Executed`. `id` is disclosed. + Marks a proposal as executed. - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * The proposal exists and is `Active`. + - Proposal must be active. #### getProposal [toc] [#ProposalManager-getProposal] @@ -485,11 +536,7 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Returns the full proposal. `id` is disclosed. - - Requirements: - - * A proposal with `id` exists. + Returns the full proposal data. #### getProposalRecipient [toc] [#ProposalManager-getProposalRecipient] @@ -499,11 +546,7 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Returns the proposal's recipient. `id` is disclosed. - - Requirements: - - * A proposal with `id` exists. + Returns the recipient of a proposal. #### getProposalAmount [toc] [#ProposalManager-getProposalAmount] @@ -513,11 +556,7 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Returns the proposal's amount. `id` is disclosed. - - Requirements: - - * A proposal with `id` exists. + Returns the amount of a proposal. #### getProposalColor [toc] [#ProposalManager-getProposalColor] @@ -527,11 +566,7 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Returns the proposal's token color. `id` is disclosed. - - Requirements: - - * A proposal with `id` exists. + Returns the token color of a proposal. #### getProposalStatus [toc] [#ProposalManager-getProposalStatus] @@ -541,11 +576,7 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Returns the proposal's status. `id` is disclosed. - - Requirements: - - * A proposal with `id` exists. + Returns the status of a proposal. --- @@ -610,16 +641,28 @@ None. kind="circuit" modulePrefix="ShieldedTreasury" > - Claims `coin` with `receiveShielded`, then merges it into the stored coin of the same color (`mergeCoinImmediate`) or inserts it when the color is new, - and adds its value to the received total. - The coin and the updated total are disclosed to the ledger. - Zero-value deposits are permitted: on a new color the zero coin occupies that color's slot until a later deposit merges into it. + Receives a shielded coin into the treasury. + + The coin is first claimed at the protocol level via `receiveShielded`, + which allocates the Merkle tree index required by `insertCoin`. + The coin is then merged with any existing coin of the same color, + or inserted as a new entry if no coin of that color exists. + + Zero-value deposits are permitted and are not always a no-op. On a color the + treasury does not yet hold, a zero-value coin is inserted and occupies that + color's single slot until a later deposit merges into it; if a coin of that + color already exists, the zero merges into it via `mergeCoinImmediate` and + the stored value is unchanged. They may serve as signaling mechanisms when + events are supported. They are not useful as privacy decoys, this + treasury's holdings are public (see the module header). - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * The received total for `coin.color` plus `coin.value` does not overflow `Uint<128>`. + - Deposit must not cause received total overflow. #### _send [toc] [#ShieldedTreasury-_send] @@ -634,22 +677,31 @@ None. modulePrefix="ShieldedTreasury" stackParams="true" > - Looks up the stored coin for `color`, sends `amount` of it to `recipient` with `sendShielded`, and adds `amount` to the sent total. - Any change is routed back to the contract as a self-owned output and retained in `_coins` for the next spend; - when the send leaves no change, the color's entry is removed. - The color, recipient, amount, and retained change coin are disclosed to the ledger. + Sends shielded tokens from the treasury. - The returned `ShieldedSendResult` carries the sent and change coins in plaintext; returning it from an entry point, writing it to the ledger, - or passing it to another contract discloses them. - The change coin belongs to the treasury and must not be spent again by the caller. + Looks up the stored coin by color, verifies sufficient value, + and executes the shielded send. `sendShielded` already routes any + change back to this contract as a self-owned output. Unlike the + stateless variant, this treasury RETAINS the change: it is recorded in + the UTXO map for the next spend. The returned `result.change` therefore + belongs to the treasury and must not be re-spent by the caller (doing + so would double-spend the recorded coin). - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + The returned `ShieldedSendResult` carries the sent and change coins + in plaintext, so sinking it (returning it from an entry point, writing it + to ledger, passing it cross-contract) discloses them. `_coins` publishes only + the retained change coin, and nothing at all when the send leaves no change, + so the sent coin's plaintext is disclosed beyond anything in public state. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * A coin of `color` exists in the treasury. - * The stored coin's value is greater than or equal to `amount`. - * The sent total for `color` plus `amount` does not overflow `Uint<128>`. + - A coin of the given `color` must exist in the treasury. + - The coin's value must be >= `amount`. + - Send must not cause sent total overflow. #### getTokenBalance [toc] [#ShieldedTreasury-getTokenBalance] @@ -659,7 +711,8 @@ None. kind="circuit" modulePrefix="ShieldedTreasury" > - Returns the value of the stored coin for `color`, or zero when none is held. `color` is disclosed. + Returns the current token balance for a color. + Reads the actual coin value from the UTXO map. #### getReceivedTotal [toc] [#ShieldedTreasury-getReceivedTotal] @@ -669,7 +722,7 @@ None. kind="circuit" modulePrefix="ShieldedTreasury" > - Returns the cumulative value received for `color`, or zero. `color` is disclosed. + Returns the cumulative received total for a color. #### getSentTotal [toc] [#ShieldedTreasury-getSentTotal] @@ -679,7 +732,7 @@ None. kind="circuit" modulePrefix="ShieldedTreasury" > - Returns the cumulative value sent for `color`, or zero. `color` is disclosed. + Returns the cumulative sent total for a color. #### getReceivedMinusSent [toc] [#ShieldedTreasury-getReceivedMinusSent] @@ -689,8 +742,16 @@ None. kind="circuit" modulePrefix="ShieldedTreasury" > - Returns the received total minus the sent total for `color`, which equals [getTokenBalance](#ShieldedTreasury-getTokenBalance) when all value moved through `_deposit` and `_send`. - The subtraction is unguarded: a consumer that writes `_coins` directly can make it fail as a range violation. + Returns the difference between cumulative received + and cumulative sent totals for a color. Should equal + `getTokenBalance` if accounting is consistent. + + The subtraction is unguarded. `_send` cannot drive the sent total + past the received total. Change re-insertion never increments `_received` + so the invariant holds for any consumer that goes through `_deposit` and + `_send`. A consumer that writes the exported `_coins` directly can break + it, and this circuit then fails as an unprovable range violation rather + than a named assert. --- @@ -727,9 +788,7 @@ None. kind="circuit" modulePrefix="ShieldedTreasuryStateless" > - Claims `coin` with `receiveShielded`. Nothing is written to the ledger; the coin is disclosed to the protocol. - - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Receives a shielded coin into the treasury. #### _send [toc] [#ShieldedTreasuryStateless-_send] @@ -744,21 +803,40 @@ None. modulePrefix="ShieldedTreasuryStateless" stackParams="true" > - Sends `amount` of the caller-supplied `coin` to `recipient` with `sendShielded` and returns the result untouched. - The coin, recipient, and amount are disclosed. - Any change is routed back to the contract as a self-owned output and returned in `result.change` as a live, spendable coin: - keep it off-chain as the `coin` of a later spend, or route it onward in the same transaction with `sendImmediateShielded`. - The change must never be spent again and still treated as held. - - The returned `ShieldedSendResult` holds the sent and change coins in plaintext. - Consuming it in-circuit is safe; returning it from an entry point, writing it to the ledger, or passing it to another contract publishes those coins, - and the compiler does not require a `disclose` at that point. - - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Sends shielded tokens using a caller-provided coin. + + The coin is supplied by the caller rather than looked up from + on-chain state; the token color is derived from `coin.color`. + Executes the shielded send and returns the result untouched. + `sendShielded` already routes any change back to this contract as a + self-owned output, so `result.change` is a live, spendable coin. The + implementing contract chooses what to do with it: retain it off-chain and + pass it back as the `coin` for a later spend, or spend it onward to a + different recipient in the same transaction via `sendImmediateShielded`. + (Re-spending change is only meaningful when it goes somewhere other than + self — `sendShielded` already routes it to self.) No balance or + received/sent accounting is tracked on the ledger. + + + The returned `ShieldedSendResult` holds the sent and change coins + in plaintext (`nonce`, `color`, `value`). Consuming it in-circuit is safe + (passing `result.change` to `sendImmediateShielded`, for instance). Returning + it from your entry point, writing it to ledger, or passing it to another + contract publishes those coin details, including the amount left over from + the coin that was spent. Note the compiler will NOT stop you: the + `disclose` calls below clear the witness taint, so such a return compiles + with no `disclose` of your own and no marker at the disclosure site. If you + do not need in-circuit change handling, ignore the result. The change is + recoverable off-chain from the transaction's private proving state. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * `coin.value` is greater than or equal to `amount` (enforced by `sendShielded`). + - `coin.value` must be >= `amount`. --- @@ -803,15 +881,24 @@ None. kind="circuit" modulePrefix="UnshieldedTreasury" > - Receives `amount` of `color` with `receiveUnshielded` and adds it to the tracked balance. - The color, amount, and new balance are disclosed. - Zero-value deposits are permitted and create a zero-valued balance entry on a new color. + Receives unshielded tokens into the treasury. + + The token receive is executed at the protocol level first via + `receiveUnshielded`. The balance map is then updated. + + Zero-value deposits are permitted. While currently a no-op + economically, they may serve as signaling mechanisms when events + are supported. On a color the treasury has not held, a zero-value + operation creates a zero-valued `_balances` entry, which + `getTokenBalance` reports the same as absence. - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * The tracked balance for `color` plus `amount` does not overflow `Uint<128>`. + - Deposit must not cause balance overflow. #### _send [toc] [#UnshieldedTreasury-_send] @@ -826,17 +913,32 @@ None. modulePrefix="UnshieldedTreasury" stackParams="true" > - Subtracts `amount` from the tracked balance and sends it to `recipient` with `sendUnshielded`. - The color, amount, recipient, and new balance are disclosed. - Zero-amount sends are permitted. - Recipients are not validated: an unspendable recipient permanently removes the funds, - and a send to the contract's own address zeroes the tracked balance while the funds stay held at the protocol level. - - Access control is NOT enforced. The consuming contract must gate this behind its own authorization policy. + Sends unshielded tokens from the treasury. Zero-amount sends + are permitted and are value-preserving no-ops. + + + Recipients are not validated. An unspendable recipient (such as a + zero address) is accepted and permanently removes the funds from the + contract. This is not a sanctioned burn — no unshielded equivalent of + `shieldedBurnAddress` is exposed to contracts — so treat recipient + validation as the consuming contract's responsibility. + + + + Sending to this contract's own address zeroes the tracked balance + while the funds stay held and spendable at the protocol level, after which + `_send` will refuse to touch them. Recovery is only possible outside this + module: a consumer may reconcile the exported `_balances`, or spend the + funds through its own `sendUnshielded` call. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Requirements: - * The tracked balance for `color` is greater than or equal to `amount`. + - The treasury must hold sufficient balance for the given token color. #### getTokenBalance [toc] [#UnshieldedTreasury-getTokenBalance] @@ -846,7 +948,7 @@ None. kind="circuit" modulePrefix="UnshieldedTreasury" > - Returns the tracked balance for `color`, or zero. `color` is disclosed. + Returns the balance for a given token color. --- @@ -883,7 +985,9 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderPrivate kind="ledger" id="ForwarderPrivate-_isInitialized" > - Boolean indicating if initialized. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. ### Witnesses [toc] [#ForwarderPrivate-Witnesses] @@ -901,12 +1005,17 @@ None. kind="circuit" modulePrefix="ForwarderPrivate" > - Stores the parent commitment, computed off-chain with [_calculateParentCommitment](#ForwarderPrivate-_calculateParentCommitment). The commitment is disclosed. + Initializes the forwarder with a parent commitment. + Called once from the preset constructor. The commitment is + immutable after init. Requirements: - * Contract is not initialized. - * `parentCommitment` is not the zero bytes. + - Contract must not be initialized. + - `parentCommitment` must not be the zero bytes. A zero commitment + is the sole drain gate and would leave every deposited coin + permanently unrecoverable (no preimage exists for `default>` + under the domain-tagged hash). #### _deposit [toc] [#ForwarderPrivate-_deposit] @@ -917,11 +1026,12 @@ None. complexity="k=13, rows=6538" modulePrefix="ForwarderPrivate" > - Claims `coin` with `receiveShielded`. Nothing is written to the ledger; the coin dwells at the contract address until drained. The coin is disclosed to the protocol. + Receives a shielded coin into the forwarder's custody. + No ledger write — coins dwell at the contract address until drained. Requirements: - * Contract is initialized. + - Contract must be initialized. #### _drain [toc] [#ForwarderPrivate-_drain] @@ -938,20 +1048,37 @@ None. modulePrefix="ForwarderPrivate" stackParams="true" > - Sends `value` of a previously deposited `coin` to `parent` with `sendShielded`, after checking that `(parent, opSecret)` hashes to the stored commitment. - The coin and value are disclosed; the parent is a coin public key, which is encrypted inside the Zswap output and does not appear on the public transcript. - `opSecret` never leaves the circuit. - Any change is routed back to the contract and returned untouched in `result.change`, to leave dwelling for a future drain or to route onward with `sendImmediateShielded`. - The change must never be spent again and still treated as held. - - Knowledge of the preimage is the only authorization; losing the operational secret makes every coin at the contract permanently inaccessible. + Spends a previously-deposited shielded coin to the parent, + a coin public key. The caller proves knowledge of `(parent, opSecret)` + matching the stored commitment; the coin is sent as a plain shielded note, + so the parent stays hidden on-chain. If `coin.value` exceeds `value`, + `sendShielded` routes the change back to this contract as a self-owned + output; `_drain` returns it untouched via `result.change`. The + implementing contract chooses what to do with it: leave it dwelling at + the contract for a future drain, or spend it onward to a different + recipient in the same transaction via `sendImmediateShielded`. + + The parent is a `ZswapCoinPublicKey`, never a `ContractAddress`: a shielded + send to a contract publishes that contract's address in cleartext on the + transaction (the protocol must route the coin to a named contract), which + would defeat the private-parent guarantee. A coin public key is encrypted + inside the Zswap output and never appears on the public transcript. Requirements: - * Contract is initialized. - * `parent` is not the zero key. - * `_calculateParentCommitment(parent.bytes, opSecret)` equals `_parentCommitment`. - * `coin.value` is greater than or equal to `value` (enforced by `sendShielded`). + - Contract must be initialized. + - `parent` must not be the zero key. + - `_calculateParentCommitment(parent.bytes, opSecret) == _parentCommitment`. + - `coin.value` must be >= `value` (enforced by `sendShielded`). + + + **Losing the operational secret is permanent fund loss.** It + is the sole drain authorization, with no rotation, revocation, or + recovery path. If the operator misplaces it, every shielded coin + accumulated at this contract is forever inaccessible, equivalent to + losing a hot-wallet private key. Back it up offline before the first + deposit and treat it with the same hygiene as a signing key. + #### _calculateParentCommitment [toc] [#ForwarderPrivate-_calculateParentCommitment] @@ -961,8 +1088,16 @@ None. kind="pure" modulePrefix="ForwarderPrivate" > - Returns `H(pad(32, "ForwarderPrivate:commitment") || parentAddr || opSecret)` using `persistentHash`. - Used off-chain by the deployer to compute the constructor argument and inside [_drain](#ForwarderPrivate-_drain) for the preimage check. Callable without initialization. + Computes the parent commitment from `(parentAddr, opSecret)`. + Pure circuit — used off-chain by the deployer to compute the + constructor argument, and inside `_drain` for the preimage check. + Callable without initialization. + + The first hash input is a fixed domain tag + (`pad(32, "ForwarderPrivate:commitment")`). The tag prevents + preimage collisions with other `persistentHash` users in the + system that hash two `Bytes<32>` values — a colliding preimage + crafted under a different domain cannot satisfy this commitment. --- @@ -989,7 +1124,10 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderShielde kind="ledger" id="ForwarderShielded-_parent" > - The parent recipient. Stored as a generic `Either` for forward compatibility; `initialize` only ever writes the `left` (coin public key) arm. + The parent recipient, stored generically as + `Either` for forward compatibility. + `initialize` only ever writes the `left` (coin public key) arm today. See + the module header. #### _isInitialized [toc] [#ForwarderShielded-_isInitialized] @@ -999,7 +1137,9 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderShielde kind="ledger" id="ForwarderShielded-_isInitialized" > - Boolean indicating if initialized. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. ### Witnesses [toc] [#ForwarderShielded-Witnesses] @@ -1017,13 +1157,19 @@ None. kind="circuit" modulePrefix="ForwarderShielded" > - Stores `parent` in the `left` arm of `_parent`. The parent is disclosed. - Beyond the zero-key check, the caller must ensure `parent` can spend forwarded coins; an unspendable parent strands every deposit. + Initializes the forwarder with a parent recipient. + Called once from the preset constructor. The coin public key is + stored in the `left` arm of the generic `_parent` ledger field. Requirements: - * Contract is not initialized. - * `parent` is not the zero key. + - Contract must not be initialized. + - `parent` must not be the zero key. A zero parent would forward + every deposit to an unspendable key with no recovery path. + + Beyond the zero-key check, the caller must ensure `parent` can + spend forwarded coins. An unspendable parent strands every deposit with + no recovery path. #### _deposit [toc] [#ForwarderShielded-_deposit] @@ -1034,11 +1180,15 @@ None. complexity="k=15, rows=24446" modulePrefix="ForwarderShielded" > - Claims `coin` with `receiveShielded` and immediately re-sends its full value to `_parent` with `sendImmediateShielded`. The coin is disclosed. + Receives a shielded coin and atomically forwards it to + `_parent`. The coin is claimed at the protocol level via + `receiveShielded`, then immediately re-sent via + `sendImmediateShielded` to the stored parent recipient (always the + `left` / coin-public-key arm today). Requirements: - * Contract is initialized. + - Contract must be initialized. --- @@ -1065,7 +1215,10 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderUnshiel kind="ledger" id="ForwarderUnshielded-_parent" > - The parent recipient. Stored as a generic `Either` for forward compatibility; `initialize` only ever writes the `right` (user address) arm. + The parent recipient, stored generically as + `Either` for forward compatibility. + `initialize` only ever writes the `right` (user address) arm today. See + the module header. #### _isInitialized [toc] [#ForwarderUnshielded-_isInitialized] @@ -1075,7 +1228,9 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderUnshiel kind="ledger" id="ForwarderUnshielded-_isInitialized" > - Boolean indicating if initialized. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. ### Witnesses [toc] [#ForwarderUnshielded-Witnesses] @@ -1093,13 +1248,19 @@ None. kind="circuit" modulePrefix="ForwarderUnshielded" > - Stores `parent` in the `right` arm of `_parent`. The parent is disclosed. - Beyond the zero-address check, the caller must ensure `parent` can spend forwarded amounts; an unspendable parent strands every deposit. + Initializes the forwarder with a parent recipient. + Called once from the preset constructor. The user address is stored + in the `right` arm of the generic `_parent` ledger field. Requirements: - * Contract is not initialized. - * `parent` is not the zero address. + - Contract must not be initialized. + - `parent` must not be the zero address. A zero parent would forward + every deposit to an unspendable address with no recovery path. + + Beyond the zero-address check, the caller must ensure `parent` + can spend forwarded amounts. An unspendable parent strands every + deposit with no recovery path. #### _deposit [toc] [#ForwarderUnshielded-_deposit] @@ -1110,11 +1271,12 @@ None. complexity="k=10, rows=565" modulePrefix="ForwarderUnshielded" > - Receives `amount` of `color` with `receiveUnshielded` and immediately forwards it to `_parent` with `sendUnshielded`. - Depositor, recipient, color, and amount all appear on the public transcript. + Receives an unshielded amount of `color` and atomically + forwards it to `_parent` via `sendUnshielded` (always the `right` / + user-address arm today). Requirements: - * Contract is initialized. + - Contract must be initialized. diff --git a/content/contracts-compact/api/native-shielded-token.mdx b/content/contracts-compact/api/native-shielded-token.mdx index 1a17734b..3a6f0192 100644 --- a/content/contracts-compact/api/native-shielded-token.mdx +++ b/content/contracts-compact/api/native-shielded-token.mdx @@ -49,7 +49,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedToken kind="sealed ledger" id="NativeShieldedToken-_domain" > - Domain separator fixed at construction. Together with the contract address it determines this token's color. + Domain separator fixed at construction; with the contract + address it determines this token's color. #### _name [toc] [#NativeShieldedToken-_name] @@ -89,7 +90,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedToken kind="ledger" id="NativeShieldedToken-_isInitialized" > - Boolean indicating if initialized. Re-exported from `NativeShieldedTokenCore`. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. ### Witnesses [toc] [#NativeShieldedToken-Witnesses] @@ -113,17 +116,11 @@ None. modulePrefix="NativeShieldedToken" stackParams="true" > - Initializes the token's domain and metadata. - - This MUST be called in the implementing contract's constructor, and it must be the only token operation - performed there. Failure to call it can lead to an irreparable contract. Perform mints, burns, and `tokenColor` - only in post-deployment circuits. - - Discloses `domainSep`, `name_`, `symbol_`, and `decimals_` to the ledger. - - Requirements: - - * Contract is not initialized. + Initializes the module's domain and metadata. + This MUST be called in the implementing contract's constructor, and it + MUST be the only token operation performed there. Failure to call it can + lead to an irreparable contract; perform mint, burn, and `tokenColor` only + in post-deployment circuits, never in the constructor. #### isInitialized [toc] [#NativeShieldedToken-isInitialized] @@ -134,7 +131,7 @@ None. complexity="k=6, rows=26" modulePrefix="NativeShieldedToken" > - Returns whether the contract has been initialized. Safe to call before `initialize`. + Returns whether the contract has been initialized. #### name [toc] [#NativeShieldedToken-name] @@ -149,7 +146,7 @@ None. Requirements: - * Contract is initialized. + - Contract is initialized. #### symbol [toc] [#NativeShieldedToken-symbol] @@ -160,11 +157,11 @@ None. complexity="k=6, rows=28" modulePrefix="NativeShieldedToken" > - Returns the token symbol. + Returns the symbol of the token. Requirements: - * Contract is initialized. + - Contract is initialized. #### decimals [toc] [#NativeShieldedToken-decimals] @@ -175,12 +172,11 @@ None. complexity="k=6, rows=28" modulePrefix="NativeShieldedToken" > - Returns the number of decimals used to get the user representation. - This is a display convention only; the protocol operates on integer values. + Returns the number of decimals used to get its user representation. Requirements: - * Contract is initialized. + - Contract is initialized. #### tokenColor [toc] [#NativeShieldedToken-tokenColor] @@ -191,11 +187,12 @@ None. complexity="k=13, rows=3971" modulePrefix="NativeShieldedToken" > - Returns this token's coin color, `tokenType(_domain, kernel.self())`, computed at call time. + Returns this token's coin color: + `tokenType(_domain, kernel.self())`, computed at call time. Requirements: - * Contract is initialized. + - Contract is initialized. #### _mint [toc] [#NativeShieldedToken-_mint] @@ -211,21 +208,24 @@ None. modulePrefix="NativeShieldedToken" stackParams="true" > - Mints `amount` of the token to `recipient` using the caller-supplied `nonce`, and returns the new coin's info. - - The caller is responsible for nonce uniqueness: reusing a nonce for the same (value, recipient) produces a - duplicate commitment, which the ledger rejects. With a secret, random nonce the mint is recipient-private; - for mints without a caller-managed nonce, compose `NativeShieldedTokenDerivedNonce` and pass its `_deriveNonce()` output. + Mints `amount` of the token to `recipient`, using a + caller-supplied nonce. - The returned coin info is the only copy available to the recipient, so callers should deliver it out of band; - wallets cannot detect contract-minted coins by scanning the chain. + The caller is fully responsible for nonce uniqueness. Reusing a + nonce for the same (value, recipient) produces a duplicate commitment, + which the protocol rejects. With a secret random nonce this is the + recipient-private mint. For mints that require no caller-managed nonce, + compose the `NativeShieldedTokenDerivedNonce` extension and pass its + `_deriveNonce()` output. - Puts the coin color, the amount, and the coin commitment on the ledger. The recipient and the nonce are not posted. + The returned coin info is the only copy available to the + recipient; callers SHOULD deliver it out of band. Wallets cannot detect + contract-minted coins by scanning the chain. Requirements: - * Contract is initialized. - * `recipient` is not the zero key. + - Contract is initialized. + - `recipient` is not zero. #### _mintToSelf [toc] [#NativeShieldedToken-_mintToSelf] @@ -236,17 +236,20 @@ None. complexity="k=14, rows=16070" modulePrefix="NativeShieldedToken" > - Mints `amount` to this contract itself and claims the coin in the same call. - Nonce uniqueness is the caller's responsibility, as in `_mint`. + Mints `amount` to this contract itself, claiming the coin in + the same call. Forwards to `NativeShieldedTokenCore._mint` with + `right(kernel.self())`. - The consumer should persist the returned coin info in its own ledger state: it is the contract's holding and - is not otherwise recoverable. Spend it later with `_burnFromSelf`, once it carries a Merkle-tree index. + Nonce uniqueness is the caller's responsibility, as in `_mint`. Spend + the coin later with `_burnFromSelf`, once it carries an `mt_index`. - Puts the coin color, the amount, and the coin commitment on the ledger. + The consumer SHOULD persist the returned coin info in its own + ledger state: it is the contract's holding and is not otherwise + recoverable. Requirements: - * Contract is initialized. + - Contract is initialized. #### _burn [toc] [#NativeShieldedToken-_burn] @@ -262,24 +265,30 @@ None. modulePrefix="NativeShieldedToken" stackParams="true" > - Burns `amount` from `coin`, a coin provided within the current transaction (for example paid in by the - caller's wallet), and routes the remaining change to `refundTo`. + Burns `amount` from `coin`, a coin provided within the + current transaction (e.g. paid in by the caller's wallet), and routes the + remaining change to `refundTo`. - Returns the refund coin created for `refundTo` as `some`, or `none` if the coin was burned in full. - To burn the coin in full, pass `coin.value` as `amount`; `refundTo` is inert in that case but must still be - supplied and non-zero. For coins the contract already holds, use `_burnFromSelf` instead. + To destroy `coin` in full, pass `coin.value` as `amount`; the change + branch will not fire and `none` is returned. The `refundTo` value is + inert in that case but must still be supplied and non-zero. - The returned refund coin info is the only copy available to `refundTo`; callers should deliver it out of band. + The coin is received by the contract and spent in the same + transaction, so the spend goes through `sendImmediateShielded` (the + transient path). For coins the contract already holds, use + `_burnFromSelf` instead. - Puts the spent coin's nullifier and the output commitments on the ledger. The amount is not posted; a change - output reveals that the burn was partial. + The returned refund coin info is the only copy available to + `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect + contract-sent coins by scanning the chain. Requirements: - * Contract is initialized. - * `coin.color` is this contract's token color. - * `amount` is less than or equal to `coin.value`. - * `refundTo` is not the zero key (the zero key is the burn address; a zero `refundTo` would burn the change as well). + - Contract is initialized. + - `coin.color` is this contract's token color. + - `amount` is less than or equal to `coin.value`. + - `refundTo` is not zero (the zero key is the burn address; a zero + `refundTo` would silently burn the change as well). #### _burnFromSelf [toc] [#NativeShieldedToken-_burnFromSelf] @@ -290,19 +299,19 @@ None. complexity="k=15, rows=23307" modulePrefix="NativeShieldedToken" > - Burns `amount` from `coin`, a coin this contract already holds (a Merkle-tree entry with a valid `mt_index`). - Any change is received by the contract automatically and returned as `some`; `none` means the coin was burned in full. - - The consumer should persist the returned change coin info in its own ledger state: the change replaces `coin` - as the contract's holding, and its info is not otherwise recoverable. + Burns `amount` from `coin`, a coin this contract already + holds (a Merkle-tree entry with a valid `mt_index`). Any change is + auto-received by the contract and returned. - Puts the spent coin's nullifier and the output commitments on the ledger. The amount is not posted. + The consumer SHOULD persist the returned change coin info in its own + ledger state: the change replaces `coin` as the contract's holding, and + its info is not otherwise recoverable. Requirements: - * Contract is initialized. - * `coin.color` is this contract's token color. - * `amount` is less than or equal to `coin.value`. + - Contract is initialized. + - `coin.color` is this contract's token color. + - `amount` is less than or equal to `coin.value`. --- @@ -359,7 +368,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedToken kind="ledger" id="NativeShieldedTokenFamily-_isInitialized" > - Boolean indicating if initialized. Re-exported from `NativeShieldedTokenCore`. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. ### Witnesses [toc] [#NativeShieldedTokenFamily-Witnesses] @@ -382,17 +393,11 @@ None. modulePrefix="NativeShieldedTokenFamily" stackParams="true" > - Initializes the family metadata shared by every token type. - - This MUST be called in the implementing contract's constructor, and it must be the only token operation - performed there. Failure to call it can lead to an irreparable contract. Perform mints, burns, and `tokenColor` - only in post-deployment circuits. - - Discloses `name_`, `symbol_`, and `decimals_` to the ledger. - - Requirements: - - * Contract is not initialized. + Initializes the module's family metadata. + This MUST be called in the implementing contract's constructor, and it + MUST be the only token operation performed there. Failure to call it can + lead to an irreparable contract; perform mint, burn, and `tokenColor` only + in post-deployment circuits, never in the constructor. #### isInitialized [toc] [#NativeShieldedTokenFamily-isInitialized] @@ -403,7 +408,7 @@ None. complexity="k=6, rows=26" modulePrefix="NativeShieldedTokenFamily" > - Returns whether the contract has been initialized. Safe to call before `initialize`. + Returns whether the contract has been initialized. #### name [toc] [#NativeShieldedTokenFamily-name] @@ -414,12 +419,13 @@ None. complexity="k=6, rows=28" modulePrefix="NativeShieldedTokenFamily" > - Returns the family name shared by all token types of this contract. - Per-type identity belongs in the consumer's own state. + Returns the family name shared by all token types of this + contract. Per-type identity belongs in the consumer's own state; see the + module notes. Requirements: - * Contract is initialized. + - Contract is initialized. #### symbol [toc] [#NativeShieldedTokenFamily-symbol] @@ -430,11 +436,12 @@ None. complexity="k=6, rows=28" modulePrefix="NativeShieldedTokenFamily" > - Returns the family symbol shared by all token types of this contract. + Returns the family symbol shared by all token types of this + contract. Requirements: - * Contract is initialized. + - Contract is initialized. #### decimals [toc] [#NativeShieldedTokenFamily-decimals] @@ -445,12 +452,12 @@ None. complexity="k=6, rows=28" modulePrefix="NativeShieldedTokenFamily" > - Returns the family-wide number of decimals used to get the user representation. - Issuers with heterogeneous decimals need their own per-domain handling. + Returns the family-wide number of decimals used to get the + user representation. Requirements: - * Contract is initialized. + - Contract is initialized. #### tokenColor [toc] [#NativeShieldedTokenFamily-tokenColor] @@ -461,14 +468,12 @@ None. complexity="k=13, rows=4248" modulePrefix="NativeShieldedTokenFamily" > - Returns the coin color (token type) for `domain` under this contract's address, - `tokenType(domain, kernel.self())`, computed at call time. - - Discloses `domain` to the ledger. + Returns the coin color (token type) for `domain` under this + contract's address, so integrators do not have to re-derive it by hand. Requirements: - * Contract is initialized. + - Contract is initialized. #### _mint [toc] [#NativeShieldedTokenFamily-_mint] @@ -485,23 +490,24 @@ None. modulePrefix="NativeShieldedTokenFamily" stackParams="true" > - Mints `amount` of the token identified by `domain` to `recipient` using the caller-supplied `nonce`, - and returns the new coin's info. The same domain under the same contract address always produces the same color. - - The caller is responsible for nonce uniqueness: reusing a nonce for the same (domain, value, recipient) - produces a duplicate commitment, which the ledger rejects. With a secret, random nonce the mint is - recipient-private; for mints without a caller-managed nonce, compose `NativeShieldedTokenDerivedNonce` - and pass its `_deriveNonce()` output. + Mints `amount` of the token identified by `domain` to + `recipient`, using a caller-supplied nonce. - The returned coin info is the only copy available to the recipient, so callers should deliver it out of band; - wallets cannot detect contract-minted coins by scanning the chain. + The caller is fully responsible for nonce uniqueness. Reusing a + nonce for the same (domain, value, recipient) produces a duplicate + commitment, which the protocol rejects. With a secret random nonce this + is the recipient-private mint. For mints that require no caller-managed + nonce, compose the `NativeShieldedTokenDerivedNonce` extension and pass + its `_deriveNonce()` output. - Puts `domain`, the coin color, the amount, and the coin commitment on the ledger. The recipient and the nonce are not posted. + The returned coin info is the only copy available to the + recipient; callers SHOULD deliver it out of band. Wallets cannot detect + contract-minted coins by scanning the chain. Requirements: - * Contract is initialized. - * `recipient` is not the zero key. + - Contract is initialized. + - `recipient` is not zero. #### _mintToSelf [toc] [#NativeShieldedTokenFamily-_mintToSelf] @@ -517,17 +523,20 @@ None. modulePrefix="NativeShieldedTokenFamily" stackParams="true" > - Mints `amount` of the token identified by `domain` to this contract itself and claims the coin in the same call. - Nonce uniqueness is the caller's responsibility, as in `_mint`. + Mints `amount` of the token identified by `domain` to this + contract itself, claiming the coin in the same call. Forwards to + `NativeShieldedTokenCore._mint` with `right(kernel.self())`. - The consumer should persist the returned coin info in its own ledger state: it is the contract's holding and - is not otherwise recoverable. Spend it later with `_burnFromSelf`, once it carries a Merkle-tree index. + Nonce uniqueness is the caller's responsibility, as in `_mint`. Spend + the coin later with `_burnFromSelf`, once it carries an `mt_index`. - Puts `domain`, the coin color, the amount, and the coin commitment on the ledger. + The consumer SHOULD persist the returned coin info in its own + ledger state: it is the contract's holding and is not otherwise + recoverable. Requirements: - * Contract is initialized. + - Contract is initialized. #### _burn [toc] [#NativeShieldedTokenFamily-_burn] @@ -544,24 +553,30 @@ None. modulePrefix="NativeShieldedTokenFamily" stackParams="true" > - Burns `amount` from `coin`, a coin of the `domain` token type provided within the current transaction - (for example paid in by the caller's wallet), and routes the remaining change to `refundTo`. + Burns `amount` from `coin`, a coin provided within the + current transaction (e.g. paid in by the caller's wallet), and routes the + remaining change to `refundTo`. - Returns the refund coin created for `refundTo` as `some`, or `none` if the coin was burned in full. - To burn the coin in full, pass `coin.value` as `amount`; `refundTo` is inert in that case but must still be - supplied and non-zero. For coins the contract already holds, use `_burnFromSelf` instead. + To destroy `coin` in full, pass `coin.value` as `amount`; the change + branch will not fire and `none` is returned. The `refundTo` value is + inert in that case but must still be supplied and non-zero. - The returned refund coin info is the only copy available to `refundTo`; callers should deliver it out of band. + The coin is received by the contract and spent in the same + transaction, so the spend goes through `sendImmediateShielded` (the + transient path). For coins the contract already holds, use + `_burnFromSelf` instead. - Puts `domain`, the spent coin's nullifier, and the output commitments on the ledger. The amount is not posted; - a change output reveals that the burn was partial. + The returned refund coin info is the only copy available to + `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect + contract-sent coins by scanning the chain. Requirements: - * Contract is initialized. - * `coin.color` is this contract's token type for `domain`. - * `amount` is less than or equal to `coin.value`. - * `refundTo` is not the zero key (the zero key is the burn address; a zero `refundTo` would burn the change as well). + - Contract is initialized. + - `coin.color` is this contract's token type for `domain`. + - `amount` is less than or equal to `coin.value`. + - `refundTo` is not zero (the zero key is the burn address; a zero + `refundTo` would silently burn the change as well). #### _burnFromSelf [toc] [#NativeShieldedTokenFamily-_burnFromSelf] @@ -577,20 +592,19 @@ None. modulePrefix="NativeShieldedTokenFamily" stackParams="true" > - Burns `amount` from `coin`, a coin of the `domain` token type that this contract already holds - (a Merkle-tree entry with a valid `mt_index`). Any change is received by the contract automatically and - returned as `some`; `none` means the coin was burned in full. + Burns `amount` from `coin`, a coin this contract already + holds (a Merkle-tree entry with a valid `mt_index`). Any change is + auto-received by the contract and returned. - The consumer should persist the returned change coin info in its own ledger state: the change replaces `coin` - as the contract's holding, and its info is not otherwise recoverable. - - Puts `domain`, the spent coin's nullifier, and the output commitments on the ledger. The amount is not posted. + The consumer SHOULD persist the returned change coin info in its own + ledger state: the change replaces `coin` as the contract's holding, and + its info is not otherwise recoverable. Requirements: - * Contract is initialized. - * `coin.color` is this contract's token type for `domain`. - * `amount` is less than or equal to `coin.value`. + - Contract is initialized. + - `coin.color` is this contract's token type for `domain`. + - `amount` is less than or equal to `coin.value`. --- @@ -607,11 +621,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedToken --- -The shared, domain-parameterized core behind `NativeShieldedToken` and `NativeShieldedTokenFamily`. -It is not meant to be composed directly: compose one of the two flavors instead. -Every token operation takes the `domain` separator as a parameter, and the assertion messages use the generic -`NativeShieldedToken:` prefix. - ### Ledger [toc] [#NativeShieldedTokenCore-Ledger] ### Ledger [!toc] [#NativeShieldedTokenCore-Ledger] @@ -622,7 +631,9 @@ Every token operation takes the `domain` separator as a parameter, and the asser kind="ledger" id="NativeShieldedTokenCore-_isInitialized" > - Boolean indicating if initialized. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. #### _name [toc] [#NativeShieldedTokenCore-_name] @@ -676,15 +687,9 @@ None. stackParams="true" > Initializes the contract-wide metadata. - - This MUST be called in the implementing contract's constructor, through the wrapping flavor's `initialize`. - Failure to do so can lead to an irreparable contract. - - Discloses `name_`, `symbol_`, and `decimals_` to the ledger. - - Requirements: - - * Contract is not initialized. + This MUST be called in the implementing contract's constructor (via + the wrapping flavor's `initialize`). Failure to do so can lead to an + irreparable contract. #### assertInitialized [toc] [#NativeShieldedTokenCore-assertInitialized] @@ -694,11 +699,12 @@ None. kind="circuit" modulePrefix="NativeShieldedTokenCore" > - Asserts that the contract has been initialized, throwing an error if not. + Asserts that the contract has been initialized, throwing an + error if not. Requirements: - * Contract is initialized. + - Contract must be initialized. #### assertNotInitialized [toc] [#NativeShieldedTokenCore-assertNotInitialized] @@ -708,11 +714,12 @@ None. kind="circuit" modulePrefix="NativeShieldedTokenCore" > - Asserts that the contract has not been initialized, throwing an error if it has. + Asserts that the contract has not been initialized, throwing + an error if it has. Requirements: - * Contract is not initialized. + - Contract must not be initialized. #### isInitialized [toc] [#NativeShieldedTokenCore-isInitialized] @@ -723,7 +730,9 @@ None. complexity="k=6, rows=26" modulePrefix="NativeShieldedTokenCore" > - Returns whether the contract has been initialized. Safe to call before `initialize`. + Returns whether the contract has been initialized. + Does not gate on init (it reports it), so it is safe to call before + `initialize`. #### name [toc] [#NativeShieldedTokenCore-name] @@ -738,7 +747,7 @@ None. Requirements: - * Contract is initialized. + - Contract is initialized. #### symbol [toc] [#NativeShieldedTokenCore-symbol] @@ -749,11 +758,11 @@ None. complexity="k=6, rows=28" modulePrefix="NativeShieldedTokenCore" > - Returns the token symbol. + Returns the symbol of the token. Requirements: - * Contract is initialized. + - Contract is initialized. #### decimals [toc] [#NativeShieldedTokenCore-decimals] @@ -764,12 +773,13 @@ None. complexity="k=6, rows=28" modulePrefix="NativeShieldedTokenCore" > - Returns the number of decimals used to get the user representation. - This is a display convention only; the protocol operates on integer values. + Returns the number of decimals used to get its user representation. + This is a display convention only. The protocol stores and operates + on integer values without reference to decimals. Requirements: - * Contract is initialized. + - Contract is initialized. #### tokenColor [toc] [#NativeShieldedTokenCore-tokenColor] @@ -780,14 +790,12 @@ None. complexity="k=13, rows=4248" modulePrefix="NativeShieldedTokenCore" > - Returns the coin color for `domain` under this contract's address, `tokenType(domain, kernel.self())`, - computed at call time. - - Discloses `domain` to the ledger. + Returns the coin color for `domain` under this contract's + address: `tokenType(domain, kernel.self())`, computed at call time. Requirements: - * Contract is initialized. + - Contract is initialized. #### _mint [toc] [#NativeShieldedTokenCore-_mint] @@ -804,25 +812,25 @@ None. modulePrefix="NativeShieldedTokenCore" stackParams="true" > - Mints `amount` of the token identified by `domain` to `recipient` using the caller-supplied `nonce`, - and returns the new coin's info. + Mints `amount` of the token identified by `domain` to + `recipient`, using a caller-supplied nonce. - `recipient` may be any coin public key, or this contract's own address. A contract-addressed coin is - claimed with `receiveShielded` in the same call, so the consumer should persist the returned coin info: - it is the contract's holding and is not otherwise recoverable. Any other contract address is rejected. + The caller is fully responsible for nonce uniqueness. Reusing a + nonce for the same (domain, value, recipient) produces a duplicate + commitment, which the protocol rejects. A `right(kernel.self())` recipient + is claimed via `receiveShielded` in this same call, so the consumer SHOULD + persist the returned coin info: it is the contract's holding and is not + otherwise recoverable. - The caller is responsible for nonce uniqueness: reusing a nonce for the same (domain, value, recipient) - produces a duplicate commitment, which the ledger rejects. The returned coin info is the only copy available - to the recipient; wallets cannot detect contract-minted coins by scanning the chain. - - Puts `domain`, the coin color, the amount, and the coin commitment on the ledger, and discloses whether the - recipient is a contract address. + The returned coin info is the only copy available to the + recipient; callers SHOULD deliver it out of band. Wallets cannot detect + contract-minted coins by scanning the chain. Requirements: - * Contract is initialized. - * `recipient` is not zero. - * `recipient`, if a contract address, is this contract's own address. + - Contract is initialized. + - `recipient` is not zero. + - `recipient`, if a contract address, is this contract's own address. #### _burn [toc] [#NativeShieldedTokenCore-_burn] @@ -839,27 +847,32 @@ None. modulePrefix="NativeShieldedTokenCore" stackParams="true" > - Burns `amount` from `coin`, a coin of the `domain` token type provided within the current transaction, - and routes the remaining change to `refundTo`. + Burns `amount` from `coin`, a coin provided within the + current transaction (e.g. paid in by the caller's wallet), and routes the + remaining change to `refundTo`. - Returns the refund coin created for `refundTo` as `some`, or `none` if the coin was burned in full. - To burn the coin in full, pass `coin.value` as `amount`; `refundTo` is inert in that case but must still be - supplied and non-zero. `refundTo` may be any coin public key, or this contract's own address, in which case - the refund is claimed with `receiveShielded` in the same call. For coins the contract already holds, - use `_burnFromSelf` instead. + To destroy `coin` in full, pass `coin.value` as `amount`; the change + branch will not fire and `none` is returned. The `refundTo` value is + inert in that case but must still be supplied and non-zero. - The returned refund coin info is the only copy available to `refundTo`; callers should deliver it out of band. + The coin is received by the contract and spent in the same + transaction, so the spend goes through `sendImmediateShielded` (the + transient path). For coins the contract already holds, use + `_burnFromSelf` instead. A `right(kernel.self())` refund is claimed via + `receiveShielded` in this same call. - Puts `domain`, the spent coin's nullifier, and the output commitments on the ledger, and discloses whether a - change output exists and whether `refundTo` is a contract address. The amount is not posted. + The returned refund coin info is the only copy available to + `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect + contract-sent coins by scanning the chain. Requirements: - * Contract is initialized. - * `coin.color` is this contract's token type for `domain`. - * `amount` is less than or equal to `coin.value`. - * `refundTo` is not zero (the zero key is the burn address; a zero `refundTo` would burn the change as well). - * `refundTo`, if a contract address, is this contract's own address. + - Contract is initialized. + - `coin.color` is this contract's token type for `domain`. + - `amount` is less than or equal to `coin.value`. + - `refundTo` is not zero (the zero key is the burn address; a zero + `refundTo` would silently burn the change as well). + - `refundTo`, if a contract address, is this contract's own address. #### _burnFromSelf [toc] [#NativeShieldedTokenCore-_burnFromSelf] @@ -875,20 +888,19 @@ None. modulePrefix="NativeShieldedTokenCore" stackParams="true" > - Burns `amount` from `coin`, a coin of the `domain` token type that this contract already holds - (a Merkle-tree entry with a valid `mt_index`). Any change is received by the contract automatically and - returned as `some`; `none` means the coin was burned in full. - - The consumer should persist the returned change coin info in its own ledger state: the change replaces `coin` - as the contract's holding, and its info is not otherwise recoverable. + Burns `amount` from `coin`, a coin this contract already + holds (a Merkle-tree entry with a valid `mt_index`). Any change is + auto-received by the contract and returned. - Puts `domain`, the spent coin's nullifier, and the output commitments on the ledger. The amount is not posted. + The consumer SHOULD persist the returned change coin info in its own + ledger state: the change replaces `coin` as the contract's holding, and + its info is not otherwise recoverable. Requirements: - * Contract is initialized. - * `coin.color` is this contract's token type for `domain`. - * `amount` is less than or equal to `coin.value`. + - Contract is initialized. + - `coin.color` is this contract's token type for `domain`. + - `amount` is less than or equal to `coin.value`. --- @@ -905,14 +917,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeSh --- -Optional extension that adds on-chain supply accounting to a `NativeShieldedToken`. -It imports no token module and needs no initialization: the consuming contract calls `_addMinted` alongside -every `_mint` / `_mintToSelf` and `_addBurned` alongside every `_burn` / `_burnFromSelf`. -All accounting lives under a single fixed key in the core maps. - -Composing this extension makes contract-mediated burn amounts public; mint amounts are already public through -the protocol's shielded-mint effect. - ### Ledger [toc] [#NativeShieldedTokenPublicSupply-Ledger] ### Ledger [!toc] [#NativeShieldedTokenPublicSupply-Ledger] @@ -923,8 +927,7 @@ the protocol's shielded-mint effect. kind="ledger" id="NativeShieldedTokenPublicSupply-_totalMinted" > - Exact amount minted, stored under the single fixed key `default>`. - Re-exported from `NativeShieldedTokenPublicSupplyCore`. + Exact amount minted per domain. See "Accounting guarantees". #### _totalBurned [toc] [#NativeShieldedTokenPublicSupply-_totalBurned] @@ -934,8 +937,7 @@ the protocol's shielded-mint effect. kind="ledger" id="NativeShieldedTokenPublicSupply-_totalBurned" > - Contract-mediated amount burned (a lower bound), stored under the single fixed key `default>`. - Re-exported from `NativeShieldedTokenPublicSupplyCore`. + Contract-mediated amount burned per domain (lower bound). ### Witnesses [toc] [#NativeShieldedTokenPublicSupply-Witnesses] @@ -954,13 +956,8 @@ None. complexity="k=9, rows=375" modulePrefix="NativeShieldedTokenPublicSupply" > - Adds `amount` to the exact minted total. Call once per successful `NativeShieldedToken._mint` or `_mintToSelf`. - - Puts the new minted total on the ledger. - - Requirements: - - * The minted total plus `amount` does not overflow `Uint<128>`. + Adds `amount` to the exact minted total. Call once per + successful `NativeShieldedToken._mint`. #### _addBurned [toc] [#NativeShieldedTokenPublicSupply-_addBurned] @@ -971,15 +968,9 @@ None. complexity="k=9, rows=477" modulePrefix="NativeShieldedTokenPublicSupply" > - Adds `amount` to the contract-mediated burned total. Call once per successful `NativeShieldedToken._burn` - or `_burnFromSelf`. Enforces `burned <= minted`, so an unpaired or duplicate burn reverts. - - Puts the new burned total on the ledger, which makes the burned amount public. - - Requirements: - - * The burned total does not already exceed the minted total. - * The burned total plus `amount` does not exceed the minted total. + Adds `amount` to the contract-mediated burned total. Call once + per successful `NativeShieldedToken._burn` / `_burnFromSelf`. + Enforces `burned <= minted`; an unpaired or duplicate burn reverts. #### totalMinted [toc] [#NativeShieldedTokenPublicSupply-totalMinted] @@ -990,7 +981,7 @@ None. complexity="k=7, rows=65" modulePrefix="NativeShieldedTokenPublicSupply" > - Returns the exact amount ever minted. Reads as 0 before the first `_addMinted`. + Returns the exact amount ever minted. #### totalBurned [toc] [#NativeShieldedTokenPublicSupply-totalBurned] @@ -1001,7 +992,8 @@ None. complexity="k=7, rows=62" modulePrefix="NativeShieldedTokenPublicSupply" > - Returns the contract-mediated amount burned. This is a lower bound: coins sent directly to the burn address + Returns the contract-mediated amount burned. + This is a lower bound: coins sent directly to the burn address without going through the contract are not counted. @@ -1013,8 +1005,9 @@ None. complexity="k=9, rows=160" modulePrefix="NativeShieldedTokenPublicSupply" > - Returns `totalMinted() - totalBurned()`. This is an upper bound on circulating supply, not an exact value: - burns that bypass the contract are invisible. + Returns `totalMinted() - totalBurned()`. + This is an UPPER BOUND on circulating supply, not an exact value: + burns that bypass the contract are invisible. See the module notes. --- @@ -1031,13 +1024,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeSh --- -Optional extension that adds per-domain on-chain supply accounting to a `NativeShieldedTokenFamily`. -It imports no token module and needs no initialization: the consuming contract calls `_addMinted` alongside -every `_mint` / `_mintToSelf` and `_addBurned` alongside every `_burn` / `_burnFromSelf`, under the matching domain. - -Composing this extension makes contract-mediated burn amounts public; mint amounts are already public through -the protocol's shielded-mint effect. - ### Ledger [toc] [#NativeShieldedTokenFamilyPublicSupply-Ledger] ### Ledger [!toc] [#NativeShieldedTokenFamilyPublicSupply-Ledger] @@ -1048,7 +1034,7 @@ the protocol's shielded-mint effect. kind="ledger" id="NativeShieldedTokenFamilyPublicSupply-_totalMinted" > - Exact amount minted per domain. Re-exported from `NativeShieldedTokenPublicSupplyCore`. + Exact amount minted per domain. See "Accounting guarantees". #### _totalBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-_totalBurned] @@ -1058,7 +1044,7 @@ the protocol's shielded-mint effect. kind="ledger" id="NativeShieldedTokenFamilyPublicSupply-_totalBurned" > - Contract-mediated amount burned per domain (a lower bound). Re-exported from `NativeShieldedTokenPublicSupplyCore`. + Contract-mediated amount burned per domain (lower bound). ### Witnesses [toc] [#NativeShieldedTokenFamilyPublicSupply-Witnesses] @@ -1077,14 +1063,8 @@ None. complexity="k=10, rows=653" modulePrefix="NativeShieldedTokenFamilyPublicSupply" > - Adds `amount` to the exact minted total for `domain`. Call once per successful - `NativeShieldedTokenFamily._mint` or `_mintToSelf`. - - Puts `domain` and its new minted total on the ledger. - - Requirements: - - * The minted total for `domain` plus `amount` does not overflow `Uint<128>`. + Adds `amount` to the exact minted total for `domain`. Call + once per successful `NativeShieldedTokenFamily._mint`. #### _addBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-_addBurned] @@ -1095,16 +1075,11 @@ None. complexity="k=10, rows=755" modulePrefix="NativeShieldedTokenFamilyPublicSupply" > - Adds `amount` to the contract-mediated burned total for `domain`. Call once per successful - `NativeShieldedTokenFamily._burn` or `_burnFromSelf`. Enforces per-domain `burned <= minted`, - so an unpaired, duplicate, or wrong-domain burn reverts. - - Puts `domain` and its new burned total on the ledger, which makes the burned amount public. - - Requirements: - - * The burned total for `domain` does not already exceed its minted total. - * The burned total for `domain` plus `amount` does not exceed its minted total. + Adds `amount` to the contract-mediated burned total for + `domain`. Call once per successful `NativeShieldedTokenFamily._burn` / + `_burnFromSelf`. + Enforces per-domain `burned <= minted`; an unpaired, duplicate, or + wrong-domain burn reverts. #### totalMinted [toc] [#NativeShieldedTokenFamilyPublicSupply-totalMinted] @@ -1115,9 +1090,7 @@ None. complexity="k=9, rows=344" modulePrefix="NativeShieldedTokenFamilyPublicSupply" > - Returns the exact amount ever minted for `domain`. An absent domain reads as 0. - - Discloses `domain` to the ledger. + Returns the exact amount ever minted for `domain`. #### totalBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-totalBurned] @@ -1128,10 +1101,9 @@ None. complexity="k=9, rows=341" modulePrefix="NativeShieldedTokenFamilyPublicSupply" > - Returns the contract-mediated amount burned for `domain`. This is a lower bound: coins sent directly to the - burn address without going through the contract are not counted. - - Discloses `domain` to the ledger. + Returns the contract-mediated amount burned for `domain`. + This is a lower bound: coins sent directly to the burn address + without going through the contract are not counted. #### totalSupply [toc] [#NativeShieldedTokenFamilyPublicSupply-totalSupply] @@ -1142,10 +1114,9 @@ None. complexity="k=9, rows=439" modulePrefix="NativeShieldedTokenFamilyPublicSupply" > - Returns `totalMinted(domain) - totalBurned(domain)`. This is an upper bound on circulating supply, - not an exact value: burns that bypass the contract are invisible. - - Discloses `domain` to the ledger. + Returns `totalMinted(domain) - totalBurned(domain)`. + This is an UPPER BOUND on circulating supply, not an exact value: + burns that bypass the contract are invisible. See the module notes. --- @@ -1162,11 +1133,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeSh --- -The shared, domain-keyed core behind `NativeShieldedTokenPublicSupply` and `NativeShieldedTokenFamilyPublicSupply`. -It is not meant to be composed directly: compose one of the two flavors instead. -It owns the per-domain minted and burned maps and needs no initialization; an absent domain reads as 0. -The assertion messages use the generic `NativeShieldedTokenPublicSupply:` prefix. - ### Ledger [toc] [#NativeShieldedTokenPublicSupplyCore-Ledger] ### Ledger [!toc] [#NativeShieldedTokenPublicSupplyCore-Ledger] @@ -1177,7 +1143,7 @@ The assertion messages use the generic `NativeShieldedTokenPublicSupply:` prefix kind="ledger" id="NativeShieldedTokenPublicSupplyCore-_totalMinted" > - Exact amount minted per domain. + Exact amount minted per domain. See "Accounting guarantees". #### _totalBurned [toc] [#NativeShieldedTokenPublicSupplyCore-_totalBurned] @@ -1187,7 +1153,7 @@ The assertion messages use the generic `NativeShieldedTokenPublicSupply:` prefix kind="ledger" id="NativeShieldedTokenPublicSupplyCore-_totalBurned" > - Contract-mediated amount burned per domain (a lower bound). + Contract-mediated amount burned per domain (lower bound). ### Witnesses [toc] [#NativeShieldedTokenPublicSupplyCore-Witnesses] @@ -1206,13 +1172,9 @@ None. complexity="k=10, rows=653" modulePrefix="NativeShieldedTokenPublicSupplyCore" > - Adds `amount` to the exact minted total for `domain`. Call once per successful token mint. - - Puts `domain` and its new minted total on the ledger. - - Requirements: - - * The minted total for `domain` plus `amount` does not overflow `Uint<128>`. + Adds `amount` to the exact minted total for `domain`. Call + once per successful token `_mint`. + Checks for overflow in order to output a readable error message. #### _addBurned [toc] [#NativeShieldedTokenPublicSupplyCore-_addBurned] @@ -1223,16 +1185,12 @@ None. complexity="k=10, rows=755" modulePrefix="NativeShieldedTokenPublicSupplyCore" > - Adds `amount` to the contract-mediated burned total for `domain`. Call once per successful token burn. - Enforces per-domain `burned <= minted`, so an unpaired, duplicate, or wrong-domain burn reverts rather than - pushing `totalSupply(domain)` negative. - - Puts `domain` and its new burned total on the ledger, which makes the burned amount public. - - Requirements: - - * The burned total for `domain` does not already exceed its minted total. - * The burned total for `domain` plus `amount` does not exceed its minted total. + Adds `amount` to the contract-mediated burned total for + `domain`. Call once per successful token `_burn` / `_burnFromSelf`. + Enforces the per-domain `burned <= minted` invariant: a wrong-domain, + duplicate, or unpaired burn reverts rather than corrupting `totalSupply`. + The guard also keeps the add and the `totalSupply` subtraction within + bounds, so no separate overflow/underflow check is needed. #### totalMinted [toc] [#NativeShieldedTokenPublicSupplyCore-totalMinted] @@ -1243,9 +1201,7 @@ None. complexity="k=9, rows=344" modulePrefix="NativeShieldedTokenPublicSupplyCore" > - Returns the exact amount ever minted for `domain`. An absent domain reads as 0. - - Discloses `domain` to the ledger. + Returns the exact amount ever minted for `domain`. #### totalBurned [toc] [#NativeShieldedTokenPublicSupplyCore-totalBurned] @@ -1256,10 +1212,9 @@ None. complexity="k=9, rows=341" modulePrefix="NativeShieldedTokenPublicSupplyCore" > - Returns the contract-mediated amount burned for `domain`. This is a lower bound: coins sent directly to the - burn address without going through the contract are not counted. - - Discloses `domain` to the ledger. + Returns the contract-mediated amount burned for `domain`. + This is a lower bound: coins sent directly to the burn address + without going through the contract are not counted. #### totalSupply [toc] [#NativeShieldedTokenPublicSupplyCore-totalSupply] @@ -1270,10 +1225,14 @@ None. complexity="k=9, rows=439" modulePrefix="NativeShieldedTokenPublicSupplyCore" > - Returns `totalMinted(domain) - totalBurned(domain)`, or 0 if the burned total somehow exceeds the minted total. - This is an upper bound on circulating supply, not an exact value: burns that bypass the contract are invisible. - - Discloses `domain` to the ledger. + Returns `totalMinted(domain) - totalBurned(domain)`, or 0 if + `totalBurned` somehow exceeds `totalMinted`. + This is an UPPER BOUND on circulating supply, not an exact value: + burns that bypass the contract are invisible. See the module notes. + The `burned <= minted` invariant enforced by `_addBurned` already makes + `burned > minted` unreachable through the accounting API, so the clamp never + fires in normal operation. It is kept so the getter is a total function that + reads cleanly (returning 0) rather than reverting on an out-of-bound state. --- @@ -1290,13 +1249,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeSh --- -Optional extension that derives coin nonces for native shielded token mints from a monotonic counter, -so callers need not manage nonces themselves. It imports no token module and needs no initialization, -and pairs with either `NativeShieldedToken` or `NativeShieldedTokenFamily`. - -Derived nonces are predictable from public state, so mints that use them are recipient-public. -For recipient privacy, call the token's `_mint` with a secret, cryptographically random nonce instead. - ### Ledger [toc] [#NativeShieldedTokenDerivedNonce-Ledger] ### Ledger [!toc] [#NativeShieldedTokenDerivedNonce-Ledger] @@ -1307,8 +1259,7 @@ For recipient privacy, call the token's `_mint` with a secret, cryptographically kind="ledger" id="NativeShieldedTokenDerivedNonce-_counter" > - Monotonic index feeding the nonce derivation. One counter serves the whole contract, so derived nonces never - repeat across token types. + Monotonic index feeding the derived-nonce derivation. ### Witnesses [toc] [#NativeShieldedTokenDerivedNonce-Witnesses] @@ -1328,14 +1279,10 @@ None. modulePrefix="NativeShieldedTokenDerivedNonce" > Advances the counter and returns the next derived coin nonce, - `evolveNonce(_counter, pad(32, "NativeShieldedToken:nonce"))`, for use as the `nonce` argument of a token mint. - Uniqueness comes from the counter; the fixed tag keeps these nonces separate from any other `evolveNonce` - use in the implementing contract. + for use as the `nonce` argument of a native token mint. - A reverted mint does not advance the counter, so retrying an identical mint after a collision collides again; - a mint with different inputs advances the counter. - - Puts the incremented counter on the ledger. + The derivation input is public ledger state, so mints using this + nonce are recipient-public; see the module notes. --- @@ -1352,17 +1299,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/presets/ZOwnableNat --- -A native shielded token with a single shielded owner. Only the owner mints (to a coin public key, with a -caller-supplied nonce) and burns (a coin the owner's wallet pays into the transaction). Minted amounts are -counted on-chain; burned amounts are not, so burns stay amount-private. - -The preset composes `ZOwnablePK` (the owner commitment), `NativeShieldedToken` (one token color), and -`NativeShieldedTokenPublicSupply` (the minted total). Implementing contracts call `initialize` from their -constructor and must not import the composed modules directly. - -Holders transfer through Zswap directly. There are no roles, freeze, seize, contract-held coins, -burned total, or total supply. - ### Ledger [toc] [#ZOwnableNativeShieldedToken-Ledger] ### Ledger [!toc] [#ZOwnableNativeShieldedToken-Ledger] @@ -1373,8 +1309,11 @@ burned total, or total supply. kind="ledger" id="ZOwnableNativeShieldedToken-_ownerCommitment" > - The current owner commitment, `H(id || instanceSalt || counter || pad(32, "ZOwnablePK:shield:"))`. - Zero means the contract is unowned. Re-exported from `ZOwnablePK`. + Stores the current hashed commitment representing the owner. + This commitment is derived from the public identifier (e.g., `SHA256(pk, nonce)`), + the `instanceSalt`, the transfer `counter`, and a domain separator. + + A commitment of `default>` (i.e., zero) indicates the contract is unowned. #### _counter [toc] [#ZOwnableNativeShieldedToken-_counter] @@ -1384,7 +1323,10 @@ burned total, or total supply. kind="ledger" id="ZOwnableNativeShieldedToken-_counter" > - The number of ownership changes. Re-exported from `ZOwnablePK`. + Internal transfer counter used to prevent commitment reuse. + + Increments by 1 on every successful ownership transfer. Combined with `id` and + `instanceSalt` to compute unique owner commitments over time. #### _instanceSalt [toc] [#ZOwnableNativeShieldedToken-_instanceSalt] @@ -1394,7 +1336,11 @@ burned total, or total supply. kind="sealed ledger" id="ZOwnableNativeShieldedToken-_instanceSalt" > - Per-deployment salt for the owner commitment. Re-exported from `ZOwnablePK`. + A per-instance value provided at initialization used to namespace + commitments for this contract instance. + + This salt prevents commitment collisions across contracts that might otherwise use + the same owner identifiers or domain parameters. It is immutable after initialization. #### _ownableIsInitialized [toc] [#ZOwnableNativeShieldedToken-_ownableIsInitialized] @@ -1404,8 +1350,9 @@ burned total, or total supply. kind="ledger" id="ZOwnableNativeShieldedToken-_ownableIsInitialized" > - Boolean indicating if the owner is initialized. This is `ZOwnablePK`'s `_isInitialized`, aliased because the - token module already owns `_isInitialized`. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. #### _domain [toc] [#ZOwnableNativeShieldedToken-_domain] @@ -1415,7 +1362,8 @@ burned total, or total supply. kind="sealed ledger" id="ZOwnableNativeShieldedToken-_domain" > - Domain separator of this contract's token color. Re-exported from `NativeShieldedToken`. + Domain separator fixed at construction; with the contract + address it determines this token's color. #### _name [toc] [#ZOwnableNativeShieldedToken-_name] @@ -1455,7 +1403,9 @@ burned total, or total supply. kind="ledger" id="ZOwnableNativeShieldedToken-_isInitialized" > - Boolean indicating if the token is initialized. Re-exported from `NativeShieldedToken`. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. #### _totalMinted [toc] [#ZOwnableNativeShieldedToken-_totalMinted] @@ -1465,8 +1415,7 @@ burned total, or total supply. kind="ledger" id="ZOwnableNativeShieldedToken-_totalMinted" > - Exact amount minted, under the single fixed key `default>`. - Re-exported from `NativeShieldedTokenPublicSupply`. + Exact amount minted per domain. See "Accounting guarantees". #### _totalBurned [toc] [#ZOwnableNativeShieldedToken-_totalBurned] @@ -1476,7 +1425,7 @@ burned total, or total supply. kind="ledger" id="ZOwnableNativeShieldedToken-_totalBurned" > - Never written by this preset, so it stays empty. Re-exported from `NativeShieldedTokenPublicSupply`. + Contract-mediated amount burned per domain (lower bound). ### Witnesses [toc] [#ZOwnableNativeShieldedToken-Witnesses] @@ -1507,20 +1456,21 @@ test-only and are not published. > Sets the owner and the token's domain and metadata. - Call from the implementing contract's constructor only. It writes sealed fields, so implementing contracts - must not expose it as a post-deploy circuit. + Call from the implementing contract's constructor only. It writes + sealed fields, so implementing contracts MUST NOT expose it as a + post-deploy circuit. - `ownerId` must be `_computeOwnerId(pk, secretNonce)` for the coin public key the owner's wallet will supply - as `ownPublicKey()`. The circuit cannot check this at deploy; any other value leaves the contract with no - usable owner and no recovery. - - Puts the owner commitment, `instanceSalt`, `tokenDomain`, `name_`, `symbol_`, and `decimals_` on the ledger. + + `ownerId` must be `_computeOwnerId(pk, secretNonce)` for the coin + public key the owner's wallet will supply as `ownPublicKey()`. The circuit + cannot check this at deploy; any other key leaves the contract with no + usable owner and no recovery. + Requirements: - * Contract is not initialized. - * `ownerId` is not zero. - * `instanceSalt` is not zero. + - Contract is not initialized. + - `ownerId` is not zero. #### mint [toc] [#ZOwnableNativeShieldedToken-mint] @@ -1536,16 +1486,13 @@ test-only and are not published. modulePrefix="ZOwnableNativeShieldedToken" stackParams="true" > - Mints `amount` to `recipient` and adds it to the minted total. Owner only. - `nonce` must be fresh random bytes per mint. Persist the returned coin: it is the only copy. - - Puts the coin color, the amount, the coin commitment, and the new minted total on the ledger. - The recipient and the nonce are not posted. + Mints `amount` to `recipient` and adds it to the minted total. + Owner only. Requirements: - * Caller is the owner. - * `recipient` is not the zero key. + - Caller is the owner. + - `recipient` is not zero. #### burn [toc] [#ZOwnableNativeShieldedToken-burn] @@ -1561,18 +1508,15 @@ test-only and are not published. modulePrefix="ZOwnableNativeShieldedToken" stackParams="true" > - Burns `amount` of `coin`, paid into the transaction by the caller's wallet, and refunds the rest to `refundTo`. - Owner only. Returns the refund coin, or `none` on a full burn. Persist the returned coin: it is the only copy. - - Puts the spent coin's nullifier and the output commitments on the ledger. The amount is not posted; - a change output reveals that the burn was partial. + Burns `amount` of `coin`, paid into the tx by the caller's + wallet, and refunds the rest to `refundTo`. Owner only. Requirements: - * Caller is the owner. - * `coin.color` is this token's color. - * `amount` is less than or equal to `coin.value`. - * `refundTo` is not the zero key. + - Caller is the owner. + - `coin.color` is this token's color. + - `amount` \<= `coin.value`. + - `refundTo` is not zero. #### transferOwnership [toc] [#ZOwnableNativeShieldedToken-transferOwnership] @@ -1583,18 +1527,19 @@ test-only and are not published. complexity="k=15, rows=17096" modulePrefix="ZOwnableNativeShieldedToken" > - Transfers ownership to `newOwnerId` in one step. Owner only. - - `newOwnerId` must be `_computeOwnerId(pk, secretNonce)` for the coin public key the new owner's wallet will - supply as `ownPublicKey()`. The circuit cannot check this; any other value leaves the contract with no usable - owner and no recovery. + Transfers ownership to `newOwnerId`. Owner only. - Puts the new owner commitment and the incremented counter on the ledger. + + `newOwnerId` must be `_computeOwnerId(pk, secretNonce)` for the + coin public key the new owner's wallet will supply as `ownPublicKey()`. + The circuit cannot check this; any other key leaves the contract with no + usable owner and no recovery. + Requirements: - * Caller is the owner. - * `newOwnerId` is not zero. + - Caller is the owner. + - `newOwnerId` is not zero. #### renounceOwnership [toc] [#ZOwnableNativeShieldedToken-renounceOwnership] @@ -1605,13 +1550,15 @@ test-only and are not published. complexity="k=14, rows=10622" modulePrefix="ZOwnableNativeShieldedToken" > - Clears the owner. Owner only. Irreversible: `mint` and `burn` stop forever. + Clears the owner. Owner only. - Puts the zero owner commitment on the ledger. + + Irreversible: mint and burn stop forever. + Requirements: - * Caller is the owner. + - Caller is the owner. #### owner [toc] [#ZOwnableNativeShieldedToken-owner] @@ -1626,7 +1573,7 @@ test-only and are not published. Requirements: - * Contract is initialized. + - Contract is initialized. #### _computeOwnerId [toc] [#ZOwnableNativeShieldedToken-_computeOwnerId] @@ -1636,12 +1583,11 @@ test-only and are not published. kind="pure" modulePrefix="ZOwnableNativeShieldedToken" > - Returns the owner id `H(pk || nonce)`. Compute it off-chain to produce the `ownerId` passed to `initialize` - and `transferOwnership`. + Returns the owner id `H(pk, nonce)`. Requirements: - * `pk` is a coin public key, not a contract address. + - `pk` is a coin public key. #### totalMinted [toc] [#ZOwnableNativeShieldedToken-totalMinted] @@ -1667,7 +1613,7 @@ test-only and are not published. Requirements: - * Contract is initialized. + - Contract is initialized. #### symbol [toc] [#ZOwnableNativeShieldedToken-symbol] @@ -1682,7 +1628,7 @@ test-only and are not published. Requirements: - * Contract is initialized. + - Contract is initialized. #### decimals [toc] [#ZOwnableNativeShieldedToken-decimals] @@ -1697,7 +1643,7 @@ test-only and are not published. Requirements: - * Contract is initialized. + - Contract is initialized. #### tokenColor [toc] [#ZOwnableNativeShieldedToken-tokenColor] @@ -1708,9 +1654,10 @@ test-only and are not published. complexity="k=13, rows=3971" modulePrefix="ZOwnableNativeShieldedToken" > - Returns this contract's token color, `tokenType(tokenDomain, kernel.self())`. + Returns this contract's token color, + `tokenType(tokenDomain, kernel.self())`. Requirements: - * Contract is initialized. + - Contract is initialized. diff --git a/content/contracts-compact/api/non-fungible-token.mdx b/content/contracts-compact/api/non-fungible-token.mdx index 649c595b..28a35aad 100644 --- a/content/contracts-compact/api/non-fungible-token.mdx +++ b/content/contracts-compact/api/non-fungible-token.mdx @@ -30,7 +30,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; kind="sealed ledger" id="NonFungibleToken-_name" > - The immutable token name. + / Public state #### _symbol [toc] [#NonFungibleToken-_symbol] @@ -50,8 +50,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; kind="ledger" id="NonFungibleToken-_isInitialized" > - Initialization flag, owned by this module. Set to `true` by [initialize](#NonFungibleToken-initialize); - every other circuit asserts it is set. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. #### _owners [toc] [#NonFungibleToken-_owners] @@ -115,7 +116,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; id="NonFungibleToken-wit_NonFungibleTokenSK" > Returns the caller's secret key used in deriving the account identifier. - The key stays private; only `persistentHash(secretKey)` reaches the ledger. The same key produces the same account identifier across all contracts. Users who desire cross-contract unlinkability should use different keys per contract. @@ -132,14 +132,14 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; complexity="k=6, rows=52" modulePrefix="NonFungibleToken" > - Initializes the contract by setting the name and symbol, and marks the module initialized. + Initializes the contract by setting the name and symbol. This MUST be called in the implementing contract's constructor. Failure to do so can lead to an irreparable contract. Requirements: - * Contract is not initialized. + - Contract is not initialized. #### balanceOf [toc] [#NonFungibleToken-balanceOf] @@ -154,7 +154,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * Contract is initialized. + - The contract is initialized. #### ownerOf [toc] [#NonFungibleToken-ownerOf] @@ -169,8 +169,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * The contract is initialized. - * The `tokenId` must exist. + - The contract is initialized. + - The `tokenId` must exist. #### name [toc] [#NonFungibleToken-name] @@ -185,7 +185,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * Contract is initialized. + - The contract is initialized. #### symbol [toc] [#NonFungibleToken-symbol] @@ -200,7 +200,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * Contract is initialized. + - The contract is initialized. #### tokenURI [toc] [#NonFungibleToken-tokenURI] @@ -211,20 +211,18 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; complexity="k=9, rows=326" modulePrefix="NonFungibleToken" > - Returns the token URI for the given `tokenId`. - Returns an empty string if a tokenURI does not exist. + Returns the token URI for the given `tokenId`. Returns the empty + string if a tokenURI does not exist. Requirements: - * The contract is initialized. - * The `tokenId` must exist. + - The contract is initialized. + - The `tokenId` must exist. - - Native strings and string operations aren't supported within the Compact language, - e.g. concatenating a base URI + token ID is not possible like in other NFT implementations. - Therefore, we propose the URI storage approach; whereby, NFTs may or may not have unique "base" URIs. - It's up to the implementation to decide on how to handle this. - + Native strings and string operations aren't supported within the Compact language, + e.g. concatenating a base URI + token ID is not possible like in other NFT implementations. + Therefore, we propose the URI storage approach; whereby, NFTs may or may not have unique "base" URIs. + It's up to the implementation to decide on how to handle this. #### _setTokenURI [toc] [#NonFungibleToken-_setTokenURI] @@ -239,8 +237,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * The contract is initialized. - * The `tokenId` must exist. + - The contract is initialized. + - The `tokenId` must exist. + + The URI for a given NFT is usually set when the NFT is minted. #### approve [toc] [#NonFungibleToken-approve] @@ -253,15 +253,17 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; > Gives permission to `to` to transfer `tokenId` token to another account. The approval is cleared when the token is transferred. - The caller's identity is derived from the `wit_NonFungibleTokenSK` witness as `persistentHash(secretKey)`. Only a single account can be approved at a time, so approving the zero value clears previous approvals. + In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK` + witness as `persistentHash(secretKey)`. + Requirements: - * The contract is initialized. - * The caller must either own the token or be an approved operator. - * `tokenId` must exist. + - The contract is initialized. + - The caller must either own the token or be an approved operator. + - `tokenId` must exist. #### getApproved [toc] [#NonFungibleToken-getApproved] @@ -276,8 +278,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * The contract is initialized. - * `tokenId` must exist. + - The contract is initialized. + - `tokenId` must exist. #### setApprovalForAll [toc] [#NonFungibleToken-setApprovalForAll] @@ -293,13 +295,15 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; stackParams="true" > Approve or remove `operator` as an operator for the caller. - Operators can call [transferFrom](#NonFungibleToken-transferFrom) for any token owned by the caller. - The caller's identity is derived from the `wit_NonFungibleTokenSK` witness as `persistentHash(secretKey)`. + Operators can call `transferFrom` for any token owned by the caller. + + In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK` + witness as `persistentHash(secretKey)`. Requirements: - * The contract is initialized. - * The `operator` cannot be zero. + - The contract is initialized. + - The `operator` cannot be zero. #### isApprovedForAll [toc] [#NonFungibleToken-isApprovedForAll] @@ -315,7 +319,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * The contract is initialized. + - The contract is initialized. #### transferFrom [toc] [#NonFungibleToken-transferFrom] @@ -328,21 +332,22 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; modulePrefix="NonFungibleToken" > Transfers `tokenId` token from `fromAddress` to `to`. - The caller's identity is derived from the `wit_NonFungibleTokenSK` witness as `persistentHash(secretKey)`. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - + Transfers to contract addresses are currently disallowed until contract-to-contract interactions + are supported in Compact. This restriction prevents assets from being inadvertently locked in contracts that cannot + currently handle token receipt. + + In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK` + witness as `persistentHash(secretKey)`. Requirements: - * The contract is initialized. - * `fromAddress` is not zero. - * `to` is not zero. - * `to` is not a ContractAddress. - * `tokenId` must exist and be owned by `fromAddress`. - * If the caller is not `fromAddress`, it must be approved to move this token by either [approve](#NonFungibleToken-approve) or [setApprovalForAll](#NonFungibleToken-setApprovalForAll). + - The contract is initialized. + - `fromAddress` is not zero. + - `to` is not zero. + - `to` is not a ContractAddress. + - `tokenId` token must be owned by `fromAddress`. + - If the caller is not `fromAddress`, it must be approved to move this token by either `approve` or `setApprovalForAll`. #### _unsafeTransferFrom [toc] [#NonFungibleToken-_unsafeTransferFrom] @@ -354,23 +359,24 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; stackParams="true" modulePrefix="NonFungibleToken" > - Unsafe variant of [transferFrom](#NonFungibleToken-transferFrom) which allows transfers to contract addresses. - The caller's identity is derived from the `wit_NonFungibleTokenSK` witness as `persistentHash(secretKey)`. - The circuit asserts that `tokenId` had an owner before the update, so a stray approval recorded for an unminted token cannot turn it into a mint. + Transfers `tokenId` token from `fromAddress` to `to`. It does NOT check if the recipient is a ContractAddress. + + In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK` + witness as `persistentHash(secretKey)`. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract calls + are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. Requirements: - * The contract is initialized. - * `fromAddress` is not zero. - * `to` is not zero. - * `tokenId` must exist and be owned by `fromAddress`. - * If the caller is not `fromAddress`, it must be approved to move this token by either [approve](#NonFungibleToken-approve) or [setApprovalForAll](#NonFungibleToken-setApprovalForAll). + - The contract is initialized. + - `fromAddress` is not zero. + - `to` is not zero. + - `tokenId` token must be owned by `fromAddress`. + - If the caller is not `fromAddress`, it must be approved to move this token by either `approve` or `setApprovalForAll`. #### _ownerOf [toc] [#NonFungibleToken-_ownerOf] @@ -381,11 +387,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; complexity="k=9, rows=308" modulePrefix="NonFungibleToken" > - Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist; returns the zero value instead. + Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist. Requirements: - * The contract is initialized. + - The contract is initialized. #### _getApproved [toc] [#NonFungibleToken-_getApproved] @@ -396,12 +402,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; complexity="k=9, rows=308" modulePrefix="NonFungibleToken" > - Returns the approved address for `tokenId`. - Returns the zero value if `tokenId` is not minted. + Returns the approved address for `tokenId`. Returns the zero value if `tokenId` is not minted. Requirements: - * The contract is initialized. + - The contract is initialized. #### _isAuthorized [toc] [#NonFungibleToken-_isAuthorized] @@ -413,15 +418,16 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; stackParams="true" modulePrefix="NonFungibleToken" > - Returns whether `spender` is allowed to manage `owner`'s tokens, - or `tokenId` in particular (ignoring whether it is owned by `owner`). + Returns whether `spender` is allowed to manage `owner`'s tokens, or `tokenId` in + particular (ignoring whether it is owned by `owner`). Requirements: - * The contract is initialized. + - The contract is initialized. - This function assumes that `owner` is the actual owner of `tokenId` and does not verify this assumption. + This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + assumption. @@ -438,34 +444,15 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * The contract is initialized. - * `spender` has approval from `owner` for `tokenId` OR `spender` has approval to manage all of `owner`'s assets. + - The contract is initialized. + - `spender` has approval from `owner` for `tokenId` OR `spender` has approval to manage all of `owner`'s assets. - This function assumes that `owner` is the actual owner of `tokenId` and does not verify this assumption. + This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + assumption. -#### _update [toc] [#NonFungibleToken-_update] -#### [!toc] [#NonFungibleToken-_update] - - Transfers `tokenId` from its current owner to `to`, - or alternatively mints (or burns) if the current owner (or `to`) is the zero address. - Clears any single-token approval on `tokenId` when it had an owner. - Returns the owner of the `tokenId` before the update. - - Requirements: - - * The contract is initialized. - * If `auth` is non-zero, then this function will check that `auth` is either the owner of the token, - or approved to operate on the token (by the owner). - - #### _mint [toc] [#NonFungibleToken-_mint] #### [!toc] [#NonFungibleToken-_mint] Mints `tokenId` and transfers it to `to`. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - - Requirements: - * The contract is initialized. - * `tokenId` must not exist. - * `to` is not zero. - * `to` is not a ContractAddress. + - The contract is initialized. + - `tokenId` must not exist. + - `to` is not zero. + - `to` is not a ContractAddress. #### _unsafeMint [toc] [#NonFungibleToken-_unsafeMint] @@ -497,19 +479,19 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; complexity="k=11, rows=1408" modulePrefix="NonFungibleToken" > - Unsafe variant of [_mint](#NonFungibleToken-_mint) which allows transfers to contract addresses. + Mints `tokenId` and transfers it to `to`. It does NOT check if the recipient is a ContractAddress. + + Requirements: + + - The contract is initialized. + - `tokenId` must not exist. + - `to` is not zero. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. - - Requirements: - - * Contract is initialized. - * `tokenId` must not exist. - * `to` is not zero. #### _burn [toc] [#NonFungibleToken-_burn] @@ -521,13 +503,13 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; modulePrefix="NonFungibleToken" > Destroys `tokenId`. - The approval and the token URI are cleared when the token is burned. + The approval is cleared when the token is burned. This circuit does not check if the sender is authorized to operate on the token. Requirements: - * The contract is initialized. - * `tokenId` must exist. + - The contract is initialized. + - `tokenId` must exist. #### _transfer [toc] [#NonFungibleToken-_transfer] @@ -539,20 +521,19 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; stackParams="true" modulePrefix="NonFungibleToken" > - Transfers `tokenId` from `fromAddress` to `to`. As opposed to [transferFrom](#NonFungibleToken-transferFrom), - this imposes no restrictions on the caller's identity. + Transfers `tokenId` from `fromAddress` to `to`. + As opposed to `transferFrom`, this imposes no restrictions on the caller's identity. - - Transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents assets from being inadvertently locked in contracts that cannot currently handle token receipt. - + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from being inadvertently + locked in contracts that cannot currently handle token receipt. Requirements: - * The contract is initialized. - * `to` is not zero. - * `to` is not a ContractAddress. - * `tokenId` must exist and be owned by `fromAddress`. + - The contract is initialized. + - `to` is not zero. + - `to` is not a ContractAddress. + - `tokenId` token must be owned by `fromAddress`. #### _unsafeTransfer [toc] [#NonFungibleToken-_unsafeTransfer] @@ -564,21 +545,21 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; stackParams="true" modulePrefix="NonFungibleToken" > - Unsafe variant of [_transfer](#NonFungibleToken-_transfer) which allows transfers to contract addresses. + Transfers `tokenId` from `fromAddress` to `to`. + As opposed to `_unsafeTransferFrom`, this imposes no restrictions on the caller's identity. + It does NOT check if the recipient is a ContractAddress. - Transfers `tokenId` from `fromAddress` to `to`. As opposed to [_unsafeTransferFrom](#NonFungibleToken-_unsafeTransferFrom), this imposes no restrictions on the caller's identity. It does NOT check if the recipient is a `ContractAddress`. + Requirements: + + - The contract is initialized. + - `to` is not zero. + - `tokenId` token must be owned by `fromAddress`. - Transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. - Tokens sent to a contract address may become irretrievable. + Transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Tokens sent to a contract address may become irretrievable. Once contract-to-contract calls are supported, this circuit may be deprecated. - - Requirements: - - * Contract is initialized. - * `to` is not zero. - * `tokenId` must exist and be owned by `fromAddress`. #### _approve [toc] [#NonFungibleToken-_approve] @@ -590,14 +571,13 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; stackParams="true" modulePrefix="NonFungibleToken" > - Approve `to` to operate on `tokenId`. The token must exist. - Equivalent to [_unsafeApprove](#NonFungibleToken-_unsafeApprove) with `isExistenceRequired` set to `true`. + Approve `to` to operate on `tokenId`. Requirements: - * The contract is initialized. - * `tokenId` must exist. - * If `auth` is non-zero, then this function will check that `auth` is either the owner of the token, + - The contract is initialized. + - `tokenId` must exist. + - If `auth` is non 0, then this function will check that `auth` is either the owner of the token, or approved to operate on the token (by the owner). @@ -615,21 +595,25 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; stackParams="true" modulePrefix="NonFungibleToken" > - Approve `to` to operate on `tokenId`, requiring the token to exist only when `isExistenceRequired` is `true`. - The owner is read only when the existence check runs or `auth` is non-zero. - Writes the canonicalized `to` (or the zero value) to `_tokenApprovals`; `isExistenceRequired` is disclosed. + Approve `to` to operate on `tokenId`, requiring the token to exist + only when `isExistenceRequired` is true. - Passing `false` skips the check that `tokenId` was minted. The transfer path relies on approvals never - existing for unminted tokens, so a planted approval can mint a token nobody authorized or outlive a later mint. - Pass `false` only where the owner has already been read, as the internal `_update` does when clearing an approval. + Passing false skips the check that `tokenId` was minted. Both + `_unsafeTransferFrom` and `_update` read that invariant back, so a planted approval + can mint a token nobody authorized or outlive a later mint. Pass false only where + the owner has already been read, as `_update` does. + Counterpart to Solidity's `_approve(to, tokenId, auth, bool emitEvent)`, where + the flag also suppresses the `Approval` event. Compact has no events, so it carries + the existence check alone. + Requirements: - * The contract is initialized. - * `tokenId` must exist, unless `isExistenceRequired` is `false` and `auth` is zero. - * If `auth` is non-zero, then this function will check that `auth` is either the owner of the token, + - The contract is initialized. + - `tokenId` must exist, unless `isExistenceRequired` is false and `auth` is zero. + - If `auth` is non 0, then this function will check that `auth` is either the owner of the token, or approved to operate on the token (by the owner). @@ -646,9 +630,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * The contract is initialized. - * `owner` is not zero. - * `operator` is not zero. + - The contract is initialized. + - `owner` is not zero. + - `operator` is not zero. #### _requireOwned [toc] [#NonFungibleToken-_requireOwned] @@ -664,22 +648,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - * The contract is initialized. - * `tokenId` must exist. + - The contract is initialized. + - `tokenId` must exist. -#### _computeAccountId [toc] [#NonFungibleToken-_computeAccountId] -#### [!toc] [#NonFungibleToken-_computeAccountId] - - Computes the caller's account identifier from the `wit_NonFungibleTokenSK` witness - by delegating to [Utils computeAccountId](./utils#Utils-computeAccountId). - - ID Derivation: `accountId = persistentHash(secretKey)` - - The result is a 32-byte commitment that uniquely identifies the caller. - The same derivation is available off-chain through `Utils_computeAccountId`. - diff --git a/content/contracts-compact/api/ownable.mdx b/content/contracts-compact/api/ownable.mdx index 63704e85..0dc27821 100644 --- a/content/contracts-compact/api/ownable.mdx +++ b/content/contracts-compact/api/ownable.mdx @@ -35,8 +35,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; kind="ledger" id="Ownable-_isInitialized" > - Boolean indicating whether [initialize](#Ownable-initialize) has been called. - Every other circuit in the module requires it to be `true`. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. #### _owner [toc] [#Ownable-_owner] @@ -64,8 +65,7 @@ The TypeScript witnesses in the repository are test-only and are not published w kind="witness" id="Ownable-wit_OwnableSK" > - Returns the caller's secret key used in deriving the account identifier `H(secretKey)`. - The key never reaches the ledger; only the derived identifier is compared against `_owner`. + Returns the caller's secret key used in deriving the account identifier. The same key produces the same account identifier across all contracts. Users who desire cross-contract unlinkability should use different keys per contract. @@ -85,13 +85,11 @@ The TypeScript witnesses in the repository are test-only and are not published w Initializes the contract by setting the `initialOwner`. This must be called in the contract's constructor. - Discloses: the canonicalized `initialOwner`, written to `_owner`. - Requirements: - * Contract is not already initialized. - * `initialOwner` is not a ContractAddress. - * `initialOwner` is not the zero address. + - Contract is not already initialized. + - `initialOwner` is not a ContractAddress. + - `initialOwner` is not the zero address. #### owner [toc] [#Ownable-owner] @@ -106,7 +104,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - * Contract is initialized. + - Contract is initialized. #### transferOwnership [toc] [#Ownable-transferOwnership] @@ -119,19 +117,16 @@ The TypeScript witnesses in the repository are test-only and are not published w > Transfers ownership of the contract to `newOwner`. - - Ownership transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents permanently disabling access to a circuit. - - - Discloses: the canonicalized `newOwner`, written to `_owner`. + Ownership transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. + This restriction prevents permanently disabling access to a circuit. Requirements: - * Contract is initialized. - * The caller is the current contract owner. - * `newOwner` is not a ContractAddress. - * `newOwner` is not the zero address. + - Contract is initialized. + - The caller is the current contract owner. + - `newOwner` is not a ContractAddress. + - `newOwner` is not the zero address. #### _unsafeTransferOwnership [toc] [#Ownable-_unsafeTransferOwnership] @@ -142,21 +137,20 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=13, rows=2956" modulePrefix="Ownable" > - Unsafe variant of [`transferOwnership`](#Ownable-transferOwnership). + Unsafe variant of `transferOwnership`. - Ownership transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. + Ownership transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Ownership privileges sent to a contract address may become uncallable. Once contract-to-contract calls are supported, this circuit may be deprecated. - Discloses: the canonicalized `newOwner`, written to `_owner`. - Requirements: - * Contract is initialized. - * The caller is the current contract owner. - * `newOwner` is not the zero address. + - Contract is initialized. + - The caller is the current contract owner. + - `newOwner` is not the zero address. #### renounceOwnership [toc] [#Ownable-renounceOwnership] @@ -167,14 +161,14 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=13, rows=2364" modulePrefix="Ownable" > - Leaves the contract without an owner by writing the zero `Bytes<32>` identifier to `_owner`. - It will not be possible to call [`assertOnlyOwner`](#Ownable-assertOnlyOwner) circuits anymore. + Leaves the contract without an owner. + It will not be possible to call `assertOnlyOwner` circuits anymore. Can only be called by the current owner. Requirements: - * Contract is initialized. - * The caller is the current contract owner. + - Contract is initialized. + - The caller is the current contract owner. #### assertOnlyOwner [toc] [#Ownable-assertOnlyOwner] @@ -187,15 +181,14 @@ The TypeScript witnesses in the repository are test-only and are not published w > Throws if called by any account other than the owner. Use this to restrict access of specific circuits to the owner. - The caller's identity is derived from the `wit_OwnableSK` witness as `H(secretKey)`, - the same derivation as [`Utils.computeAccountId`](./utils#Utils-computeAccountId), - and compared against `_owner`. + + In the case of an external (non-contract) caller, the caller’s identity is + derived from the `wit_OwnableSK` witness as `persistentHash(secretKey)`. Requirements: - * Contract is initialized. - * `_owner` holds a `Bytes<32>` account identifier (a contract-address owner cannot authenticate). - * The caller is the current contract owner. + - Contract is initialized. + - The caller is the current contract owner. #### _transferOwnership [toc] [#Ownable-_transferOwnership] @@ -206,19 +199,17 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=10, rows=600" modulePrefix="Ownable" > - Transfers ownership of the contract to a `newOwner` without enforcing permission checks on the caller. + Transfers ownership of the contract to `newOwner` without + enforcing permission checks on the caller. - - Ownership transfers to contract addresses are currently disallowed until contract-to-contract interactions are supported in Compact. - This restriction prevents circuits from being inadvertently locked in contracts. - - - Discloses: the canonicalized `newOwner`, written to `_owner`. + Ownership transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. + This restriction prevents circuits from being inadvertently locked in contracts. Requirements: - * Contract is initialized. - * `newOwner` is not a ContractAddress. + - Contract is initialized. + - `newOwner` is not a ContractAddress. #### _unsafeUncheckedTransferOwnership [toc] [#Ownable-_unsafeUncheckedTransferOwnership] @@ -229,20 +220,18 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=10, rows=597" modulePrefix="Ownable" > - Unsafe variant of [`_transferOwnership`](#Ownable-_transferOwnership). - This is the single write point for `_owner`: every ownership change flows through it. + Unsafe variant of `_transferOwnership`. - Ownership transfers to contract addresses are considered unsafe because contract-to-contract calls are not currently supported. + Ownership transfers to contract addresses are considered unsafe because contract-to-contract + calls are not currently supported. Ownership privileges sent to a contract address may become uncallable. Once contract-to-contract calls are supported, this circuit may be deprecated. - Discloses: the canonicalized `newOwner`, written to `_owner`. - Requirements: - * Contract is initialized. + - Contract is initialized. --- @@ -269,8 +258,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; kind="ledger" id="ZOwnablePK-_isInitialized" > - Boolean indicating whether [initialize](#ZOwnablePK-initialize) has been called. - Every other non-pure circuit in the module requires it to be `true`. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. #### _ownerCommitment [toc] [#ZOwnablePK-_ownerCommitment] @@ -281,10 +271,10 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; id="ZOwnablePK-_ownerCommitment" > Stores the current hashed commitment representing the owner. - This commitment is derived from the owner identifier `SHA256(pk || nonce)`, + This commitment is derived from the public identifier (e.g., `SHA256(pk, nonce)`), the `instanceSalt`, the transfer `counter`, and a domain separator. - A commitment of `default>` (i.e. zero) indicates the contract is unowned. + A commitment of `default>` (i.e., zero) indicates the contract is unowned. #### _counter [toc] [#ZOwnablePK-_counter] @@ -296,8 +286,8 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; > Internal transfer counter used to prevent commitment reuse. - Increments by 1 on every successful ownership transfer. - Combined with `id` and `instanceSalt` to compute unique owner commitments over time. + Increments by 1 on every successful ownership transfer. Combined with `id` and + `instanceSalt` to compute unique owner commitments over time. #### _instanceSalt [toc] [#ZOwnablePK-_instanceSalt] @@ -307,11 +297,11 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; kind="sealed ledger" id="ZOwnablePK-_instanceSalt" > - A per-instance value provided at initialization used to namespace commitments for this contract instance. + A per-instance value provided at initialization used to namespace + commitments for this contract instance. - This salt prevents commitment collisions across contracts that might otherwise use the same owner identifiers or domain parameters. - Use a fresh random value per deployment: a reused salt links the same owner across deployments. - It is immutable after initialization. + This salt prevents commitment collisions across contracts that might otherwise use + the same owner identifiers or domain parameters. It is immutable after initialization. ### Witnesses [toc] [#ZOwnablePK-Witnesses] @@ -329,15 +319,8 @@ The TypeScript witnesses in the repository are test-only and are not published w > A private per-user nonce used in deriving the shielded owner identifier. - Combined with the user's public key as `SHA256(pk || nonce)` to produce an obfuscated, + Combined with the user's public key as `SHA256(pk, nonce)` to produce an obfuscated, unlinkable identity commitment. Users are encouraged to rotate this value on ownership changes. - - - `ownPublicKey()` is itself a witness: it returns the caller's Zswap coin public key, and nothing proves the caller - holds the matching private key. The secret nonce is therefore the owner credential. Anyone holding the nonce and - the owner's public key can pass `assertOnlyOwner`. Generate it as 32 bytes from a CSPRNG and keep it private; - losing it prevents the owner from proving ownership or transferring it. - ### Circuits [toc] [#ZOwnablePK-Circuits] @@ -352,23 +335,19 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="ZOwnablePK" > Initializes the contract by setting the initial owner via `ownerId` - and storing the `instanceSalt` that acts as a privacy additive - for preventing duplicate commitments among other contracts implementing ZOwnablePK. + and storing the `instanceSalt` that acts as a privacy additive for preventing + duplicate commitments among other contracts implementing ZOwnablePK. The `ownerId` must be calculated prior to contract deployment using the SHA256 hashing algorithm. Using any other algorithm will result in a permanent loss of contract access. - See [_computeOwnerId](#ZOwnablePK-_computeOwnerId). - Discloses: `instanceSalt`, written to `_instanceSalt`, and the resulting owner commitment, written to `_ownerCommitment`. - The `ownerId` itself is not disclosed. - Requirements: - * Contract is not initialized. - * `ownerId` is not zero. - * `instanceSalt` is not zero. + - Contract is not initialized. + - `ownerId` is not zero. + - `instanceSalt` is not zero. #### owner [toc] [#ZOwnablePK-owner] @@ -380,11 +359,11 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="ZOwnablePK" > Returns the current commitment representing the contract owner. - The full commitment is `SHA256(SHA256(pk || nonce) || instanceSalt || counter || domain)`. + The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`. Requirements: - * Contract is initialized. + - Contract is initialized. #### transferOwnership [toc] [#ZOwnablePK-transferOwnership] @@ -398,13 +377,11 @@ The TypeScript witnesses in the repository are test-only and are not published w Transfers ownership to `newOwnerId`. `newOwnerId` must be precalculated and given to the current owner off chain. - Discloses: the new owner commitment, written to `_ownerCommitment`. The `newOwnerId` itself is not disclosed. - Requirements: - * Contract is initialized. - * Caller is the current owner. - * `newOwnerId` is not zero. + - Contract is initialized. + - Caller is the current owner. + - `newOwnerId` is not an empty array. #### renounceOwnership [toc] [#ZOwnablePK-renounceOwnership] @@ -415,14 +392,14 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=15, rows=24442" modulePrefix="ZOwnablePK" > - Leaves the contract without an owner by resetting `_ownerCommitment` to zero. + Leaves the contract without an owner. It will not be possible to call `assertOnlyOwner` circuits anymore. Can only be called by the current owner. Requirements: - * Contract is initialized. - * Caller is the current owner. + - Contract is initialized. + - Caller is the current owner. #### assertOnlyOwner [toc] [#ZOwnablePK-assertOnlyOwner] @@ -433,17 +410,15 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=15, rows=24437" modulePrefix="ZOwnablePK" > - Throws if called by any account whose id hash `SHA256(pk || nonce)` does not match + Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match the stored owner commitment. Use this to only allow the owner to call specific circuits. - The `pk` is read from `ownPublicKey()` and the `nonce` from `wit_secretNonce`; both are witness values. - Neither reaches the ledger: the check compares the recomputed commitment against `_ownerCommitment` inside the proof. - Requirements: - * Contract is initialized. - * Caller's id (`SHA256(pk || nonce)`) when used in `_computeOwnerCommitment` equals the stored `_ownerCommitment`. + - Contract is initialized. + - Caller's id (`SHA256(pk, nonce)`) when used in `_computeOwnerCommitment` equals + the stored `_ownerCommitment`, thus verifying themselves as the owner. #### _computeOwnerCommitment [toc] [#ZOwnablePK-_computeOwnerCommitment] @@ -456,18 +431,27 @@ The TypeScript witnesses in the repository are test-only and are not published w > Computes the owner commitment from the given `id` and `counter`. - The commitment derivation follows: - `commitment = SHA256(id || instanceSalt || counter || "ZOwnablePK:shield:")` + ## Owner ID (`id`) + The `id` is expected to be computed off-chain as: + `id = SHA256(pk, nonce)` + + - `pk`: The owner's public key. + - `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. + + ## Commitment Derivation + `commitment = SHA256(id, instanceSalt, counter, domain)` - Where: - - `id`: The unique identifier `SHA256(pk || nonce)`. - - `instanceSalt`: Per-deployment salt to prevent collisions. - - `counter`: Incremented with each transfer for uniqueness. Cast to `Field` then `Bytes<32>` for hashing. - - Domain separator: `"ZOwnablePK:shield:"` padded to 32 bytes. + - `id`: See above. + - `instanceSalt`: A unique per-deployment salt, stored during initialization. + This prevents commitment collisions across deployments. + - `counter`: Incremented with each ownership transfer, ensuring uniqueness + even with repeated `id` values. Cast to `Field` then `Bytes<32>` for hashing. + - `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent + hash collisions when extending the module or using similar commitment schemes. Requirements: - * Contract is initialized. + - Contract is initialized. #### _computeOwnerId [toc] [#ZOwnablePK-_computeOwnerId] @@ -477,17 +461,32 @@ The TypeScript witnesses in the repository are test-only and are not published w kind="pure" modulePrefix="ZOwnablePK" > - Computes the unique identifier (`id`) of the owner from their public key and a secret nonce. + Computes the unique identifier (`id`) of the owner from their + public key and a secret nonce. - The ID derivation follows: `id = SHA256(pk || nonce)` + ## ID Derivation + `id = SHA256(pk, nonce)` - - Currently only supports `ZswapCoinPublicKey`. Contract address owners are not yet supported. - + - `pk`: The public key of the caller. This is passed explicitly to allow + for off-chain derivation, testing, or scenarios where the caller is + different from the subject of the computation. + We recommend using an Air-Gapped Public Key. + - `nonce`: A secret nonce tied to the identity. The generation strategy is + left to the user, offering different security/convenience trade-offs. + + The result is a 32-byte commitment that uniquely identifies the owner. + This value is later used in owner commitment hashing, + and acts as a privacy-preserving alternative to a raw public key. + + This module allows ownership to be tied to an identity commitment derived + from a public key and secret nonce. + While typically used with user public keys, this mechanism may also + support contract addresses as identifiers in future contract-to-contract + interactions. Both are treated as 32-byte values (`Bytes<32>`). Requirements: - * `pk` is not a ContractAddress. + - `pk` is not a ContractAddress. #### _transferOwnership [toc] [#ZOwnablePK-_transferOwnership] @@ -498,13 +497,10 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=14, rows=14823" modulePrefix="ZOwnablePK" > - Transfers ownership to owner id `newOwnerId` without enforcing permission checks on the caller. - - This is an internal circuit that increments the counter and updates the owner commitment. - - Discloses: the new owner commitment, written to `_ownerCommitment`. The `newOwnerId` itself is not disclosed. + Transfers ownership to owner id `newOwnerId` without + enforcing permission checks on the caller. Requirements: - * Contract is initialized. + - Contract is initialized. diff --git a/content/contracts-compact/api/security.mdx b/content/contracts-compact/api/security.mdx index 981aa8ea..90d4bd73 100644 --- a/content/contracts-compact/api/security.mdx +++ b/content/contracts-compact/api/security.mdx @@ -68,7 +68,7 @@ None. Requirements: - * Contract must not be initialized. + - Contract must not be initialized. #### assertInitialized [toc] [#Initializable-assertInitialized] @@ -83,7 +83,7 @@ None. Requirements: - * Contract must be initialized. + - Contract must be initialized. #### assertNotInitialized [toc] [#Initializable-assertNotInitialized] @@ -98,7 +98,7 @@ None. Requirements: - * Contract must not be initialized. + - Contract must not be initialized. --- @@ -159,7 +159,7 @@ None. Requirements: - * Contract must be paused. + - Contract must be paused. #### assertNotPaused [toc] [#Pausable-assertNotPaused] @@ -174,7 +174,7 @@ None. Requirements: - * Contract must not be paused. + - Contract must not be paused. #### _pause [toc] [#Pausable-_pause] @@ -189,7 +189,7 @@ None. Requirements: - * Contract must not be paused. + - Contract must not be paused. #### _unpause [toc] [#Pausable-_unpause] @@ -204,7 +204,7 @@ None. Requirements: - * Contract must be paused. + - Contract must be paused. --- @@ -236,7 +236,7 @@ Membership is public and the approved set is publicly enumerable. kind="ledger" id="Allowlist-_allowed" > - Approved accounts. An absent account is not allowed. + Approved accounts. Absent means not allowed. ### Witnesses [toc] [#Allowlist-Witnesses] @@ -256,8 +256,6 @@ None. modulePrefix="Allowlist" > Returns whether `account` is currently allowed. - - Discloses `account` to the ledger. #### assertAllowed [toc] [#Allowlist-assertAllowed] @@ -268,13 +266,12 @@ None. complexity="k=9, rows=305" modulePrefix="Allowlist" > - Asserts that `account` is allowed. The implementing contract decides when this gate applies. - - Discloses `account` to the ledger. + Asserts that `account` is allowed. The composing contract + decides when this gate applies. Requirements: - * `account` is a member of the allowlist. + - `account` is a member of the allowlist. #### _allow [toc] [#Allowlist-_allow] @@ -285,9 +282,7 @@ None. complexity="k=9, rows=303" modulePrefix="Allowlist" > - Adds `account` to the allowlist. Ungated: the implementing contract is responsible for protecting this circuit. - - Discloses `account` to the ledger. + Adds `account` to the allowlist. #### _disallow [toc] [#Allowlist-_disallow] @@ -298,9 +293,7 @@ None. complexity="k=9, rows=303" modulePrefix="Allowlist" > - Removes `account` from the allowlist. Ungated: the implementing contract is responsible for protecting this circuit. - - Discloses `account` to the ledger. + Removes `account` from the allowlist. --- @@ -332,7 +325,7 @@ Membership is public and the blocked set is publicly enumerable. kind="ledger" id="Blocklist-_blocked" > - Blocked accounts. An absent account is not blocked. + Blocked accounts. Absent means not blocked. ### Witnesses [toc] [#Blocklist-Witnesses] @@ -352,8 +345,6 @@ None. modulePrefix="Blocklist" > Returns whether `account` is currently blocked. - - Discloses `account` to the ledger. #### assertNotBlocked [toc] [#Blocklist-assertNotBlocked] @@ -364,13 +355,12 @@ None. complexity="k=9, rows=308" modulePrefix="Blocklist" > - Asserts that `account` is not blocked. The implementing contract decides when this gate applies. - - Discloses `account` to the ledger. + Asserts that `account` is not blocked. The composing contract + decides when this gate applies. Requirements: - * `account` is not a member of the blocklist. + - `account` is not a member of the blocklist. #### _block [toc] [#Blocklist-_block] @@ -381,9 +371,7 @@ None. complexity="k=9, rows=303" modulePrefix="Blocklist" > - Adds `account` to the blocklist. Ungated: the implementing contract is responsible for protecting this circuit. - - Discloses `account` to the ledger. + Adds `account` to the blocklist. #### _unblock [toc] [#Blocklist-_unblock] @@ -394,7 +382,5 @@ None. complexity="k=9, rows=303" modulePrefix="Blocklist" > - Removes `account` from the blocklist. Ungated: the implementing contract is responsible for protecting this circuit. - - Discloses `account` to the ledger. + Removes `account` from the blocklist. diff --git a/content/contracts-compact/api/shielded-access-control.mdx b/content/contracts-compact/api/shielded-access-control.mdx index 378025a6..2bb52bf4 100644 --- a/content/contracts-compact/api/shielded-access-control.mdx +++ b/content/contracts-compact/api/shielded-access-control.mdx @@ -149,8 +149,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="ledger" id="ShieldedAccessControl-_isInitialized" > - Boolean indicating whether [initialize](#ShieldedAccessControl-initialize) has been called. - The role-management and role-proof circuits require it to be `true`. + Initialization flag, tracked per-module to avoid the compiler's + shared transitive-dependency state bug (LFDT-Minokawa/compact#270). See the + Initializable module for rationale. #### _operatorRoles [toc] [#ShieldedAccessControl-_operatorRoles] @@ -160,9 +161,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="ledger" id="ShieldedAccessControl-_operatorRoles" > - A Merkle tree of role commitments stored as `SHA256(role || accountId || instanceSalt || commitmentDomain)`. - Has a fixed capacity of 2^20 leaf slots, one per distinct `(role, accountId)` pairing ever granted. - Slots are not reclaimed on revocation. The tree reports occupancy via `isFull()` in circuits and `firstFree()` from TypeScript. + A Merkle tree of role commitments stored as SHA256(role | accountId | instanceSalt | commitmentDomain) + Role commitments are derived from a public role identifier (e.g., `persistentHash>(pad(32, "MY_ROLE")`), + an account identifier (e.g., `SHA256(secretKey, instanceSalt, accountIdDomain)`), the `instanceSalt`, and a domain separator. #### _adminRoles [toc] [#ShieldedAccessControl-_adminRoles] @@ -182,7 +183,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="ledger" id="ShieldedAccessControl-_roleCommitmentNullifiers" > - A set of nullifiers used to prove a role has been revoked. + A set of nullifiers used to prove a role has been revoked #### _grantedRoleNullifiers [toc] [#ShieldedAccessControl-_grantedRoleNullifiers] @@ -192,9 +193,9 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="ledger" id="ShieldedAccessControl-_grantedRoleNullifiers" > - Nullifiers of every `(role, accountId)` pairing ever granted. - Makes a repeated grant a no-op so one pairing can never consume more than one `_operatorRoles` slot, - and gates revocation so a nullifier can only be inserted into `_roleCommitmentNullifiers` for a granted pairing. + Nullifiers of every pairing ever granted. Makes a repeated grant a no-op so + one pairing can never consume more than one `_operatorRoles` slot, and gates revocation + so a nullifier can only exist for a granted pairing. #### _instanceSalt [toc] [#ShieldedAccessControl-_instanceSalt] @@ -204,12 +205,16 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont kind="sealed ledger" id="ShieldedAccessControl-_instanceSalt" > - A per-instance value provided at initialization used to namespace commitments for this contract instance. - - This salt prevents commitment collisions across contracts that might otherwise use the same identifiers or domain parameters. - It should be a cryptographically strong random value. It is immutable after initialization. - If two deployments share the same salt and a user reuses their secret key across both, - the resulting `accountId`, commitments, and nullifiers are identical in both contracts, which links the user across them. + A per-instance value provided at initialization used to namespace + commitments for this contract instance. + + This salt prevents commitment collisions across contracts that might otherwise use + the same identifiers or domain parameters. It should be a cryptographically strong random value. + If two deployments share the same instanceSalt and a user reuses their secretKey across both, + the resulting accountId and all role commitments will be identical in both contracts. + An observer who sees the same nullifier appear in both contracts' nullifier sets can conclude + that it's the same user. + It is immutable after initialization. ### Witnesses [toc] [#ShieldedAccessControl-Witnesses] @@ -225,9 +230,7 @@ The TypeScript witnesses in the repository are test-only and are not published w kind="witness" id="ShieldedAccessControl-wit_getRoleCommitmentPath" > - Returns a path to a role commitment in the `_operatorRoles` Merkle tree if one exists. - Otherwise, returns an invalid path. - The consumer's witness implementation builds this path from the exported `_operatorRoles` ledger state. + Returns a path to a role commitment in the `_operatorRoles` Merkle tree if one exists. Otherwise, returns an invalid path. #### wit_secretKey [toc] [#ShieldedAccessControl-wit_secretKey] @@ -238,7 +241,6 @@ The TypeScript witnesses in the repository are test-only and are not published w id="ShieldedAccessControl-wit_secretKey" > Returns the user's secret key used in deriving the shielded account identifier. - The key never reaches the ledger; it is hashed with `_instanceSalt` inside the proof. The same key can be used across multiple roles within a contract instance. If a role is revoked and re-granted, a new secret key must be generated to produce a new `accountId`. @@ -259,15 +261,12 @@ The TypeScript witnesses in the repository are test-only and are not published w The `instanceSalt` must be calculated prior to contract deployment using a cryptographically - secure random number generator (e.g. `crypto.getRandomValues()`) to maintain strong privacy guarantees. + secure random number generator e.g. crypto.getRandomValues() to maintain strong privacy guarantees - Discloses: `instanceSalt`, written to `_instanceSalt`. - Requirements: - * Contract is not initialized. - * `instanceSalt` must not be zero. + - Contract is not initialized. #### DEFAULT_ADMIN_ROLE [toc] [#ShieldedAccessControl-DEFAULT_ADMIN_ROLE] @@ -277,9 +276,11 @@ The TypeScript witnesses in the repository are test-only and are not published w kind="pure" modulePrefix="ShieldedAccessControl" > - The default admin role for all roles. Returns zero bytes (`default>`). - Only accounts with this role will be able to grant or revoke other roles - unless custom admin roles are created via [_setRoleAdmin](#ShieldedAccessControl-_setRoleAdmin). + The default admin role for all roles. Only accounts with this role will be able to grant or revoke other roles + unless custom admin roles are created. + + The Compact language does not support constant declarations, + so DEFAULT_ADMIN_ROLE is implemented as a circuit that returns a constant value by necessity. #### assertOnlyRole [toc] [#ShieldedAccessControl-assertOnlyRole] @@ -289,18 +290,19 @@ The TypeScript witnesses in the repository are test-only and are not published w kind="circuit" modulePrefix="ShieldedAccessControl" > - Reverts if the caller cannot provide a valid proof of ownership for `role`. - The caller's `accountId` is derived from the `wit_secretKey` witness and `_instanceSalt`; - the Merkle path comes from `wit_getRoleCommitmentPath`. - - Discloses: a Merkle tree path to a role commitment, a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. - Every call under the same role instance discloses the same nullifier, so an observer can link those calls over time. + Reverts if caller cannot provide a valid proof of ownership for `role`. Requirements: - * Contract is initialized. - * Caller must prove ownership of `role`. - * Caller must not provide a valid Merkle tree path for a different `(role, accountId)` pairing. + - caller must prove ownership of `role`. + - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + - Contract is initialized. + + Disclosures: + + - A Merkle tree path to a role commitment. + - A role commitment corresponding to a `(role, accountId)` pairing. + - A nullifier for the respective role commitment. #### canProveRole [toc] [#ShieldedAccessControl-canProveRole] @@ -310,17 +312,20 @@ The TypeScript witnesses in the repository are test-only and are not published w kind="circuit" modulePrefix="ShieldedAccessControl" > - Returns `true` if the caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed + Returns `true` if a caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed caller if the proving environment supplies an invalid Merkle path. This circuit will never return true for an unauthorized caller. - Discloses: a Merkle tree path to a role commitment, a role commitment corresponding to a `(role, accountId)` pairing, and a nullifier for the respective role commitment. - Every call under the same role instance discloses the same nullifier, so an observer can link those calls over time. - Requirements: - * Contract is initialized. - * Caller must not provide a valid Merkle tree path for a different `(role, accountId)` pairing. + - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + - Contract is initialized. + + Disclosures: + + - A Merkle tree path to a role commitment. + - A role commitment corresponding to a `(role, accountId)` pairing. + - A nullifier for the respective role commitment. #### grantRole [toc] [#ShieldedAccessControl-grantRole] @@ -332,18 +337,23 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="ShieldedAccessControl" > Grants `role` to `accountId` by inserting a role commitment unique to the - `(role, accountId)` pairing into the `_operatorRoles` Merkle tree and recording its nullifier in `_grantedRoleNullifiers`. - Granting an already active pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new + `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Granting an already active + pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new `accountId` must be generated to be re-authorized for a revoked `role`. - Discloses: a Merkle tree path to a role commitment, a role commitment (on the first grant only), a nullifier, and a role identifier. - Requirements: - * Contract is initialized. - * Caller must prove they are an admin for `role`. - * Caller must not provide a valid Merkle tree path for a different `(role, accountId)` pairing. - * The `(role, accountId)` pairing must not be already revoked. + - caller must prove they're an admin for `role`. + - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + - The `(role, accountId)` pairing must not be already revoked. + - Contract is initialized. + + Disclosures: + + - A Merkle tree path to a role commitment. + - A role commitment corresponding to a `(role, accountId)` pairing. + - A nullifier for the respective role commitment. + - A role identifier. #### revokeRole [toc] [#ShieldedAccessControl-revokeRole] @@ -358,15 +368,20 @@ The TypeScript witnesses in the repository are test-only and are not published w `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for `role`. - Discloses: a Merkle tree path to a role commitment, a role commitment, a nullifier, and a role identifier. - Requirements: - * Contract is initialized. - * Caller must prove they are an admin for `role`. - * Caller must not provide a valid Merkle tree path for a different `(role, accountId)` pairing. - * The `(role, accountId)` pairing must have been granted (reverts with `role was never granted` otherwise). - * The `(role, accountId)` pairing must not be already revoked. + - caller must prove they're an admin for `role`. + - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + - The `(role, accountId)` pairing must have been granted. + - The `(role, accountId)` pairing must not be already revoked. + - Contract is initialized. + + Disclosures: + + - A Merkle tree path to a role commitment. + - A role commitment corresponding to a `(role, accountId)` pairing. + - A nullifier for the respective role commitment. + - A role identifier. #### renounceRole [toc] [#ShieldedAccessControl-renounceRole] @@ -381,24 +396,26 @@ The TypeScript witnesses in the repository are test-only and are not published w `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for `role`. - Roles are often managed via [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole): - this circuit's purpose is to provide a mechanism for accounts to lose their privileges + Roles are often managed via `grantRole` and `revokeRole`: this circuit's + purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). Consumers with no need for self-revocation should not export this circuit. - Discloses: a nullifier for the respective role commitment. - Outside observers may be able to use timing and pattern analysis to weaken pseudonymity - guarantees if `renounceRole` is used in tandem with other on-chain actions. + guarantees if renounceRole is used in tandem with other on-chain actions. Requirements: - * Contract is initialized. - * `accountIdConfirmation` must equal the caller's computed `accountId`. - * The `(role, accountId)` pairing must have been granted (reverts with `role was never granted` otherwise). - * The `(role, accountId)` pairing must not be already revoked. + - The caller must provide a valid `accountId` for the role. + - The `(role, accountId)` pairing must have been granted. + - The `(role, accountId)` pairing must not be already revoked. + - Contract is initialized. + + Disclosures: + + - A nullifier for the respective role commitment. #### getRoleAdmin [toc] [#ShieldedAccessControl-getRoleAdmin] @@ -411,11 +428,13 @@ The TypeScript witnesses in the repository are test-only and are not published w Returns the admin role that controls `role`. Returns `DEFAULT_ADMIN_ROLE` for roles with no explicitly set admin. Since `DEFAULT_ADMIN_ROLE` is the zero byte array, there is no distinction between a nonexistent `role` and one whose admin is `DEFAULT_ADMIN_ROLE`. - See [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole). + See `grantRole` and `revokeRole`. + + To change a role's admin use `_setRoleAdmin`. - To change a role's admin use [_setRoleAdmin](#ShieldedAccessControl-_setRoleAdmin). + Disclosures: - Discloses: a role identifier. + - A role identifier. #### _grantRole [toc] [#ShieldedAccessControl-_grantRole] @@ -427,27 +446,31 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="ShieldedAccessControl" > Grants `role` to `accountId` by inserting a role commitment unique to the - `(role, accountId)` pairing into the `_operatorRoles` Merkle tree and recording its nullifier in `_grantedRoleNullifiers`. - Granting an already active pairing is a no-op. + `(role, accountId)` pairing into the `_operatorRoles` Merkle tree. Granting an already active + pairing is a no-op. Once revoked, a role cannot be re-granted to the same `accountId`. A new + `accountId` must be generated to be re-authorized for a revoked `role`. Internal circuit without access restriction. - Discloses: a role commitment corresponding to a `(role, accountId)` pairing (on the first grant only), and a nullifier for the respective role commitment. - Exposing this circuit directly in an implementing contract would allow anyone to grant roles without authorization. It must be wrapped with appropriate access control. - The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots, one per distinct pairing. - Slots are never reclaimed. Deployers should monitor slot consumption off-chain. + The `_operatorRoles` Merkle tree has a fixed capacity of 2^20 leaf slots, one per + distinct pairing. Deployers should monitor slot consumption off-chain. Requirements: - * Contract is initialized. - * The `(role, accountId)` pairing must not be already revoked. + - The `(role, accountId)` pairing must not be already revoked. + - Contract is initialized. + + Disclosures: + + - A role commitment corresponding to a `(role, accountId)` pairing. + - A nullifier for the respective role commitment. #### _revokeRole [toc] [#ShieldedAccessControl-_revokeRole] @@ -464,8 +487,6 @@ The TypeScript witnesses in the repository are test-only and are not published w Internal circuit without access restriction. - Discloses: a nullifier for the respective role commitment. - Exposing this circuit directly in an implementing contract would allow anyone to revoke roles without authorization. It must be wrapped with appropriate access control. @@ -473,9 +494,13 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - * Contract is initialized. - * The `(role, accountId)` pairing must have been granted (reverts with `role was never granted` otherwise). - * The `(role, accountId)` pairing must not be already revoked. + - The `(role, accountId)` pairing must have been granted. + - The `(role, accountId)` pairing must not be already revoked. + - Contract is initialized. + + Disclosures: + + - A nullifier for the respective role commitment. #### _setRoleAdmin [toc] [#ShieldedAccessControl-_setRoleAdmin] @@ -488,16 +513,15 @@ The TypeScript witnesses in the repository are test-only and are not published w Sets `adminId` as `role`'s admin identifier. Users with valid admin identifiers may grant and revoke access to the specified `role`. Internal circuit without access restriction. - Discloses: the role identifier and the admin identifier, written to `_adminRoles`. - Exposing this circuit directly in an implementing contract would allow anyone to assign arbitrary admin roles without authorization. It must be wrapped with appropriate access control. - - Requirements: + Disclosures: - * Contract is initialized. + - The role identifier + - The admin identifier + #### computeRoleCommitment [toc] [#ShieldedAccessControl-computeRoleCommitment] @@ -508,14 +532,28 @@ The TypeScript witnesses in the repository are test-only and are not published w stackParams="true" modulePrefix="ShieldedAccessControl" > - Computes the role commitment from the given `role` and `accountId` using the stored `_instanceSalt`. - - Role Commitment Derivation: `roleCommitment = SHA256(role || accountId || instanceSalt || commitmentDomain)` + Computes the role commitment from the given `accountId` and `role`. - This circuit does not perform an initialization check. It is only meant to be used as - an internal helper. Using this circuit outside of the module may cause undefined behavior - and break security guarantees. + This circuit does not perform an initialization check. It's only meant to be used as + an internal helper in the Shielded Access Control module. Using this circuit outside of the + module may cause undefined behavior and break security guarantees. + + ## Account ID (`accountId`) + The `accountId` is expected to be computed as: + `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)` + + - `secretKey`: A 32-byte cryptographically secure random value. + + ## Role Commitment Derivation + `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)` + + - `accountId`: See above. + - `role`: A unique role identifier. + - `instanceSalt`: A unique per-deployment salt, stored during initialization. + This prevents commitment collisions across deployments. + - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent + hash collisions when extending the module or using similar commitment schemes. @@ -528,7 +566,12 @@ The TypeScript witnesses in the repository are test-only and are not published w > Computes the role nullifier for a given `roleCommitment`. - Role Nullifier Derivation: `roleNullifier = SHA256(roleCommitment || nullifierDomain)` + ## Role Nullifier Derivation + `roleNullifier = SHA256(roleCommitment, nullifierDomain)` + + - `roleCommitment`: See `computeRoleCommitment`. + - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent + hash collisions when extending the module or using similar commitment schemes. #### computeAccountId [toc] [#ShieldedAccessControl-computeAccountId] @@ -541,14 +584,28 @@ The TypeScript witnesses in the repository are test-only and are not published w > Computes an `accountId` without on-chain state, allowing a user to derive their shielded identity commitment before submitting it in a grant or revoke operation. - It produces the same value the module derives internally from `wit_secretKey` and `_instanceSalt`. - - ID Derivation: `accountId = SHA256(secretKey || instanceSalt || accountIdDomain)` + This is the off-chain counterpart to `_computeAccountId` and produces an identical result + given the same inputs. - The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the privacy guarantees of this system. - Never log or persist the key in plaintext. Use cryptographically secure randomness to generate keys. - Treat key loss as identity loss. - A lost key cannot be recovered. + OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can + permanently compromise the privacy guarantees of this system: + + - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools, + application logs, analytics pipelines, or any observable side-channel. + - **Store offline or in secure enclaves** — hardware security modules (HSMs), + air-gapped devices, or encrypted vaults are strongly preferred over hot storage. + - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()` + or equivalent; weak or predictable keys can be brute-forced to reveal your identity. + - **Treat key loss as identity loss** — a lost key cannot be recovered. Back up + keys securely before using them in role commitments. + - **Avoid calling this circuit in untrusted environments** — executing this in an + unverified browser extension, compromised runtime, or shared machine may expose + the key to a malicious observer. + + ## ID Derivation + `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)` + + See `_computeAccountId` for further details. diff --git a/content/contracts-compact/api/utils.mdx b/content/contracts-compact/api/utils.mdx index 2df521da..c5459d4b 100644 --- a/content/contracts-compact/api/utils.mdx +++ b/content/contracts-compact/api/utils.mdx @@ -20,11 +20,6 @@ import "./node_modules/@openzeppelin/compact-contracts/utils/Utils"; --- - - The circuits below are helpers that are compiled into the circuit that calls them, - so no standalone complexity is listed for them. - - ### Ledger [toc] [#Utils-Ledger] ### Ledger [!toc] [#Utils-Ledger] @@ -48,10 +43,8 @@ None. > Returns whether `keyOrAddress` is the zero address. - - Midnight's burn address is represented as `left(default)` in Compact, - so the zero address is represented as this structure as well. - + Midnight's burn address is represented as left\(default\) + in Compact, so we've chosen to represent the zero address as this structure as well. #### isKeyZero [toc] [#Utils-isKeyZero] @@ -72,8 +65,8 @@ None. stackParams modulePrefix="Utils" > - Returns whether `keyOrAddress` is equal to `other`. - Assumes that a `ZswapCoinPublicKey` and a `ContractAddress` can never be equal. + Returns whether `keyOrAddress` is equal to `other`. Assumes that a ZswapCoinPublicKey + and a ContractAddress can never be equal #### isContractAddress [toc] [#Utils-isContractAddress] @@ -84,7 +77,7 @@ None. stackParams modulePrefix="Utils" > - Returns whether `keyOrAddress` is a `ContractAddress` type. + Returns whether `keyOrAddress` is a ContractAddress type. #### emptyString [toc] [#Utils-emptyString] @@ -94,7 +87,7 @@ None. kind="pure" modulePrefix="Utils" > - A helper function that returns the empty string: `""`. + A helper function that returns the empty string: "". #### canonicalize [toc] [#Utils-canonicalize] @@ -104,7 +97,9 @@ None. kind="pure" modulePrefix="Utils" > - Zeroes out the unused side of an `Either` value. Prevents crafted inputs where both `left` and `right` fields carry data from bypassing checks that only inspect the active side. + Zeroes out the unused side of an `Either` value. Prevents crafted + inputs where both `left` and `right` fields carry data from bypassing checks that + only inspect the active side. #### zeroAccount [toc] [#Utils-zeroAccount] @@ -114,12 +109,14 @@ None. kind="pure" modulePrefix="Utils" > - Returns the canonical zero `Either, ContractAddress>` value: the left variant (`Bytes<32>`) set to zero. - Used as the sentinel for mint and burn operations (a zero `from` signals a mint, a zero `to` signals a burn) - and for cleared approvals. - - The left variant is returned so that `isTargetZero` compares against `default>` - and so that the value does not trigger contract-address guards. + Returns the canonical zero `Either, ContractAddress>` value: the + left variant (`Bytes<32>`) set to zero. Used as the sentinel for mint/burn operations + (a zero `from` signals a mint, a zero `to` signals a burn) and for cleared approvals. + + The left variant is chosen so that `isTargetZero` checks against `default>`, + and to avoid triggering contract-address guards (e.g. circuits that check `!to.is_left` + before delegating to an `_unsafe` variant). A right variant with `default` + would also pass `isTargetZero`, but the left variant keeps guards and error messages consistent. #### isTargetZero [toc] [#Utils-isTargetZero] @@ -129,7 +126,8 @@ None. kind="pure" modulePrefix="Utils" > - Returns `true` if the active branch of `target` (as indicated by `is_left`) holds the zero value. + Returns `true` if `target`'s active branch (as indicated by `is_left`) + holds the zero value. #### computeAccountId [toc] [#Utils-computeAccountId] @@ -142,27 +140,34 @@ None. Computes an account identifier without on-chain state, allowing a user to derive their identity commitment before submitting an operation. - Derivation: `accountId = persistentHash(secretKey)`, with no domain-separation tag. - - The absence of a tag is deliberate. The identifier is global by design: the same key material yields the same - identity in every module deriving one, so a user who wishes to carry one identity across modules can. - Modules wanting a per-deployment, unlinkable identity should not use this circuit. - - Not for private derivation. This returns an identifier, not a secret, and the construction is untagged. - A circuit deriving a private value from a secret must use its own domain-separation tag. - - `secretKey` is a sensitive secret. Never log or persist it in plaintext (browser devtools, application logs, - analytics pipelines, or any observable side-channel). Prefer offline storage or secure enclaves (HSMs, - air-gapped devices, encrypted vaults) over hot storage. Generate it with `crypto.getRandomValues()` or equivalent; - weak or predictable keys can be brute-forced to reveal the identity. Treat key loss as identity loss: a lost key - cannot be recovered. Avoid evaluating this circuit in untrusted environments (an unverified browser extension, - a compromised runtime, or a shared machine) where the key may be exposed to a malicious observer. + OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can + permanently compromise the security of this system: + + - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools, + application logs, analytics pipelines, or any observable side-channel. + - **Store offline or in secure enclaves** — hardware security modules (HSMs), + air-gapped devices, or encrypted vaults are strongly preferred over hot storage. + - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()` + or equivalent; weak or predictable keys can be brute-forced to reveal your identity. + - **Treat key loss as identity loss** — a lost key cannot be recovered. + - **Avoid calling this circuit in untrusted environments** — executing this in an + unverified browser extension, compromised runtime, or shared machine may expose + the key to a malicious observer. + + ## ID Derivation + `accountId = persistentHash(secretKey)` - Requirements: + The absence of a domain-separation tag is DELIBERATE. This identifier + is global by design: the same key material yields the same identity in + every module deriving one, so a user who wishes to carry one identity + across modules can. Modules wanting a per-deployment, unlinkable identity + should not use this circuit. - * `secretKey` is a 32-byte cryptographically secure random value. + NOT FOR PRIVATE DERIVATION. This returns an identifier, not a secret, + and the construction is untagged. A circuit deriving a PRIVATE value from a + secret MUST use its own domain-separation tag. #### selfAsRecipient [toc] [#Utils-selfAsRecipient] @@ -172,8 +177,9 @@ None. kind="circuit" modulePrefix="Utils" > - Returns the current contract's address as an `Either` - for use as a recipient in shielded send operations (deposits and receiving change). + Returns the current contract's address as an + `Either` for use as a + recipient in shielded send operations (deposits and receiving change). #### UINT128_MAX [toc] [#Utils-UINT128_MAX] @@ -183,5 +189,7 @@ None. kind="pure" modulePrefix="Utils" > - The maximum value representable by a `Uint<128>`: `2^128 - 1`. + The maximum value representable by a `Uint<128>`. + + TODO: Remove once a math module providing this constant is available. From ad464c61987cb1acf4f27e173b900d7c54224e96 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 25 Oct 2026 23:34:49 -0300 Subject: [PATCH 03/10] re-add callouts --- .../contracts-compact/api/access-control.mdx | 28 +-- .../api/confidential-fungible-token.mdx | 98 +++++---- content/contracts-compact/api/crypto.mdx | 112 +++++----- .../contracts-compact/api/fungible-token.mdx | 46 ++-- content/contracts-compact/api/multi-token.mdx | 62 +++--- content/contracts-compact/api/multisig.mdx | 196 +++++++++++------- .../api/native-shielded-token.mdx | 144 ++++++++----- .../api/non-fungible-token.mdx | 54 +++-- content/contracts-compact/api/ownable.mdx | 40 ++-- .../api/shielded-access-control.mdx | 22 +- content/contracts-compact/api/utils.mdx | 14 +- 11 files changed, 480 insertions(+), 336 deletions(-) diff --git a/content/contracts-compact/api/access-control.mdx b/content/contracts-compact/api/access-control.mdx index 2da99115..25c93b74 100644 --- a/content/contracts-compact/api/access-control.mdx +++ b/content/contracts-compact/api/access-control.mdx @@ -111,7 +111,7 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="AccessControl" > The default admin role for all roles. Only accounts with this role will be able - to grant or revoke other roles unless custom admin roles are created via `_setRoleAdmin`. + to grant or revoke other roles unless custom admin roles are created via [_setRoleAdmin](#AccessControl-_setRoleAdmin). #### hasRole [toc] [#AccessControl-hasRole] @@ -135,7 +135,7 @@ The TypeScript witnesses in the repository are test-only and are not published w > Reverts if the caller cannot prove ownership of `roleId`. In the case of an external (non-contract) caller, the caller’s identity is - derived from the `wit_AccessControlSK` witness as `persistentHash(secretKey)`. + derived from the [wit_AccessControlSK](#AccessControl-wit_AccessControlSK) witness as `persistentHash(secretKey)`. Requirements: @@ -166,9 +166,9 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="AccessControl" > Returns the admin role that controls `roleId` or - a byte array with all zero bytes if `roleId` doesn't exist. See `grantRole` and `revokeRole`. + a byte array with all zero bytes if `roleId` doesn't exist. See [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole). - To change a role's admin use `_setRoleAdmin`. + To change a role's admin use [_setRoleAdmin](#AccessControl-_setRoleAdmin). #### grantRole [toc] [#AccessControl-grantRole] @@ -213,13 +213,15 @@ The TypeScript witnesses in the repository are test-only and are not published w > Revokes `roleId` from the calling account. - Roles are often managed via `grantRole` and `revokeRole`: this circuit's - purpose is to provide a mechanism for accounts to lose their privileges - if they are compromised (such as when a trusted device is misplaced). + + Roles are often managed via [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole): this circuit's + purpose is to provide a mechanism for accounts to lose their privileges + if they are compromised (such as when a trusted device is misplaced). - The caller's identity is derived from the `wit_AccessControlSK` witness as - `persistentHash(secretKey)`. The `callerConfirmation` parameter must match - the caller's computed account identifier to prevent accidental renunciation. + The caller's identity is derived from the [wit_AccessControlSK](#AccessControl-wit_AccessControlSK) witness as + `persistentHash(secretKey)`. The `callerConfirmation` parameter must match + the caller's computed account identifier to prevent accidental renunciation. + Requirements: @@ -265,8 +267,10 @@ The TypeScript witnesses in the repository are test-only and are not published w Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. Internal circuit without access restriction. It does NOT check if the role is granted to a ContractAddress. - External smart contracts cannot call the access control contract at this time, so granting a role to a ContractAddress may - render a circuit permanently inaccessible. + + External smart contracts cannot call the access control contract at this time, so granting a role to a ContractAddress may + render a circuit permanently inaccessible. + #### _revokeRole [toc] [#AccessControl-_revokeRole] diff --git a/content/contracts-compact/api/confidential-fungible-token.mdx b/content/contracts-compact/api/confidential-fungible-token.mdx index 23508f77..22cc81cc 100644 --- a/content/contracts-compact/api/confidential-fungible-token.mdx +++ b/content/contracts-compact/api/confidential-fungible-token.mdx @@ -53,10 +53,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl > Dual-balance grief fix. `_balances` is the SPENDABLE (confirmed) balance that debits draw from; `_pending` is the incoming pool that credits land in. Only - the owner moves pending -> spendable, via `sweep()`. So a third party spamming + the owner moves pending -> spendable, via [sweep](#ConfidentialFungibleToken-sweep). So a third party spamming dust credits cannot churn a victim's spendable ciphertext and invalidate the victim's in-flight spend proofs (a liveness grief). A wallet's total balance - is `balanceOf` (spendable) + `pendingOf` (pending). + is [balanceOf](#ConfidentialFungibleToken-balanceOf) (spendable) + [pendingOf](#ConfidentialFungibleToken-pendingOf) (pending). #### _pending [toc] [#ConfidentialFungibleToken-_pending] @@ -114,7 +114,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl never share a one-time pad. It restarts for each pair, which is why both counterparties are bound in too; see `SpendRandomnessPreimage`. - Kept out of `EscrowEntry`: `approve` replaces that entry wholesale, so a + Kept out of `EscrowEntry`: [approve](#ConfidentialFungibleToken-approve) replaces that entry wholesale, so a counter living there would restart on every re-approve. Note that this exposes a per-pair spend count which is already derivable @@ -267,10 +267,12 @@ The TypeScript witnesses in the repository are test-only and are not published; values directly (see `crypto/EcdhMask`), that constraint is gone. Values stay well under the Jubjub scalar-field order, so homomorphic sums do not wrap. - This equals the argument type's own maximum, so the per-op check is - redundant today. KEEP IT: it compares against a hard constant rather than - the argument type, so it still catches a later widening of the transfer - type — which is what the balance-claim width is sized against. + + This equals the argument type's own maximum, so the per-op check is + redundant today. KEEP IT: it compares against a hard constant rather than + the argument type, so it still catches a later widening of the transfer + type — which is what the balance-claim width is sized against. + #### initialize [toc] [#ConfidentialFungibleToken-initialize] @@ -320,7 +322,7 @@ The TypeScript witnesses in the repository are test-only and are not published; > Returns `account`'s PENDING balance ciphertext (incoming credits not yet swept into spendable), or Enc(0) if unregistered. The spendable - balance is `balanceOf`; a wallet's total is the sum of the two. See the + balance is [balanceOf](#ConfidentialFungibleToken-balanceOf); a wallet's total is the sum of the two. See the dual-balance note on the ledger declarations. Requirements: @@ -344,10 +346,12 @@ The TypeScript witnesses in the repository are test-only and are not published; contract-to-contract call) and safe for a wallet to decrypt, whereas a zeroed default holds off-curve points. The cap is encrypted, not public. - "No allowance" and "an allowance of 0" both read as encrypted-zero; - a caller that must distinguish them reads `_escrow` membership off-chain. This - getter grants nothing, the spend path (`transferFrom`/`burnFrom`) gates on - `_escrow` membership independently, so a non-existent allowance is unspendable. + + "No allowance" and "an allowance of 0" both read as encrypted-zero; + a caller that must distinguish them reads `_escrow` membership off-chain. This + getter grants nothing, the spend path ([transferFrom](#ConfidentialFungibleToken-transferFrom)/`burnFrom`) gates on + `_escrow` membership independently, so a non-existent allowance is unspendable. + #### isRegistered [toc] [#ConfidentialFungibleToken-isRegistered] @@ -370,12 +374,14 @@ The TypeScript witnesses in the repository are test-only and are not published; modulePrefix="ConfidentialFungibleToken" > Registers the caller: derives their encryption public key from - `wit_ConfidentialTokenEK` and initializes their balance to an encryption of + [wit_ConfidentialTokenEK](#ConfidentialFungibleToken-wit_ConfidentialTokenEK) and initializes their balance to an encryption of zero. Registration is a prerequisite for sending or receiving. - Returns the caller's `accountId`. A composing contract can gate on - the returned value (e.g. assert it is KYC-approved) to restrict who may - register. See the module header note on caller-identity gating. + + Returns the caller's `accountId`. A composing contract can gate on + the returned value (e.g. assert it is KYC-approved) to restrict who may + register. See the module header note on caller-identity gating. + Requirements: @@ -400,7 +406,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - Contract is initialized. - - `value` is within `MAX_TRANSFER_VALUE()`. + - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - `account` is registered. @@ -421,7 +427,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - Contract is initialized. - - `value` is within `MAX_TRANSFER_VALUE()`. + - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - The caller is registered and has a balance of at least `value`. @@ -437,14 +443,16 @@ The TypeScript witnesses in the repository are test-only and are not published; credits `to`, and pushes an encrypted memo to `to`. The amount is hidden; the (sender, recipient) pair is public. - Returns the caller's (sender's) `accountId` for caller-side gating. - The sender is derived from the same authentication `_debit` performs, so the - returned id is exactly the account whose funds moved. + + Returns the caller's (sender's) `accountId` for caller-side gating. + The sender is derived from the same authentication `_debit` performs, so the + returned id is exactly the account whose funds moved. + Requirements: - Contract is initialized. - - `value` is within `MAX_TRANSFER_VALUE()`. + - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - The caller is registered with a balance of at least `value`. - `to` is registered and is not the caller (no self-transfer). @@ -460,17 +468,19 @@ The TypeScript witnesses in the repository are test-only and are not published; The conserving value-movement primitive: debits the caller and credits `to` by the same `value`, net zero. It NEVER changes supply, so it is the safe surface for composition and the primitive this supply-free base - exposes. `transfer` is `_move` plus a self-transfer guard; supply-changing + exposes. [transfer](#ConfidentialFungibleToken-transfer) is [_move](#ConfidentialFungibleToken-_move) plus a self-transfer guard; supply-changing operations (`mint`/`burn`) live in a separate layer outside this conserving surface, and only they may break conservation. - Like `transfer`, the credit lands in the recipient's pending pool - (see the dual-balance note); the caller sweeps their own incoming value. + + Like [transfer](#ConfidentialFungibleToken-transfer), the credit lands in the recipient's pending pool + (see the dual-balance note); the caller sweeps their own incoming value. + - Self-move (`to` == caller) is PERMITTED here, unlike `transfer`, which + Self-move (`to` == caller) is PERMITTED here, unlike [transfer](#ConfidentialFungibleToken-transfer), which rejects it. A self-move is conserving and safe: it debits the caller's spendable and credits the caller's own pending (net zero, distinct ledger - cells; the value is recovered with `sweep`). `transfer`'s self-transfer guard + cells; the value is recovered with [sweep](#ConfidentialFungibleToken-sweep)). [transfer](#ConfidentialFungibleToken-transfer)'s self-transfer guard is therefore a UX choice, not a safety requirement: it spares users a self-transfer with no foreseeable benefit (a shuffle of spendable into pending plus a self-memo). This primitive stays permissive so a composer can @@ -494,16 +504,18 @@ The TypeScript witnesses in the repository are test-only and are not published; > Sweeps the caller's incoming `pending` pool into their `spendable` balance. Credits land in `pending` (see the dual-balance note); - this `sweep` is the ONLY path from pending into spendable, and only the owner + this [sweep](#ConfidentialFungibleToken-sweep) is the ONLY path from pending into spendable, and only the owner can invoke it (the caller proves BOTH witness secrets: the account secret identifies the account, the encryption secret proves the caller can read it). So a third party spamming credits cannot force a victim's spendable ciphertext to change, which is what would otherwise invalidate the victim's in-flight spend proofs (a liveness grief). - Purely homomorphic: adds the two ciphertexts (same key) and resets - pending to Enc(0). No plaintext is needed; the wallet already learned the - pending amounts from its memos. + + Purely homomorphic: adds the two ciphertexts (same key) and resets + pending to Enc(0). No plaintext is needed; the wallet already learned the + pending amounts from its memos. + Requirements: @@ -526,7 +538,9 @@ The TypeScript witnesses in the repository are test-only and are not published; refunds any prior escrow, so allowances replace rather than stack. The cap is not public. - Returns the caller's (approver's) `accountId` for caller-side gating. + + Returns the caller's (approver's) `accountId` for caller-side gating. + Also stores an owner-readable `ownerMemo` (ECDH one-time pad; see `crypto/EcdhMask`) in the escrow entry, carrying the current remaining @@ -538,7 +552,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - Contract is initialized. - - `value` is within `MAX_TRANSFER_VALUE()`. + - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - `spender` is registered, is not the zero account, and is not the caller. - The caller is registered with a balance of at least `value`. @@ -557,15 +571,17 @@ The TypeScript witnesses in the repository are test-only and are not published; stackParams="true" > Spends `value` from `fromAddress`'s escrow (granted to the - caller via `approve`) and credits it to `to`, reducing both escrow copies by + caller via [approve](#ConfidentialFungibleToken-approve)) and credits it to `to`, reducing both escrow copies by `value`. The amount is hidden. - Returns the caller's (spender's) `accountId` for caller-side gating. + + Returns the caller's (spender's) `accountId` for caller-side gating. + Requirements: - Contract is initialized. - - `value` is within `MAX_TRANSFER_VALUE()`. + - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - `to` is registered and is not the zero account. - An escrow from `fromAddress` to the caller exists with at least `value`. @@ -636,12 +652,12 @@ The TypeScript witnesses in the repository are test-only and are not published; Burns `value` from `fromAddress` by consuming an escrow that owner granted to the caller: `_spendEscrow` under its intent name, with no recipient credit. The building block for a composing contract's - `burnFrom`; it performs no supply accounting (see `_burn`). + `burnFrom`; it performs no supply accounting (see [_burn](#ConfidentialFungibleToken-_burn)). Requirements: - Contract is initialized. - - `value` is within `MAX_TRANSFER_VALUE()`. + - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - An escrow from `fromAddress` to the caller exists with at least `value`. @@ -694,7 +710,7 @@ None. complexity="k=9, rows=401" modulePrefix="ConfidentialFungibleTokenPublicSupply" > - Increases `totalSupply` by `value`. Call alongside the token + Increases [totalSupply](#ConfidentialFungibleTokenPublicSupply-totalSupply) by `value`. Call alongside the token module's `_mint`. The added amount is disclosed via the supply delta. Requirements: @@ -710,13 +726,13 @@ None. complexity="k=9, rows=256" modulePrefix="ConfidentialFungibleTokenPublicSupply" > - Decreases `totalSupply` by `value`. Call alongside the token + Decreases [totalSupply](#ConfidentialFungibleTokenPublicSupply-totalSupply) by `value`. Call alongside the token module's `_burn` or `_burnFrom`. The removed amount is disclosed via the supply delta. Requirements: - - `totalSupply` is at least `value`. + - [totalSupply](#ConfidentialFungibleTokenPublicSupply-totalSupply) is at least `value`. #### totalSupply [toc] [#ConfidentialFungibleTokenPublicSupply-totalSupply] diff --git a/content/contracts-compact/api/crypto.mdx b/content/contracts-compact/api/crypto.mdx index 7e2299a9..47f4d8a2 100644 --- a/content/contracts-compact/api/crypto.mdx +++ b/content/contracts-compact/api/crypto.mdx @@ -74,12 +74,12 @@ None. wallet MAY safely derive its account secret and its encryption secret from common key material. - The tag is FIRST, deliberately. `expandRandomness` hashes + The tag is FIRST, deliberately. [expandRandomness](#ElGamal-expandRandomness) hashes `[seed, tag]` with both slots caller-supplied, so tagging this circuit as `[secret, tag]` would make `secretToScalar(x)` identical to `expandRandomness(x, )` reachable by passing this domain string to the parameter that exists to receive domain strings. Leading with the - tag means reproducing this output through `expandRandomness` would require + tag means reproducing this output through [expandRandomness](#ElGamal-expandRandomness) would require passing the domain constant as the *seed*. Do not normalise the orderings. @@ -118,10 +118,12 @@ None. The identity ciphertext `Enc(0)` (both components are the curve identity). Useful for initializing a fresh balance. - This is a fixed, deterministic value `(O, O)` — NOT a hiding - encryption. It is publicly recognizable, so a slot left at `encryptZero` is - distinguishable as never-written. Rerandomize (or credit with fresh `r`) - before treating it as private. + + This is a fixed, deterministic value `(O, O)` — NOT a hiding + encryption. It is publicly recognizable, so a slot left at [encryptZero](#ElGamal-encryptZero) is + distinguishable as never-written. Rerandomize (or credit with fresh `r`) + before treating it as private. + #### encryptPoint [toc] [#ElGamal-encryptPoint] @@ -138,16 +140,18 @@ None. > Encrypts an arbitrary message point `m` under `pk` with randomness `r`, producing the ciphertext `(g^r, pk^r * m)`. This is the - general (non-lifted) ElGamal encryption; `encrypt` is the lifted special + general (non-lifted) ElGamal encryption; [encrypt](#ElGamal-encrypt) is the lifted special case `encryptPoint(pk, g^value, r)`. - `m` (like `pk`) is constrained into the prime-order subgroup by the - runtime (see the module-level subgroup note), so no in-circuit membership - check is performed. `pk` is asserted non-identity and `r` non-zero — the - two inputs that would zero the mask and expose `m` (see the weak-keys and - randomness notes). Decrypting `m` is only feasible when it is a known or - small-exponent point; the caller owns that concern (the lifted `encrypt` - bounds it via the discrete-log range). + + `m` (like `pk`) is constrained into the prime-order subgroup by the + runtime (see the module-level subgroup note), so no in-circuit membership + check is performed. `pk` is asserted non-identity and `r` non-zero — the + two inputs that would zero the mask and expose `m` (see the weak-keys and + randomness notes). Decrypting `m` is only feasible when it is a known or + small-exponent point; the caller owns that concern (the lifted [encrypt](#ElGamal-encrypt) + bounds it via the discrete-log range). + Requirements: @@ -170,7 +174,7 @@ None. Produces a fresh ciphertext of `value` under `pk` with randomness `r`, lifting `value` to the point `g^value`. Randomness is taken as a `Field` directly to avoid a redundant `Bytes<32> -> Field` conversion - at every call site; `expandRandomness` produces `Field` for the same reason. + at every call site; [expandRandomness](#ElGamal-expandRandomness) produces `Field` for the same reason. Requirements: @@ -187,7 +191,7 @@ None. > Negates a ciphertext componentwise: if `ct` encrypts `v` then the result encrypts `-v` under the same key (with negated randomness). - This is the building block `sub` rests on. + This is the building block [sub](#ElGamal-sub) rests on. #### add [toc] [#ElGamal-add] @@ -201,11 +205,13 @@ None. decrypts to `a + b`. This is the defining additive-homomorphism primitive and combines two independently produced ciphertexts componentwise. - Both inputs MUST be encrypted under the same public key for the - result to decrypt under that key's secret; combining ciphertexts under - different keys yields a ciphertext no single key can open. The randomness - of the result is the sum of the inputs' randomness, so the result is a - valid fresh ciphertext under that key. + + Both inputs MUST be encrypted under the same public key for the + result to decrypt under that key's secret; combining ciphertexts under + different keys yields a ciphertext no single key can open. The randomness + of the result is the sum of the inputs' randomness, so the result is a + valid fresh ciphertext under that key. + #### sub [toc] [#ElGamal-sub] @@ -218,9 +224,11 @@ None. Homomorphically subtracts two ciphertexts: `Enc(a) - Enc(b)` decrypts to `a - b`. Implemented as `add(a, negate(b))`. - Same-key requirement as `add`. The caller MUST ensure the - underlying plaintext subtraction does not underflow; this circuit cannot - check it (see `subEncrypted`). + + Same-key requirement as [add](#ElGamal-add). The caller MUST ensure the + underlying plaintext subtraction does not underflow; this circuit cannot + check it (see [subEncrypted](#ElGamal-subEncrypted)). + #### scalarMul [toc] [#ElGamal-scalarMul] @@ -237,13 +245,15 @@ None. This is the scalar-multiplication leg of the homomorphism, and the only way to express linear combinations: a weighted sum `Enc(3a + 5b)` is - `add(scalarMul(ca, 3), scalarMul(cb, 5))`, which repeated `add` cannot do + `add(scalarMul(ca, 3), scalarMul(cb, 5))`, which repeated [add](#ElGamal-add) cannot do for large weights. - `k` MUST be a valid Jubjub scalar (less than the subgroup order); - larger values fault `ecMul` (see the module-level hash-to-scalar note). The - caller also remains responsible for the discrete-log bound — `k * v` must - stay within the range a recovery search can clear. + + `k` MUST be a valid Jubjub scalar (less than the subgroup order); + larger values fault `ecMul` (see the module-level hash-to-scalar note). The + caller also remains responsible for the discrete-log bound — `k * v` must + stay within the range a recovery search can clear. + #### addEncrypted [toc] [#ElGamal-addEncrypted] @@ -261,7 +271,7 @@ None. > Homomorphically adds `value` to the plaintext encrypted by `old`, producing a fresh ciphertext under `pk` with new randomness `r`. - Convenience over `add` for folding a known plaintext into a ciphertext: + Convenience over [add](#ElGamal-add) for folding a known plaintext into a ciphertext: `add(old, encrypt(pk, value, r))`. Requirements: @@ -284,12 +294,14 @@ None. stackParams="true" > Homomorphically subtracts `value` from the plaintext encrypted - by `old`. Convenience over `sub`: `sub(old, encrypt(pk, value, r))`. + by `old`. Convenience over [sub](#ElGamal-sub): `sub(old, encrypt(pk, value, r))`. - The caller MUST ensure the underlying plaintext subtraction does - not underflow; this circuit cannot check it. Callers verify sufficiency by - reading the plaintext via their own witness and asserting it with - `assertDecryptsTo` before calling. + + The caller MUST ensure the underlying plaintext subtraction does + not underflow; this circuit cannot check it. Callers verify sufficiency by + reading the plaintext via their own witness and asserting it with + [assertDecryptsTo](#ElGamal-assertDecryptsTo) before calling. + Requirements: @@ -315,11 +327,13 @@ None. value) yet decrypts identically. Equivalent to homomorphically adding an encryption of zero. - `r = 0` is rejected (it would be a no-op via the `encryptPoint` - zero-randomness assert). One silent failure mode remains that the circuit - cannot catch: a `pk` that is not the key `ct` was encrypted under corrupts - the plaintext (the result no longer decrypts to the original value). Pass - fresh randomness and the matching `pk`. + + `r = 0` is rejected (it would be a no-op via the [encryptPoint](#ElGamal-encryptPoint) + zero-randomness assert). One silent failure mode remains that the circuit + cannot catch: a `pk` that is not the key `ct` was encrypted under corrupts + the plaintext (the result no longer decrypts to the original value). Pass + fresh randomness and the matching `pk`. + Requirements: @@ -358,7 +372,7 @@ None. > Asserts that `ct` decrypts under `(pk, ek)` to the message point `m`, and that `ek` is the secret for `pk`. This is the general - (non-lifted) verification; `assertDecryptsTo` is the lifted special case + (non-lifted) verification; [assertDecryptsTo](#ElGamal-assertDecryptsTo) is the lifted special case with `m = g^claimedValue`. Requirements: @@ -385,12 +399,14 @@ None. path an importing module uses to bind its witness-supplied `ek` and cached plaintext to publicly stored state: a wrong `ek` fails the key check, a wrong plaintext fails the decryption check. Lifted special case of - `assertDecryptsToPoint` with the message point `g^claimedValue`. + [assertDecryptsToPoint](#ElGamal-assertDecryptsToPoint) with the message point `g^claimedValue`. - `claimedValue` is the widest `Uint` Compact offers, so this parameter - never bounds a caller. A narrower value costs proportionally fewer rows: the - range constraint belongs to where the value is declared, not to this - signature. + + `claimedValue` is the widest `Uint` Compact offers, so this parameter + never bounds a caller. A narrower value costs proportionally fewer rows: the + range constraint belongs to where the value is declared, not to this + signature. + The claim domain must stay below the Jubjub subgroup order `l`: @@ -480,7 +496,9 @@ None. > Encrypts `value` to `recipientPk` under ephemeral scalar `e`. - `e` MUST be fresh per call (see the module freshness precondition). + + `e` MUST be fresh per call (see the module freshness precondition). + Requirements: diff --git a/content/contracts-compact/api/fungible-token.mdx b/content/contracts-compact/api/fungible-token.mdx index c955a135..2be23fcb 100644 --- a/content/contracts-compact/api/fungible-token.mdx +++ b/content/contracts-compact/api/fungible-token.mdx @@ -219,9 +219,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; > Moves a `value` amount of tokens from the caller's account to `to`. - Transfers to contract addresses are currently disallowed until contract-to-contract - interactions are supported in Compact. This restriction prevents assets from - being inadvertently locked in contracts that cannot currently handle token receipt. + + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. + Requirements: @@ -239,7 +241,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; complexity="k=13, rows=3982" modulePrefix="FungibleToken" > - Unsafe variant of `transfer` which allows transfers to contract addresses. + Unsafe variant of [transfer](#FungibleToken-transfer) which allows transfers to contract addresses. Transfers to contract addresses are considered unsafe because contract-to-contract @@ -264,7 +266,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; stackParams="true" > Returns the remaining number of tokens that `spender` will be allowed to spend on behalf of `owner` - through `transferFrom`. This value changes when `approve` or `transferFrom` are called. + through [transferFrom](#FungibleToken-transferFrom). This value changes when [approve](#FungibleToken-approve) or [transferFrom](#FungibleToken-transferFrom) are called. Manually checks if `owner` and `spender` are keys in the map and returns 0 if they are not. @@ -305,9 +307,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Moves `value` tokens from `fromAddress` to `to` using the allowance mechanism. `value` is the deducted from the caller's allowance. - Transfers to contract addresses are currently disallowed until contract-to-contract - interactions are supported in Compact. This restriction prevents assets from - being inadvertently locked in contracts that cannot currently handle token receipt. + + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. + Requirements: @@ -332,7 +336,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; stackParams="true" modulePrefix="FungibleToken" > - Unsafe variant of `transferFrom` which allows transfers to contract addresses. + Unsafe variant of [transferFrom](#FungibleToken-transferFrom) which allows transfers to contract addresses. Transfers to contract addresses are considered unsafe because contract-to-contract @@ -363,12 +367,14 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" > Moves a `value` amount of tokens from `fromAddress` to `to`. - This circuit is equivalent to `transfer`, and can be used to + This circuit is equivalent to [transfer](#FungibleToken-transfer), and can be used to e.g. implement automatic token fees, slashing mechanisms, etc. - Transfers to contract addresses are currently disallowed until contract-to-contract - interactions are supported in Compact. This restriction prevents assets from - being inadvertently locked in contracts that cannot currently handle token receipt. + + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. + Requirements: @@ -392,7 +398,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; modulePrefix="FungibleToken" stackParams="true" > - Unsafe variant of `transferFrom` which allows transfers to contract addresses. + Unsafe variant of [transferFrom](#FungibleToken-transferFrom) which allows transfers to contract addresses. Transfers to contract addresses are considered unsafe because contract-to-contract @@ -418,9 +424,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Creates a `value` amount of tokens and assigns them to `account`, by transferring it from the zero address. Relies on the `update` mechanism. - Transfers to contract addresses are currently disallowed until contract-to-contract - interactions are supported in Compact. This restriction prevents assets from - being inadvertently locked in contracts that cannot currently handle token receipt. + + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. + Requirements: @@ -437,7 +445,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; complexity="k=11, rows=1434" modulePrefix="FungibleToken" > - Unsafe variant of `_mint` which allows transfers to contract addresses. + Unsafe variant of [_mint](#FungibleToken-_mint) which allows transfers to contract addresses. Transfers to contract addresses are considered unsafe because contract-to-contract @@ -483,7 +491,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; stackParams="true" > Sets `value` as the allowance of `spender` over the `owner`'s tokens. - This circuit is equivalent to `approve`, and can be used to + This circuit is equivalent to [approve](#FungibleToken-approve), and can be used to e.g. set automatic allowances for certain subsystems, etc. Requirements: diff --git a/content/contracts-compact/api/multi-token.mdx b/content/contracts-compact/api/multi-token.mdx index be2fef61..9a328f11 100644 --- a/content/contracts-compact/api/multi-token.mdx +++ b/content/contracts-compact/api/multi-token.mdx @@ -152,7 +152,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; > Enables or disables approval for `operator` to manage all of the caller's assets. - In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK` + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_MultiTokenSK](#MultiToken-wit_MultiTokenSK) witness as `persistentHash(secretKey)`. Requirements: @@ -196,12 +196,14 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Transfers ownership of `value` amount of `id` tokens from `fromAddress` to `to`. The caller must be `fromAddress` or approved to transfer on their behalf. - In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK` + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_MultiTokenSK](#MultiToken-wit_MultiTokenSK) witness as `persistentHash(secretKey)`. - Transfers to contract addresses are currently disallowed until contract-to-contract - interactions are supported in Compact. This restriction prevents assets from - being inadvertently locked in contracts that cannot currently handle token receipt. + + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. + Requirements: @@ -209,7 +211,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; - `to` is not a ContractAddress. - `to` is not zero. - `fromAddress` is not zero. - - Caller must be `fromAddress` or approved via `setApprovalForAll`. + - Caller must be `fromAddress` or approved via [setApprovalForAll](#MultiToken-setApprovalForAll). - `fromAddress` must have an `id` balance of at least `value`. @@ -231,9 +233,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Does not impose restrictions on the caller, making it suitable for composition in higher-level contract logic. - Transfers to contract addresses are currently disallowed until contract-to-contract - interactions are supported in Compact. This restriction prevents assets from - being inadvertently locked in contracts that cannot currently handle token receipt. + + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. + Requirements: @@ -258,10 +262,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; stackParams="true" modulePrefix="MultiToken" > - Unsafe variant of `transferFrom` which allows transfers to contract addresses. + Unsafe variant of [transferFrom](#MultiToken-transferFrom) which allows transfers to contract addresses. The caller must be `fromAddress` or approved to transfer on their behalf. - In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_MultiTokenSK` + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_MultiTokenSK](#MultiToken-wit_MultiTokenSK) witness as `persistentHash(secretKey)`. @@ -275,7 +279,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; - Contract is initialized. - `to` is not zero. - `fromAddress` is not zero. - - Caller must be `fromAddress` or approved via `setApprovalForAll`. + - Caller must be `fromAddress` or approved via [setApprovalForAll](#MultiToken-setApprovalForAll). - `fromAddress` must have an `id` balance of at least `value`. @@ -293,7 +297,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; stackParams="true" modulePrefix="MultiToken" > - Unsafe variant of `_transfer` which allows transfers to contract addresses. + Unsafe variant of [_transfer](#MultiToken-_transfer) which allows transfers to contract addresses. Does not impose restrictions on the caller, making it suitable as a low-level building block for advanced contract logic. @@ -323,14 +327,16 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; substitution mechanism defined in the MultiToken standard. See https://eips.ethereum.org/EIPS/eip-1155#metadata. - By this mechanism, any occurrence of the `\{id\}` substring in either the - URI or any of the values in the JSON file at said URI will be replaced by - clients with the token type ID. + + By this mechanism, any occurrence of the `\{id\}` substring in either the + URI or any of the values in the JSON file at said URI will be replaced by + clients with the token type ID. - For example, the `https://token-cdn-domain/\{id\}.json` URI would be - interpreted by clients as - `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` - for token type ID 0x4cce0. + For example, the `https://token-cdn-domain/\{id\}.json` URI would be + interpreted by clients as + `https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json` + for token type ID 0x4cce0. + Requirements: @@ -348,9 +354,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; > Creates a `value` amount of tokens of type `id`, and assigns them to `to`. - Transfers to contract addresses are currently disallowed until contract-to-contract - interactions are supported in Compact. This restriction prevents assets from - being inadvertently locked in contracts that cannot currently handle token receipt. + + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from + being inadvertently locked in contracts that cannot currently handle token receipt. + Requirements: @@ -372,7 +380,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; stackParams="true" modulePrefix="MultiToken" > - Unsafe variant of `_mint` which allows transfers to contract addresses. + Unsafe variant of [_mint](#MultiToken-_mint) which allows transfers to contract addresses. Transfers to contract addresses are considered unsafe because contract-to-contract @@ -423,8 +431,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; > Enables or disables approval for `operator` to manage all of `owner`'s assets. - This circuit does not check for access permissions but can be useful as a building block - for more complex contract logic. + + This circuit does not check for access permissions but can be useful as a building block + for more complex contract logic. + Requirements: diff --git a/content/contracts-compact/api/multisig.mdx b/content/contracts-compact/api/multisig.mdx index 5bad5164..9bc1dd51 100644 --- a/content/contracts-compact/api/multisig.mdx +++ b/content/contracts-compact/api/multisig.mdx @@ -130,16 +130,18 @@ None. > Asserts that the given approval count meets the threshold. - `approvalCount` is supplied by the consuming contract and is - compared against the threshold only. This circuit never consults the signer - registry. Ensuring the count reflects distinct, currently-registered signers - is the consuming contract's responsibility, and a contract whose approval - accounting is not itself gated by `assertSigner` gets no protection here. - - The count is deliberately left unbounded by `_signerCount`: approvals - collected before a signer was removed must still count toward a threshold - the remaining signers satisfy, and bounding the count would reject those. - See the stale-signer-set guidance in the module header. + + `approvalCount` is supplied by the consuming contract and is + compared against the threshold only. This circuit never consults the signer + registry. Ensuring the count reflects distinct, currently-registered signers + is the consuming contract's responsibility, and a contract whose approval + accounting is not itself gated by [assertSigner](#Signer-assertSigner) gets no protection here. + + The count is deliberately left unbounded by `_signerCount`: approvals + collected before a signer was removed must still count toward a threshold + the remaining signers satisfy, and bounding the count would reject those. + See the stale-signer-set guidance in the module header. + Requirements: @@ -191,9 +193,11 @@ None. > Adds a new signer to the registry. - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -209,9 +213,11 @@ None. > Removes a signer from the registry. - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -229,9 +235,11 @@ None. > Updates the approval threshold. - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -255,18 +263,20 @@ None. or custom setup flows where signers may not yet be registered. Leaving `newThreshold` above the signer count is permitted and expected - mid-setup, and is not self-enforcing: `assertThresholdMet` compares + mid-setup, and is not self-enforcing: [assertThresholdMet](#Signer-assertThresholdMet) compares `approvalCount` against the threshold alone, so a caller supplying a matching count satisfies it even against an empty registry. Contracts must not gate - operations on `assertThresholdMet` while setup is incomplete. Lower the - threshold with `_setThreshold`; `_changeThreshold` cannot lower one while the + operations on [assertThresholdMet](#Signer-assertThresholdMet) while setup is incomplete. Lower the + threshold with [_setThreshold](#Signer-_setThreshold); [_changeThreshold](#Signer-_changeThreshold) cannot lower one while the signer count is zero, because that would require a zero threshold. - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. Use `_changeThreshold` for - operational threshold changes with signer count validation. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. Use [_changeThreshold](#Signer-_changeThreshold) for + operational threshold changes with signer count validation. + Requirements: @@ -298,7 +308,7 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ProposalManager" id="ProposalManager-ProposalStatus" > Proposal lifecycle states. `Inactive` is the zero value and is - never stored: `_createProposal` writes `Active`, and every view asserts the + never stored: [_createProposal](#ProposalManager-_createProposal) writes `Active`, and every view asserts the proposal exists before reading, so a stored proposal can never read `Inactive`. @@ -475,18 +485,22 @@ None. - A `kind` the execution rail cannot accept (an `UnshieldedUser` recipient on a shielded treasury, say) creates a proposal that passes here and then - reverts in `toShieldedRecipient` at execution. It stays `Active` and - unexecutable on that rail until `_cancelProposal` retires it. + reverts in [toShieldedRecipient](#ProposalManager-toShieldedRecipient) at execution. It stays `Active` and + unexecutable on that rail until [_cancelProposal](#ProposalManager-_cancelProposal) retires it. - A zero or otherwise unspendable `address` is accepted, and funds sent to it on execution are unrecoverable. - `_nextProposalId` is incremented and then read back for the map key, - making this a pinned read-modify-write. + + `_nextProposalId` is incremented and then read back for the map key, + making this a pinned read-modify-write. + - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -502,9 +516,11 @@ None. > Cancels a proposal. - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -520,9 +536,11 @@ None. > Marks a proposal as executed. - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -656,9 +674,11 @@ None. events are supported. They are not useful as privacy decoys, this treasury's holdings are public (see the module header). - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -687,15 +707,19 @@ None. belongs to the treasury and must not be re-spent by the caller (doing so would double-spend the recorded coin). - The returned `ShieldedSendResult` carries the sent and change coins - in plaintext, so sinking it (returning it from an entry point, writing it - to ledger, passing it cross-contract) discloses them. `_coins` publishes only - the retained change coin, and nothing at all when the send leaves no change, - so the sent coin's plaintext is disclosed beyond anything in public state. + + The returned `ShieldedSendResult` carries the sent and change coins + in plaintext, so sinking it (returning it from an entry point, writing it + to ledger, passing it cross-contract) discloses them. `_coins` publishes only + the retained change coin, and nothing at all when the send leaves no change, + so the sent coin's plaintext is disclosed beyond anything in public state. + - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -744,14 +768,16 @@ None. > Returns the difference between cumulative received and cumulative sent totals for a color. Should equal - `getTokenBalance` if accounting is consistent. - - The subtraction is unguarded. `_send` cannot drive the sent total - past the received total. Change re-insertion never increments `_received` - so the invariant holds for any consumer that goes through `_deposit` and - `_send`. A consumer that writes the exported `_coins` directly can break - it, and this circuit then fails as an unprovable range violation rather - than a named assert. + [getTokenBalance](#ShieldedTreasury-getTokenBalance) if accounting is consistent. + + + The subtraction is unguarded. [_send](#ShieldedTreasury-_send) cannot drive the sent total + past the received total. Change re-insertion never increments `_received` + so the invariant holds for any consumer that goes through [_deposit](#ShieldedTreasury-_deposit) and + [_send](#ShieldedTreasury-_send). A consumer that writes the exported `_coins` directly can break + it, and this circuit then fails as an unprovable range violation rather + than a named assert. + --- @@ -830,9 +856,11 @@ None. recoverable off-chain from the transaction's private proving state. - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -890,11 +918,13 @@ None. economically, they may serve as signaling mechanisms when events are supported. On a color the treasury has not held, a zero-value operation creates a zero-valued `_balances` entry, which - `getTokenBalance` reports the same as absence. + [getTokenBalance](#UnshieldedTreasury-getTokenBalance) reports the same as absence. - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -927,14 +957,16 @@ None. Sending to this contract's own address zeroes the tracked balance while the funds stay held and spendable at the protocol level, after which - `_send` will refuse to touch them. Recovery is only possible outside this + [_send](#UnshieldedTreasury-_send) will refuse to touch them. Recovery is only possible outside this module: a consumer may reconcile the exported `_balances`, or spend the funds through its own `sendUnshielded` call. - Access control is NOT enforced here. - The consuming contract must gate this behind its own - authorization policy. + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + Requirements: @@ -1053,7 +1085,7 @@ None. matching the stored commitment; the coin is sent as a plain shielded note, so the parent stays hidden on-chain. If `coin.value` exceeds `value`, `sendShielded` routes the change back to this contract as a self-owned - output; `_drain` returns it untouched via `result.change`. The + output; [_drain](#ForwarderPrivate-_drain) returns it untouched via `result.change`. The implementing contract chooses what to do with it: leave it dwelling at the contract for a future drain, or spend it onward to a different recipient in the same transaction via `sendImmediateShielded`. @@ -1090,7 +1122,7 @@ None. > Computes the parent commitment from `(parentAddr, opSecret)`. Pure circuit — used off-chain by the deployer to compute the - constructor argument, and inside `_drain` for the preimage check. + constructor argument, and inside [_drain](#ForwarderPrivate-_drain) for the preimage check. Callable without initialization. The first hash input is a fixed domain tag @@ -1126,7 +1158,7 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderShielde > The parent recipient, stored generically as `Either` for forward compatibility. - `initialize` only ever writes the `left` (coin public key) arm today. See + [initialize](#ForwarderShielded-initialize) only ever writes the `left` (coin public key) arm today. See the module header. @@ -1167,9 +1199,11 @@ None. - `parent` must not be the zero key. A zero parent would forward every deposit to an unspendable key with no recovery path. - Beyond the zero-key check, the caller must ensure `parent` can - spend forwarded coins. An unspendable parent strands every deposit with - no recovery path. + + Beyond the zero-key check, the caller must ensure `parent` can + spend forwarded coins. An unspendable parent strands every deposit with + no recovery path. + #### _deposit [toc] [#ForwarderShielded-_deposit] @@ -1217,7 +1251,7 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderUnshiel > The parent recipient, stored generically as `Either` for forward compatibility. - `initialize` only ever writes the `right` (user address) arm today. See + [initialize](#ForwarderUnshielded-initialize) only ever writes the `right` (user address) arm today. See the module header. @@ -1258,9 +1292,11 @@ None. - `parent` must not be the zero address. A zero parent would forward every deposit to an unspendable address with no recovery path. - Beyond the zero-address check, the caller must ensure `parent` - can spend forwarded amounts. An unspendable parent strands every - deposit with no recovery path. + + Beyond the zero-address check, the caller must ensure `parent` + can spend forwarded amounts. An unspendable parent strands every + deposit with no recovery path. + #### _deposit [toc] [#ForwarderUnshielded-_deposit] diff --git a/content/contracts-compact/api/native-shielded-token.mdx b/content/contracts-compact/api/native-shielded-token.mdx index 3a6f0192..da5ad4ff 100644 --- a/content/contracts-compact/api/native-shielded-token.mdx +++ b/content/contracts-compact/api/native-shielded-token.mdx @@ -119,7 +119,7 @@ None. Initializes the module's domain and metadata. This MUST be called in the implementing contract's constructor, and it MUST be the only token operation performed there. Failure to call it can - lead to an irreparable contract; perform mint, burn, and `tokenColor` only + lead to an irreparable contract; perform mint, burn, and [tokenColor](#NativeShieldedToken-tokenColor) only in post-deployment circuits, never in the constructor. @@ -218,9 +218,11 @@ None. compose the `NativeShieldedTokenDerivedNonce` extension and pass its `_deriveNonce()` output. - The returned coin info is the only copy available to the - recipient; callers SHOULD deliver it out of band. Wallets cannot detect - contract-minted coins by scanning the chain. + + The returned coin info is the only copy available to the + recipient; callers SHOULD deliver it out of band. Wallets cannot detect + contract-minted coins by scanning the chain. + Requirements: @@ -240,12 +242,14 @@ None. the same call. Forwards to `NativeShieldedTokenCore._mint` with `right(kernel.self())`. - Nonce uniqueness is the caller's responsibility, as in `_mint`. Spend - the coin later with `_burnFromSelf`, once it carries an `mt_index`. + Nonce uniqueness is the caller's responsibility, as in [_mint](#NativeShieldedToken-_mint). Spend + the coin later with [_burnFromSelf](#NativeShieldedToken-_burnFromSelf), once it carries an `mt_index`. - The consumer SHOULD persist the returned coin info in its own - ledger state: it is the contract's holding and is not otherwise - recoverable. + + The consumer SHOULD persist the returned coin info in its own + ledger state: it is the contract's holding and is not otherwise + recoverable. + Requirements: @@ -276,11 +280,13 @@ None. The coin is received by the contract and spent in the same transaction, so the spend goes through `sendImmediateShielded` (the transient path). For coins the contract already holds, use - `_burnFromSelf` instead. + [_burnFromSelf](#NativeShieldedToken-_burnFromSelf) instead. - The returned refund coin info is the only copy available to - `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect - contract-sent coins by scanning the chain. + + The returned refund coin info is the only copy available to + `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect + contract-sent coins by scanning the chain. + Requirements: @@ -396,7 +402,7 @@ None. Initializes the module's family metadata. This MUST be called in the implementing contract's constructor, and it MUST be the only token operation performed there. Failure to call it can - lead to an irreparable contract; perform mint, burn, and `tokenColor` only + lead to an irreparable contract; perform mint, burn, and [tokenColor](#NativeShieldedTokenFamily-tokenColor) only in post-deployment circuits, never in the constructor. @@ -500,9 +506,11 @@ None. nonce, compose the `NativeShieldedTokenDerivedNonce` extension and pass its `_deriveNonce()` output. - The returned coin info is the only copy available to the - recipient; callers SHOULD deliver it out of band. Wallets cannot detect - contract-minted coins by scanning the chain. + + The returned coin info is the only copy available to the + recipient; callers SHOULD deliver it out of band. Wallets cannot detect + contract-minted coins by scanning the chain. + Requirements: @@ -527,12 +535,14 @@ None. contract itself, claiming the coin in the same call. Forwards to `NativeShieldedTokenCore._mint` with `right(kernel.self())`. - Nonce uniqueness is the caller's responsibility, as in `_mint`. Spend - the coin later with `_burnFromSelf`, once it carries an `mt_index`. + Nonce uniqueness is the caller's responsibility, as in [_mint](#NativeShieldedTokenFamily-_mint). Spend + the coin later with [_burnFromSelf](#NativeShieldedTokenFamily-_burnFromSelf), once it carries an `mt_index`. - The consumer SHOULD persist the returned coin info in its own - ledger state: it is the contract's holding and is not otherwise - recoverable. + + The consumer SHOULD persist the returned coin info in its own + ledger state: it is the contract's holding and is not otherwise + recoverable. + Requirements: @@ -564,11 +574,13 @@ None. The coin is received by the contract and spent in the same transaction, so the spend goes through `sendImmediateShielded` (the transient path). For coins the contract already holds, use - `_burnFromSelf` instead. + [_burnFromSelf](#NativeShieldedTokenFamily-_burnFromSelf) instead. - The returned refund coin info is the only copy available to - `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect - contract-sent coins by scanning the chain. + + The returned refund coin info is the only copy available to + `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect + contract-sent coins by scanning the chain. + Requirements: @@ -688,7 +700,7 @@ None. > Initializes the contract-wide metadata. This MUST be called in the implementing contract's constructor (via - the wrapping flavor's `initialize`). Failure to do so can lead to an + the wrapping flavor's [initialize](#NativeShieldedTokenCore-initialize)). Failure to do so can lead to an irreparable contract. @@ -732,7 +744,7 @@ None. > Returns whether the contract has been initialized. Does not gate on init (it reports it), so it is safe to call before - `initialize`. + [initialize](#NativeShieldedTokenCore-initialize). #### name [toc] [#NativeShieldedTokenCore-name] @@ -822,9 +834,11 @@ None. persist the returned coin info: it is the contract's holding and is not otherwise recoverable. - The returned coin info is the only copy available to the - recipient; callers SHOULD deliver it out of band. Wallets cannot detect - contract-minted coins by scanning the chain. + + The returned coin info is the only copy available to the + recipient; callers SHOULD deliver it out of band. Wallets cannot detect + contract-minted coins by scanning the chain. + Requirements: @@ -858,12 +872,14 @@ None. The coin is received by the contract and spent in the same transaction, so the spend goes through `sendImmediateShielded` (the transient path). For coins the contract already holds, use - `_burnFromSelf` instead. A `right(kernel.self())` refund is claimed via + [_burnFromSelf](#NativeShieldedTokenCore-_burnFromSelf) instead. A `right(kernel.self())` refund is claimed via `receiveShielded` in this same call. - The returned refund coin info is the only copy available to - `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect - contract-sent coins by scanning the chain. + + The returned refund coin info is the only copy available to + `refundTo`; callers SHOULD deliver it out of band. Wallets cannot detect + contract-sent coins by scanning the chain. + Requirements: @@ -993,8 +1009,10 @@ None. modulePrefix="NativeShieldedTokenPublicSupply" > Returns the contract-mediated amount burned. - This is a lower bound: coins sent directly to the burn address - without going through the contract are not counted. + + This is a lower bound: coins sent directly to the burn address + without going through the contract are not counted. + #### totalSupply [toc] [#NativeShieldedTokenPublicSupply-totalSupply] @@ -1006,8 +1024,10 @@ None. modulePrefix="NativeShieldedTokenPublicSupply" > Returns `totalMinted() - totalBurned()`. - This is an UPPER BOUND on circulating supply, not an exact value: - burns that bypass the contract are invisible. See the module notes. + + This is an UPPER BOUND on circulating supply, not an exact value: + burns that bypass the contract are invisible. See the module notes. + --- @@ -1102,8 +1122,10 @@ None. modulePrefix="NativeShieldedTokenFamilyPublicSupply" > Returns the contract-mediated amount burned for `domain`. - This is a lower bound: coins sent directly to the burn address - without going through the contract are not counted. + + This is a lower bound: coins sent directly to the burn address + without going through the contract are not counted. + #### totalSupply [toc] [#NativeShieldedTokenFamilyPublicSupply-totalSupply] @@ -1115,8 +1137,10 @@ None. modulePrefix="NativeShieldedTokenFamilyPublicSupply" > Returns `totalMinted(domain) - totalBurned(domain)`. - This is an UPPER BOUND on circulating supply, not an exact value: - burns that bypass the contract are invisible. See the module notes. + + This is an UPPER BOUND on circulating supply, not an exact value: + burns that bypass the contract are invisible. See the module notes. + --- @@ -1188,8 +1212,8 @@ None. Adds `amount` to the contract-mediated burned total for `domain`. Call once per successful token `_burn` / `_burnFromSelf`. Enforces the per-domain `burned <= minted` invariant: a wrong-domain, - duplicate, or unpaired burn reverts rather than corrupting `totalSupply`. - The guard also keeps the add and the `totalSupply` subtraction within + duplicate, or unpaired burn reverts rather than corrupting [totalSupply](#NativeShieldedTokenPublicSupplyCore-totalSupply). + The guard also keeps the add and the [totalSupply](#NativeShieldedTokenPublicSupplyCore-totalSupply) subtraction within bounds, so no separate overflow/underflow check is needed. @@ -1213,8 +1237,10 @@ None. modulePrefix="NativeShieldedTokenPublicSupplyCore" > Returns the contract-mediated amount burned for `domain`. - This is a lower bound: coins sent directly to the burn address - without going through the contract are not counted. + + This is a lower bound: coins sent directly to the burn address + without going through the contract are not counted. + #### totalSupply [toc] [#NativeShieldedTokenPublicSupplyCore-totalSupply] @@ -1226,10 +1252,12 @@ None. modulePrefix="NativeShieldedTokenPublicSupplyCore" > Returns `totalMinted(domain) - totalBurned(domain)`, or 0 if - `totalBurned` somehow exceeds `totalMinted`. - This is an UPPER BOUND on circulating supply, not an exact value: - burns that bypass the contract are invisible. See the module notes. - The `burned <= minted` invariant enforced by `_addBurned` already makes + [totalBurned](#NativeShieldedTokenPublicSupplyCore-totalBurned) somehow exceeds [totalMinted](#NativeShieldedTokenPublicSupplyCore-totalMinted). + + This is an UPPER BOUND on circulating supply, not an exact value: + burns that bypass the contract are invisible. See the module notes. + + The `burned <= minted` invariant enforced by [_addBurned](#NativeShieldedTokenPublicSupplyCore-_addBurned) already makes `burned > minted` unreachable through the accounting API, so the clamp never fires in normal operation. It is kept so the getter is a total function that reads cleanly (returning 0) rather than reverting on an out-of-bound state. @@ -1281,8 +1309,10 @@ None. Advances the counter and returns the next derived coin nonce, for use as the `nonce` argument of a native token mint. - The derivation input is public ledger state, so mints using this - nonce are recipient-public; see the module notes. + + The derivation input is public ledger state, so mints using this + nonce are recipient-public; see the module notes. + --- @@ -1456,9 +1486,11 @@ test-only and are not published. > Sets the owner and the token's domain and metadata. - Call from the implementing contract's constructor only. It writes - sealed fields, so implementing contracts MUST NOT expose it as a - post-deploy circuit. + + Call from the implementing contract's constructor only. It writes + sealed fields, so implementing contracts MUST NOT expose it as a + post-deploy circuit. + `ownerId` must be `_computeOwnerId(pk, secretNonce)` for the coin diff --git a/content/contracts-compact/api/non-fungible-token.mdx b/content/contracts-compact/api/non-fungible-token.mdx index 28a35aad..2cc16ff3 100644 --- a/content/contracts-compact/api/non-fungible-token.mdx +++ b/content/contracts-compact/api/non-fungible-token.mdx @@ -219,10 +219,12 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; - The contract is initialized. - The `tokenId` must exist. - Native strings and string operations aren't supported within the Compact language, - e.g. concatenating a base URI + token ID is not possible like in other NFT implementations. - Therefore, we propose the URI storage approach; whereby, NFTs may or may not have unique "base" URIs. - It's up to the implementation to decide on how to handle this. + + Native strings and string operations aren't supported within the Compact language, + e.g. concatenating a base URI + token ID is not possible like in other NFT implementations. + Therefore, we propose the URI storage approach; whereby, NFTs may or may not have unique "base" URIs. + It's up to the implementation to decide on how to handle this. + #### _setTokenURI [toc] [#NonFungibleToken-_setTokenURI] @@ -233,14 +235,16 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; complexity="k=9, rows=289" modulePrefix="NonFungibleToken" > - Sets the URI as `tokenURI` for the given `tokenId`. + Sets the URI as [tokenURI](#NonFungibleToken-tokenURI) for the given `tokenId`. Requirements: - The contract is initialized. - The `tokenId` must exist. - The URI for a given NFT is usually set when the NFT is minted. + + The URI for a given NFT is usually set when the NFT is minted. + #### approve [toc] [#NonFungibleToken-approve] @@ -256,7 +260,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Only a single account can be approved at a time, so approving the zero value clears previous approvals. - In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK` + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_NonFungibleTokenSK](#NonFungibleToken-wit_NonFungibleTokenSK) witness as `persistentHash(secretKey)`. Requirements: @@ -295,9 +299,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; stackParams="true" > Approve or remove `operator` as an operator for the caller. - Operators can call `transferFrom` for any token owned by the caller. + Operators can call [transferFrom](#NonFungibleToken-transferFrom) for any token owned by the caller. - In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK` + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_NonFungibleTokenSK](#NonFungibleToken-wit_NonFungibleTokenSK) witness as `persistentHash(secretKey)`. Requirements: @@ -333,12 +337,14 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; > Transfers `tokenId` token from `fromAddress` to `to`. - Transfers to contract addresses are currently disallowed until contract-to-contract interactions - are supported in Compact. This restriction prevents assets from being inadvertently locked in contracts that cannot - currently handle token receipt. + + Transfers to contract addresses are currently disallowed until contract-to-contract interactions + are supported in Compact. This restriction prevents assets from being inadvertently locked in contracts that cannot + currently handle token receipt. - In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK` - witness as `persistentHash(secretKey)`. + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_NonFungibleTokenSK](#NonFungibleToken-wit_NonFungibleTokenSK) + witness as `persistentHash(secretKey)`. + Requirements: @@ -347,7 +353,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; - `to` is not zero. - `to` is not a ContractAddress. - `tokenId` token must be owned by `fromAddress`. - - If the caller is not `fromAddress`, it must be approved to move this token by either `approve` or `setApprovalForAll`. + - If the caller is not `fromAddress`, it must be approved to move this token by either [approve](#NonFungibleToken-approve) or [setApprovalForAll](#NonFungibleToken-setApprovalForAll). #### _unsafeTransferFrom [toc] [#NonFungibleToken-_unsafeTransferFrom] @@ -361,7 +367,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; > Transfers `tokenId` token from `fromAddress` to `to`. It does NOT check if the recipient is a ContractAddress. - In the case of an external (non-contract) caller, the caller's identity is derived from the `wit_NonFungibleTokenSK` + In the case of an external (non-contract) caller, the caller's identity is derived from the [wit_NonFungibleTokenSK](#NonFungibleToken-wit_NonFungibleTokenSK) witness as `persistentHash(secretKey)`. @@ -376,7 +382,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; - `fromAddress` is not zero. - `to` is not zero. - `tokenId` token must be owned by `fromAddress`. - - If the caller is not `fromAddress`, it must be approved to move this token by either `approve` or `setApprovalForAll`. + - If the caller is not `fromAddress`, it must be approved to move this token by either [approve](#NonFungibleToken-approve) or [setApprovalForAll](#NonFungibleToken-setApprovalForAll). #### _ownerOf [toc] [#NonFungibleToken-_ownerOf] @@ -522,11 +528,13 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; modulePrefix="NonFungibleToken" > Transfers `tokenId` from `fromAddress` to `to`. - As opposed to `transferFrom`, this imposes no restrictions on the caller's identity. + As opposed to [transferFrom](#NonFungibleToken-transferFrom), this imposes no restrictions on the caller's identity. - Transfers to contract addresses are currently disallowed until contract-to-contract - interactions are supported in Compact. This restriction prevents assets from being inadvertently - locked in contracts that cannot currently handle token receipt. + + Transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. This restriction prevents assets from being inadvertently + locked in contracts that cannot currently handle token receipt. + Requirements: @@ -546,7 +554,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; modulePrefix="NonFungibleToken" > Transfers `tokenId` from `fromAddress` to `to`. - As opposed to `_unsafeTransferFrom`, this imposes no restrictions on the caller's identity. + As opposed to [_unsafeTransferFrom](#NonFungibleToken-_unsafeTransferFrom), this imposes no restrictions on the caller's identity. It does NOT check if the recipient is a ContractAddress. Requirements: @@ -600,7 +608,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Passing false skips the check that `tokenId` was minted. Both - `_unsafeTransferFrom` and `_update` read that invariant back, so a planted approval + [_unsafeTransferFrom](#NonFungibleToken-_unsafeTransferFrom) and `_update` read that invariant back, so a planted approval can mint a token nobody authorized or outlive a later mint. Pass false only where the owner has already been read, as `_update` does. diff --git a/content/contracts-compact/api/ownable.mdx b/content/contracts-compact/api/ownable.mdx index 0dc27821..29f53f36 100644 --- a/content/contracts-compact/api/ownable.mdx +++ b/content/contracts-compact/api/ownable.mdx @@ -117,9 +117,11 @@ The TypeScript witnesses in the repository are test-only and are not published w > Transfers ownership of the contract to `newOwner`. - Ownership transfers to contract addresses are currently disallowed until contract-to-contract - interactions are supported in Compact. - This restriction prevents permanently disabling access to a circuit. + + Ownership transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. + This restriction prevents permanently disabling access to a circuit. + Requirements: @@ -137,7 +139,7 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=13, rows=2956" modulePrefix="Ownable" > - Unsafe variant of `transferOwnership`. + Unsafe variant of [transferOwnership](#Ownable-transferOwnership). Ownership transfers to contract addresses are considered unsafe because contract-to-contract @@ -162,7 +164,7 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="Ownable" > Leaves the contract without an owner. - It will not be possible to call `assertOnlyOwner` circuits anymore. + It will not be possible to call [assertOnlyOwner](#Ownable-assertOnlyOwner) circuits anymore. Can only be called by the current owner. Requirements: @@ -183,7 +185,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Use this to restrict access of specific circuits to the owner. In the case of an external (non-contract) caller, the caller’s identity is - derived from the `wit_OwnableSK` witness as `persistentHash(secretKey)`. + derived from the [wit_OwnableSK](#Ownable-wit_OwnableSK) witness as `persistentHash(secretKey)`. Requirements: @@ -202,9 +204,11 @@ The TypeScript witnesses in the repository are test-only and are not published w Transfers ownership of the contract to `newOwner` without enforcing permission checks on the caller. - Ownership transfers to contract addresses are currently disallowed until contract-to-contract - interactions are supported in Compact. - This restriction prevents circuits from being inadvertently locked in contracts. + + Ownership transfers to contract addresses are currently disallowed until contract-to-contract + interactions are supported in Compact. + This restriction prevents circuits from being inadvertently locked in contracts. + Requirements: @@ -220,7 +224,7 @@ The TypeScript witnesses in the repository are test-only and are not published w complexity="k=10, rows=597" modulePrefix="Ownable" > - Unsafe variant of `_transferOwnership`. + Unsafe variant of [_transferOwnership](#Ownable-_transferOwnership). Ownership transfers to contract addresses are considered unsafe because contract-to-contract @@ -393,7 +397,7 @@ The TypeScript witnesses in the repository are test-only and are not published w modulePrefix="ZOwnablePK" > Leaves the contract without an owner. - It will not be possible to call `assertOnlyOwner` circuits anymore. + It will not be possible to call [assertOnlyOwner](#ZOwnablePK-assertOnlyOwner) circuits anymore. Can only be called by the current owner. Requirements: @@ -417,7 +421,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - Contract is initialized. - - Caller's id (`SHA256(pk, nonce)`) when used in `_computeOwnerCommitment` equals + - Caller's id (`SHA256(pk, nonce)`) when used in [_computeOwnerCommitment](#ZOwnablePK-_computeOwnerCommitment) equals the stored `_ownerCommitment`, thus verifying themselves as the owner. @@ -478,11 +482,13 @@ The TypeScript witnesses in the repository are test-only and are not published w This value is later used in owner commitment hashing, and acts as a privacy-preserving alternative to a raw public key. - This module allows ownership to be tied to an identity commitment derived - from a public key and secret nonce. - While typically used with user public keys, this mechanism may also - support contract addresses as identifiers in future contract-to-contract - interactions. Both are treated as 32-byte values (`Bytes<32>`). + + This module allows ownership to be tied to an identity commitment derived + from a public key and secret nonce. + While typically used with user public keys, this mechanism may also + support contract addresses as identifiers in future contract-to-contract + interactions. Both are treated as 32-byte values (`Bytes<32>`). + Requirements: diff --git a/content/contracts-compact/api/shielded-access-control.mdx b/content/contracts-compact/api/shielded-access-control.mdx index 2bb52bf4..180580c0 100644 --- a/content/contracts-compact/api/shielded-access-control.mdx +++ b/content/contracts-compact/api/shielded-access-control.mdx @@ -396,10 +396,12 @@ The TypeScript witnesses in the repository are test-only and are not published w `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for `role`. - Roles are often managed via `grantRole` and `revokeRole`: this circuit's - purpose is to provide a mechanism for accounts to lose their privileges - if they are compromised (such as when a trusted device is misplaced). - Consumers with no need for self-revocation should not export this circuit. + + Roles are often managed via [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole): this circuit's + purpose is to provide a mechanism for accounts to lose their privileges + if they are compromised (such as when a trusted device is misplaced). + Consumers with no need for self-revocation should not export this circuit. + Outside observers may be able to use timing and pattern analysis to weaken pseudonymity @@ -425,12 +427,12 @@ The TypeScript witnesses in the repository are test-only and are not published w kind="circuit" modulePrefix="ShieldedAccessControl" > - Returns the admin role that controls `role`. Returns `DEFAULT_ADMIN_ROLE` for - roles with no explicitly set admin. Since `DEFAULT_ADMIN_ROLE` is the zero byte array, - there is no distinction between a nonexistent `role` and one whose admin is `DEFAULT_ADMIN_ROLE`. - See `grantRole` and `revokeRole`. + Returns the admin role that controls `role`. Returns [DEFAULT_ADMIN_ROLE](#ShieldedAccessControl-DEFAULT_ADMIN_ROLE) for + roles with no explicitly set admin. Since [DEFAULT_ADMIN_ROLE](#ShieldedAccessControl-DEFAULT_ADMIN_ROLE) is the zero byte array, + there is no distinction between a nonexistent `role` and one whose admin is [DEFAULT_ADMIN_ROLE](#ShieldedAccessControl-DEFAULT_ADMIN_ROLE). + See [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole). - To change a role's admin use `_setRoleAdmin`. + To change a role's admin use [_setRoleAdmin](#ShieldedAccessControl-_setRoleAdmin). Disclosures: @@ -569,7 +571,7 @@ The TypeScript witnesses in the repository are test-only and are not published w ## Role Nullifier Derivation `roleNullifier = SHA256(roleCommitment, nullifierDomain)` - - `roleCommitment`: See `computeRoleCommitment`. + - `roleCommitment`: See [computeRoleCommitment](#ShieldedAccessControl-computeRoleCommitment). - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent hash collisions when extending the module or using similar commitment schemes. diff --git a/content/contracts-compact/api/utils.mdx b/content/contracts-compact/api/utils.mdx index c5459d4b..20adb9fb 100644 --- a/content/contracts-compact/api/utils.mdx +++ b/content/contracts-compact/api/utils.mdx @@ -43,8 +43,10 @@ None. > Returns whether `keyOrAddress` is the zero address. - Midnight's burn address is represented as left\(default\) - in Compact, so we've chosen to represent the zero address as this structure as well. + + Midnight's burn address is represented as left\(default\) + in Compact, so we've chosen to represent the zero address as this structure as well. + #### isKeyZero [toc] [#Utils-isKeyZero] @@ -113,10 +115,10 @@ None. left variant (`Bytes<32>`) set to zero. Used as the sentinel for mint/burn operations (a zero `from` signals a mint, a zero `to` signals a burn) and for cleared approvals. - The left variant is chosen so that `isTargetZero` checks against `default>`, + The left variant is chosen so that [isTargetZero](#Utils-isTargetZero) checks against `default>`, and to avoid triggering contract-address guards (e.g. circuits that check `!to.is_left` before delegating to an `_unsafe` variant). A right variant with `default` - would also pass `isTargetZero`, but the left variant keeps guards and error messages consistent. + would also pass [isTargetZero](#Utils-isTargetZero), but the left variant keeps guards and error messages consistent. #### isTargetZero [toc] [#Utils-isTargetZero] @@ -191,5 +193,7 @@ None. > The maximum value representable by a `Uint<128>`. - TODO: Remove once a math module providing this constant is available. + + TODO: Remove once a math module providing this constant is available. + From 63115882c84fce3a95d9316f9d86706f75d8fef5 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 25 Oct 2026 23:48:46 -0300 Subject: [PATCH 04/10] fix bullets --- .../contracts-compact/api/access-control.mdx | 16 +-- .../api/confidential-fungible-token.mdx | 78 +++++------ content/contracts-compact/api/crypto.mdx | 34 ++--- .../contracts-compact/api/fungible-token.mdx | 94 ++++++------- content/contracts-compact/api/multi-token.mdx | 76 +++++------ content/contracts-compact/api/multisig.mdx | 86 ++++++------ .../api/native-shielded-token.mdx | 124 +++++++++--------- .../api/non-fungible-token.mdx | 122 ++++++++--------- content/contracts-compact/api/ownable.mdx | 80 +++++------ content/contracts-compact/api/security.mdx | 18 +-- .../api/shielded-access-control.mdx | 114 ++++++++-------- content/contracts-compact/api/utils.mdx | 10 +- 12 files changed, 426 insertions(+), 426 deletions(-) diff --git a/content/contracts-compact/api/access-control.mdx b/content/contracts-compact/api/access-control.mdx index 25c93b74..4e8d4fb9 100644 --- a/content/contracts-compact/api/access-control.mdx +++ b/content/contracts-compact/api/access-control.mdx @@ -139,7 +139,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - The caller must have `roleId`. + * The caller must have `roleId`. #### _checkRole [toc] [#AccessControl-_checkRole] @@ -154,7 +154,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - `account` must have `roleId`. + * `account` must have `roleId`. #### getRoleAdmin [toc] [#AccessControl-getRoleAdmin] @@ -183,8 +183,8 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - `account` must not be a ContractAddress. - - The caller must have `roleId`'s admin role. + * `account` must not be a ContractAddress. + * The caller must have `roleId`'s admin role. #### revokeRole [toc] [#AccessControl-revokeRole] @@ -199,7 +199,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - The caller must have `roleId`'s admin role. + * The caller must have `roleId`'s admin role. #### renounceRole [toc] [#AccessControl-renounceRole] @@ -225,8 +225,8 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - The caller's computed account identifier must match `callerConfirmation`. - - The caller must not be a `ContractAddress`. + * The caller's computed account identifier must match `callerConfirmation`. + * The caller must not be a `ContractAddress`. #### _setRoleAdmin [toc] [#AccessControl-_setRoleAdmin] @@ -253,7 +253,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - `account` must not be a ContractAddress. + * `account` must not be a ContractAddress. #### _unsafeGrantRole [toc] [#AccessControl-_unsafeGrantRole] diff --git a/content/contracts-compact/api/confidential-fungible-token.mdx b/content/contracts-compact/api/confidential-fungible-token.mdx index 22cc81cc..ff4a2660 100644 --- a/content/contracts-compact/api/confidential-fungible-token.mdx +++ b/content/contracts-compact/api/confidential-fungible-token.mdx @@ -292,7 +292,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is not already initialized. + * Contract is not already initialized. #### balanceOf [toc] [#ConfidentialFungibleToken-balanceOf] @@ -309,7 +309,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. + * Contract is initialized. #### pendingOf [toc] [#ConfidentialFungibleToken-pendingOf] @@ -327,7 +327,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. + * Contract is initialized. #### allowance [toc] [#ConfidentialFungibleToken-allowance] @@ -385,8 +385,8 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. - - The caller is not already registered. + * Contract is initialized. + * The caller is not already registered. #### _mint [toc] [#ConfidentialFungibleToken-_mint] @@ -405,9 +405,9 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. - - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - - `account` is registered. + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * `account` is registered. #### _burn [toc] [#ConfidentialFungibleToken-_burn] @@ -426,9 +426,9 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. - - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - - The caller is registered and has a balance of at least `value`. + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * The caller is registered and has a balance of at least `value`. #### transfer [toc] [#ConfidentialFungibleToken-transfer] @@ -451,10 +451,10 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. - - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - - The caller is registered with a balance of at least `value`. - - `to` is registered and is not the caller (no self-transfer). + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * The caller is registered with a balance of at least `value`. + * `to` is registered and is not the caller (no self-transfer). #### _move [toc] [#ConfidentialFungibleToken-_move] @@ -489,9 +489,9 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. - - The caller is registered with a spendable balance of at least `value`. - - `to` is registered. + * Contract is initialized. + * The caller is registered with a spendable balance of at least `value`. + * `to` is registered. #### sweep [toc] [#ConfidentialFungibleToken-sweep] @@ -519,9 +519,9 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. - - The caller is registered. - - The caller holds the encryption secret registered for their account. + * Contract is initialized. + * The caller is registered. + * The caller holds the encryption secret registered for their account. #### approve [toc] [#ConfidentialFungibleToken-approve] @@ -551,10 +551,10 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. - - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - - `spender` is registered, is not the zero account, and is not the caller. - - The caller is registered with a balance of at least `value`. + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * `spender` is registered, is not the zero account, and is not the caller. + * The caller is registered with a balance of at least `value`. #### transferFrom [toc] [#ConfidentialFungibleToken-transferFrom] @@ -580,10 +580,10 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. - - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - - `to` is registered and is not the zero account. - - An escrow from `fromAddress` to the caller exists with at least `value`. + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * `to` is registered and is not the zero account. + * An escrow from `fromAddress` to the caller exists with at least `value`. #### clearMemos [toc] [#ConfidentialFungibleToken-clearMemos] @@ -618,11 +618,11 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. - - The caller is registered. - - The caller holds the encryption secret registered for their account. - - The caller has a memo list. - - `expectedEpoch` equals the caller's current credit epoch. + * Contract is initialized. + * The caller is registered. + * The caller holds the encryption secret registered for their account. + * The caller has a memo list. + * `expectedEpoch` equals the caller's current credit epoch. #### computeAccountId [toc] [#ConfidentialFungibleToken-computeAccountId] @@ -656,9 +656,9 @@ The TypeScript witnesses in the repository are test-only and are not published; Requirements: - - Contract is initialized. - - `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). - - An escrow from `fromAddress` to the caller exists with at least `value`. + * Contract is initialized. + * `value` is within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * An escrow from `fromAddress` to the caller exists with at least `value`. --- @@ -715,7 +715,7 @@ None. Requirements: - - `totalSupply + value` does not overflow `Uint<128>`. + * `totalSupply + value` does not overflow `Uint<128>`. #### _subSupply [toc] [#ConfidentialFungibleTokenPublicSupply-_subSupply] @@ -732,7 +732,7 @@ None. Requirements: - - [totalSupply](#ConfidentialFungibleTokenPublicSupply-totalSupply) is at least `value`. + * [totalSupply](#ConfidentialFungibleTokenPublicSupply-totalSupply) is at least `value`. #### totalSupply [toc] [#ConfidentialFungibleTokenPublicSupply-totalSupply] diff --git a/content/contracts-compact/api/crypto.mdx b/content/contracts-compact/api/crypto.mdx index 47f4d8a2..dc5b7561 100644 --- a/content/contracts-compact/api/crypto.mdx +++ b/content/contracts-compact/api/crypto.mdx @@ -155,8 +155,8 @@ None. Requirements: - - `pk` is not the identity point. - - `r` is non-zero. + * `pk` is not the identity point. + * `r` is non-zero. #### encrypt [toc] [#ElGamal-encrypt] @@ -178,8 +178,8 @@ None. Requirements: - - `pk` is not the identity point. - - `r` is non-zero. + * `pk` is not the identity point. + * `r` is non-zero. #### negate [toc] [#ElGamal-negate] @@ -276,8 +276,8 @@ None. Requirements: - - `pk` is not the identity point. - - `r` is non-zero. + * `pk` is not the identity point. + * `r` is non-zero. #### subEncrypted [toc] [#ElGamal-subEncrypted] @@ -305,8 +305,8 @@ None. Requirements: - - `pk` is not the identity point. - - `r` is non-zero. + * `pk` is not the identity point. + * `r` is non-zero. #### rerandomize [toc] [#ElGamal-rerandomize] @@ -337,8 +337,8 @@ None. Requirements: - - `pk` is not the identity point. - - `r` is non-zero. + * `pk` is not the identity point. + * `r` is non-zero. #### assertKeyPair [toc] [#ElGamal-assertKeyPair] @@ -354,7 +354,7 @@ None. Requirements: - - `ek` is the secret for `pk` (`derivePk(ek) == pk`). + * `ek` is the secret for `pk` (`derivePk(ek) == pk`). #### assertDecryptsToPoint [toc] [#ElGamal-assertDecryptsToPoint] @@ -377,8 +377,8 @@ None. Requirements: - - `ek` is the secret for `pk`. - - `ct` decrypts under `(pk, ek)` to `m`. + * `ek` is the secret for `pk`. + * `ct` decrypts under `(pk, ek)` to `m`. #### assertDecryptsTo [toc] [#ElGamal-assertDecryptsTo] @@ -418,8 +418,8 @@ None. Requirements: - - `ek` is the secret for `pk`. - - `ct` decrypts under `(pk, ek)` to `claimedValue`. + * `ek` is the secret for `pk`. + * `ct` decrypts under `(pk, ek)` to `claimedValue`. --- @@ -502,8 +502,8 @@ None. Requirements: - - `recipientPk` is not the identity point. - - `e` is non-zero, a valid Jubjub scalar (`< ℓ`), secret, and fresh per call. + * `recipientPk` is not the identity point. + * `e` is non-zero, a valid Jubjub scalar (`< ℓ`), secret, and fresh per call. #### decrypt [toc] [#EcdhMask-decrypt] diff --git a/content/contracts-compact/api/fungible-token.mdx b/content/contracts-compact/api/fungible-token.mdx index 2be23fcb..0315ff38 100644 --- a/content/contracts-compact/api/fungible-token.mdx +++ b/content/contracts-compact/api/fungible-token.mdx @@ -144,7 +144,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. + * Contract is initialized. #### symbol [toc] [#FungibleToken-symbol] @@ -159,7 +159,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. + * Contract is initialized. #### decimals [toc] [#FungibleToken-decimals] @@ -174,7 +174,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. + * Contract is initialized. #### totalSupply [toc] [#FungibleToken-totalSupply] @@ -189,7 +189,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. + * Contract is initialized. #### balanceOf [toc] [#FungibleToken-balanceOf] @@ -206,7 +206,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. + * Contract is initialized. #### transfer [toc] [#FungibleToken-transfer] @@ -227,10 +227,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `to` is not a ContractAddress. - - `to` is not the zero address. - - The caller has a balance of at least `value`. + * Contract is initialized. + * `to` is not a ContractAddress. + * `to` is not the zero address. + * The caller has a balance of at least `value`. #### _unsafeTransfer [toc] [#FungibleToken-_unsafeTransfer] @@ -251,9 +251,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `to` is not the zero address. - - The caller has a balance of at least `value`. + * Contract is initialized. + * `to` is not the zero address. + * The caller has a balance of at least `value`. #### allowance [toc] [#FungibleToken-allowance] @@ -272,7 +272,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. + * Contract is initialized. #### approve [toc] [#FungibleToken-approve] @@ -287,8 +287,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `spender` is not the zero address. + * Contract is initialized. + * `spender` is not the zero address. #### transferFrom [toc] [#FungibleToken-transferFrom] @@ -315,12 +315,12 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `fromAddress` is not the zero address. - - `fromAddress` must have a balance of at least `value`. - - `to` is not the zero address. - - `to` is not a ContractAddress. - - The caller has an allowance of `fromAddress`'s tokens of at least `value`. + * Contract is initialized. + * `fromAddress` is not the zero address. + * `fromAddress` must have a balance of at least `value`. + * `to` is not the zero address. + * `to` is not a ContractAddress. + * The caller has an allowance of `fromAddress`'s tokens of at least `value`. #### _unsafeTransferFrom [toc] [#FungibleToken-_unsafeTransferFrom] @@ -346,11 +346,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `fromAddress` is not the zero address. - - `fromAddress` must have a balance of at least `value`. - - `to` is not the zero address. - - The caller has an allowance of `fromAddress`'s tokens of at least `value`. + * Contract is initialized. + * `fromAddress` is not the zero address. + * `fromAddress` must have a balance of at least `value`. + * `to` is not the zero address. + * The caller has an allowance of `fromAddress`'s tokens of at least `value`. #### _transfer [toc] [#FungibleToken-_transfer] @@ -378,11 +378,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `fromAddress` is not be the zero address. - - `fromAddress` must have at least a balance of `value`. - - `to` must not be the zero address. - - `to` must not be a ContractAddress. + * Contract is initialized. + * `fromAddress` is not be the zero address. + * `fromAddress` must have at least a balance of `value`. + * `to` must not be the zero address. + * `to` must not be a ContractAddress. #### _unsafeUncheckedTransfer [toc] [#FungibleToken-_unsafeUncheckedTransfer] @@ -408,9 +408,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `fromAddress` is not the zero address. - - `to` is not the zero address. + * Contract is initialized. + * `fromAddress` is not the zero address. + * `to` is not the zero address. #### _mint [toc] [#FungibleToken-_mint] @@ -432,9 +432,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `account` is not a ContractAddress. - - `account` is not the zero address. + * Contract is initialized. + * `account` is not a ContractAddress. + * `account` is not the zero address. #### _unsafeMint [toc] [#FungibleToken-_unsafeMint] @@ -455,8 +455,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `account` is not the zero address. + * Contract is initialized. + * `account` is not the zero address. #### _burn [toc] [#FungibleToken-_burn] @@ -472,9 +472,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `account` is not the zero address. - - `account` must have at least a balance of `value`. + * Contract is initialized. + * `account` is not the zero address. + * `account` must have at least a balance of `value`. #### _approve [toc] [#FungibleToken-_approve] @@ -496,9 +496,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `owner` is not the zero address. - - `spender` is not the zero address. + * Contract is initialized. + * `owner` is not the zero address. + * `spender` is not the zero address. #### _spendAllowance [toc] [#FungibleToken-_spendAllowance] @@ -519,7 +519,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: - - Contract is initialized. - - `spender` must have at least an allowance of `value` from `owner`. + * Contract is initialized. + * `spender` must have at least an allowance of `value` from `owner`. diff --git a/content/contracts-compact/api/multi-token.mdx b/content/contracts-compact/api/multi-token.mdx index 9a328f11..e7942a17 100644 --- a/content/contracts-compact/api/multi-token.mdx +++ b/content/contracts-compact/api/multi-token.mdx @@ -97,7 +97,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Must be called in the contract's constructor. + * Must be called in the contract's constructor. #### uri [toc] [#MultiToken-uri] @@ -116,7 +116,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. + * Contract is initialized. #### balanceOf [toc] [#MultiToken-balanceOf] @@ -135,7 +135,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. + * Contract is initialized. #### setApprovalForAll [toc] [#MultiToken-setApprovalForAll] @@ -157,8 +157,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. - - `operator` is not zero. + * Contract is initialized. + * `operator` is not zero. #### isApprovedForAll [toc] [#MultiToken-isApprovedForAll] @@ -177,7 +177,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. + * Contract is initialized. #### transferFrom [toc] [#MultiToken-transferFrom] @@ -207,12 +207,12 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. - - `to` is not a ContractAddress. - - `to` is not zero. - - `fromAddress` is not zero. - - Caller must be `fromAddress` or approved via [setApprovalForAll](#MultiToken-setApprovalForAll). - - `fromAddress` must have an `id` balance of at least `value`. + * Contract is initialized. + * `to` is not a ContractAddress. + * `to` is not zero. + * `fromAddress` is not zero. + * Caller must be `fromAddress` or approved via [setApprovalForAll](#MultiToken-setApprovalForAll). + * `fromAddress` must have an `id` balance of at least `value`. #### _transfer [toc] [#MultiToken-_transfer] @@ -241,11 +241,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. - - `to` is not a ContractAddress. - - `to` is not zero. - - `fromAddress` is not zero. - - `fromAddress` must have an `id` balance of at least `value`. + * Contract is initialized. + * `to` is not a ContractAddress. + * `to` is not zero. + * `fromAddress` is not zero. + * `fromAddress` must have an `id` balance of at least `value`. #### _unsafeTransferFrom [toc] [#MultiToken-_unsafeTransferFrom] @@ -276,11 +276,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. - - `to` is not zero. - - `fromAddress` is not zero. - - Caller must be `fromAddress` or approved via [setApprovalForAll](#MultiToken-setApprovalForAll). - - `fromAddress` must have an `id` balance of at least `value`. + * Contract is initialized. + * `to` is not zero. + * `fromAddress` is not zero. + * Caller must be `fromAddress` or approved via [setApprovalForAll](#MultiToken-setApprovalForAll). + * `fromAddress` must have an `id` balance of at least `value`. #### _unsafeTransfer [toc] [#MultiToken-_unsafeTransfer] @@ -309,10 +309,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. - - `fromAddress` is not zero. - - `to` is not zero. - - `fromAddress` must have an `id` balance of at least `value`. + * Contract is initialized. + * `fromAddress` is not zero. + * `to` is not zero. + * `fromAddress` must have an `id` balance of at least `value`. #### _setURI [toc] [#MultiToken-_setURI] @@ -340,7 +340,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. + * Contract is initialized. #### _mint [toc] [#MultiToken-_mint] @@ -362,9 +362,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. - - `to` is not zero. - - `to` is not a ContractAddress. + * Contract is initialized. + * `to` is not zero. + * `to` is not a ContractAddress. #### _unsafeMint [toc] [#MultiToken-_unsafeMint] @@ -390,8 +390,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. - - `to` is not zero. + * Contract is initialized. + * `to` is not zero. #### _burn [toc] [#MultiToken-_burn] @@ -411,9 +411,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. - - `fromAddress` is not zero. - - `fromAddress` must have an `id` balance of at least `value`. + * Contract is initialized. + * `fromAddress` is not zero. + * `fromAddress` must have an `id` balance of at least `value`. #### _setApprovalForAll [toc] [#MultiToken-_setApprovalForAll] @@ -438,8 +438,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: - - Contract is initialized. - - `owner` is not zero. - - `operator` is not zero. + * Contract is initialized. + * `owner` is not zero. + * `operator` is not zero. diff --git a/content/contracts-compact/api/multisig.mdx b/content/contracts-compact/api/multisig.mdx index 9bc1dd51..487d3469 100644 --- a/content/contracts-compact/api/multisig.mdx +++ b/content/contracts-compact/api/multisig.mdx @@ -100,10 +100,10 @@ None. Requirements: - - If used, can only be called once (in the constructor). - - `thresh` must not be zero. - - `thresh` must not exceed the number of `signers`. - - `signers` must not contain duplicates. + * If used, can only be called once (in the constructor). + * `thresh` must not be zero. + * `thresh` must not exceed the number of `signers`. + * `signers` must not contain duplicates. #### assertSigner [toc] [#Signer-assertSigner] @@ -117,7 +117,7 @@ None. Requirements: - - `caller` must be a member of the signers registry. + * `caller` must be a member of the signers registry. #### assertThresholdMet [toc] [#Signer-assertThresholdMet] @@ -145,9 +145,9 @@ None. Requirements: - - Ledger threshold must be set (not be zero). An unconfigured module has a + * Ledger threshold must be set (not be zero). An unconfigured module has a zero threshold, so this rejects before any approval count is compared. - - `approvalCount` must be >= threshold. + * `approvalCount` must be >= threshold. #### getSignerCount [toc] [#Signer-getSignerCount] @@ -201,7 +201,7 @@ None. Requirements: - - `signer` must not already be an active signer. + * `signer` must not already be an active signer. #### _removeSigner [toc] [#Signer-_removeSigner] @@ -221,8 +221,8 @@ None. Requirements: - - `signer` must be an active signer. - - Removal must not drop signer count below threshold. + * `signer` must be an active signer. + * Removal must not drop signer count below threshold. #### _changeThreshold [toc] [#Signer-_changeThreshold] @@ -243,8 +243,8 @@ None. Requirements: - - `newThreshold` must not be zero. - - `newThreshold` must not exceed the current signer count. + * `newThreshold` must not be zero. + * `newThreshold` must not exceed the current signer count. #### _setThreshold [toc] [#Signer-_setThreshold] @@ -280,7 +280,7 @@ None. Requirements: - - `newThreshold` must not be zero. + * `newThreshold` must not be zero. --- @@ -418,7 +418,7 @@ None. Requirements: - - Recipient kind must be ShieldedUser or Contract. + * Recipient kind must be ShieldedUser or Contract. #### toUnshieldedRecipient [toc] [#ProposalManager-toUnshieldedRecipient] @@ -433,7 +433,7 @@ None. Requirements: - - Recipient kind must be UnshieldedUser or Contract. + * Recipient kind must be UnshieldedUser or Contract. #### assertProposalExists [toc] [#ProposalManager-assertProposalExists] @@ -447,7 +447,7 @@ None. Requirements: - - Proposal with `id` must have been created. + * Proposal with `id` must have been created. #### assertProposalActive [toc] [#ProposalManager-assertProposalActive] @@ -461,8 +461,8 @@ None. Requirements: - - Proposal must exist. - - Proposal status must be Active. + * Proposal must exist. + * Proposal status must be Active. #### _createProposal [toc] [#ProposalManager-_createProposal] @@ -483,11 +483,11 @@ None. The recipient is NOT validated beyond being well-typed. Two consequences the consumer must handle: - - A `kind` the execution rail cannot accept (an `UnshieldedUser` recipient + * A `kind` the execution rail cannot accept (an `UnshieldedUser` recipient on a shielded treasury, say) creates a proposal that passes here and then reverts in [toShieldedRecipient](#ProposalManager-toShieldedRecipient) at execution. It stays `Active` and unexecutable on that rail until [_cancelProposal](#ProposalManager-_cancelProposal) retires it. - - A zero or otherwise unspendable `address` is accepted, and funds sent to + * A zero or otherwise unspendable `address` is accepted, and funds sent to it on execution are unrecoverable. @@ -504,7 +504,7 @@ None. Requirements: - - `amount` must be greater than 0. + * `amount` must be greater than 0. #### _cancelProposal [toc] [#ProposalManager-_cancelProposal] @@ -524,7 +524,7 @@ None. Requirements: - - Proposal must be active. + * Proposal must be active. #### _markExecuted [toc] [#ProposalManager-_markExecuted] @@ -544,7 +544,7 @@ None. Requirements: - - Proposal must be active. + * Proposal must be active. #### getProposal [toc] [#ProposalManager-getProposal] @@ -682,7 +682,7 @@ None. Requirements: - - Deposit must not cause received total overflow. + * Deposit must not cause received total overflow. #### _send [toc] [#ShieldedTreasury-_send] @@ -723,9 +723,9 @@ None. Requirements: - - A coin of the given `color` must exist in the treasury. - - The coin's value must be >= `amount`. - - Send must not cause sent total overflow. + * A coin of the given `color` must exist in the treasury. + * The coin's value must be >= `amount`. + * Send must not cause sent total overflow. #### getTokenBalance [toc] [#ShieldedTreasury-getTokenBalance] @@ -864,7 +864,7 @@ None. Requirements: - - `coin.value` must be >= `amount`. + * `coin.value` must be >= `amount`. --- @@ -928,7 +928,7 @@ None. Requirements: - - Deposit must not cause balance overflow. + * Deposit must not cause balance overflow. #### _send [toc] [#UnshieldedTreasury-_send] @@ -970,7 +970,7 @@ None. Requirements: - - The treasury must hold sufficient balance for the given token color. + * The treasury must hold sufficient balance for the given token color. #### getTokenBalance [toc] [#UnshieldedTreasury-getTokenBalance] @@ -1043,8 +1043,8 @@ None. Requirements: - - Contract must not be initialized. - - `parentCommitment` must not be the zero bytes. A zero commitment + * Contract must not be initialized. + * `parentCommitment` must not be the zero bytes. A zero commitment is the sole drain gate and would leave every deposited coin permanently unrecoverable (no preimage exists for `default>` under the domain-tagged hash). @@ -1063,7 +1063,7 @@ None. Requirements: - - Contract must be initialized. + * Contract must be initialized. #### _drain [toc] [#ForwarderPrivate-_drain] @@ -1098,10 +1098,10 @@ None. Requirements: - - Contract must be initialized. - - `parent` must not be the zero key. - - `_calculateParentCommitment(parent.bytes, opSecret) == _parentCommitment`. - - `coin.value` must be >= `value` (enforced by `sendShielded`). + * Contract must be initialized. + * `parent` must not be the zero key. + * `_calculateParentCommitment(parent.bytes, opSecret) == _parentCommitment`. + * `coin.value` must be >= `value` (enforced by `sendShielded`). **Losing the operational secret is permanent fund loss.** It @@ -1195,8 +1195,8 @@ None. Requirements: - - Contract must not be initialized. - - `parent` must not be the zero key. A zero parent would forward + * Contract must not be initialized. + * `parent` must not be the zero key. A zero parent would forward every deposit to an unspendable key with no recovery path. @@ -1222,7 +1222,7 @@ None. Requirements: - - Contract must be initialized. + * Contract must be initialized. --- @@ -1288,8 +1288,8 @@ None. Requirements: - - Contract must not be initialized. - - `parent` must not be the zero address. A zero parent would forward + * Contract must not be initialized. + * `parent` must not be the zero address. A zero parent would forward every deposit to an unspendable address with no recovery path. @@ -1313,6 +1313,6 @@ None. Requirements: - - Contract must be initialized. + * Contract must be initialized. diff --git a/content/contracts-compact/api/native-shielded-token.mdx b/content/contracts-compact/api/native-shielded-token.mdx index da5ad4ff..496db788 100644 --- a/content/contracts-compact/api/native-shielded-token.mdx +++ b/content/contracts-compact/api/native-shielded-token.mdx @@ -146,7 +146,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### symbol [toc] [#NativeShieldedToken-symbol] @@ -161,7 +161,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### decimals [toc] [#NativeShieldedToken-decimals] @@ -176,7 +176,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### tokenColor [toc] [#NativeShieldedToken-tokenColor] @@ -192,7 +192,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### _mint [toc] [#NativeShieldedToken-_mint] @@ -226,8 +226,8 @@ None. Requirements: - - Contract is initialized. - - `recipient` is not zero. + * Contract is initialized. + * `recipient` is not zero. #### _mintToSelf [toc] [#NativeShieldedToken-_mintToSelf] @@ -253,7 +253,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### _burn [toc] [#NativeShieldedToken-_burn] @@ -290,10 +290,10 @@ None. Requirements: - - Contract is initialized. - - `coin.color` is this contract's token color. - - `amount` is less than or equal to `coin.value`. - - `refundTo` is not zero (the zero key is the burn address; a zero + * Contract is initialized. + * `coin.color` is this contract's token color. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not zero (the zero key is the burn address; a zero `refundTo` would silently burn the change as well). @@ -315,9 +315,9 @@ None. Requirements: - - Contract is initialized. - - `coin.color` is this contract's token color. - - `amount` is less than or equal to `coin.value`. + * Contract is initialized. + * `coin.color` is this contract's token color. + * `amount` is less than or equal to `coin.value`. --- @@ -431,7 +431,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### symbol [toc] [#NativeShieldedTokenFamily-symbol] @@ -447,7 +447,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### decimals [toc] [#NativeShieldedTokenFamily-decimals] @@ -463,7 +463,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### tokenColor [toc] [#NativeShieldedTokenFamily-tokenColor] @@ -479,7 +479,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### _mint [toc] [#NativeShieldedTokenFamily-_mint] @@ -514,8 +514,8 @@ None. Requirements: - - Contract is initialized. - - `recipient` is not zero. + * Contract is initialized. + * `recipient` is not zero. #### _mintToSelf [toc] [#NativeShieldedTokenFamily-_mintToSelf] @@ -546,7 +546,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### _burn [toc] [#NativeShieldedTokenFamily-_burn] @@ -584,10 +584,10 @@ None. Requirements: - - Contract is initialized. - - `coin.color` is this contract's token type for `domain`. - - `amount` is less than or equal to `coin.value`. - - `refundTo` is not zero (the zero key is the burn address; a zero + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not zero (the zero key is the burn address; a zero `refundTo` would silently burn the change as well). @@ -614,9 +614,9 @@ None. Requirements: - - Contract is initialized. - - `coin.color` is this contract's token type for `domain`. - - `amount` is less than or equal to `coin.value`. + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. --- @@ -716,7 +716,7 @@ None. Requirements: - - Contract must be initialized. + * Contract must be initialized. #### assertNotInitialized [toc] [#NativeShieldedTokenCore-assertNotInitialized] @@ -731,7 +731,7 @@ None. Requirements: - - Contract must not be initialized. + * Contract must not be initialized. #### isInitialized [toc] [#NativeShieldedTokenCore-isInitialized] @@ -759,7 +759,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### symbol [toc] [#NativeShieldedTokenCore-symbol] @@ -774,7 +774,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### decimals [toc] [#NativeShieldedTokenCore-decimals] @@ -791,7 +791,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### tokenColor [toc] [#NativeShieldedTokenCore-tokenColor] @@ -807,7 +807,7 @@ None. Requirements: - - Contract is initialized. + * Contract is initialized. #### _mint [toc] [#NativeShieldedTokenCore-_mint] @@ -842,9 +842,9 @@ None. Requirements: - - Contract is initialized. - - `recipient` is not zero. - - `recipient`, if a contract address, is this contract's own address. + * Contract is initialized. + * `recipient` is not zero. + * `recipient`, if a contract address, is this contract's own address. #### _burn [toc] [#NativeShieldedTokenCore-_burn] @@ -883,12 +883,12 @@ None. Requirements: - - Contract is initialized. - - `coin.color` is this contract's token type for `domain`. - - `amount` is less than or equal to `coin.value`. - - `refundTo` is not zero (the zero key is the burn address; a zero + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not zero (the zero key is the burn address; a zero `refundTo` would silently burn the change as well). - - `refundTo`, if a contract address, is this contract's own address. + * `refundTo`, if a contract address, is this contract's own address. #### _burnFromSelf [toc] [#NativeShieldedTokenCore-_burnFromSelf] @@ -914,9 +914,9 @@ None. Requirements: - - Contract is initialized. - - `coin.color` is this contract's token type for `domain`. - - `amount` is less than or equal to `coin.value`. + * Contract is initialized. + * `coin.color` is this contract's token type for `domain`. + * `amount` is less than or equal to `coin.value`. --- @@ -1501,8 +1501,8 @@ test-only and are not published. Requirements: - - Contract is not initialized. - - `ownerId` is not zero. + * Contract is not initialized. + * `ownerId` is not zero. #### mint [toc] [#ZOwnableNativeShieldedToken-mint] @@ -1523,8 +1523,8 @@ test-only and are not published. Requirements: - - Caller is the owner. - - `recipient` is not zero. + * Caller is the owner. + * `recipient` is not zero. #### burn [toc] [#ZOwnableNativeShieldedToken-burn] @@ -1545,10 +1545,10 @@ test-only and are not published. Requirements: - - Caller is the owner. - - `coin.color` is this token's color. - - `amount` \<= `coin.value`. - - `refundTo` is not zero. + * Caller is the owner. + * `coin.color` is this token's color. + * `amount` \<= `coin.value`. + * `refundTo` is not zero. #### transferOwnership [toc] [#ZOwnableNativeShieldedToken-transferOwnership] @@ -1570,8 +1570,8 @@ test-only and are not published. Requirements: - - Caller is the owner. - - `newOwnerId` is not zero. + * Caller is the owner. + * `newOwnerId` is not zero. #### renounceOwnership [toc] [#ZOwnableNativeShieldedToken-renounceOwnership] @@ -1590,7 +1590,7 @@ test-only and are not published. Requirements: - - Caller is the owner. + * Caller is the owner. #### owner [toc] [#ZOwnableNativeShieldedToken-owner] @@ -1605,7 +1605,7 @@ test-only and are not published. Requirements: - - Contract is initialized. + * Contract is initialized. #### _computeOwnerId [toc] [#ZOwnableNativeShieldedToken-_computeOwnerId] @@ -1619,7 +1619,7 @@ test-only and are not published. Requirements: - - `pk` is a coin public key. + * `pk` is a coin public key. #### totalMinted [toc] [#ZOwnableNativeShieldedToken-totalMinted] @@ -1645,7 +1645,7 @@ test-only and are not published. Requirements: - - Contract is initialized. + * Contract is initialized. #### symbol [toc] [#ZOwnableNativeShieldedToken-symbol] @@ -1660,7 +1660,7 @@ test-only and are not published. Requirements: - - Contract is initialized. + * Contract is initialized. #### decimals [toc] [#ZOwnableNativeShieldedToken-decimals] @@ -1675,7 +1675,7 @@ test-only and are not published. Requirements: - - Contract is initialized. + * Contract is initialized. #### tokenColor [toc] [#ZOwnableNativeShieldedToken-tokenColor] @@ -1691,5 +1691,5 @@ test-only and are not published. Requirements: - - Contract is initialized. + * Contract is initialized. diff --git a/content/contracts-compact/api/non-fungible-token.mdx b/content/contracts-compact/api/non-fungible-token.mdx index 2cc16ff3..a1ef742c 100644 --- a/content/contracts-compact/api/non-fungible-token.mdx +++ b/content/contracts-compact/api/non-fungible-token.mdx @@ -139,7 +139,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - Contract is not initialized. + * Contract is not initialized. #### balanceOf [toc] [#NonFungibleToken-balanceOf] @@ -154,7 +154,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. + * The contract is initialized. #### ownerOf [toc] [#NonFungibleToken-ownerOf] @@ -169,8 +169,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - The `tokenId` must exist. + * The contract is initialized. + * The `tokenId` must exist. #### name [toc] [#NonFungibleToken-name] @@ -185,7 +185,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. + * The contract is initialized. #### symbol [toc] [#NonFungibleToken-symbol] @@ -200,7 +200,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. + * The contract is initialized. #### tokenURI [toc] [#NonFungibleToken-tokenURI] @@ -216,8 +216,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - The `tokenId` must exist. + * The contract is initialized. + * The `tokenId` must exist. Native strings and string operations aren't supported within the Compact language, @@ -239,8 +239,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - The `tokenId` must exist. + * The contract is initialized. + * The `tokenId` must exist. The URI for a given NFT is usually set when the NFT is minted. @@ -265,9 +265,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - The caller must either own the token or be an approved operator. - - `tokenId` must exist. + * The contract is initialized. + * The caller must either own the token or be an approved operator. + * `tokenId` must exist. #### getApproved [toc] [#NonFungibleToken-getApproved] @@ -282,8 +282,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `tokenId` must exist. + * The contract is initialized. + * `tokenId` must exist. #### setApprovalForAll [toc] [#NonFungibleToken-setApprovalForAll] @@ -306,8 +306,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - The `operator` cannot be zero. + * The contract is initialized. + * The `operator` cannot be zero. #### isApprovedForAll [toc] [#NonFungibleToken-isApprovedForAll] @@ -323,7 +323,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. + * The contract is initialized. #### transferFrom [toc] [#NonFungibleToken-transferFrom] @@ -348,12 +348,12 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `fromAddress` is not zero. - - `to` is not zero. - - `to` is not a ContractAddress. - - `tokenId` token must be owned by `fromAddress`. - - If the caller is not `fromAddress`, it must be approved to move this token by either [approve](#NonFungibleToken-approve) or [setApprovalForAll](#NonFungibleToken-setApprovalForAll). + * The contract is initialized. + * `fromAddress` is not zero. + * `to` is not zero. + * `to` is not a ContractAddress. + * `tokenId` token must be owned by `fromAddress`. + * If the caller is not `fromAddress`, it must be approved to move this token by either [approve](#NonFungibleToken-approve) or [setApprovalForAll](#NonFungibleToken-setApprovalForAll). #### _unsafeTransferFrom [toc] [#NonFungibleToken-_unsafeTransferFrom] @@ -378,11 +378,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `fromAddress` is not zero. - - `to` is not zero. - - `tokenId` token must be owned by `fromAddress`. - - If the caller is not `fromAddress`, it must be approved to move this token by either [approve](#NonFungibleToken-approve) or [setApprovalForAll](#NonFungibleToken-setApprovalForAll). + * The contract is initialized. + * `fromAddress` is not zero. + * `to` is not zero. + * `tokenId` token must be owned by `fromAddress`. + * If the caller is not `fromAddress`, it must be approved to move this token by either [approve](#NonFungibleToken-approve) or [setApprovalForAll](#NonFungibleToken-setApprovalForAll). #### _ownerOf [toc] [#NonFungibleToken-_ownerOf] @@ -397,7 +397,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. + * The contract is initialized. #### _getApproved [toc] [#NonFungibleToken-_getApproved] @@ -412,7 +412,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. + * The contract is initialized. #### _isAuthorized [toc] [#NonFungibleToken-_isAuthorized] @@ -429,7 +429,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. + * The contract is initialized. This function assumes that `owner` is the actual owner of `tokenId` and does not verify this @@ -450,8 +450,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `spender` has approval from `owner` for `tokenId` OR `spender` has approval to manage all of `owner`'s assets. + * The contract is initialized. + * `spender` has approval from `owner` for `tokenId` OR `spender` has approval to manage all of `owner`'s assets. This function assumes that `owner` is the actual owner of `tokenId` and does not verify this @@ -471,10 +471,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `tokenId` must not exist. - - `to` is not zero. - - `to` is not a ContractAddress. + * The contract is initialized. + * `tokenId` must not exist. + * `to` is not zero. + * `to` is not a ContractAddress. #### _unsafeMint [toc] [#NonFungibleToken-_unsafeMint] @@ -489,9 +489,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `tokenId` must not exist. - - `to` is not zero. + * The contract is initialized. + * `tokenId` must not exist. + * `to` is not zero. Transfers to contract addresses are considered unsafe because contract-to-contract @@ -514,8 +514,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `tokenId` must exist. + * The contract is initialized. + * `tokenId` must exist. #### _transfer [toc] [#NonFungibleToken-_transfer] @@ -538,10 +538,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `to` is not zero. - - `to` is not a ContractAddress. - - `tokenId` token must be owned by `fromAddress`. + * The contract is initialized. + * `to` is not zero. + * `to` is not a ContractAddress. + * `tokenId` token must be owned by `fromAddress`. #### _unsafeTransfer [toc] [#NonFungibleToken-_unsafeTransfer] @@ -559,9 +559,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `to` is not zero. - - `tokenId` token must be owned by `fromAddress`. + * The contract is initialized. + * `to` is not zero. + * `tokenId` token must be owned by `fromAddress`. Transfers to contract addresses are considered unsafe because contract-to-contract @@ -583,9 +583,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `tokenId` must exist. - - If `auth` is non 0, then this function will check that `auth` is either the owner of the token, + * The contract is initialized. + * `tokenId` must exist. + * If `auth` is non 0, then this function will check that `auth` is either the owner of the token, or approved to operate on the token (by the owner). @@ -619,9 +619,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `tokenId` must exist, unless `isExistenceRequired` is false and `auth` is zero. - - If `auth` is non 0, then this function will check that `auth` is either the owner of the token, + * The contract is initialized. + * `tokenId` must exist, unless `isExistenceRequired` is false and `auth` is zero. + * If `auth` is non 0, then this function will check that `auth` is either the owner of the token, or approved to operate on the token (by the owner). @@ -638,9 +638,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `owner` is not zero. - - `operator` is not zero. + * The contract is initialized. + * `owner` is not zero. + * `operator` is not zero. #### _requireOwned [toc] [#NonFungibleToken-_requireOwned] @@ -656,7 +656,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Requirements: - - The contract is initialized. - - `tokenId` must exist. + * The contract is initialized. + * `tokenId` must exist. diff --git a/content/contracts-compact/api/ownable.mdx b/content/contracts-compact/api/ownable.mdx index 29f53f36..31a29a89 100644 --- a/content/contracts-compact/api/ownable.mdx +++ b/content/contracts-compact/api/ownable.mdx @@ -87,9 +87,9 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is not already initialized. - - `initialOwner` is not a ContractAddress. - - `initialOwner` is not the zero address. + * Contract is not already initialized. + * `initialOwner` is not a ContractAddress. + * `initialOwner` is not the zero address. #### owner [toc] [#Ownable-owner] @@ -104,7 +104,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. + * Contract is initialized. #### transferOwnership [toc] [#Ownable-transferOwnership] @@ -125,10 +125,10 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. - - The caller is the current contract owner. - - `newOwner` is not a ContractAddress. - - `newOwner` is not the zero address. + * Contract is initialized. + * The caller is the current contract owner. + * `newOwner` is not a ContractAddress. + * `newOwner` is not the zero address. #### _unsafeTransferOwnership [toc] [#Ownable-_unsafeTransferOwnership] @@ -150,9 +150,9 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. - - The caller is the current contract owner. - - `newOwner` is not the zero address. + * Contract is initialized. + * The caller is the current contract owner. + * `newOwner` is not the zero address. #### renounceOwnership [toc] [#Ownable-renounceOwnership] @@ -169,8 +169,8 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. - - The caller is the current contract owner. + * Contract is initialized. + * The caller is the current contract owner. #### assertOnlyOwner [toc] [#Ownable-assertOnlyOwner] @@ -189,8 +189,8 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. - - The caller is the current contract owner. + * Contract is initialized. + * The caller is the current contract owner. #### _transferOwnership [toc] [#Ownable-_transferOwnership] @@ -212,8 +212,8 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. - - `newOwner` is not a ContractAddress. + * Contract is initialized. + * `newOwner` is not a ContractAddress. #### _unsafeUncheckedTransferOwnership [toc] [#Ownable-_unsafeUncheckedTransferOwnership] @@ -235,7 +235,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. + * Contract is initialized. --- @@ -349,9 +349,9 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is not initialized. - - `ownerId` is not zero. - - `instanceSalt` is not zero. + * Contract is not initialized. + * `ownerId` is not zero. + * `instanceSalt` is not zero. #### owner [toc] [#ZOwnablePK-owner] @@ -367,7 +367,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. + * Contract is initialized. #### transferOwnership [toc] [#ZOwnablePK-transferOwnership] @@ -383,9 +383,9 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. - - Caller is the current owner. - - `newOwnerId` is not an empty array. + * Contract is initialized. + * Caller is the current owner. + * `newOwnerId` is not an empty array. #### renounceOwnership [toc] [#ZOwnablePK-renounceOwnership] @@ -402,8 +402,8 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. - - Caller is the current owner. + * Contract is initialized. + * Caller is the current owner. #### assertOnlyOwner [toc] [#ZOwnablePK-assertOnlyOwner] @@ -420,8 +420,8 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. - - Caller's id (`SHA256(pk, nonce)`) when used in [_computeOwnerCommitment](#ZOwnablePK-_computeOwnerCommitment) equals + * Contract is initialized. + * Caller's id (`SHA256(pk, nonce)`) when used in [_computeOwnerCommitment](#ZOwnablePK-_computeOwnerCommitment) equals the stored `_ownerCommitment`, thus verifying themselves as the owner. @@ -439,23 +439,23 @@ The TypeScript witnesses in the repository are test-only and are not published w The `id` is expected to be computed off-chain as: `id = SHA256(pk, nonce)` - - `pk`: The owner's public key. - - `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. + * `pk`: The owner's public key. + * `nonce`: A secret nonce scoped to the instance, ideally rotated with each transfer. ## Commitment Derivation `commitment = SHA256(id, instanceSalt, counter, domain)` - - `id`: See above. - - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * `id`: See above. + * `instanceSalt`: A unique per-deployment salt, stored during initialization. This prevents commitment collisions across deployments. - - `counter`: Incremented with each ownership transfer, ensuring uniqueness + * `counter`: Incremented with each ownership transfer, ensuring uniqueness even with repeated `id` values. Cast to `Field` then `Bytes<32>` for hashing. - - `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent + * `domain`: Domain separator `"ZOwnablePK:shield:"` (padded to 32 bytes) to prevent hash collisions when extending the module or using similar commitment schemes. Requirements: - - Contract is initialized. + * Contract is initialized. #### _computeOwnerId [toc] [#ZOwnablePK-_computeOwnerId] @@ -471,11 +471,11 @@ The TypeScript witnesses in the repository are test-only and are not published w ## ID Derivation `id = SHA256(pk, nonce)` - - `pk`: The public key of the caller. This is passed explicitly to allow + * `pk`: The public key of the caller. This is passed explicitly to allow for off-chain derivation, testing, or scenarios where the caller is different from the subject of the computation. We recommend using an Air-Gapped Public Key. - - `nonce`: A secret nonce tied to the identity. The generation strategy is + * `nonce`: A secret nonce tied to the identity. The generation strategy is left to the user, offering different security/convenience trade-offs. The result is a 32-byte commitment that uniquely identifies the owner. @@ -492,7 +492,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - `pk` is not a ContractAddress. + * `pk` is not a ContractAddress. #### _transferOwnership [toc] [#ZOwnablePK-_transferOwnership] @@ -508,5 +508,5 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is initialized. + * Contract is initialized. diff --git a/content/contracts-compact/api/security.mdx b/content/contracts-compact/api/security.mdx index 90d4bd73..7321d7e0 100644 --- a/content/contracts-compact/api/security.mdx +++ b/content/contracts-compact/api/security.mdx @@ -68,7 +68,7 @@ None. Requirements: - - Contract must not be initialized. + * Contract must not be initialized. #### assertInitialized [toc] [#Initializable-assertInitialized] @@ -83,7 +83,7 @@ None. Requirements: - - Contract must be initialized. + * Contract must be initialized. #### assertNotInitialized [toc] [#Initializable-assertNotInitialized] @@ -98,7 +98,7 @@ None. Requirements: - - Contract must not be initialized. + * Contract must not be initialized. --- @@ -159,7 +159,7 @@ None. Requirements: - - Contract must be paused. + * Contract must be paused. #### assertNotPaused [toc] [#Pausable-assertNotPaused] @@ -174,7 +174,7 @@ None. Requirements: - - Contract must not be paused. + * Contract must not be paused. #### _pause [toc] [#Pausable-_pause] @@ -189,7 +189,7 @@ None. Requirements: - - Contract must not be paused. + * Contract must not be paused. #### _unpause [toc] [#Pausable-_unpause] @@ -204,7 +204,7 @@ None. Requirements: - - Contract must be paused. + * Contract must be paused. --- @@ -271,7 +271,7 @@ None. Requirements: - - `account` is a member of the allowlist. + * `account` is a member of the allowlist. #### _allow [toc] [#Allowlist-_allow] @@ -360,7 +360,7 @@ None. Requirements: - - `account` is not a member of the blocklist. + * `account` is not a member of the blocklist. #### _block [toc] [#Blocklist-_block] diff --git a/content/contracts-compact/api/shielded-access-control.mdx b/content/contracts-compact/api/shielded-access-control.mdx index 180580c0..f17fdce3 100644 --- a/content/contracts-compact/api/shielded-access-control.mdx +++ b/content/contracts-compact/api/shielded-access-control.mdx @@ -266,7 +266,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - Contract is not initialized. + * Contract is not initialized. #### DEFAULT_ADMIN_ROLE [toc] [#ShieldedAccessControl-DEFAULT_ADMIN_ROLE] @@ -294,15 +294,15 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - caller must prove ownership of `role`. - - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - - Contract is initialized. + * caller must prove ownership of `role`. + * caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * Contract is initialized. Disclosures: - - A Merkle tree path to a role commitment. - - A role commitment corresponding to a `(role, accountId)` pairing. - - A nullifier for the respective role commitment. + * A Merkle tree path to a role commitment. + * A role commitment corresponding to a `(role, accountId)` pairing. + * A nullifier for the respective role commitment. #### canProveRole [toc] [#ShieldedAccessControl-canProveRole] @@ -318,14 +318,14 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - - Contract is initialized. + * caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * Contract is initialized. Disclosures: - - A Merkle tree path to a role commitment. - - A role commitment corresponding to a `(role, accountId)` pairing. - - A nullifier for the respective role commitment. + * A Merkle tree path to a role commitment. + * A role commitment corresponding to a `(role, accountId)` pairing. + * A nullifier for the respective role commitment. #### grantRole [toc] [#ShieldedAccessControl-grantRole] @@ -343,17 +343,17 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - caller must prove they're an admin for `role`. - - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - - The `(role, accountId)` pairing must not be already revoked. - - Contract is initialized. + * caller must prove they're an admin for `role`. + * caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * The `(role, accountId)` pairing must not be already revoked. + * Contract is initialized. Disclosures: - - A Merkle tree path to a role commitment. - - A role commitment corresponding to a `(role, accountId)` pairing. - - A nullifier for the respective role commitment. - - A role identifier. + * A Merkle tree path to a role commitment. + * A role commitment corresponding to a `(role, accountId)` pairing. + * A nullifier for the respective role commitment. + * A role identifier. #### revokeRole [toc] [#ShieldedAccessControl-revokeRole] @@ -370,18 +370,18 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - caller must prove they're an admin for `role`. - - caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. - - The `(role, accountId)` pairing must have been granted. - - The `(role, accountId)` pairing must not be already revoked. - - Contract is initialized. + * caller must prove they're an admin for `role`. + * caller must not provide valid Merkle tree path for a different `(role, accountId)` pairing. + * The `(role, accountId)` pairing must have been granted. + * The `(role, accountId)` pairing must not be already revoked. + * Contract is initialized. Disclosures: - - A Merkle tree path to a role commitment. - - A role commitment corresponding to a `(role, accountId)` pairing. - - A nullifier for the respective role commitment. - - A role identifier. + * A Merkle tree path to a role commitment. + * A role commitment corresponding to a `(role, accountId)` pairing. + * A nullifier for the respective role commitment. + * A role identifier. #### renounceRole [toc] [#ShieldedAccessControl-renounceRole] @@ -410,14 +410,14 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - The caller must provide a valid `accountId` for the role. - - The `(role, accountId)` pairing must have been granted. - - The `(role, accountId)` pairing must not be already revoked. - - Contract is initialized. + * The caller must provide a valid `accountId` for the role. + * The `(role, accountId)` pairing must have been granted. + * The `(role, accountId)` pairing must not be already revoked. + * Contract is initialized. Disclosures: - - A nullifier for the respective role commitment. + * A nullifier for the respective role commitment. #### getRoleAdmin [toc] [#ShieldedAccessControl-getRoleAdmin] @@ -436,7 +436,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Disclosures: - - A role identifier. + * A role identifier. #### _grantRole [toc] [#ShieldedAccessControl-_grantRole] @@ -466,13 +466,13 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - The `(role, accountId)` pairing must not be already revoked. - - Contract is initialized. + * The `(role, accountId)` pairing must not be already revoked. + * Contract is initialized. Disclosures: - - A role commitment corresponding to a `(role, accountId)` pairing. - - A nullifier for the respective role commitment. + * A role commitment corresponding to a `(role, accountId)` pairing. + * A nullifier for the respective role commitment. #### _revokeRole [toc] [#ShieldedAccessControl-_revokeRole] @@ -496,13 +496,13 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: - - The `(role, accountId)` pairing must have been granted. - - The `(role, accountId)` pairing must not be already revoked. - - Contract is initialized. + * The `(role, accountId)` pairing must have been granted. + * The `(role, accountId)` pairing must not be already revoked. + * Contract is initialized. Disclosures: - - A nullifier for the respective role commitment. + * A nullifier for the respective role commitment. #### _setRoleAdmin [toc] [#ShieldedAccessControl-_setRoleAdmin] @@ -521,8 +521,8 @@ The TypeScript witnesses in the repository are test-only and are not published w Disclosures: - - The role identifier - - The admin identifier + * The role identifier + * The admin identifier @@ -545,16 +545,16 @@ The TypeScript witnesses in the repository are test-only and are not published w The `accountId` is expected to be computed as: `accountId = SHA256(secretKey, instanceSalt, accountIdDomain)` - - `secretKey`: A 32-byte cryptographically secure random value. + * `secretKey`: A 32-byte cryptographically secure random value. ## Role Commitment Derivation `roleCommitment = SHA256(role, accountId, instanceSalt, commitmentDomain)` - - `accountId`: See above. - - `role`: A unique role identifier. - - `instanceSalt`: A unique per-deployment salt, stored during initialization. + * `accountId`: See above. + * `role`: A unique role identifier. + * `instanceSalt`: A unique per-deployment salt, stored during initialization. This prevents commitment collisions across deployments. - - `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent + * `commitmentDomain`: Domain separator `"ShieldedAccessControl:commitment"` (padded to 32 bytes) to prevent hash collisions when extending the module or using similar commitment schemes. @@ -571,8 +571,8 @@ The TypeScript witnesses in the repository are test-only and are not published w ## Role Nullifier Derivation `roleNullifier = SHA256(roleCommitment, nullifierDomain)` - - `roleCommitment`: See [computeRoleCommitment](#ShieldedAccessControl-computeRoleCommitment). - - `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent + * `roleCommitment`: See [computeRoleCommitment](#ShieldedAccessControl-computeRoleCommitment). + * `nullifierDomain`: Domain separator `"ShieldedAccessControl:nullifier"` (padded to 32 bytes) to prevent hash collisions when extending the module or using similar commitment schemes. @@ -593,15 +593,15 @@ The TypeScript witnesses in the repository are test-only and are not published w OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the privacy guarantees of this system: - - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools, + * **Never log or persist** the `secretKey` in plaintext — avoid browser devtools, application logs, analytics pipelines, or any observable side-channel. - - **Store offline or in secure enclaves** — hardware security modules (HSMs), + * **Store offline or in secure enclaves** — hardware security modules (HSMs), air-gapped devices, or encrypted vaults are strongly preferred over hot storage. - - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()` + * **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()` or equivalent; weak or predictable keys can be brute-forced to reveal your identity. - - **Treat key loss as identity loss** — a lost key cannot be recovered. Back up + * **Treat key loss as identity loss** — a lost key cannot be recovered. Back up keys securely before using them in role commitments. - - **Avoid calling this circuit in untrusted environments** — executing this in an + * **Avoid calling this circuit in untrusted environments** — executing this in an unverified browser extension, compromised runtime, or shared machine may expose the key to a malicious observer. diff --git a/content/contracts-compact/api/utils.mdx b/content/contracts-compact/api/utils.mdx index 20adb9fb..6304705a 100644 --- a/content/contracts-compact/api/utils.mdx +++ b/content/contracts-compact/api/utils.mdx @@ -146,14 +146,14 @@ None. OpSec: The `secretKey` parameter is a sensitive secret. Mishandling it can permanently compromise the security of this system: - - **Never log or persist** the `secretKey` in plaintext — avoid browser devtools, + * **Never log or persist** the `secretKey` in plaintext — avoid browser devtools, application logs, analytics pipelines, or any observable side-channel. - - **Store offline or in secure enclaves** — hardware security modules (HSMs), + * **Store offline or in secure enclaves** — hardware security modules (HSMs), air-gapped devices, or encrypted vaults are strongly preferred over hot storage. - - **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()` + * **Use cryptographically secure randomness** — generate keys with `crypto.getRandomValues()` or equivalent; weak or predictable keys can be brute-forced to reveal your identity. - - **Treat key loss as identity loss** — a lost key cannot be recovered. - - **Avoid calling this circuit in untrusted environments** — executing this in an + * **Treat key loss as identity loss** — a lost key cannot be recovered. + * **Avoid calling this circuit in untrusted environments** — executing this in an unverified browser extension, compromised runtime, or shared machine may expose the key to a malicious observer. From b7588c8d863d6952b4107fa808ceb602eaa00a3d Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 25 Oct 2026 23:54:28 -0300 Subject: [PATCH 05/10] fix typo --- content/contracts-compact/api/fungible-token.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/contracts-compact/api/fungible-token.mdx b/content/contracts-compact/api/fungible-token.mdx index 0315ff38..d0120079 100644 --- a/content/contracts-compact/api/fungible-token.mdx +++ b/content/contracts-compact/api/fungible-token.mdx @@ -379,7 +379,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Requirements: * Contract is initialized. - * `fromAddress` is not be the zero address. + * `fromAddress` is not the zero address. * `fromAddress` must have at least a balance of `value`. * `to` must not be the zero address. * `to` must not be a ContractAddress. From f0b94f19aafba7eef4aa7d7eed67eb719cf2998e Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 29 Oct 2026 16:48:07 -0300 Subject: [PATCH 06/10] improve intro section in index --- content/contracts-compact/index.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/content/contracts-compact/index.mdx b/content/contracts-compact/index.mdx index 896cae91..ae64ef1b 100644 --- a/content/contracts-compact/index.mdx +++ b/content/contracts-compact/index.mdx @@ -12,8 +12,10 @@ title: Contracts for Compact This library consists of modules to build custom smart contracts. -This repo contains highly experimental code. +Pre-release versions (`-alpha`, `-rc`) contain experimental, unaudited code. Expect rapid iteration. **Use at your own risk.** +For production, use a stable release (no `-alpha` or `-rc` suffix) on the Midnight ledger it targets. +See the [release and audit table](https://github.com/OpenZeppelin/compact-contracts/blob/v0.3.0/README.md#security) in the repository. ## Usage From 3ced9fefcd946c2f3ec4460a3362c21b41225617 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 29 Oct 2026 17:21:26 -0300 Subject: [PATCH 07/10] improve subgroup guarantee --- content/contracts-compact/crypto.mdx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/content/contracts-compact/crypto.mdx b/content/contracts-compact/crypto.mdx index d110b017..602e1867 100644 --- a/content/contracts-compact/crypto.mdx +++ b/content/contracts-compact/crypto.mdx @@ -156,6 +156,9 @@ by cofactor clearing at the constraint level, so an off-curve, low-order, or mix This holds for points a caller passes as arguments or witnesses (a recipient `pk`, the message point of `encryptPoint`), which is why the modules perform no in-circuit membership check and why the homomorphic algebra is sound. The identity point is a valid subgroup member, so it is NOT caught by this guarantee; that is why the modules assert against it explicitly. +This is a substantiated-but-unaudited assumption: +it was verified in the source of the pinned Midnight runtime and is regression-tested by the library's runtime canary test, +but compactc and midnight-ledger are unaudited, so re-verify it before using the modules on a newer runtime. * ***Negation is scalar multiplication by `ℓ - 1`, never by `-1`.*** A `Field` literal `-1` is the BLS scalar modulus minus one, which exceeds the Jubjub scalar order and faults `ecMul`. For any `P` in the subgroup, `(ℓ - 1) * P = -P`, and that identity is what `negate`, `sub`, and the decryption check build on. Do not multiply by `ℓ` itself either: it is out of range and faults as well. From 027d82ba770cd82a4636bd93dffde324e92bf15d Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 29 Oct 2026 17:28:32 -0300 Subject: [PATCH 08/10] fix dead anchors --- content/contracts-compact/confidential-fungible-token.mdx | 2 +- content/contracts-compact/fungible-token.mdx | 2 +- content/contracts-compact/multi-token.mdx | 2 +- content/contracts-compact/multisig.mdx | 2 +- content/contracts-compact/native-shielded-token.mdx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/content/contracts-compact/confidential-fungible-token.mdx b/content/contracts-compact/confidential-fungible-token.mdx index 012b81bd..0c2b324e 100644 --- a/content/contracts-compact/confidential-fungible-token.mdx +++ b/content/contracts-compact/confidential-fungible-token.mdx @@ -4,7 +4,7 @@ title: ConfidentialFungibleToken {/* links */} [FungibleToken]: ./fungible-token.mdx -[Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [Ownable]: ./ownable.mdx [crypto guide]: ./crypto [ElGamal]: api/crypto#ElGamal diff --git a/content/contracts-compact/fungible-token.mdx b/content/contracts-compact/fungible-token.mdx index cb53b204..85995e86 100644 --- a/content/contracts-compact/fungible-token.mdx +++ b/content/contracts-compact/fungible-token.mdx @@ -5,7 +5,7 @@ title: FungibleToken {/* links */} [fungible tokens]: ../contracts/5.x/tokens.mdx#different-kinds-of-tokens [EIP-20]: https://eips.ethereum.org/EIPS/eip-20 -[Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [_mint]: api/fungible-token#FungibleToken-_mint [transfer]: api/fungible-token#FungibleToken-transfer [approve]: api/fungible-token#FungibleToken-approve diff --git a/content/contracts-compact/multi-token.mdx b/content/contracts-compact/multi-token.mdx index 9e2a4b40..5fa6faf5 100644 --- a/content/contracts-compact/multi-token.mdx +++ b/content/contracts-compact/multi-token.mdx @@ -5,7 +5,7 @@ title: MultiToken {/* links */} [EIP-1155]: https://eips.ethereum.org/EIPS/eip-1155 [ERC165]: https://eips.ethereum.org/EIPS/eip-165 -[Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [_mint]: ./api/multi-token#MultiToken-_mint [setApprovalForAll]: ./api/multi-token#MultiToken-setApprovalForAll [transferFrom]: ./api/multi-token#MultiToken-transferFrom diff --git a/content/contracts-compact/multisig.mdx b/content/contracts-compact/multisig.mdx index 21b1c8c6..bfe4077c 100644 --- a/content/contracts-compact/multisig.mdx +++ b/content/contracts-compact/multisig.mdx @@ -3,7 +3,7 @@ title: Multisig --- {/* links */} -[Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [Ownable]: ./ownable [Signer API]: api/multisig#Signer [initialize]: api/multisig#Signer-initialize diff --git a/content/contracts-compact/native-shielded-token.mdx b/content/contracts-compact/native-shielded-token.mdx index 9599346c..34d32c49 100644 --- a/content/contracts-compact/native-shielded-token.mdx +++ b/content/contracts-compact/native-shielded-token.mdx @@ -3,7 +3,7 @@ title: NativeShieldedToken --- {/* links */} -[Module/Contract Pattern]: ./extensibility.mdx#the_module_contract_pattern +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [ZOwnablePK]: ./ownable [wit_secretNonce]: api/ownable#ZOwnablePK-wit_secretNonce [ZOwnablePK__computeOwnerId]: api/ownable#ZOwnablePK-_computeOwnerId From 99cca8c7dc799faf334f43e7160c9f0351552fb9 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 29 Oct 2026 17:36:37 -0300 Subject: [PATCH 09/10] improve id model snippets --- content/contracts-compact/access-control.mdx | 9 +++++---- content/contracts-compact/fungible-token.mdx | 10 ++++++++-- content/contracts-compact/multi-token.mdx | 10 ++++++++-- content/contracts-compact/non-fungible-token.mdx | 10 ++++++++-- content/contracts-compact/ownable.mdx | 6 ++++-- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/content/contracts-compact/access-control.mdx b/content/contracts-compact/access-control.mdx index d80d342c..b4ec0879 100644 --- a/content/contracts-compact/access-control.mdx +++ b/content/contracts-compact/access-control.mdx @@ -75,12 +75,13 @@ use [computeAccountId] from the [Utils] module: import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" prefix Utils_; -const accountId = Utils_computeAccountId(secretKey); +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} ``` -`computeAccountId` is a pure circuit, so it can be evaluated off-chain (for example through a contract simulator), -or recomputed in TypeScript as a `persistentHash` over a `Vector<1, Bytes<32>>` containing the key -(see the [Ownable] guide for a runtime snippet). +A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side. +The [Utils guide](./utils#deriving-an-identifier-off-chain) shows the equivalent computation with the Compact runtime. ### Using `AccessControl` diff --git a/content/contracts-compact/fungible-token.mdx b/content/contracts-compact/fungible-token.mdx index 85995e86..ffedf221 100644 --- a/content/contracts-compact/fungible-token.mdx +++ b/content/contracts-compact/fungible-token.mdx @@ -75,11 +75,17 @@ Users who want cross-contract unlinkability can use a different secret key per c To derive an account identifier off-chain (for example, to pass as the `recipient` of a mint), use [computeAccountId] from the Utils module: ```typescript -import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" prefix Utils_; +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; -const accountId = Utils_computeAccountId(secretKey); +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} ``` +A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side. +The [Utils guide](./utils#deriving-an-identifier-off-chain) shows the equivalent computation with the Compact runtime. + The TypeScript witness implementation lives with the DApp, not with the library. The witnesses in the `compact-contracts` repository are test-only and are not published; consumers author their own `wit_FungibleTokenSK` that returns the caller's 32-byte secret key. diff --git a/content/contracts-compact/multi-token.mdx b/content/contracts-compact/multi-token.mdx index 5fa6faf5..4a567e74 100644 --- a/content/contracts-compact/multi-token.mdx +++ b/content/contracts-compact/multi-token.mdx @@ -93,11 +93,17 @@ Users who want cross-contract unlinkability can use a different secret key per c To derive an account identifier off-chain (for example, to pass as the `recipient` of a mint), use [computeAccountId] from the Utils module: ```typescript -import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" prefix Utils_; +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; -const accountId = Utils_computeAccountId(secretKey); +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} ``` +A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side. +The [Utils guide](./utils#deriving-an-identifier-off-chain) shows the equivalent computation with the Compact runtime. + The TypeScript witness implementation lives with the DApp, not with the library. The witnesses in the `compact-contracts` repository are test-only and are not published; consumers author their own `wit_MultiTokenSK` that returns the caller's 32-byte secret key. diff --git a/content/contracts-compact/non-fungible-token.mdx b/content/contracts-compact/non-fungible-token.mdx index f6ae620b..0d31dd42 100644 --- a/content/contracts-compact/non-fungible-token.mdx +++ b/content/contracts-compact/non-fungible-token.mdx @@ -84,11 +84,17 @@ Users who want cross-contract unlinkability can use a different secret key per c To derive an account identifier off-chain (for example, to pass as the `recipient` of a mint), use [computeAccountId] from the Utils module: ```typescript -import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" prefix Utils_; +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; -const accountId = Utils_computeAccountId(secretKey); +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} ``` +A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side. +The [Utils guide](./utils#deriving-an-identifier-off-chain) shows the equivalent computation with the Compact runtime. + The TypeScript witness implementation lives with the DApp, not with the library. The witnesses in the `compact-contracts` repository are test-only and are not published; consumers author their own `wit_NonFungibleTokenSK` that returns the caller's 32-byte secret key. diff --git a/content/contracts-compact/ownable.mdx b/content/contracts-compact/ownable.mdx index 0c1f2b2c..2c25eb5b 100644 --- a/content/contracts-compact/ownable.mdx +++ b/content/contracts-compact/ownable.mdx @@ -67,10 +67,12 @@ use [computeAccountId] from the [Utils] module: import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" prefix Utils_; -const accountId = Utils_computeAccountId(secretKey); +export pure circuit computeAccountId(secretKey: Bytes<32>): Bytes<32> { + return Utils_computeAccountId(secretKey); +} ``` -`computeAccountId` is a pure circuit, so it can be evaluated off-chain (for example through a contract simulator). +A contract that re-exports it this way exposes `pureCircuits.computeAccountId(secretKey)` to its TypeScript side. The same value can be computed directly in TypeScript with the Compact runtime: ```typescript From 5d8598061003f804646b62c7e619bf53cfe13d43 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 29 Oct 2026 17:38:51 -0300 Subject: [PATCH 10/10] qualify callout --- content/contracts-compact/native-shielded-token.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/content/contracts-compact/native-shielded-token.mdx b/content/contracts-compact/native-shielded-token.mdx index 34d32c49..a7b867b2 100644 --- a/content/contracts-compact/native-shielded-token.mdx +++ b/content/contracts-compact/native-shielded-token.mdx @@ -83,10 +83,11 @@ Private: A change output does reveal that a burn was partial. -Contract-initiated mints and refunds do not create coin ciphertexts, so a recipient's wallet cannot discover -those coins by scanning the chain. The `ShieldedCoinInfo` a circuit returns is the only copy of the coin: -the DApp must capture it and deliver it to the recipient (or persist it, when the contract is the recipient). -Dropping it strands the value irrecoverably. +A contract-initiated mint or refund carries a coin ciphertext only when the DApp can resolve the recipient's +encryption key at proving time (its own, or one supplied through `additionalCoinEncPublicKeyMappings`). +Without that, the recipient's wallet cannot discover the coin by scanning the chain, and the `ShieldedCoinInfo` +a circuit returns is the only copy of the coin: the DApp must capture it and deliver it to the recipient +(or persist it, when the contract is the recipient). Dropping it strands the value irrecoverably. ## Single token or a family