@towns-protocol/contracts 0.0.336 → 0.0.338

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.338",
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.338",
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": "c6e34eeab8c1753ecae0ed36e1953f2a828f08d8"
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
+ }
@@ -1,11 +1,13 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
- import {IDiamond} from "@towns-protocol/diamond/src/Diamond.sol";
4
+ import {IDiamond, Diamond} from "@towns-protocol/diamond/src/Diamond.sol";
5
5
 
6
6
  interface IDiamondInitHelper is IDiamond {
7
7
  function diamondInitHelper(
8
8
  address deployer,
9
9
  string[] memory facetNames
10
10
  ) external returns (FacetCut[] memory);
11
+
12
+ function diamondInitParams(address deployer) external returns (Diamond.InitParams memory);
11
13
  }
@@ -15,11 +15,12 @@ library DeployAppAccount {
15
15
  using DynamicArrayLib for DynamicArrayLib.DynamicArray;
16
16
 
17
17
  function selectors() internal pure returns (bytes4[] memory res) {
18
- DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(11);
18
+ DynamicArrayLib.DynamicArray memory arr = DynamicArrayLib.p().reserve(12);
19
19
  arr.p(AppAccount.execute.selector);
20
20
  arr.p(AppAccount.onInstallApp.selector);
21
21
  arr.p(AppAccount.onUninstallApp.selector);
22
22
  arr.p(AppAccount.onRenewApp.selector);
23
+ arr.p(AppAccount.isAppExecuting.selector);
23
24
  arr.p(AppAccount.isAppEntitled.selector);
24
25
  arr.p(AppAccount.disableApp.selector);
25
26
  arr.p(AppAccount.getInstalledApps.selector);
@@ -1,10 +1,10 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.29;
3
3
 
4
- import {SimpleApp} from "src/apps/helpers/SimpleApp.sol";
4
+ import {LibDeploy} from "@towns-protocol/diamond/src/utils/LibDeploy.sol";
5
5
 
6
6
  library DeploySimpleApp {
7
7
  function deploy() internal returns (address) {
8
- return address(new SimpleApp());
8
+ return LibDeploy.deployCode("SimpleApp.sol", "");
9
9
  }
10
10
  }
@@ -0,0 +1,65 @@
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(20);
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.isOperator.selector);
34
+ arr.p(SubscriptionModuleFacet.grantOperator.selector);
35
+ arr.p(SubscriptionModuleFacet.revokeOperator.selector);
36
+ arr.p(bytes4(keccak256("MAX_BATCH_SIZE()")));
37
+ arr.p(bytes4(keccak256("RENEWAL_BUFFER()")));
38
+ arr.p(bytes4(keccak256("GRACE_PERIOD()")));
39
+
40
+ bytes32[] memory selectors_ = arr.asBytes32Array();
41
+ assembly ("memory-safe") {
42
+ res := selectors_
43
+ }
44
+ }
45
+
46
+ function makeInitData() internal pure returns (bytes memory) {
47
+ return abi.encodeCall(SubscriptionModuleFacet.__SubscriptionModule_init, ());
48
+ }
49
+
50
+ function makeCut(
51
+ address facetAddress,
52
+ IDiamond.FacetCutAction action
53
+ ) internal pure returns (IDiamond.FacetCut memory) {
54
+ return
55
+ IDiamond.FacetCut({
56
+ action: action,
57
+ facetAddress: facetAddress,
58
+ functionSelectors: selectors()
59
+ });
60
+ }
61
+
62
+ function deploy() internal returns (address) {
63
+ return LibDeploy.deployCode("SubscriptionModuleFacet.sol", "");
64
+ }
65
+ }
@@ -2,10 +2,8 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
- import {IDiamondCut} from "@towns-protocol/diamond/src/facets/cut/IDiamondCut.sol";
6
5
 
7
6
  // libraries
8
- import {console} from "forge-std/console.sol";
9
7
 
10
8
  // contracts
11
9
  import {DeployBaseRegistry} from "scripts/deployments/diamonds/DeployBaseRegistry.s.sol";
