@towns-protocol/contracts 0.0.369 → 0.0.371
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 +3 -3
- package/scripts/deployments/diamonds/DeployAppRegistry.s.sol +9 -9
- package/scripts/deployments/diamonds/DeploySubscriptionModule.s.sol +9 -9
- package/scripts/deployments/facets/DeploySubscriptionModuleFacet.s.sol +2 -0
- package/scripts/interactions/InteractDiamondCut.s.sol +4 -0
- package/scripts/interactions/helpers/AlphaHelper.sol +4 -2
- package/scripts/interactions/helpers/RiverConfigValues.sol +0 -2
- package/src/apps/modules/subscription/ISubscriptionModule.sol +13 -0
- package/src/apps/modules/subscription/SubscriptionModuleBase.sol +267 -0
- package/src/apps/modules/subscription/SubscriptionModuleFacet.sol +68 -249
- package/src/apps/modules/subscription/SubscriptionModuleStorage.sol +5 -1
- package/scripts/interactions/InteractEnableNewSnapshotFormat.s.sol +0 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towns-protocol/contracts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.371",
|
|
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.371",
|
|
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": "1b8084e16a1892d0cc2eee713b926c259dc4a93b"
|
|
61
61
|
}
|
|
@@ -54,7 +54,6 @@ contract DeployAppRegistry is IDiamondInitHelper, DiamondHelper, Deployer {
|
|
|
54
54
|
facetHelper.add("DiamondLoupeFacet");
|
|
55
55
|
facetHelper.add("IntrospectionFacet");
|
|
56
56
|
facetHelper.add("OwnableFacet");
|
|
57
|
-
facetHelper.add("MetadataFacet");
|
|
58
57
|
|
|
59
58
|
// Deploy the first batch of facets
|
|
60
59
|
facetHelper.deployBatch(deployer);
|
|
@@ -87,26 +86,27 @@ contract DeployAppRegistry is IDiamondInitHelper, DiamondHelper, Deployer {
|
|
|
87
86
|
facet,
|
|
88
87
|
DeployOwnable.makeInitData(deployer)
|
|
89
88
|
);
|
|
90
|
-
|
|
91
|
-
facet = facetHelper.predictAddress("MetadataFacet");
|
|
92
|
-
addFacet(
|
|
93
|
-
makeCut(facet, FacetCutAction.Add, DeployMetadata.selectors()),
|
|
94
|
-
facet,
|
|
95
|
-
DeployMetadata.makeInitData(bytes32("AppRegistry"), "")
|
|
96
|
-
);
|
|
97
89
|
}
|
|
98
90
|
|
|
99
91
|
function diamondInitParams(address deployer) public returns (Diamond.InitParams memory) {
|
|
100
92
|
// Queue up feature facets for batch deployment
|
|
101
93
|
facetHelper.add("MultiInit");
|
|
94
|
+
facetHelper.add("MetadataFacet");
|
|
102
95
|
facetHelper.add("UpgradeableBeaconFacet");
|
|
103
96
|
facetHelper.add("AppRegistryFacet");
|
|
104
97
|
facetHelper.add("SimpleApp");
|
|
105
98
|
|
|
106
99
|
facetHelper.deployBatch(deployer);
|
|
107
100
|
|
|
101
|
+
address facet = facetHelper.getDeployedAddress("MetadataFacet");
|
|
102
|
+
addFacet(
|
|
103
|
+
makeCut(facet, FacetCutAction.Add, DeployMetadata.selectors()),
|
|
104
|
+
facet,
|
|
105
|
+
DeployMetadata.makeInitData(bytes32("AppRegistry"), "")
|
|
106
|
+
);
|
|
107
|
+
|
|
108
108
|
address simpleApp = facetHelper.getDeployedAddress("SimpleApp");
|
|
109
|
-
|
|
109
|
+
facet = facetHelper.getDeployedAddress("UpgradeableBeaconFacet");
|
|
110
110
|
|
|
111
111
|
addFacet(
|
|
112
112
|
makeCut(facet, FacetCutAction.Add, DeployUpgradeableBeacon.selectors()),
|
|
@@ -62,13 +62,21 @@ contract DeploySubscriptionModule is DiamondHelper, Deployer, IDiamondInitHelper
|
|
|
62
62
|
function diamondInitParams(address deployer) public returns (Diamond.InitParams memory) {
|
|
63
63
|
// Queue up feature facets for batch deployment
|
|
64
64
|
facetHelper.add("MultiInit");
|
|
65
|
+
facetHelper.add("MetadataFacet");
|
|
65
66
|
facetHelper.add("SubscriptionModuleFacet");
|
|
66
67
|
|
|
67
68
|
// Deploy all facets in a batch
|
|
68
69
|
facetHelper.deployBatch(deployer);
|
|
69
70
|
|
|
70
71
|
// Add feature facets
|
|
71
|
-
address facet = facetHelper.getDeployedAddress("
|
|
72
|
+
address facet = facetHelper.getDeployedAddress("MetadataFacet");
|
|
73
|
+
addFacet(
|
|
74
|
+
makeCut(facet, FacetCutAction.Add, DeployMetadata.selectors()),
|
|
75
|
+
facet,
|
|
76
|
+
DeployMetadata.makeInitData(METADATA_NAME, "")
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
facet = facetHelper.getDeployedAddress("SubscriptionModuleFacet");
|
|
72
80
|
addFacet(
|
|
73
81
|
makeCut(facet, FacetCutAction.Add, DeploySubscriptionModuleFacet.selectors()),
|
|
74
82
|
facet,
|
|
@@ -95,7 +103,6 @@ contract DeploySubscriptionModule is DiamondHelper, Deployer, IDiamondInitHelper
|
|
|
95
103
|
facetHelper.add("DiamondLoupeFacet");
|
|
96
104
|
facetHelper.add("IntrospectionFacet");
|
|
97
105
|
facetHelper.add("OwnableFacet");
|
|
98
|
-
facetHelper.add("MetadataFacet");
|
|
99
106
|
|
|
100
107
|
// Get predicted addresses
|
|
101
108
|
address facet = facetHelper.predictAddress("DiamondCutFacet");
|
|
@@ -125,13 +132,6 @@ contract DeploySubscriptionModule is DiamondHelper, Deployer, IDiamondInitHelper
|
|
|
125
132
|
facet,
|
|
126
133
|
DeployOwnable.makeInitData(deployer)
|
|
127
134
|
);
|
|
128
|
-
|
|
129
|
-
facet = facetHelper.predictAddress("MetadataFacet");
|
|
130
|
-
addFacet(
|
|
131
|
-
makeCut(facet, FacetCutAction.Add, DeployMetadata.selectors()),
|
|
132
|
-
facet,
|
|
133
|
-
DeployMetadata.makeInitData(METADATA_NAME, "")
|
|
134
|
-
);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
function __deploy(address deployer) internal override returns (address) {
|
|
@@ -34,6 +34,8 @@ library DeploySubscriptionModuleFacet {
|
|
|
34
34
|
arr.p(SubscriptionModuleFacet.isOperator.selector);
|
|
35
35
|
arr.p(SubscriptionModuleFacet.grantOperator.selector);
|
|
36
36
|
arr.p(SubscriptionModuleFacet.revokeOperator.selector);
|
|
37
|
+
arr.p(SubscriptionModuleFacet.setSpaceFactory.selector);
|
|
38
|
+
arr.p(SubscriptionModuleFacet.getSpaceFactory.selector);
|
|
37
39
|
arr.p(bytes4(keccak256("MAX_BATCH_SIZE()")));
|
|
38
40
|
arr.p(bytes4(keccak256("GRACE_PERIOD()")));
|
|
39
41
|
|
|
@@ -13,6 +13,7 @@ import {AlphaHelper} from "./helpers/AlphaHelper.sol";
|
|
|
13
13
|
|
|
14
14
|
// fetch facet deployer contract
|
|
15
15
|
import {DeploySubscriptionModuleFacet} from "scripts/deployments/facets/DeploySubscriptionModuleFacet.s.sol";
|
|
16
|
+
import {SubscriptionModuleFacet} from "src/apps/modules/subscription/SubscriptionModuleFacet.sol";
|
|
16
17
|
|
|
17
18
|
contract InteractDiamondCut is Interaction, AlphaHelper {
|
|
18
19
|
function __interact(address deployer) internal override {
|
|
@@ -41,5 +42,8 @@ contract InteractDiamondCut is Interaction, AlphaHelper {
|
|
|
41
42
|
// execute the diamond cut
|
|
42
43
|
vm.broadcast(deployer);
|
|
43
44
|
IDiamondCut(diamond).diamondCut(baseFacets(), address(0), "");
|
|
45
|
+
|
|
46
|
+
vm.broadcast(deployer);
|
|
47
|
+
SubscriptionModuleFacet(diamond).setSpaceFactory(getDeployment("spaceFactory"));
|
|
44
48
|
}
|
|
45
49
|
}
|
|
@@ -8,6 +8,7 @@ import {IDiamondLoupe, IDiamondLoupeBase} from "@towns-protocol/diamond/src/face
|
|
|
8
8
|
import {IERC173} from "@towns-protocol/diamond/src/facets/ownable/IERC173.sol";
|
|
9
9
|
import {IOwnablePending} from "@towns-protocol/diamond/src/facets/ownable/pending/IOwnablePending.sol";
|
|
10
10
|
import {IDiamondInitHelper} from "scripts/deployments/diamonds/IDiamondInitHelper.sol";
|
|
11
|
+
import {IMetadata} from "src/diamond/facets/metadata/IMetadata.sol";
|
|
11
12
|
|
|
12
13
|
// libraries
|
|
13
14
|
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
|
|
@@ -55,13 +56,14 @@ abstract contract AlphaHelper is Interaction, DiamondHelper, IDiamondLoupeBase {
|
|
|
55
56
|
function getCoreFacetAddresses(
|
|
56
57
|
address diamond
|
|
57
58
|
) internal view returns (address[] memory coreFacets) {
|
|
58
|
-
coreFacets = new address[](
|
|
59
|
+
coreFacets = new address[](6);
|
|
59
60
|
|
|
60
61
|
coreFacets[0] = IDiamondLoupe(diamond).facetAddress(IDiamondCut.diamondCut.selector);
|
|
61
62
|
coreFacets[1] = IDiamondLoupe(diamond).facetAddress(IDiamondLoupe.facets.selector);
|
|
62
63
|
coreFacets[2] = IDiamondLoupe(diamond).facetAddress(IERC165.supportsInterface.selector);
|
|
63
64
|
coreFacets[3] = IDiamondLoupe(diamond).facetAddress(IERC173.owner.selector);
|
|
64
65
|
coreFacets[4] = IDiamondLoupe(diamond).facetAddress(IOwnablePending.currentOwner.selector);
|
|
66
|
+
coreFacets[5] = IDiamondLoupe(diamond).facetAddress(IMetadata.contractType.selector);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/// @notice Check if an address is a core facet that should not be removed
|
|
@@ -254,7 +256,7 @@ abstract contract AlphaHelper is Interaction, DiamondHelper, IDiamondLoupeBase {
|
|
|
254
256
|
}
|
|
255
257
|
|
|
256
258
|
if (removeSelectors.length() > 0) {
|
|
257
|
-
addCut(FacetCut(
|
|
259
|
+
addCut(FacetCut(facetAddr, FacetCutAction.Remove, asBytes4Array(removeSelectors)));
|
|
258
260
|
}
|
|
259
261
|
}
|
|
260
262
|
}
|
|
@@ -8,8 +8,6 @@ pragma solidity ^0.8.23;
|
|
|
8
8
|
// contracts
|
|
9
9
|
|
|
10
10
|
library RiverConfigValues {
|
|
11
|
-
bytes32 public constant ENABLE_NEW_SNAPSHOT_FORMAT =
|
|
12
|
-
keccak256("stream.enablenewsnapshotformat");
|
|
13
11
|
bytes32 public constant XCHAIN_BLOCKCHAINS = keccak256("xchain.blockchains");
|
|
14
12
|
bytes32 public constant NODE_BLOCKLIST = keccak256("node.blocklist");
|
|
15
13
|
bytes32 public constant STREAM_MINIBLOCK_REGISTRATION_FREQUENCY =
|
|
@@ -41,6 +41,10 @@ interface ISubscriptionModuleBase {
|
|
|
41
41
|
error SubscriptionModule__InsufficientBalance();
|
|
42
42
|
error SubscriptionModule__ActiveSubscription();
|
|
43
43
|
error SubscriptionModule__MembershipBanned();
|
|
44
|
+
error SubscriptionModule__MembershipExpired();
|
|
45
|
+
error SubscriptionModule__SubscriptionAlreadyInstalled();
|
|
46
|
+
error SubscriptionModule__SubscriptionNotInstalled();
|
|
47
|
+
|
|
44
48
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
45
49
|
/* Events */
|
|
46
50
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
@@ -88,6 +92,7 @@ interface ISubscriptionModuleBase {
|
|
|
88
92
|
|
|
89
93
|
event OperatorGranted(address indexed operator);
|
|
90
94
|
event OperatorRevoked(address indexed operator);
|
|
95
|
+
event SpaceFactoryChanged(address indexed spaceFactory);
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
interface ISubscriptionModule is ISubscriptionModuleBase {
|
|
@@ -137,4 +142,12 @@ interface ISubscriptionModule is ISubscriptionModuleBase {
|
|
|
137
142
|
/// @notice Revokes an operator access to call processRenewal
|
|
138
143
|
/// @param operator The address of the operator to revoke
|
|
139
144
|
function revokeOperator(address operator) external;
|
|
145
|
+
|
|
146
|
+
/// @notice Sets the space factory
|
|
147
|
+
/// @param spaceFactory The address of the space factory
|
|
148
|
+
function setSpaceFactory(address spaceFactory) external;
|
|
149
|
+
|
|
150
|
+
/// @notice Gets the space factory
|
|
151
|
+
/// @return The address of the space factory
|
|
152
|
+
function getSpaceFactory() external view returns (address);
|
|
140
153
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.29;
|
|
3
|
+
|
|
4
|
+
// interfaces
|
|
5
|
+
import {IModularAccount} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol";
|
|
6
|
+
import {IMembership} from "../../../spaces/facets/membership/IMembership.sol";
|
|
7
|
+
import {IBanning} from "../../../spaces/facets/banning/IBanning.sol";
|
|
8
|
+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
9
|
+
import {ISubscriptionModule, ISubscriptionModuleBase} from "./ISubscriptionModule.sol";
|
|
10
|
+
|
|
11
|
+
// libraries
|
|
12
|
+
import {LibCall} from "solady/utils/LibCall.sol";
|
|
13
|
+
import {ValidationLocatorLib} from "modular-account/src/libraries/ValidationLocatorLib.sol";
|
|
14
|
+
import {SafeCastLib} from "solady/utils/SafeCastLib.sol";
|
|
15
|
+
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
16
|
+
import {Validator} from "../../../utils/libraries/Validator.sol";
|
|
17
|
+
import {Subscription, SubscriptionModuleStorage} from "./SubscriptionModuleStorage.sol";
|
|
18
|
+
import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
|
|
19
|
+
|
|
20
|
+
/// @title Subscription Module Base
|
|
21
|
+
/// @notice Base contract with internal logic for subscription management
|
|
22
|
+
abstract contract SubscriptionModuleBase is ISubscriptionModuleBase {
|
|
23
|
+
using SafeCastLib for uint256;
|
|
24
|
+
using CustomRevert for bytes4;
|
|
25
|
+
using EnumerableSetLib for EnumerableSetLib.Uint256Set;
|
|
26
|
+
|
|
27
|
+
/// @dev Reasons why a renewal might be skipped
|
|
28
|
+
enum SkipReason {
|
|
29
|
+
NONE,
|
|
30
|
+
NOT_DUE,
|
|
31
|
+
INACTIVE,
|
|
32
|
+
PAST_GRACE,
|
|
33
|
+
MEMBERSHIP_BANNED,
|
|
34
|
+
NOT_OWNER,
|
|
35
|
+
RENEWAL_PRICE_CHANGED,
|
|
36
|
+
INSUFFICIENT_BALANCE,
|
|
37
|
+
DURATION_CHANGED
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
uint256 public constant GRACE_PERIOD = 3 days;
|
|
41
|
+
|
|
42
|
+
// Dynamic buffer times based on expiration proximity
|
|
43
|
+
uint256 public constant BUFFER_IMMEDIATE = 2 minutes; // For expirations within 1 hour
|
|
44
|
+
uint256 public constant BUFFER_SHORT = 1 hours; // For expirations within 6 hours
|
|
45
|
+
uint256 public constant BUFFER_MEDIUM = 6 hours; // For expirations within 24 hours
|
|
46
|
+
uint256 public constant BUFFER_LONG = 12 hours; // For expirations more than 24 hours away
|
|
47
|
+
|
|
48
|
+
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
49
|
+
/* Internal */
|
|
50
|
+
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
51
|
+
|
|
52
|
+
function _hasEntityId(
|
|
53
|
+
SubscriptionModuleStorage.Layout storage $,
|
|
54
|
+
address account,
|
|
55
|
+
uint32 entityId
|
|
56
|
+
) internal view returns (bool) {
|
|
57
|
+
return $.entityIds[account].contains(entityId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// @dev Syncs subscription state with current membership data
|
|
61
|
+
/// @param sub The subscription storage to update
|
|
62
|
+
/// @param membershipFacet The membership facet to query
|
|
63
|
+
/// @param expiresAt The current expiration timestamp
|
|
64
|
+
/// @param duration The membership duration
|
|
65
|
+
function _syncSubscriptionState(
|
|
66
|
+
Subscription storage sub,
|
|
67
|
+
IMembership membershipFacet,
|
|
68
|
+
uint256 expiresAt,
|
|
69
|
+
uint64 duration
|
|
70
|
+
) internal {
|
|
71
|
+
sub.active = true;
|
|
72
|
+
sub.lastKnownRenewalPrice = membershipFacet.getMembershipRenewalPrice(sub.tokenId);
|
|
73
|
+
sub.lastKnownExpiresAt = expiresAt;
|
|
74
|
+
sub.duration = duration;
|
|
75
|
+
sub.nextRenewalTime = _calculateBaseRenewalTime(expiresAt, duration);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// @dev Processes a single subscription renewal
|
|
79
|
+
/// @param sub The subscription to renew
|
|
80
|
+
/// @param params The parameters for the renewal
|
|
81
|
+
function _processRenewal(
|
|
82
|
+
Subscription storage sub,
|
|
83
|
+
ISubscriptionModule.RenewalParams calldata params,
|
|
84
|
+
IMembership membershipFacet,
|
|
85
|
+
uint256 actualRenewalPrice
|
|
86
|
+
) internal {
|
|
87
|
+
// Construct the renewal call to space contract
|
|
88
|
+
bytes memory renewalCall = abi.encodeCall(IMembership.renewMembership, (sub.tokenId));
|
|
89
|
+
|
|
90
|
+
// Create the data parameter for executeWithRuntimeValidation
|
|
91
|
+
// This should be an execute() call to the space contract
|
|
92
|
+
bytes memory executeData = abi.encodeCall(
|
|
93
|
+
IModularAccount.execute,
|
|
94
|
+
(
|
|
95
|
+
sub.space, // target
|
|
96
|
+
actualRenewalPrice, // value
|
|
97
|
+
renewalCall // data
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Use the proper pack function from ValidationLocatorLib
|
|
102
|
+
bytes memory authorization = ValidationLocatorLib.packSignature(
|
|
103
|
+
params.entityId,
|
|
104
|
+
false, // selector-based
|
|
105
|
+
bytes.concat(hex"ff", abi.encode(sub.space, sub.tokenId))
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Call executeWithRuntimeValidation with the correct parameters
|
|
109
|
+
bytes memory runtimeValidationCall = abi.encodeCall(
|
|
110
|
+
IModularAccount.executeWithRuntimeValidation,
|
|
111
|
+
(
|
|
112
|
+
executeData, // The execute() call data
|
|
113
|
+
authorization // Authorization for validation
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// External call happens here
|
|
118
|
+
LibCall.callContract(params.account, 0, runtimeValidationCall);
|
|
119
|
+
|
|
120
|
+
// Get the actual new expiration time after successful renewal
|
|
121
|
+
uint256 newExpiresAt = membershipFacet.expiresAt(sub.tokenId);
|
|
122
|
+
|
|
123
|
+
// Calculate next renewal time ensuring it's strictly in the future
|
|
124
|
+
uint256 duration = membershipFacet.getMembershipDuration();
|
|
125
|
+
sub.nextRenewalTime = _calculateBaseRenewalTime(newExpiresAt, duration);
|
|
126
|
+
sub.lastRenewalTime = block.timestamp.toUint40();
|
|
127
|
+
sub.lastKnownExpiresAt = newExpiresAt;
|
|
128
|
+
sub.spent += actualRenewalPrice;
|
|
129
|
+
|
|
130
|
+
emit SubscriptionRenewed(
|
|
131
|
+
params.account,
|
|
132
|
+
params.entityId,
|
|
133
|
+
sub.space,
|
|
134
|
+
sub.tokenId,
|
|
135
|
+
sub.nextRenewalTime,
|
|
136
|
+
newExpiresAt
|
|
137
|
+
);
|
|
138
|
+
emit SubscriptionSpent(params.account, params.entityId, actualRenewalPrice, sub.spent);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// @dev Determines the appropriate renewal buffer time based on membership duration
|
|
142
|
+
/// @param duration The membership duration in seconds
|
|
143
|
+
/// @return The appropriate buffer time in seconds before expiration
|
|
144
|
+
function _getRenewalBuffer(uint256 duration) internal pure returns (uint256) {
|
|
145
|
+
// For memberships shorter than 1 hour, use immediate buffer (2 minutes)
|
|
146
|
+
if (duration <= 1 hours) return BUFFER_IMMEDIATE;
|
|
147
|
+
|
|
148
|
+
// For memberships shorter than 6 hours, use short buffer (1 hour)
|
|
149
|
+
if (duration <= 6 hours) return BUFFER_SHORT;
|
|
150
|
+
|
|
151
|
+
// For memberships shorter than 24 hours, use medium buffer (6 hours)
|
|
152
|
+
if (duration <= 24 hours) return BUFFER_MEDIUM;
|
|
153
|
+
|
|
154
|
+
// For memberships longer than 24 hours, use long buffer (12 hours)
|
|
155
|
+
return BUFFER_LONG;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// @dev Calculates the base renewal time without minimum buffer enforcement
|
|
159
|
+
/// @param expirationTime The expiration timestamp of the membership
|
|
160
|
+
/// @param duration The membership duration in seconds
|
|
161
|
+
/// @return The base renewal time as uint40
|
|
162
|
+
function _calculateBaseRenewalTime(
|
|
163
|
+
uint256 expirationTime,
|
|
164
|
+
uint256 duration
|
|
165
|
+
) internal view returns (uint40) {
|
|
166
|
+
if (expirationTime <= block.timestamp) return block.timestamp.toUint40();
|
|
167
|
+
|
|
168
|
+
uint256 buffer = _getRenewalBuffer(duration);
|
|
169
|
+
uint256 timeUntilExpiration = expirationTime - block.timestamp;
|
|
170
|
+
|
|
171
|
+
if (buffer >= timeUntilExpiration) {
|
|
172
|
+
// If buffer is larger than time until expiration,
|
|
173
|
+
// schedule for after the expiration by the same amount
|
|
174
|
+
return (expirationTime + (buffer - timeUntilExpiration)).toUint40();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (expirationTime - buffer).toUint40();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// @dev Requires that msg.sender is the owner of the membership token
|
|
181
|
+
/// @param sub The subscription storage reference
|
|
182
|
+
function _requireOwnership(Subscription storage sub) internal view {
|
|
183
|
+
address owner = IERC721(sub.space).ownerOf(sub.tokenId);
|
|
184
|
+
if (msg.sender != owner) SubscriptionModule__InvalidCaller.selector.revertWith();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// @dev Validates renewal eligibility and returns skip reason if any
|
|
188
|
+
/// @param sub The subscription to validate
|
|
189
|
+
/// @param account The account address
|
|
190
|
+
/// @param actualRenewalPrice The current renewal price
|
|
191
|
+
/// @param actualDuration The current membership duration
|
|
192
|
+
/// @return shouldSkip Whether to skip this renewal
|
|
193
|
+
/// @return shouldPause Whether to pause the subscription (only relevant if shouldSkip is true)
|
|
194
|
+
/// @return reason The skip reason if shouldSkip is true
|
|
195
|
+
function _validateRenewalEligibility(
|
|
196
|
+
Subscription storage sub,
|
|
197
|
+
address account,
|
|
198
|
+
uint256 actualRenewalPrice,
|
|
199
|
+
uint256 actualDuration
|
|
200
|
+
) internal view returns (bool shouldSkip, bool shouldPause, SkipReason reason) {
|
|
201
|
+
// Check if renewal is due
|
|
202
|
+
if (sub.nextRenewalTime > block.timestamp) {
|
|
203
|
+
return (true, false, SkipReason.NOT_DUE);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if subscription is active
|
|
207
|
+
if (!sub.active) {
|
|
208
|
+
return (true, false, SkipReason.INACTIVE);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check if past grace period
|
|
212
|
+
if (sub.nextRenewalTime + GRACE_PERIOD < block.timestamp) {
|
|
213
|
+
return (true, true, SkipReason.PAST_GRACE);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check if membership is banned
|
|
217
|
+
if (IBanning(sub.space).isBanned(sub.tokenId)) {
|
|
218
|
+
return (true, true, SkipReason.MEMBERSHIP_BANNED);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check if account is still the owner
|
|
222
|
+
if (IERC721(sub.space).ownerOf(sub.tokenId) != account) {
|
|
223
|
+
return (true, true, SkipReason.NOT_OWNER);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check if renewal price changed
|
|
227
|
+
if (sub.lastKnownRenewalPrice != actualRenewalPrice) {
|
|
228
|
+
return (true, true, SkipReason.RENEWAL_PRICE_CHANGED);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check if account has sufficient balance
|
|
232
|
+
if (account.balance < actualRenewalPrice) {
|
|
233
|
+
return (true, true, SkipReason.INSUFFICIENT_BALANCE);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check if duration changed
|
|
237
|
+
if (sub.duration != actualDuration) {
|
|
238
|
+
return (true, true, SkipReason.DURATION_CHANGED);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return (false, false, SkipReason.NONE);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// @dev Converts a SkipReason enum to its string representation
|
|
245
|
+
/// @param reason The skip reason to convert
|
|
246
|
+
/// @return The string representation of the reason
|
|
247
|
+
function _skipReasonToString(SkipReason reason) internal pure returns (string memory) {
|
|
248
|
+
if (reason == SkipReason.NOT_DUE) return "NOT_DUE";
|
|
249
|
+
if (reason == SkipReason.INACTIVE) return "INACTIVE";
|
|
250
|
+
if (reason == SkipReason.PAST_GRACE) return "PAST_GRACE";
|
|
251
|
+
if (reason == SkipReason.MEMBERSHIP_BANNED) return "MEMBERSHIP_BANNED";
|
|
252
|
+
if (reason == SkipReason.NOT_OWNER) return "NOT_OWNER";
|
|
253
|
+
if (reason == SkipReason.RENEWAL_PRICE_CHANGED) return "RENEWAL_PRICE_CHANGED";
|
|
254
|
+
if (reason == SkipReason.INSUFFICIENT_BALANCE) return "INSUFFICIENT_BALANCE";
|
|
255
|
+
if (reason == SkipReason.DURATION_CHANGED) return "DURATION_CHANGED";
|
|
256
|
+
return "";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function _pauseSubscription(
|
|
260
|
+
Subscription storage sub,
|
|
261
|
+
address account,
|
|
262
|
+
uint32 entityId
|
|
263
|
+
) internal {
|
|
264
|
+
sub.active = false;
|
|
265
|
+
emit SubscriptionPaused(account, entityId);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -3,7 +3,6 @@ pragma solidity ^0.8.29;
|
|
|
3
3
|
|
|
4
4
|
// interfaces
|
|
5
5
|
import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol";
|
|
6
|
-
import {IModularAccount} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol";
|
|
7
6
|
import {IValidationHookModule} from "@erc6900/reference-implementation/interfaces/IValidationHookModule.sol";
|
|
8
7
|
import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";
|
|
9
8
|
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
@@ -14,11 +13,10 @@ import {IBanning} from "../../../spaces/facets/banning/IBanning.sol";
|
|
|
14
13
|
// libraries
|
|
15
14
|
import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
|
|
16
15
|
import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
|
|
17
|
-
import {LibCall} from "solady/utils/LibCall.sol";
|
|
18
|
-
import {ValidationLocatorLib} from "modular-account/src/libraries/ValidationLocatorLib.sol";
|
|
19
16
|
import {ReentrancyGuardTransient} from "solady/utils/ReentrancyGuardTransient.sol";
|
|
20
17
|
import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
|
|
21
18
|
import {Validator} from "../../../utils/libraries/Validator.sol";
|
|
19
|
+
import {IArchitect} from "../../../factory/facets/architect/IArchitect.sol";
|
|
22
20
|
import {Subscription, SubscriptionModuleStorage} from "./SubscriptionModuleStorage.sol";
|
|
23
21
|
import {SafeCastLib} from "solady/utils/SafeCastLib.sol";
|
|
24
22
|
|
|
@@ -26,6 +24,7 @@ import {SafeCastLib} from "solady/utils/SafeCastLib.sol";
|
|
|
26
24
|
import {ModuleBase} from "modular-account/src/modules/ModuleBase.sol";
|
|
27
25
|
import {OwnableBase} from "@towns-protocol/diamond/src/facets/ownable/OwnableBase.sol";
|
|
28
26
|
import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
|
|
27
|
+
import {SubscriptionModuleBase} from "./SubscriptionModuleBase.sol";
|
|
29
28
|
|
|
30
29
|
/// @title Subscription Module
|
|
31
30
|
/// @notice Module for managing subscriptions to spaces
|
|
@@ -36,6 +35,7 @@ contract SubscriptionModuleFacet is
|
|
|
36
35
|
ModuleBase,
|
|
37
36
|
OwnableBase,
|
|
38
37
|
ReentrancyGuardTransient,
|
|
38
|
+
SubscriptionModuleBase,
|
|
39
39
|
Facet
|
|
40
40
|
{
|
|
41
41
|
using EnumerableSetLib for EnumerableSetLib.Uint256Set;
|
|
@@ -46,13 +46,6 @@ contract SubscriptionModuleFacet is
|
|
|
46
46
|
uint256 internal constant _SIG_VALIDATION_FAILED = 1;
|
|
47
47
|
|
|
48
48
|
uint256 public constant MAX_BATCH_SIZE = 50;
|
|
49
|
-
uint256 public constant GRACE_PERIOD = 3 days;
|
|
50
|
-
|
|
51
|
-
// Dynamic buffer times based on expiration proximity
|
|
52
|
-
uint256 public constant BUFFER_IMMEDIATE = 2 minutes; // For expirations within 1 hour
|
|
53
|
-
uint256 public constant BUFFER_SHORT = 1 hours; // For expirations within 6 hours
|
|
54
|
-
uint256 public constant BUFFER_MEDIUM = 6 hours; // For expirations within 24 hours
|
|
55
|
-
uint256 public constant BUFFER_LONG = 12 hours; // For expirations more than 24 hours away
|
|
56
49
|
|
|
57
50
|
function __SubscriptionModule_init() external onlyInitializing {
|
|
58
51
|
_addInterface(type(ISubscriptionModule).interfaceId);
|
|
@@ -88,19 +81,32 @@ contract SubscriptionModuleFacet is
|
|
|
88
81
|
|
|
89
82
|
SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
|
|
90
83
|
|
|
84
|
+
if (IArchitect($.spaceFactory).getTokenIdBySpace(space) == 0)
|
|
85
|
+
SubscriptionModule__InvalidSpace.selector.revertWith();
|
|
86
|
+
|
|
91
87
|
if (!$.entityIds[msg.sender].add(entityId))
|
|
92
88
|
SubscriptionModule__InvalidEntityId.selector.revertWith();
|
|
93
89
|
|
|
90
|
+
if ($.tokenIdByAccountBySpace[msg.sender][space] != 0)
|
|
91
|
+
SubscriptionModule__SubscriptionAlreadyInstalled.selector.revertWith();
|
|
92
|
+
|
|
93
|
+
$.tokenIdByAccountBySpace[msg.sender][space] = tokenId;
|
|
94
|
+
|
|
94
95
|
IMembership membershipFacet = IMembership(space);
|
|
96
|
+
|
|
95
97
|
uint256 expiresAt = membershipFacet.expiresAt(tokenId);
|
|
96
|
-
|
|
98
|
+
|
|
99
|
+
// Prevent installation with expired membership
|
|
100
|
+
if (expiresAt <= block.timestamp)
|
|
101
|
+
SubscriptionModule__MembershipExpired.selector.revertWith();
|
|
102
|
+
|
|
103
|
+
uint64 duration = membershipFacet.getMembershipDuration();
|
|
97
104
|
|
|
98
105
|
Subscription storage sub = $.subscriptions[msg.sender][entityId];
|
|
99
106
|
sub.space = space;
|
|
100
|
-
sub.active = true;
|
|
101
107
|
sub.tokenId = tokenId;
|
|
102
108
|
sub.installTime = block.timestamp.toUint40();
|
|
103
|
-
sub
|
|
109
|
+
_syncSubscriptionState(sub, membershipFacet, expiresAt, duration);
|
|
104
110
|
|
|
105
111
|
emit SubscriptionConfigured(
|
|
106
112
|
msg.sender,
|
|
@@ -121,6 +127,9 @@ contract SubscriptionModuleFacet is
|
|
|
121
127
|
if (!$.entityIds[msg.sender].remove(entityId))
|
|
122
128
|
SubscriptionModule__InvalidEntityId.selector.revertWith();
|
|
123
129
|
|
|
130
|
+
Subscription storage sub = $.subscriptions[msg.sender][entityId];
|
|
131
|
+
|
|
132
|
+
delete $.tokenIdByAccountBySpace[msg.sender][sub.space];
|
|
124
133
|
delete $.subscriptions[msg.sender][entityId];
|
|
125
134
|
|
|
126
135
|
emit SubscriptionDeactivated(msg.sender, entityId);
|
|
@@ -158,7 +167,6 @@ contract SubscriptionModuleFacet is
|
|
|
158
167
|
if (sender != address(this)) SubscriptionModule__InvalidSender.selector.revertWith();
|
|
159
168
|
bool active = SubscriptionModuleStorage.getLayout().subscriptions[account][entityId].active;
|
|
160
169
|
if (!active) SubscriptionModule__InactiveSubscription.selector.revertWith();
|
|
161
|
-
return;
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
/// @inheritdoc IValidationHookModule
|
|
@@ -203,68 +211,46 @@ contract SubscriptionModuleFacet is
|
|
|
203
211
|
if (!$.operators.contains(msg.sender))
|
|
204
212
|
SubscriptionModule__InvalidCaller.selector.revertWith();
|
|
205
213
|
|
|
206
|
-
IMembership membershipFacet;
|
|
207
|
-
|
|
208
214
|
for (uint256 i; i < paramsLen; ++i) {
|
|
209
215
|
Subscription storage sub = $.subscriptions[params[i].account][params[i].entityId];
|
|
210
216
|
|
|
211
|
-
|
|
212
|
-
if (sub.nextRenewalTime > block.timestamp) {
|
|
213
|
-
emit SubscriptionNotDue(params[i].account, params[i].entityId);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Skip inactive subscriptions
|
|
218
|
-
if (!sub.active) {
|
|
219
|
-
emit BatchRenewalSkipped(params[i].account, params[i].entityId, "INACTIVE");
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Skip if past grace period
|
|
224
|
-
if (sub.nextRenewalTime + GRACE_PERIOD < block.timestamp) {
|
|
225
|
-
_pauseSubscription(sub, params[i].account, params[i].entityId);
|
|
226
|
-
emit BatchRenewalSkipped(params[i].account, params[i].entityId, "PAST_GRACE");
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
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
|
-
|
|
240
|
-
// Skip if account isn't owner anymore (for safety)
|
|
241
|
-
if (IERC721(sub.space).ownerOf(sub.tokenId) != params[i].account) {
|
|
242
|
-
_pauseSubscription(sub, params[i].account, params[i].entityId);
|
|
243
|
-
emit BatchRenewalSkipped(params[i].account, params[i].entityId, "NOT_OWNER");
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
membershipFacet = IMembership(sub.space);
|
|
248
|
-
|
|
217
|
+
IMembership membershipFacet = IMembership(sub.space);
|
|
249
218
|
uint256 actualRenewalPrice = membershipFacet.getMembershipRenewalPrice(sub.tokenId);
|
|
219
|
+
uint256 actualDuration = membershipFacet.getMembershipDuration();
|
|
220
|
+
|
|
221
|
+
// Validate renewal eligibility
|
|
222
|
+
(bool shouldSkip, bool shouldPause, SkipReason reason) = _validateRenewalEligibility(
|
|
223
|
+
sub,
|
|
224
|
+
params[i].account,
|
|
225
|
+
actualRenewalPrice,
|
|
226
|
+
actualDuration
|
|
227
|
+
);
|
|
250
228
|
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
params[i].account,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
229
|
+
if (shouldSkip) {
|
|
230
|
+
// Handle special case for "NOT_DUE" - different event
|
|
231
|
+
if (reason == SkipReason.NOT_DUE) {
|
|
232
|
+
emit SubscriptionNotDue(params[i].account, params[i].entityId);
|
|
233
|
+
} else {
|
|
234
|
+
// Pause subscription if needed
|
|
235
|
+
if (shouldPause) {
|
|
236
|
+
_pauseSubscription(sub, params[i].account, params[i].entityId);
|
|
237
|
+
}
|
|
238
|
+
emit BatchRenewalSkipped(
|
|
239
|
+
params[i].account,
|
|
240
|
+
params[i].entityId,
|
|
241
|
+
_skipReasonToString(reason)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
258
244
|
continue;
|
|
259
245
|
}
|
|
260
246
|
|
|
261
|
-
|
|
262
|
-
uint256
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
sub.nextRenewalTime = nextRenewalTime;
|
|
247
|
+
// Check for manual renewal (expiresAt changed)
|
|
248
|
+
uint256 actualExpiresAt = membershipFacet.expiresAt(sub.tokenId);
|
|
249
|
+
if (sub.lastKnownExpiresAt != actualExpiresAt) {
|
|
250
|
+
sub.nextRenewalTime = _calculateBaseRenewalTime(actualExpiresAt, sub.duration);
|
|
251
|
+
sub.lastKnownExpiresAt = actualExpiresAt;
|
|
267
252
|
emit SubscriptionSynced(params[i].account, params[i].entityId, sub.nextRenewalTime);
|
|
253
|
+
continue;
|
|
268
254
|
}
|
|
269
255
|
|
|
270
256
|
_processRenewal(sub, params[i], membershipFacet, actualRenewalPrice);
|
|
@@ -295,34 +281,25 @@ contract SubscriptionModuleFacet is
|
|
|
295
281
|
|
|
296
282
|
if (sub.active) SubscriptionModule__ActiveSubscription.selector.revertWith();
|
|
297
283
|
|
|
298
|
-
|
|
299
|
-
if (msg.sender != owner) SubscriptionModule__InvalidCaller.selector.revertWith();
|
|
284
|
+
_requireOwnership(sub);
|
|
300
285
|
|
|
301
286
|
IMembership membershipFacet = IMembership(sub.space);
|
|
302
287
|
uint256 expiresAt = membershipFacet.expiresAt(sub.tokenId);
|
|
303
|
-
|
|
288
|
+
uint64 duration = membershipFacet.getMembershipDuration();
|
|
304
289
|
|
|
305
|
-
|
|
306
|
-
uint40 correctNextRenewalTime = _calculateBaseRenewalTime(expiresAt, duration);
|
|
307
|
-
if (sub.nextRenewalTime != correctNextRenewalTime) {
|
|
308
|
-
sub.nextRenewalTime = correctNextRenewalTime;
|
|
309
|
-
emit SubscriptionSynced(msg.sender, entityId, sub.nextRenewalTime);
|
|
310
|
-
}
|
|
290
|
+
_syncSubscriptionState(sub, membershipFacet, expiresAt, duration);
|
|
311
291
|
|
|
312
|
-
sub.active = true;
|
|
313
292
|
emit SubscriptionActivated(msg.sender, entityId);
|
|
314
293
|
}
|
|
315
294
|
|
|
316
295
|
/// @inheritdoc ISubscriptionModule
|
|
317
296
|
function pauseSubscription(uint32 entityId) external nonReentrant {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
];
|
|
297
|
+
SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
|
|
298
|
+
Subscription storage sub = $.subscriptions[msg.sender][entityId];
|
|
321
299
|
|
|
322
300
|
if (!sub.active) SubscriptionModule__InactiveSubscription.selector.revertWith();
|
|
323
301
|
|
|
324
|
-
|
|
325
|
-
if (msg.sender != owner) SubscriptionModule__InvalidCaller.selector.revertWith();
|
|
302
|
+
_requireOwnership(sub);
|
|
326
303
|
|
|
327
304
|
_pauseSubscription(sub, msg.sender, entityId);
|
|
328
305
|
}
|
|
@@ -352,176 +329,18 @@ contract SubscriptionModuleFacet is
|
|
|
352
329
|
}
|
|
353
330
|
|
|
354
331
|
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
|
|
355
|
-
/*
|
|
332
|
+
/* SPACE FACTORY */
|
|
356
333
|
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
|
|
357
334
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return $.entityIds[account].contains(entityId);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/// @dev Processes a single subscription renewal
|
|
367
|
-
/// @param sub The subscription to renew
|
|
368
|
-
/// @param params The parameters for the renewal
|
|
369
|
-
function _processRenewal(
|
|
370
|
-
Subscription storage sub,
|
|
371
|
-
RenewalParams calldata params,
|
|
372
|
-
IMembership membershipFacet,
|
|
373
|
-
uint256 actualRenewalPrice
|
|
374
|
-
) internal {
|
|
375
|
-
// Construct the renewal call to space contract
|
|
376
|
-
bytes memory renewalCall = abi.encodeCall(IMembership.renewMembership, (sub.tokenId));
|
|
377
|
-
|
|
378
|
-
// Create the data parameter for executeWithRuntimeValidation
|
|
379
|
-
// This should be an execute() call to the space contract
|
|
380
|
-
bytes memory executeData = abi.encodeCall(
|
|
381
|
-
IModularAccount.execute,
|
|
382
|
-
(
|
|
383
|
-
sub.space, // target
|
|
384
|
-
actualRenewalPrice, // value
|
|
385
|
-
renewalCall // data
|
|
386
|
-
)
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
// Use the proper pack function from ValidationLocatorLib
|
|
390
|
-
bytes memory authorization = ValidationLocatorLib.packSignature(
|
|
391
|
-
params.entityId,
|
|
392
|
-
false, // selector-based
|
|
393
|
-
bytes.concat(hex"ff", abi.encode(sub.space, sub.tokenId))
|
|
394
|
-
);
|
|
395
|
-
|
|
396
|
-
// Call executeWithRuntimeValidation with the correct parameters
|
|
397
|
-
bytes memory runtimeValidationCall = abi.encodeCall(
|
|
398
|
-
IModularAccount.executeWithRuntimeValidation,
|
|
399
|
-
(
|
|
400
|
-
executeData, // The execute() call data
|
|
401
|
-
authorization // Authorization for validation
|
|
402
|
-
)
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
// External call happens here
|
|
406
|
-
LibCall.callContract(params.account, 0, runtimeValidationCall);
|
|
407
|
-
|
|
408
|
-
// Get the actual new expiration time after successful renewal
|
|
409
|
-
uint256 newExpiresAt = membershipFacet.expiresAt(sub.tokenId);
|
|
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();
|
|
419
|
-
sub.spent += actualRenewalPrice;
|
|
420
|
-
|
|
421
|
-
emit SubscriptionRenewed(
|
|
422
|
-
params.account,
|
|
423
|
-
params.entityId,
|
|
424
|
-
sub.space,
|
|
425
|
-
sub.tokenId,
|
|
426
|
-
sub.nextRenewalTime,
|
|
427
|
-
newExpiresAt
|
|
428
|
-
);
|
|
429
|
-
emit SubscriptionSpent(params.account, params.entityId, actualRenewalPrice, sub.spent);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/// @dev Determines the appropriate renewal buffer time based on membership duration
|
|
433
|
-
/// @param duration The membership duration in seconds
|
|
434
|
-
/// @return The appropriate buffer time in seconds before expiration
|
|
435
|
-
function _getRenewalBuffer(uint256 duration) internal pure returns (uint256) {
|
|
436
|
-
// For memberships shorter than 1 hour, use immediate buffer (2 minutes)
|
|
437
|
-
if (duration <= 1 hours) return BUFFER_IMMEDIATE;
|
|
438
|
-
|
|
439
|
-
// For memberships shorter than 6 hours, use short buffer (1 hour)
|
|
440
|
-
if (duration <= 6 hours) return BUFFER_SHORT;
|
|
441
|
-
|
|
442
|
-
// For memberships shorter than 24 hours, use medium buffer (6 hours)
|
|
443
|
-
if (duration <= 24 hours) return BUFFER_MEDIUM;
|
|
444
|
-
|
|
445
|
-
// For memberships longer than 24 hours, use long buffer (12 hours)
|
|
446
|
-
return BUFFER_LONG;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/// @dev Calculates the base renewal time without minimum buffer enforcement
|
|
450
|
-
/// @param expirationTime The expiration timestamp of the membership
|
|
451
|
-
/// @param duration The membership duration in seconds
|
|
452
|
-
/// @return The base renewal time as uint40
|
|
453
|
-
function _calculateBaseRenewalTime(
|
|
454
|
-
uint256 expirationTime,
|
|
455
|
-
uint256 duration
|
|
456
|
-
) internal view returns (uint40) {
|
|
457
|
-
// If membership is already expired, schedule for the future
|
|
458
|
-
if (expirationTime <= block.timestamp) {
|
|
459
|
-
return (block.timestamp + duration).toUint40();
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
uint256 buffer = _getRenewalBuffer(duration);
|
|
463
|
-
uint256 timeUntilExpiration = expirationTime - block.timestamp;
|
|
464
|
-
|
|
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
|
-
}
|
|
470
|
-
|
|
471
|
-
return (expirationTime - buffer).toUint40();
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/// @dev Enforces minimum buffer to prevent double renewals
|
|
475
|
-
/// @param baseTime The base calculated renewal time
|
|
476
|
-
/// @param expirationTime The expiration timestamp of the membership
|
|
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();
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/// @dev Creates the runtime final data for the renewal
|
|
504
|
-
/// @param entityId The entity ID of the subscription
|
|
505
|
-
/// @param finalData The final data for the renewal
|
|
506
|
-
/// @return The runtime final data
|
|
507
|
-
function _runtimeFinal(
|
|
508
|
-
uint32 entityId,
|
|
509
|
-
bytes memory finalData
|
|
510
|
-
) internal pure returns (bytes memory) {
|
|
511
|
-
return
|
|
512
|
-
ValidationLocatorLib.packSignature(
|
|
513
|
-
entityId,
|
|
514
|
-
false, // selector-based
|
|
515
|
-
bytes.concat(hex"ff", finalData)
|
|
516
|
-
);
|
|
335
|
+
/// @inheritdoc ISubscriptionModule
|
|
336
|
+
function setSpaceFactory(address spaceFactory) external onlyOwner {
|
|
337
|
+
Validator.checkAddress(spaceFactory);
|
|
338
|
+
SubscriptionModuleStorage.getLayout().spaceFactory = spaceFactory;
|
|
339
|
+
emit SpaceFactoryChanged(spaceFactory);
|
|
517
340
|
}
|
|
518
341
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
uint32 entityId
|
|
523
|
-
) internal {
|
|
524
|
-
sub.active = false;
|
|
525
|
-
emit SubscriptionPaused(account, entityId);
|
|
342
|
+
/// @inheritdoc ISubscriptionModule
|
|
343
|
+
function getSpaceFactory() external view returns (address) {
|
|
344
|
+
return SubscriptionModuleStorage.getLayout().spaceFactory;
|
|
526
345
|
}
|
|
527
346
|
}
|
|
@@ -11,7 +11,9 @@ struct Subscription {
|
|
|
11
11
|
uint40 lastRenewalTime; // 5 bytes
|
|
12
12
|
uint40 nextRenewalTime; // 5 bytes
|
|
13
13
|
bool active; // 1 byte
|
|
14
|
-
uint64 duration;
|
|
14
|
+
uint64 duration; // 8 bytes
|
|
15
|
+
uint256 lastKnownRenewalPrice; // 32 bytes
|
|
16
|
+
uint256 lastKnownExpiresAt; // 32 bytes
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
struct OperatorConfig {
|
|
@@ -32,6 +34,8 @@ library SubscriptionModuleStorage {
|
|
|
32
34
|
mapping(address account => mapping(uint32 entityId => Subscription)) subscriptions;
|
|
33
35
|
mapping(address account => EnumerableSetLib.Uint256Set entityIds) entityIds;
|
|
34
36
|
mapping(address operator => OperatorConfig) operatorConfig;
|
|
37
|
+
mapping(address account => mapping(address space => uint256 tokenId)) tokenIdByAccountBySpace;
|
|
38
|
+
address spaceFactory;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
// keccak256(abi.encode(uint256(keccak256("towns.subscription.validation.module.storage")) - 1)) & ~bytes32(uint256(0xff))
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity ^0.8.24;
|
|
3
|
-
|
|
4
|
-
// interfaces
|
|
5
|
-
import {IRiverConfig} from "src/river/registry/facets/config/IRiverConfig.sol";
|
|
6
|
-
|
|
7
|
-
// libraries
|
|
8
|
-
import {RiverConfigValues} from "scripts/interactions/helpers/RiverConfigValues.sol";
|
|
9
|
-
|
|
10
|
-
// contracts
|
|
11
|
-
import {Interaction} from "scripts/common/Interaction.s.sol";
|
|
12
|
-
|
|
13
|
-
contract InteractEnableNewSnapshotFormat is Interaction {
|
|
14
|
-
function __interact(address deployer) internal override {
|
|
15
|
-
address riverRegistry = getDeployment("riverRegistry");
|
|
16
|
-
|
|
17
|
-
uint64 value = 1;
|
|
18
|
-
|
|
19
|
-
vm.startBroadcast(deployer);
|
|
20
|
-
IRiverConfig(riverRegistry).setConfiguration(
|
|
21
|
-
RiverConfigValues.ENABLE_NEW_SNAPSHOT_FORMAT,
|
|
22
|
-
0,
|
|
23
|
-
abi.encode(value)
|
|
24
|
-
);
|
|
25
|
-
vm.stopBroadcast();
|
|
26
|
-
}
|
|
27
|
-
}
|