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:
- ConfidentialFungibleToken: the token with ElGamal-encrypted balances.
- ConfidentialFungibleTokenPublicSupply: an extension that tracks a public total supply.
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.
_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.
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.
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.
isRegistered(account: Bytes<32>) → Boolean
circuit
#Returns whether account has registered an encryption key.
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.
_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:
- Contract is initialized.
valueis within MAX_TRANSFER_VALUE.accountis registered.
_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.
valueis within MAX_TRANSFER_VALUE.- The caller is registered and has a balance of at least
value.
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.
valueis within MAX_TRANSFER_VALUE.- The caller is registered with a balance of at least
value. tois registered and is not the caller (no self-transfer).
_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. tois registered.
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.
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.
valueis within MAX_TRANSFER_VALUE.spenderis registered, is not the zero account, and is not the caller.- The caller is registered with a balance of at least
value.
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.
valueis within MAX_TRANSFER_VALUE.tois registered and is not the zero account.- An escrow from
fromAddressto the caller exists with at leastvalue. fromAddressand the caller are registered.
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.
expectedEpochequals the caller's current credit epoch.
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.
valueis within MAX_TRANSFER_VALUE.- An escrow from
fromAddressto the caller exists with at leastvalue. fromAddressand the caller are registered.
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 + valuedoes not overflowUint<128>.
_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:
- totalSupply is at least
value.
totalSupply() → Uint<128>
circuit
#Returns the public total token supply.