@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.
Files changed (83) hide show
  1. package/package.json +7 -7
  2. package/scripts/common/Interaction.s.sol +3 -3
  3. package/scripts/deployments/diamonds/DeployBaseRegistry.s.sol +1 -4
  4. package/scripts/deployments/diamonds/DeployL1Resolver.s.sol +155 -0
  5. package/scripts/deployments/diamonds/DeployL2Registrar.s.sol +173 -0
  6. package/scripts/deployments/diamonds/DeployL2Resolver.s.sol +196 -0
  7. package/scripts/deployments/diamonds/DeploySpace.s.sol +1 -9
  8. package/scripts/deployments/diamonds/DeploySpaceFactory.s.sol +1 -5
  9. package/scripts/deployments/facets/DeployAddrResolverFacet.s.sol +36 -0
  10. package/scripts/deployments/facets/DeployAppFactoryFacet.s.sol +3 -3
  11. package/scripts/deployments/facets/DeployAppInstallerFacet.s.sol +3 -3
  12. package/scripts/deployments/facets/DeployAppRegistryFacet.s.sol +3 -3
  13. package/scripts/deployments/facets/DeployArchitect.s.sol +3 -3
  14. package/scripts/deployments/facets/DeployAttestationRegistry.s.sol +3 -3
  15. package/scripts/deployments/facets/DeployContentHashResolverFacet.s.sol +34 -0
  16. package/scripts/deployments/facets/DeployERC721ANonTransferable.s.sol +3 -3
  17. package/scripts/deployments/facets/DeployExtendedResolverFacet.s.sol +33 -0
  18. package/scripts/deployments/facets/DeployFeatureManager.s.sol +3 -3
  19. package/scripts/deployments/facets/DeployL1ResolverFacet.s.sol +61 -0
  20. package/scripts/deployments/facets/DeployL2RegistrarFacet.s.sol +56 -0
  21. package/scripts/deployments/facets/DeployL2RegistryFacet.s.sol +71 -0
  22. package/scripts/deployments/facets/DeployMainnetDelegation.s.sol +3 -3
  23. package/scripts/deployments/facets/DeployMembershipMetadata.s.sol +3 -3
  24. package/scripts/deployments/facets/DeployMerkleAirdrop.s.sol +3 -3
  25. package/scripts/deployments/facets/DeployMetadata.s.sol +3 -3
  26. package/scripts/deployments/facets/DeployMockLegacyArchitect.s.sol +3 -3
  27. package/scripts/deployments/facets/DeployNodeOperator.s.sol +3 -3
  28. package/scripts/deployments/facets/DeployPartnerRegistry.s.sol +3 -3
  29. package/scripts/deployments/facets/DeployPricingModules.s.sol +3 -3
  30. package/scripts/deployments/facets/DeploySchemaRegistry.s.sol +3 -3
  31. package/scripts/deployments/facets/DeploySpaceFactoryInit.s.sol +3 -3
  32. package/scripts/deployments/facets/DeploySpaceOwnerFacet.s.sol +3 -3
  33. package/scripts/deployments/facets/DeployTextResolverFacet.s.sol +34 -0
  34. package/scripts/deployments/facets/DeployTokenMigration.s.sol +3 -3
  35. package/scripts/deployments/facets/DeployXChain.s.sol +3 -3
  36. package/scripts/deployments/utils/DeployAccountFactory.s.sol +3 -3
  37. package/scripts/deployments/utils/DeployEntitlementGatedExample.s.sol +3 -3
  38. package/scripts/deployments/utils/DeployEntrypoint.s.sol +3 -3
  39. package/scripts/deployments/utils/DeployMember.s.sol +3 -3
  40. package/scripts/deployments/utils/DeployMockLegacyMembership.s.sol +3 -3
  41. package/scripts/deployments/utils/DeployMockMessenger.s.sol +3 -3
  42. package/scripts/deployments/utils/DeploySpaceProxyInitializer.s.sol +3 -3
  43. package/scripts/deployments/utils/DeployTieredLogPricingV2.s.sol +3 -3
  44. package/scripts/deployments/utils/DeployTieredLogPricingV3.s.sol +3 -3
  45. package/scripts/deployments/utils/DeployTownsBase.s.sol +3 -3
  46. package/scripts/deployments/utils/DeployTownsMainnet.s.sol +3 -3
  47. package/scripts/deployments/utils/pricing/TieredLogPricing.s.sol +3 -3
  48. package/scripts/interactions/InteractAirdrop.s.sol +3 -3
  49. package/scripts/interactions/InteractBaseBridge.s.sol +3 -3
  50. package/scripts/interactions/InteractRegisterApp.s.sol +2 -2
  51. package/scripts/interactions/InteractRiverRegistrySetTrimByStreamId.s.sol +44 -0
  52. package/scripts/interactions/helpers/RiverConfigValues.sol +1 -2
  53. package/src/account/facets/app/AppManagerFacet.sol +43 -14
  54. package/src/account/facets/app/AppManagerMod.sol +311 -278
  55. package/src/account/facets/hub/AccountHubFacet.sol +12 -11
  56. package/src/account/facets/hub/AccountHubMod.sol +117 -116
  57. package/src/account/facets/tipping/AccountTippingFacet.sol +10 -9
  58. package/src/account/facets/tipping/AccountTippingMod.sol +133 -124
  59. package/src/apps/modules/subscription/SubscriptionModuleStorage.sol +1 -1
  60. package/src/domains/facets/l1/IL1ResolverService.sol +20 -0
  61. package/src/domains/facets/l1/L1ResolverFacet.sol +100 -0
  62. package/src/domains/facets/l1/L1ResolverMod.sol +245 -0
  63. package/src/domains/facets/l2/AddrResolverFacet.sol +59 -0
  64. package/src/domains/facets/l2/ContentHashResolverFacet.sol +41 -0
  65. package/src/domains/facets/l2/ExtendedResolverFacet.sol +38 -0
  66. package/src/domains/facets/l2/IL2Registry.sol +79 -0
  67. package/src/domains/facets/l2/L2RegistryFacet.sol +203 -0
  68. package/src/domains/facets/l2/TextResolverFacet.sol +43 -0
  69. package/src/domains/facets/l2/modules/AddrResolverMod.sol +110 -0
  70. package/src/domains/facets/l2/modules/ContentHashResolverMod.sol +60 -0
  71. package/src/domains/facets/l2/modules/L2RegistryMod.sol +286 -0
  72. package/src/domains/facets/l2/modules/TextResolverMod.sol +62 -0
  73. package/src/domains/facets/l2/modules/VersionRecordMod.sol +42 -0
  74. package/src/domains/facets/registrar/IL2Registrar.sol +57 -0
  75. package/src/domains/facets/registrar/L2RegistrarFacet.sol +127 -0
  76. package/src/domains/facets/registrar/L2RegistrarMod.sol +224 -0
  77. package/src/domains/hooks/DomainFeeHook.sol +212 -0
  78. package/src/factory/facets/fee/FeeTypesLib.sol +3 -0
  79. package/src/river/registry/facets/stream/StreamRegistry.sol +4 -1
  80. package/src/spaces/facets/ProtocolFeeLib.sol +56 -1
  81. package/src/tokens/Member.sol +3 -4
  82. package/LICENSE.txt +0 -21
  83. 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
+ }