@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@towns-protocol/contracts",
3
- "version": "0.0.353",
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.353",
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": "8ef3c377692fbe02a8825159d53e390684290560"
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(12);
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(16);
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 {DeploySpaceOwnerFacet} from "scripts/deployments/facets/DeploySpaceOwnerFacet.s.sol";
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
- address diamond = getDeployment("spaceOwner");
21
- address spaceOwnerFacet = 0x09FCbC926F9Ec236fa3f825bF65b62776a9413aD;
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] = spaceOwnerFacet;
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
- // add the new facet to the diamond
36
- addCut(DeploySpaceOwnerFacet.makeCut(spaceOwnerFacet, FacetCutAction.Add));
35
+ // update with a call to the deploy function from your facet deployer contract
36
+ address facetAddress = DeploySubscriptionModuleFacet.deploy();
37
37
 
38
- bytes memory initData = "";
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, NotFound} from "@ethereum-attestation-service/eas-contracts/Common.sol";
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
- AppRegistryStorage.AppInfo storage appInfo = AppRegistryStorage.getLayout().apps[app];
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
- /// @notice Registers a new app in the registry
148
- /// @param app The address of the app to register
149
- /// @param client The client address that can use the app
150
- /// @return version The version ID of the registered app
151
- /// @dev Reverts if app is banned, inputs are invalid, or caller is not the owner
152
- function _registerApp(address app, address client) internal returns (bytes32 version) {
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
- AppRegistryStorage.Layout storage $ = AppRegistryStorage.getLayout();
156
- AppRegistryStorage.AppInfo storage appInfo = $.apps[app];
157
- AppRegistryStorage.ClientInfo storage clientInfo = $.client[client];
154
+ (
155
+ address owner,
156
+ bytes32[] memory permissions,
157
+ ExecutionManifest memory manifest,
158
+ uint48 duration
159
+ ) = _validateApp(app, client);
158
160
 
159
- if (clientInfo.app != address(0)) ClientAlreadyRegistered.selector.revertWith();
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
- ITownsApp appContract = ITownsApp(app);
170
+ ClientInfo storage clientInfo = $.client[client];
171
+ if (clientInfo.app == address(0)) ClientNotRegistered.selector.revertWith();
163
172
 
164
- uint256 installPrice = appContract.installPrice();
165
- _validatePricing(installPrice);
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
- uint48 accessDuration = appContract.accessDuration();
168
- uint48 duration = _validateDuration(accessDuration);
183
+ newVersionId = _attestApp(appData);
169
184
 
170
- bytes32[] memory permissions = appContract.requiredPermissions();
171
- if (permissions.length == 0) InvalidArrayInput.selector.revertWith();
185
+ appInfo.latestVersion = newVersionId;
186
+ emit AppUpgraded(appAddress, versionId, newVersionId);
187
+ }
172
188
 
173
- address owner = appContract.moduleOwner();
174
- if (owner == address(0)) InvalidAddressInput.selector.revertWith();
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
- ExecutionManifest memory manifest = appContract.executionManifest();
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: app,
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
- AttestationRequest memory request;
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 = app;
198
- clientInfo.app = app;
223
+ appInfo.app = appAddress;
224
+ clientInfo.app = appAddress;
199
225
 
200
- emit AppRegistered(app, version);
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(address revoker, bytes32 appId) internal {
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
- AppRegistryStorage.AppInfo storage appInfo = AppRegistryStorage.getLayout().apps[
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, revoker, 0, true);
250
+ _revoke(att.schema, request, msg.sender, 0, true);
227
251
 
228
- AppRegistryStorage.ClientInfo storage clientInfo = AppRegistryStorage.getLayout().client[
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
- AppRegistryStorage.AppInfo storage appInfo = AppRegistryStorage.getLayout().apps[app];
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 _validatePricing(uint256 price) internal view {
372
- IPlatformRequirements reqs = _getPlatformRequirements();
373
- uint256 minPlatformFee = reqs.getMembershipFee();
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 Verifies inputs for adding a new app
384
- /// @param app The app address to verify
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
- function _verifyAddAppInputs(address app, address client) internal view {
388
- if (app == address(0)) InvalidAddressInput.selector.revertWith();
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(app).supportsInterface(type(IModule).interfaceId) ||
393
- !IERC165(app).supportsInterface(type(IExecutionModule).interfaceId) ||
394
- !IERC165(app).supportsInterface(type(ITownsApp).interfaceId)
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 versionId The version ID of the registered app
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 versionId) {
63
- return _registerApp(address(app), client);
70
+ ) external payable nonReentrant returns (bytes32) {
71
+ return _registerApp(app, client);
64
72
  }
65
73
 
66
- /// @notice Remove a app from the registry
67
- /// @param versionId The app ID to remove
68
- /// @dev Only the owner of the app can remove it
69
- function removeApp(bytes32 versionId) external nonReentrant {
70
- _removeApp(msg.sender, versionId);
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 Create an upgradeable simple app contract
74
- /// @param params The parameters of the app
75
- function createApp(
76
- AppParams calldata params
77
- ) external payable nonReentrant returns (address app, bytes32 versionId) {
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 account The account to uninstall the app from
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 account,
117
+ IAppAccount space,
101
118
  bytes calldata data
102
119
  ) external nonReentrant {
103
- _onlyAllowed(address(account));
104
- return _uninstallApp(address(app), address(account), data);
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
- library AppRegistryStorage {
11
- /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
12
- /* STRUCTS */
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
- struct ClientInfo {
22
- address app;
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 ITownsApp
63
- function requiredPermissions() external view returns (bytes32[] memory) {
63
+ /// @inheritdoc ISimpleApp
64
+ function updatePermissions(bytes32[] calldata permissions) external onlyOwner {
64
65
  SimpleAppStorage.Layout storage $ = SimpleAppStorage.getLayout();
65
- return $.permissions;
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 IExecutionModule
75
- function executionManifest() external pure returns (ExecutionManifest memory) {
76
- // solhint-disable no-empty-blocks
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
- MembershipFacet membershipFacet = MembershipFacet(space);
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
- MembershipFacet membershipFacet = MembershipFacet(sub.space);
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
- Subscription storage sub = SubscriptionModuleStorage.getLayout().subscriptions[msg.sender][
257
- entityId
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
- MembershipFacet membershipFacet,
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(MembershipFacet.renewMembership, (sub.tokenId));
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
- // External Functions
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, _getAppExpiration(app.appId, app.duration));
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 = _getAppExpiration(app.appId, app.duration);
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
- function _getAppExpiration(
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;