@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.
Files changed (24) hide show
  1. package/docs/membership_architecture.md +237 -0
  2. package/package.json +3 -3
  3. package/scripts/deployments/diamonds/DeploySpaceFactory.s.sol +2 -2
  4. package/scripts/deployments/facets/DeployMembership.s.sol +2 -1
  5. package/scripts/deployments/utils/DeployMockERC20.s.sol +1 -1
  6. package/scripts/deployments/utils/DeployMockUSDC.s.sol +19 -0
  7. package/scripts/interactions/InteractPostDeploy.s.sol +11 -0
  8. package/src/factory/facets/feature/FeatureManagerFacet.sol +32 -29
  9. package/src/factory/facets/feature/FeatureManagerMod.sol +248 -0
  10. package/src/factory/facets/feature/{IFeatureManagerFacet.sol → IFeatureManager.sol} +2 -35
  11. package/src/factory/facets/fee/FeeManagerFacet.sol +1 -1
  12. package/src/factory/facets/fee/FeeTypesLib.sol +8 -1
  13. package/src/spaces/facets/dispatcher/DispatcherBase.sol +13 -5
  14. package/src/spaces/facets/gated/EntitlementGated.sol +9 -5
  15. package/src/spaces/facets/membership/IMembership.sol +11 -1
  16. package/src/spaces/facets/membership/MembershipBase.sol +30 -59
  17. package/src/spaces/facets/membership/MembershipFacet.sol +19 -1
  18. package/src/spaces/facets/membership/MembershipStorage.sol +1 -0
  19. package/src/spaces/facets/membership/join/MembershipJoin.sol +186 -110
  20. package/src/spaces/facets/treasury/ITreasury.sol +2 -1
  21. package/src/spaces/facets/treasury/Treasury.sol +21 -24
  22. package/src/spaces/facets/xchain/SpaceEntitlementGated.sol +3 -2
  23. package/src/factory/facets/feature/FeatureManagerBase.sol +0 -152
  24. 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 {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
6
- import {FeatureCondition} from "./FeatureManagerStorage.sol";
5
+ import {FeatureCondition} from "./FeatureManagerMod.sol";
7
6
 
8
- /// @title IFeatureManagerFacetBase
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
@@ -116,7 +116,7 @@ contract FeeManagerFacet is
116
116
  true
117
117
  );
118
118
 
119
- // membership fee
119
+ // membership fee (ETH)
120
120
  _setFeeConfig(
121
121
  FeeTypesLib.MEMBERSHIP,
122
122
  protocolRecipient,
@@ -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] += msg.value;
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
- if (msg.value != 0) _captureValue(transactionId);
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 IEntitlementGated, EntitlementGatedBase, ReentrancyGuard, Facet {
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
- // Called by the xchain node to post the result of the entitlement check
30
- // the internal function validates the transactionId and the result
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
- $.membershipCurrency = CurrencyTransfer.NATIVE_TOKEN;
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
- IPlatformRequirements platform = _getPlatformRequirements();
79
- uint256 baseFee = platform.getMembershipFee();
80
- if (membershipPrice == 0) return baseFee;
81
- uint256 bpsFee = BasisPoints.calculate(membershipPrice, platform.getMembershipBps());
82
- return FixedPointMathLib.max(bpsFee, baseFee);
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
- if (membershipPrice == 0) return (protocolFee, protocolFee);
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
- function _getMembershipCurrency() internal view returns (address) {
238
- return MembershipStorage.layout().membershipCurrency;
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
- /* GETTERS */
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
  }
@@ -25,6 +25,7 @@ library MembershipStorage {
25
25
  uint256 freeAllocation;
26
26
  address pricingModule;
27
27
  mapping(uint256 => uint256) renewalPriceByTokenId;
28
+ // deprecated
28
29
  uint256 tokenBalance;
29
30
  mapping(bytes32 => address) pendingJoinRequests;
30
31
  string membershipImage;