@towns-protocol/contracts 0.0.442 → 0.0.444
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/docs/membership_architecture.md +237 -0
- package/package.json +3 -3
- package/scripts/deployments/diamonds/DeploySpaceFactory.s.sol +2 -2
- package/scripts/deployments/facets/DeployMembership.s.sol +2 -1
- package/scripts/deployments/utils/DeployMockERC20.s.sol +1 -1
- package/scripts/deployments/utils/DeployMockUSDC.s.sol +19 -0
- package/scripts/interactions/InteractPostDeploy.s.sol +11 -0
- package/src/factory/facets/feature/FeatureManagerFacet.sol +32 -29
- package/src/factory/facets/feature/FeatureManagerMod.sol +248 -0
- package/src/factory/facets/feature/{IFeatureManagerFacet.sol → IFeatureManager.sol} +2 -35
- package/src/factory/facets/fee/FeeManagerFacet.sol +1 -1
- package/src/factory/facets/fee/FeeTypesLib.sol +8 -1
- package/src/spaces/facets/dispatcher/DispatcherBase.sol +13 -5
- package/src/spaces/facets/gated/EntitlementGated.sol +9 -5
- package/src/spaces/facets/membership/IMembership.sol +11 -1
- package/src/spaces/facets/membership/MembershipBase.sol +30 -59
- package/src/spaces/facets/membership/MembershipFacet.sol +19 -1
- package/src/spaces/facets/membership/MembershipStorage.sol +1 -0
- package/src/spaces/facets/membership/join/MembershipJoin.sol +186 -110
- package/src/spaces/facets/treasury/ITreasury.sol +2 -1
- package/src/spaces/facets/treasury/Treasury.sol +21 -24
- package/src/spaces/facets/xchain/SpaceEntitlementGated.sol +3 -2
- package/src/factory/facets/feature/FeatureManagerBase.sol +0 -152
- package/src/factory/facets/feature/FeatureManagerStorage.sol +0 -47
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
// interfaces
|
|
5
|
+
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
|
|
6
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
7
|
+
import {IRewardsDistribution} from "../../../base/registry/facets/distribution/v2/IRewardsDistribution.sol";
|
|
8
|
+
|
|
9
|
+
// libraries
|
|
10
|
+
import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
|
|
11
|
+
import {LibCall} from "solady/utils/LibCall.sol";
|
|
12
|
+
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
13
|
+
|
|
14
|
+
// types
|
|
15
|
+
using EnumerableSetLib for EnumerableSetLib.Bytes32Set;
|
|
16
|
+
using CustomRevert for bytes4;
|
|
17
|
+
|
|
18
|
+
/// @notice Emitted when a feature condition is set or updated
|
|
19
|
+
/// @param featureId The unique identifier for the feature whose condition was set
|
|
20
|
+
/// @param condition The condition parameters that were set for the feature
|
|
21
|
+
event FeatureConditionSet(bytes32 indexed featureId, FeatureCondition condition);
|
|
22
|
+
|
|
23
|
+
/// @notice Emitted when a feature condition is disabled
|
|
24
|
+
/// @param featureId The unique identifier for the feature whose condition was disabled
|
|
25
|
+
event FeatureConditionDisabled(bytes32 indexed featureId);
|
|
26
|
+
|
|
27
|
+
/// @notice Error thrown when a threshold exceeds the token's total supply
|
|
28
|
+
error InvalidThreshold();
|
|
29
|
+
|
|
30
|
+
/// @notice Error thrown when a token has zero total supply
|
|
31
|
+
error InvalidTotalSupply();
|
|
32
|
+
|
|
33
|
+
/// @notice Error thrown when an invalid token address is provided (e.g., zero address)
|
|
34
|
+
error InvalidToken();
|
|
35
|
+
|
|
36
|
+
/// @notice Error thrown when the token does not implement the required interfaces (e.g., IVotes and ERC20 totalSupply)
|
|
37
|
+
error InvalidInterface();
|
|
38
|
+
|
|
39
|
+
/// @notice Error thrown when a feature condition is not active
|
|
40
|
+
error FeatureNotActive();
|
|
41
|
+
|
|
42
|
+
/// @notice Error thrown when a feature condition already exists
|
|
43
|
+
error FeatureAlreadyExists();
|
|
44
|
+
|
|
45
|
+
/// @notice Error thrown when an invalid condition type is provided
|
|
46
|
+
error InvalidConditionType();
|
|
47
|
+
|
|
48
|
+
/// @notice The type of condition for a feature
|
|
49
|
+
/// @param VotingPower The condition is based on voting power
|
|
50
|
+
/// @param StakingPower The condition is based on staking power
|
|
51
|
+
enum ConditionType {
|
|
52
|
+
VotingPower,
|
|
53
|
+
StakingPower
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// @notice Represents a condition for feature activation
|
|
57
|
+
/// @dev Used to determine if a feature should be enabled based on token voting power
|
|
58
|
+
/// @param checker The address of the checker used for voting (must implement IVotes)
|
|
59
|
+
/// @param threshold The minimum voting power (votes) required to activate the feature
|
|
60
|
+
/// @param active Whether the condition is currently active
|
|
61
|
+
/// @param extraData Additional data that might be used for specialized condition logic
|
|
62
|
+
struct FeatureCondition {
|
|
63
|
+
address checker;
|
|
64
|
+
bool active;
|
|
65
|
+
uint256 threshold;
|
|
66
|
+
bytes extraData;
|
|
67
|
+
ConditionType conditionType;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// keccak256(abi.encode(uint256(keccak256("factory.facets.feature.manager.storage")) - 1)) & ~bytes32(uint256(0xff))
|
|
71
|
+
bytes32 constant STORAGE_SLOT = 0x20c456a8ea15fcf7965033c954321ffd9dc82a2c65f686a77e2a67da65c29000;
|
|
72
|
+
|
|
73
|
+
/// @notice Storage layout for the FeatureManager
|
|
74
|
+
/// @dev Maps feature IDs to their activation conditions
|
|
75
|
+
/// @custom:storage-location erc7201:factory.facets.feature.manager.storage
|
|
76
|
+
struct Layout {
|
|
77
|
+
// Feature IDs
|
|
78
|
+
EnumerableSetLib.Bytes32Set featureIds;
|
|
79
|
+
// Feature ID => Condition
|
|
80
|
+
mapping(bytes32 featureId => FeatureCondition condition) conditions;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getStorage() pure returns (Layout storage $) {
|
|
84
|
+
assembly {
|
|
85
|
+
$.slot := STORAGE_SLOT
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getSelf() view returns (address self) {
|
|
90
|
+
assembly {
|
|
91
|
+
self := address()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// @notice Creates or updates a feature condition based on the create flag
|
|
96
|
+
/// @dev Validates token interface compliance before storing the condition
|
|
97
|
+
/// @param featureId The unique identifier for the feature
|
|
98
|
+
/// @param condition The condition struct containing token, threshold, active status, and extra data
|
|
99
|
+
/// @param writeIfNotExists True to write the condition if it does not exist, false to revert if it does not exist
|
|
100
|
+
function upsertFeatureCondition(
|
|
101
|
+
bytes32 featureId,
|
|
102
|
+
FeatureCondition calldata condition,
|
|
103
|
+
bool writeIfNotExists
|
|
104
|
+
) {
|
|
105
|
+
if (condition.conditionType == ConditionType.VotingPower) {
|
|
106
|
+
validateVotingInterface(condition);
|
|
107
|
+
} else if (condition.conditionType == ConditionType.StakingPower) {
|
|
108
|
+
validateStakingInterface(condition);
|
|
109
|
+
} else {
|
|
110
|
+
InvalidConditionType.selector.revertWith();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Layout storage $ = getStorage();
|
|
114
|
+
if (!writeIfNotExists) {
|
|
115
|
+
if (!$.featureIds.contains(featureId)) FeatureNotActive.selector.revertWith();
|
|
116
|
+
} else {
|
|
117
|
+
if (!$.featureIds.add(featureId)) FeatureAlreadyExists.selector.revertWith();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$.conditions[featureId] = condition;
|
|
121
|
+
emit FeatureConditionSet(featureId, condition);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// @notice Disables a feature by setting its active flag to false
|
|
125
|
+
/// @dev This does not delete the condition, only deactivates it
|
|
126
|
+
/// @param featureId The unique identifier for the feature to disable
|
|
127
|
+
function disableFeatureCondition(bytes32 featureId) {
|
|
128
|
+
FeatureCondition storage condition = getFeatureCondition(featureId);
|
|
129
|
+
if (!condition.active) FeatureNotActive.selector.revertWith();
|
|
130
|
+
condition.active = false;
|
|
131
|
+
emit FeatureConditionDisabled(featureId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// @notice Retrieves the condition for a specific feature
|
|
135
|
+
/// @dev Returns the complete condition struct with all parameters
|
|
136
|
+
/// @param featureId The unique identifier for the feature
|
|
137
|
+
/// @return The complete condition configuration for the feature
|
|
138
|
+
function getFeatureCondition(bytes32 featureId) view returns (FeatureCondition storage) {
|
|
139
|
+
return getStorage().conditions[featureId];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// @notice Retrieves all feature conditions
|
|
143
|
+
/// @dev Returns an array of all feature conditions
|
|
144
|
+
/// @return conditions An array of all feature conditions
|
|
145
|
+
function getFeatureConditions() view returns (FeatureCondition[] memory conditions) {
|
|
146
|
+
Layout storage $ = getStorage();
|
|
147
|
+
// Use values() over at() for full iteration - avoids bounds checking overhead
|
|
148
|
+
bytes32[] memory ids = $.featureIds.values();
|
|
149
|
+
uint256 featureCount = ids.length;
|
|
150
|
+
|
|
151
|
+
conditions = new FeatureCondition[](featureCount);
|
|
152
|
+
for (uint256 i; i < featureCount; ++i) {
|
|
153
|
+
conditions[i] = $.conditions[ids[i]];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// @notice Retrieves all feature conditions for a specific addr
|
|
158
|
+
/// @dev Returns an array of all feature conditions that are active for the addr
|
|
159
|
+
/// @return conditions An array of all feature conditions that are active for the addr
|
|
160
|
+
function getFeatureConditionsForAddress(
|
|
161
|
+
address addr
|
|
162
|
+
) view returns (FeatureCondition[] memory conditions) {
|
|
163
|
+
Layout storage $ = getStorage();
|
|
164
|
+
// Use values() over at() for full iteration - avoids bounds checking overhead
|
|
165
|
+
bytes32[] memory ids = $.featureIds.values();
|
|
166
|
+
uint256 featureCount = ids.length;
|
|
167
|
+
|
|
168
|
+
// Gas optimization: Allocate full array then resize (memory cheaper than storage reads)
|
|
169
|
+
conditions = new FeatureCondition[](featureCount);
|
|
170
|
+
uint256 index;
|
|
171
|
+
|
|
172
|
+
for (uint256 i; i < featureCount; ++i) {
|
|
173
|
+
FeatureCondition storage cond = $.conditions[ids[i]];
|
|
174
|
+
|
|
175
|
+
if (isValidCondition(cond, addr)) {
|
|
176
|
+
conditions[index++] = cond;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Resize array to actual number of valid conditions
|
|
181
|
+
assembly ("memory-safe") {
|
|
182
|
+
mstore(conditions, index)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function validateVotingInterface(FeatureCondition calldata condition) view {
|
|
187
|
+
if (condition.checker == address(0)) InvalidToken.selector.revertWith();
|
|
188
|
+
|
|
189
|
+
address self = getSelf();
|
|
190
|
+
|
|
191
|
+
// Check if the token implements IVotes.getVotes with proper return data
|
|
192
|
+
(bool success, bool exceededMaxCopy, bytes memory data) = LibCall.tryStaticCall(
|
|
193
|
+
condition.checker,
|
|
194
|
+
gasleft(),
|
|
195
|
+
32,
|
|
196
|
+
abi.encodeCall(IVotes.getVotes, (self))
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (!success || exceededMaxCopy || data.length != 32) InvalidInterface.selector.revertWith();
|
|
200
|
+
|
|
201
|
+
// Check if the token implements ERC20.totalSupply with proper return data
|
|
202
|
+
(success, exceededMaxCopy, data) = LibCall.tryStaticCall(
|
|
203
|
+
condition.checker,
|
|
204
|
+
gasleft(),
|
|
205
|
+
32,
|
|
206
|
+
abi.encodeCall(IERC20.totalSupply, ())
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (!success || exceededMaxCopy || data.length != 32) InvalidInterface.selector.revertWith();
|
|
210
|
+
|
|
211
|
+
uint256 totalSupply = abi.decode(data, (uint256));
|
|
212
|
+
if (totalSupply == 0) InvalidTotalSupply.selector.revertWith();
|
|
213
|
+
if (condition.threshold > totalSupply) InvalidThreshold.selector.revertWith();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function validateStakingInterface(FeatureCondition calldata condition) view {
|
|
217
|
+
if (condition.checker == address(0)) InvalidToken.selector.revertWith();
|
|
218
|
+
if (condition.threshold > type(uint96).max) InvalidThreshold.selector.revertWith();
|
|
219
|
+
|
|
220
|
+
address self = getSelf();
|
|
221
|
+
(bool success, bool exceededMaxCopy, bytes memory data) = LibCall.tryStaticCall(
|
|
222
|
+
condition.checker,
|
|
223
|
+
gasleft(),
|
|
224
|
+
32,
|
|
225
|
+
abi.encodeCall(IRewardsDistribution.stakedByDepositor, (self))
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (!success || exceededMaxCopy || data.length != 32) InvalidInterface.selector.revertWith();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// @notice Checks if a condition should be included for a given address
|
|
232
|
+
/// @dev Returns true if the condition is active, has a valid token, and meets the threshold
|
|
233
|
+
/// @param condition The condition to check
|
|
234
|
+
/// @param addr The address to check against
|
|
235
|
+
/// @return True if the condition should be included, false otherwise
|
|
236
|
+
function isValidCondition(FeatureCondition storage condition, address addr) view returns (bool) {
|
|
237
|
+
if (!condition.active) return false;
|
|
238
|
+
if (condition.checker == address(0)) return false;
|
|
239
|
+
if (condition.conditionType == ConditionType.VotingPower) {
|
|
240
|
+
uint256 votes = IVotes(condition.checker).getVotes(addr);
|
|
241
|
+
return votes >= condition.threshold;
|
|
242
|
+
} else if (condition.conditionType == ConditionType.StakingPower) {
|
|
243
|
+
uint96 staked = IRewardsDistribution(condition.checker).stakedByDepositor(addr);
|
|
244
|
+
return staked >= condition.threshold;
|
|
245
|
+
} else {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -2,42 +2,9 @@
|
|
|
2
2
|
pragma solidity ^0.8.23;
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
|
-
import {
|
|
6
|
-
import {FeatureCondition} from "./FeatureManagerStorage.sol";
|
|
5
|
+
import {FeatureCondition} from "./FeatureManagerMod.sol";
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
/// @notice Base interface for the FeatureManager facet defining errors and events
|
|
10
|
-
/// @dev Used by the FeatureManager facet and any other facets that need to access feature conditions
|
|
11
|
-
interface IFeatureManagerFacetBase {
|
|
12
|
-
/// @notice Emitted when a feature condition is set or updated
|
|
13
|
-
/// @param featureId The unique identifier for the feature whose condition was set
|
|
14
|
-
/// @param condition The condition parameters that were set for the feature
|
|
15
|
-
event FeatureConditionSet(bytes32 indexed featureId, FeatureCondition condition);
|
|
16
|
-
|
|
17
|
-
/// @notice Emitted when a feature condition is disabled
|
|
18
|
-
/// @param featureId The unique identifier for the feature whose condition was disabled
|
|
19
|
-
event FeatureConditionDisabled(bytes32 indexed featureId);
|
|
20
|
-
|
|
21
|
-
/// @notice Error thrown when a threshold exceeds the token's total supply
|
|
22
|
-
error InvalidThreshold();
|
|
23
|
-
|
|
24
|
-
/// @notice Error thrown when a token has zero total supply
|
|
25
|
-
error InvalidTotalSupply();
|
|
26
|
-
|
|
27
|
-
/// @notice Error thrown when an invalid token address is provided (e.g., zero address)
|
|
28
|
-
error InvalidToken();
|
|
29
|
-
|
|
30
|
-
/// @notice Error thrown when the token does not implement the required interfaces (e.g., IVotes and ERC20 totalSupply)
|
|
31
|
-
error InvalidInterface();
|
|
32
|
-
|
|
33
|
-
/// @notice Error thrown when a feature condition is not active
|
|
34
|
-
error FeatureNotActive();
|
|
35
|
-
|
|
36
|
-
/// @notice Error thrown when a feature condition already exists
|
|
37
|
-
error FeatureAlreadyExists();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface IFeatureManagerFacet is IFeatureManagerFacetBase {
|
|
7
|
+
interface IFeatureManager {
|
|
41
8
|
/// @notice Sets the condition for a feature
|
|
42
9
|
/// @param featureId The unique identifier for the feature
|
|
43
10
|
/// @param condition The condition struct containing token, threshold, active status, and extra data
|
|
@@ -5,7 +5,7 @@ pragma solidity ^0.8.29;
|
|
|
5
5
|
/// @notice Library defining fee type constants for the FeeManager system
|
|
6
6
|
/// @dev Uses keccak256 for gas-efficient constant generation
|
|
7
7
|
library FeeTypesLib {
|
|
8
|
-
/// @notice Fee for space membership purchases
|
|
8
|
+
/// @notice Fee for space membership purchases (ETH)
|
|
9
9
|
bytes32 internal constant MEMBERSHIP = keccak256("FEE_TYPE.MEMBERSHIP");
|
|
10
10
|
|
|
11
11
|
/// @notice Fee for app installations
|
|
@@ -25,4 +25,11 @@ library FeeTypesLib {
|
|
|
25
25
|
|
|
26
26
|
/// @notice Fee for bot actions
|
|
27
27
|
bytes32 internal constant BOT_ACTION = keccak256("FEE_TYPE.BOT_ACTION");
|
|
28
|
+
|
|
29
|
+
/// @notice Generates fee type for membership based on currency
|
|
30
|
+
/// @param currency The payment currency address
|
|
31
|
+
/// @return The fee type identifier for the given currency
|
|
32
|
+
function membership(address currency) internal pure returns (bytes32) {
|
|
33
|
+
return keccak256(abi.encodePacked("FEE_TYPE.MEMBERSHIP", currency));
|
|
34
|
+
}
|
|
28
35
|
}
|
|
@@ -25,9 +25,9 @@ abstract contract DispatcherBase is IDispatcherBase {
|
|
|
25
25
|
return ds.transactionData.get(transactionId).value;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function _captureValue(bytes32 transactionId) internal {
|
|
28
|
+
function _captureValue(bytes32 transactionId, uint256 value) internal {
|
|
29
29
|
DispatcherStorage.Layout storage ds = DispatcherStorage.layout();
|
|
30
|
-
ds.transactionBalance[transactionId] +=
|
|
30
|
+
ds.transactionBalance[transactionId] += value;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function _releaseCapturedValue(bytes32 transactionId, uint256 value) internal {
|
|
@@ -62,9 +62,15 @@ abstract contract DispatcherBase is IDispatcherBase {
|
|
|
62
62
|
return keccak256(abi.encodePacked(keyHash, inputSeed));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/// @notice Registers a transaction and captures payment
|
|
66
|
+
/// @param sender The address initiating the transaction
|
|
67
|
+
/// @param data The encoded transaction data
|
|
68
|
+
/// @param capturedAmount The amount captured (ETH or ERC20)
|
|
69
|
+
/// @return transactionId The unique identifier for this transaction
|
|
65
70
|
function _registerTransaction(
|
|
66
71
|
address sender,
|
|
67
|
-
bytes memory data
|
|
72
|
+
bytes memory data,
|
|
73
|
+
uint256 capturedAmount
|
|
68
74
|
) internal returns (bytes32 transactionId) {
|
|
69
75
|
bytes32 keyHash = keccak256(abi.encodePacked(sender, block.number));
|
|
70
76
|
|
|
@@ -75,12 +81,14 @@ abstract contract DispatcherBase is IDispatcherBase {
|
|
|
75
81
|
|
|
76
82
|
DispatcherStorage.Layout storage ds = DispatcherStorage.layout();
|
|
77
83
|
StoragePointer.Bytes storage capturedDataRef = ds.transactionData.get(transactionId);
|
|
84
|
+
|
|
78
85
|
// revert if the transaction already exists
|
|
79
86
|
if (capturedDataRef.value.length > 0) {
|
|
80
87
|
Dispatcher__TransactionAlreadyExists.selector.revertWith();
|
|
81
88
|
}
|
|
82
|
-
|
|
83
89
|
capturedDataRef.value = data;
|
|
84
|
-
|
|
90
|
+
|
|
91
|
+
// Capture payment amount
|
|
92
|
+
if (capturedAmount != 0) _captureValue(transactionId, capturedAmount);
|
|
85
93
|
}
|
|
86
94
|
}
|
|
@@ -3,7 +3,6 @@ pragma solidity ^0.8.23;
|
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
5
|
import {IEntitlementGated} from "./IEntitlementGated.sol";
|
|
6
|
-
|
|
7
6
|
import {IEntitlementChecker} from "src/base/registry/facets/checker/IEntitlementChecker.sol";
|
|
8
7
|
import {IRuleEntitlement} from "src/spaces/entitlements/rule/IRuleEntitlement.sol";
|
|
9
8
|
|
|
@@ -14,7 +13,12 @@ import {EntitlementGatedBase} from "./EntitlementGatedBase.sol";
|
|
|
14
13
|
import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
|
|
15
14
|
import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
|
|
16
15
|
|
|
17
|
-
contract EntitlementGated is
|
|
16
|
+
abstract contract EntitlementGated is
|
|
17
|
+
IEntitlementGated,
|
|
18
|
+
EntitlementGatedBase,
|
|
19
|
+
ReentrancyGuard,
|
|
20
|
+
Facet
|
|
21
|
+
{
|
|
18
22
|
function __EntitlementGated_init(
|
|
19
23
|
IEntitlementChecker entitlementChecker
|
|
20
24
|
) external onlyInitializing {
|
|
@@ -26,8 +30,8 @@ contract EntitlementGated is IEntitlementGated, EntitlementGatedBase, Reentrancy
|
|
|
26
30
|
_setEntitlementChecker(entitlementChecker);
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
|
|
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
35
|
function postEntitlementCheckResult(
|
|
32
36
|
bytes32 transactionId,
|
|
33
37
|
uint256 roleId,
|
|
@@ -49,7 +53,7 @@ contract EntitlementGated is IEntitlementGated, EntitlementGatedBase, Reentrancy
|
|
|
49
53
|
_postEntitlementCheckResultV2(transactionId, roleId, result);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
/// deprecated Use EntitlementDataQueryable.getCrossChainEntitlementData instead
|
|
56
|
+
/// @dev deprecated Use EntitlementDataQueryable.getCrossChainEntitlementData instead
|
|
53
57
|
function getRuleData(
|
|
54
58
|
bytes32 transactionId,
|
|
55
59
|
uint256 roleId
|
|
@@ -58,6 +58,12 @@ interface IMembershipBase {
|
|
|
58
58
|
error Membership__InvalidAction();
|
|
59
59
|
error Membership__CannotSetFreeAllocationOnPaidSpace();
|
|
60
60
|
|
|
61
|
+
/// @notice Error thrown when ETH is sent for ERC20 payment
|
|
62
|
+
error Membership__UnexpectedValue();
|
|
63
|
+
|
|
64
|
+
/// @notice Error thrown when currency is not supported for fees
|
|
65
|
+
error Membership__UnsupportedCurrency();
|
|
66
|
+
|
|
61
67
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
62
68
|
/* EVENTS */
|
|
63
69
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
@@ -67,7 +73,7 @@ interface IMembershipBase {
|
|
|
67
73
|
event MembershipCurrencyUpdated(address indexed currency);
|
|
68
74
|
event MembershipFeeRecipientUpdated(address indexed recipient);
|
|
69
75
|
event MembershipFreeAllocationUpdated(uint256 indexed allocation);
|
|
70
|
-
event MembershipWithdrawal(address indexed recipient, uint256 amount);
|
|
76
|
+
event MembershipWithdrawal(address indexed currency, address indexed recipient, uint256 amount);
|
|
71
77
|
event MembershipTokenIssued(address indexed recipient, uint256 indexed tokenId);
|
|
72
78
|
event MembershipTokenRejected(address indexed recipient);
|
|
73
79
|
}
|
|
@@ -191,6 +197,10 @@ interface IMembership is IMembershipBase {
|
|
|
191
197
|
/// @return The membership currency
|
|
192
198
|
function getMembershipCurrency() external view returns (address);
|
|
193
199
|
|
|
200
|
+
/// @notice Set the membership currency
|
|
201
|
+
/// @param currency The new membership currency address
|
|
202
|
+
function setMembershipCurrency(address currency) external;
|
|
203
|
+
|
|
194
204
|
/// @notice Get the space factory
|
|
195
205
|
/// @return The space factory
|
|
196
206
|
function getSpaceFactory() external view returns (address);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
pragma solidity ^0.8.23;
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
|
+
import {IFeeManager} from "../../../factory/facets/fee/IFeeManager.sol";
|
|
5
6
|
import {IPlatformRequirements} from "../../../factory/facets/platform/requirements/IPlatformRequirements.sol";
|
|
6
7
|
import {IPricingModules} from "../../../factory/facets/architect/pricing/IPricingModules.sol";
|
|
7
8
|
import {IMembershipBase} from "./IMembership.sol";
|
|
@@ -10,9 +11,10 @@ import {IMembershipPricing} from "./pricing/IMembershipPricing.sol";
|
|
|
10
11
|
// libraries
|
|
11
12
|
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
|
|
12
13
|
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
|
|
13
|
-
import {BasisPoints} from "../../../utils/libraries/BasisPoints.sol";
|
|
14
14
|
import {CurrencyTransfer} from "../../../utils/libraries/CurrencyTransfer.sol";
|
|
15
15
|
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
16
|
+
import {FeeTypesLib} from "../../../factory/facets/fee/FeeTypesLib.sol";
|
|
17
|
+
import {FeeConfig} from "../../../factory/facets/fee/FeeManagerStorage.sol";
|
|
16
18
|
import {MembershipStorage} from "./MembershipStorage.sol";
|
|
17
19
|
|
|
18
20
|
abstract contract MembershipBase is IMembershipBase {
|
|
@@ -24,7 +26,7 @@ abstract contract MembershipBase is IMembershipBase {
|
|
|
24
26
|
|
|
25
27
|
$.spaceFactory = spaceFactory;
|
|
26
28
|
$.pricingModule = info.pricingModule;
|
|
27
|
-
|
|
29
|
+
_setMembershipCurrency(info.currency);
|
|
28
30
|
$.membershipMaxSupply = info.maxSupply;
|
|
29
31
|
|
|
30
32
|
if (info.freeAllocation > 0) {
|
|
@@ -50,21 +52,6 @@ abstract contract MembershipBase is IMembershipBase {
|
|
|
50
52
|
/* MEMBERSHIP */
|
|
51
53
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
52
54
|
|
|
53
|
-
function _collectProtocolFee(
|
|
54
|
-
address payer,
|
|
55
|
-
uint256 membershipPrice
|
|
56
|
-
) internal returns (uint256 protocolFee) {
|
|
57
|
-
protocolFee = _getProtocolFee(membershipPrice);
|
|
58
|
-
|
|
59
|
-
// transfer the platform fee to the platform fee recipient
|
|
60
|
-
CurrencyTransfer.transferCurrency(
|
|
61
|
-
_getMembershipCurrency(),
|
|
62
|
-
payer, // from
|
|
63
|
-
_getPlatformRequirements().getFeeRecipient(), // to
|
|
64
|
-
protocolFee
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
55
|
function _getMembershipPrice(
|
|
69
56
|
uint256 totalSupply
|
|
70
57
|
) internal view virtual returns (uint256 membershipPrice) {
|
|
@@ -75,51 +62,23 @@ abstract contract MembershipBase is IMembershipBase {
|
|
|
75
62
|
}
|
|
76
63
|
|
|
77
64
|
function _getProtocolFee(uint256 membershipPrice) internal view returns (uint256) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
bytes32 feeType = _getMembershipFeeType(_getMembershipCurrency());
|
|
66
|
+
return
|
|
67
|
+
IFeeManager(_getSpaceFactory()).calculateFee(feeType, address(0), membershipPrice, "");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// @notice Returns the fee type for the membership currency
|
|
71
|
+
/// @dev Uses dynamic fee type for non-native currencies
|
|
72
|
+
function _getMembershipFeeType(address currency) internal pure returns (bytes32) {
|
|
73
|
+
if (currency == CurrencyTransfer.NATIVE_TOKEN) return FeeTypesLib.MEMBERSHIP;
|
|
74
|
+
return FeeTypesLib.membership(currency);
|
|
83
75
|
}
|
|
84
76
|
|
|
85
77
|
function _getTotalMembershipPayment(
|
|
86
78
|
uint256 membershipPrice
|
|
87
79
|
) internal view returns (uint256 totalRequired, uint256 protocolFee) {
|
|
88
80
|
protocolFee = _getProtocolFee(membershipPrice);
|
|
89
|
-
|
|
90
|
-
return (membershipPrice + protocolFee, protocolFee);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function _transferIn(address from, uint256 amount) internal returns (uint256) {
|
|
94
|
-
MembershipStorage.Layout storage $ = MembershipStorage.layout();
|
|
95
|
-
|
|
96
|
-
// get the currency being used for membership
|
|
97
|
-
address currency = _getMembershipCurrency();
|
|
98
|
-
|
|
99
|
-
if (currency == CurrencyTransfer.NATIVE_TOKEN) {
|
|
100
|
-
$.tokenBalance += amount;
|
|
101
|
-
return amount;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// handle erc20 tokens
|
|
105
|
-
uint256 balanceBefore = currency.balanceOf(address(this));
|
|
106
|
-
CurrencyTransfer.safeTransferERC20(currency, from, address(this), amount);
|
|
107
|
-
uint256 balanceAfter = currency.balanceOf(address(this));
|
|
108
|
-
|
|
109
|
-
// Calculate the amount of tokens transferred
|
|
110
|
-
uint256 finalAmount = balanceAfter - balanceBefore;
|
|
111
|
-
if (finalAmount != amount) Membership__InsufficientPayment.selector.revertWith();
|
|
112
|
-
|
|
113
|
-
$.tokenBalance += finalAmount;
|
|
114
|
-
return finalAmount;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function _getCreatorBalance() internal view returns (uint256) {
|
|
118
|
-
return MembershipStorage.layout().tokenBalance;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function _setCreatorBalance(uint256 newBalance) internal {
|
|
122
|
-
MembershipStorage.layout().tokenBalance = newBalance;
|
|
81
|
+
totalRequired = membershipPrice + protocolFee;
|
|
123
82
|
}
|
|
124
83
|
|
|
125
84
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
@@ -202,7 +161,6 @@ abstract contract MembershipBase is IMembershipBase {
|
|
|
202
161
|
function _setMembershipFreeAllocation(uint256 newAllocation) internal {
|
|
203
162
|
MembershipStorage.Layout storage $ = MembershipStorage.layout();
|
|
204
163
|
($.freeAllocation, $.freeAllocationEnabled) = (newAllocation, true);
|
|
205
|
-
emit MembershipFreeAllocationUpdated(newAllocation);
|
|
206
164
|
}
|
|
207
165
|
|
|
208
166
|
function _getMembershipFreeAllocation() internal view returns (uint256) {
|
|
@@ -234,8 +192,21 @@ abstract contract MembershipBase is IMembershipBase {
|
|
|
234
192
|
/* CURRENCY */
|
|
235
193
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
236
194
|
|
|
237
|
-
|
|
238
|
-
|
|
195
|
+
/// @dev Sets the membership currency. Only currencies with an enabled fee config in FeeManager
|
|
196
|
+
/// can be set. Native token (address(0) or NATIVE_TOKEN) is always supported.
|
|
197
|
+
function _setMembershipCurrency(address currency) internal {
|
|
198
|
+
if (!(currency == address(0) || currency == CurrencyTransfer.NATIVE_TOKEN)) {
|
|
199
|
+
bytes32 feeType = FeeTypesLib.membership(currency);
|
|
200
|
+
FeeConfig memory config = IFeeManager(_getSpaceFactory()).getFeeConfig(feeType);
|
|
201
|
+
if (!config.enabled) Membership__UnsupportedCurrency.selector.revertWith();
|
|
202
|
+
}
|
|
203
|
+
MembershipStorage.layout().membershipCurrency = currency;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function _getMembershipCurrency() internal view returns (address currency) {
|
|
207
|
+
currency = MembershipStorage.layout().membershipCurrency;
|
|
208
|
+
// Normalize address(0) to NATIVE_TOKEN for backwards compatibility
|
|
209
|
+
if (currency == address(0)) currency = CurrencyTransfer.NATIVE_TOKEN;
|
|
239
210
|
}
|
|
240
211
|
|
|
241
212
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
@@ -6,6 +6,8 @@ import {IMembership} from "./IMembership.sol";
|
|
|
6
6
|
import {IMembershipPricing} from "./pricing/IMembershipPricing.sol";
|
|
7
7
|
|
|
8
8
|
// libraries
|
|
9
|
+
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
|
|
10
|
+
import {CurrencyTransfer} from "../../../utils/libraries/CurrencyTransfer.sol";
|
|
9
11
|
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
10
12
|
|
|
11
13
|
// contracts
|
|
@@ -15,6 +17,7 @@ import {MembershipJoin} from "./join/MembershipJoin.sol";
|
|
|
15
17
|
|
|
16
18
|
contract MembershipFacet is IMembership, MembershipJoin, ReentrancyGuard, Facet {
|
|
17
19
|
using CustomRevert for bytes4;
|
|
20
|
+
using SafeTransferLib for address;
|
|
18
21
|
|
|
19
22
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
20
23
|
/* JOIN */
|
|
@@ -141,6 +144,7 @@ contract MembershipFacet is IMembership, MembershipJoin, ReentrancyGuard, Facet
|
|
|
141
144
|
|
|
142
145
|
_verifyFreeAllocation(newAllocation);
|
|
143
146
|
_setMembershipFreeAllocation(newAllocation);
|
|
147
|
+
emit MembershipFreeAllocationUpdated(newAllocation);
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
/// @inheritdoc IMembership
|
|
@@ -178,14 +182,24 @@ contract MembershipFacet is IMembership, MembershipJoin, ReentrancyGuard, Facet
|
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
181
|
-
/*
|
|
185
|
+
/* CURRENCY */
|
|
182
186
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
183
187
|
|
|
188
|
+
/// @inheritdoc IMembership
|
|
189
|
+
function setMembershipCurrency(address currency) external onlyOwner {
|
|
190
|
+
_setMembershipCurrency(currency);
|
|
191
|
+
emit MembershipCurrencyUpdated(currency);
|
|
192
|
+
}
|
|
193
|
+
|
|
184
194
|
/// @inheritdoc IMembership
|
|
185
195
|
function getMembershipCurrency() external view returns (address) {
|
|
186
196
|
return _getMembershipCurrency();
|
|
187
197
|
}
|
|
188
198
|
|
|
199
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
200
|
+
/* GETTERS */
|
|
201
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
202
|
+
|
|
189
203
|
/// @inheritdoc IMembership
|
|
190
204
|
function getSpaceFactory() external view returns (address) {
|
|
191
205
|
return _getSpaceFactory();
|
|
@@ -193,6 +207,10 @@ contract MembershipFacet is IMembership, MembershipJoin, ReentrancyGuard, Facet
|
|
|
193
207
|
|
|
194
208
|
/// @inheritdoc IMembership
|
|
195
209
|
function revenue() external view returns (uint256) {
|
|
210
|
+
address currency = _getMembershipCurrency();
|
|
211
|
+
if (currency != CurrencyTransfer.NATIVE_TOKEN) {
|
|
212
|
+
return currency.balanceOf(address(this));
|
|
213
|
+
}
|
|
196
214
|
return address(this).balance;
|
|
197
215
|
}
|
|
198
216
|
}
|