@towns-protocol/contracts 0.0.364 → 0.0.365

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.364",
3
+ "version": "0.0.365",
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.364",
38
+ "@towns-protocol/prettier-config": "^0.0.365",
39
39
  "@typechain/ethers-v5": "^11.1.2",
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": "14393591e10dacdc499c872086597abae8aa2fe1"
60
+ "gitHead": "e499a31e1f0533416eb611c2c026a79b2dc1dead"
61
61
  }
@@ -40,6 +40,7 @@ interface ISubscriptionModuleBase {
40
40
  error SubscriptionModule__InvalidTokenOwner();
41
41
  error SubscriptionModule__InsufficientBalance();
42
42
  error SubscriptionModule__ActiveSubscription();
43
+ error SubscriptionModule__MembershipBanned();
43
44
  /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
44
45
  /* Events */
45
46
  /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -107,10 +108,10 @@ interface ISubscriptionModule is ISubscriptionModuleBase {
107
108
  uint32 entityId
108
109
  ) external view returns (Subscription memory);
109
110
 
110
- /// @notice Gets the renewal buffer for an expiration time
111
- /// @param expirationTime The expiration time to get the renewal buffer for
112
- /// @return The renewal buffer for the expiration time
113
- function getRenewalBuffer(uint256 expirationTime) external view returns (uint256);
111
+ /// @notice Gets the renewal buffer for a membership duration
112
+ /// @param duration The membership duration to get the renewal buffer for
113
+ /// @return The renewal buffer for the duration
114
+ function getRenewalBuffer(uint256 duration) external pure returns (uint256);
114
115
 
115
116
  /// @notice Activates a subscription
116
117
  /// @param entityId The entity ID of the subscription to activate
@@ -9,6 +9,7 @@ import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IV
9
9
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
10
10
  import {ISubscriptionModule} from "./ISubscriptionModule.sol";
11
11
  import {IMembership} from "../../../spaces/facets/membership/IMembership.sol";
12
+ import {IBanning} from "../../../spaces/facets/banning/IBanning.sol";
12
13
 
13
14
  // libraries
14
15
  import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
@@ -19,6 +20,7 @@ import {ReentrancyGuardTransient} from "solady/utils/ReentrancyGuardTransient.so
19
20
  import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
20
21
  import {Validator} from "../../../utils/libraries/Validator.sol";
21
22
  import {Subscription, SubscriptionModuleStorage} from "./SubscriptionModuleStorage.sol";
23
+ import {SafeCastLib} from "solady/utils/SafeCastLib.sol";
22
24
 
23
25
  // contracts
24
26
  import {ModuleBase} from "modular-account/src/modules/ModuleBase.sol";
@@ -38,6 +40,7 @@ contract SubscriptionModuleFacet is
38
40
  {
39
41
  using EnumerableSetLib for EnumerableSetLib.Uint256Set;
40
42
  using EnumerableSetLib for EnumerableSetLib.AddressSet;
43
+ using SafeCastLib for uint256;
41
44
  using CustomRevert for bytes4;
42
45
 
43
46
  uint256 internal constant _SIG_VALIDATION_FAILED = 1;
@@ -77,23 +80,27 @@ contract SubscriptionModuleFacet is
77
80
 
78
81
  if (entityId == 0) SubscriptionModule__InvalidEntityId.selector.revertWith();
79
82
 
83
+ if (IBanning(space).isBanned(tokenId))
84
+ SubscriptionModule__MembershipBanned.selector.revertWith();
85
+
80
86
  if (IERC721(space).ownerOf(tokenId) != msg.sender)
81
87
  SubscriptionModule__InvalidTokenOwner.selector.revertWith();
82
88
 
83
- IMembership membershipFacet = IMembership(space);
84
- uint256 expiresAt = membershipFacet.expiresAt(tokenId);
85
-
86
89
  SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
87
90
 
88
91
  if (!$.entityIds[msg.sender].add(entityId))
89
92
  SubscriptionModule__InvalidEntityId.selector.revertWith();
90
93
 
94
+ IMembership membershipFacet = IMembership(space);
95
+ uint256 expiresAt = membershipFacet.expiresAt(tokenId);
96
+ uint256 duration = membershipFacet.getMembershipDuration();
97
+
91
98
  Subscription storage sub = $.subscriptions[msg.sender][entityId];
92
99
  sub.space = space;
93
100
  sub.active = true;
94
101
  sub.tokenId = tokenId;
95
- sub.installTime = uint40(block.timestamp);
96
- sub.nextRenewalTime = _calculateNextRenewalTime(expiresAt, sub.installTime);
102
+ sub.installTime = block.timestamp.toUint40();
103
+ sub.nextRenewalTime = _calculateBaseRenewalTime(expiresAt, duration);
97
104
 
98
105
  emit SubscriptionConfigured(
99
106
  msg.sender,
@@ -220,6 +227,16 @@ contract SubscriptionModuleFacet is
220
227
  continue;
221
228
  }
222
229
 
230
+ if (IBanning(sub.space).isBanned(sub.tokenId)) {
231
+ _pauseSubscription(sub, params[i].account, params[i].entityId);
232
+ emit BatchRenewalSkipped(
233
+ params[i].account,
234
+ params[i].entityId,
235
+ "MEMBERSHIP_BANNED"
236
+ );
237
+ continue;
238
+ }
239
+
223
240
  // Skip if account isn't owner anymore (for safety)
224
241
  if (IERC721(sub.space).ownerOf(sub.tokenId) != params[i].account) {
225
242
  _pauseSubscription(sub, params[i].account, params[i].entityId);
@@ -242,10 +259,11 @@ contract SubscriptionModuleFacet is
242
259
  }
243
260
 
244
261
  uint256 expiresAt = membershipFacet.expiresAt(sub.tokenId);
245
- uint40 correctNextRenewalTime = _calculateNextRenewalTime(expiresAt, sub.installTime);
262
+ uint256 duration = membershipFacet.getMembershipDuration();
263
+ uint40 nextRenewalTime = _calculateBaseRenewalTime(expiresAt, duration);
246
264
 
247
- if (sub.nextRenewalTime != correctNextRenewalTime) {
248
- sub.nextRenewalTime = correctNextRenewalTime;
265
+ if (sub.nextRenewalTime != nextRenewalTime) {
266
+ sub.nextRenewalTime = nextRenewalTime;
249
267
  emit SubscriptionSynced(params[i].account, params[i].entityId, sub.nextRenewalTime);
250
268
  }
251
269
 
@@ -262,8 +280,8 @@ contract SubscriptionModuleFacet is
262
280
  }
263
281
 
264
282
  /// @inheritdoc ISubscriptionModule
265
- function getRenewalBuffer(uint256 expirationTime) external view returns (uint256) {
266
- return _getRenewalBuffer(expirationTime);
283
+ function getRenewalBuffer(uint256 duration) external pure returns (uint256) {
284
+ return _getRenewalBuffer(duration);
267
285
  }
268
286
 
269
287
  /// @inheritdoc ISubscriptionModule
@@ -282,9 +300,10 @@ contract SubscriptionModuleFacet is
282
300
 
283
301
  IMembership membershipFacet = IMembership(sub.space);
284
302
  uint256 expiresAt = membershipFacet.expiresAt(sub.tokenId);
303
+ uint256 duration = membershipFacet.getMembershipDuration();
285
304
 
286
305
  // 6. Always sync renewal time to current membership state
287
- uint40 correctNextRenewalTime = _calculateNextRenewalTime(expiresAt, sub.installTime);
306
+ uint40 correctNextRenewalTime = _calculateBaseRenewalTime(expiresAt, duration);
288
307
  if (sub.nextRenewalTime != correctNextRenewalTime) {
289
308
  sub.nextRenewalTime = correctNextRenewalTime;
290
309
  emit SubscriptionSynced(msg.sender, entityId, sub.nextRenewalTime);
@@ -388,8 +407,15 @@ contract SubscriptionModuleFacet is
388
407
 
389
408
  // Get the actual new expiration time after successful renewal
390
409
  uint256 newExpiresAt = membershipFacet.expiresAt(sub.tokenId);
391
- sub.nextRenewalTime = _calculateNextRenewalTime(newExpiresAt, sub.installTime);
392
- sub.lastRenewalTime = uint40(block.timestamp);
410
+
411
+ // Calculate next renewal time ensuring it's strictly in the future
412
+ uint256 duration = membershipFacet.getMembershipDuration();
413
+ sub.nextRenewalTime = _enforceMinimumBuffer(
414
+ _calculateBaseRenewalTime(newExpiresAt, duration),
415
+ newExpiresAt,
416
+ duration
417
+ );
418
+ sub.lastRenewalTime = block.timestamp.toUint40();
393
419
  sub.spent += actualRenewalPrice;
394
420
 
395
421
  emit SubscriptionRenewed(
@@ -403,59 +429,75 @@ contract SubscriptionModuleFacet is
403
429
  emit SubscriptionSpent(params.account, params.entityId, actualRenewalPrice, sub.spent);
404
430
  }
405
431
 
406
- /// @dev Determines the appropriate renewal buffer time based on original membership duration
407
- /// @param expirationTime The expiration timestamp of the membership
408
- /// @param installTime The time when the subscription was installed
432
+ /// @dev Determines the appropriate renewal buffer time based on membership duration
433
+ /// @param duration The membership duration in seconds
409
434
  /// @return The appropriate buffer time in seconds before expiration
410
- function _getRenewalBuffer(
411
- uint256 expirationTime,
412
- uint256 installTime
413
- ) internal pure returns (uint256) {
414
- uint256 originalDuration = expirationTime >= installTime ? expirationTime - installTime : 0;
415
-
435
+ function _getRenewalBuffer(uint256 duration) internal pure returns (uint256) {
416
436
  // For memberships shorter than 1 hour, use immediate buffer (2 minutes)
417
- if (originalDuration <= 1 hours) return BUFFER_IMMEDIATE;
437
+ if (duration <= 1 hours) return BUFFER_IMMEDIATE;
418
438
 
419
439
  // For memberships shorter than 6 hours, use short buffer (1 hour)
420
- if (originalDuration <= 6 hours) return BUFFER_SHORT;
440
+ if (duration <= 6 hours) return BUFFER_SHORT;
421
441
 
422
442
  // For memberships shorter than 24 hours, use medium buffer (6 hours)
423
- if (originalDuration <= 24 hours) return BUFFER_MEDIUM;
443
+ if (duration <= 24 hours) return BUFFER_MEDIUM;
424
444
 
425
445
  // For memberships longer than 24 hours, use long buffer (12 hours)
426
446
  return BUFFER_LONG;
427
447
  }
428
448
 
429
- /// @dev Legacy function for backward compatibility - uses current time as install time
449
+ /// @dev Calculates the base renewal time without minimum buffer enforcement
430
450
  /// @param expirationTime The expiration timestamp of the membership
431
- /// @return The appropriate buffer time in seconds before expiration
432
- function _getRenewalBuffer(uint256 expirationTime) internal view returns (uint256) {
433
- return _getRenewalBuffer(expirationTime, block.timestamp);
434
- }
435
-
436
- /// @dev Calculates the correct next renewal time for a given expiration using install time
437
- /// @param expirationTime The expiration timestamp of the membership
438
- /// @param installTime The time when the subscription was installed
439
- /// @return The next renewal time as uint40
440
- function _calculateNextRenewalTime(
451
+ /// @param duration The membership duration in seconds
452
+ /// @return The base renewal time as uint40
453
+ function _calculateBaseRenewalTime(
441
454
  uint256 expirationTime,
442
- uint256 installTime
455
+ uint256 duration
443
456
  ) internal view returns (uint40) {
444
- if (expirationTime <= block.timestamp) return uint40(block.timestamp);
457
+ // If membership is already expired, schedule for the future
458
+ if (expirationTime <= block.timestamp) {
459
+ return (block.timestamp + duration).toUint40();
460
+ }
445
461
 
446
- uint256 buffer = _getRenewalBuffer(expirationTime, installTime);
462
+ uint256 buffer = _getRenewalBuffer(duration);
447
463
  uint256 timeUntilExpiration = expirationTime - block.timestamp;
448
464
 
449
- if (buffer >= timeUntilExpiration) return uint40(block.timestamp);
465
+ if (buffer >= timeUntilExpiration) {
466
+ // If buffer is larger than time until expiration,
467
+ // schedule for after the expiration by the same amount
468
+ return (expirationTime + (buffer - timeUntilExpiration)).toUint40();
469
+ }
450
470
 
451
- return uint40(expirationTime - buffer);
471
+ return (expirationTime - buffer).toUint40();
452
472
  }
453
473
 
454
- /// @dev Legacy function for backward compatibility - uses current time as install time
474
+ /// @dev Enforces minimum buffer to prevent double renewals
475
+ /// @param baseTime The base calculated renewal time
455
476
  /// @param expirationTime The expiration timestamp of the membership
456
- /// @return The next renewal time as uint40
457
- function _calculateNextRenewalTime(uint256 expirationTime) internal view returns (uint40) {
458
- return _calculateNextRenewalTime(expirationTime, block.timestamp);
477
+ /// @param duration The membership duration in seconds
478
+ /// @return The adjusted renewal time with minimum buffer enforced
479
+ function _enforceMinimumBuffer(
480
+ uint40 baseTime,
481
+ uint256 expirationTime,
482
+ uint256 duration
483
+ ) internal view returns (uint40) {
484
+ uint256 operatorBuffer = SubscriptionModuleStorage.getOperatorBuffer(msg.sender);
485
+
486
+ // If base time is far enough in the future, use it
487
+ if (baseTime > block.timestamp + operatorBuffer) {
488
+ return baseTime;
489
+ }
490
+
491
+ // For very short durations, schedule close to expiration with minimum buffer
492
+ if (duration <= 1 hours) {
493
+ return (expirationTime - operatorBuffer).toUint40();
494
+ }
495
+
496
+ // For longer durations, use standard calculation with minimum buffer
497
+ uint256 buffer = _getRenewalBuffer(duration);
498
+ uint256 minFutureTime = block.timestamp + duration - buffer;
499
+
500
+ return (minFutureTime > baseTime ? minFutureTime : baseTime).toUint40();
459
501
  }
460
502
 
461
503
  /// @dev Creates the runtime final data for the renewal
@@ -11,17 +11,27 @@ struct Subscription {
11
11
  uint40 lastRenewalTime; // 5 bytes
12
12
  uint40 nextRenewalTime; // 5 bytes
13
13
  bool active; // 1 byte
14
+ uint64 duration;
15
+ }
16
+
17
+ struct OperatorConfig {
18
+ uint256 interval;
19
+ uint256 buffer;
14
20
  }
15
21
 
16
22
  library SubscriptionModuleStorage {
17
23
  using EnumerableSetLib for EnumerableSetLib.Uint256Set;
18
24
  using EnumerableSetLib for EnumerableSetLib.AddressSet;
19
25
 
26
+ uint256 public constant KEEPER_INTERVAL = 5 minutes;
27
+ uint256 public constant MIN_RENEWAL_BUFFER = KEEPER_INTERVAL + 1 minutes; // Minimum buffer to prevent double renewals
28
+
20
29
  /// @custom:storage-location erc7201:towns.subscription.validation.module.storage
21
30
  struct Layout {
22
31
  EnumerableSetLib.AddressSet operators;
23
32
  mapping(address account => mapping(uint32 entityId => Subscription)) subscriptions;
24
33
  mapping(address account => EnumerableSetLib.Uint256Set entityIds) entityIds;
34
+ mapping(address operator => OperatorConfig) operatorConfig;
25
35
  }
26
36
 
27
37
  // keccak256(abi.encode(uint256(keccak256("towns.subscription.validation.module.storage")) - 1)) & ~bytes32(uint256(0xff))
@@ -33,4 +43,10 @@ library SubscriptionModuleStorage {
33
43
  $.slot := STORAGE_SLOT
34
44
  }
35
45
  }
46
+
47
+ function getOperatorBuffer(address operator) internal view returns (uint256) {
48
+ OperatorConfig storage config = getLayout().operatorConfig[operator];
49
+ if (config.interval == 0) return MIN_RENEWAL_BUFFER;
50
+ return config.buffer;
51
+ }
36
52
  }