@@ -34,141 +32,24 @@ contract InteractBaseAlpha is AlphaHelper {
34
32
  address riverAirdrop = getDeployment("riverAirdrop");
35
33
  address appRegistry = getDeployment("appRegistry");
36
34
 
37
- deploySpaceCuts(deployer, space);
38
- deploySpaceOwnerCuts(deployer, spaceOwner);
39
- deploySpaceFactoryCuts(deployer, spaceFactory);
40
- deployBaseRegistryCuts(deployer, baseRegistry);
41
- deployRiverAirdropCuts(deployer, riverAirdrop);
42
- deployAppRegistryCuts(deployer, appRegistry);
43
-
44
- vm.resumeGasMetering();
45
- }
46
-
47
- function deploySpaceCuts(address deployer, address space) internal {
48
- console.log("[INFO]: === Upgrading Space diamond ===");
49
- deploySpace.diamondInitParams(deployer);
50
- FacetCut[] memory proposedCuts = deploySpace.getCuts();
51
- FacetCut[] memory smartCuts = generateSmartCuts(space, proposedCuts);
52
-
53
- console.log(
54
- "[INFO]: Generated %d smart cuts from %d proposed cuts",
55
- smartCuts.length,
56
- proposedCuts.length
57
- );
58
-
59
- if (smartCuts.length > 0) {
60
- vm.broadcast(deployer);
61
- IDiamondCut(space).diamondCut(smartCuts, address(0), "");
62
- console.log("[INFO]: \u2705 Space diamond upgrade completed");
63
- } else {
64
- console.log("[INFO]: Space diamond already up to date - no cuts needed");
65
- }
66
- }
67
-
68
- function deploySpaceOwnerCuts(address deployer, address spaceOwner) internal {
69
- console.log("[INFO]: === Upgrading SpaceOwner diamond ===");
70
- deploySpaceOwner.diamondInitParams(deployer);
71
- FacetCut[] memory proposedCuts = deploySpaceOwner.getCuts();
72
- FacetCut[] memory smartCuts = generateSmartCuts(spaceOwner, proposedCuts);
73
-
74
- console.log(
75
- "[INFO]: Generated %d smart cuts from %d proposed cuts",
76
- smartCuts.length,
77
- proposedCuts.length
78
- );
79
-
80
- if (smartCuts.length > 0) {
81
- vm.broadcast(deployer);
82
- IDiamondCut(spaceOwner).diamondCut(smartCuts, address(0), "");
83
- console.log("[INFO]: \u2705 SpaceOwner diamond upgrade completed");
84
- } else {
85
- console.log("[INFO]: SpaceOwner diamond already up to date - no cuts needed");
86
- }
87
- }
88
-
89
- function deploySpaceFactoryCuts(address deployer, address spaceFactory) internal {
90
- console.log("[INFO]: === Upgrading SpaceFactory diamond ===");
91
- deploySpaceFactory.diamondInitParams(deployer);
92
- FacetCut[] memory proposedCuts = deploySpaceFactory.getCuts();
93
- FacetCut[] memory smartCuts = generateSmartCuts(spaceFactory, proposedCuts);
94
-
95
- console.log(
96
- "[INFO]: Generated %d smart cuts from %d proposed cuts",
97
- smartCuts.length,
98
- proposedCuts.length
35
+ executeDiamondCutsWithLogging(deployer, space, "Space", deploySpace);
36
+ executeDiamondCutsWithLogging(deployer, spaceOwner, "SpaceOwner", deploySpaceOwner);
37
+
38
+ address spaceFactoryInit = deploySpaceFactory.spaceFactoryInit();
39
+ bytes memory initData = deploySpaceFactory.spaceFactoryInitData();
40
+ executeDiamondCutsWithLogging(
41
+ deployer,
42
+ spaceFactory,
43
+ "SpaceFactory",
44
+ deploySpaceFactory,
45
+ spaceFactoryInit,
46
+ initData
99
47
  );
100
48
 
101
- if (smartCuts.length > 0) {
102
- address spaceFactoryInit = deploySpaceFactory.spaceFactoryInit();
103
- bytes memory initData = deploySpaceFactory.spaceFactoryInitData();
104
- vm.broadcast(deployer);
105
- IDiamondCut(spaceFactory).diamondCut(smartCuts, spaceFactoryInit, initData);
106
- console.log("[INFO]: \u2705 SpaceFactory diamond upgrade completed");
107
- } else {
108
- console.log("[INFO]: SpaceFactory diamond already up to date - no cuts needed");
109
- }
110
- }
111
-
112
- function deployBaseRegistryCuts(address deployer, address baseRegistry) internal {
113
- console.log("[INFO]: === Upgrading BaseRegistry diamond ===");
114
- deployBaseRegistry.diamondInitParams(deployer);
115
- FacetCut[] memory proposedCuts = deployBaseRegistry.getCuts();
116
- FacetCut[] memory smartCuts = generateSmartCuts(baseRegistry, proposedCuts);
117
-
118
- console.log(
119
- "[INFO]: Generated %d smart cuts from %d proposed cuts",
120
- smartCuts.length,
121
- proposedCuts.length
122
- );
49
+ executeDiamondCutsWithLogging(deployer, baseRegistry, "BaseRegistry", deployBaseRegistry);
50
+ executeDiamondCutsWithLogging(deployer, riverAirdrop, "RiverAirdrop", deployRiverAirdrop);
51
+ executeDiamondCutsWithLogging(deployer, appRegistry, "AppRegistry", deployAppRegistry);
123
52
 
124
- if (smartCuts.length > 0) {
125
- vm.broadcast(deployer);
126
- IDiamondCut(baseRegistry).diamondCut(smartCuts, address(0), "");
127
- console.log("[INFO]: \u2705 BaseRegistry diamond upgrade completed");
128
- } else {
129
- console.log("[INFO]: BaseRegistry diamond already up to date - no cuts needed");
130
- }
131
- }
132
-
133
- function deployRiverAirdropCuts(address deployer, address riverAirdrop) internal {
134
- console.log("[INFO]: === Upgrading RiverAirdrop diamond ===");
135
- deployRiverAirdrop.diamondInitParams(deployer);
136
- FacetCut[] memory proposedCuts = deployRiverAirdrop.getCuts();
137
- FacetCut[] memory smartCuts = generateSmartCuts(riverAirdrop, proposedCuts);
138
-
139
- console.log(
140
- "[INFO]: Generated %d smart cuts from %d proposed cuts",
141
- smartCuts.length,
142
- proposedCuts.length
143
- );
144
-
145
- if (smartCuts.length > 0) {
146
- vm.broadcast(deployer);
147
- IDiamondCut(riverAirdrop).diamondCut(smartCuts, address(0), "");
148
- console.log("[INFO]: \u2705 RiverAirdrop diamond upgrade completed");
149
- } else {
150
- console.log("[INFO]: RiverAirdrop diamond already up to date - no cuts needed");
151
- }
152
- }
153
-
154
- function deployAppRegistryCuts(address deployer, address appRegistry) internal {
155
- console.log("[INFO]: === Upgrading AppRegistry diamond ===");
156
- deployAppRegistry.diamondInitParams(deployer);
157
- FacetCut[] memory proposedCuts = deployAppRegistry.getCuts();
158
- FacetCut[] memory smartCuts = generateSmartCuts(appRegistry, proposedCuts);
159
-
160
- console.log(
161
- "[INFO]: Generated %d smart cuts from %d proposed cuts",
162
- smartCuts.length,
163
- proposedCuts.length
164
- );
165
-
166
- if (smartCuts.length > 0) {
167
- vm.broadcast(deployer);
168
- IDiamondCut(appRegistry).diamondCut(smartCuts, address(0), "");
169
- console.log("[INFO]: \u2705 AppRegistry diamond upgrade completed");
170
- } else {
171
- console.log("[INFO]: AppRegistry diamond already up to date - no cuts needed");
172
- }
53
+ vm.resumeGasMetering();
173
54
  }
174
55
  }
