@towns-protocol/contracts 0.0.336 → 0.0.337

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.336",
3
+ "version": "0.0.337",
4
4
  "packageManager": "yarn@3.8.0",
5
5
  "scripts": {
6
6
  "build-types": "bash scripts/build-contract-types.sh",
@@ -25,6 +25,7 @@
25
25
  "@towns-protocol/diamond": "^0.6.3",
26
26
  "@uniswap/permit2": "https://github.com/towns-protocol/permit2/archive/refs/tags/v1.0.0.tar.gz",
27
27
  "crypto-lib": "https://github.com/towns-protocol/crypto-lib/archive/refs/tags/v1.0.0.tar.gz",
28
+ "modular-account": "https://github.com/towns-protocol/modular-account/archive/refs/tags/v1.0.0.tar.gz",
28
29
  "solady": "^0.1.24"
29
30
  },
30
31
  "devDependencies": {
@@ -33,7 +34,7 @@
33
34
  "@layerzerolabs/oapp-evm": "^0.3.2",
34
35
  "@openzeppelin/merkle-tree": "^1.0.8",
35
36
  "@prb/test": "^0.6.4",
36
- "@towns-protocol/prettier-config": "^0.0.336",
37
+ "@towns-protocol/prettier-config": "^0.0.337",
37
38
  "@typechain/ethers-v5": "^10.1.1",
38
39
  "@wagmi/cli": "^2.2.0",
39
40
  "account-abstraction": "https://github.com/eth-infinitism/account-abstraction/archive/refs/tags/v0.7.0.tar.gz",
@@ -56,5 +57,5 @@
56
57
  "publishConfig": {
57
58
  "access": "public"
58
59
  },
59
- "gitHead": "71595d375f76d341ff01c55a0fb3d749c04d710f"
60
+ "gitHead": "a8dae6ecef2c3bc7713acf56e6738fd47d608482"
60
61
  }
