@towns-protocol/contracts 0.0.448 → 0.0.450

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towns-protocol/contracts",
3
- "version": "0.0.448",
3
+ "version": "0.0.450",
4
4
  "scripts": {
5
5
  "clean": "forge clean",
6
6
  "compile": "forge build",
@@ -33,7 +33,7 @@
33
33
  "@layerzerolabs/oapp-evm": "^0.3.2",
34
34
  "@openzeppelin/merkle-tree": "^1.0.8",
35
35
  "@prb/test": "^0.6.4",
36
- "@towns-protocol/prettier-config": "^0.0.448",
36
+ "@towns-protocol/prettier-config": "^0.0.450",
37
37
  "@wagmi/cli": "^2.2.0",
38
38
  "forge-std": "github:foundry-rs/forge-std#v1.10.0",
39
39
  "prettier": "^3.5.3",
@@ -50,5 +50,5 @@
50
50
  "publishConfig": {
51
51
  "access": "public"
52
52
  },
53
- "gitHead": "165feaa79f39a38ce8d928e8fc5ffc09c3d42733"
53
+ "gitHead": "cbbeb47f3b927004f889268ab727858eab3f1cf9"
54
54
  }
@@ -15,14 +15,17 @@ library DeployEntitlementChecker {
15
15
  using DynamicArrayLib for DynamicArrayLib.DynamicArray;
16
16
 
17
17
  function selectors() internal pure returns (bytes4[] memory res) {
18
- DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(9);
18
+ DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(10);
19
19
  arr.p(EntitlementChecker.registerNode.selector);
20
20
  arr.p(EntitlementChecker.unregisterNode.selector);
21
21
  arr.p(EntitlementChecker.isValidNode.selector);
22
22
  arr.p(EntitlementChecker.getNodeCount.selector);
23
23
  arr.p(EntitlementChecker.getNodeAtIndex.selector);
24
24
  arr.p(EntitlementChecker.getRandomNodes.selector);
25
- arr.p(EntitlementChecker.requestEntitlementCheck.selector);
25
+ // requestEntitlementCheck V1 (legacy)
26
+ arr.p(bytes4(keccak256("requestEntitlementCheck(address,bytes32,uint256,address[])")));
27
+ // requestEntitlementCheck unified (enum dispatch)
28
+ arr.p(bytes4(keccak256("requestEntitlementCheck(uint8,bytes)")));
26
29
  arr.p(EntitlementChecker.requestEntitlementCheckV2.selector);
27
30
  arr.p(EntitlementChecker.getNodesByOperator.selector);
28
31
 
@@ -2,16 +2,17 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
+ import {IEntitlementGatedBase} from "../../../../spaces/facets/gated/IEntitlementGated.sol";
5
6
  import {IEntitlementChecker} from "./IEntitlementChecker.sol";
6
- import {IEntitlementGatedBase} from "src/spaces/facets/gated/IEntitlementGated.sol";
7
7
 
8
8
  // libraries
9
-
10
- import {EntitlementCheckerStorage} from "./EntitlementCheckerStorage.sol";
11
9
  import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
12
- import {NodeOperatorStatus, NodeOperatorStorage} from "src/base/registry/facets/operator/NodeOperatorStorage.sol";
13
- import {XChainLib} from "src/base/registry/facets/xchain/XChainLib.sol";
14
- import {CustomRevert} from "src/utils/libraries/CustomRevert.sol";
10
+ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
11
+ import {CustomRevert} from "../../../../utils/libraries/CustomRevert.sol";
12
+ import {CurrencyTransfer} from "../../../../utils/libraries/CurrencyTransfer.sol";
13
+ import {NodeOperatorStatus, NodeOperatorStorage} from "../operator/NodeOperatorStorage.sol";
14
+ import {XChainLib} from "../xchain/XChainLib.sol";
15
+ import {EntitlementCheckerStorage} from "./EntitlementCheckerStorage.sol";
15
16
 
16
17
  // contracts
17
18
  import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
@@ -21,6 +22,7 @@ contract EntitlementChecker is IEntitlementChecker, Facet {
21
22
  using EnumerableSet for EnumerableSet.UintSet;
22
23
  using EnumerableSet for EnumerableSet.Bytes32Set;
23
24
  using CustomRevert for bytes4;
25
+ using SafeTransferLib for address;
24
26
 
25
27
  // =============================================================
26
28
  // Initializer
@@ -129,8 +131,79 @@ contract EntitlementChecker is IEntitlementChecker, Facet {
129
131
  uint256 requestId,
130
132
  bytes memory extraData
131
133
  ) external payable {
132
- address space = msg.sender;
133
134
  address senderAddress = abi.decode(extraData, (address));
135
+ _requestEntitlementCheck(
136
+ walletAddress,
137
+ transactionId,
138
+ requestId,
139
+ CurrencyTransfer.NATIVE_TOKEN,
140
+ msg.value,
141
+ senderAddress
142
+ );
143
+ }
144
+
145
+ /// @inheritdoc IEntitlementChecker
146
+ function requestEntitlementCheck(CheckType checkType, bytes calldata data) external payable {
147
+ if (checkType == CheckType.V1) {
148
+ if (msg.value != 0) EntitlementChecker_InvalidValue.selector.revertWith();
149
+ (
150
+ address walletAddress,
151
+ bytes32 transactionId,
152
+ uint256 roleId,
153
+ address[] memory nodes
154
+ ) = abi.decode(data, (address, bytes32, uint256, address[]));
155
+ emit EntitlementCheckRequested(walletAddress, msg.sender, transactionId, roleId, nodes);
156
+ } else if (checkType == CheckType.V2) {
157
+ (
158
+ address walletAddress,
159
+ bytes32 transactionId,
160
+ uint256 requestId,
161
+ bytes memory extraData
162
+ ) = abi.decode(data, (address, bytes32, uint256, bytes));
163
+ address senderAddress = abi.decode(extraData, (address));
164
+ _requestEntitlementCheck(
165
+ walletAddress,
166
+ transactionId,
167
+ requestId,
168
+ CurrencyTransfer.NATIVE_TOKEN,
169
+ msg.value,
170
+ senderAddress
171
+ );
172
+ } else if (checkType == CheckType.V3) {
173
+ (
174
+ address walletAddress,
175
+ bytes32 transactionId,
176
+ uint256 requestId,
177
+ address currency,
178
+ uint256 amount,
179
+ address senderAddress
180
+ ) = abi.decode(data, (address, bytes32, uint256, address, uint256, address));
181
+ _requestEntitlementCheck(
182
+ walletAddress,
183
+ transactionId,
184
+ requestId,
185
+ currency,
186
+ amount,
187
+ senderAddress
188
+ );
189
+ } else {
190
+ EntitlementChecker_InvalidCheckType.selector.revertWith();
191
+ }
192
+ }
193
+
194
+ // =============================================================
195
+ // Internal
196
+ // =============================================================
197
+
198
+ function _requestEntitlementCheck(
199
+ address walletAddress,
200
+ bytes32 transactionId,
201
+ uint256 requestId,
202
+ address currency,
203
+ uint256 amount,
204
+ address senderAddress
205
+ ) internal {
206
+ address space = msg.sender;
134
207
 
135
208
  XChainLib.Layout storage layout = XChainLib.layout();
136
209
 
@@ -139,25 +212,34 @@ contract EntitlementChecker is IEntitlementChecker, Facet {
139
212
  // Only create the request if it doesn't exist yet
140
213
  XChainLib.Request storage request = layout.requests[transactionId];
141
214
  if (request.caller == address(0)) {
142
- // First time creating this request
143
- layout.requests[transactionId] = XChainLib.Request({
144
- caller: space,
145
- blockNumber: block.number,
146
- value: msg.value,
147
- completed: false,
148
- receiver: walletAddress
149
- });
150
- } else {
151
- if (msg.value != 0) {
152
- EntitlementChecker_InvalidValue.selector.revertWith();
215
+ request.caller = space;
216
+ request.blockNumber = block.number;
217
+ request.value = amount;
218
+ request.receiver = walletAddress;
219
+ request.currency = currency;
220
+
221
+ if (currency == CurrencyTransfer.NATIVE_TOKEN) {
222
+ if (amount != msg.value) EntitlementChecker_InvalidValue.selector.revertWith();
223
+ } else {
224
+ // ERC20: reject any ETH sent
225
+ if (msg.value != 0) EntitlementChecker_InvalidValue.selector.revertWith();
226
+ // ERC20: pull tokens from Space
227
+ if (amount != 0) currency.safeTransferFrom(space, address(this), amount);
153
228
  }
229
+ } else {
230
+ // Request already exists from a previous requestId on this transactionId.
231
+ // Escrow was established on the first request - reject any additional ETH
232
+ // to prevent funds being sent but not tracked.
233
+ if (msg.value != 0) EntitlementChecker_InvalidValue.selector.revertWith();
154
234
  }
155
235
 
156
236
  address[] memory randomNodes = _getRandomNodes(5);
157
237
 
158
- XChainLib.Check storage check = XChainLib.layout().checks[transactionId];
238
+ XChainLib.Check storage check = layout.checks[transactionId];
159
239
 
160
- check.requestIds.add(requestId);
240
+ if (!check.requestIds.add(requestId)) {
241
+ EntitlementChecker_DuplicateRequestId.selector.revertWith();
242
+ }
161
243
 
162
244
  for (uint256 i; i < randomNodes.length; ++i) {
163
245
  check.nodes[requestId].add(randomNodes[i]);
@@ -2,6 +2,24 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  interface IEntitlementCheckerBase {
5
+ /// @notice CheckType enum for unified entitlement check dispatch
6
+ /// @dev To encode data for each type:
7
+ /// switch (checkType) {
8
+ /// case CheckType.V1:
9
+ /// data = abi.encode(walletAddress, transactionId, roleId, nodes);
10
+ /// case CheckType.V2:
11
+ /// data = abi.encode(walletAddress, transactionId, requestId, extraData);
12
+ /// // where extraData = abi.encode(senderAddress)
13
+ /// case CheckType.V3:
14
+ /// data = abi.encode(walletAddress, transactionId, requestId, currency, amount, senderAddress);
15
+ /// }
16
+ enum CheckType {
17
+ V1, // Legacy check with explicit nodes
18
+ V2, // ETH escrow (uses msg.value, currency is always NATIVE_TOKEN)
19
+ V3 // ETH or ERC20 escrow (uses currency + amount)
20
+ }
21
+
22
+ error EntitlementChecker_InvalidCheckType();
5
23
  error EntitlementChecker_NodeAlreadyRegistered();
6
24
  error EntitlementChecker_NodeNotRegistered();
7
25
  error EntitlementChecker_InsufficientNumberOfNodes();
@@ -12,6 +30,7 @@ interface IEntitlementCheckerBase {
12
30
  error EntitlementChecker_InsufficientFunds();
13
31
  error EntitlementChecker_NoRefundsAvailable();
14
32
  error EntitlementChecker_InvalidValue();
33
+ error EntitlementChecker_DuplicateRequestId();
15
34
 
16
35
  // Events
17
36
  event NodeRegistered(address indexed nodeAddress);
@@ -88,6 +107,11 @@ interface IEntitlementChecker is IEntitlementCheckerBase {
88
107
  bytes memory extraData
89
108
  ) external payable;
90
109
 
110
+ /// @notice Unified entitlement check request with enum-based dispatch
111
+ /// @param checkType The type of check to perform (V1, V2, or V3)
112
+ /// @param data Encoded parameters specific to the check type (see CheckType enum docs)
113
+ function requestEntitlementCheck(CheckType checkType, bytes calldata data) external payable;
114
+
91
115
  /// @notice Get all nodes registered to a specific operator
92
116
  /// @param operator The address of the operator
93
117
  /// @return address[] Array of node addresses registered to the operator
@@ -2,20 +2,21 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
-
6
5
  import {IEntitlementCheckerBase} from "src/base/registry/facets/checker/IEntitlementChecker.sol";
7
6
  import {IEntitlementGatedBase} from "src/spaces/facets/gated/IEntitlementGated.sol";
8
7
 
9
- // libraries
10
-
11
- // contracts
12
-
13
8
  /// @dev Struct to hold voting context and avoid stack too deep
9
+ /// @param transactionId The unique identifier of the transaction
10
+ /// @param caller The space contract that initiated the request
11
+ /// @param value Amount escrowed (ETH or ERC20)
12
+ /// @param completed Whether the transaction has been finalized
13
+ /// @param currency Token address (NATIVE_TOKEN for ETH)
14
14
  struct VotingContext {
15
15
  bytes32 transactionId;
16
16
  address caller;
17
17
  uint256 value;
18
18
  bool completed;
19
+ address currency;
19
20
  }
20
21
 
21
22
  /// @dev Struct to hold vote counting results
@@ -2,21 +2,20 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
+ import {IEntitlementGated} from "../../../../spaces/facets/gated/IEntitlementGated.sol";
5
6
  import {IXChain, VotingContext, VoteResults} from "./IXChain.sol";
6
- import {IEntitlementGated} from "src/spaces/facets/gated/IEntitlementGated.sol";
7
7
 
8
8
  // libraries
9
- import {XChainLib} from "./XChainLib.sol";
10
9
  import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
11
- import {CurrencyTransfer} from "src/utils/libraries/CurrencyTransfer.sol";
12
- import {CustomRevert} from "src/utils/libraries/CustomRevert.sol";
10
+ import {CurrencyTransfer} from "../../../../utils/libraries/CurrencyTransfer.sol";
11
+ import {CustomRevert} from "../../../../utils/libraries/CustomRevert.sol";
13
12
  import {XChainCheckLib} from "./XChainCheckLib.sol";
13
+ import {XChainLib} from "./XChainLib.sol";
14
14
 
15
15
  // contracts
16
16
  import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
17
- import {EntitlementGated} from "src/spaces/facets/gated/EntitlementGated.sol";
18
- import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
19
17
  import {OwnableBase} from "@towns-protocol/diamond/src/facets/ownable/OwnableBase.sol";
18
+ import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
20
19
 
21
20
  contract XChain is IXChain, ReentrancyGuard, OwnableBase, Facet {
22
21
  using EnumerableSet for EnumerableSet.AddressSet;
@@ -39,11 +38,13 @@ contract XChain is IXChain, ReentrancyGuard, OwnableBase, Facet {
39
38
 
40
39
  /// @inheritdoc IXChain
41
40
  function provideXChainRefund(address senderAddress, bytes32 transactionId) external onlyOwner {
42
- if (!XChainLib.layout().requestsBySender[senderAddress].remove(transactionId)) {
41
+ XChainLib.Layout storage layout = XChainLib.layout();
42
+
43
+ if (!layout.requestsBySender[senderAddress].remove(transactionId)) {
43
44
  EntitlementGated_TransactionCheckAlreadyCompleted.selector.revertWith();
44
45
  }
45
46
 
46
- XChainLib.Request storage request = XChainLib.layout().requests[transactionId];
47
+ XChainLib.Request storage request = layout.requests[transactionId];
47
48
 
48
49
  if (request.completed) {
49
50
  EntitlementGated_TransactionCheckAlreadyCompleted.selector.revertWith();
@@ -55,23 +56,19 @@ contract XChain is IXChain, ReentrancyGuard, OwnableBase, Facet {
55
56
 
56
57
  request.completed = true;
57
58
 
58
- XChainLib.Check storage check = XChainLib.layout().checks[transactionId];
59
+ XChainLib.Check storage check = layout.checks[transactionId];
59
60
 
60
61
  // clean up checks if any
61
- uint256 requestIdsLength = check.requestIds.length();
62
- if (requestIdsLength > 0) {
63
- for (uint256 i; i < requestIdsLength; ++i) {
64
- uint256 requestId = check.requestIds.at(i);
65
- check.voteCompleted[requestId] = true;
66
- }
62
+ uint256[] memory requestIds = check.requestIds.values();
63
+ for (uint256 i; i < requestIds.length; ++i) {
64
+ check.voteCompleted[requestIds[i]] = true;
67
65
  }
68
66
 
69
- CurrencyTransfer.transferCurrency(
70
- CurrencyTransfer.NATIVE_TOKEN,
71
- address(this),
72
- senderAddress,
73
- request.value
74
- );
67
+ // normalize address(0) to NATIVE_TOKEN for pre-upgrade requests
68
+ address currency = request.currency;
69
+ currency = currency == address(0) ? CurrencyTransfer.NATIVE_TOKEN : currency;
70
+ // refund escrowed amount
71
+ CurrencyTransfer.transferCurrency(currency, address(this), senderAddress, request.value);
75
72
  }
76
73
 
77
74
  /// @inheritdoc IXChain
@@ -80,8 +77,9 @@ contract XChain is IXChain, ReentrancyGuard, OwnableBase, Facet {
80
77
  uint256 requestId,
81
78
  NodeVoteStatus result
82
79
  ) external nonReentrant {
83
- XChainLib.Request storage request = XChainLib.layout().requests[transactionId];
84
- XChainLib.Check storage check = XChainLib.layout().checks[transactionId];
80
+ XChainLib.Layout storage layout = XChainLib.layout();
81
+ XChainLib.Request storage request = layout.requests[transactionId];
82
+ XChainLib.Check storage check = layout.checks[transactionId];
85
83
 
86
84
  VotingContext memory context = check.validateVotingEligibility(
87
85
  request,
@@ -129,11 +127,18 @@ contract XChain is IXChain, ReentrancyGuard, OwnableBase, Facet {
129
127
  NodeVoteStatus finalStatus
130
128
  ) internal {
131
129
  // Mark transaction as completed and clean up
132
- XChainLib.layout().requests[context.transactionId].completed = true;
133
- XChainLib.layout().requestsBySender[context.caller].remove(context.transactionId);
130
+ XChainLib.Layout storage layout = XChainLib.layout();
131
+ layout.requests[context.transactionId].completed = true;
132
+ layout.requestsBySender[context.caller].remove(context.transactionId);
134
133
 
135
- // Call back to the original caller with the result
136
- EntitlementGated(context.caller).postEntitlementCheckResultV2{value: context.value}(
134
+ // return escrowed funds to Space, then callback
135
+ CurrencyTransfer.transferCurrency(
136
+ context.currency,
137
+ address(this),
138
+ context.caller,
139
+ context.value
140
+ );
141
+ IEntitlementGated(context.caller).postEntitlementCheckResultV2(
137
142
  context.transactionId,
138
143
  0,
139
144
  finalStatus
@@ -2,15 +2,14 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
+ import {IEntitlementGatedBase} from "../../../../spaces/facets/gated/IEntitlementGated.sol";
6
+ import {VoteResults, VotingContext} from "./IXChain.sol";
5
7
 
6
8
  // libraries
7
-
8
- // contracts
9
- import {XChainLib} from "./XChainLib.sol";
10
- import {IEntitlementGatedBase} from "src/spaces/facets/gated/IEntitlementGated.sol";
11
- import {CustomRevert} from "src/utils/libraries/CustomRevert.sol";
12
9
  import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
13
- import {VoteResults, VotingContext} from "./IXChain.sol";
10
+ import {CurrencyTransfer} from "../../../../utils/libraries/CurrencyTransfer.sol";
11
+ import {CustomRevert} from "../../../../utils/libraries/CustomRevert.sol";
12
+ import {XChainLib} from "./XChainLib.sol";
14
13
 
15
14
  library XChainCheckLib {
16
15
  using CustomRevert for bytes4;
@@ -50,6 +49,9 @@ library XChainCheckLib {
50
49
  context.caller = request.caller;
51
50
  context.value = request.value;
52
51
  context.completed = request.completed;
52
+ // normalize address(0) to NATIVE_TOKEN for pre-upgrade requests
53
+ address currency = request.currency;
54
+ context.currency = currency == address(0) ? CurrencyTransfer.NATIVE_TOKEN : currency;
53
55
  }
54
56
 
55
57
  function processNodeVote(
@@ -2,13 +2,12 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
- import {IEntitlementChecker} from "src/base/registry/facets/checker/IEntitlementChecker.sol";
6
- import {IEntitlementGatedBase} from "src/spaces/facets/gated/IEntitlementGated.sol";
5
+ import {IEntitlementGatedBase} from "../../../../spaces/facets/gated/IEntitlementGated.sol";
6
+ import {IEntitlementChecker} from "../checker/IEntitlementChecker.sol";
7
+
7
8
  // libraries
8
9
  import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
9
10
 
10
- // contracts
11
-
12
11
  library XChainLib {
13
12
  // keccak256(abi.encode(uint256(keccak256("xchain.entitlement.transactions.storage")) - 1)) &
14
13
  // ~bytes32(uint256(0xff))
@@ -22,12 +21,20 @@ library XChainLib {
22
21
  mapping(uint256 requestId => bool voteCompleted) voteCompleted;
23
22
  }
24
23
 
24
+ /// @dev Stores crosschain entitlement check request data
25
+ /// @param value Amount escrowed (ETH or ERC20)
26
+ /// @param blockNumber Block when request was created
27
+ /// @param caller Space contract that initiated the request
28
+ /// @param completed Whether the request has been finalized
29
+ /// @param receiver Wallet address being checked for entitlement
30
+ /// @param currency Token address (NATIVE_TOKEN for ETH)
25
31
  struct Request {
26
32
  uint256 value;
27
33
  uint256 blockNumber;
28
34
  address caller;
29
35
  bool completed;
30
36
  address receiver;
37
+ address currency;
31
38
  }
32
39
 
33
40
  struct Layout {
@@ -2,16 +2,14 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
+ import {IEntitlementChecker} from "../../../base/registry/facets/checker/IEntitlementChecker.sol";
6
+ import {IRuleEntitlement} from "../../entitlements/rule/IRuleEntitlement.sol";
5
7
  import {IEntitlementGated} from "./IEntitlementGated.sol";
6
- import {IEntitlementChecker} from "src/base/registry/facets/checker/IEntitlementChecker.sol";
7
- import {IRuleEntitlement} from "src/spaces/entitlements/rule/IRuleEntitlement.sol";
8
-
9
- // libraries
10
8
 
11
9
  // contracts
12
- import {EntitlementGatedBase} from "./EntitlementGatedBase.sol";
13
10
  import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
14
11
  import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
12
+ import {EntitlementGatedBase} from "./EntitlementGatedBase.sol";
15
13
 
16
14
  abstract contract EntitlementGated is
17
15
  IEntitlementGated,
@@ -30,8 +28,7 @@ abstract contract EntitlementGated is
30
28
  _setEntitlementChecker(entitlementChecker);
31
29
  }
32
30
 
33
- /// @notice Called by the xchain node to post the result of the entitlement check
34
- /// @dev the internal function validates the transactionId and the result
31
+ /// @inheritdoc IEntitlementGated
35
32
  function postEntitlementCheckResult(
36
33
  bytes32 transactionId,
37
34
  uint256 roleId,
@@ -40,11 +37,7 @@ abstract contract EntitlementGated is
40
37
  _postEntitlementCheckResult(transactionId, roleId, result);
41
38
  }
42
39
 
43
- /// @notice Post the result of the entitlement check for a specific role
44
- /// @dev Only the entitlement checker can call this function
45
- /// @param transactionId The unique identifier for the transaction
46
- /// @param roleId The role ID for the entitlement check
47
- /// @param result The result of the entitlement check (PASSED or FAILED)
40
+ /// @inheritdoc IEntitlementGated
48
41
  function postEntitlementCheckResultV2(
49
42
  bytes32 transactionId,
50
43
  uint256 roleId,
@@ -53,7 +46,7 @@ abstract contract EntitlementGated is
53
46
  _postEntitlementCheckResultV2(transactionId, roleId, result);
54
47
  }
55
48
 
56
- /// @dev deprecated Use EntitlementDataQueryable.getCrossChainEntitlementData instead
49
+ /// @inheritdoc IEntitlementGated
57
50
  function getRuleData(
58
51
  bytes32 transactionId,
59
52
  uint256 roleId
@@ -2,23 +2,25 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
+ import {IEntitlementChecker, IEntitlementCheckerBase} from "../../../base/registry/facets/checker/IEntitlementChecker.sol";
6
+ import {IImplementationRegistry} from "../../../factory/facets/registry/IImplementationRegistry.sol";
7
+ import {IRuleEntitlement} from "../../entitlements/rule/IRuleEntitlement.sol";
5
8
  import {IEntitlementGatedBase} from "./IEntitlementGated.sol";
6
9
 
7
- import {IEntitlementChecker} from "src/base/registry/facets/checker/IEntitlementChecker.sol";
8
- import {IImplementationRegistry} from "src/factory/facets/registry/IImplementationRegistry.sol";
9
- import {IRuleEntitlement} from "src/spaces/entitlements/rule/IRuleEntitlement.sol";
10
-
11
10
  // libraries
11
+ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
12
+ import {CurrencyTransfer} from "../../../utils/libraries/CurrencyTransfer.sol";
13
+ import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
14
+ import {MembershipStorage} from "../membership/MembershipStorage.sol";
12
15
  import {EntitlementGatedStorage} from "./EntitlementGatedStorage.sol";
13
- import {MembershipStorage} from "src/spaces/facets/membership/MembershipStorage.sol";
14
- import {CustomRevert} from "src/utils/libraries/CustomRevert.sol";
15
16
 
16
17
  abstract contract EntitlementGatedBase is IEntitlementGatedBase {
17
18
  using CustomRevert for bytes4;
19
+ using SafeTransferLib for address;
18
20
 
19
21
  modifier onlyEntitlementChecker() {
20
22
  if (msg.sender != address(EntitlementGatedStorage.layout().entitlementChecker)) {
21
- CustomRevert.revertWith(EntitlementGated_OnlyEntitlementChecker.selector);
23
+ EntitlementGated_OnlyEntitlementChecker.selector.revertWith();
22
24
  }
23
25
  _;
24
26
  }
@@ -33,9 +35,7 @@ abstract contract EntitlementGatedBase is IEntitlementGatedBase {
33
35
  IRuleEntitlement entitlement,
34
36
  uint256 roleId
35
37
  ) internal {
36
- if (callerAddress == address(0)) {
37
- CustomRevert.revertWith(EntitlementGated_InvalidAddress.selector);
38
- }
38
+ if (callerAddress == address(0)) EntitlementGated_InvalidAddress.selector.revertWith();
39
39
 
40
40
  EntitlementGatedStorage.Layout storage ds = EntitlementGatedStorage.layout();
41
41
  Transaction storage transaction = ds.transactions[transactionId];
@@ -43,7 +43,7 @@ abstract contract EntitlementGatedBase is IEntitlementGatedBase {
43
43
  uint256 _length = transaction.roleIds.length;
44
44
  for (uint256 i; i < _length; ++i) {
45
45
  if (transaction.roleIds[i] == roleId) {
46
- revert EntitlementGated_TransactionCheckAlreadyRegistered();
46
+ EntitlementGated_TransactionCheckAlreadyRegistered.selector.revertWith();
47
47
  }
48
48
  }
49
49
  }
@@ -155,94 +155,51 @@ abstract contract EntitlementGatedBase is IEntitlementGatedBase {
155
155
  /* V2 */
156
156
  /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
157
157
 
158
- /// @notice Requests a V2 entitlement check with optimized validation and execution
159
- /// @param walletAddress The wallet address to check entitlements for
160
- /// @param senderAddress The original sender address (encoded in extraData)
161
- /// @param transactionId The unique transaction identifier
162
- /// @param entitlement The entitlement contract to use for checking
163
- /// @param requestId The specific request identifier
164
- /// @param value The amount of ETH to send with the request (if any)
165
- function _requestEntitlementCheckV2(
158
+ /// @notice Requests a crosschain entitlement check with payment escrow
159
+ /// @dev Stores entitlement reference for RuleData queries, escrows payment with
160
+ /// EntitlementChecker, and emits event for off-chain nodes to process.
161
+ function _requestEntitlementCheck(
166
162
  address walletAddress,
167
163
  address senderAddress,
168
164
  bytes32 transactionId,
169
165
  IRuleEntitlement entitlement,
170
166
  uint256 requestId,
171
- uint256 value
167
+ address currency,
168
+ uint256 amount
172
169
  ) internal {
173
- // Validate all inputs upfront
174
- _validateEntitlementCheckInputs(walletAddress, entitlement, value);
175
-
176
- // Setup transaction state
177
- EntitlementGatedStorage.Layout storage $ = EntitlementGatedStorage.layout();
178
- _setupTransaction($.transactions[transactionId], entitlement);
179
-
180
- // Execute the entitlement check request
181
- _executeEntitlementCheckRequest(
182
- $.entitlementChecker,
183
- walletAddress,
184
- transactionId,
185
- requestId,
186
- value,
187
- abi.encode(senderAddress)
188
- );
189
- }
190
-
191
- /// @notice Validates inputs for entitlement check request
192
- /// @param walletAddress The wallet address being checked
193
- /// @param entitlement The entitlement contract
194
- /// @param value The ETH value being sent
195
- function _validateEntitlementCheckInputs(
196
- address walletAddress,
197
- IRuleEntitlement entitlement,
198
- uint256 value
199
- ) private view {
200
- if (value > msg.value) {
201
- EntitlementGated_InvalidValue.selector.revertWith();
202
- }
203
170
  if (walletAddress == address(0)) {
204
171
  EntitlementGated_InvalidAddress.selector.revertWith();
205
172
  }
206
173
  if (address(entitlement) == address(0)) {
207
174
  EntitlementGated_InvalidEntitlement.selector.revertWith();
208
175
  }
209
- }
210
176
 
211
- /// @notice Sets up transaction state for entitlement checking
212
- /// @param transaction Storage reference to the transaction
213
- /// @param entitlement The entitlement contract to associate
214
- function _setupTransaction(
215
- Transaction storage transaction,
216
- IRuleEntitlement entitlement
217
- ) private {
218
- transaction.finalized = true;
219
- transaction.entitlement = entitlement;
220
- }
177
+ EntitlementGatedStorage.Layout storage $ = EntitlementGatedStorage.layout();
178
+ Transaction storage transaction = $.transactions[transactionId];
179
+ // Only set on first call; subsequent calls for other roleIds reuse existing state
180
+ if (!transaction.finalized) {
181
+ transaction.finalized = true;
182
+ transaction.entitlement = entitlement;
183
+ }
221
184
 
222
- /// @notice Executes the entitlement check request with optimized call pattern
223
- /// @param checker The entitlement checker contract
224
- /// @param walletAddress The wallet address to check
225
- /// @param transactionId The transaction identifier
226
- /// @param requestId The request identifier
227
- /// @param value The ETH value to send
228
- /// @param extraData Encoded additional data
229
- function _executeEntitlementCheckRequest(
230
- IEntitlementChecker checker,
231
- address walletAddress,
232
- bytes32 transactionId,
233
- uint256 requestId,
234
- uint256 value,
235
- bytes memory extraData
236
- ) private {
237
- if (value > 0) {
238
- checker.requestEntitlementCheckV2{value: value}(
239
- walletAddress,
240
- transactionId,
241
- requestId,
242
- extraData
185
+ IEntitlementChecker checker = $.entitlementChecker;
186
+ bytes memory data = abi.encode(
187
+ walletAddress,
188
+ transactionId,
189
+ requestId,
190
+ currency,
191
+ amount,
192
+ senderAddress
193
+ );
194
+
195
+ if (currency == CurrencyTransfer.NATIVE_TOKEN) {
196
+ checker.requestEntitlementCheck{value: amount}(
197
+ IEntitlementCheckerBase.CheckType.V3,
198
+ data
243
199
  );
244
200
  } else {
245
- checker.requestEntitlementCheckV2(walletAddress, transactionId, requestId, extraData);
201
+ if (amount != 0) currency.safeApproveWithRetry(address(checker), amount);
202
+ checker.requestEntitlementCheck(IEntitlementCheckerBase.CheckType.V3, data);
246
203
  }
247
204
  }
248
205
 
@@ -42,12 +42,32 @@ interface IEntitlementGatedBase {
42
42
  }
43
43
 
44
44
  interface IEntitlementGated is IEntitlementGatedBase {
45
+ /// @notice Called by the xchain node to post the result of the entitlement check
46
+ /// @param transactionId The unique identifier for the transaction
47
+ /// @param roleId The role ID for the entitlement check
48
+ /// @param result The result of the entitlement check (PASSED or FAILED)
45
49
  function postEntitlementCheckResult(
46
50
  bytes32 transactionId,
47
51
  uint256 roleId,
48
52
  NodeVoteStatus result
49
53
  ) external;
50
54
 
55
+ /// @notice Post the result of the entitlement check for a specific role
56
+ /// @dev Only the entitlement checker can call this function
57
+ /// @param transactionId The unique identifier for the transaction
58
+ /// @param roleId The role ID for the entitlement check
59
+ /// @param result The result of the entitlement check (PASSED or FAILED)
60
+ function postEntitlementCheckResultV2(
61
+ bytes32 transactionId,
62
+ uint256 roleId,
63
+ NodeVoteStatus result
64
+ ) external payable;
65
+
66
+ /// @notice Get the rule data for a specific transaction and role
67
+ /// @dev Deprecated: Use EntitlementDataQueryable.getCrossChainEntitlementData instead
68
+ /// @param transactionId The unique identifier for the transaction
69
+ /// @param roleId The role ID for the entitlement check
70
+ /// @return The rule data for the transaction and role
51
71
  function getRuleData(
52
72
  bytes32 transactionId,
53
73
  uint256 roleId
@@ -289,13 +289,8 @@ abstract contract MembershipJoin is
289
289
  return false;
290
290
  }
291
291
 
292
- /// @notice Handles crosschain entitlement checks with proper payment distribution
293
- /// @param receiver The address to check entitlements for
294
- /// @param sender The address sending the transaction
295
- /// @param transactionId The transaction identifier
296
- /// @param requiredAmount The required payment amount
297
- /// @return isEntitled Whether user is entitled (always false for crosschain)
298
- /// @return isCrosschainPending Whether crosschain checks are pending
292
+ /// @dev Checks all crosschain entitlements across roles. User passes if any check succeeds.
293
+ /// Payment is escrowed only once (on first check) and returned when any check completes.
299
294
  function _checkCrosschainEntitlements(
300
295
  IRolesBase.Role[] memory roles,
301
296
  address receiver,
@@ -303,7 +298,7 @@ abstract contract MembershipJoin is
303
298
  bytes32 transactionId,
304
299
  uint256 requiredAmount
305
300
  ) internal returns (bool isEntitled, bool isCrosschainPending) {
306
- bool paymentSent = false;
301
+ bool paymentSent;
307
302
 
308
303
  for (uint256 i; i < roles.length; ++i) {
309
304
  if (roles[i].disabled) continue;
@@ -312,28 +307,16 @@ abstract contract MembershipJoin is
312
307
  IEntitlement entitlement = IEntitlement(roles[i].entitlements[j]);
313
308
 
314
309
  if (entitlement.isCrosschain()) {
315
- if (!paymentSent) {
316
- // Send only the required amount to crosschain check
317
- // Excess will be handled by existing refund mechanisms
318
- _requestEntitlementCheckV2(
319
- receiver,
320
- sender,
321
- transactionId,
322
- IRuleEntitlement(address(entitlement)),
323
- roles[i].id,
324
- requiredAmount
325
- );
326
- paymentSent = true;
327
- } else {
328
- _requestEntitlementCheckV2(
329
- receiver,
330
- sender,
331
- transactionId,
332
- IRuleEntitlement(address(entitlement)),
333
- roles[i].id,
334
- 0
335
- );
336
- }
310
+ _requestEntitlementCheck(
311
+ receiver,
312
+ sender,
313
+ transactionId,
314
+ IRuleEntitlement(address(entitlement)),
315
+ roles[i].id,
316
+ _getMembershipCurrency(),
317
+ paymentSent ? 0 : requiredAmount
318
+ );
319
+ paymentSent = true;
337
320
  isCrosschainPending = true;
338
321
  }
339
322
  }
@@ -238,14 +238,16 @@ abstract contract TippingBase is ITippingBase, PointsBase {
238
238
  // Reset ERC20 approval
239
239
  if (!isNative) SafeTransferLib.safeApprove(currency, spaceFactory, 0);
240
240
 
241
- // Mint points for fee payment
242
- address airdropDiamond = _getAirdropDiamond();
243
- uint256 points = _getPoints(
244
- airdropDiamond,
245
- ITownsPointsBase.Action.Tip,
246
- abi.encode(protocolFee)
247
- );
248
- _mintPoints(airdropDiamond, msg.sender, points);
241
+ // Mint points for fee payment (only for ETH tips)
242
+ if (isNative) {
243
+ address airdropDiamond = _getAirdropDiamond();
244
+ uint256 points = _getPoints(
245
+ airdropDiamond,
246
+ ITownsPointsBase.Action.Tip,
247
+ abi.encode(protocolFee)
248
+ );
249
+ _mintPoints(airdropDiamond, msg.sender, points);
250
+ }
249
251
  }
250
252
 
251
253
  /// @dev Validates common tip requirements