@@ -2,7 +2,6 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  // interfaces
5
- import {IDiamondCut} from "@towns-protocol/diamond/src/facets/cut/IDiamondCut.sol";
6
5
 
7
6
  // libraries
8
7
 
@@ -15,16 +14,13 @@ contract InteractRiverAlpha is AlphaHelper {
15
14
  DeployRiverRegistry deployRiverRegistry = new DeployRiverRegistry();
16
15
 
17
16
  function __interact(address deployer) internal override {
18
- vm.setEnv("OVERRIDE_DEPLOYMENTS", "1");
19
17
  address riverRegistry = getDeployment("riverRegistry");
20
18
 
21
- removeRemoteFacets(deployer, riverRegistry);
22
- FacetCut[] memory newCuts;
23
-
24
- deployRiverRegistry.diamondInitParams(deployer);
25
- newCuts = deployRiverRegistry.getCuts();
26
-
27
- vm.broadcast(deployer);
28
- IDiamondCut(riverRegistry).diamondCut(newCuts, address(0), "");
19
+ executeDiamondCutsWithLogging(
20
+ deployer,
21
+ riverRegistry,
22
+ "RiverRegistry",
23
+ deployRiverRegistry
24
+ );
29
25
  }
30
26
  }
@@ -7,10 +7,12 @@ import {IDiamondCut} from "@towns-protocol/diamond/src/facets/cut/IDiamondCut.so
7
7
  import {IDiamondLoupe, IDiamondLoupeBase} from "@towns-protocol/diamond/src/facets/loupe/IDiamondLoupe.sol";
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
+ import {IDiamondInitHelper} from "scripts/deployments/diamonds/IDiamondInitHelper.sol";
10
11
 
