|
| 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(to, amount); |
| 35 | +``` |
| 36 | + |
| 37 | +The wrapper will mint the corresponding confidential token to the `to` address 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 | +- `to` 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 | +> ⚠️ **Unsupported `from`:** Accounts with a zero balance that have never held tokens cannot be the `from` address in unwrap requests. |
| 51 | +
|
| 52 | +##### With input proof |
| 53 | + |
| 54 | +> ℹ️ **Input proof:** To unwrap any amount of confidential tokens, the `from` address 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). |
| 55 | +
|
| 56 | +```solidity |
| 57 | +wrapper.unwrap(from, to, encryptedAmount, inputProof); |
| 58 | +``` |
| 59 | + |
| 60 | +Alternatively, an unwrap request can be made without an input proof if the encrypted amount (`euint64`) is known to `from`. For example, this can be the confidential balance of `from`. |
| 61 | + |
| 62 | +This requests an unwrap request of `encryptedAmount` confidential tokens from `from`. Considerations: |
| 63 | +- `msg.sender` must be `from` or an approved operator for `from`. |
| 64 | +- `from` mut not be the zero address. |
| 65 | +- `encryptedAmount` will be burned in the request. |
| 66 | +- **NO** transfer of underlying tokens is made in this request. |
| 67 | + |
| 68 | + |
| 69 | +It emits an `UnwrapRequested` event: |
| 70 | +```solidity |
| 71 | +event UnwrapRequested(address indexed receiver, euint64 amount); |
| 72 | +``` |
| 73 | + |
| 74 | +###### Without input proof |
| 75 | + |
| 76 | +Alternatively, an unwrap request can be made without an input proof if the encrypted amount (`euint64`) is known to `from`. For example, this can be the confidential balance of `from`. |
| 77 | + |
| 78 | +```solidity |
| 79 | +wrapper.unwrap(from, to, encryptedAmount); |
| 80 | +``` |
| 81 | + |
| 82 | +On top of the above unwrap request considerations: |
| 83 | +- `msg.sender` must be approved by ACL for the given `encryptedAmount` ⚠️ (see [ACL documentation](https://docs.zama.org/protocol/protocol/overview/library#access-control)). |
| 84 | + |
| 85 | + |
| 86 | +#### 2) Finalize unwrap |
| 87 | + |
| 88 | +> ℹ️ **Public decryption:** The encrypted burned amount `burntAmount` 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). |
| 89 | +
|
| 90 | +```solidity |
| 91 | +wrapper.finalizeUnwrap(burntAmount, cleartextAmount, decryptionProof); |
| 92 | +``` |
| 93 | + |
| 94 | +This finalizes the unwrap request by sending the corresponding amount of underlying tokens to the `to` defined in the `unwrap` request. |
| 95 | + |
| 96 | +### Transfer confidential tokens |
| 97 | + |
| 98 | +> ℹ️ **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`. |
| 99 | +
|
| 100 | +> ⚠️ **Unsupported `from`:** Accounts with a zero balance that have never held tokens cannot be the `from` address in confidential transfers. |
| 101 | +
|
| 102 | +#### Direct transfer |
| 103 | + |
| 104 | +```solidity |
| 105 | +token.confidentialTransfer(to, encryptedAmount, inputProof); |
| 106 | +
|
| 107 | +token.confidentialTransfer(to, encryptedAmount); |
| 108 | +``` |
| 109 | + |
| 110 | +#### Operator-based transfer |
| 111 | + |
| 112 | +```solidity |
| 113 | +token.confidentialTransferFrom(from, to, encryptedAmount, inputProof); |
| 114 | +
|
| 115 | +token.confidentialTransferFrom(from, to, encryptedAmount); |
| 116 | +``` |
| 117 | + |
| 118 | +Considerations: |
| 119 | +- `msg.sender` must be `from` or an approved operator for `from`. |
| 120 | + |
| 121 | +#### Transfer with callback |
| 122 | + |
| 123 | +The callback can be used along an ERC-7984 receiver contract. |
| 124 | + |
| 125 | +```solidity |
| 126 | +token.confidentialTransferAndCall(to, encryptedAmount, inputProof, callbackData); |
| 127 | +
|
| 128 | +token.confidentialTransferAndCall(to, encryptedAmount, callbackData); |
| 129 | +``` |
| 130 | + |
| 131 | +#### Operator-based transfer with callback |
| 132 | + |
| 133 | +The callback can be used along an ERC-7984 receiver contract. |
| 134 | + |
| 135 | +```solidity |
| 136 | +token.confidentialTransferFromAndCall(from, to, encryptedAmount, inputProof, callbackData); |
| 137 | +
|
| 138 | +token.confidentialTransferFromAndCall(from, to, encryptedAmount, callbackData); |
| 139 | +``` |
| 140 | + |
| 141 | +Considerations: |
| 142 | +- `msg.sender` must be `from` or an approved operator for `from`. |
| 143 | + |
| 144 | +### Check the conversion rate and decimals |
| 145 | + |
| 146 | +```solidity |
| 147 | +uint256 conversionRate = wrapper.rate(); |
| 148 | +uint8 wrapperDecimals = wrapper.decimals(); |
| 149 | +``` |
| 150 | + |
| 151 | +**Examples:** |
| 152 | +| Underlying Decimals | Wrapper Decimals | Rate | Effect | |
| 153 | +|---------------------|------------------|------|--------| |
| 154 | +| 18 | 6 | 10^12 | 1 wrapped = 10^12 underlying | |
| 155 | +| 6 | 6 | 1 | 1:1 mapping | |
| 156 | +| 2 | 2 | 1 | 1:1 mapping | |
| 157 | + |
| 158 | +### Check supplies |
| 159 | + |
| 160 | +#### Non-confidential total supply |
| 161 | + |
| 162 | +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. |
| 163 | + |
| 164 | +> ℹ️ **Total Value Shielded (TVS):** This view function is useful for getting a good approximation of the wrapper's Total Value Shielded (TVS). |
| 165 | +
|
| 166 | +```solidity |
| 167 | +uint256 nonConfidentialSupply = wrapper.totalSupply(); |
| 168 | +``` |
| 169 | + |
| 170 | +#### Encrypted (confidential) total supply |
| 171 | + |
| 172 | +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. |
| 173 | + |
| 174 | +```solidity |
| 175 | +euint64 encryptedSupply = wrapper.confidentialTotalSupply(); |
| 176 | +``` |
| 177 | + |
| 178 | +#### Maximum total supply |
| 179 | + |
| 180 | +The maximum number of wrapped tokens supported by the encrypted datatype (uint64 limit). If this maximum is exceeded, wrapping new tokens will revert. |
| 181 | + |
| 182 | +```solidity |
| 183 | +uint256 maxSupply = wrapper.maxTotalSupply(); |
| 184 | +``` |
| 185 | + |
| 186 | +--- |
| 187 | + |
| 188 | +## Integration Patterns |
| 189 | + |
| 190 | +### Operator system |
| 191 | + |
| 192 | +Delegate transfer capabilities with time-based expiration: |
| 193 | + |
| 194 | +```solidity |
| 195 | +// Grant operator permission until a specific timestamp |
| 196 | +token.setOperator(operatorAddress, validUntilTimestamp); |
| 197 | +
|
| 198 | +// Check if an address is an authorized operator |
| 199 | +bool isAuthorized = token.isOperator(holder, spender); |
| 200 | +``` |
| 201 | + |
| 202 | +### Amount disclosure |
| 203 | + |
| 204 | +Optionally reveal encrypted amounts publicly: |
| 205 | + |
| 206 | +```solidity |
| 207 | +// Request disclosure (initiates async decryption) |
| 208 | +token.requestDiscloseEncryptedAmount(encryptedAmount); |
| 209 | +
|
| 210 | +// Complete disclosure with proof |
| 211 | +token.discloseEncryptedAmount(encryptedAmount, cleartextAmount, decryptionProof); |
| 212 | +``` |
| 213 | + |
| 214 | +### Check ACL permissions |
| 215 | + |
| 216 | +Before using encrypted amounts in transactions, callers must be authorized: |
| 217 | + |
| 218 | +```solidity |
| 219 | +require(FHE.isAllowed(encryptedAmount, msg.sender), "Unauthorized"); |
| 220 | +``` |
| 221 | + |
| 222 | +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). |
| 223 | + |
| 224 | +--- |
| 225 | + |
| 226 | +## Architecture |
| 227 | + |
| 228 | +``` |
| 229 | +┌─────────────────────────────────────────────────────────────────┐ |
| 230 | +│ ConfidentialWrapper │ |
| 231 | +│ (UUPS Upgradeable, Ownable2Step) │ |
| 232 | +├─────────────────────────────────────────────────────────────────┤ |
| 233 | +│ ERC7984ERC20WrapperUpgradeable │ |
| 234 | +│ (Wrapping/Unwrapping Logic, ERC1363 Receiver) │ |
| 235 | +├─────────────────────────────────────────────────────────────────┤ |
| 236 | +│ ERC7984Upgradeable │ |
| 237 | +│ (Confidential Token Standard - Encrypted Balances/Transfers) │ |
| 238 | +├─────────────────────────────────────────────────────────────────┤ |
| 239 | +│ ZamaEthereumConfigUpgradeable │ |
| 240 | +│ (FHE Coprocessor Configuration) │ |
| 241 | +└─────────────────────────────────────────────────────────────────┘ |
| 242 | +``` |
| 243 | + |
| 244 | +--- |
| 245 | + |
| 246 | +## Events |
| 247 | + |
| 248 | +| Event | Description | |
| 249 | +|-------|-------------| |
| 250 | +| `ConfidentialTransfer(from, to, encryptedAmount)` | Emitted on every transfer (including mint/burn) | |
| 251 | +| `OperatorSet(holder, operator, until)` | Emitted when operator permissions change | |
| 252 | +| `UnwrapRequested(receiver, encryptedAmount)` | Emitted when unwrap is initiated | |
| 253 | +| `UnwrapFinalized(receiver, encryptedAmount, cleartextAmount)` | Emitted when unwrap completes | |
| 254 | +| `AmountDiscloseRequested(encryptedAmount, requester)` | Emitted when disclosure is requested | |
| 255 | +| `AmountDisclosed(encryptedAmount, cleartextAmount)` | Emitted when amount is publicly disclosed | |
| 256 | + |
| 257 | +--- |
| 258 | + |
| 259 | +## Errors |
| 260 | + |
| 261 | +| Error | Cause | |
| 262 | +|-------|-------| |
| 263 | +| `ERC7984InvalidReceiver(address)` | Transfer to zero address | |
| 264 | +| `ERC7984InvalidSender(address)` | Transfer from zero address | |
| 265 | +| `ERC7984UnauthorizedSpender(holder, spender)` | Caller not authorized as operator | |
| 266 | +| `ERC7984ZeroBalance(holder)` | Sender has never held tokens | |
| 267 | +| `ERC7984UnauthorizedUseOfEncryptedAmount(amount, user)` | Caller lacks ACL permission for ciphertext | |
| 268 | +| `ERC7984UnauthorizedCaller(caller)` | Invalid caller for operation | |
| 269 | +| `InvalidUnwrapRequest(amount)` | Finalizing non-existent unwrap request | |
| 270 | +| `ERC7984TotalSupplyOverflow()` | Minting would exceed uint64 max | |
| 271 | + |
| 272 | +--- |
| 273 | + |
| 274 | +## Important Considerations |
| 275 | + |
| 276 | +### Ciphertext uniqueness assumption |
| 277 | + |
| 278 | +The unwrap mechanism stores requests in a mapping keyed by ciphertext and the current implementation assumes these ciphertexts are unique. This holds in this very specific case but be aware of this architectural decision as it is **NOT** true in the general case. |
| 279 | + |
| 280 | +--- |
| 281 | + |
| 282 | +## Interface Support (ERC-165) |
| 283 | + |
| 284 | +```solidity |
| 285 | +wrapper.supportsInterface(type(IERC7984).interfaceId); |
| 286 | +wrapper.supportsInterface(type(IERC7984ERC20Wrapper).interfaceId); |
| 287 | +wrapper.supportsInterface(type(IERC165).interfaceId); |
| 288 | +``` |
| 289 | + |
| 290 | +--- |
| 291 | + |
| 292 | +## Upgradeability |
| 293 | + |
| 294 | +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