Skip to content

Commit bebfdce

Browse files
committed
docs(protocol-contracts): write first integration doc for wrappers
1 parent b116634 commit bebfdce

File tree

2 files changed

+2121
-750
lines changed

2 files changed

+2121
-750
lines changed
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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

Comments
 (0)