11
12
  // libraries
12
13
  import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
13
14
  import {DynamicArrayLib} from "solady/utils/DynamicArrayLib.sol";
15
+ import {LibString} from "solady/utils/LibString.sol";
14
16
 
15
17
  // contracts
16
18
  import {DiamondHelper} from "@towns-protocol/diamond/scripts/common/helpers/DiamondHelper.s.sol";
@@ -37,6 +39,7 @@ abstract contract AlphaHelper is Interaction, DiamondHelper, IDiamondLoupeBase {
37
39
  using DynamicArrayLib for DynamicArrayLib.DynamicArray;
38
40
  using EnumerableSet for EnumerableSet.AddressSet;
39
41
  using EnumerableSet for EnumerableSet.Bytes32Set;
42
+ using LibString for *;
40
43
 
41
44
  struct DiamondInfo {
42
45
  DiamondCutStorage.Layout layout;
@@ -256,6 +259,68 @@ abstract contract AlphaHelper is Interaction, DiamondHelper, IDiamondLoupeBase {
256
259
  }
257
260
  }
258
261
 
262
+ /// @notice Execute diamond cuts with smart cut optimization and logging
263
+ /// @param deployer The address that will execute the diamond cut
264
+ /// @param diamond The diamond contract address
265
+ /// @param diamondName Name of the diamond for logging purposes
266
+ /// @param deployContract The deployment contract that provides cuts and initialization
267
+ /// @param initAddress Optional initialization contract address
268
+ /// @param initData Optional initialization data
269
+ function executeDiamondCutsWithLogging(
270
+ address deployer,
271
+ address diamond,
272
+ string memory diamondName,
273
+ IDiamondInitHelper deployContract,
274
+ address initAddress,
275
+ bytes memory initData
276
+ ) internal {
277
+ info(string.concat("=== Upgrading ", diamondName, " diamond ==="), "");
278
+
279
+ deployContract.diamondInitParams(deployer);
280
+ FacetCut[] memory proposedCuts = DiamondHelper(address(deployContract)).getCuts();
281
+ FacetCut[] memory smartCuts = generateSmartCuts(diamond, proposedCuts);
282
+
283
+ info(
284
+ string.concat(
285
+ "Generated ",
286
+ smartCuts.length.toString(),
287
+ " smart cuts from ",
288
+ proposedCuts.length.toString(),
289
+ " proposed cuts"
290
+ ),
291
+ ""
292
+ );
293
+
294
+ if (smartCuts.length > 0) {
295
+ vm.broadcast(deployer);
296
+ IDiamondCut(diamond).diamondCut(smartCuts, initAddress, initData);
297
+ info(string.concat(unicode"✅ ", diamondName, " diamond upgrade completed"), "");
298
+ } else {
299
+ info(string.concat(diamondName, " diamond already up to date - no cuts needed"), "");
300
+ }
301
+ }
302
+
303
+ /// @notice Execute diamond cuts with smart cut optimization and logging (no initialization)
304
+ /// @param deployer The address that will execute the diamond cut
305
+ /// @param diamond The diamond contract address
306
+ /// @param diamondName Name of the diamond for logging purposes
307
+ /// @param deployContract The deployment contract that provides cuts and initialization
308
+ function executeDiamondCutsWithLogging(
309
+ address deployer,
310
+ address diamond,
311
+ string memory diamondName,
312
+ IDiamondInitHelper deployContract
313
+ ) internal {
314
+ executeDiamondCutsWithLogging(
315
+ deployer,
316
+ diamond,
317
+ diamondName,
318
+ deployContract,
319
+ address(0),
320
+ ""
321
+ );
322
+ }
323
+
259
324
  function asBytes4Array(
260
325
  DynamicArrayLib.DynamicArray memory input
261
326
  ) internal pure returns (bytes4[] memory selectors) {
@@ -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,119 @@
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
+ error SubscriptionModule__InsufficientBalance();
42
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
43
+ /* Events */
44
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
45
+
46
+ event SubscriptionConfigured(
47
+ address indexed account,
48
+ uint32 indexed entityId,
49
+ address indexed space,
50
+ uint256 tokenId,
51
+ uint64 nextRenewalTime
52
+ );
53
+
54
+ event SubscriptionDeactivated(address indexed account, uint32 indexed entityId);
55
+
56
+ event SubscriptionSpent(
57
+ address indexed account,
58
+ uint32 indexed entityId,
59
+ uint256 amount,
60
+ uint256 totalSpent
61
+ );
62
+
63
+ event SubscriptionRenewed(
64
+ address indexed account,
65
+ uint32 indexed entityId,
66
+ uint256 nextRenewalTime
67
+ );
68
+
69
+ event SubscriptionPaused(address indexed account, uint32 indexed entityId);
70
+
71
+ event BatchRenewalSkipped(address indexed account, uint32 indexed entityId, string reason);
72
+
73
+ event OperatorGranted(address indexed operator);
74
+ event OperatorRevoked(address indexed operator);
75
+ }
76
+
77
+ interface ISubscriptionModule is ISubscriptionModuleBase {
78
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
79
+ /* Functions */
80
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
81
+
82
+ /// @notice Processes multiple Towns membership renewals in batch
83
+ /// @param params The parameters for the renewals
84
+ function batchProcessRenewals(RenewalParams[] calldata params) external;
85
+
86
+ /// @notice Processes a single Towns membership renewal
87
+ /// @param params The parameters for the renewal
88
+ function processRenewal(RenewalParams calldata params) external;
89
+
90
+ /// @notice Gets the subscription for an account and entity ID
91
+ /// @param account The address of the account to get the subscription for
92
+ /// @param entityId The entity ID of the subscription to get
93
+ /// @return The subscription for the account and entity ID
94
+ function getSubscription(
95
+ address account,
96
+ uint32 entityId
97
+ ) external view returns (Subscription memory);
98
+
99
+ /// @notice Pauses a subscription
100
+ /// @param entityId The entity ID of the subscription to pause
101
+ function pauseSubscription(uint32 entityId) external;
102
+
103
+ /// @notice Gets the entity IDs for an account
104
+ /// @param account The address of the account to get the entity IDs for
105
+ /// @return The entity IDs for the account
106
+ function getEntityIds(address account) external view returns (uint256[] memory);
107
+
108
+ /// @notice Checks if an operator has access to call processRenewal
109
+ /// @param operator The address of the operator to check
110
+ function isOperator(address operator) external view returns (bool);
111
+
112
+ /// @notice Grants an operator access to call processRenewal
113
+ /// @param operator The address of the operator to grant
114
+ function grantOperator(address operator) external;
115
+
116
+ /// @notice Revokes an operator access to call processRenewal
117
+ /// @param operator The address of the operator to revoke
118
+ function revokeOperator(address operator) external;
119
+ }
@@ -0,0 +1,359 @@
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
+ emit OperatorGranted(operator);
246
+ }
247
+
248
+ /// @inheritdoc ISubscriptionModule
249
+ function isOperator(address operator) external view returns (bool) {
250
+ return SubscriptionModuleStorage.getLayout().operators.contains(operator);
251
+ }
252
+
253
+ /// @inheritdoc ISubscriptionModule
254
+ function revokeOperator(address operator) external onlyOwner {
255
+ Validator.checkAddress(operator);
256
+ SubscriptionModuleStorage.getLayout().operators.remove(operator);
257
+ emit OperatorRevoked(operator);
258
+ }
259
+
260
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
261
+ /* Internal */
262
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
263
+
264
+ /// @dev Processes a single subscription renewal
265
+ /// @param sub The subscription to renew
266
+ /// @param params The parameters for the renewal
267
+ function _processRenewal(Subscription storage sub, RenewalParams calldata params) internal {
268
+ if (!sub.active) SubscriptionModule__InactiveSubscription.selector.revertWith();
269
+
270
+ if (block.timestamp < sub.nextRenewalTime)
271
+ SubscriptionModule__RenewalNotDue.selector.revertWith();
272
+
273
+ // Check if we're past the grace period
274
+ if (block.timestamp > sub.nextRenewalTime + GRACE_PERIOD) {
275
+ sub.active = false;
276
+ emit SubscriptionPaused(params.account, params.entityId);
277
+ return;
278
+ }
279
+
280
+ MembershipFacet membershipFacet = MembershipFacet(sub.space);
281
+
282
+ // Get current renewal price from Towns contract
283
+ uint256 actualRenewalPrice = membershipFacet.getMembershipRenewalPrice(sub.tokenId);
284
+
285
+ // Check if the account has enough balance
286
+ if (params.account.balance < actualRenewalPrice)
287
+ SubscriptionModule__InsufficientBalance.selector.revertWith();
288
+
289
+ // Construct the renewal call to space contract
290
+ bytes memory renewalCall = abi.encodeCall(MembershipFacet.renewMembership, (sub.tokenId));
291
+
292
+ // Create the data parameter for executeWithRuntimeValidation
293
+ // This should be an execute() call to the space contract
294
+ bytes memory executeData = abi.encodeCall(
295
+ IModularAccount.execute,
296
+ (
297
+ sub.space, // target
298
+ actualRenewalPrice, // value
299
+ renewalCall // data
300
+ )
301
+ );
302
+
303
+ // Use the proper pack function from ValidationLocatorLib
304
+ bytes memory authorization = _runtimeFinal(
305
+ params.entityId,
306
+ abi.encode(sub.space, sub.tokenId)
307
+ );
308
+
309
+ // Call executeWithRuntimeValidation with the correct parameters
310
+ bytes memory runtimeValidationCall = abi.encodeCall(
311
+ IModularAccount.executeWithRuntimeValidation,
312
+ (
313
+ executeData, // The execute() call data
314
+ authorization // Authorization for validation
315
+ )
316
+ );
317
+
318
+ // External call happens here
319
+ LibCall.callContract(params.account, 0, runtimeValidationCall);
320
+
321
+ // Get the actual new expiration time after successful renewal
322
+ uint256 newExpiresAt = membershipFacet.expiresAt(sub.tokenId);
323
+
324
+ // Update subscription state after successful renewal
325
+ sub.nextRenewalTime = uint40(newExpiresAt - RENEWAL_BUFFER);
326
+ sub.lastRenewalTime = uint40(block.timestamp);
327
+ sub.spent += actualRenewalPrice;
328
+
329
+ emit SubscriptionRenewed(params.account, params.entityId, sub.nextRenewalTime);
330
+ }
331
+
332
+ /// @dev Creates the runtime final data for the renewal
333
+ /// @param entityId The entity ID of the subscription
334
+ /// @param finalData The final data for the renewal
335
+ /// @return The runtime final data
336
+ function _runtimeFinal(
337
+ uint32 entityId,
338
+ bytes memory finalData
339
+ ) internal pure returns (bytes memory) {
340
+ return
341
+ ValidationLocatorLib.packSignature(
342
+ entityId,
343
+ false, // selector-based
344
+ bytes.concat(hex"ff", finalData)
345
+ );
346
+ }
347
+
348
+ /// @dev Checks if the caller is allowed to call the function
349
+ /// @param operators The set of operators
350
+ /// @param account The account to check
351
+ /// @return True if the caller is allowed to call the function
352
+ function _isAllowed(
353
+ EnumerableSetLib.AddressSet storage operators,
354
+ address account
355
+ ) internal view returns (bool) {
356
+ if (account == msg.sender) return true;
357
+ return operators.contains(msg.sender);
358
+ }
359
+ }
@@ -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
+ uint256 tokenId; // 32 bytes
8
+ uint256 spent; // 32 bytes
9
+ address space; // 20 bytes
10
+ uint40 lastRenewalTime; // 5 bytes
11
+ uint40 nextRenewalTime; // 5 bytes
12
+ bool active; // 1 byte
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
+ }
@@ -22,6 +22,7 @@ contract AppAccount is IAppAccount, AppAccountBase, ReentrancyGuard, Facet {
22
22
  /* Execution */
23
23
  /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
24
24
 
25
+ /// @inheritdoc IAppAccount
25
26
  function execute(
26
27
  address target,
27
28
  uint256 value,
@@ -90,4 +91,9 @@ contract AppAccount is IAppAccount, AppAccountBase, ReentrancyGuard, Facet {
90
91
  ) external view returns (bool) {
91
92
  return _isAppEntitled(app, publicKey, permission);
92
93
  }
94
+
95
+ /// @inheritdoc IAppAccount
96
+ function isAppExecuting(address app) external view returns (bool) {
97
+ return _isAppExecuting(app);
98
+ }
93
99
  }
@@ -18,6 +18,7 @@ import {DependencyLib} from "../DependencyLib.sol";
18
18
  import {LibCall} from "solady/utils/LibCall.sol";
19
19
  import {AppAccountStorage} from "./AppAccountStorage.sol";
20
20
  import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
21
+ import {ExecutorStorage} from "../executor/ExecutorStorage.sol";
21
22
 
22
23
  // contracts
23
24
  import {ExecutorBase} from "../executor/ExecutorBase.sol";
@@ -207,6 +208,20 @@ abstract contract AppAccountBase is
207
208
  return AppAccountStorage.getLayout().installedApps.values();
208
209
  }