@@ -0,0 +1,118 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ // libraries
5
+ import {DeployDiamondCut} from "@towns-protocol/diamond/scripts/deployments/facets/DeployDiamondCut.sol";
6
+ import {DeployDiamondLoupe} from "@towns-protocol/diamond/scripts/deployments/facets/DeployDiamondLoupe.sol";
7
+ import {DeployIntrospection} from "@towns-protocol/diamond/scripts/deployments/facets/DeployIntrospection.sol";
8
+ import {DeployOwnable} from "@towns-protocol/diamond/scripts/deployments/facets/DeployOwnable.sol";
9
+ import {DeployMetadata} from "../facets/DeployMetadata.s.sol";
10
+ import {DeploySubscriptionModuleFacet} from "../facets/DeploySubscriptionModuleFacet.s.sol";
11
+ import {LibString} from "solady/utils/LibString.sol";
12
+
13
+ // contracts
14
+ import {Diamond} from "@towns-protocol/diamond/src/Diamond.sol";
15
+ import {MultiInit} from "@towns-protocol/diamond/src/initializers/MultiInit.sol";
16
+ import {DiamondHelper} from "@towns-protocol/diamond/scripts/common/helpers/DiamondHelper.s.sol";
17
+
18
+ // deployers
19
+ import {DeployFacet} from "../../common/DeployFacet.s.sol";
20
+ import {Deployer} from "../../common/Deployer.s.sol";
21
+
22
+ contract DeploySubscriptionModule is DiamondHelper, Deployer {
23
+ using LibString for string;
24
+
25
+ DeployFacet private facetHelper = new DeployFacet();
26
+
27
+ bytes32 internal constant METADATA_NAME = bytes32("SubscriptionModule");
28
+
29
+ function versionName() public pure override returns (string memory) {
30
+ return "subscriptionModule";
31
+ }
32
+
33
+ function diamondInitParams(address deployer) public returns (Diamond.InitParams memory) {
34
+ // Queue up feature facets for batch deployment
35
+ facetHelper.add("MultiInit");
36
+ facetHelper.add("SubscriptionModuleFacet");
37
+
38
+ // Deploy all facets in a batch
39
+ facetHelper.deployBatch(deployer);
40
+
41
+ // Add feature facets
42
+ address facet = facetHelper.getDeployedAddress("SubscriptionModuleFacet");
43
+ addFacet(
44
+ makeCut(facet, FacetCutAction.Add, DeploySubscriptionModuleFacet.selectors()),
45
+ facet,
46
+ DeploySubscriptionModuleFacet.makeInitData()
47
+ );
48
+
49
+ address multiInit = facetHelper.getDeployedAddress("MultiInit");
50
+
51
+ return
52
+ Diamond.InitParams({
53
+ baseFacets: baseFacets(),
54
+ init: multiInit,
55
+ initData: abi.encodeCall(MultiInit.multiInit, (_initAddresses, _initDatas))
56
+ });
57
+ }
58
+
59
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
60
+ /* Internal */
61
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
62
+
63
+ function _coreFacets(address deployer) private {
64
+ // Queue up all core facets for batch deployment
65
+ facetHelper.add("DiamondCutFacet");
66
+ facetHelper.add("DiamondLoupeFacet");
67
+ facetHelper.add("IntrospectionFacet");
68
+ facetHelper.add("OwnableFacet");
69
+ facetHelper.add("MetadataFacet");
70
+
71
+ // Get predicted addresses
72
+ address facet = facetHelper.predictAddress("DiamondCutFacet");
73
+ addFacet(
74
+ makeCut(facet, FacetCutAction.Add, DeployDiamondCut.selectors()),
75
+ facet,
76
+ DeployDiamondCut.makeInitData()
77
+ );
78
+
79
+ facet = facetHelper.predictAddress("DiamondLoupeFacet");
80
+ addFacet(
81
+ makeCut(facet, FacetCutAction.Add, DeployDiamondLoupe.selectors()),
82
+ facet,
83
+ DeployDiamondLoupe.makeInitData()
84
+ );
85
+
86
+ facet = facetHelper.predictAddress("IntrospectionFacet");
87
+ addFacet(
88
+ makeCut(facet, FacetCutAction.Add, DeployIntrospection.selectors()),
89
+ facet,
90
+ DeployIntrospection.makeInitData()
91
+ );
92
+
93
+ facet = facetHelper.predictAddress("OwnableFacet");
94
+ addFacet(
95
+ makeCut(facet, FacetCutAction.Add, DeployOwnable.selectors()),
96
+ facet,
97
+ DeployOwnable.makeInitData(deployer)
98
+ );
99
+
100
+ facet = facetHelper.predictAddress("MetadataFacet");
101
+ addFacet(
102
+ makeCut(facet, FacetCutAction.Add, DeployMetadata.selectors()),
103
+ facet,
104
+ DeployMetadata.makeInitData(METADATA_NAME, "")
105
+ );
106
+ }
107
+
108
+ function __deploy(address deployer) internal override returns (address) {
109
+ _coreFacets(deployer);
110
+
111
+ Diamond.InitParams memory initDiamondCut = diamondInitParams(deployer);
112
+
113
+ vm.broadcast(deployer);
114
+ Diamond diamond = new Diamond(initDiamondCut);
115
+
116
+ return address(diamond);
117
+ }
118
+ }
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ // interfaces
5
+ import {IDiamond} from "@towns-protocol/diamond/src/IDiamond.sol";
6
+
7
+ // libraries
8
+ import {LibDeploy} from "@towns-protocol/diamond/src/utils/LibDeploy.sol";
9
+
10
+ // contracts
11
+ import {SubscriptionModuleFacet} from "../../../src/apps/modules/subscription/SubscriptionModuleFacet.sol";
12
+ import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol";
13
+
14
+ library DeploySubscriptionModuleFacet {
15
+ using DynamicArrayLib for DynamicArrayLib.DynamicArray;
16
+
17
+ function selectors() internal pure returns (bytes4[] memory res) {
18
+ DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(19);
19
+ arr.p(SubscriptionModuleFacet.moduleId.selector);
20
+ arr.p(SubscriptionModuleFacet.onInstall.selector);
21
+ arr.p(SubscriptionModuleFacet.onUninstall.selector);
22
+ arr.p(SubscriptionModuleFacet.validateUserOp.selector);
23
+ arr.p(SubscriptionModuleFacet.validateSignature.selector);
24
+ arr.p(SubscriptionModuleFacet.validateRuntime.selector);
25
+ arr.p(SubscriptionModuleFacet.preUserOpValidationHook.selector);
26
+ arr.p(SubscriptionModuleFacet.preRuntimeValidationHook.selector);
27
+ arr.p(SubscriptionModuleFacet.preSignatureValidationHook.selector);
28
+ arr.p(SubscriptionModuleFacet.batchProcessRenewals.selector);
29
+ arr.p(SubscriptionModuleFacet.processRenewal.selector);
30
+ arr.p(SubscriptionModuleFacet.getSubscription.selector);
31
+ arr.p(SubscriptionModuleFacet.pauseSubscription.selector);
32
+ arr.p(SubscriptionModuleFacet.getEntityIds.selector);
33
+ arr.p(SubscriptionModuleFacet.grantOperator.selector);
34
+ arr.p(SubscriptionModuleFacet.revokeOperator.selector);
35
+ arr.p(bytes4(keccak256("MAX_BATCH_SIZE()")));
36
+ arr.p(bytes4(keccak256("RENEWAL_BUFFER()")));
37
+ arr.p(bytes4(keccak256("GRACE_PERIOD()")));
38
+
39
+ bytes32[] memory selectors_ = arr.asBytes32Array();
40
+ assembly ("memory-safe") {
41
+ res := selectors_
42
+ }
43
+ }
44
+
45
+ function makeInitData() internal pure returns (bytes memory) {
46
+ return abi.encodeCall(SubscriptionModuleFacet.__SubscriptionModule_init, ());
47
+ }
48
+
49
+ function makeCut(
50
+ address facetAddress,
51
+ IDiamond.FacetCutAction action
52
+ ) internal pure returns (IDiamond.FacetCut memory) {
53
+ return
54
+ IDiamond.FacetCut({
55
+ action: action,
56
+ facetAddress: facetAddress,
57
+ functionSelectors: selectors()
58
+ });
59
+ }
60
+
61
+ function deploy() internal returns (address) {
62
+ return LibDeploy.deployCode("SubscriptionModuleFacet.sol", "");
63
+ }
64
+ }
@@ -11,7 +11,6 @@ import {ITownsApp} from "./ITownsApp.sol";
11
11
  /// @dev Provides base implementation for module installation/uninstallation and interface support
