@towns-protocol/contracts 0.0.453 → 1.0.1
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/package.json +7 -7
- package/scripts/common/Interaction.s.sol +3 -3
- package/scripts/deployments/diamonds/DeployBaseRegistry.s.sol +1 -4
- package/scripts/deployments/diamonds/DeployL1Resolver.s.sol +155 -0
- package/scripts/deployments/diamonds/DeployL2Registrar.s.sol +173 -0
- package/scripts/deployments/diamonds/DeployL2Resolver.s.sol +196 -0
- package/scripts/deployments/diamonds/DeploySpace.s.sol +1 -9
- package/scripts/deployments/diamonds/DeploySpaceFactory.s.sol +1 -5
- package/scripts/deployments/facets/DeployAddrResolverFacet.s.sol +36 -0
- package/scripts/deployments/facets/DeployAppFactoryFacet.s.sol +3 -3
- package/scripts/deployments/facets/DeployAppInstallerFacet.s.sol +3 -3
- package/scripts/deployments/facets/DeployAppRegistryFacet.s.sol +3 -3
- package/scripts/deployments/facets/DeployArchitect.s.sol +3 -3
- package/scripts/deployments/facets/DeployAttestationRegistry.s.sol +3 -3
- package/scripts/deployments/facets/DeployContentHashResolverFacet.s.sol +34 -0
- package/scripts/deployments/facets/DeployERC721ANonTransferable.s.sol +3 -3
- package/scripts/deployments/facets/DeployExtendedResolverFacet.s.sol +33 -0
- package/scripts/deployments/facets/DeployFeatureManager.s.sol +3 -3
- package/scripts/deployments/facets/DeployL1ResolverFacet.s.sol +61 -0
- package/scripts/deployments/facets/DeployL2RegistrarFacet.s.sol +56 -0
- package/scripts/deployments/facets/DeployL2RegistryFacet.s.sol +71 -0
- package/scripts/deployments/facets/DeployMainnetDelegation.s.sol +3 -3
- package/scripts/deployments/facets/DeployMembershipMetadata.s.sol +3 -3
- package/scripts/deployments/facets/DeployMerkleAirdrop.s.sol +3 -3
- package/scripts/deployments/facets/DeployMetadata.s.sol +3 -3
- package/scripts/deployments/facets/DeployMockLegacyArchitect.s.sol +3 -3
- package/scripts/deployments/facets/DeployNodeOperator.s.sol +3 -3
- package/scripts/deployments/facets/DeployPartnerRegistry.s.sol +3 -3
- package/scripts/deployments/facets/DeployPricingModules.s.sol +3 -3
- package/scripts/deployments/facets/DeploySchemaRegistry.s.sol +3 -3
- package/scripts/deployments/facets/DeploySpaceFactoryInit.s.sol +3 -3
- package/scripts/deployments/facets/DeploySpaceOwnerFacet.s.sol +3 -3
- package/scripts/deployments/facets/DeployTextResolverFacet.s.sol +34 -0
- package/scripts/deployments/facets/DeployTokenMigration.s.sol +3 -3
- package/scripts/deployments/facets/DeployXChain.s.sol +3 -3
- package/scripts/deployments/utils/DeployAccountFactory.s.sol +3 -3
- package/scripts/deployments/utils/DeployEntitlementGatedExample.s.sol +3 -3
- package/scripts/deployments/utils/DeployEntrypoint.s.sol +3 -3
- package/scripts/deployments/utils/DeployMember.s.sol +3 -3
- package/scripts/deployments/utils/DeployMockLegacyMembership.s.sol +3 -3
- package/scripts/deployments/utils/DeployMockMessenger.s.sol +3 -3
- package/scripts/deployments/utils/DeploySpaceProxyInitializer.s.sol +3 -3
- package/scripts/deployments/utils/DeployTieredLogPricingV2.s.sol +3 -3
- package/scripts/deployments/utils/DeployTieredLogPricingV3.s.sol +3 -3
- package/scripts/deployments/utils/DeployTownsBase.s.sol +3 -3
- package/scripts/deployments/utils/DeployTownsMainnet.s.sol +3 -3
- package/scripts/deployments/utils/pricing/TieredLogPricing.s.sol +3 -3
- package/scripts/interactions/InteractAirdrop.s.sol +3 -3
- package/scripts/interactions/InteractBaseBridge.s.sol +3 -3
- package/scripts/interactions/InteractRegisterApp.s.sol +2 -2
- package/scripts/interactions/InteractRiverRegistrySetTrimByStreamId.s.sol +44 -0
- package/scripts/interactions/helpers/RiverConfigValues.sol +1 -2
- package/src/account/facets/app/AppManagerFacet.sol +43 -14
- package/src/account/facets/app/AppManagerMod.sol +311 -278
- package/src/account/facets/hub/AccountHubFacet.sol +12 -11
- package/src/account/facets/hub/AccountHubMod.sol +117 -116
- package/src/account/facets/tipping/AccountTippingFacet.sol +10 -9
- package/src/account/facets/tipping/AccountTippingMod.sol +133 -124
- package/src/apps/modules/subscription/SubscriptionModuleStorage.sol +1 -1
- package/src/domains/facets/l1/IL1ResolverService.sol +20 -0
- package/src/domains/facets/l1/L1ResolverFacet.sol +100 -0
- package/src/domains/facets/l1/L1ResolverMod.sol +245 -0
- package/src/domains/facets/l2/AddrResolverFacet.sol +59 -0
- package/src/domains/facets/l2/ContentHashResolverFacet.sol +41 -0
- package/src/domains/facets/l2/ExtendedResolverFacet.sol +38 -0
- package/src/domains/facets/l2/IL2Registry.sol +79 -0
- package/src/domains/facets/l2/L2RegistryFacet.sol +203 -0
- package/src/domains/facets/l2/TextResolverFacet.sol +43 -0
- package/src/domains/facets/l2/modules/AddrResolverMod.sol +110 -0
- package/src/domains/facets/l2/modules/ContentHashResolverMod.sol +60 -0
- package/src/domains/facets/l2/modules/L2RegistryMod.sol +286 -0
- package/src/domains/facets/l2/modules/TextResolverMod.sol +62 -0
- package/src/domains/facets/l2/modules/VersionRecordMod.sol +42 -0
- package/src/domains/facets/registrar/IL2Registrar.sol +57 -0
- package/src/domains/facets/registrar/L2RegistrarFacet.sol +127 -0
- package/src/domains/facets/registrar/L2RegistrarMod.sol +224 -0
- package/src/domains/hooks/DomainFeeHook.sol +212 -0
- package/src/factory/facets/fee/FeeTypesLib.sol +3 -0
- package/src/river/registry/facets/stream/StreamRegistry.sol +4 -1
- package/src/spaces/facets/ProtocolFeeLib.sol +56 -1
- package/src/tokens/Member.sol +3 -4
- package/LICENSE.txt +0 -21
- package/scripts/interactions/InteractRiverRegistrySetFreq.s.sol +0 -27
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.29;
|
|
3
|
+
|
|
4
|
+
// libraries
|
|
5
|
+
import {CustomRevert} from "src/utils/libraries/CustomRevert.sol";
|
|
6
|
+
import {MinimalERC721Storage, ERC721Lib} from "@towns-protocol/diamond/src/primitive/ERC721.sol";
|
|
7
|
+
import {NameCoder} from "@ensdomains/ens-contracts/utils/NameCoder.sol";
|
|
8
|
+
import {Base64} from "solady/utils/Base64.sol";
|
|
9
|
+
|
|
10
|
+
/// @title L2RegistryMod
|
|
11
|
+
/// @notice L2 domain registry module for managing ENS-compatible subdomains as NFTs on L2
|
|
12
|
+
/// @dev Provides storage and logic for minting subdomains, managing registrars, and storing DNS-encoded names
|
|
13
|
+
library L2RegistryMod {
|
|
14
|
+
using CustomRevert for bytes4;
|
|
15
|
+
using ERC721Lib for MinimalERC721Storage;
|
|
16
|
+
|
|
17
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
18
|
+
/* STORAGE */
|
|
19
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
20
|
+
|
|
21
|
+
// keccak256(abi.encode(uint256(keccak256("towns.domains.facets.l2.registry.storage")) - 1)) & ~bytes32(uint256(0xff))
|
|
22
|
+
bytes32 constant STORAGE_SLOT =
|
|
23
|
+
0xd006f5666f0513641f83237d8fe37b671902940d193477fdbf21bf0fa624e400;
|
|
24
|
+
|
|
25
|
+
/// @notice Storage layout for the L2 registry
|
|
26
|
+
/// @dev baseNode is the root domain hash (e.g., namehash("towns.eth")), names maps node to DNS-encoded name, metadata is arbitrary bytes set by registrar, registrars are approved minters, token is the ERC721 storage
|
|
27
|
+
struct Layout {
|
|
28
|
+
bytes32 baseNode;
|
|
29
|
+
mapping(bytes32 node => bytes name) names;
|
|
30
|
+
mapping(bytes32 node => bytes) metadata;
|
|
31
|
+
mapping(address registrar => bool approved) registrars;
|
|
32
|
+
MinimalERC721Storage token;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// @notice Returns the storage layout for this module
|
|
36
|
+
function getStorage() internal pure returns (Layout storage $) {
|
|
37
|
+
assembly {
|
|
38
|
+
$.slot := STORAGE_SLOT
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
43
|
+
/* EVENTS */
|
|
44
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
45
|
+
|
|
46
|
+
/// @notice Emitted when a name is created at any level
|
|
47
|
+
event SubnodeCreated(bytes32 indexed node, bytes name, address owner);
|
|
48
|
+
|
|
49
|
+
/// @notice Emitted when a subnode is registered at any level
|
|
50
|
+
/// @dev Same event signature as the ENS Registry
|
|
51
|
+
event NewOwner(bytes32 indexed parentNode, bytes32 indexed labelhash, address owner);
|
|
52
|
+
|
|
53
|
+
/// @notice Emitted when a registrar is added
|
|
54
|
+
event RegistrarAdded(address registrar);
|
|
55
|
+
|
|
56
|
+
/// @notice Emitted when a registrar is removed
|
|
57
|
+
event RegistrarRemoved(address registrar);
|
|
58
|
+
|
|
59
|
+
/// @notice Emitted when the base node is set
|
|
60
|
+
event BaseNodeUpdated(bytes32 baseNode);
|
|
61
|
+
|
|
62
|
+
/// @notice Emitted when metadata is set or updated for a node
|
|
63
|
+
event MetadataSet(bytes32 indexed node, bytes metadata);
|
|
64
|
+
|
|
65
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
66
|
+
/* ERRORS */
|
|
67
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
68
|
+
error L2RegistryMod_LabelTooShort();
|
|
69
|
+
error L2RegistryMod_LabelTooLong();
|
|
70
|
+
error L2RegistryMod_NotAvailable();
|
|
71
|
+
error L2RegistryMod_NotOwnerOrRegistrar();
|
|
72
|
+
error L2RegistryMod_SetRecordsFailed();
|
|
73
|
+
error L2RegistryMod_SetRecordsInvalidNamehash();
|
|
74
|
+
error L2RegistryMod_NotOwner();
|
|
75
|
+
error L2RegistryMod_DomainAlreadyExists();
|
|
76
|
+
error L2RegistryMod_NotAuthorized();
|
|
77
|
+
error L2RegistryMod_NotRegistrar();
|
|
78
|
+
|
|
79
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
80
|
+
/* FUNCTIONS */
|
|
81
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
82
|
+
|
|
83
|
+
/// @notice Creates the root domain for this registry (e.g., "towns.eth") and mints it as an NFT to the owner
|
|
84
|
+
/// @dev Can only be called once per registry; sets baseNode and stores the DNS-encoded name
|
|
85
|
+
/// @param domain The full domain name (e.g., "towns.eth")
|
|
86
|
+
/// @param owner The address that will own the root domain NFT
|
|
87
|
+
/// @return domainHash The namehash of the created domain
|
|
88
|
+
function createDomain(
|
|
89
|
+
Layout storage $,
|
|
90
|
+
string calldata domain,
|
|
91
|
+
address owner
|
|
92
|
+
) internal returns (bytes32 domainHash) {
|
|
93
|
+
bytes memory dnsEncodedName = NameCoder.encode(domain);
|
|
94
|
+
domainHash = NameCoder.namehash(dnsEncodedName, 0);
|
|
95
|
+
|
|
96
|
+
if ($.token.owners.get(uint256(domainHash)) != address(0))
|
|
97
|
+
L2RegistryMod_DomainAlreadyExists.selector.revertWith();
|
|
98
|
+
|
|
99
|
+
$.baseNode = domainHash;
|
|
100
|
+
$.names[domainHash] = dnsEncodedName;
|
|
101
|
+
$.token.mint(owner, uint256(domainHash));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// @notice Creates a subdomain from a parent domain hash and label
|
|
105
|
+
/// @dev Only callable by the owner of the parent domain or an approved registrar. Once minted, a subdomain cannot be re-registered.
|
|
106
|
+
/// @param domainHash The hash of the domain, e.g. `namehash("name.eth")` for "name.eth"
|
|
107
|
+
/// @param subdomain The subdomain of the subdomain, e.g. "x" for "x.name.eth"
|
|
108
|
+
/// @param owner The address that will own the subdomain
|
|
109
|
+
/// @param records The encoded calldata for resolver setters
|
|
110
|
+
/// @param metadata Arbitrary bytes that registrar can use (e.g., expiration, tier, etc.)
|
|
111
|
+
/// @return subdomainHash The resulting subdomain hash, e.g. `namehash("x.name.eth")` for "x.name.eth"
|
|
112
|
+
function createSubdomain(
|
|
113
|
+
Layout storage $,
|
|
114
|
+
bytes32 domainHash,
|
|
115
|
+
string calldata subdomain,
|
|
116
|
+
address owner,
|
|
117
|
+
bytes[] calldata records,
|
|
118
|
+
bytes calldata metadata
|
|
119
|
+
) internal returns (bytes32 subdomainHash) {
|
|
120
|
+
subdomainHash = encodeNode(domainHash, subdomain);
|
|
121
|
+
uint256 subnodeId = uint256(subdomainHash);
|
|
122
|
+
|
|
123
|
+
// Revert if subdomain already exists
|
|
124
|
+
if ($.token.owners.get(subnodeId) != address(0)) {
|
|
125
|
+
L2RegistryMod_NotAvailable.selector.revertWith();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// mint NFT and update storage
|
|
129
|
+
$.token.mint(owner, subnodeId);
|
|
130
|
+
$.names[subdomainHash] = encodeName(subdomain, $.names[domainHash]);
|
|
131
|
+
if (metadata.length > 0) $.metadata[subdomainHash] = metadata;
|
|
132
|
+
|
|
133
|
+
// delegatecall to resolver setters
|
|
134
|
+
setRecords(subdomainHash, records);
|
|
135
|
+
|
|
136
|
+
// Events
|
|
137
|
+
emit NewOwner(domainHash, keccak256(bytes(subdomain)), owner);
|
|
138
|
+
emit SubnodeCreated(subdomainHash, $.names[subdomainHash], owner);
|
|
139
|
+
if (metadata.length > 0) emit MetadataSet(subdomainHash, metadata);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// @notice Executes multiple resolver record setter calls via delegatecall (multicall pattern)
|
|
143
|
+
/// @dev Each call's first 32 bytes after selector must match subdomainHash to prevent cross-node writes
|
|
144
|
+
/// @param subdomainHash The node hash that all records must belong to (pass bytes32(0) to skip validation)
|
|
145
|
+
/// @param data Array of encoded function calls (e.g., setAddr, setText) to execute
|
|
146
|
+
/// @return results Array of return data from each delegatecall
|
|
147
|
+
function setRecords(
|
|
148
|
+
bytes32 subdomainHash,
|
|
149
|
+
bytes[] calldata data
|
|
150
|
+
) internal returns (bytes[] memory results) {
|
|
151
|
+
uint256 length = data.length;
|
|
152
|
+
results = new bytes[](length);
|
|
153
|
+
for (uint256 i; i < length; ++i) {
|
|
154
|
+
if (subdomainHash != bytes32(0)) {
|
|
155
|
+
if (data[i].length < 36)
|
|
156
|
+
L2RegistryMod_SetRecordsInvalidNamehash.selector.revertWith();
|
|
157
|
+
bytes32 txNamehash = bytes32(data[i][4:36]);
|
|
158
|
+
if (txNamehash != subdomainHash)
|
|
159
|
+
L2RegistryMod_SetRecordsInvalidNamehash.selector.revertWith();
|
|
160
|
+
}
|
|
161
|
+
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
|
|
162
|
+
if (!success) L2RegistryMod_SetRecordsFailed.selector.revertWith();
|
|
163
|
+
results[i] = result;
|
|
164
|
+
}
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// @notice Adds an address as an approved registrar that can mint subdomains
|
|
169
|
+
function addRegistrar(Layout storage $, address registrar) internal {
|
|
170
|
+
$.registrars[registrar] = true;
|
|
171
|
+
emit RegistrarAdded(registrar);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// @notice Removes an address from the approved registrars list
|
|
175
|
+
function removeRegistrar(Layout storage $, address registrar) internal {
|
|
176
|
+
$.registrars[registrar] = false;
|
|
177
|
+
emit RegistrarRemoved(registrar);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// @notice Updates the base node (root domain hash) for this registry
|
|
181
|
+
function setBaseNode(Layout storage $, bytes32 baseNode) internal {
|
|
182
|
+
$.baseNode = baseNode;
|
|
183
|
+
emit BaseNodeUpdated(baseNode);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// @notice Checks if an address is an approved registrar
|
|
187
|
+
function isRegistrar(Layout storage $, address registrar) internal view returns (bool) {
|
|
188
|
+
return $.registrars[registrar];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// @notice Sets or updates the metadata for a node
|
|
192
|
+
/// @dev Only callable by registrar. Metadata is arbitrary bytes that the registrar can interpret (e.g., expiration, tier, etc.)
|
|
193
|
+
/// @param node The namehash of the subdomain
|
|
194
|
+
/// @param data The metadata bytes to store
|
|
195
|
+
function setMetadata(Layout storage $, bytes32 node, bytes calldata data) internal {
|
|
196
|
+
$.metadata[node] = data;
|
|
197
|
+
emit MetadataSet(node, data);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// @notice Returns the metadata bytes for a node
|
|
201
|
+
/// @param node The namehash of the subdomain
|
|
202
|
+
/// @return The metadata bytes (empty if not set)
|
|
203
|
+
function getMetadata(Layout storage $, bytes32 node) internal view returns (bytes memory) {
|
|
204
|
+
return $.metadata[node];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// @notice Returns the token URI for a domain NFT as a base64-encoded JSON with the decoded domain name
|
|
208
|
+
function tokenURI(Layout storage $, uint256 tokenId) internal view returns (string memory) {
|
|
209
|
+
if ($.token.ownerOf(tokenId) == address(0)) L2RegistryMod_NotOwner.selector.revertWith();
|
|
210
|
+
|
|
211
|
+
string memory json = string.concat(
|
|
212
|
+
'{"name": "',
|
|
213
|
+
NameCoder.decode($.names[bytes32(tokenId)]),
|
|
214
|
+
'"}'
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return string.concat("data:application/json;base64,", Base64.encode(bytes(json)));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// @notice Computes the namehash of a subdomain from its parent node and label (keccak256(parentNode, keccak256(label)))
|
|
221
|
+
function encodeNode(bytes32 parentNode, string calldata label) internal pure returns (bytes32) {
|
|
222
|
+
bytes32 labelhash = keccak256(bytes(label));
|
|
223
|
+
return keccak256(abi.encodePacked(parentNode, labelhash));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// @notice Encodes a label and parent name into DNS wire format (length-prefixed label + parent name bytes)
|
|
227
|
+
function encodeName(
|
|
228
|
+
string memory label,
|
|
229
|
+
bytes memory name
|
|
230
|
+
) internal pure returns (bytes memory ret) {
|
|
231
|
+
if (bytes(label).length < 1) {
|
|
232
|
+
L2RegistryMod_LabelTooShort.selector.revertWith();
|
|
233
|
+
}
|
|
234
|
+
if (bytes(label).length > 255) {
|
|
235
|
+
L2RegistryMod_LabelTooLong.selector.revertWith();
|
|
236
|
+
}
|
|
237
|
+
return abi.encodePacked(uint8(bytes(label).length), label, name);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// @notice Reverts if caller is neither the node owner nor an approved registrar
|
|
241
|
+
function onlyOwnerOrRegistrar(Layout storage $, bytes32 node) internal view {
|
|
242
|
+
if ($.token.ownerOf(uint256(node)) != msg.sender && !$.registrars[msg.sender]) {
|
|
243
|
+
revert L2RegistryMod_NotOwnerOrRegistrar();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// @notice Reverts if caller is not the owner of the base (root) node
|
|
248
|
+
function onlyOwner(Layout storage $) internal view {
|
|
249
|
+
if ($.token.ownerOf(uint256($.baseNode)) != msg.sender) {
|
|
250
|
+
revert L2RegistryMod_NotOwner();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// @notice Reverts if caller is not an approved registrar
|
|
255
|
+
function onlyRegistrar(Layout storage $) internal view {
|
|
256
|
+
if (!$.registrars[msg.sender]) L2RegistryMod_NotRegistrar.selector.revertWith();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// @notice Reverts if caller is not authorized to modify the given node
|
|
260
|
+
function onlyAuthorized(bytes32 node) internal view {
|
|
261
|
+
if (!isAuthorized(node)) L2RegistryMod_NotAuthorized.selector.revertWith();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// @notice Checks if msg.sender is authorized to modify the given node (owner, approved, or registrar)
|
|
265
|
+
function isAuthorized(bytes32 node) internal view returns (bool) {
|
|
266
|
+
return isAuthorizedForAddress(getStorage(), msg.sender, node);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// @notice Checks if an address is authorized to modify a node (registrar, owner, or approved operator)
|
|
270
|
+
function isAuthorizedForAddress(
|
|
271
|
+
Layout storage $,
|
|
272
|
+
address addr,
|
|
273
|
+
bytes32 node
|
|
274
|
+
) internal view returns (bool) {
|
|
275
|
+
if ($.registrars[addr]) return true;
|
|
276
|
+
|
|
277
|
+
uint256 tokenId = uint256(node);
|
|
278
|
+
address owner = $.token.ownerOf(tokenId);
|
|
279
|
+
|
|
280
|
+
if ((owner != addr) && ($.token.getApproved(tokenId) != addr)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.29;
|
|
3
|
+
|
|
4
|
+
// interfaces
|
|
5
|
+
import {ITextResolver} from "@ensdomains/ens-contracts/resolvers/profiles/ITextResolver.sol";
|
|
6
|
+
|
|
7
|
+
/// @title TextResolverMod
|
|
8
|
+
/// @notice Stores and retrieves arbitrary text records (key-value strings) for ENS nodes with versioning support
|
|
9
|
+
/// @dev Implements ENS ITextResolver storage; records are keyed by (version, node, key) for atomic clearing
|
|
10
|
+
library TextResolverMod {
|
|
11
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
12
|
+
/* STORAGE */
|
|
13
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
14
|
+
|
|
15
|
+
// keccak256(abi.encode(uint256(keccak256("ens.domains.text.resolver.storage")) - 1)) & ~bytes32(uint256(0xff))
|
|
16
|
+
bytes32 constant STORAGE_SLOT =
|
|
17
|
+
0xccd57d47affdb87d4363b03aa46f3e8c1b9394057f472ce512f3a1ec1c667400;
|
|
18
|
+
|
|
19
|
+
/// @notice Storage layout with versioned text records: version => node => key => value
|
|
20
|
+
struct Layout {
|
|
21
|
+
mapping(uint64 => mapping(bytes32 => mapping(string => string))) versionable_texts;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// @notice Returns the storage layout for this module
|
|
25
|
+
function getStorage() internal pure returns (Layout storage $) {
|
|
26
|
+
assembly {
|
|
27
|
+
$.slot := STORAGE_SLOT
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
32
|
+
/* FUNCTIONS */
|
|
33
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
34
|
+
|
|
35
|
+
/// @notice Sets the text data associated with an ENS node and key
|
|
36
|
+
/// @param node The node to update
|
|
37
|
+
/// @param key The key to set
|
|
38
|
+
/// @param value The text data value to set
|
|
39
|
+
function setText(
|
|
40
|
+
Layout storage $,
|
|
41
|
+
uint64 version,
|
|
42
|
+
bytes32 node,
|
|
43
|
+
string calldata key,
|
|
44
|
+
string calldata value
|
|
45
|
+
) internal {
|
|
46
|
+
$.versionable_texts[version][node][key] = value;
|
|
47
|
+
emit ITextResolver.TextChanged(node, key, key, value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// @notice Returns the text data associated with an ENS node and key
|
|
51
|
+
/// @param node The ENS node to query
|
|
52
|
+
/// @param key The text data key to query
|
|
53
|
+
/// @return The associated text data
|
|
54
|
+
function text(
|
|
55
|
+
Layout storage $,
|
|
56
|
+
uint64 version,
|
|
57
|
+
bytes32 node,
|
|
58
|
+
string calldata key
|
|
59
|
+
) internal view returns (string memory) {
|
|
60
|
+
return $.versionable_texts[version][node][key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.29;
|
|
3
|
+
|
|
4
|
+
// interfaces
|
|
5
|
+
import {IVersionableResolver} from "@ensdomains/ens-contracts/resolvers/profiles/IVersionableResolver.sol";
|
|
6
|
+
|
|
7
|
+
/// @title VersionRecordMod
|
|
8
|
+
/// @notice Manages record versioning for ENS resolver records, enabling atomic invalidation of all records for a node
|
|
9
|
+
/// @dev Incrementing a node's version effectively clears all its records since resolvers key records by (version, node)
|
|
10
|
+
library VersionRecordMod {
|
|
11
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
12
|
+
/* STORAGE */
|
|
13
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
14
|
+
|
|
15
|
+
// keccak256(abi.encode(uint256(keccak256("ens.domains.version.record.storage")) - 1)) & ~bytes32(uint256(0xff))
|
|
16
|
+
bytes32 constant STORAGE_SLOT =
|
|
17
|
+
0xf22205753714587a1da0c0ad29dc01aa3237bb3fbfb1b984b23c0773da3cb700;
|
|
18
|
+
|
|
19
|
+
/// @notice Storage layout mapping each node to its current record version number
|
|
20
|
+
struct Layout {
|
|
21
|
+
mapping(bytes32 => uint64) recordVersions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// @notice Returns the storage layout for this module
|
|
25
|
+
function getStorage() internal pure returns (Layout storage $) {
|
|
26
|
+
assembly {
|
|
27
|
+
$.slot := STORAGE_SLOT
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
32
|
+
/* FUNCTIONS */
|
|
33
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
34
|
+
|
|
35
|
+
/// @notice Clears all records for a node by incrementing its version number
|
|
36
|
+
/// @dev Records are keyed by (version, node), so incrementing version makes old records inaccessible
|
|
37
|
+
function clearRecords(bytes32 node) internal {
|
|
38
|
+
Layout storage $ = getStorage();
|
|
39
|
+
uint64 version = $.recordVersions[node]++;
|
|
40
|
+
emit IVersionableResolver.VersionChanged(node, version);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.29;
|
|
3
|
+
|
|
4
|
+
/// @title IL2Registrar
|
|
5
|
+
/// @notice Interface for the L2 subdomain registrar
|
|
6
|
+
/// @dev Handles subdomain registration with label validation for Towns domains.
|
|
7
|
+
/// Only Towns smart accounts (IModularAccount) can register domains.
|
|
8
|
+
/// First registration per account is free, subsequent registrations are charged via FeeManager.
|
|
9
|
+
interface IL2Registrar {
|
|
10
|
+
/// @notice Registers a new subdomain
|
|
11
|
+
/// @dev Only callable by Towns smart accounts (IModularAccount).
|
|
12
|
+
/// First registration is free, subsequent ones are charged via FeeManager.
|
|
13
|
+
/// @param label The subdomain label to register (e.g., "alice" for "alice.towns.eth")
|
|
14
|
+
/// @param owner The address that will own the subdomain NFT
|
|
15
|
+
function register(string calldata label, address owner) external;
|
|
16
|
+
|
|
17
|
+
/// @notice Sets the space factory address (owner only)
|
|
18
|
+
/// @param spaceFactory The new space factory address
|
|
19
|
+
function setSpaceFactory(address spaceFactory) external;
|
|
20
|
+
|
|
21
|
+
/// @notice Sets the registry address (owner only)
|
|
22
|
+
/// @param registry The new registry address
|
|
23
|
+
function setRegistry(address registry) external;
|
|
24
|
+
|
|
25
|
+
/// @notice Sets the currency (owner only)
|
|
26
|
+
/// @param currency The new currency
|
|
27
|
+
function setCurrency(address currency) external;
|
|
28
|
+
|
|
29
|
+
/// @notice Checks if a label is available for registration
|
|
30
|
+
/// @dev Returns false if label is invalid OR already registered
|
|
31
|
+
/// @param label The subdomain label to check (e.g., "alice")
|
|
32
|
+
/// @return True if the label can be registered, false otherwise
|
|
33
|
+
function isAvailable(string calldata label) external view returns (bool);
|
|
34
|
+
|
|
35
|
+
/// @notice Returns the registry address this registrar points to
|
|
36
|
+
/// @return The address of the L2Registry diamond contract
|
|
37
|
+
function getRegistry() external view returns (address);
|
|
38
|
+
|
|
39
|
+
/// @notice Returns the coinType used for address resolution
|
|
40
|
+
/// @dev Computed as 0x80000000 | chainId per ENSIP-11
|
|
41
|
+
/// @return The ENSIP-11 compliant coinType for this chain
|
|
42
|
+
function getCoinType() external view returns (uint256);
|
|
43
|
+
|
|
44
|
+
/// @notice Returns the space factory address
|
|
45
|
+
/// @return The address of the SpaceFactory diamond contract
|
|
46
|
+
function getSpaceFactory() external view returns (address);
|
|
47
|
+
|
|
48
|
+
/// @notice Returns the currency used for registration fees
|
|
49
|
+
/// @return The currency used for registration fees
|
|
50
|
+
function getCurrency() external view returns (address);
|
|
51
|
+
|
|
52
|
+
/// @notice Checks if a label format is valid (without checking availability)
|
|
53
|
+
/// @dev Validates: length (3-63), allowed chars (a-z, 0-9, hyphen), no leading/trailing hyphen
|
|
54
|
+
/// @param label The subdomain label to validate
|
|
55
|
+
/// @return True if the label format is valid
|
|
56
|
+
function isValidLabel(string calldata label) external pure returns (bool);
|
|
57
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.29;
|
|
3
|
+
|
|
4
|
+
// interfaces
|
|
5
|
+
import {IL2Registrar} from "./IL2Registrar.sol";
|
|
6
|
+
|
|
7
|
+
// libraries
|
|
8
|
+
import {L2RegistrarMod} from "./L2RegistrarMod.sol";
|
|
9
|
+
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
10
|
+
import {Validator} from "../../../utils/libraries/Validator.sol";
|
|
11
|
+
|
|
12
|
+
// contracts
|
|
13
|
+
import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
|
|
14
|
+
import {OwnableBase} from "@towns-protocol/diamond/src/facets/ownable/OwnableBase.sol";
|
|
15
|
+
import {ReentrancyGuardTransient} from "solady/utils/ReentrancyGuardTransient.sol";
|
|
16
|
+
|
|
17
|
+
/// @title L2RegistrarFacet
|
|
18
|
+
/// @notice Handles subdomain registration for Towns domains
|
|
19
|
+
/// @dev Registrar that validates callers are Towns smart accounts, charges fees, and creates subdomains
|
|
20
|
+
contract L2RegistrarFacet is IL2Registrar, OwnableBase, ReentrancyGuardTransient, Facet {
|
|
21
|
+
using CustomRevert for bytes4;
|
|
22
|
+
using L2RegistrarMod for L2RegistrarMod.Layout;
|
|
23
|
+
|
|
24
|
+
/// @notice Initializes the registrar with a registry contract and space factory
|
|
25
|
+
/// @param registry Address of the L2Registry diamond contract
|
|
26
|
+
/// @param spaceFactory Address of the SpaceFactory diamond (contains FeeManager facet)
|
|
27
|
+
/// @param currency Address of the ERC20 token used for fee payments
|
|
28
|
+
function __L2Registrar_init(
|
|
29
|
+
address registry,
|
|
30
|
+
address spaceFactory,
|
|
31
|
+
address currency
|
|
32
|
+
) external onlyInitializing {
|
|
33
|
+
_addInterface(type(IL2Registrar).interfaceId);
|
|
34
|
+
__L2Registrar_init_unchained(registry, spaceFactory, currency);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// @notice Internal initialization without interface registration
|
|
38
|
+
/// @dev Sets storage values for registry, factory, currency, and coinType
|
|
39
|
+
/// @param registry Address of the L2Registry diamond contract
|
|
40
|
+
/// @param spaceFactory Address of the SpaceFactory diamond (contains FeeManager facet)
|
|
41
|
+
/// @param currency Address of the ERC20 token used for fee payments
|
|
42
|
+
function __L2Registrar_init_unchained(
|
|
43
|
+
address registry,
|
|
44
|
+
address spaceFactory,
|
|
45
|
+
address currency
|
|
46
|
+
) internal {
|
|
47
|
+
Validator.checkAddress(registry);
|
|
48
|
+
Validator.checkAddress(spaceFactory);
|
|
49
|
+
Validator.checkAddress(currency);
|
|
50
|
+
L2RegistrarMod.Layout storage $ = L2RegistrarMod.getStorage();
|
|
51
|
+
$.registry = registry;
|
|
52
|
+
$.spaceFactory = spaceFactory;
|
|
53
|
+
$.currency = currency;
|
|
54
|
+
// ENSIP-11: Maps EVM chainId to ENS coinType by setting MSB (bit 31).
|
|
55
|
+
// This avoids collisions with SLIP-44 native coin types and enables
|
|
56
|
+
// deterministic L2 address resolution. Formula: coinType = 0x80000000 | chainId
|
|
57
|
+
$.coinType = 0x80000000 | block.chainid;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// @inheritdoc IL2Registrar
|
|
61
|
+
function register(string calldata label, address owner) external nonReentrant {
|
|
62
|
+
// 1. Verify caller is a Towns smart account
|
|
63
|
+
L2RegistrarMod.onlySmartAccount();
|
|
64
|
+
|
|
65
|
+
// 2. Validate label format
|
|
66
|
+
L2RegistrarMod.validateLabel(label);
|
|
67
|
+
|
|
68
|
+
L2RegistrarMod.Layout storage $ = L2RegistrarMod.getStorage();
|
|
69
|
+
|
|
70
|
+
// 3. Charge fee via FeeManager (hook handles first-free + tiers)
|
|
71
|
+
$.chargeFee(label);
|
|
72
|
+
|
|
73
|
+
// 4. Register domain (existing logic, minus duplicate label validation)
|
|
74
|
+
$.register(label, owner);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// @inheritdoc IL2Registrar
|
|
78
|
+
function setSpaceFactory(address spaceFactory) external onlyOwner {
|
|
79
|
+
Validator.checkAddress(spaceFactory);
|
|
80
|
+
L2RegistrarMod.getStorage().spaceFactory = spaceFactory;
|
|
81
|
+
emit L2RegistrarMod.SpaceFactorySet(spaceFactory);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// @inheritdoc IL2Registrar
|
|
85
|
+
function setRegistry(address registry) external onlyOwner {
|
|
86
|
+
Validator.checkAddress(registry);
|
|
87
|
+
L2RegistrarMod.getStorage().registry = registry;
|
|
88
|
+
emit L2RegistrarMod.RegistrySet(registry);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// @inheritdoc IL2Registrar
|
|
92
|
+
function setCurrency(address currency) external onlyOwner {
|
|
93
|
+
Validator.checkAddress(currency);
|
|
94
|
+
L2RegistrarMod.getStorage().currency = currency;
|
|
95
|
+
emit L2RegistrarMod.CurrencySet(currency);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// @inheritdoc IL2Registrar
|
|
99
|
+
function isAvailable(string calldata label) external view returns (bool) {
|
|
100
|
+
return L2RegistrarMod.getStorage().available(label);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// @inheritdoc IL2Registrar
|
|
104
|
+
function getRegistry() external view returns (address) {
|
|
105
|
+
return L2RegistrarMod.getStorage().registry;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// @inheritdoc IL2Registrar
|
|
109
|
+
function getCurrency() external view returns (address) {
|
|
110
|
+
return L2RegistrarMod.getStorage().currency;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// @inheritdoc IL2Registrar
|
|
114
|
+
function getCoinType() external view returns (uint256) {
|
|
115
|
+
return L2RegistrarMod.getStorage().coinType;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// @inheritdoc IL2Registrar
|
|
119
|
+
function getSpaceFactory() external view returns (address) {
|
|
120
|
+
return L2RegistrarMod.getStorage().spaceFactory;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// @inheritdoc IL2Registrar
|
|
124
|
+
function isValidLabel(string calldata label) external pure returns (bool) {
|
|
125
|
+
return L2RegistrarMod.isValidLabel(label);
|
|
126
|
+
}
|
|
127
|
+
}
|