@towns-protocol/contracts 0.0.455 → 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 (32) hide show
  1. package/package.json +5 -5
  2. package/scripts/deployments/diamonds/DeployL1Resolver.s.sol +155 -0
  3. package/scripts/deployments/diamonds/DeployL2Registrar.s.sol +173 -0
  4. package/scripts/deployments/diamonds/DeployL2Resolver.s.sol +196 -0
  5. package/scripts/deployments/facets/DeployAddrResolverFacet.s.sol +36 -0
  6. package/scripts/deployments/facets/DeployContentHashResolverFacet.s.sol +34 -0
  7. package/scripts/deployments/facets/DeployExtendedResolverFacet.s.sol +33 -0
  8. package/scripts/deployments/facets/DeployL1ResolverFacet.s.sol +61 -0
  9. package/scripts/deployments/facets/DeployL2RegistrarFacet.s.sol +56 -0
  10. package/scripts/deployments/facets/DeployL2RegistryFacet.s.sol +71 -0
  11. package/scripts/deployments/facets/DeployTextResolverFacet.s.sol +34 -0
  12. package/src/domains/facets/l1/IL1ResolverService.sol +20 -0
  13. package/src/domains/facets/l1/L1ResolverFacet.sol +100 -0
  14. package/src/domains/facets/l1/L1ResolverMod.sol +245 -0
  15. package/src/domains/facets/l2/AddrResolverFacet.sol +59 -0
  16. package/src/domains/facets/l2/ContentHashResolverFacet.sol +41 -0
  17. package/src/domains/facets/l2/ExtendedResolverFacet.sol +38 -0
  18. package/src/domains/facets/l2/IL2Registry.sol +79 -0
  19. package/src/domains/facets/l2/L2RegistryFacet.sol +203 -0
  20. package/src/domains/facets/l2/TextResolverFacet.sol +43 -0
  21. package/src/domains/facets/l2/modules/AddrResolverMod.sol +110 -0
  22. package/src/domains/facets/l2/modules/ContentHashResolverMod.sol +60 -0
  23. package/src/domains/facets/l2/modules/L2RegistryMod.sol +286 -0
  24. package/src/domains/facets/l2/modules/TextResolverMod.sol +62 -0
  25. package/src/domains/facets/l2/modules/VersionRecordMod.sol +42 -0
  26. package/src/domains/facets/registrar/IL2Registrar.sol +57 -0
  27. package/src/domains/facets/registrar/L2RegistrarFacet.sol +127 -0
  28. package/src/domains/facets/registrar/L2RegistrarMod.sol +224 -0
  29. package/src/domains/hooks/DomainFeeHook.sol +212 -0
  30. package/src/factory/facets/fee/FeeTypesLib.sol +3 -0
  31. package/src/spaces/facets/ProtocolFeeLib.sol +56 -1
  32. package/LICENSE.txt +0 -21
