|
| 1 | +# Confidential token integration guide for Wallets and Exchanges |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +For deeper SDK details, follow the [Relayer SDK guide](https://docs.zama.org/protocol/relayer-sdk-guides/). |
| 6 | + |
| 7 | +By the end of this guide, you will be able to: |
| 8 | + |
| 9 | +- Understand [Zama Protocol](../protocol/architecture/overview.md) at a high-level. |
| 10 | +- Build ERC-7984 confidential token transfers using encrypted inputs. |
| 11 | +- Display ERC-7984 confidential token balances. |
| 12 | +- Query the Confidential Token Wrappers Registry to discover wrapped token pairs. |
| 13 | +- Understand the wrapping and unwrapping flow between ERC-20 and ERC-7984 tokens. |
| 14 | + |
| 15 | +## **Core concepts in this guide** |
| 16 | + |
| 17 | +While building support for [ERC-7984 confidential tokens](https://eips.ethereum.org/EIPS/eip-7984) in your wallet/app, you might come across the following terminology related to [various parts of the Zama Protocol](../protocol/architecture/overview.md). A brief explanation of common terms you might encounter are: |
| 18 | + |
| 19 | +- **FHEVM**: Zama's [FHEVM library](../protocol/architecture/library.md) that supports computations on encrypted values. Encrypted values are represented on‑chain as **ciphertext handles** (bytes32). |
| 20 | +- **Host chain**: The EVM network your users connect to in a wallet with confidential smart contracts. Example: Ethereum / Ethereum Sepolia. |
| 21 | +- **Gateway chain**: Zama's Arbitrum L3 [Gateway chain](../protocol/architecture/gateway.md) that coordinates FHE encryptions/decryptions. |
| 22 | +- **Relayer**: Off‑chain [Relayer](../protocol/architecture/relayer_oracle.md) that registers encrypted inputs, coordinate decryptions, and return results to users or contracts. Wallets and dApps talk to the Relayer via the JavaScript SDK. |
| 23 | +- **ACL:** Access control for ciphertext handles. Contracts grant per‑address permissions so a user can read data they should have access to. |
| 24 | +- **Native confidential token**: An ERC-7984 token where balances and transfer amounts are encrypted by default. The token is natively confidential and is not derived from an underlying ERC-20. |
| 25 | +- **Wrapped confidential token**: A standard ERC-20 token that has been wrapped into an ERC-7984 confidential form via a wrapper contract. The underlying ERC-20 remains unchanged. |
| 26 | +- **Confidential Token Wrappers Registry**: An on-chain registry that maps ERC-20 tokens to their corresponding ERC-7984 confidential token wrappers. |
| 27 | + |
| 28 | +## Wallet and exchange integration at a glance |
| 29 | + |
| 30 | +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](https://docs.zama.org/protocol/relayer-sdk-guides) in your wallet or app. These are the steps at a high-level: |
| 31 | + |
| 32 | +1. **Relayer SDK initialization** in web app, browser extension, or mobile app. Follow the [setup guide for Relayer SDK](https://docs.zama.org/protocol/relayer-sdk-guides/development-guide/webapp). In browser contexts, importing the library via the CDN links is easiest. Alternatively, do this by importing the `@zama-fhe/relayer-sdk` NPM package. |
| 33 | +2. [Configure and initialize settings](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer/initialization) for the library. |
| 34 | +3. **Confidential token (ERC-7984) basics**: |
| 35 | + - Show encrypted balances using **user decryption**. |
| 36 | + - Build **transfers** using encrypted inputs. Refer to [OpenZeppelin's ERC-7984 token guide](https://docs.openzeppelin.com/confidential-contracts/token). |
| 37 | + - Manage **operators** for delegated transfers with an expiry, including clear revoke UX. |
| 38 | + |
| 39 | +### **What wallets and exchanges should support** |
| 40 | + |
| 41 | +- **Transfers**: Support the ERC-7984 transfer variants documented by OpenZeppelin, including forms that use an input proof and optional receiver callbacks. |
| 42 | +- **Operators**: Operators can move any amount during an active window. Your UX must capture an expiry, show risk clearly, and make revoke easy. |
| 43 | +- **Events and metadata**: Names and symbols behave like conventional tokens, but on-chain amounts remain encrypted. Render user-specific amounts after user decryption. |
| 44 | + |
| 45 | +## Wrapping and unwrapping |
| 46 | + |
| 47 | +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. |
| 48 | + |
| 49 | +For the full confidential wrapper contract reference, see the [Zama Protocol documentation](https://docs.zama.org/protocol/protocol-apps/confidential-wrapper). |
| 50 | + |
| 51 | +### Fungibility between ERC-20 and confidential tokens |
| 52 | + |
| 53 | +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. |
| 54 | + |
| 55 | +This means exchanges should consider supporting the following flows: |
| 56 | + |
| 57 | +- **User deposits ERC-20** (e.g., USDT): the exchange wraps it into the confidential form (cUSDT) if needed for on-chain operations. |
| 58 | +- **User deposits confidential token** (e.g., cUSDT): no wrapping needed; the exchange credits the same underlying balance. |
| 59 | +- **User withdraws as ERC-20** (e.g., USDT): the exchange unwraps the confidential token and sends standard ERC-20. |
| 60 | +- **User withdraws as confidential token** (e.g., cUSDT): no unwrapping needed; the exchange sends the confidential token directly. |
| 61 | + |
| 62 | +In all cases, the user sees a single unified balance for the underlying asset. |
| 63 | + |
| 64 | +### How wrapping works |
| 65 | + |
| 66 | +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.) |
| 67 | + |
| 68 | +Prior to wrapping, the wrapper contract must be approved by the caller on the underlying ERC-20 token (a standard ERC-20 `approve` call). |
| 69 | + |
| 70 | +```solidity |
| 71 | +wrapper.wrap(to, amount); |
| 72 | +``` |
| 73 | + |
| 74 | +- `amount` uses the same decimal precision as the underlying ERC-20 token. |
| 75 | +- The wrapper mints the corresponding confidential tokens to the `to` address. |
| 76 | +- Due to decimal conversion (see below), any excess tokens below the conversion rate are refunded to the caller. |
| 77 | + |
| 78 | +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. |
| 79 | + |
| 80 | +### How unwrapping works |
| 81 | + |
| 82 | +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. |
| 83 | + |
| 84 | +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. |
| 85 | + |
| 86 | +**Step 1: Unwrap request** |
| 87 | + |
| 88 | +```solidity |
| 89 | +wrapper.unwrap(from, to, encryptedAmount, inputProof); |
| 90 | +``` |
| 91 | + |
| 92 | +- The caller must be `from` or an approved operator for `from`. |
| 93 | +- The `encryptedAmount` of confidential tokens is burned. |
| 94 | +- No transfer of underlying ERC-20 tokens happens in this step. |
| 95 | + |
| 96 | +**Step 2: Finalise unwrap** |
| 97 | + |
| 98 | +The encrypted burned amount from the `UnwrapRequested` event must be [publicly decrypted](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer/decryption/public-decryption) to obtain the cleartext amount and a decryption proof. |
| 99 | + |
| 100 | +```solidity |
| 101 | +wrapper.finalizeUnwrap(burntAmount, cleartextAmount, decryptionProof); |
| 102 | +``` |
| 103 | + |
| 104 | +This sends the corresponding amount of underlying ERC-20 tokens to the `to` address specified in the unwrap request. |
| 105 | + |
| 106 | +### Decimal conversion |
| 107 | + |
| 108 | +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. |
| 109 | + |
| 110 | +| Underlying decimals | Wrapper decimals | Conversion rate | Effect | |
| 111 | +| --- | --- | --- | --- | |
| 112 | +| 18 | 6 | 10^12 | 1 wrapped unit = 10^12 underlying units | |
| 113 | +| 6 | 6 | 1 | 1:1 mapping | |
| 114 | +| 2 | 2 | 1 | 1:1 mapping | |
| 115 | + |
| 116 | +The conversion rate and wrapper decimals can be checked on-chain: |
| 117 | + |
| 118 | +```solidity |
| 119 | +uint256 conversionRate = wrapper.rate(); |
| 120 | +uint8 wrapperDecimals = wrapper.decimals(); |
| 121 | +``` |
| 122 | + |
| 123 | +### Finding the underlying token |
| 124 | + |
| 125 | +The underlying ERC-20 address for any wrapped confidential token can be looked up via the [Confidential Token Wrappers Registry](#confidential-token-wrappers-registry): |
| 126 | + |
| 127 | +```solidity |
| 128 | +(bool isValid, address token) = registry.getTokenAddress(confidentialWrapperAddress); |
| 129 | +``` |
| 130 | + |
| 131 | +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. |
| 132 | + |
| 133 | +## Confidential Token Wrappers Registry |
| 134 | + |
| 135 | +The [Confidential Token Wrappers Registry](https://docs.zama.org/protocol/protocol-apps/registry-contract) 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. |
| 136 | + |
| 137 | +The registry is currently deployed at: |
| 138 | + |
| 139 | +- **Ethereum mainnet**: [`0xeb5015fF021DB115aCe010f23F55C2591059bBA0`](https://etherscan.io/address/0xeb5015fF021DB115aCe010f23F55C2591059bBA0) |
| 140 | +- **Sepolia testnet**: [`0x2f0750Bbb0A246059d80e94c454586a7F27a128e`](https://sepolia.etherscan.io/address/0x2f0750Bbb0A246059d80e94c454586a7F27a128e) |
| 141 | + |
| 142 | +Each entry in the registry is a `TokenWrapperPair` struct: |
| 143 | + |
| 144 | +```solidity |
| 145 | +struct TokenWrapperPair { |
| 146 | + address tokenAddress; // The ERC-20 token |
| 147 | + address confidentialTokenAddress; // The ERC-7984 wrapper |
| 148 | + bool isValid; // false if revoked |
| 149 | +} |
| 150 | +``` |
| 151 | + |
| 152 | +A token can only be associated with one confidential wrapper, and a confidential wrapper can only be associated with one token. |
| 153 | + |
| 154 | +> **Always check validity:** A non-zero wrapper address may have been revoked. Always verify the `isValid` flag before use. |
| 155 | +
|
| 156 | +### Querying the registry |
| 157 | + |
| 158 | +**Find the confidential wrapper for an ERC-20 token:** |
| 159 | + |
| 160 | +```solidity |
| 161 | +(bool isValid, address confidentialToken) = registry.getConfidentialTokenAddress(erc20TokenAddress); |
| 162 | +``` |
| 163 | + |
| 164 | +Returns `(true, wrapperAddress)` if registered and valid, `(false, address(0))` if never registered, or `(false, wrapperAddress)` if the wrapper has been revoked. |
| 165 | + |
| 166 | +**Find the underlying ERC-20 for a confidential wrapper:** |
| 167 | + |
| 168 | +```solidity |
| 169 | +(bool isValid, address token) = registry.getTokenAddress(confidentialWrapperAddress); |
| 170 | +``` |
| 171 | + |
| 172 | +Returns `(true, tokenAddress)` if registered and valid, `(false, address(0))` if never registered, or `(false, tokenAddress)` if the wrapper has been revoked. |
| 173 | + |
| 174 | +**Get all registered token pairs:** |
| 175 | + |
| 176 | +```solidity |
| 177 | +TokenWrapperPair[] memory pairs = registry.getTokenConfidentialTokenPairs(); |
| 178 | +``` |
| 179 | + |
| 180 | +Returns all registered pairs, including revoked ones. For large registries, use paginated access: |
| 181 | + |
| 182 | +```solidity |
| 183 | +uint256 totalPairs = registry.getTokenConfidentialTokenPairsLength(); |
| 184 | +TokenWrapperPair[] memory slice = registry.getTokenConfidentialTokenPairsSlice(fromIndex, toIndex); |
| 185 | +TokenWrapperPair memory single = registry.getTokenConfidentialTokenPair(index); |
| 186 | +``` |
| 187 | + |
| 188 | +For the full registry contract reference, see the [Zama Protocol documentation](https://docs.zama.org/protocol/protocol-apps/registry-contract). |
| 189 | + |
| 190 | +### Currently registered confidential tokens |
| 191 | + |
| 192 | +The following wrapped confidential tokens are currently registered on Ethereum mainnet: |
| 193 | + |
| 194 | +| Confidential token | Address | |
| 195 | +| --- | --- | |
| 196 | +| cUSDC | [`0xe978F22157048E5DB8E5d07971376e86671672B2`](https://etherscan.io/address/0xe978F22157048E5DB8E5d07971376e86671672B2) | |
| 197 | +| cUSDT | [`0xAe0207C757Aa2B4019Ad96edD0092ddc63EF0c50`](https://etherscan.io/address/0xAe0207C757Aa2B4019Ad96edD0092ddc63EF0c50) | |
| 198 | +| cWETH | [`0xda9396b82634Ea99243cE51258B6A5Ae512D4893`](https://etherscan.io/address/0xda9396b82634Ea99243cE51258B6A5Ae512D4893) | |
| 199 | +| cBRON | [`0x85dE671c3bec1aDeD752c3Cea943521181C826bc`](https://etherscan.io/address/0x85dE671c3bec1aDeD752c3Cea943521181C826bc) | |
| 200 | +| cZAMA | [`0x80CB147Fd86dC6dEe3Eee7e4Cee33d1397d98071`](https://etherscan.io/address/0x80CB147Fd86dC6dEe3Eee7e4Cee33d1397d98071) | |
| 201 | +| cTGBP | [`0xa873750ccbafd5ec7dd13bfd5237d7129832edd9`](https://etherscan.io/address/0xa873750ccbafd5ec7dd13bfd5237d7129832edd9) | |
| 202 | + |
| 203 | +The underlying ERC-20 address for each can be looked up using `getTokenAddress()` on the registry contract. |
| 204 | + |
| 205 | +## Quick start: ERC-7984 example app |
| 206 | + |
| 207 | +To see these concepts in action, check out the [ERC-7984 demo](https://github.com/zama-ai/dapps/tree/main/packages/erc7984example) from the [zama-ai/dapps](https://github.com/zama-ai/dapps) Github repository. |
| 208 | + |
| 209 | +The demo shows how a frontend or wallet app: |
| 210 | + |
| 211 | +1. [**Register encrypted inputs**](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer/input) for contract calls such as confidential token transfers. |
| 212 | +2. Request [**User decryption**](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer/decryption/user-decryption) so users can view private data like balances. |
| 213 | + |
| 214 | +### Run locally |
| 215 | + |
| 216 | +1. Clone the [zama-ai/dapps](https://github.com/zama-ai/dapps) Github repository |
| 217 | +2. Install dependencies and deploy a local Hardhat chain |
| 218 | + |
| 219 | +```bash |
| 220 | +pnpm install |
| 221 | +pnpm chain |
| 222 | +pnpm deploy:localhost |
| 223 | +``` |
| 224 | + |
| 225 | +1. Navigate to the [ERC-7984 demo](https://github.com/zama-ai/dapps/tree/main/packages/erc7984example) folder in the cloned repo |
| 226 | + |
| 227 | +```bash |
| 228 | +cd packages/erc7984example |
| 229 | +``` |
| 230 | + |
| 231 | +1. Run the demo application on local Hardhat chain |
| 232 | + |
| 233 | +```bash |
| 234 | +pnpm run start |
| 235 | +``` |
| 236 | + |
| 237 | +### Steps demonstrated by the ERC-7984 demo app |
| 238 | + |
| 239 | +**Step 1**: On initially logging in and connect a wallet, a user's confidential token balances are not yet visible/decrypted. |
| 240 | + |
| 241 | + |
| 242 | + |
| 243 | +**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**](https://docs.zama.ai/protocol/relayer-sdk-guides/v0.1/fhevm-relayer/decryption/user-decryption) with an EIP-712-authorized session in the wallet. Ensure the token grants ACL permission to the user before decrypting. |
| 244 | + |
| 245 | + |
| 246 | + |
| 247 | + |
| 248 | + |
| 249 | +**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**](https://docs.openzeppelin.com/confidential-contracts/token#transfer) for function variants and receiver callbacks. Amounts are passed as encrypted inputs that your wallet prepares with the Relayer SDK. |
| 250 | + |
| 251 | + |
| 252 | + |
| 253 | +## **UI and UX recommendations** |
| 254 | + |
| 255 | +- **Caching**: Cache decrypted values client‑side for the session lifetime. Offer a refresh action that repeats the flow. |
| 256 | +- **Permissions:** treat user decryption as a permission grant with scope and duration. Show which contracts are included and when access expires. |
| 257 | +- **Indicators:** use distinct icons or badges for encrypted amounts. Avoid showing zero when a value is simply undisclosed. |
| 258 | +- **Operator visibility**: always show current operator approvals with expiry and a one-tap revoke |
| 259 | +- **Wrapping/unwrapping**: clearly indicate which token a user is converting between. Show the underlying ERC-20 token name and symbol alongside the confidential token when displaying wrapped tokens. |
| 260 | +- **Failure modes:** differentiate between decryption denied, missing ACL grant, and expired decryption session. Offer guided recovery actions. |
| 261 | + |
| 262 | +## **Testing and environments** |
| 263 | + |
| 264 | +- **Testnet configuration:** Start with the SDK's built‑in Sepolia configuration or a local Hardhat network. Swap to other supported networks by replacing the config object. Keep chain selection in a single source of truth in your app. |
| 265 | +- **Mocks:** for unit tests, prefer SDK mocked mode or local fixtures that bypass the Gateway but maintain identical call shapes for your UI logic. |
| 266 | + |
| 267 | +## Further reading |
| 268 | + |
| 269 | +- Detailed [**confidential contracts guide from OpenZeppelin**](https://docs.openzeppelin.com/confidential-contracts) (besides ERC-7984) |
| 270 | +- [**ERC-7984 tutorial and examples**](./openzeppelin/README.md) |
| 271 | +- [**Confidential wrapper documentation**](https://docs.zama.org/protocol/protocol-apps/confidential-wrapper) |
| 272 | +- [**Confidential Token Wrappers Registry documentation**](https://docs.zama.org/protocol/protocol-apps/registry-contract) |
0 commit comments