@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
|
@@ -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
|
|
@@ -29,7 +31,18 @@ import {MembershipBase} from "../MembershipBase.sol";
|
|
|
29
31
|
/// @title MembershipJoin
|
|
30
32
|
/// @notice Handles the logic for joining a space, including entitlement checks and payment
|
|
31
33
|
/// processing
|
|
32
|
-
/// @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
|
|
33
46
|
abstract contract MembershipJoin is
|
|
34
47
|
IRolesBase,
|
|
35
48
|
IPartnerRegistryBase,
|
|
@@ -44,6 +57,7 @@ abstract contract MembershipJoin is
|
|
|
44
57
|
ERC721ABase
|
|
45
58
|
{
|
|
46
59
|
using CustomRevert for bytes4;
|
|
60
|
+
using SafeTransferLib for address;
|
|
47
61
|
|
|
48
62
|
/// @notice Constant representing the permission to join a space
|
|
49
63
|
bytes32 internal constant JOIN_SPACE = bytes32(abi.encodePacked(Permissions.JoinSpace));
|
|
@@ -90,6 +104,10 @@ abstract contract MembershipJoin is
|
|
|
90
104
|
(joinDetails.amountDue, joinDetails.shouldCharge) = (totalRequired, true);
|
|
91
105
|
}
|
|
92
106
|
|
|
107
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
108
|
+
/* JOIN */
|
|
109
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
110
|
+
|
|
93
111
|
/// @notice Handles the process of joining a space
|
|
94
112
|
/// @param receiver The address that will receive the membership token
|
|
95
113
|
function _joinSpace(address receiver) internal {
|
|
@@ -97,14 +115,14 @@ abstract contract MembershipJoin is
|
|
|
97
115
|
|
|
98
116
|
PricingDetails memory joinDetails = _getPricingDetails();
|
|
99
117
|
|
|
100
|
-
// Validate payment
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
118
|
+
// Validate and capture payment (handles both free and paid memberships)
|
|
119
|
+
address currency = _getMembershipCurrency();
|
|
120
|
+
uint256 capturedAmount = _validateAndCapturePayment(currency, joinDetails.amountDue);
|
|
104
121
|
|
|
105
122
|
bytes32 transactionId = _registerTransaction(
|
|
106
123
|
receiver,
|
|
107
|
-
_encodeJoinSpaceData(JOIN_SPACE_SELECTOR, msg.sender, receiver, "")
|
|
124
|
+
_encodeJoinSpaceData(JOIN_SPACE_SELECTOR, msg.sender, receiver, ""),
|
|
125
|
+
capturedAmount
|
|
108
126
|
);
|
|
109
127
|
|
|
110
128
|
(bool isEntitled, bool isCrosschainPending) = _checkEntitlement(
|
|
@@ -116,7 +134,11 @@ abstract contract MembershipJoin is
|
|
|
116
134
|
|
|
117
135
|
if (!isCrosschainPending) {
|
|
118
136
|
if (isEntitled) {
|
|
119
|
-
if (joinDetails.shouldCharge)
|
|
137
|
+
if (!joinDetails.shouldCharge) {
|
|
138
|
+
_afterChargeForJoinSpace(transactionId, receiver, 0);
|
|
139
|
+
} else {
|
|
140
|
+
_chargeForJoinSpace(transactionId, joinDetails);
|
|
141
|
+
}
|
|
120
142
|
_refundBalance(transactionId, receiver);
|
|
121
143
|
_issueToken(receiver);
|
|
122
144
|
} else {
|
|
@@ -133,10 +155,9 @@ abstract contract MembershipJoin is
|
|
|
133
155
|
|
|
134
156
|
PricingDetails memory joinDetails = _getPricingDetails();
|
|
135
157
|
|
|
136
|
-
// Validate payment
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
158
|
+
// Validate and capture payment (handles both free and paid memberships)
|
|
159
|
+
address currency = _getMembershipCurrency();
|
|
160
|
+
uint256 capturedAmount = _validateAndCapturePayment(currency, joinDetails.amountDue);
|
|
140
161
|
|
|
141
162
|
_validateUserReferral(receiver, referral);
|
|
142
163
|
|
|
@@ -146,7 +167,8 @@ abstract contract MembershipJoin is
|
|
|
146
167
|
|
|
147
168
|
bytes32 transactionId = _registerTransaction(
|
|
148
169
|
receiver,
|
|
149
|
-
_encodeJoinSpaceData(selector, msg.sender, receiver, referralData)
|
|
170
|
+
_encodeJoinSpaceData(selector, msg.sender, receiver, referralData),
|
|
171
|
+
capturedAmount
|
|
150
172
|
);
|
|
151
173
|
|
|
152
174
|
(bool isEntitled, bool isCrosschainPending) = _checkEntitlement(
|
|
@@ -158,9 +180,11 @@ abstract contract MembershipJoin is
|
|
|
158
180
|
|
|
159
181
|
if (!isCrosschainPending) {
|
|
160
182
|
if (isEntitled) {
|
|
161
|
-
if (joinDetails.shouldCharge)
|
|
183
|
+
if (!joinDetails.shouldCharge) {
|
|
184
|
+
_afterChargeForJoinSpace(transactionId, receiver, 0);
|
|
185
|
+
} else {
|
|
162
186
|
_chargeForJoinSpaceWithReferral(transactionId, joinDetails);
|
|
163
|
-
|
|
187
|
+
}
|
|
164
188
|
_refundBalance(transactionId, receiver);
|
|
165
189
|
_issueToken(receiver);
|
|
166
190
|
} else {
|
|
@@ -175,12 +199,46 @@ abstract contract MembershipJoin is
|
|
|
175
199
|
emit MembershipTokenRejected(receiver);
|
|
176
200
|
}
|
|
177
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
|
+
|
|
178
232
|
function _validateUserReferral(address receiver, ReferralTypes memory referral) internal view {
|
|
179
233
|
if (referral.userReferral == receiver || referral.userReferral == msg.sender) {
|
|
180
234
|
Membership__InvalidAddress.selector.revertWith();
|
|
181
235
|
}
|
|
182
236
|
}
|
|
183
237
|
|
|
238
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
239
|
+
/* ENTITLEMENT */
|
|
240
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
241
|
+
|
|
184
242
|
/// @notice Checks if a user is entitled to join the space and handles the entitlement process
|
|
185
243
|
/// @dev This function checks both local and crosschain entitlements
|
|
186
244
|
/// @param receiver The address of the user trying to join the space
|
|
@@ -291,7 +349,7 @@ abstract contract MembershipJoin is
|
|
|
291
349
|
bytes32 transactionId,
|
|
292
350
|
PricingDetails memory joinDetails
|
|
293
351
|
) internal {
|
|
294
|
-
(bytes4 selector,
|
|
352
|
+
(bytes4 selector, , address receiver, ) = abi.decode(
|
|
295
353
|
_getCapturedData(transactionId),
|
|
296
354
|
(bytes4, address, address, bytes)
|
|
297
355
|
);
|
|
@@ -300,16 +358,9 @@ abstract contract MembershipJoin is
|
|
|
300
358
|
Membership__InvalidTransactionType.selector.revertWith();
|
|
301
359
|
}
|
|
302
360
|
|
|
303
|
-
|
|
304
|
-
uint256 ownerProceeds = joinDetails.amountDue - protocolFee;
|
|
361
|
+
_payProtocolFee(_getMembershipCurrency(), joinDetails.basePrice);
|
|
305
362
|
|
|
306
|
-
_afterChargeForJoinSpace(
|
|
307
|
-
transactionId,
|
|
308
|
-
sender,
|
|
309
|
-
receiver,
|
|
310
|
-
joinDetails.amountDue,
|
|
311
|
-
ownerProceeds
|
|
312
|
-
);
|
|
363
|
+
_afterChargeForJoinSpace(transactionId, receiver, joinDetails.amountDue);
|
|
313
364
|
}
|
|
314
365
|
|
|
315
366
|
/// @notice Processes the charge for joining a space with referral
|
|
@@ -330,45 +381,31 @@ abstract contract MembershipJoin is
|
|
|
330
381
|
|
|
331
382
|
ReferralTypes memory referral = abi.decode(referralData, (ReferralTypes));
|
|
332
383
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
sender,
|
|
339
|
-
referral.partner,
|
|
340
|
-
joinDetails.basePrice
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
uint256 referralFee = _collectReferralCodeFee(
|
|
344
|
-
sender,
|
|
345
|
-
referral.userReferral,
|
|
346
|
-
referral.referralCode,
|
|
347
|
-
joinDetails.basePrice
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
ownerProceeds = joinDetails.amountDue - protocolFee - partnerFee - referralFee;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
_afterChargeForJoinSpace(
|
|
354
|
-
transactionId,
|
|
384
|
+
address currency = _getMembershipCurrency();
|
|
385
|
+
_payProtocolFee(currency, joinDetails.basePrice);
|
|
386
|
+
_payPartnerFee(currency, referral.partner, joinDetails.basePrice);
|
|
387
|
+
_payReferralFee(
|
|
388
|
+
currency,
|
|
355
389
|
sender,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
390
|
+
referral.userReferral,
|
|
391
|
+
referral.referralCode,
|
|
392
|
+
joinDetails.basePrice
|
|
359
393
|
);
|
|
394
|
+
|
|
395
|
+
_afterChargeForJoinSpace(transactionId, receiver, joinDetails.amountDue);
|
|
360
396
|
}
|
|
361
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)
|
|
362
404
|
function _afterChargeForJoinSpace(
|
|
363
405
|
bytes32 transactionId,
|
|
364
|
-
address payer,
|
|
365
406
|
address receiver,
|
|
366
|
-
uint256 paymentRequired
|
|
367
|
-
uint256 ownerProceeds
|
|
407
|
+
uint256 paymentRequired
|
|
368
408
|
) internal {
|
|
369
|
-
// account for owner's proceeds
|
|
370
|
-
if (ownerProceeds != 0) _transferIn(payer, ownerProceeds);
|
|
371
|
-
|
|
372
409
|
_releaseCapturedValue(transactionId, paymentRequired);
|
|
373
410
|
_deleteCapturedData(transactionId);
|
|
374
411
|
|
|
@@ -404,81 +441,98 @@ abstract contract MembershipJoin is
|
|
|
404
441
|
}
|
|
405
442
|
}
|
|
406
443
|
|
|
407
|
-
/// @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.
|
|
408
448
|
/// @param transactionId The unique identifier for this join transaction
|
|
409
|
-
/// @param
|
|
410
|
-
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 {
|
|
411
451
|
uint256 userValue = _getCapturedValue(transactionId);
|
|
412
452
|
if (userValue > 0) {
|
|
413
453
|
_releaseCapturedValue(transactionId, userValue);
|
|
414
454
|
CurrencyTransfer.transferCurrency(
|
|
415
455
|
_getMembershipCurrency(),
|
|
416
456
|
address(this),
|
|
417
|
-
|
|
457
|
+
receiver,
|
|
418
458
|
userValue
|
|
419
459
|
);
|
|
420
460
|
}
|
|
421
461
|
}
|
|
422
462
|
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
425
489
|
/// @param referralCode The referral code used
|
|
426
490
|
/// @param membershipPrice The price of the membership
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
address
|
|
491
|
+
function _payReferralFee(
|
|
492
|
+
address currency,
|
|
493
|
+
address sender,
|
|
430
494
|
address userReferral,
|
|
431
495
|
string memory referralCode,
|
|
432
496
|
uint256 membershipPrice
|
|
433
|
-
) internal
|
|
497
|
+
) internal {
|
|
434
498
|
if (bytes(referralCode).length != 0) {
|
|
435
499
|
Referral memory referral = _referralInfo(referralCode);
|
|
436
500
|
|
|
437
|
-
if (referral.recipient == address(0) || referral.basisPoints == 0) return
|
|
501
|
+
if (referral.recipient == address(0) || referral.basisPoints == 0) return;
|
|
438
502
|
|
|
439
|
-
referralFee = BasisPoints.calculate(membershipPrice, referral.basisPoints);
|
|
503
|
+
uint256 referralFee = BasisPoints.calculate(membershipPrice, referral.basisPoints);
|
|
440
504
|
|
|
441
505
|
CurrencyTransfer.transferCurrency(
|
|
442
|
-
|
|
443
|
-
|
|
506
|
+
currency,
|
|
507
|
+
address(this),
|
|
444
508
|
referral.recipient,
|
|
445
509
|
referralFee
|
|
446
510
|
);
|
|
447
511
|
} else if (userReferral != address(0)) {
|
|
448
|
-
if (userReferral ==
|
|
512
|
+
if (userReferral == sender) return;
|
|
449
513
|
|
|
450
|
-
referralFee = BasisPoints.calculate(membershipPrice, _defaultBpsFee());
|
|
514
|
+
uint256 referralFee = BasisPoints.calculate(membershipPrice, _defaultBpsFee());
|
|
451
515
|
|
|
452
|
-
CurrencyTransfer.transferCurrency(
|
|
453
|
-
_getMembershipCurrency(),
|
|
454
|
-
payer,
|
|
455
|
-
userReferral,
|
|
456
|
-
referralFee
|
|
457
|
-
);
|
|
516
|
+
CurrencyTransfer.transferCurrency(currency, address(this), userReferral, referralFee);
|
|
458
517
|
}
|
|
459
518
|
}
|
|
460
519
|
|
|
461
|
-
/// @notice
|
|
462
|
-
/// @param
|
|
520
|
+
/// @notice Pays the partner fee if applicable
|
|
521
|
+
/// @param currency The currency to pay in
|
|
463
522
|
/// @param partner The address of the partner
|
|
464
523
|
/// @param membershipPrice The price of the membership
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
address payer,
|
|
468
|
-
address partner,
|
|
469
|
-
uint256 membershipPrice
|
|
470
|
-
) internal returns (uint256 partnerFee) {
|
|
471
|
-
if (partner == address(0)) return 0;
|
|
524
|
+
function _payPartnerFee(address currency, address partner, uint256 membershipPrice) internal {
|
|
525
|
+
if (partner == address(0)) return;
|
|
472
526
|
|
|
473
527
|
Partner memory partnerInfo = IPartnerRegistry(_getSpaceFactory()).partnerInfo(partner);
|
|
474
528
|
|
|
475
|
-
if (partnerInfo.fee == 0) return
|
|
529
|
+
if (partnerInfo.fee == 0) return;
|
|
476
530
|
|
|
477
|
-
partnerFee = BasisPoints.calculate(membershipPrice, partnerInfo.fee);
|
|
531
|
+
uint256 partnerFee = BasisPoints.calculate(membershipPrice, partnerInfo.fee);
|
|
478
532
|
|
|
479
533
|
CurrencyTransfer.transferCurrency(
|
|
480
|
-
|
|
481
|
-
|
|
534
|
+
currency,
|
|
535
|
+
address(this),
|
|
482
536
|
partnerInfo.recipient,
|
|
483
537
|
partnerFee
|
|
484
538
|
);
|
|
@@ -490,12 +544,13 @@ abstract contract MembershipJoin is
|
|
|
490
544
|
|
|
491
545
|
uint256 duration = _getMembershipDuration();
|
|
492
546
|
uint256 basePrice = _getMembershipRenewalPrice(tokenId, _totalSupply());
|
|
547
|
+
address currency = _getMembershipCurrency();
|
|
493
548
|
|
|
494
549
|
// Handle free renewal
|
|
495
550
|
if (basePrice == 0) {
|
|
496
|
-
// Refund any ETH sent
|
|
551
|
+
// Refund any ETH sent (regardless of membership currency)
|
|
497
552
|
CurrencyTransfer.transferCurrency(
|
|
498
|
-
|
|
553
|
+
CurrencyTransfer.NATIVE_TOKEN,
|
|
499
554
|
address(this),
|
|
500
555
|
payer,
|
|
501
556
|
msg.value
|
|
@@ -506,26 +561,25 @@ abstract contract MembershipJoin is
|
|
|
506
561
|
|
|
507
562
|
(uint256 totalRequired, ) = _getTotalMembershipPayment(basePrice);
|
|
508
563
|
|
|
509
|
-
if (
|
|
564
|
+
if (currency == CurrencyTransfer.NATIVE_TOKEN) {
|
|
565
|
+
// ETH payment: validate msg.value
|
|
566
|
+
if (totalRequired > msg.value) Membership__InvalidPayment.selector.revertWith();
|
|
510
567
|
|
|
511
|
-
|
|
512
|
-
uint256 protocolFee = _collectProtocolFee(payer, basePrice);
|
|
568
|
+
_payProtocolFee(currency, basePrice);
|
|
513
569
|
|
|
514
|
-
|
|
515
|
-
|
|
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();
|
|
516
578
|
|
|
517
|
-
|
|
518
|
-
|
|
579
|
+
// Transfer ERC20 from payer to contract
|
|
580
|
+
_transferIn(currency, payer, totalRequired);
|
|
519
581
|
|
|
520
|
-
|
|
521
|
-
uint256 excess = msg.value - totalRequired;
|
|
522
|
-
if (excess > 0) {
|
|
523
|
-
CurrencyTransfer.transferCurrency(
|
|
524
|
-
_getMembershipCurrency(),
|
|
525
|
-
address(this),
|
|
526
|
-
payer,
|
|
527
|
-
excess
|
|
528
|
-
);
|
|
582
|
+
_payProtocolFee(currency, basePrice);
|
|
529
583
|
}
|
|
530
584
|
|
|
531
585
|
_mintMembershipPoints(receiver, totalRequired);
|
|
@@ -533,11 +587,16 @@ abstract contract MembershipJoin is
|
|
|
533
587
|
}
|
|
534
588
|
|
|
535
589
|
/// @notice Mints points to a member based on their paid amount
|
|
536
|
-
/// @dev
|
|
590
|
+
/// @dev Only mints points for ETH payments. ERC20 support requires oracle integration.
|
|
537
591
|
/// @param receiver The address receiving the points
|
|
538
592
|
/// @param paidAmount The amount paid for membership
|
|
539
593
|
function _mintMembershipPoints(address receiver, uint256 paidAmount) internal {
|
|
540
|
-
//
|
|
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
|
+
|
|
541
600
|
address airdropDiamond = _getAirdropDiamond();
|
|
542
601
|
uint256 points = _getPoints(
|
|
543
602
|
airdropDiamond,
|
|
@@ -547,4 +606,21 @@ abstract contract MembershipJoin is
|
|
|
547
606
|
_mintPoints(airdropDiamond, receiver, points);
|
|
548
607
|
_mintPoints(airdropDiamond, _owner(), points);
|
|
549
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
|
+
}
|
|
550
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
|
|
@@ -2,53 +2,50 @@
|
|
|
2
2
|
pragma solidity ^0.8.23;
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
|
-
|
|
6
5
|
import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
|
|
7
|
-
import {IMembershipBase} from "
|
|
8
|
-
import {ITreasury} from "
|
|
6
|
+
import {IMembershipBase} from "../membership/IMembership.sol";
|
|
7
|
+
import {ITreasury} from "./ITreasury.sol";
|
|
9
8
|
|
|
10
9
|
// libraries
|
|
11
|
-
import {
|
|
10
|
+
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
|
|
11
|
+
import {CurrencyTransfer} from "../../../utils/libraries/CurrencyTransfer.sol";
|
|
12
|
+
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
12
13
|
|
|
13
14
|
// contracts
|
|
14
|
-
|
|
15
15
|
import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
|
|
16
16
|
import {TokenOwnableBase} from "@towns-protocol/diamond/src/facets/ownable/token/TokenOwnableBase.sol";
|
|
17
|
-
import {MembershipStorage} from "src/spaces/facets/membership/MembershipStorage.sol";
|
|
18
|
-
import {CustomRevert} from "src/utils/libraries/CustomRevert.sol";
|
|
19
17
|
import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol";
|
|
20
18
|
|
|
21
19
|
contract Treasury is TokenOwnableBase, ReentrancyGuard, Facet, ITreasury {
|
|
20
|
+
using CustomRevert for bytes4;
|
|
21
|
+
using SafeTransferLib for address;
|
|
22
|
+
|
|
22
23
|
function __Treasury_init() external onlyInitializing {
|
|
23
24
|
_addInterface(type(IERC1155Receiver).interfaceId);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
function withdraw(address account) external onlyOwner nonReentrant {
|
|
28
|
-
if (account == address(0))
|
|
29
|
-
CustomRevert.revertWith(IMembershipBase.Membership__InvalidAddress.selector);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// get the balance
|
|
33
|
-
uint256 balance = address(this).balance;
|
|
27
|
+
/// @inheritdoc ITreasury
|
|
28
|
+
function withdraw(address currency, address account) external onlyOwner nonReentrant {
|
|
29
|
+
if (account == address(0)) IMembershipBase.Membership__InvalidAddress.selector.revertWith();
|
|
34
30
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
// Get balance based on currency type
|
|
32
|
+
uint256 balance = currency == CurrencyTransfer.NATIVE_TOKEN
|
|
33
|
+
? address(this).balance
|
|
34
|
+
: currency.balanceOf(address(this));
|
|
39
35
|
|
|
40
|
-
|
|
36
|
+
// Verify the balance is not 0
|
|
37
|
+
if (balance == 0) IMembershipBase.Membership__InsufficientPayment.selector.revertWith();
|
|
41
38
|
|
|
42
39
|
CurrencyTransfer.transferCurrency(currency, address(this), account, balance);
|
|
43
40
|
|
|
44
|
-
emit IMembershipBase.MembershipWithdrawal(account, balance);
|
|
41
|
+
emit IMembershipBase.MembershipWithdrawal(currency, account, balance);
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
48
45
|
/* Hooks */
|
|
49
46
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
50
47
|
|
|
51
|
-
|
|
48
|
+
/// @inheritdoc ITreasury
|
|
52
49
|
function onERC721Received(
|
|
53
50
|
address,
|
|
54
51
|
address,
|
|
@@ -58,7 +55,7 @@ contract Treasury is TokenOwnableBase, ReentrancyGuard, Facet, ITreasury {
|
|
|
58
55
|
return this.onERC721Received.selector;
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
/// @inheritdoc ITreasury
|
|
62
59
|
function onERC1155Received(
|
|
63
60
|
address,
|
|
64
61
|
address,
|
|
@@ -69,7 +66,7 @@ contract Treasury is TokenOwnableBase, ReentrancyGuard, Facet, ITreasury {
|
|
|
69
66
|
return this.onERC1155Received.selector;
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
|
|
69
|
+
/// @inheritdoc ITreasury
|
|
73
70
|
function onERC1155BatchReceived(
|
|
74
71
|
address,
|
|
75
72
|
address,
|
|
@@ -10,7 +10,6 @@ import {MembershipJoin} from "../membership/join/MembershipJoin.sol";
|
|
|
10
10
|
|
|
11
11
|
/// @title SpaceEntitlementGated
|
|
12
12
|
/// @notice Handles entitlement-gated access to spaces and membership token issuance
|
|
13
|
-
/// @dev Inherits from ISpaceEntitlementGatedBase, MembershipJoin, and EntitlementGated
|
|
14
13
|
contract SpaceEntitlementGated is MembershipJoin, EntitlementGated {
|
|
15
14
|
/// @notice Processes the result of an entitlement check
|
|
16
15
|
/// @dev This function is called when the result of an entitlement check is posted
|
|
@@ -32,7 +31,9 @@ contract SpaceEntitlementGated is MembershipJoin, EntitlementGated {
|
|
|
32
31
|
if (result == NodeVoteStatus.PASSED) {
|
|
33
32
|
PricingDetails memory joinDetails = _getPricingDetails();
|
|
34
33
|
|
|
35
|
-
if (joinDetails.shouldCharge) {
|
|
34
|
+
if (!joinDetails.shouldCharge) {
|
|
35
|
+
_afterChargeForJoinSpace(transactionId, receiver, 0);
|
|
36
|
+
} else {
|
|
36
37
|
uint256 payment = _getCapturedValue(transactionId);
|
|
37
38
|
|
|
38
39
|
if (payment < joinDetails.amountDue) {
|