@@ -0,0 +1,224 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ // interfaces
5
+ import {IL2Registry} from "../l2/IL2Registry.sol";
6
+ import {IModularAccount} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol";
7
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
8
+ import {IFeeManager} from "../../../factory/facets/fee/IFeeManager.sol";
9
+
10
+ // libraries
11
+ import {LibString} from "solady/utils/LibString.sol";
12
+ import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
13
+ import {FeeTypesLib} from "../../../factory/facets/fee/FeeTypesLib.sol";
14
+ import {ProtocolFeeLib} from "../../../spaces/facets/ProtocolFeeLib.sol";
15
+
16
+ // contracts
17
+ import {AddrResolverFacet} from "../l2/AddrResolverFacet.sol";
18
+
19
+ library L2RegistrarMod {
20
+ using CustomRevert for bytes4;
21
+
22
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
23
+ /* LAYOUT */
24
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
25
+
26
+ struct Layout {
27
+ address registry;
28
+ uint256 coinType;
29
+ address spaceFactory;
30
+ address currency;
31
+ }
32
+
33
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
34
+ /* CONSTANTS */
35
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
36
+
37
+ /// @notice Minimum length required for a subdomain label (3 characters)
38
+ uint256 internal constant MIN_LABEL_LENGTH = 3;
39
+
40
+ /// @notice Maximum length allowed for a subdomain label (63 characters per DNS spec)
41
+ uint256 internal constant MAX_LABEL_LENGTH = 63;
42
+
43
+ /// @notice Allowed characters: lowercase a-z, digits 0-9, and hyphen
44
+ /// @dev Computed as: LOWERCASE_7_BIT_ASCII | DIGITS_7_BIT_ASCII | (1 << 45)
45
+ /// where 45 is the ASCII code for hyphen '-'
46
+ uint128 internal constant ALLOWED_LABEL_CHARS =
47
+ LibString.LOWERCASE_7_BIT_ASCII | LibString.DIGITS_7_BIT_ASCII | 0x200000000000;
48
+
49
+ /// @notice ASCII code for hyphen character
50
+ bytes1 internal constant HYPHEN = 0x2d;
51
+
52
+ /// keccak256(abi.encode(uint256(keccak256("towns.domains.registrar.storage")) - 1)) & ~bytes32(uint256(0xff))
53
+ bytes32 internal constant STORAGE_SLOT =
54
+ 0xd8d4140311c6e36bf92b2f6d963b373d4b20ef4c79fad106c706a4d652ed9b00;
55
+
56
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
57
+ /* EVENTS */
58
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
59
+
60
+ /// @notice Emitted when a new subdomain is registered
61
+ /// @param label The subdomain label
62
+ /// @param owner The owner of the subdomain
63
+ event NameRegistered(string indexed label, address indexed owner);
64
+
65
+ /// @notice Emitted when the space factory address is updated
66
+ /// @param spaceFactory The new space factory address
67
+ event SpaceFactorySet(address spaceFactory);
68
+
69
+ /// @notice Emitted when the registry address is updated
70
+ /// @param registry The new registry address
71
+ event RegistrySet(address registry);
72
+
73
+ /// @notice Emitted when the currency is updated
74
+ /// @param currency The new currency
75
+ event CurrencySet(address currency);
76
+
77
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
78
+ /* ERRORS */
79
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
80
+
81
+ /// @notice Thrown when a label is invalid (wrong length, chars, or format)
82
+ error L2Registrar__InvalidLabel();
83
+
84
+ /// @notice Thrown when caller is not a Towns smart account (IModularAccount)
85
+ error L2Registrar__NotSmartAccount();
86
+
87
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
88
+ /* FUNCTIONS */
89
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
90
+
91
+ /// @notice Registers a subdomain in the L2 registry
92
+ /// @dev Reverts if the subdomain is not available or the label is invalid
93
+ /// @param $ The storage layout
94
+ /// @param subdomain The subdomain label to register (e.g., "alice")
95
+ /// @param owner The address of the owner of the subdomain
96
+ function register(Layout storage $, string calldata subdomain, address owner) internal {
97
+ // Get registry interface to interact with domain management functions
98
+ IL2Registry registry = IL2Registry($.registry);
99
+
100
+ // Retrieve the base domain hash (e.g., namehash("towns.eth"))
101
+ bytes32 domainHash = registry.baseDomainHash();
102
+
103
+ // Compute the subdomain namehash (e.g., namehash("alice.towns.eth"))
104
+ bytes32 subdomainHash = registry.encodeSubdomain(domainHash, subdomain);
105
+
106
+ // Register the name in the L2 registry
107
+ registry.createSubdomain(domainHash, subdomain, owner, new bytes[](0), "");
108
+
109
+ // Encode owner address as bytes for resolver record storage
110
+ bytes memory addr = abi.encodePacked(owner);
111
+
112
+ // Get address resolver facet to set address records for the subdomain
113
+ AddrResolverFacet addrResolver = AddrResolverFacet($.registry);
114
+
115
+ // Set the forward address for the current chain. This is needed for reverse resolution.
116
+ // E.g. if this contract is deployed to Base, set an address for chainId 8453 which is
117
+ // coinType 2147492101 according to ENSIP-11.
118
+ addrResolver.setAddr(subdomainHash, $.coinType, addr);
119
+
120
+ // Set the forward address for mainnet ETH (coinType 60) for easier debugging.
121
+ addrResolver.setAddr(subdomainHash, 60, addr);
122
+
123
+ // Emit event to notify about the successful registration
124
+ emit NameRegistered(subdomain, owner);
125
+ }
126
+
127
+ function chargeFee(Layout storage $, string calldata label) internal {
128
+ if ($.spaceFactory == address(0)) return;
129
+ if ($.currency == address(0)) return;
130
+
131
+ bytes memory extraData = abi.encode(bytes(label).length);
132
+
133
+ uint256 expectedFee = IFeeManager($.spaceFactory).calculateFee(
134
+ FeeTypesLib.DOMAIN_REGISTRATION,
135
+ msg.sender,
136
+ 0,
137
+ extraData
138
+ );
139
+
140
+ ProtocolFeeLib.chargeAlways(
141
+ $.spaceFactory,
142
+ FeeTypesLib.DOMAIN_REGISTRATION,
143
+ msg.sender,
144
+ $.currency,
145
+ expectedFee,
146
+ expectedFee,
147
+ extraData
148
+ );
149
+ }
150
+
151
+ /// @notice Validates caller is a Towns smart account (IModularAccount)
152
+ /// @dev Uses ERC-165 supportsInterface check
153
+ function onlySmartAccount() internal view {
154
+ // Must be a contract
155
+ if (msg.sender.code.length == 0) {
156
+ L2Registrar__NotSmartAccount.selector.revertWith();
157
+ }
158
+
159
+ // Must support IModularAccount interface
160
+ try IERC165(msg.sender).supportsInterface(type(IModularAccount).interfaceId) returns (
161
+ bool supported
162
+ ) {
163
+ if (!supported) L2Registrar__NotSmartAccount.selector.revertWith();
164
+ } catch {
165
+ L2Registrar__NotSmartAccount.selector.revertWith();
166
+ }
167
+ }
168
+
169
+ /// @notice Checks if a subdomain is available for registration
170
+ /// @dev Returns true if the label is valid and the subdomain doesn't exist
171
+ /// @param $ The storage layout
172
+ /// @param subdomain The subdomain label to check (e.g., "alice")
173
+ /// @return True if the subdomain is available, false otherwise
174
+ function available(Layout storage $, string calldata subdomain) internal view returns (bool) {
175
+ // Check label validity first
176
+ if (!isValidLabel(subdomain)) return false;
177
+
178
+ // Check if subdomain already exists by querying its owner
179
+ // subdomainOwner returns address(0) if the subdomain doesn't exist
180
+ IL2Registry registry = IL2Registry($.registry);
181
+
182
+ // Compute the subdomain namehash
183
+ bytes32 subdomainHash = registry.encodeSubdomain(registry.baseDomainHash(), subdomain);
184
+
185
+ // Available if owner is zero address (subdomain doesn't exist)
186
+ return registry.subdomainOwner(subdomainHash) == address(0);
187
+ }
188
+
189
+ /// @notice Validates a subdomain label for registration
190
+ /// @dev Checks: length (3-63), allowed chars (a-z, 0-9, hyphen), no leading/trailing hyphen
191
+ /// @param label The subdomain label to validate (e.g., "alice")
192
+ function validateLabel(string calldata label) internal pure {
193
+ if (!isValidLabel(label)) L2Registrar__InvalidLabel.selector.revertWith();
194
+ }
195
+
196
+ /// @notice Checks if a label is valid without checking availability
197
+ /// @dev Use this for UI validation before attempting registration
198
+ /// @param label The subdomain label to validate
199
+ /// @return True if the label format is valid
200
+ function isValidLabel(string calldata label) internal pure returns (bool) {
201
+ uint256 len = bytes(label).length;
202
+
203
+ // Check length bounds
204
+ if (len < MIN_LABEL_LENGTH || len > MAX_LABEL_LENGTH) return false;
205
+
206
+ // Check all characters are allowed (a-z, 0-9, hyphen)
207
+ if (!LibString.is7BitASCII(label, ALLOWED_LABEL_CHARS)) return false;
208
+
209
+ // Check hyphen not at start or end
210
+ bytes calldata b = bytes(label);
211
+ if (b[0] == HYPHEN || b[len - 1] == HYPHEN) return false;
212
+
213
+ return true;
214
+ }
215
+
216
+ /// @notice Returns the storage layout for the L2Registrar facet
217
+ /// @dev Uses diamond storage pattern to avoid slot collisions between facets
218
+ /// @return $ The storage pointer to the facet's layout
219
+ function getStorage() internal pure returns (Layout storage $) {
220
+ assembly {
221
+ $.slot := STORAGE_SLOT
222
+ }
223
+ }
224
+ }
@@ -0,0 +1,212 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ // interfaces
5
+ import {IFeeHook, FeeHookResult} from "../../factory/facets/fee/IFeeHook.sol";
6
+
7
+ // libraries
8
+ import {CustomRevert} from "../../utils/libraries/CustomRevert.sol";
9
+ import {Validator} from "../../utils/libraries/Validator.sol";
10
+
11
+ // contracts
12
+ import {Ownable} from "solady/auth/Ownable.sol";
13
+
14
+ /// @title DomainFeeHook
15
+ /// @notice Fee hook for domain registration with tiered pricing and first-free logic
16
+ /// @dev Implements IFeeHook to integrate with FeeManager system
17
+ contract DomainFeeHook is IFeeHook, Ownable {
18
+ using CustomRevert for bytes4;
19
+
20
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
21
+ /* STORAGE */
22
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
23
+
24
+ struct Layout {
25
+ uint256 defaultPrice;
26
+ mapping(uint256 length => uint256 price) priceTiers;
27
+ mapping(address account => uint256 count) registrationCount;
28
+ address feeManager;
29
+ }
30
+
31
+ /// keccak256(abi.encode(uint256(keccak256("towns.domains.registrar.domain.feehook.storage")) - 1)) & ~bytes32(uint256(0xff))
32
+ bytes32 internal constant STORAGE_SLOT =
33
+ 0x54805b38b00d627862a1394d5554c4c16c8fd44fa1e31b86f37dcbff65fe4e00;
34
+
35
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
36
+ /* EVENTS */
37
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
38
+
39
+ /// @notice Emitted when the default price is updated
40
+ event DefaultPriceSet(uint256 price);
41
+
42
+ /// @notice Emitted when a price tier is set
43
+ event PriceTierSet(uint256 indexed length, uint256 price);
44
+
45
+ /// @notice Emitted when the fee manager is updated
46
+ event FeeManagerSet(address indexed feeManager);
47
+
48
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
49
+ /* ERRORS */
50
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
51
+
52
+ /// @notice Thrown when context data is invalid or missing
53
+ error DomainFeeHook__InvalidContext();
54
+
55
+ /// @notice Thrown when array lengths don't match in batch operations
56
+ error DomainFeeHook__LengthMismatch();
57
+
58
+ /// @notice Thrown when caller is not the authorized FeeManager
59
+ error DomainFeeHook__Unauthorized();
60
+
61
+ /// @notice Thrown when the number of lengths exceeds the maximum allowed
62
+ error DomainFeeHook__TooManyLengths();
63
+
64
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
65
+ /* CONSTRUCTOR */
66
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
67
+
68
+ /// @notice Initializes the fee hook with an owner and default price
69
+ /// @param owner The owner address
70
+ /// @param defaultPrice The default price for registrations
71
+ constructor(address owner, address feeManager, uint256 defaultPrice) {
72
+ _initializeOwner(owner);
73
+ Layout storage $ = getStorage();
74
+ Validator.checkAddress(feeManager);
75
+ ($.feeManager, $.defaultPrice) = (feeManager, defaultPrice);
76
+ emit FeeManagerSet(feeManager);
77
+ emit DefaultPriceSet(defaultPrice);
78
+ }
79
+
80
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
81
+ /* FEE HOOK */
82
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
83
+
84
+ /// @inheritdoc IFeeHook
85
+ function onChargeFee(
86
+ bytes32,
87
+ address user,
88
+ uint256,
89
+ bytes calldata context
90
+ ) external returns (FeeHookResult memory result) {
91
+ Layout storage $ = getStorage();
92
+
93
+ // Restrict to authorized FeeManager only
94
+ if (msg.sender != $.feeManager) DomainFeeHook__Unauthorized.selector.revertWith();
95
+
96
+ result = _calculateFee(user, context);
97
+ // Increment registration count (state change)
98
+ ++$.registrationCount[user];
99
+ }
100
+
101
+ /// @notice Sets the fee manager address
102
+ /// @param feeManager The fee manager address authorized to call onChargeFee
103
+ function setFeeManager(address feeManager) external onlyOwner {
104
+ Validator.checkAddress(feeManager);
105
+ getStorage().feeManager = feeManager;
106
+ emit FeeManagerSet(feeManager);
107
+ }
108
+
109
+ /// @notice Sets the default price for registrations
110
+ /// @param price The new default price
111
+ function setDefaultPrice(uint256 price) external onlyOwner {
112
+ getStorage().defaultPrice = price;
113
+ emit DefaultPriceSet(price);
114
+ }
115
+
116
+ /// @notice Sets the price for a specific label length
117
+ /// @param length The label length
118
+ /// @param price The price for that length
119
+ function setPriceTier(uint256 length, uint256 price) external onlyOwner {
120
+ getStorage().priceTiers[length] = price;
121
+ emit PriceTierSet(length, price);
122
+ }
123
+
124
+ /// @notice Sets multiple price tiers at once
125
+ /// @param lengths Array of label lengths
126
+ /// @param prices Array of prices corresponding to each length
127
+ function setPriceTiers(
128
+ uint256[] calldata lengths,
129
+ uint256[] calldata prices
130
+ ) external onlyOwner {
131
+ if (lengths.length != prices.length) DomainFeeHook__LengthMismatch.selector.revertWith();
132
+ if (lengths.length > 10) DomainFeeHook__TooManyLengths.selector.revertWith();
133
+
134
+ Layout storage $ = getStorage();
135
+ for (uint256 i; i < lengths.length; ++i) {
136
+ $.priceTiers[lengths[i]] = prices[i];
137
+ emit PriceTierSet(lengths[i], prices[i]);
138
+ }
139
+ }
140
+
141
+ /// @inheritdoc IFeeHook
142
+ function calculateFee(
143
+ bytes32,
144
+ address user,
145
+ uint256,
146
+ bytes calldata context
147
+ ) external view returns (FeeHookResult memory) {
148
+ return _calculateFee(user, context);
149
+ }
150
+
151
+ /// @notice Returns the price for a specific label length
152
+ /// @param length The label length to query
153
+ /// @return The price for that length (or default if not set)
154
+ function getPrice(uint256 length) external view returns (uint256) {
155
+ Layout storage $ = getStorage();
156
+ uint256 price = $.priceTiers[length];
157
+ return price == 0 ? $.defaultPrice : price;
158
+ }
159
+
160
+ /// @notice Returns the default price for registrations
161
+ /// @return The default price
162
+ function getDefaultPrice() external view returns (uint256) {
163
+ return getStorage().defaultPrice;
164
+ }
165
+
166
+ /// @notice Returns the registration count for a user
167
+ /// @param user The user to query
168
+ /// @return The registration count
169
+ function getRegistrationCount(address user) external view returns (uint256) {
170
+ return getStorage().registrationCount[user];
171
+ }
172
+
173
+ /// @notice Returns the authorized fee manager address
174
+ /// @return The fee manager address
175
+ function getFeeManager() external view returns (address) {
176
+ return getStorage().feeManager;
177
+ }
178
+
179
+ /// @notice Internal fee calculation logic
180
+ /// @param user The user being charged
181
+ /// @param context ABI-encoded label length
182
+ /// @return result The fee calculation result
183
+ function _calculateFee(
184
+ address user,
185
+ bytes calldata context
186
+ ) internal view returns (FeeHookResult memory) {
187
+ Layout storage $ = getStorage();
188
+ // First registration is free
189
+ if ($.registrationCount[user] == 0) {
190
+ return FeeHookResult({finalFee: 0, metadata: ""});
191
+ }
192
+
193
+ // Decode label length from context
194
+ if (context.length < 32) DomainFeeHook__InvalidContext.selector.revertWith();
195
+ uint256 labelLength = abi.decode(context, (uint256));
196
+
197
+ // Get tiered price (fall back to default)
198
+ uint256 price = $.priceTiers[labelLength];
199
+ if (price == 0) price = $.defaultPrice;
200
+
201
+ return FeeHookResult({finalFee: price, metadata: ""});
202
+ }
203
+
204
+ /// @notice Returns the storage layout for the L2Registrar facet
205
+ /// @dev Uses diamond storage pattern to avoid slot collisions between facets
206
+ /// @return $ The storage pointer to the facet's layout
207
+ function getStorage() internal pure returns (Layout storage $) {
208
+ assembly {
209
+ $.slot := STORAGE_SLOT
210
+ }
211
+ }
212
+ }
@@ -26,6 +26,9 @@ library FeeTypesLib {
26
26
  /// @notice Fee for bot actions
27
27
  bytes32 internal constant BOT_ACTION = keccak256("FEE_TYPE.BOT_ACTION");
28
28
 
29
+ /// @notice Fee for domain registration
30
+ bytes32 internal constant DOMAIN_REGISTRATION = keccak256("FEE_TYPE.DOMAIN_REGISTRATION");
31
+
29
32
  /// @notice Generates fee type for membership based on currency
30
33
  /// @param currency The payment currency address
31
34
  /// @return The fee type identifier for the given currency
@@ -23,6 +23,27 @@ library ProtocolFeeLib {
23
23
  address currency,
24
24
  uint256 amount,
25
25
  uint256 expectedFee
26
+ ) internal returns (uint256 protocolFee) {
27
+ return charge(spaceFactory, feeType, user, currency, amount, expectedFee, "");
28
+ }
29
+
30
+ /// @notice Charges protocol fee via FeeManager with ERC20 approval handling and extra data
31
+ /// @param spaceFactory The FeeManager address
32
+ /// @param feeType The type of fee being charged
33
+ /// @param user The user paying the fee
34
+ /// @param currency The payment currency (NATIVE_TOKEN or ERC20)
35
+ /// @param amount The base amount for fee calculation
36
+ /// @param expectedFee The pre-calculated expected fee
37
+ /// @param extraData Additional data for fee calculation (e.g., label length for domain registration)
38
+ /// @return protocolFee The actual fee charged
39
+ function charge(
40
+ address spaceFactory,
41
+ bytes32 feeType,
42
+ address user,
43
+ address currency,
44
+ uint256 amount,
45
+ uint256 expectedFee,
46
+ bytes memory extraData
26
47
  ) internal returns (uint256 protocolFee) {
27
48
  if (expectedFee == 0) return 0;
28
49
 
@@ -35,9 +56,43 @@ library ProtocolFeeLib {
35
56
  amount,
36
57
  currency,
37
58
  expectedFee,
38
- ""
59
+ extraData
39
60
  );
40
61
 
41
62
  if (!isNative) currency.safeApprove(spaceFactory, 0);
42
63
  }
64
+
65
+ /// @notice Charges protocol fee via FeeManager, always calling chargeFee even when expectedFee is 0
66
+ /// @dev Use this when the fee hook needs to track calls regardless of fee amount (e.g., domain registration count)
67
+ /// @param spaceFactory The FeeManager address
68
+ /// @param feeType The type of fee being charged
69
+ /// @param user The user paying the fee
70
+ /// @param currency The payment currency (NATIVE_TOKEN or ERC20)
71
+ /// @param amount The base amount for fee calculation
72
+ /// @param expectedFee The pre-calculated expected fee
73
+ /// @param extraData Additional data for fee calculation
74
+ /// @return protocolFee The actual fee charged
75
+ function chargeAlways(
76
+ address spaceFactory,
77
+ bytes32 feeType,
78
+ address user,
79
+ address currency,
80
+ uint256 amount,
81
+ uint256 expectedFee,
82
+ bytes memory extraData
83
+ ) internal returns (uint256 protocolFee) {
84
+ bool isNative = currency == CurrencyTransfer.NATIVE_TOKEN;
85
+ if (!isNative && expectedFee > 0) currency.safeApproveWithRetry(spaceFactory, expectedFee);
86
+
87
+ protocolFee = IFeeManager(spaceFactory).chargeFee{value: isNative ? expectedFee : 0}(
88
+ feeType,
89
+ user,
90
+ amount,
91
+ currency,
92
+ expectedFee,
93
+ extraData
94
+ );
95
+
96
+ if (!isNative && expectedFee > 0) currency.safeApprove(spaceFactory, 0);
97
+ }
43
98
  }
package/LICENSE.txt DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 River Association
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.