@towns-protocol/contracts 0.0.451 → 0.0.453
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 +23 -6
- package/package.json +3 -3
- package/src/apps/modules/subscription/ISubscriptionModule.sol +40 -43
- package/src/apps/modules/subscription/SubscriptionModuleBase.sol +41 -16
- package/src/apps/modules/subscription/SubscriptionModuleFacet.sol +113 -105
- package/src/apps/modules/subscription/SubscriptionModuleStorage.sol +1 -0
- package/src/base/registry/facets/checker/EntitlementChecker.sol +43 -17
- package/src/spaces/facets/ProtocolFeeLib.sol +43 -0
- package/src/spaces/facets/membership/IMembership.sol +6 -1
- package/src/spaces/facets/membership/MembershipFacet.sol +16 -7
- package/src/spaces/facets/membership/join/MembershipJoin.sol +34 -23
- package/src/spaces/facets/tipping/TippingBase.sol +10 -26
- package/src/utils/libraries/CurrencyTransfer.sol +6 -2
|
@@ -19,8 +19,12 @@ MembershipFacet (external interface)
|
|
|
19
19
|
├─ PointsBase (rewards points)
|
|
20
20
|
└─ ERC721ABase (NFT implementation)
|
|
21
21
|
|
|
22
|
-
SpaceEntitlementGated (
|
|
22
|
+
SpaceEntitlementGated (xchain/SpaceEntitlementGated.sol)
|
|
23
23
|
└─ Overrides _onEntitlementCheckResultPosted()
|
|
24
|
+
|
|
25
|
+
ProtocolFeeLib (ProtocolFeeLib.sol)
|
|
26
|
+
└─ Wraps FeeManager.chargeFee with ERC20 approval handling
|
|
27
|
+
└─ Used by: MembershipJoin, TippingBase
|
|
24
28
|
```
|
|
25
29
|
|
|
26
30
|
### Storage Architecture
|
|
@@ -67,6 +71,8 @@ Memberships can be priced in:
|
|
|
67
71
|
|
|
68
72
|
Currency validation happens in `_setMembershipCurrency()` which requires the token to have an enabled fee configuration in FeeManager.
|
|
69
73
|
|
|
74
|
+
**Note**: Rewards points (`_mintMembershipPoints`) are only minted for native ETH payments. ERC20 payments require an oracle for point calculation.
|
|
75
|
+
|
|
70
76
|
## Core Flow: Join → Entitlement Check → Token
|
|
71
77
|
|
|
72
78
|
### Happy Path (Local Entitlement)
|
|
@@ -84,10 +90,10 @@ User: joinSpace(receiver) + payment
|
|
|
84
90
|
│ └─ ✓ User has local entitlement → PASS
|
|
85
91
|
│
|
|
86
92
|
├─ Charge for Join
|
|
87
|
-
│ ├─ Protocol fee → Platform
|
|
93
|
+
│ ├─ Protocol fee → Platform (via FeeManager)
|
|
88
94
|
│ ├─ Partner fee → Partner (if any)
|
|
89
95
|
│ ├─ Referral fee → Referrer (if any)
|
|
90
|
-
│ └─ Base price
|
|
96
|
+
│ └─ Base price remains in contract as revenue
|
|
91
97
|
│
|
|
92
98
|
├─ Refund Excess (if overpaid)
|
|
93
99
|
│
|
|
@@ -218,10 +224,21 @@ function _releaseCapturedValue(bytes32 transactionId, uint256 amount) internal
|
|
|
218
224
|
### Fee Distribution
|
|
219
225
|
|
|
220
226
|
```solidity
|
|
227
|
+
// MembershipJoin.sol - pays protocol fee via FeeManager
|
|
228
|
+
function _payProtocolFee(address currency, uint256 basePrice, uint256 expectedFee) internal
|
|
229
|
+
|
|
230
|
+
// ProtocolFeeLib.sol - shared fee charging logic
|
|
231
|
+
function charge(
|
|
232
|
+
address spaceFactory, // FeeManager address
|
|
233
|
+
bytes32 feeType, // fee type identifier
|
|
234
|
+
address user, // payer (msg.sender)
|
|
235
|
+
address currency, // NATIVE_TOKEN or ERC20
|
|
236
|
+
uint256 amount, // base price for fee calculation
|
|
237
|
+
uint256 expectedFee // pre-calculated fee amount
|
|
238
|
+
) internal returns (uint256 protocolFee)
|
|
239
|
+
|
|
221
240
|
// MembershipBase.sol
|
|
222
|
-
function
|
|
223
|
-
internal returns (uint256 protocolFee)
|
|
224
|
-
function _transferIn(address from, uint256 amount) internal returns (uint256)
|
|
241
|
+
function _transferIn(address currency, address from, uint256 amount) internal returns (uint256)
|
|
225
242
|
```
|
|
226
243
|
|
|
227
244
|
### Entitlement Checking
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towns-protocol/contracts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.453",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"clean": "forge clean",
|
|
6
6
|
"compile": "forge build",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@layerzerolabs/oapp-evm": "^0.3.2",
|
|
34
34
|
"@openzeppelin/merkle-tree": "^1.0.8",
|
|
35
35
|
"@prb/test": "^0.6.4",
|
|
36
|
-
"@towns-protocol/prettier-config": "^0.0.
|
|
36
|
+
"@towns-protocol/prettier-config": "^0.0.453",
|
|
37
37
|
"@wagmi/cli": "^2.2.0",
|
|
38
38
|
"forge-std": "github:foundry-rs/forge-std#v1.10.0",
|
|
39
39
|
"prettier": "^3.5.3",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "f94c8877f1ce645d78158e8367c0c05b171c9141"
|
|
54
54
|
}
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.23;
|
|
3
3
|
|
|
4
|
-
// interfaces
|
|
5
|
-
|
|
6
|
-
// libraries
|
|
7
|
-
|
|
8
|
-
// contracts
|
|
9
|
-
|
|
10
4
|
import {Subscription} from "./SubscriptionModuleStorage.sol";
|
|
11
5
|
|
|
12
6
|
interface ISubscriptionModuleBase {
|
|
13
7
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
14
|
-
/*
|
|
8
|
+
/* STRUCTS */
|
|
15
9
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
16
10
|
|
|
17
11
|
/// @notice Parameters for renewing a subscription
|
|
@@ -23,30 +17,25 @@ interface ISubscriptionModuleBase {
|
|
|
23
17
|
}
|
|
24
18
|
|
|
25
19
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
26
|
-
/*
|
|
20
|
+
/* ERRORS */
|
|
27
21
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
28
22
|
|
|
29
23
|
error SubscriptionModule__InactiveSubscription();
|
|
30
24
|
error SubscriptionModule__InvalidSpace();
|
|
31
|
-
error SubscriptionModule__RenewalNotDue();
|
|
32
|
-
error SubscriptionModule__RenewalFailed();
|
|
33
25
|
error SubscriptionModule__InvalidSender();
|
|
34
26
|
error SubscriptionModule__NotSupported();
|
|
35
27
|
error SubscriptionModule__InvalidEntityId();
|
|
36
28
|
error SubscriptionModule__InvalidCaller();
|
|
37
|
-
error SubscriptionModule__InvalidAddress();
|
|
38
29
|
error SubscriptionModule__ExceedsMaxBatchSize();
|
|
39
30
|
error SubscriptionModule__EmptyBatch();
|
|
40
31
|
error SubscriptionModule__InvalidTokenOwner();
|
|
41
|
-
error SubscriptionModule__InsufficientBalance();
|
|
42
32
|
error SubscriptionModule__ActiveSubscription();
|
|
43
33
|
error SubscriptionModule__MembershipBanned();
|
|
44
34
|
error SubscriptionModule__MembershipExpired();
|
|
45
35
|
error SubscriptionModule__SubscriptionAlreadyInstalled();
|
|
46
|
-
error SubscriptionModule__SubscriptionNotInstalled();
|
|
47
36
|
|
|
48
37
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
49
|
-
/*
|
|
38
|
+
/* EVENTS */
|
|
50
39
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
51
40
|
|
|
52
41
|
event SubscriptionConfigured(
|
|
@@ -97,13 +86,45 @@ interface ISubscriptionModuleBase {
|
|
|
97
86
|
|
|
98
87
|
interface ISubscriptionModule is ISubscriptionModuleBase {
|
|
99
88
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
100
|
-
/*
|
|
89
|
+
/* ADMIN FUNCTIONS */
|
|
90
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
91
|
+
|
|
92
|
+
/// @notice Sets the space factory
|
|
93
|
+
/// @param spaceFactory The address of the space factory
|
|
94
|
+
function setSpaceFactory(address spaceFactory) external;
|
|
95
|
+
|
|
96
|
+
/// @notice Grants an operator access to call processRenewal
|
|
97
|
+
/// @param operator The address of the operator to grant
|
|
98
|
+
function grantOperator(address operator) external;
|
|
99
|
+
|
|
100
|
+
/// @notice Revokes an operator access to call processRenewal
|
|
101
|
+
/// @param operator The address of the operator to revoke
|
|
102
|
+
function revokeOperator(address operator) external;
|
|
103
|
+
|
|
104
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
105
|
+
/* STATE-CHANGING FUNCTIONS */
|
|
101
106
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
102
107
|
|
|
103
108
|
/// @notice Processes multiple Towns membership renewals in batch
|
|
104
109
|
/// @param params The parameters for the renewals
|
|
105
110
|
function batchProcessRenewals(RenewalParams[] calldata params) external;
|
|
106
111
|
|
|
112
|
+
/// @notice Activates a subscription
|
|
113
|
+
/// @param entityId The entity ID of the subscription to activate
|
|
114
|
+
function activateSubscription(uint32 entityId) external;
|
|
115
|
+
|
|
116
|
+
/// @notice Pauses a subscription
|
|
117
|
+
/// @param entityId The entity ID of the subscription to pause
|
|
118
|
+
function pauseSubscription(uint32 entityId) external;
|
|
119
|
+
|
|
120
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
121
|
+
/* GETTERS */
|
|
122
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
123
|
+
|
|
124
|
+
/// @notice Gets the space factory
|
|
125
|
+
/// @return The address of the space factory
|
|
126
|
+
function getSpaceFactory() external view returns (address);
|
|
127
|
+
|
|
107
128
|
/// @notice Gets the subscription for an account and entity ID
|
|
108
129
|
/// @param account The address of the account to get the subscription for
|
|
109
130
|
/// @param entityId The entity ID of the subscription to get
|
|
@@ -113,19 +134,6 @@ interface ISubscriptionModule is ISubscriptionModuleBase {
|
|
|
113
134
|
uint32 entityId
|
|
114
135
|
) external view returns (Subscription memory);
|
|
115
136
|
|
|
116
|
-
/// @notice Gets the renewal buffer for a membership duration
|
|
117
|
-
/// @param duration The membership duration to get the renewal buffer for
|
|
118
|
-
/// @return The renewal buffer for the duration
|
|
119
|
-
function getRenewalBuffer(uint256 duration) external pure returns (uint256);
|
|
120
|
-
|
|
121
|
-
/// @notice Activates a subscription
|
|
122
|
-
/// @param entityId The entity ID of the subscription to activate
|
|
123
|
-
function activateSubscription(uint32 entityId) external;
|
|
124
|
-
|
|
125
|
-
/// @notice Pauses a subscription
|
|
126
|
-
/// @param entityId The entity ID of the subscription to pause
|
|
127
|
-
function pauseSubscription(uint32 entityId) external;
|
|
128
|
-
|
|
129
137
|
/// @notice Gets the entity IDs for an account
|
|
130
138
|
/// @param account The address of the account to get the entity IDs for
|
|
131
139
|
/// @return The entity IDs for the account
|
|
@@ -135,19 +143,8 @@ interface ISubscriptionModule is ISubscriptionModuleBase {
|
|
|
135
143
|
/// @param operator The address of the operator to check
|
|
136
144
|
function isOperator(address operator) external view returns (bool);
|
|
137
145
|
|
|
138
|
-
/// @notice
|
|
139
|
-
/// @param
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
/// @notice Revokes an operator access to call processRenewal
|
|
143
|
-
/// @param operator The address of the operator to revoke
|
|
144
|
-
function revokeOperator(address operator) external;
|
|
145
|
-
|
|
146
|
-
/// @notice Sets the space factory
|
|
147
|
-
/// @param spaceFactory The address of the space factory
|
|
148
|
-
function setSpaceFactory(address spaceFactory) external;
|
|
149
|
-
|
|
150
|
-
/// @notice Gets the space factory
|
|
151
|
-
/// @return The address of the space factory
|
|
152
|
-
function getSpaceFactory() external view returns (address);
|
|
146
|
+
/// @notice Gets the renewal buffer for a membership duration
|
|
147
|
+
/// @param duration The membership duration to get the renewal buffer for
|
|
148
|
+
/// @return The renewal buffer for the duration
|
|
149
|
+
function getRenewalBuffer(uint256 duration) external pure returns (uint256);
|
|
153
150
|
}
|
|
@@ -2,20 +2,22 @@
|
|
|
2
2
|
pragma solidity ^0.8.29;
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
|
-
import {IModularAccount} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol";
|
|
5
|
+
import {IModularAccount, Call} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol";
|
|
6
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
7
|
+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
6
8
|
import {IMembership} from "../../../spaces/facets/membership/IMembership.sol";
|
|
7
9
|
import {IBanning} from "../../../spaces/facets/banning/IBanning.sol";
|
|
8
|
-
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
9
10
|
import {ISubscriptionModule, ISubscriptionModuleBase} from "./ISubscriptionModule.sol";
|
|
10
11
|
|
|
11
12
|
// libraries
|
|
12
|
-
import {LibCall} from "solady/utils/LibCall.sol";
|
|
13
13
|
import {ValidationLocatorLib} from "modular-account/src/libraries/ValidationLocatorLib.sol";
|
|
14
|
+
import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
|
|
15
|
+
import {LibCall} from "solady/utils/LibCall.sol";
|
|
14
16
|
import {SafeCastLib} from "solady/utils/SafeCastLib.sol";
|
|
17
|
+
import {CurrencyTransfer} from "../../../utils/libraries/CurrencyTransfer.sol";
|
|
15
18
|
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
16
19
|
import {Validator} from "../../../utils/libraries/Validator.sol";
|
|
17
20
|
import {Subscription, SubscriptionModuleStorage} from "./SubscriptionModuleStorage.sol";
|
|
18
|
-
import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
|
|
19
21
|
|
|
20
22
|
/// @title Subscription Module Base
|
|
21
23
|
/// @notice Base contract with internal logic for subscription management
|
|
@@ -34,7 +36,8 @@ abstract contract SubscriptionModuleBase is ISubscriptionModuleBase {
|
|
|
34
36
|
NOT_OWNER,
|
|
35
37
|
RENEWAL_PRICE_CHANGED,
|
|
36
38
|
INSUFFICIENT_BALANCE,
|
|
37
|
-
DURATION_CHANGED
|
|
39
|
+
DURATION_CHANGED,
|
|
40
|
+
CURRENCY_CHANGED
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
uint256 public constant GRACE_PERIOD = 3 days;
|
|
@@ -72,6 +75,7 @@ abstract contract SubscriptionModuleBase is ISubscriptionModuleBase {
|
|
|
72
75
|
sub.lastKnownRenewalPrice = membershipFacet.getMembershipRenewalPrice(sub.tokenId);
|
|
73
76
|
sub.lastKnownExpiresAt = expiresAt;
|
|
74
77
|
sub.duration = duration;
|
|
78
|
+
sub.lastKnownCurrency = membershipFacet.getMembershipCurrency();
|
|
75
79
|
sub.nextRenewalTime = _calculateBaseRenewalTime(expiresAt, duration);
|
|
76
80
|
}
|
|
77
81
|
|
|
@@ -84,19 +88,33 @@ abstract contract SubscriptionModuleBase is ISubscriptionModuleBase {
|
|
|
84
88
|
IMembership membershipFacet,
|
|
85
89
|
uint256 actualRenewalPrice
|
|
86
90
|
) internal {
|
|
91
|
+
address currency = membershipFacet.getMembershipCurrency();
|
|
87
92
|
// Construct the renewal call to space contract
|
|
88
93
|
bytes memory renewalCall = abi.encodeCall(IMembership.renewMembership, (sub.tokenId));
|
|
89
94
|
|
|
90
95
|
// Create the data parameter for executeWithRuntimeValidation
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
bytes memory executeData;
|
|
97
|
+
if (currency == CurrencyTransfer.NATIVE_TOKEN) {
|
|
98
|
+
// ETH path: single execute with value
|
|
99
|
+
executeData = abi.encodeCall(
|
|
100
|
+
IModularAccount.execute,
|
|
101
|
+
(
|
|
102
|
+
sub.space, // target
|
|
103
|
+
actualRenewalPrice, // value
|
|
104
|
+
renewalCall // data
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
// ERC20 path: batch execute (approve + renewMembership)
|
|
109
|
+
Call[] memory calls = new Call[](2);
|
|
110
|
+
calls[0] = Call({
|
|
111
|
+
target: currency,
|
|
112
|
+
value: 0,
|
|
113
|
+
data: abi.encodeCall(IERC20.approve, (sub.space, actualRenewalPrice))
|
|
114
|
+
});
|
|
115
|
+
calls[1] = Call({target: sub.space, value: 0, data: renewalCall});
|
|
116
|
+
executeData = abi.encodeCall(IModularAccount.executeBatch, (calls));
|
|
117
|
+
}
|
|
100
118
|
|
|
101
119
|
// Use the proper pack function from ValidationLocatorLib
|
|
102
120
|
bytes memory authorization = ValidationLocatorLib.packSignature(
|
|
@@ -228,8 +246,14 @@ abstract contract SubscriptionModuleBase is ISubscriptionModuleBase {
|
|
|
228
246
|
return (true, true, SkipReason.RENEWAL_PRICE_CHANGED);
|
|
229
247
|
}
|
|
230
248
|
|
|
231
|
-
// Check if
|
|
232
|
-
|
|
249
|
+
// Check if currency changed
|
|
250
|
+
address actualCurrency = IMembership(sub.space).getMembershipCurrency();
|
|
251
|
+
if (sub.lastKnownCurrency != actualCurrency) {
|
|
252
|
+
return (true, true, SkipReason.CURRENCY_CHANGED);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check if account has sufficient balance (ETH or ERC20)
|
|
256
|
+
if (CurrencyTransfer.balanceOf(actualCurrency, account) < actualRenewalPrice) {
|
|
233
257
|
return (true, true, SkipReason.INSUFFICIENT_BALANCE);
|
|
234
258
|
}
|
|
235
259
|
|
|
@@ -253,6 +277,7 @@ abstract contract SubscriptionModuleBase is ISubscriptionModuleBase {
|
|
|
253
277
|
if (reason == SkipReason.RENEWAL_PRICE_CHANGED) return "RENEWAL_PRICE_CHANGED";
|
|
254
278
|
if (reason == SkipReason.INSUFFICIENT_BALANCE) return "INSUFFICIENT_BALANCE";
|
|
255
279
|
if (reason == SkipReason.DURATION_CHANGED) return "DURATION_CHANGED";
|
|
280
|
+
if (reason == SkipReason.CURRENCY_CHANGED) return "CURRENCY_CHANGED";
|
|
256
281
|
return "";
|
|
257
282
|
}
|
|
258
283
|
|
|
@@ -6,19 +6,19 @@ import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol"
|
|
|
6
6
|
import {IValidationHookModule} from "@erc6900/reference-implementation/interfaces/IValidationHookModule.sol";
|
|
7
7
|
import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";
|
|
8
8
|
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
9
|
-
import {ISubscriptionModule} from "./ISubscriptionModule.sol";
|
|
10
9
|
import {IMembership} from "../../../spaces/facets/membership/IMembership.sol";
|
|
11
10
|
import {IBanning} from "../../../spaces/facets/banning/IBanning.sol";
|
|
11
|
+
import {ISubscriptionModule} from "./ISubscriptionModule.sol";
|
|
12
12
|
|
|
13
13
|
// libraries
|
|
14
14
|
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
|
|
15
15
|
import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
|
|
16
16
|
import {ReentrancyGuardTransient} from "solady/utils/ReentrancyGuardTransient.sol";
|
|
17
|
+
import {SafeCastLib} from "solady/utils/SafeCastLib.sol";
|
|
17
18
|
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
18
19
|
import {Validator} from "../../../utils/libraries/Validator.sol";
|
|
19
20
|
import {IArchitect} from "../../../factory/facets/architect/IArchitect.sol";
|
|
20
21
|
import {Subscription, SubscriptionModuleStorage} from "./SubscriptionModuleStorage.sol";
|
|
21
|
-
import {SafeCastLib} from "solady/utils/SafeCastLib.sol";
|
|
22
22
|
|
|
23
23
|
// contracts
|
|
24
24
|
import {ModuleBase} from "modular-account/src/modules/ModuleBase.sol";
|
|
@@ -38,10 +38,10 @@ contract SubscriptionModuleFacet is
|
|
|
38
38
|
SubscriptionModuleBase,
|
|
39
39
|
Facet
|
|
40
40
|
{
|
|
41
|
-
using EnumerableSetLib for EnumerableSetLib.Uint256Set;
|
|
42
41
|
using EnumerableSetLib for EnumerableSetLib.AddressSet;
|
|
43
|
-
using
|
|
42
|
+
using EnumerableSetLib for EnumerableSetLib.Uint256Set;
|
|
44
43
|
using CustomRevert for bytes4;
|
|
44
|
+
using SafeCastLib for uint256;
|
|
45
45
|
|
|
46
46
|
uint256 internal constant _SIG_VALIDATION_FAILED = 1;
|
|
47
47
|
|
|
@@ -54,14 +54,34 @@ contract SubscriptionModuleFacet is
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
57
|
-
/*
|
|
57
|
+
/* ADMIN FUNCTIONS */
|
|
58
58
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
59
59
|
|
|
60
|
-
/// @inheritdoc
|
|
61
|
-
function
|
|
62
|
-
|
|
60
|
+
/// @inheritdoc ISubscriptionModule
|
|
61
|
+
function setSpaceFactory(address spaceFactory) external onlyOwner {
|
|
62
|
+
Validator.checkAddress(spaceFactory);
|
|
63
|
+
SubscriptionModuleStorage.getLayout().spaceFactory = spaceFactory;
|
|
64
|
+
emit SpaceFactoryChanged(spaceFactory);
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/// @inheritdoc ISubscriptionModule
|
|
68
|
+
function grantOperator(address operator) external onlyOwner {
|
|
69
|
+
Validator.checkAddress(operator);
|
|
70
|
+
SubscriptionModuleStorage.getLayout().operators.add(operator);
|
|
71
|
+
emit OperatorGranted(operator);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// @inheritdoc ISubscriptionModule
|
|
75
|
+
function revokeOperator(address operator) external onlyOwner {
|
|
76
|
+
Validator.checkAddress(operator);
|
|
77
|
+
SubscriptionModuleStorage.getLayout().operators.remove(operator);
|
|
78
|
+
emit OperatorRevoked(operator);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
82
|
+
/* MODULE LIFECYCLE */
|
|
83
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
84
|
+
|
|
65
85
|
/// @inheritdoc IModule
|
|
66
86
|
function onInstall(bytes calldata data) external override nonReentrant {
|
|
67
87
|
(uint32 entityId, address space, uint256 tokenId) = abi.decode(
|
|
@@ -135,69 +155,9 @@ contract SubscriptionModuleFacet is
|
|
|
135
155
|
emit SubscriptionDeactivated(msg.sender, entityId);
|
|
136
156
|
}
|
|
137
157
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
PackedUserOperation calldata,
|
|
142
|
-
bytes32
|
|
143
|
-
) external pure override returns (uint256) {
|
|
144
|
-
return _SIG_VALIDATION_FAILED;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/// @inheritdoc IValidationModule
|
|
148
|
-
function validateSignature(
|
|
149
|
-
address,
|
|
150
|
-
uint32,
|
|
151
|
-
address,
|
|
152
|
-
bytes32,
|
|
153
|
-
bytes calldata
|
|
154
|
-
) external pure override returns (bytes4) {
|
|
155
|
-
return 0xffffffff;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/// @inheritdoc IValidationModule
|
|
159
|
-
function validateRuntime(
|
|
160
|
-
address account,
|
|
161
|
-
uint32 entityId,
|
|
162
|
-
address sender,
|
|
163
|
-
uint256,
|
|
164
|
-
bytes calldata,
|
|
165
|
-
bytes calldata
|
|
166
|
-
) external view override {
|
|
167
|
-
if (sender != address(this)) SubscriptionModule__InvalidSender.selector.revertWith();
|
|
168
|
-
bool active = SubscriptionModuleStorage.getLayout().subscriptions[account][entityId].active;
|
|
169
|
-
if (!active) SubscriptionModule__InactiveSubscription.selector.revertWith();
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/// @inheritdoc IValidationHookModule
|
|
173
|
-
function preUserOpValidationHook(
|
|
174
|
-
uint32 /* entityId */,
|
|
175
|
-
PackedUserOperation calldata /* userOp */,
|
|
176
|
-
bytes32 /* userOpHash */
|
|
177
|
-
) external pure override returns (uint256) {
|
|
178
|
-
return _SIG_VALIDATION_FAILED;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/// @inheritdoc IValidationHookModule
|
|
182
|
-
function preRuntimeValidationHook(
|
|
183
|
-
uint32 /* entityId */,
|
|
184
|
-
address /* sender */,
|
|
185
|
-
uint256 /* value */,
|
|
186
|
-
bytes calldata /* data */,
|
|
187
|
-
bytes calldata /* authorization */
|
|
188
|
-
) external pure override {
|
|
189
|
-
SubscriptionModule__NotSupported.selector.revertWith();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/// @inheritdoc IValidationHookModule
|
|
193
|
-
function preSignatureValidationHook(
|
|
194
|
-
uint32 /* entityId */,
|
|
195
|
-
address /* sender */,
|
|
196
|
-
bytes32 /* hash */,
|
|
197
|
-
bytes calldata /* signature */
|
|
198
|
-
) external pure override {
|
|
199
|
-
SubscriptionModule__NotSupported.selector.revertWith();
|
|
200
|
-
}
|
|
158
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
159
|
+
/* STATE-CHANGING FUNCTIONS */
|
|
160
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
201
161
|
|
|
202
162
|
/// @inheritdoc ISubscriptionModule
|
|
203
163
|
function batchProcessRenewals(RenewalParams[] calldata params) external nonReentrant {
|
|
@@ -257,19 +217,6 @@ contract SubscriptionModuleFacet is
|
|
|
257
217
|
}
|
|
258
218
|
}
|
|
259
219
|
|
|
260
|
-
/// @inheritdoc ISubscriptionModule
|
|
261
|
-
function getSubscription(
|
|
262
|
-
address account,
|
|
263
|
-
uint32 entityId
|
|
264
|
-
) external view returns (Subscription memory) {
|
|
265
|
-
return SubscriptionModuleStorage.getLayout().subscriptions[account][entityId];
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/// @inheritdoc ISubscriptionModule
|
|
269
|
-
function getRenewalBuffer(uint256 duration) external pure returns (uint256) {
|
|
270
|
-
return _getRenewalBuffer(duration);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
220
|
/// @inheritdoc ISubscriptionModule
|
|
274
221
|
function activateSubscription(uint32 entityId) external nonReentrant {
|
|
275
222
|
SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
|
|
@@ -304,16 +251,26 @@ contract SubscriptionModuleFacet is
|
|
|
304
251
|
_pauseSubscription(sub, msg.sender, entityId);
|
|
305
252
|
}
|
|
306
253
|
|
|
254
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
255
|
+
/* GETTERS */
|
|
256
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
257
|
+
|
|
307
258
|
/// @inheritdoc ISubscriptionModule
|
|
308
|
-
function
|
|
309
|
-
return SubscriptionModuleStorage.getLayout().
|
|
259
|
+
function getSpaceFactory() external view returns (address) {
|
|
260
|
+
return SubscriptionModuleStorage.getLayout().spaceFactory;
|
|
310
261
|
}
|
|
311
262
|
|
|
312
263
|
/// @inheritdoc ISubscriptionModule
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
264
|
+
function getSubscription(
|
|
265
|
+
address account,
|
|
266
|
+
uint32 entityId
|
|
267
|
+
) external view returns (Subscription memory) {
|
|
268
|
+
return SubscriptionModuleStorage.getLayout().subscriptions[account][entityId];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/// @inheritdoc ISubscriptionModule
|
|
272
|
+
function getEntityIds(address account) external view returns (uint256[] memory) {
|
|
273
|
+
return SubscriptionModuleStorage.getLayout().entityIds[account].values();
|
|
317
274
|
}
|
|
318
275
|
|
|
319
276
|
/// @inheritdoc ISubscriptionModule
|
|
@@ -321,26 +278,77 @@ contract SubscriptionModuleFacet is
|
|
|
321
278
|
return SubscriptionModuleStorage.getLayout().operators.contains(operator);
|
|
322
279
|
}
|
|
323
280
|
|
|
324
|
-
/// @inheritdoc
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
281
|
+
/// @inheritdoc IValidationModule
|
|
282
|
+
function validateRuntime(
|
|
283
|
+
address account,
|
|
284
|
+
uint32 entityId,
|
|
285
|
+
address sender,
|
|
286
|
+
uint256,
|
|
287
|
+
bytes calldata,
|
|
288
|
+
bytes calldata
|
|
289
|
+
) external view override {
|
|
290
|
+
if (sender != address(this)) SubscriptionModule__InvalidSender.selector.revertWith();
|
|
291
|
+
bool active = SubscriptionModuleStorage.getLayout().subscriptions[account][entityId].active;
|
|
292
|
+
if (!active) SubscriptionModule__InactiveSubscription.selector.revertWith();
|
|
329
293
|
}
|
|
330
294
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
295
|
+
/// @inheritdoc IModule
|
|
296
|
+
function moduleId() external pure returns (string memory) {
|
|
297
|
+
return "towns.subscription-module.1.0.0";
|
|
298
|
+
}
|
|
334
299
|
|
|
335
300
|
/// @inheritdoc ISubscriptionModule
|
|
336
|
-
function
|
|
337
|
-
|
|
338
|
-
SubscriptionModuleStorage.getLayout().spaceFactory = spaceFactory;
|
|
339
|
-
emit SpaceFactoryChanged(spaceFactory);
|
|
301
|
+
function getRenewalBuffer(uint256 duration) external pure returns (uint256) {
|
|
302
|
+
return _getRenewalBuffer(duration);
|
|
340
303
|
}
|
|
341
304
|
|
|
342
|
-
/// @inheritdoc
|
|
343
|
-
function
|
|
344
|
-
|
|
305
|
+
/// @inheritdoc IValidationModule
|
|
306
|
+
function validateUserOp(
|
|
307
|
+
uint32,
|
|
308
|
+
PackedUserOperation calldata,
|
|
309
|
+
bytes32
|
|
310
|
+
) external pure override returns (uint256) {
|
|
311
|
+
return _SIG_VALIDATION_FAILED;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// @inheritdoc IValidationModule
|
|
315
|
+
function validateSignature(
|
|
316
|
+
address,
|
|
317
|
+
uint32,
|
|
318
|
+
address,
|
|
319
|
+
bytes32,
|
|
320
|
+
bytes calldata
|
|
321
|
+
) external pure override returns (bytes4) {
|
|
322
|
+
return 0xffffffff;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// @inheritdoc IValidationHookModule
|
|
326
|
+
function preUserOpValidationHook(
|
|
327
|
+
uint32 /* entityId */,
|
|
328
|
+
PackedUserOperation calldata /* userOp */,
|
|
329
|
+
bytes32 /* userOpHash */
|
|
330
|
+
) external pure override returns (uint256) {
|
|
331
|
+
return _SIG_VALIDATION_FAILED;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// @inheritdoc IValidationHookModule
|
|
335
|
+
function preRuntimeValidationHook(
|
|
336
|
+
uint32 /* entityId */,
|
|
337
|
+
address /* sender */,
|
|
338
|
+
uint256 /* value */,
|
|
339
|
+
bytes calldata /* data */,
|
|
340
|
+
bytes calldata /* authorization */
|
|
341
|
+
) external pure override {
|
|
342
|
+
SubscriptionModule__NotSupported.selector.revertWith();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/// @inheritdoc IValidationHookModule
|
|
346
|
+
function preSignatureValidationHook(
|
|
347
|
+
uint32 /* entityId */,
|
|
348
|
+
address /* sender */,
|
|
349
|
+
bytes32 /* hash */,
|
|
350
|
+
bytes calldata /* signature */
|
|
351
|
+
) external pure override {
|
|
352
|
+
SubscriptionModule__NotSupported.selector.revertWith();
|
|
345
353
|
}
|
|
346
354
|
}
|
|
@@ -121,17 +121,36 @@ contract EntitlementChecker is IEntitlementChecker, Facet {
|
|
|
121
121
|
function requestEntitlementCheck(CheckType checkType, bytes calldata data) external payable {
|
|
122
122
|
if (checkType == CheckType.V1) {
|
|
123
123
|
if (msg.value != 0) EntitlementChecker_InvalidValue.selector.revertWith();
|
|
124
|
-
(address
|
|
125
|
-
|
|
124
|
+
// equivalent: abi.decode(data, (address, bytes32, uint256, address[]))
|
|
125
|
+
address receiver;
|
|
126
|
+
bytes32 transactionId;
|
|
127
|
+
uint256 roleId;
|
|
128
|
+
address[] calldata nodes;
|
|
129
|
+
assembly {
|
|
130
|
+
receiver := shr(96, shl(96, calldataload(data.offset)))
|
|
131
|
+
transactionId := calldataload(add(data.offset, 0x20))
|
|
132
|
+
roleId := calldataload(add(data.offset, 0x40))
|
|
133
|
+
// nodes is dynamic: offset at 0x60, array starts at data.offset + offset
|
|
134
|
+
let nodesPtr := add(data.offset, calldataload(add(data.offset, 0x60)))
|
|
135
|
+
nodes.length := calldataload(nodesPtr)
|
|
136
|
+
nodes.offset := add(nodesPtr, 0x20)
|
|
137
|
+
}
|
|
126
138
|
emit EntitlementCheckRequested(receiver, msg.sender, transactionId, roleId, nodes);
|
|
127
139
|
} else if (checkType == CheckType.V2) {
|
|
128
|
-
(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
// equivalent: abi.decode(data, (address, bytes32, uint256, bytes))
|
|
141
|
+
// extraData contains: (address sender)
|
|
142
|
+
address receiver;
|
|
143
|
+
bytes32 transactionId;
|
|
144
|
+
uint256 requestId;
|
|
145
|
+
address sender;
|
|
146
|
+
assembly {
|
|
147
|
+
receiver := shr(96, shl(96, calldataload(data.offset)))
|
|
148
|
+
transactionId := calldataload(add(data.offset, 0x20))
|
|
149
|
+
requestId := calldataload(add(data.offset, 0x40))
|
|
150
|
+
// extraData offset at 0x60, sender is first word after length
|
|
151
|
+
let extraDataPtr := add(data.offset, calldataload(add(data.offset, 0x60)))
|
|
152
|
+
sender := shr(96, shl(96, calldataload(add(extraDataPtr, 0x20))))
|
|
153
|
+
}
|
|
135
154
|
_requestEntitlementCheck(
|
|
136
155
|
receiver,
|
|
137
156
|
transactionId,
|
|
@@ -141,14 +160,21 @@ contract EntitlementChecker is IEntitlementChecker, Facet {
|
|
|
141
160
|
sender
|
|
142
161
|
);
|
|
143
162
|
} else if (checkType == CheckType.V3) {
|
|
144
|
-
(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
163
|
+
// equivalent: abi.decode(data, (address, bytes32, uint256, address, uint256, address))
|
|
164
|
+
address receiver;
|
|
165
|
+
bytes32 transactionId;
|
|
166
|
+
uint256 requestId;
|
|
167
|
+
address currency;
|
|
168
|
+
uint256 amount;
|
|
169
|
+
address sender;
|
|
170
|
+
assembly {
|
|
171
|
+
receiver := shr(96, shl(96, calldataload(data.offset)))
|
|
172
|
+
transactionId := calldataload(add(data.offset, 0x20))
|
|
173
|
+
requestId := calldataload(add(data.offset, 0x40))
|
|
174
|
+
currency := shr(96, shl(96, calldataload(add(data.offset, 0x60))))
|
|
175
|
+
amount := calldataload(add(data.offset, 0x80))
|
|
176
|
+
sender := shr(96, shl(96, calldataload(add(data.offset, 0xa0))))
|
|
177
|
+
}
|
|
152
178
|
_requestEntitlementCheck(receiver, transactionId, requestId, currency, amount, sender);
|
|
153
179
|
} else {
|
|
154
180
|
EntitlementChecker_InvalidCheckType.selector.revertWith();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
|
|
5
|
+
import {IFeeManager} from "../../factory/facets/fee/IFeeManager.sol";
|
|
6
|
+
import {CurrencyTransfer} from "../../utils/libraries/CurrencyTransfer.sol";
|
|
7
|
+
|
|
8
|
+
library ProtocolFeeLib {
|
|
9
|
+
using SafeTransferLib for address;
|
|
10
|
+
|
|
11
|
+
/// @notice Charges protocol fee via FeeManager with ERC20 approval handling
|
|
12
|
+
/// @param spaceFactory The FeeManager address
|
|
13
|
+
/// @param feeType The type of fee being charged
|
|
14
|
+
/// @param user The user paying the fee
|
|
15
|
+
/// @param currency The payment currency (NATIVE_TOKEN or ERC20)
|
|
16
|
+
/// @param amount The base amount for fee calculation
|
|
17
|
+
/// @param expectedFee The pre-calculated expected fee
|
|
18
|
+
/// @return protocolFee The actual fee charged
|
|
19
|
+
function charge(
|
|
20
|
+
address spaceFactory,
|
|
21
|
+
bytes32 feeType,
|
|
22
|
+
address user,
|
|
23
|
+
address currency,
|
|
24
|
+
uint256 amount,
|
|
25
|
+
uint256 expectedFee
|
|
26
|
+
) internal returns (uint256 protocolFee) {
|
|
27
|
+
if (expectedFee == 0) return 0;
|
|
28
|
+
|
|
29
|
+
bool isNative = currency == CurrencyTransfer.NATIVE_TOKEN;
|
|
30
|
+
if (!isNative) currency.safeApproveWithRetry(spaceFactory, expectedFee);
|
|
31
|
+
|
|
32
|
+
protocolFee = IFeeManager(spaceFactory).chargeFee{value: isNative ? expectedFee : 0}(
|
|
33
|
+
feeType,
|
|
34
|
+
user,
|
|
35
|
+
amount,
|
|
36
|
+
currency,
|
|
37
|
+
expectedFee,
|
|
38
|
+
""
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (!isNative) currency.safeApprove(spaceFactory, 0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -75,6 +75,11 @@ interface IMembershipBase {
|
|
|
75
75
|
event MembershipFreeAllocationUpdated(uint256 indexed allocation);
|
|
76
76
|
event MembershipWithdrawal(address indexed currency, address indexed recipient, uint256 amount);
|
|
77
77
|
event MembershipTokenIssued(address indexed recipient, uint256 indexed tokenId);
|
|
78
|
+
/// @notice Emitted when a membership payment is processed (new membership or renewal)
|
|
79
|
+
/// @param currency The currency used for payment (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE for ETH)
|
|
80
|
+
/// @param price The base membership price paid
|
|
81
|
+
/// @param protocolFee The protocol fee paid
|
|
82
|
+
event MembershipPaid(address indexed currency, uint256 price, uint256 protocolFee);
|
|
78
83
|
event MembershipTokenRejected(address indexed recipient);
|
|
79
84
|
}
|
|
80
85
|
|
|
@@ -97,7 +102,7 @@ interface IMembership is IMembershipBase {
|
|
|
97
102
|
/// @param referral The referral data
|
|
98
103
|
function joinSpaceWithReferral(
|
|
99
104
|
address receiver,
|
|
100
|
-
ReferralTypes
|
|
105
|
+
ReferralTypes calldata referral
|
|
101
106
|
) external payable;
|
|
102
107
|
|
|
103
108
|
/// @notice Renew a space membership
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
pragma solidity ^0.8.23;
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
|
-
import {IMembership} from "./IMembership.sol";
|
|
6
5
|
import {IMembershipPricing} from "./pricing/IMembershipPricing.sol";
|
|
6
|
+
import {IMembership} from "./IMembership.sol";
|
|
7
7
|
|
|
8
8
|
// libraries
|
|
9
9
|
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
|
|
@@ -26,13 +26,22 @@ contract MembershipFacet is IMembership, MembershipJoin, ReentrancyGuard, Facet
|
|
|
26
26
|
/// @inheritdoc IMembership
|
|
27
27
|
function joinSpace(JoinType action, bytes calldata data) external payable nonReentrant {
|
|
28
28
|
if (action == JoinType.Basic) {
|
|
29
|
-
|
|
29
|
+
// equivalent: abi.decode(data, (address))
|
|
30
|
+
address receiver;
|
|
31
|
+
assembly {
|
|
32
|
+
receiver := shr(96, shl(96, calldataload(data.offset)))
|
|
33
|
+
}
|
|
30
34
|
_joinSpace(receiver);
|
|
31
35
|
} else if (action == JoinType.WithReferral) {
|
|
32
|
-
(address
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
// equivalent: abi.decode(data, (address, ReferralTypes))
|
|
37
|
+
address receiver;
|
|
38
|
+
ReferralTypes calldata referral;
|
|
39
|
+
assembly {
|
|
40
|
+
receiver := shr(96, shl(96, calldataload(data.offset)))
|
|
41
|
+
// this is a variable length struct, so (data.offset + 0x20) contains
|
|
42
|
+
// the offset from data.offset at which the struct begins
|
|
43
|
+
referral := add(data.offset, calldataload(add(data.offset, 0x20)))
|
|
44
|
+
}
|
|
36
45
|
_joinSpaceWithReferral(receiver, referral);
|
|
37
46
|
} else {
|
|
38
47
|
Membership__InvalidAction.selector.revertWith();
|
|
@@ -47,7 +56,7 @@ contract MembershipFacet is IMembership, MembershipJoin, ReentrancyGuard, Facet
|
|
|
47
56
|
/// @inheritdoc IMembership
|
|
48
57
|
function joinSpaceWithReferral(
|
|
49
58
|
address receiver,
|
|
50
|
-
ReferralTypes
|
|
59
|
+
ReferralTypes calldata referral
|
|
51
60
|
) external payable nonReentrant {
|
|
52
61
|
_joinSpaceWithReferral(receiver, referral);
|
|
53
62
|
}
|
|
@@ -16,6 +16,7 @@ import {CurrencyTransfer} from "../../../../utils/libraries/CurrencyTransfer.sol
|
|
|
16
16
|
import {CustomRevert} from "../../../../utils/libraries/CustomRevert.sol";
|
|
17
17
|
import {MembershipStorage} from "../MembershipStorage.sol";
|
|
18
18
|
import {Permissions} from "../../Permissions.sol";
|
|
19
|
+
import {ProtocolFeeLib} from "../../ProtocolFeeLib.sol";
|
|
19
20
|
|
|
20
21
|
// contracts
|
|
21
22
|
import {ERC5643Base} from "../../../../diamond/facets/token/ERC5643/ERC5643Base.sol";
|
|
@@ -67,6 +68,7 @@ abstract contract MembershipJoin is
|
|
|
67
68
|
|
|
68
69
|
struct PricingDetails {
|
|
69
70
|
uint256 basePrice;
|
|
71
|
+
uint256 protocolFee;
|
|
70
72
|
uint256 amountDue;
|
|
71
73
|
bool shouldCharge;
|
|
72
74
|
}
|
|
@@ -100,8 +102,12 @@ abstract contract MembershipJoin is
|
|
|
100
102
|
return joinDetails;
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
(uint256 totalRequired, ) = _getTotalMembershipPayment(membershipPrice);
|
|
104
|
-
(joinDetails.amountDue, joinDetails.shouldCharge) = (
|
|
105
|
+
(uint256 totalRequired, uint256 protocolFee) = _getTotalMembershipPayment(membershipPrice);
|
|
106
|
+
(joinDetails.protocolFee, joinDetails.amountDue, joinDetails.shouldCharge) = (
|
|
107
|
+
protocolFee,
|
|
108
|
+
totalRequired,
|
|
109
|
+
true
|
|
110
|
+
);
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
@@ -150,7 +156,7 @@ abstract contract MembershipJoin is
|
|
|
150
156
|
/// @notice Handles the process of joining a space with a referral
|
|
151
157
|
/// @param receiver The address that will receive the membership token
|
|
152
158
|
/// @param referral The referral information
|
|
153
|
-
function _joinSpaceWithReferral(address receiver, ReferralTypes
|
|
159
|
+
function _joinSpaceWithReferral(address receiver, ReferralTypes calldata referral) internal {
|
|
154
160
|
_validateJoinSpace(receiver);
|
|
155
161
|
|
|
156
162
|
PricingDetails memory joinDetails = _getPricingDetails();
|
|
@@ -229,7 +235,10 @@ abstract contract MembershipJoin is
|
|
|
229
235
|
return amountRequired;
|
|
230
236
|
}
|
|
231
237
|
|
|
232
|
-
function _validateUserReferral(
|
|
238
|
+
function _validateUserReferral(
|
|
239
|
+
address receiver,
|
|
240
|
+
ReferralTypes calldata referral
|
|
241
|
+
) internal view {
|
|
233
242
|
if (referral.userReferral == receiver || referral.userReferral == msg.sender) {
|
|
234
243
|
Membership__InvalidAddress.selector.revertWith();
|
|
235
244
|
}
|
|
@@ -341,7 +350,10 @@ abstract contract MembershipJoin is
|
|
|
341
350
|
Membership__InvalidTransactionType.selector.revertWith();
|
|
342
351
|
}
|
|
343
352
|
|
|
344
|
-
|
|
353
|
+
address currency = _getMembershipCurrency();
|
|
354
|
+
_payProtocolFee(currency, joinDetails.basePrice, joinDetails.protocolFee);
|
|
355
|
+
|
|
356
|
+
emit MembershipPaid(currency, joinDetails.basePrice, joinDetails.protocolFee);
|
|
345
357
|
|
|
346
358
|
_afterChargeForJoinSpace(transactionId, receiver, joinDetails.amountDue);
|
|
347
359
|
}
|
|
@@ -365,7 +377,7 @@ abstract contract MembershipJoin is
|
|
|
365
377
|
ReferralTypes memory referral = abi.decode(referralData, (ReferralTypes));
|
|
366
378
|
|
|
367
379
|
address currency = _getMembershipCurrency();
|
|
368
|
-
_payProtocolFee(currency, joinDetails.basePrice);
|
|
380
|
+
_payProtocolFee(currency, joinDetails.basePrice, joinDetails.protocolFee);
|
|
369
381
|
_payPartnerFee(currency, referral.partner, joinDetails.basePrice);
|
|
370
382
|
_payReferralFee(
|
|
371
383
|
currency,
|
|
@@ -375,6 +387,8 @@ abstract contract MembershipJoin is
|
|
|
375
387
|
joinDetails.basePrice
|
|
376
388
|
);
|
|
377
389
|
|
|
390
|
+
emit MembershipPaid(currency, joinDetails.basePrice, joinDetails.protocolFee);
|
|
391
|
+
|
|
378
392
|
_afterChargeForJoinSpace(transactionId, receiver, joinDetails.amountDue);
|
|
379
393
|
}
|
|
380
394
|
|
|
@@ -410,7 +424,6 @@ abstract contract MembershipJoin is
|
|
|
410
424
|
// set expiration of membership
|
|
411
425
|
_renewSubscription(tokenId, _getMembershipDuration());
|
|
412
426
|
|
|
413
|
-
// emit event
|
|
414
427
|
emit MembershipTokenIssued(receiver, tokenId);
|
|
415
428
|
}
|
|
416
429
|
|
|
@@ -447,21 +460,18 @@ abstract contract MembershipJoin is
|
|
|
447
460
|
/* FEE DISTRIBUTION */
|
|
448
461
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
449
462
|
|
|
450
|
-
/// @notice Pays the protocol fee
|
|
463
|
+
/// @notice Pays the protocol fee via FeeManager
|
|
451
464
|
/// @param currency The currency to pay in
|
|
452
|
-
/// @param
|
|
453
|
-
/// @
|
|
454
|
-
function _payProtocolFee(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
CurrencyTransfer.transferCurrency(
|
|
465
|
+
/// @param basePrice The base price of the membership (for fee calculation)
|
|
466
|
+
/// @param expectedFee The pre-calculated protocol fee
|
|
467
|
+
function _payProtocolFee(address currency, uint256 basePrice, uint256 expectedFee) internal {
|
|
468
|
+
ProtocolFeeLib.charge(
|
|
469
|
+
_getSpaceFactory(),
|
|
470
|
+
_getMembershipFeeType(currency),
|
|
471
|
+
msg.sender,
|
|
461
472
|
currency,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
protocolFee
|
|
473
|
+
basePrice,
|
|
474
|
+
expectedFee
|
|
465
475
|
);
|
|
466
476
|
}
|
|
467
477
|
|
|
@@ -542,13 +552,13 @@ abstract contract MembershipJoin is
|
|
|
542
552
|
return;
|
|
543
553
|
}
|
|
544
554
|
|
|
545
|
-
(uint256 totalRequired, ) = _getTotalMembershipPayment(basePrice);
|
|
555
|
+
(uint256 totalRequired, uint256 protocolFee) = _getTotalMembershipPayment(basePrice);
|
|
546
556
|
|
|
547
557
|
if (currency == CurrencyTransfer.NATIVE_TOKEN) {
|
|
548
558
|
// ETH payment: validate msg.value
|
|
549
559
|
if (totalRequired > msg.value) Membership__InvalidPayment.selector.revertWith();
|
|
550
560
|
|
|
551
|
-
_payProtocolFee(currency, basePrice);
|
|
561
|
+
_payProtocolFee(currency, basePrice, protocolFee);
|
|
552
562
|
|
|
553
563
|
// Handle excess payment
|
|
554
564
|
uint256 excess = msg.value - totalRequired;
|
|
@@ -562,9 +572,10 @@ abstract contract MembershipJoin is
|
|
|
562
572
|
// Transfer ERC20 from payer to contract
|
|
563
573
|
_transferIn(currency, payer, totalRequired);
|
|
564
574
|
|
|
565
|
-
_payProtocolFee(currency, basePrice);
|
|
575
|
+
_payProtocolFee(currency, basePrice, protocolFee);
|
|
566
576
|
}
|
|
567
577
|
|
|
578
|
+
emit MembershipPaid(currency, basePrice, protocolFee);
|
|
568
579
|
_mintMembershipPoints(receiver, totalRequired);
|
|
569
580
|
_renewSubscription(tokenId, uint64(duration));
|
|
570
581
|
}
|
|
@@ -13,8 +13,7 @@ import {CurrencyTransfer} from "../../../utils/libraries/CurrencyTransfer.sol";
|
|
|
13
13
|
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
14
14
|
import {MembershipStorage} from "../membership/MembershipStorage.sol";
|
|
15
15
|
import {TippingStorage} from "./TippingStorage.sol";
|
|
16
|
-
import {
|
|
17
|
-
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
|
|
16
|
+
import {ProtocolFeeLib} from "../ProtocolFeeLib.sol";
|
|
18
17
|
|
|
19
18
|
// contracts
|
|
20
19
|
import {PointsBase} from "../points/PointsBase.sol";
|
|
@@ -23,8 +22,6 @@ abstract contract TippingBase is ITippingBase, PointsBase {
|
|
|
23
22
|
using EnumerableSet for EnumerableSet.AddressSet;
|
|
24
23
|
using CustomRevert for bytes4;
|
|
25
24
|
|
|
26
|
-
uint256 internal constant MAX_FEE_TOLERANCE = 100; // 1% tolerance
|
|
27
|
-
|
|
28
25
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
29
26
|
/* Internal Functions */
|
|
30
27
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
@@ -205,41 +202,28 @@ abstract contract TippingBase is ITippingBase, PointsBase {
|
|
|
205
202
|
address currency,
|
|
206
203
|
uint256 amount
|
|
207
204
|
) internal returns (uint256 protocolFee) {
|
|
208
|
-
|
|
209
|
-
address spaceFactory = ds.spaceFactory;
|
|
205
|
+
address spaceFactory = MembershipStorage.layout().spaceFactory;
|
|
210
206
|
|
|
211
|
-
// Calculate
|
|
212
|
-
|
|
207
|
+
// Calculate fee first
|
|
208
|
+
protocolFee = IFeeManager(spaceFactory).calculateFee(
|
|
213
209
|
FeeTypesLib.TIP_MEMBER,
|
|
214
210
|
msg.sender,
|
|
215
211
|
amount,
|
|
216
212
|
""
|
|
217
213
|
);
|
|
218
214
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
uint256 maxFee = expectedFee + BasisPoints.calculate(expectedFee, MAX_FEE_TOLERANCE);
|
|
223
|
-
|
|
224
|
-
// Approve ERC20 if needed (native token sends value with call)
|
|
225
|
-
bool isNative = currency == CurrencyTransfer.NATIVE_TOKEN;
|
|
226
|
-
if (!isNative) SafeTransferLib.safeApproveWithRetry(currency, spaceFactory, maxFee);
|
|
227
|
-
|
|
228
|
-
// Charge fee (excess native token will be refunded)
|
|
229
|
-
protocolFee = IFeeManager(spaceFactory).chargeFee{value: isNative ? maxFee : 0}(
|
|
215
|
+
// Charge with pre-calculated fee
|
|
216
|
+
protocolFee = ProtocolFeeLib.charge(
|
|
217
|
+
spaceFactory,
|
|
230
218
|
FeeTypesLib.TIP_MEMBER,
|
|
231
219
|
msg.sender,
|
|
232
|
-
amount,
|
|
233
220
|
currency,
|
|
234
|
-
|
|
235
|
-
|
|
221
|
+
amount,
|
|
222
|
+
protocolFee
|
|
236
223
|
);
|
|
237
224
|
|
|
238
|
-
// Reset ERC20 approval
|
|
239
|
-
if (!isNative) SafeTransferLib.safeApprove(currency, spaceFactory, 0);
|
|
240
|
-
|
|
241
225
|
// Mint points for fee payment (only for ETH tips)
|
|
242
|
-
if (
|
|
226
|
+
if (protocolFee > 0 && currency == CurrencyTransfer.NATIVE_TOKEN) {
|
|
243
227
|
address airdropDiamond = _getAirdropDiamond();
|
|
244
228
|
uint256 points = _getPoints(
|
|
245
229
|
airdropDiamond,
|
|
@@ -8,8 +8,6 @@ import {IWETH} from "../interfaces/IWETH.sol";
|
|
|
8
8
|
import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
|
|
9
9
|
import {CustomRevert} from "./CustomRevert.sol";
|
|
10
10
|
|
|
11
|
-
// contracts
|
|
12
|
-
|
|
13
11
|
library CurrencyTransfer {
|
|
14
12
|
using SafeTransferLib for address;
|
|
15
13
|
using CustomRevert for bytes4;
|
|
@@ -124,4 +122,10 @@ library CurrencyTransfer {
|
|
|
124
122
|
if (actualFee > 0) safeTransferERC20(currency, payer, recipient, actualFee);
|
|
125
123
|
}
|
|
126
124
|
}
|
|
125
|
+
|
|
126
|
+
/// @dev Returns the balance of `account` in `currency`.
|
|
127
|
+
function balanceOf(address currency, address account) internal view returns (uint256) {
|
|
128
|
+
if (currency == NATIVE_TOKEN) return account.balance;
|
|
129
|
+
return currency.balanceOf(account);
|
|
130
|
+
}
|
|
127
131
|
}
|