Concealed Balances
The Concealed Layer. Balances are stored as ElGamal ciphertexts on-chain. Transfer amounts are hidden from public observers, but the transaction graph (who sends to whom) remains visible.
Below we present the details of how the system is designed.
User Accounts
Each user has an EVM account addr, and to enter the system they generate a fresh Grumpkin Curve key pair (BSK, BPK) (balance secret key, and, balance public key) and register their BPK in the contract on-chain by calling the registerBPK(...) method. This way their EVM address addr is bound to their BPK. The registration is immutable -- once set, the BPK for an address cannot be changed.
Balances
Suppose a user has registered their BPK key in the contract. Now, for each supported token (assume for the sake of this description there is just one), a balance map holds a value balance[BPK] being the El-Gamal encryption of a value amount: u64 (the current balance of the user). More specifically, the ciphertext is a pair of curve points (R, C) where:
R = r * G
C = amount * G + r * BPK
Here r is a random scalar (the randomizing element) and G is the Grumpkin generator. The storage uses four field elements: (R_x, R_y, C_x, C_y).
An important thing to note is that the balance of user A is then encrypted by this user's public key, meaning that we expect only this particular user to be able to learn their balance.
The second thing to note is that an exponential encoding is used in the encryption scheme. This means that when decrypting the user obtains a point M = m*G but recovering m is not immediate. In fact the user must solve a discrete logarithm problem to get it. See Discrete Log Solver.
Operations
Gas Abstraction
The system is designed so that a user without access to gas tokens of the underlying chain can still perform each operation. Gas sponsorship is provided by an ERC-4337 Paymaster. Users construct their transactions as ERC-4337 UserOperations, which are submitted by a bundler and have their gas paid by the Paymaster. The user never needs to hold ETH or interact with the chain directly.
In the descriptions below we focus on the cryptographic and contract-level logic of each operation, and omit the ERC-4337 wrapping for clarity. Every operation described can be submitted as a UserOperation sponsored by the Paymaster.
Conceal (Public to Concealed)
TODO
Transfer (Concealed to Concealed)
Suppose user A wants to send amount of tokens to user B. A zk-circuit is run with the following public inputs:
amountc_A_old-- the initial encrypted balance of userAc_A_new-- the new encrypted balance of userAc_amount-- the encrypted value ofamount
The following constraints are verified in the circuit:
- There is a value
balance_oldthat is the decryption ofc_A_oldandbalance_old >= amount balance_old = balance_new + amountc_A_newis the encryption ofbalance_newunder keyAc_amountis the encryption ofamountunder keyB
Part of the transaction payload is the proof for the above statement and the contract updates balances accordingly:
balance[A] = c_A_newbalance[B] = balance[B] + c_amount
There is a tradeoff on how much gas the call costs, vs how expensive is the proof computation client-side. For instance, one could move the computation of c_amount from the circuit to the contract. This will make the proof faster to compute at the cost of increased gas expenditure.
Reveal (Concealed to Public)
Suppose user A wants to withdraw amount tokens back to a public EVM address recipient. A zk-circuit is run with the following public inputs:
c_A_old-- the initial encrypted balance of userAc_A_new-- the new encrypted balance of userAamount-- the reveal amount (public, unlike in transfer)recipient-- the destination EVM address
The following constraints are verified in the circuit:
- The prover knows the secret key corresponding to
BPK_A - There is a value
balance_oldthat is the decryption ofc_A_oldandbalance_old >= amount balance_old = balance_new + amountc_A_newis the encryption ofbalance_newunder keyA
The recipient address is bound as a public input to the circuit. This ensures the proof cannot be front-run and redirected in the mempool — an attacker who copies the proof and submits it with a different recipient will fail verification.
The contract updates state accordingly:
balance[A] = c_A_newamountpublic tokens are minted torecipient
Since amount is public, there is no need to encrypt it under a recipient key -- the circuit is simpler than the transfer circuit.
Autoconceal
A user who has registered their BPK can call setAutoconceal(true) on the contract. This sets a per-address boolean flag autoconceal[addr] = true in storage. Once enabled, the contract overrides the standard ERC-20 transfer and transferFrom functions: when anyone sends public tokens to that address, instead of crediting the recipient's public balance, the contract burns the tokens and adds the encrypted amount to balance[BPK] -- the same operation as a conceal with r = 0.
This is transparent to the sender -- they call a normal ERC-20 transfer and the interception happens inside the contract. No ZK proof is involved; the encryption is done on-chain.