@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 +3 -3
- package/scripts/deployments/facets/DeployEntitlementChecker.s.sol +5 -2
- package/src/base/registry/facets/checker/EntitlementChecker.sol +102 -20
- package/src/base/registry/facets/checker/IEntitlementChecker.sol +24 -0
- package/src/base/registry/facets/xchain/IXChain.sol +6 -5
- package/src/base/registry/facets/xchain/XChain.sol +32 -27
- package/src/base/registry/facets/xchain/XChainCheckLib.sol +8 -6
- package/src/base/registry/facets/xchain/XChainLib.sol +11 -4
- package/src/spaces/facets/gated/EntitlementGated.sol +6 -13
- package/src/spaces/facets/gated/EntitlementGatedBase.sol +40 -83
- package/src/spaces/facets/gated/IEntitlementGated.sol +20 -0
- package/src/spaces/facets/membership/join/MembershipJoin.sol +13 -30
- package/src/spaces/facets/tipping/TippingBase.sol +10 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towns-protocol/contracts",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
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": "
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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 =
|
|
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 "
|
|
12
|
-
import {CustomRevert} from "
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
59
|
+
XChainLib.Check storage check = layout.checks[transactionId];
|
|
59
60
|
|
|
60
61
|
// clean up checks if any
|
|
61
|
-
uint256
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
84
|
-
XChainLib.
|
|
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
|
|
133
|
-
|
|
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
|
-
//
|
|
136
|
-
|
|
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 {
|
|
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 {
|
|
6
|
-
import {
|
|
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
|
-
/// @
|
|
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
|
-
/// @
|
|
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
|
-
/// @
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
159
|
-
/// @
|
|
160
|
-
///
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
/// @
|
|
293
|
-
///
|
|
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
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|