@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.
|
|
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.
|
|
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": "
|
|
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
|
|
111
|
-
/// @param
|
|
112
|
-
/// @return The renewal buffer for the
|
|
113
|
-
function getRenewalBuffer(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 =
|
|
96
|
-
sub.nextRenewalTime =
|
|
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
|
-
|
|
262
|
+
uint256 duration = membershipFacet.getMembershipDuration();
|
|
263
|
+
uint40 nextRenewalTime = _calculateBaseRenewalTime(expiresAt, duration);
|
|
246
264
|
|
|
247
|
-
if (sub.nextRenewalTime !=
|
|
248
|
-
sub.nextRenewalTime =
|
|
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
|
|
266
|
-
return _getRenewalBuffer(
|
|
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 =
|
|
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
|
-
|
|
392
|
-
|
|
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
|
|
407
|
-
/// @param
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
449
|
+
/// @dev Calculates the base renewal time without minimum buffer enforcement
|
|
430
450
|
/// @param expirationTime The expiration timestamp of the membership
|
|
431
|
-
/// @
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
455
|
+
uint256 duration
|
|
443
456
|
) internal view returns (uint40) {
|
|
444
|
-
|
|
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(
|
|
462
|
+
uint256 buffer = _getRenewalBuffer(duration);
|
|
447
463
|
uint256 timeUntilExpiration = expirationTime - block.timestamp;
|
|
448
464
|
|
|
449
|
-
if (buffer >= timeUntilExpiration)
|
|
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
|
|
471
|
+
return (expirationTime - buffer).toUint40();
|
|
452
472
|
}
|
|
453
473
|
|
|
454
|
-
/// @dev
|
|
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
|
-
/// @
|
|
457
|
-
|
|
458
|
-
|
|
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
|
}
|