|
| 1 | +# Confidential Wrapper |
| 2 | + |
| 3 | +The **Confidential Wrapper** is a smart contract that wraps standard ERC-20 tokens into confidential ERC-7984 tokens. Built on Zama's FHEVM, it enables privacy-preserving token transfers where balances and transfer amounts remain encrypted. |
| 4 | + |
| 5 | +## Terminology |
| 6 | + |
| 7 | +- **Confidential Token**: The ERC-7984 confidential token wrapper. |
| 8 | +- **Underlying Token**: The standard ERC-20 token wrapped by the confidential wrapper. |
| 9 | +- **Wrapping**: Converting ERC-20 tokens into confidential tokens. |
| 10 | +- **Unwrapping**: Converting confidential tokens back into ERC-20 tokens. |
| 11 | +- **Rate**: The conversion ratio between underlying token units and confidential token units (due to decimal differences). |
| 12 | +- **Operator**: An address authorized to transfer confidential tokens on behalf of another address. |
| 13 | +- **Owner**: The owner of the wrapper contract. In the FHEVM protocol, this is initially set to a DAO governance contract handled by Zama. Ownership will then be transferred to the underlying token's owner. |
| 14 | +- **Registry**: The registry contract that maps ERC-20 tokens to their corresponding confidential wrappers. More information [here](../../confidential-token-wrappers-registry/docs/README.md). |
| 15 | +- **ACL**: The ACL contract that manages the ACL permissions for encrypted amounts. More information in the [FHEVM library documentation](https://docs.zama.org/protocol/protocol/overview/library#access-control). |
| 16 | +- **Input proof**: A proof that the encrypted amount is valid. More information in the [`relayer-sdk` documentation](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer/input). |
| 17 | +- **Public decryption**: A request to publicly decrypt an encrypted amount. More information in the [`relayer-sdk` documentation](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer/decryption/public-decryption). |
| 18 | + |
| 19 | +## Quick Start |
| 20 | + |
| 21 | +> ⚠️ **Decimal conversion:** The wrapper enforces a maximum of **6 decimals** for the confidential token. When wrapping, amounts are rounded down and excess tokens are refunded. |
| 22 | +
|
| 23 | +> ⚠️ **Unsupported tokens:** Non-standard tokens such as fee-on-transfer or any deflationary-type tokens are NOT supported. |
| 24 | +
|
| 25 | +### Get the confidential wrapper address of an ERC-20 token |
| 26 | + |
| 27 | +Zama provides a registry contract that maps ERC-20 tokens to their corresponding verified confidential wrappers. Make sure to check the registry contract to ensure the confidential wrapper is valid before wrapping. More information [here](../../confidential-token-wrappers-registry/docs/README.md). |
| 28 | + |
| 29 | +### Wrap ERC-20 → Confidential Token |
| 30 | + |
| 31 | +**Important:** Prior to wrapping, the confidential wrapper contract must be approved by the `msg.sender` on the underlying token. |
| 32 | + |
| 33 | +```solidity |
| 34 | +wrapper.wrap(recipientAddress, amount); |
| 35 | +``` |
| 36 | + |
| 37 | +The wrapper will mint the corresponding confidential token to the `recipientAddress` and refund the excess tokens to the `msg.sender` (due to decimal conversion). Considerations: |
| 38 | +- `amount` must be a value using the same decimal precision as the underlying token. |
| 39 | +- `recipientAddress` must not be the zero address. |
| 40 | + |
| 41 | +> ℹ️ **Low amount handling:** If the amount is less than the rate, the wrapping will succeed but the recipient will receive 0 confidential tokens and the excess tokens will be refunded to the `msg.sender`. |
| 42 | +
|
| 43 | + |
| 44 | +### Unwrap Confidential Token → ERC-20 |
| 45 | + |
| 46 | +Unwrapping is a **two-step asynchronous process**: an `unwrap` must be first made and then finalized with `finalizeUnwrap`. The `unwrap` function can be called with or without an input proof. |
| 47 | + |
| 48 | +#### 1) Unwrap request |
| 49 | + |
| 50 | +##### With input proof |
| 51 | + |
| 52 | +> ℹ️ **Input proof:** To unwrap any amount of confidential tokens, the `fromAddress` must first create an encrypted input to generate an `encryptedAmount` (`externalEuint64`) along its `inputProof`. The amount to be encrypted must use the same decimal precision as the confidential wrapper. More information in the [`relayer-sdk` documentation](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer/input). |
| 53 | +
|
| 54 | +```solidity |
| 55 | +wrapper.unwrap(fromAddress, recipientAddress, encryptedAmount, inputProof); |
| 56 | +``` |
| 57 | + |
| 58 | +Alternatively, an unwrap request can be made without an input proof if the encrypted amount (`euint64`) is known to `fromAddress`. For example, this can be the confidential balance of `fromAddress`. |
| 59 | + |
| 60 | +This requests an unwrap request of `encryptedAmount` confidential tokens from `fromAddress`. Considerations: |
| 61 | +- `msg.sender` must be `fromAddress` or an approved operator for `fromAddress`. |
| 62 | +- `fromAddress` mut not be the zero address. |
| 63 | +- `encryptedAmount` will be burned in the request. |
| 64 | +- **NO** transfer of underlying tokens is made in this request. |
| 65 | + |
| 66 | + |
| 67 | +It emits an `UnwrapRequested` event: |
| 68 | +```solidity |
| 69 | +event UnwrapRequested(address indexed receiver, euint64 amount); |
| 70 | +``` |
| 71 | + |
| 72 | +###### Without input proof |
| 73 | + |
| 74 | +Alternatively, an unwrap request can be made without an input proof if the encrypted amount (`euint64`) is known to `fromAddress`. For example, this can be the confidential balance of `fromAddress`. |
| 75 | + |
| 76 | +```solidity |
| 77 | +wrapper.unwrap(fromAddress, recipientAddress, encryptedAmount); |
| 78 | +``` |
| 79 | + |
| 80 | +On top of the above unwrap request considerations: |
| 81 | +- `msg.sender` must be approved by ACL for the given `encryptedAmount` ⚠️ (see [ACL documentation](https://docs.zama.org/protocol/protocol/overview/library#access-control)). |
| 82 | + |
| 83 | + |
| 84 | +#### 2) Finalize unwrap |
| 85 | + |
| 86 | +> ℹ️ **Public decryption:** The encrypted burned amount emitted by the `UnwrapRequested` event must be publicly decrypted to get the `cleartextAmount` along its `decryptionProof`. More information in the [`relayer-sdk` documentation](https://docs.zama.org/protocol/relayer-sdk-guides/fhevm-relayer/decryption/public-decryption). |
| 87 | +
|
| 88 | +```solidity |
| 89 | +wrapper.finalizeUnwrap(burntAmount, cleartextAmount, decryptionProof); |
| 90 | +``` |
| 91 | + |
| 92 | +This finalizes the unwrap request by sending the corresponding amount of underlying tokens to the `recipientAddress` defined in the `unwrap` request. |
| 93 | + |
| 94 | +### Transfer confidential tokens |
| 95 | + |
| 96 | +> ℹ️ **Transfer with input proof:** Similarly to the unwrap process, transfers can be made with or without an input proof and the encrypted amount must be approved by the ACL for the `msg.sender`. |
| 97 | +
|
| 98 | +#### Direct transfer |
| 99 | + |
| 100 | +```solidity |
| 101 | +token.confidentialTransfer(to, encryptedAmount, inputProof); |
| 102 | +
|
| 103 | +token.confidentialTransfer(to, encryptedAmount); |
| 104 | +``` |
| 105 | + |
| 106 | +#### Operator-based transfer |
| 107 | + |
| 108 | +```solidity |
| 109 | +token.confidentialTransferFrom(from, to, encryptedAmount, inputProof); |
| 110 | +
|
| 111 | +token.confidentialTransferFrom(from, to, encryptedAmount); |
| 112 | +``` |
| 113 | + |
| 114 | +Considerations: |
| 115 | +- `msg.sender` must be `fromAddress` or an approved operator for `fromAddress`. |
| 116 | + |
| 117 | +#### Transfer with callback |
| 118 | + |
| 119 | +The callback can be used along an ERC-7984 receiver contract. |
| 120 | + |
| 121 | +```solidity |
| 122 | +token.confidentialTransferAndCall(to, encryptedAmount, inputProof, callbackData); |
| 123 | +
|
| 124 | +token.confidentialTransferAndCall(to, encryptedAmount, callbackData); |
| 125 | +``` |
| 126 | + |
| 127 | +#### Operator-based transfer with callback |
| 128 | + |
| 129 | +The callback can be used along an ERC-7984 receiver contract. |
| 130 | + |
| 131 | +```solidity |
| 132 | +token.confidentialTransferFromAndCall(from, to, encryptedAmount, inputProof, callbackData); |
| 133 | +
|
| 134 | +token.confidentialTransferFromAndCall(from, to, encryptedAmount, callbackData); |
| 135 | +``` |
| 136 | + |
| 137 | +Considerations: |
| 138 | +- `msg.sender` must be `fromAddress` or an approved operator for `fromAddress`. |
| 139 | + |
| 140 | +### Check the conversion rate and decimals |
| 141 | + |
| 142 | +```solidity |
| 143 | +uint256 conversionRate = wrapper.rate(); |
| 144 | +uint8 wrapperDecimals = wrapper.decimals(); |
| 145 | +``` |
| 146 | + |
| 147 | +**Examples:** |
| 148 | +| Underlying Decimals | Wrapper Decimals | Rate | Effect | |
| 149 | +|---------------------|------------------|------|--------| |
| 150 | +| 18 | 6 | 10^12 | 1 wrapped = 10^12 underlying | |
| 151 | +| 6 | 6 | 1 | 1:1 mapping | |
| 152 | +| 2 | 2 | 1 | 1:1 mapping | |
| 153 | + |
| 154 | +### Check supplies |
| 155 | + |
| 156 | +#### Non-confidential total supply |
| 157 | + |
| 158 | +The wrapper exposes a non-confidential view of the total supply, computed from the underlying ERC20 balance held by the wrapper contract. This value may be higher than `confidentialTotalSupply()` if tokens are sent directly to the wrapper outside of the wrapping process. |
| 159 | + |
| 160 | +```solidity |
| 161 | +uint256 nonConfidentialSupply = wrapper.totalSupply(); |
| 162 | +``` |
| 163 | + |
| 164 | +#### Encrypted (confidential) total supply |
| 165 | + |
| 166 | +The actual supply tracked by the confidential token contract, represented as an encrypted value. To determine the cleartext value, you need to request decryption and appropriate ACL authorization. |
| 167 | + |
| 168 | +```solidity |
| 169 | +euint64 encryptedSupply = wrapper.confidentialTotalSupply(); |
| 170 | +``` |
| 171 | + |
| 172 | +#### Maximum total supply |
| 173 | + |
| 174 | +The maximum number of wrapped tokens supported by the encrypted datatype (uint64 limit). If this maximum is exceeded, wrapping new tokens will revert. |
| 175 | + |
| 176 | +```solidity |
| 177 | +uint256 maxSupply = wrapper.maxTotalSupply(); |
| 178 | +``` |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +## Integration Patterns |
| 183 | + |
| 184 | +### Operator system |
| 185 | + |
| 186 | +Delegate transfer capabilities with time-based expiration: |
| 187 | + |
| 188 | +```solidity |
| 189 | +// Grant operator permission until a specific timestamp |
| 190 | +token.setOperator(operatorAddress, validUntilTimestamp); |
| 191 | +
|
| 192 | +// Check if an address is an authorized operator |
| 193 | +bool isAuthorized = token.isOperator(holder, spender); |
| 194 | +``` |
| 195 | + |
| 196 | +### Amount disclosure |
| 197 | + |
| 198 | +Optionally reveal encrypted amounts publicly: |
| 199 | + |
| 200 | +```solidity |
| 201 | +// Request disclosure (initiates async decryption) |
| 202 | +token.requestDiscloseEncryptedAmount(encryptedAmount); |
| 203 | +
|
| 204 | +// Complete disclosure with proof |
| 205 | +token.discloseEncryptedAmount(encryptedAmount, cleartextAmount, decryptionProof); |
| 206 | +``` |
| 207 | + |
| 208 | +### Check ACL permissions |
| 209 | + |
| 210 | +Before using encrypted amounts in transactions, callers must be authorized: |
| 211 | + |
| 212 | +```solidity |
| 213 | +require(FHE.isAllowed(encryptedAmount, msg.sender), "Unauthorized"); |
| 214 | +``` |
| 215 | + |
| 216 | +Transfer functions with `euint64` (not `externalEuint64`) require the caller to already have ACL permission for that ciphertext. More information in the [FHEVM library documentation](https://docs.zama.org/protocol/protocol/overview/library#access-control). |
| 217 | + |
| 218 | +--- |
| 219 | + |
| 220 | +## Architecture |
| 221 | + |
| 222 | +``` |
| 223 | +┌─────────────────────────────────────────────────────────────────┐ |
| 224 | +│ ConfidentialWrapper │ |
| 225 | +│ (UUPS Upgradeable, Ownable2Step) │ |
| 226 | +├─────────────────────────────────────────────────────────────────┤ |
| 227 | +│ ERC7984ERC20WrapperUpgradeable │ |
| 228 | +│ (Wrapping/Unwrapping Logic, ERC1363 Receiver) │ |
| 229 | +├─────────────────────────────────────────────────────────────────┤ |
| 230 | +│ ERC7984Upgradeable │ |
| 231 | +│ (Confidential Token Standard - Encrypted Balances/Transfers) │ |
| 232 | +├─────────────────────────────────────────────────────────────────┤ |
| 233 | +│ ZamaEthereumConfigUpgradeable │ |
| 234 | +│ (FHE Coprocessor Configuration) │ |
| 235 | +└─────────────────────────────────────────────────────────────────┘ |
| 236 | +``` |
| 237 | + |
| 238 | +--- |
| 239 | + |
| 240 | +## Events |
| 241 | + |
| 242 | +| Event | Description | |
| 243 | +|-------|-------------| |
| 244 | +| `ConfidentialTransfer(from, to, encryptedAmount)` | Emitted on every transfer (including mint/burn) | |
| 245 | +| `OperatorSet(holder, operator, until)` | Emitted when operator permissions change | |
| 246 | +| `UnwrapRequested(receiver, encryptedAmount)` | Emitted when unwrap is initiated | |
| 247 | +| `UnwrapFinalized(receiver, encryptedAmount, cleartextAmount)` | Emitted when unwrap completes | |
| 248 | +| `AmountDiscloseRequested(encryptedAmount, requester)` | Emitted when disclosure is requested | |
| 249 | +| `AmountDisclosed(encryptedAmount, cleartextAmount)` | Emitted when amount is publicly disclosed | |
| 250 | + |
| 251 | +--- |
| 252 | + |
| 253 | +## Errors |
| 254 | + |
| 255 | +| Error | Cause | |
| 256 | +|-------|-------| |
| 257 | +| `ERC7984InvalidReceiver(address)` | Transfer to zero address | |
| 258 | +| `ERC7984InvalidSender(address)` | Transfer from zero address | |
| 259 | +| `ERC7984UnauthorizedSpender(holder, spender)` | Caller not authorized as operator | |
| 260 | +| `ERC7984ZeroBalance(holder)` | Sender has never held tokens | |
| 261 | +| `ERC7984UnauthorizedUseOfEncryptedAmount(amount, user)` | Caller lacks ACL permission for ciphertext | |
| 262 | +| `ERC7984UnauthorizedCaller(caller)` | Invalid caller for operation | |
| 263 | +| `InvalidUnwrapRequest(amount)` | Finalizing non-existent unwrap request | |
| 264 | +| `ERC7984TotalSupplyOverflow()` | Minting would exceed uint64 max | |
| 265 | + |
| 266 | +--- |
| 267 | + |
| 268 | +## Important Considerations |
| 269 | + |
| 270 | +### Zero balance requirement |
| 271 | + |
| 272 | +Accounts with a zero balance that have never held tokens cannot be the `from` address in transfers. This will revert with `ERC7984ZeroBalance`. |
| 273 | + |
| 274 | +### Ciphertext uniqueness assumption |
| 275 | + |
| 276 | +The unwrap mechanism stores requests in a mapping keyed by ciphertext. The current implementation assumes ciphertexts are unique. This holds in practice but be aware of this architectural decision as it is not true in the general case. |
| 277 | + |
| 278 | +--- |
| 279 | + |
| 280 | +## Interface Support (ERC-165) |
| 281 | + |
| 282 | +```solidity |
| 283 | +wrapper.supportsInterface(type(IERC7984).interfaceId); |
| 284 | +wrapper.supportsInterface(type(IERC7984ERC20Wrapper).interfaceId); |
| 285 | +wrapper.supportsInterface(type(IERC165).interfaceId); |
| 286 | +``` |
| 287 | + |
| 288 | +--- |
| 289 | + |
| 290 | +## Upgradeability |
| 291 | + |
| 292 | +The contract uses **UUPS (Universal Upgradeable Proxy Standard)** with 2-step ownership transfer. Only the owner can upgrade the contract. Initially, the owner is set to a DAO governance contract handled by Zama. Ownership will then be transferred to the underlying token's owner. |
0 commit comments