@towns-protocol/contracts 0.0.449 → 0.0.451

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.449",
3
+ "version": "0.0.451",
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.449",
36
+ "@towns-protocol/prettier-config": "^0.0.451",
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": "268f44c69d00993f57e702b176a9a9c34ea8f6cc"
53
+ "gitHead": "ad68c0f2514e44f51293824ebba5ccc2f8a6494c"
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,41 +2,40 @@
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";
18
19
 
19
20
  contract EntitlementChecker is IEntitlementChecker, Facet {
20
21
  using EnumerableSet for EnumerableSet.AddressSet;
21
- using EnumerableSet for EnumerableSet.UintSet;
22
22
  using EnumerableSet for EnumerableSet.Bytes32Set;
23
+ using EnumerableSet for EnumerableSet.UintSet;
23
24
  using CustomRevert for bytes4;
24
-
25
- // =============================================================
26
- // Initializer
27
- // =============================================================
25
+ using SafeTransferLib for address;
28
26
 
29
27
  function __EntitlementChecker_init() external onlyInitializing {
30
28
  _addInterface(type(IEntitlementChecker).interfaceId);
31
29
  }
32
30
 
33
- // =============================================================
34
- // Modifiers
35
- // =============================================================
36
- modifier onlyNodeOperator(address node, address operator) {
37
- EntitlementCheckerStorage.Layout storage layout = EntitlementCheckerStorage.layout();
31
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
32
+ /* MODIFIERS */
33
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
34
+
35
+ modifier onlyNodeOperator(address node) {
36
+ EntitlementCheckerStorage.Layout storage $ = EntitlementCheckerStorage.layout();
38
37
 
39
- if (layout.operatorByNode[node] != operator) {
38
+ if (msg.sender != $.operatorByNode[node]) {
40
39
  EntitlementChecker_InvalidNodeOperator.selector.revertWith();
41
40
  }
42
41
  _;
@@ -48,63 +47,134 @@ contract EntitlementChecker is IEntitlementChecker, Facet {
48
47
  if (!nodeOperatorLayout.operators.contains(msg.sender)) {
49
48
  EntitlementChecker_InvalidOperator.selector.revertWith();
50
49
  }
51
- _;
52
-
53
50
  if (nodeOperatorLayout.statusByOperator[msg.sender] != NodeOperatorStatus.Approved) {
54
51
  EntitlementChecker_OperatorNotActive.selector.revertWith();
55
52
  }
53
+ _;
56
54
  }
57
55
 
58
- // =============================================================
59
- // External
60
- // =============================================================
56
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
57
+ /* ADMIN FUNCTIONS */
58
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
61
59
 
62
60
  /// @inheritdoc IEntitlementChecker
63
61
  function registerNode(address node) external onlyRegisteredApprovedOperator {
64
- EntitlementCheckerStorage.Layout storage layout = EntitlementCheckerStorage.layout();
62
+ EntitlementCheckerStorage.Layout storage $ = EntitlementCheckerStorage.layout();
65
63
 
66
- if (layout.nodes.contains(node)) {
64
+ if ($.nodes.contains(node)) {
67
65
  EntitlementChecker_NodeAlreadyRegistered.selector.revertWith();
68
66
  }
69
67
 
70
- layout.nodes.add(node);
71
- layout.operatorByNode[node] = msg.sender;
68
+ $.nodes.add(node);
69
+ $.operatorByNode[node] = msg.sender;
72
70
 
73
71
  emit NodeRegistered(node);
74
72
  }
75
73
 
76
74
  /// @inheritdoc IEntitlementChecker
77
- function unregisterNode(address node) external onlyNodeOperator(node, msg.sender) {
78
- EntitlementCheckerStorage.Layout storage layout = EntitlementCheckerStorage.layout();
75
+ function unregisterNode(address node) external onlyNodeOperator(node) {
76
+ EntitlementCheckerStorage.Layout storage $ = EntitlementCheckerStorage.layout();
79
77
 
80
- if (!layout.nodes.contains(node)) {
78
+ if (!$.nodes.contains(node)) {
81
79
  EntitlementChecker_NodeNotRegistered.selector.revertWith();
82
80
  }
83
81
 
84
- layout.nodes.remove(node);
85
- delete layout.operatorByNode[node];
82
+ $.nodes.remove(node);
83
+ delete $.operatorByNode[node];
86
84
 
87
85
  emit NodeUnregistered(node);
88
86
  }
89
87
 
88
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
89
+ /* ENTITLEMENT */
90
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
91
+
92
+ /// @inheritdoc IEntitlementChecker
93
+ function requestEntitlementCheck(
94
+ address receiver,
95
+ bytes32 transactionId,
96
+ uint256 roleId,
97
+ address[] memory nodes
98
+ ) external {
99
+ emit EntitlementCheckRequested(receiver, msg.sender, transactionId, roleId, nodes);
100
+ }
101
+
102
+ /// @inheritdoc IEntitlementChecker
103
+ function requestEntitlementCheckV2(
104
+ address receiver,
105
+ bytes32 transactionId,
106
+ uint256 requestId,
107
+ bytes memory extraData
108
+ ) external payable {
109
+ address sender = abi.decode(extraData, (address));
110
+ _requestEntitlementCheck(
111
+ receiver,
112
+ transactionId,
113
+ requestId,
114
+ CurrencyTransfer.NATIVE_TOKEN,
115
+ msg.value,
116
+ sender
117
+ );
118
+ }
119
+
120
+ /// @inheritdoc IEntitlementChecker
121
+ function requestEntitlementCheck(CheckType checkType, bytes calldata data) external payable {
122
+ if (checkType == CheckType.V1) {
123
+ if (msg.value != 0) EntitlementChecker_InvalidValue.selector.revertWith();
124
+ (address receiver, bytes32 transactionId, uint256 roleId, address[] memory nodes) = abi
125
+ .decode(data, (address, bytes32, uint256, address[]));
126
+ emit EntitlementCheckRequested(receiver, msg.sender, transactionId, roleId, nodes);
127
+ } else if (checkType == CheckType.V2) {
128
+ (
129
+ address receiver,
130
+ bytes32 transactionId,
131
+ uint256 requestId,
132
+ bytes memory extraData
133
+ ) = abi.decode(data, (address, bytes32, uint256, bytes));
134
+ address sender = abi.decode(extraData, (address));
135
+ _requestEntitlementCheck(
136
+ receiver,
137
+ transactionId,
138
+ requestId,
139
+ CurrencyTransfer.NATIVE_TOKEN,
140
+ msg.value,
141
+ sender
142
+ );
143
+ } else if (checkType == CheckType.V3) {
144
+ (
145
+ address receiver,
146
+ bytes32 transactionId,
147
+ uint256 requestId,
148
+ address currency,
149
+ uint256 amount,
150
+ address sender
151
+ ) = abi.decode(data, (address, bytes32, uint256, address, uint256, address));
152
+ _requestEntitlementCheck(receiver, transactionId, requestId, currency, amount, sender);
153
+ } else {
154
+ EntitlementChecker_InvalidCheckType.selector.revertWith();
155
+ }
156
+ }
157
+
158
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
159
+ /* GETTERS */
160
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
161
+
90
162
  /// @inheritdoc IEntitlementChecker
91
163
  function isValidNode(address node) external view returns (bool) {
92
- EntitlementCheckerStorage.Layout storage layout = EntitlementCheckerStorage.layout();
93
- return layout.nodes.contains(node);
164
+ return EntitlementCheckerStorage.layout().nodes.contains(node);
94
165
  }
95
166
 
96
167
  /// @inheritdoc IEntitlementChecker
97
168
  function getNodeCount() external view returns (uint256) {
98
- EntitlementCheckerStorage.Layout storage layout = EntitlementCheckerStorage.layout();
99
- return layout.nodes.length();
169
+ return EntitlementCheckerStorage.layout().nodes.length();
100
170
  }
101
171
 
102
172
  /// @inheritdoc IEntitlementChecker
103
173
  function getNodeAtIndex(uint256 index) external view returns (address) {
104
- EntitlementCheckerStorage.Layout storage layout = EntitlementCheckerStorage.layout();
174
+ EntitlementCheckerStorage.Layout storage $ = EntitlementCheckerStorage.layout();
105
175
 
106
- require(index < layout.nodes.length(), "Index out of bounds");
107
- return layout.nodes.at(index);
176
+ require(index < $.nodes.length(), "Index out of bounds");
177
+ return $.nodes.at(index);
108
178
  }
109
179
 
110
180
  /// @inheritdoc IEntitlementChecker
@@ -113,51 +183,74 @@ contract EntitlementChecker is IEntitlementChecker, Facet {
113
183
  }
114
184
 
115
185
  /// @inheritdoc IEntitlementChecker
116
- function requestEntitlementCheck(
117
- address walletAddress,
118
- bytes32 transactionId,
119
- uint256 roleId,
120
- address[] memory nodes
121
- ) external {
122
- emit EntitlementCheckRequested(walletAddress, msg.sender, transactionId, roleId, nodes);
186
+ function getNodesByOperator(address operator) external view returns (address[] memory nodes) {
187
+ EntitlementCheckerStorage.Layout storage $ = EntitlementCheckerStorage.layout();
188
+ address[] memory allNodes = $.nodes.values();
189
+ uint256 totalNodeCount = allNodes.length;
190
+ nodes = new address[](totalNodeCount);
191
+ uint256 nodeCount;
192
+ for (uint256 i; i < totalNodeCount; ++i) {
193
+ address node = allNodes[i];
194
+ if ($.operatorByNode[node] == operator) {
195
+ unchecked {
196
+ nodes[nodeCount++] = node;
197
+ }
198
+ }
199
+ }
200
+ assembly ("memory-safe") {
201
+ mstore(nodes, nodeCount)
202
+ }
123
203
  }
124
204
 
125
- /// @inheritdoc IEntitlementChecker
126
- function requestEntitlementCheckV2(
127
- address walletAddress,
205
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
206
+ /* INTERNAL */
207
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
208
+
209
+ function _requestEntitlementCheck(
210
+ address receiver,
128
211
  bytes32 transactionId,
129
212
  uint256 requestId,
130
- bytes memory extraData
131
- ) external payable {
213
+ address currency,
214
+ uint256 amount,
215
+ address sender
216
+ ) internal {
132
217
  address space = msg.sender;
133
- address senderAddress = abi.decode(extraData, (address));
134
218
 
135
219
  XChainLib.Layout storage layout = XChainLib.layout();
136
220
 
137
- layout.requestsBySender[senderAddress].add(transactionId);
221
+ layout.requestsBySender[sender].add(transactionId);
138
222
 
139
223
  // Only create the request if it doesn't exist yet
140
224
  XChainLib.Request storage request = layout.requests[transactionId];
141
225
  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();
226
+ request.caller = space;
227
+ request.blockNumber = block.number;
228
+ request.value = amount;
229
+ request.receiver = receiver;
230
+ request.currency = currency;
231
+
232
+ if (currency == CurrencyTransfer.NATIVE_TOKEN) {
233
+ if (amount != msg.value) EntitlementChecker_InvalidValue.selector.revertWith();
234
+ } else {
235
+ // ERC20: reject any ETH sent
236
+ if (msg.value != 0) EntitlementChecker_InvalidValue.selector.revertWith();
237
+ // ERC20: pull tokens from Space
238
+ if (amount != 0) currency.safeTransferFrom(space, address(this), amount);
153
239
  }
240
+ } else {
241
+ // Request already exists from a previous requestId on this transactionId.
242
+ // Escrow was established on the first request - reject any additional ETH
243
+ // to prevent funds being sent but not tracked.
244
+ if (msg.value != 0) EntitlementChecker_InvalidValue.selector.revertWith();
154
245
  }
155
246
 
156
247
  address[] memory randomNodes = _getRandomNodes(5);
157
248
 
158
- XChainLib.Check storage check = XChainLib.layout().checks[transactionId];
249
+ XChainLib.Check storage check = layout.checks[transactionId];
159
250
 
160
- check.requestIds.add(requestId);
251
+ if (!check.requestIds.add(requestId)) {
252
+ EntitlementChecker_DuplicateRequestId.selector.revertWith();
253
+ }
161
254
 
162
255
  for (uint256 i; i < randomNodes.length; ++i) {
163
256
  check.nodes[requestId].add(randomNodes[i]);
@@ -170,7 +263,7 @@ contract EntitlementChecker is IEntitlementChecker, Facet {
170
263
  }
171
264
 
172
265
  emit EntitlementCheckRequestedV2(
173
- walletAddress,
266
+ receiver,
174
267
  space,
175
268
  address(this),
176
269
  transactionId,
@@ -179,36 +272,12 @@ contract EntitlementChecker is IEntitlementChecker, Facet {
179
272
  );
180
273
  }
181
274
 
182
- /// @inheritdoc IEntitlementChecker
183
- function getNodesByOperator(address operator) external view returns (address[] memory nodes) {
184
- EntitlementCheckerStorage.Layout storage layout = EntitlementCheckerStorage.layout();
185
- uint256 totalNodeCount = layout.nodes.length();
186
- nodes = new address[](totalNodeCount);
187
- uint256 nodeCount;
188
- for (uint256 i; i < totalNodeCount; ++i) {
189
- address node = layout.nodes.at(i);
190
- if (layout.operatorByNode[node] == operator) {
191
- unchecked {
192
- nodes[nodeCount++] = node;
193
- }
194
- }
195
- }
196
- assembly ("memory-safe") {
197
- mstore(nodes, nodeCount) // Update the length of the array
198
- }
199
- }
200
-
201
- // =============================================================
202
- // Internal
203
- // =============================================================
204
275
  function _getRandomNodes(uint256 count) internal view returns (address[] memory randomNodes) {
205
- EntitlementCheckerStorage.Layout storage layout = EntitlementCheckerStorage.layout();
276
+ EntitlementCheckerStorage.Layout storage $ = EntitlementCheckerStorage.layout();
206
277
 
207
- uint256 nodeCount = layout.nodes.length();
278
+ uint256 nodeCount = $.nodes.length();
208
279
 
209
- if (count > nodeCount) {
210
- EntitlementChecker_InsufficientNumberOfNodes.selector.revertWith();
211
- }
280
+ if (count > nodeCount) EntitlementChecker_InsufficientNumberOfNodes.selector.revertWith();
212
281
 
213
282
  randomNodes = new address[](count);
214
283
  uint256[] memory indices = new uint256[](nodeCount);
@@ -221,7 +290,7 @@ contract EntitlementChecker is IEntitlementChecker, Facet {
221
290
  for (uint256 i; i < count; ++i) {
222
291
  // Adjust random function to generate within range 0 to n-1
223
292
  uint256 rand = _pseudoRandom(i, nodeCount);
224
- randomNodes[i] = layout.nodes.at(indices[rand]);
293
+ randomNodes[i] = $.nodes.at(indices[rand]);
225
294
  // Move the last element to the used slot and reduce the pool size
226
295
  indices[rand] = indices[--nodeCount];
227
296
  }
@@ -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(receiver, transactionId, roleId, nodes);
10
+ /// case CheckType.V2:
11
+ /// data = abi.encode(receiver, transactionId, requestId, extraData);
12
+ /// // where extraData = abi.encode(sender)
13
+ /// case CheckType.V3:
14
+ /// data = abi.encode(receiver, transactionId, requestId, currency, amount, sender);
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);
@@ -19,17 +38,17 @@ interface IEntitlementCheckerBase {
19
38
 
20
39
  /// @notice Event emitted when an entitlement check is requested
21
40
  event EntitlementCheckRequested(
22
- address callerAddress,
23
- address contractAddress,
41
+ address receiver,
42
+ address space,
24
43
  bytes32 transactionId,
25
44
  uint256 roleId,
26
45
  address[] selectedNodes
27
46
  );
28
47
 
29
48
  event EntitlementCheckRequestedV2(
30
- address walletAddress,
31
- address spaceAddress,
32
- address resolverAddress,
49
+ address receiver,
50
+ address space,
51
+ address resolver,
33
52
  bytes32 transactionId,
34
53
  uint256 roleId,
35
54
  address[] selectedNodes
@@ -45,49 +64,54 @@ interface IEntitlementChecker is IEntitlementCheckerBase {
45
64
  /// @param node The address of the node to unregister
46
65
  function unregisterNode(address node) external;
47
66
 
48
- /// @notice Check if a node address is registered and valid
49
- /// @param node The address of the node to check
50
- /// @return bool True if the node is valid, false otherwise
51
- function isValidNode(address node) external view returns (bool);
52
-
53
- /// @notice Get the total number of registered nodes
54
- /// @return uint256 The count of registered nodes
55
- function getNodeCount() external view returns (uint256);
56
-
57
- /// @notice Get the node address at a specific index
58
- /// @param index The index of the node to retrieve
59
- /// @return address The address of the node at the given index
60
- function getNodeAtIndex(uint256 index) external view returns (address);
61
-
62
- /// @notice Get a random selection of registered nodes
63
- /// @param count The number of random nodes to return
64
- /// @return address[] Array of randomly selected node addresses
65
- function getRandomNodes(uint256 count) external view returns (address[] memory);
66
-
67
67
  /// @notice Request an entitlement check for a transaction
68
- /// @param callerAddress The address initiating the check
68
+ /// @param receiver The address to check entitlements for (membership recipient)
69
69
  /// @param transactionId The unique identifier of the transaction
70
70
  /// @param roleId The role ID to check entitlements against
71
71
  /// @param nodes Array of node addresses that will perform the check
72
72
  function requestEntitlementCheck(
73
- address callerAddress,
73
+ address receiver,
74
74
  bytes32 transactionId,
75
75
  uint256 roleId,
76
76
  address[] memory nodes
77
77
  ) external;
78
78
 
79
79
  /// @notice Request an entitlement check with additional data (V2)
80
- /// @param walletAddress The wallet address to check entitlements for
80
+ /// @param receiver The address to check entitlements for (membership recipient)
81
81
  /// @param transactionId The unique identifier of the transaction
82
82
  /// @param requestId The unique identifier for this specific request
83
83
  /// @param extraData Additional data required for the check
84
84
  function requestEntitlementCheckV2(
85
- address walletAddress,
85
+ address receiver,
86
86
  bytes32 transactionId,
87
87
  uint256 requestId,
88
88
  bytes memory extraData
89
89
  ) external payable;
90
90
 
91
+ /// @notice Unified entitlement check request with enum-based dispatch
92
+ /// @param checkType The type of check to perform (V1, V2, or V3)
93
+ /// @param data Encoded parameters specific to the check type (see CheckType enum docs)
94
+ function requestEntitlementCheck(CheckType checkType, bytes calldata data) external payable;
95
+
96
+ /// @notice Check if a node address is registered and valid
97
+ /// @param node The address of the node to check
98
+ /// @return bool True if the node is valid, false otherwise
99
+ function isValidNode(address node) external view returns (bool);
100
+
101
+ /// @notice Get the total number of registered nodes
102
+ /// @return uint256 The count of registered nodes
103
+ function getNodeCount() external view returns (uint256);
104
+
105
+ /// @notice Get the node address at a specific index
106
+ /// @param index The index of the node to retrieve
107
+ /// @return address The address of the node at the given index
108
+ function getNodeAtIndex(uint256 index) external view returns (address);
109
+
110
+ /// @notice Get a random selection of registered nodes
111
+ /// @param count The number of random nodes to return
112
+ /// @return address[] Array of randomly selected node addresses
113
+ function getRandomNodes(uint256 count) external view returns (address[] memory);
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
  }