Skip to main content
Version: 0.3.0

Encrypted Balances and ElGamal homomorphic encryption

An encrypted balance is a ciphertext stored by the token contract for an encrypted account — it represents a token amount, but neither the contract nor external observer can read that amount. Encrypted accounts are the accounts behind zk1... addresses, each of which encodes an EPK (encryption public key) used to receive encrypted credits. Spending from the encrypted account is controlled by a controller address. The link between the EPK and controller must be registered on-chain before the account can receive those credits; the SDK account-creation flow handles that first chain interaction. For the exact address format and key derivation, see the SDK keys and addresses guide.

What is stored on-chain

For each encrypted account, the contract stores an ElGamal ciphertext for the current amount. Only the holder of the corresponding secret key can decrypt the balance locally. Public observers and contracts see only ciphertexts.

Why ElGamal

The system needs the contract to update balances without decrypting them.

ElGamal is a public-key encryption scheme, and the variant used here — ElGamal in exponential form, also called lifted ElGamal — has a property called additive homomorphism: ciphertexts encrypted under the same public key can be added together, and the result is a valid ciphertext for the sum of the underlying amounts under that same key. When the contract adds an incoming ciphertext encrypted for the recipient EPK to the recipient's existing balance ciphertext, it gets a ciphertext for the new summed balance.

This lets the contract update encrypted balances without decrypting them. The contract updates the ciphertext, but never learns the old balance, the transfer amount, or the new balance.

Role of randomness in encryption

For hidden-amount operations, encryption uses randomness so that the same amount encrypted twice does not look the same.

This matters because token amounts often repeat. Without randomness, encrypting 100 would always produce the same ciphertext, and observers could recognize repeated values. Hidden-amount operations use fresh randomness so ciphertexts are unlinkable by amount.

This creates a separate question: if the contract cannot read the values, how can it reject malformed ciphertexts? Zero-knowledge proofs are the answer: they let the prover convince the contract that the hidden operation follows the rules. The next concept page explains them more directly.

The zero-knowledge proof checks that prover-generated hidden-amount ciphertexts were formed correctly, including consistency with the supplied randomness. It does not prove that the randomness was fresh or high-entropy. In the SDK/prover path, that randomness is sampled fresh for hidden-amount encryptions.

Public-amount credits are different. For example, public-to-encrypted deposits and auto-encrypted public transfers already expose the amount on-chain, so the contract can use a deterministic ciphertext for that public amount.

How operations are built from these pieces

So far we have three ingredients: ciphertexts the contract can add together, randomness that keeps repeated amounts unlinkable, and the rule that public amounts can be encrypted deterministically. The third one matters more than it may seem at first glance: if an amount is already public, the contract can build its ciphertext on its own and check that it is correct, without trusting anyone to do it off-chain. Every operation in the Encrypted Balances layer is built from some combination of these.

The simplest operation is a public-to-encrypted deposit. The amount starts visible in the public ERC-20 layer, so the contract debits it publicly, encrypts it deterministically (no randomness needed — the amount is already public), and adds the resulting ciphertext to the recipient's encrypted balance. One subtraction, one addition, and because the contract itself constructed every value involved, no zero-knowledge proof is needed. This is the warm-up case.

The more interesting operation is an encrypted transfer, where both the balances and the amount are hidden. The contract now has to update two encrypted balances — the sender's and the recipient's — using ciphertexts it cannot read, that are encrypted under different keys, and that were prepared by the sender off-chain. It has to be convinced that those ciphertexts fit together honestly: that the sender actually had enough, that the amount leaving the sender matches the amount arriving at the recipient, and that nothing has been fabricated along the way. This is where the zero-knowledge proof does its work — it lets the sender prove all of that without revealing any of the hidden values.

Encrypted transfers in detail

An encrypted transfer moves a hidden amount from one encrypted account to another. Because the contract cannot see any of the values involved, the sender's SDK does the heavy lifting off-chain: it prepares the necessary ciphertexts, generates a proof that they are consistent, and submits everything to the contract for verification.

The ciphertexts involved

A single transfer uses one ciphertext already stored on-chain and submits three new ciphertexts:

CiphertextEncrypted underWhy
Sender's old balancesender EPKAlready stored on-chain before the transfer
Submitted sender's new balancesender EPKSo the sender can read the remaining balance afterward
Submitted transfer amount (for recipient)recipient EPKSo the recipient can decrypt the incoming amount
Submitted transfer amount (for compliance)Guardian Committee compliance public keySo controlled disclosure can recover the amount when authorized

The last two encrypt the same amount under different keys — that equality is one of the things the proof has to enforce.

What the proof attests to

The private witness includes the sender encryption secret key, cleartext balance and amount values, and encryption randomness. The proof convinces the contract of four things at once:

  • the sender knows the encryption secret key behind the sender EPK,
  • the old balance was at least as large as the transfer amount (no overdraft),
  • the new sender balance equals the old balance minus the hidden transfer amount (the debit is correct),
  • the recipient and compliance ciphertexts encrypt that same hidden amount (no value is created or skimmed in transit).

What the contract does

Verifying the proof is not, by itself, enough to move funds. Authorization is checked separately, either by controller signature in the standard SDK flow or by an authorized controller caller in direct paths.

Once both checks pass, the contract replaces the sender's stored balance ciphertext with the new one and adds the transfer ciphertext to the recipient's stored balance. No decryption happens on-chain at any point — the contract has updated two hidden balances using only ciphertexts and a proof that the update was honest.

Where to go deeper