@towns-protocol/contracts 0.0.352 → 0.0.354

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.352",
3
+ "version": "0.0.354",
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.352",
38
+ "@towns-protocol/prettier-config": "^0.0.354",
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": "b3b37f6306c5f111996218bd0772adfc03498fcd"
60
+ "gitHead": "a6dc807ec679a6507c2f1b10bb54af6adf78e01e"
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);
@@ -15,7 +15,7 @@ library DeploySubscriptionModuleFacet {
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(20);
18
+ DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(19);
19
19
  arr.p(SubscriptionModuleFacet.moduleId.selector);
20
20
  arr.p(SubscriptionModuleFacet.onInstall.selector);
21
21
  arr.p(SubscriptionModuleFacet.onUninstall.selector);
@@ -26,7 +26,7 @@ library DeploySubscriptionModuleFacet {
26
26
  arr.p(SubscriptionModuleFacet.preRuntimeValidationHook.selector);
27
27
  arr.p(SubscriptionModuleFacet.preSignatureValidationHook.selector);
28
28
  arr.p(SubscriptionModuleFacet.batchProcessRenewals.selector);
29
- arr.p(SubscriptionModuleFacet.processRenewal.selector);
29
+ arr.p(SubscriptionModuleFacet.getRenewalBuffer.selector);
30
30
  arr.p(SubscriptionModuleFacet.getSubscription.selector);
31
31
  arr.p(SubscriptionModuleFacet.pauseSubscription.selector);
32
32
  arr.p(SubscriptionModuleFacet.getEntityIds.selector);
@@ -34,7 +34,6 @@ library DeploySubscriptionModuleFacet {
34
34
  arr.p(SubscriptionModuleFacet.grantOperator.selector);
35
35
  arr.p(SubscriptionModuleFacet.revokeOperator.selector);
36
36
  arr.p(bytes4(keccak256("MAX_BATCH_SIZE()")));
37
- arr.p(bytes4(keccak256("RENEWAL_BUFFER()")));
38
37
  arr.p(bytes4(keccak256("GRACE_PERIOD()")));
39
38
 
40
39
  bytes32[] memory selectors_ = arr.asBytes32Array();
@@ -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;
@@ -39,6 +39,7 @@ interface ISubscriptionModuleBase {
39
39
  error SubscriptionModule__EmptyBatch();
40
40
  error SubscriptionModule__InvalidTokenOwner();
41
41
  error SubscriptionModule__InsufficientBalance();
42
+ error SubscriptionModule__ActiveSubscription();
42
43
  /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
43
44
  /* Events */
44
45
  /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -53,6 +54,8 @@ interface ISubscriptionModuleBase {
53
54
 
54
55
  event SubscriptionDeactivated(address indexed account, uint32 indexed entityId);
55
56
 
57
+ event SubscriptionActivated(address indexed account, uint32 indexed entityId);
58
+
56
59
  event SubscriptionSpent(
57
60
  address indexed account,
58
61
  uint32 indexed entityId,
@@ -66,7 +69,15 @@ interface ISubscriptionModuleBase {
66
69
  uint256 nextRenewalTime
67
70
  );
68
71
 
72
+ /// @notice Emitted when a subscription's next renewal time is synced to on-chain expiration
73
+ event SubscriptionSynced(
74
+ address indexed account,
75
+ uint32 indexed entityId,
76
+ uint256 newNextRenewalTime
77
+ );
78
+
69
79
  event SubscriptionPaused(address indexed account, uint32 indexed entityId);
80
+ event SubscriptionNotDue(address indexed account, uint32 indexed entityId);
70
81
 
71
82
  event BatchRenewalSkipped(address indexed account, uint32 indexed entityId, string reason);
72
83
 
@@ -83,10 +94,6 @@ interface ISubscriptionModule is ISubscriptionModuleBase {
83
94
  /// @param params The parameters for the renewals
84
95
  function batchProcessRenewals(RenewalParams[] calldata params) external;
85
96
 
86
- /// @notice Processes a single Towns membership renewal
87
- /// @param params The parameters for the renewal
88
- function processRenewal(RenewalParams calldata params) external;
89
-
90
97
  /// @notice Gets the subscription for an account and entity ID
91
98
  /// @param account The address of the account to get the subscription for
92
99
  /// @param entityId The entity ID of the subscription to get
@@ -96,6 +103,15 @@ interface ISubscriptionModule is ISubscriptionModuleBase {
96
103
  uint32 entityId
97
104
  ) external view returns (Subscription memory);
98
105
 
106
+ /// @notice Gets the renewal buffer for an expiration time
107
+ /// @param expirationTime The expiration time to get the renewal buffer for
108
+ /// @return The renewal buffer for the expiration time
109
+ function getRenewalBuffer(uint256 expirationTime) external view returns (uint256);
110
+
111
+ /// @notice Activates a subscription
112
+ /// @param entityId The entity ID of the subscription to activate
113
+ function activateSubscription(uint32 entityId) external;
114
+
99
115
  /// @notice Pauses a subscription
100
116
  /// @param entityId The entity ID of the subscription to pause
101
117
  function pauseSubscription(uint32 entityId) external;
@@ -43,9 +43,14 @@ contract SubscriptionModuleFacet is
43
43
  uint256 internal constant _SIG_VALIDATION_FAILED = 1;
44
44
 
45
45
  uint256 public constant MAX_BATCH_SIZE = 50;
46
- uint256 public constant RENEWAL_BUFFER = 1 days;
47
46
  uint256 public constant GRACE_PERIOD = 3 days;
48
47
 
48
+ // Dynamic buffer times based on expiration proximity
49
+ uint256 public constant BUFFER_IMMEDIATE = 2 minutes; // For expirations within 1 hour
50
+ uint256 public constant BUFFER_SHORT = 1 hours; // For expirations within 6 hours
51
+ uint256 public constant BUFFER_MEDIUM = 6 hours; // For expirations within 24 hours
52
+ uint256 public constant BUFFER_LONG = 12 hours; // For expirations more than 24 hours away
53
+
49
54
  function __SubscriptionModule_init() external onlyInitializing {
50
55
  _addInterface(type(ISubscriptionModule).interfaceId);
51
56
  _addInterface(type(IValidationModule).interfaceId);
@@ -81,7 +86,8 @@ contract SubscriptionModuleFacet is
81
86
  sub.space = space;
82
87
  sub.active = true;
83
88
  sub.tokenId = tokenId;
84
- sub.nextRenewalTime = uint40(expiresAt - RENEWAL_BUFFER);
89
+ sub.installTime = uint40(block.timestamp);
90
+ sub.nextRenewalTime = _calculateNextRenewalTime(expiresAt, sub.installTime);
85
91
 
86
92
  $.entityIds[msg.sender].add(entityId);
87
93
 
@@ -168,49 +174,69 @@ contract SubscriptionModuleFacet is
168
174
 
169
175
  /// @inheritdoc ISubscriptionModule
170
176
  function batchProcessRenewals(RenewalParams[] calldata params) external nonReentrant {
171
- uint256 length = params.length;
172
- if (length > MAX_BATCH_SIZE) SubscriptionModule__ExceedsMaxBatchSize.selector.revertWith();
173
- if (length == 0) SubscriptionModule__EmptyBatch.selector.revertWith();
177
+ uint256 paramsLen = params.length;
178
+ if (paramsLen > MAX_BATCH_SIZE)
179
+ SubscriptionModule__ExceedsMaxBatchSize.selector.revertWith();
180
+ if (paramsLen == 0) SubscriptionModule__EmptyBatch.selector.revertWith();
174
181
 
175
182
  SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
176
183
 
177
- for (uint256 i; i < length; ++i) {
178
- if (!_isAllowed($.operators, params[i].account))
179
- SubscriptionModule__InvalidCaller.selector.revertWith();
184
+ if (!$.operators.contains(msg.sender))
185
+ SubscriptionModule__InvalidCaller.selector.revertWith();
180
186
 
187
+ for (uint256 i; i < paramsLen; ++i) {
181
188
  Subscription storage sub = $.subscriptions[params[i].account][params[i].entityId];
182
189
 
190
+ // Skip if renewal not due (check original nextRenewalTime first)
191
+ if (sub.nextRenewalTime > block.timestamp) {
192
+ emit SubscriptionNotDue(params[i].account, params[i].entityId);
193
+ continue;
194
+ }
195
+
183
196
  // Skip inactive subscriptions
184
197
  if (!sub.active) {
185
198
  emit BatchRenewalSkipped(params[i].account, params[i].entityId, "INACTIVE");
186
199
  continue;
187
200
  }
188
201
 
189
- // Skip if renewal not due
190
- if (block.timestamp < sub.nextRenewalTime) {
191
- emit BatchRenewalSkipped(params[i].account, params[i].entityId, "NOT_DUE");
202
+ // Skip if past grace period
203
+ if (sub.nextRenewalTime + GRACE_PERIOD < block.timestamp) {
204
+ _pauseSubscription(sub, params[i].account, params[i].entityId);
205
+ emit BatchRenewalSkipped(params[i].account, params[i].entityId, "PAST_GRACE");
192
206
  continue;
193
207
  }
194
208
 
195
- // Skip if past grace period (will be handled by individual call)
196
- if (block.timestamp > sub.nextRenewalTime + GRACE_PERIOD) {
197
- emit BatchRenewalSkipped(params[i].account, params[i].entityId, "PAST_GRACE");
209
+ // Skip if account isn't owner anymore (for safety)
210
+ if (IERC721(sub.space).ownerOf(sub.tokenId) != params[i].account) {
211
+ _pauseSubscription(sub, params[i].account, params[i].entityId);
212
+ emit BatchRenewalSkipped(params[i].account, params[i].entityId, "NOT_OWNER");
198
213
  continue;
199
214
  }
200
215
 
201
- _processRenewal(sub, params[i]);
202
- }
203
- }
216
+ MembershipFacet membershipFacet = MembershipFacet(sub.space);
217
+ uint256 expiresAt = membershipFacet.expiresAt(sub.tokenId);
204
218
 
205
- /// @inheritdoc ISubscriptionModule
206
- function processRenewal(RenewalParams calldata renewalParams) external nonReentrant {
207
- SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
208
- if (!_isAllowed($.operators, renewalParams.account))
209
- SubscriptionModule__InvalidCaller.selector.revertWith();
210
- _processRenewal(
211
- $.subscriptions[renewalParams.account][renewalParams.entityId],
212
- renewalParams
213
- );
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
+ }
225
+
226
+ uint256 actualRenewalPrice = membershipFacet.getMembershipRenewalPrice(sub.tokenId);
227
+
228
+ if (params[i].account.balance < actualRenewalPrice) {
229
+ _pauseSubscription(sub, params[i].account, params[i].entityId);
230
+ emit BatchRenewalSkipped(
231
+ params[i].account,
232
+ params[i].entityId,
233
+ "INSUFFICIENT_BALANCE"
234
+ );
235
+ continue;
236
+ }
237
+
238
+ _processRenewal(sub, params[i], membershipFacet, actualRenewalPrice);
239
+ }
214
240
  }
215
241
 
216
242
  /// @inheritdoc ISubscriptionModule
@@ -222,15 +248,38 @@ contract SubscriptionModuleFacet is
222
248
  }
223
249
 
224
250
  /// @inheritdoc ISubscriptionModule
225
- function pauseSubscription(uint32 entityId) external {
251
+ function getRenewalBuffer(uint256 expirationTime) external view returns (uint256) {
252
+ return _getRenewalBuffer(expirationTime);
253
+ }
254
+
255
+ /// @inheritdoc ISubscriptionModule
256
+ function activateSubscription(uint32 entityId) external nonReentrant {
257
+ Subscription storage sub = SubscriptionModuleStorage.getLayout().subscriptions[msg.sender][
258
+ entityId
259
+ ];
260
+
261
+ if (sub.active) SubscriptionModule__ActiveSubscription.selector.revertWith();
262
+
263
+ sub.active = true;
264
+
265
+ address owner = IERC721(sub.space).ownerOf(sub.tokenId);
266
+ if (msg.sender != owner) SubscriptionModule__InvalidCaller.selector.revertWith();
267
+
268
+ emit SubscriptionActivated(msg.sender, entityId);
269
+ }
270
+
271
+ /// @inheritdoc ISubscriptionModule
272
+ function pauseSubscription(uint32 entityId) external nonReentrant {
226
273
  Subscription storage sub = SubscriptionModuleStorage.getLayout().subscriptions[msg.sender][
227
274
  entityId
228
275
  ];
229
276
 
230
277
  if (!sub.active) SubscriptionModule__InactiveSubscription.selector.revertWith();
231
278
 
232
- sub.active = false;
233
- emit SubscriptionPaused(msg.sender, entityId);
279
+ _pauseSubscription(sub, msg.sender, entityId);
280
+
281
+ address owner = IERC721(sub.space).ownerOf(sub.tokenId);
282
+ if (msg.sender != owner) SubscriptionModule__InvalidCaller.selector.revertWith();
234
283
  }
235
284
 
236
285
  /// @inheritdoc ISubscriptionModule
@@ -264,28 +313,12 @@ contract SubscriptionModuleFacet is
264
313
  /// @dev Processes a single subscription renewal
265
314
  /// @param sub The subscription to renew
266
315
  /// @param params The parameters for the renewal
267
- function _processRenewal(Subscription storage sub, RenewalParams calldata params) internal {
268
- if (!sub.active) SubscriptionModule__InactiveSubscription.selector.revertWith();
269
-
270
- if (block.timestamp < sub.nextRenewalTime)
271
- SubscriptionModule__RenewalNotDue.selector.revertWith();
272
-
273
- // Check if we're past the grace period
274
- if (block.timestamp > sub.nextRenewalTime + GRACE_PERIOD) {
275
- sub.active = false;
276
- emit SubscriptionPaused(params.account, params.entityId);
277
- return;
278
- }
279
-
280
- MembershipFacet membershipFacet = MembershipFacet(sub.space);
281
-
282
- // Get current renewal price from Towns contract
283
- uint256 actualRenewalPrice = membershipFacet.getMembershipRenewalPrice(sub.tokenId);
284
-
285
- // Check if the account has enough balance
286
- if (params.account.balance < actualRenewalPrice)
287
- SubscriptionModule__InsufficientBalance.selector.revertWith();
288
-
316
+ function _processRenewal(
317
+ Subscription storage sub,
318
+ RenewalParams calldata params,
319
+ MembershipFacet membershipFacet,
320
+ uint256 actualRenewalPrice
321
+ ) internal {
289
322
  // Construct the renewal call to space contract
290
323
  bytes memory renewalCall = abi.encodeCall(MembershipFacet.renewMembership, (sub.tokenId));
291
324
 
@@ -301,9 +334,10 @@ contract SubscriptionModuleFacet is
301
334
  );
302
335
 
303
336
  // Use the proper pack function from ValidationLocatorLib
304
- bytes memory authorization = _runtimeFinal(
337
+ bytes memory authorization = ValidationLocatorLib.packSignature(
305
338
  params.entityId,
306
- abi.encode(sub.space, sub.tokenId)
339
+ false, // selector-based
340
+ bytes.concat(hex"ff", abi.encode(sub.space, sub.tokenId))
307
341
  );
308
342
 
309
343
  // Call executeWithRuntimeValidation with the correct parameters
@@ -320,15 +354,68 @@ contract SubscriptionModuleFacet is
320
354
 
321
355
  // Get the actual new expiration time after successful renewal
322
356
  uint256 newExpiresAt = membershipFacet.expiresAt(sub.tokenId);
323
-
324
- // Update subscription state after successful renewal
325
- sub.nextRenewalTime = uint40(newExpiresAt - RENEWAL_BUFFER);
357
+ sub.nextRenewalTime = _calculateNextRenewalTime(newExpiresAt, sub.installTime);
326
358
  sub.lastRenewalTime = uint40(block.timestamp);
327
359
  sub.spent += actualRenewalPrice;
328
360
 
329
361
  emit SubscriptionRenewed(params.account, params.entityId, sub.nextRenewalTime);
330
362
  }
331
363
 
364
+ /// @dev Determines the appropriate renewal buffer time based on original membership duration
365
+ /// @param expirationTime The expiration timestamp of the membership
366
+ /// @param installTime The time when the subscription was installed
367
+ /// @return The appropriate buffer time in seconds before expiration
368
+ function _getRenewalBuffer(
369
+ uint256 expirationTime,
370
+ uint256 installTime
371
+ ) internal pure returns (uint256) {
372
+ uint256 originalDuration = expirationTime >= installTime ? expirationTime - installTime : 0;
373
+
374
+ // For memberships shorter than 1 hour, use immediate buffer (2 minutes)
375
+ if (originalDuration <= 1 hours) return BUFFER_IMMEDIATE;
376
+
377
+ // For memberships shorter than 6 hours, use short buffer (1 hour)
378
+ if (originalDuration <= 6 hours) return BUFFER_SHORT;
379
+
380
+ // For memberships shorter than 24 hours, use medium buffer (6 hours)
381
+ if (originalDuration <= 24 hours) return BUFFER_MEDIUM;
382
+
383
+ // For memberships longer than 24 hours, use long buffer (12 hours)
384
+ return BUFFER_LONG;
385
+ }
386
+
387
+ /// @dev Legacy function for backward compatibility - uses current time as install time
388
+ /// @param expirationTime The expiration timestamp of the membership
389
+ /// @return The appropriate buffer time in seconds before expiration
390
+ function _getRenewalBuffer(uint256 expirationTime) internal view returns (uint256) {
391
+ return _getRenewalBuffer(expirationTime, block.timestamp);
392
+ }
393
+
394
+ /// @dev Calculates the correct next renewal time for a given expiration using install time
395
+ /// @param expirationTime The expiration timestamp of the membership
396
+ /// @param installTime The time when the subscription was installed
397
+ /// @return The next renewal time as uint40
398
+ function _calculateNextRenewalTime(
399
+ uint256 expirationTime,
400
+ uint256 installTime
401
+ ) internal view returns (uint40) {
402
+ if (expirationTime <= block.timestamp) return uint40(block.timestamp);
403
+
404
+ uint256 buffer = _getRenewalBuffer(expirationTime, installTime);
405
+ uint256 timeUntilExpiration = expirationTime - block.timestamp;
406
+
407
+ if (buffer >= timeUntilExpiration) return uint40(block.timestamp);
408
+
409
+ return uint40(expirationTime - buffer);
410
+ }
411
+
412
+ /// @dev Legacy function for backward compatibility - uses current time as install time
413
+ /// @param expirationTime The expiration timestamp of the membership
414
+ /// @return The next renewal time as uint40
415
+ function _calculateNextRenewalTime(uint256 expirationTime) internal view returns (uint40) {
416
+ return _calculateNextRenewalTime(expirationTime, block.timestamp);
417
+ }
418
+
332
419
  /// @dev Creates the runtime final data for the renewal
333
420
  /// @param entityId The entity ID of the subscription
334
421
  /// @param finalData The final data for the renewal
@@ -345,15 +432,12 @@ contract SubscriptionModuleFacet is
345
432
  );
346
433
  }
347
434
 
348
- /// @dev Checks if the caller is allowed to call the function
349
- /// @param operators The set of operators
350
- /// @param account The account to check
351
- /// @return True if the caller is allowed to call the function
352
- function _isAllowed(
353
- EnumerableSetLib.AddressSet storage operators,
354
- address account
355
- ) internal view returns (bool) {
356
- if (account == msg.sender) return true;
357
- return operators.contains(msg.sender);
435
+ function _pauseSubscription(
436
+ Subscription storage sub,
437
+ address account,
438
+ uint32 entityId
439
+ ) internal {
440
+ sub.active = false;
441
+ emit SubscriptionPaused(account, entityId);
358
442
  }
359
443
  }
@@ -7,6 +7,7 @@ struct Subscription {
7
7
  uint256 tokenId; // 32 bytes
8
8
  uint256 spent; // 32 bytes
9
9
  address space; // 20 bytes
10
+ uint40 installTime; // 5 bytes - when subscription was first installed
10
11
  uint40 lastRenewalTime; // 5 bytes
11
12
  uint40 nextRenewalTime; // 5 bytes
12
13
  bool active; // 1 byte
@@ -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;