@towns-protocol/contracts 0.0.353 → 0.0.355
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/package.json +3 -3
- package/scripts/deployments/diamonds/DeployAppRegistry.s.sol +3 -0
- package/scripts/deployments/facets/DeployAppAccount.s.sol +2 -1
- package/scripts/deployments/facets/DeployAppRegistryFacet.s.sol +3 -1
- package/scripts/interactions/InteractDiamondCut.s.sol +14 -16
- package/src/apps/facets/attest/AttestationBase.sol +2 -2
- package/src/apps/facets/registry/AppRegistryBase.sol +132 -56
- package/src/apps/facets/registry/AppRegistryFacet.sol +43 -18
- package/src/apps/facets/registry/AppRegistryStorage.sol +9 -13
- package/src/apps/facets/registry/IAppRegistry.sol +7 -0
- package/src/apps/helpers/ISimpleApp.sol +8 -0
- package/src/apps/helpers/SimpleApp.sol +16 -10
- package/src/apps/modules/subscription/SubscriptionModuleFacet.sol +52 -29
- package/src/spaces/facets/account/AppAccount.sol +6 -0
- package/src/spaces/facets/account/AppAccountBase.sol +82 -4
- package/src/spaces/facets/account/IAppAccount.sol +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towns-protocol/contracts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.355",
|
|
4
4
|
"packageManager": "yarn@3.8.0",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build-types": "bash scripts/build-contract-types.sh",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@layerzerolabs/oapp-evm": "^0.3.2",
|
|
36
36
|
"@openzeppelin/merkle-tree": "^1.0.8",
|
|
37
37
|
"@prb/test": "^0.6.4",
|
|
38
|
-
"@towns-protocol/prettier-config": "^0.0.
|
|
38
|
+
"@towns-protocol/prettier-config": "^0.0.355",
|
|
39
39
|
"@typechain/ethers-v5": "^10.1.1",
|
|
40
40
|
"@wagmi/cli": "^2.2.0",
|
|
41
41
|
"forge-std": "github:foundry-rs/forge-std#v1.10.0",
|
|
@@ -57,5 +57,5 @@
|
|
|
57
57
|
"publishConfig": {
|
|
58
58
|
"access": "public"
|
|
59
59
|
},
|
|
60
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "f0192ebd59ae6a99003a0753d1821713e2407bc5"
|
|
61
61
|
}
|
|
@@ -56,6 +56,9 @@ contract DeployAppRegistry is IDiamondInitHelper, DiamondHelper, Deployer {
|
|
|
56
56
|
facetHelper.add("OwnableFacet");
|
|
57
57
|
facetHelper.add("MetadataFacet");
|
|
58
58
|
|
|
59
|
+
// Deploy the first batch of facets
|
|
60
|
+
facetHelper.deployBatch(deployer);
|
|
61
|
+
|
|
59
62
|
// Get predicted addresses
|
|
60
63
|
address facet = facetHelper.predictAddress("DiamondCutFacet");
|
|
61
64
|
addFacet(
|
|
@@ -15,11 +15,12 @@ library DeployAppAccount {
|
|
|
15
15
|
using DynamicArrayLib for DynamicArrayLib.DynamicArray;
|
|
16
16
|
|
|
17
17
|
function selectors() internal pure returns (bytes4[] memory res) {
|
|
18
|
-
DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(
|
|
18
|
+
DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(13);
|
|
19
19
|
arr.p(AppAccount.execute.selector);
|
|
20
20
|
arr.p(AppAccount.onInstallApp.selector);
|
|
21
21
|
arr.p(AppAccount.onUninstallApp.selector);
|
|
22
22
|
arr.p(AppAccount.onRenewApp.selector);
|
|
23
|
+
arr.p(AppAccount.onUpdateApp.selector);
|
|
23
24
|
arr.p(AppAccount.isAppExecuting.selector);
|
|
24
25
|
arr.p(AppAccount.isAppEntitled.selector);
|
|
25
26
|
arr.p(AppAccount.disableApp.selector);
|
|
@@ -16,7 +16,7 @@ library DeployAppRegistryFacet {
|
|
|
16
16
|
using DynamicArrayLib for DynamicArrayLib.DynamicArray;
|
|
17
17
|
|
|
18
18
|
function selectors() internal pure returns (bytes4[] memory res) {
|
|
19
|
-
DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(
|
|
19
|
+
DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(18);
|
|
20
20
|
arr.p(AppRegistryFacet.getAppSchema.selector);
|
|
21
21
|
arr.p(AppRegistryFacet.getAppSchemaId.selector);
|
|
22
22
|
arr.p(AppRegistryFacet.getAppById.selector);
|
|
@@ -24,8 +24,10 @@ library DeployAppRegistryFacet {
|
|
|
24
24
|
arr.p(AppRegistryFacet.registerApp.selector);
|
|
25
25
|
arr.p(AppRegistryFacet.removeApp.selector);
|
|
26
26
|
arr.p(AppRegistryFacet.createApp.selector);
|
|
27
|
+
arr.p(AppRegistryFacet.upgradeApp.selector);
|
|
27
28
|
arr.p(AppRegistryFacet.installApp.selector);
|
|
28
29
|
arr.p(AppRegistryFacet.uninstallApp.selector);
|
|
30
|
+
arr.p(AppRegistryFacet.updateApp.selector);
|
|
29
31
|
arr.p(AppRegistryFacet.getAppPrice.selector);
|
|
30
32
|
arr.p(AppRegistryFacet.getAppDuration.selector);
|
|
31
33
|
arr.p(AppRegistryFacet.adminRegisterAppSchema.selector);
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
pragma solidity ^0.8.23;
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
|
-
import {IDiamond} from "@towns-protocol/diamond/src/Diamond.sol";
|
|
6
5
|
import {IDiamondCut} from "@towns-protocol/diamond/src/facets/cut/IDiamondCut.sol";
|
|
7
6
|
|
|
8
7
|
// libraries
|
|
@@ -12,16 +11,19 @@ import {console} from "forge-std/console.sol";
|
|
|
12
11
|
import {Interaction} from "../common/Interaction.s.sol";
|
|
13
12
|
import {AlphaHelper} from "./helpers/AlphaHelper.sol";
|
|
14
13
|
|
|
15
|
-
// facet
|
|
16
|
-
import {
|
|
14
|
+
// fetch facet deployer contract
|
|
15
|
+
import {DeploySubscriptionModuleFacet} from "scripts/deployments/facets/DeploySubscriptionModuleFacet.s.sol";
|
|
17
16
|
|
|
18
17
|
contract InteractDiamondCut is Interaction, AlphaHelper {
|
|
19
18
|
function __interact(address deployer) internal override {
|
|
20
|
-
|
|
21
|
-
address
|
|
19
|
+
// update with the diamond to cut
|
|
20
|
+
address diamond = getDeployment("subscriptionModule");
|
|
21
|
+
|
|
22
|
+
// update with the facet to remove
|
|
23
|
+
address facetToRemove = 0xf86be0b52aABa39C3fAAa2a78e340a658B90d9DB;
|
|
22
24
|
|
|
23
25
|
address[] memory facetAddresses = new address[](1);
|
|
24
|
-
facetAddresses[0] =
|
|
26
|
+
facetAddresses[0] = facetToRemove;
|
|
25
27
|
|
|
26
28
|
// add the diamond cut to remove the facet
|
|
27
29
|
addCutsToRemove(diamond, facetAddresses);
|
|
@@ -29,19 +31,15 @@ contract InteractDiamondCut is Interaction, AlphaHelper {
|
|
|
29
31
|
// deploy the new facet
|
|
30
32
|
console.log("deployer", deployer);
|
|
31
33
|
vm.setEnv("OVERRIDE_DEPLOYMENTS", "1");
|
|
32
|
-
vm.broadcast(deployer);
|
|
33
|
-
spaceOwnerFacet = DeploySpaceOwnerFacet.deploy();
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
35
|
+
// update with a call to the deploy function from your facet deployer contract
|
|
36
|
+
address facetAddress = DeploySubscriptionModuleFacet.deploy();
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// update with a call to the makeCut function from your facet deployer contract
|
|
39
|
+
addCut(DeploySubscriptionModuleFacet.makeCut(facetAddress, FacetCutAction.Add));
|
|
39
40
|
|
|
41
|
+
// execute the diamond cut
|
|
40
42
|
vm.broadcast(deployer);
|
|
41
|
-
IDiamondCut(diamond).diamondCut(
|
|
42
|
-
baseFacets(),
|
|
43
|
-
initData.length > 0 ? spaceOwnerFacet : address(0),
|
|
44
|
-
initData
|
|
45
|
-
);
|
|
43
|
+
IDiamondCut(diamond).diamondCut(baseFacets(), address(0), "");
|
|
46
44
|
}
|
|
47
45
|
}
|
|
@@ -4,7 +4,6 @@ pragma solidity ^0.8.23;
|
|
|
4
4
|
// interfaces
|
|
5
5
|
import {ISchemaResolver} from "@ethereum-attestation-service/eas-contracts/resolver/ISchemaResolver.sol";
|
|
6
6
|
import {IAttestationRegistryBase} from "./IAttestationRegistry.sol";
|
|
7
|
-
import {ISchemaBase} from "../schema/ISchema.sol";
|
|
8
7
|
|
|
9
8
|
// libraries
|
|
10
9
|
import {CustomRevert} from "src/utils/libraries/CustomRevert.sol";
|
|
@@ -14,7 +13,7 @@ import {AttestationStorage} from "./AttestationStorage.sol";
|
|
|
14
13
|
|
|
15
14
|
// types
|
|
16
15
|
import {SchemaStorage} from "../schema/SchemaStorage.sol";
|
|
17
|
-
import {Attestation, EMPTY_UID, NO_EXPIRATION_TIME
|
|
16
|
+
import {Attestation, EMPTY_UID, NO_EXPIRATION_TIME} from "@ethereum-attestation-service/eas-contracts/Common.sol";
|
|
18
17
|
import {AttestationRequest, AttestationRequestData, IEAS, RevocationRequestData} from "@ethereum-attestation-service/eas-contracts/IEAS.sol";
|
|
19
18
|
import {SchemaRecord} from "@ethereum-attestation-service/eas-contracts/ISchemaRegistry.sol";
|
|
20
19
|
|
|
@@ -154,6 +153,7 @@ abstract contract AttestationBase is IAttestationRegistryBase {
|
|
|
154
153
|
|
|
155
154
|
// Get the resolver contract for this schema
|
|
156
155
|
ISchemaResolver resolver = ISchemaResolver(schema.resolver);
|
|
156
|
+
|
|
157
157
|
// If no resolver is set, handle zero-value case and refund if this is the last batch
|
|
158
158
|
if (address(resolver) == address(0)) {
|
|
159
159
|
_refundIfZeroValue(values, availableValue, last);
|
|
@@ -16,7 +16,7 @@ import {IAppAccount} from "../../../spaces/facets/account/IAppAccount.sol";
|
|
|
16
16
|
// libraries
|
|
17
17
|
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
18
18
|
import {BasisPoints} from "../../../utils/libraries/BasisPoints.sol";
|
|
19
|
-
import {AppRegistryStorage} from "./AppRegistryStorage.sol";
|
|
19
|
+
import {AppRegistryStorage, ClientInfo, AppInfo} from "./AppRegistryStorage.sol";
|
|
20
20
|
import {LibClone} from "solady/utils/LibClone.sol";
|
|
21
21
|
import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";
|
|
22
22
|
import {CurrencyTransfer} from "../../../utils/libraries/CurrencyTransfer.sol";
|
|
@@ -73,7 +73,7 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
|
|
|
73
73
|
/// @param app The address of the app
|
|
74
74
|
/// @return The latest version ID
|
|
75
75
|
function _getLatestAppId(address app) internal view returns (bytes32) {
|
|
76
|
-
|
|
76
|
+
AppInfo storage appInfo = AppRegistryStorage.getLayout().apps[app];
|
|
77
77
|
return appInfo.latestVersion;
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -140,44 +140,77 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
|
|
|
140
140
|
duration
|
|
141
141
|
);
|
|
142
142
|
|
|
143
|
-
version = _registerApp(app, params.client);
|
|
143
|
+
version = _registerApp(ITownsApp(app), params.client);
|
|
144
144
|
emit AppCreated(app, version);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
_verifyAddAppInputs(app, client);
|
|
147
|
+
function _upgradeApp(
|
|
148
|
+
ITownsApp app,
|
|
149
|
+
address client,
|
|
150
|
+
bytes32 versionId
|
|
151
|
+
) internal returns (bytes32 newVersionId) {
|
|
152
|
+
if (versionId == EMPTY_UID) InvalidAppId.selector.revertWith();
|
|
154
153
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
(
|
|
155
|
+
address owner,
|
|
156
|
+
bytes32[] memory permissions,
|
|
157
|
+
ExecutionManifest memory manifest,
|
|
158
|
+
uint48 duration
|
|
159
|
+
) = _validateApp(app, client);
|
|
158
160
|
|
|
159
|
-
if (
|
|
161
|
+
if (msg.sender != owner) NotAllowed.selector.revertWith();
|
|
162
|
+
|
|
163
|
+
address appAddress = address(app);
|
|
164
|
+
|
|
165
|
+
AppRegistryStorage.Layout storage $ = AppRegistryStorage.getLayout();
|
|
166
|
+
AppInfo storage appInfo = $.apps[appAddress];
|
|
160
167
|
if (appInfo.isBanned) BannedApp.selector.revertWith();
|
|
168
|
+
if (appInfo.latestVersion != versionId) InvalidAppId.selector.revertWith();
|
|
161
169
|
|
|
162
|
-
|
|
170
|
+
ClientInfo storage clientInfo = $.client[client];
|
|
171
|
+
if (clientInfo.app == address(0)) ClientNotRegistered.selector.revertWith();
|
|
163
172
|
|
|
164
|
-
|
|
165
|
-
|
|
173
|
+
App memory appData = App({
|
|
174
|
+
appId: versionId,
|
|
175
|
+
module: appAddress,
|
|
176
|
+
owner: owner,
|
|
177
|
+
client: client,
|
|
178
|
+
permissions: permissions,
|
|
179
|
+
manifest: manifest,
|
|
180
|
+
duration: duration
|
|
181
|
+
});
|
|
166
182
|
|
|
167
|
-
|
|
168
|
-
uint48 duration = _validateDuration(accessDuration);
|
|
183
|
+
newVersionId = _attestApp(appData);
|
|
169
184
|
|
|
170
|
-
|
|
171
|
-
|
|
185
|
+
appInfo.latestVersion = newVersionId;
|
|
186
|
+
emit AppUpgraded(appAddress, versionId, newVersionId);
|
|
187
|
+
}
|
|
172
188
|
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
/// @notice Registers a new app in the registry
|
|
190
|
+
/// @param app The address of the app to register
|
|
191
|
+
/// @param client The client address that can use the app
|
|
192
|
+
/// @return version The version ID of the registered app
|
|
193
|
+
/// @dev Reverts if app is banned, inputs are invalid, or caller is not the owner
|
|
194
|
+
function _registerApp(ITownsApp app, address client) internal returns (bytes32 version) {
|
|
195
|
+
(
|
|
196
|
+
address owner,
|
|
197
|
+
bytes32[] memory permissions,
|
|
198
|
+
ExecutionManifest memory manifest,
|
|
199
|
+
uint48 duration
|
|
200
|
+
) = _validateApp(app, client);
|
|
175
201
|
|
|
176
|
-
|
|
202
|
+
address appAddress = address(app);
|
|
203
|
+
|
|
204
|
+
AppRegistryStorage.Layout storage $ = AppRegistryStorage.getLayout();
|
|
205
|
+
AppInfo storage appInfo = $.apps[appAddress];
|
|
206
|
+
ClientInfo storage clientInfo = $.client[client];
|
|
207
|
+
|
|
208
|
+
if (appInfo.isBanned) BannedApp.selector.revertWith();
|
|
209
|
+
if (clientInfo.app != address(0)) ClientAlreadyRegistered.selector.revertWith();
|
|
177
210
|
|
|
178
211
|
App memory appData = App({
|
|
179
212
|
appId: EMPTY_UID,
|
|
180
|
-
module:
|
|
213
|
+
module: appAddress,
|
|
181
214
|
owner: owner,
|
|
182
215
|
client: client,
|
|
183
216
|
permissions: permissions,
|
|
@@ -185,27 +218,19 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
|
|
|
185
218
|
duration: duration
|
|
186
219
|
});
|
|
187
220
|
|
|
188
|
-
|
|
189
|
-
request.schema = _getSchemaId();
|
|
190
|
-
request.data.recipient = app;
|
|
191
|
-
request.data.revocable = true;
|
|
192
|
-
request.data.refUID = appInfo.latestVersion;
|
|
193
|
-
request.data.data = abi.encode(appData);
|
|
194
|
-
version = _attest(msg.sender, msg.value, request).uid;
|
|
195
|
-
|
|
221
|
+
version = _attestApp(appData);
|
|
196
222
|
appInfo.latestVersion = version;
|
|
197
|
-
appInfo.app =
|
|
198
|
-
clientInfo.app =
|
|
223
|
+
appInfo.app = appAddress;
|
|
224
|
+
clientInfo.app = appAddress;
|
|
199
225
|
|
|
200
|
-
emit AppRegistered(
|
|
226
|
+
emit AppRegistered(appAddress, version);
|
|
201
227
|
}
|
|
202
228
|
|
|
203
229
|
/// @notice Removes a app from the registry
|
|
204
|
-
/// @param revoker The address revoking the app
|
|
205
230
|
/// @param appId The version ID of the app to remove
|
|
206
231
|
/// @dev Reverts if app is not registered, revoked, or banned
|
|
207
232
|
/// @dev Spaces that install this app will need to uninstall it
|
|
208
|
-
function _removeApp(
|
|
233
|
+
function _removeApp(bytes32 appId) internal {
|
|
209
234
|
if (appId == EMPTY_UID) InvalidAppId.selector.revertWith();
|
|
210
235
|
|
|
211
236
|
Attestation memory att = _getAttestation(appId);
|
|
@@ -214,20 +239,17 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
|
|
|
214
239
|
if (att.revocationTime > 0) AppRevoked.selector.revertWith();
|
|
215
240
|
|
|
216
241
|
App memory appData = abi.decode(att.data, (App));
|
|
242
|
+
if (appData.owner != msg.sender) NotAllowed.selector.revertWith();
|
|
217
243
|
|
|
218
|
-
|
|
219
|
-
appData.module
|
|
220
|
-
];
|
|
244
|
+
AppInfo storage appInfo = AppRegistryStorage.getLayout().apps[appData.module];
|
|
221
245
|
|
|
222
246
|
if (appInfo.isBanned) BannedApp.selector.revertWith();
|
|
223
247
|
|
|
224
248
|
RevocationRequestData memory request;
|
|
225
249
|
request.uid = appId;
|
|
226
|
-
_revoke(att.schema, request,
|
|
250
|
+
_revoke(att.schema, request, msg.sender, 0, true);
|
|
227
251
|
|
|
228
|
-
|
|
229
|
-
appData.client
|
|
230
|
-
];
|
|
252
|
+
ClientInfo storage clientInfo = AppRegistryStorage.getLayout().client[appData.client];
|
|
231
253
|
clientInfo.app = address(0);
|
|
232
254
|
|
|
233
255
|
emit AppUnregistered(appData.module, appId);
|
|
@@ -263,6 +285,20 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
|
|
|
263
285
|
emit AppUninstalled(app, address(account), appId);
|
|
264
286
|
}
|
|
265
287
|
|
|
288
|
+
function _updateApp(address app, address space) internal {
|
|
289
|
+
if (_isBanned(app)) BannedApp.selector.revertWith();
|
|
290
|
+
|
|
291
|
+
bytes32 appId = _getLatestAppId(app);
|
|
292
|
+
if (appId == EMPTY_UID) AppNotInstalled.selector.revertWith();
|
|
293
|
+
|
|
294
|
+
Attestation memory att = _getAttestation(appId);
|
|
295
|
+
if (att.uid == EMPTY_UID) AppNotRegistered.selector.revertWith();
|
|
296
|
+
if (att.revocationTime > 0) AppRevoked.selector.revertWith();
|
|
297
|
+
|
|
298
|
+
IAppAccount(space).onUpdateApp(appId, abi.encode(app));
|
|
299
|
+
emit AppUpdated(app, space, appId);
|
|
300
|
+
}
|
|
301
|
+
|
|
266
302
|
function _renewApp(address app, address account, bytes calldata data) internal {
|
|
267
303
|
bytes32 appId = IAppAccount(account).getAppId(app);
|
|
268
304
|
if (appId == EMPTY_UID) AppNotInstalled.selector.revertWith();
|
|
@@ -356,7 +392,7 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
|
|
|
356
392
|
function _banApp(address app) internal returns (bytes32 version) {
|
|
357
393
|
if (app == address(0)) AppNotRegistered.selector.revertWith();
|
|
358
394
|
|
|
359
|
-
|
|
395
|
+
AppInfo storage appInfo = AppRegistryStorage.getLayout().apps[app];
|
|
360
396
|
|
|
361
397
|
if (appInfo.app == address(0)) AppNotRegistered.selector.revertWith();
|
|
362
398
|
if (appInfo.isBanned) BannedApp.selector.revertWith();
|
|
@@ -368,10 +404,20 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
|
|
|
368
404
|
return appInfo.latestVersion;
|
|
369
405
|
}
|
|
370
406
|
|
|
371
|
-
function
|
|
372
|
-
|
|
373
|
-
|
|
407
|
+
function _attestApp(App memory appData) internal returns (bytes32 newVersionId) {
|
|
408
|
+
AttestationRequest memory request;
|
|
409
|
+
request.schema = _getSchemaId();
|
|
410
|
+
request.data.recipient = appData.module;
|
|
411
|
+
request.data.revocable = true;
|
|
412
|
+
request.data.refUID = appData.appId;
|
|
413
|
+
request.data.data = abi.encode(appData);
|
|
414
|
+
newVersionId = _attest(msg.sender, msg.value, request).uid;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function _validatePricing(uint256 price) internal view returns (uint256) {
|
|
418
|
+
uint256 minPlatformFee = _getPlatformRequirements().getMembershipFee();
|
|
374
419
|
if (price > 0 && price < minPlatformFee) InvalidPrice.selector.revertWith();
|
|
420
|
+
return price;
|
|
375
421
|
}
|
|
376
422
|
|
|
377
423
|
function _validateDuration(uint48 duration) internal pure returns (uint48) {
|
|
@@ -380,21 +426,51 @@ abstract contract AppRegistryBase is IAppRegistryBase, SchemaBase, AttestationBa
|
|
|
380
426
|
return duration;
|
|
381
427
|
}
|
|
382
428
|
|
|
383
|
-
/// @notice
|
|
384
|
-
/// @param
|
|
429
|
+
/// @notice Validates inputs for adding a new app
|
|
430
|
+
/// @param appContract The app contract to verify
|
|
385
431
|
/// @param client The client address to verify
|
|
386
432
|
/// @dev Reverts if any input is invalid or app doesn't implement required interfaces
|
|
387
|
-
|
|
388
|
-
|
|
433
|
+
/// @return owner The owner of the app
|
|
434
|
+
/// @return permissions The permissions of the app
|
|
435
|
+
/// @return manifest The manifest of the app
|
|
436
|
+
/// @return duration The duration of the app
|
|
437
|
+
function _validateApp(
|
|
438
|
+
ITownsApp appContract,
|
|
439
|
+
address client
|
|
440
|
+
) internal view returns (address, bytes32[] memory, ExecutionManifest memory, uint48) {
|
|
441
|
+
address appAddress = address(appContract);
|
|
442
|
+
if (appAddress == address(0)) InvalidAddressInput.selector.revertWith();
|
|
389
443
|
if (client == address(0)) InvalidAddressInput.selector.revertWith();
|
|
390
444
|
|
|
445
|
+
(
|
|
446
|
+
uint256 installPrice,
|
|
447
|
+
uint48 accessDuration,
|
|
448
|
+
bytes32[] memory permissions,
|
|
449
|
+
address owner,
|
|
450
|
+
ExecutionManifest memory manifest
|
|
451
|
+
) = (
|
|
452
|
+
appContract.installPrice(),
|
|
453
|
+
appContract.accessDuration(),
|
|
454
|
+
appContract.requiredPermissions(),
|
|
455
|
+
appContract.moduleOwner(),
|
|
456
|
+
appContract.executionManifest()
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
if (permissions.length == 0) InvalidArrayInput.selector.revertWith();
|
|
460
|
+
if (owner == address(0)) InvalidAddressInput.selector.revertWith();
|
|
461
|
+
|
|
462
|
+
_validatePricing(installPrice);
|
|
463
|
+
uint48 duration = _validateDuration(accessDuration);
|
|
464
|
+
|
|
391
465
|
if (
|
|
392
|
-
!IERC165(
|
|
393
|
-
!IERC165(
|
|
394
|
-
!IERC165(
|
|
466
|
+
!IERC165(appAddress).supportsInterface(type(IModule).interfaceId) ||
|
|
467
|
+
!IERC165(appAddress).supportsInterface(type(IExecutionModule).interfaceId) ||
|
|
468
|
+
!IERC165(appAddress).supportsInterface(type(ITownsApp).interfaceId)
|
|
395
469
|
) {
|
|
396
470
|
AppDoesNotImplementInterface.selector.revertWith();
|
|
397
471
|
}
|
|
472
|
+
|
|
473
|
+
return (owner, permissions, manifest, duration);
|
|
398
474
|
}
|
|
399
475
|
|
|
400
476
|
function _getPlatformRequirements() internal view returns (IPlatformRequirements) {
|
|
@@ -52,32 +52,49 @@ contract AppRegistryFacet is IAppRegistry, AppRegistryBase, OwnableBase, Reentra
|
|
|
52
52
|
/* App Functions */
|
|
53
53
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
54
54
|
|
|
55
|
+
/// @notice Create an upgradeable simple app contract
|
|
56
|
+
/// @param params The parameters of the app
|
|
57
|
+
function createApp(
|
|
58
|
+
AppParams calldata params
|
|
59
|
+
) external payable nonReentrant returns (address app, bytes32 appId) {
|
|
60
|
+
return _createApp(params);
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
/// @notice Register a new app with permissions
|
|
56
64
|
/// @param app The app address to register
|
|
57
65
|
/// @param client The client address that will make calls from this app
|
|
58
|
-
/// @return
|
|
66
|
+
/// @return appId The app ID of the registered app
|
|
59
67
|
function registerApp(
|
|
60
68
|
ITownsApp app,
|
|
61
69
|
address client
|
|
62
|
-
) external payable nonReentrant returns (bytes32
|
|
63
|
-
return _registerApp(
|
|
70
|
+
) external payable nonReentrant returns (bytes32) {
|
|
71
|
+
return _registerApp(app, client);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
/// @notice
|
|
67
|
-
/// @param
|
|
68
|
-
/// @
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
/// @notice Upgrade an app
|
|
75
|
+
/// @param app The app address to update
|
|
76
|
+
/// @param client The client address part of the app's identity
|
|
77
|
+
/// @param appId The app ID to upgrade
|
|
78
|
+
/// @return appId The new app ID of the updated app
|
|
79
|
+
function upgradeApp(
|
|
80
|
+
ITownsApp app,
|
|
81
|
+
address client,
|
|
82
|
+
bytes32 appId
|
|
83
|
+
) external payable nonReentrant returns (bytes32) {
|
|
84
|
+
return _upgradeApp(app, client, appId);
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
/// @notice
|
|
74
|
-
/// @param
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return _createApp(params);
|
|
87
|
+
/// @notice Remove an app from the registry
|
|
88
|
+
/// @param appId The app ID to remove
|
|
89
|
+
/// @dev Only the owner of the app can remove it
|
|
90
|
+
function removeApp(bytes32 appId) external nonReentrant {
|
|
91
|
+
_removeApp(appId);
|
|
79
92
|
}
|
|
80
93
|
|
|
94
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
95
|
+
/* Space Functions */
|
|
96
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
97
|
+
|
|
81
98
|
/// @notice Install an app
|
|
82
99
|
/// @param app The app address to install
|
|
83
100
|
/// @param space The space to install the app to
|
|
@@ -93,15 +110,23 @@ contract AppRegistryFacet is IAppRegistry, AppRegistryBase, OwnableBase, Reentra
|
|
|
93
110
|
|
|
94
111
|
/// @notice Uninstall an app
|
|
95
112
|
/// @param app The app address to uninstall
|
|
96
|
-
/// @param
|
|
113
|
+
/// @param space The space to uninstall the app from
|
|
97
114
|
/// @param data The data to pass to the app's onUninstall function
|
|
98
115
|
function uninstallApp(
|
|
99
116
|
ITownsApp app,
|
|
100
|
-
IAppAccount
|
|
117
|
+
IAppAccount space,
|
|
101
118
|
bytes calldata data
|
|
102
119
|
) external nonReentrant {
|
|
103
|
-
_onlyAllowed(address(
|
|
104
|
-
|
|
120
|
+
_onlyAllowed(address(space));
|
|
121
|
+
_uninstallApp(address(app), address(space), data);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// @notice Update an app to the latest version
|
|
125
|
+
/// @param app The app address to update
|
|
126
|
+
/// @param space The space to update the app to
|
|
127
|
+
function updateApp(ITownsApp app, IAppAccount space) external nonReentrant {
|
|
128
|
+
_onlyAllowed(address(space));
|
|
129
|
+
_updateApp(address(app), address(space));
|
|
105
130
|
}
|
|
106
131
|
|
|
107
132
|
/// @notice Renew an app
|
|
@@ -7,21 +7,17 @@ pragma solidity ^0.8.23;
|
|
|
7
7
|
|
|
8
8
|
// contracts
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
struct AppInfo {
|
|
16
|
-
address app;
|
|
17
|
-
bytes32 latestVersion;
|
|
18
|
-
bool isBanned;
|
|
19
|
-
}
|
|
10
|
+
struct AppInfo {
|
|
11
|
+
address app;
|
|
12
|
+
bytes32 latestVersion;
|
|
13
|
+
bool isBanned;
|
|
14
|
+
}
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
struct ClientInfo {
|
|
17
|
+
address app;
|
|
18
|
+
}
|
|
24
19
|
|
|
20
|
+
library AppRegistryStorage {
|
|
25
21
|
struct Layout {
|
|
26
22
|
// Registered schema ID
|
|
27
23
|
bytes32 schemaId;
|
|
@@ -48,6 +48,7 @@ interface IAppRegistryBase {
|
|
|
48
48
|
error InsufficientPayment();
|
|
49
49
|
error NotAllowed();
|
|
50
50
|
error ClientAlreadyRegistered();
|
|
51
|
+
error ClientNotRegistered();
|
|
51
52
|
|
|
52
53
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
53
54
|
/* EVENTS */
|
|
@@ -61,6 +62,12 @@ interface IAppRegistryBase {
|
|
|
61
62
|
event AppInstalled(address indexed app, address indexed account, bytes32 indexed appId);
|
|
62
63
|
event AppUninstalled(address indexed app, address indexed account, bytes32 indexed appId);
|
|
63
64
|
event AppRenewed(address indexed app, address indexed account, bytes32 indexed appId);
|
|
65
|
+
event AppUpdated(address indexed app, address indexed account, bytes32 indexed appId);
|
|
66
|
+
event AppUpgraded(
|
|
67
|
+
address indexed app,
|
|
68
|
+
bytes32 indexed oldVersionId,
|
|
69
|
+
bytes32 indexed newVersionId
|
|
70
|
+
);
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
/// @title IAppRegistry Interface
|
|
@@ -23,6 +23,10 @@ interface ISimpleAppBase {
|
|
|
23
23
|
/// @param installPrice The new install price
|
|
24
24
|
/// @param accessDuration The new access duration
|
|
25
25
|
event PricingUpdated(uint256 installPrice, uint48 accessDuration);
|
|
26
|
+
|
|
27
|
+
/// @notice Emitted when permissions are updated
|
|
28
|
+
/// @param permissions The new permissions
|
|
29
|
+
event PermissionsUpdated(bytes32[] permissions);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
interface ISimpleApp is ISimpleAppBase {
|
|
@@ -35,6 +39,10 @@ interface ISimpleApp is ISimpleAppBase {
|
|
|
35
39
|
/// @param accessDuration The new access duration
|
|
36
40
|
function updatePricing(uint256 installPrice, uint48 accessDuration) external;
|
|
37
41
|
|
|
42
|
+
/// @notice Updates the permissions of the app
|
|
43
|
+
/// @param permissions The new permissions of the app
|
|
44
|
+
function updatePermissions(bytes32[] calldata permissions) external;
|
|
45
|
+
|
|
38
46
|
/// @notice Initializes the app
|
|
39
47
|
/// @param owner The owner of the app
|
|
40
48
|
/// @param appId The ID of the app
|
|
@@ -21,6 +21,7 @@ contract SimpleApp is ISimpleApp, Ownable, BaseApp, Initializable {
|
|
|
21
21
|
using CustomRevert for bytes4;
|
|
22
22
|
using SimpleAppStorage for SimpleAppStorage.Layout;
|
|
23
23
|
|
|
24
|
+
// External functions
|
|
24
25
|
/// @inheritdoc ISimpleApp
|
|
25
26
|
function initialize(
|
|
26
27
|
address owner,
|
|
@@ -59,27 +60,32 @@ contract SimpleApp is ISimpleApp, Ownable, BaseApp, Initializable {
|
|
|
59
60
|
emit PricingUpdated(installPrice, accessDuration);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
/// @inheritdoc
|
|
63
|
-
function
|
|
63
|
+
/// @inheritdoc ISimpleApp
|
|
64
|
+
function updatePermissions(bytes32[] calldata permissions) external onlyOwner {
|
|
64
65
|
SimpleAppStorage.Layout storage $ = SimpleAppStorage.getLayout();
|
|
65
|
-
|
|
66
|
+
$.permissions = permissions;
|
|
67
|
+
emit PermissionsUpdated(permissions);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// @inheritdoc IExecutionModule
|
|
71
|
+
function executionManifest() external pure returns (ExecutionManifest memory) {
|
|
72
|
+
// solhint-disable no-empty-blocks
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
// Public functions
|
|
68
76
|
/// @inheritdoc IModule
|
|
69
77
|
function moduleId() public view returns (string memory) {
|
|
70
78
|
SimpleAppStorage.Layout storage $ = SimpleAppStorage.getLayout();
|
|
71
79
|
return $.name;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
|
-
/// @inheritdoc
|
|
75
|
-
function
|
|
76
|
-
|
|
82
|
+
/// @inheritdoc ITownsApp
|
|
83
|
+
function requiredPermissions() external view returns (bytes32[] memory) {
|
|
84
|
+
SimpleAppStorage.Layout storage $ = SimpleAppStorage.getLayout();
|
|
85
|
+
return $.permissions;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
/* OVERRIDES */
|
|
81
|
-
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
82
|
-
|
|
88
|
+
// Internal functions
|
|
83
89
|
function _installPrice() internal view override returns (uint256) {
|
|
84
90
|
SimpleAppStorage.Layout storage $ = SimpleAppStorage.getLayout();
|
|
85
91
|
return $.installPrice;
|
|
@@ -8,6 +8,7 @@ import {IValidationHookModule} from "@erc6900/reference-implementation/interface
|
|
|
8
8
|
import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";
|
|
9
9
|
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
10
10
|
import {ISubscriptionModule} from "./ISubscriptionModule.sol";
|
|
11
|
+
import {IMembership} from "../../../spaces/facets/membership/IMembership.sol";
|
|
11
12
|
|
|
12
13
|
// libraries
|
|
13
14
|
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
|
|
@@ -22,7 +23,6 @@ import {Subscription, SubscriptionModuleStorage} from "./SubscriptionModuleStora
|
|
|
22
23
|
// contracts
|
|
23
24
|
import {ModuleBase} from "modular-account/src/modules/ModuleBase.sol";
|
|
24
25
|
import {OwnableBase} from "@towns-protocol/diamond/src/facets/ownable/OwnableBase.sol";
|
|
25
|
-
import {MembershipFacet} from "../../../spaces/facets/membership/MembershipFacet.sol";
|
|
26
26
|
import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
|
|
27
27
|
|
|
28
28
|
/// @title Subscription Module
|
|
@@ -75,13 +75,19 @@ contract SubscriptionModuleFacet is
|
|
|
75
75
|
|
|
76
76
|
Validator.checkAddress(space);
|
|
77
77
|
|
|
78
|
+
if (entityId == 0) SubscriptionModule__InvalidEntityId.selector.revertWith();
|
|
79
|
+
|
|
78
80
|
if (IERC721(space).ownerOf(tokenId) != msg.sender)
|
|
79
81
|
SubscriptionModule__InvalidTokenOwner.selector.revertWith();
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
IMembership membershipFacet = IMembership(space);
|
|
82
84
|
uint256 expiresAt = membershipFacet.expiresAt(tokenId);
|
|
83
85
|
|
|
84
86
|
SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
|
|
87
|
+
|
|
88
|
+
if (!$.entityIds[msg.sender].add(entityId))
|
|
89
|
+
SubscriptionModule__InvalidEntityId.selector.revertWith();
|
|
90
|
+
|
|
85
91
|
Subscription storage sub = $.subscriptions[msg.sender][entityId];
|
|
86
92
|
sub.space = space;
|
|
87
93
|
sub.active = true;
|
|
@@ -89,8 +95,6 @@ contract SubscriptionModuleFacet is
|
|
|
89
95
|
sub.installTime = uint40(block.timestamp);
|
|
90
96
|
sub.nextRenewalTime = _calculateNextRenewalTime(expiresAt, sub.installTime);
|
|
91
97
|
|
|
92
|
-
$.entityIds[msg.sender].add(entityId);
|
|
93
|
-
|
|
94
98
|
emit SubscriptionConfigured(msg.sender, entityId, space, tokenId, sub.nextRenewalTime);
|
|
95
99
|
}
|
|
96
100
|
|
|
@@ -102,6 +106,7 @@ contract SubscriptionModuleFacet is
|
|
|
102
106
|
|
|
103
107
|
if (!$.entityIds[msg.sender].remove(entityId))
|
|
104
108
|
SubscriptionModule__InvalidEntityId.selector.revertWith();
|
|
109
|
+
|
|
105
110
|
delete $.subscriptions[msg.sender][entityId];
|
|
106
111
|
|
|
107
112
|
emit SubscriptionDeactivated(msg.sender, entityId);
|
|
@@ -184,6 +189,8 @@ contract SubscriptionModuleFacet is
|
|
|
184
189
|
if (!$.operators.contains(msg.sender))
|
|
185
190
|
SubscriptionModule__InvalidCaller.selector.revertWith();
|
|
186
191
|
|
|
192
|
+
IMembership membershipFacet;
|
|
193
|
+
|
|
187
194
|
for (uint256 i; i < paramsLen; ++i) {
|
|
188
195
|
Subscription storage sub = $.subscriptions[params[i].account][params[i].entityId];
|
|
189
196
|
|
|
@@ -213,19 +220,12 @@ contract SubscriptionModuleFacet is
|
|
|
213
220
|
continue;
|
|
214
221
|
}
|
|
215
222
|
|
|
216
|
-
|
|
217
|
-
uint256 expiresAt = membershipFacet.expiresAt(sub.tokenId);
|
|
218
|
-
|
|
219
|
-
// Sync next renewal time from on-chain expiration if user called renewMembership directly
|
|
220
|
-
uint40 correctNextRenewalTime = _calculateNextRenewalTime(expiresAt, sub.installTime);
|
|
221
|
-
if (sub.nextRenewalTime != correctNextRenewalTime) {
|
|
222
|
-
sub.nextRenewalTime = correctNextRenewalTime;
|
|
223
|
-
emit SubscriptionSynced(params[i].account, params[i].entityId, sub.nextRenewalTime);
|
|
224
|
-
}
|
|
223
|
+
membershipFacet = IMembership(sub.space);
|
|
225
224
|
|
|
226
225
|
uint256 actualRenewalPrice = membershipFacet.getMembershipRenewalPrice(sub.tokenId);
|
|
227
226
|
|
|
228
227
|
if (params[i].account.balance < actualRenewalPrice) {
|
|
228
|
+
_pauseSubscription(sub, params[i].account, params[i].entityId);
|
|
229
229
|
emit BatchRenewalSkipped(
|
|
230
230
|
params[i].account,
|
|
231
231
|
params[i].entityId,
|
|
@@ -234,6 +234,14 @@ contract SubscriptionModuleFacet is
|
|
|
234
234
|
continue;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
uint256 expiresAt = membershipFacet.expiresAt(sub.tokenId);
|
|
238
|
+
uint40 correctNextRenewalTime = _calculateNextRenewalTime(expiresAt, sub.installTime);
|
|
239
|
+
|
|
240
|
+
if (sub.nextRenewalTime != correctNextRenewalTime) {
|
|
241
|
+
sub.nextRenewalTime = correctNextRenewalTime;
|
|
242
|
+
emit SubscriptionSynced(params[i].account, params[i].entityId, sub.nextRenewalTime);
|
|
243
|
+
}
|
|
244
|
+
|
|
237
245
|
_processRenewal(sub, params[i], membershipFacet, actualRenewalPrice);
|
|
238
246
|
}
|
|
239
247
|
}
|
|
@@ -252,22 +260,35 @@ contract SubscriptionModuleFacet is
|
|
|
252
260
|
}
|
|
253
261
|
|
|
254
262
|
/// @inheritdoc ISubscriptionModule
|
|
255
|
-
function activateSubscription(uint32 entityId) external {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
263
|
+
function activateSubscription(uint32 entityId) external nonReentrant {
|
|
264
|
+
SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
|
|
265
|
+
|
|
266
|
+
if (!_hasEntityId($, msg.sender, entityId))
|
|
267
|
+
SubscriptionModule__InvalidEntityId.selector.revertWith();
|
|
268
|
+
|
|
269
|
+
Subscription storage sub = $.subscriptions[msg.sender][entityId];
|
|
259
270
|
|
|
260
271
|
if (sub.active) SubscriptionModule__ActiveSubscription.selector.revertWith();
|
|
261
272
|
|
|
262
273
|
address owner = IERC721(sub.space).ownerOf(sub.tokenId);
|
|
263
274
|
if (msg.sender != owner) SubscriptionModule__InvalidCaller.selector.revertWith();
|
|
264
275
|
|
|
276
|
+
IMembership membershipFacet = IMembership(sub.space);
|
|
277
|
+
uint256 expiresAt = membershipFacet.expiresAt(sub.tokenId);
|
|
278
|
+
|
|
279
|
+
// 6. Always sync renewal time to current membership state
|
|
280
|
+
uint40 correctNextRenewalTime = _calculateNextRenewalTime(expiresAt, sub.installTime);
|
|
281
|
+
if (sub.nextRenewalTime != correctNextRenewalTime) {
|
|
282
|
+
sub.nextRenewalTime = correctNextRenewalTime;
|
|
283
|
+
emit SubscriptionSynced(msg.sender, entityId, sub.nextRenewalTime);
|
|
284
|
+
}
|
|
285
|
+
|
|
265
286
|
sub.active = true;
|
|
266
287
|
emit SubscriptionActivated(msg.sender, entityId);
|
|
267
288
|
}
|
|
268
289
|
|
|
269
290
|
/// @inheritdoc ISubscriptionModule
|
|
270
|
-
function pauseSubscription(uint32 entityId) external {
|
|
291
|
+
function pauseSubscription(uint32 entityId) external nonReentrant {
|
|
271
292
|
Subscription storage sub = SubscriptionModuleStorage.getLayout().subscriptions[msg.sender][
|
|
272
293
|
entityId
|
|
273
294
|
];
|
|
@@ -308,17 +329,25 @@ contract SubscriptionModuleFacet is
|
|
|
308
329
|
/* Internal */
|
|
309
330
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
310
331
|
|
|
332
|
+
function _hasEntityId(
|
|
333
|
+
SubscriptionModuleStorage.Layout storage $,
|
|
334
|
+
address account,
|
|
335
|
+
uint32 entityId
|
|
336
|
+
) internal view returns (bool) {
|
|
337
|
+
return $.entityIds[account].contains(entityId);
|
|
338
|
+
}
|
|
339
|
+
|
|
311
340
|
/// @dev Processes a single subscription renewal
|
|
312
341
|
/// @param sub The subscription to renew
|
|
313
342
|
/// @param params The parameters for the renewal
|
|
314
343
|
function _processRenewal(
|
|
315
344
|
Subscription storage sub,
|
|
316
345
|
RenewalParams calldata params,
|
|
317
|
-
|
|
346
|
+
IMembership membershipFacet,
|
|
318
347
|
uint256 actualRenewalPrice
|
|
319
348
|
) internal {
|
|
320
349
|
// Construct the renewal call to space contract
|
|
321
|
-
bytes memory renewalCall = abi.encodeCall(
|
|
350
|
+
bytes memory renewalCall = abi.encodeCall(IMembership.renewMembership, (sub.tokenId));
|
|
322
351
|
|
|
323
352
|
// Create the data parameter for executeWithRuntimeValidation
|
|
324
353
|
// This should be an execute() call to the space contract
|
|
@@ -370,19 +399,13 @@ contract SubscriptionModuleFacet is
|
|
|
370
399
|
uint256 originalDuration = expirationTime >= installTime ? expirationTime - installTime : 0;
|
|
371
400
|
|
|
372
401
|
// For memberships shorter than 1 hour, use immediate buffer (2 minutes)
|
|
373
|
-
if (originalDuration <= 1 hours)
|
|
374
|
-
return BUFFER_IMMEDIATE;
|
|
375
|
-
}
|
|
402
|
+
if (originalDuration <= 1 hours) return BUFFER_IMMEDIATE;
|
|
376
403
|
|
|
377
404
|
// For memberships shorter than 6 hours, use short buffer (1 hour)
|
|
378
|
-
if (originalDuration <= 6 hours)
|
|
379
|
-
return BUFFER_SHORT;
|
|
380
|
-
}
|
|
405
|
+
if (originalDuration <= 6 hours) return BUFFER_SHORT;
|
|
381
406
|
|
|
382
407
|
// For memberships shorter than 24 hours, use medium buffer (6 hours)
|
|
383
|
-
if (originalDuration <= 24 hours)
|
|
384
|
-
return BUFFER_MEDIUM;
|
|
385
|
-
}
|
|
408
|
+
if (originalDuration <= 24 hours) return BUFFER_MEDIUM;
|
|
386
409
|
|
|
387
410
|
// For memberships longer than 24 hours, use long buffer (12 hours)
|
|
388
411
|
return BUFFER_LONG;
|
|
@@ -53,6 +53,12 @@ contract AppAccount is IAppAccount, AppAccountBase, ReentrancyGuard, Facet {
|
|
|
53
53
|
_onRenewApp(appId, data);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/// @inheritdoc IAppAccount
|
|
57
|
+
function onUpdateApp(bytes32 appId, bytes calldata data) external nonReentrant {
|
|
58
|
+
_onlyRegistry();
|
|
59
|
+
_onUpdateApp(appId, data);
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
/// @inheritdoc IAppAccount
|
|
57
63
|
function enableApp(address app) external onlyOwner {
|
|
58
64
|
_enableApp(app);
|
|
@@ -40,11 +40,15 @@ abstract contract AppAccountBase is
|
|
|
40
40
|
|
|
41
41
|
uint48 private constant DEFAULT_DURATION = 365 days;
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
/// @notice Checks if the caller is the registry.
|
|
44
|
+
/// @dev Reverts if the caller is not the registry.
|
|
44
45
|
function _onlyRegistry() internal view {
|
|
45
46
|
if (msg.sender != address(_getAppRegistry())) InvalidCaller.selector.revertWith();
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/// @notice Installs an app.
|
|
50
|
+
/// @param appId The ID of the app to install.
|
|
51
|
+
/// @param postInstallData The data to pass to the app's onInstall function.
|
|
48
52
|
function _installApp(bytes32 appId, bytes calldata postInstallData) internal {
|
|
49
53
|
if (appId == EMPTY_UID) InvalidAppId.selector.revertWith();
|
|
50
54
|
|
|
@@ -57,7 +61,7 @@ abstract contract AppAccountBase is
|
|
|
57
61
|
if (_isAppInstalled(app.module)) AppAlreadyInstalled.selector.revertWith();
|
|
58
62
|
|
|
59
63
|
// set the group status to active
|
|
60
|
-
_setGroupStatus(app.appId, true,
|
|
64
|
+
_setGroupStatus(app.appId, true, _calcExpiration(app.appId, app.duration));
|
|
61
65
|
_addApp(app.module, app.appId);
|
|
62
66
|
_grantGroupAccess({
|
|
63
67
|
groupId: app.appId,
|
|
@@ -127,13 +131,42 @@ abstract contract AppAccountBase is
|
|
|
127
131
|
emit ExecutionUninstalled(app.module, onUninstallSuccess, app.manifest);
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
function _onUpdateApp(bytes32 appId, bytes calldata data) internal {
|
|
135
|
+
if (data.length < 32) InvalidAppAddress.selector.revertWith();
|
|
136
|
+
|
|
137
|
+
address module = abi.decode(data, (address));
|
|
138
|
+
if (module == address(0)) InvalidAppAddress.selector.revertWith();
|
|
139
|
+
|
|
140
|
+
bytes32 currentAppId = _getInstalledAppId(module);
|
|
141
|
+
if (currentAppId == EMPTY_UID) AppNotInstalled.selector.revertWith();
|
|
142
|
+
if (currentAppId == appId) AppAlreadyInstalled.selector.revertWith();
|
|
143
|
+
|
|
144
|
+
App memory app = _getAppRegistry().getAppById(appId);
|
|
145
|
+
|
|
146
|
+
// revoke the current app
|
|
147
|
+
_revokeGroupAccess(currentAppId, app.client);
|
|
148
|
+
_setGroupStatus(currentAppId, false);
|
|
149
|
+
|
|
150
|
+
// update the app
|
|
151
|
+
_addApp(app.module, appId);
|
|
152
|
+
_setGroupStatus(appId, true, _calcExpiration(appId, app.duration));
|
|
153
|
+
_grantGroupAccess({
|
|
154
|
+
groupId: appId,
|
|
155
|
+
account: app.client,
|
|
156
|
+
grantDelay: _getGroupGrantDelay(appId),
|
|
157
|
+
executionDelay: 0
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
emit ExecutionUpdated(app.module, app.manifest);
|
|
161
|
+
}
|
|
162
|
+
|
|
130
163
|
function _onRenewApp(bytes32 appId, bytes calldata /* data */) internal {
|
|
131
164
|
// Get the app data to determine the duration
|
|
132
165
|
App memory app = _getAppRegistry().getAppById(appId);
|
|
133
166
|
if (app.appId == EMPTY_UID) AppNotRegistered.selector.revertWith();
|
|
134
167
|
|
|
135
168
|
// Calculate the new expiration time (extends current expiration by app duration)
|
|
136
|
-
uint48 newExpiration =
|
|
169
|
+
uint48 newExpiration = _calcExpiration(app.appId, app.duration);
|
|
137
170
|
|
|
138
171
|
// Update the group expiration
|
|
139
172
|
_setGroupExpiration(app.appId, newExpiration);
|
|
@@ -149,7 +182,12 @@ abstract contract AppAccountBase is
|
|
|
149
182
|
}
|
|
150
183
|
|
|
151
184
|
// Internal Functions
|
|
152
|
-
|
|
185
|
+
|
|
186
|
+
/// @notice Calculates the expiration for a group.
|
|
187
|
+
/// @param appId The ID of the app.
|
|
188
|
+
/// @param duration The duration of the app.
|
|
189
|
+
/// @return expiration The expiration for the group.
|
|
190
|
+
function _calcExpiration(
|
|
153
191
|
bytes32 appId,
|
|
154
192
|
uint48 duration
|
|
155
193
|
) internal view returns (uint48 expiration) {
|
|
@@ -161,29 +199,43 @@ abstract contract AppAccountBase is
|
|
|
161
199
|
}
|
|
162
200
|
}
|
|
163
201
|
|
|
202
|
+
/// @notice Adds an app to the account.
|
|
203
|
+
/// @param module The module of the app.
|
|
204
|
+
/// @param appId The ID of the app.
|
|
164
205
|
function _addApp(address module, bytes32 appId) internal {
|
|
165
206
|
AppAccountStorage.Layout storage $ = AppAccountStorage.getLayout();
|
|
166
207
|
$.installedApps.add(module);
|
|
167
208
|
$.appIdByApp[module] = appId;
|
|
168
209
|
}
|
|
169
210
|
|
|
211
|
+
/// @notice Removes an app from the account.
|
|
212
|
+
/// @param module The module of the app.
|
|
170
213
|
function _removeApp(address module) internal {
|
|
171
214
|
AppAccountStorage.getLayout().installedApps.remove(module);
|
|
172
215
|
delete AppAccountStorage.getLayout().appIdByApp[module];
|
|
173
216
|
}
|
|
174
217
|
|
|
218
|
+
/// @notice Enables an app.
|
|
219
|
+
/// @param app The address of the app.
|
|
175
220
|
function _enableApp(address app) internal {
|
|
176
221
|
bytes32 appId = AppAccountStorage.getInstalledAppId(app);
|
|
177
222
|
if (appId == EMPTY_UID) AppNotRegistered.selector.revertWith();
|
|
178
223
|
_setGroupStatus(appId, true);
|
|
179
224
|
}
|
|
180
225
|
|
|
226
|
+
/// @notice Disables an app.
|
|
227
|
+
/// @param app The address of the app.
|
|
181
228
|
function _disableApp(address app) internal {
|
|
182
229
|
bytes32 appId = AppAccountStorage.getInstalledAppId(app);
|
|
183
230
|
if (appId == EMPTY_UID) AppNotRegistered.selector.revertWith();
|
|
184
231
|
_setGroupStatus(appId, false);
|
|
185
232
|
}
|
|
186
233
|
|
|
234
|
+
/// @notice Checks if an app is entitled to a permission.
|
|
235
|
+
/// @param module The module of the app.
|
|
236
|
+
/// @param client The client of the app.
|
|
237
|
+
/// @param permission The permission to check.
|
|
238
|
+
/// @return entitled True if the app is entitled to the permission, false otherwise.
|
|
187
239
|
function _isAppEntitled(
|
|
188
240
|
address module,
|
|
189
241
|
address client,
|
|
@@ -192,22 +244,35 @@ abstract contract AppAccountBase is
|
|
|
192
244
|
return AppAccountStorage.isAppEntitled(module, client, permission);
|
|
193
245
|
}
|
|
194
246
|
|
|
247
|
+
/// @notice Gets the ID of the installed app.
|
|
248
|
+
/// @param module The module of the app.
|
|
249
|
+
/// @return appId The ID of the installed app.
|
|
195
250
|
function _getInstalledAppId(address module) internal view returns (bytes32) {
|
|
196
251
|
return AppAccountStorage.getInstalledAppId(module);
|
|
197
252
|
}
|
|
198
253
|
|
|
254
|
+
/// @notice Gets the app.
|
|
255
|
+
/// @param module The module of the app.
|
|
256
|
+
/// @return app The app.
|
|
199
257
|
function _getApp(address module) internal view returns (App memory app) {
|
|
200
258
|
return AppAccountStorage.getApp(module);
|
|
201
259
|
}
|
|
202
260
|
|
|
261
|
+
/// @notice Gets the app registry.
|
|
262
|
+
/// @return appRegistry The app registry.
|
|
203
263
|
function _getAppRegistry() internal view returns (IAppRegistry) {
|
|
204
264
|
return DependencyLib.getAppRegistry();
|
|
205
265
|
}
|
|
206
266
|
|
|
267
|
+
/// @notice Gets the apps.
|
|
268
|
+
/// @return apps The apps.
|
|
207
269
|
function _getApps() internal view returns (address[] memory) {
|
|
208
270
|
return AppAccountStorage.getLayout().installedApps.values();
|
|
209
271
|
}
|
|
210
272
|
|
|
273
|
+
/// @notice Checks if an app is executing.
|
|
274
|
+
/// @param app The address of the app.
|
|
275
|
+
/// @return executing True if the app is executing, false otherwise.
|
|
211
276
|
function _isAppExecuting(address app) internal view returns (bool) {
|
|
212
277
|
bytes32 currentExecutionId = ExecutorStorage.getExecutionId();
|
|
213
278
|
if (currentExecutionId == bytes32(0)) return false;
|
|
@@ -222,10 +287,15 @@ abstract contract AppAccountBase is
|
|
|
222
287
|
return true;
|
|
223
288
|
}
|
|
224
289
|
|
|
290
|
+
/// @notice Checks if an app is installed.
|
|
291
|
+
/// @param module The module of the app.
|
|
292
|
+
/// @return installed True if the app is installed, false otherwise.
|
|
225
293
|
function _isAppInstalled(address module) internal view returns (bool) {
|
|
226
294
|
return AppAccountStorage.getLayout().installedApps.contains(module);
|
|
227
295
|
}
|
|
228
296
|
|
|
297
|
+
/// @notice Checks if an app is authorized.
|
|
298
|
+
/// @param module The module of the app.
|
|
229
299
|
function _checkAuthorized(address module) internal view {
|
|
230
300
|
if (module == address(0)) InvalidAppAddress.selector.revertWith();
|
|
231
301
|
|
|
@@ -250,6 +320,11 @@ abstract contract AppAccountBase is
|
|
|
250
320
|
}
|
|
251
321
|
}
|
|
252
322
|
|
|
323
|
+
/// @notice Checks if an app is unauthorized.
|
|
324
|
+
/// @param module The module of the app.
|
|
325
|
+
/// @param factory The factory of the app.
|
|
326
|
+
/// @param deps The dependencies of the app.
|
|
327
|
+
/// @return unauthorized True if the app is unauthorized, false otherwise.
|
|
253
328
|
function _isUnauthorizedTarget(
|
|
254
329
|
address module,
|
|
255
330
|
address factory,
|
|
@@ -263,6 +338,9 @@ abstract contract AppAccountBase is
|
|
|
263
338
|
module == deps[3]; // AppRegistry
|
|
264
339
|
}
|
|
265
340
|
|
|
341
|
+
/// @notice Checks if a selector is invalid.
|
|
342
|
+
/// @param selector The selector to check.
|
|
343
|
+
/// @return invalid True if the selector is invalid, false otherwise.
|
|
266
344
|
function _isInvalidSelector(bytes4 selector) internal pure returns (bool) {
|
|
267
345
|
return
|
|
268
346
|
selector == IERC165.supportsInterface.selector ||
|
|
@@ -18,6 +18,7 @@ interface IAppAccountBase {
|
|
|
18
18
|
|
|
19
19
|
event ExecutionInstalled(address indexed module, ExecutionManifest manifest);
|
|
20
20
|
event ExecutionUninstalled(address indexed module, bool success, ExecutionManifest manifest);
|
|
21
|
+
event ExecutionUpdated(address indexed module, ExecutionManifest manifest);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
interface IAppAccount is IAppAccountBase {
|
|
@@ -36,6 +37,11 @@ interface IAppAccount is IAppAccountBase {
|
|
|
36
37
|
/// @param data The data required for app renewal
|
|
37
38
|
function onRenewApp(bytes32 appId, bytes calldata data) external;
|
|
38
39
|
|
|
40
|
+
/// @notice Updates an app
|
|
41
|
+
/// @param appId The ID of the app to update
|
|
42
|
+
/// @param data The data required for app update
|
|
43
|
+
function onUpdateApp(bytes32 appId, bytes calldata data) external;
|
|
44
|
+
|
|
39
45
|
/// @notice Enables an app
|
|
40
46
|
/// @param app The address of the app to enable
|
|
41
47
|
function enableApp(address app) external;
|