docs/examples/integration-guide.md
This guide is for wallet developers, dApp developers, and exchanges who want to support confidential tokens on Zama Protocol. It covers ERC-7984 wallet flows (showing balances via user decryption, sending transfers with encrypted inputs), as well as how to work with the Confidential Token Wrappers Registry and wrapping/unwrapping flows.
For deeper SDK details, follow the Relayer SDK guide.
By the end of this guide, you will be able to:
While building support for ERC-7984 confidential tokens in your wallet/app, you might come across the following terminology related to various parts of the Zama Protocol. A brief explanation of common terms you might encounter are:
At a high-level, to integrate Zama Protocol into a wallet or exchange, you do not need to run FHE infrastructure. You can interact with the Zama Protocol using Relayer SDK in your wallet or app. These are the steps at a high-level:
@zama-fhe/relayer-sdk NPM package.Wrapped confidential tokens allow users to convert standard ERC-20 tokens into an ERC-7984 confidential form. Once wrapped, the token behaves as a confidential token: balances and transfer amounts are encrypted on-chain. The underlying ERC-20 remains unchanged and can be recovered by unwrapping.
For the full confidential wrapper contract reference, see the Zama Protocol documentation.
For exchanges, a wrapped confidential token should be treated as fungible with its underlying ERC-20 from the user's perspective. A user who deposits USDT and a user who deposits cUSDT are depositing the same underlying asset. The exchange handles wrapping and unwrapping internally as an implementation detail.
This means exchanges should consider supporting the following flows:
In all cases, the user sees a single unified balance for the underlying asset.
When a user holds a standard ERC-20 token (e.g., USDT), wrapping converts standard ERC-20 tokens into their confidential ERC-7984 form. Wrapping deposits them into the wrapper contract for the token, with the user receiving an equivalent amount of the confidential token (e.g., cUSDT) with an encrypted balance. (In a custodial scenario, these actions can be carried out on behalf of the user.)
Prior to wrapping, the wrapper contract must be approved by the caller on the underlying ERC-20 token (a standard ERC-20 approve call).
wrapper.wrap(to, amount);
amount uses the same decimal precision as the underlying ERC-20 token.to address.Once the wrapping is complete, the confidential token can be transferred, held, or used in as assets within an exchange. Any recipient would then need to decrypt the balances or transaction amounts to utilize the tokens in a transaction.
When a user holds a wrapped confidential token (e.g., cUSDT), to get the ERC-20 equivalent back they (or someone on their behalf) needs to unwrap the confidential token.
Unwrapping is a two-step asynchronous process: first an unwrap request is made, then it is finalised once the encrypted amount has been publicly decrypted. During this process once the unwrapping is complete, the confidential tokens are and the user or their proxy receives the equivalent amount of the underlying ERC-20 token back.
Step 1: Unwrap request
wrapper.unwrap(from, to, encryptedAmount, inputProof);
from or an approved operator for from.encryptedAmount of confidential tokens is burned.Step 2: Finalise unwrap
The encrypted burned amount from the UnwrapRequested event must be publicly decrypted to obtain the cleartext amount and a decryption proof.
wrapper.finalizeUnwrap(burntAmount, cleartextAmount, decryptionProof);
This sends the corresponding amount of underlying ERC-20 tokens to the to address specified in the unwrap request.
The wrapper enforces a maximum of 6 decimals for the confidential token. When wrapping tokens with higher precision (e.g., 18-decimal tokens), amounts are rounded down and excess tokens are refunded.
| Underlying decimals | Wrapper decimals | Conversion rate | Effect |
|---|---|---|---|
| 18 | 6 | 10^12 | 1 wrapped unit = 10^12 underlying units |
| 6 | 6 | 1 | 1:1 mapping |
| 2 | 2 | 1 | 1:1 mapping |
The conversion rate and wrapper decimals can be checked on-chain:
uint256 conversionRate = wrapper.rate();
uint8 wrapperDecimals = wrapper.decimals();
The underlying ERC-20 address for any wrapped confidential token can be looked up via the Confidential Token Wrappers Registry:
(bool isValid, address token) = registry.getTokenAddress(confidentialWrapperAddress);
Wallets and exchanges should provide clear UX for both wrapping and unwrapping flows, making it obvious to the user which token they are converting between.
The Confidential Token Wrappers Registry is an on-chain contract that maps ERC-20 tokens to their corresponding ERC-7984 confidential token wrappers. It provides a canonical directory for discovering which ERC-20 tokens have official confidential wrappers.
The registry is currently deployed at:
0xeb5015fF021DB115aCe010f23F55C2591059bBA00x2f0750Bbb0A246059d80e94c454586a7F27a128eEach entry in the registry is a TokenWrapperPair struct:
struct TokenWrapperPair {
address tokenAddress; // The ERC-20 token
address confidentialTokenAddress; // The ERC-7984 wrapper
bool isValid; // false if revoked
}
A token can only be associated with one confidential wrapper, and a confidential wrapper can only be associated with one token.
Always check validity: A non-zero wrapper address may have been revoked. Always verify the
isValidflag before use.
Find the confidential wrapper for an ERC-20 token:
(bool isValid, address confidentialToken) = registry.getConfidentialTokenAddress(erc20TokenAddress);
Returns (true, wrapperAddress) if registered and valid, (false, address(0)) if never registered, or (false, wrapperAddress) if the wrapper has been revoked.
Find the underlying ERC-20 for a confidential wrapper:
(bool isValid, address token) = registry.getTokenAddress(confidentialWrapperAddress);
Returns (true, tokenAddress) if registered and valid, (false, address(0)) if never registered, or (false, tokenAddress) if the wrapper has been revoked.
Get all registered token pairs:
TokenWrapperPair[] memory pairs = registry.getTokenConfidentialTokenPairs();
Returns all registered pairs, including revoked ones. For large registries, use paginated access:
uint256 totalPairs = registry.getTokenConfidentialTokenPairsLength();
TokenWrapperPair[] memory slice = registry.getTokenConfidentialTokenPairsSlice(fromIndex, toIndex);
TokenWrapperPair memory single = registry.getTokenConfidentialTokenPair(index);
For the full registry contract reference, see the Zama Protocol documentation.
The following wrapped confidential tokens are currently registered on Ethereum mainnet:
The underlying ERC-20 address for each can be looked up using getTokenAddress() on the registry contract.
To see these concepts in action, check out the ERC-7984 demo from the zama-ai/dapps Github repository.
The demo shows how a frontend or wallet app:
pnpm install
pnpm chain
pnpm deploy:localhost
cd packages/erc7984example
pnpm run start
Step 1: On initially logging in and connect a wallet, a user's confidential token balances are not yet visible/decrypted.
Step 2: User can now sign and fetch their decrypted ERC-7984 confidential token balance. Balances are stored as ciphertext handles. To display a user's balance, read the balance handle from your token and perform user decryption with an EIP-712-authorized session in the wallet. Ensure the token grants ACL permission to the user before decrypting.
Step 3: User chooses ERC-7984 confidential token amount to send, which is encrypted, signed and sent to destination address. Follow OpenZeppelin's ERC-7984 transfer documentation for function variants and receiver callbacks. Amounts are passed as encrypted inputs that your wallet prepares with the Relayer SDK.