12
12
  /// @dev Inheriting contracts should override _onInstall and _onUninstall as needed
13
13
  /// @dev Implements IModule, IExecutionModule, and ITownsApp interfaces
14
-
15
14
  abstract contract BaseApp is ITownsApp {
16
15
  receive() external payable {
17
16
  _onPayment(msg.sender, msg.value);
@@ -0,0 +1,111 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ // interfaces
5
+
6
+ // libraries
7
+
8
+ // contracts
9
+
10
+ import {Subscription} from "./SubscriptionModuleStorage.sol";
11
+
12
+ interface ISubscriptionModuleBase {
13
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
14
+ /* Structs */
15
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
16
+
17
+ /// @notice Parameters for renewing a subscription
18
+ /// @param account The address of the account to renew the subscription for
19
+ /// @param entityId The entity ID of the subscription to renew
20
+ struct RenewalParams {
21
+ address account;
22
+ uint32 entityId;
23
+ }
24
+
25
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
26
+ /* Errors */
27
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
28
+
29
+ error SubscriptionModule__InactiveSubscription();
30
+ error SubscriptionModule__InvalidSpace();
31
+ error SubscriptionModule__RenewalNotDue();
32
+ error SubscriptionModule__RenewalFailed();
33
+ error SubscriptionModule__InvalidSender();
34
+ error SubscriptionModule__NotSupported();
35
+ error SubscriptionModule__InvalidEntityId();
36
+ error SubscriptionModule__InvalidCaller();
37
+ error SubscriptionModule__InvalidAddress();
38
+ error SubscriptionModule__ExceedsMaxBatchSize();
39
+ error SubscriptionModule__EmptyBatch();
40
+ error SubscriptionModule__InvalidTokenOwner();
41
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
42
+ /* Events */
43
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
44
+
45
+ event SubscriptionConfigured(
46
+ address indexed account,
47
+ uint32 indexed entityId,
48
+ address indexed space,
49
+ uint256 tokenId,
50
+ uint64 nextRenewalTime
51
+ );
52
+
53
+ event SubscriptionDeactivated(address indexed account, uint32 indexed entityId);
54
+
55
+ event SubscriptionSpent(
56
+ address indexed account,
57
+ uint32 indexed entityId,
58
+ uint256 amount,
59
+ uint256 totalSpent
60
+ );
61
+
62
+ event SubscriptionRenewed(
63
+ address indexed account,
64
+ uint32 indexed entityId,
65
+ uint256 nextRenewalTime
66
+ );
67
+
68
+ event SubscriptionPaused(address indexed account, uint32 indexed entityId);
69
+
70
+ event BatchRenewalSkipped(address indexed account, uint32 indexed entityId, string reason);
71
+ }
72
+
73
+ interface ISubscriptionModule is ISubscriptionModuleBase {
74
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
75
+ /* Functions */
76
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
77
+
78
+ /// @notice Processes multiple Towns membership renewals in batch
79
+ /// @param params The parameters for the renewals
80
+ function batchProcessRenewals(RenewalParams[] calldata params) external;
81
+
82
+ /// @notice Processes a single Towns membership renewal
83
+ /// @param params The parameters for the renewal
84
+ function processRenewal(RenewalParams calldata params) external;
85
+
86
+ /// @notice Gets the subscription for an account and entity ID
87
+ /// @param account The address of the account to get the subscription for
88
+ /// @param entityId The entity ID of the subscription to get
89
+ /// @return The subscription for the account and entity ID
90
+ function getSubscription(
91
+ address account,
92
+ uint32 entityId
93
+ ) external view returns (Subscription memory);
94
+
95
+ /// @notice Pauses a subscription
96
+ /// @param entityId The entity ID of the subscription to pause
97
+ function pauseSubscription(uint32 entityId) external;
98
+
99
+ /// @notice Gets the entity IDs for an account
100
+ /// @param account The address of the account to get the entity IDs for
101
+ /// @return The entity IDs for the account
102
+ function getEntityIds(address account) external view returns (uint256[] memory);
103
+
104
+ /// @notice Grants an operator access to call processRenewal
105
+ /// @param operator The address of the operator to grant
106
+ function grantOperator(address operator) external;
107
+
108
+ /// @notice Revokes an operator access to call processRenewal
109
+ /// @param operator The address of the operator to revoke
110
+ function revokeOperator(address operator) external;
111
+ }
@@ -0,0 +1,342 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ // interfaces
5
+ import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol";
6
+ import {IValidationModule} from "@erc6900/reference-implementation/interfaces/IValidationModule.sol";
7
+ import {IValidationHookModule} from "@erc6900/reference-implementation/interfaces/IValidationHookModule.sol";
8
+ import {ISubscriptionModule} from "./ISubscriptionModule.sol";
9
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
10
+ import {IModularAccount} from "@erc6900/reference-implementation/interfaces/IModularAccount.sol";
11
+
12
+ // libraries
13
+ import {Subscription, SubscriptionModuleStorage} from "./SubscriptionModuleStorage.sol";
14
+ import {PackedUserOperation} from "@eth-infinitism/account-abstraction/interfaces/PackedUserOperation.sol";
15
+ import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
16
+ import {LibCall} from "solady/utils/LibCall.sol";
17
+ import {ValidationLocatorLib} from "modular-account/src/libraries/ValidationLocatorLib.sol";
18
+ import {ReentrancyGuardTransient} from "solady/utils/ReentrancyGuardTransient.sol";
19
+ import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
20
+ import {Validator} from "../../../utils/libraries/Validator.sol";
21
+
22
+ // contracts
23
+ import {ModuleBase} from "modular-account/src/modules/ModuleBase.sol";
24
+ import {OwnableBase} from "@towns-protocol/diamond/src/facets/ownable/OwnableBase.sol";
25
+ import {MembershipFacet} from "../../../spaces/facets/membership/MembershipFacet.sol";
26
+ import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
27
+
28
+ /// @title Subscription Module
29
+ /// @notice Module for managing subscriptions to spaces
30
+ contract SubscriptionModuleFacet is
31
+ ISubscriptionModule,
32
+ IValidationModule,
33
+ IValidationHookModule,
34
+ ModuleBase,
35
+ OwnableBase,
36
+ ReentrancyGuardTransient,
37
+ Facet
38
+ {
39
+ using EnumerableSetLib for EnumerableSetLib.Uint256Set;
40
+ using EnumerableSetLib for EnumerableSetLib.AddressSet;
41
+ using CustomRevert for bytes4;
42
+
43
+ uint256 internal constant _SIG_VALIDATION_FAILED = 1;
44
+
45
+ uint256 public constant MAX_BATCH_SIZE = 50;
46
+ uint256 public constant RENEWAL_BUFFER = 1 days;
47
+ uint256 public constant GRACE_PERIOD = 3 days;
48
+
49
+ function __SubscriptionModule_init() external onlyInitializing {
50
+ _addInterface(type(ISubscriptionModule).interfaceId);
51
+ _addInterface(type(IValidationModule).interfaceId);
52
+ _addInterface(type(IValidationHookModule).interfaceId);
53
+ }
54
+
55
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
56
+ /* External */
57
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
58
+
59
+ /// @inheritdoc IModule
60
+ function moduleId() external pure returns (string memory) {
61
+ return "towns.subscription-module.1.0.0";
62
+ }
63
+
64
+ /// @inheritdoc IModule
65
+ function onInstall(bytes calldata data) external override nonReentrant {
66
+ (uint32 entityId, address space, uint256 tokenId) = abi.decode(
67
+ data,
68
+ (uint32, address, uint256)
69
+ );
70
+
71
+ Validator.checkAddress(space);
72
+
73
+ if (IERC721(space).ownerOf(tokenId) != msg.sender)
74
+ SubscriptionModule__InvalidTokenOwner.selector.revertWith();
75
+
76
+ MembershipFacet membershipFacet = MembershipFacet(space);
77
+ uint256 expiresAt = membershipFacet.expiresAt(tokenId);
78
+
79
+ SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
80
+ Subscription storage sub = $.subscriptions[msg.sender][entityId];
81
+ sub.space = space;
82
+ sub.active = true;
83
+ sub.tokenId = tokenId;
84
+ sub.nextRenewalTime = uint40(expiresAt - RENEWAL_BUFFER);
85
+
86
+ $.entityIds[msg.sender].add(entityId);
87
+
88
+ emit SubscriptionConfigured(msg.sender, entityId, space, tokenId, sub.nextRenewalTime);
89
+ }
90
+
91
+ /// @inheritdoc IModule
92
+ function onUninstall(bytes calldata data) external override nonReentrant {
93
+ uint32 entityId = abi.decode(data, (uint32));
94
+
95
+ SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
96
+
97
+ if (!$.entityIds[msg.sender].remove(entityId))
98
+ SubscriptionModule__InvalidEntityId.selector.revertWith();
99
+ delete $.subscriptions[msg.sender][entityId];
100
+
101
+ emit SubscriptionDeactivated(msg.sender, entityId);
102
+ }
103
+
104
+ /// @inheritdoc IValidationModule
105
+ function validateUserOp(
106
+ uint32,
107
+ PackedUserOperation calldata,
108
+ bytes32
109
+ ) external pure override returns (uint256) {
110
+ return _SIG_VALIDATION_FAILED;
111
+ }
112
+
113
+ /// @inheritdoc IValidationModule
114
+ function validateSignature(
115
+ address,
116
+ uint32,
117
+ address,
118
+ bytes32,
119
+ bytes calldata
120
+ ) external pure override returns (bytes4) {
121
+ return 0xffffffff;
122
+ }
123
+
124
+ /// @inheritdoc IValidationModule
125
+ function validateRuntime(
126
+ address account,
127
+ uint32 entityId,
128
+ address sender,
129
+ uint256,
130
+ bytes calldata,
131
+ bytes calldata
132
+ ) external view override {
133
+ if (sender != address(this)) SubscriptionModule__InvalidSender.selector.revertWith();
134
+ bool active = SubscriptionModuleStorage.getLayout().subscriptions[account][entityId].active;
135
+ if (!active) SubscriptionModule__InactiveSubscription.selector.revertWith();
136
+ return;
137
+ }
138
+
139
+ /// @inheritdoc IValidationHookModule
140
+ function preUserOpValidationHook(
141
+ uint32 /* entityId */,
142
+ PackedUserOperation calldata /* userOp */,
143
+ bytes32 /* userOpHash */
144
+ ) external pure override returns (uint256) {
145
+ return _SIG_VALIDATION_FAILED;
146
+ }
147
+
148
+ /// @inheritdoc IValidationHookModule
149
+ function preRuntimeValidationHook(
150
+ uint32 /* entityId */,
151
+ address /* sender */,
152
+ uint256 /* value */,
153
+ bytes calldata /* data */,
154
+ bytes calldata /* authorization */
155
+ ) external pure override {
156
+ SubscriptionModule__NotSupported.selector.revertWith();
157
+ }
158
+
159
+ /// @inheritdoc IValidationHookModule
160
+ function preSignatureValidationHook(
161
+ uint32 /* entityId */,
162
+ address /* sender */,
163
+ bytes32 /* hash */,
164
+ bytes calldata /* signature */
165
+ ) external pure override {
166
+ SubscriptionModule__NotSupported.selector.revertWith();
167
+ }
168
+
169
+ /// @inheritdoc ISubscriptionModule
170
+ 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();
174
+
175
+ SubscriptionModuleStorage.Layout storage $ = SubscriptionModuleStorage.getLayout();
176
+
177
+ for (uint256 i; i < length; ++i) {
178
+ if (!_isAllowed($.operators, params[i].account))
179
+ SubscriptionModule__InvalidCaller.selector.revertWith();
180
+
181
+ Subscription storage sub = $.subscriptions[params[i].account][params[i].entityId];
182
+
183
+ // Skip inactive subscriptions
184
+ if (!sub.active) {
185
+ emit BatchRenewalSkipped(params[i].account, params[i].entityId, "INACTIVE");
186
+ continue;
187
+ }
188
+
189
+ // Skip if renewal not due
190
+ if (block.timestamp < sub.nextRenewalTime) {
191
+ emit BatchRenewalSkipped(params[i].account, params[i].entityId, "NOT_DUE");
192
+ continue;
193
+ }
194
+
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");
198
+ continue;
199
+ }
200
+
201
+ _processRenewal(sub, params[i]);
202
+ }
203
+ }
204
+
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
+ );
214
+ }
215
+
216
+ /// @inheritdoc ISubscriptionModule
217
+ function getSubscription(
218
+ address account,
219
+ uint32 entityId
220
+ ) external view returns (Subscription memory) {
221
+ return SubscriptionModuleStorage.getLayout().subscriptions[account][entityId];
222
+ }
223
+
224
+ /// @inheritdoc ISubscriptionModule
225
+ function pauseSubscription(uint32 entityId) external {
226
+ Subscription storage sub = SubscriptionModuleStorage.getLayout().subscriptions[msg.sender][
227
+ entityId
228
+ ];
229
+
230
+ if (!sub.active) SubscriptionModule__InactiveSubscription.selector.revertWith();
231
+
232
+ sub.active = false;
233
+ emit SubscriptionPaused(msg.sender, entityId);
234
+ }
235
+
236
+ /// @inheritdoc ISubscriptionModule
237
+ function getEntityIds(address account) external view returns (uint256[] memory) {
238
+ return SubscriptionModuleStorage.getLayout().entityIds[account].values();
239
+ }
240
+
241
+ /// @inheritdoc ISubscriptionModule
242
+ function grantOperator(address operator) external onlyOwner {
243
+ Validator.checkAddress(operator);
244
+ SubscriptionModuleStorage.getLayout().operators.add(operator);
245
+ }
246
+
247
+ /// @inheritdoc ISubscriptionModule
248
+ function revokeOperator(address operator) external onlyOwner {
249
+ Validator.checkAddress(operator);
250
+ SubscriptionModuleStorage.getLayout().operators.remove(operator);
251
+ }
252
+
253
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
254
+ /* Internal */
255
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
256
+
257
+ function _processRenewal(Subscription storage sub, RenewalParams calldata params) internal {
258
+ // Validate subscription state
259
+ if (!sub.active) SubscriptionModule__InactiveSubscription.selector.revertWith();
260
+ if (block.timestamp < sub.nextRenewalTime)
261
+ SubscriptionModule__RenewalNotDue.selector.revertWith();
262
+
263
+ // Check if we're past the grace period
264
+ if (block.timestamp > sub.nextRenewalTime + GRACE_PERIOD) {
265
+ sub.active = false;
266
+ emit SubscriptionPaused(params.account, params.entityId);
267
+ return;
268
+ }
269
+
270
+ MembershipFacet membershipFacet = MembershipFacet(sub.space);
271
+
272
+ // Get current renewal price from Towns contract
273
+ uint256 actualRenewalPrice = membershipFacet.getMembershipRenewalPrice(sub.tokenId);
274
+
275
+ // Update state BEFORE external calls to prevent reentrancy
276
+ // Set to a far future time to prevent re-entry while processing
277
+ // This ensures the "renewal not due" check will fail if re-entered
278
+ sub.nextRenewalTime = uint40(block.timestamp + 365 days);
279
+
280
+ // Construct the renewal call to space contract
281
+ bytes memory renewalCall = abi.encodeCall(MembershipFacet.renewMembership, (sub.tokenId));
282
+
283
+ // Create the data parameter for executeWithRuntimeValidation
284
+ // This should be an execute() call to the space contract
285
+ bytes memory executeData = abi.encodeCall(
286
+ IModularAccount.execute,
287
+ (
288
+ sub.space, // target
289
+ actualRenewalPrice, // value
290
+ renewalCall // data
291
+ )
292
+ );
293
+
294
+ // Use the proper pack function from ValidationLocatorLib
295
+ bytes memory authorization = _runtimeFinal(
296
+ params.entityId,
297
+ abi.encode(sub.space, sub.tokenId)
298
+ );
299
+
300
+ // Call executeWithRuntimeValidation with the correct parameters
301
+ bytes memory runtimeValidationCall = abi.encodeCall(
302
+ IModularAccount.executeWithRuntimeValidation,
303
+ (
304
+ executeData, // The execute() call data
305
+ authorization // Authorization for validation
306
+ )
307
+ );
308
+
309
+ // External call happens here
310
+ LibCall.callContract(params.account, 0, runtimeValidationCall);
311
+
312
+ // Get the actual new expiration time after successful renewal
313
+ uint256 newExpiresAt = membershipFacet.expiresAt(sub.tokenId);
314
+
315
+ // Update subscription state after successful renewal
316
+ sub.nextRenewalTime = uint40(newExpiresAt - RENEWAL_BUFFER);
317
+ sub.lastRenewalTime = uint40(block.timestamp);
318
+ sub.spent += actualRenewalPrice;
319
+
320
+ emit SubscriptionRenewed(params.account, params.entityId, sub.nextRenewalTime);
321
+ }
322
+
323
+ function _runtimeFinal(
324
+ uint32 entityId,
325
+ bytes memory finalData
326
+ ) internal pure returns (bytes memory) {
327
+ return
328
+ ValidationLocatorLib.packSignature(
329
+ entityId,
330
+ false, // selector-based
331
+ bytes.concat(hex"ff", finalData)
332
+ );
333
+ }
334
+
335
+ function _isAllowed(
336
+ EnumerableSetLib.AddressSet storage operators,
337
+ address account
338
+ ) internal view returns (bool) {
339
+ if (account == msg.sender) return true;
340
+ return operators.contains(msg.sender);
341
+ }
342
+ }
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
5
+
6
+ struct Subscription {
7
+ address space;
8
+ bool active;
9
+ uint40 lastRenewalTime;
10
+ uint40 nextRenewalTime;
11
+ uint256 spent;
12
+ uint256 tokenId;
13
+ }
14
+
15
+ library SubscriptionModuleStorage {
16
+ using EnumerableSetLib for EnumerableSetLib.Uint256Set;
17
+ using EnumerableSetLib for EnumerableSetLib.AddressSet;
18
+
19
+ /// @custom:storage-location erc7201:towns.subscription.validation.module.storage
20
+ struct Layout {
21
+ EnumerableSetLib.AddressSet operators;
22
+ mapping(address account => mapping(uint32 entityId => Subscription)) subscriptions;
23
+ mapping(address account => EnumerableSetLib.Uint256Set entityIds) entityIds;
24
+ }
25
+
26
+ // keccak256(abi.encode(uint256(keccak256("towns.subscription.validation.module.storage")) - 1)) & ~bytes32(uint256(0xff))
27
+ bytes32 private constant STORAGE_SLOT =
28
+ 0xd241b3ceee256b40f80fe7a66fe789234ac389ed1408c472c4ee1cbb1deb8600;
29
+
30
+ function getLayout() internal pure returns (Layout storage $) {
31
+ assembly {
32
+ $.slot := STORAGE_SLOT
33
+ }
34
+ }
35
+ }