209
210
 
211
+ function _isAppExecuting(address app) internal view returns (bool) {
212
+ bytes32 currentExecutionId = ExecutorStorage.getExecutionId();
213
+ if (currentExecutionId == bytes32(0)) return false;
214
+
215
+ bytes32 targetId = ExecutorStorage.getTargetExecutionId(app);
216
+ if (targetId == bytes32(0)) return false;
217
+ if (currentExecutionId != targetId) return false;
218
+
219
+ bytes32 appId = _getInstalledAppId(app);
220
+ if (appId == EMPTY_UID) return false;
221
+
222
+ return true;
223
+ }
224
+
210
225
  function _isAppInstalled(address module) internal view returns (bool) {
211
226
  return AppAccountStorage.getLayout().installedApps.contains(module);
212
227
  }
@@ -72,4 +72,20 @@ interface IAppAccount is IAppAccountBase {
72
72
  address publicKey,
73
73
  bytes32 permission
74
74
  ) external view returns (bool);
75
+
76
+ /// @notice Checks if an app is executing
77
+ /// @param app The address of the app to check
78
+ /// @return True if the app is executing, false otherwise
79
+ function isAppExecuting(address app) external view returns (bool);
80
+
81
+ /// @notice Executes a function on the app
82
+ /// @param target The address of the app to execute the function on
83
+ /// @param value The value to send with the function
84
+ /// @param data The data to send with the function
85
+ /// @return The result of the function
86
+ function execute(
87
+ address target,
88
+ uint256 value,
89
+ bytes calldata data
90
+ ) external payable returns (bytes memory);
75
91
  }
