@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
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Membership Contract Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Towns Protocol membership system manages space memberships through NFT tokens with time-based expiration. It supports flexible pricing models, role-based entitlements (including cross-chain validation), referral systems, and fee distribution. Built on the Diamond pattern (EIP-2535), the system supports both native ETH and ERC20 (e.g., USDC) payments.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
### Contract Hierarchy
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
MembershipFacet (external interface)
|
|
13
|
+
└─ MembershipJoin (join + renewal logic)
|
|
14
|
+
├─ MembershipBase (pricing, fees, storage)
|
|
15
|
+
├─ DispatcherBase (transaction capture)
|
|
16
|
+
├─ EntitlementGatedBase (entitlement checks)
|
|
17
|
+
├─ ReferralsBase (referral fees)
|
|
18
|
+
├─ PrepayBase (prepaid memberships)
|
|
19
|
+
├─ PointsBase (rewards points)
|
|
20
|
+
└─ ERC721ABase (NFT implementation)
|
|
21
|
+
|
|
22
|
+
SpaceEntitlementGated (result handler)
|
|
23
|
+
└─ Overrides _onEntitlementCheckResultPosted()
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Storage Architecture
|
|
27
|
+
|
|
28
|
+
**Diamond Storage Pattern** - Each facet uses isolated storage:
|
|
29
|
+
|
|
30
|
+
- `MembershipStorage` - pricing, duration, limits, currency
|
|
31
|
+
- `DispatcherStorage` - transactionBalance, transactionData
|
|
32
|
+
- `EntitlementGatedStorage` - crosschain check state
|
|
33
|
+
- `ReferralsStorage` - referral codes and fees
|
|
34
|
+
- `PrepayStorage` - prepaid supply tracking
|
|
35
|
+
|
|
36
|
+
### Key Facets
|
|
37
|
+
|
|
38
|
+
| Facet | Responsibility |
|
|
39
|
+
|-------|---------------|
|
|
40
|
+
| `MembershipFacet.sol` | External API (join, renew, setters) |
|
|
41
|
+
| `MembershipJoin.sol` | Join logic, payment processing |
|
|
42
|
+
| `MembershipBase.sol` | Pricing, fees, validation |
|
|
43
|
+
| `DispatcherBase.sol` | Transaction capture (ETH/ERC20) |
|
|
44
|
+
| `EntitlementGatedBase.sol` | Entitlement check requests |
|
|
45
|
+
| `SpaceEntitlementGated.sol` | Entitlement result handling |
|
|
46
|
+
|
|
47
|
+
## Payment Model
|
|
48
|
+
|
|
49
|
+
### Fee-Added Pricing
|
|
50
|
+
|
|
51
|
+
The system uses a **fee-added** pricing model where protocol fees are added on top of the base price:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Total Price = Base Price + Protocol Fee
|
|
55
|
+
|
|
56
|
+
Example (ETH):
|
|
57
|
+
Base Price: 1.0 ETH → Space owner
|
|
58
|
+
Protocol Fee: 0.1 ETH → Platform
|
|
59
|
+
Total: 1.1 ETH ← User pays this
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Currency Support
|
|
63
|
+
|
|
64
|
+
Memberships can be priced in:
|
|
65
|
+
- **Native ETH** (`address(0)` or `NATIVE_TOKEN`)
|
|
66
|
+
- **ERC20 tokens** (e.g., USDC) - must be enabled in FeeManager
|
|
67
|
+
|
|
68
|
+
Currency validation happens in `_setMembershipCurrency()` which requires the token to have an enabled fee configuration in FeeManager.
|
|
69
|
+
|
|
70
|
+
## Core Flow: Join → Entitlement Check → Token
|
|
71
|
+
|
|
72
|
+
### Happy Path (Local Entitlement)
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
User: joinSpace(receiver) + payment
|
|
76
|
+
│
|
|
77
|
+
├─ Validate: supply limit, payment amount
|
|
78
|
+
│
|
|
79
|
+
├─ Register Transaction
|
|
80
|
+
│ └─ Capture payment in transactionBalance[txId]
|
|
81
|
+
│ └─ Store: [selector, sender, receiver, referral]
|
|
82
|
+
│
|
|
83
|
+
├─ Check Entitlement (Local)
|
|
84
|
+
│ └─ ✓ User has local entitlement → PASS
|
|
85
|
+
│
|
|
86
|
+
├─ Charge for Join
|
|
87
|
+
│ ├─ Protocol fee → Platform
|
|
88
|
+
│ ├─ Partner fee → Partner (if any)
|
|
89
|
+
│ ├─ Referral fee → Referrer (if any)
|
|
90
|
+
│ └─ Base price → Space owner
|
|
91
|
+
│
|
|
92
|
+
├─ Refund Excess (if overpaid)
|
|
93
|
+
│
|
|
94
|
+
└─ Issue Token
|
|
95
|
+
└─ Mint NFT with expiration timestamp
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Crosschain Entitlement Path
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
User: joinSpace(receiver) + payment
|
|
102
|
+
│
|
|
103
|
+
├─ Validate & Register (same as above)
|
|
104
|
+
│
|
|
105
|
+
├─ Check Entitlement
|
|
106
|
+
│ ├─ Local entitlements: NONE
|
|
107
|
+
│ └─ Crosschain entitlements: EXISTS
|
|
108
|
+
│
|
|
109
|
+
├─ Request Crosschain Check
|
|
110
|
+
│ └─ Send gas fee to EntitlementChecker
|
|
111
|
+
│ └─ Payment remains locked in transactionBalance[txId]
|
|
112
|
+
│
|
|
113
|
+
└─ Return (pending state)
|
|
114
|
+
|
|
115
|
+
[Later - Entitlement result posted by checker]
|
|
116
|
+
|
|
117
|
+
EntitlementChecker: postEntitlementCheckResultV2(txId, PASSED)
|
|
118
|
+
│
|
|
119
|
+
├─ _onEntitlementCheckResultPosted(txId, PASSED)
|
|
120
|
+
│ ├─ Retrieve: payment = transactionBalance[txId]
|
|
121
|
+
│ ├─ Charge for Join (same fee distribution)
|
|
122
|
+
│ ├─ Refund Excess
|
|
123
|
+
│ └─ Issue Token
|
|
124
|
+
│
|
|
125
|
+
└─ Delete Transaction
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Rejection Path
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Entitlement Check: FAILED
|
|
132
|
+
│
|
|
133
|
+
├─ _rejectMembership(txId, receiver)
|
|
134
|
+
│ ├─ Delete: transactionData[txId]
|
|
135
|
+
│ ├─ Refund: full transactionBalance[txId] → receiver
|
|
136
|
+
│ └─ Emit: MembershipTokenRejected
|
|
137
|
+
│
|
|
138
|
+
└─ Transaction cleaned up
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Payment Capture System
|
|
142
|
+
|
|
143
|
+
### Transaction Registration
|
|
144
|
+
|
|
145
|
+
Generates unique transaction ID and captures payment amount:
|
|
146
|
+
|
|
147
|
+
```solidity
|
|
148
|
+
// Called in _joinSpace() or _joinSpaceWithReferral()
|
|
149
|
+
bytes32 txId = _registerTransaction(sender, encodedData, capturedAmount);
|
|
150
|
+
|
|
151
|
+
// Inside DispatcherBase:
|
|
152
|
+
// 1. Generate txId from sender + block.number + nonce
|
|
153
|
+
// 2. Store: transactionData[txId] = encodedData
|
|
154
|
+
// 3. Capture: transactionBalance[txId] = capturedAmount
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Payment Release
|
|
158
|
+
|
|
159
|
+
```solidity
|
|
160
|
+
// Release consumed amount (after fee distribution)
|
|
161
|
+
_releaseCapturedValue(txId, amountDue)
|
|
162
|
+
|
|
163
|
+
// Refund remaining balance
|
|
164
|
+
_refundBalance(txId, receiver)
|
|
165
|
+
└─ CurrencyTransfer.transferCurrency(currency, address(this), receiver, remaining)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Entitlement Check System
|
|
169
|
+
|
|
170
|
+
### Local vs Crosschain
|
|
171
|
+
|
|
172
|
+
**Local Entitlements**: Immediate validation (same transaction)
|
|
173
|
+
- ERC20/ERC721 ownership checks
|
|
174
|
+
- Token balance thresholds
|
|
175
|
+
- User allowlists
|
|
176
|
+
|
|
177
|
+
**Crosschain Entitlements**: Async validation
|
|
178
|
+
- Checks on other chains (Ethereum L1, Base L2)
|
|
179
|
+
- Requires ETH for gas (sent to EntitlementChecker)
|
|
180
|
+
- Payment held until result posted
|
|
181
|
+
|
|
182
|
+
### Check Flow
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
_checkEntitlement(receiver, sender, txId, amountDue)
|
|
186
|
+
│
|
|
187
|
+
├─ PHASE 1: Check Local Entitlements
|
|
188
|
+
│ └─ If ANY pass → return (true, false)
|
|
189
|
+
│
|
|
190
|
+
└─ PHASE 2: Check Crosschain Entitlements
|
|
191
|
+
└─ For each crosschain entitlement:
|
|
192
|
+
├─ First request: send gas fee
|
|
193
|
+
└─ return (false, true) ← pending state
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Appendix: Key Functions
|
|
197
|
+
|
|
198
|
+
### Join Flow Entry Points
|
|
199
|
+
|
|
200
|
+
```solidity
|
|
201
|
+
// MembershipFacet.sol
|
|
202
|
+
function joinSpace(address receiver) external payable nonReentrant
|
|
203
|
+
function joinSpace(JoinType joinType, bytes calldata data) external payable nonReentrant
|
|
204
|
+
function joinSpaceWithReferral(address receiver, ReferralTypes memory referral)
|
|
205
|
+
external payable nonReentrant
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Payment Capture
|
|
209
|
+
|
|
210
|
+
```solidity
|
|
211
|
+
// DispatcherBase.sol
|
|
212
|
+
function _registerTransaction(address sender, bytes memory data, uint256 capturedAmount)
|
|
213
|
+
internal returns (bytes32 transactionId)
|
|
214
|
+
function _getCapturedValue(bytes32 transactionId) internal view returns (uint256)
|
|
215
|
+
function _releaseCapturedValue(bytes32 transactionId, uint256 amount) internal
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Fee Distribution
|
|
219
|
+
|
|
220
|
+
```solidity
|
|
221
|
+
// MembershipBase.sol
|
|
222
|
+
function _collectProtocolFee(address payer, uint256 membershipPrice)
|
|
223
|
+
internal returns (uint256 protocolFee)
|
|
224
|
+
function _transferIn(address from, uint256 amount) internal returns (uint256)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Entitlement Checking
|
|
228
|
+
|
|
229
|
+
```solidity
|
|
230
|
+
// MembershipJoin.sol
|
|
231
|
+
function _checkEntitlement(address receiver, address sender, bytes32 txId, uint256 amount)
|
|
232
|
+
internal returns (bool isEntitled, bool isCrosschainPending)
|
|
233
|
+
|
|
234
|
+
// EntitlementGatedBase.sol
|
|
235
|
+
function _requestEntitlementCheckV2(...)
|
|
236
|
+
function _postEntitlementCheckResultV2(bytes32 txId, uint256 roleId, NodeVoteStatus result)
|
|
237
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towns-protocol/contracts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.443",
|
|
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.443",
|
|
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": "8a69e02b8ed000e86db49a324dd5fd05077c7d7b"
|
|
54
54
|
}
|
|
@@ -20,7 +20,6 @@ import {DeployEntitlementsManager} from "../facets/DeployEntitlementsManager.s.s
|
|
|
20
20
|
import {DeployMembership} from "../facets/DeployMembership.s.sol";
|
|
21
21
|
import {DeployMembershipMetadata} from "../facets/DeployMembershipMetadata.s.sol";
|
|
22
22
|
import {DeployMembershipToken} from "../facets/DeployMembershipToken.s.sol";
|
|
23
|
-
import {DeployPrepayFacet} from "../facets/DeployPrepayFacet.s.sol";
|
|
24
23
|
import {DeployReferrals} from "../facets/DeployReferrals.s.sol";
|
|
25
24
|
import {DeployReviewFacet} from "../facets/DeployReviewFacet.s.sol";
|
|
26
25
|
import {DeployRoles} from "../facets/DeployRoles.s.sol";
|
|
@@ -112,7 +111,6 @@ contract DeploySpace is IDiamondInitHelper, DiamondHelper, Deployer {
|
|
|
112
111
|
facetHelper.add("Roles");
|
|
113
112
|
facetHelper.add("Channels");
|
|
114
113
|
facetHelper.add("TokenPausableFacet");
|
|
115
|
-
facetHelper.add("PrepayFacet");
|
|
116
114
|
facetHelper.add("ReferralsFacet");
|
|
117
115
|
facetHelper.add("ReviewFacet");
|
|
118
116
|
|
|
@@ -164,9 +162,6 @@ contract DeploySpace is IDiamondInitHelper, DiamondHelper, Deployer {
|
|
|
164
162
|
facet = facetHelper.getDeployedAddress("TokenPausableFacet");
|
|
165
163
|
addCut(makeCut(facet, FacetCutAction.Add, DeployTokenPausable.selectors()));
|
|
166
164
|
|
|
167
|
-
facet = facetHelper.getDeployedAddress("PrepayFacet");
|
|
168
|
-
addCut(makeCut(facet, FacetCutAction.Add, DeployPrepayFacet.selectors()));
|
|
169
|
-
|
|
170
165
|
facet = facetHelper.getDeployedAddress("ReferralsFacet");
|
|
171
166
|
addCut(makeCut(facet, FacetCutAction.Add, DeployReferrals.selectors()));
|
|
172
167
|
|
|
@@ -247,8 +242,6 @@ contract DeploySpace is IDiamondInitHelper, DiamondHelper, Deployer {
|
|
|
247
242
|
addCut(makeCut(facet, FacetCutAction.Add, DeployChannels.selectors()));
|
|
248
243
|
} else if (facetName.eq("TokenPausableFacet")) {
|
|
249
244
|
addCut(makeCut(facet, FacetCutAction.Add, DeployTokenPausable.selectors()));
|
|
250
|
-
} else if (facetName.eq("PrepayFacet")) {
|
|
251
|
-
addCut(makeCut(facet, FacetCutAction.Add, DeployPrepayFacet.selectors()));
|
|
252
245
|
} else if (facetName.eq("ReferralsFacet")) {
|
|
253
246
|
addCut(makeCut(facet, FacetCutAction.Add, DeployReferrals.selectors()));
|
|
254
247
|
} else if (facetName.eq("ReviewFacet")) {
|
|
@@ -223,7 +223,7 @@ contract DeploySpaceFactory is IDiamondInitHelper, DiamondHelper, Deployer {
|
|
|
223
223
|
facet,
|
|
224
224
|
DeployPlatformRequirements.makeInitData(
|
|
225
225
|
deployer, // feeRecipient
|
|
226
|
-
|
|
226
|
+
1000, // membershipBps 10%
|
|
227
227
|
0.0005 ether, // membershipFee
|
|
228
228
|
1000, // membershipFreeAllocation
|
|
229
229
|
365 days, // membershipDuration
|
|
@@ -354,7 +354,7 @@ contract DeploySpaceFactory is IDiamondInitHelper, DiamondHelper, Deployer {
|
|
|
354
354
|
facet,
|
|
355
355
|
DeployPlatformRequirements.makeInitData(
|
|
356
356
|
deployer, // feeRecipient
|
|
357
|
-
|
|
357
|
+
1000, // membershipBps 10%
|
|
358
358
|
0.0005 ether, // membershipFee
|
|
359
359
|
1000, // membershipFreeAllocation
|
|
360
360
|
365 days, // membershipDuration
|
|
@@ -13,7 +13,7 @@ library DeployMembership {
|
|
|
13
13
|
using DynamicArrayLib for DynamicArrayLib.DynamicArray;
|
|
14
14
|
|
|
15
15
|
function selectors() internal pure returns (bytes4[] memory res) {
|
|
16
|
-
DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(
|
|
16
|
+
DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(23);
|
|
17
17
|
|
|
18
18
|
// Funds
|
|
19
19
|
arr.p(IMembership.revenue.selector);
|
|
@@ -50,6 +50,7 @@ library DeployMembership {
|
|
|
50
50
|
|
|
51
51
|
// Currency
|
|
52
52
|
arr.p(IMembership.getMembershipCurrency.selector);
|
|
53
|
+
arr.p(IMembership.setMembershipCurrency.selector);
|
|
53
54
|
|
|
54
55
|
// Image
|
|
55
56
|
arr.p(IMembership.setMembershipImage.selector);
|
|
@@ -16,7 +16,7 @@ contract DeployMockERC20 is Deployer {
|
|
|
16
16
|
// address predeterminedAddress = vm.computeCreate2Address(salt, initCodeHash);
|
|
17
17
|
|
|
18
18
|
vm.startBroadcast(deployer);
|
|
19
|
-
MockERC20 deployment = new MockERC20("TownsTest", "TToken");
|
|
19
|
+
MockERC20 deployment = new MockERC20("TownsTest", "TToken", 18);
|
|
20
20
|
vm.stopBroadcast();
|
|
21
21
|
|
|
22
22
|
return address(deployment);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {Deployer} from "scripts/common/Deployer.s.sol";
|
|
5
|
+
import {MockERC20} from "test/mocks/MockERC20.sol";
|
|
6
|
+
|
|
7
|
+
contract DeployMockUSDC is Deployer {
|
|
8
|
+
function versionName() public pure override returns (string memory) {
|
|
9
|
+
return "utils/mockUSDC";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function __deploy(address deployer) internal override returns (address) {
|
|
13
|
+
vm.startBroadcast(deployer);
|
|
14
|
+
MockERC20 deployment = new MockERC20("USD Coin", "USDC", 6);
|
|
15
|
+
vm.stopBroadcast();
|
|
16
|
+
|
|
17
|
+
return address(deployment);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -38,8 +38,11 @@ contract InteractBaseAlpha is AlphaHelper {
|
|
|
38
38
|
executeDiamondCutsWithLogging(deployer, space, "Space", deploySpace);
|
|
39
39
|
executeDiamondCutsWithLogging(deployer, spaceOwner, "SpaceOwner", deploySpaceOwner);
|
|
40
40
|
|
|
41
|
+
deploySpaceFactory.diamondInitParams(deployer);
|
|
41
42
|
address spaceFactoryInit = deploySpaceFactory.spaceFactoryInit();
|
|
42
43
|
bytes memory initData = deploySpaceFactory.spaceFactoryInitData();
|
|
44
|
+
deploySpaceFactory.clearCuts();
|
|
45
|
+
|
|
43
46
|
executeDiamondCutsWithLogging(
|
|
44
47
|
deployer,
|
|
45
48
|
spaceFactory,
|
|
@@ -39,6 +39,7 @@ contract InteractPostDeploy is Interaction {
|
|
|
39
39
|
address riverAirdrop = getDeployment("riverAirdrop");
|
|
40
40
|
address appRegistry = getDeployment("appRegistry");
|
|
41
41
|
address subscriptionModule = getDeployment("subscriptionModule");
|
|
42
|
+
address mockUSDC = getDeployment("mockUSDC");
|
|
42
43
|
address townsBase = deployTownsBase.deploy(deployer);
|
|
43
44
|
address proxyDelegation = deployProxyDelegation.deploy(deployer);
|
|
44
45
|
|
|
@@ -59,6 +60,16 @@ contract InteractPostDeploy is Interaction {
|
|
|
59
60
|
INodeOperator operatorFacet = INodeOperator(baseRegistry);
|
|
60
61
|
operatorFacet.registerOperator(OPERATOR);
|
|
61
62
|
operatorFacet.setOperatorStatus(OPERATOR, NodeOperatorStatus.Approved);
|
|
63
|
+
|
|
64
|
+
// Configure membership fee for mock USDC
|
|
65
|
+
IFeeManager(spaceFactory).setFeeConfig(
|
|
66
|
+
FeeTypesLib.membership(mockUSDC),
|
|
67
|
+
deployer,
|
|
68
|
+
FeeCalculationMethod.HYBRID,
|
|
69
|
+
1000, // 10%
|
|
70
|
+
1_500_000, // $1.50 (6 decimals)
|
|
71
|
+
true
|
|
72
|
+
);
|
|
62
73
|
vm.stopBroadcast();
|
|
63
74
|
}
|
|
64
75
|
}
|
|
@@ -9,7 +9,6 @@ import {ITownsApp} from "../../ITownsApp.sol";
|
|
|
9
9
|
import {IAppRegistryBase} from "./IAppRegistry.sol";
|
|
10
10
|
import {ISchemaResolver} from "@ethereum-attestation-service/eas-contracts/resolver/ISchemaResolver.sol";
|
|
11
11
|
import {IPlatformRequirements} from "../../../factory/facets/platform/requirements/IPlatformRequirements.sol";
|
|
12
|
-
import {IERC173} from "@towns-protocol/diamond/src/facets/ownable/IERC173.sol";
|
|
13
12
|
import {IAppAccount} from "../../../spaces/facets/account/IAppAccount.sol";
|
|
14
13
|
import {IEntitlementsManager} from "../../../spaces/facets/entitlements/IEntitlementsManager.sol";
|
|
15
14
|
|
|
@@ -320,7 +319,7 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
|
|
|
320
319
|
uint256 installPrice
|
|
321
320
|
) internal view returns (uint256 totalRequired, uint256 protocolFee) {
|
|
322
321
|
protocolFee = _getProtocolFee(installPrice);
|
|
323
|
-
if (installPrice == 0) return (
|
|
322
|
+
if (installPrice == 0) return (0, protocolFee);
|
|
324
323
|
return (installPrice + protocolFee, protocolFee);
|
|
325
324
|
}
|
|
326
325
|
|
|
@@ -332,6 +331,9 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
|
|
|
332
331
|
function _chargeForInstall(address payer, address recipient, uint256 installPrice) internal {
|
|
333
332
|
(uint256 totalRequired, uint256 protocolFee) = _getTotalRequiredPayment(installPrice);
|
|
334
333
|
|
|
334
|
+
if (totalRequired == 0 && msg.value != 0) UnexpectedETH.selector.revertWith();
|
|
335
|
+
if (totalRequired == 0) return;
|
|
336
|
+
|
|
335
337
|
if (msg.value < totalRequired) InsufficientPayment.selector.revertWith();
|
|
336
338
|
|
|
337
339
|
// Cache platform requirements to avoid multiple storage reads
|
|
@@ -3,7 +3,6 @@ pragma solidity ^0.8.23;
|
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
5
|
import {ISchemaResolver} from "@ethereum-attestation-service/eas-contracts/resolver/ISchemaResolver.sol";
|
|
6
|
-
import {IAppAccount} from "../../../spaces/facets/account/IAppAccount.sol";
|
|
7
6
|
import {ITownsApp} from "../../ITownsApp.sol";
|
|
8
7
|
|
|
9
8
|
// libraries
|
|
@@ -39,6 +38,7 @@ interface IAppRegistryBase {
|
|
|
39
38
|
error NotAllowed();
|
|
40
39
|
error ClientAlreadyRegistered();
|
|
41
40
|
error ClientNotRegistered();
|
|
41
|
+
error UnexpectedETH();
|
|
42
42
|
|
|
43
43
|
error AppRegistry__InvalidDuration();
|
|
44
44
|
error AppRegistry__InvalidPrice();
|
|
@@ -97,6 +97,7 @@ interface IArchitectBase {
|
|
|
97
97
|
error Architect__InvalidAddress();
|
|
98
98
|
error Architect__NotContract();
|
|
99
99
|
error Architect__InvalidPricingModule();
|
|
100
|
+
error Architect__UnexpectedETH();
|
|
100
101
|
|
|
101
102
|
event Architect__ProxyInitializerSet(address indexed proxyInitializer);
|
|
102
103
|
}
|
|
@@ -12,7 +12,6 @@ import {IUserEntitlement} from "../../../spaces/entitlements/user/IUserEntitleme
|
|
|
12
12
|
import {IChannel} from "../../../spaces/facets/channels/IChannel.sol";
|
|
13
13
|
import {IEntitlementsManager} from "../../../spaces/facets/entitlements/IEntitlementsManager.sol";
|
|
14
14
|
import {IMembershipBase} from "../../../spaces/facets/membership/IMembership.sol";
|
|
15
|
-
import {IPrepay} from "../../../spaces/facets/prepay/IPrepay.sol";
|
|
16
15
|
import {IRoles, IRolesBase} from "../../../spaces/facets/roles/IRoles.sol";
|
|
17
16
|
import {IArchitectBase} from "../architect/IArchitect.sol";
|
|
18
17
|
|
|
@@ -43,6 +42,7 @@ abstract contract CreateSpaceBase is IArchitectBase {
|
|
|
43
42
|
CreateSpace calldata space,
|
|
44
43
|
SpaceOptions memory spaceOptions
|
|
45
44
|
) internal returns (address spaceAddress) {
|
|
45
|
+
if (msg.value > 0) revert Architect__UnexpectedETH();
|
|
46
46
|
Validator.checkAddress(space.membership.settings.pricingModule);
|
|
47
47
|
Validator.checkAddress(spaceOptions.to);
|
|
48
48
|
|
|
@@ -54,10 +54,6 @@ abstract contract CreateSpaceBase is IArchitectBase {
|
|
|
54
54
|
space.channel,
|
|
55
55
|
spaceOptions
|
|
56
56
|
);
|
|
57
|
-
|
|
58
|
-
if (space.prepay.supply > 0) {
|
|
59
|
-
IPrepay(spaceAddress).prepayMembership{value: msg.value}(space.prepay.supply);
|
|
60
|
-
}
|
|
61
57
|
}
|
|
62
58
|
|
|
63
59
|
/// @dev Converts legacy CreateSpaceOld format and creates space
|
|
@@ -65,6 +61,7 @@ abstract contract CreateSpaceBase is IArchitectBase {
|
|
|
65
61
|
CreateSpaceOld calldata space,
|
|
66
62
|
SpaceOptions memory spaceOptions
|
|
67
63
|
) internal returns (address spaceAddress) {
|
|
64
|
+
if (msg.value > 0) revert Architect__UnexpectedETH();
|
|
68
65
|
Validator.checkAddress(space.membership.settings.pricingModule);
|
|
69
66
|
Validator.checkAddress(spaceOptions.to);
|
|
70
67
|
|
|
@@ -84,10 +81,6 @@ abstract contract CreateSpaceBase is IArchitectBase {
|
|
|
84
81
|
space.channel,
|
|
85
82
|
spaceOptions
|
|
86
83
|
);
|
|
87
|
-
|
|
88
|
-
if (space.prepay.supply > 0) {
|
|
89
|
-
IPrepay(spaceAddress).prepayMembership{value: msg.value}(space.prepay.supply);
|
|
90
|
-
}
|
|
91
84
|
}
|
|
92
85
|
|
|
93
86
|
function _createSpaceCore(
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
pragma solidity ^0.8.23;
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
|
-
import {
|
|
6
|
-
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
|
|
5
|
+
import {IFeatureManager} from "./IFeatureManager.sol";
|
|
7
6
|
|
|
8
7
|
// libraries
|
|
9
|
-
import
|
|
10
|
-
import {FeatureCondition} from "./IFeatureManagerFacet.sol";
|
|
8
|
+
import "./FeatureManagerMod.sol" as FeatureManagerMod;
|
|
11
9
|
|
|
12
10
|
// contracts
|
|
13
11
|
import {OwnableBase} from "@towns-protocol/diamond/src/facets/ownable/OwnableBase.sol";
|
|
@@ -16,61 +14,66 @@ import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
|
|
|
16
14
|
/// @title FeatureManagerFacet
|
|
17
15
|
/// @notice Manages feature conditions and checks for spaces
|
|
18
16
|
/// @dev This facet is responsible for managing feature conditions and checking if a space meets the condition for a feature to be enabled
|
|
19
|
-
contract FeatureManagerFacet is
|
|
17
|
+
contract FeatureManagerFacet is IFeatureManager, OwnableBase, Facet {
|
|
20
18
|
function __FeatureManagerFacet_init() external onlyInitializing {
|
|
21
|
-
_addInterface(type(
|
|
19
|
+
_addInterface(type(IFeatureManager).interfaceId);
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
/// @inheritdoc
|
|
22
|
+
/// @inheritdoc IFeatureManager
|
|
25
23
|
function setFeatureCondition(
|
|
26
24
|
bytes32 featureId,
|
|
27
|
-
FeatureCondition calldata condition
|
|
25
|
+
FeatureManagerMod.FeatureCondition calldata condition
|
|
28
26
|
) external onlyOwner {
|
|
29
|
-
|
|
30
|
-
emit FeatureConditionSet(featureId, condition);
|
|
27
|
+
FeatureManagerMod.upsertFeatureCondition(featureId, condition, true);
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
/// @inheritdoc
|
|
30
|
+
/// @inheritdoc IFeatureManager
|
|
34
31
|
function updateFeatureCondition(
|
|
35
32
|
bytes32 featureId,
|
|
36
|
-
FeatureCondition calldata condition
|
|
33
|
+
FeatureManagerMod.FeatureCondition calldata condition
|
|
37
34
|
) external onlyOwner {
|
|
38
|
-
|
|
39
|
-
emit FeatureConditionSet(featureId, condition);
|
|
35
|
+
FeatureManagerMod.upsertFeatureCondition(featureId, condition, false);
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
/// @inheritdoc
|
|
38
|
+
/// @inheritdoc IFeatureManager
|
|
43
39
|
function disableFeatureCondition(bytes32 featureId) external onlyOwner {
|
|
44
|
-
|
|
45
|
-
emit FeatureConditionDisabled(featureId);
|
|
40
|
+
FeatureManagerMod.disableFeatureCondition(featureId);
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
/// @inheritdoc
|
|
43
|
+
/// @inheritdoc IFeatureManager
|
|
49
44
|
function getFeatureCondition(
|
|
50
45
|
bytes32 featureId
|
|
51
|
-
) external view returns (FeatureCondition memory result) {
|
|
46
|
+
) external view returns (FeatureManagerMod.FeatureCondition memory result) {
|
|
52
47
|
// Gas optimization: Reclaim implicit memory allocation for return variable
|
|
53
48
|
// since we're loading from storage, not using the pre-allocated memory
|
|
54
49
|
assembly ("memory-safe") {
|
|
55
50
|
mstore(0x40, result)
|
|
56
51
|
}
|
|
57
|
-
result =
|
|
52
|
+
result = FeatureManagerMod.getFeatureCondition(featureId);
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
/// @inheritdoc
|
|
61
|
-
function getFeatureConditions()
|
|
62
|
-
|
|
55
|
+
/// @inheritdoc IFeatureManager
|
|
56
|
+
function getFeatureConditions()
|
|
57
|
+
external
|
|
58
|
+
view
|
|
59
|
+
returns (FeatureManagerMod.FeatureCondition[] memory)
|
|
60
|
+
{
|
|
61
|
+
return FeatureManagerMod.getFeatureConditions();
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
/// @inheritdoc
|
|
64
|
+
/// @inheritdoc IFeatureManager
|
|
66
65
|
function getFeatureConditionsForSpace(
|
|
67
66
|
address space
|
|
68
|
-
) external view returns (FeatureCondition[] memory) {
|
|
69
|
-
return
|
|
67
|
+
) external view returns (FeatureManagerMod.FeatureCondition[] memory) {
|
|
68
|
+
return FeatureManagerMod.getFeatureConditionsForAddress(space);
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
/// @inheritdoc
|
|
73
|
-
function checkFeatureCondition(bytes32 featureId, address
|
|
74
|
-
return
|
|
71
|
+
/// @inheritdoc IFeatureManager
|
|
72
|
+
function checkFeatureCondition(bytes32 featureId, address addr) external view returns (bool) {
|
|
73
|
+
return
|
|
74
|
+
FeatureManagerMod.isValidCondition(
|
|
75
|
+
FeatureManagerMod.getFeatureCondition(featureId),
|
|
76
|
+
addr
|
|
77
|
+
);
|
|
75
78
|
}
|
|
76
79
|
}
|