|
| 1 | +# Confidential Token Wrappers Registry |
| 2 | + |
| 3 | +The **Confidential Token Wrappers Registry** is an on-chain directory that maps ERC-20 tokens to their corresponding ERC-7984 confidential token wrappers. Use it to discover, validate, and integrate confidential wrappers within the FHEVM ecosystem. |
| 4 | + |
| 5 | +## Terminology |
| 6 | + |
| 7 | +- **Token**: An ERC-20 token. |
| 8 | +- **Confidential Wrapper**: An ERC-7984 confidential token wrapper. Also called "confidential token". |
| 9 | +- **Underlying Token**: The ERC-20 token that the confidential wrapper is associated with. |
| 10 | +- **TokenWrapperPair**: A pair of a token and its confidential wrapper. |
| 11 | +- **Valid**: A valid confidential wrapper has been verified by the registry owner and can be used to wrap and unwrap tokens from the underlying token. |
| 12 | +- **Invalid**: An invalid confidential wrapper has been revoked by the registry owner and should not be used to wrap and unwrap tokens from the underlying token. |
| 13 | +- **Owner**: The owner of the registry. In the FHEVM protocol, this is a DAO governance contract handled by Zama. |
| 14 | + |
| 15 | +## Quick Start |
| 16 | + |
| 17 | +> A token can only be associated with one confidential wrapper. A confidential wrapper can only be associated with one token. |
| 18 | +
|
| 19 | +> ⚠️ **Always check validity:** A non-zero wrapper address may be revoked by the owner. Always verify the `isValid` flag associated with the (token, wrapper) pair before use. |
| 20 | +
|
| 21 | +### Find the confidential wrapper of a token |
| 22 | + |
| 23 | +```solidity |
| 24 | +(bool isValid, address confidentialToken) = registry.getConfidentialTokenAddress(erc20TokenAddress); |
| 25 | +``` |
| 26 | + |
| 27 | +If the token has been registered with a confidential wrapper: |
| 28 | +- `isValid` will be `true` |
| 29 | +- `confidentialToken` will be the address of the confidential wrapper. |
| 30 | + |
| 31 | +If the token has never been registered with a confidential wrapper: |
| 32 | +- `isValid` will be `false` |
| 33 | +- `confidentialToken` will be `address(0)`. |
| 34 | + |
| 35 | +If the confidential wrapper has been revoked: |
| 36 | +- `isValid` will be `false` |
| 37 | +- `confidentialToken` will be the address of the (revoked) confidential wrapper. |
| 38 | + |
| 39 | +### Find the underlying token of a confidential wrapper |
| 40 | + |
| 41 | +```solidity |
| 42 | +(bool isValid, address token) = registry.getTokenAddress(confidentialWrapperAddress); |
| 43 | +``` |
| 44 | + |
| 45 | +If the confidential wrapper has been registered with a token: |
| 46 | +- `isValid` will be `true` |
| 47 | +- `token` will be the address of its underlying token. |
| 48 | + |
| 49 | +If the confidential wrapper has never been registered with a token: |
| 50 | +- `isValid` will be `false` |
| 51 | +- `token` will be `address(0)`. |
| 52 | + |
| 53 | +If the confidential wrapper has been revoked: |
| 54 | +- `isValid` will be `false` |
| 55 | +- `token` will be the address of its underlying token. |
| 56 | + |
| 57 | +### Check if a confidential wrapper is valid |
| 58 | + |
| 59 | +```solidity |
| 60 | +bool isValid = registry.isConfidentialTokenValid(confidentialWrapperAddress); |
| 61 | +``` |
| 62 | + |
| 63 | +If the confidential wrapper has been revoked, |
| 64 | +- `isValid` will be `false`. |
| 65 | + |
| 66 | +If the confidential wrapper has not been revoked (it is still valid), |
| 67 | +- `isValid` will be `true`. |
| 68 | + |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | +## Integration patterns |
| 73 | + |
| 74 | +Token and confidential wrapper pairs are stored in the registry as `TokenWrapperPair` structs. Each struct contains: |
| 75 | +- `tokenAddress`: the address of the underlying token. |
| 76 | +- `confidentialTokenAddress`: the address of the confidential wrapper. |
| 77 | +- `isValid`: `true` if the confidential wrapper is valid, `false` if it has been revoked. |
| 78 | + |
| 79 | +### Get all valid confidential (token, wrapper) pairs |
| 80 | +```solidity |
| 81 | +TokenWrapperPair[] memory tokenConfidentialTokenPairs = registry.getTokenConfidentialTokenPairs(); |
| 82 | +``` |
| 83 | + |
| 84 | +It returns all confidential wrappers (including revoked ones). |
| 85 | + |
| 86 | +### Get the total number of confidential (token, wrapper) pairs |
| 87 | + |
| 88 | +```solidity |
| 89 | +uint256 totalTokenConfidentialTokenPairs = registry.getTokenConfidentialTokenPairsLength(); |
| 90 | +``` |
| 91 | + |
| 92 | +### Get the index of a token |
| 93 | + |
| 94 | +```solidity |
| 95 | +uint256 tokenIndex = registry.getTokenIndex(tokenAddress); |
| 96 | +``` |
| 97 | + |
| 98 | +`tokenAddress` must be a registered token. Otherwise, it will revert with `TokenNotRegistered`. |
| 99 | + |
| 100 | +### Get a valid confidential (token, wrapper) pair by index |
| 101 | + |
| 102 | +```solidity |
| 103 | +TokenWrapperPair memory tokenConfidentialTokenPair = registry.getTokenConfidentialTokenPair(index); |
| 104 | +``` |
| 105 | + |
| 106 | +It returns a single confidential (token, wrapper) pair (including revoked ones). |
| 107 | + |
| 108 | +### Get a slice of confidential (token, wrapper) pairs |
| 109 | + |
| 110 | +```solidity |
| 111 | +TokenWrapperPair[] memory tokenConfidentialTokenPairsSlice = registry.getTokenConfidentialTokenPairsSlice(fromIndex, toIndex); |
| 112 | +``` |
| 113 | + |
| 114 | +It returns a slice of confidential (token, wrapper) pairs (including revoked ones). `fromIndex` is included and `toIndex` is excluded. |
| 115 | + |
| 116 | +--- |
| 117 | + |
| 118 | +## Data Structures |
| 119 | + |
| 120 | +### TokenWrapperPair |
| 121 | + |
| 122 | +```solidity |
| 123 | +struct TokenWrapperPair { |
| 124 | + address tokenAddress; // The ERC-20 token |
| 125 | + address confidentialTokenAddress; // The ERC-7984 wrapper |
| 126 | + bool isValid; // false if revoked |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## Events |
| 133 | + |
| 134 | +| Event | Description | |
| 135 | +|-------|-------------| |
| 136 | +| `ConfidentialTokenRegistered(tokenAddress, confidentialTokenAddress)` | Emitted when a new pair is registered | |
| 137 | +| `ConfidentialTokenRevoked(tokenAddress, confidentialTokenAddress)` | Emitted when a wrapper is revoked | |
| 138 | + |
| 139 | +--- |
| 140 | + |
| 141 | +## Errors |
| 142 | + |
| 143 | +| Error | Cause | |
| 144 | +|-------|-------| |
| 145 | +| `TokenZeroAddress()` | Attempted to register with zero token address | |
| 146 | +| `ConfidentialTokenZeroAddress()` | Attempted to register/revoke with zero wrapper address | |
| 147 | +| `NotERC7984(confidentialTokenAddress)` | Wrapper doesn't support ERC-7984 interface | |
| 148 | +| `ConfidentialTokenDoesNotSupportERC165(confidentialTokenAddress)` | Wrapper doesn't implement ERC-165 | |
| 149 | +| `ConfidentialTokenAlreadyAssociatedWithToken(tokenAddress, existingConfidentialTokenAddress)` | Wrapper already registered to another token | |
| 150 | +| `TokenAlreadyAssociatedWithConfidentialToken(tokenAddress, existingConfidentialTokenAddress)` | Token already has a registered wrapper | |
| 151 | +| `RevokedConfidentialToken(confidentialTokenAddress)` | Attempting to revoke an already-revoked wrapper | |
| 152 | +| `NoTokenAssociatedWithConfidentialToken(confidentialTokenAddress)` | Attempting to revoke unregistered wrapper | |
| 153 | +| `FromIndexGreaterOrEqualToIndex(fromIndex, toIndex)` | Invalid slice range | |
| 154 | +| `TokenNotRegistered(tokenAddress)` | Token has not been registered | |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +## Owner Administration |
| 159 | + |
| 160 | +> All administrative actions are restricted to the registry owner. |
| 161 | +
|
| 162 | +### Register a confidential token |
| 163 | + |
| 164 | +```solidity |
| 165 | +registry.registerConfidentialToken( |
| 166 | + erc20TokenAddress, |
| 167 | + confidentialWrapperAddress |
| 168 | +); |
| 169 | +``` |
| 170 | + |
| 171 | +**Validation performed:** |
| 172 | +- Neither address can be zero |
| 173 | +- Confidential token must implement ERC-165 (`supportsInterface` function) and support the ERC-7984 interface (`0x4958f2a4`) |
| 174 | +- Token must not already have an associated wrapper |
| 175 | +- Confidential token must not already be associated with another token |
| 176 | + |
| 177 | +### Revoke a confidential token |
| 178 | + |
| 179 | +```solidity |
| 180 | +registry.revokeConfidentialToken(confidentialWrapperAddress); |
| 181 | +``` |
| 182 | + |
| 183 | +**Important:** Revocation does NOT delete the mapping—it only sets `isValid = false`. This preserves historical records and prevents re-registration of malicious addresses. |
| 184 | + |
| 185 | +Revoking is currently permanent. When a wrapper is revoked: |
| 186 | +- `isValid` is set to `false` |
| 187 | +- The mapping entries remain in storage |
| 188 | +- The token cannot be registered with a new wrapper |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## Upgradeability |
| 193 | + |
| 194 | +The contract uses **UUPS (Universal Upgradeable Proxy Standard)** with 2-step ownership transfer. Only the owner can upgrade the contract. |
0 commit comments