@@ -395,8 +395,9 @@ abstract contract ExecutorBase is IExecutorBase {
395
395
  _executePreHooks($, target, selector, value, data);
396
396
 
397
397
  // Set the executionId for the target and selector using transient storage
398
+ bytes32 executionIdBefore = ExecutorStorage.getExecutionId();
398
399
  bytes32 executionId = _hashExecutionId(target, selector);
399
- ExecutorStorage.setTransientExecutionId(executionId);
400
+ ExecutorStorage.setExecutionId(executionId);
400
401
  ExecutorStorage.setTargetExecutionId(target, executionId);
401
402
 
402
403
  // Call the target
@@ -406,7 +407,8 @@ abstract contract ExecutorBase is IExecutorBase {
406
407
  _executePostHooks($, target, selector);
407
408
 
408
409
  // Clear transient storage to prevent composability issues
409
- ExecutorStorage.clearTransientStorage(target);
410
+ ExecutorStorage.setExecutionId(executionIdBefore);
411
+ ExecutorStorage.clearTargetExecutionId(target);
410
412
  }
411
413
 
412
414
  /// @notice Gets the scheduled timepoint for an operation.
@@ -421,7 +423,7 @@ abstract contract ExecutorBase is IExecutorBase {
421
423
  /// @param target The target contract.
422
424
  /// @return True if currently executing.
423
425
  function _isTargetExecuting(address target) internal view returns (bool) {
424
- bytes32 globalId = ExecutorStorage.getTransientExecutionId();
426
+ bytes32 globalId = ExecutorStorage.getExecutionId();
425
427
  bytes32 targetId = ExecutorStorage.getTargetExecutionId(target);
426
428
  return globalId != 0 && targetId == globalId;
427
429
  }
@@ -524,7 +526,7 @@ abstract contract ExecutorBase is IExecutorBase {
524
526
  /// @param selector The function selector.
525
527
  /// @return True if currently executing.
526
528
  function _isExecuting(address target, bytes4 selector) private view returns (bool) {
527
- return ExecutorStorage.getTransientExecutionId() == _hashExecutionId(target, selector);
529
+ return ExecutorStorage.getExecutionId() == _hashExecutionId(target, selector);
528
530
  }
529
531
 
530
532
  /// @dev Computes a unique hash for the execution context.
@@ -66,13 +66,13 @@ library ExecutorStorage {
66
66
  /* Transient Execution Id */
67
67
  /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
68
68
 
69
- function getTransientExecutionId() internal view returns (bytes32 id) {
69
+ function getExecutionId() internal view returns (bytes32 id) {
70
70
  assembly {
71
71
  id := tload(TRANSIENT_STORAGE_SLOT)
72
72
  }
73
73
  }
74
74
 
75
- function setTransientExecutionId(bytes32 id) internal {
75
+ function setExecutionId(bytes32 id) internal {
76
76
  assembly {
77
77
  tstore(TRANSIENT_STORAGE_SLOT, id)
78
78
  }
@@ -90,10 +90,16 @@ library ExecutorStorage {
90
90
  }
91
91
  }
92
92
 
93
- function clearTransientStorage(address target) internal {
93
+ function clearExecutionId(address target) internal {
94
94
  assembly {
95
95
  tstore(TRANSIENT_STORAGE_SLOT, 0)
96
96
  tstore(target, 0)
97
97
  }
98
98
  }
99
+
100
+ function clearTargetExecutionId(address target) internal {
101
+ assembly {
102
+ tstore(target, 0)
103
+ }
104
+ }
99
105
  }