Get the code
dfns/dfns-solutions: confidential-token
What this demonstrates
- Confidential balances: balances live on-chain as encrypted
euint64values (Zama FHEVM). The EVM only sees ciphertext handles; amounts are never revealed on-chain. - Confidential transfers: amounts are encrypted client-side by the Zama relayer SDK before submission. The on-chain transaction carries only the ciphertext and an input proof.
- Holder-only reveal: the holder signs an EIP-712 user-decryption request with their DFNS wallet. The Zama KMS decrypts the balance and re-encrypts it to a one-time NaCl keypair before returning it to the client.
- Public reveal: the holder marks their balance handle publicly decryptable on-chain. Any client can then fetch the cleartext from the relayer.
How it works
Reference implementation
What you’ll need
- A DFNS account
- A service account with these permissions:
Wallets:BroadcastTransactionon the bank, sender, and receiver walletsWallets:GenerateSignatureon the sender and receiver wallets (for reveal flows)Wallets:Readon all three
- Three DFNS wallets on Ethereum Sepolia:
bank: deploys the contract and mints tokenssender: receives the mint and initiates transfersreceiver: receives transfers
- Testnet ETH on all three wallets (Sepolia faucet). The bank wallet needs more, since it deploys the contract and signs the mint.
- Node.js v22+
Setup
1
Clone and install
2
Configure environment variables
.env
3
Compile contracts
Running the demo
Deploy
ConfidentialToken from the bank wallet and writes the token address to deployment.json.
Mint
(tokenAddress, bankAddress) before submission. The on-chain call carries only the ciphertext handle and an input proof.
Transfer
(tokenAddress, senderAddress), and the contract updates both encrypted balances without ever learning the amounts.
Reveal balances
Holder-only reveal: the holder signs an EIP-712 authorization with their DFNS wallet; the Zama KMS decrypts and returns the balance only to them:How DFNS fits in
DFNS is involved at two distinct points in the flow. Broadcasting transactions. Every on-chain call (deploy, mint, transfer, and therequestDiscloseEncryptedAmount call in the public reveal) is broadcast via wallets.broadcastTransaction. The application never holds a private key; it constructs the calldata and hands it to DFNS.
Signing EIP-712 user-decrypt requests. The holder-only reveal requires an EIP-712 signature authorizing the Zama relayer to decrypt a specific encrypted handle. This is a standard Zama user-decryption request. DFNS signs it via wallets.generateSignature with kind: 'Eip712' instead of a local signer.
FHE notes
Input binding. When the relayer SDK encrypts an amount, the ciphertext is bound to a(contractAddress, callerAddress) pair. The FHEVM input verifier checks this binding on-chain: a ciphertext encrypted for the bank cannot be submitted by the sender, and a ciphertext encrypted for the token contract cannot be replayed against a different contract.
Handle-based storage. The contract stores 32-byte handles, not ciphertexts. The actual ciphertexts live off-chain in the FHEVM coprocessor. The EVM runs homomorphic add and sub on handles, keeping gas costs low.
ACL. The on-chain Access Control List (ACL) maps handles to addresses permitted to decrypt them. FHE.allow(handle, addr) grants user-decrypt rights; FHE.makePubliclyDecryptable(handle) grants the public role. The KMS refuses to decrypt without a matching ACL entry.
Decimals. ERC-7984 uses 6 decimals (like USDC). 1,000 cUSD = 1_000_000_000 raw units.
Related
Swap confidential tokens
Atomic ERC-20 ↔ ERC-7984 swaps including async KMS decryption and two-party encrypted exchange
Issue tokenized bonds
ERC-20 bond lifecycle with DFNS signing
Build programmable approval policies
Service accounts that decode ABI call data
Wallets API
broadcastTransaction and generateSignature reference