@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 +4 -3
- package/scripts/deployments/diamonds/DeploySubscriptionModule.s.sol +118 -0
- package/scripts/deployments/facets/DeploySubscriptionModuleFacet.s.sol +64 -0
- package/src/apps/BaseApp.sol +0 -1
- package/src/apps/modules/subscription/ISubscriptionModule.sol +111 -0
- package/src/apps/modules/subscription/SubscriptionModuleFacet.sol +342 -0
- package/src/apps/modules/subscription/SubscriptionModuleStorage.sol +35 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@towns-protocol/contracts",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
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": "
|
|
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
|
+
}
|
package/src/apps/BaseApp.sol
CHANGED
|
@@ -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
|
+
}
|