API Reference

ConfidentialFungibleToken API

This module provides the full ConfidentialFungibleToken module API.

For an overview of the module, read the ConfidentialFungibleToken guide. The ElGamal_Ciphertext and EcdhMask_Ciphertext types used below are documented in the Crypto API.

The page covers the following modules:

import "./node_modules/@openzeppelin/compact-contracts/token/ConfidentialFungibleToken";

Types

struct EscrowEntry { spenderCt: ElGamal_Ciphertext; ownerCt: ElGamal_Ciphertext; ownerMemo: EcdhMask_Ciphertext; }

struct

#

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

_balances: Map<Bytes<32>, ElGamal_Ciphertext>

ledger

#

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, 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 + pendingOf.

_pending: Map<Bytes<32>, ElGamal_Ciphertext>

ledger

#

Incoming (unconfirmed) balances, encrypted under each account's ElGamal key. Credits land here rather than in _balances; the owner moves them across with sweep.

_encryptionKeys: Map<Bytes<32>, JubjubPoint>

ledger

#

Mapping from account identifiers to their ElGamal public key, set by 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: Map<Bytes<32>, List<EcdhMask_Ciphertext>>

ledger

#

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, so the newest memo comes first. Memos record credits only; debits leave none.

_escrow: Map<Bytes<32>, Map<Bytes<32>, EscrowEntry>>

ledger

#

Mapping from owner accounts to spender accounts to their 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: Map<Bytes<32>, Map<Bytes<32>, Counter>>

ledger

#

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: Map<Bytes<32>, Counter>

ledger

#

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 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: Boolean

ledger

#

Whether initialize has run. Guards against re-initialization and against use before initialization.

_name: Opaque<"string">

sealed ledger

#

The token's human-readable name, set once at construction.

_symbol: Opaque<"string">

sealed ledger

#

The token's ticker symbol, set once at construction.

_decimals: Uint<8>

sealed ledger

#

The number of decimals used to display balances, set once at construction. Purely presentational; no circuit scales by it.

Witnesses

The TypeScript witnesses in the repository are test-only and are not published; a consumer authors its own.

wit_ConfidentialTokenSK() → Bytes<32>

witness

#

Returns the caller's account secret. Verified in-circuit by matching persistentHash(SK) against the public accountId.

wit_ConfidentialTokenEK() → Bytes<32>

witness

#

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(ct: ElGamal_Ciphertext) → Uint<248>

witness

#

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<ElGamal_Ciphertext, Uint248> lookup.

Verified in-circuit by ElGamal_assertDecryptsTo: a wrong plaintext fails the assertion Dec(ct, EK) == claimedValue.

wit_RandomnessSeed() → Bytes<32>

witness

#

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

MAX_TRANSFER_VALUE() → Uint<128>

pure

#

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(
  name_: Opaque<"string">,
  symbol_: Opaque<"string">,
  decimals_: Uint<8>
) → []

circuit

#

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(account: Bytes<32>) → ElGamal_Ciphertext

circuit

#

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.
k=10, rows=984

pendingOf(account: Bytes<32>) → ElGamal_Ciphertext

circuit

#

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.
k=10, rows=981

allowance(owner: Bytes<32>, spender: Bytes<32>) → EscrowEntry

circuit

#

Returns the escrow entry (the encrypted allowance) that owner granted spender. When no allowance exists, returns a placeholder entry rather than default<EscrowEntry>: 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.

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 stores a real memo even for 0. This getter grants nothing, the spend path (transferFrom/burnFrom) gates on _escrow membership independently, so a non-existent allowance is unspendable.

k=10, rows=936

isRegistered(account: Bytes<32>) → Boolean

circuit

#

Returns whether account has registered an encryption key.

k=9, rows=303

register() → Bytes<32>

circuit

#

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. 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.
k=13, rows=8046

_mint(account: Bytes<32>, value: Uint<128>) → []

circuit

#

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:

k=15, rows=26962

_burn(value: Uint<128>) → Bytes<32>

circuit

#

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.
  • The caller is registered and has a balance of at least value.
k=14, rows=14889

transfer(to: Bytes<32>, value: Uint<128>) → Bytes<32>

circuit

#

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.
  • The caller is registered with a balance of at least value.
  • to is registered and is not the caller (no self-transfer).
k=16, rows=41623

_move(to: Bytes<32>, value: Uint<128>) → Bytes<32>

circuit

#

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.
  • The caller is registered with a spendable balance of at least value.
  • to is registered.
k=16, rows=41612

sweep() → Bytes<32>

circuit

#

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).

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.
k=13, rows=7541

approve(spender: Bytes<32>, value: Uint<128>) → Bytes<32>

circuit

#

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.

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.

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.
  • spender is registered, is not the zero account, and is not the caller.
  • The caller is registered with a balance of at least value.
k=16, rows=42938

transferFrom(
  fromAddress: Bytes<32>,
  to: Bytes<32>,
  value: Uint<128>
) → Bytes<32>

circuit

#

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 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.
  • fromAddress and the caller are registered.
k=17, rows=68924

clearMemos(expectedEpoch: Uint<64>) → []

circuit

#

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.
k=13, rows=7019

computeAccountId(sk: Bytes<32>) → Bytes<32>

pure

#

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(fromAddress: Bytes<32>, value: Uint<128>) → Bytes<32>

circuit

#

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 within MAX_TRANSFER_VALUE.
  • An escrow from fromAddress to the caller exists with at least value.
  • fromAddress and the caller are registered.
k=16, rows=42205

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

_totalSupply: Uint<128>

ledger

#

Total tokens in circulation, tracked in the clear even though balances are encrypted. Raised by _addSupply and lowered by _subSupply, each rejecting over/underflow.

Mint and burn disclose their amounts through the delta on this value.

Witnesses

None.

Circuits

_addSupply(value: Uint<128>) → []

circuit

#

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>.
k=9, rows=399

_subSupply(value: Uint<128>) → []

circuit

#

Decreases totalSupply by value. Call alongside the token module's _burn or _burnFrom. The removed amount is disclosed via the supply delta.

Requirements:

k=9, rows=254

totalSupply() → Uint<128>

circuit

#

Returns the public total token supply.

k=6, rows=24