@towns-protocol/contracts 0.0.441 → 0.0.443
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/DeploySpace.s.sol +0 -7
- 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/InteractBaseAlpha.s.sol +3 -0
- package/scripts/interactions/InteractPostDeploy.s.sol +11 -0
- package/src/apps/facets/registry/AppRegistryBase.sol +4 -2
- package/src/apps/facets/registry/IAppRegistry.sol +1 -1
- package/src/factory/facets/architect/IArchitect.sol +1 -0
- package/src/factory/facets/create/CreateSpaceBase.sol +2 -9
- 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 -17
- 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 +192 -125
- 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 -4
- package/scripts/deployments/facets/DeployPrepayFacet.s.sol +0 -31
- package/scripts/interactions/InteractPrepay.s.sol +0 -30
- package/src/factory/facets/feature/FeatureManagerBase.sol +0 -152
- package/src/factory/facets/feature/FeatureManagerStorage.sol +0 -47
- package/src/spaces/facets/prepay/IPrepay.sol +0 -44
- package/src/spaces/facets/prepay/PrepayBase.sol +0 -27
- package/src/spaces/facets/prepay/PrepayFacet.sol +0 -65
- package/src/spaces/facets/prepay/PrepayStorage.sol +0 -26
|
@@ -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
|
}
|
|
@@ -10,9 +10,11 @@ import {IRolesBase} from "../../roles/IRoles.sol";
|
|
|
10
10
|
import {IMembership} from "../IMembership.sol";
|
|
11
11
|
|
|
12
12
|
// libraries
|
|
13
|
+
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
|
|
13
14
|
import {BasisPoints} from "../../../../utils/libraries/BasisPoints.sol";
|
|
14
15
|
import {CurrencyTransfer} from "../../../../utils/libraries/CurrencyTransfer.sol";
|
|
15
16
|
import {CustomRevert} from "../../../../utils/libraries/CustomRevert.sol";
|
|
17
|
+
import {MembershipStorage} from "../MembershipStorage.sol";
|
|
16
18
|
import {Permissions} from "../../Permissions.sol";
|
|
17
19
|
|
|
18
20
|
// contracts
|
|
@@ -22,7 +24,6 @@ import {DispatcherBase} from "../../dispatcher/DispatcherBase.sol";
|
|
|
22
24
|
import {Entitled} from "../../Entitled.sol";
|
|
23
25
|
import {EntitlementGatedBase} from "../../gated/EntitlementGatedBase.sol";
|
|
24
26
|
import {PointsBase} from "../../points/PointsBase.sol";
|
|
25
|
-
import {PrepayBase} from "../../prepay/PrepayBase.sol";
|
|
26
27
|
import {ReferralsBase} from "../../referrals/ReferralsBase.sol";
|
|
27
28
|
import {RolesBase} from "../../roles/RolesBase.sol";
|
|
28
29
|
import {MembershipBase} from "../MembershipBase.sol";
|
|
@@ -30,7 +31,18 @@ import {MembershipBase} from "../MembershipBase.sol";
|
|
|
30
31
|
/// @title MembershipJoin
|
|
31
32
|
/// @notice Handles the logic for joining a space, including entitlement checks and payment
|
|
32
33
|
/// processing
|
|
33
|
-
/// @dev
|
|
34
|
+
/// @dev Join Flow:
|
|
35
|
+
/// 1. Payment captured upfront via `_validateAndCapturePayment` (ETH held, ERC20 transferred in)
|
|
36
|
+
/// 2. Entitlement check determines sync vs async path:
|
|
37
|
+
/// - Sync (local entitlements): immediate token issuance or rejection
|
|
38
|
+
/// - Async (crosschain entitlements): payment held in dispatcher, resolved via callback
|
|
39
|
+
/// 3. On success: fees distributed from contract via `_pay*` functions, token issued
|
|
40
|
+
/// 4. On failure: full refund via `_refundBalance`
|
|
41
|
+
///
|
|
42
|
+
/// Payment Handling:
|
|
43
|
+
/// - ETH: received via payable, excess refunded after fees
|
|
44
|
+
/// - ERC20: exact amount transferred in, no refund needed
|
|
45
|
+
/// - All fee payments use `address(this)` as source since funds are already in contract
|
|
34
46
|
abstract contract MembershipJoin is
|
|
35
47
|
IRolesBase,
|
|
36
48
|
IPartnerRegistryBase,
|
|
@@ -41,11 +53,11 @@ abstract contract MembershipJoin is
|
|
|
41
53
|
RolesBase,
|
|
42
54
|
EntitlementGatedBase,
|
|
43
55
|
Entitled,
|
|
44
|
-
PrepayBase,
|
|
45
56
|
PointsBase,
|
|
46
57
|
ERC721ABase
|
|
47
58
|
{
|
|
48
59
|
using CustomRevert for bytes4;
|
|
60
|
+
using SafeTransferLib for address;
|
|
49
61
|
|
|
50
62
|
/// @notice Constant representing the permission to join a space
|
|
51
63
|
bytes32 internal constant JOIN_SPACE = bytes32(abi.encodePacked(Permissions.JoinSpace));
|
|
@@ -53,6 +65,12 @@ abstract contract MembershipJoin is
|
|
|
53
65
|
/// @notice Constant representing the joinSpace(address) function selector
|
|
54
66
|
bytes4 internal constant JOIN_SPACE_SELECTOR = bytes4(keccak256("joinSpace(address)"));
|
|
55
67
|
|
|
68
|
+
struct PricingDetails {
|
|
69
|
+
uint256 basePrice;
|
|
70
|
+
uint256 amountDue;
|
|
71
|
+
bool shouldCharge;
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
/// @notice Encodes data for joining a space
|
|
57
75
|
/// @param selector The type of transaction (join with or without referral)
|
|
58
76
|
/// @param sender The address of the sender
|
|
@@ -82,17 +100,14 @@ abstract contract MembershipJoin is
|
|
|
82
100
|
return joinDetails;
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
// Check if this is a free join due to prepaid supply
|
|
86
|
-
uint256 prepaidSupply = _getPrepaidSupply();
|
|
87
|
-
if (prepaidSupply > 0) {
|
|
88
|
-
joinDetails.isPrepaid = true;
|
|
89
|
-
return joinDetails;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
103
|
(uint256 totalRequired, ) = _getTotalMembershipPayment(membershipPrice);
|
|
93
104
|
(joinDetails.amountDue, joinDetails.shouldCharge) = (totalRequired, true);
|
|
94
105
|
}
|
|
95
106
|
|
|
107
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
108
|
+
/* JOIN */
|
|
109
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
110
|
+
|
|
96
111
|
/// @notice Handles the process of joining a space
|
|
97
112
|
/// @param receiver The address that will receive the membership token
|
|
98
113
|
function _joinSpace(address receiver) internal {
|
|
@@ -100,17 +115,14 @@ abstract contract MembershipJoin is
|
|
|
100
115
|
|
|
101
116
|
PricingDetails memory joinDetails = _getPricingDetails();
|
|
102
117
|
|
|
103
|
-
// Validate payment
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Consume prepaid membership if applicable
|
|
109
|
-
if (joinDetails.isPrepaid) _reducePrepay(1);
|
|
118
|
+
// Validate and capture payment (handles both free and paid memberships)
|
|
119
|
+
address currency = _getMembershipCurrency();
|
|
120
|
+
uint256 capturedAmount = _validateAndCapturePayment(currency, joinDetails.amountDue);
|
|
110
121
|
|
|
111
122
|
bytes32 transactionId = _registerTransaction(
|
|
112
123
|
receiver,
|
|
113
|
-
_encodeJoinSpaceData(JOIN_SPACE_SELECTOR, msg.sender, receiver, "")
|
|
124
|
+
_encodeJoinSpaceData(JOIN_SPACE_SELECTOR, msg.sender, receiver, ""),
|
|
125
|
+
capturedAmount
|
|
114
126
|
);
|
|
115
127
|
|
|
116
128
|
(bool isEntitled, bool isCrosschainPending) = _checkEntitlement(
|
|
@@ -122,7 +134,11 @@ abstract contract MembershipJoin is
|
|
|
122
134
|
|
|
123
135
|
if (!isCrosschainPending) {
|
|
124
136
|
if (isEntitled) {
|
|
125
|
-
if (joinDetails.shouldCharge)
|
|
137
|
+
if (!joinDetails.shouldCharge) {
|
|
138
|
+
_afterChargeForJoinSpace(transactionId, receiver, 0);
|
|
139
|
+
} else {
|
|
140
|
+
_chargeForJoinSpace(transactionId, joinDetails);
|
|
141
|
+
}
|
|
126
142
|
_refundBalance(transactionId, receiver);
|
|
127
143
|
_issueToken(receiver);
|
|
128
144
|
} else {
|
|
@@ -139,13 +155,9 @@ abstract contract MembershipJoin is
|
|
|
139
155
|
|
|
140
156
|
PricingDetails memory joinDetails = _getPricingDetails();
|
|
141
157
|
|
|
142
|
-
// Validate payment
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Consume prepaid membership if applicable
|
|
148
|
-
if (joinDetails.isPrepaid) _reducePrepay(1);
|
|
158
|
+
// Validate and capture payment (handles both free and paid memberships)
|
|
159
|
+
address currency = _getMembershipCurrency();
|
|
160
|
+
uint256 capturedAmount = _validateAndCapturePayment(currency, joinDetails.amountDue);
|
|
149
161
|
|
|
150
162
|
_validateUserReferral(receiver, referral);
|
|
151
163
|
|
|
@@ -155,7 +167,8 @@ abstract contract MembershipJoin is
|
|
|
155
167
|
|
|
156
168
|
bytes32 transactionId = _registerTransaction(
|
|
157
169
|
receiver,
|
|
158
|
-
_encodeJoinSpaceData(selector, msg.sender, receiver, referralData)
|
|
170
|
+
_encodeJoinSpaceData(selector, msg.sender, receiver, referralData),
|
|
171
|
+
capturedAmount
|
|
159
172
|
);
|
|
160
173
|
|
|
161
174
|
(bool isEntitled, bool isCrosschainPending) = _checkEntitlement(
|
|
@@ -167,9 +180,11 @@ abstract contract MembershipJoin is
|
|
|
167
180
|
|
|
168
181
|
if (!isCrosschainPending) {
|
|
169
182
|
if (isEntitled) {
|
|
170
|
-
if (joinDetails.shouldCharge)
|
|
183
|
+
if (!joinDetails.shouldCharge) {
|
|
184
|
+
_afterChargeForJoinSpace(transactionId, receiver, 0);
|
|
185
|
+
} else {
|
|
171
186
|
_chargeForJoinSpaceWithReferral(transactionId, joinDetails);
|
|
172
|
-
|
|
187
|
+
}
|
|
173
188
|
_refundBalance(transactionId, receiver);
|
|
174
189
|
_issueToken(receiver);
|
|
175
190
|
} else {
|
|
@@ -184,12 +199,46 @@ abstract contract MembershipJoin is
|
|
|
184
199
|
emit MembershipTokenRejected(receiver);
|
|
185
200
|
}
|
|
186
201
|
|
|
202
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
203
|
+
/* VALIDATION */
|
|
204
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
205
|
+
|
|
206
|
+
/// @notice Validates and captures payment based on currency type
|
|
207
|
+
/// @dev For ETH: validates msg.value >= required amount, returns msg.value for refund handling
|
|
208
|
+
/// @dev For ERC20: rejects any ETH sent, transfers tokens from sender to this contract
|
|
209
|
+
/// @dev Handles free memberships (amountRequired = 0) correctly for both currency types
|
|
210
|
+
/// @param currency The currency address (NATIVE_TOKEN for ETH, or ERC20 address)
|
|
211
|
+
/// @param amountRequired The required payment amount (0 for free memberships)
|
|
212
|
+
/// @return The amount to capture (msg.value for ETH, amountRequired for ERC20)
|
|
213
|
+
function _validateAndCapturePayment(
|
|
214
|
+
address currency,
|
|
215
|
+
uint256 amountRequired
|
|
216
|
+
) internal returns (uint256) {
|
|
217
|
+
if (currency == CurrencyTransfer.NATIVE_TOKEN) {
|
|
218
|
+
// ETH payment: validate msg.value, return full amount for refund handling
|
|
219
|
+
if (msg.value < amountRequired) Membership__InsufficientPayment.selector.revertWith();
|
|
220
|
+
return msg.value;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ERC20 payment: reject any ETH sent
|
|
224
|
+
if (msg.value != 0) revert Membership__UnexpectedValue();
|
|
225
|
+
|
|
226
|
+
// Transfer ERC20 tokens from sender to contract (skip for free memberships)
|
|
227
|
+
if (amountRequired != 0) _transferIn(currency, msg.sender, amountRequired);
|
|
228
|
+
|
|
229
|
+
return amountRequired;
|
|
230
|
+
}
|
|
231
|
+
|
|
187
232
|
function _validateUserReferral(address receiver, ReferralTypes memory referral) internal view {
|
|
188
233
|
if (referral.userReferral == receiver || referral.userReferral == msg.sender) {
|
|
189
234
|
Membership__InvalidAddress.selector.revertWith();
|
|
190
235
|
}
|
|
191
236
|
}
|
|
192
237
|
|
|
238
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
239
|
+
/* ENTITLEMENT */
|
|
240
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
241
|
+
|
|
193
242
|
/// @notice Checks if a user is entitled to join the space and handles the entitlement process
|
|
194
243
|
/// @dev This function checks both local and crosschain entitlements
|
|
195
244
|
/// @param receiver The address of the user trying to join the space
|
|
@@ -300,7 +349,7 @@ abstract contract MembershipJoin is
|
|
|
300
349
|
bytes32 transactionId,
|
|
301
350
|
PricingDetails memory joinDetails
|
|
302
351
|
) internal {
|
|
303
|
-
(bytes4 selector,
|
|
352
|
+
(bytes4 selector, , address receiver, ) = abi.decode(
|
|
304
353
|
_getCapturedData(transactionId),
|
|
305
354
|
(bytes4, address, address, bytes)
|
|
306
355
|
);
|
|
@@ -309,16 +358,9 @@ abstract contract MembershipJoin is
|
|
|
309
358
|
Membership__InvalidTransactionType.selector.revertWith();
|
|
310
359
|
}
|
|
311
360
|
|
|
312
|
-
|
|
313
|
-
uint256 ownerProceeds = joinDetails.amountDue - protocolFee;
|
|
361
|
+
_payProtocolFee(_getMembershipCurrency(), joinDetails.basePrice);
|
|
314
362
|
|
|
315
|
-
_afterChargeForJoinSpace(
|
|
316
|
-
transactionId,
|
|
317
|
-
sender,
|
|
318
|
-
receiver,
|
|
319
|
-
joinDetails.amountDue,
|
|
320
|
-
ownerProceeds
|
|
321
|
-
);
|
|
363
|
+
_afterChargeForJoinSpace(transactionId, receiver, joinDetails.amountDue);
|
|
322
364
|
}
|
|
323
365
|
|
|
324
366
|
/// @notice Processes the charge for joining a space with referral
|
|
@@ -339,45 +381,31 @@ abstract contract MembershipJoin is
|
|
|
339
381
|
|
|
340
382
|
ReferralTypes memory referral = abi.decode(referralData, (ReferralTypes));
|
|
341
383
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
sender,
|
|
348
|
-
referral.partner,
|
|
349
|
-
joinDetails.basePrice
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
uint256 referralFee = _collectReferralCodeFee(
|
|
353
|
-
sender,
|
|
354
|
-
referral.userReferral,
|
|
355
|
-
referral.referralCode,
|
|
356
|
-
joinDetails.basePrice
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
ownerProceeds = joinDetails.amountDue - protocolFee - partnerFee - referralFee;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
_afterChargeForJoinSpace(
|
|
363
|
-
transactionId,
|
|
384
|
+
address currency = _getMembershipCurrency();
|
|
385
|
+
_payProtocolFee(currency, joinDetails.basePrice);
|
|
386
|
+
_payPartnerFee(currency, referral.partner, joinDetails.basePrice);
|
|
387
|
+
_payReferralFee(
|
|
388
|
+
currency,
|
|
364
389
|
sender,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
390
|
+
referral.userReferral,
|
|
391
|
+
referral.referralCode,
|
|
392
|
+
joinDetails.basePrice
|
|
368
393
|
);
|
|
394
|
+
|
|
395
|
+
_afterChargeForJoinSpace(transactionId, receiver, joinDetails.amountDue);
|
|
369
396
|
}
|
|
370
397
|
|
|
398
|
+
/// @notice Finalizes the charge after fees have been paid
|
|
399
|
+
/// @dev Releases `paymentRequired` from captured value (leaving excess for refund),
|
|
400
|
+
/// cleans up transaction data, and mints points. Called by both sync and async flows.
|
|
401
|
+
/// @param transactionId The unique identifier for this join transaction
|
|
402
|
+
/// @param receiver The address receiving the membership
|
|
403
|
+
/// @param paymentRequired The amount consumed for payment (fees + owner proceeds)
|
|
371
404
|
function _afterChargeForJoinSpace(
|
|
372
405
|
bytes32 transactionId,
|
|
373
|
-
address payer,
|
|
374
406
|
address receiver,
|
|
375
|
-
uint256 paymentRequired
|
|
376
|
-
uint256 ownerProceeds
|
|
407
|
+
uint256 paymentRequired
|
|
377
408
|
) internal {
|
|
378
|
-
// account for owner's proceeds
|
|
379
|
-
if (ownerProceeds != 0) _transferIn(payer, ownerProceeds);
|
|
380
|
-
|
|
381
409
|
_releaseCapturedValue(transactionId, paymentRequired);
|
|
382
410
|
_deleteCapturedData(transactionId);
|
|
383
411
|
|
|
@@ -413,81 +441,98 @@ abstract contract MembershipJoin is
|
|
|
413
441
|
}
|
|
414
442
|
}
|
|
415
443
|
|
|
416
|
-
/// @notice Refunds the
|
|
444
|
+
/// @notice Refunds the remaining captured balance
|
|
445
|
+
/// @dev NOTE: Currently refunds go to `receiver` (the membership recipient), not the original
|
|
446
|
+
/// payer (msg.sender). This means if Alice pays for Bob's membership, excess ETH is refunded
|
|
447
|
+
/// to Bob, not Alice. This design may change in a future update to refund the original payer.
|
|
417
448
|
/// @param transactionId The unique identifier for this join transaction
|
|
418
|
-
/// @param
|
|
419
|
-
function _refundBalance(bytes32 transactionId, address
|
|
449
|
+
/// @param receiver The address to receive the refund (currently the membership receiver)
|
|
450
|
+
function _refundBalance(bytes32 transactionId, address receiver) internal {
|
|
420
451
|
uint256 userValue = _getCapturedValue(transactionId);
|
|
421
452
|
if (userValue > 0) {
|
|
422
453
|
_releaseCapturedValue(transactionId, userValue);
|
|
423
454
|
CurrencyTransfer.transferCurrency(
|
|
424
455
|
_getMembershipCurrency(),
|
|
425
456
|
address(this),
|
|
426
|
-
|
|
457
|
+
receiver,
|
|
427
458
|
userValue
|
|
428
459
|
);
|
|
429
460
|
}
|
|
430
461
|
}
|
|
431
462
|
|
|
432
|
-
|
|
433
|
-
|
|
463
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
464
|
+
/* FEE DISTRIBUTION */
|
|
465
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
466
|
+
|
|
467
|
+
/// @notice Pays the protocol fee to the platform fee recipient
|
|
468
|
+
/// @param currency The currency to pay in
|
|
469
|
+
/// @param membershipPrice The price of the membership
|
|
470
|
+
/// @return protocolFee The amount of protocol fee paid
|
|
471
|
+
function _payProtocolFee(
|
|
472
|
+
address currency,
|
|
473
|
+
uint256 membershipPrice
|
|
474
|
+
) internal returns (uint256 protocolFee) {
|
|
475
|
+
protocolFee = _getProtocolFee(membershipPrice);
|
|
476
|
+
|
|
477
|
+
CurrencyTransfer.transferCurrency(
|
|
478
|
+
currency,
|
|
479
|
+
address(this),
|
|
480
|
+
_getPlatformRequirements().getFeeRecipient(),
|
|
481
|
+
protocolFee
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/// @notice Pays the referral fee if applicable
|
|
486
|
+
/// @param currency The currency to pay in
|
|
487
|
+
/// @param sender The original sender address (used for self-referral check)
|
|
488
|
+
/// @param userReferral The user referral address
|
|
434
489
|
/// @param referralCode The referral code used
|
|
435
490
|
/// @param membershipPrice The price of the membership
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
address
|
|
491
|
+
function _payReferralFee(
|
|
492
|
+
address currency,
|
|
493
|
+
address sender,
|
|
439
494
|
address userReferral,
|
|
440
495
|
string memory referralCode,
|
|
441
496
|
uint256 membershipPrice
|
|
442
|
-
) internal
|
|
497
|
+
) internal {
|
|
443
498
|
if (bytes(referralCode).length != 0) {
|
|
444
499
|
Referral memory referral = _referralInfo(referralCode);
|
|
445
500
|
|
|
446
|
-
if (referral.recipient == address(0) || referral.basisPoints == 0) return
|
|
501
|
+
if (referral.recipient == address(0) || referral.basisPoints == 0) return;
|
|
447
502
|
|
|
448
|
-
referralFee = BasisPoints.calculate(membershipPrice, referral.basisPoints);
|
|
503
|
+
uint256 referralFee = BasisPoints.calculate(membershipPrice, referral.basisPoints);
|
|
449
504
|
|
|
450
505
|
CurrencyTransfer.transferCurrency(
|
|
451
|
-
|
|
452
|
-
|
|
506
|
+
currency,
|
|
507
|
+
address(this),
|
|
453
508
|
referral.recipient,
|
|
454
509
|
referralFee
|
|
455
510
|
);
|
|
456
511
|
} else if (userReferral != address(0)) {
|
|
457
|
-
if (userReferral ==
|
|
512
|
+
if (userReferral == sender) return;
|
|
458
513
|
|
|
459
|
-
referralFee = BasisPoints.calculate(membershipPrice, _defaultBpsFee());
|
|
514
|
+
uint256 referralFee = BasisPoints.calculate(membershipPrice, _defaultBpsFee());
|
|
460
515
|
|
|
461
|
-
CurrencyTransfer.transferCurrency(
|
|
462
|
-
_getMembershipCurrency(),
|
|
463
|
-
payer,
|
|
464
|
-
userReferral,
|
|
465
|
-
referralFee
|
|
466
|
-
);
|
|
516
|
+
CurrencyTransfer.transferCurrency(currency, address(this), userReferral, referralFee);
|
|
467
517
|
}
|
|
468
518
|
}
|
|
469
519
|
|
|
470
|
-
/// @notice
|
|
471
|
-
/// @param
|
|
520
|
+
/// @notice Pays the partner fee if applicable
|
|
521
|
+
/// @param currency The currency to pay in
|
|
472
522
|
/// @param partner The address of the partner
|
|
473
523
|
/// @param membershipPrice The price of the membership
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
address payer,
|
|
477
|
-
address partner,
|
|
478
|
-
uint256 membershipPrice
|
|
479
|
-
) internal returns (uint256 partnerFee) {
|
|
480
|
-
if (partner == address(0)) return 0;
|
|
524
|
+
function _payPartnerFee(address currency, address partner, uint256 membershipPrice) internal {
|
|
525
|
+
if (partner == address(0)) return;
|
|
481
526
|
|
|
482
527
|
Partner memory partnerInfo = IPartnerRegistry(_getSpaceFactory()).partnerInfo(partner);
|
|
483
528
|
|
|
484
|
-
if (partnerInfo.fee == 0) return
|
|
529
|
+
if (partnerInfo.fee == 0) return;
|
|
485
530
|
|
|
486
|
-
partnerFee = BasisPoints.calculate(membershipPrice, partnerInfo.fee);
|
|
531
|
+
uint256 partnerFee = BasisPoints.calculate(membershipPrice, partnerInfo.fee);
|
|
487
532
|
|
|
488
533
|
CurrencyTransfer.transferCurrency(
|
|
489
|
-
|
|
490
|
-
|
|
534
|
+
currency,
|
|
535
|
+
address(this),
|
|
491
536
|
partnerInfo.recipient,
|
|
492
537
|
partnerFee
|
|
493
538
|
);
|
|
@@ -499,12 +544,13 @@ abstract contract MembershipJoin is
|
|
|
499
544
|
|
|
500
545
|
uint256 duration = _getMembershipDuration();
|
|
501
546
|
uint256 basePrice = _getMembershipRenewalPrice(tokenId, _totalSupply());
|
|
547
|
+
address currency = _getMembershipCurrency();
|
|
502
548
|
|
|
503
549
|
// Handle free renewal
|
|
504
550
|
if (basePrice == 0) {
|
|
505
|
-
// Refund any ETH sent
|
|
551
|
+
// Refund any ETH sent (regardless of membership currency)
|
|
506
552
|
CurrencyTransfer.transferCurrency(
|
|
507
|
-
|
|
553
|
+
CurrencyTransfer.NATIVE_TOKEN,
|
|
508
554
|
address(this),
|
|
509
555
|
payer,
|
|
510
556
|
msg.value
|
|
@@ -515,26 +561,25 @@ abstract contract MembershipJoin is
|
|
|
515
561
|
|
|
516
562
|
(uint256 totalRequired, ) = _getTotalMembershipPayment(basePrice);
|
|
517
563
|
|
|
518
|
-
if (
|
|
564
|
+
if (currency == CurrencyTransfer.NATIVE_TOKEN) {
|
|
565
|
+
// ETH payment: validate msg.value
|
|
566
|
+
if (totalRequired > msg.value) Membership__InvalidPayment.selector.revertWith();
|
|
519
567
|
|
|
520
|
-
|
|
521
|
-
uint256 protocolFee = _collectProtocolFee(payer, basePrice);
|
|
568
|
+
_payProtocolFee(currency, basePrice);
|
|
522
569
|
|
|
523
|
-
|
|
524
|
-
|
|
570
|
+
// Handle excess payment
|
|
571
|
+
uint256 excess = msg.value - totalRequired;
|
|
572
|
+
if (excess > 0) {
|
|
573
|
+
CurrencyTransfer.transferCurrency(currency, address(this), payer, excess);
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
// ERC20 payment: reject any ETH sent
|
|
577
|
+
if (msg.value != 0) revert Membership__UnexpectedValue();
|
|
525
578
|
|
|
526
|
-
|
|
527
|
-
|
|
579
|
+
// Transfer ERC20 from payer to contract
|
|
580
|
+
_transferIn(currency, payer, totalRequired);
|
|
528
581
|
|
|
529
|
-
|
|
530
|
-
uint256 excess = msg.value - totalRequired;
|
|
531
|
-
if (excess > 0) {
|
|
532
|
-
CurrencyTransfer.transferCurrency(
|
|
533
|
-
_getMembershipCurrency(),
|
|
534
|
-
address(this),
|
|
535
|
-
payer,
|
|
536
|
-
excess
|
|
537
|
-
);
|
|
582
|
+
_payProtocolFee(currency, basePrice);
|
|
538
583
|
}
|
|
539
584
|
|
|
540
585
|
_mintMembershipPoints(receiver, totalRequired);
|
|
@@ -542,11 +587,16 @@ abstract contract MembershipJoin is
|
|
|
542
587
|
}
|
|
543
588
|
|
|
544
589
|
/// @notice Mints points to a member based on their paid amount
|
|
545
|
-
/// @dev
|
|
590
|
+
/// @dev Only mints points for ETH payments. ERC20 support requires oracle integration.
|
|
546
591
|
/// @param receiver The address receiving the points
|
|
547
592
|
/// @param paidAmount The amount paid for membership
|
|
548
593
|
function _mintMembershipPoints(address receiver, uint256 paidAmount) internal {
|
|
549
|
-
//
|
|
594
|
+
// No points for free memberships
|
|
595
|
+
if (paidAmount == 0) return;
|
|
596
|
+
|
|
597
|
+
// TODO: Add ERC20 support - requires price oracle to convert token amount to points
|
|
598
|
+
if (_getMembershipCurrency() != CurrencyTransfer.NATIVE_TOKEN) return;
|
|
599
|
+
|
|
550
600
|
address airdropDiamond = _getAirdropDiamond();
|
|
551
601
|
uint256 points = _getPoints(
|
|
552
602
|
airdropDiamond,
|
|
@@ -556,4 +606,21 @@ abstract contract MembershipJoin is
|
|
|
556
606
|
_mintPoints(airdropDiamond, receiver, points);
|
|
557
607
|
_mintPoints(airdropDiamond, _owner(), points);
|
|
558
608
|
}
|
|
609
|
+
|
|
610
|
+
/// @notice Transfers tokens from an address to this contract
|
|
611
|
+
/// @param currency The currency address (NATIVE_TOKEN for ETH, or ERC20 address)
|
|
612
|
+
/// @param from The address to transfer from
|
|
613
|
+
/// @param amount The amount to transfer
|
|
614
|
+
function _transferIn(address currency, address from, uint256 amount) internal {
|
|
615
|
+
if (currency == CurrencyTransfer.NATIVE_TOKEN) return;
|
|
616
|
+
|
|
617
|
+
// Handle ERC20 tokens with fee-on-transfer check
|
|
618
|
+
uint256 balanceBefore = currency.balanceOf(address(this));
|
|
619
|
+
currency.safeTransferFrom(from, address(this), amount);
|
|
620
|
+
uint256 balanceAfter = currency.balanceOf(address(this));
|
|
621
|
+
|
|
622
|
+
if (balanceAfter - balanceBefore < amount) {
|
|
623
|
+
Membership__InsufficientPayment.selector.revertWith();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
559
626
|
}
|
|
@@ -11,8 +11,9 @@ interface ITreasury {
|
|
|
11
11
|
/// @notice Withdraw funds from the treasury to a specified account
|
|
12
12
|
/// @dev Can only be called by the owner of the contract. Will revert if account is zero address
|
|
13
13
|
/// or if balance is 0
|
|
14
|
+
/// @param currency The currency to withdraw (NATIVE_TOKEN for ETH, or ERC20 address)
|
|
14
15
|
/// @param account The address to withdraw funds to
|
|
15
|
-
function withdraw(address account) external;
|
|
16
|
+
function withdraw(address currency, address account) external;
|
|
16
17
|
|
|
17
18
|
/// @notice Handle the receipt of a single ERC721 token
|
|
18
19
|
/// @dev Implements the IERC721Receiver interface to safely receive ERC721 tokens
|