Skip to content

Commit b116634

Browse files
authored
docs(protocol-contracts): write integration doc for the registry (#1646)
1 parent 9be32eb commit b116634

File tree

3 files changed

+214
-2
lines changed

3 files changed

+214
-2
lines changed

protocol-contracts/confidential-token-wrappers-registry/contracts/ConfidentialTokenWrappersRegistry.sol

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ contract ConfidentialTokenWrappersRegistry is Ownable2StepUpgradeable, UUPSUpgra
6868
/// @notice Error thrown when no token is associated with a confidential token.
6969
error NoTokenAssociatedWithConfidentialToken(address confidentialTokenAddress);
7070

71+
/// @notice Error thrown when a token has not been registered yet.
72+
error TokenNotRegistered(address tokenAddress);
73+
7174
/// @notice Emitted when a token is registered and associated with a confidential token.
7275
event ConfidentialTokenRegistered(address indexed tokenAddress, address indexed confidentialTokenAddress);
7376

@@ -200,11 +203,17 @@ contract ConfidentialTokenWrappersRegistry is Ownable2StepUpgradeable, UUPSUpgra
200203
/**
201204
* @notice Returns the index of the registered token in the array of
202205
* (tokenAddress, confidentialTokenAddress, isValid) tuples.
203-
* Will raise an error if token has not been registered yet.
206+
* It will raise an error if the token has not been registered yet.
204207
* @param tokenAddress The address of the token.
205208
* @return The index of the token.
206209
*/
207210
function getTokenIndex(address tokenAddress) public view returns (uint256) {
211+
ConfidentialTokenWrappersRegistryStorage storage $ = _getConfidentialTokenWrappersRegistryStorage();
212+
(, address confidentialTokenAddress) = getConfidentialTokenAddress(tokenAddress);
213+
if (confidentialTokenAddress == address(0)) {
214+
revert TokenNotRegistered(tokenAddress);
215+
}
216+
208217
uint256 tokenIndex = _getConfidentialTokenWrappersRegistryStorage()._tokenIndex[tokenAddress];
209218
return tokenIndex;
210219
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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.

protocol-contracts/confidential-token-wrappers-registry/test/confidentialTokenWrappersRegistry.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe('ConfidentialTokenWrappersRegistry', function () {
9090
]);
9191
});
9292

93-
it.only('should register many confidential tokens', async function () {
93+
it('should register many confidential tokens', async function () {
9494
await this.registry.connect(this.owner).registerConfidentialToken(this.token1, this.confidentialToken1);
9595
await this.registry.connect(this.owner).registerConfidentialToken(this.token2, this.confidentialToken2);
9696
await this.registry.connect(this.owner).registerConfidentialToken(this.token3, this.confidentialToken3);
@@ -257,5 +257,14 @@ describe('ConfidentialTokenWrappersRegistry', function () {
257257
.to.be.revertedWithCustomError(this.registry, 'NoTokenAssociatedWithConfidentialToken')
258258
.withArgs(this.confidentialToken2);
259259
});
260+
261+
describe('getTokenIndex', function () {
262+
it('should revert if token has not been registered', async function () {
263+
const randomTokenAddress = createRandomAddress();
264+
await expect(this.registry.getTokenIndex(randomTokenAddress))
265+
.to.be.revertedWithCustomError(this.registry, 'TokenNotRegistered')
266+
.withArgs(randomTokenAddress);
267+
});
268+
});
260269
});
261270
});

0 commit comments

Comments
 (0)