From 5c85c497ed0b7b26da76cd0ad3115105c0324b16 Mon Sep 17 00:00:00 2001 From: andrew Date: Sun, 25 Oct 2026 13:19:19 -0300 Subject: [PATCH 01/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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 From 44912a66ff79c737a97a947fef502060e086405c Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 4 Nov 2026 16:23:43 +0100 Subject: [PATCH 11/12] docs(midnight): version the contracts-compact set Move the 0.3.0 pages under 0.3.x/, restore main's 0.1.x pages under 0.1.x/ with their links pinned to v0.1.0, and wire the contracts-cairo layout: landing page, latest-versions.js, Previous Versions nav, outdated banner, search filter and Netlify passthroughs. Old flat URLs redirect to the latest set. Two moved pages change beyond the rename: api/utils.mdx linked the Utils guide by absolute path and the token guides linked ../contracts/5.x, both of which resolve inside the version folder after the move. --- .../0.1.x/access-control.mdx | 254 +++ .../0.1.x/api/access-control.mdx | 347 ++++ .../0.1.x/api/fungible-token.mdx | 586 ++++++ .../0.1.x/api/multi-token.mdx | 490 +++++ .../0.1.x/api/non-fungible-token.mdx | 677 +++++++ .../contracts-compact/0.1.x/api/ownable.mdx | 487 +++++ .../contracts-compact/0.1.x/api/security.mdx | 193 ++ .../0.1.x/api/shielded-access-control.mdx | 609 ++++++ content/contracts-compact/0.1.x/api/utils.mdx | 107 ++ .../contracts-compact/0.1.x/extensibility.mdx | 175 ++ .../0.1.x/fungible-token.mdx | 153 ++ content/contracts-compact/0.1.x/index.mdx | 175 ++ .../contracts-compact/0.1.x/multi-token.mdx | 162 ++ .../0.1.x/non-fungible-token.mdx | 164 ++ content/contracts-compact/0.1.x/ownable.mdx | 384 ++++ content/contracts-compact/0.1.x/security.mdx | 103 + .../0.1.x/shielded-access-control.mdx | 193 ++ content/contracts-compact/0.1.x/utils.mdx | 27 + .../0.1.x/utils/constants.js | 2 + .../{ => 0.1.x}/utils/replacements.ts | 2 +- .../{ => 0.1.x}/zk-circuits-101.mdx | 0 .../{ => 0.3.x}/access-control.mdx | 0 .../{ => 0.3.x}/api/access-control.mdx | 0 .../api/confidential-fungible-token.mdx | 0 .../{ => 0.3.x}/api/crypto.mdx | 0 .../{ => 0.3.x}/api/fungible-token.mdx | 0 .../{ => 0.3.x}/api/multi-token.mdx | 0 .../{ => 0.3.x}/api/multisig.mdx | 0 .../{ => 0.3.x}/api/native-shielded-token.mdx | 0 .../{ => 0.3.x}/api/non-fungible-token.mdx | 0 .../{ => 0.3.x}/api/ownable.mdx | 0 .../{ => 0.3.x}/api/security.mdx | 0 .../api/shielded-access-control.mdx | 0 .../{ => 0.3.x}/api/utils.mdx | 2 +- .../confidential-fungible-token.mdx | 0 .../contracts-compact/{ => 0.3.x}/crypto.mdx | 0 .../{ => 0.3.x}/extensibility.mdx | 0 .../0.3.x/fungible-token.mdx | 205 ++ content/contracts-compact/0.3.x/index.mdx | 180 ++ .../{ => 0.3.x}/multi-token.mdx | 0 .../{ => 0.3.x}/multisig.mdx | 0 .../{ => 0.3.x}/native-shielded-token.mdx | 0 .../0.3.x/non-fungible-token.mdx | 216 +++ .../contracts-compact/{ => 0.3.x}/ownable.mdx | 0 .../{ => 0.3.x}/security.mdx | 0 .../{ => 0.3.x}/shielded-access-control.mdx | 0 .../contracts-compact/{ => 0.3.x}/utils.mdx | 0 .../{ => 0.3.x}/utils/constants.js | 0 .../0.3.x/utils/replacements.ts | 9 + .../0.3.x/zk-circuits-101.mdx | 36 + .../0.4.x/access-control.mdx | 281 +++ .../0.4.x/api/access-control.mdx | 286 +++ .../0.4.x/api/confidential-fungible-token.mdx | 747 ++++++++ .../contracts-compact/0.4.x/api/crypto.mdx | 523 +++++ .../0.4.x/api/fungible-token.mdx | 525 +++++ .../0.4.x/api/multi-token.mdx | 445 +++++ .../contracts-compact/0.4.x/api/multisig.mdx | 1318 +++++++++++++ .../0.4.x/api/native-shielded-token.mdx | 1695 +++++++++++++++++ .../0.4.x/api/non-fungible-token.mdx | 662 +++++++ .../contracts-compact/0.4.x/api/ownable.mdx | 512 +++++ .../contracts-compact/0.4.x/api/security.mdx | 386 ++++ .../0.4.x/api/shielded-access-control.mdx | 613 ++++++ content/contracts-compact/0.4.x/api/utils.mdx | 199 ++ .../0.4.x/confidential-fungible-token.mdx | 407 ++++ content/contracts-compact/0.4.x/crypto.mdx | 264 +++ .../contracts-compact/0.4.x/extensibility.mdx | 175 ++ .../{ => 0.4.x}/fungible-token.mdx | 0 content/contracts-compact/0.4.x/index.mdx | 180 ++ .../contracts-compact/0.4.x/multi-token.mdx | 213 +++ content/contracts-compact/0.4.x/multisig.mdx | 355 ++++ .../0.4.x/native-shielded-token.mdx | 375 ++++ .../{ => 0.4.x}/non-fungible-token.mdx | 0 content/contracts-compact/0.4.x/ownable.mdx | 439 +++++ content/contracts-compact/0.4.x/security.mdx | 282 +++ .../0.4.x/shielded-access-control.mdx | 226 +++ content/contracts-compact/0.4.x/utils.mdx | 114 ++ .../0.4.x/utils/constants.js | 2 + .../0.4.x/utils/replacements.ts | 9 + .../0.4.x/zk-circuits-101.mdx | 36 + .../contracts-compact/compact-replacements.ts | 9 + content/contracts-compact/index.mdx | 275 +-- content/contracts-compact/latest-version.js | 1 - content/contracts-compact/latest-versions.js | 1 + netlify.toml | 26 +- public/llms.txt | 109 +- source.config.ts | 4 +- src/components/banners/version-banner.tsx | 5 + src/lib/export-search-indexes.ts | 22 +- src/navigation/index.ts | 2 +- .../{midnight.json => midnight/current.json} | 59 +- src/navigation/midnight/index.ts | 22 + .../midnight/previous-versions/0.1.x.json | 131 ++ .../midnight/previous-versions/0.3.x.json | 171 ++ 93 files changed, 17807 insertions(+), 237 deletions(-) create mode 100644 content/contracts-compact/0.1.x/access-control.mdx create mode 100644 content/contracts-compact/0.1.x/api/access-control.mdx create mode 100644 content/contracts-compact/0.1.x/api/fungible-token.mdx create mode 100644 content/contracts-compact/0.1.x/api/multi-token.mdx create mode 100644 content/contracts-compact/0.1.x/api/non-fungible-token.mdx create mode 100644 content/contracts-compact/0.1.x/api/ownable.mdx create mode 100644 content/contracts-compact/0.1.x/api/security.mdx create mode 100644 content/contracts-compact/0.1.x/api/shielded-access-control.mdx create mode 100644 content/contracts-compact/0.1.x/api/utils.mdx create mode 100644 content/contracts-compact/0.1.x/extensibility.mdx create mode 100644 content/contracts-compact/0.1.x/fungible-token.mdx create mode 100644 content/contracts-compact/0.1.x/index.mdx create mode 100644 content/contracts-compact/0.1.x/multi-token.mdx create mode 100644 content/contracts-compact/0.1.x/non-fungible-token.mdx create mode 100644 content/contracts-compact/0.1.x/ownable.mdx create mode 100644 content/contracts-compact/0.1.x/security.mdx create mode 100644 content/contracts-compact/0.1.x/shielded-access-control.mdx create mode 100644 content/contracts-compact/0.1.x/utils.mdx create mode 100644 content/contracts-compact/0.1.x/utils/constants.js rename content/contracts-compact/{ => 0.1.x}/utils/replacements.ts (80%) rename content/contracts-compact/{ => 0.1.x}/zk-circuits-101.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/access-control.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/access-control.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/confidential-fungible-token.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/crypto.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/fungible-token.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/multi-token.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/multisig.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/native-shielded-token.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/non-fungible-token.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/ownable.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/security.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/shielded-access-control.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/api/utils.mdx (98%) rename content/contracts-compact/{ => 0.3.x}/confidential-fungible-token.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/crypto.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/extensibility.mdx (100%) create mode 100644 content/contracts-compact/0.3.x/fungible-token.mdx create mode 100644 content/contracts-compact/0.3.x/index.mdx rename content/contracts-compact/{ => 0.3.x}/multi-token.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/multisig.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/native-shielded-token.mdx (100%) create mode 100644 content/contracts-compact/0.3.x/non-fungible-token.mdx rename content/contracts-compact/{ => 0.3.x}/ownable.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/security.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/shielded-access-control.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/utils.mdx (100%) rename content/contracts-compact/{ => 0.3.x}/utils/constants.js (100%) create mode 100644 content/contracts-compact/0.3.x/utils/replacements.ts create mode 100644 content/contracts-compact/0.3.x/zk-circuits-101.mdx create mode 100644 content/contracts-compact/0.4.x/access-control.mdx create mode 100644 content/contracts-compact/0.4.x/api/access-control.mdx create mode 100644 content/contracts-compact/0.4.x/api/confidential-fungible-token.mdx create mode 100644 content/contracts-compact/0.4.x/api/crypto.mdx create mode 100644 content/contracts-compact/0.4.x/api/fungible-token.mdx create mode 100644 content/contracts-compact/0.4.x/api/multi-token.mdx create mode 100644 content/contracts-compact/0.4.x/api/multisig.mdx create mode 100644 content/contracts-compact/0.4.x/api/native-shielded-token.mdx create mode 100644 content/contracts-compact/0.4.x/api/non-fungible-token.mdx create mode 100644 content/contracts-compact/0.4.x/api/ownable.mdx create mode 100644 content/contracts-compact/0.4.x/api/security.mdx create mode 100644 content/contracts-compact/0.4.x/api/shielded-access-control.mdx create mode 100644 content/contracts-compact/0.4.x/api/utils.mdx create mode 100644 content/contracts-compact/0.4.x/confidential-fungible-token.mdx create mode 100644 content/contracts-compact/0.4.x/crypto.mdx create mode 100644 content/contracts-compact/0.4.x/extensibility.mdx rename content/contracts-compact/{ => 0.4.x}/fungible-token.mdx (100%) create mode 100644 content/contracts-compact/0.4.x/index.mdx create mode 100644 content/contracts-compact/0.4.x/multi-token.mdx create mode 100644 content/contracts-compact/0.4.x/multisig.mdx create mode 100644 content/contracts-compact/0.4.x/native-shielded-token.mdx rename content/contracts-compact/{ => 0.4.x}/non-fungible-token.mdx (100%) create mode 100644 content/contracts-compact/0.4.x/ownable.mdx create mode 100644 content/contracts-compact/0.4.x/security.mdx create mode 100644 content/contracts-compact/0.4.x/shielded-access-control.mdx create mode 100644 content/contracts-compact/0.4.x/utils.mdx create mode 100644 content/contracts-compact/0.4.x/utils/constants.js create mode 100644 content/contracts-compact/0.4.x/utils/replacements.ts create mode 100644 content/contracts-compact/0.4.x/zk-circuits-101.mdx create mode 100644 content/contracts-compact/compact-replacements.ts delete mode 100644 content/contracts-compact/latest-version.js create mode 100644 content/contracts-compact/latest-versions.js rename src/navigation/{midnight.json => midnight/current.json} (59%) create mode 100644 src/navigation/midnight/index.ts create mode 100644 src/navigation/midnight/previous-versions/0.1.x.json create mode 100644 src/navigation/midnight/previous-versions/0.3.x.json diff --git a/content/contracts-compact/0.1.x/access-control.mdx b/content/contracts-compact/0.1.x/access-control.mdx new file mode 100644 index 00000000..846282ee --- /dev/null +++ b/content/contracts-compact/0.1.x/access-control.mdx @@ -0,0 +1,254 @@ +--- +title: AccessControl +--- + +{/* links */} +[Role-Based Access Control (RBAC)]: https://en.wikipedia.org/wiki/Role-based_access_control +[principle of least privilege]: https://en.wikipedia.org/wiki/Principle_of_least_privilege + +[FungibleToken]: ./fungible-token.mdx +[Ownable]: ./ownable.mdx +[Initializable]: ./security#initializable +[AccessControl]: api/access-control +[ShieldedAccessControl]: ./shielded-access-control.mdx + +[assertOnlyRole]: api/access-control#AccessControl-assertOnlyRole +[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 + +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. + +Roles can be enforced using the [assertOnlyRole] circuit. +Separately, you will be able to define rules for how accounts can be granted a role, have it revoked, and more. + + + For a privacy-preserving variant that hides role holder identities on-chain, see [ShieldedAccessControl]. + + +## Role-Based Access Control + +While the simplicity of _ownership_ can be useful for simple systems or quick prototyping, +different levels of authorization are often needed. +You may want for an account to have permission to ban users from a system, but not create new tokens. +[Role-Based Access Control (RBAC)] offers flexibility in this regard. + +In essence, we will be defining multiple _roles_, each allowed to perform different sets of actions. +An account may have, for example, 'moderator', 'minter' or 'admin' roles, +which you will then check for instead of simply using `assertOnlyOwner`. +This check can be enforced through the [assertOnlyRole] circuit. +Separately, you will be able to define rules for how accounts can be granted a role, have it revoked, and more. + +Most software uses access control systems that are role-based: +some users are regular users, some may be supervisors or managers, and a few will often have administrative privileges. + +### Identity Model + +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, +pseudonymous on-chain identity. + +The same secret key produces the same identity across all contracts (analogous to Solidity's `msg.sender`). +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]. + +### Using `AccessControl` + +The Compact contracts library provides `AccessControl` for implementing role-based access control. +Its usage is straightforward: for each role that you want to define, +you will create a new role identifier that is used to grant, revoke, and check if an account has that role. + +Here's a simple example of using `AccessControl` with [FungibleToken] to define a 'minter' role, +which allows accounts that have this role to create new tokens: + +```ts +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl" + prefix AccessControl_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +export sealed ledger MINTER_ROLE: Bytes<32>; + +/** + * Initialize FungibleToken and MINTER_ROLE + */ +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, + minter: Either, ContractAddress> +) { + FungibleToken_initialize(name, symbol, decimals); + MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); + AccessControl__grantRole(MINTER_ROLE, minter); +} + +export circuit mint(recipient: Either, ContractAddress>, value: Uint<128>): [] { + AccessControl_assertOnlyRole(MINTER_ROLE); + FungibleToken__mint(recipient, value); +} + +``` + +Make sure you fully understand how [AccessControl] works before using it on your system, +or copy-pasting the examples from this guide. + + +While clear and explicit, this isn't anything we wouldn't have been able to achieve with [Ownable]. +Indeed, where `AccessControl` shines is in scenarios where granular permissions are required, +which can be implemented by defining _multiple_ roles. + + +Let's augment our FungibleToken example by also defining a 'burner' role, which lets accounts destroy tokens. + +```ts +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl" + prefix AccessControl_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +export sealed ledger MINTER_ROLE: Bytes<32>; +export sealed ledger BURNER_ROLE: Bytes<32>; + +/** + * Initialize FungibleToken and MINTER_ROLE + */ +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, + minter: Either, ContractAddress>, + burner: Either, ContractAddress> +) { + FungibleToken_initialize(name, symbol, decimals); + MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); + BURNER_ROLE = persistentHash>(pad(32, "BURNER_ROLE")); + AccessControl__grantRole(MINTER_ROLE, minter); + AccessControl__grantRole(BURNER_ROLE, burner); +} + +export circuit mint(recipient: Either, ContractAddress>, value: Uint<128>): [] { + AccessControl_assertOnlyRole(MINTER_ROLE); + FungibleToken__mint(recipient, value); +} + +export circuit burn(recipient: Either, ContractAddress>, value: Uint<128>): [] { + AccessControl_assertOnlyRole(BURNER_ROLE); + FungibleToken__burn(recipient, value); +} + +``` + +So clean! By splitting concerns this way, +more granular levels of permission may be implemented than were possible with the simpler _ownership_ approach to access control. +Limiting what each component of a system is able to do is known as the [principle of least privilege], +and is a good security practice. +Note that each account may still have more than one role, if so desired. + +### Granting and Revoking Roles + +The FungibleToken example above uses [_grantRole], +an internal circuit that is useful when programmatically assigning roles (such as during construction). +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. +To grant and revoke roles dynamically, you will need help from the _role's admin_. + +Every role has an associated admin role, +which grants permission to call the [grantRole] and [revokeRole] circuits. +A role can be granted or revoked by using these if the calling account has the corresponding admin role. +Multiple roles may have the same admin role to make management easier. +A role's admin can even be the same role itself, +which would cause accounts with that role to be able to also grant and revoke it. + +This mechanism can be used to create complex permissioning structures resembling organizational charts, +but it also provides an easy way to manage simpler applications. +`AccessControl` includes a special role, called `DEFAULT_ADMIN_ROLE`, +which acts as the **default admin role for all roles**. +An account with this role will be able to manage any other role, +unless [_setRoleAdmin] is used to select a new admin role. + +Since it is the admin for all roles by default, +and in fact it is also its own admin, this role carries significant risk. + +Let's take a look at the FungibleToken example, this time taking advantage of the default admin role: + +```ts +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl" + prefix AccessControl_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +export sealed ledger MINTER_ROLE: Bytes<32>; +export sealed ledger BURNER_ROLE: Bytes<32>; + +/** + * Initialize FungibleToken and MINTER_ROLE + */ +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, + defaultAdmin: Either, ContractAddress> +) { + FungibleToken_initialize(name, symbol, decimals); + MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); + 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). + AccessControl__grantRole(AccessControl_DEFAULT_ADMIN_ROLE(), defaultAdmin); +} + +export circuit mint(recipient: Either, ContractAddress>, value: Uint<128>): [] { + AccessControl_assertOnlyRole(MINTER_ROLE); + FungibleToken__mint(recipient, value); +} + +export circuit burn(recipient: Either, ContractAddress>, value: Uint<128>): [] { + AccessControl_assertOnlyRole(BURNER_ROLE); + FungibleToken__burn(recipient, value); +} + +``` + +Note that, unlike the previous examples, no accounts are granted the 'minter' or 'burner' roles. +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)`, +where `secretKey` is a 32-byte cryptographically secure random value held by the deployer. + +Dynamic role allocation is often a desirable property, +for example in systems where trust in a participant may vary over time. +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 + +This module offers an experimental 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. +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. + + +The unsafe circuits are planned to become deprecated once contract-to-contract calls become available. + diff --git a/content/contracts-compact/0.1.x/api/access-control.mdx b/content/contracts-compact/0.1.x/api/access-control.mdx new file mode 100644 index 00000000..84568fec --- /dev/null +++ b/content/contracts-compact/0.1.x/api/access-control.mdx @@ -0,0 +1,347 @@ +--- +title: AccessControl API +--- + +This page provides the full AccessControl module API. + +Roles are referred to by their `Bytes<32>` identifier. +These should be exposed in the top-level contract and be unique. +The best way to achieve this is by using `export sealed ledger` hash digests that are initialized in the top-level contract: + +```typescript +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl" + prefix AccessControl_; + +export sealed ledger MY_ROLE: Bytes<32>; + +constructor() { + MY_ROLE = persistentHash>(pad(32, "MY_ROLE")); +} +``` + +To restrict access to a circuit, use [assertOnlyRole](#AccessControl-assertOnlyRole): + + +```typescript +circuit foo(): [] { + AccessControl_assertOnlyRole(MY_ROLE); +} +``` + +Roles can be granted and revoked dynamically via the [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole) circuits. +Each role has an associated admin role, +and only accounts that have a role's admin role can call [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole). + +By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE` (zero bytes), +which means that only accounts with this role will be able to grant or revoke other roles. +More complex role relationships can be created by using [_setRoleAdmin](#AccessControl-_setRoleAdmin). + + + The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to grant and revoke this role. + Extra precautions should be taken to secure accounts that have been granted it. + + + + For an overview of the module, read the [AccessControl guide](../access-control). + + +## AccessControl [toc] [#AccessControl] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; +``` + +--- + +### Ledger [toc] [#AccessControl-Ledger] +### Ledger [!toc] [#AccessControl-Ledger] + +#### _operatorRoles [toc] [#AccessControl-_operatorRoles] +#### [!toc] [#AccessControl-_operatorRoles] + + Mapping from a role identifier -> account -> its permissions. + + +#### _adminRoles [toc] [#AccessControl-_adminRoles] +#### [!toc] [#AccessControl-_adminRoles] + + Mapping from a role identifier to an admin role identifier. + + +### Witnesses [toc] [#AccessControl-Witnesses] +### Witnesses [!toc] [#AccessControl-Witnesses] + +#### wit_AccessControlSK [toc] [#AccessControl-wit_AccessControlSK] +#### [!toc] [#AccessControl-wit_AccessControlSK] + + 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. + + +### Circuits [toc] [#AccessControl-Circuits] +### Circuits [!toc] [#AccessControl-Circuits] + +#### DEFAULT_ADMIN_ROLE [toc] [#AccessControl-DEFAULT_ADMIN_ROLE] +#### [!toc] [#AccessControl-DEFAULT_ADMIN_ROLE] + + 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). + + +#### hasRole [toc] [#AccessControl-hasRole] +#### [!toc] [#AccessControl-hasRole] + + Returns `true` if `account` has been granted `roleId`. + + +#### _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. + + +#### assertOnlyRole [toc] [#AccessControl-assertOnlyRole] +#### [!toc] [#AccessControl-assertOnlyRole] + + Reverts if the caller cannot prove ownership of `roleId`. + The caller's identity is derived from the `wit_AccessControlSK` witness as `persistentHash(secretKey)`. + + Requirements: + + * The caller must have `roleId`. + + +#### _checkRole [toc] [#AccessControl-_checkRole] +#### [!toc] [#AccessControl-_checkRole] + + Reverts if `account` is missing `roleId`. + + Requirements: + + * `account` must have `roleId`. + + +#### getRoleAdmin [toc] [#AccessControl-getRoleAdmin] +#### [!toc] [#AccessControl-getRoleAdmin] + + 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). + + To change a role's admin use [_setRoleAdmin](#AccessControl-_setRoleAdmin). + + +#### grantRole [toc] [#AccessControl-grantRole] +#### [!toc] [#AccessControl-grantRole] + + 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. + + + Requirements: + + * `account` must not be a ContractAddress. + * The caller must have `roleId`'s admin role. + + +#### revokeRole [toc] [#AccessControl-revokeRole] +#### [!toc] [#AccessControl-revokeRole] + + Revokes `roleId` from `account`. + + Requirements: + + * The caller must have `roleId`'s admin role. + + +#### renounceRole [toc] [#AccessControl-renounceRole] +#### [!toc] [#AccessControl-renounceRole] + + 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 + 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. + + + 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. + + + Requirements: + + * The caller's computed account identifier must match `callerConfirmation`. + * The caller must not be a `ContractAddress`. + + +#### _setRoleAdmin [toc] [#AccessControl-_setRoleAdmin] +#### [!toc] [#AccessControl-_setRoleAdmin] + + Sets `adminRole` as `roleId`'s admin role. + + +#### _grantRole [toc] [#AccessControl-_grantRole] +#### [!toc] [#AccessControl-_grantRole] + + 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. + + + Requirements: + + * `account` must not be a ContractAddress. + + +#### _unsafeGrantRole [toc] [#AccessControl-_unsafeGrantRole] +#### [!toc] [#AccessControl-_unsafeGrantRole] + + Unsafe variant of [_grantRole](#AccessControl-_grantRole). + + + 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. + + + +#### _revokeRole [toc] [#AccessControl-_revokeRole] +#### [!toc] [#AccessControl-_revokeRole] + + Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. + + 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. + + diff --git a/content/contracts-compact/0.1.x/api/fungible-token.mdx b/content/contracts-compact/0.1.x/api/fungible-token.mdx new file mode 100644 index 00000000..1571706f --- /dev/null +++ b/content/contracts-compact/0.1.x/api/fungible-token.mdx @@ -0,0 +1,586 @@ +--- +title: FungibleToken API +--- + +This module provides the full FungibleToken module API. + + + For an overview of the module, read the [FungibleToken guide](../fungible-token). + + +## FungibleToken [toc] [#FungibleToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; +``` + +--- + +### Ledger [toc] [#FungibleToken-Ledger] +### Ledger [!toc] [#FungibleToken-Ledger] + +#### _balances [toc] [#FungibleToken-_balances] +#### [!toc] [#FungibleToken-_balances] + + Mapping from account addresses to their token balances. + + +#### _allowances [toc] [#FungibleToken-_allowances] +#### [!toc] [#FungibleToken-_allowances] + + Mapping from owner accounts to spender accounts to their allowances. + + +#### _totalSupply [toc] [#FungibleToken-_totalSupply] +#### [!toc] [#FungibleToken-_totalSupply] + + The total token supply. + + +#### _name [toc] [#FungibleToken-_name] +#### [!toc] [#FungibleToken-_name] + + The immutable token name. + + +#### _symbol [toc] [#FungibleToken-_symbol] +#### [!toc] [#FungibleToken-_symbol] + + The immutable token symbol. + + +#### _decimals [toc] [#FungibleToken-_decimals] +#### [!toc] [#FungibleToken-_decimals] + + The immutable token decimals. + + +### Witnesses [toc] [#FungibleToken-Witnesses] +### Witnesses [!toc] [#FungibleToken-Witnesses] + +#### wit_FungibleTokenSK [toc] [#FungibleToken-wit_FungibleTokenSK] +#### [!toc] [#FungibleToken-wit_FungibleTokenSK] + + 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. + + +### 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. + + 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. + + +#### name [toc] [#FungibleToken-name] +#### [!toc] [#FungibleToken-name] + + Returns the token name. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#FungibleToken-symbol] +#### [!toc] [#FungibleToken-symbol] + + Returns the symbol of the token. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#FungibleToken-decimals] +#### [!toc] [#FungibleToken-decimals] + + Returns the number of decimals used to get its user representation. + + Requirements: + + * Contract is initialized. + + +#### totalSupply [toc] [#FungibleToken-totalSupply] +#### [!toc] [#FungibleToken-totalSupply] + + Returns the value of tokens in existence. + + Requirements: + + * Contract is initialized. + + +#### balanceOf [toc] [#FungibleToken-balanceOf] +#### [!toc] [#FungibleToken-balanceOf] + + Returns the value of tokens owned by `account`. + + Requirements: + + * Contract is initialized. + + +#### transfer [toc] [#FungibleToken-transfer] +#### [!toc] [#FungibleToken-transfer] + + 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. + + + Requirements: + + * 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] +#### [!toc] [#FungibleToken-_unsafeTransfer] + + Unsafe variant of [transfer](#FungibleToken-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. + 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`. + + +#### allowance [toc] [#FungibleToken-allowance] +#### [!toc] [#FungibleToken-allowance] + + 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. + + Requirements: + + * Contract is initialized. + + +#### approve [toc] [#FungibleToken-approve] +#### [!toc] [#FungibleToken-approve] + + Sets a `value` amount of tokens as allowance of `spender` over the caller's tokens. + + Requirements: + + * Contract is initialized. + * `spender` is not the zero address. + + +#### transferFrom [toc] [#FungibleToken-transferFrom] +#### [!toc] [#FungibleToken-transferFrom] + + Moves `value` tokens from `fromAddress` to `to` using the allowance mechanism. + `value` is then 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. + + + 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`. + + +#### _unsafeTransferFrom [toc] [#FungibleToken-_unsafeTransferFrom] +#### [!toc] [#FungibleToken-_unsafeTransferFrom] + + Unsafe variant of [transferFrom](#FungibleToken-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. + 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`. + + +#### _transfer [toc] [#FungibleToken-_transfer] +#### [!toc] [#FungibleToken-_transfer] + + 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. + + + 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. + + +#### _unsafeUncheckedTransfer [toc] [#FungibleToken-_unsafeUncheckedTransfer] +#### [!toc] [#FungibleToken-_unsafeUncheckedTransfer] + + Unsafe variant of [_transfer](#FungibleToken-_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. + 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. + + +#### _mint [toc] [#FungibleToken-_mint] +#### [!toc] [#FungibleToken-_mint] + + 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. + + + Requirements: + + * Contract is initialized. + * `account` is not a ContractAddress. + * `account` is not the zero address. + + +#### _unsafeMint [toc] [#FungibleToken-_unsafeMint] +#### [!toc] [#FungibleToken-_unsafeMint] + + Unsafe variant of [_mint](#FungibleToken-_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. + Once contract-to-contract calls are supported, this circuit may be deprecated. + + + Requirements: + + * Contract is initialized. + * `account` is not the zero address. + + +#### _burn [toc] [#FungibleToken-_burn] +#### [!toc] [#FungibleToken-_burn] + + Destroys a `value` amount of tokens from `account`, lowering the total supply. + Relies on the `_update` mechanism. + + Requirements: + + * Contract is initialized. + * `account` is not the zero address. + * `account` must have at least a balance of `value`. + + +#### _approve [toc] [#FungibleToken-_approve] +#### [!toc] [#FungibleToken-_approve] + + 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. + + Requirements: + + * Contract is initialized. + * `owner` is not the zero address. + * `spender` is not the zero address. + + +#### _spendAllowance [toc] [#FungibleToken-_spendAllowance] +#### [!toc] [#FungibleToken-_spendAllowance] + + Updates `owner`'s allowance for `spender` based on spent `value`. + Does not update the allowance value in case of infinite allowance. + + Requirements: + + * 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. + + 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. + diff --git a/content/contracts-compact/0.1.x/api/multi-token.mdx b/content/contracts-compact/0.1.x/api/multi-token.mdx new file mode 100644 index 00000000..55545dfe --- /dev/null +++ b/content/contracts-compact/0.1.x/api/multi-token.mdx @@ -0,0 +1,490 @@ +--- +title: MultiToken API +--- + +This module provides the full MultiToken module API. + + + For an overview of the module, read the [MultiToken guide](../multi-token). + + +## MultiToken [toc] [#MultiToken] + + +```ts +import "./node_modules/@openzeppelin/token/src/MultiToken"; +``` + +--- + +### Ledger [toc] [#MultiToken-Ledger] +### Ledger [!toc] [#MultiToken-Ledger] + +#### _balances [toc] [#MultiToken-_balances] +#### [!toc] [#MultiToken-_balances] + + Mapping from token ID to account balances. + + +#### _operatorApprovals [toc] [#MultiToken-_operatorApprovals] +#### [!toc] [#MultiToken-_operatorApprovals] + + Mapping from account to operator approvals. + + +#### _uri [toc] [#MultiToken-_uri] +#### [!toc] [#MultiToken-_uri] + + Base URI for computing token URIs. + + +### Witnesses [toc] [#MultiToken-Witnesses] +### Witnesses [!toc] [#MultiToken-Witnesses] + +#### wit_MultiTokenSK [toc] [#MultiToken-wit_MultiTokenSK] +#### [!toc] [#MultiToken-wit_MultiTokenSK] + + 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. + + +### 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. + + 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. + + +#### uri [toc] [#MultiToken-uri] +#### [!toc] [#MultiToken-uri] + + 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. + + Requirements: + + * Contract is initialized. + + +#### balanceOf [toc] [#MultiToken-balanceOf] +#### [!toc] [#MultiToken-balanceOf] + + Returns the amount of `id` tokens owned by `account`. + + Requirements: + + * Contract is initialized. + + +#### setApprovalForAll [toc] [#MultiToken-setApprovalForAll] +#### [!toc] [#MultiToken-setApprovalForAll] + + 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)`. + + Requirements: + + * Contract is initialized. + * `operator` is not zero. + + +#### isApprovedForAll [toc] [#MultiToken-isApprovedForAll] +#### [!toc] [#MultiToken-isApprovedForAll] + + Queries if `operator` is an authorized operator for `account`. + + Requirements: + + * Contract is initialized. + + +#### transferFrom [toc] [#MultiToken-transferFrom] +#### [!toc] [#MultiToken-transferFrom] + + 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. + + + 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`. + + +#### _transfer [toc] [#MultiToken-_transfer] +#### [!toc] [#MultiToken-_transfer] + + 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. + + + 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. + + Requirements: + + * Contract is initialized. + * If `fromAddress` is not zero, the balance of `id` of `fromAddress` must be >= `value`. + + +#### _unsafeTransferFrom [toc] [#MultiToken-_unsafeTransferFrom] +#### [!toc] [#MultiToken-_unsafeTransferFrom] + + Unsafe variant of [transferFrom](#MultiToken-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)`. + + + 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`. + + +#### _unsafeTransfer [toc] [#MultiToken-_unsafeTransfer] +#### [!toc] [#MultiToken-_unsafeTransfer] + + 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. + + + 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`. + + +#### _setURI [toc] [#MultiToken-_setURI] +#### [!toc] [#MultiToken-_setURI] + + 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. + + 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. + + +#### _mint [toc] [#MultiToken-_mint] +#### [!toc] [#MultiToken-_mint] + + 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. + + + Requirements: + + * Contract is initialized. + * `to` is not zero. + * `to` is not a ContractAddress. + + +#### _unsafeMint [toc] [#MultiToken-_unsafeMint] +#### [!toc] [#MultiToken-_unsafeMint] + + Unsafe variant of [_mint](#MultiToken-_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. + Once contract-to-contract calls are supported, this circuit may be deprecated. + + + Requirements: + + * Contract is initialized. + * `to` is not zero. + + +#### _burn [toc] [#MultiToken-_burn] +#### [!toc] [#MultiToken-_burn] + + Destroys a `value` amount of tokens of type `id` from `fromAddress`. + + Requirements: + + * Contract is initialized. + * `fromAddress` is not zero. + * `fromAddress` must have an `id` balance of at least `value`. + + +#### _setApprovalForAll [toc] [#MultiToken-_setApprovalForAll] +#### [!toc] [#MultiToken-_setApprovalForAll] + + 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. + + Requirements: + + * 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. + + 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. + diff --git a/content/contracts-compact/0.1.x/api/non-fungible-token.mdx b/content/contracts-compact/0.1.x/api/non-fungible-token.mdx new file mode 100644 index 00000000..cc0dd3a3 --- /dev/null +++ b/content/contracts-compact/0.1.x/api/non-fungible-token.mdx @@ -0,0 +1,677 @@ +--- +title: NonFungibleToken API +--- + +This module provides the full NonFungibleToken module API. + + + For an overview of the module, read the [NonFungibleToken guide](../non-fungible-token). + + +## NonFungibleToken [toc] [#NonFungibleToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; +``` + +--- + +### Ledger [toc] [#NonFungibleToken-Ledger] +### Ledger [!toc] [#NonFungibleToken-Ledger] + +#### _name [toc] [#NonFungibleToken-_name] +#### [!toc] [#NonFungibleToken-_name] + + The immutable token name. + + +#### _symbol [toc] [#NonFungibleToken-_symbol] +#### [!toc] [#NonFungibleToken-_symbol] + + The immutable token symbol. + + +#### _owners [toc] [#NonFungibleToken-_owners] +#### [!toc] [#NonFungibleToken-_owners] + + Mapping from token IDs to their owner addresses. + + +#### _balances [toc] [#NonFungibleToken-_balances] +#### [!toc] [#NonFungibleToken-_balances] + + Mapping from account addresses to their token balances. + + +#### _tokenApprovals [toc] [#NonFungibleToken-_tokenApprovals] +#### [!toc] [#NonFungibleToken-_tokenApprovals] + + Mapping from token IDs to approved addresses. + + +#### _operatorApprovals [toc] [#NonFungibleToken-_operatorApprovals] +#### [!toc] [#NonFungibleToken-_operatorApprovals] + + Mapping from owner addresses to operator approvals. + + +#### _tokenURIs [toc] [#NonFungibleToken-_tokenURIs] +#### [!toc] [#NonFungibleToken-_tokenURIs] + + Mapping from token IDs to their metadata URIs. + + +### Witnesses [toc] [#NonFungibleToken-Witnesses] +### Witnesses [!toc] [#NonFungibleToken-Witnesses] + +#### wit_NonFungibleTokenSK [toc] [#NonFungibleToken-wit_NonFungibleTokenSK] +#### [!toc] [#NonFungibleToken-wit_NonFungibleTokenSK] + + 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. + + +### 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. + + 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] [#NonFungibleToken-balanceOf] +#### [!toc] [#NonFungibleToken-balanceOf] + + Returns the number of tokens in `owner`'s account. + + Requirements: + + * Contract is initialized. + + +#### ownerOf [toc] [#NonFungibleToken-ownerOf] +#### [!toc] [#NonFungibleToken-ownerOf] + + Returns the owner of the `tokenId` token. + + Requirements: + + * The contract is initialized. + * The `tokenId` must exist. + + +#### name [toc] [#NonFungibleToken-name] +#### [!toc] [#NonFungibleToken-name] + + Returns the token name. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#NonFungibleToken-symbol] +#### [!toc] [#NonFungibleToken-symbol] + + Returns the symbol of the token. + + Requirements: + + * Contract is initialized. + + +#### tokenURI [toc] [#NonFungibleToken-tokenURI] +#### [!toc] [#NonFungibleToken-tokenURI] + + Returns the token URI for the given `tokenId`. + Returns an empty string if a tokenURI does not exist. + + Requirements: + + * 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. + + + +#### _setTokenURI [toc] [#NonFungibleToken-_setTokenURI] +#### [!toc] [#NonFungibleToken-_setTokenURI] + + Sets the URI as `tokenURI` for the given `tokenId`. + + Requirements: + + * The contract is initialized. + * The `tokenId` must exist. + + +#### approve [toc] [#NonFungibleToken-approve] +#### [!toc] [#NonFungibleToken-approve] + + 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. + + Requirements: + + * The contract is initialized. + * The caller must either own the token or be an approved operator. + * `tokenId` must exist. + + +#### getApproved [toc] [#NonFungibleToken-getApproved] +#### [!toc] [#NonFungibleToken-getApproved] + + Returns the account approved for `tokenId` token. + + Requirements: + + * The contract is initialized. + * `tokenId` must exist. + + +#### setApprovalForAll [toc] [#NonFungibleToken-setApprovalForAll] +#### [!toc] [#NonFungibleToken-setApprovalForAll] + + 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)`. + + Requirements: + + * The contract is initialized. + * The `operator` cannot be zero. + + +#### isApprovedForAll [toc] [#NonFungibleToken-isApprovedForAll] +#### [!toc] [#NonFungibleToken-isApprovedForAll] + + Returns if the `operator` is allowed to manage all of the assets of `owner`. + + Requirements: + + * The contract is initialized. + + +#### transferFrom [toc] [#NonFungibleToken-transferFrom] +#### [!toc] [#NonFungibleToken-transferFrom] + + 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. + + + 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). + + +#### _unsafeTransferFrom [toc] [#NonFungibleToken-_unsafeTransferFrom] +#### [!toc] [#NonFungibleToken-_unsafeTransferFrom] + + 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)`. + + + 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` 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] +#### [!toc] [#NonFungibleToken-_ownerOf] + + Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist. + + Requirements: + + * The contract is initialized. + + +#### _getApproved [toc] [#NonFungibleToken-_getApproved] +#### [!toc] [#NonFungibleToken-_getApproved] + + Returns the approved address for `tokenId`. + Returns the zero value if `tokenId` is not minted. + + Requirements: + + * The contract is initialized. + + +#### _isAuthorized [toc] [#NonFungibleToken-_isAuthorized] +#### [!toc] [#NonFungibleToken-_isAuthorized] + + 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. + + + This function assumes that `owner` is the actual owner of `tokenId` and does not verify this assumption. + + + +#### _checkAuthorized [toc] [#NonFungibleToken-_checkAuthorized] +#### [!toc] [#NonFungibleToken-_checkAuthorized] + + Checks if `spender` can operate on `tokenId`, assuming the provided `owner` is the actual owner. + + Requirements: + + * 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. + + + +#### _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. + 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, + 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. + + +#### _unsafeMint [toc] [#NonFungibleToken-_unsafeMint] +#### [!toc] [#NonFungibleToken-_unsafeMint] + + Unsafe variant of [_mint](#NonFungibleToken-_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. + 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] +#### [!toc] [#NonFungibleToken-_burn] + + Destroys `tokenId`. + 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. + + +#### _transfer [toc] [#NonFungibleToken-_transfer] +#### [!toc] [#NonFungibleToken-_transfer] + + Transfers `tokenId` from `fromAddress` to `to`. 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. + + + Requirements: + + * The contract is initialized. + * `to` is not zero. + * `to` is not a ContractAddress. + * `tokenId` token must be owned by `fromAddress`. + + +#### _unsafeTransfer [toc] [#NonFungibleToken-_unsafeTransfer] +#### [!toc] [#NonFungibleToken-_unsafeTransfer] + + Unsafe variant of [_transfer](#NonFungibleToken-_transfer) which allows transfers to contract addresses. + + 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`. + + + 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` token must be owned by `fromAddress`. + + +#### _approve [toc] [#NonFungibleToken-_approve] +#### [!toc] [#NonFungibleToken-_approve] + + Approve `to` to operate on `tokenId`. + + Requirements: + + * The contract is initialized. + * 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). + + +#### _setApprovalForAll [toc] [#NonFungibleToken-_setApprovalForAll] +#### [!toc] [#NonFungibleToken-_setApprovalForAll] + + Approve `operator` to operate on all of `owner` tokens. + + Requirements: + + * The contract is initialized. + * `owner` is not zero. + * `operator` is not zero. + + +#### _requireOwned [toc] [#NonFungibleToken-_requireOwned] +#### [!toc] [#NonFungibleToken-_requireOwned] + + Reverts if the `tokenId` doesn't have a current owner (it hasn't been minted, or it has been burned). + Returns the owner. + + Requirements: + + * 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. + + 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. + diff --git a/content/contracts-compact/0.1.x/api/ownable.mdx b/content/contracts-compact/0.1.x/api/ownable.mdx new file mode 100644 index 00000000..124819c8 --- /dev/null +++ b/content/contracts-compact/0.1.x/api/ownable.mdx @@ -0,0 +1,487 @@ +--- +title: Ownable API +--- + +This module provides the full Ownable module API. + + +For an overview of the module, read the [Ownable guide](../ownable). + + +## Ownable [toc] [#Ownable] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; +``` + +--- + +### Ledger [toc] [#Ownable-Ledger] +### Ledger [!toc] [#Ownable-Ledger] + +#### _owner [toc] [#Ownable-_owner] +#### [!toc] [#Ownable-_owner] + + Either a `Bytes<32>` account identifier or `ContractAddress` representing the owner. + + +### Witnesses [toc] [#Ownable-Witnesses] +### Witnesses [!toc] [#Ownable-Witnesses] + +#### wit_OwnableSK [toc] [#Ownable-wit_OwnableSK] +#### [!toc] [#Ownable-wit_OwnableSK] + + 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. + + +### Circuits [toc] [#Ownable-Circuits] +### Circuits [!toc] [#Ownable-Circuits] + +#### initialize [toc] [#Ownable-initialize] +#### [!toc] [#Ownable-initialize] + + Initializes the contract by setting the `initialOwner`. + This must be called in the contract's constructor. + + Requirements: + + * Contract is not already initialized. + * `initialOwner` is not a ContractAddress. + * `initialOwner` is not the zero address. + + +#### owner [toc] [#Ownable-owner] +#### [!toc] [#Ownable-owner] + + Returns the current contract owner. + + Requirements: + + * Contract is initialized. + + +#### transferOwnership [toc] [#Ownable-transferOwnership] +#### [!toc] [#Ownable-transferOwnership] + + 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. + + + Requirements: + + * 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] +#### [!toc] [#Ownable-_unsafeTransferOwnership] + + Unsafe variant of [`transferOwnership`](#Ownable-transferOwnership). + + + 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. + + + Requirements: + + * Contract is initialized. + * The caller is the current contract owner. + * `newOwner` is not the zero address. + + +#### renounceOwnership [toc] [#Ownable-renounceOwnership] +#### [!toc] [#Ownable-renounceOwnership] + + Leaves the contract without an owner. + It will not be possible to call [`assertOnlyOwner`](#Ownable-assertOnlyOwner) circuits anymore. + Can only be called by the current owner. + + Requirements: + + * Contract is initialized. + * The caller is the current contract owner. + + +#### assertOnlyOwner [toc] [#Ownable-assertOnlyOwner] +#### [!toc] [#Ownable-assertOnlyOwner] + + 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)`. + + Requirements: + + * Contract is initialized. + * The caller is the current contract owner. + + +#### _transferOwnership [toc] [#Ownable-_transferOwnership] +#### [!toc] [#Ownable-_transferOwnership] + + Transfers ownership of the contract to a `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. + + + Requirements: + + * Contract is initialized. + * `newOwner` is not a ContractAddress. + + +#### _unsafeUncheckedTransferOwnership [toc] [#Ownable-_unsafeUncheckedTransferOwnership] +#### [!toc] [#Ownable-_unsafeUncheckedTransferOwnership] + + Unsafe variant of [`_transferOwnership`](#Ownable-_transferOwnership). + + + 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. + + + 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 +import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; +``` + +--- + +### Ledger [toc] [#ZOwnablePK-ledger] +### Ledger [!toc] [#ZOwnablePK-ledger] + +#### _ownerCommitment [toc] [#ZOwnablePK-_ownerCommitment] +#### [!toc] [#ZOwnablePK-_ownerCommitment] + + 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] [#ZOwnablePK-_counter] +#### [!toc] [#ZOwnablePK-_counter] + + 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] [#ZOwnablePK-_instanceSalt] +#### [!toc] [#ZOwnablePK-_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 owner identifiers or domain parameters. + It is immutable after initialization. + + +### Witnesses [toc] [#ZOwnablePK-witnesses] +### Witnesses [!toc] [#ZOwnablePK-witnesses] + +#### 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. + + +### Circuits [toc] [#ZOwnablePK-circuits] +### Circuits [!toc] [#ZOwnablePK-circuits] + +#### initialize [toc] [#ZOwnablePK-initialize] +#### [!toc] [#ZOwnablePK-initialize] + + 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. + + + 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). + + + Requirements: + + * Contract is not initialized. + * `ownerId` 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)`. + + Requirements: + + * Contract is initialized. + + +#### transferOwnership [toc] [#ZOwnablePK-transferOwnership] +#### [!toc] [#ZOwnablePK-transferOwnership] + + Transfers ownership to `newOwnerId`. + `newOwnerId` must be precalculated and given to the current owner off chain. + + Requirements: + + * Contract is initialized. + * Caller is the current owner. + * `newOwnerId` is not an empty array. + + +#### renounceOwnership [toc] [#ZOwnablePK-renounceOwnership] +#### [!toc] [#ZOwnablePK-renounceOwnership] + + 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. + + +#### assertOnlyOwner [toc] [#ZOwnablePK-assertOnlyOwner] +#### [!toc] [#ZOwnablePK-assertOnlyOwner] + + 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. + + Requirements: + + * Contract is initialized. + * Caller's id (`SHA256(pk, nonce)`) when used in `_computeOwnerCommitment` equals the stored `_ownerCommitment`. + + +#### _computeOwnerCommitment [toc] [#ZOwnablePK-_computeOwnerCommitment] +#### [!toc] [#ZOwnablePK-_computeOwnerCommitment] + + Computes the owner commitment from the given `id` and `counter`. + + The commitment derivation follows: + `commitment = SHA256(id, instanceSalt, counter, "ZOwnablePK:shield:")` + + 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. + + Requirements: + + * Contract is initialized. + + +#### _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)` + + + Currently only supports `ZswapCoinPublicKey`. Contract address owners are not yet supported. + + + Requirements: + + * `pk` is not a ContractAddress. + + +#### _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. + + 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/0.1.x/api/security.mdx b/content/contracts-compact/0.1.x/api/security.mdx new file mode 100644 index 00000000..e1404bb3 --- /dev/null +++ b/content/contracts-compact/0.1.x/api/security.mdx @@ -0,0 +1,193 @@ +--- +title: Security +--- + +This package provides the API for all Security modules. + + + For an overview of the module, read the [Security guide](../security). + + +## Initializable [toc] [#Initializable] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/security/Initializable"; +``` + +--- + +### Ledger [toc] [#Initializable-Ledger] +### Ledger [!toc] [#Initializable-Ledger] + +#### _isInitialized [toc] [#Initializable-_isInitialized] +#### [!toc] [#Initializable-_isInitialized] + + Boolean indicating if initialized. + + +### Witnesses [toc] [#Initializable-Witnesses] +### Witnesses [!toc] [#Initializable-Witnesses] + +None. + +### Circuits [toc] [#Initializable-Circuits] +### Circuits [!toc] [#Initializable-Circuits] + +#### initialize [toc] [#Initializable-initialize] +#### [!toc] [#Initializable-initialize] + + Initializes the state thus ensuring the calling circuit can only be called once. + + Requirements: + + * Contract must not be initialized. + + +#### assertInitialized [toc] [#Initializable-assertInitialized] +#### [!toc] [#Initializable-assertInitialized] + + Asserts that the contract has been initialized, throwing an error if not. + + Requirements: + + * Contract must be initialized. + + +#### assertNotInitialized [toc] [#Initializable-assertNotInitialized] +#### [!toc] [#Initializable-assertNotInitialized] + + Asserts that the contract has not been initialized, throwing an error if it has. + + Requirements: + + * Contract must not be initialized. + + +--- + +## Pausable [toc] [#Pausable] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/security/Pausable"; +``` + +--- + +### Ledger [toc] [#Pausable-Ledger] +### Ledger [!toc] [#Pausable-Ledger] + +#### _isPaused [toc] [#Pausable-_isPaused] +#### [!toc] [#Pausable-_isPaused] + + Boolean indicating if paused. + + +### Witnesses [toc] [#Pausable-Witnesses] +### Witnesses [!toc] [#Pausable-Witnesses] + +None. + +### Circuits [toc] [#Pausable-Circuits] +### Circuits [!toc] [#Pausable-Circuits] + +#### isPaused [toc] [#Pausable-isPaused] +#### [!toc] [#Pausable-isPaused] + + Returns true if the contract is paused, and false otherwise. + + +#### assertPaused [toc] [#Pausable-assertPaused] +#### [!toc] [#Pausable-assertPaused] + + Makes a circuit only callable when the contract is paused. + + Requirements: + + * Contract must be paused. + + +#### assertNotPaused [toc] [#Pausable-assertNotPaused] +#### [!toc] [#Pausable-assertNotPaused] + + Makes a circuit only callable when the contract is not paused. + + Requirements: + + * Contract must not be paused. + + +#### _pause [toc] [#Pausable-_pause] +#### [!toc] [#Pausable-_pause] + + Triggers a stopped state. + + Requirements: + + * Contract must not be paused. + + +#### _unpause [toc] [#Pausable-_unpause] +#### [!toc] [#Pausable-_unpause] + + Lifts the pause on the contract. + + Requirements: + + * Contract must be paused. + diff --git a/content/contracts-compact/0.1.x/api/shielded-access-control.mdx b/content/contracts-compact/0.1.x/api/shielded-access-control.mdx new file mode 100644 index 00000000..4662d66a --- /dev/null +++ b/content/contracts-compact/0.1.x/api/shielded-access-control.mdx @@ -0,0 +1,609 @@ +--- +title: ShieldedAccessControl API +--- + +This page provides the full ShieldedAccessControl module API. + +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>`: + +``` +roleCommitment := SHA256( role ‖ accountId ‖ instanceSalt ‖ commitmentDomain ) + +accountId := SHA256( secretKey ‖ instanceSalt ‖ accountIdDomain ) + +roleNullifier := SHA256( roleCommitment ‖ nullifierDomain ) + +commitmentDomain := pad(32, "ShieldedAccessControl:commitment") +accountIdDomain := pad(32, "ShieldedAccessControl:accountId") +nullifierDomain := pad(32, "ShieldedAccessControl:nullifier") +``` + +In this RBAC model, role commitments behave like private bearer tokens. +Possession of a valid, non-revoked role commitment grants authorization. +Revocation permanently burns the role instance, requiring explicit new issuance under a new account identifier. +Users must rotate their secret key to generate a new `accountId` to be re-authorized. + +A single `secretKey` may be used for all roles within a contract instance. +This is safe because the `role` identifier is mixed into the commitment (not the `accountId`), +so different roles produce different commitments and different nullifiers even when the underlying `accountId` is the same. + +Roles are referred to by their `Bytes<32>` identifier. +These should be exposed in the top-level contract and be unique. +The best way to achieve this is by using `export sealed ledger` hash digests that are initialized in the top-level contract: + +```typescript +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl" + prefix ShieldedAccessControl_; + +export sealed ledger MY_ROLE: Bytes<32>; + +constructor(instanceSalt: Bytes<32>, defaultAdmin: ShieldedAccessControl_AccountIdentifier) { + MY_ROLE = persistentHash>(pad(32, "MY_ROLE")); + ShieldedAccessControl_initialize(instanceSalt); + ShieldedAccessControl__grantRole(ShieldedAccessControl_DEFAULT_ADMIN_ROLE(), defaultAdmin); +} +``` + +To restrict access to a circuit, use [assertOnlyRole](#ShieldedAccessControl-assertOnlyRole): + +```typescript +circuit foo(): [] { + ShieldedAccessControl_assertOnlyRole(MY_ROLE as ShieldedAccessControl_RoleIdentifier); +} +``` + +Roles can be granted and revoked dynamically via the [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole) circuits. +Each role has an associated admin role, +and only accounts that have a role's admin role can call [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole). + +By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, +which means that only accounts with this role will be able to grant or revoke other roles. +More complex role relationships can be created by using [_setRoleAdmin](#ShieldedAccessControl-_setRoleAdmin). + + + The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to grant and revoke this role. + Extra precautions should be taken to secure accounts that have been granted it. + + + + For an overview of the module, read the [ShieldedAccessControl guide](../shielded-access-control). + + +## ShieldedAccessControl [toc] [#ShieldedAccessControl] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl" +``` + +--- + +### Types [toc] [#ShieldedAccessControl-Types] +### Types [!toc] [#ShieldedAccessControl-Types] + +#### UpdateType [toc] [#ShieldedAccessControl-UpdateType] +#### [!toc] [#ShieldedAccessControl-UpdateType] + + Enum indicating whether a role update is a grant or a revocation. + + +#### RoleCommitment [toc] [#ShieldedAccessControl-RoleCommitment] +#### [!toc] [#ShieldedAccessControl-RoleCommitment] + + A Merkle tree leaf committing a `(role, accountId)` pairing. Computed as `SHA256(role, accountId, instanceSalt, commitmentDomain)`. + + +#### RoleIdentifier [toc] [#ShieldedAccessControl-RoleIdentifier] +#### [!toc] [#ShieldedAccessControl-RoleIdentifier] + + A unique identifier for a role. + + +#### AccountIdentifier [toc] [#ShieldedAccessControl-AccountIdentifier] +#### [!toc] [#ShieldedAccessControl-AccountIdentifier] + + A privacy-preserving identity commitment. Computed as `SHA256(secretKey, instanceSalt, accountIdDomain)`. + + +#### RoleNullifier [toc] [#ShieldedAccessControl-RoleNullifier] +#### [!toc] [#ShieldedAccessControl-RoleNullifier] + + 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] + +#### _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. + + +#### _adminRoles [toc] [#ShieldedAccessControl-_adminRoles] +#### [!toc] [#ShieldedAccessControl-_adminRoles] + + Mapping from a role identifier to an admin role identifier. + + +#### _roleCommitmentNullifiers [toc] [#ShieldedAccessControl-_roleCommitmentNullifiers] +#### [!toc] [#ShieldedAccessControl-_roleCommitmentNullifiers] + + A set of nullifiers used to prove a role has been revoked. + + +#### _instanceSalt [toc] [#ShieldedAccessControl-_instanceSalt] +#### [!toc] [#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. + + +### Witnesses [toc] [#ShieldedAccessControl-Witnesses] +### Witnesses [!toc] [#ShieldedAccessControl-Witnesses] + +#### 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. + + +#### wit_secretKey [toc] [#ShieldedAccessControl-wit_secretKey] +#### [!toc] [#ShieldedAccessControl-wit_secretKey] + + Returns the user's secret key used in deriving the shielded account identifier. + + 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`. + + +### Circuits [toc] [#ShieldedAccessControl-Circuits] +### Circuits [!toc] [#ShieldedAccessControl-Circuits] + +#### initialize [toc] [#ShieldedAccessControl-initialize] +#### [!toc] [#ShieldedAccessControl-initialize] + + Initializes the contract by storing the `instanceSalt` that acts as a privacy additive + for preventing duplicate commitments among other contracts implementing ShieldedAccessControl. + + + 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. + + + Requirements: + + * Contract is not initialized. + * `instanceSalt` must not be zero. + + +#### DEFAULT_ADMIN_ROLE [toc] [#ShieldedAccessControl-DEFAULT_ADMIN_ROLE] +#### [!toc] [#ShieldedAccessControl-DEFAULT_ADMIN_ROLE] + + 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). + + +#### assertOnlyRole [toc] [#ShieldedAccessControl-assertOnlyRole] +#### [!toc] [#ShieldedAccessControl-assertOnlyRole] + + Reverts if the caller cannot provide a valid proof of ownership for `role`. + + 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. + + 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. + + +#### canProveRole [toc] [#ShieldedAccessControl-canProveRole] +#### [!toc] [#ShieldedAccessControl-canProveRole] + + 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. + + Requirements: + + * Contract is initialized. + * 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 + `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. + + 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. + + +#### revokeRole [toc] [#ShieldedAccessControl-revokeRole] +#### [!toc] [#ShieldedAccessControl-revokeRole] + + Permanently revokes `role` from `accountId` by inserting a role nullifier into the + `_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. + + 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. + + +#### renounceRole [toc] [#ShieldedAccessControl-renounceRole] +#### [!toc] [#ShieldedAccessControl-renounceRole] + + Revokes `role` from the calling account by inserting a role nullifier into the + `_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 + if they are compromised (such as when a trusted device is misplaced). + + Disclosures: 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. + + + Requirements: + + * Contract is initialized. + * The caller must provide a valid `accountId` for the role. + * 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] + + 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). + + To change a role's admin use [_setRoleAdmin](#ShieldedAccessControl-_setRoleAdmin). + + Disclosures: a role identifier. + + +#### _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. + + Internal circuit without access restriction. + + Disclosures: a role commitment corresponding to a `(role, accountId)` pairing, 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. + 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. + + + Requirements: + + * Contract is initialized. + * The `(role, accountId)` pairing must not be already revoked. + + +#### _revokeRole [toc] [#ShieldedAccessControl-_revokeRole] +#### [!toc] [#ShieldedAccessControl-_revokeRole] + + Permanently revokes `role` from `accountId` by inserting a role nullifier into the + `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for + `role`. + + Internal circuit without access restriction. + + Disclosures: 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. + + + Requirements: + + * Contract is initialized. + * The `(role, accountId)` pairing must not be already revoked. + + +#### _setRoleAdmin [toc] [#ShieldedAccessControl-_setRoleAdmin] +#### [!toc] [#ShieldedAccessControl-_setRoleAdmin] + + 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. + + + 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: + + * 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`. + + 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 + an internal helper. Using this circuit outside of the module may cause undefined behavior + and break security guarantees. + + + +#### computeNullifier [toc] [#ShieldedAccessControl-computeNullifier] +#### [!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. + + + +#### computeAccountId [toc] [#ShieldedAccessControl-computeAccountId] +#### [!toc] [#ShieldedAccessControl-computeAccountId] + + 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. + + 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. + 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. + + diff --git a/content/contracts-compact/0.1.x/api/utils.mdx b/content/contracts-compact/0.1.x/api/utils.mdx new file mode 100644 index 00000000..7de76b9d --- /dev/null +++ b/content/contracts-compact/0.1.x/api/utils.mdx @@ -0,0 +1,107 @@ +--- +title: Utils +--- + +This package provides the API for all Utils modules. + + + For an overview of the module, read the [Utils guide](../utils). + + +## Utils [toc] [#Utils] + + +```ts +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. + + +### Ledger [toc] [#Utils-Ledger] +### Ledger [!toc] [#Utils-Ledger] + +None. + +### Witnesses [toc] [#Utils-Witnesses] +### Witnesses [!toc] [#Utils-Witnesses] + +None. + +### Circuits [toc] [#Utils-Circuits] +### Circuits [!toc] [#Utils-Circuits] + +#### isKeyOrAddressZero [toc] [#Utils-isKeyOrAddressZero] +#### [!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. + + + +#### isKeyZero [toc] [#Utils-isKeyZero] +#### [!toc] [#Utils-isKeyZero] + + Returns whether `key` is the zero address. + + +#### isKeyOrAddressEqual [toc] [#Utils-isKeyOrAddressEqual] +#### [!toc] [#Utils-isKeyOrAddressEqual] + + Returns whether `keyOrAddress` is equal to `other`. + Assumes that a `ZswapCoinPublicKey` and a `ContractAddress` can never be equal. + + +#### isContractAddress [toc] [#Utils-isContractAddress] +#### [!toc] [#Utils-isContractAddress] + + Returns whether `keyOrAddress` is a `ContractAddress` type. + + +#### emptyString [toc] [#Utils-emptyString] +#### [!toc] [#Utils-emptyString] + + A helper function that returns the empty string: `""`. + + +#### canonicalize [toc] [#Utils-canonicalize] +#### [!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. + diff --git a/content/contracts-compact/0.1.x/extensibility.mdx b/content/contracts-compact/0.1.x/extensibility.mdx new file mode 100644 index 00000000..08531c39 --- /dev/null +++ b/content/contracts-compact/0.1.x/extensibility.mdx @@ -0,0 +1,175 @@ +--- +title: Extensibility +--- + +## The Module/Contract Pattern + +We use the term **modular composition by delegation** to describe the practice of having contracts call into module-defined circuits to implement behavior. +Rather than inheriting or overriding functionality, +a contract delegates responsibility to the module by explicitly invoking its exported circuits. + +The idea is that there are two types of compact files: modules and contracts. +To minimize risk, boilerplate, and avoid naming clashes, we follow these rules: + +### Modules + +Modules expose functionality through three circuit types: + +1. `internal`: private helpers → used to break up logic within the module. +2. `public`: composable building blocks → intended for contracts to use in complex flows (`_mint`, `_burn`). +3. `external`: standalone circuits → safe to expose as-is (`transfer`, `approve`). + +Modules must: + +* Export only `public` and `external` circuits. +* Prefix `public` circuits with `_` (e.g., `FungibleToken._mint`). +* Avoid `_` prefix for `external` circuits (e.g., `FungibleToken.transfer`). +* Avoid defining or calling constructors or `initialize()` directly. +* Optionally define an `initialize()` circuit for internal setup—but execution must be delegated to the contract. + + +Compact files must contain only one top-level module and all logic must be defined **inside** the module declaration. + + +### Contracts + +Contracts compose behavior by explicitly invoking the relevant circuits from imported modules. +Therefore, contracts: + +* Can import from modules. +* Should add prefix to imports (`import "FungibleToken" prefix FungibleToken_;`). +* Should re-expose external module circuits through wrapper circuits to control naming and layering. +Avoid raw re-exports to prevent name clashes. +* Should implement constructor that calls `initialize` from imported modules. +* Must not call initializers outside of the constructor. + +This pattern balances modularity with local control, avoids tight coupling, and works within Compact’s language constraints. +As Compact matures, this pattern will likely evolve as well. + +### Example contract implementing modules + +```ts +// FungibleTokenMintablePausableOwnableContract + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; +import "./node_modules/@openzeppelin/compact-contracts/security/Pausable" + prefix Pausable_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + _name: Maybe>, + _symbol: Maybe>, + _decimals:Uint<8>, + _owner: Either, ContractAddress> +) { + FungibleToken_initialize(_name, _symbol, _decimals); + Ownable_initialize(_owner); +} + +/** IFungibleTokenMetadata */ + +export circuit name(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return FungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return FungibleToken_decimals(); +} + +/** IFungibleToken */ + +export circuit totalSupply(): Uint<128> { + return FungibleToken_totalSupply(); +} + +export circuit balanceOf( + account: Either, ContractAddress> +): Uint<128> { + return FungibleToken_balanceOf(account); +} + +export circuit allowance( + owner: Either, ContractAddress>, + spender: Either, ContractAddress> +): Uint<128> { + return FungibleToken_allowance(owner, spender); +} + +export circuit transfer( + to: Either, ContractAddress>, + value: Uint<128> +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_transfer(to, value); +} + +export circuit transferFrom( + from: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128> +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_transferFrom(from, to, value); +} + +export circuit approve( + spender: Either, ContractAddress>, + value: Uint<128> +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_approve(spender, value); +} + +/** IMintable */ + +export circuit mint( + account: Either, ContractAddress>, + value: Uint<128> +): [] { + Pausable_assertNotPaused(); + Ownable_assertOnlyOwner(); + FungibleToken__mint(account, value); +} + +/** IPausable */ + +export circuit isPaused(): Boolean { + return Pausable_isPaused(); +} + +export circuit pause(): [] { + Ownable_assertOnlyOwner(); + Pausable__pause(); +} + +export circuit unpause(): [] { + Ownable_assertOnlyOwner(); + Pausable__unpause(); +} + +/** IOwnable */ + +export circuit owner(): Either, ContractAddress> { + return Ownable_owner(); +} + +export circuit transferOwnership( + newOwner: Either, ContractAddress> +): [] { + Ownable_transferOwnership(newOwner); +} + +export circuit renounceOwnership(): [] { + Ownable_renounceOwnership(); +} + +``` diff --git a/content/contracts-compact/0.1.x/fungible-token.mdx b/content/contracts-compact/0.1.x/fungible-token.mdx new file mode 100644 index 00000000..645f2777 --- /dev/null +++ b/content/contracts-compact/0.1.x/fungible-token.mdx @@ -0,0 +1,153 @@ +--- +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 +[_mint]: api/fungible-token#FungibleToken-_mint + +FungibleToken is a specification for [fungible tokens], +a type of token where all the units are exactly equal to each other. +This module is an approximation of [EIP-20] written in the Compact programming language for the Midnight network. + +## ERC20 Compatibility + +Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. +Some features and behaviors are either not possible, not possible yet, +or changed because of the vastly different tech stack and Compact language constraints. + +***Notable changes*** + +* ***`Uint<128>` as value type*** - Since 256-bit unsigned integers are not supported, +the library uses the Compact type `Uint<128>`. + +***Features and specifications NOT supported*** + +* ***Events*** - Midnight does not currently support events, but this is planned on being supported in the future. +* ***Uint256 type*** - There's ongoing research on ways to support uint256 in the future. +* ***Interface*** - Compact currently does not have a way to define a contract interface. +This library offers modules of contracts with free floating circuits; +nevertheless, there are no means of enforcing that all circuits are provided. + +## Contract-to-contract calls + +Contract-to-contract calls are currently not supported in the Compact language. +Due to this limitation, the current iteration of FungibleToken disallows transfers and mints to the `ContractAddress` type. +Transferring tokens to a contract may result in those tokens being locked forever. +The FungibleToken module, however, does provide `unsafe` circuit variants for users who wish to experiment with sending tokens to contracts. + + +The `unsafe` circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning +`transfer`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. + + +## Usage + +Import the FungibleToken module into the implementing contract. +It's recommended to prefix the module with `FungibleToken_` to avoid circuit signature clashes. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8> +) { + FungibleToken_initialize(name, symbol, decimals); +} + +``` + +Next, expose the circuits that users may call in the contract. + +This library enables extensibility by following the rules of the [Module/Contract Pattern]. +Note that circuits with a preceding underscore (`_likeThis`) are meant to be building blocks for implementing contracts. +Exposing [_mint] without some sort of access control, for example, would allow ANYONE to mint tokens. + +```typescript +export circuit name(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return FungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return FungibleToken_decimals(); +} + +(...) +``` + +The following example is a simple token contract with a fixed supply that's minted to the passed recipient upon construction. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, + recipient: Either, ContractAddress>, + fixedSupply: Uint<128>, +) { + FungibleToken_initialize(name, symbol, decimals); + FungibleToken__mint(recipient, fixedSupply); +} + +export circuit name(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return FungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return FungibleToken_decimals(); +} + +export circuit totalSupply(): Uint<128> { + return FungibleToken_totalSupply(); +} + +export circuit balanceOf(account: Either, ContractAddress>): Uint<128> { + return FungibleToken_balanceOf(account); +} + +export circuit allowance( + owner: Either, ContractAddress>, + spender: Either, ContractAddress> +): Uint<128> { + return FungibleToken_allowance(owner, spender); +} + +export circuit transfer(to: Either, ContractAddress>, value: Uint<128>): Boolean { + return FungibleToken_transfer(to, value); +} + +export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128>, +): Boolean { + return FungibleToken_transferFrom(fromAddress, to, value); +} + +export circuit approve(spender: Either, ContractAddress>, value: Uint<128>): Boolean { + return FungibleToken_approve(spender, value); +} + +``` diff --git a/content/contracts-compact/0.1.x/index.mdx b/content/contracts-compact/0.1.x/index.mdx new file mode 100644 index 00000000..1963a214 --- /dev/null +++ b/content/contracts-compact/0.1.x/index.mdx @@ -0,0 +1,175 @@ +--- +title: Contracts for Compact +--- + +{/* links */} +[Midnight]: https://midnight.network/ +[nvm]: https://github.com/nvm-sh/nvm +[yarn]: https://yarnpkg.com/getting-started/install +[compact-dev-tools]: https://docs.midnight.network/blog/compact-developer-tools + +**A library for secure smart contract development** written in Compact for [Midnight]. +This library consists of modules to build custom smart contracts. + + +This repo contains highly experimental code. +Expect rapid iteration. **Use at your own risk.** + + +## Usage + +Make sure you have [nvm] and [yarn] installed on your machine. + +Follow Midnight's [Compact Developer Tools][compact-dev-tools] installation guide and confirm that `compact` is in the `PATH` env variable. + +```bash +$ compact compile --version + +Compactc version: {{compact_compiler_version}} +{{compact_compiler_version}} +``` + +### Installation + +Create a directory for your project. + +```bash +mkdir my-project +cd my-project +``` + +Install the library. + +```bash +yarn add @openzeppelin/compact-contracts +``` + +### Write a custom contract using library modules + +In the root of `my-project`, create a custom contract using OpenZeppelin Compact modules. +Import the modules through `./node_modules/@openzeppelin/compact-contracts/...`. + +```typescript +// MyContract.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; +import "./node_modules/@openzeppelin/compact-contracts/security/Pausable" + prefix Pausable_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + _name: Opaque<"string">, + _symbol: Opaque<"string">, + _decimals: Uint<8>, + _recipient: Either, ContractAddress>, + _amount: Uint<128>, + _initOwner: Either, ContractAddress>, +) { + Ownable_initialize(_initOwner); + FungibleToken_initialize(_name, _symbol, _decimals); + FungibleToken__mint(_recipient, _amount); +} + +/** IFungibleToken */ + +export circuit name(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return FungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return FungibleToken_decimals(); +} + +export circuit totalSupply(): Uint<128> { + return FungibleToken_totalSupply(); +} + +export circuit balanceOf(account: Either, ContractAddress>): Uint<128> { + return FungibleToken_balanceOf(account); +} + +export circuit allowance( + owner: Either, ContractAddress>, + spender: Either, ContractAddress> +): Uint<128> { + return FungibleToken_allowance(owner, spender); +} + +export circuit transfer( + to: Either, ContractAddress>, + value: Uint<128>, +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_transfer(to, value); +} + +export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128> +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_transferFrom(fromAddress, to, value); +} + +export circuit approve(spender: Either, ContractAddress>, value: Uint<128>): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_approve(spender, value); +} + +/** IOwnable */ + +export circuit owner(): Either, ContractAddress> { + return Ownable_owner(); +} + +export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { + return Ownable_transferOwnership(newOwner); +} + +export circuit renounceOwnership(): [] { + return Ownable_renounceOwnership(); +} + +/** IPausable */ + +export circuit pause(): [] { + Ownable_assertOnlyOwner(); + Pausable__pause(); +} + +export circuit unpause(): [] { + Ownable_assertOnlyOwner(); + Pausable__unpause(); +} +``` + +### Compile the contract + +In the project root, compile the contract. + +```bash +% compact compile MyContract.compact artifacts/MyContract +Compiling 11 circuits: + circuit "allowance" (k=11, rows=1352) + circuit "approve" (k=13, rows=3075) + circuit "balanceOf" (k=10, rows=673) + circuit "decimals" (k=6, rows=28) + circuit "name" (k=6, rows=28) + circuit "pause" (k=13, rows=2365) + 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 "unpause" (k=13, rows=2362) +Overall progress [====================] 11/11 +``` diff --git a/content/contracts-compact/0.1.x/multi-token.mdx b/content/contracts-compact/0.1.x/multi-token.mdx new file mode 100644 index 00000000..d4123f2b --- /dev/null +++ b/content/contracts-compact/0.1.x/multi-token.mdx @@ -0,0 +1,162 @@ +--- +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 +[_mint]: ./api/multi-token#MultiToken-_mint + +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. + +## ERC1155 Compatibility + +Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. +Some features and behaviors are either not possible, not possible yet, +or changed because of the vastly different tech stack and Compact language constraints. + +**Notable changes** + +* **`Uint<128>` as value and id type** - Since 256-bit unsigned integers are not supported, +the library uses the Compact type `Uint<128>`. + +**Features and specifications NOT supported** + +* **Events** - Midnight does not currently support events, but this is planned on being supported in the future. +* **Uint256 type** - There's ongoing research on ways to support uint256 in the future. +* **Interface** - Compact currently does not have a way to define a contract interface. +This library offers modules of contracts with free floating circuits; +nevertheless, there's no means of enforcing that all circuits are provided. +* **Batch mint, burn, transfer** - Without support for dynamic arrays, +batching transfers is difficult to do without a hacky solution. +For instance, we could change the `to` and `fromAddress` parameters to be vectors. +This would change the signature and would be both difficult to use and easy to misuse. +* **Querying batched balances** - This can be somewhat supported. +The issue, without dynamic arrays, is that the module circuit must use `Vector` for accounts and ids; +therefore, the implementing contract must explicitly define the number of balances to query in the circuit i.e. + +```ts +balanceOfBatch_10( + accounts: Vector<10, Either, ContractAddress>>, + ids: Vector<10, Uint<128>> +): Vector<10, Uint<128>> +``` + +Since this module does not offer mint or transfer batching, balance batching is also not included at this time. + +* **Introspection** - Compact currently cannot support contract-to-contract queries for introspection. +[ERC165] (or an equivalent thereof) is NOT included in the contract. +* **Safe transfers** - The lack of an introspection mechanism means safe transfers of any kind can not be supported. + +## Contract-to-contract calls + +Contract-to-contract calls are currently not supported in the Compact language. +Due to this limitation, the current iteration of MultiToken disallows transfers and mints to the `ContractAddress` type. +Transferring tokens to a contract may result in those tokens being locked forever. +The MultiToken module, however, does provide `unsafe` circuit variants for users who wish to experiment with sending tokens to contracts. + + +The `unsafe` circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning +`transferFrom`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. + + +## Usage + +Import the MultiToken module into the implementing contract. +It's recommended to prefix the module with `MultiToken_` to avoid circuit signature clashes. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken" + prefix MultiToken_; + +constructor( + uri: Opaque<"string">, +) { + MultiToken_initialize(uri); +} + +``` + +Next, expose the circuits that users may call in the contract. + +This library enables extensibility by following the rules of the [Module/Contract Pattern]. +Note that circuits with a preceding underscore (`_likeThis`) are meant to be building blocks for implementing contracts. +Exposing [_mint] without some sort of access control, for example, would allow ANYONE to mint tokens. + +```typescript +export circuit uri(id: Uint<128>): Opaque<"string"> { + return MultiToken_uri(id); +} + +export circuit balanceOf(account: Either, ContractAddress>, id: Uint<128>): Uint<128> { + return MultiToken_balanceOf(account, id); +} + +(...) +``` + +The following example is a simple multi-token contract that creates both a fixed-supply fungible token and an NFT using the same contract. + +```typescript +// MultiTokenTwoTokenTypes.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken" + prefix MultiToken_; + +constructor( + _uri: Opaque<"string">, + recipient: Either, ContractAddress>, + fungibleFixedSupply: Uint<128>, +) { + // `initialize` sets the URI (base) for all tokens minted from this contract + MultiToken_initialize(_uri); + + // Token id `123` is a fungible token (with a fixed supply of `fungibleFixedSupply`) + const fungibleTokenId = 123; + MultiToken__mint(recipient, fungibleTokenId, fungibleFixedSupply); + + // Token id `987` is a non-fungible token (so the supply is only 1) + const nonFungibleTokenId = 987; + MultiToken__mint(recipient, nonFungibleTokenId, 1); +} + +export circuit uri(id: Uint<128>): Opaque<"string"> { + return MultiToken_uri(id); +} + +export circuit balanceOf(account: Either, ContractAddress>, id: Uint<128>): Uint<128> { + return MultiToken_balanceOf(account, id); +} + +export circuit setApprovalForAll( + operator: Either, ContractAddress>, + approved: Boolean +): [] { + return MultiToken_setApprovalForAll(operator, approved); +} + +export circuit isApprovedForAll( + account: Either, ContractAddress>, + operator: Either, ContractAddress> +): Boolean { + return MultiToken_isApprovedForAll(account, operator); +} + +export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + id: Uint<128>, + value: Uint<128>, +): [] { + return MultiToken_transferFrom(fromAddress, to, id, value); +} + +``` diff --git a/content/contracts-compact/0.1.x/non-fungible-token.mdx b/content/contracts-compact/0.1.x/non-fungible-token.mdx new file mode 100644 index 00000000..a70985ac --- /dev/null +++ b/content/contracts-compact/0.1.x/non-fungible-token.mdx @@ -0,0 +1,164 @@ +--- +title: NonFungibleToken +--- + +{/* links */} +[non-fungible tokens]: ../contracts/5.x/tokens.mdx#different-kinds-of-tokens +[EIP-721]: https://eips.ethereum.org/EIPS/eip-721 +[ERC-165]: https://eips.ethereum.org/EIPS/eip-165 +[Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern +[_mint]: ./api/non-fungible-token#NonFungibleToken-_mint + +NonFungibleToken is a specification for [non-fungible tokens], +a type of token where all the units are unique and distinct from each other. +This module is an approximation of [EIP-721] written in the Compact programming language for the Midnight network. + +## ERC721 Compatibility + +Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. +Some features and behaviors are either not possible, not possible yet, +or changed because of the vastly different tech stack and Compact language constraints. + +***Notable changes*** + +* ***`Uint<128>` tokenIds*** - Since 256-bit unsigned integers are not supported, +the library uses the Compact type `Uint<128>`. +* ***No _baseURI() support*** - Native strings and string operations are not supported within the Compact language, +so 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. + +***Features and specifications NOT supported*** + +* ***Events*** - Midnight does not currently support events, but this is planned on being supported in the future. +* ***Uint256 type*** - There's ongoing research on ways to support uint256 in the future. +* ***Interface*** - Compact currently does not have a way to define a contract interface. +This library offers modules of contracts with free floating circuits; +nevertheless, there are no means of enforcing that all circuits are provided. +* ***ERC-165 Standard*** - Since Compact doesn't provide a way to define a contract interface, +it's not possible to implement an [ERC-165] like interface standard at this time. +* ***Safe Transfers*** - It's not possible to implement safe transfers without an [ERC-165]-like +interface standard at this time. + +## Contract-to-contract calls + +Contract-to-contract calls are currently not supported in the Compact language. +Due to this limitation, the current iteration of NonFungibleToken disallows transfers and mints to the `ContractAddress` type. +Transferring tokens to a contract may result in those tokens being locked forever. +The NonFungibleToken module, however, does provide `unsafe` circuit variants for users who wish to experiment with sending tokens to contracts. + + +The `unsafe` circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning +`_transfer`, `_mint`, etc. are planned to eventually allow the recipients to be of the `ContractAddress` type. + + +## Usage + +Import the NonFungibleToken module into the implementing contract. +It's recommended to prefix the module with `NonFungibleToken_` to avoid circuit signature clashes. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken" + prefix NonFungibleToken_; + +constructor(name: Opaque<"string">, symbol: Opaque<"string">) { + NonFungibleToken_initialize(name, symbol); +} + +``` + +Next, expose the circuits that users may call in the contract. + +This library enables extensibility by following the rules of the [Module/Contract Pattern]. +Note that circuits with a preceding underscore (`_likeThis`) are meant to be building blocks for implementing contracts. +Exposing [_mint] without some sort of access control, for example, would allow ANYONE to mint tokens. + +```typescript +export circuit name(): Opaque<"string"> { + return NonFungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return NonFungibleToken_symbol(); +} + +(...) +``` + +The following example is a simple non-fungible token contract that mints an NFT to the passed recipient upon construction. + +```typescript +// SimpleNonFungibleToken.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken" + prefix NonFungibleToken_; + +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + recipient: Either, ContractAddress>, + tokenURI: Opaque<"string">, +) { + const tokenId = 1 as Uint<128>; + NonFungibleToken_initialize(name, symbol); + NonFungibleToken__mint(recipient, tokenId); + NonFungibleToken__setTokenURI(tokenId, tokenURI); +} + +export circuit balanceOf(owner: Either, ContractAddress>): Uint<128> { + return NonFungibleToken_balanceOf(owner); +} + +export circuit ownerOf(tokenId: Uint<128>): Either, ContractAddress> { + return NonFungibleToken_ownerOf(tokenId); +} + +export circuit name(): Opaque<"string"> { + return NonFungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return NonFungibleToken_symbol(); +} + +export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> { + return NonFungibleToken_tokenURI(tokenId); +} + +export circuit approve(to: Either, ContractAddress>, tokenId: Uint<128>): [] { + NonFungibleToken_approve(to, tokenId); +} + +export circuit getApproved(tokenId: Uint<128>): Either, ContractAddress> { + return NonFungibleToken_getApproved(tokenId); +} + +export circuit setApprovalForAll( + operator: Either, ContractAddress>, + approved: Boolean +): [] { + NonFungibleToken_setApprovalForAll(operator, approved); +} + +export circuit isApprovedForAll( + owner: Either, ContractAddress>, + operator: Either, ContractAddress> +): Boolean { + return NonFungibleToken_isApprovedForAll(owner, operator); +} + +export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + tokenId: Uint<128> +): [] { + NonFungibleToken_transferFrom(fromAddress, to, tokenId); +} + +``` diff --git a/content/contracts-compact/0.1.x/ownable.mdx b/content/contracts-compact/0.1.x/ownable.mdx new file mode 100644 index 00000000..5deb387f --- /dev/null +++ b/content/contracts-compact/0.1.x/ownable.mdx @@ -0,0 +1,384 @@ +--- +title: Ownable +--- + +{/* links */} +[initialize]: api/ownable#Ownable-initialize +[transferOwnership]: api/ownable#Ownable-transferOwnership +[_transferOwnership]: api/ownable#Ownable-_transferOwnership +[_unsafeTransferOwnership]: api/ownable#Ownable-_unsafeTransferOwnership +[_unsafeUncheckedTransferOwnership]: api/ownable#Ownable-_unsafeUncheckedTransferOwnership +[computeAccountId]: api/ownable#Ownable-computeAccountId +[assertOnlyOwner]: api/ownable#Ownable-assertOnlyOwner +[Ownable API]: api/ownable#Ownable + +[ZOwnablePK_initialize]: api/ownable#ZOwnablePK-initialize +[ZOwnablePK_transferOwnership]: api/ownable#ZOwnablePK-transferOwnership +[ZOwnablePK_assertOnlyOwner]: api/ownable#ZOwnablePK-assertOnlyOwner +[_computeOwnerId]: api/ownable#ZOwnablePK-_computeOwnerId + +[ed25519]: https://ed25519.cr.yp.to/ +[rfc6979]: https://datatracker.ietf.org/doc/html/rfc6979 +[AccessControl]: ./access-control.mdx + +## Access Control Defined + +Access control—that is, "who is allowed to do this thing"—is incredibly important in the world of smart contracts. +The access control of your contract may govern who can mint tokens, vote on proposals, freeze transfers, and many other things. +It is therefore critical to understand how you implement it, lest someone else steals your whole system. +This library provides a variety of access control modules to suit your application and privacy needs. + +## Ownership and `Ownable` + +The most common and basic form of access control is the concept of ownership: +there's an account that is the owner of a contract and can do administrative tasks on it. +This approach is perfectly reasonable for contracts that have a single administrative user. + +This library provides an Ownable module for implementing ownership in your contracts. +The initial owner must be set by using the [initialize] circuit during construction. +This can later be changed with [transferOwnership]. + + + For more granular, role-based access control, see [AccessControl]. + + +### Identity Model + +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, +pseudonymous on-chain identity. + +To derive an account identifier off-chain (e.g. for the `initialOwner` parameter during construction), +use [computeAccountId]: + +```typescript +const accountId = Ownable_computeAccountId(secretKey); +``` + +The `secretKey` is a 32-byte cryptographically secure random value that the owner keeps private. + + + The `secretKey` must never be logged or persisted in plaintext. Loss of the key means loss of ownership. + Key exposure allows impersonation. Use `crypto.getRandomValues()` or equivalent to generate keys. + + +### Ownership Transfers + +Ownership can only be transferred to `Bytes<32>` account identifiers through the main transfer circuits +([transferOwnership] and [_transferOwnership]). + +In other words, ownership transfers to contract addresses are disallowed through these circuits. +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 + +This module offers experimental 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. +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. + + + The unsafe circuits are planned to become deprecated once contract-to-contract calls become available. + + +### Usage + +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)`: + +```ts +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +constructor( + initialOwner: Either, ContractAddress> +) { + Ownable_initialize(initialOwner); +} +``` + +To protect a circuit so that only the contract owner may call it, +insert the `assertOnlyOwner` circuit in the beginning of the circuit body like this: + +```ts +export circuit mySensitiveCircuit(): [] { + Ownable_assertOnlyOwner(); + + // Do something +} +``` + +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)`: + +```ts +export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { + Ownable_transferOwnership(newOwner); +} +``` + +Here's a complete contract showcasing how to integrate the Ownable module and protect sensitive circuits. + +```ts +// SimpleOwnable.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +/** + * Set `initialOwner` as the owner of the contract. + * `initialOwner` should be derived off-chain using computeAccountId(secretKey). + */ +constructor(initialOwner: Either, ContractAddress>) { + Ownable_initialize(initialOwner); +} + +/** + * The current owner of the contract. + */ +export circuit owner(): Either, ContractAddress> { + return Ownable_owner(); +} + +/** + * 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). + */ +export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { + Ownable_transferOwnership(newOwner); +} + +/** + * Leaves the contract without an owner. + * Can only be called by the current owner. + * Renouncing ownership means `mySensitiveCircuit` can never be called again. + */ +export circuit renounceOwnership(): [] { + Ownable_renounceOwnership(); +} + +/** + * This is the protected circuit that only the current owner can call. + */ +export circuit mySensitiveCircuit(): [] { + // Protects the circuit + Ownable_assertOnlyOwner(); + + // Do something +} +``` + + + For more complex logic, + contracts may transfer ownership to another user irrespective of the caller by leveraging [_transferOwnership]. + This is generally more useful when contract addresses are the owner or when a contract has a unique deployment process. + + +## Shielded Ownership and `ZOwnablePK` + +Privacy-preserving access control is a fundamental building block for confidential smart contracts on Midnight. +While the `Ownable` module stores the owner's account identifier on-chain, +many applications require administrative control without revealing who holds that authority. + +### Privacy-First Ownership + +The `ZOwnablePK` module implements shielded ownership meaning administrative control without identity disclosure. +The owner's public key is never revealed on-chain; +instead, the contract stores only a cryptographic commitment that proves ownership without exposing the underlying identity. + +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. + +### Commitment Scheme + +The `ZOwnablePK` module employs a two-layer cryptographic commitment scheme designed to provide privacy, +unlinkability, and collision resistance across deployments and ownership transfers. + +#### Owner ID Computation + +The foundation of the system is the owner identifier, computed as: + +```typescript +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 +or deterministically derived for recoverability. +This identifier serves as a privacy-preserving alternative to exposing the raw public key, +ensuring the owner's identity remains confidential. + +#### Owner Commitment Computation + +The final ownership commitment stored on-chain is computed as: + +```typescript +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. +* `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 +with other commitment schemes and enables safe protocol extensions. + +#### Security Properties + +This commitment scheme ensures that: + +* Public keys are never revealed on-chain. +* Observers cannot correlate past and future ownership. +* Cross-contract collisions are prevented through instance-specific salting. + +### Nonce Generation Strategies + +The choice of nonce generation strategy represents a fundamental trade-off between simplicity/security and recoverability. +Both approaches are valid, and the best choice depends on your specific threat model and operational requirements. + +#### Random Nonce + +Generating a cryptographically strong random nonce provides the strongest privacy guarantees: + +```typescript +const randomNonce = crypto.getRandomValues(new Uint8Array(32)); +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. +**Loss of either component results in permanent, irrecoverable loss of ownership.** + +#### Deterministic Nonce + +Deriving the nonce deterministically enables recovery through derivation schemes. Some examples: + +* `H(passphrase + context)` — recoverable from passphrase only, but passphrase becomes critical single point of failure. +* `H(publicKey + userPassphrase + context)` — requires both public key and passphrase. +* `H(signature + context) where signature = sign(context)` — leverages wallet without exposing private key. + + + When using signature-based nonce derivation, + ensure the wallet/library uses deterministic signatures ([ed25519] or [rfc6979] for ECDSA). + Non-deterministic signatures will generate different nonces on each signing, making recovery impossible. + Test the implementation by signing the same message twice then verify that the signatures match. + + +**Context-Dependent Derivations:** + +* Include contract address, deployment timestamp, user ID, etc. +* Trade-off: more context is more unique but harder to recreate. + + + Approaches that avoid private key exposure (public key + passphrase, signature-based) + are generally recommended for operational security. + + +**Security Considerations** + +The `ZOwnablePK` module remains agnostic to nonce generation methods, +placing the security/convenience decision entirely with the user. +Key considerations include: + +* **Backup requirements**: Random nonces require additional secure storage. +* **Recovery scenarios**: Deterministic nonces enable recovery. +* **Cross-contract correlation**: Reusing nonce strategies may reduce privacy across deployments. +* **Rotation costs**: Changing nonces requires ownership transfer transactions with associated costs. + +Users should carefully evaluate their threat model, operational requirements, and privacy needs when selecting a nonce generation strategy, +as this choice cannot be easily changed without transferring ownership. + +### Usage + +Import the `ZOwnablePK` module into the implementing contract and expose the ownership-handling circuits. +It's recommended to prefix the module with `ZOwnablePK_` to avoid circuit signature clashes. + +```typescript +// MyZOwnablePKContract.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK" + prefix ZOwnablePK_; + +constructor( + initOwnerCommitment: Bytes<32>, + instanceSalt: Bytes<32>, +) { + ZOwnablePK_initialize(initOwnerCommitment, instanceSalt); +} + +export circuit owner(): Bytes<32> { + return ZOwnablePK_owner(); +} + +export circuit transferOwnership(newOwnerCommitment: Bytes<32>): [] { + return ZOwnablePK_transferOwnership(disclose(newOwnerCommitment)); +} + +export circuit renounceOwnership(): [] { + return ZOwnablePK_renounceOwnership(); +} +``` + +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: + +```typescript +export circuit mySensitiveCircuit(): [] { + ZOwnablePK_assertOnlyOwner(); + + // Do something +} +``` + +This covers the basics for creating a contract, but before deploying the contract, +the owner's id must be derived for the commitment scheme because it's required to deploy the contract. + +First, the owner needs to generate a secret nonce that's stored in the owner's private state. +See [Nonce Generation Strategies](#nonce-generation-strategies). + +Once the owner has the secret nonce generated, they can derive their owner ID: + +```typescript +import { + CompactTypeBytes, + CompactTypeVector, + persistentHash, +} from '@midnight-ntwrk/compact-runtime'; +import { getRandomValues } from 'node:crypto'; + +// Owner ID +const generateId = ( + pk: Uint8Array, + nonce: Uint8Array, +): Uint8Array => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + return persistentHash(rt_type, [pk, nonce]); +}; + +// Instance salt for the constructor +const generateInstanceSalt = (): Uint8Array => { + return getRandomValues(new Uint8Array(32)); +} +``` + + + Another way to get the user ID is to expose [_computeOwnerId] in the contract and call this circuit off-chain through a contract simulator. + diff --git a/content/contracts-compact/0.1.x/security.mdx b/content/contracts-compact/0.1.x/security.mdx new file mode 100644 index 00000000..e8419e1b --- /dev/null +++ b/content/contracts-compact/0.1.x/security.mdx @@ -0,0 +1,103 @@ +--- +title: Security +--- + +The following documentation provides context, reasoning, and examples of modules found in the Security directory. + +## Initializable + +The Initializable module provides a simple mechanism that mimics the functionality of a constructor. +More specifically, it enables logic to be performed once and only once to set up a contract's initial state. +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: + +- Don't allow circuit calls until the contract is initialized. +- Can only initialize the contract once. + +### Usage + +```ts +// CustomContractStateSetup.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/security/Initializable" + prefix Initializable_; + +export ledger _fieldAfterDeployment: Field; + +export circuit doSomethingBeforeInitialized(): [] { + // Ensure action can only be done prior to custom initialization + Initializable_assertNotInitialized(); + // Do the thing +} + +export circuit setFieldAfterDeployment(f: Field): [] { + // Initialize so the circuit cannot be called again + Initializable_initialize(); + _fieldAfterDeployment = f; +} + +export circuit checkFieldAfterDeployment(): Field { + // Can only be called after the contract is initialized + Initializable_assertInitialized(); + return _fieldAfterDeployment; +} +``` + +## Pausable + +[Ownable]: ./ownable +[assertPaused]: ./api/security#Pausable-assertPaused +[assertNotPaused]: ./api/security#Pausable-assertNotPaused + +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 +or having an emergency switch to freeze all transactions in the event of a large bug. + +To become pausable, the contract should include `pause` and `unpause` circuits (which should be protected). +For circuits that should be available only when paused or not, +insert calls to [assertPaused] and [assertNotPaused] respectively. + +### Usage + +For example (using the [Ownable] module for access control): + +```ts +// OwnablePausable.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/security/Pausable" + prefix Pausable_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +constructor(initOwner: Either, ContractAddress>) { + Ownable_initialize(initOwner); +} + +export circuit pause(): [] { + Ownable_assertOnlyOwner(); + Pausable__pause(); +} + +export circuit unpause(): [] { + Ownable_assertOnlyOwner(); + Pausable__unpause(); +} + +export circuit whenNotPaused(): [] { + Pausable_assertNotPaused(); + // Do something +} + +export circuit whenPaused(): [] { + Pausable_assertPaused(); + // Do something +} +``` diff --git a/content/contracts-compact/0.1.x/shielded-access-control.mdx b/content/contracts-compact/0.1.x/shielded-access-control.mdx new file mode 100644 index 00000000..e3a3d34c --- /dev/null +++ b/content/contracts-compact/0.1.x/shielded-access-control.mdx @@ -0,0 +1,193 @@ +--- +title: ShieldedAccessControl +--- + +{/* links */} +[Role-Based Access Control (RBAC)]: https://en.wikipedia.org/wiki/Role-based_access_control +[AccessControl]: ./access-control.mdx +[ShieldedAccessControl API]: api/shielded-access-control + +[assertOnlyRole]: api/shielded-access-control#ShieldedAccessControl-assertOnlyRole +[canProveRole]: api/shielded-access-control#ShieldedAccessControl-canProveRole +[grantRole]: api/shielded-access-control#ShieldedAccessControl-grantRole +[_grantRole]: api/shielded-access-control#ShieldedAccessControl-_grantRole +[revokeRole]: api/shielded-access-control#ShieldedAccessControl-revokeRole +[_revokeRole]: api/shielded-access-control#ShieldedAccessControl-_revokeRole +[renounceRole]: api/shielded-access-control#ShieldedAccessControl-renounceRole +[_setRoleAdmin]: api/shielded-access-control#ShieldedAccessControl-_setRoleAdmin +[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 + +This module provides a **privacy-preserving** role-based access control mechanism. +Unlike [AccessControl], which stores role assignments in a public map, +ShieldedAccessControl stores roles as Merkle tree commitments which hides _who_ holds a role +while still allowing on-chain verification of authorization. + +If your contract does not require privacy for role holders, the simpler [AccessControl] module is recommended. + +## Why Shielded Access Control? + +In many applications, revealing which accounts hold administrative or privileged roles +creates a security and privacy risk. ShieldedAccessControl addresses this by ensuring +that role holders are never identified on-chain. Instead, callers prove ownership of a role +using zero-knowledge proofs against Merkle tree commitments. + +This is useful in scenarios such as: + +- **Private governance**: voters or committee members prove eligibility without revealing identity. +- **Confidential operations**: admins can manage a system without their addresses being publicly linked to the contract. +- **Pseudonymous authorization**: role holders maintain a stable pseudonymous identity for the lifetime of their role, but that identity cannot be linked to a real-world identity without key compromise. + +## How It Works + +ShieldedAccessControl is built on three cryptographic primitives: + +**Role commitments** are Merkle tree leaves that bind a role to an account without revealing either. +When a role is granted, a commitment is inserted into a Merkle tree: + +``` +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) +``` + +**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) +``` + +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. + +## Using `ShieldedAccessControl` + +Usage follows the same pattern as [AccessControl] which is to define role identifiers, +grant them during construction, and enforce them with [assertOnlyRole]. +The key difference is that role assignments are stored as Merkle tree commitments rather than in a public map, +and revocation uses a nullifier scheme rather than simply flipping a boolean. +Account identifiers also incorporate a per-instance salt, preventing cross-contract correlation of the same secret key. + +```ts +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl" + prefix SAC_; + +export sealed ledger MINTER_ROLE: Bytes<32>; + +constructor( + instanceSalt: Bytes<32>, + defaultAdmin: SAC_AccountIdentifier, + minter: SAC_AccountIdentifier +) { + SAC_initialize(instanceSalt); + MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); + // Grant the default admin role and the minter role + SAC__grantRole(SAC_DEFAULT_ADMIN_ROLE(), defaultAdmin); + SAC__grantRole(MINTER_ROLE as SAC_RoleIdentifier, minter); +} + +export circuit mint(/* ... */): [] { + SAC_assertOnlyRole(MINTER_ROLE as SAC_RoleIdentifier); + // ... minting logic +} +``` + + + The `instanceSalt` must be generated using a cryptographically secure random number generator + (e.g. `crypto.getRandomValues()`) prior to deployment. Weak or predictable salts weaken privacy guarantees. + + +### Deriving Account Identifiers + +Before an account can be granted a role, the grantor needs the grantee's `AccountIdentifier`. +This is derived off-chain using [computeAccountId]: + +```ts +const accountId = SAC_computeAccountId(secretKey, instanceSalt); +``` + +The `secretKey` is a 32-byte cryptographically secure random value that the user keeps private. +The `instanceSalt` is the same value used during contract initialization. + + + The `secretKey` must never be logged, persisted in plaintext, or exposed in browser devtools. + Loss of the key means loss of all roles held under that identity. Key exposure allows impersonation + and retroactive deanonymization. + + +### Granting and Revoking Roles + +Role management works similarly to [AccessControl]. Every role has an associated admin role. +Only accounts that can prove they hold the admin role may call [grantRole] and [revokeRole]. +By default, all roles are administered by [DEFAULT_ADMIN_ROLE]. +Custom admin relationships can be set using [_setRoleAdmin]. + +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. +To be re-authorized, the user must generate a new `secretKey` to produce a new `accountId`. + +Other roles held under the old secret key remain valid. The user will need to retain both keys +until all roles under the old key are no longer needed. + +### Renouncing Roles + +[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. + +### Proving Role Ownership + +Two circuits are available for verifying role ownership: + +- [assertOnlyRole]: reverts if the caller cannot prove they hold the role. Use this to gate access to circuits. +- [canProveRole]: returns a boolean without reverting. Useful for conditional logic where lacking a role is not an error. + +Both circuits disclose a nullifier, which means every call made under the same role instance +can be linked by an outside observer. This provides **pseudonymity, not anonymity**. + +## Privacy Considerations + +ShieldedAccessControl provides strong privacy guarantees, but it's important to understand what is and isn't hidden: + +**What observers CAN see:** +- When roles are granted and revoked +- All role identifiers and which are admin identifiers (disclosed via [getRoleAdmin], [_setRoleAdmin], [grantRole], and [revokeRole]) +- How many admins exist +- All role nullifiers (disclosed via `Set` operations in every grant, revoke, and role-proof transaction) +- 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 + +**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 + + + The internal circuits ([_grantRole], [_revokeRole]) do not disclose the role identifier, offering stronger privacy + when wrapped with custom authorization logic. The trade-off is that the implementing contract takes full + responsibility for access control. + + +## 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. + + + For the full API reference, see the [ShieldedAccessControl API]. + diff --git a/content/contracts-compact/0.1.x/utils.mdx b/content/contracts-compact/0.1.x/utils.mdx new file mode 100644 index 00000000..1437ed3e --- /dev/null +++ b/content/contracts-compact/0.1.x/utils.mdx @@ -0,0 +1,27 @@ +--- +title: Utils +--- + +The Utils module provides miscellaneous circuits and common utilities for Compact contract development. + +### Usage + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +export circuit performActionWhenEqual( + a: Either, + b: Either, +): [] { + const isEqual = Utils_isKeyOrAddressEqual(a, b); + if (isEqual) { + // Do something + } else { + // Do something else + } +} +``` diff --git a/content/contracts-compact/0.1.x/utils/constants.js b/content/contracts-compact/0.1.x/utils/constants.js new file mode 100644 index 00000000..27c5d08b --- /dev/null +++ b/content/contracts-compact/0.1.x/utils/constants.js @@ -0,0 +1,2 @@ +export const COMPACT_COMPILER_VERSION = "0.29.0"; +export const COMPACT_LANGUAGE_VERSION = "0.21.0"; diff --git a/content/contracts-compact/utils/replacements.ts b/content/contracts-compact/0.1.x/utils/replacements.ts similarity index 80% rename from content/contracts-compact/utils/replacements.ts rename to content/contracts-compact/0.1.x/utils/replacements.ts index 6072127f..598cd7f6 100644 --- a/content/contracts-compact/utils/replacements.ts +++ b/content/contracts-compact/0.1.x/utils/replacements.ts @@ -1,7 +1,7 @@ import { COMPACT_COMPILER_VERSION, COMPACT_LANGUAGE_VERSION } from "./constants"; export const REPLACEMENTS = { - include: ['**/content/contracts-compact/**/*.mdx'], + include: ['**/content/contracts-compact/0.1.x/**/*.mdx'], replacements: { compact_compiler_version: COMPACT_COMPILER_VERSION, compact_language_version: COMPACT_LANGUAGE_VERSION diff --git a/content/contracts-compact/zk-circuits-101.mdx b/content/contracts-compact/0.1.x/zk-circuits-101.mdx similarity index 100% rename from content/contracts-compact/zk-circuits-101.mdx rename to content/contracts-compact/0.1.x/zk-circuits-101.mdx diff --git a/content/contracts-compact/access-control.mdx b/content/contracts-compact/0.3.x/access-control.mdx similarity index 100% rename from content/contracts-compact/access-control.mdx rename to content/contracts-compact/0.3.x/access-control.mdx diff --git a/content/contracts-compact/api/access-control.mdx b/content/contracts-compact/0.3.x/api/access-control.mdx similarity index 100% rename from content/contracts-compact/api/access-control.mdx rename to content/contracts-compact/0.3.x/api/access-control.mdx diff --git a/content/contracts-compact/api/confidential-fungible-token.mdx b/content/contracts-compact/0.3.x/api/confidential-fungible-token.mdx similarity index 100% rename from content/contracts-compact/api/confidential-fungible-token.mdx rename to content/contracts-compact/0.3.x/api/confidential-fungible-token.mdx diff --git a/content/contracts-compact/api/crypto.mdx b/content/contracts-compact/0.3.x/api/crypto.mdx similarity index 100% rename from content/contracts-compact/api/crypto.mdx rename to content/contracts-compact/0.3.x/api/crypto.mdx diff --git a/content/contracts-compact/api/fungible-token.mdx b/content/contracts-compact/0.3.x/api/fungible-token.mdx similarity index 100% rename from content/contracts-compact/api/fungible-token.mdx rename to content/contracts-compact/0.3.x/api/fungible-token.mdx diff --git a/content/contracts-compact/api/multi-token.mdx b/content/contracts-compact/0.3.x/api/multi-token.mdx similarity index 100% rename from content/contracts-compact/api/multi-token.mdx rename to content/contracts-compact/0.3.x/api/multi-token.mdx diff --git a/content/contracts-compact/api/multisig.mdx b/content/contracts-compact/0.3.x/api/multisig.mdx similarity index 100% rename from content/contracts-compact/api/multisig.mdx rename to content/contracts-compact/0.3.x/api/multisig.mdx diff --git a/content/contracts-compact/api/native-shielded-token.mdx b/content/contracts-compact/0.3.x/api/native-shielded-token.mdx similarity index 100% rename from content/contracts-compact/api/native-shielded-token.mdx rename to content/contracts-compact/0.3.x/api/native-shielded-token.mdx diff --git a/content/contracts-compact/api/non-fungible-token.mdx b/content/contracts-compact/0.3.x/api/non-fungible-token.mdx similarity index 100% rename from content/contracts-compact/api/non-fungible-token.mdx rename to content/contracts-compact/0.3.x/api/non-fungible-token.mdx diff --git a/content/contracts-compact/api/ownable.mdx b/content/contracts-compact/0.3.x/api/ownable.mdx similarity index 100% rename from content/contracts-compact/api/ownable.mdx rename to content/contracts-compact/0.3.x/api/ownable.mdx diff --git a/content/contracts-compact/api/security.mdx b/content/contracts-compact/0.3.x/api/security.mdx similarity index 100% rename from content/contracts-compact/api/security.mdx rename to content/contracts-compact/0.3.x/api/security.mdx diff --git a/content/contracts-compact/api/shielded-access-control.mdx b/content/contracts-compact/0.3.x/api/shielded-access-control.mdx similarity index 100% rename from content/contracts-compact/api/shielded-access-control.mdx rename to content/contracts-compact/0.3.x/api/shielded-access-control.mdx diff --git a/content/contracts-compact/api/utils.mdx b/content/contracts-compact/0.3.x/api/utils.mdx similarity index 98% rename from content/contracts-compact/api/utils.mdx rename to content/contracts-compact/0.3.x/api/utils.mdx index 6304705a..06957fb9 100644 --- a/content/contracts-compact/api/utils.mdx +++ b/content/contracts-compact/0.3.x/api/utils.mdx @@ -5,7 +5,7 @@ title: Utils This package provides the API for all Utils modules. - For an overview of the module, read the [Utils guide](/contracts-compact/utils). + For an overview of the module, read the [Utils guide](../utils). ## Utils [toc] [#Utils] diff --git a/content/contracts-compact/confidential-fungible-token.mdx b/content/contracts-compact/0.3.x/confidential-fungible-token.mdx similarity index 100% rename from content/contracts-compact/confidential-fungible-token.mdx rename to content/contracts-compact/0.3.x/confidential-fungible-token.mdx diff --git a/content/contracts-compact/crypto.mdx b/content/contracts-compact/0.3.x/crypto.mdx similarity index 100% rename from content/contracts-compact/crypto.mdx rename to content/contracts-compact/0.3.x/crypto.mdx diff --git a/content/contracts-compact/extensibility.mdx b/content/contracts-compact/0.3.x/extensibility.mdx similarity index 100% rename from content/contracts-compact/extensibility.mdx rename to content/contracts-compact/0.3.x/extensibility.mdx diff --git a/content/contracts-compact/0.3.x/fungible-token.mdx b/content/contracts-compact/0.3.x/fungible-token.mdx new file mode 100644 index 00000000..93324a76 --- /dev/null +++ b/content/contracts-compact/0.3.x/fungible-token.mdx @@ -0,0 +1,205 @@ +--- +title: FungibleToken +--- + +{/* links */} +[fungible tokens]: /contracts/5.x/tokens#different-kinds-of-tokens +[EIP-20]: https://eips.ethereum.org/EIPS/eip-20 +[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 +[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. +This module is an approximation of [EIP-20] written in the Compact programming language for the Midnight network. + +## ERC20 Compatibility + +Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. +Some features and behaviors are either not possible, not possible yet, +or changed because of the vastly different tech stack and Compact language constraints. + +***Notable changes*** + +* ***`Uint<128>` as value type*** - Since 256-bit unsigned integers are not supported, +the library uses the Compact type `Uint<128>`. + +***Features and specifications NOT supported*** + +* ***Events*** - Midnight does not currently support events, but this is planned on being supported in the future. +* ***Uint256 type*** - There's ongoing research on ways to support uint256 in the future. +* ***Interface*** - Compact currently does not have a way to define a contract interface. +This library offers modules of contracts with free floating circuits; +nevertheless, there are no means of enforcing that all circuits are provided. + +## Contract-to-contract calls + +Contract-to-contract calls are currently not supported in the Compact language. +Due to this limitation, the current iteration of FungibleToken disallows transfers and mints to the `ContractAddress` type. +Transferring tokens to a contract may result in those tokens being locked forever. +The FungibleToken module, however, does provide `unsafe` circuit variants for users who wish to experiment with sending tokens to contracts. + + +The `unsafe` circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning +`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_; + +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. + + + 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. +It's recommended to prefix the module with `FungibleToken_` to avoid circuit signature clashes. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8> +) { + FungibleToken_initialize(name, symbol, decimals); +} + +``` + +Next, expose the circuits that users may call in the contract. + +This library enables extensibility by following the rules of the [Module/Contract Pattern]. +Note that circuits with a preceding underscore (`_likeThis`) are meant to be building blocks for implementing contracts. +Exposing [_mint] without some sort of access control, for example, would allow ANYONE to mint tokens. + +```typescript +export circuit name(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return FungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return FungibleToken_decimals(); +} + +(...) +``` + +The following example is a simple token contract with a fixed supply that's minted to the passed recipient upon construction. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, + recipient: Either, ContractAddress>, + fixedSupply: Uint<128>, +) { + FungibleToken_initialize(name, symbol, decimals); + FungibleToken__mint(recipient, fixedSupply); +} + +export circuit name(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return FungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return FungibleToken_decimals(); +} + +export circuit totalSupply(): Uint<128> { + return FungibleToken_totalSupply(); +} + +export circuit balanceOf(account: Either, ContractAddress>): Uint<128> { + return FungibleToken_balanceOf(account); +} + +export circuit allowance( + owner: Either, ContractAddress>, + spender: Either, ContractAddress> +): Uint<128> { + return FungibleToken_allowance(owner, spender); +} + +export circuit transfer(to: Either, ContractAddress>, value: Uint<128>): Boolean { + return FungibleToken_transfer(to, value); +} + +export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128>, +): Boolean { + return FungibleToken_transferFrom(fromAddress, to, value); +} + +export circuit approve(spender: Either, ContractAddress>, value: Uint<128>): Boolean { + return FungibleToken_approve(spender, value); +} + +``` diff --git a/content/contracts-compact/0.3.x/index.mdx b/content/contracts-compact/0.3.x/index.mdx new file mode 100644 index 00000000..ae64ef1b --- /dev/null +++ b/content/contracts-compact/0.3.x/index.mdx @@ -0,0 +1,180 @@ +--- +title: Contracts for Compact +--- + +{/* links */} +[Midnight]: https://midnight.network/ +[nvm]: https://github.com/nvm-sh/nvm +[yarn]: https://yarnpkg.com/getting-started/install +[compact-dev-tools]: https://docs.midnight.network/blog/compact-developer-tools + +**A library for secure smart contract development** written in Compact for [Midnight]. +This library consists of modules to build custom smart contracts. + + +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 + +Make sure you have [nvm] and [yarn] installed on your machine. + +Follow Midnight's [Compact Developer Tools][compact-dev-tools] installation guide and confirm that `compact` is in the `PATH` env variable. + +```bash +$ compact compile --version + +Compactc version: {{compact_compiler_version}} +{{compact_compiler_version}} +``` + +### Installation + +Create a directory for your project. + +```bash +mkdir my-project +cd my-project +``` + +Install the library. + +```bash +yarn add @openzeppelin/compact-contracts +``` + +### Write a custom contract using library modules + +In the root of `my-project`, create a custom contract using OpenZeppelin Compact modules. +Import the modules through `./node_modules/@openzeppelin/compact-contracts/...`. + +```typescript +// MyContract.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; +import "./node_modules/@openzeppelin/compact-contracts/security/Pausable" + prefix Pausable_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + _name: Opaque<"string">, + _symbol: Opaque<"string">, + _decimals: Uint<8>, + _recipient: Either, ContractAddress>, + _amount: Uint<128>, + _initOwner: Either, ContractAddress>, +) { + Ownable_initialize(_initOwner); + FungibleToken_initialize(_name, _symbol, _decimals); + FungibleToken__mint(_recipient, _amount); +} + +/** IFungibleToken */ + +export circuit name(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return FungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return FungibleToken_decimals(); +} + +export circuit totalSupply(): Uint<128> { + return FungibleToken_totalSupply(); +} + +export circuit balanceOf(account: Either, ContractAddress>): Uint<128> { + return FungibleToken_balanceOf(account); +} + +export circuit allowance( + owner: Either, ContractAddress>, + spender: Either, ContractAddress> +): Uint<128> { + return FungibleToken_allowance(owner, spender); +} + +export circuit transfer( + to: Either, ContractAddress>, + value: Uint<128>, +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_transfer(to, value); +} + +export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128> +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_transferFrom(fromAddress, to, value); +} + +export circuit approve(spender: Either, ContractAddress>, value: Uint<128>): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_approve(spender, value); +} + +/** IOwnable */ + +export circuit owner(): Either, ContractAddress> { + return Ownable_owner(); +} + +export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { + return Ownable_transferOwnership(newOwner); +} + +export circuit renounceOwnership(): [] { + return Ownable_renounceOwnership(); +} + +/** IPausable */ + +export circuit pause(): [] { + Ownable_assertOnlyOwner(); + Pausable__pause(); +} + +export circuit unpause(): [] { + Ownable_assertOnlyOwner(); + Pausable__unpause(); +} +``` + +### Compile the contract + +In the project root, compile the contract. + +```bash +% compact compile MyContract.compact artifacts/MyContract +Compiling 14 circuits: + circuit "allowance" (k=11, rows=1352) + 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=4977) + circuit "transferOwnership" (k=13, rows=2959) + circuit "unpause" (k=13, rows=2362) +Overall progress [====================] 14/14 +``` diff --git a/content/contracts-compact/multi-token.mdx b/content/contracts-compact/0.3.x/multi-token.mdx similarity index 100% rename from content/contracts-compact/multi-token.mdx rename to content/contracts-compact/0.3.x/multi-token.mdx diff --git a/content/contracts-compact/multisig.mdx b/content/contracts-compact/0.3.x/multisig.mdx similarity index 100% rename from content/contracts-compact/multisig.mdx rename to content/contracts-compact/0.3.x/multisig.mdx diff --git a/content/contracts-compact/native-shielded-token.mdx b/content/contracts-compact/0.3.x/native-shielded-token.mdx similarity index 100% rename from content/contracts-compact/native-shielded-token.mdx rename to content/contracts-compact/0.3.x/native-shielded-token.mdx diff --git a/content/contracts-compact/0.3.x/non-fungible-token.mdx b/content/contracts-compact/0.3.x/non-fungible-token.mdx new file mode 100644 index 00000000..5cb1346d --- /dev/null +++ b/content/contracts-compact/0.3.x/non-fungible-token.mdx @@ -0,0 +1,216 @@ +--- +title: NonFungibleToken +--- + +{/* links */} +[non-fungible tokens]: /contracts/5.x/tokens#different-kinds-of-tokens +[EIP-721]: https://eips.ethereum.org/EIPS/eip-721 +[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. +This module is an approximation of [EIP-721] written in the Compact programming language for the Midnight network. + +## ERC721 Compatibility + +Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. +Some features and behaviors are either not possible, not possible yet, +or changed because of the vastly different tech stack and Compact language constraints. + +***Notable changes*** + +* ***`Uint<128>` tokenIds*** - Since 256-bit unsigned integers are not supported, +the library uses the Compact type `Uint<128>`. +* ***No _baseURI() support*** - Native strings and string operations are not supported within the Compact language, +so 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. + +***Features and specifications NOT supported*** + +* ***Events*** - Midnight does not currently support events, but this is planned on being supported in the future. +* ***Uint256 type*** - There's ongoing research on ways to support uint256 in the future. +* ***Interface*** - Compact currently does not have a way to define a contract interface. +This library offers modules of contracts with free floating circuits; +nevertheless, there are no means of enforcing that all circuits are provided. +* ***ERC-165 Standard*** - Since Compact doesn't provide a way to define a contract interface, +it's not possible to implement an [ERC-165] like interface standard at this time. +* ***Safe Transfers*** - It's not possible to implement safe transfers without an [ERC-165]-like +interface standard at this time. + +## Contract-to-contract calls + +Contract-to-contract calls are currently not supported in the Compact language. +Due to this limitation, the current iteration of NonFungibleToken disallows transfers and mints to the `ContractAddress` type. +Transferring tokens to a contract may result in those tokens being locked forever. +The NonFungibleToken module, however, does provide `unsafe` circuit variants for users who wish to experiment with sending tokens to contracts. + + +The `unsafe` circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning +`_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_; + +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. + + + 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. +It's recommended to prefix the module with `NonFungibleToken_` to avoid circuit signature clashes. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken" + prefix NonFungibleToken_; + +constructor(name: Opaque<"string">, symbol: Opaque<"string">) { + NonFungibleToken_initialize(name, symbol); +} + +``` + +Next, expose the circuits that users may call in the contract. + +This library enables extensibility by following the rules of the [Module/Contract Pattern]. +Note that circuits with a preceding underscore (`_likeThis`) are meant to be building blocks for implementing contracts. +Exposing [_mint] without some sort of access control, for example, would allow ANYONE to mint tokens. + +```typescript +export circuit name(): Opaque<"string"> { + return NonFungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return NonFungibleToken_symbol(); +} + +(...) +``` + +The following example is a simple non-fungible token contract that mints an NFT to the passed recipient upon construction. + +```typescript +// SimpleNonFungibleToken.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken" + prefix NonFungibleToken_; + +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + recipient: Either, ContractAddress>, + tokenURI: Opaque<"string">, +) { + const tokenId = 1 as Uint<128>; + NonFungibleToken_initialize(name, symbol); + NonFungibleToken__mint(recipient, tokenId); + NonFungibleToken__setTokenURI(tokenId, tokenURI); +} + +export circuit balanceOf(owner: Either, ContractAddress>): Uint<128> { + return NonFungibleToken_balanceOf(owner); +} + +export circuit ownerOf(tokenId: Uint<128>): Either, ContractAddress> { + return NonFungibleToken_ownerOf(tokenId); +} + +export circuit name(): Opaque<"string"> { + return NonFungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return NonFungibleToken_symbol(); +} + +export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> { + return NonFungibleToken_tokenURI(tokenId); +} + +export circuit approve(to: Either, ContractAddress>, tokenId: Uint<128>): [] { + NonFungibleToken_approve(to, tokenId); +} + +export circuit getApproved(tokenId: Uint<128>): Either, ContractAddress> { + return NonFungibleToken_getApproved(tokenId); +} + +export circuit setApprovalForAll( + operator: Either, ContractAddress>, + approved: Boolean +): [] { + NonFungibleToken_setApprovalForAll(operator, approved); +} + +export circuit isApprovedForAll( + owner: Either, ContractAddress>, + operator: Either, ContractAddress> +): Boolean { + return NonFungibleToken_isApprovedForAll(owner, operator); +} + +export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + tokenId: Uint<128> +): [] { + NonFungibleToken_transferFrom(fromAddress, to, tokenId); +} + +``` diff --git a/content/contracts-compact/ownable.mdx b/content/contracts-compact/0.3.x/ownable.mdx similarity index 100% rename from content/contracts-compact/ownable.mdx rename to content/contracts-compact/0.3.x/ownable.mdx diff --git a/content/contracts-compact/security.mdx b/content/contracts-compact/0.3.x/security.mdx similarity index 100% rename from content/contracts-compact/security.mdx rename to content/contracts-compact/0.3.x/security.mdx diff --git a/content/contracts-compact/shielded-access-control.mdx b/content/contracts-compact/0.3.x/shielded-access-control.mdx similarity index 100% rename from content/contracts-compact/shielded-access-control.mdx rename to content/contracts-compact/0.3.x/shielded-access-control.mdx diff --git a/content/contracts-compact/utils.mdx b/content/contracts-compact/0.3.x/utils.mdx similarity index 100% rename from content/contracts-compact/utils.mdx rename to content/contracts-compact/0.3.x/utils.mdx diff --git a/content/contracts-compact/utils/constants.js b/content/contracts-compact/0.3.x/utils/constants.js similarity index 100% rename from content/contracts-compact/utils/constants.js rename to content/contracts-compact/0.3.x/utils/constants.js diff --git a/content/contracts-compact/0.3.x/utils/replacements.ts b/content/contracts-compact/0.3.x/utils/replacements.ts new file mode 100644 index 00000000..c38c569d --- /dev/null +++ b/content/contracts-compact/0.3.x/utils/replacements.ts @@ -0,0 +1,9 @@ +import { COMPACT_COMPILER_VERSION, COMPACT_LANGUAGE_VERSION } from "./constants"; + +export const REPLACEMENTS = { + include: ['**/content/contracts-compact/0.3.x/**/*.mdx'], + replacements: { + compact_compiler_version: COMPACT_COMPILER_VERSION, + compact_language_version: COMPACT_LANGUAGE_VERSION + } +} diff --git a/content/contracts-compact/0.3.x/zk-circuits-101.mdx b/content/contracts-compact/0.3.x/zk-circuits-101.mdx new file mode 100644 index 00000000..5e954d15 --- /dev/null +++ b/content/contracts-compact/0.3.x/zk-circuits-101.mdx @@ -0,0 +1,36 @@ +--- +title: ZK Circuits 101 +--- + +{/* links */} +[the math behind the scenes needs it that way]: https://zkhack.dev/whiteboard/module-three/ + +## What are ZK Circuits? + +Compact code is compiled into arithmetic circuits, which are mathematical representations of the contract's logic. +These circuits are made up of arithmetic gates. +The gates enforce the rules and constraints defined in the Compact program. +At a high level, ZK circuits can be thought of as a magic puzzle board that proves a series of steps was followed correctly - like a recipe. +The puzzle board is laid out in a grid like a giant sheet of graph paper with a certain number of rows. +Each row is a space where a step or rule that needs to be followed is written. +These steps/rules correspond to the gates that make up the arithmetic circuit. + +## Circuit Size and Domain + +The size of the board is called the **domain size** which is referred to as `k` in the documentation. +It’s always a power of 2 (like 256, 512, 1024, etc.), +because [the math behind the scenes needs it that way]. +Now, just because the board has 1024 rows doesn’t mean it uses all of them. +Maybe the recipe takes only 563 steps, so only 563 rows are filled and the rest are left blank. +These filled-in rows are called **used rows**. +So "k = 10, rows = 563" in the API Reference documentation that means "this circuit has a size of 2^10 = 1024 rows and only uses 563 rows". + +## Why Circuit Size Matters + +Why is this important? Well, when writing ZK circuits the size and number of rules to follow should be as small as possible. +The number of rules in a zero-knowledge circuit directly impacts both the prover time +(how long it takes to generate a proof) and, to a lesser extent, the proof size and verifier time. +Larger circuits with more rules require more computation to generate a proof, +which can make proof generation slower and more resource-intensive. +This is especially relevant for privacy-preserving blockchains like Midnight, +where proof generation is often the most computationally expensive part of a transaction. diff --git a/content/contracts-compact/0.4.x/access-control.mdx b/content/contracts-compact/0.4.x/access-control.mdx new file mode 100644 index 00000000..b4ec0879 --- /dev/null +++ b/content/contracts-compact/0.4.x/access-control.mdx @@ -0,0 +1,281 @@ +--- +title: AccessControl +--- + +{/* links */} +[Role-Based Access Control (RBAC)]: https://en.wikipedia.org/wiki/Role-based_access_control +[principle of least privilege]: https://en.wikipedia.org/wiki/Principle_of_least_privilege + +[FungibleToken]: ./fungible-token.mdx +[Ownable]: ./ownable.mdx +[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/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. + +Roles can be enforced using the [assertOnlyRole] circuit. +Separately, you will be able to define rules for how accounts can be granted a role, have it revoked, and more. + + + For a privacy-preserving variant that hides role holder identities on-chain, see [ShieldedAccessControl]. + + +## Role-Based Access Control + +While the simplicity of _ownership_ can be useful for simple systems or quick prototyping, +different levels of authorization are often needed. +You may want for an account to have permission to ban users from a system, but not create new tokens. +[Role-Based Access Control (RBAC)] offers flexibility in this regard. + +In essence, we will be defining multiple _roles_, each allowed to perform different sets of actions. +An account may have, for example, 'moderator', 'minter' or 'admin' roles, +which you will then check for instead of simply using `assertOnlyOwner`. +This check can be enforced through the [assertOnlyRole] circuit. +Separately, you will be able to define rules for how accounts can be granted a role, have it revoked, and more. + +Most software uses access control systems that are role-based: +some users are regular users, some may be supervisors or managers, and a few will often have administrative privileges. + +### Identity Model + +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 `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**: `_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] from the [Utils] module: + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +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. + +### Using `AccessControl` + +The Compact contracts library provides `AccessControl` for implementing role-based access control. +Its usage is straightforward: for each role that you want to define, +you will create a new role identifier that is used to grant, revoke, and check if an account has that role. + +Here's a simple example of using `AccessControl` with [FungibleToken] to define a 'minter' role, +which allows accounts that have this role to create new tokens: + +```ts +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl" + prefix AccessControl_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +export sealed ledger MINTER_ROLE: Bytes<32>; + +/** + * Initialize FungibleToken and MINTER_ROLE + */ +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, + minter: Either, ContractAddress> +) { + FungibleToken_initialize(name, symbol, decimals); + MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); + AccessControl__grantRole(MINTER_ROLE, minter); +} + +export circuit mint(recipient: Either, ContractAddress>, value: Uint<128>): [] { + AccessControl_assertOnlyRole(MINTER_ROLE); + FungibleToken__mint(recipient, value); +} + +``` + +Make sure you fully understand how [AccessControl] works before using it on your system, +or copy-pasting the examples from this guide. + + +While clear and explicit, this isn't anything we wouldn't have been able to achieve with [Ownable]. +Indeed, where `AccessControl` shines is in scenarios where granular permissions are required, +which can be implemented by defining _multiple_ roles. + + +Let's augment our FungibleToken example by also defining a 'burner' role, which lets accounts destroy tokens. + +```ts +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl" + prefix AccessControl_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +export sealed ledger MINTER_ROLE: Bytes<32>; +export sealed ledger BURNER_ROLE: Bytes<32>; + +/** + * Initialize FungibleToken and MINTER_ROLE + */ +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, + minter: Either, ContractAddress>, + burner: Either, ContractAddress> +) { + FungibleToken_initialize(name, symbol, decimals); + MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); + BURNER_ROLE = persistentHash>(pad(32, "BURNER_ROLE")); + AccessControl__grantRole(MINTER_ROLE, minter); + AccessControl__grantRole(BURNER_ROLE, burner); +} + +export circuit mint(recipient: Either, ContractAddress>, value: Uint<128>): [] { + AccessControl_assertOnlyRole(MINTER_ROLE); + FungibleToken__mint(recipient, value); +} + +export circuit burn(recipient: Either, ContractAddress>, value: Uint<128>): [] { + AccessControl_assertOnlyRole(BURNER_ROLE); + FungibleToken__burn(recipient, value); +} + +``` + +So clean! By splitting concerns this way, +more granular levels of permission may be implemented than were possible with the simpler _ownership_ approach to access control. +Limiting what each component of a system is able to do is known as the [principle of least privilege], +and is a good security practice. +Note that each account may still have more than one role, if so desired. + +### Granting and Revoking Roles + +The FungibleToken example above uses [_grantRole], +an internal circuit that is useful when programmatically assigning roles (such as during construction). +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. +To grant and revoke roles dynamically, you will need help from the _role's admin_. + +Every role has an associated admin role, +which grants permission to call the [grantRole] and [revokeRole] circuits. +A role can be granted or revoked by using these if the calling account has the corresponding admin role. +Multiple roles may have the same admin role to make management easier. +A role's admin can even be the same role itself, +which would cause accounts with that role to be able to also grant and revoke it. + +This mechanism can be used to create complex permissioning structures resembling organizational charts, +but it also provides an easy way to manage simpler applications. +`AccessControl` includes a special role, called `DEFAULT_ADMIN_ROLE`, +which acts as the **default admin role for all roles**. +An account with this role will be able to manage any other role, +unless [_setRoleAdmin] is used to select a new admin role. + +Since it is the admin for all roles by default, +and in fact it is also its own admin, this role carries significant risk. + +Let's take a look at the FungibleToken example, this time taking advantage of the default admin role: + +```ts +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl" + prefix AccessControl_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +export sealed ledger MINTER_ROLE: Bytes<32>; +export sealed ledger BURNER_ROLE: Bytes<32>; + +/** + * Initialize FungibleToken and MINTER_ROLE + */ +constructor( + name: Opaque<"string">, + symbol: Opaque<"string">, + decimals: Uint<8>, + defaultAdmin: Either, ContractAddress> +) { + FungibleToken_initialize(name, symbol, decimals); + MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); + 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 Utils_computeAccountId(secretKey). + AccessControl__grantRole(AccessControl_DEFAULT_ADMIN_ROLE(), defaultAdmin); +} + +export circuit mint(recipient: Either, ContractAddress>, value: Uint<128>): [] { + AccessControl_assertOnlyRole(MINTER_ROLE); + FungibleToken__mint(recipient, value); +} + +export circuit burn(recipient: Either, ContractAddress>, value: Uint<128>): [] { + AccessControl_assertOnlyRole(BURNER_ROLE); + FungibleToken__burn(recipient, value); +} + +``` + +Note that, unlike the previous examples, no accounts are granted the 'minter' or 'burner' roles. +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 [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. +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. + +### Unsafe circuits + +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"). +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. + + +The unsafe circuits are planned to become deprecated once contract-to-contract calls become available. + diff --git a/content/contracts-compact/0.4.x/api/access-control.mdx b/content/contracts-compact/0.4.x/api/access-control.mdx new file mode 100644 index 00000000..fada507f --- /dev/null +++ b/content/contracts-compact/0.4.x/api/access-control.mdx @@ -0,0 +1,286 @@ +--- +title: AccessControl API +--- + +This page provides the full AccessControl module API. + +Roles are referred to by their `Bytes<32>` identifier. +These should be exposed in the top-level contract and be unique. +The best way to achieve this is by using `export sealed ledger` hash digests that are initialized in the top-level contract: + +```typescript +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl" + prefix AccessControl_; + +export sealed ledger MY_ROLE: Bytes<32>; + +constructor() { + MY_ROLE = persistentHash>(pad(32, "MY_ROLE")); +} +``` + +To restrict access to a circuit, use [assertOnlyRole](#AccessControl-assertOnlyRole): + + +```typescript +circuit foo(): [] { + AccessControl_assertOnlyRole(MY_ROLE); +} +``` + +Roles can be granted and revoked dynamically via the [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole) circuits. +Each role has an associated admin role, +and only accounts that have a role's admin role can call [grantRole](#AccessControl-grantRole) and [revokeRole](#AccessControl-revokeRole). + +By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE` (zero bytes), +which means that only accounts with this role will be able to grant or revoke other roles. +More complex role relationships can be created by using [_setRoleAdmin](#AccessControl-_setRoleAdmin). + + + The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to grant and revoke this role. + Extra precautions should be taken to secure accounts that have been granted it. + + + + For an overview of the module, read the [AccessControl guide](../access-control). + + +## AccessControl [toc] [#AccessControl] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/access/AccessControl"; +``` + +--- + +### Ledger [toc] [#AccessControl-Ledger] +### Ledger [!toc] [#AccessControl-Ledger] + +#### _operatorRoles [toc] [#AccessControl-_operatorRoles] +#### [!toc] [#AccessControl-_operatorRoles] + + Mapping from a role identifier -> account -> its permissions. + + +#### _adminRoles [toc] [#AccessControl-_adminRoles] +#### [!toc] [#AccessControl-_adminRoles] + + Mapping from a role identifier to an admin role identifier. + + +### 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. + + The same key produces the same account identifier across all contracts. Users who + desire cross-contract unlinkability should use different keys per contract. + + +### Circuits [toc] [#AccessControl-Circuits] +### Circuits [!toc] [#AccessControl-Circuits] + +#### DEFAULT_ADMIN_ROLE [toc] [#AccessControl-DEFAULT_ADMIN_ROLE] +#### [!toc] [#AccessControl-DEFAULT_ADMIN_ROLE] + + 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](#AccessControl-_setRoleAdmin). + + +#### hasRole [toc] [#AccessControl-hasRole] +#### [!toc] [#AccessControl-hasRole] + + Returns `true` if `account` has been granted `roleId`. + + +#### assertOnlyRole [toc] [#AccessControl-assertOnlyRole] +#### [!toc] [#AccessControl-assertOnlyRole] + + 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](#AccessControl-wit_AccessControlSK) witness as `persistentHash(secretKey)`. + + Requirements: + + * The caller must have `roleId`. + + +#### _checkRole [toc] [#AccessControl-_checkRole] +#### [!toc] [#AccessControl-_checkRole] + + Reverts if `account` is missing `roleId`. + + Requirements: + + * `account` must have `roleId`. + + +#### getRoleAdmin [toc] [#AccessControl-getRoleAdmin] +#### [!toc] [#AccessControl-getRoleAdmin] + + 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). + + To change a role's admin use [_setRoleAdmin](#AccessControl-_setRoleAdmin). + + +#### grantRole [toc] [#AccessControl-grantRole] +#### [!toc] [#AccessControl-grantRole] + + Grants `roleId` to `account`. + + Requirements: + + * `account` must not be a ContractAddress. + * The caller must have `roleId`'s admin role. + + +#### revokeRole [toc] [#AccessControl-revokeRole] +#### [!toc] [#AccessControl-revokeRole] + + Revokes `roleId` from `account`. + + Requirements: + + * The caller must have `roleId`'s admin role. + + +#### renounceRole [toc] [#AccessControl-renounceRole] +#### [!toc] [#AccessControl-renounceRole] + + 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 + if they are compromised (such as when a trusted device is misplaced). + + 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: + + * The caller's computed account identifier must match `callerConfirmation`. + * The caller must not be a `ContractAddress`. + + +#### _setRoleAdmin [toc] [#AccessControl-_setRoleAdmin] +#### [!toc] [#AccessControl-_setRoleAdmin] + + Sets `adminRole` as `roleId`'s admin role. + + +#### _grantRole [toc] [#AccessControl-_grantRole] +#### [!toc] [#AccessControl-_grantRole] + + Attempts to grant `roleId` to `account` and returns a boolean indicating if `roleId` was granted. + Internal circuit without access restriction. + + Requirements: + + * `account` must not be a ContractAddress. + + +#### _unsafeGrantRole [toc] [#AccessControl-_unsafeGrantRole] +#### [!toc] [#AccessControl-_unsafeGrantRole] + + 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. + + + +#### _revokeRole [toc] [#AccessControl-_revokeRole] +#### [!toc] [#AccessControl-_revokeRole] + + Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. + Internal circuit without access restriction. + diff --git a/content/contracts-compact/0.4.x/api/confidential-fungible-token.mdx b/content/contracts-compact/0.4.x/api/confidential-fungible-token.mdx new file mode 100644 index 00000000..3b9e0cba --- /dev/null +++ b/content/contracts-compact/0.4.x/api/confidential-fungible-token.mdx @@ -0,0 +1,747 @@ +--- +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] + + 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] +### Ledger [!toc] [#ConfidentialFungibleToken-Ledger] + +#### _balances [toc] [#ConfidentialFungibleToken-_balances] +#### [!toc] [#ConfidentialFungibleToken-_balances] + + 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](#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](#ConfidentialFungibleToken-balanceOf) (spendable) + [pendingOf](#ConfidentialFungibleToken-pendingOf) (pending). + + +#### _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] + + 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](#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 + from tx history. + + +#### _creditEpochs [toc] [#ConfidentialFungibleToken-_creditEpochs] +#### [!toc] [#ConfidentialFungibleToken-_creditEpochs] + + 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] +#### [!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 by + matching `persistentHash(SK)` against the public accountId. + + +#### wit_ConfidentialTokenEK [toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenEK] +#### [!toc] [#ConfidentialFungibleToken-wit_ConfidentialTokenEK] + + 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] +#### [!toc] [#ConfidentialFungibleToken-wit_PlaintextBalance] + + 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] +#### [!toc] [#ConfidentialFungibleToken-wit_RandomnessSeed] + + Returns a single 32-byte seed from which all per-operation + randomness (and each memo's ephemeral) is derived in-circuit. + + 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] +### Circuits [!toc] [#ConfidentialFungibleToken-Circuits] + +#### MAX_TRANSFER_VALUE [toc] [#ConfidentialFungibleToken-MAX_TRANSFER_VALUE] +#### [!toc] [#ConfidentialFungibleToken-MAX_TRANSFER_VALUE] + + 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] +#### [!toc] [#ConfidentialFungibleToken-initialize] + + Initializes the token's name, symbol, and decimals. Intended to + be called once, from the deploying contract's constructor. + + Requirements: + + * Contract is not already initialized. + + +#### balanceOf [toc] [#ConfidentialFungibleToken-balanceOf] +#### [!toc] [#ConfidentialFungibleToken-balanceOf] + + 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. + + +#### pendingOf [toc] [#ConfidentialFungibleToken-pendingOf] +#### [!toc] [#ConfidentialFungibleToken-pendingOf] + + Returns `account`'s PENDING balance ciphertext (incoming credits + not yet swept into spendable), or Enc(0) if unregistered. The spendable + 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: + + * Contract is initialized. + + +#### allowance [toc] [#ConfidentialFungibleToken-allowance] +#### [!toc] [#ConfidentialFungibleToken-allowance] + + 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](#ConfidentialFungibleToken-transferFrom)/`burnFrom`) gates on + `_escrow` membership independently, so a non-existent allowance is unspendable. + + + +#### 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 encryption public key from + [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. + + + Requirements: + + * Contract is initialized. + * The caller is not already registered. + + +#### _mint [toc] [#ConfidentialFungibleToken-_mint] +#### [!toc] [#ConfidentialFungibleToken-_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 within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * `account` is registered. + + +#### _burn [toc] [#ConfidentialFungibleToken-_burn] +#### [!toc] [#ConfidentialFungibleToken-_burn] + + 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 within [MAX_TRANSFER_VALUE](#ConfidentialFungibleToken-MAX_TRANSFER_VALUE). + * The caller is registered and has a balance of at least `value`. + + +#### transfer [toc] [#ConfidentialFungibleToken-transfer] +#### [!toc] [#ConfidentialFungibleToken-transfer] + + 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 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] +#### [!toc] [#ConfidentialFungibleToken-_move] + + 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](#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](#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](#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](#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 + set its own policy; add a `to != caller` check in the wrapper to reject a + self-move. + + Requirements: + + * Contract is initialized. + * The caller is registered with a spendable balance of at least `value`. + * `to` is registered. + + +#### sweep [toc] [#ConfidentialFungibleToken-sweep] +#### [!toc] [#ConfidentialFungibleToken-sweep] + + Sweeps the caller's incoming `pending` pool into their + `spendable` balance. Credits land in `pending` (see the dual-balance note); + 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. + + + 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: 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 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] +#### [!toc] [#ConfidentialFungibleToken-transferFrom] + + Spends `value` from `fromAddress`'s escrow (granted to the + 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. + + + 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`. + + +#### clearMemos [toc] [#ConfidentialFungibleToken-clearMemos] +#### [!toc] [#ConfidentialFungibleToken-clearMemos] + + 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. + + + 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. + + +#### computeAccountId [toc] [#ConfidentialFungibleToken-computeAccountId] +#### [!toc] [#ConfidentialFungibleToken-computeAccountId] + + 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. + + 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] +#### [!toc] [#ConfidentialFungibleToken-_burnFrom] + + 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](#ConfidentialFungibleToken-_burn)). + + 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`. + + +--- + +## 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. Mint and burn disclose their amounts + through the delta on this value. + + +### 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 [totalSupply](#ConfidentialFungibleTokenPublicSupply-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>`. + + +#### _subSupply [toc] [#ConfidentialFungibleTokenPublicSupply-_subSupply] +#### [!toc] [#ConfidentialFungibleTokenPublicSupply-_subSupply] + + 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](#ConfidentialFungibleTokenPublicSupply-totalSupply) is at least `value`. + + +#### totalSupply [toc] [#ConfidentialFungibleTokenPublicSupply-totalSupply] +#### [!toc] [#ConfidentialFungibleTokenPublicSupply-totalSupply] + + Returns the public total token supply. + diff --git a/content/contracts-compact/0.4.x/api/crypto.mdx b/content/contracts-compact/0.4.x/api/crypto.mdx new file mode 100644 index 00000000..fd5c6c06 --- /dev/null +++ b/content/contracts-compact/0.4.x/api/crypto.mdx @@ -0,0 +1,523 @@ +--- +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] + + Single-receiver ElGamal ciphertext over Jubjub. + c1 = g^r + c2 = pk^r * m (lifted: m = g^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. 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](#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](#ElGamal-expandRandomness) would require + passing the domain constant as the *seed*. Do not normalise the orderings. + + +#### derivePk [toc] [#ElGamal-derivePk] +#### [!toc] [#ElGamal-derivePk] + + Derives the ElGamal public key `pk = g^secretToScalar(ek)` + from a secret `ek`. + + +#### expandRandomness [toc] [#ElGamal-expandRandomness] +#### [!toc] [#ElGamal-expandRandomness] + + 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] +#### [!toc] [#ElGamal-encryptZero] + + 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](#ElGamal-encryptZero) is + distinguishable as never-written. Rerandomize (or credit with fresh `r`) + before treating it as private. + + + +#### encryptPoint [toc] [#ElGamal-encryptPoint] +#### [!toc] [#ElGamal-encryptPoint] + + 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](#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](#ElGamal-encrypt) + bounds it via the discrete-log range). + + + 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`. Randomness is taken + as a `Field` directly to avoid a redundant `Bytes<32> -> Field` conversion + at every call site; [expandRandomness](#ElGamal-expandRandomness) produces `Field` for the same reason. + + 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` then + the result encrypts `-v` under the same key (with negated randomness). + This is the building block [sub](#ElGamal-sub) rests on. + + +#### add [toc] [#ElGamal-add] +#### [!toc] [#ElGamal-add] + + 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] +#### [!toc] [#ElGamal-sub] + + Homomorphically subtracts two ciphertexts: `Enc(a) - Enc(b)` + decrypts to `a - b`. Implemented as `add(a, negate(b))`. + + + 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] +#### [!toc] [#ElGamal-scalarMul] + + 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](#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. + + + +#### 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`. + Convenience over [add](#ElGamal-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. + + +#### subEncrypted [toc] [#ElGamal-subEncrypted] +#### [!toc] [#ElGamal-subEncrypted] + + Homomorphically subtracts `value` from the plaintext encrypted + 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](#ElGamal-assertDecryptsTo) before calling. + + + Requirements: + + * `pk` is not the identity point. + * `r` is non-zero. + + +#### rerandomize [toc] [#ElGamal-rerandomize] +#### [!toc] [#ElGamal-rerandomize] + + 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](#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: + + * `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: + + * `ek` is the secret for `pk` (`derivePk(ek) == pk`). + + +#### assertDecryptsToPoint [toc] [#ElGamal-assertDecryptsToPoint] +#### [!toc] [#ElGamal-assertDecryptsToPoint] + + 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](#ElGamal-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 `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](#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. + + + + 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`. + + +--- + +## 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 and + the field-masked value. + + +### 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 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] +#### [!toc] [#EcdhMask-encrypt] + + Encrypts `value` to `recipientPk` under ephemeral scalar `e`. + + + `e` MUST be fresh per call (see the module freshness precondition). + + + Requirements: + + * `recipientPk` is not the identity point. + * `e` is non-zero, a valid Jubjub scalar (`< ℓ`), secret, and fresh per call. + + +#### decrypt [toc] [#EcdhMask-decrypt] +#### [!toc] [#EcdhMask-decrypt] + + 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/0.4.x/api/fungible-token.mdx b/content/contracts-compact/0.4.x/api/fungible-token.mdx new file mode 100644 index 00000000..598a12f2 --- /dev/null +++ b/content/contracts-compact/0.4.x/api/fungible-token.mdx @@ -0,0 +1,525 @@ +--- +title: FungibleToken API +--- + +This module provides the full FungibleToken module API. + + + For an overview of the module, read the [FungibleToken guide](../fungible-token). + + +## FungibleToken [toc] [#FungibleToken] + + +```ts +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, 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] +#### [!toc] [#FungibleToken-_balances] + + Mapping from account addresses to their token balances. + + +#### _allowances [toc] [#FungibleToken-_allowances] +#### [!toc] [#FungibleToken-_allowances] + + Mapping from owner accounts to spender accounts and their allowances. + + +#### _totalSupply [toc] [#FungibleToken-_totalSupply] +#### [!toc] [#FungibleToken-_totalSupply] + + The total token supply. + + +#### _name [toc] [#FungibleToken-_name] +#### [!toc] [#FungibleToken-_name] + + The immutable token name. + + +#### _symbol [toc] [#FungibleToken-_symbol] +#### [!toc] [#FungibleToken-_symbol] + + The immutable token symbol. + + +#### _decimals [toc] [#FungibleToken-_decimals] +#### [!toc] [#FungibleToken-_decimals] + + The immutable token decimals. + + +### Witnesses [toc] [#FungibleToken-Witnesses] +### Witnesses [!toc] [#FungibleToken-Witnesses] + +#### wit_FungibleTokenSK [toc] [#FungibleToken-wit_FungibleTokenSK] +#### [!toc] [#FungibleToken-wit_FungibleTokenSK] + + 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. + + +### Circuits [toc] [#FungibleToken-Circuits] +### Circuits [!toc] [#FungibleToken-Circuits] + +#### initialize [toc] [#FungibleToken-initialize] +#### [!toc] [#FungibleToken-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. + + +#### name [toc] [#FungibleToken-name] +#### [!toc] [#FungibleToken-name] + + Returns the token name. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#FungibleToken-symbol] +#### [!toc] [#FungibleToken-symbol] + + Returns the symbol of the token. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#FungibleToken-decimals] +#### [!toc] [#FungibleToken-decimals] + + Returns the number of decimals used to get its user representation. + + Requirements: + + * Contract is initialized. + + +#### totalSupply [toc] [#FungibleToken-totalSupply] +#### [!toc] [#FungibleToken-totalSupply] + + Returns the value of tokens in existence. + + Requirements: + + * Contract is initialized. + + +#### balanceOf [toc] [#FungibleToken-balanceOf] +#### [!toc] [#FungibleToken-balanceOf] + + 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. + + +#### transfer [toc] [#FungibleToken-transfer] +#### [!toc] [#FungibleToken-transfer] + + 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. + + + Requirements: + + * 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] +#### [!toc] [#FungibleToken-_unsafeTransfer] + + Unsafe variant of [transfer](#FungibleToken-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. + 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`. + + +#### allowance [toc] [#FungibleToken-allowance] +#### [!toc] [#FungibleToken-allowance] + + 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. + + Manually checks if `owner` and `spender` are keys in the map and returns 0 if they are not. + + Requirements: + + * Contract is initialized. + + +#### approve [toc] [#FungibleToken-approve] +#### [!toc] [#FungibleToken-approve] + + Sets a `value` amount of tokens as allowance of `spender` over the caller's tokens. + + Requirements: + + * Contract is initialized. + * `spender` is not the zero address. + + +#### transferFrom [toc] [#FungibleToken-transferFrom] +#### [!toc] [#FungibleToken-transferFrom] + + 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. + + + 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`. + + +#### _unsafeTransferFrom [toc] [#FungibleToken-_unsafeTransferFrom] +#### [!toc] [#FungibleToken-_unsafeTransferFrom] + + Unsafe variant of [transferFrom](#FungibleToken-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. + 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`. + + +#### _transfer [toc] [#FungibleToken-_transfer] +#### [!toc] [#FungibleToken-_transfer] + + 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. + + + 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. + + +#### _unsafeUncheckedTransfer [toc] [#FungibleToken-_unsafeUncheckedTransfer] +#### [!toc] [#FungibleToken-_unsafeUncheckedTransfer] + + Unsafe variant of [transferFrom](#FungibleToken-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. + 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. + + +#### _mint [toc] [#FungibleToken-_mint] +#### [!toc] [#FungibleToken-_mint] + + 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. + + + Requirements: + + * Contract is initialized. + * `account` is not a ContractAddress. + * `account` is not the zero address. + + +#### _unsafeMint [toc] [#FungibleToken-_unsafeMint] +#### [!toc] [#FungibleToken-_unsafeMint] + + Unsafe variant of [_mint](#FungibleToken-_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. + Once contract-to-contract calls are supported, this circuit may be deprecated. + + + Requirements: + + * Contract is initialized. + * `account` is not the zero address. + + +#### _burn [toc] [#FungibleToken-_burn] +#### [!toc] [#FungibleToken-_burn] + + Destroys a `value` amount of tokens from `account`, lowering the total supply. + Relies on the `_update` mechanism. + + Requirements: + + * Contract is initialized. + * `account` is not the zero address. + * `account` must have at least a balance of `value`. + + +#### _approve [toc] [#FungibleToken-_approve] +#### [!toc] [#FungibleToken-_approve] + + Sets `value` as the allowance of `spender` over the `owner`'s tokens. + This circuit is equivalent to [approve](#FungibleToken-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. + + +#### _spendAllowance [toc] [#FungibleToken-_spendAllowance] +#### [!toc] [#FungibleToken-_spendAllowance] + + Updates `owner`'s allowance for `spender` based on spent `value`. + Does not update the allowance value in case of infinite allowance. + + Requirements: + + * Contract is initialized. + * `spender` must have at least an allowance of `value` from `owner`. + + diff --git a/content/contracts-compact/0.4.x/api/multi-token.mdx b/content/contracts-compact/0.4.x/api/multi-token.mdx new file mode 100644 index 00000000..cd94a071 --- /dev/null +++ b/content/contracts-compact/0.4.x/api/multi-token.mdx @@ -0,0 +1,445 @@ +--- +title: MultiToken API +--- + +This module provides the full MultiToken module API. + + + For an overview of the module, read the [MultiToken guide](../multi-token). + + +## MultiToken [toc] [#MultiToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; +``` + +--- + +### Ledger [toc] [#MultiToken-Ledger] +### Ledger [!toc] [#MultiToken-Ledger] + +#### _isInitialized [toc] [#MultiToken-_isInitialized] +#### [!toc] [#MultiToken-_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. + + +#### _balances [toc] [#MultiToken-_balances] +#### [!toc] [#MultiToken-_balances] + + Mapping from token ID to account balances. + + +#### _operatorApprovals [toc] [#MultiToken-_operatorApprovals] +#### [!toc] [#MultiToken-_operatorApprovals] + + Mapping from account to operator approvals. + + +#### _uri [toc] [#MultiToken-_uri] +#### [!toc] [#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] +### Witnesses [!toc] [#MultiToken-Witnesses] + +#### wit_MultiTokenSK [toc] [#MultiToken-wit_MultiTokenSK] +#### [!toc] [#MultiToken-wit_MultiTokenSK] + + 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. + + +### Circuits [toc] [#MultiToken-Circuits] +### Circuits [!toc] [#MultiToken-Circuits] + +#### initialize [toc] [#MultiToken-initialize] +#### [!toc] [#MultiToken-initialize] + + Initializes the contract by setting the base URI for all tokens. + + Requirements: + + * Must be called in the contract's constructor. + + +#### uri [toc] [#MultiToken-uri] +#### [!toc] [#MultiToken-uri] + + 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. + + +#### balanceOf [toc] [#MultiToken-balanceOf] +#### [!toc] [#MultiToken-balanceOf] + + Returns the amount of `id` tokens owned by `account`. + + Requirements: + + * Contract is initialized. + + +#### setApprovalForAll [toc] [#MultiToken-setApprovalForAll] +#### [!toc] [#MultiToken-setApprovalForAll] + + 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](#MultiToken-wit_MultiTokenSK) + witness as `persistentHash(secretKey)`. + + Requirements: + + * Contract is initialized. + * `operator` is not zero. + + +#### isApprovedForAll [toc] [#MultiToken-isApprovedForAll] +#### [!toc] [#MultiToken-isApprovedForAll] + + Queries if `operator` is an authorized operator for `account`. + + Requirements: + + * Contract is initialized. + + +#### transferFrom [toc] [#MultiToken-transferFrom] +#### [!toc] [#MultiToken-transferFrom] + + 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](#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. + + + 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`. + + +#### _transfer [toc] [#MultiToken-_transfer] +#### [!toc] [#MultiToken-_transfer] + + 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. + + + 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`. + + +#### _unsafeTransferFrom [toc] [#MultiToken-_unsafeTransferFrom] +#### [!toc] [#MultiToken-_unsafeTransferFrom] + + 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](#MultiToken-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. + 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`. + + +#### _unsafeTransfer [toc] [#MultiToken-_unsafeTransfer] +#### [!toc] [#MultiToken-_unsafeTransfer] + + 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. + + + 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`. + + +#### _setURI [toc] [#MultiToken-_setURI] +#### [!toc] [#MultiToken-_setURI] + + 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. + + 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. + + +#### _mint [toc] [#MultiToken-_mint] +#### [!toc] [#MultiToken-_mint] + + 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. + + + Requirements: + + * Contract is initialized. + * `to` is not zero. + * `to` is not a ContractAddress. + + +#### _unsafeMint [toc] [#MultiToken-_unsafeMint] +#### [!toc] [#MultiToken-_unsafeMint] + + Unsafe variant of [_mint](#MultiToken-_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. + Once contract-to-contract calls are supported, this circuit may be deprecated. + + + Requirements: + + * Contract is initialized. + * `to` is not zero. + + +#### _burn [toc] [#MultiToken-_burn] +#### [!toc] [#MultiToken-_burn] + + Destroys a `value` amount of tokens of type `id` from `fromAddress`. + + Requirements: + + * Contract is initialized. + * `fromAddress` is not zero. + * `fromAddress` must have an `id` balance of at least `value`. + + +#### _setApprovalForAll [toc] [#MultiToken-_setApprovalForAll] +#### [!toc] [#MultiToken-_setApprovalForAll] + + 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. + + + Requirements: + + * Contract is initialized. + * `owner` is not zero. + * `operator` is not zero. + + diff --git a/content/contracts-compact/0.4.x/api/multisig.mdx b/content/contracts-compact/0.4.x/api/multisig.mdx new file mode 100644 index 00000000..d6caf43f --- /dev/null +++ b/content/contracts-compact/0.4.x/api/multisig.mdx @@ -0,0 +1,1318 @@ +--- +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] + + 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] +### Witnesses [!toc] [#Signer-Witnesses] + +None. + +### Circuits [toc] [#Signer-Circuits] +### Circuits [!toc] [#Signer-Circuits] + +#### initialize [toc] [#Signer-initialize] +#### [!toc] [#Signer-initialize] + + 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: + + * 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] +#### [!toc] [#Signer-assertSigner] + + Asserts that the given caller is an active signer. + + Requirements: + + * `caller` must be a member of the signers registry. + + +#### assertThresholdMet [toc] [#Signer-assertThresholdMet] +#### [!toc] [#Signer-assertThresholdMet] + + 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](#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: + + * 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] +#### [!toc] [#Signer-getSignerCount] + + Returns the current signer count. Returns zero when no + signers have been registered. + + +#### getThreshold [toc] [#Signer-getThreshold] +#### [!toc] [#Signer-getThreshold] + + Returns the approval threshold. Returns zero when the + threshold has not been set. + + +#### isSigner [toc] [#Signer-isSigner] +#### [!toc] [#Signer-isSigner] + + Returns whether the given account is an active signer. + + +#### _addSigner [toc] [#Signer-_addSigner] +#### [!toc] [#Signer-_addSigner] + + Adds a new signer to the registry. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * `signer` must not already be an active signer. + + +#### _removeSigner [toc] [#Signer-_removeSigner] +#### [!toc] [#Signer-_removeSigner] + + Removes a signer from the registry. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * `signer` must be an active signer. + * Removal must not drop signer count below threshold. + + +#### _changeThreshold [toc] [#Signer-_changeThreshold] +#### [!toc] [#Signer-_changeThreshold] + + Updates the approval threshold. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * `newThreshold` must not be zero. + * `newThreshold` must not exceed the current signer count. + + +#### _setThreshold [toc] [#Signer-_setThreshold] +#### [!toc] [#Signer-_setThreshold] + + 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. + + Leaving `newThreshold` above the signer count is permitted and expected + 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](#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](#Signer-_changeThreshold) for + operational threshold changes with signer count validation. + + + Requirements: + + * `newThreshold` must not be 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, so a stored proposal can never read + `Inactive`. + + +#### 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 shielded user recipient. + + +#### unshieldedUserRecipient [toc] [#ProposalManager-unshieldedUserRecipient] +#### [!toc] [#ProposalManager-unshieldedUserRecipient] + + Constructs an unshielded user recipient. + + +#### contractRecipient [toc] [#ProposalManager-contractRecipient] +#### [!toc] [#ProposalManager-contractRecipient] + + Constructs a contract recipient. + + +#### toShieldedRecipient [toc] [#ProposalManager-toShieldedRecipient] +#### [!toc] [#ProposalManager-toShieldedRecipient] + + Converts a Recipient to a shielded send recipient. + Handles both ShieldedUser and Contract kinds. + + Requirements: + + * Recipient kind must be ShieldedUser or Contract. + + +#### toUnshieldedRecipient [toc] [#ProposalManager-toUnshieldedRecipient] +#### [!toc] [#ProposalManager-toUnshieldedRecipient] + + Converts a Recipient to an unshielded send recipient. + Handles both UnshieldedUser and Contract kinds. + + Requirements: + + * Recipient kind must be UnshieldedUser or Contract. + + +#### assertProposalExists [toc] [#ProposalManager-assertProposalExists] +#### [!toc] [#ProposalManager-assertProposalExists] + + Asserts that a proposal exists. + + Requirements: + + * Proposal with `id` must have been created. + + +#### assertProposalActive [toc] [#ProposalManager-assertProposalActive] +#### [!toc] [#ProposalManager-assertProposalActive] + + Asserts that a proposal exists and is active. + + Requirements: + + * Proposal must exist. + * Proposal status must be Active. + + +#### _createProposal [toc] [#ProposalManager-_createProposal] +#### [!toc] [#ProposalManager-_createProposal] + + Creates a new proposal. + + + 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 + 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 + it on execution are unrecoverable. + + + + `_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` must be greater than 0. + + +#### _cancelProposal [toc] [#ProposalManager-_cancelProposal] +#### [!toc] [#ProposalManager-_cancelProposal] + + Cancels a proposal. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * Proposal must be active. + + +#### _markExecuted [toc] [#ProposalManager-_markExecuted] +#### [!toc] [#ProposalManager-_markExecuted] + + Marks a proposal as executed. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * Proposal must be active. + + +#### getProposal [toc] [#ProposalManager-getProposal] +#### [!toc] [#ProposalManager-getProposal] + + Returns the full proposal data. + + +#### getProposalRecipient [toc] [#ProposalManager-getProposalRecipient] +#### [!toc] [#ProposalManager-getProposalRecipient] + + Returns the recipient of a proposal. + + +#### getProposalAmount [toc] [#ProposalManager-getProposalAmount] +#### [!toc] [#ProposalManager-getProposalAmount] + + Returns the amount of a proposal. + + +#### getProposalColor [toc] [#ProposalManager-getProposalColor] +#### [!toc] [#ProposalManager-getProposalColor] + + Returns the token color of a proposal. + + +#### getProposalStatus [toc] [#ProposalManager-getProposalStatus] +#### [!toc] [#ProposalManager-getProposalStatus] + + Returns the status of a proposal. + + +--- + +## 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] + + 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 here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * Deposit must not cause received total overflow. + + +#### _send [toc] [#ShieldedTreasury-_send] +#### [!toc] [#ShieldedTreasury-_send] + + Sends shielded tokens from the treasury. + + 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). + + + 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 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] +#### [!toc] [#ShieldedTreasury-getTokenBalance] + + Returns the current token balance for a color. + Reads the actual coin value from the UTXO map. + + +#### getReceivedTotal [toc] [#ShieldedTreasury-getReceivedTotal] +#### [!toc] [#ShieldedTreasury-getReceivedTotal] + + Returns the cumulative received total for a color. + + +#### getSentTotal [toc] [#ShieldedTreasury-getSentTotal] +#### [!toc] [#ShieldedTreasury-getSentTotal] + + Returns the cumulative sent total for a color. + + +#### getReceivedMinusSent [toc] [#ShieldedTreasury-getReceivedMinusSent] +#### [!toc] [#ShieldedTreasury-getReceivedMinusSent] + + Returns the difference between cumulative received + and cumulative sent totals for a color. Should equal + [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. + + + +--- + +## 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] + + Receives a shielded coin into the treasury. + + +#### _send [toc] [#ShieldedTreasuryStateless-_send] +#### [!toc] [#ShieldedTreasuryStateless-_send] + + 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` must be >= `amount`. + + +--- + +## 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 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](#UnshieldedTreasury-getTokenBalance) reports the same as absence. + + + Access control is NOT enforced here. + The consuming contract must gate this behind its own + authorization policy. + + + Requirements: + + * Deposit must not cause balance overflow. + + +#### _send [toc] [#UnshieldedTreasury-_send] +#### [!toc] [#UnshieldedTreasury-_send] + + 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](#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. + + + Requirements: + + * The treasury must hold sufficient balance for the given token color. + + +#### getTokenBalance [toc] [#UnshieldedTreasury-getTokenBalance] +#### [!toc] [#UnshieldedTreasury-getTokenBalance] + + Returns the balance for a given token color. + + +--- + +## 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] + + 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] +### Witnesses [!toc] [#ForwarderPrivate-Witnesses] + +None. + +### Circuits [toc] [#ForwarderPrivate-Circuits] +### Circuits [!toc] [#ForwarderPrivate-Circuits] + +#### initialize [toc] [#ForwarderPrivate-initialize] +#### [!toc] [#ForwarderPrivate-initialize] + + Initializes the forwarder with a parent commitment. + Called once from the preset constructor. The commitment is + immutable after init. + + Requirements: + + * 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] +#### [!toc] [#ForwarderPrivate-_deposit] + + Receives a shielded coin into the forwarder's custody. + No ledger write — coins dwell at the contract address until drained. + + Requirements: + + * Contract must be initialized. + + +#### _drain [toc] [#ForwarderPrivate-_drain] +#### [!toc] [#ForwarderPrivate-_drain] + + 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](#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`. + + 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 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] +#### [!toc] [#ForwarderPrivate-_calculateParentCommitment] + + Computes the parent commitment from `(parentAddr, opSecret)`. + Pure circuit — used off-chain by the deployer to compute the + constructor argument, and inside [_drain](#ForwarderPrivate-_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. + + +--- + +## 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 generically as + `Either` for forward compatibility. + [initialize](#ForwarderShielded-initialize) only ever writes the `left` (coin public key) arm today. See + the module header. + + +#### _isInitialized [toc] [#ForwarderShielded-_isInitialized] +#### [!toc] [#ForwarderShielded-_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. + + +### Witnesses [toc] [#ForwarderShielded-Witnesses] +### Witnesses [!toc] [#ForwarderShielded-Witnesses] + +None. + +### Circuits [toc] [#ForwarderShielded-Circuits] +### Circuits [!toc] [#ForwarderShielded-Circuits] + +#### initialize [toc] [#ForwarderShielded-initialize] +#### [!toc] [#ForwarderShielded-initialize] + + 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 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] +#### [!toc] [#ForwarderShielded-_deposit] + + 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 must be 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 generically as + `Either` for forward compatibility. + [initialize](#ForwarderUnshielded-initialize) only ever writes the `right` (user address) arm today. See + the module header. + + +#### _isInitialized [toc] [#ForwarderUnshielded-_isInitialized] +#### [!toc] [#ForwarderUnshielded-_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. + + +### Witnesses [toc] [#ForwarderUnshielded-Witnesses] +### Witnesses [!toc] [#ForwarderUnshielded-Witnesses] + +None. + +### Circuits [toc] [#ForwarderUnshielded-Circuits] +### Circuits [!toc] [#ForwarderUnshielded-Circuits] + +#### initialize [toc] [#ForwarderUnshielded-initialize] +#### [!toc] [#ForwarderUnshielded-initialize] + + 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 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] +#### [!toc] [#ForwarderUnshielded-_deposit] + + Receives an unshielded amount of `color` and atomically + forwards it to `_parent` via `sendUnshielded` (always the `right` / + user-address arm today). + + Requirements: + + * Contract must be initialized. + + diff --git a/content/contracts-compact/0.4.x/api/native-shielded-token.mdx b/content/contracts-compact/0.4.x/api/native-shielded-token.mdx new file mode 100644 index 00000000..81b5224a --- /dev/null +++ b/content/contracts-compact/0.4.x/api/native-shielded-token.mdx @@ -0,0 +1,1695 @@ +--- +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; 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] + + 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] +### Witnesses [!toc] [#NativeShieldedToken-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedToken-Circuits] +### Circuits [!toc] [#NativeShieldedToken-Circuits] + +#### initialize [toc] [#NativeShieldedToken-initialize] +#### [!toc] [#NativeShieldedToken-initialize] + + 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](#NativeShieldedToken-tokenColor) only + in post-deployment circuits, never in the constructor. + + +#### isInitialized [toc] [#NativeShieldedToken-isInitialized] +#### [!toc] [#NativeShieldedToken-isInitialized] + + Returns whether the contract has been initialized. + + +#### name [toc] [#NativeShieldedToken-name] +#### [!toc] [#NativeShieldedToken-name] + + Returns the token name. + + Requirements: + + * Contract is initialized. + + +#### symbol [toc] [#NativeShieldedToken-symbol] +#### [!toc] [#NativeShieldedToken-symbol] + + Returns the symbol of the token. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#NativeShieldedToken-decimals] +#### [!toc] [#NativeShieldedToken-decimals] + + Returns the number of decimals used to get its user representation. + + 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 a + caller-supplied nonce. + + 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. + + + 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. + + +#### _mintToSelf [toc] [#NativeShieldedToken-_mintToSelf] +#### [!toc] [#NativeShieldedToken-_mintToSelf] + + Mints `amount` to this 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](#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. + + + Requirements: + + * Contract is initialized. + + +#### _burn [toc] [#NativeShieldedToken-_burn] +#### [!toc] [#NativeShieldedToken-_burn] + + 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`. + + 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 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](#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. + + + 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 + `refundTo` would silently 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 + 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. + + 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] + + 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] +### Witnesses [!toc] [#NativeShieldedTokenFamily-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenFamily-Circuits] +### Circuits [!toc] [#NativeShieldedTokenFamily-Circuits] + +#### initialize [toc] [#NativeShieldedTokenFamily-initialize] +#### [!toc] [#NativeShieldedTokenFamily-initialize] + + 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](#NativeShieldedTokenFamily-tokenColor) only + in post-deployment circuits, never in the constructor. + + +#### isInitialized [toc] [#NativeShieldedTokenFamily-isInitialized] +#### [!toc] [#NativeShieldedTokenFamily-isInitialized] + + Returns whether the contract has been initialized. + + +#### 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; see the + module notes. + + 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. + + Requirements: + + * Contract is initialized. + + +#### tokenColor [toc] [#NativeShieldedTokenFamily-tokenColor] +#### [!toc] [#NativeShieldedTokenFamily-tokenColor] + + 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. + + +#### _mint [toc] [#NativeShieldedTokenFamily-_mint] +#### [!toc] [#NativeShieldedTokenFamily-_mint] + + Mints `amount` of the token identified by `domain` to + `recipient`, using a caller-supplied nonce. + + 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. + + + 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. + + +#### _mintToSelf [toc] [#NativeShieldedTokenFamily-_mintToSelf] +#### [!toc] [#NativeShieldedTokenFamily-_mintToSelf] + + 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())`. + + 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. + + + Requirements: + + * Contract is initialized. + + +#### _burn [toc] [#NativeShieldedTokenFamily-_burn] +#### [!toc] [#NativeShieldedTokenFamily-_burn] + + 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`. + + 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 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](#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. + + + 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 silently burn the change as well). + + +#### _burnFromSelf [toc] [#NativeShieldedTokenFamily-_burnFromSelf] +#### [!toc] [#NativeShieldedTokenFamily-_burnFromSelf] + + 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. + + 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"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenCore-Ledger] +### Ledger [!toc] [#NativeShieldedTokenCore-Ledger] + +#### _isInitialized [toc] [#NativeShieldedTokenCore-_isInitialized] +#### [!toc] [#NativeShieldedTokenCore-_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. + + +#### _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 (via + the wrapping flavor's [initialize](#NativeShieldedTokenCore-initialize)). Failure to do so can lead to an + irreparable contract. + + +#### assertInitialized [toc] [#NativeShieldedTokenCore-assertInitialized] +#### [!toc] [#NativeShieldedTokenCore-assertInitialized] + + Asserts that the contract has been initialized, throwing an + error if not. + + Requirements: + + * Contract must be initialized. + + +#### assertNotInitialized [toc] [#NativeShieldedTokenCore-assertNotInitialized] +#### [!toc] [#NativeShieldedTokenCore-assertNotInitialized] + + Asserts that the contract has not been initialized, throwing + an error if it has. + + Requirements: + + * Contract must not be initialized. + + +#### isInitialized [toc] [#NativeShieldedTokenCore-isInitialized] +#### [!toc] [#NativeShieldedTokenCore-isInitialized] + + Returns whether the contract has been initialized. + Does not gate on init (it reports it), so it is safe to call before + [initialize](#NativeShieldedTokenCore-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 symbol of the token. + + Requirements: + + * Contract is initialized. + + +#### decimals [toc] [#NativeShieldedTokenCore-decimals] +#### [!toc] [#NativeShieldedTokenCore-decimals] + + 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. + + +#### 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. + + Requirements: + + * Contract is initialized. + + +#### _mint [toc] [#NativeShieldedTokenCore-_mint] +#### [!toc] [#NativeShieldedTokenCore-_mint] + + Mints `amount` of the token identified by `domain` to + `recipient`, using a caller-supplied nonce. + + 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 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. + + +#### _burn [toc] [#NativeShieldedTokenCore-_burn] +#### [!toc] [#NativeShieldedTokenCore-_burn] + + 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`. + + 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 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](#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. + + + 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 silently 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 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. + + 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"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenPublicSupply-Ledger] +### Ledger [!toc] [#NativeShieldedTokenPublicSupply-Ledger] + +#### _totalMinted [toc] [#NativeShieldedTokenPublicSupply-_totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupply-_totalMinted] + + Exact amount minted per domain. See "Accounting guarantees". + + +#### _totalBurned [toc] [#NativeShieldedTokenPublicSupply-_totalBurned] +#### [!toc] [#NativeShieldedTokenPublicSupply-_totalBurned] + + Contract-mediated amount burned per domain (lower bound). + + +### 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`. + + +#### _addBurned [toc] [#NativeShieldedTokenPublicSupply-_addBurned] +#### [!toc] [#NativeShieldedTokenPublicSupply-_addBurned] + + 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] +#### [!toc] [#NativeShieldedTokenPublicSupply-totalMinted] + + Returns the exact amount ever minted. + + +#### 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. See the module notes. + + + +--- + +## NativeShieldedTokenFamilyPublicSupply [toc] [#NativeShieldedTokenFamilyPublicSupply] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenFamilyPublicSupply"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenFamilyPublicSupply-Ledger] +### Ledger [!toc] [#NativeShieldedTokenFamilyPublicSupply-Ledger] + +#### _totalMinted [toc] [#NativeShieldedTokenFamilyPublicSupply-_totalMinted] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_totalMinted] + + Exact amount minted per domain. See "Accounting guarantees". + + +#### _totalBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-_totalBurned] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_totalBurned] + + Contract-mediated amount burned per domain (lower bound). + + +### 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`. + + +#### _addBurned [toc] [#NativeShieldedTokenFamilyPublicSupply-_addBurned] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-_addBurned] + + 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] +#### [!toc] [#NativeShieldedTokenFamilyPublicSupply-totalMinted] + + Returns the exact amount ever minted for `domain`. + + +#### 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. + + + +#### 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. See the module notes. + + + +--- + +## NativeShieldedTokenPublicSupplyCore [toc] [#NativeShieldedTokenPublicSupplyCore] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenPublicSupplyCore"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenPublicSupplyCore-Ledger] +### Ledger [!toc] [#NativeShieldedTokenPublicSupplyCore-Ledger] + +#### _totalMinted [toc] [#NativeShieldedTokenPublicSupplyCore-_totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_totalMinted] + + Exact amount minted per domain. See "Accounting guarantees". + + +#### _totalBurned [toc] [#NativeShieldedTokenPublicSupplyCore-_totalBurned] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_totalBurned] + + Contract-mediated amount burned per domain (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`. + Checks for overflow in order to output a readable error message. + + +#### _addBurned [toc] [#NativeShieldedTokenPublicSupplyCore-_addBurned] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-_addBurned] + + 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](#NativeShieldedTokenPublicSupplyCore-totalSupply). + The guard also keeps the add and the [totalSupply](#NativeShieldedTokenPublicSupplyCore-totalSupply) subtraction within + bounds, so no separate overflow/underflow check is needed. + + +#### totalMinted [toc] [#NativeShieldedTokenPublicSupplyCore-totalMinted] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-totalMinted] + + Returns the exact amount ever minted for `domain`. + + +#### 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. + + + +#### totalSupply [toc] [#NativeShieldedTokenPublicSupplyCore-totalSupply] +#### [!toc] [#NativeShieldedTokenPublicSupplyCore-totalSupply] + + Returns `totalMinted(domain) - totalBurned(domain)`, or 0 if + [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. + + +--- + +## NativeShieldedTokenDerivedNonce [toc] [#NativeShieldedTokenDerivedNonce] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/extensions/NativeShieldedTokenDerivedNonce"; +``` + +--- + +### Ledger [toc] [#NativeShieldedTokenDerivedNonce-Ledger] +### Ledger [!toc] [#NativeShieldedTokenDerivedNonce-Ledger] + +#### _counter [toc] [#NativeShieldedTokenDerivedNonce-_counter] +#### [!toc] [#NativeShieldedTokenDerivedNonce-_counter] + + Monotonic index feeding the derived-nonce derivation. + + +### 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, + 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. + + + +--- + +## ZOwnableNativeShieldedToken [toc] [#ZOwnableNativeShieldedToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/presets/ZOwnableNativeShieldedToken"; +``` + +--- + +### Ledger [toc] [#ZOwnableNativeShieldedToken-Ledger] +### Ledger [!toc] [#ZOwnableNativeShieldedToken-Ledger] + +#### _ownerCommitment [toc] [#ZOwnableNativeShieldedToken-_ownerCommitment] +#### [!toc] [#ZOwnableNativeShieldedToken-_ownerCommitment] + + 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] +#### [!toc] [#ZOwnableNativeShieldedToken-_counter] + + 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] +#### [!toc] [#ZOwnableNativeShieldedToken-_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 owner identifiers or domain parameters. It is immutable after initialization. + + +#### _ownableIsInitialized [toc] [#ZOwnableNativeShieldedToken-_ownableIsInitialized] +#### [!toc] [#ZOwnableNativeShieldedToken-_ownableIsInitialized] + + 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] +#### [!toc] [#ZOwnableNativeShieldedToken-_domain] + + Domain separator fixed at construction; with the contract + address it determines this token's color. + + +#### _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] + + 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] +#### [!toc] [#ZOwnableNativeShieldedToken-_totalMinted] + + Exact amount minted per domain. See "Accounting guarantees". + + +#### _totalBurned [toc] [#ZOwnableNativeShieldedToken-_totalBurned] +#### [!toc] [#ZOwnableNativeShieldedToken-_totalBurned] + + Contract-mediated amount burned per domain (lower bound). + + +### 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 key leaves the contract with no + usable owner and no recovery. + + + Requirements: + + * Contract is not initialized. + * `ownerId` is not zero. + + +#### mint [toc] [#ZOwnableNativeShieldedToken-mint] +#### [!toc] [#ZOwnableNativeShieldedToken-mint] + + Mints `amount` to `recipient` and adds it to the minted total. + Owner only. + + Requirements: + + * Caller is the owner. + * `recipient` is not zero. + + +#### burn [toc] [#ZOwnableNativeShieldedToken-burn] +#### [!toc] [#ZOwnableNativeShieldedToken-burn] + + 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` \<= `coin.value`. + * `refundTo` is not zero. + + +#### transferOwnership [toc] [#ZOwnableNativeShieldedToken-transferOwnership] +#### [!toc] [#ZOwnableNativeShieldedToken-transferOwnership] + + Transfers ownership to `newOwnerId`. 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 key leaves the contract with no + usable owner and no recovery. + + + 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. + + + 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)`. + + Requirements: + + * `pk` is a coin public key. + + +#### 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/0.4.x/api/non-fungible-token.mdx b/content/contracts-compact/0.4.x/api/non-fungible-token.mdx new file mode 100644 index 00000000..51d3044d --- /dev/null +++ b/content/contracts-compact/0.4.x/api/non-fungible-token.mdx @@ -0,0 +1,662 @@ +--- +title: NonFungibleToken API +--- + +This module provides the full NonFungibleToken module API. + + + For an overview of the module, read the [NonFungibleToken guide](../non-fungible-token). + + +## NonFungibleToken [toc] [#NonFungibleToken] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; +``` + +--- + +### Ledger [toc] [#NonFungibleToken-Ledger] +### Ledger [!toc] [#NonFungibleToken-Ledger] + +#### _name [toc] [#NonFungibleToken-_name] +#### [!toc] [#NonFungibleToken-_name] + + / Public state + + +#### _symbol [toc] [#NonFungibleToken-_symbol] +#### [!toc] [#NonFungibleToken-_symbol] + + The immutable token symbol. + + +#### _isInitialized [toc] [#NonFungibleToken-_isInitialized] +#### [!toc] [#NonFungibleToken-_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. + + +#### _owners [toc] [#NonFungibleToken-_owners] +#### [!toc] [#NonFungibleToken-_owners] + + Mapping from token IDs to their owner addresses. + + +#### _balances [toc] [#NonFungibleToken-_balances] +#### [!toc] [#NonFungibleToken-_balances] + + Mapping from account addresses to their token balances. + + +#### _tokenApprovals [toc] [#NonFungibleToken-_tokenApprovals] +#### [!toc] [#NonFungibleToken-_tokenApprovals] + + Mapping from token IDs to approved addresses. + + +#### _operatorApprovals [toc] [#NonFungibleToken-_operatorApprovals] +#### [!toc] [#NonFungibleToken-_operatorApprovals] + + Mapping from owner addresses to operator approvals. + + +#### _tokenURIs [toc] [#NonFungibleToken-_tokenURIs] +#### [!toc] [#NonFungibleToken-_tokenURIs] + + Mapping from token IDs to their metadata URIs. + + +### Witnesses [toc] [#NonFungibleToken-Witnesses] +### Witnesses [!toc] [#NonFungibleToken-Witnesses] + +#### wit_NonFungibleTokenSK [toc] [#NonFungibleToken-wit_NonFungibleTokenSK] +#### [!toc] [#NonFungibleToken-wit_NonFungibleTokenSK] + + 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. + + +### Circuits [toc] [#NonFungibleToken-Circuits] +### Circuits [!toc] [#NonFungibleToken-Circuits] + +#### initialize [toc] [#NonFungibleToken-initialize] +#### [!toc] [#NonFungibleToken-initialize] + + 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. + + +#### balanceOf [toc] [#NonFungibleToken-balanceOf] +#### [!toc] [#NonFungibleToken-balanceOf] + + Returns the number of tokens in `owner`'s account. + + Requirements: + + * The contract is initialized. + + +#### ownerOf [toc] [#NonFungibleToken-ownerOf] +#### [!toc] [#NonFungibleToken-ownerOf] + + Returns the owner of the `tokenId` token. + + Requirements: + + * The contract is initialized. + * The `tokenId` must exist. + + +#### name [toc] [#NonFungibleToken-name] +#### [!toc] [#NonFungibleToken-name] + + Returns the token name. + + Requirements: + + * The contract is initialized. + + +#### symbol [toc] [#NonFungibleToken-symbol] +#### [!toc] [#NonFungibleToken-symbol] + + Returns the symbol of the token. + + Requirements: + + * The contract is initialized. + + +#### tokenURI [toc] [#NonFungibleToken-tokenURI] +#### [!toc] [#NonFungibleToken-tokenURI] + + 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. + + + 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] +#### [!toc] [#NonFungibleToken-_setTokenURI] + + 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. + + + +#### approve [toc] [#NonFungibleToken-approve] +#### [!toc] [#NonFungibleToken-approve] + + Gives permission to `to` to transfer `tokenId` token to another account. + The approval is cleared when the token is transferred. + + 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](#NonFungibleToken-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. + + +#### getApproved [toc] [#NonFungibleToken-getApproved] +#### [!toc] [#NonFungibleToken-getApproved] + + Returns the account approved for `tokenId` token. + + Requirements: + + * The contract is initialized. + * `tokenId` must exist. + + +#### setApprovalForAll [toc] [#NonFungibleToken-setApprovalForAll] +#### [!toc] [#NonFungibleToken-setApprovalForAll] + + Approve or remove `operator` as an operator for 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](#NonFungibleToken-wit_NonFungibleTokenSK) + witness as `persistentHash(secretKey)`. + + Requirements: + + * The contract is initialized. + * The `operator` cannot be zero. + + +#### isApprovedForAll [toc] [#NonFungibleToken-isApprovedForAll] +#### [!toc] [#NonFungibleToken-isApprovedForAll] + + Returns if the `operator` is allowed to manage all of the assets of `owner`. + + Requirements: + + * The contract is initialized. + + +#### transferFrom [toc] [#NonFungibleToken-transferFrom] +#### [!toc] [#NonFungibleToken-transferFrom] + + 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. + + 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: + + * 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] +#### [!toc] [#NonFungibleToken-_unsafeTransferFrom] + + 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](#NonFungibleToken-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. + 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` 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] +#### [!toc] [#NonFungibleToken-_ownerOf] + + Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist. + + Requirements: + + * The contract is initialized. + + +#### _getApproved [toc] [#NonFungibleToken-_getApproved] +#### [!toc] [#NonFungibleToken-_getApproved] + + Returns the approved address for `tokenId`. Returns the zero value if `tokenId` is not minted. + + Requirements: + + * The contract is initialized. + + +#### _isAuthorized [toc] [#NonFungibleToken-_isAuthorized] +#### [!toc] [#NonFungibleToken-_isAuthorized] + + 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. + + + This function assumes that `owner` is the actual owner of `tokenId` and does not verify this + assumption. + + + +#### _checkAuthorized [toc] [#NonFungibleToken-_checkAuthorized] +#### [!toc] [#NonFungibleToken-_checkAuthorized] + + Checks if `spender` can operate on `tokenId`, assuming the provided `owner` is the actual owner. + + Requirements: + + * 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. + + + +#### _mint [toc] [#NonFungibleToken-_mint] +#### [!toc] [#NonFungibleToken-_mint] + + Mints `tokenId` and transfers it to `to`. + + Requirements: + + * The contract is initialized. + * `tokenId` must not exist. + * `to` is not zero. + * `to` is not a ContractAddress. + + +#### _unsafeMint [toc] [#NonFungibleToken-_unsafeMint] +#### [!toc] [#NonFungibleToken-_unsafeMint] + + 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. + Once contract-to-contract calls are supported, this circuit may be deprecated. + + + +#### _burn [toc] [#NonFungibleToken-_burn] +#### [!toc] [#NonFungibleToken-_burn] + + Destroys `tokenId`. + 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. + + +#### _transfer [toc] [#NonFungibleToken-_transfer] +#### [!toc] [#NonFungibleToken-_transfer] + + Transfers `tokenId` from `fromAddress` to `to`. + 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. + + + Requirements: + + * The contract is initialized. + * `to` is not zero. + * `to` is not a ContractAddress. + * `tokenId` token must be owned by `fromAddress`. + + +#### _unsafeTransfer [toc] [#NonFungibleToken-_unsafeTransfer] +#### [!toc] [#NonFungibleToken-_unsafeTransfer] + + 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. + Once contract-to-contract calls are supported, this circuit may be deprecated. + + + +#### _approve [toc] [#NonFungibleToken-_approve] +#### [!toc] [#NonFungibleToken-_approve] + + Approve `to` to operate on `tokenId`. + + 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, + 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. + + + Passing false skips the check that `tokenId` was minted. Both + [_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. + + + 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 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). + + +#### _setApprovalForAll [toc] [#NonFungibleToken-_setApprovalForAll] +#### [!toc] [#NonFungibleToken-_setApprovalForAll] + + Approve `operator` to operate on all of `owner` tokens. + + Requirements: + + * The contract is initialized. + * `owner` is not zero. + * `operator` is not zero. + + +#### _requireOwned [toc] [#NonFungibleToken-_requireOwned] +#### [!toc] [#NonFungibleToken-_requireOwned] + + Reverts if the `tokenId` doesn't have a current owner (it hasn't been minted, or it has been burned). + Returns the owner. + + Requirements: + + * The contract is initialized. + * `tokenId` must exist. + + diff --git a/content/contracts-compact/0.4.x/api/ownable.mdx b/content/contracts-compact/0.4.x/api/ownable.mdx new file mode 100644 index 00000000..36c10ac6 --- /dev/null +++ b/content/contracts-compact/0.4.x/api/ownable.mdx @@ -0,0 +1,512 @@ +--- +title: Ownable API +--- + +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 +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; +``` + +--- + +### Ledger [toc] [#Ownable-Ledger] +### Ledger [!toc] [#Ownable-Ledger] + +#### _isInitialized [toc] [#Ownable-_isInitialized] +#### [!toc] [#Ownable-_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. + + +#### _owner [toc] [#Ownable-_owner] +#### [!toc] [#Ownable-_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. + + The same key produces the same account identifier across all contracts. Users who + desire cross-contract unlinkability should use different keys per contract. + + +### Circuits [toc] [#Ownable-Circuits] +### Circuits [!toc] [#Ownable-Circuits] + +#### initialize [toc] [#Ownable-initialize] +#### [!toc] [#Ownable-initialize] + + Initializes the contract by setting the `initialOwner`. + This must be called in the contract's constructor. + + Requirements: + + * Contract is not already initialized. + * `initialOwner` is not a ContractAddress. + * `initialOwner` is not the zero address. + + +#### owner [toc] [#Ownable-owner] +#### [!toc] [#Ownable-owner] + + Returns the current contract owner. + + Requirements: + + * Contract is initialized. + + +#### transferOwnership [toc] [#Ownable-transferOwnership] +#### [!toc] [#Ownable-transferOwnership] + + 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. + + + Requirements: + + * 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] +#### [!toc] [#Ownable-_unsafeTransferOwnership] + + Unsafe variant of [transferOwnership](#Ownable-transferOwnership). + + + 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. + + + Requirements: + + * Contract is initialized. + * The caller is the current contract owner. + * `newOwner` is not the zero address. + + +#### renounceOwnership [toc] [#Ownable-renounceOwnership] +#### [!toc] [#Ownable-renounceOwnership] + + Leaves the contract without an owner. + It will not be possible to call [assertOnlyOwner](#Ownable-assertOnlyOwner) circuits anymore. + Can only be called by the current owner. + + Requirements: + + * Contract is initialized. + * The caller is the current contract owner. + + +#### assertOnlyOwner [toc] [#Ownable-assertOnlyOwner] +#### [!toc] [#Ownable-assertOnlyOwner] + + Throws if called by any account other than the owner. + 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](#Ownable-wit_OwnableSK) witness as `persistentHash(secretKey)`. + + Requirements: + + * Contract is initialized. + * The caller is the current contract owner. + + +#### _transferOwnership [toc] [#Ownable-_transferOwnership] +#### [!toc] [#Ownable-_transferOwnership] + + 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. + + + Requirements: + + * Contract is initialized. + * `newOwner` is not a ContractAddress. + + +#### _unsafeUncheckedTransferOwnership [toc] [#Ownable-_unsafeUncheckedTransferOwnership] +#### [!toc] [#Ownable-_unsafeUncheckedTransferOwnership] + + Unsafe variant of [_transferOwnership](#Ownable-_transferOwnership). + + + 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. + + + Requirements: + + * Contract is initialized. + + +--- + +## ZOwnablePK [toc] [#ZOwnablePK] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK"; +``` + +--- + +### Ledger [toc] [#ZOwnablePK-Ledger] +### Ledger [!toc] [#ZOwnablePK-Ledger] + +#### _isInitialized [toc] [#ZOwnablePK-_isInitialized] +#### [!toc] [#ZOwnablePK-_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. + + +#### _ownerCommitment [toc] [#ZOwnablePK-_ownerCommitment] +#### [!toc] [#ZOwnablePK-_ownerCommitment] + + 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] [#ZOwnablePK-_counter] +#### [!toc] [#ZOwnablePK-_counter] + + 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] [#ZOwnablePK-_instanceSalt] +#### [!toc] [#ZOwnablePK-_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 owner identifiers or domain parameters. It is immutable after initialization. + + +### 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. + + +### Circuits [toc] [#ZOwnablePK-Circuits] +### Circuits [!toc] [#ZOwnablePK-Circuits] + +#### initialize [toc] [#ZOwnablePK-initialize] +#### [!toc] [#ZOwnablePK-initialize] + + 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. + + + 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. + + + 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)`. + + Requirements: + + * Contract is initialized. + + +#### transferOwnership [toc] [#ZOwnablePK-transferOwnership] +#### [!toc] [#ZOwnablePK-transferOwnership] + + Transfers ownership to `newOwnerId`. + `newOwnerId` must be precalculated and given to the current owner off chain. + + Requirements: + + * Contract is initialized. + * Caller is the current owner. + * `newOwnerId` is not an empty array. + + +#### renounceOwnership [toc] [#ZOwnablePK-renounceOwnership] +#### [!toc] [#ZOwnablePK-renounceOwnership] + + Leaves the contract without an owner. + It will not be possible to call [assertOnlyOwner](#ZOwnablePK-assertOnlyOwner) circuits anymore. + Can only be called by the current owner. + + Requirements: + + * Contract is initialized. + * Caller is the current owner. + + +#### assertOnlyOwner [toc] [#ZOwnablePK-assertOnlyOwner] +#### [!toc] [#ZOwnablePK-assertOnlyOwner] + + 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. + + Requirements: + + * 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. + + +#### _computeOwnerCommitment [toc] [#ZOwnablePK-_computeOwnerCommitment] +#### [!toc] [#ZOwnablePK-_computeOwnerCommitment] + + Computes the owner commitment from the given `id` and `counter`. + + ## 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)` + + * `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. + + +#### _computeOwnerId [toc] [#ZOwnablePK-_computeOwnerId] +#### [!toc] [#ZOwnablePK-_computeOwnerId] + + Computes the unique identifier (`id`) of the owner from their + public key and a secret nonce. + + ## ID Derivation + `id = SHA256(pk, nonce)` + + * `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. + + +#### _transferOwnership [toc] [#ZOwnablePK-_transferOwnership] +#### [!toc] [#ZOwnablePK-_transferOwnership] + + Transfers ownership to owner id `newOwnerId` without + enforcing permission checks on the caller. + + Requirements: + + * Contract is initialized. + diff --git a/content/contracts-compact/0.4.x/api/security.mdx b/content/contracts-compact/0.4.x/api/security.mdx new file mode 100644 index 00000000..d4f77419 --- /dev/null +++ b/content/contracts-compact/0.4.x/api/security.mdx @@ -0,0 +1,386 @@ +--- +title: Security +--- + +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] +### Ledger [!toc] [#Initializable-Ledger] + +#### _isInitialized [toc] [#Initializable-_isInitialized] +#### [!toc] [#Initializable-_isInitialized] + + Boolean indicating if initialized. + + +### Witnesses [toc] [#Initializable-Witnesses] +### Witnesses [!toc] [#Initializable-Witnesses] + +None. + +### Circuits [toc] [#Initializable-Circuits] +### Circuits [!toc] [#Initializable-Circuits] + +#### initialize [toc] [#Initializable-initialize] +#### [!toc] [#Initializable-initialize] + + Initializes the state thus ensuring the calling circuit can only be called once. + + Requirements: + + * Contract must not be initialized. + + +#### assertInitialized [toc] [#Initializable-assertInitialized] +#### [!toc] [#Initializable-assertInitialized] + + Asserts that the contract has been initialized, throwing an error if not. + + Requirements: + + * Contract must be initialized. + + +#### assertNotInitialized [toc] [#Initializable-assertNotInitialized] +#### [!toc] [#Initializable-assertNotInitialized] + + Asserts that the contract has not been initialized, throwing an error if it has. + + Requirements: + + * Contract must not be initialized. + + +--- + +## Pausable [toc] [#Pausable] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/security/Pausable"; +``` + +--- + +### Ledger [toc] [#Pausable-Ledger] +### Ledger [!toc] [#Pausable-Ledger] + +#### _isPaused [toc] [#Pausable-_isPaused] +#### [!toc] [#Pausable-_isPaused] + + Boolean indicating if paused. + + +### Witnesses [toc] [#Pausable-Witnesses] +### Witnesses [!toc] [#Pausable-Witnesses] + +None. + +### Circuits [toc] [#Pausable-Circuits] +### Circuits [!toc] [#Pausable-Circuits] + +#### isPaused [toc] [#Pausable-isPaused] +#### [!toc] [#Pausable-isPaused] + + Returns true if the contract is paused, and false otherwise. + + +#### assertPaused [toc] [#Pausable-assertPaused] +#### [!toc] [#Pausable-assertPaused] + + Makes a circuit only callable when the contract is paused. + + Requirements: + + * Contract must be paused. + + +#### assertNotPaused [toc] [#Pausable-assertNotPaused] +#### [!toc] [#Pausable-assertNotPaused] + + Makes a circuit only callable when the contract is not paused. + + Requirements: + + * Contract must not be paused. + + +#### _pause [toc] [#Pausable-_pause] +#### [!toc] [#Pausable-_pause] + + Triggers a stopped state. + + Requirements: + + * Contract must not be paused. + + +#### _unpause [toc] [#Pausable-_unpause] +#### [!toc] [#Pausable-_unpause] + + Lifts the pause on the contract. + + Requirements: + + * 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. Absent means 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. + + +#### assertAllowed [toc] [#Allowlist-assertAllowed] +#### [!toc] [#Allowlist-assertAllowed] + + Asserts that `account` is allowed. The composing contract + decides when this gate applies. + + Requirements: + + * `account` is a member of the allowlist. + + +#### _allow [toc] [#Allowlist-_allow] +#### [!toc] [#Allowlist-_allow] + + Adds `account` to the allowlist. + + +#### _disallow [toc] [#Allowlist-_disallow] +#### [!toc] [#Allowlist-_disallow] + + Removes `account` from the allowlist. + + +--- + +## 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. Absent means 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. + + +#### assertNotBlocked [toc] [#Blocklist-assertNotBlocked] +#### [!toc] [#Blocklist-assertNotBlocked] + + Asserts that `account` is not blocked. The composing contract + decides when this gate applies. + + Requirements: + + * `account` is not a member of the blocklist. + + +#### _block [toc] [#Blocklist-_block] +#### [!toc] [#Blocklist-_block] + + Adds `account` to the blocklist. + + +#### _unblock [toc] [#Blocklist-_unblock] +#### [!toc] [#Blocklist-_unblock] + + Removes `account` from the blocklist. + diff --git a/content/contracts-compact/0.4.x/api/shielded-access-control.mdx b/content/contracts-compact/0.4.x/api/shielded-access-control.mdx new file mode 100644 index 00000000..b0e7420e --- /dev/null +++ b/content/contracts-compact/0.4.x/api/shielded-access-control.mdx @@ -0,0 +1,613 @@ +--- +title: ShieldedAccessControl API +--- + +This page provides the full ShieldedAccessControl module API. + +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>`: + +``` +roleCommitment := SHA256( role || accountId || instanceSalt || commitmentDomain ) + +accountId := SHA256( secretKey || instanceSalt || accountIdDomain ) + +roleNullifier := SHA256( roleCommitment || nullifierDomain ) + +commitmentDomain := pad(32, "ShieldedAccessControl:commitment") +accountIdDomain := pad(32, "ShieldedAccessControl:accountId") +nullifierDomain := pad(32, "ShieldedAccessControl:nullifier") +``` + +In this RBAC model, role commitments behave like private bearer tokens. +Possession of a valid, non-revoked role commitment grants authorization. +Revocation permanently burns the role instance, requiring explicit new issuance under a new account identifier. +Users must rotate their secret key to generate a new `accountId` to be re-authorized. + +A single `secretKey` may be used for all roles within a contract instance. +This is safe because the `role` identifier is mixed into the commitment (not the `accountId`), +so different roles produce different commitments and different nullifiers even when the underlying `accountId` is the same. + +Roles are referred to by their `Bytes<32>` identifier. +These should be exposed in the top-level contract and be unique. +The best way to achieve this is by using `export sealed ledger` hash digests that are initialized in the top-level contract: + +```typescript +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl" + prefix ShieldedAccessControl_; + +export sealed ledger MY_ROLE: Bytes<32>; + +constructor(instanceSalt: Bytes<32>, defaultAdmin: ShieldedAccessControl_AccountIdentifier) { + MY_ROLE = persistentHash>(pad(32, "MY_ROLE")); + ShieldedAccessControl_initialize(instanceSalt); + ShieldedAccessControl__grantRole(ShieldedAccessControl_DEFAULT_ADMIN_ROLE(), defaultAdmin); +} +``` + +To restrict access to a circuit, use [assertOnlyRole](#ShieldedAccessControl-assertOnlyRole): + +```typescript +circuit foo(): [] { + ShieldedAccessControl_assertOnlyRole(MY_ROLE as ShieldedAccessControl_RoleIdentifier); +} +``` + +Roles can be granted and revoked dynamically via the [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole) circuits. +Each role has an associated admin role, +and only accounts that have a role's admin role can call [grantRole](#ShieldedAccessControl-grantRole) and [revokeRole](#ShieldedAccessControl-revokeRole). + +By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, +which means that only accounts with this role will be able to grant or revoke other roles. +More complex role relationships can be created by using [_setRoleAdmin](#ShieldedAccessControl-_setRoleAdmin). + + + The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to grant and revoke this role. + Extra precautions should be taken to secure accounts that have been granted it. + + + + For an overview of the module, read the [ShieldedAccessControl guide](../shielded-access-control). + + +## ShieldedAccessControl [toc] [#ShieldedAccessControl] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl"; +``` + +--- + +### Types [toc] [#ShieldedAccessControl-Types] +### Types [!toc] [#ShieldedAccessControl-Types] + +#### UpdateType [toc] [#ShieldedAccessControl-UpdateType] +#### [!toc] [#ShieldedAccessControl-UpdateType] + + Enum indicating whether a role update is a grant or a revocation. + + +#### RoleCommitment [toc] [#ShieldedAccessControl-RoleCommitment] +#### [!toc] [#ShieldedAccessControl-RoleCommitment] + + A Merkle tree leaf committing a `(role, accountId)` pairing. Computed as `SHA256(role || accountId || instanceSalt || commitmentDomain)`. + + +#### RoleIdentifier [toc] [#ShieldedAccessControl-RoleIdentifier] +#### [!toc] [#ShieldedAccessControl-RoleIdentifier] + + A unique identifier for a role. + + +#### AccountIdentifier [toc] [#ShieldedAccessControl-AccountIdentifier] +#### [!toc] [#ShieldedAccessControl-AccountIdentifier] + + A privacy-preserving identity commitment. Computed as `SHA256(secretKey || instanceSalt || accountIdDomain)`. + + +#### RoleNullifier [toc] [#ShieldedAccessControl-RoleNullifier] +#### [!toc] [#ShieldedAccessControl-RoleNullifier] + + 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] + + 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] +#### [!toc] [#ShieldedAccessControl-_operatorRoles] + + 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] +#### [!toc] [#ShieldedAccessControl-_adminRoles] + + Mapping from a role identifier to an admin role identifier. + + +#### _roleCommitmentNullifiers [toc] [#ShieldedAccessControl-_roleCommitmentNullifiers] +#### [!toc] [#ShieldedAccessControl-_roleCommitmentNullifiers] + + A set of nullifiers used to prove a role has been revoked + + +#### _grantedRoleNullifiers [toc] [#ShieldedAccessControl-_grantedRoleNullifiers] +#### [!toc] [#ShieldedAccessControl-_grantedRoleNullifiers] + + 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] +#### [!toc] [#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. + 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] +### 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. + + +#### wit_secretKey [toc] [#ShieldedAccessControl-wit_secretKey] +#### [!toc] [#ShieldedAccessControl-wit_secretKey] + + Returns the user's secret key used in deriving the shielded account identifier. + + 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`. + + +### Circuits [toc] [#ShieldedAccessControl-Circuits] +### Circuits [!toc] [#ShieldedAccessControl-Circuits] + +#### initialize [toc] [#ShieldedAccessControl-initialize] +#### [!toc] [#ShieldedAccessControl-initialize] + + Initializes the contract by storing the `instanceSalt` that acts as a privacy additive + for preventing duplicate commitments among other contracts implementing ShieldedAccessControl. + + + 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 + + + Requirements: + + * Contract is not initialized. + + +#### DEFAULT_ADMIN_ROLE [toc] [#ShieldedAccessControl-DEFAULT_ADMIN_ROLE] +#### [!toc] [#ShieldedAccessControl-DEFAULT_ADMIN_ROLE] + + 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] +#### [!toc] [#ShieldedAccessControl-assertOnlyRole] + + Reverts if caller cannot provide a valid proof of ownership for `role`. + + Requirements: + + * 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] +#### [!toc] [#ShieldedAccessControl-canProveRole] + + 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. + + Requirements: + + * 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] +#### [!toc] [#ShieldedAccessControl-grantRole] + + Grants `role` to `accountId` by inserting a role commitment unique to the + `(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`. + + 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. + + 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] +#### [!toc] [#ShieldedAccessControl-revokeRole] + + Permanently revokes `role` from `accountId` by inserting a role nullifier into the + `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for + `role`. + + 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. + + 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] +#### [!toc] [#ShieldedAccessControl-renounceRole] + + Revokes `role` from the calling account by inserting a role nullifier into the + `_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 + 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 + guarantees if renounceRole is used in tandem with other on-chain actions. + + + 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. + + Disclosures: + + * A nullifier for the respective role commitment. + + +#### getRoleAdmin [toc] [#ShieldedAccessControl-getRoleAdmin] +#### [!toc] [#ShieldedAccessControl-getRoleAdmin] + + 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](#ShieldedAccessControl-_setRoleAdmin). + + Disclosures: + + * A role identifier. + + +#### _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. 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. + + + 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. Deployers should monitor slot consumption off-chain. + + + Requirements: + + * 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] +#### [!toc] [#ShieldedAccessControl-_revokeRole] + + Permanently revokes `role` from `accountId` by inserting a role nullifier into the + `_roleCommitmentNullifiers` set. Once revoked, a new `accountId` must be generated to be re-authorized for + `role`. + + Internal circuit without access restriction. + + + Exposing this circuit directly in an implementing contract would allow anyone to revoke + roles without authorization. It must be wrapped with appropriate access control. + + + Requirements: + + * 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] +#### [!toc] [#ShieldedAccessControl-_setRoleAdmin] + + 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. + + + 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. + + Disclosures: + + * The role identifier + * The admin identifier + + + +#### computeRoleCommitment [toc] [#ShieldedAccessControl-computeRoleCommitment] +#### [!toc] [#ShieldedAccessControl-computeRoleCommitment] + + Computes the role commitment from the given `accountId` and `role`. + + + 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. + + + +#### computeNullifier [toc] [#ShieldedAccessControl-computeNullifier] +#### [!toc] [#ShieldedAccessControl-computeNullifier] + + Computes the role nullifier for a given `roleCommitment`. + + ## Role Nullifier Derivation + `roleNullifier = SHA256(roleCommitment, nullifierDomain)` + + * `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. + + +#### computeAccountId [toc] [#ShieldedAccessControl-computeAccountId] +#### [!toc] [#ShieldedAccessControl-computeAccountId] + + 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 `_computeAccountId` and produces an identical result + given the same inputs. + + + 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/0.4.x/api/utils.mdx b/content/contracts-compact/0.4.x/api/utils.mdx new file mode 100644 index 00000000..de75b136 --- /dev/null +++ b/content/contracts-compact/0.4.x/api/utils.mdx @@ -0,0 +1,199 @@ +--- +title: Utils +--- + +This package provides the API for all Utils modules. + + + For an overview of the module, read the [Utils guide](../utils). + + +## Utils [toc] [#Utils] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils"; +``` + +--- + +### Ledger [toc] [#Utils-Ledger] +### Ledger [!toc] [#Utils-Ledger] + +None. + +### Witnesses [toc] [#Utils-Witnesses] +### Witnesses [!toc] [#Utils-Witnesses] + +None. + +### Circuits [toc] [#Utils-Circuits] +### Circuits [!toc] [#Utils-Circuits] + +#### isKeyOrAddressZero [toc] [#Utils-isKeyOrAddressZero] +#### [!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. + + + +#### isKeyZero [toc] [#Utils-isKeyZero] +#### [!toc] [#Utils-isKeyZero] + + Returns whether `key` is the zero address. + + +#### isKeyOrAddressEqual [toc] [#Utils-isKeyOrAddressEqual] +#### [!toc] [#Utils-isKeyOrAddressEqual] + + Returns whether `keyOrAddress` is equal to `other`. Assumes that a ZswapCoinPublicKey + and a ContractAddress can never be equal + + +#### isContractAddress [toc] [#Utils-isContractAddress] +#### [!toc] [#Utils-isContractAddress] + + Returns whether `keyOrAddress` is a ContractAddress type. + + +#### emptyString [toc] [#Utils-emptyString] +#### [!toc] [#Utils-emptyString] + + A helper function that returns the empty string: "". + + +#### canonicalize [toc] [#Utils-canonicalize] +#### [!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/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](#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](#Utils-isTargetZero), but the left variant keeps guards and error messages consistent. + + +#### isTargetZero [toc] [#Utils-isTargetZero] +#### [!toc] [#Utils-isTargetZero] + + Returns `true` if `target`'s active branch (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. + + + 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)` + + + 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. + + 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] +#### [!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>`. + + + TODO: Remove once a math module providing this constant is available. + + diff --git a/content/contracts-compact/0.4.x/confidential-fungible-token.mdx b/content/contracts-compact/0.4.x/confidential-fungible-token.mdx new file mode 100644 index 00000000..0c2b324e --- /dev/null +++ b/content/contracts-compact/0.4.x/confidential-fungible-token.mdx @@ -0,0 +1,407 @@ +--- +title: ConfidentialFungibleToken +--- + +{/* links */} +[FungibleToken]: ./fungible-token.mdx +[Module/Contract Pattern]: ./extensibility#the-modulecontract-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/0.4.x/crypto.mdx b/content/contracts-compact/0.4.x/crypto.mdx new file mode 100644 index 00000000..602e1867 --- /dev/null +++ b/content/contracts-compact/0.4.x/crypto.mdx @@ -0,0 +1,264 @@ +--- +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. +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. +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/0.4.x/extensibility.mdx b/content/contracts-compact/0.4.x/extensibility.mdx new file mode 100644 index 00000000..f030f54e --- /dev/null +++ b/content/contracts-compact/0.4.x/extensibility.mdx @@ -0,0 +1,175 @@ +--- +title: Extensibility +--- + +## The Module/Contract Pattern + +We use the term **modular composition by delegation** to describe the practice of having contracts call into module-defined circuits to implement behavior. +Rather than inheriting or overriding functionality, +a contract delegates responsibility to the module by explicitly invoking its exported circuits. + +The idea is that there are two types of compact files: modules and contracts. +To minimize risk, boilerplate, and avoid naming clashes, we follow these rules: + +### Modules + +Modules expose functionality through three circuit types: + +1. `internal`: private helpers → used to break up logic within the module. +2. `public`: composable building blocks → intended for contracts to use in complex flows (`_mint`, `_burn`). +3. `external`: standalone circuits → safe to expose as-is (`transfer`, `approve`). + +Modules must: + +* Export only `public` and `external` circuits. +* Prefix `public` circuits with `_` (e.g., `FungibleToken._mint`). +* Avoid `_` prefix for `external` circuits (e.g., `FungibleToken.transfer`). +* Avoid defining or calling constructors or `initialize()` directly. +* Optionally define an `initialize()` circuit for internal setup—but execution must be delegated to the contract. + + +Compact files must contain only one top-level module and all logic must be defined **inside** the module declaration. + + +### Contracts + +Contracts compose behavior by explicitly invoking the relevant circuits from imported modules. +Therefore, contracts: + +* Can import from modules. +* Should add prefix to imports (`import "FungibleToken" prefix FungibleToken_;`). +* Should re-expose external module circuits through wrapper circuits to control naming and layering. +Avoid raw re-exports to prevent name clashes. +* Should implement constructor that calls `initialize` from imported modules. +* Must not call initializers outside of the constructor. + +This pattern balances modularity with local control, avoids tight coupling, and works within Compact’s language constraints. +As Compact matures, this pattern will likely evolve as well. + +### Example contract implementing modules + +```ts +// FungibleTokenMintablePausableOwnableContract + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; +import "./node_modules/@openzeppelin/compact-contracts/security/Pausable" + prefix Pausable_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + _name: Opaque<"string">, + _symbol: Opaque<"string">, + _decimals: Uint<8>, + _owner: Either, ContractAddress> +) { + FungibleToken_initialize(_name, _symbol, _decimals); + Ownable_initialize(_owner); +} + +/** IFungibleTokenMetadata */ + +export circuit name(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return FungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return FungibleToken_decimals(); +} + +/** IFungibleToken */ + +export circuit totalSupply(): Uint<128> { + return FungibleToken_totalSupply(); +} + +export circuit balanceOf( + account: Either, ContractAddress> +): Uint<128> { + return FungibleToken_balanceOf(account); +} + +export circuit allowance( + owner: Either, ContractAddress>, + spender: Either, ContractAddress> +): Uint<128> { + return FungibleToken_allowance(owner, spender); +} + +export circuit transfer( + to: Either, ContractAddress>, + value: Uint<128> +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_transfer(to, value); +} + +export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128> +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_transferFrom(fromAddress, to, value); +} + +export circuit approve( + spender: Either, ContractAddress>, + value: Uint<128> +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_approve(spender, value); +} + +/** IMintable */ + +export circuit mint( + account: Either, ContractAddress>, + value: Uint<128> +): [] { + Pausable_assertNotPaused(); + Ownable_assertOnlyOwner(); + FungibleToken__mint(account, value); +} + +/** IPausable */ + +export circuit isPaused(): Boolean { + return Pausable_isPaused(); +} + +export circuit pause(): [] { + Ownable_assertOnlyOwner(); + Pausable__pause(); +} + +export circuit unpause(): [] { + Ownable_assertOnlyOwner(); + Pausable__unpause(); +} + +/** IOwnable */ + +export circuit owner(): Either, ContractAddress> { + return Ownable_owner(); +} + +export circuit transferOwnership( + newOwner: Either, ContractAddress> +): [] { + Ownable_transferOwnership(newOwner); +} + +export circuit renounceOwnership(): [] { + Ownable_renounceOwnership(); +} + +``` diff --git a/content/contracts-compact/fungible-token.mdx b/content/contracts-compact/0.4.x/fungible-token.mdx similarity index 100% rename from content/contracts-compact/fungible-token.mdx rename to content/contracts-compact/0.4.x/fungible-token.mdx diff --git a/content/contracts-compact/0.4.x/index.mdx b/content/contracts-compact/0.4.x/index.mdx new file mode 100644 index 00000000..e7a2d15b --- /dev/null +++ b/content/contracts-compact/0.4.x/index.mdx @@ -0,0 +1,180 @@ +--- +title: Contracts for Compact +--- + +{/* links */} +[Midnight]: https://midnight.network/ +[nvm]: https://github.com/nvm-sh/nvm +[yarn]: https://yarnpkg.com/getting-started/install +[compact-dev-tools]: https://docs.midnight.network/blog/compact-developer-tools + +**A library for secure smart contract development** written in Compact for [Midnight]. +This library consists of modules to build custom smart contracts. + + +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.4.0/README.md#security) in the repository. + + +## Usage + +Make sure you have [nvm] and [yarn] installed on your machine. + +Follow Midnight's [Compact Developer Tools][compact-dev-tools] installation guide and confirm that `compact` is in the `PATH` env variable. + +```bash +$ compact compile --version + +Compactc version: {{compact_compiler_version}} +{{compact_compiler_version}} +``` + +### Installation + +Create a directory for your project. + +```bash +mkdir my-project +cd my-project +``` + +Install the library. + +```bash +yarn add @openzeppelin/compact-contracts +``` + +### Write a custom contract using library modules + +In the root of `my-project`, create a custom contract using OpenZeppelin Compact modules. +Import the modules through `./node_modules/@openzeppelin/compact-contracts/...`. + +```typescript +// MyContract.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; +import "./node_modules/@openzeppelin/compact-contracts/security/Pausable" + prefix Pausable_; +import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" + prefix FungibleToken_; + +constructor( + _name: Opaque<"string">, + _symbol: Opaque<"string">, + _decimals: Uint<8>, + _recipient: Either, ContractAddress>, + _amount: Uint<128>, + _initOwner: Either, ContractAddress>, +) { + Ownable_initialize(_initOwner); + FungibleToken_initialize(_name, _symbol, _decimals); + FungibleToken__mint(_recipient, _amount); +} + +/** IFungibleToken */ + +export circuit name(): Opaque<"string"> { + return FungibleToken_name(); +} + +export circuit symbol(): Opaque<"string"> { + return FungibleToken_symbol(); +} + +export circuit decimals(): Uint<8> { + return FungibleToken_decimals(); +} + +export circuit totalSupply(): Uint<128> { + return FungibleToken_totalSupply(); +} + +export circuit balanceOf(account: Either, ContractAddress>): Uint<128> { + return FungibleToken_balanceOf(account); +} + +export circuit allowance( + owner: Either, ContractAddress>, + spender: Either, ContractAddress> +): Uint<128> { + return FungibleToken_allowance(owner, spender); +} + +export circuit transfer( + to: Either, ContractAddress>, + value: Uint<128>, +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_transfer(to, value); +} + +export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + value: Uint<128> +): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_transferFrom(fromAddress, to, value); +} + +export circuit approve(spender: Either, ContractAddress>, value: Uint<128>): Boolean { + Pausable_assertNotPaused(); + return FungibleToken_approve(spender, value); +} + +/** IOwnable */ + +export circuit owner(): Either, ContractAddress> { + return Ownable_owner(); +} + +export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { + return Ownable_transferOwnership(newOwner); +} + +export circuit renounceOwnership(): [] { + return Ownable_renounceOwnership(); +} + +/** IPausable */ + +export circuit pause(): [] { + Ownable_assertOnlyOwner(); + Pausable__pause(); +} + +export circuit unpause(): [] { + Ownable_assertOnlyOwner(); + Pausable__unpause(); +} +``` + +### Compile the contract + +In the project root, compile the contract. + +```bash +% compact compile MyContract.compact artifacts/MyContract +Compiling 14 circuits: + circuit "allowance" (k=11, rows=1352) + 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=4977) + circuit "transferOwnership" (k=13, rows=2959) + circuit "unpause" (k=13, rows=2362) +Overall progress [====================] 14/14 +``` diff --git a/content/contracts-compact/0.4.x/multi-token.mdx b/content/contracts-compact/0.4.x/multi-token.mdx new file mode 100644 index 00000000..4a567e74 --- /dev/null +++ b/content/contracts-compact/0.4.x/multi-token.mdx @@ -0,0 +1,213 @@ +--- +title: MultiToken +--- + +{/* links */} +[EIP-1155]: https://eips.ethereum.org/EIPS/eip-1155 +[ERC165]: https://eips.ethereum.org/EIPS/eip-165 +[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 +[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. + +## ERC1155 Compatibility + +Even though Midnight is not EVM-compatible, this implementation attempts to be an approximation of the standard. +Some features and behaviors are either not possible, not possible yet, +or changed because of the vastly different tech stack and Compact language constraints. + +**Notable changes** + +* **`Uint<128>` as value and id type** - Since 256-bit unsigned integers are not supported, +the library uses the Compact type `Uint<128>`. + +**Features and specifications NOT supported** + +* **Events** - Midnight does not currently support events, but this is planned on being supported in the future. +* **Uint256 type** - There's ongoing research on ways to support uint256 in the future. +* **Interface** - Compact currently does not have a way to define a contract interface. +This library offers modules of contracts with free floating circuits; +nevertheless, there's no means of enforcing that all circuits are provided. +* **Batch mint, burn, transfer** - Without support for dynamic arrays, +batching transfers is difficult to do without a hacky solution. +For instance, we could change the `to` and `fromAddress` parameters to be vectors. +This would change the signature and would be both difficult to use and easy to misuse. +* **Querying batched balances** - This can be somewhat supported. +The issue, without dynamic arrays, is that the module circuit must use `Vector` for accounts and ids; +therefore, the implementing contract must explicitly define the number of balances to query in the circuit i.e. + +```ts +balanceOfBatch_10( + accounts: Vector<10, Either, ContractAddress>>, + ids: Vector<10, Uint<128>> +): Vector<10, Uint<128>> +``` + +Since this module does not offer mint or transfer batching, balance batching is also not included at this time. + +* **Introspection** - Compact currently cannot support contract-to-contract queries for introspection. +[ERC165] (or an equivalent thereof) is NOT included in the contract. +* **Safe transfers** - The lack of an introspection mechanism means safe transfers of any kind can not be supported. + +## Contract-to-contract calls + +Contract-to-contract calls are currently not supported in the Compact language. +Due to this limitation, the current iteration of MultiToken disallows transfers and mints to the `ContractAddress` type. +Transferring tokens to a contract may result in those tokens being locked forever. +The MultiToken module, however, does provide `unsafe` circuit variants for users who wish to experiment with sending tokens to contracts. + + +The `unsafe` circuits will eventually be deprecated after Compact supports contract-to-contract calls—meaning +`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_; + +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. + + + 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. +It's recommended to prefix the module with `MultiToken_` to avoid circuit signature clashes. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken" + prefix MultiToken_; + +constructor( + uri: Opaque<"string">, +) { + MultiToken_initialize(uri); +} + +``` + +Next, expose the circuits that users may call in the contract. + +This library enables extensibility by following the rules of the [Module/Contract Pattern]. +Note that circuits with a preceding underscore (`_likeThis`) are meant to be building blocks for implementing contracts. +Exposing [_mint] without some sort of access control, for example, would allow ANYONE to mint tokens. + +```typescript +export circuit uri(id: Uint<128>): Opaque<"string"> { + return MultiToken_uri(id); +} + +export circuit balanceOf(account: Either, ContractAddress>, id: Uint<128>): Uint<128> { + return MultiToken_balanceOf(account, id); +} + +(...) +``` + +The following example is a simple multi-token contract that creates both a fixed-supply fungible token and an NFT using the same contract. + +```typescript +// MultiTokenTwoTokenTypes.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken" + prefix MultiToken_; + +constructor( + _uri: Opaque<"string">, + recipient: Either, ContractAddress>, + fungibleFixedSupply: Uint<128>, +) { + // `initialize` sets the URI (base) for all tokens minted from this contract + MultiToken_initialize(_uri); + + // Token id `123` is a fungible token (with a fixed supply of `fungibleFixedSupply`) + const fungibleTokenId = 123; + MultiToken__mint(recipient, fungibleTokenId, fungibleFixedSupply); + + // Token id `987` is a non-fungible token (so the supply is only 1) + const nonFungibleTokenId = 987; + MultiToken__mint(recipient, nonFungibleTokenId, 1); +} + +export circuit uri(id: Uint<128>): Opaque<"string"> { + return MultiToken_uri(id); +} + +export circuit balanceOf(account: Either, ContractAddress>, id: Uint<128>): Uint<128> { + return MultiToken_balanceOf(account, id); +} + +export circuit setApprovalForAll( + operator: Either, ContractAddress>, + approved: Boolean +): [] { + return MultiToken_setApprovalForAll(operator, approved); +} + +export circuit isApprovedForAll( + account: Either, ContractAddress>, + operator: Either, ContractAddress> +): Boolean { + return MultiToken_isApprovedForAll(account, operator); +} + +export circuit transferFrom( + fromAddress: Either, ContractAddress>, + to: Either, ContractAddress>, + id: Uint<128>, + value: Uint<128>, +): [] { + return MultiToken_transferFrom(fromAddress, to, id, value); +} + +``` diff --git a/content/contracts-compact/0.4.x/multisig.mdx b/content/contracts-compact/0.4.x/multisig.mdx new file mode 100644 index 00000000..7de06652 --- /dev/null +++ b/content/contracts-compact/0.4.x/multisig.mdx @@ -0,0 +1,355 @@ +--- +title: Multisig +--- + +{/* links */} +[Module/Contract Pattern]: ./extensibility#the-modulecontract-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.4.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/0.4.x/native-shielded-token.mdx b/content/contracts-compact/0.4.x/native-shielded-token.mdx new file mode 100644 index 00000000..a7b867b2 --- /dev/null +++ b/content/contracts-compact/0.4.x/native-shielded-token.mdx @@ -0,0 +1,375 @@ +--- +title: NativeShieldedToken +--- + +{/* links */} +[Module/Contract Pattern]: ./extensibility#the-modulecontract-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. + + +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 + +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/0.4.x/non-fungible-token.mdx similarity index 100% rename from content/contracts-compact/non-fungible-token.mdx rename to content/contracts-compact/0.4.x/non-fungible-token.mdx diff --git a/content/contracts-compact/0.4.x/ownable.mdx b/content/contracts-compact/0.4.x/ownable.mdx new file mode 100644 index 00000000..2c25eb5b --- /dev/null +++ b/content/contracts-compact/0.4.x/ownable.mdx @@ -0,0 +1,439 @@ +--- +title: Ownable +--- + +{/* links */} +[initialize]: api/ownable#Ownable-initialize +[transferOwnership]: api/ownable#Ownable-transferOwnership +[_transferOwnership]: api/ownable#Ownable-_transferOwnership +[_unsafeTransferOwnership]: api/ownable#Ownable-_unsafeTransferOwnership +[_unsafeUncheckedTransferOwnership]: api/ownable#Ownable-_unsafeUncheckedTransferOwnership +[computeAccountId]: api/utils#Utils-computeAccountId +[assertOnlyOwner]: api/ownable#Ownable-assertOnlyOwner +[Ownable API]: api/ownable#Ownable + +[ZOwnablePK_initialize]: api/ownable#ZOwnablePK-initialize +[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 + +Access control—that is, "who is allowed to do this thing"—is incredibly important in the world of smart contracts. +The access control of your contract may govern who can mint tokens, vote on proposals, freeze transfers, and many other things. +It is therefore critical to understand how you implement it, lest someone else steals your whole system. +This library provides a variety of access control modules to suit your application and privacy needs. + +## Ownership and `Ownable` + +The most common and basic form of access control is the concept of ownership: +there's an account that is the owner of a contract and can do administrative tasks on it. +This approach is perfectly reasonable for contracts that have a single administrative user. + +This library provides an Ownable module for implementing ownership in your contracts. +The initial owner must be set by using the [initialize] circuit during construction. +This can later be changed with [transferOwnership]. + + + For more granular, role-based access control, see [AccessControl]. + + +### Identity Model + +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 `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] from the [Utils] module: + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +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 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. + Key exposure allows impersonation. Use `crypto.getRandomValues()` or equivalent to generate keys. + + +### Ownership Transfers + +Ownership can only be transferred to `Bytes<32>` account identifiers through the main transfer circuits +([transferOwnership] and [_transferOwnership]). + +In other words, ownership transfers to contract addresses are disallowed through these circuits. +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. + +### Unsafe Circuits + +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"). +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. + + + The unsafe circuits are planned to become deprecated once contract-to-contract calls become available. + + +### Usage + +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 `Utils_computeAccountId(secretKey)`: + +```ts +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +constructor( + initialOwner: Either, ContractAddress> +) { + Ownable_initialize(initialOwner); +} +``` + +To protect a circuit so that only the contract owner may call it, +insert the `assertOnlyOwner` circuit in the beginning of the circuit body like this: + +```ts +export circuit mySensitiveCircuit(): [] { + Ownable_assertOnlyOwner(); + + // Do something +} +``` + +Contracts may expose [transferOwnership] to allow the owner to transfer ownership. +The `newOwner` should be an account identifier derived by the new owner using `Utils_computeAccountId(secretKey)`: + +```ts +export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { + Ownable_transferOwnership(newOwner); +} +``` + +Here's a complete contract showcasing how to integrate the Ownable module and protect sensitive circuits. + +```ts +// SimpleOwnable.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +/** + * Set `initialOwner` as the owner of the contract. + * `initialOwner` should be derived off-chain using Utils_computeAccountId(secretKey). + */ +constructor(initialOwner: Either, ContractAddress>) { + Ownable_initialize(initialOwner); +} + +/** + * The current owner of the contract. + */ +export circuit owner(): Either, ContractAddress> { + return Ownable_owner(); +} + +/** + * Transfers ownership of the contract. + * Can only be called by the current owner. + * `newOwner` should be derived off-chain by the new owner using Utils_computeAccountId(secretKey). + */ +export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { + Ownable_transferOwnership(newOwner); +} + +/** + * Leaves the contract without an owner. + * Can only be called by the current owner. + * Renouncing ownership means `mySensitiveCircuit` can never be called again. + */ +export circuit renounceOwnership(): [] { + Ownable_renounceOwnership(); +} + +/** + * This is the protected circuit that only the current owner can call. + */ +export circuit mySensitiveCircuit(): [] { + // Protects the circuit + Ownable_assertOnlyOwner(); + + // Do something +} +``` + + + For more complex logic, + contracts may transfer ownership to another user irrespective of the caller by leveraging [_transferOwnership]. + This is generally more useful when contract addresses are the owner or when a contract has a unique deployment process. + + +## Shielded Ownership and `ZOwnablePK` + +Privacy-preserving access control is a fundamental building block for confidential smart contracts on Midnight. +While the `Ownable` module stores the owner's account identifier on-chain, +many applications require administrative control without revealing who holds that authority. + +### Privacy-First Ownership + +The `ZOwnablePK` module implements shielded ownership meaning administrative control without identity disclosure. +The owner's public key is never revealed on-chain; +instead, the contract stores only a cryptographic commitment that proves ownership without exposing the underlying identity. + +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, +unlinkability, and collision resistance across deployments and ownership transfers. + +#### Owner ID Computation + +The foundation of the system is the owner identifier, computed as: + +```typescript +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 +or deterministically derived for recoverability. +This identifier serves as a privacy-preserving alternative to exposing the raw public key, +ensuring the owner's identity remains confidential. + +#### Owner Commitment Computation + +The final ownership commitment stored on-chain is computed as: + +```typescript +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. 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 +with other commitment schemes and enables safe protocol extensions. + +#### Security Properties + +This commitment scheme ensures that: + +* Public keys are never revealed on-chain. +* 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. +Both approaches are valid, and the best choice depends on your specific threat model and operational requirements. + +#### Random Nonce + +Generating a cryptographically strong random nonce provides the strongest privacy guarantees: + +```typescript +const randomNonce = crypto.getRandomValues(new Uint8Array(32)); +const ownerId = ZOwnablePK._computeOwnerId(publicKey, randomNonce); +``` + +This approach is easy to generate and ensures maximum unlinkability. +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 + +Deriving the nonce deterministically enables recovery through derivation schemes. Some examples: + +* `H(passphrase + context)` — recoverable from passphrase only, but passphrase becomes critical single point of failure. +* `H(publicKey + userPassphrase + context)` — requires both public key and passphrase. +* `H(signature + context) where signature = sign(context)` — leverages wallet without exposing private key. + + + When using signature-based nonce derivation, + ensure the wallet/library uses deterministic signatures ([ed25519] or [rfc6979] for ECDSA). + Non-deterministic signatures will generate different nonces on each signing, making recovery impossible. + Test the implementation by signing the same message twice then verify that the signatures match. + + +**Context-Dependent Derivations:** + +* Include contract address, deployment timestamp, user ID, etc. +* Trade-off: more context is more unique but harder to recreate. + + + Approaches that avoid private key exposure (public key + passphrase, signature-based) + are generally recommended for operational security. + + +**Security Considerations** + +The `ZOwnablePK` module remains agnostic to nonce generation methods, +placing the security/convenience decision entirely with the user. +Key considerations include: + +* **Backup requirements**: Random nonces require additional secure storage. +* **Recovery scenarios**: Deterministic nonces enable recovery. +* **Cross-contract correlation**: Reusing nonce strategies may reduce privacy across deployments. +* **Rotation costs**: Changing nonces requires ownership transfer transactions with associated costs. + +Users should carefully evaluate their threat model, operational requirements, and privacy needs when selecting a nonce generation strategy, +as this choice cannot be easily changed without transferring ownership. + +### Usage + +Import the `ZOwnablePK` module into the implementing contract and expose the ownership-handling circuits. +It's recommended to prefix the module with `ZOwnablePK_` to avoid circuit signature clashes. + +```typescript +// MyZOwnablePKContract.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/ZOwnablePK" + prefix ZOwnablePK_; + +constructor( + initOwnerId: Bytes<32>, + instanceSalt: Bytes<32>, +) { + ZOwnablePK_initialize(initOwnerId, instanceSalt); +} + +export circuit owner(): Bytes<32> { + return ZOwnablePK_owner(); +} + +export circuit transferOwnership(newOwnerId: Bytes<32>): [] { + return ZOwnablePK_transferOwnership(disclose(newOwnerId)); +} + +export circuit renounceOwnership(): [] { + return ZOwnablePK_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: + +```typescript +export circuit mySensitiveCircuit(): [] { + ZOwnablePK_assertOnlyOwner(); + + // Do something +} +``` + +This covers the basics for creating a contract, but before deploying the contract, +the owner's id must be derived for the commitment scheme because it's required to deploy the contract. + +First, the owner needs to generate a secret nonce that's stored in the owner's private state. +See [Nonce Generation Strategies](#nonce-generation-strategies). + +Once the owner has the secret nonce generated, they can derive their owner ID: + +```typescript +import { + CompactTypeBytes, + CompactTypeVector, + persistentHash, +} from '@midnight-ntwrk/compact-runtime'; +import { getRandomValues } from 'node:crypto'; + +// Owner ID +const generateId = ( + pk: Uint8Array, + nonce: Uint8Array, +): Uint8Array => { + const rt_type = new CompactTypeVector(2, new CompactTypeBytes(32)); + return persistentHash(rt_type, [pk, nonce]); +}; + +// Instance salt for the constructor (must be nonzero) +const generateInstanceSalt = (): Uint8Array => { + return getRandomValues(new Uint8Array(32)); +} +``` + + + Another way to get the user ID is to expose [_computeOwnerId] in the contract and call this circuit off-chain through a contract simulator. + diff --git a/content/contracts-compact/0.4.x/security.mdx b/content/contracts-compact/0.4.x/security.mdx new file mode 100644 index 00000000..b2824704 --- /dev/null +++ b/content/contracts-compact/0.4.x/security.mdx @@ -0,0 +1,282 @@ +--- +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 + +The Initializable module provides a simple mechanism that mimics the functionality of a constructor. +More specifically, it enables logic to be performed once and only once to set up a contract's initial state. +This is useful when a constructor cannot be used, +for example when there are circular dependencies at construction time. + +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 +// CustomContractStateSetup.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/security/Initializable" + prefix Initializable_; + +export ledger _fieldAfterDeployment: Field; + +export circuit doSomethingBeforeInitialized(): [] { + // Ensure action can only be done prior to custom initialization + Initializable_assertNotInitialized(); + // Do the thing +} + +export circuit setFieldAfterDeployment(f: Field): [] { + // Initialize so the circuit cannot be called again + Initializable_initialize(); + _fieldAfterDeployment = disclose(f); +} + +export circuit checkFieldAfterDeployment(): Field { + // Can only be called after the contract is initialized + Initializable_assertInitialized(); + return _fieldAfterDeployment; +} +``` + +See the [Initializable API] for the full list of circuits. + +## 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 +or having an emergency switch to freeze all transactions in the event of a large bug. + +To become pausable, the contract should include `pause` and `unpause` circuits (which should be protected). +For circuits that should be available only when paused or not, +insert calls to [assertPaused] and [assertNotPaused] respectively. + +### Usage + +For example (using the [Ownable] module for access control): + +```ts +// OwnablePausable.compact + +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/security/Pausable" + prefix Pausable_; +import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" + prefix Ownable_; + +constructor(initOwner: Either, ContractAddress>) { + Ownable_initialize(initOwner); +} + +export circuit pause(): [] { + Ownable_assertOnlyOwner(); + Pausable__pause(); +} + +export circuit unpause(): [] { + Ownable_assertOnlyOwner(); + Pausable__unpause(); +} + +export circuit whenNotPaused(): [] { + Pausable_assertNotPaused(); + // Do something +} + +export circuit whenPaused(): [] { + Pausable_assertPaused(); + // 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/0.4.x/shielded-access-control.mdx b/content/contracts-compact/0.4.x/shielded-access-control.mdx new file mode 100644 index 00000000..578933c0 --- /dev/null +++ b/content/contracts-compact/0.4.x/shielded-access-control.mdx @@ -0,0 +1,226 @@ +--- +title: ShieldedAccessControl +--- + +{/* links */} +[Role-Based Access Control (RBAC)]: https://en.wikipedia.org/wiki/Role-based_access_control +[AccessControl]: ./access-control.mdx +[ShieldedAccessControl API]: api/shielded-access-control + +[assertOnlyRole]: api/shielded-access-control#ShieldedAccessControl-assertOnlyRole +[canProveRole]: api/shielded-access-control#ShieldedAccessControl-canProveRole +[grantRole]: api/shielded-access-control#ShieldedAccessControl-grantRole +[_grantRole]: api/shielded-access-control#ShieldedAccessControl-_grantRole +[revokeRole]: api/shielded-access-control#ShieldedAccessControl-revokeRole +[_revokeRole]: api/shielded-access-control#ShieldedAccessControl-_revokeRole +[renounceRole]: api/shielded-access-control#ShieldedAccessControl-renounceRole +[_setRoleAdmin]: api/shielded-access-control#ShieldedAccessControl-_setRoleAdmin +[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, +ShieldedAccessControl stores roles as Merkle tree commitments which hides _who_ holds a role +while still allowing on-chain verification of authorization. + +If your contract does not require privacy for role holders, the simpler [AccessControl] module is recommended. + +## Why Shielded Access Control? + +In many applications, revealing which accounts hold administrative or privileged roles +creates a security and privacy risk. ShieldedAccessControl addresses this by ensuring +that role holders are never identified on-chain. Instead, callers prove ownership of a role +using zero-knowledge proofs against Merkle tree commitments. + +This is useful in scenarios such as: + +- **Private governance**: voters or committee members prove eligibility without revealing identity. +- **Confidential operations**: admins can manage a system without their addresses being publicly linked to the contract. +- **Pseudonymous authorization**: role holders maintain a stable pseudonymous identity for the lifetime of their role, but that identity cannot be linked to a real-world identity without key compromise. + +## How It Works + +ShieldedAccessControl is built on three cryptographic primitives: + +**Role commitments** are Merkle tree leaves that bind a role to an account without revealing either. +When a role is granted, a commitment is inserted into a Merkle tree: + +``` +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) +``` + +**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) +``` + +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, +grant them during construction, and enforce them with [assertOnlyRole]. +The key difference is that role assignments are stored as Merkle tree commitments rather than in a public map, +and revocation uses a nullifier scheme rather than simply flipping a boolean. +Account identifiers also incorporate a per-instance salt, preventing cross-contract correlation of the same secret key. + +```ts +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessControl" + prefix SAC_; + +export sealed ledger MINTER_ROLE: Bytes<32>; + +constructor( + instanceSalt: Bytes<32>, + defaultAdmin: SAC_AccountIdentifier, + minter: SAC_AccountIdentifier +) { + SAC_initialize(instanceSalt); + MINTER_ROLE = persistentHash>(pad(32, "MINTER_ROLE")); + // Grant the default admin role and the minter role + SAC__grantRole(SAC_DEFAULT_ADMIN_ROLE(), defaultAdmin); + SAC__grantRole(MINTER_ROLE as SAC_RoleIdentifier, minter); +} + +export circuit mint(/* ... */): [] { + SAC_assertOnlyRole(MINTER_ROLE as SAC_RoleIdentifier); + // ... minting logic +} +``` + + + 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. + + +### Deriving Account Identifiers + +Before an account can be granted a role, the grantor needs the grantee's `AccountIdentifier`. +This is derived off-chain using [computeAccountId]: + +```ts +const accountId = SAC_computeAccountId(secretKey, instanceSalt); +``` + +The `secretKey` is a 32-byte cryptographically secure random value that the user keeps private. +The `instanceSalt` is the same value used during contract initialization. + + + The `secretKey` must never be logged, persisted in plaintext, or exposed in browser devtools. + Loss of the key means loss of all roles held under that identity. Key exposure allows impersonation + and retroactive deanonymization. + + +### Granting and Revoking Roles + +Role management works similarly to [AccessControl]. Every role has an associated admin role. +Only accounts that can prove they hold the admin role may call [grantRole] and [revokeRole]. +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. +To be re-authorized, the user must generate a new `secretKey` to produce a new `accountId`. + +Other roles held under the old secret key remain valid. The user will need to retain both keys +until all roles under the old key are no longer needed. + +### Renouncing Roles + +[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, +and the pairing must have been granted. Consumers with no need for self-revocation should not export this circuit. + +### Proving Role Ownership + +Two circuits are available for verifying role ownership: + +- [assertOnlyRole]: reverts if the caller cannot prove they hold the role. Use this to gate access to circuits. +- [canProveRole]: returns a boolean without reverting. Useful for conditional logic where lacking a role is not an error. + +Both circuits disclose a nullifier, which means every call made under the same role instance +can be linked by an outside observer. This provides **pseudonymity, not anonymity**. + +## Privacy Considerations + +ShieldedAccessControl provides strong privacy guarantees, but it's important to understand what is and isn't hidden: + +**What observers CAN see:** +- When roles are granted and revoked +- All role identifiers and which are admin identifiers (disclosed via [getRoleAdmin], [_setRoleAdmin], [grantRole], and [revokeRole]) +- How many admins exist +- All role nullifiers (disclosed via `Set` operations in every grant, revoke, and role-proof transaction) +- 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 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. + + + The internal circuits ([_grantRole], [_revokeRole]) do not disclose the role identifier, offering stronger privacy + when wrapped with custom authorization logic. The trade-off is that the implementing contract takes full + responsibility for access control. + + +## Security Considerations + +- 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/0.4.x/utils.mdx b/content/contracts-compact/0.4.x/utils.mdx new file mode 100644 index 00000000..a7589eaa --- /dev/null +++ b/content/contracts-compact/0.4.x/utils.mdx @@ -0,0 +1,114 @@ +--- +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. + +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}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/utils/Utils" + prefix Utils_; + +export circuit performActionWhenEqual( + a: Either, + b: Either, +): [] { + const isEqual = Utils_isKeyOrAddressEqual(a, b); + if (isEqual) { + // Do something + } else { + // 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/0.4.x/utils/constants.js b/content/contracts-compact/0.4.x/utils/constants.js new file mode 100644 index 00000000..614f46a8 --- /dev/null +++ b/content/contracts-compact/0.4.x/utils/constants.js @@ -0,0 +1,2 @@ +export const COMPACT_COMPILER_VERSION = "0.34.0"; +export const COMPACT_LANGUAGE_VERSION = "0.26.0"; diff --git a/content/contracts-compact/0.4.x/utils/replacements.ts b/content/contracts-compact/0.4.x/utils/replacements.ts new file mode 100644 index 00000000..ae8b306d --- /dev/null +++ b/content/contracts-compact/0.4.x/utils/replacements.ts @@ -0,0 +1,9 @@ +import { COMPACT_COMPILER_VERSION, COMPACT_LANGUAGE_VERSION } from "./constants"; + +export const REPLACEMENTS = { + include: ['**/content/contracts-compact/0.4.x/**/*.mdx'], + replacements: { + compact_compiler_version: COMPACT_COMPILER_VERSION, + compact_language_version: COMPACT_LANGUAGE_VERSION + } +} diff --git a/content/contracts-compact/0.4.x/zk-circuits-101.mdx b/content/contracts-compact/0.4.x/zk-circuits-101.mdx new file mode 100644 index 00000000..5e954d15 --- /dev/null +++ b/content/contracts-compact/0.4.x/zk-circuits-101.mdx @@ -0,0 +1,36 @@ +--- +title: ZK Circuits 101 +--- + +{/* links */} +[the math behind the scenes needs it that way]: https://zkhack.dev/whiteboard/module-three/ + +## What are ZK Circuits? + +Compact code is compiled into arithmetic circuits, which are mathematical representations of the contract's logic. +These circuits are made up of arithmetic gates. +The gates enforce the rules and constraints defined in the Compact program. +At a high level, ZK circuits can be thought of as a magic puzzle board that proves a series of steps was followed correctly - like a recipe. +The puzzle board is laid out in a grid like a giant sheet of graph paper with a certain number of rows. +Each row is a space where a step or rule that needs to be followed is written. +These steps/rules correspond to the gates that make up the arithmetic circuit. + +## Circuit Size and Domain + +The size of the board is called the **domain size** which is referred to as `k` in the documentation. +It’s always a power of 2 (like 256, 512, 1024, etc.), +because [the math behind the scenes needs it that way]. +Now, just because the board has 1024 rows doesn’t mean it uses all of them. +Maybe the recipe takes only 563 steps, so only 563 rows are filled and the rest are left blank. +These filled-in rows are called **used rows**. +So "k = 10, rows = 563" in the API Reference documentation that means "this circuit has a size of 2^10 = 1024 rows and only uses 563 rows". + +## Why Circuit Size Matters + +Why is this important? Well, when writing ZK circuits the size and number of rules to follow should be as small as possible. +The number of rules in a zero-knowledge circuit directly impacts both the prover time +(how long it takes to generate a proof) and, to a lesser extent, the proof size and verifier time. +Larger circuits with more rules require more computation to generate a proof, +which can make proof generation slower and more resource-intensive. +This is especially relevant for privacy-preserving blockchains like Midnight, +where proof generation is often the most computationally expensive part of a transaction. diff --git a/content/contracts-compact/compact-replacements.ts b/content/contracts-compact/compact-replacements.ts new file mode 100644 index 00000000..494ab81a --- /dev/null +++ b/content/contracts-compact/compact-replacements.ts @@ -0,0 +1,9 @@ +import { REPLACEMENTS as zeroOneXReplacements } from "./0.1.x/utils/replacements"; +import { REPLACEMENTS as zeroThreeXReplacements } from "./0.3.x/utils/replacements"; +import { REPLACEMENTS as zeroFourXReplacements } from "./0.4.x/utils/replacements"; + +export const COMPACT_REPLACEMENTS = [ + zeroOneXReplacements, + zeroThreeXReplacements, + zeroFourXReplacements, +]; diff --git a/content/contracts-compact/index.mdx b/content/contracts-compact/index.mdx index ae64ef1b..07fcecc9 100644 --- a/content/contracts-compact/index.mdx +++ b/content/contracts-compact/index.mdx @@ -2,179 +2,102 @@ title: Contracts for Compact --- -{/* links */} -[Midnight]: https://midnight.network/ -[nvm]: https://github.com/nvm-sh/nvm -[yarn]: https://yarnpkg.com/getting-started/install -[compact-dev-tools]: https://docs.midnight.network/blog/compact-developer-tools - -**A library for secure smart contract development** written in Compact for [Midnight]. -This library consists of modules to build custom smart contracts. - - -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 - -Make sure you have [nvm] and [yarn] installed on your machine. - -Follow Midnight's [Compact Developer Tools][compact-dev-tools] installation guide and confirm that `compact` is in the `PATH` env variable. - -```bash -$ compact compile --version - -Compactc version: {{compact_compiler_version}} -{{compact_compiler_version}} -``` - -### Installation - -Create a directory for your project. - -```bash -mkdir my-project -cd my-project -``` - -Install the library. - -```bash -yarn add @openzeppelin/compact-contracts -``` - -### Write a custom contract using library modules - -In the root of `my-project`, create a custom contract using OpenZeppelin Compact modules. -Import the modules through `./node_modules/@openzeppelin/compact-contracts/...`. - -```typescript -// MyContract.compact - -pragma language_version >= {{compact_language_version}}; - -import CompactStandardLibrary; -import "./node_modules/@openzeppelin/compact-contracts/access/Ownable" - prefix Ownable_; -import "./node_modules/@openzeppelin/compact-contracts/security/Pausable" - prefix Pausable_; -import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken" - prefix FungibleToken_; - -constructor( - _name: Opaque<"string">, - _symbol: Opaque<"string">, - _decimals: Uint<8>, - _recipient: Either, ContractAddress>, - _amount: Uint<128>, - _initOwner: Either, ContractAddress>, -) { - Ownable_initialize(_initOwner); - FungibleToken_initialize(_name, _symbol, _decimals); - FungibleToken__mint(_recipient, _amount); -} - -/** IFungibleToken */ - -export circuit name(): Opaque<"string"> { - return FungibleToken_name(); -} - -export circuit symbol(): Opaque<"string"> { - return FungibleToken_symbol(); -} - -export circuit decimals(): Uint<8> { - return FungibleToken_decimals(); -} - -export circuit totalSupply(): Uint<128> { - return FungibleToken_totalSupply(); -} - -export circuit balanceOf(account: Either, ContractAddress>): Uint<128> { - return FungibleToken_balanceOf(account); -} - -export circuit allowance( - owner: Either, ContractAddress>, - spender: Either, ContractAddress> -): Uint<128> { - return FungibleToken_allowance(owner, spender); -} - -export circuit transfer( - to: Either, ContractAddress>, - value: Uint<128>, -): Boolean { - Pausable_assertNotPaused(); - return FungibleToken_transfer(to, value); -} - -export circuit transferFrom( - fromAddress: Either, ContractAddress>, - to: Either, ContractAddress>, - value: Uint<128> -): Boolean { - Pausable_assertNotPaused(); - return FungibleToken_transferFrom(fromAddress, to, value); -} - -export circuit approve(spender: Either, ContractAddress>, value: Uint<128>): Boolean { - Pausable_assertNotPaused(); - return FungibleToken_approve(spender, value); -} - -/** IOwnable */ - -export circuit owner(): Either, ContractAddress> { - return Ownable_owner(); -} - -export circuit transferOwnership(newOwner: Either, ContractAddress>): [] { - return Ownable_transferOwnership(newOwner); -} - -export circuit renounceOwnership(): [] { - return Ownable_renounceOwnership(); -} - -/** IPausable */ - -export circuit pause(): [] { - Ownable_assertOnlyOwner(); - Pausable__pause(); -} - -export circuit unpause(): [] { - Ownable_assertOnlyOwner(); - Pausable__unpause(); -} -``` - -### Compile the contract - -In the project root, compile the contract. - -```bash -% compact compile MyContract.compact artifacts/MyContract -Compiling 14 circuits: - circuit "allowance" (k=11, rows=1352) - 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=4977) - circuit "transferOwnership" (k=13, rows=2959) - circuit "unpause" (k=13, rows=2362) -Overall progress [====================] 14/14 -``` +import { latestStable } from "./latest-versions.js"; + +**OpenZeppelin Contracts for Compact** is a library of modules for secure smart contract development on Midnight. + +## Getting Started + + + + Install the library, write a contract from its modules, and compile it. + + + Compose contracts from modules with the Module/Contract pattern. + + + Learn how Compact code compiles to circuits and what drives their cost. + + + +## Modules + + + + Restrict circuits to a single owner, with a shielded-owner variant. + + + Grant and enforce permissions with role-based access control. + + + Role-based access control that hides who holds each role. + + + Initializable, Pausable, Allowlist, and Blocklist modules. + + + Fungible tokens, an approximation of EIP-20. + + + Non-fungible tokens, an approximation of EIP-721. + + + Multiple token types in one contract, an approximation of EIP-1155. + + + Mint and burn Midnight shielded coins of the contract's own color. + + + Fungible tokens with ElGamal-encrypted balances on the public ledger. + + + ElGamal and EcdhMask encryption over Jubjub, the ECDSA low-s rule, and the EIP-712 envelope. + + + Signer registry, proposals, treasuries, and threshold-ECDSA presets for contract-held funds. + + + Account identifiers, helper circuits, and EVM ABI word encoding. + + + +## API Reference + + + + Circuits, ledger state, and errors of the AccessControl module. + + + Circuits, ledger state, and errors of the ConfidentialFungibleToken module. + + + Circuits and types of the ElGamal, EcdhMask, Ecdsa, and Eip712 modules. + + + Circuits, ledger state, and errors of the FungibleToken module. + + + Circuits, ledger state, and errors of the MultiToken module. + + + Circuits, ledger state, and errors of the multisig modules. + + + Circuits, ledger state, and errors of the NativeShieldedToken module. + + + Circuits, ledger state, and errors of the NonFungibleToken module. + + + Circuits, ledger state, and errors of the Ownable module. + + + Circuits and errors of the Initializable, Pausable, Allowlist, and Blocklist modules. + + + Circuits, ledger state, and errors of the ShieldedAccessControl module. + + + Circuits of the Utils and EvmAbi modules. + + diff --git a/content/contracts-compact/latest-version.js b/content/contracts-compact/latest-version.js deleted file mode 100644 index 32018183..00000000 --- a/content/contracts-compact/latest-version.js +++ /dev/null @@ -1 +0,0 @@ -export const latestStable = "0.3.x"; diff --git a/content/contracts-compact/latest-versions.js b/content/contracts-compact/latest-versions.js new file mode 100644 index 00000000..d6f8f170 --- /dev/null +++ b/content/contracts-compact/latest-versions.js @@ -0,0 +1 @@ +export const latestStable = "0.4.x"; diff --git a/netlify.toml b/netlify.toml index 451e4f8e..86c5765d 100644 --- a/netlify.toml +++ b/netlify.toml @@ -136,9 +136,29 @@ status = 301 # Midnight Compact [[redirects]] -from = "/contracts-compact/0.0.1/*" -to = "/contracts-compact/:splat" -status = 301 + from = "/contracts-compact/0.1.x/*" + to = "/contracts-compact/0.1.x/:splat" + status = 200 + +[[redirects]] + from = "/contracts-compact/0.3.x/*" + to = "/contracts-compact/0.3.x/:splat" + status = 200 + +[[redirects]] + from = "/contracts-compact/0.4.x/*" + to = "/contracts-compact/0.4.x/:splat" + status = 200 + +[[redirects]] + from = "/contracts-compact/0.0.1/*" + to = "/contracts-compact/0.4.x/:splat" + status = 301 + +[[redirects]] + from = "/contracts-compact/*" + to = "/contracts-compact/0.4.x/:splat" + status = 301 # Stylus diff --git a/public/llms.txt b/public/llms.txt index 4e4f94f4..856786a4 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -162,6 +162,20 @@ Each ecosystem section lists the smart-contract libraries and language-specific - [Tokens — Non-Fungible](https://docs.openzeppelin.com/stellar-contracts/tokens/non-fungible/non-fungible) - [Tokens — RWA](https://docs.openzeppelin.com/stellar-contracts/tokens/rwa/rwa) - [Tokens — Vault](https://docs.openzeppelin.com/stellar-contracts/tokens/vault/vault) +- [Tokens — Confidential — Confidential Token](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/confidential) +- [Tokens — Confidential — Privacy Model](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/privacy-model) +- [Tokens — Confidential — Proof System](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/proof-system) +- [Tokens — Confidential — Operations](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/operations) +- [Tokens — Confidential — Operations — Spenders](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/spenders) +- [Tokens — Confidential — Auditing](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/auditing) +- [Tokens — Confidential — Selective Disclosure](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/selective-disclosure) +- [Tokens — Confidential — Compliance](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/compliance) +- [Tokens — Confidential — Compliance — Clawback](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/clawback) +- [Tokens — Confidential — Wallet Integration](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/wallet-integration) +- [Tokens — Confidential — Wallet Integration — Indexing and Recovery](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/indexing) +- [Tokens — Confidential — Best Practices — Account Holders](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/best-practices/account-holders) +- [Tokens — Confidential — Best Practices — Administrators](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/best-practices/administrators) +- [Tokens — Confidential — Best Practices — Auditors](https://docs.openzeppelin.com/stellar-contracts/tokens/confidential/best-practices/auditors) - [Access — Access Control](https://docs.openzeppelin.com/stellar-contracts/access/access-control) - [Access — Ownable](https://docs.openzeppelin.com/stellar-contracts/access/ownable) - [Governance — Governor](https://docs.openzeppelin.com/stellar-contracts/governance/governor) @@ -170,6 +184,7 @@ Each ecosystem section lists the smart-contract libraries and language-specific - [Utilities — Pausable](https://docs.openzeppelin.com/stellar-contracts/utils/pausable) - [Utilities — Upgradeable](https://docs.openzeppelin.com/stellar-contracts/utils/upgradeable) - [Utilities — Math — WAD](https://docs.openzeppelin.com/stellar-contracts/utils/math/wad): High-precision decimal arithmetic +- [Utilities — Math — Fixed-Point](https://docs.openzeppelin.com/stellar-contracts/utils/math/fixed-point): Full-precision x * y / denominator for i128 and I256 with explicit rounding - [Utilities — Cryptography](https://docs.openzeppelin.com/stellar-contracts/utils/crypto/crypto) - [Utilities — Cryptography — Merkle Distributor](https://docs.openzeppelin.com/stellar-contracts/utils/crypto/merkle-distributor) - [Fee Abstraction](https://docs.openzeppelin.com/stellar-contracts/fee-abstraction) @@ -395,27 +410,83 @@ Each ecosystem section lists the smart-contract libraries and language-specific ## Midnight +- [Getting Started](https://docs.openzeppelin.com/contracts-compact) + ### Midnight Contracts -- [Overview](https://docs.openzeppelin.com/contracts-compact) -- [Learn — Extensibility](https://docs.openzeppelin.com/contracts-compact/extensibility) -- [Learn — ZK Circuits 101](https://docs.openzeppelin.com/contracts-compact/zk-circuits-101) -- [Modules — Access — Ownable](https://docs.openzeppelin.com/contracts-compact/ownable) -- [Modules — Access — AccessControl](https://docs.openzeppelin.com/contracts-compact/access-control) -- [Modules — Access — ShieldedAccessControl](https://docs.openzeppelin.com/contracts-compact/shielded-access-control) -- [Modules — Security](https://docs.openzeppelin.com/contracts-compact/security) -- [Modules — Tokens — FungibleToken](https://docs.openzeppelin.com/contracts-compact/fungible-token) -- [Modules — Tokens — NonFungibleToken](https://docs.openzeppelin.com/contracts-compact/non-fungible-token) -- [Modules — Tokens — MultiToken](https://docs.openzeppelin.com/contracts-compact/multi-token) -- [Modules — Utils](https://docs.openzeppelin.com/contracts-compact/utils) -- [API Reference — AccessControl API](https://docs.openzeppelin.com/contracts-compact/api/access-control) -- [API Reference — FungibleToken API](https://docs.openzeppelin.com/contracts-compact/api/fungible-token) -- [API Reference — MultiToken API](https://docs.openzeppelin.com/contracts-compact/api/multi-token) -- [API Reference — NonFungibleToken API](https://docs.openzeppelin.com/contracts-compact/api/non-fungible-token) -- [API Reference — Ownable API](https://docs.openzeppelin.com/contracts-compact/api/ownable) -- [API Reference — Security API](https://docs.openzeppelin.com/contracts-compact/api/security) -- [API Reference — ShieldedAccessControl API](https://docs.openzeppelin.com/contracts-compact/api/shielded-access-control) -- [API Reference — Utils API](https://docs.openzeppelin.com/contracts-compact/api/utils) +- [Overview](https://docs.openzeppelin.com/contracts-compact/0.4.x) +- [Learn — Extensibility](https://docs.openzeppelin.com/contracts-compact/0.4.x/extensibility) +- [Learn — ZK Circuits 101](https://docs.openzeppelin.com/contracts-compact/0.4.x/zk-circuits-101) +- [Modules — Access — Ownable](https://docs.openzeppelin.com/contracts-compact/0.4.x/ownable) +- [Modules — Access — AccessControl](https://docs.openzeppelin.com/contracts-compact/0.4.x/access-control) +- [Modules — Access — ShieldedAccessControl](https://docs.openzeppelin.com/contracts-compact/0.4.x/shielded-access-control) +- [Modules — Security](https://docs.openzeppelin.com/contracts-compact/0.4.x/security) +- [Modules — Tokens — FungibleToken](https://docs.openzeppelin.com/contracts-compact/0.4.x/fungible-token) +- [Modules — Tokens — NonFungibleToken](https://docs.openzeppelin.com/contracts-compact/0.4.x/non-fungible-token) +- [Modules — Tokens — MultiToken](https://docs.openzeppelin.com/contracts-compact/0.4.x/multi-token) +- [Modules — Tokens — NativeShieldedToken](https://docs.openzeppelin.com/contracts-compact/0.4.x/native-shielded-token) +- [Modules — Tokens — ConfidentialFungibleToken](https://docs.openzeppelin.com/contracts-compact/0.4.x/confidential-fungible-token) +- [Modules — Crypto](https://docs.openzeppelin.com/contracts-compact/0.4.x/crypto) +- [Modules — Multisig](https://docs.openzeppelin.com/contracts-compact/0.4.x/multisig) +- [Modules — Utils](https://docs.openzeppelin.com/contracts-compact/0.4.x/utils) +- [API Reference — AccessControl API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/access-control) +- [API Reference — ConfidentialFungibleToken API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/confidential-fungible-token) +- [API Reference — Crypto API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/crypto) +- [API Reference — FungibleToken API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/fungible-token) +- [API Reference — MultiToken API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/multi-token) +- [API Reference — Multisig API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/multisig) +- [API Reference — NativeShieldedToken API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/native-shielded-token) +- [API Reference — NonFungibleToken API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/non-fungible-token) +- [API Reference — Ownable API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/ownable) +- [API Reference — Security API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/security) +- [API Reference — ShieldedAccessControl API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/shielded-access-control) +- [API Reference — Utils API](https://docs.openzeppelin.com/contracts-compact/0.4.x/api/utils) +- [Previous Versions — 0.3.x — Overview](https://docs.openzeppelin.com/contracts-compact/0.3.x) +- [Previous Versions — 0.3.x — Learn — Extensibility](https://docs.openzeppelin.com/contracts-compact/0.3.x/extensibility) +- [Previous Versions — 0.3.x — Learn — ZK Circuits 101](https://docs.openzeppelin.com/contracts-compact/0.3.x/zk-circuits-101) +- [Previous Versions — 0.3.x — Modules — Access — Ownable](https://docs.openzeppelin.com/contracts-compact/0.3.x/ownable) +- [Previous Versions — 0.3.x — Modules — Access — AccessControl](https://docs.openzeppelin.com/contracts-compact/0.3.x/access-control) +- [Previous Versions — 0.3.x — Modules — Access — ShieldedAccessControl](https://docs.openzeppelin.com/contracts-compact/0.3.x/shielded-access-control) +- [Previous Versions — 0.3.x — Modules — Security](https://docs.openzeppelin.com/contracts-compact/0.3.x/security) +- [Previous Versions — 0.3.x — Modules — Tokens — FungibleToken](https://docs.openzeppelin.com/contracts-compact/0.3.x/fungible-token) +- [Previous Versions — 0.3.x — Modules — Tokens — NonFungibleToken](https://docs.openzeppelin.com/contracts-compact/0.3.x/non-fungible-token) +- [Previous Versions — 0.3.x — Modules — Tokens — MultiToken](https://docs.openzeppelin.com/contracts-compact/0.3.x/multi-token) +- [Previous Versions — 0.3.x — Modules — Tokens — NativeShieldedToken](https://docs.openzeppelin.com/contracts-compact/0.3.x/native-shielded-token) +- [Previous Versions — 0.3.x — Modules — Tokens — ConfidentialFungibleToken](https://docs.openzeppelin.com/contracts-compact/0.3.x/confidential-fungible-token) +- [Previous Versions — 0.3.x — Modules — Crypto](https://docs.openzeppelin.com/contracts-compact/0.3.x/crypto) +- [Previous Versions — 0.3.x — Modules — Multisig](https://docs.openzeppelin.com/contracts-compact/0.3.x/multisig) +- [Previous Versions — 0.3.x — Modules — Utils](https://docs.openzeppelin.com/contracts-compact/0.3.x/utils) +- [Previous Versions — 0.3.x — API Reference — AccessControl API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/access-control) +- [Previous Versions — 0.3.x — API Reference — ConfidentialFungibleToken API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/confidential-fungible-token) +- [Previous Versions — 0.3.x — API Reference — Crypto API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/crypto) +- [Previous Versions — 0.3.x — API Reference — FungibleToken API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/fungible-token) +- [Previous Versions — 0.3.x — API Reference — MultiToken API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/multi-token) +- [Previous Versions — 0.3.x — API Reference — Multisig API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/multisig) +- [Previous Versions — 0.3.x — API Reference — NativeShieldedToken API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/native-shielded-token) +- [Previous Versions — 0.3.x — API Reference — NonFungibleToken API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/non-fungible-token) +- [Previous Versions — 0.3.x — API Reference — Ownable API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/ownable) +- [Previous Versions — 0.3.x — API Reference — Security API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/security) +- [Previous Versions — 0.3.x — API Reference — ShieldedAccessControl API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/shielded-access-control) +- [Previous Versions — 0.3.x — API Reference — Utils API](https://docs.openzeppelin.com/contracts-compact/0.3.x/api/utils) +- [Previous Versions — 0.1.x — Overview](https://docs.openzeppelin.com/contracts-compact/0.1.x) +- [Previous Versions — 0.1.x — Learn — Extensibility](https://docs.openzeppelin.com/contracts-compact/0.1.x/extensibility) +- [Previous Versions — 0.1.x — Learn — ZK Circuits 101](https://docs.openzeppelin.com/contracts-compact/0.1.x/zk-circuits-101) +- [Previous Versions — 0.1.x — Modules — Access — Ownable](https://docs.openzeppelin.com/contracts-compact/0.1.x/ownable) +- [Previous Versions — 0.1.x — Modules — Access — AccessControl](https://docs.openzeppelin.com/contracts-compact/0.1.x/access-control) +- [Previous Versions — 0.1.x — Modules — Access — ShieldedAccessControl](https://docs.openzeppelin.com/contracts-compact/0.1.x/shielded-access-control) +- [Previous Versions — 0.1.x — Modules — Security](https://docs.openzeppelin.com/contracts-compact/0.1.x/security) +- [Previous Versions — 0.1.x — Modules — Tokens — FungibleToken](https://docs.openzeppelin.com/contracts-compact/0.1.x/fungible-token) +- [Previous Versions — 0.1.x — Modules — Tokens — NonFungibleToken](https://docs.openzeppelin.com/contracts-compact/0.1.x/non-fungible-token) +- [Previous Versions — 0.1.x — Modules — Tokens — MultiToken](https://docs.openzeppelin.com/contracts-compact/0.1.x/multi-token) +- [Previous Versions — 0.1.x — Modules — Utils](https://docs.openzeppelin.com/contracts-compact/0.1.x/utils) +- [Previous Versions — 0.1.x — API Reference — AccessControl API](https://docs.openzeppelin.com/contracts-compact/0.1.x/api/access-control) +- [Previous Versions — 0.1.x — API Reference — FungibleToken API](https://docs.openzeppelin.com/contracts-compact/0.1.x/api/fungible-token) +- [Previous Versions — 0.1.x — API Reference — MultiToken API](https://docs.openzeppelin.com/contracts-compact/0.1.x/api/multi-token) +- [Previous Versions — 0.1.x — API Reference — NonFungibleToken API](https://docs.openzeppelin.com/contracts-compact/0.1.x/api/non-fungible-token) +- [Previous Versions — 0.1.x — API Reference — Ownable API](https://docs.openzeppelin.com/contracts-compact/0.1.x/api/ownable) +- [Previous Versions — 0.1.x — API Reference — Security API](https://docs.openzeppelin.com/contracts-compact/0.1.x/api/security) +- [Previous Versions — 0.1.x — API Reference — ShieldedAccessControl API](https://docs.openzeppelin.com/contracts-compact/0.1.x/api/shielded-access-control) +- [Previous Versions — 0.1.x — API Reference — Utils API](https://docs.openzeppelin.com/contracts-compact/0.1.x/api/utils) ## Zama FHEVM diff --git a/source.config.ts b/source.config.ts index c1c81411..99374168 100644 --- a/source.config.ts +++ b/source.config.ts @@ -4,7 +4,7 @@ import rehypeKatex from "rehype-katex"; import { remarkMdxMermaid } from "fumadocs-core/mdx-plugins"; import remarkReplace from "@/lib/remark-replace"; import { CAIRO_REPLACEMENTS as cairoContractReplacements } from "content/contracts-cairo/cairo-replacements"; -import { REPLACEMENTS as compactContractReplacements } from "content/contracts-compact/utils/replacements"; +import { COMPACT_REPLACEMENTS as compactContractReplacements } from "content/contracts-compact/compact-replacements"; // You can customise Zod schemas for frontmatter and `meta.json` here // see https://fumadocs.vercel.app/docs/mdx/collections#define-docs @@ -35,7 +35,7 @@ export default defineConfig({ remarkMdxMermaid, [ remarkReplace, - [...cairoContractReplacements, compactContractReplacements], + [...cairoContractReplacements, ...compactContractReplacements], ], ], rehypePlugins: (v) => [rehypeKatex, ...v], diff --git a/src/components/banners/version-banner.tsx b/src/components/banners/version-banner.tsx index fece6529..9ea9e557 100644 --- a/src/components/banners/version-banner.tsx +++ b/src/components/banners/version-banner.tsx @@ -1,6 +1,7 @@ "use client"; import { latestStable } from "content/contracts-cairo/latest-versions"; +import { latestStable as compactLatestStable } from "content/contracts-compact/latest-versions"; import { latestStable as suiLatestStable } from "content/contracts-sui/latest-versions"; import { Callout } from "fumadocs-ui/components/callout"; import Link from "next/link"; @@ -25,6 +26,10 @@ export function VersionBanner() { version: latestStable, path: `/contracts-cairo/${latestStable}`, }, + "contracts-compact": { + version: compactLatestStable, + path: `/contracts-compact/${compactLatestStable}`, + }, "contracts-sui": { version: suiLatestStable, path: `/contracts-sui/${suiLatestStable}`, diff --git a/src/lib/export-search-indexes.ts b/src/lib/export-search-indexes.ts index 5a37b5af..b7c29ea2 100644 --- a/src/lib/export-search-indexes.ts +++ b/src/lib/export-search-indexes.ts @@ -1,8 +1,17 @@ import { latestStable as cairoLatestStable } from "content/contracts-cairo/latest-versions"; +import { latestStable as compactLatestStable } from "content/contracts-compact/latest-versions"; import type { DocumentRecord } from "fumadocs-core/search/algolia"; import type { Page } from "fumadocs-core/source"; import { source } from "@/lib/source"; +const isCompactPage = (url: string) => + url === "/contracts-compact" || url.startsWith("/contracts-compact/"); + +const isCurrentCompactPage = (url: string) => + url === "/contracts-compact" || + url === `/contracts-compact/${compactLatestStable}` || + url.startsWith(`/contracts-compact/${compactLatestStable}/`); + export async function exportSearchIndexes() { const results: DocumentRecord[] = []; @@ -18,12 +27,13 @@ export async function exportSearchIndexes() { const filteredPages = source .getPages() - .filter( - (page: Page) => - page.url.startsWith("/contracts-sui/") || - page.url === `/contracts-cairo/${cairoLatestStable}` || - page.url.startsWith(`/contracts-cairo/${cairoLatestStable}/`) || - !excludedVersions.some((excluded) => page.url.includes(excluded)), + .filter((page: Page) => + isCompactPage(page.url) + ? isCurrentCompactPage(page.url) + : page.url.startsWith("/contracts-sui/") || + page.url === `/contracts-cairo/${cairoLatestStable}` || + page.url.startsWith(`/contracts-cairo/${cairoLatestStable}/`) || + !excludedVersions.some((excluded) => page.url.includes(excluded)), ); for (const page of filteredPages) { diff --git a/src/navigation/index.ts b/src/navigation/index.ts index 59ccf948..2aa55225 100644 --- a/src/navigation/index.ts +++ b/src/navigation/index.ts @@ -3,7 +3,7 @@ import cantonData from "./canton.json"; import ethereumEvmData from "./ethereum-evm.json"; import impactData from "./impact.json"; import { mergeDeveloperLibraries } from "./merge-developer-libraries"; -import midnightData from "./midnight.json"; +import midnightData from "./midnight"; import polkadotData from "./polkadot.json"; import starknetData from "./starknet"; import stellarData from "./stellar.json"; diff --git a/src/navigation/midnight.json b/src/navigation/midnight/current.json similarity index 59% rename from src/navigation/midnight.json rename to src/navigation/midnight/current.json index b24a8fe9..fdd9c751 100644 --- a/src/navigation/midnight.json +++ b/src/navigation/midnight/current.json @@ -1,4 +1,9 @@ [ + { + "type": "page", + "name": "Getting Started", + "url": "/contracts-compact" + }, { "type": "separator", "name": "Midnight Contracts" @@ -6,7 +11,7 @@ { "type": "page", "name": "Overview", - "url": "/contracts-compact" + "url": "/contracts-compact/0.4.x" }, { "type": "folder", @@ -16,12 +21,12 @@ { "type": "page", "name": "Extensibility", - "url": "/contracts-compact/extensibility" + "url": "/contracts-compact/0.4.x/extensibility" }, { "type": "page", "name": "ZK Circuits 101", - "url": "/contracts-compact/zk-circuits-101" + "url": "/contracts-compact/0.4.x/zk-circuits-101" } ] }, @@ -36,24 +41,24 @@ { "type": "page", "name": "Ownable", - "url": "/contracts-compact/ownable" + "url": "/contracts-compact/0.4.x/ownable" }, { "type": "page", "name": "AccessControl", - "url": "/contracts-compact/access-control" + "url": "/contracts-compact/0.4.x/access-control" }, { "type": "page", "name": "ShieldedAccessControl", - "url": "/contracts-compact/shielded-access-control" + "url": "/contracts-compact/0.4.x/shielded-access-control" } ] }, { "type": "page", "name": "Security", - "url": "/contracts-compact/security" + "url": "/contracts-compact/0.4.x/security" }, { "type": "folder", @@ -62,44 +67,44 @@ { "type": "page", "name": "FungibleToken", - "url": "/contracts-compact/fungible-token" + "url": "/contracts-compact/0.4.x/fungible-token" }, { "type": "page", "name": "NonFungibleToken", - "url": "/contracts-compact/non-fungible-token" + "url": "/contracts-compact/0.4.x/non-fungible-token" }, { "type": "page", "name": "MultiToken", - "url": "/contracts-compact/multi-token" + "url": "/contracts-compact/0.4.x/multi-token" }, { "type": "page", "name": "NativeShieldedToken", - "url": "/contracts-compact/native-shielded-token" + "url": "/contracts-compact/0.4.x/native-shielded-token" }, { "type": "page", "name": "ConfidentialFungibleToken", - "url": "/contracts-compact/confidential-fungible-token" + "url": "/contracts-compact/0.4.x/confidential-fungible-token" } ] }, { "type": "page", "name": "Crypto", - "url": "/contracts-compact/crypto" + "url": "/contracts-compact/0.4.x/crypto" }, { "type": "page", "name": "Multisig", - "url": "/contracts-compact/multisig" + "url": "/contracts-compact/0.4.x/multisig" }, { "type": "page", "name": "Utils", - "url": "/contracts-compact/utils" + "url": "/contracts-compact/0.4.x/utils" } ] }, @@ -110,62 +115,62 @@ { "type": "page", "name": "AccessControl API", - "url": "/contracts-compact/api/access-control" + "url": "/contracts-compact/0.4.x/api/access-control" }, { "type": "page", "name": "ConfidentialFungibleToken API", - "url": "/contracts-compact/api/confidential-fungible-token" + "url": "/contracts-compact/0.4.x/api/confidential-fungible-token" }, { "type": "page", "name": "Crypto API", - "url": "/contracts-compact/api/crypto" + "url": "/contracts-compact/0.4.x/api/crypto" }, { "type": "page", "name": "FungibleToken API", - "url": "/contracts-compact/api/fungible-token" + "url": "/contracts-compact/0.4.x/api/fungible-token" }, { "type": "page", "name": "MultiToken API", - "url": "/contracts-compact/api/multi-token" + "url": "/contracts-compact/0.4.x/api/multi-token" }, { "type": "page", "name": "Multisig API", - "url": "/contracts-compact/api/multisig" + "url": "/contracts-compact/0.4.x/api/multisig" }, { "type": "page", "name": "NativeShieldedToken API", - "url": "/contracts-compact/api/native-shielded-token" + "url": "/contracts-compact/0.4.x/api/native-shielded-token" }, { "type": "page", "name": "NonFungibleToken API", - "url": "/contracts-compact/api/non-fungible-token" + "url": "/contracts-compact/0.4.x/api/non-fungible-token" }, { "type": "page", "name": "Ownable API", - "url": "/contracts-compact/api/ownable" + "url": "/contracts-compact/0.4.x/api/ownable" }, { "type": "page", "name": "Security API", - "url": "/contracts-compact/api/security" + "url": "/contracts-compact/0.4.x/api/security" }, { "type": "page", "name": "ShieldedAccessControl API", - "url": "/contracts-compact/api/shielded-access-control" + "url": "/contracts-compact/0.4.x/api/shielded-access-control" }, { "type": "page", "name": "Utils API", - "url": "/contracts-compact/api/utils" + "url": "/contracts-compact/0.4.x/api/utils" } ] } diff --git a/src/navigation/midnight/index.ts b/src/navigation/midnight/index.ts new file mode 100644 index 00000000..780bb298 --- /dev/null +++ b/src/navigation/midnight/index.ts @@ -0,0 +1,22 @@ +import type { NavigationNode } from "../types"; +import currentData from "./current.json"; +import zeroOneXData from "./previous-versions/0.1.x.json"; +import zeroThreeXData from "./previous-versions/0.3.x.json"; + +const current = currentData as NavigationNode[]; +const previousVersions: NavigationNode[] = [ + zeroThreeXData as NavigationNode, + zeroOneXData as NavigationNode, +]; + +const midnightNavigation: NavigationNode[] = [ + ...current, + { + type: "folder", + name: "Previous Versions", + children: previousVersions, + defaultOpen: true, + }, +]; + +export default midnightNavigation; diff --git a/src/navigation/midnight/previous-versions/0.1.x.json b/src/navigation/midnight/previous-versions/0.1.x.json new file mode 100644 index 00000000..fc2b3409 --- /dev/null +++ b/src/navigation/midnight/previous-versions/0.1.x.json @@ -0,0 +1,131 @@ +{ + "type": "folder", + "name": "0.1.x", + "children": [ + { + "type": "page", + "name": "Overview", + "url": "/contracts-compact/0.1.x" + }, + { + "type": "folder", + "name": "Learn", + "children": [ + { + "type": "page", + "name": "Extensibility", + "url": "/contracts-compact/0.1.x/extensibility" + }, + { + "type": "page", + "name": "ZK Circuits 101", + "url": "/contracts-compact/0.1.x/zk-circuits-101" + } + ] + }, + { + "type": "folder", + "name": "Modules", + "children": [ + { + "type": "folder", + "name": "Access", + "children": [ + { + "type": "page", + "name": "Ownable", + "url": "/contracts-compact/0.1.x/ownable" + }, + { + "type": "page", + "name": "AccessControl", + "url": "/contracts-compact/0.1.x/access-control" + }, + { + "type": "page", + "name": "ShieldedAccessControl", + "url": "/contracts-compact/0.1.x/shielded-access-control" + } + ] + }, + { + "type": "page", + "name": "Security", + "url": "/contracts-compact/0.1.x/security" + }, + { + "type": "folder", + "name": "Tokens", + "children": [ + { + "type": "page", + "name": "FungibleToken", + "url": "/contracts-compact/0.1.x/fungible-token" + }, + { + "type": "page", + "name": "NonFungibleToken", + "url": "/contracts-compact/0.1.x/non-fungible-token" + }, + { + "type": "page", + "name": "MultiToken", + "url": "/contracts-compact/0.1.x/multi-token" + } + ] + }, + { + "type": "page", + "name": "Utils", + "url": "/contracts-compact/0.1.x/utils" + } + ] + }, + { + "type": "folder", + "name": "API Reference", + "children": [ + { + "type": "page", + "name": "AccessControl API", + "url": "/contracts-compact/0.1.x/api/access-control" + }, + { + "type": "page", + "name": "FungibleToken API", + "url": "/contracts-compact/0.1.x/api/fungible-token" + }, + { + "type": "page", + "name": "MultiToken API", + "url": "/contracts-compact/0.1.x/api/multi-token" + }, + { + "type": "page", + "name": "NonFungibleToken API", + "url": "/contracts-compact/0.1.x/api/non-fungible-token" + }, + { + "type": "page", + "name": "Ownable API", + "url": "/contracts-compact/0.1.x/api/ownable" + }, + { + "type": "page", + "name": "Security API", + "url": "/contracts-compact/0.1.x/api/security" + }, + { + "type": "page", + "name": "ShieldedAccessControl API", + "url": "/contracts-compact/0.1.x/api/shielded-access-control" + }, + { + "type": "page", + "name": "Utils API", + "url": "/contracts-compact/0.1.x/api/utils" + } + ] + } + ] +} diff --git a/src/navigation/midnight/previous-versions/0.3.x.json b/src/navigation/midnight/previous-versions/0.3.x.json new file mode 100644 index 00000000..cc070b66 --- /dev/null +++ b/src/navigation/midnight/previous-versions/0.3.x.json @@ -0,0 +1,171 @@ +{ + "type": "folder", + "name": "0.3.x", + "children": [ + { + "type": "page", + "name": "Overview", + "url": "/contracts-compact/0.3.x" + }, + { + "type": "folder", + "name": "Learn", + "children": [ + { + "type": "page", + "name": "Extensibility", + "url": "/contracts-compact/0.3.x/extensibility" + }, + { + "type": "page", + "name": "ZK Circuits 101", + "url": "/contracts-compact/0.3.x/zk-circuits-101" + } + ] + }, + { + "type": "folder", + "name": "Modules", + "children": [ + { + "type": "folder", + "name": "Access", + "children": [ + { + "type": "page", + "name": "Ownable", + "url": "/contracts-compact/0.3.x/ownable" + }, + { + "type": "page", + "name": "AccessControl", + "url": "/contracts-compact/0.3.x/access-control" + }, + { + "type": "page", + "name": "ShieldedAccessControl", + "url": "/contracts-compact/0.3.x/shielded-access-control" + } + ] + }, + { + "type": "page", + "name": "Security", + "url": "/contracts-compact/0.3.x/security" + }, + { + "type": "folder", + "name": "Tokens", + "children": [ + { + "type": "page", + "name": "FungibleToken", + "url": "/contracts-compact/0.3.x/fungible-token" + }, + { + "type": "page", + "name": "NonFungibleToken", + "url": "/contracts-compact/0.3.x/non-fungible-token" + }, + { + "type": "page", + "name": "MultiToken", + "url": "/contracts-compact/0.3.x/multi-token" + }, + { + "type": "page", + "name": "NativeShieldedToken", + "url": "/contracts-compact/0.3.x/native-shielded-token" + }, + { + "type": "page", + "name": "ConfidentialFungibleToken", + "url": "/contracts-compact/0.3.x/confidential-fungible-token" + } + ] + }, + { + "type": "page", + "name": "Crypto", + "url": "/contracts-compact/0.3.x/crypto" + }, + { + "type": "page", + "name": "Multisig", + "url": "/contracts-compact/0.3.x/multisig" + }, + { + "type": "page", + "name": "Utils", + "url": "/contracts-compact/0.3.x/utils" + } + ] + }, + { + "type": "folder", + "name": "API Reference", + "children": [ + { + "type": "page", + "name": "AccessControl API", + "url": "/contracts-compact/0.3.x/api/access-control" + }, + { + "type": "page", + "name": "ConfidentialFungibleToken API", + "url": "/contracts-compact/0.3.x/api/confidential-fungible-token" + }, + { + "type": "page", + "name": "Crypto API", + "url": "/contracts-compact/0.3.x/api/crypto" + }, + { + "type": "page", + "name": "FungibleToken API", + "url": "/contracts-compact/0.3.x/api/fungible-token" + }, + { + "type": "page", + "name": "MultiToken API", + "url": "/contracts-compact/0.3.x/api/multi-token" + }, + { + "type": "page", + "name": "Multisig API", + "url": "/contracts-compact/0.3.x/api/multisig" + }, + { + "type": "page", + "name": "NativeShieldedToken API", + "url": "/contracts-compact/0.3.x/api/native-shielded-token" + }, + { + "type": "page", + "name": "NonFungibleToken API", + "url": "/contracts-compact/0.3.x/api/non-fungible-token" + }, + { + "type": "page", + "name": "Ownable API", + "url": "/contracts-compact/0.3.x/api/ownable" + }, + { + "type": "page", + "name": "Security API", + "url": "/contracts-compact/0.3.x/api/security" + }, + { + "type": "page", + "name": "ShieldedAccessControl API", + "url": "/contracts-compact/0.3.x/api/shielded-access-control" + }, + { + "type": "page", + "name": "Utils API", + "url": "/contracts-compact/0.3.x/api/utils" + } + ] + } + ] +} From e19ef23865acaccbf0dc19a323e2f9955b4b2d9c Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 4 Nov 2026 16:23:43 +0100 Subject: [PATCH 12/12] docs(midnight): add contracts-compact 0.4.x Copy of 0.3.x reconciled page by page against the exports of every module at release/v0.4.0 (7c3ce3ee): new Ecdsa, Eip712, EvmAbi and Secp256k1EcdsaMultisig modules, presets rewritten as modules with the examples as the deployable contracts, JubjubScalar-typed ElGamal and EcdhMask scalars, ProposalManager expiry, and every constraint figure re-sourced from the 0.34.0 compiler. --- .../0.4.x/api/access-control.mdx | 22 +- .../0.4.x/api/confidential-fungible-token.mdx | 117 +- .../contracts-compact/0.4.x/api/crypto.mdx | 183 ++- .../0.4.x/api/fungible-token.mdx | 51 +- .../0.4.x/api/multi-token.mdx | 26 +- .../contracts-compact/0.4.x/api/multisig.mdx | 1016 +++++++++++++++-- .../0.4.x/api/native-shielded-token.mdx | 128 ++- .../0.4.x/api/non-fungible-token.mdx | 55 +- .../contracts-compact/0.4.x/api/ownable.mdx | 35 +- .../contracts-compact/0.4.x/api/security.mdx | 37 +- .../0.4.x/api/shielded-access-control.mdx | 24 +- content/contracts-compact/0.4.x/api/utils.mdx | 91 +- .../0.4.x/confidential-fungible-token.mdx | 24 +- content/contracts-compact/0.4.x/crypto.mdx | 171 ++- .../0.4.x/fungible-token.mdx | 2 +- content/contracts-compact/0.4.x/index.mdx | 12 + content/contracts-compact/0.4.x/multisig.mdx | 354 +++++- .../0.4.x/native-shielded-token.mdx | 15 +- .../0.4.x/non-fungible-token.mdx | 2 +- content/contracts-compact/0.4.x/utils.mdx | 33 + 20 files changed, 2040 insertions(+), 358 deletions(-) diff --git a/content/contracts-compact/0.4.x/api/access-control.mdx b/content/contracts-compact/0.4.x/api/access-control.mdx index fada507f..c7fc04d6 100644 --- a/content/contracts-compact/0.4.x/api/access-control.mdx +++ b/content/contracts-compact/0.4.x/api/access-control.mdx @@ -119,7 +119,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Returns `true` if `account` has been granted `roleId`. @@ -130,7 +130,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Reverts if the caller cannot prove ownership of `roleId`. @@ -147,7 +147,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Reverts if `account` is missing `roleId`. @@ -162,7 +162,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Returns the admin role that controls `roleId` or @@ -176,7 +176,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Grants `roleId` to `account`. @@ -192,7 +192,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Revokes `roleId` from `account`. @@ -208,7 +208,7 @@ The TypeScript witnesses in the repository are test-only and are not published w circuitSig="renounceRole(roleId: Bytes<32>, callerConfirmation: Either, ContractAddress>) → []" stackParams="true" kind="circuit" - complexity="k=13, rows=3322" + complexity="k=13, rows=3313" modulePrefix="AccessControl" > Revokes `roleId` from the calling account. @@ -234,7 +234,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Sets `adminRole` as `roleId`'s admin role. @@ -245,7 +245,7 @@ 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. @@ -261,7 +261,7 @@ 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. @@ -278,7 +278,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Attempts to revoke `roleId` from `account` and returns a boolean indicating if `roleId` was revoked. diff --git a/content/contracts-compact/0.4.x/api/confidential-fungible-token.mdx b/content/contracts-compact/0.4.x/api/confidential-fungible-token.mdx index 3b9e0cba..10ecc5d2 100644 --- a/content/contracts-compact/0.4.x/api/confidential-fungible-token.mdx +++ b/content/contracts-compact/0.4.x/api/confidential-fungible-token.mdx @@ -51,12 +51,11 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl kind="ledger" id="ConfidentialFungibleToken-_balances" > - 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](#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](#ConfidentialFungibleToken-balanceOf) (spendable) + [pendingOf](#ConfidentialFungibleToken-pendingOf) (pending). + Spendable (confirmed) balances, encrypted under each account's + ElGamal key. Debits draw from here. Paired with `_pending`: only the owner + moves pending into spendable, via [sweep](#ConfidentialFungibleToken-sweep), so a third party spamming dust + credits cannot churn a victim's spendable ciphertext and invalidate their + in-flight spend proofs. A wallet's total is [balanceOf](#ConfidentialFungibleToken-balanceOf) + [pendingOf](#ConfidentialFungibleToken-pendingOf). #### _pending [toc] [#ConfidentialFungibleToken-_pending] @@ -66,8 +65,9 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl kind="ledger" id="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). + Incoming (unconfirmed) balances, encrypted under each account's + ElGamal key. Credits land here rather than in `_balances`; the owner moves + them across with [sweep](#ConfidentialFungibleToken-sweep). #### _encryptionKeys [toc] [#ConfidentialFungibleToken-_encryptionKeys] @@ -79,6 +79,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl > 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. + Registration is one-shot: a second registration for the same account is + rejected. #### _memos [toc] [#ConfidentialFungibleToken-_memos] @@ -89,7 +91,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl id="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. + encrypted to the recipient's key, to the front of the list, so the newest memo comes first. + Memos record credits only; debits leave none. #### _escrow [toc] [#ConfidentialFungibleToken-_escrow] @@ -100,6 +103,10 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl id="ConfidentialFungibleToken-_escrow" > Mapping from owner accounts to spender accounts to their [EscrowEntry](#ConfidentialFungibleToken-EscrowEntry). + Membership is checked independently of the entry, so an allowance that was + never escrowed is unspendable. A spend also requires both owner and spender + to be registered, so an entry written here for an unregistered account (for + example by a composing contract) is unspendable as well. #### _escrowSpendEpochs [toc] [#ConfidentialFungibleToken-_escrowSpendEpochs] @@ -133,7 +140,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl 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. + account's lifetime credit count is 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 @@ -148,7 +155,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl kind="ledger" id="ConfidentialFungibleToken-_isInitialized" > - Boolean indicating if initialized. + Whether [initialize](#ConfidentialFungibleToken-initialize) has run. Guards against re-initialization + and against use before initialization. #### _name [toc] [#ConfidentialFungibleToken-_name] @@ -158,7 +166,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl kind="sealed ledger" id="ConfidentialFungibleToken-_name" > - The immutable token name. + The token's human-readable name, set once at construction. #### _symbol [toc] [#ConfidentialFungibleToken-_symbol] @@ -168,7 +176,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl kind="sealed ledger" id="ConfidentialFungibleToken-_symbol" > - The immutable token symbol. + The token's ticker symbol, set once at construction. #### _decimals [toc] [#ConfidentialFungibleToken-_decimals] @@ -178,7 +186,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibl kind="sealed ledger" id="ConfidentialFungibleToken-_decimals" > - The immutable token decimals. + The number of decimals used to display balances, set once at + construction. Purely presentational; no circuit scales by it. ### Witnesses [toc] [#ConfidentialFungibleToken-Witnesses] @@ -300,7 +309,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Returns `account`'s balance ciphertext, or the default (zero) @@ -317,7 +326,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Returns `account`'s PENDING balance ciphertext (incoming credits @@ -335,22 +344,30 @@ The TypeScript witnesses in the repository are test-only and are not published; 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. + granted `spender`. When no allowance exists, returns a placeholder entry + 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), + 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](#ConfidentialFungibleToken-transferFrom)/`burnFrom`) gates on - `_escrow` membership independently, so a non-existent allowance is unspendable. + The placeholder's `spenderCt` and `ownerCt` are genuine encryptions + of 0. Its `ownerMemo` is not: it is a sentinel for an absent allowance, + recognised by `ephemeralPk == identity`, which `EcdhMask_encrypt` never + produces and `EcdhMask_decrypt` rejects. A wallet must test for the + sentinel rather than decrypt and narrow it. + + + + "No allowance" and "an allowance of 0" both read as encrypted-zero in + the amount fields; the memo sentinel tells them apart, since [approve](#ConfidentialFungibleToken-approve) stores + a real memo even for 0. This getter grants nothing, the spend path + ([transferFrom](#ConfidentialFungibleToken-transferFrom)/`burnFrom`) gates on `_escrow` membership independently, so a + non-existent allowance is unspendable. @@ -359,7 +376,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Returns whether `account` has registered an encryption key. @@ -370,7 +387,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Registers the caller: derives their encryption public key from @@ -394,7 +411,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Mints `value` to `account`: `_credit` under its intent name. @@ -415,7 +432,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Burns `value` from the caller's own balance: `_debit` under @@ -436,7 +453,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Transfers `value` from the caller to `to`: debits the caller, @@ -462,7 +479,7 @@ The TypeScript witnesses in the repository are test-only and are not published; The conserving value-movement primitive: debits the caller and @@ -499,7 +516,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Sweeps the caller's incoming `pending` pool into their @@ -529,7 +546,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Sets `spender`'s allowance over the caller's balance to `value` @@ -538,6 +555,13 @@ 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. + The allowance is funded at approve time, and funding is required: the + spender holds no owner encryption key, so at spend time it can only prove + against its own escrow copy, never that the owner's balance covers the + allowance. An approve conserves the owner's holdings (spendable plus every + outstanding escrow), so allowances across spenders can sum to the whole + holding but never exceed it. + Returns the caller's (approver's) `accountId` for caller-side gating. @@ -566,7 +590,7 @@ The TypeScript witnesses in the repository are test-only and are not published; value: Uint<128> ) → Bytes<32>" kind="circuit" - complexity="k=16, rows=72346" + complexity="k=17, rows=68924" modulePrefix="ConfidentialFungibleToken" stackParams="true" > @@ -584,6 +608,7 @@ The TypeScript witnesses in the repository are test-only and are not published; * `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`. + * `fromAddress` and the caller are registered. #### clearMemos [toc] [#ConfidentialFungibleToken-clearMemos] @@ -591,7 +616,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Clears the caller's memo list, letting a wallet that has folded @@ -646,7 +671,7 @@ The TypeScript witnesses in the repository are test-only and are not published; Burns `value` from `fromAddress` by consuming an escrow that @@ -659,6 +684,7 @@ The TypeScript witnesses in the repository are test-only and are not published; * 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`. + * `fromAddress` and the caller are registered. --- @@ -690,8 +716,13 @@ with the matching token operation. kind="ledger" id="ConfidentialFungibleTokenPublicSupply-_totalSupply" > - The public circulating supply. Mint and burn disclose their amounts - through the delta on this value. + Total tokens in circulation, tracked in the clear even though + balances are encrypted. Raised by [_addSupply](#ConfidentialFungibleTokenPublicSupply-_addSupply) and lowered by + [_subSupply](#ConfidentialFungibleTokenPublicSupply-_subSupply), each rejecting over/underflow. + + + Mint and burn disclose their amounts through the delta on this value. + ### Witnesses [toc] [#ConfidentialFungibleTokenPublicSupply-Witnesses] @@ -707,7 +738,7 @@ None. Increases [totalSupply](#ConfidentialFungibleTokenPublicSupply-totalSupply) by `value`. Call alongside the token @@ -723,7 +754,7 @@ None. Decreases [totalSupply](#ConfidentialFungibleTokenPublicSupply-totalSupply) by `value`. Call alongside the token @@ -740,7 +771,7 @@ None. Returns the public total token supply. diff --git a/content/contracts-compact/0.4.x/api/crypto.mdx b/content/contracts-compact/0.4.x/api/crypto.mdx index fd5c6c06..cf68c9b1 100644 --- a/content/contracts-compact/0.4.x/api/crypto.mdx +++ b/content/contracts-compact/0.4.x/api/crypto.mdx @@ -12,6 +12,8 @@ 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. +- [Ecdsa](#Ecdsa): the low-s rule for secp256k1 ECDSA signatures. +- [Eip712](#Eip712): the EIP-712 typed-data envelope, for verifying signatures produced by Ethereum signing infrastructure. ## ElGamal [toc] [#ElGamal] @@ -97,7 +102,7 @@ None. #### expandRandomness [toc] [#ElGamal-expandRandomness] #### [!toc] [#ElGamal-expandRandomness] @@ -132,7 +137,7 @@ None. circuitSig='encryptPoint( pk: JubjubPoint, m: JubjubPoint, - r: Field + r: JubjubScalar ) → Ciphertext' kind="pure" modulePrefix="ElGamal" @@ -165,7 +170,7 @@ None. circuitSig='encrypt( pk: JubjubPoint, value: Uint<128>, - r: Field + r: JubjubScalar ) → Ciphertext' kind="pure" modulePrefix="ElGamal" @@ -173,8 +178,8 @@ 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](#ElGamal-expandRandomness) produces `Field` for the same reason. + as a scalar directly to avoid a redundant conversion at every call site; + [expandRandomness](#ElGamal-expandRandomness) produces one for the same reason. Requirements: @@ -234,7 +239,7 @@ None. #### scalarMul [toc] [#ElGamal-scalarMul] #### [!toc] [#ElGamal-scalarMul] @@ -263,7 +268,7 @@ None. old: Ciphertext, pk: JubjubPoint, value: Uint<128>, - r: Field + r: JubjubScalar ) → Ciphertext' kind="pure" modulePrefix="ElGamal" @@ -287,7 +292,7 @@ None. old: Ciphertext, pk: JubjubPoint, value: Uint<128>, - r: Field + r: JubjubScalar ) → Ciphertext' kind="pure" modulePrefix="ElGamal" @@ -315,7 +320,7 @@ None. circuitSig='rerandomize( ct: Ciphertext, pk: JubjubPoint, - r: Field + r: JubjubScalar ) → Ciphertext' kind="pure" modulePrefix="ElGamal" @@ -487,7 +492,7 @@ None. circuitSig='encrypt( recipientPk: JubjubPoint, value: Uint<128>, - e: Field, + e: JubjubScalar, domain: Bytes<32> ) → Ciphertext' kind="pure" @@ -511,7 +516,7 @@ None. Recovers the value from a ciphertext using the recipient's secret scalar. Pure and off-chain (no proof needed to read a received value). + + Requirements: + + * `ciphertext.ephemeralPk` is not the identity point. + + +--- + +## Ecdsa [toc] [#Ecdsa] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/crypto/Ecdsa"; +``` + +`secp256k1EcdsaVerify` accepts both `(r, s)` and `(r, n - s)`, where `n` is the secp256k1 group order. +This module rejects `s > n / 2`, so each signature has one encoding. +Requires ZKIR v3: the `Secp256k1` types are unavailable on the v2 backend. + +--- + +### Ledger [toc] [#Ecdsa-Ledger] +### Ledger [!toc] [#Ecdsa-Ledger] + +None. + +### Witnesses [toc] [#Ecdsa-Witnesses] +### Witnesses [!toc] [#Ecdsa-Witnesses] + +None. + +### Circuits [toc] [#Ecdsa-Circuits] +### Circuits [!toc] [#Ecdsa-Circuits] + +#### isLowS [toc] [#Ecdsa-isLowS] +#### [!toc] [#Ecdsa-isLowS] + + Returns whether `s` is canonical, i.e. `s <= n / 2` where `n` + is the secp256k1 group order. + + +#### assertLowS [toc] [#Ecdsa-assertLowS] +#### [!toc] [#Ecdsa-assertLowS] + + Rejects a signature in the high half. + + Requirements: + + * `sig.s` must satisfy `s <= n / 2`. + + +#### secp256k1EcdsaVerifyLowS [toc] [#Ecdsa-secp256k1EcdsaVerifyLowS] +#### [!toc] [#Ecdsa-secp256k1EcdsaVerifyLowS] + + Verifies `sig` over `msgHash` under `pk`, accepting only the + low-s encoding. Reports the result instead of asserting, so a caller can + fold it into a count or a threshold. + + +--- + +## Eip712 [toc] [#Eip712] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/crypto/Eip712"; +``` + +The domain is `(name, version, salt)`; `chainId` and `verifyingContract` are omitted. +Requires ZKIR v3: `keccak256` is unavailable on the v2 backend. + +--- + +### Ledger [toc] [#Eip712-Ledger] +### Ledger [!toc] [#Eip712-Ledger] + +None. + +### Witnesses [toc] [#Eip712-Witnesses] +### Witnesses [!toc] [#Eip712-Witnesses] + +None. + +### Circuits [toc] [#Eip712-Circuits] +### Circuits [!toc] [#Eip712-Circuits] + +#### domainSeparator [toc] [#Eip712-domainSeparator] +#### [!toc] [#Eip712-domainSeparator] + + Computes the domain separator for `(name, version, salt)`. + + EIP-712 encodes a `string` field as the keccak hash of its UTF-8 bytes, + so the name and version arrive already hashed: `hashedName` is `keccak256(bytes(name))` + and `hashedVersion` is `keccak256(bytes(version))`. Compact has no dynamic types, + so a consumer hashes its own fixed-length literals. `salt` is a per-deployment, + per-network random value. + + +#### hashTypedData [toc] [#Eip712-hashTypedData] +#### [!toc] [#Eip712-hashTypedData] + + Wraps a struct hash in the typed-data envelope `keccak256(0x1901 || separator || structHash)`, + yielding the digest an EVM signer actually signs. `structHash` is + `keccak256(typeHash || the operation's ABI words)`. + + + A zero separator is what an uninitialized ledger cell reads as, and a digest + built from one still verifies signatures, so this circuit fails closed on it. + + + Requirements: + + * `separator` must be non-zero, which an initialized domain always is. diff --git a/content/contracts-compact/0.4.x/api/fungible-token.mdx b/content/contracts-compact/0.4.x/api/fungible-token.mdx index 598a12f2..f3fcc3cd 100644 --- a/content/contracts-compact/0.4.x/api/fungible-token.mdx +++ b/content/contracts-compact/0.4.x/api/fungible-token.mdx @@ -62,7 +62,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; kind="ledger" id="FungibleToken-_totalSupply" > - The total token supply. + Total tokens in circulation. `_update` raises it on a mint and + lowers it on a burn, rejecting an increase that would overflow `Uint<128>`. #### _name [toc] [#FungibleToken-_name] @@ -72,7 +73,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; kind="sealed ledger" id="FungibleToken-_name" > - The immutable token name. + The token's human-readable name, set once at construction. #### _symbol [toc] [#FungibleToken-_symbol] @@ -82,7 +83,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; kind="sealed ledger" id="FungibleToken-_symbol" > - The immutable token symbol. + The token's ticker symbol, set once at construction. #### _decimals [toc] [#FungibleToken-_decimals] @@ -92,7 +93,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; kind="sealed ledger" id="FungibleToken-_decimals" > - The immutable token decimals. + The number of decimals used to display balances, set once at + construction. Purely presentational; no circuit scales by it. ### Witnesses [toc] [#FungibleToken-Witnesses] @@ -123,13 +125,16 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; decimals_: Uint<8> ) → []' kind="circuit" - complexity="k=10, rows=71" modulePrefix="FungibleToken" 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. + + Requirements: + + * Contract is not initialized. #### name [toc] [#FungibleToken-name] @@ -137,7 +142,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Returns the token name. @@ -152,7 +157,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Returns the symbol of the token. @@ -167,7 +172,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Returns the number of decimals used to get its user representation. @@ -182,7 +187,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Returns the value of tokens in existence. @@ -197,7 +202,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Returns the value of tokens owned by `account`. @@ -214,7 +219,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Moves a `value` amount of tokens from the caller's account to `to`. @@ -238,7 +243,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Unsafe variant of [transfer](#FungibleToken-transfer) which allows transfers to contract addresses. @@ -261,7 +266,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; @@ -280,7 +285,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Sets a `value` amount of tokens as allowance of `spender` over the caller's tokens. @@ -300,7 +305,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; value: Uint<128> ) → Boolean" kind="circuit" - complexity="k=13, rows=4960" + complexity="k=13, rows=4906" stackParams="true" modulePrefix="FungibleToken" > @@ -332,7 +337,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; value: Uint<128> ) → Boolean" kind="circuit" - complexity="k=13, rows=4957" + complexity="k=13, rows=4903" stackParams="true" modulePrefix="FungibleToken" > @@ -362,7 +367,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; value: Uint<128> ) → []" kind="circuit" - complexity="k=12, rows=2345" + complexity="k=12, rows=2125" stackParams="true" modulePrefix="FungibleToken" > @@ -394,7 +399,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; value: Uint<128> ) → []" kind="circuit" - complexity="k=12, rows=2342" + complexity="k=12, rows=2122" modulePrefix="FungibleToken" stackParams="true" > @@ -418,7 +423,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Creates a `value` amount of tokens and assigns them to `account`, @@ -442,7 +447,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Unsafe variant of [_mint](#FungibleToken-_mint) which allows transfers to contract addresses. @@ -464,7 +469,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; Destroys a `value` amount of tokens from `account`, lowering the total supply. @@ -486,7 +491,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; value: Uint<128> ) → []" kind="circuit" - complexity="k=11, rows=1406" + complexity="k=11, rows=1407" modulePrefix="FungibleToken" stackParams="true" > @@ -510,7 +515,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/FungibleToken"; value: Uint<128> ) → []" kind="circuit" - complexity="k=11, rows=1729" + complexity="k=11, rows=1935" modulePrefix="FungibleToken" stackParams="true" > diff --git a/content/contracts-compact/0.4.x/api/multi-token.mdx b/content/contracts-compact/0.4.x/api/multi-token.mdx index cd94a071..e7e54cb8 100644 --- a/content/contracts-compact/0.4.x/api/multi-token.mdx +++ b/content/contracts-compact/0.4.x/api/multi-token.mdx @@ -90,7 +90,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Initializes the contract by setting the base URI for all tokens. @@ -98,6 +97,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Requirements: * Must be called in the contract's constructor. + * Contract is not initialized. #### uri [toc] [#MultiToken-uri] @@ -127,7 +127,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; id: Uint<128> ) → Uint<128>" kind="circuit" - complexity="k=10, rows=875" + complexity="k=10, rows=867" modulePrefix="MultiToken" stackParams="true" > @@ -146,7 +146,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; approved: Boolean ) → []" kind="circuit" - complexity="k=13, rows=2944" + complexity="k=13, rows=2938" stackParams="true" modulePrefix="MultiToken" > @@ -169,7 +169,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; operator: Either, ContractAddress> ) → Boolean" kind="circuit" - complexity="k=11, rows=1343" + complexity="k=11, rows=1347" stackParams="true" modulePrefix="MultiToken" > @@ -189,7 +189,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; id: Uint<128>, value: Uint<128> ) → []" kind="circuit" - complexity="k=13, rows=4926" + complexity="k=13, rows=4833" stackParams="true" modulePrefix="MultiToken" > @@ -225,7 +225,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; value: Uint<128> ) → []" kind="circuit" - complexity="k=12, rows=2450" + complexity="k=12, rows=2381" stackParams="true" modulePrefix="MultiToken" > @@ -258,7 +258,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; value: Uint<128> ) → []" kind="circuit" - complexity="k=13, rows=4925" + complexity="k=13, rows=4832" stackParams="true" modulePrefix="MultiToken" > @@ -293,7 +293,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; value: Uint<128> ) → []" kind="circuit" - complexity="k=12, rows=2449" + complexity="k=12, rows=2380" stackParams="true" modulePrefix="MultiToken" > @@ -320,7 +320,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; Sets a new URI for all token types, by relying on the token type ID @@ -348,7 +348,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; @@ -376,7 +376,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; value: Uint<128> ) → []" kind="circuit" - complexity="k=11, rows=1530" + complexity="k=11, rows=1448" stackParams="true" modulePrefix="MultiToken" > @@ -403,7 +403,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; value: Uint<128> ) → []" kind="circuit" - complexity="k=11, rows=1222" + complexity="k=11, rows=1275" stackParams="true" modulePrefix="MultiToken" > @@ -425,7 +425,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/MultiToken"; approved: Boolean ) → []" kind="circuit" - complexity="k=11, rows=1279" + complexity="k=11, rows=1280" stackParams="true" modulePrefix="MultiToken" > diff --git a/content/contracts-compact/0.4.x/api/multisig.mdx b/content/contracts-compact/0.4.x/api/multisig.mdx index d6caf43f..54be9394 100644 --- a/content/contracts-compact/0.4.x/api/multisig.mdx +++ b/content/contracts-compact/0.4.x/api/multisig.mdx @@ -11,6 +11,7 @@ This page provides the full API for the Multisig modules. 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. +- [Secp256k1EcdsaMultisig](#Secp256k1EcdsaMultisig): a `t`-of-`n` signer set of secp256k1 key commitments that verifies `t` ECDSA approvals over a digest. - [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. @@ -18,6 +19,8 @@ The page covers the following modules: - [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. +- [ShieldedMultiSigV2](#ShieldedMultiSigV2): preset; a stateless shielded wallet whose spends are authorized by `t` EIP-712 ECDSA signatures. +- [NativeShieldedTokenIssuer](#NativeShieldedTokenIssuer): preset; a native shielded token minted and burned only with `t` EIP-712 ECDSA signatures. ## Signer [toc] [#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. +It has no `initialize`: the consuming contract registers signers with [_addSigner](#Signer-_addSigner) and sets the threshold with [_changeThreshold](#Signer-_changeThreshold) or [_setThreshold](#Signer-_setThreshold). --- @@ -67,18 +71,6 @@ The module never resolves the caller; the consuming contract passes an identity The minimum number of approvals required. Zero until it is set. -#### _isInitialized [toc] [#Signer-_isInitialized] -#### [!toc] [#Signer-_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. - - ### Witnesses [toc] [#Signer-Witnesses] ### Witnesses [!toc] [#Signer-Witnesses] @@ -87,25 +79,6 @@ None. ### Circuits [toc] [#Signer-Circuits] ### Circuits [!toc] [#Signer-Circuits] -#### initialize [toc] [#Signer-initialize] -#### [!toc] [#Signer-initialize] - - 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: - - * 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] #### [!toc] [#Signer-assertSigner] Asserts that the given approval count meets the threshold. @@ -155,7 +128,7 @@ None. Returns the current signer count. Returns zero when no @@ -167,7 +140,7 @@ None. Returns the approval threshold. Returns zero when the @@ -230,7 +203,7 @@ None. Updates the approval threshold. @@ -252,7 +225,7 @@ None. Sets the approval threshold without checking @@ -285,6 +258,191 @@ None. --- +## Secp256k1EcdsaMultisig [toc] [#Secp256k1EcdsaMultisig] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/Secp256k1EcdsaMultisig"<2>; +``` + +`Secp256k1EcdsaMultisig<#t>` takes the threshold `t` as its import generic; `initialize` fixes the signer count `n`. +It wraps a `Signer>` registry, requires ZKIR v3, and must be imported at one width per contract and never alongside `Signer`. + +--- + +### Ledger [toc] [#Secp256k1EcdsaMultisig-Ledger] +### Ledger [!toc] [#Secp256k1EcdsaMultisig-Ledger] + +#### _signers [toc] [#Secp256k1EcdsaMultisig-_signers] +#### [!toc] [#Secp256k1EcdsaMultisig-_signers] + + The registered signer commitments. Re-exported from `Signer`. + + +#### _signerCount [toc] [#Secp256k1EcdsaMultisig-_signerCount] +#### [!toc] [#Secp256k1EcdsaMultisig-_signerCount] + + The number of registered signers, `n`. Re-exported from `Signer`. + + +#### _threshold [toc] [#Secp256k1EcdsaMultisig-_threshold] +#### [!toc] [#Secp256k1EcdsaMultisig-_threshold] + + The approval threshold, `t` once initialized. Re-exported from `Signer`. + + +#### _instanceSalt [toc] [#Secp256k1EcdsaMultisig-_instanceSalt] +#### [!toc] [#Secp256k1EcdsaMultisig-_instanceSalt] + + Deployment-supplied salt for signer commitments. Set at + initialization and never changed. The registered commitments must have been + derived under this same salt. + + +### Witnesses [toc] [#Secp256k1EcdsaMultisig-Witnesses] +### Witnesses [!toc] [#Secp256k1EcdsaMultisig-Witnesses] + +None. + +### Circuits [toc] [#Secp256k1EcdsaMultisig-Circuits] +### Circuits [!toc] [#Secp256k1EcdsaMultisig-Circuits] + +#### initialize [toc] [#Secp256k1EcdsaMultisig-initialize] +#### [!toc] [#Secp256k1EcdsaMultisig-initialize] + + Stores the instance salt and registers the signer set at threshold `t`. + It writes the `sealed` salt, so it MUST only be called from the consuming contract's constructor. + The set is final: signers must be known at deploy, and no circuit adds, removes, or replaces one afterwards. + + Requirements: + + * Can only be called once. + * `instanceSalt` should be cryptographically random. + * `signerCommitments` must not contain duplicates. + * `n` must be at least `t`. + + +#### assertApprovals [toc] [#Secp256k1EcdsaMultisig-assertApprovals] +#### [!toc] [#Secp256k1EcdsaMultisig-assertApprovals] + + Verifies `t` approvals over `msgHash`: distinct registered signers, + each with a valid low-s signature. + `pubkeys` and `signatures` are parallel: entry `i` of each belongs to the same signer. + + The threshold check runs first, so an uninitialized registry fails before any signature work. + Then all `t(t-1)/2` pairs of commitments are compared, and each entry's commitment and signature are checked. + Verification goes through [secp256k1EcdsaVerifyLowS](./crypto#Ecdsa), so a high-s encoding is an invalid signature. + The commitments of the `t` approvers are disclosed, so which signers approved is public; keys and signatures are not. + + Cost (ZKIR v3): `t = 1` is k=16, rows=40538; `t = 2` is k=17, rows=64908; `t = 3` is k=17, rows=94995. + + + This circuit verifies signatures over whatever `msgHash` it is handed. + Replay, domain, and instance binding belong in the consumer's digest. + + + Requirements: + + * The module must be initialized. + * No two entries may be the same signer. + * Every public key must hash to a registered signer commitment. + * Every signature must be valid over `msgHash` and use the low-s encoding. + + +#### calculateSignerId [toc] [#Secp256k1EcdsaMultisig-calculateSignerId] +#### [!toc] [#Secp256k1EcdsaMultisig-calculateSignerId] + + Computes a signer commitment from a secp256k1 public key, + as `persistentHash` of the affine coordinates `pkX` and `pkY`, the `salt`, and the domain `pad(32, "Secp256k1EcdsaMultisig:signer")`. + Pure, so the deployer can derive constructor commitments off-chain + the same way the circuits derive them in-circuit. + + + The hash is over the Compact encoding of an internal struct, not a byte concatenation. + Do not reimplement it: call this circuit. + A wrong derivation registers commitments no key can satisfy. + + + Requirements: + + * `pk` must not be the secp256k1 identity point, which has no coordinates. + + +#### getSignerCount [toc] [#Secp256k1EcdsaMultisig-getSignerCount] +#### [!toc] [#Secp256k1EcdsaMultisig-getSignerCount] + + Returns the number of registered signers. + + +#### getThreshold [toc] [#Secp256k1EcdsaMultisig-getThreshold] +#### [!toc] [#Secp256k1EcdsaMultisig-getThreshold] + + Returns the approval threshold. + + +#### isSigner [toc] [#Secp256k1EcdsaMultisig-isSigner] +#### [!toc] [#Secp256k1EcdsaMultisig-isSigner] + + Returns whether the given commitment is a registered signer. + + +--- + ## ProposalManager [toc] [#ProposalManager] - 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, so a stored proposal can never read - `Inactive`. + Proposal lifecycle states. `Proposal.status` stores `Active`, `Executed`, or `Cancelled`; + [getProposalStatus](#ProposalManager-getProposalStatus) derives the other two. + + * `Inactive`: no proposal with that id exists. Never stored. + * `Active`: created and not retired. Executable only while the block time is below `Proposal.expiry`. + * `Executed`: [_markExecuted](#ProposalManager-_markExecuted) ran. Terminal. + * `Cancelled`: [_cancelProposal](#ProposalManager-_cancelProposal) ran. Terminal. + * `Expired`: stored `Active` with an `expiry` at or before the current block time. Never stored. #### RecipientKind [toc] [#ProposalManager-RecipientKind] @@ -338,11 +500,19 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ProposalManager" #### Proposal [toc] [#ProposalManager-Proposal] #### [!toc] [#ProposalManager-Proposal] - A transfer proposal: recipient, token color, amount, and lifecycle status. + A transfer proposal: recipient, token color, amount, deadline, and stored lifecycle stage. + `expiry` is in seconds since the Unix epoch. Reaching a terminal stage changes `status` only, + so an executed or cancelled proposal keeps the deadline it was created with. + + + `expiry` marks when a proposal STOPS being executable, not when it starts: + an `Active` proposal is executable while `blockTime < expiry` and inert once `blockTime >= expiry`. + The module implements no execution delay; a consumer that needs one must compose it. + ### Ledger [toc] [#ProposalManager-Ledger] @@ -365,7 +535,9 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ProposalManager" kind="ledger" id="ProposalManager-_proposals" > - Mapping from proposal id to proposal. Proposals are public: every recipient address, color, amount, and status is readable off-chain. + Mapping from proposal id to proposal. Proposals are public: every recipient address, color, amount, expiry, and status is readable off-chain. + An off-chain reader takes `status` as stored and, when it is `Active`, compares `expiry` with the current block time. + Entries are never deleted. ### Witnesses [toc] [#ProposalManager-Witnesses] @@ -455,14 +627,23 @@ None. - Asserts that a proposal exists and is active. + Asserts that a proposal exists and is still executable. + A `status` other than `Active` and a deadline that has passed fail with different errors. + + + Expiry is terminal. A transaction rejected for a stale read succeeds once rebuilt against fresh state, + but block time only moves forward, so an expired proposal stays expired. + Check the proposal's status before re-proving. + Requirements: * Proposal must exist. - * Proposal status must be Active. + * Proposal `status` must be `Active`. + * Proposal must not have expired. #### _createProposal [toc] [#ProposalManager-_createProposal] @@ -471,13 +652,16 @@ None. circuitSig='_createProposal( to: Recipient, color: Bytes<32>, - amount: Uint<128> + amount: Uint<128>, + expiry: Uint<64> ) → Uint<64>' kind="circuit" + complexity="k=10, rows=883" modulePrefix="ProposalManager" stackParams="true" > - Creates a new proposal. + Creates a new proposal that is executable until `expiry`, + in seconds since the Unix epoch. The recipient is NOT validated beyond being well-typed. Two @@ -486,7 +670,8 @@ 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](#ProposalManager-toShieldedRecipient) at execution. It stays `Active` and - unexecutable on that rail until [_cancelProposal](#ProposalManager-_cancelProposal) retires it. + unexecutable on that rail until [_cancelProposal](#ProposalManager-_cancelProposal) retires it or its + `expiry` lapses. * A zero or otherwise unspendable `address` is accepted, and funds sent to it on execution are unrecoverable. @@ -496,6 +681,12 @@ None. making this a pinned read-modify-write. + + `expiry` is only checked against the present: a proposal cannot be born expired. + There is deliberately no maximum horizon, so a caller may pass an effectively unreachable expiry. + Consuming contracts that need a bounded lifetime must impose their own cap. + + Access control is NOT enforced here. The consuming contract must gate this behind its own @@ -505,6 +696,7 @@ None. Requirements: * `amount` must be greater than 0. + * `expiry` must be in the future. #### _cancelProposal [toc] [#ProposalManager-_cancelProposal] @@ -514,7 +706,7 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Cancels a proposal. + Cancels a proposal, retiring it before its expiry. Access control is NOT enforced here. @@ -524,7 +716,7 @@ None. Requirements: - * Proposal must be active. + * Proposal must be active and not expired. #### _markExecuted [toc] [#ProposalManager-_markExecuted] @@ -544,7 +736,7 @@ None. Requirements: - * Proposal must be active. + * Proposal must be active and not expired. #### getProposal [toc] [#ProposalManager-getProposal] @@ -554,7 +746,9 @@ None. kind="circuit" modulePrefix="ProposalManager" > - Returns the full proposal data. + Returns the full proposal data. The returned `status` is the stored stage, + so an `Active` entry may already be past its `expiry`. + Prefer [getProposalStatus](#ProposalManager-getProposalStatus) when the effective status is what is wanted. #### getProposalRecipient [toc] [#ProposalManager-getProposalRecipient] @@ -592,9 +786,12 @@ None. - Returns the status of a proposal. + Returns the effective status of a proposal: the stored `status` unless it is `Active`, + in which case `expiry` and the current block time decide between `Active` and `Expired`. + Returns `Inactive` for an id that was never created rather than failing. --- @@ -621,7 +818,8 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasury kind="ledger" id="ShieldedTreasury-_coins" > - One stored coin per token color. Every coin held (nonce, color, value, and Merkle index) is readable off-chain in plaintext. + One stored coin per token color. A deposit of an existing color merges into the stored coin rather than adding a second entry. + Every coin held (nonce, color, value, and Merkle index) is readable off-chain in plaintext. #### _received [toc] [#ShieldedTreasury-_received] @@ -631,7 +829,8 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasury kind="ledger" id="ShieldedTreasury-_received" > - Cumulative value received per token color. + Cumulative value received per token color. Only ever increases, so it is a running total rather than a balance; + [getTokenBalance](#ShieldedTreasury-getTokenBalance) reads `_coins` and is the canonical balance. #### _sent [toc] [#ShieldedTreasury-_sent] @@ -641,7 +840,7 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasury kind="ledger" id="ShieldedTreasury-_sent" > - Cumulative value sent per token color. + Cumulative value sent per token color. The counterpart to `_received`; also only ever increases. ### Witnesses [toc] [#ShieldedTreasury-Witnesses] @@ -715,6 +914,9 @@ None. so the sent coin's plaintext is disclosed beyond anything in public state. + A self-addressed coin is auto-received by `sendShielded` but never recorded in `_coins`, + so it would be stranded. This circuit therefore rejects sends to self. + Access control is NOT enforced here. The consuming contract must gate this behind its own @@ -723,6 +925,7 @@ None. Requirements: + * `recipient` must not be this contract's own address. * 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. @@ -792,6 +995,9 @@ None. import "./node_modules/@openzeppelin/compact-contracts/multisig/ShieldedTreasuryStateless"; ``` +Keeping coin data off the ledger hides it from the public, not from a depositor. +Whoever knows a deposited coin can find the spend that consumes it, recover the amount sent, and follow every later spend descending from its change. + --- ### Ledger [toc] [#ShieldedTreasuryStateless-Ledger] @@ -891,7 +1097,7 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/UnshieldedTreasu kind="ledger" id="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`. + Tracked balance per token color. Deposits add to it and sends subtract. 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] @@ -943,24 +1149,18 @@ None. modulePrefix="UnshieldedTreasury" stackParams="true" > - Sends unshielded tokens from the treasury. Zero-amount sends - are permitted and are value-preserving no-ops. + Sends unshielded tokens from the treasury. - Recipients are not validated. An unspendable recipient (such as a + Recipients other than this contract 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 + 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](#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. - + A self-addressed send is auto-received by `sendUnshielded` but still debited from `_balances`, + so the funds would be stranded. This circuit therefore rejects sends to self. Access control is NOT enforced here. @@ -970,6 +1170,8 @@ None. Requirements: + * `recipient` must not be this contract's own address. + * `amount` must be non-zero. The ledger rejects zero-value outputs. * The treasury must hold sufficient balance for the given token color. @@ -995,6 +1197,9 @@ None. import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderPrivate"; ``` +The commitment hides the parent from the public, not from a depositor. +Whoever knows a deposited coin can find the drain that spends it, recover the drained amount, and test candidate parent keys against the sent coin's commitment. + --- ### Ledger [toc] [#ForwarderPrivate-Ledger] @@ -1007,7 +1212,9 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/ForwarderPrivate kind="ledger" id="ForwarderPrivate-_parentCommitment" > - Commitment to the `(parent, opSecret)` pair, `H(pad(32, "ForwarderPrivate:commitment") || parent || opSecret)`. Immutable after initialization. + Commitment to the `(parent, opSecret)` pair, `H(pad(32, "ForwarderPrivate:commitment") || parent || opSecret)`, published in place of the parent itself. + The parent key alone is not secret, so `opSecret` supplies the hiding: without it a candidate parent could be hashed and matched against this value. + Immutable after initialization. #### _isInitialized [toc] [#ForwarderPrivate-_isInitialized] @@ -1038,7 +1245,7 @@ None. modulePrefix="ForwarderPrivate" > Initializes the forwarder with a parent commitment. - Called once from the preset constructor. The commitment is + Called once from the consuming contract's constructor. The commitment is immutable after init. Requirements: @@ -1055,7 +1262,7 @@ None. Receives a shielded coin into the forwarder's custody. @@ -1076,14 +1283,15 @@ None. value: Uint<128> ) → ShieldedSendResult' kind="circuit" - complexity="k=15, rows=23937" + complexity="k=15, rows=23837" modulePrefix="ForwarderPrivate" stackParams="true" > 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`, + so the parent stays hidden from anyone who holds no deposited coin. + If `coin.value` exceeds `value`, `sendShielded` routes the change back to this contract as a self-owned output; [_drain](#ForwarderPrivate-_drain) returns it untouched via `result.change`. The implementing contract chooses what to do with it: leave it dwelling at @@ -1190,7 +1398,7 @@ None. modulePrefix="ForwarderShielded" > Initializes the forwarder with a parent recipient. - Called once from the preset constructor. The coin public key is + Called once from the consuming contract's constructor. The coin public key is stored in the `left` arm of the generic `_parent` ledger field. Requirements: @@ -1209,9 +1417,9 @@ None. #### _deposit [toc] [#ForwarderShielded-_deposit] #### [!toc] [#ForwarderShielded-_deposit] Receives a shielded coin and atomically forwards it to @@ -1219,10 +1427,20 @@ None. `receiveShielded`, then immediately re-sent via `sendImmediateShielded` to the stored parent recipient (always the `left` / coin-public-key arm today). + Returns the coin sent to the parent, with no change: the whole value is forwarded. + + + The depositor must encrypt the forwarded output to the parent's + encryption public key, obtained off-chain. The ledger cannot check that + key against `_parent`, so a wrong or missing key leaves the coin + spendable by the parent but invisible to its wallet. + Requirements: * Contract must be initialized. + * The forwarded output must be encrypted to the parent's encryption public key. + If unsure, hand the returned `ShieldedSendResult` to the parent so it can recover the coin. --- @@ -1283,7 +1501,7 @@ None. modulePrefix="ForwarderUnshielded" > Initializes the forwarder with a parent recipient. - Called once from the preset constructor. The user address is stored + Called once from the consuming contract's constructor. The user address is stored in the `right` arm of the generic `_parent` ledger field. Requirements: @@ -1304,7 +1522,7 @@ None. Receives an unshielded amount of `color` and atomically @@ -1316,3 +1534,641 @@ None. * Contract must be initialized. +--- + +## ShieldedMultiSigV2 [toc] [#ShieldedMultiSigV2] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/presets/ShieldedMultiSigV2"<2>; +``` + +`ShieldedMultiSigV2<#t>` composes [Secp256k1EcdsaMultisig](#Secp256k1EcdsaMultisig) and [ShieldedTreasuryStateless](#ShieldedTreasuryStateless); `t` is both the threshold and the approval width. +It requires ZKIR v3, and implementing contracts MUST NOT import `Signer` or `Secp256k1EcdsaMultisig` directly. The `execute` complexity is measured at `t = 2`. + +--- + +### Types [toc] [#ShieldedMultiSigV2-Types] +### Types [!toc] [#ShieldedMultiSigV2-Types] + +#### Proposal_Recipient [toc] [#ShieldedMultiSigV2-Proposal_Recipient] +#### [!toc] [#ShieldedMultiSigV2-Proposal_Recipient] + + Re-export of the ProposalManager [Recipient](#ProposalManager-Recipient), needed for the [execute](#ShieldedMultiSigV2-execute) signature. + Importing `ProposalManager` again instead would give the implementing contract a second, unwritten copy of its state. + + +### Ledger [toc] [#ShieldedMultiSigV2-Ledger] +### Ledger [!toc] [#ShieldedMultiSigV2-Ledger] + +#### _domainSeparator [toc] [#ShieldedMultiSigV2-_domainSeparator] +#### [!toc] [#ShieldedMultiSigV2-_domainSeparator] + + The EIP-712 domain separator every [execute](#ShieldedMultiSigV2-execute) digest is built + under, derived from `(name, version, instanceSalt)`. Set once at construction. + + +### Witnesses [toc] [#ShieldedMultiSigV2-Witnesses] +### Witnesses [!toc] [#ShieldedMultiSigV2-Witnesses] + +None. + +### Circuits [toc] [#ShieldedMultiSigV2-Circuits] +### Circuits [!toc] [#ShieldedMultiSigV2-Circuits] + +#### initialize [toc] [#ShieldedMultiSigV2-initialize] +#### [!toc] [#ShieldedMultiSigV2-initialize] + + Registers the signer set at threshold `t` and fixes the EIP-712 domain separator. + It writes the `sealed` `_domainSeparator`, so it MUST only be called from the implementing contract's constructor + and MUST NOT be exposed as a post-deploy circuit. + + The EIP-712 domain fixed here is `("ShieldedMultiSigV2", "1", instanceSalt)`. `chainId` and + `verifyingContract` are absent: no network identifier is available + in-circuit, and a 32-byte Midnight address does not fit + `verifyingContract`'s `address` type. + Replay protection comes from [execute](#ShieldedMultiSigV2-execute) binding `kernel.self()` in its struct instead. + + `instanceSalt` also keys the signer commitments, so a fresh value per + deployment keeps one public key from producing the same commitment in two + deployments, which would link the signer set across them. + + Requirements: + + * Can only be called once. + * `instanceSalt` should be cryptographically random. + * `signerCommitments` must not contain duplicates. + * `n` must be at least `t`. + + +#### deposit [toc] [#ShieldedMultiSigV2-deposit] +#### [!toc] [#ShieldedMultiSigV2-deposit] + + Receives a shielded coin into the multisig treasury. Anyone may deposit. + The coin is claimed through `receiveShielded`. No coin data is stored on the + public ledger, so no balance is readable from it; a depositor can still + follow the coin they deposited (see [ShieldedTreasuryStateless](#ShieldedTreasuryStateless)). + + The operator discovers the coin's Merkle tree index by subscribing + to `ZswapOutput` events via the indexer, filtering by contract address, + and extracting `mt_index`. Combined with the known `ShieldedCoinInfo`, + this produces the `QualifiedShieldedCoinInfo` needed for spending. + + +#### execute [toc] [#ShieldedMultiSigV2-execute] +#### [!toc] [#ShieldedMultiSigV2-execute] + + Executes a shielded send authorized by threshold signatures. + + The circuit reads the current nonce, increments it, then rebuilds the EIP-712 digest that signers must have signed + off-chain: the `Execute` struct hashed under [_domainSeparator](#ShieldedMultiSigV2-_domainSeparator), with type string + `Execute(bytes32 contractAddress,uint256 nonce,uint8 recipientKind,bytes32 recipient,bytes32 coinColor,uint256 amount)`. + `contractAddress` is `kernel.self()`, so a signature authorizes exactly one operation on one instance. + Approvals are checked by [assertApprovals](#Secp256k1EcdsaMultisig-assertApprovals). + The spend goes through [ShieldedTreasuryStateless _send](#ShieldedTreasuryStateless-_send); + read its warning on the returned `ShieldedSendResult` before returning it from an entry point. + + + `to` must be a `ShieldedUser`. A contract-addressed coin would be + unclaimable, making the transaction malformed. + + + Requirements: + + * No two entries may be the same signer. + * Every public key must hash to a registered signer commitment. + * Every signature must be valid over the message hash and use the low-s encoding. + * `to.kind` must be `ShieldedUser`. + * `coin.value` must be >= `amount`. + + +#### _calculateSignerId [toc] [#ShieldedMultiSigV2-_calculateSignerId] +#### [!toc] [#ShieldedMultiSigV2-_calculateSignerId] + + Computes a signer commitment from a secp256k1 public key with + [calculateSignerId](#Secp256k1EcdsaMultisig-calculateSignerId). + Pure, so the deployer can derive constructor commitments off-chain. + + Requirements: + + * `pk` must not be the secp256k1 identity point, which has no coordinates. + + +#### getNonce [toc] [#ShieldedMultiSigV2-getNonce] +#### [!toc] [#ShieldedMultiSigV2-getNonce] + + Returns the nonce the next [execute](#ShieldedMultiSigV2-execute) digest will bind to. + + +#### getSignerCount [toc] [#ShieldedMultiSigV2-getSignerCount] +#### [!toc] [#ShieldedMultiSigV2-getSignerCount] + + Returns the number of registered signers. + + +#### getThreshold [toc] [#ShieldedMultiSigV2-getThreshold] +#### [!toc] [#ShieldedMultiSigV2-getThreshold] + + Returns the approval threshold. + + +#### isSigner [toc] [#ShieldedMultiSigV2-isSigner] +#### [!toc] [#ShieldedMultiSigV2-isSigner] + + Returns whether the given commitment is a registered signer. + + +--- + +## NativeShieldedTokenIssuer [toc] [#NativeShieldedTokenIssuer] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/multisig/presets/NativeShieldedTokenIssuer"<2>; +``` + +`NativeShieldedTokenIssuer<#t>` composes [Secp256k1EcdsaMultisig](#Secp256k1EcdsaMultisig), [NativeShieldedToken](./native-shielded-token#NativeShieldedToken), and [NativeShieldedTokenDerivedNonce](./native-shielded-token#NativeShieldedTokenDerivedNonce); every mint and burn carries `t` approvals. +It requires ZKIR v3, and implementing contracts MUST NOT import `Signer` or `Secp256k1EcdsaMultisig` directly. The gated circuits' complexity is measured at `t = 2`. + +--- + +### Ledger [toc] [#NativeShieldedTokenIssuer-Ledger] +### Ledger [!toc] [#NativeShieldedTokenIssuer-Ledger] + +#### _counter [toc] [#NativeShieldedTokenIssuer-_counter] +#### [!toc] [#NativeShieldedTokenIssuer-_counter] + + Signature replay counter. Every operation ([mint](#NativeShieldedTokenIssuer-mint), + [mintToSelf](#NativeShieldedTokenIssuer-mintToSelf), [burn](#NativeShieldedTokenIssuer-burn), [burnFromSelf](#NativeShieldedTokenIssuer-burnFromSelf)) + reads it into its digest and increments it. The read is pinned, so at most one operation lands per block; + the losing signers re-sign at the next nonce. + + +#### _domainSeparator [toc] [#NativeShieldedTokenIssuer-_domainSeparator] +#### [!toc] [#NativeShieldedTokenIssuer-_domainSeparator] + + The EIP-712 domain separator every operation digest is + built under, derived from `(name, version, instanceSalt)`. Set once at construction. + + +#### _derivedNonceCounter [toc] [#NativeShieldedTokenIssuer-_derivedNonceCounter] +#### [!toc] [#NativeShieldedTokenIssuer-_derivedNonceCounter] + + The counter that feeds coin nonces, separate from `_counter`. + Re-exported from `NativeShieldedTokenDerivedNonce` as `_counter`, renamed because this module's replay counter owns that name. + + +#### _instanceSalt [toc] [#NativeShieldedTokenIssuer-_instanceSalt] +#### [!toc] [#NativeShieldedTokenIssuer-_instanceSalt] + + Salt for the signer commitments and the EIP-712 domain. Re-exported from `Secp256k1EcdsaMultisig`. + + +#### _signers [toc] [#NativeShieldedTokenIssuer-_signers] +#### [!toc] [#NativeShieldedTokenIssuer-_signers] + + The registered signer commitments. Re-exported from `Secp256k1EcdsaMultisig`. + + +#### _signerCount [toc] [#NativeShieldedTokenIssuer-_signerCount] +#### [!toc] [#NativeShieldedTokenIssuer-_signerCount] + + The number of registered signers. Re-exported from `Secp256k1EcdsaMultisig`. + + +#### _threshold [toc] [#NativeShieldedTokenIssuer-_threshold] +#### [!toc] [#NativeShieldedTokenIssuer-_threshold] + + The approval threshold. Re-exported from `Secp256k1EcdsaMultisig`. + + +#### _domain [toc] [#NativeShieldedTokenIssuer-_domain] +#### [!toc] [#NativeShieldedTokenIssuer-_domain] + + Domain separator fixed at construction; with the contract + address it determines this token's color. Re-exported from `NativeShieldedToken`. + + +#### _name [toc] [#NativeShieldedTokenIssuer-_name] +#### [!toc] [#NativeShieldedTokenIssuer-_name] + + The immutable token name. Re-exported from `NativeShieldedToken`. + + +#### _symbol [toc] [#NativeShieldedTokenIssuer-_symbol] +#### [!toc] [#NativeShieldedTokenIssuer-_symbol] + + The immutable token symbol. Re-exported from `NativeShieldedToken`. + + +#### _decimals [toc] [#NativeShieldedTokenIssuer-_decimals] +#### [!toc] [#NativeShieldedTokenIssuer-_decimals] + + The immutable token decimals. Re-exported from `NativeShieldedToken`. + + +#### _isInitialized [toc] [#NativeShieldedTokenIssuer-_isInitialized] +#### [!toc] [#NativeShieldedTokenIssuer-_isInitialized] + + The token module's initialization flag. Re-exported from `NativeShieldedToken`. + + +### Witnesses [toc] [#NativeShieldedTokenIssuer-Witnesses] +### Witnesses [!toc] [#NativeShieldedTokenIssuer-Witnesses] + +None. + +### Circuits [toc] [#NativeShieldedTokenIssuer-Circuits] +### Circuits [!toc] [#NativeShieldedTokenIssuer-Circuits] + +#### initialize [toc] [#NativeShieldedTokenIssuer-initialize] +#### [!toc] [#NativeShieldedTokenIssuer-initialize] + + Registers the signer set at threshold `t`, initializes the + token metadata and domain, and fixes the EIP-712 domain separator + `("NativeShieldedTokenIssuer", "1", instanceSalt)`. + It writes the token module's `sealed` domain and `_domainSeparator`, so it MUST only be called from the + implementing contract's constructor and MUST NOT be exposed as a post-deploy circuit. + + `instanceSalt` is both the EIP-712 domain salt and the signer-commitment + salt, so a fresh value per deployment keeps one public key from + producing the same commitment in two deployments. + The token color is `tokenType(tokenDomain, kernel.self())`. Only coins + of this color can be burned through this contract. + + Requirements: + + * Can only be called once. + * `instanceSalt` should be cryptographically random. + * `signerCommitments` must not contain duplicates. + * `n` must be at least `t`. + + +#### _calculateSignerId [toc] [#NativeShieldedTokenIssuer-_calculateSignerId] +#### [!toc] [#NativeShieldedTokenIssuer-_calculateSignerId] + + Computes a signer commitment from a secp256k1 public key with + [calculateSignerId](#Secp256k1EcdsaMultisig-calculateSignerId). + Pure, so the deployer can derive constructor commitments off-chain. + + Requirements: + + * `pk` must not be the secp256k1 identity point, which has no coordinates. + + +#### mint [toc] [#NativeShieldedTokenIssuer-mint] +#### [!toc] [#NativeShieldedTokenIssuer-mint] + + Mints a new shielded coin of this contract's token to + `recipient`, authorized by threshold signatures over + `Mint(bytes32 contractAddress,bytes32 recipient,uint256 nonce,uint256 amount)`. + + The amount is public. The coin nonce comes from `NativeShieldedTokenDerivedNonce`, so anyone + can test a candidate recipient against the coin commitment. + + + The returned coin info is the only copy available to the + recipient; deliver it out of band. + + + Requirements: + + * No two entries may be the same signer. + * Every public key must hash to a registered signer commitment. + * Every signature must be valid over the mint message hash and use the low-s encoding. + * `recipient` is not zero. + + +#### mintToSelf [toc] [#NativeShieldedTokenIssuer-mintToSelf] +#### [!toc] [#NativeShieldedTokenIssuer-mintToSelf] + + Mints a new shielded coin of this contract's token to the + contract itself, claimed in the same call, authorized by threshold + signatures over `MintToSelf(bytes32 contractAddress,uint256 nonce,uint256 amount)`. + + + The returned coin info is the contract's only record of the + holding; persist it for [burnFromSelf](#NativeShieldedTokenIssuer-burnFromSelf). + + + Requirements: + + * No two entries may be the same signer. + * Every public key must hash to a registered signer commitment. + * Every signature must be valid over the mint-to-self message hash and use the low-s encoding. + + +#### burn [toc] [#NativeShieldedTokenIssuer-burn] +#### [!toc] [#NativeShieldedTokenIssuer-burn] + + Burns `amount` from a holder's coin, authorized by threshold signatures over + `Burn(bytes32 contractAddress,bytes32 refundTo,uint256 nonce,uint256 amount)`. + The holder's transaction pays `coin` in; the contract + receives it and burns it in the same call. Any change goes to `refundTo`. + Returns the refund coin, or `none` if the coin was burned in full. + + + The returned refund coin info is the only copy available to + `refundTo`; deliver it out of band. + + + + `amount`, `coin.value`, and `refundTo` stay hidden only while `coin` carries a nonce the observer does not know. + A coin passed straight from [mint](#NativeShieldedTokenIssuer-mint) has a derived nonce, + so the burn links to its mint and all three are recoverable by trial. + For an unlinkable burn the holder re-nonces the coin first with a wallet self-transfer; the contract cannot enforce this. + + + Requirements: + + * No two entries may be the same signer. + * Every public key must hash to a registered signer commitment. + * Every signature must be valid over the burn message hash and use the low-s encoding. + * `coin.color` is this contract's token color. + * `amount` is less than or equal to `coin.value`. + * `refundTo` is not zero. + + +#### burnFromSelf [toc] [#NativeShieldedTokenIssuer-burnFromSelf] +#### [!toc] [#NativeShieldedTokenIssuer-burnFromSelf] + + Burns `amount` from a coin this contract holds, authorized + by threshold signatures over + `BurnFromSelf(bytes32 contractAddress,uint256 nonce,uint256 amount,bytes32 coinNonce,uint256 coinValue)`. + + The operator supplies the `QualifiedShieldedCoinInfo` from the off-chain + UTXO pool. On a partial burn the change is auto-received by the contract + and returned; it replaces `coin` as the contract's holding, so the + operator must persist it. + The digest binds `coin.nonce` and `coin.value`, so approvals authorize one + specific holding rather than an amount. Pick the coin before collecting signatures. + + Requirements: + + * No two entries may be the same signer. + * Every public key must hash to a registered signer commitment. + * Every signature must be valid over the burn-from-self message hash and use the low-s encoding. + * `coin.color` is this contract's token color. + * `amount` is less than or equal to `coin.value`. + * `coin.nonce` and `coin.value` match the approved coin. + + +#### getNonce [toc] [#NativeShieldedTokenIssuer-getNonce] +#### [!toc] [#NativeShieldedTokenIssuer-getNonce] + + Returns the counter the next operation's digest will bind to. + Submitted as a transaction, it pins `_counter` and conflicts with any same-block operation. + + +#### name [toc] [#NativeShieldedTokenIssuer-name] +#### [!toc] [#NativeShieldedTokenIssuer-name] + + Returns the token name. + + +#### symbol [toc] [#NativeShieldedTokenIssuer-symbol] +#### [!toc] [#NativeShieldedTokenIssuer-symbol] + + Returns the token symbol. + + +#### decimals [toc] [#NativeShieldedTokenIssuer-decimals] +#### [!toc] [#NativeShieldedTokenIssuer-decimals] + + Returns the token decimals. + + +#### tokenColor [toc] [#NativeShieldedTokenIssuer-tokenColor] +#### [!toc] [#NativeShieldedTokenIssuer-tokenColor] + + Returns this contract's token color, `tokenType(tokenDomain, kernel.self())`. + + +#### getSignerCount [toc] [#NativeShieldedTokenIssuer-getSignerCount] +#### [!toc] [#NativeShieldedTokenIssuer-getSignerCount] + + Returns the number of registered signers. + + +#### getThreshold [toc] [#NativeShieldedTokenIssuer-getThreshold] +#### [!toc] [#NativeShieldedTokenIssuer-getThreshold] + + Returns the approval threshold. + + +#### isSigner [toc] [#NativeShieldedTokenIssuer-isSigner] +#### [!toc] [#NativeShieldedTokenIssuer-isSigner] + + Returns whether the given commitment is a registered signer. + diff --git a/content/contracts-compact/0.4.x/api/native-shielded-token.mdx b/content/contracts-compact/0.4.x/api/native-shielded-token.mdx index 81b5224a..259a8725 100644 --- a/content/contracts-compact/0.4.x/api/native-shielded-token.mdx +++ b/content/contracts-compact/0.4.x/api/native-shielded-token.mdx @@ -128,7 +128,7 @@ None. Returns whether the contract has been initialized. @@ -139,7 +139,7 @@ None. Returns the token name. @@ -154,7 +154,7 @@ None. Returns the symbol of the token. @@ -169,7 +169,7 @@ None. Returns the number of decimals used to get its user representation. @@ -184,7 +184,7 @@ None. Returns this token's coin color: @@ -204,7 +204,7 @@ None. nonce: Bytes<32> ) → ShieldedCoinInfo' kind="circuit" - complexity="k=14, rows=10473" + complexity="k=14, rows=10450" modulePrefix="NativeShieldedToken" stackParams="true" > @@ -235,7 +235,7 @@ None. Mints `amount` to this contract itself, claiming the coin in @@ -248,7 +248,12 @@ None. The consumer SHOULD persist the returned coin info in its own ledger state: it is the contract's holding and is not otherwise - recoverable. + recoverable. The coin is already claimed as a receive by the mint, so a + custody module must record it without calling `receiveShielded` on it + again. [ShieldedTreasury._deposit](./multisig#ShieldedTreasury-_deposit) claims on the way in and is not that path; + a record-only circuit is tracked in compact-contracts#1112. Until a + custody module is composed, the only spend of a contract-held coin is + [_burnFromSelf](#NativeShieldedToken-_burnFromSelf). Requirements: @@ -265,7 +270,7 @@ None. refundTo: ZswapCoinPublicKey ) → Maybe' kind="circuit" - complexity="k=16, rows=41492" + complexity="k=16, rows=41327" modulePrefix="NativeShieldedToken" stackParams="true" > @@ -302,7 +307,7 @@ None. Burns `amount` from `coin`, a coin this contract already @@ -311,7 +316,9 @@ None. 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. + its info is not otherwise recoverable. Like a [_mintToSelf](#NativeShieldedToken-_mintToSelf) coin, the + change is already claimed as a receive, so a custody module must record + it without a second `receiveShielded`. Requirements: @@ -411,7 +418,7 @@ None. Returns whether the contract has been initialized. @@ -422,7 +429,7 @@ None. Returns the family name shared by all token types of this @@ -439,7 +446,7 @@ None. Returns the family symbol shared by all token types of this @@ -455,7 +462,7 @@ None. Returns the family-wide number of decimals used to get the @@ -471,7 +478,7 @@ None. Returns the coin color (token type) for `domain` under this @@ -492,7 +499,7 @@ None. nonce: Bytes<32> ) → ShieldedCoinInfo' kind="circuit" - complexity="k=14, rows=10750" + complexity="k=14, rows=10727" modulePrefix="NativeShieldedTokenFamily" stackParams="true" > @@ -527,7 +534,7 @@ None. nonce: Bytes<32> ) → ShieldedCoinInfo' kind="circuit" - complexity="k=14, rows=16346" + complexity="k=14, rows=10497" modulePrefix="NativeShieldedTokenFamily" stackParams="true" > @@ -559,7 +566,7 @@ None. refundTo: ZswapCoinPublicKey ) → Maybe' kind="circuit" - complexity="k=16, rows=41768" + complexity="k=16, rows=41603" modulePrefix="NativeShieldedTokenFamily" stackParams="true" > @@ -600,7 +607,7 @@ None. amount: Uint<128> ) → Maybe' kind="circuit" - complexity="k=15, rows=23583" + complexity="k=15, rows=23484" modulePrefix="NativeShieldedTokenFamily" stackParams="true" > @@ -655,7 +662,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedToken kind="sealed ledger" id="NativeShieldedTokenCore-_name" > - The immutable token name. + The token's human-readable name, set once at construction. #### _symbol [toc] [#NativeShieldedTokenCore-_symbol] @@ -665,7 +672,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedToken kind="sealed ledger" id="NativeShieldedTokenCore-_symbol" > - The immutable token symbol. + The token's ticker symbol, set once at construction. #### _decimals [toc] [#NativeShieldedTokenCore-_decimals] @@ -675,7 +682,8 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NativeShieldedToken kind="sealed ledger" id="NativeShieldedTokenCore-_decimals" > - The immutable token decimals. + The number of decimals used to display balances, set once at + construction. Purely presentational; no circuit scales by it. ### Witnesses [toc] [#NativeShieldedTokenCore-Witnesses] @@ -739,7 +747,7 @@ None. Returns whether the contract has been initialized. @@ -752,7 +760,7 @@ None. Returns the token name. @@ -767,7 +775,7 @@ None. Returns the symbol of the token. @@ -782,7 +790,7 @@ None. Returns the number of decimals used to get its user representation. @@ -799,7 +807,7 @@ None. Returns the coin color for `domain` under this contract's @@ -820,7 +828,7 @@ None. nonce: Bytes<32> ) → ShieldedCoinInfo' kind="circuit" - complexity="k=15, rows=17017" + complexity="k=14, rows=11109" modulePrefix="NativeShieldedTokenCore" stackParams="true" > @@ -830,7 +838,7 @@ None. 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 + is claimed by `mintShieldedToken` in this same call, so the consumer SHOULD persist the returned coin info: it is the contract's holding and is not otherwise recoverable. @@ -857,7 +865,7 @@ None. refundTo: Either ) → Maybe' kind="circuit" - complexity="k=16, rows=53832" + complexity="k=16, rows=47764" modulePrefix="NativeShieldedTokenCore" stackParams="true" > @@ -872,8 +880,8 @@ 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](#NativeShieldedTokenCore-_burnFromSelf) instead. A `right(kernel.self())` refund is claimed via - `receiveShielded` in this same call. + [_burnFromSelf](#NativeShieldedTokenCore-_burnFromSelf) instead. A `right(kernel.self())` refund is claimed by + `sendImmediateShielded` in this same call. The returned refund coin info is the only copy available to @@ -900,7 +908,7 @@ None. amount: Uint<128> ) → Maybe' kind="circuit" - complexity="k=15, rows=23583" + complexity="k=15, rows=23484" modulePrefix="NativeShieldedTokenCore" stackParams="true" > @@ -969,7 +977,7 @@ None. Adds `amount` to the exact minted total. Call once per @@ -981,7 +989,7 @@ None. Adds `amount` to the contract-mediated burned total. Call once @@ -994,7 +1002,7 @@ None. Returns the exact amount ever minted. @@ -1005,7 +1013,7 @@ None. Returns the contract-mediated amount burned. @@ -1020,7 +1028,7 @@ None. Returns `totalMinted() - totalBurned()`. @@ -1080,7 +1088,7 @@ None. Adds `amount` to the exact minted total for `domain`. Call @@ -1092,7 +1100,7 @@ None. Adds `amount` to the contract-mediated burned total for @@ -1107,7 +1115,7 @@ None. Returns the exact amount ever minted for `domain`. @@ -1118,7 +1126,7 @@ None. Returns the contract-mediated amount burned for `domain`. @@ -1133,7 +1141,7 @@ None. Returns `totalMinted(domain) - totalBurned(domain)`. @@ -1193,7 +1201,7 @@ None. Adds `amount` to the exact minted total for `domain`. Call @@ -1206,7 +1214,7 @@ None. Adds `amount` to the contract-mediated burned total for @@ -1222,7 +1230,7 @@ None. Returns the exact amount ever minted for `domain`. @@ -1233,7 +1241,7 @@ None. Returns the contract-mediated amount burned for `domain`. @@ -1248,7 +1256,7 @@ None. Returns `totalMinted(domain) - totalBurned(domain)`, or 0 if @@ -1303,7 +1311,7 @@ None. Advances the counter and returns the next derived coin nonce, @@ -1514,7 +1522,7 @@ test-only and are not published. nonce: Bytes<32> ) → ShieldedCoinInfo' kind="circuit" - complexity="k=15, rows=21354" + complexity="k=15, rows=21252" modulePrefix="ZOwnableNativeShieldedToken" stackParams="true" > @@ -1536,7 +1544,7 @@ test-only and are not published. refundTo: ZswapCoinPublicKey ) → Maybe' kind="circuit" - complexity="k=16, rows=52024" + complexity="k=16, rows=51814" modulePrefix="ZOwnableNativeShieldedToken" stackParams="true" > @@ -1556,7 +1564,7 @@ test-only and are not published. Transfers ownership to `newOwnerId`. Owner only. @@ -1579,7 +1587,7 @@ test-only and are not published. Clears the owner. Owner only. @@ -1598,7 +1606,7 @@ test-only and are not published. Returns the current owner commitment. @@ -1627,7 +1635,7 @@ test-only and are not published. Returns the total amount ever minted. @@ -1638,7 +1646,7 @@ test-only and are not published. Returns the token name. @@ -1653,7 +1661,7 @@ test-only and are not published. Returns the token symbol. @@ -1668,7 +1676,7 @@ test-only and are not published. Returns the token decimals. @@ -1683,7 +1691,7 @@ test-only and are not published. Returns this contract's token color, diff --git a/content/contracts-compact/0.4.x/api/non-fungible-token.mdx b/content/contracts-compact/0.4.x/api/non-fungible-token.mdx index 51d3044d..a04d52d9 100644 --- a/content/contracts-compact/0.4.x/api/non-fungible-token.mdx +++ b/content/contracts-compact/0.4.x/api/non-fungible-token.mdx @@ -30,7 +30,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; kind="sealed ledger" id="NonFungibleToken-_name" > - / Public state + The token's human-readable name, set once at construction. #### _symbol [toc] [#NonFungibleToken-_symbol] @@ -40,7 +40,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; kind="sealed ledger" id="NonFungibleToken-_symbol" > - The immutable token symbol. + The token's ticker symbol, set once at construction. #### _isInitialized [toc] [#NonFungibleToken-_isInitialized] @@ -129,7 +129,6 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Initializes the contract by setting the name and symbol. @@ -147,7 +146,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Returns the number of tokens in `owner`'s account. @@ -162,7 +161,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Returns the owner of the `tokenId` token. @@ -178,7 +177,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Returns the token name. @@ -193,7 +192,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Returns the symbol of the token. @@ -208,7 +207,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Returns the token URI for the given `tokenId`. Returns the empty @@ -232,7 +231,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Sets the URI as [tokenURI](#NonFungibleToken-tokenURI) for the given `tokenId`. @@ -252,7 +251,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Gives permission to `to` to transfer `tokenId` token to another account. @@ -275,7 +274,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Returns the account approved for `tokenId` token. @@ -294,7 +293,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; approved: Boolean ) → []' kind="circuit" - complexity="k=13, rows=2947" + complexity="k=13, rows=2938" modulePrefix="NonFungibleToken" stackParams="true" > @@ -315,7 +314,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -331,7 +330,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -361,7 +360,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -390,7 +389,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist. @@ -405,7 +404,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Returns the approved address for `tokenId`. Returns the zero value if `tokenId` is not minted. @@ -420,7 +419,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -442,7 +441,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -464,7 +463,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Mints `tokenId` and transfers it to `to`. @@ -482,7 +481,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Mints `tokenId` and transfers it to `to`. It does NOT check if the recipient is a ContractAddress. @@ -505,7 +504,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Destroys `tokenId`. @@ -523,7 +522,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -549,7 +548,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -575,7 +574,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -599,7 +598,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; isExistenceRequired: Boolean ) → []' kind="circuit" - complexity="k=11, rows=1870" + complexity="k=11, rows=1868" stackParams="true" modulePrefix="NonFungibleToken" > @@ -630,7 +629,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; @@ -648,7 +647,7 @@ import "./node_modules/@openzeppelin/compact-contracts/token/NonFungibleToken"; Reverts if the `tokenId` doesn't have a current owner (it hasn't been minted, or it has been burned). diff --git a/content/contracts-compact/0.4.x/api/ownable.mdx b/content/contracts-compact/0.4.x/api/ownable.mdx index 36c10ac6..7e918970 100644 --- a/content/contracts-compact/0.4.x/api/ownable.mdx +++ b/content/contracts-compact/0.4.x/api/ownable.mdx @@ -47,9 +47,10 @@ import "./node_modules/@openzeppelin/compact-contracts/access/Ownable"; kind="ledger" id="Ownable-_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. + The current owner, canonicalized on write. A `left` holds an + account identifier, stored as supplied and never checked against the secret + key it should derive from; a `right` holds a contract address. The zero + value means ownership has been renounced. ### Witnesses [toc] [#Ownable-Witnesses] @@ -79,7 +80,6 @@ The TypeScript witnesses in the repository are test-only and are not published w Initializes the contract by setting the `initialOwner`. @@ -97,7 +97,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Returns the current contract owner. @@ -112,7 +112,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Transfers ownership of the contract to `newOwner`. @@ -136,7 +136,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Unsafe variant of [transferOwnership](#Ownable-transferOwnership). @@ -160,7 +160,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Leaves the contract without an owner. @@ -178,7 +178,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Throws if called by any account other than the owner. @@ -198,7 +198,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Transfers ownership of the contract to `newOwner` without @@ -221,7 +221,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Unsafe variant of [_transferOwnership](#Ownable-_transferOwnership). @@ -335,7 +335,6 @@ The TypeScript witnesses in the repository are test-only and are not published w Initializes the contract by setting the initial owner via `ownerId` @@ -359,7 +358,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Returns the current commitment representing the contract owner. @@ -375,7 +374,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Transfers ownership to `newOwnerId`. @@ -393,7 +392,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Leaves the contract without an owner. @@ -411,7 +410,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match @@ -430,7 +429,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Computes the owner commitment from the given `id` and `counter`. @@ -500,7 +499,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Transfers ownership to owner id `newOwnerId` without diff --git a/content/contracts-compact/0.4.x/api/security.mdx b/content/contracts-compact/0.4.x/api/security.mdx index d4f77419..d829ab29 100644 --- a/content/contracts-compact/0.4.x/api/security.mdx +++ b/content/contracts-compact/0.4.x/api/security.mdx @@ -45,7 +45,8 @@ import "./node_modules/@openzeppelin/compact-contracts/security/Initializable"; kind="ledger" id="Initializable-_isInitialized" > - Boolean indicating if initialized. + Whether `initialize` has run. Guards against re-initialization + and against use before initialization. ### Witnesses [toc] [#Initializable-Witnesses] @@ -61,7 +62,6 @@ None. Initializes the state thus ensuring the calling circuit can only be called once. @@ -76,7 +76,7 @@ None. Asserts that the contract has been initialized, throwing an error if not. @@ -91,7 +91,7 @@ None. Asserts that the contract has not been initialized, throwing an error if it has. @@ -125,7 +125,8 @@ import "./node_modules/@openzeppelin/compact-contracts/security/Pausable"; kind="ledger" id="Pausable-_isPaused" > - Boolean indicating if paused. + Whether the contract is paused. Consuming contracts gate + state-changing circuits on it via `assertPaused` / `assertNotPaused`. ### Witnesses [toc] [#Pausable-Witnesses] @@ -141,7 +142,7 @@ None. Returns true if the contract is paused, and false otherwise. @@ -152,7 +153,7 @@ None. Makes a circuit only callable when the contract is paused. @@ -167,7 +168,7 @@ None. Makes a circuit only callable when the contract is not paused. @@ -182,7 +183,7 @@ None. Triggers a stopped state. @@ -197,7 +198,7 @@ None. Lifts the pause on the contract. @@ -252,7 +253,7 @@ None. Returns whether `account` is currently allowed. @@ -263,7 +264,7 @@ None. Asserts that `account` is allowed. The composing contract @@ -279,7 +280,7 @@ None. Adds `account` to the allowlist. @@ -290,7 +291,7 @@ None. Removes `account` from the allowlist. @@ -341,7 +342,7 @@ None. Returns whether `account` is currently blocked. @@ -352,7 +353,7 @@ None. Asserts that `account` is not blocked. The composing contract @@ -368,7 +369,7 @@ None. Adds `account` to the blocklist. @@ -379,7 +380,7 @@ None. Removes `account` from the blocklist. diff --git a/content/contracts-compact/0.4.x/api/shielded-access-control.mdx b/content/contracts-compact/0.4.x/api/shielded-access-control.mdx index b0e7420e..dd7263ba 100644 --- a/content/contracts-compact/0.4.x/api/shielded-access-control.mdx +++ b/content/contracts-compact/0.4.x/api/shielded-access-control.mdx @@ -174,6 +174,7 @@ import "./node_modules/@openzeppelin/compact-contracts/access/ShieldedAccessCont id="ShieldedAccessControl-_adminRoles" > Mapping from a role identifier to an admin role identifier. + The key is the role being administered and the value is the role permitted to grant and revoke it. #### _roleCommitmentNullifiers [toc] [#ShieldedAccessControl-_roleCommitmentNullifiers] @@ -267,6 +268,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Requirements: * Contract is not initialized. + * `instanceSalt` is not zero. #### DEFAULT_ADMIN_ROLE [toc] [#ShieldedAccessControl-DEFAULT_ADMIN_ROLE] @@ -288,6 +290,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Reverts if caller cannot provide a valid proof of ownership for `role`. @@ -310,6 +313,7 @@ The TypeScript witnesses in the repository are test-only and are not published w Returns `true` if a caller proves ownership of `role` and is not revoked. MAY return false for a legitimately credentialed @@ -333,6 +337,7 @@ The TypeScript witnesses in the repository are test-only and are not published w @@ -361,6 +366,7 @@ The TypeScript witnesses in the repository are test-only and are not published w @@ -389,6 +395,7 @@ The TypeScript witnesses in the repository are test-only and are not published w @@ -425,6 +432,7 @@ 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](#ShieldedAccessControl-DEFAULT_ADMIN_ROLE) for @@ -444,6 +452,7 @@ The TypeScript witnesses in the repository are test-only and are not published w @@ -480,6 +489,7 @@ The TypeScript witnesses in the repository are test-only and are not published w @@ -510,6 +520,7 @@ 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 @@ -518,12 +529,16 @@ The TypeScript witnesses in the repository are test-only and are not published w 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. + - Disclosures: + Requirements: - * The role identifier - * The admin identifier - + * Contract is initialized. + + Disclosures: + + * The role identifier + * The admin identifier #### computeRoleCommitment [toc] [#ShieldedAccessControl-computeRoleCommitment] @@ -531,6 +546,7 @@ The TypeScript witnesses in the repository are test-only and are not published w diff --git a/content/contracts-compact/0.4.x/api/utils.mdx b/content/contracts-compact/0.4.x/api/utils.mdx index de75b136..7b185ac7 100644 --- a/content/contracts-compact/0.4.x/api/utils.mdx +++ b/content/contracts-compact/0.4.x/api/utils.mdx @@ -5,9 +5,14 @@ title: Utils This package provides the API for all Utils modules. - For an overview of the module, read the [Utils guide](../utils). + For an overview of the modules, read the [Utils guide](../utils). +The page covers the following modules: + +- [Utils](#Utils): miscellaneous circuits and common utilities for Compact contract development. +- [EvmAbi](#EvmAbi): Ethereum ABI word encoding, for messages that must hash identically in Compact and on the EVM. + ## Utils [toc] [#Utils] + +--- + +## EvmAbi [toc] [#EvmAbi] + + +```ts +import "./node_modules/@openzeppelin/compact-contracts/utils/EvmAbi"; +``` + +The module needs no `keccak256` of its own and compiles on either ZKIR backend. +Every circuit size below is a ZKIR v3 figure. + +--- + +### Ledger [toc] [#EvmAbi-Ledger] +### Ledger [!toc] [#EvmAbi-Ledger] + +None. + +### Witnesses [toc] [#EvmAbi-Witnesses] +### Witnesses [!toc] [#EvmAbi-Witnesses] + +None. + +### Circuits [toc] [#EvmAbi-Circuits] +### Circuits [!toc] [#EvmAbi-Circuits] + +#### uint8Word [toc] [#EvmAbi-uint8Word] +#### [!toc] [#EvmAbi-uint8Word] + + Encodes a `Uint<8>` as an `abi.encode(uint8)` word: the + value in the least significant byte, zero elsewhere. + + This is also how a Compact `enum` should reach a digest, matching Solidity, + where an enum ABI-encodes as its `uint8` ordinal: `uint8Word(kind as Uint<8>)`. + + +#### uint64Word [toc] [#EvmAbi-uint64Word] +#### [!toc] [#EvmAbi-uint64Word] + + Encodes a `Uint<64>` as an `abi.encode(uint256)` word: the + value big-endian in the low 8 bytes, zero-padded on the left. + + +#### uint128Word [toc] [#EvmAbi-uint128Word] +#### [!toc] [#EvmAbi-uint128Word] + + Encodes a `Uint<128>` as an `abi.encode(uint256)` word: the + value big-endian in the low 16 bytes, zero-padded on the left. + + Costs roughly twice [uint64Word](#EvmAbi-uint64Word), so prefer the narrower + encoder when the value fits in 64 bits. + + +#### boolWord [toc] [#EvmAbi-boolWord] +#### [!toc] [#EvmAbi-boolWord] + + Encodes a `Boolean` as an `abi.encode(bool)` word: zero + everywhere except the least significant byte, which is 1 for true. + diff --git a/content/contracts-compact/0.4.x/confidential-fungible-token.mdx b/content/contracts-compact/0.4.x/confidential-fungible-token.mdx index 0c2b324e..36217730 100644 --- a/content/contracts-compact/0.4.x/confidential-fungible-token.mdx +++ b/content/contracts-compact/0.4.x/confidential-fungible-token.mdx @@ -36,6 +36,7 @@ title: ConfidentialFungibleToken [_addSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply-_addSupply [_subSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply-_subSupply [totalSupply]: api/confidential-fungible-token#ConfidentialFungibleTokenPublicSupply-totalSupply +[compact#757]: https://github.com/LFDT-Minokawa/compact/issues/757 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. @@ -65,6 +66,7 @@ Every ledger field of the module is exported, so an observer can read all of the * **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. + `_decimals` is presentational only; no circuit scales by it. 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. @@ -120,6 +122,7 @@ Escrow claims are narrowed back to `Uint<128>`, since an allowance is set by one 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. +Registration is one-shot: a second [register] for the same `accountId` is rejected, so the stored key cannot be replaced. Before any on-chain call, a wallet derives its own id with the pure [computeAccountId] circuit (no proof needed): @@ -197,10 +200,17 @@ no plaintext needed) and then escrows the new amount, so allowances replace rath 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. +Funding the allowance up front is required. The spender holds no owner encryption key, so at spend time it can only prove +against its own escrow copy, never that the owner's balance covers the allowance. +An approve conserves the owner's holdings (spendable balance plus every outstanding escrow), +so allowances across spenders can sum to the whole holding but never exceed it. + [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. +Both require the owner and the spender to be registered, so an `_escrow` entry written for an unregistered account +(by a composing contract, for instance) is unspendable. ### What a partial spend leaves behind @@ -210,8 +220,10 @@ The owner receives no balance memo (its spendable balance did not change) and in 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. +[allowance] returns the entry, or an on-curve placeholder when none exists. The placeholder's `spenderCt` and `ownerCt` +are genuine encryptions of zero, but its `ownerMemo` is a sentinel whose `ephemeralPk` is the identity point, +which `EcdhMask_decrypt` rejects. A wallet tests for the sentinel instead of decrypting it. +The sentinel is also what tells "no allowance" from "an allowance of zero", since `approve` stores a real memo even for zero. Granting nothing is safe because the spend path gates on escrow membership independently. ## Minting, burning and supply @@ -405,3 +417,11 @@ A wallet integrating this contract typically: * 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). +* An upgrade may add operations but must not change how existing state is derived or laid out. + Key derivation is the quiet case: [register] stores the key `ElGamal.derivePk` returns and refuses a second one, + and the account id does not move, so an upgrade that derives keys differently leaves every balance, pending balance, + escrow and memo permanently unreachable. A changed derivation applies to fresh deployments only, + and off-chain code that mirrors `derivePk` must keep the instance's derivation for its lifetime. +* The library targets ZKIR v2, which compiler 0.34.0 emits by default. + Under `--feature-zkir-v3`, key generation fails with `Unsupported test_eq: JubjubScalar == JubjubScalar` + for any impure circuit that reaches `ElGamal.encryptPoint`, which every value circuit of this module does ([compact#757]). diff --git a/content/contracts-compact/0.4.x/crypto.mdx b/content/contracts-compact/0.4.x/crypto.mdx index 602e1867..d6100668 100644 --- a/content/contracts-compact/0.4.x/crypto.mdx +++ b/content/contracts-compact/0.4.x/crypto.mdx @@ -4,8 +4,12 @@ title: Crypto {/* links */} [ConfidentialFungibleToken guide]: ./confidential-fungible-token +[Multisig guide]: ./multisig [ElGamal API]: api/crypto#ElGamal [EcdhMask API]: api/crypto#EcdhMask +[Ecdsa API]: api/crypto#Ecdsa +[Eip712 API]: api/crypto#Eip712 +[EvmAbi API]: api/utils#EvmAbi [secretToScalar]: api/crypto#ElGamal-secretToScalar [derivePk]: api/crypto#ElGamal-derivePk [expandRandomness]: api/crypto#ElGamal-expandRandomness @@ -24,18 +28,35 @@ title: Crypto [kdf]: api/crypto#EcdhMask-kdf [EcdhMask.encrypt]: api/crypto#EcdhMask-encrypt [EcdhMask.decrypt]: api/crypto#EcdhMask-decrypt +[isLowS]: api/crypto#Ecdsa-isLowS +[assertLowS]: api/crypto#Ecdsa-assertLowS +[secp256k1EcdsaVerifyLowS]: api/crypto#Ecdsa-secp256k1EcdsaVerifyLowS +[domainSeparator]: api/crypto#Eip712-domainSeparator +[hashTypedData]: api/crypto#Eip712-hashTypedData +[LFDT-Minokawa/compact#757]: https://github.com/LFDT-Minokawa/compact/issues/757 +[LFDT-Minokawa/compact#616]: https://github.com/LFDT-Minokawa/compact/issues/616 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. +It also provides two modules for verifying secp256k1 signatures produced by Ethereum signing infrastructure: +Ecdsa, which enforces the low-s encoding, and Eip712, which builds the typed-data digest an EVM signer signs. +All four are pure modules: they hold no ledger state, declare no witnesses, +and take their keys, messages, 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. + +Ecdsa and Eip712 require ZKIR v3: the `Secp256k1` types and `keccak256` are unavailable on the v2 backend, +which compiler 0.34.0 emits by default. Compile a contract that imports either with `--feature-zkir-v3`; +the library's `compile:crypto` script passes it. +ElGamal and EcdhMask target v2. Under v3, an impure circuit that reaches [encryptPoint] fails at key generation +([LFDT-Minokawa/compact#757]), and exporting [derivePk] as an impure circuit panics ([LFDT-Minokawa/compact#616]). + + ## ElGamal The [ElGamal API] implements single-receiver ElGamal encryption over Jubjub. @@ -97,10 +118,10 @@ and is normally stored on the ledger next to the ciphertexts encrypted under it. 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. +Every encryption takes its randomness `r` as a `JubjubScalar` 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`. +* `r` must be a valid Jubjub scalar (strictly less than `ℓ`), derived rather than cast from a `Field` (see below). [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`, @@ -146,10 +167,68 @@ that commits to the value elsewhere and is immutable once written, which is the 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 `ℓ`). +`decrypt` guards the same point: it refuses an identity ephemeral rather than unmasking against that public constant. + +## Ecdsa + +The [Ecdsa API] supplies the low-s rule for secp256k1 ECDSA signatures. +The standard library's `secp256k1EcdsaVerify` accepts both `(r, s)` and `(r, n - s)`, where `n` is the secp256k1 group order, +so every valid signature has a second valid encoding that anyone can derive without the signing key. +`secp256k1EcdsaVerify` leaves the low-s constraint to its caller, and this module is that constraint: +rejecting `s > n / 2` gives each signature one encoding, which is what replay sets, nullifiers, +and signature-derived identifiers rely on. + +* [isLowS] returns whether `s <= n / 2`. +* [assertLowS] reverts with `Ecdsa: high-s signature` on a signature in the high half. +* [secp256k1EcdsaVerifyLowS] verifies a signature and accepts only the low-s encoding. +It reports the result instead of asserting, so a caller can fold it into a count or a threshold. + +## Eip712 + +The [Eip712 API] implements the EIP-712 typed-data envelope, for verifying signatures produced by Ethereum signing infrastructure. +A typed-data signature covers: + +``` +digest = keccak256(0x1901 || domainSeparator || structHash) +``` + +The domain separator binds the signature to one application, so a signature obtained for any other domain cannot be replayed here. +`personal_sign` does not provide that binding, since it signs an opaque hash. + +* [domainSeparator] computes the separator for `(name, version, salt)`. +EIP-712 encodes a `string` field as the keccak hash of its UTF-8 bytes and Compact has no dynamic types, +so the consumer hashes its own fixed-length name and version literals and passes the hashes in. +* [hashTypedData] wraps a struct hash in the envelope and returns the digest the signer actually signed. +It reverts with `Eip712: domain separator not set` on a zero separator, which is what an uninitialized ledger cell reads as. + +The struct hash is `keccak256` over EVM ABI words with the operation's type hash as the first word; +the [EvmAbi API] supplies the word encoders. The multisig presets build their approval digests this way (see the [Multisig guide]). + +### The domain + +The domain is `(name, version, salt)`, with the type string `EIP712Domain(string name,string version,bytes32 salt)`. +The signer's domain must have exactly these fields: a type string that differs by one character yields a different separator, and every signature fails. +`chainId` and `verifyingContract` are omitted, which EIP-712 permits since every domain field is optional. +Neither is expressible: a Midnight contract address is 32 bytes and does not fit `verifyingContract`'s `address` type, +and no network identifier is available in-circuit. + +On an EVM chain, `verifyingContract` is what makes a domain identify one deployment rather than one application. +Without it, the consumer has to get that binding elsewhere, and the two places to put it are not equivalent: + +* ***As a field of the operation struct***, holding the contract's own address (`kernel.self()`). +This is the stronger option and the one to reach for by default: a Midnight address is randomized at deployment and cannot be predicted, +so no two instances share one, and a signature for one deployment cannot authorize another. +* ***As `salt`***, a per-deployment value in the domain. This is weaker, because it separates domains rather than messages. +It is worth adding when the signer gates on domains but cannot read struct fields, since that is the only level at which it is visible. + + +A consumer that binds the deployment in neither place has one domain shared by every deployment of the contract, +and nothing distinguishes them: a signature for one deployment verifies on all of them. + ## Jubjub facts a consumer must respect -Both modules rely on two properties of Midnight's embedded-curve arithmetic. +ElGamal and EcdhMask 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. @@ -159,15 +238,16 @@ The identity point is a valid subgroup member, so it is NOT caught by this guara 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. -A consumer that needs to negate a point of its own should follow the same pattern. +* ***Scalars are typed `JubjubScalar`, not `Field`.*** Every scalar parameter of both modules (`r`, `k`, `e`, `ekScalar`) is a `JubjubScalar`. +A `Field as JubjubScalar` cast reduces modulo `ℓ`, so casting at a call site silently accepts an out-of-range value that the typed parameter rejects. +Derive scalars with [secretToScalar] and [expandRandomness] instead of casting; their 31-byte output is always below `ℓ`. +Point negation uses the standard library's `ecNeg`, so no circuit needs a negative scalar. ## 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 modules are imported like any other module; prefix each with its name (`ElGamal_`, `EcdhMask_`, `Ecdsa_`, `Eip712_`) +to avoid circuit signature clashes. +ElGamal and EcdhMask each export 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. @@ -262,3 +342,72 @@ export circuit notify(recipient: Bytes<32>, value: Uint<128>): [] { 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. + +### Verifying an EVM-signed approval + +A contract that accepts approvals from an Ethereum key rebuilds the digest the signer signed and checks the signature against it. +The struct hash covers the type hash, the contract's own address, a nonce, and the operation's fields, each as an EVM ABI word. +The domain separator is computed once in the constructor and sealed. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/crypto/Ecdsa" + prefix Ecdsa_; +import "./node_modules/@openzeppelin/compact-contracts/crypto/Eip712" + prefix Eip712_; +import "./node_modules/@openzeppelin/compact-contracts/utils/EvmAbi" + prefix EvmAbi_; + +export sealed ledger _domainSeparator: Bytes<32>; +export sealed ledger _approver: Secp256k1Point; +export ledger _nonce: Counter; +export ledger _limit: Uint<128>; +export ledger _enabled: Boolean; + +constructor(approver: Secp256k1Point, salt: Bytes<32>) { + _approver = disclose(approver); + _domainSeparator = Eip712_domainSeparator( + keccak256>(pad(5, "MyApp")), + keccak256>(pad(1, "1")), + disclose(salt) + ); +} + +export circuit setLimit( + limit: Uint<128>, + enabled: Boolean, + sig: Secp256k1EcdsaSignature +): [] { + const nonce = _nonce; + _nonce.increment(1); + + // The type string must match the signer's byte for byte. + // A precomputed literal word saves one keccak permutation per call. + const typeHash = keccak256>( + pad(74, "SetLimit(bytes32 contractAddress,uint256 nonce,uint256 limit,bool enabled)") + ); + const structHash = keccak256>>([ + typeHash, + kernel.self().bytes, + EvmAbi_uint64Word(nonce), + EvmAbi_uint128Word(limit), + EvmAbi_boolWord(enabled) + ]); + const digest = Eip712_hashTypedData(_domainSeparator, structHash); + + assert( + Ecdsa_secp256k1EcdsaVerifyLowS(digest, sig, _approver), + "MyApp: invalid approval" + ); + _limit = disclose(limit); + _enabled = disclose(enabled); +} +``` + +The signer signs the `SetLimit` struct as typed data (`eth_signTypedData_v4`) under the domain `{ name: "MyApp", version: "1", salt }`. +`kernel.self()` in the struct binds each signature to this deployment, and the nonce makes it single-use. +The type string declares `uint256` fields while the encoders read the values at their own width; the resulting word is the same. +`pad(n, s)` zero-pads a string shorter than `n`, so `n` must equal the string's byte length or the hash no longer matches the signer's. +Compile the contract with `--feature-zkir-v3`. diff --git a/content/contracts-compact/0.4.x/fungible-token.mdx b/content/contracts-compact/0.4.x/fungible-token.mdx index ffedf221..93324a76 100644 --- a/content/contracts-compact/0.4.x/fungible-token.mdx +++ b/content/contracts-compact/0.4.x/fungible-token.mdx @@ -3,7 +3,7 @@ title: FungibleToken --- {/* links */} -[fungible tokens]: ../contracts/5.x/tokens.mdx#different-kinds-of-tokens +[fungible tokens]: /contracts/5.x/tokens#different-kinds-of-tokens [EIP-20]: https://eips.ethereum.org/EIPS/eip-20 [Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [_mint]: api/fungible-token#FungibleToken-_mint diff --git a/content/contracts-compact/0.4.x/index.mdx b/content/contracts-compact/0.4.x/index.mdx index e7a2d15b..056962ea 100644 --- a/content/contracts-compact/0.4.x/index.mdx +++ b/content/contracts-compact/0.4.x/index.mdx @@ -7,9 +7,11 @@ title: Contracts for Compact [nvm]: https://github.com/nvm-sh/nvm [yarn]: https://yarnpkg.com/getting-started/install [compact-dev-tools]: https://docs.midnight.network/blog/compact-developer-tools +[Known issues]: https://github.com/OpenZeppelin/compact-contracts/blob/v0.4.0/CHANGELOG.md#known-issues **A library for secure smart contract development** written in Compact for [Midnight]. This library consists of modules to build custom smart contracts. +Version 0.4.0 targets Midnight Ledger v9 and Compact compiler 0.34.0. Pre-release versions (`-alpha`, `-rc`) contain experimental, unaudited code. @@ -178,3 +180,13 @@ Compiling 14 circuits: circuit "unpause" (k=13, rows=2362) Overall progress [====================] 14/14 ``` + +**ZKIR v3.** The library targets ZKIR v2, the compiler 0.34.0 default. +Pass `--feature-zkir-v3` only when the contract reaches a module that needs it: +the secp256k1-based `crypto/Ecdsa` and `multisig/Secp256k1EcdsaMultisig`, +or the `keccak256`-based `crypto/Eip712`. +`multisig/presets/ShieldedMultiSigV2`, `multisig/presets/NativeShieldedTokenIssuer` +and the `multisig/examples/` contracts reach both. +`utils/EvmAbi` needs no `keccak256` of its own and compiles on either backend. +Compiling the other modules under v3 currently hits upstream blockers, +listed under [Known issues] in the changelog. diff --git a/content/contracts-compact/0.4.x/multisig.mdx b/content/contracts-compact/0.4.x/multisig.mdx index 7de06652..b14d8d84 100644 --- a/content/contracts-compact/0.4.x/multisig.mdx +++ b/content/contracts-compact/0.4.x/multisig.mdx @@ -5,8 +5,8 @@ title: Multisig {/* links */} [Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern [Ownable]: ./ownable +[NativeShieldedToken]: ./native-shielded-token [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 @@ -14,10 +14,17 @@ title: Multisig [_removeSigner]: api/multisig#Signer-_removeSigner [_changeThreshold]: api/multisig#Signer-_changeThreshold [_setThreshold]: api/multisig#Signer-_setThreshold +[Secp256k1EcdsaMultisig]: api/multisig#Secp256k1EcdsaMultisig +[Secp256k1EcdsaMultisig API]: api/multisig#Secp256k1EcdsaMultisig +[Secp256k1EcdsaMultisig initialize]: api/multisig#Secp256k1EcdsaMultisig-initialize +[assertApprovals]: api/multisig#Secp256k1EcdsaMultisig-assertApprovals +[calculateSignerId]: api/multisig#Secp256k1EcdsaMultisig-calculateSignerId [ProposalManager API]: api/multisig#ProposalManager [_createProposal]: api/multisig#ProposalManager-_createProposal [_cancelProposal]: api/multisig#ProposalManager-_cancelProposal [_markExecuted]: api/multisig#ProposalManager-_markExecuted +[assertProposalActive]: api/multisig#ProposalManager-assertProposalActive +[getProposalStatus]: api/multisig#ProposalManager-getProposalStatus [toShieldedRecipient]: api/multisig#ProposalManager-toShieldedRecipient [toUnshieldedRecipient]: api/multisig#ProposalManager-toUnshieldedRecipient [ShieldedTreasury API]: api/multisig#ShieldedTreasury @@ -29,25 +36,44 @@ title: Multisig [UnshieldedTreasury _deposit]: api/multisig#UnshieldedTreasury-_deposit [UnshieldedTreasury _send]: api/multisig#UnshieldedTreasury-_send [ForwarderShielded API]: api/multisig#ForwarderShielded +[ForwarderShielded _deposit]: api/multisig#ForwarderShielded-_deposit [ForwarderUnshielded API]: api/multisig#ForwarderUnshielded [ForwarderPrivate API]: api/multisig#ForwarderPrivate [_drain]: api/multisig#ForwarderPrivate-_drain [_calculateParentCommitment]: api/multisig#ForwarderPrivate-_calculateParentCommitment +[ShieldedMultiSigV2 API]: api/multisig#ShieldedMultiSigV2 +[execute]: api/multisig#ShieldedMultiSigV2-execute +[NativeShieldedTokenIssuer API]: api/multisig#NativeShieldedTokenIssuer +[mint]: api/multisig#NativeShieldedTokenIssuer-mint +[mintToSelf]: api/multisig#NativeShieldedTokenIssuer-mintToSelf +[burn]: api/multisig#NativeShieldedTokenIssuer-burn +[burnFromSelf]: api/multisig#NativeShieldedTokenIssuer-burnFromSelf +[Eip712]: api/crypto#Eip712 +[Ecdsa]: api/crypto#Ecdsa +[EvmAbi]: api/utils#EvmAbi [presets directory]: https://github.com/OpenZeppelin/compact-contracts/tree/v0.4.0/contracts/src/multisig/presets +[examples directory]: https://github.com/OpenZeppelin/compact-contracts/tree/v0.4.0/contracts/src/multisig/examples 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, +a `Signer` registry with an approval threshold, a `Secp256k1EcdsaMultisig` signer set that verifies ECDSA approvals, +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. +Apart from `Secp256k1EcdsaMultisig`, which wraps `Signer`, 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). +The package also ships two presets, modules that compose these into a ready-made ECDSA policy; 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. + + `Secp256k1EcdsaMultisig`, the presets, and the contracts in the [examples directory] require ZKIR v3 (`--feature-zkir-v3`): + the `Secp256k1` types and `keccak256` are unavailable on the v2 backend. + The library's `compile:multisig` script passes the flag for the whole directory, but the other multisig modules do not need it. + + ## Signer The [Signer API] keeps the registry of who may approve and how many approvals are required. @@ -66,18 +92,18 @@ import "./node_modules/@openzeppelin/compact-contracts/multisig/Signer"`, 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 module has no `initialize`. + The consuming contract registers signers with [_addSigner] and sets the threshold with [_changeThreshold] or [_setThreshold], + either from the constructor or behind its own authorization policy. * The guards [assertSigner] and [assertThresholdMet] and the views `getSignerCount`, `getThreshold`, and [isSigner] are open to anyone - and deliberately do not check initialization. + and carry no "configured" flag. 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. @@ -121,7 +147,10 @@ constructor( thresh: Uint<8> ) { Ownable_initialize(initialOwner); - Signer_initialize<3>(signers, thresh); + for (const signer of signers) { + Signer__addSigner(signer); + } + Signer__changeThreshold(thresh); } export circuit addSigner(signer: Bytes<32>): [] { @@ -148,11 +177,57 @@ export circuit getThreshold(): Uint<8> { } ``` +## Secp256k1EcdsaMultisig + +The [Secp256k1EcdsaMultisig API] is a `t`-of-`n` signer set over secp256k1 ECDSA, built on a `Signer>` registry. +`n` signer commitments are registered at deployment, and a call passes when `t` distinct registered signers have signed its digest. +The threshold `t` is the module generic, fixed at import, and [Secp256k1EcdsaMultisig initialize] fixes `n`. +No circuit takes a threshold argument, and `t = 0` does not compile. + +```typescript +import "./node_modules/@openzeppelin/compact-contracts/multisig/Secp256k1EcdsaMultisig"<2> + prefix Secp256k1EcdsaMultisig_; +``` + +A signer commitment is [calculateSignerId] over the public key's affine coordinates, a per-deployment salt, and the domain `Secp256k1EcdsaMultisig:signer`. +It hashes the Compact encoding of a struct, not a byte concatenation, +so derive commitments with the pure circuit rather than reimplementing it: a wrong derivation registers commitments no key can satisfy. + +[assertApprovals] takes the digest, `t` public keys, and `t` signatures in matching positions. +It checks the threshold first, so an uninitialized registry fails before any signature work, +then rejects a repeated signer, then checks each commitment against the registry and each signature through the low-s verifier in [Ecdsa]. +The high-s encoding of an otherwise valid signature is rejected. +The module verifies whatever digest it is handed: replay, domain, and instance binding belong in the consumer's digest (see [Signing approvals](#signing-approvals)). + +### Public ledger state + +The registry fields `_signers`, `_signerCount`, and `_threshold` are re-exported unprefixed, and `_instanceSalt` is sealed. +[assertApprovals] discloses the `t` approving commitments, so which signers approved each call is public. +Public keys and signatures are verified in-circuit and never disclosed. +The salt is public, so an observer holding a candidate key can test membership; one without cannot correlate a signer across deployments. + +### Who may call what + +* [Secp256k1EcdsaMultisig initialize] writes the sealed salt, so it MUST only be called from the constructor. + It rejects a second call while signers are registered. +* [assertApprovals], [calculateSignerId], and the views are open. +* No signer or threshold change is exported, so the signer set is final once deployed. + +Import the module at exactly one width per contract. +Two widths instantiate two modules over one shared `Signer>` registry, so the second `initialize` trips the once-only guard. +Do not import `Signer` alongside it either: a direct import instantiates a separate registry that [assertApprovals] never reads. + + + Losing more than `n - t` keys freezes every circuit gated by [assertApprovals]. + There is no rotation or recovery path. + + ## 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 `Proposal` records a `Recipient`, a token `color`, an `amount`, an `expiry`, and a stored `ProposalStatus` (`Active`, `Executed`, or `Cancelled`). +[getProposalStatus] derives the other two values and never stores them: +`Inactive` for an id that was never created, and `Expired` for an `Active` proposal past its deadline. 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`, @@ -161,11 +236,18 @@ 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. +`expiry` is a deadline in seconds since the Unix epoch, and [_createProposal] rejects one that is not in the future. +An `Active` proposal is executable while the block time is below it and inert from then on: +[assertProposalActive] fails once it passes, and so do [_cancelProposal] and [_markExecuted]. +Expiry is terminal, since block time only moves forward and rebuilding the transaction cannot help. +The module sets no maximum horizon and no execution delay; a contract that needs a bounded lifetime or a minimum wait imposes its own. + ### 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, +`_proposals` is exported ledger state, so every proposal's recipient address, color, amount, expiry, and status is readable off-chain, and `_nextProposalId` reveals how many have been created. +An off-chain reader takes `status` as stored and, when it is `Active`, compares `expiry` with the current block time. 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. @@ -179,7 +261,7 @@ so two transactions that create proposals against the same ledger state conflict [_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, +creates a proposal that stays `Active` and unexecutable until [_cancelProposal] retires it or its `expiry` lapses, and a zero or otherwise unspendable address is accepted, so funds sent to it on execution are unrecoverable. ## Treasuries @@ -194,7 +276,9 @@ 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`, +[ShieldedTreasury _send] rejects the contract's own address as recipient, +since a self-addressed coin would be auto-received but never recorded in `_coins`. +It 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. @@ -231,7 +315,11 @@ The only rule is that the change must never be re-spent and still treated as hel 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. +"No ledger state" is not the same as "unobservable". +A coin's nullifier is a hash of the coin and the contract's address, +and `sendShielded` derives the nonces of the sent coin and of the change from a public hash of the spent coin's nonce. +Whoever knows a deposited coin can therefore find the spend that consumes it, recover the amount sent, +test candidate recipients against the sent coin's commitment, and repeat from the change for every later spend descending from it. ### UnshieldedTreasury @@ -244,8 +332,9 @@ 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. +[UnshieldedTreasury _send] rejects the contract's own address, since a self-addressed send would be auto-received +but still debited from `_balances`, and a zero amount, which the ledger refuses as a zero-value output. +Other recipients are not validated, so an unspendable recipient permanently removes the funds. ### Usage @@ -296,6 +385,7 @@ export circuit getSentTotal(color: Bytes<32>): Uint<128> { 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. +The [examples directory] has one deployable contract per forwarder. ### ForwarderShielded and ForwarderUnshielded @@ -313,9 +403,14 @@ and a third-party contract would never claim the output. 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. +[ForwarderShielded _deposit] returns the forwarded coin as a `ShieldedSendResult`. +The depositor must encrypt the forwarded output to the parent's encryption public key, obtained off-chain. +The ledger cannot check that key against `_parent`, so a wrong or missing key leaves the coin spendable by the parent but invisible to its wallet. +If unsure, hand the returned result to the parent so it can recover the coin. + ### ForwarderPrivate -The [ForwarderPrivate API] hides the parent. +The [ForwarderPrivate API] hides the parent from the public. The ledger holds `_parentCommitment`, computed off-chain by the deployer as `H(pad(32, "ForwarderPrivate:commitment") || parent || opSecret)` with the pure [_calculateParentCommitment], plus `_isInitialized`. @@ -329,9 +424,15 @@ Any change is routed back to the contract and returned untouched in `result.chan 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. +Two forwarders bound to the same parent with different operational secrets produce different commitments, so the ledger does not link them. Deposits disclose the incoming coin; drains disclose the spent coin and the value. +The commitment hides the parent from the public, not from a depositor. +A contract-owned coin's nullifier is a hash of the coin and the contract address, +and `sendShielded` derives the nonces of the sent coin and of the change from a public hash of the spent coin's nonce. +Whoever knows a deposited coin can therefore find the drain that spends it, recover the drained amount, +and test candidate parent keys against the sent coin's commitment: a depositor who can guess the parent can confirm it. + Losing the operational secret is permanent fund loss. It is the only drain authorization, with no rotation, revocation, or recovery path: @@ -341,15 +442,210 @@ Deposits disclose the incoming coin; drains disclose the spent coin and the valu ## 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 package ships two presets under its [presets directory]: `ShieldedMultiSigV2` and `NativeShieldedTokenIssuer`. +Each is a module that composes [Secp256k1EcdsaMultisig] with a treasury or a token and gates every state-changing circuit on `t` ECDSA approvals. +The threshold `t` is the import generic, and the preset's `initialize` fixes the signer count `n`. +A deployable contract imports the preset at one width, calls `initialize` from its constructor, and re-exports the circuits it needs; +the [examples directory] holds a 2-of-3 contract for each preset. + +* `initialize` writes sealed fields, so it MUST only be called from the constructor and never exposed as a post-deploy circuit. +* Implementing contracts MUST NOT import `Signer` or `Secp256k1EcdsaMultisig` directly, and import one width per contract. +* No signer or threshold change is exported. Losing more than `n - t` keys freezes the contract. + +### Signing approvals + +Signers approve an operation off-chain by signing an EIP-712 typed-data digest. +The circuit rebuilds the same digest with [Eip712] and [EvmAbi], then passes it to [assertApprovals]. + +* Before deployment, pick a random `instanceSalt`, derive each signer commitment with the preset's `_calculateSignerId(pk, instanceSalt)`, + and pass the commitments to `initialize`. + A fresh salt per deployment keeps one public key from producing the same commitment in two deployments. +* The EIP-712 domain is `(name, version, salt)`: the preset name, `"1"`, and `instanceSalt`. + `chainId` and `verifyingContract` are absent, because no network identifier is available in-circuit + and a 32-byte Midnight address does not fit the `address` type. +* Every operation struct starts with `bytes32 contractAddress`, filled in-circuit with `kernel.self()`. + The ledger randomizes a contract's address at deployment, so a signature for one deployment cannot authorize another. +* Each preset keeps a replay counter that every gated circuit reads into its digest as `nonce` and then increments. + `getNonce` returns the value the next digest binds to. + The read pins the counter, so two operations in one block conflict and the losing signers re-sign at the next nonce. +* Integers are big-endian ABI words, so an EVM signer's typed-data encoder reproduces the digest. +* Each call carries exactly `t` public keys and `t` low-s signatures from distinct signers, in matching `Vector` positions. + A high-s signature is rejected. + +`contracts/src/multisig/test/EcdsaTestUtils.ts` in the library repository rebuilds every preset digest through ethers' `TypedDataEncoder`, +which shows what the off-chain signer must produce +(like the witness implementations, it is test-only and is not published). + +### ShieldedMultiSigV2 + +The [ShieldedMultiSigV2 API] is a shielded wallet with no proposal lifecycle: one [execute] call verifies `t` approvals and spends. +It composes [Secp256k1EcdsaMultisig] with `ShieldedTreasuryStateless`, so coin data never reaches the public ledger. +Anyone may `deposit`. +The operator tracks the wallet's coins off-chain from `ZswapOutput` events and passes the coin to spend as an argument. + +The digest is the struct +`Execute(bytes32 contractAddress,uint256 nonce,uint8 recipientKind,bytes32 recipient,bytes32 coinColor,uint256 amount)` +under the domain `("ShieldedMultiSigV2", "1", instanceSalt)`, +so a signature binds the full recipient (kind and address), the coin color, and the amount. +[execute] accepts only a `ShieldedUser` recipient: a coin addressed to a contract would be unclaimable. +It returns the `ShieldedSendResult` of [ShieldedTreasuryStateless _send], whose warning on returning that result applies here. + +Each execution publishes which signers approved and what `ShieldedTreasuryStateless` discloses for the spend. +Keeping coin data off the ledger hides the balance from the public, not from a depositor: +whoever knows a deposited coin can follow every spend descending from it (see [ShieldedTreasuryStateless](#shieldedtreasurystateless)). + +### NativeShieldedTokenIssuer + +The [NativeShieldedTokenIssuer API] mints and burns one [NativeShieldedToken], for tokenized deposits, with every operation carrying `t` approvals. +Value enters only through [mint] (to a holder's coin public key) or [mintToSelf] (a coin the contract holds), +and leaves only through [burn] (a holder's coin paid into the transaction, with any change refunded to `refundTo`) +or [burnFromSelf] (a coin the contract holds). +Coins minted to holders are ordinary Zswap coins that holders transfer without the issuer. +No send circuit exists, so contract-held coins leave only through [burnFromSelf], and no supply is tracked. + +The four digests share the domain `("NativeShieldedTokenIssuer", "1", instanceSalt)`: + +* `Mint(bytes32 contractAddress,bytes32 recipient,uint256 nonce,uint256 amount)` +* `MintToSelf(bytes32 contractAddress,uint256 nonce,uint256 amount)` +* `Burn(bytes32 contractAddress,bytes32 refundTo,uint256 nonce,uint256 amount)` +* `BurnFromSelf(bytes32 contractAddress,uint256 nonce,uint256 amount,bytes32 coinNonce,uint256 coinValue)` + +`BurnFromSelf` binds the coin's nonce and value, so approvals authorize one holding: pick the coin before collecting signatures. +[mint] and [burn] return the only copy of the new coin or refund; hand it to its owner out of band, +since a contract-initiated mint creates no ciphertext a wallet can find by scanning. +[mintToSelf] and a partial [burnFromSelf] return the contract's new holding, which the operator must persist +together with its `mt_index` from the indexer. + +Every mint amount is public. +Coin nonces derive from the public `_derivedNonceCounter`, so anyone can test a candidate recipient against a minted coin's commitment, +and [burnFromSelf] amounts are recoverable by trial. - 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. + A coin burned exactly as it was minted is linkable to its mint, + which exposes the burn `amount`, the coin value, and `refundTo` by trial. + For an unlinkable [burn], the holder re-nonces the coin first with a wallet self-transfer. + The contract cannot enforce this. + +### A 2-of-3 shielded multisig + +The following contract trims `ShieldedMultiSigV2Example` from the [examples directory]. +It fixes `t = 2` at import and `n = 3` in the constructor, and surfaces the preset's domain separator unprefixed for `ledger()` readers. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/multisig/presets/ShieldedMultiSigV2"<2> + prefix Multisig_; + +export { ShieldedCoinInfo, QualifiedShieldedCoinInfo, ShieldedSendResult }; + +import { _domainSeparator } from "./node_modules/@openzeppelin/compact-contracts/multisig/presets/ShieldedMultiSigV2"<2>; +export { _domainSeparator }; + +constructor( + instanceSalt: Bytes<32>, + signerCommitments: Vector<3, Bytes<32>> +) { + Multisig_initialize<3>(instanceSalt, signerCommitments); +} + +export circuit deposit(coin: ShieldedCoinInfo): [] { + return Multisig_deposit(coin); +} + +export circuit execute( + to: Multisig_Proposal_Recipient, + amount: Uint<128>, + coin: QualifiedShieldedCoinInfo, + pubkeys: Vector<2, Secp256k1Point>, + signatures: Vector<2, Secp256k1EcdsaSignature> +): ShieldedSendResult { + return Multisig_execute(to, amount, coin, pubkeys, signatures); +} + +export pure circuit _calculateSignerId(pk: Secp256k1Point, salt: Bytes<32>): Bytes<32> { + return Multisig__calculateSignerId(pk, salt); +} + +export circuit getNonce(): Uint<64> { + return Multisig_getNonce(); +} +``` + +### A 2-of-3 token issuer + +The following contract trims `NativeShieldedTokenIssuerExample` from the [examples directory]. +The constructor also fixes the token domain and metadata. +The published example additionally re-exports the preset's ledger fields unprefixed and the remaining views. + +```typescript +pragma language_version >= {{compact_language_version}}; + +import CompactStandardLibrary; +import "./node_modules/@openzeppelin/compact-contracts/multisig/presets/NativeShieldedTokenIssuer"<2> + prefix Issuer_; + +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; +export { ShieldedCoinInfo, QualifiedShieldedCoinInfo }; + +constructor( + instanceSalt: Bytes<32>, + tokenDomain: Bytes<32>, + name_: Opaque<"string">, + symbol_: Opaque<"string">, + decimals_: Uint<8>, + signerCommitments: Vector<3, Bytes<32>> +) { + Issuer_initialize<3>(instanceSalt, tokenDomain, name_, symbol_, decimals_, signerCommitments); +} + +export circuit mint( + amount: Uint<64>, + recipient: ZswapCoinPublicKey, + pubkeys: Vector<2, Secp256k1Point>, + signatures: Vector<2, Secp256k1EcdsaSignature> +): ShieldedCoinInfo { + return Issuer_mint(amount, recipient, pubkeys, signatures); +} + +export circuit mintToSelf( + amount: Uint<64>, + pubkeys: Vector<2, Secp256k1Point>, + signatures: Vector<2, Secp256k1EcdsaSignature> +): ShieldedCoinInfo { + return Issuer_mintToSelf(amount, pubkeys, signatures); +} + +export circuit burn( + coin: ShieldedCoinInfo, + amount: Uint<128>, + refundTo: ZswapCoinPublicKey, + pubkeys: Vector<2, Secp256k1Point>, + signatures: Vector<2, Secp256k1EcdsaSignature> +): Maybe { + return Issuer_burn(coin, amount, refundTo, pubkeys, signatures); +} + +export circuit burnFromSelf( + coin: QualifiedShieldedCoinInfo, + amount: Uint<128>, + pubkeys: Vector<2, Secp256k1Point>, + signatures: Vector<2, Secp256k1EcdsaSignature> +): Maybe { + return Issuer_burnFromSelf(coin, amount, pubkeys, signatures); +} + +export pure circuit _calculateSignerId(pk: Secp256k1Point, salt: Bytes<32>): Bytes<32> { + return Issuer__calculateSignerId(pk, salt); +} + +export circuit getNonce(): Uint<64> { + return Issuer_getNonce(); +} + +export circuit tokenColor(): Bytes<32> { + return Issuer_tokenColor(); +} +``` diff --git a/content/contracts-compact/0.4.x/native-shielded-token.mdx b/content/contracts-compact/0.4.x/native-shielded-token.mdx index a7b867b2..1a1fef55 100644 --- a/content/contracts-compact/0.4.x/native-shielded-token.mdx +++ b/content/contracts-compact/0.4.x/native-shielded-token.mdx @@ -28,6 +28,8 @@ title: NativeShieldedToken [ZOwnableNativeShieldedToken]: api/native-shielded-token#ZOwnableNativeShieldedToken [mint]: api/native-shielded-token#ZOwnableNativeShieldedToken-mint [burn]: api/native-shielded-token#ZOwnableNativeShieldedToken-burn +[ShieldedTreasury _deposit]: api/multisig#ShieldedTreasury-_deposit +[compact-contracts#1112]: https://github.com/OpenZeppelin/compact-contracts/issues/1112 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. @@ -118,13 +120,19 @@ and expose [_mintToSelf] for the contract's own coins. 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. +which mints to `kernel.self()`. +`mintShieldedToken` itself claims a self-addressed coin as the contract's receive 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. +A coin minted to the contract is the contract's holding: persist the returned coin info in your own ledger state. +Because the mint already claimed it, a custody module must record the coin without calling `receiveShielded` again. +A second receive emits a duplicate Zswap output for the same commitment. +[ShieldedTreasury _deposit] claims coins on the way in, so it is not that path. +A record-only path is tracked in [compact-contracts#1112]. +Until a custody module is composed, the only way to spend a contract-held coin is [_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. @@ -151,6 +159,7 @@ so a zero `refundTo` would silently burn the change as well. (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. +Like a coin minted to self, the change is already claimed, so a custody module records it without a second `receiveShielded`. 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 diff --git a/content/contracts-compact/0.4.x/non-fungible-token.mdx b/content/contracts-compact/0.4.x/non-fungible-token.mdx index 0d31dd42..5cb1346d 100644 --- a/content/contracts-compact/0.4.x/non-fungible-token.mdx +++ b/content/contracts-compact/0.4.x/non-fungible-token.mdx @@ -3,7 +3,7 @@ title: NonFungibleToken --- {/* links */} -[non-fungible tokens]: ../contracts/5.x/tokens.mdx#different-kinds-of-tokens +[non-fungible tokens]: /contracts/5.x/tokens#different-kinds-of-tokens [EIP-721]: https://eips.ethereum.org/EIPS/eip-721 [ERC-165]: https://eips.ethereum.org/EIPS/eip-165 [Module/Contract Pattern]: ./extensibility#the-modulecontract-pattern diff --git a/content/contracts-compact/0.4.x/utils.mdx b/content/contracts-compact/0.4.x/utils.mdx index a7589eaa..4c6f5120 100644 --- a/content/contracts-compact/0.4.x/utils.mdx +++ b/content/contracts-compact/0.4.x/utils.mdx @@ -8,12 +8,20 @@ title: Utils [zeroAccount]: ./api/utils#Utils-zeroAccount [isTargetZero]: ./api/utils#Utils-isTargetZero [selfAsRecipient]: ./api/utils#Utils-selfAsRecipient +[EvmAbi API]: ./api/utils#EvmAbi +[uint8Word]: ./api/utils#EvmAbi-uint8Word +[uint64Word]: ./api/utils#EvmAbi-uint64Word +[uint128Word]: ./api/utils#EvmAbi-uint128Word +[boolWord]: ./api/utils#EvmAbi-boolWord [Ownable]: ./ownable [AccessControl]: ./access-control +[Crypto guide]: ./crypto +The Utils directory contains two modules. 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. +The EvmAbi module encodes values as Ethereum ABI words, for messages that must hash identically in Compact and on the EVM. ## Account identifiers @@ -83,6 +91,31 @@ which the token modules use as the mint and burn sentinel and for cleared approv [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. +## EvmAbi + +The [EvmAbi API] supplies the encoders a contract needs to hash the exact bytes an EVM signer hashed. +`keccak256(value)` hashes Compact's binary representation of `T`, which coincides with `abi.encode` for some types +and diverges for others: + +* `Bytes` is emitted as exactly `n` bytes. A `Bytes<32>` already is an ABI word, +and `keccak256>>(words)` is byte-identical to Solidity's `keccak256(abi.encode(word0, ..., wordN))`. +* Integers cast with `as Bytes` are emitted little-endian at their own width (a `Uint<64>` occupies 8 bytes), +while `abi.encode(uint256)` is a big-endian 32-byte word. [uint8Word], [uint64Word], and [uint128Word] produce that word. +* `Boolean` is emitted as a single byte, while `abi.encode(bool)` is a full word holding 0 or 1 in its least significant byte. +[boolWord] produces that word. + +A message is assembled as a `Vector>` of ABI words and hashed with `keccak256` to reproduce an EVM digest exactly. +A Compact `enum` reaches a digest as `uint8Word(kind as Uint<8>)`, matching Solidity, where an enum ABI-encodes as its `uint8` ordinal. +Pick the narrowest encoder that fits the value: [uint128Word] costs roughly twice [uint64Word] for the same 32-byte word. + +These are encoding primitives only. The signing envelope (a raw digest, EIP-191, or EIP-712 typed data) +belongs to the contract that owns the message; the [Crypto guide] composes EvmAbi with the Eip712 and Ecdsa modules. + + +EvmAbi needs no `keccak256` of its own and compiles on either ZKIR backend. +Hashing its words with `keccak256` requires ZKIR v3 (`--feature-zkir-v3`). + + ## Usage ```typescript