@towns-protocol/contracts 0.0.352 → 0.0.353

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.353",
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.353",
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": "8ef3c377692fbe02a8825159d53e390684290560"
61
61
  }
@@ -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();
@@ -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,68 @@ 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
+ emit BatchRenewalSkipped(
230
+ params[i].account,
231
+ params[i].entityId,
232
+ "INSUFFICIENT_BALANCE"
233
+ );
234
+ continue;
235
+ }
236
+
237
+ _processRenewal(sub, params[i], membershipFacet, actualRenewalPrice);
238
+ }
214
239
  }
215
240
 
216
241
  /// @inheritdoc ISubscriptionModule
@@ -221,6 +246,26 @@ contract SubscriptionModuleFacet is
221
246
  return SubscriptionModuleStorage.getLayout().subscriptions[account][entityId];
222
247
  }
223
248
 
249
+ /// @inheritdoc ISubscriptionModule
250
+ function getRenewalBuffer(uint256 expirationTime) external view returns (uint256) {
251
+ return _getRenewalBuffer(expirationTime);
252
+ }
253
+
254
+ /// @inheritdoc ISubscriptionModule
255
+ function activateSubscription(uint32 entityId) external {
256
+ Subscription storage sub = SubscriptionModuleStorage.getLayout().subscriptions[msg.sender][
257
+ entityId
258
+ ];
259
+
260
+ if (sub.active) SubscriptionModule__ActiveSubscription.selector.revertWith();
261
+
262
+ address owner = IERC721(sub.space).ownerOf(sub.tokenId);
263
+ if (msg.sender != owner) SubscriptionModule__InvalidCaller.selector.revertWith();
264
+
265
+ sub.active = true;
266
+ emit SubscriptionActivated(msg.sender, entityId);
267
+ }
268
+
224
269
  /// @inheritdoc ISubscriptionModule
