@towns-protocol/contracts 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/domains_architecture.md +541 -0
- package/package.json +1 -1
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# Global Usernames
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Towns Protocol Global Usernames system implements an ENS-compatible cross-chain domain registry system that enables **Global Usernames** - portable usernames (e.g., `ben.towns.eth`) that work across all Towns spaces, channels, chats, GDMS, and DMs. Instead of showing wallet addresses, users see `@ben` with their profile.
|
|
6
|
+
|
|
7
|
+
### Key Features
|
|
8
|
+
|
|
9
|
+
| Feature | Description |
|
|
10
|
+
| -------------------------- | ----------------------------------------------------------------- |
|
|
11
|
+
| **L2 Subdomains** | Register subdomains on Base L2 (e.g., `ben.towns.eth`) |
|
|
12
|
+
| **NFT Ownership** | Each subdomain is an ERC721 token, enabling transfers |
|
|
13
|
+
| **Cross-chain Resolution** | Resolves via ENS from L1 using CCIP-Read (EIP-3668) |
|
|
14
|
+
| **Profile Data** | Store display name, avatar, bio as resolver text records |
|
|
15
|
+
| **Multi-coin Addresses** | Support ETH (coinType 60) and chain-specific addresses (ENSIP-11) |
|
|
16
|
+
| **Tiered Pricing** | First registration free, subsequent charged via FeeManager |
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
The system consists of three main components deployed across two chains:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
24
|
+
│ ETHEREUM L1 (MAINNET) │
|
|
25
|
+
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|
26
|
+
│ │ L1 Resolver Diamond │ │
|
|
27
|
+
│ │ ┌─────────────────────┐ │ │
|
|
28
|
+
│ │ │ L1ResolverFacet │ ─── CCIP-Read (EIP-3668) │ │
|
|
29
|
+
│ │ │ - resolve() │ OffchainLookup → Gateway │ │
|
|
30
|
+
│ │ │ - resolveWithProof │ ← Verified response │ │
|
|
31
|
+
│ │ └─────────────────────┘ │ │
|
|
32
|
+
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|
33
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
34
|
+
│
|
|
35
|
+
│ CCIP-Read
|
|
36
|
+
▼
|
|
37
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
38
|
+
│ CCIP GATEWAY SERVICE │
|
|
39
|
+
│ - Receives OffchainLookup requests │
|
|
40
|
+
│ - Queries L2 Registry via RPC │
|
|
41
|
+
│ - Signs responses with expiration │
|
|
42
|
+
│ - Returns (result, expires, signature) │
|
|
43
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
44
|
+
│
|
|
45
|
+
│ RPC Query
|
|
46
|
+
▼
|
|
47
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
48
|
+
│ BASE L2 │
|
|
49
|
+
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|
50
|
+
│ │ L2 Registry Diamond │ │
|
|
51
|
+
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
|
|
52
|
+
│ │ │ L2RegistryFacet │ │ AddrResolverFacet │ │ │
|
|
53
|
+
│ │ │ - createSubdomain()│ │ - setAddr() │ │ │
|
|
54
|
+
│ │ │ - ERC721 functions │ │ - addr() │ │ │
|
|
55
|
+
│ │ └─────────────────────┘ └─────────────────────┘ │ │
|
|
56
|
+
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
|
|
57
|
+
│ │ │ TextResolverFacet │ │ ContentHashFacet │ │ │
|
|
58
|
+
│ │ │ - setText() │ │ - setContenthash() │ │ │
|
|
59
|
+
│ │ │ - text() │ │ - contenthash() │ │ │
|
|
60
|
+
│ │ └─────────────────────┘ └─────────────────────┘ │ │
|
|
61
|
+
│ │ ┌─────────────────────┐ │ │
|
|
62
|
+
│ │ │ ExtendedResolverFacet│ ← Direct L2 resolution │ │
|
|
63
|
+
│ │ └─────────────────────┘ │ │
|
|
64
|
+
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|
65
|
+
│ │
|
|
66
|
+
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
|
67
|
+
│ │ L2 Registrar Diamond │ │
|
|
68
|
+
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
|
|
69
|
+
│ │ │ L2RegistrarFacet │ │ DomainFeeHook │ │ │
|
|
70
|
+
│ │ │ - register() │──│ - onChargeFee() │ │ │
|
|
71
|
+
│ │ │ - isAvailable() │ │ - tiered pricing │ │ │
|
|
72
|
+
│ │ └─────────────────────┘ └─────────────────────┘ │ │
|
|
73
|
+
│ └─────────────────────────────────────────────────────────────────────┘ │
|
|
74
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Contract Hierarchy
|
|
78
|
+
|
|
79
|
+
| Component | Location | Purpose |
|
|
80
|
+
| -------------------------- | -------------------- | ------------------------------------------- |
|
|
81
|
+
| `L1ResolverFacet` | `facets/l1/` | CCIP-Read resolver on Ethereum mainnet |
|
|
82
|
+
| `L1ResolverMod` | `facets/l1/` | L1 resolver storage, signature verification |
|
|
83
|
+
| `L2RegistryFacet` | `facets/l2/` | Domain NFT registry (ERC721) on Base |
|
|
84
|
+
| `L2RegistryMod` | `facets/l2/modules/` | Subdomain creation, registrar management |
|
|
85
|
+
| `AddrResolverFacet` | `facets/l2/` | Multi-coin address records (SLIP-44) |
|
|
86
|
+
| `TextResolverFacet` | `facets/l2/` | Key-value text records |
|
|
87
|
+
| `ContentHashResolverFacet` | `facets/l2/` | IPFS/IPNS/Swarm content hashes |
|
|
88
|
+
| `ExtendedResolverFacet` | `facets/l2/` | EIP-3668 direct on-chain resolution |
|
|
89
|
+
| `L2RegistrarFacet` | `facets/registrar/` | Subdomain registration with validation |
|
|
90
|
+
| `L2RegistrarMod` | `facets/registrar/` | Label validation, fee charging |
|
|
91
|
+
| `DomainFeeHook` | `hooks/` | Tiered pricing, first-free logic |
|
|
92
|
+
|
|
93
|
+
### Module Files
|
|
94
|
+
|
|
95
|
+
| Module | Purpose |
|
|
96
|
+
| ------------------------ | ---------------------------------------------------- |
|
|
97
|
+
| `AddrResolverMod` | Address storage with versioning (SLIP-44 coin types) |
|
|
98
|
+
| `TextResolverMod` | Text record storage with versioning |
|
|
99
|
+
| `ContentHashResolverMod` | Content hash storage with versioning |
|
|
100
|
+
| `VersionRecordMod` | Record versioning for atomic invalidation |
|
|
101
|
+
|
|
102
|
+
## Storage Layout
|
|
103
|
+
|
|
104
|
+
Each module uses the diamond storage pattern with unique storage slots:
|
|
105
|
+
|
|
106
|
+
| Module | Storage Slot | Contents |
|
|
107
|
+
| ------------------------ | --------------- | ------------------------------------------------------- |
|
|
108
|
+
| `L1ResolverMod` | `0xad5af01a...` | gatewayUrl, gatewaySigner, nameWrapper, registryByNode |
|
|
109
|
+
| `L2RegistryMod` | `0xd006f566...` | baseNode, names, metadata, registrars, token (ERC721) |
|
|
110
|
+
| `AddrResolverMod` | `0x8b8a0bb4...` | versionable_addresses[version][node][coinType] |
|
|
111
|
+
| `TextResolverMod` | `0xccd57d47...` | versionable_texts[version][node][key] |
|
|
112
|
+
| `ContentHashResolverMod` | `0x792df830...` | versionable_hashes[version][node] |
|
|
113
|
+
| `VersionRecordMod` | `0xf2220575...` | recordVersions[node] |
|
|
114
|
+
| `L2RegistrarMod` | `0xd8d41403...` | registry, coinType, spaceFactory, currency |
|
|
115
|
+
| `DomainFeeHook` | `0x54805b38...` | defaultPrice, priceTiers, registrationCount, feeManager |
|
|
116
|
+
|
|
117
|
+
## Core Flows
|
|
118
|
+
|
|
119
|
+
### 1. Cross-chain Resolution Flow (L1 → Gateway → L2)
|
|
120
|
+
|
|
121
|
+
When a user resolves `ben.towns.eth` from Ethereum mainnet:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
User/Client: resolve("ben.towns.eth", addr(bytes32))
|
|
125
|
+
│
|
|
126
|
+
├─ 1. ENS Registry forwards to L1ResolverFacet
|
|
127
|
+
│
|
|
128
|
+
├─ 2. L1ResolverFacet.resolve()
|
|
129
|
+
│ ├─ Parse name → extract parent domain (towns.eth)
|
|
130
|
+
│ ├─ Look up L2Registry for parent node
|
|
131
|
+
│ └─ REVERT OffchainLookup(gatewayUrl, callData, callback)
|
|
132
|
+
│
|
|
133
|
+
├─ 3. Client calls CCIP Gateway
|
|
134
|
+
│ ├─ Gateway calls L2 Registry via RPC
|
|
135
|
+
│ ├─ Gets resolver data (addr, text, etc.)
|
|
136
|
+
│ └─ Returns signed response (result, expires, sig)
|
|
137
|
+
│
|
|
138
|
+
├─ 4. Client calls L1ResolverFacet.resolveWithProof()
|
|
139
|
+
│ ├─ Verify signature from gatewaySigner
|
|
140
|
+
│ ├─ Check expiration timestamp
|
|
141
|
+
│ └─ Return verified result
|
|
142
|
+
│
|
|
143
|
+
└─ 5. Client receives resolved address
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 2. Subdomain Registration Flow
|
|
147
|
+
|
|
148
|
+
When a user registers `ben.towns.eth`:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
User (Smart Account): L2RegistrarFacet.register("ben", accountAddr)
|
|
152
|
+
│
|
|
153
|
+
├─ 1. Verify Caller
|
|
154
|
+
│ └─ Must be IModularAccount (towns smart account)
|
|
155
|
+
│
|
|
156
|
+
├─ 2. Validate Label
|
|
157
|
+
│ ├─ Length: 3-63 characters
|
|
158
|
+
│ ├─ Characters: a-z, 0-9, hyphen
|
|
159
|
+
│ └─ No leading/trailing hyphens
|
|
160
|
+
│
|
|
161
|
+
├─ 3. Charge Fee (via FeeManager + DomainFeeHook)
|
|
162
|
+
│ ├─ First registration: FREE
|
|
163
|
+
│ └─ Subsequent: Tiered by label length
|
|
164
|
+
│
|
|
165
|
+
├─ 4. Create Subdomain (L2Registry.createSubdomain)
|
|
166
|
+
│ ├─ Compute subdomainHash = keccak256(parentNode, keccak256(label))
|
|
167
|
+
│ ├─ Mint NFT (tokenId = uint256(subdomainHash))
|
|
168
|
+
│ ├─ Store DNS-encoded name
|
|
169
|
+
│ └─ Set initial records via delegatecall
|
|
170
|
+
│
|
|
171
|
+
├─ 5. Set Address Records
|
|
172
|
+
│ ├─ setAddr(node, coinType, owner) for current chain
|
|
173
|
+
│ └─ setAddr(node, 60, owner) for ETH mainnet
|
|
174
|
+
│
|
|
175
|
+
└─ 6. Emit Events
|
|
176
|
+
├─ NewOwner(parentNode, labelhash, owner)
|
|
177
|
+
├─ SubnodeCreated(node, name, owner)
|
|
178
|
+
└─ NameRegistered(label, owner)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 3. Record Update Flow
|
|
182
|
+
|
|
183
|
+
When a domain owner updates resolver records:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
Owner: AddrResolverFacet.setAddr(usernameHash, coinType, addr)
|
|
187
|
+
TextResolverFacet.setText(usernameHash, "avatar", "ipfs://...")
|
|
188
|
+
│
|
|
189
|
+
├─ 1. Authorization Check (L2RegistryMod.onlyAuthorized)
|
|
190
|
+
│ ├─ Is caller the token owner?
|
|
191
|
+
│ ├─ Is caller approved by token owner?
|
|
192
|
+
│ └─ Is caller an approved registrar?
|
|
193
|
+
│
|
|
194
|
+
├─ 2. Get Current Version
|
|
195
|
+
│ └─ version = VersionRecordMod.recordVersions[usernameHash]
|
|
196
|
+
│
|
|
197
|
+
├─ 3. Update Storage
|
|
198
|
+
│ └─ versionable_addresses[version][usernameHash][coinType] = addr
|
|
199
|
+
│
|
|
200
|
+
└─ 4. Emit Event
|
|
201
|
+
└─ AddressChanged(usernameHash, coinType, addr)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 4. Record Clearing Flow
|
|
205
|
+
|
|
206
|
+
When an owner wants to clear all records atomically:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
Owner: clearRecords(node)
|
|
210
|
+
│
|
|
211
|
+
├─ 1. Increment version number
|
|
212
|
+
│ └─ recordVersions[node]++
|
|
213
|
+
│
|
|
214
|
+
└─ 2. All previous records become inaccessible
|
|
215
|
+
└─ Queries use new version, old records orphaned
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Access Control
|
|
219
|
+
|
|
220
|
+
### L2 Registry Access Control
|
|
221
|
+
|
|
222
|
+
| Action | Who Can Perform |
|
|
223
|
+
| ---------------------- | ------------------------------------------- |
|
|
224
|
+
| `createSubdomain` | Domain owner OR approved registrar |
|
|
225
|
+
| `addRegistrar` | Root domain owner only |
|
|
226
|
+
| `removeRegistrar` | Root domain owner only |
|
|
227
|
+
| `setMetadata` | Approved registrar only |
|
|
228
|
+
| `setAddr/setText/etc.` | Node owner, approved operator, OR registrar |
|
|
229
|
+
|
|
230
|
+
### L2 Registrar Access Control
|
|
231
|
+
|
|
232
|
+
| Action | Who Can Perform |
|
|
233
|
+
| ----------------- | ------------------------------------------- |
|
|
234
|
+
| `register` | Towns smart accounts only (IModularAccount) |
|
|
235
|
+
| `setRegistry` | Contract owner only |
|
|
236
|
+
| `setSpaceFactory` | Contract owner only |
|
|
237
|
+
| `setCurrency` | Contract owner only |
|
|
238
|
+
|
|
239
|
+
### L1 Resolver Access Control
|
|
240
|
+
|
|
241
|
+
| Action | Who Can Perform |
|
|
242
|
+
| ------------------ | ------------------------------------------ |
|
|
243
|
+
| `setL2Registry` | ENS node owner (via NameWrapper or direct) |
|
|
244
|
+
| `setGatewayURL` | Contract owner only |
|
|
245
|
+
| `setGatewaySigner` | Contract owner only |
|
|
246
|
+
|
|
247
|
+
## Label Validation Rules
|
|
248
|
+
|
|
249
|
+
The registrar enforces DNS-compatible label rules:
|
|
250
|
+
|
|
251
|
+
| Rule | Constraint |
|
|
252
|
+
| ------------------- | ------------------------------------- |
|
|
253
|
+
| **Length** | 3-63 characters |
|
|
254
|
+
| **Characters** | Lowercase a-z, digits 0-9, hyphen (-) |
|
|
255
|
+
| **Hyphen Position** | Cannot start or end with hyphen |
|
|
256
|
+
| **Case** | Must be lowercase (no uppercase) |
|
|
257
|
+
|
|
258
|
+
```solidity
|
|
259
|
+
// Character validation bitmask
|
|
260
|
+
ALLOWED_LABEL_CHARS = LOWERCASE_7_BIT_ASCII | DIGITS_7_BIT_ASCII | (1 << 45)
|
|
261
|
+
// 45 = ASCII code for hyphen '-'
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Fee Structure
|
|
265
|
+
|
|
266
|
+
### DomainFeeHook Pricing Logic
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
Registration Fee Calculation:
|
|
270
|
+
│
|
|
271
|
+
├─ If registrationCount[user] == 0
|
|
272
|
+
│ └─ Fee = 0 (first registration FREE)
|
|
273
|
+
│
|
|
274
|
+
└─ Else
|
|
275
|
+
├─ Get labelLength from context
|
|
276
|
+
├─ price = priceTiers[labelLength]
|
|
277
|
+
└─ If price == 0 → use defaultPrice $5.00
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Fee Hook Integration
|
|
281
|
+
|
|
282
|
+
```solidity
|
|
283
|
+
// In L2RegistrarMod.chargeFee()
|
|
284
|
+
uint256 expectedFee = IFeeManager(spaceFactory).calculateFee(
|
|
285
|
+
FeeTypesLib.DOMAIN_REGISTRATION,
|
|
286
|
+
msg.sender,
|
|
287
|
+
0,
|
|
288
|
+
abi.encode(bytes(label).length) // extraData = label length
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
ProtocolFeeLib.chargeAlways(
|
|
292
|
+
spaceFactory,
|
|
293
|
+
FeeTypesLib.DOMAIN_REGISTRATION,
|
|
294
|
+
msg.sender,
|
|
295
|
+
currency,
|
|
296
|
+
expectedFee,
|
|
297
|
+
expectedFee,
|
|
298
|
+
extraData
|
|
299
|
+
);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## ENS Compatibility
|
|
303
|
+
|
|
304
|
+
### Supported Resolver Interfaces
|
|
305
|
+
|
|
306
|
+
| Interface | EIP | Function |
|
|
307
|
+
| ---------------------- | -------- | ---------------------------------------------- |
|
|
308
|
+
| `IAddrResolver` | EIP-137 | `addr(bytes32 node)` → ETH address |
|
|
309
|
+
| `IAddressResolver` | EIP-2304 | `addr(bytes32, uint256 coinType)` → multi-coin |
|
|
310
|
+
| `ITextResolver` | EIP-634 | `text(bytes32, string key)` → text records |
|
|
311
|
+
| `IContentHashResolver` | EIP-1577 | `contenthash(bytes32)` → IPFS/Swarm |
|
|
312
|
+
| `IExtendedResolver` | EIP-3668 | `resolve(bytes, bytes)` → CCIP-Read |
|
|
313
|
+
|
|
314
|
+
### Coin Type Mapping (ENSIP-11)
|
|
315
|
+
|
|
316
|
+
For L2 address resolution, coin types are computed as:
|
|
317
|
+
|
|
318
|
+
```solidity
|
|
319
|
+
// ENSIP-11: Maps EVM chainId to ENS coinType
|
|
320
|
+
coinType = 0x80000000 | block.chainid
|
|
321
|
+
|
|
322
|
+
// Examples:
|
|
323
|
+
// Base (chainId 8453) → coinType 2147492101
|
|
324
|
+
// Ethereum (chainId 1) → coinType 60 (SLIP-44 standard)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Common Text Record Keys
|
|
328
|
+
|
|
329
|
+
| Key | Purpose |
|
|
330
|
+
| --------------------- | --------------------------------- |
|
|
331
|
+
| `displayName` | Human-readable display name |
|
|
332
|
+
| `avatar` | Profile picture URL (IPFS, HTTPS) |
|
|
333
|
+
| `description` / `bio` | User bio text |
|
|
334
|
+
| `url` | Personal website |
|
|
335
|
+
| `email` | Email address |
|
|
336
|
+
| `com.twitter` | Twitter handle |
|
|
337
|
+
| `com.discord` | Discord username |
|
|
338
|
+
| `com.github` | GitHub username |
|
|
339
|
+
|
|
340
|
+
## Integration Examples
|
|
341
|
+
|
|
342
|
+
### Register a Subdomain
|
|
343
|
+
|
|
344
|
+
```solidity
|
|
345
|
+
// User must be a Towns smart account (IModularAccount)
|
|
346
|
+
IL2Registrar registrar = IL2Registrar(REGISTRAR_ADDRESS);
|
|
347
|
+
|
|
348
|
+
// Check availability first
|
|
349
|
+
require(registrar.isAvailable("ben"), "Not available");
|
|
350
|
+
|
|
351
|
+
// Register (first one is free!)
|
|
352
|
+
registrar.register("ben", msg.sender);
|
|
353
|
+
|
|
354
|
+
// Result: ben.towns.eth is now owned by msg.sender
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Resolve an Address (L2 Direct)
|
|
358
|
+
|
|
359
|
+
```solidity
|
|
360
|
+
IL2Registry registry = IL2Registry(REGISTRY_ADDRESS);
|
|
361
|
+
|
|
362
|
+
// Compute namehash
|
|
363
|
+
bytes32 node = registry.namehash("ben.towns.eth");
|
|
364
|
+
|
|
365
|
+
// Get ETH address (coinType 60)
|
|
366
|
+
address owner = IAddrResolver(address(registry)).addr(node);
|
|
367
|
+
|
|
368
|
+
// Get Base address (coinType 2147492101)
|
|
369
|
+
bytes memory baseAddr = IAddressResolver(address(registry)).addr(node, 2147492101);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Set Profile Records
|
|
373
|
+
|
|
374
|
+
```solidity
|
|
375
|
+
IL2Registry registry = IL2Registry(REGISTRY_ADDRESS);
|
|
376
|
+
bytes32 usernameHash = registry.namehash("ben.towns.eth");
|
|
377
|
+
|
|
378
|
+
// Only usernameHash owner can set records
|
|
379
|
+
ITextResolver(address(registry)).setText(usernameHash, "displayName", "ben");
|
|
380
|
+
ITextResolver(address(registry)).setText(usernameHash, "avatar", "ipfs://Qm...");
|
|
381
|
+
ITextResolver(address(registry)).setText(usernameHash, "bio", "Building on Towns");
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Resolve via CCIP-Read (L1)
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
// Using viem/ethers with ENS resolution
|
|
388
|
+
import { normalize } from "viem/ens";
|
|
389
|
+
|
|
390
|
+
const client = createPublicClient({ chain: mainnet, transport: http() });
|
|
391
|
+
|
|
392
|
+
// This triggers CCIP-Read automatically
|
|
393
|
+
const address = await client.getEnsAddress({
|
|
394
|
+
name: normalize("ben.towns.eth"),
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Events
|
|
399
|
+
|
|
400
|
+
### L2 Registry Events
|
|
401
|
+
|
|
402
|
+
| Event | Parameters | When |
|
|
403
|
+
| ------------------ | ------------------------------ | ------------------------------ |
|
|
404
|
+
| `SubnodeCreated` | `node, name, owner` | New subdomain minted |
|
|
405
|
+
| `NewOwner` | `parentNode, labelhash, owner` | ENS-compatible ownership event |
|
|
406
|
+
| `RegistrarAdded` | `registrar` | New registrar approved |
|
|
407
|
+
| `RegistrarRemoved` | `registrar` | Registrar removed |
|
|
408
|
+
| `MetadataSet` | `node, metadata` | Subdomain metadata updated |
|
|
409
|
+
|
|
410
|
+
### Resolver Events
|
|
411
|
+
|
|
412
|
+
| Event | Parameters | When |
|
|
413
|
+
| -------------------- | ----------------------- | -------------------------- |
|
|
414
|
+
| `AddrChanged` | `node, addr` | ETH address updated |
|
|
415
|
+
| `AddressChanged` | `node, coinType, addr` | Multi-coin address updated |
|
|
416
|
+
| `TextChanged` | `node, key, key, value` | Text record updated |
|
|
417
|
+
| `ContenthashChanged` | `node, hash` | Content hash updated |
|
|
418
|
+
| `VersionChanged` | `node, version` | Records cleared |
|
|
419
|
+
|
|
420
|
+
### L1 Resolver Events
|
|
421
|
+
|
|
422
|
+
| Event | Parameters | When |
|
|
423
|
+
| ------------------ | -------------------------------- | ------------------------- |
|
|
424
|
+
| `GatewayURLSet` | `gatewayUrl` | Gateway URL updated |
|
|
425
|
+
| `GatewaySignerSet` | `gatewaySigner` | Signer address updated |
|
|
426
|
+
| `L2RegistrySet` | `node, chainId, registryAddress` | L2 registry mapping added |
|
|
427
|
+
|
|
428
|
+
## Error Reference
|
|
429
|
+
|
|
430
|
+
### L1 Resolver Errors
|
|
431
|
+
|
|
432
|
+
| Error | Cause |
|
|
433
|
+
| ---------------------------------- | -------------------------------- |
|
|
434
|
+
| `L1Resolver__InvalidGatewayURL` | Empty gateway URL |
|
|
435
|
+
| `L1Resolver__InvalidGatewaySigner` | Zero address signer |
|
|
436
|
+
| `L1Resolver__InvalidL2Registry` | No L2 registry for parent domain |
|
|
437
|
+
| `L1Resolver__InvalidName` | Name has fewer than 2 parts |
|
|
438
|
+
| `L1Resolver__InvalidOwner` | Caller not ENS node owner |
|
|
439
|
+
| `L1Resolver__SignatureExpired` | Gateway response expired |
|
|
440
|
+
| `L1Resolver__InvalidSignature` | Signature verification failed |
|
|
441
|
+
|
|
442
|
+
### L2 Registry Errors
|
|
443
|
+
|
|
444
|
+
| Error | Cause |
|
|
445
|
+
| ----------------------------------- | ----------------------------------- |
|
|
446
|
+
| `L2RegistryMod_LabelTooShort` | Label < 1 character |
|
|
447
|
+
| `L2RegistryMod_LabelTooLong` | Label > 255 characters |
|
|
448
|
+
| `L2RegistryMod_NotAvailable` | Subdomain already exists |
|
|
449
|
+
| `L2RegistryMod_NotOwnerOrRegistrar` | Unauthorized for subdomain creation |
|
|
450
|
+
| `L2RegistryMod_NotOwner` | Not root domain owner |
|
|
451
|
+
| `L2RegistryMod_NotRegistrar` | Not approved registrar |
|
|
452
|
+
| `L2RegistryMod_NotAuthorized` | Not authorized for record updates |
|
|
453
|
+
|
|
454
|
+
### L2 Registrar Errors
|
|
455
|
+
|
|
456
|
+
| Error | Cause |
|
|
457
|
+
| ------------------------------ | ---------------------------- |
|
|
458
|
+
| `L2Registrar__InvalidLabel` | Label fails validation rules |
|
|
459
|
+
| `L2Registrar__NotSmartAccount` | Caller not IModularAccount |
|
|
460
|
+
|
|
461
|
+
### DomainFeeHook Errors
|
|
462
|
+
|
|
463
|
+
| Error | Cause |
|
|
464
|
+
| ------------------------------- | --------------------------------------- |
|
|
465
|
+
| `DomainFeeHook__InvalidContext` | Missing/invalid label length in context |
|
|
466
|
+
| `DomainFeeHook__LengthMismatch` | Array lengths don't match in batch set |
|
|
467
|
+
| `DomainFeeHook__Unauthorized` | Caller not authorized FeeManager |
|
|
468
|
+
| `DomainFeeHook__TooManyLengths` | Batch update exceeds 10 tiers |
|
|
469
|
+
|
|
470
|
+
## Security Considerations
|
|
471
|
+
|
|
472
|
+
1. **Gateway Signer Key Security**: The L1 resolver trusts responses signed by `gatewaySigner`. Key compromise allows forged resolutions.
|
|
473
|
+
|
|
474
|
+
2. **Signature Expiration**: Gateway responses include expiration timestamps. Clients should verify freshness.
|
|
475
|
+
|
|
476
|
+
3. **Registrar Approval**: Only approved registrars can mint subdomains without being domain owners. Carefully manage registrar list.
|
|
477
|
+
|
|
478
|
+
4. **Smart Account Requirement**: Registration requires IModularAccount to prevent bot spam and ensure identity verification.
|
|
479
|
+
|
|
480
|
+
5. **Record Versioning**: Incrementing a node's version atomically invalidates all records - useful for ownership transfers.
|
|
481
|
+
|
|
482
|
+
6. **Cross-chain Consistency**: L1 resolver maps to specific L2 registry addresses. Ensure mappings stay synchronized.
|
|
483
|
+
|
|
484
|
+
## Appendix: Key Functions
|
|
485
|
+
|
|
486
|
+
### Registration
|
|
487
|
+
|
|
488
|
+
```solidity
|
|
489
|
+
// L2RegistrarFacet.sol
|
|
490
|
+
function register(string calldata label, address owner) external nonReentrant
|
|
491
|
+
|
|
492
|
+
// L2RegistryFacet.sol
|
|
493
|
+
function createSubdomain(
|
|
494
|
+
bytes32 domainHash,
|
|
495
|
+
string calldata subdomain,
|
|
496
|
+
address owner,
|
|
497
|
+
bytes[] calldata records,
|
|
498
|
+
bytes calldata metadata
|
|
499
|
+
) external
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Resolution
|
|
503
|
+
|
|
504
|
+
```solidity
|
|
505
|
+
// L1ResolverFacet.sol
|
|
506
|
+
function resolve(bytes calldata name, bytes calldata data) external view returns (bytes memory)
|
|
507
|
+
function resolveWithProof(bytes calldata response, bytes calldata extraData) external view returns (bytes memory)
|
|
508
|
+
|
|
509
|
+
// AddrResolverFacet.sol
|
|
510
|
+
function addr(bytes32 node) external view returns (address payable)
|
|
511
|
+
function addr(bytes32 node, uint256 coinType) external view returns (bytes memory)
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Record Management
|
|
515
|
+
|
|
516
|
+
```solidity
|
|
517
|
+
// TextResolverFacet.sol
|
|
518
|
+
function setText(bytes32 node, string calldata key, string calldata value) external
|
|
519
|
+
function text(bytes32 node, string calldata key) external view returns (string memory)
|
|
520
|
+
|
|
521
|
+
// AddrResolverFacet.sol
|
|
522
|
+
function setAddr(bytes32 node, address a) external
|
|
523
|
+
function setAddr(bytes32 node, uint256 coinType, bytes memory a) external
|
|
524
|
+
|
|
525
|
+
// ContentHashResolverFacet.sol
|
|
526
|
+
function setContenthash(bytes32 node, bytes calldata hash) external
|
|
527
|
+
function contenthash(bytes32 node) external view returns (bytes memory)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Administration
|
|
531
|
+
|
|
532
|
+
```solidity
|
|
533
|
+
// L2RegistryFacet.sol
|
|
534
|
+
function addRegistrar(address registrar) external
|
|
535
|
+
function removeRegistrar(address registrar) external
|
|
536
|
+
|
|
537
|
+
// L1ResolverFacet.sol
|
|
538
|
+
function setL2Registry(bytes32 node, uint64 chainId, address registryAddress) external
|
|
539
|
+
function setGatewayURL(string calldata gatewayUrl) external onlyOwner
|
|
540
|
+
function setGatewaySigner(address gatewaySigner) external onlyOwner
|
|
541
|
+
```
|