225
270
  function pauseSubscription(uint32 entityId) external {
226
271
  Subscription storage sub = SubscriptionModuleStorage.getLayout().subscriptions[msg.sender][
@@ -229,8 +274,10 @@ contract SubscriptionModuleFacet is
229
274
 
230
275
  if (!sub.active) SubscriptionModule__InactiveSubscription.selector.revertWith();
231
276
 
232
- sub.active = false;
233
- emit SubscriptionPaused(msg.sender, entityId);
277
+ address owner = IERC721(sub.space).ownerOf(sub.tokenId);
278
+ if (msg.sender != owner) SubscriptionModule__InvalidCaller.selector.revertWith();
279
+
280
+ _pauseSubscription(sub, msg.sender, entityId);
234
281
  }
235
282
 
236
283
  /// @inheritdoc ISubscriptionModule
@@ -264,28 +311,12 @@ contract SubscriptionModuleFacet is
264
311
  /// @dev Processes a single subscription renewal
265
312
  /// @param sub The subscription to renew
266
313
  /// @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
-
314
+ function _processRenewal(
315
+ Subscription storage sub,
316
+ RenewalParams calldata params,
317
+ MembershipFacet membershipFacet,
318
+ uint256 actualRenewalPrice
319
+ ) internal {
289
320
  // Construct the renewal call to space contract
290
321
  bytes memory renewalCall = abi.encodeCall(MembershipFacet.renewMembership, (sub.tokenId));
291
322
 
@@ -301,9 +332,10 @@ contract SubscriptionModuleFacet is
301
332
  );
302
333
 
303
334
  // Use the proper pack function from ValidationLocatorLib
304
- bytes memory authorization = _runtimeFinal(
335
+ bytes memory authorization = ValidationLocatorLib.packSignature(
305
336
  params.entityId,
306
- abi.encode(sub.space, sub.tokenId)
337
+ false, // selector-based
338
+ bytes.concat(hex"ff", abi.encode(sub.space, sub.tokenId))
307
339
  );
308
340
 
309
341
  // Call executeWithRuntimeValidation with the correct parameters
@@ -320,15 +352,74 @@ contract SubscriptionModuleFacet is
320
352
 
321
353
  // Get the actual new expiration time after successful renewal
322
354
  uint256 newExpiresAt = membershipFacet.expiresAt(sub.tokenId);
323
-
324
- // Update subscription state after successful renewal
325
- sub.nextRenewalTime = uint40(newExpiresAt - RENEWAL_BUFFER);
355
+ sub.nextRenewalTime = _calculateNextRenewalTime(newExpiresAt, sub.installTime);
326
356
  sub.lastRenewalTime = uint40(block.timestamp);
327
357
  sub.spent += actualRenewalPrice;
328
358
 
329
359
  emit SubscriptionRenewed(params.account, params.entityId, sub.nextRenewalTime);
330
360
  }
331
361
 
362
+ /// @dev Determines the appropriate renewal buffer time based on original membership duration
363
+ /// @param expirationTime The expiration timestamp of the membership
364
+ /// @param installTime The time when the subscription was installed
365
+ /// @return The appropriate buffer time in seconds before expiration
366
+ function _getRenewalBuffer(
367
+ uint256 expirationTime,
368
+ uint256 installTime
369
+ ) internal pure returns (uint256) {
370
+ uint256 originalDuration = expirationTime >= installTime ? expirationTime - installTime : 0;
371
+
372
+ // For memberships shorter than 1 hour, use immediate buffer (2 minutes)
373
+ if (originalDuration <= 1 hours) {
374
+ return BUFFER_IMMEDIATE;
375
+ }
376
+
377
+ // For memberships shorter than 6 hours, use short buffer (1 hour)
378
+ if (originalDuration <= 6 hours) {
379
+ return BUFFER_SHORT;
380
+ }
381
+
382
+ // For memberships shorter than 24 hours, use medium buffer (6 hours)
383
+ if (originalDuration <= 24 hours) {
384
+ return BUFFER_MEDIUM;
385
+ }
386
+
387
+ // For memberships longer than 24 hours, use long buffer (12 hours)
388
+ return BUFFER_LONG;
389
+ }
390
+
391
+ /// @dev Legacy function for backward compatibility - uses current time as install time
392
+ /// @param expirationTime The expiration timestamp of the membership
393
+ /// @return The appropriate buffer time in seconds before expiration
394
+ function _getRenewalBuffer(uint256 expirationTime) internal view returns (uint256) {
395
+ return _getRenewalBuffer(expirationTime, block.timestamp);
396
+ }
397
+
398
+ /// @dev Calculates the correct next renewal time for a given expiration using install time
399
+ /// @param expirationTime The expiration timestamp of the membership
400
+ /// @param installTime The time when the subscription was installed
401
+ /// @return The next renewal time as uint40
402
+ function _calculateNextRenewalTime(
403
+ uint256 expirationTime,
404
+ uint256 installTime
405
+ ) internal view returns (uint40) {
406
+ if (expirationTime <= block.timestamp) return uint40(block.timestamp);
407
+
408
+ uint256 buffer = _getRenewalBuffer(expirationTime, installTime);
409
+ uint256 timeUntilExpiration = expirationTime - block.timestamp;
410
+
411
+ if (buffer >= timeUntilExpiration) return uint40(block.timestamp);
412
+
413
+ return uint40(expirationTime - buffer);
414
+ }
415
+
416
+ /// @dev Legacy function for backward compatibility - uses current time as install time
417
+ /// @param expirationTime The expiration timestamp of the membership
418
+ /// @return The next renewal time as uint40
419
+ function _calculateNextRenewalTime(uint256 expirationTime) internal view returns (uint40) {
420
+ return _calculateNextRenewalTime(expirationTime, block.timestamp);
421
+ }
422
+
332
423
  /// @dev Creates the runtime final data for the renewal
333
424
  /// @param entityId The entity ID of the subscription
334
425
  /// @param finalData The final data for the renewal
@@ -345,15 +436,12 @@ contract SubscriptionModuleFacet is
345
436
  );
346
437
  }
347
438
 
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);
439
+ function _pauseSubscription(
440
+ Subscription storage sub,
441
+ address account,
442
+ uint32 entityId
443
+ ) internal {
444
+ sub.active = false;
445
+ emit SubscriptionPaused(account, entityId);
358
446
  }
359
447
  }
@@ -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