@towns-protocol/contracts 0.0.439 → 0.0.441

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.
@@ -0,0 +1,321 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ // interfaces
5
+ import {IAppRegistry, IAppRegistryBase} from "src/apps/facets/registry/IAppRegistry.sol";
6
+ import {IModule} from "@erc6900/reference-implementation/interfaces/IModule.sol";
7
+ // libraries
8
+ import {EnumerableSetLib} from "solady/utils/EnumerableSetLib.sol";
9
+ import {CustomRevert} from "src/utils/libraries/CustomRevert.sol";
10
+ import {Attestation, EMPTY_UID} from "@ethereum-attestation-service/eas-contracts/Common.sol";
11
+ import {LibCall} from "solady/utils/LibCall.sol";
12
+ import "../hub/AccountHubMod.sol" as AccountHub;
13
+
14
+ // types
15
+ using CustomRevert for bytes4;
16
+ using EnumerableSetLib for EnumerableSetLib.Bytes32Set;
17
+
18
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
19
+ /* ERRORS */
20
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
21
+
22
+ /// @notice Thrown when the app ID is invalid
23
+ error AppManager__InvalidAppId();
24
+
25
+ /// @notice Thrown when the app is already installed
26
+ error AppManager__AppAlreadyInstalled();
27
+
28
+ /// @notice Thrown when the app is not installed
29
+ error AppManager__AppNotInstalled();
30
+
31
+ /// @notice Thrown when the app is not registered
32
+ error AppManager__AppNotRegistered();
33
+
34
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
35
+ /* STORAGE */
36
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
37
+
38
+ // keccak256(abi.encode(uint256(keccak256("towns.account.app.manager.storage")) - 1)) & ~bytes32(uint256(0xff))
39
+ bytes32 constant STORAGE_SLOT = 0x2e45e2674c3081261f26138b3a1b39b0261feef8186e18fcf5badd4929fe7b00;
40
+
41
+ struct App {
42
+ bytes32 appId;
43
+ address app;
44
+ uint48 installedAt;
45
+ uint48 expiration;
46
+ bool active;
47
+ }
48
+
49
+ /// @notice Storage layout for the AppManager
50
+ /// @custom:storage-location erc7201:towns.account.app.manager.storage
51
+ struct Layout {
52
+ mapping(address account => EnumerableSetLib.Bytes32Set) apps;
53
+ mapping(address account => mapping(bytes32 appId => App)) appById;
54
+ mapping(address account => mapping(address app => bytes32 appId)) appIdByApp;
55
+ }
56
+
57
+ /// @notice Returns the storage layout for the AppManager
58
+ /// @return $ The storage layout
59
+ function getStorage() pure returns (Layout storage $) {
60
+ assembly {
61
+ $.slot := STORAGE_SLOT
62
+ }
63
+ }
64
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
65
+ /* FUNCTIONS */
66
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
67
+
68
+ /// @notice Installs an app
69
+ /// @param account The account to install the app to
70
+ /// @param appId The ID of the app to install
71
+ /// @param data The data to pass to the app's onInstall function
72
+ function installApp(address account, bytes32 appId, bytes calldata data) {
73
+ if (appId == EMPTY_UID) AppManager__InvalidAppId.selector.revertWith();
74
+
75
+ IAppRegistry registry = IAppRegistry(AccountHub.getAppRegistry());
76
+ IAppRegistryBase.App memory app = registry.getAppById(appId);
77
+ if (app.appId == EMPTY_UID) AppManager__AppNotRegistered.selector.revertWith();
78
+
79
+ Layout storage $ = getStorage();
80
+ EnumerableSetLib.Bytes32Set storage apps = $.apps[account];
81
+
82
+ if (apps.contains(app.appId)) AppManager__AppAlreadyInstalled.selector.revertWith();
83
+
84
+ apps.add(app.appId);
85
+ $.appIdByApp[account][app.module] = app.appId;
86
+ $.appById[account][app.appId] = App({
87
+ appId: app.appId,
88
+ app: app.module,
89
+ installedAt: uint48(block.timestamp),
90
+ expiration: calcExpiration($, account, app.appId, app.duration),
91
+ active: true
92
+ });
93
+
94
+ if (data.length > 0) {
95
+ bytes memory callData = abi.encodeCall(IModule.onInstall, (data));
96
+ LibCall.callContract(app.module, 0, callData);
97
+ }
98
+ }
99
+
100
+ /// @notice Uninstalls an app
101
+ /// @param account The account to uninstall the app from
102
+ /// @param appId The ID of the app to uninstall
103
+ /// @param data The data to pass to the app's onUninstall function
104
+ function uninstallApp(address account, bytes32 appId, bytes calldata data) {
105
+ if (appId == EMPTY_UID) AppManager__InvalidAppId.selector.revertWith();
106
+
107
+ IAppRegistry registry = IAppRegistry(AccountHub.getAppRegistry());
108
+ IAppRegistryBase.App memory app = registry.getAppById(appId);
109
+ if (app.appId == EMPTY_UID) AppManager__AppNotRegistered.selector.revertWith();
110
+
111
+ Layout storage $ = getStorage();
112
+ EnumerableSetLib.Bytes32Set storage apps = $.apps[account];
113
+
114
+ if (!apps.contains(app.appId)) AppManager__AppNotInstalled.selector.revertWith();
115
+
116
+ address module = $.appById[account][app.appId].app;
117
+
118
+ // Remove from storage
119
+ apps.remove(app.appId);
120
+ delete $.appIdByApp[account][module];
121
+ delete $.appById[account][app.appId];
122
+
123
+ // Call module's onUninstall if data is provided (non-reverting)
124
+ if (data.length > 0) {
125
+ // solhint-disable-next-line no-empty-blocks
126
+ try IModule(module).onUninstall(data) {} catch {}
127
+ }
128
+ }
129
+
130
+ /// @notice Renews an app subscription
131
+ /// @param account The account that owns the app
132
+ /// @param appId The ID of the app to renew
133
+ function renewApp(address account, bytes32 appId, bytes calldata) {
134
+ if (appId == EMPTY_UID) AppManager__InvalidAppId.selector.revertWith();
135
+
136
+ IAppRegistry registry = IAppRegistry(AccountHub.getAppRegistry());
137
+ IAppRegistryBase.App memory app = registry.getAppById(appId);
138
+ if (app.appId == EMPTY_UID) AppManager__AppNotRegistered.selector.revertWith();
139
+
140
+ Layout storage $ = getStorage();
141
+ if (!$.apps[account].contains(appId)) AppManager__AppNotInstalled.selector.revertWith();
142
+
143
+ // Calculate and update the new expiration
144
+ $.appById[account][appId].expiration = calcExpiration($, account, appId, app.duration);
145
+ }
146
+
147
+ /// @notice Updates an app to a new version
148
+ /// @param account The account that owns the app
149
+ /// @param newAppId The ID of the new app version
150
+ /// @param data The data containing the current module address
151
+ function updateApp(address account, bytes32 newAppId, bytes calldata data) {
152
+ if (data.length < 32) AppManager__InvalidAppId.selector.revertWith();
153
+
154
+ address module = abi.decode(data, (address));
155
+ if (module == address(0)) AppManager__InvalidAppId.selector.revertWith();
156
+
157
+ Layout storage $ = getStorage();
158
+
159
+ // Get current app ID from module
160
+ bytes32 currentAppId = $.appIdByApp[account][module];
161
+ if (currentAppId == EMPTY_UID) AppManager__AppNotInstalled.selector.revertWith();
162
+ if (currentAppId == newAppId) AppManager__AppAlreadyInstalled.selector.revertWith();
163
+
164
+ IAppRegistry registry = IAppRegistry(AccountHub.getAppRegistry());
165
+ IAppRegistryBase.App memory newApp = registry.getAppById(newAppId);
166
+ if (newApp.appId == EMPTY_UID) AppManager__AppNotRegistered.selector.revertWith();
167
+
168
+ // Read the old module from the stored app before deletion
169
+ address oldModule = $.appById[account][currentAppId].app;
170
+
171
+ // Remove old app from storage
172
+ $.apps[account].remove(currentAppId);
173
+ delete $.appById[account][currentAppId];
174
+ delete $.appIdByApp[account][oldModule];
175
+
176
+ // Add new app
177
+ $.apps[account].add(newAppId);
178
+ $.appIdByApp[account][newApp.module] = newAppId;
179
+ $.appById[account][newAppId] = App({
180
+ appId: newAppId,
181
+ app: newApp.module,
182
+ installedAt: uint48(block.timestamp),
183
+ expiration: calcExpiration($, account, newAppId, newApp.duration),
184
+ active: true
185
+ });
186
+ }
187
+
188
+ /// @notice Checks if an app is installed
189
+ /// @param account The account to check
190
+ /// @param app The app address
191
+ /// @return True if the app is installed
192
+ function isAppInstalled(address account, address app) view returns (bool) {
193
+ Layout storage $ = getStorage();
194
+ bytes32 appId = $.appIdByApp[account][app];
195
+ return appId != EMPTY_UID && $.appById[account][appId].active;
196
+ }
197
+
198
+ /// @notice Gets the app ID for a given app address
199
+ /// @param account The account to check
200
+ /// @param app The app address
201
+ /// @return The app ID
202
+ function getAppId(address account, address app) view returns (bytes32) {
203
+ return getStorage().appIdByApp[account][app];
204
+ }
205
+
206
+ /// @notice Gets the expiration timestamp for a given app
207
+ /// @param account The account to check
208
+ /// @param app The app address
209
+ /// @return The expiration timestamp
210
+ function getAppExpiration(address account, address app) view returns (uint48) {
211
+ Layout storage $ = getStorage();
212
+ bytes32 appId = $.appIdByApp[account][app];
213
+ return $.appById[account][appId].expiration;
214
+ }
215
+
216
+ /// @notice Gets all installed app addresses for an account
217
+ /// @param account The account to check
218
+ /// @return apps The array of installed app addresses (only active apps)
219
+ function getInstalledApps(address account) view returns (address[] memory apps) {
220
+ Layout storage $ = getStorage();
221
+ bytes32[] memory appIds = $.apps[account].values();
222
+ uint256 length = appIds.length;
223
+
224
+ // Count active apps first
225
+ uint256 activeCount;
226
+ for (uint256 i; i < length; ++i) {
227
+ if ($.appById[account][appIds[i]].active) ++activeCount;
228
+ }
229
+
230
+ // Build array of active apps only
231
+ apps = new address[](activeCount);
232
+ uint256 j;
233
+ for (uint256 i; i < length; ++i) {
234
+ App storage app = $.appById[account][appIds[i]];
235
+ if (app.active) {
236
+ apps[j++] = app.app;
237
+ }
238
+ }
239
+ }
240
+
241
+ /// @notice Enables an app
242
+ /// @param account The account that owns the app
243
+ /// @param app The app address to enable
244
+ function enableApp(address account, address app) {
245
+ Layout storage $ = getStorage();
246
+ bytes32 appId = $.appIdByApp[account][app];
247
+ if (appId == EMPTY_UID) AppManager__AppNotInstalled.selector.revertWith();
248
+ $.appById[account][appId].active = true;
249
+ }
250
+
251
+ /// @notice Disables an app
252
+ /// @param account The account that owns the app
253
+ /// @param app The app address to disable
254
+ function disableApp(address account, address app) {
255
+ Layout storage $ = getStorage();
256
+ bytes32 appId = $.appIdByApp[account][app];
257
+ if (appId == EMPTY_UID) AppManager__AppNotInstalled.selector.revertWith();
258
+ $.appById[account][appId].active = false;
259
+ }
260
+
261
+ /// @notice Checks if an app is entitled to a permission
262
+ /// @param account The account to check
263
+ /// @param module The app module address
264
+ /// @param client The client address making the request
265
+ /// @param permission The permission to check
266
+ /// @return True if the app is entitled to the permission
267
+ function isAppEntitled(
268
+ address account,
269
+ address module,
270
+ address client,
271
+ bytes32 permission
272
+ ) view returns (bool) {
273
+ Layout storage $ = getStorage();
274
+ bytes32 appId = $.appIdByApp[account][module];
275
+ if (appId == EMPTY_UID) return false;
276
+
277
+ // Check app is active
278
+ if (!$.appById[account][appId].active) return false;
279
+
280
+ IAppRegistry registry = IAppRegistry(AccountHub.getAppRegistry());
281
+
282
+ // Check app not banned
283
+ if (registry.isAppBanned(module)) return false;
284
+
285
+ // Check app has not expired
286
+ if ($.appById[account][appId].expiration < block.timestamp) return false;
287
+
288
+ IAppRegistryBase.App memory app = registry.getAppById(appId);
289
+ if (app.appId == EMPTY_UID) return false;
290
+
291
+ // Check client matches the registered client
292
+ if (app.client != client) return false;
293
+
294
+ // Check permission exists in app.permissions
295
+ uint256 permissionsLength = app.permissions.length;
296
+ for (uint256 i; i < permissionsLength; ++i) {
297
+ if (app.permissions[i] == permission) return true;
298
+ }
299
+
300
+ return false;
301
+ }
302
+
303
+ /// @notice Calculates the expiration for an app
304
+ /// @param $ The storage layout
305
+ /// @param account The account that owns the app
306
+ /// @param appId The ID of the app
307
+ /// @param newDuration The new duration of the app
308
+ /// @return The expiration timestamp
309
+ function calcExpiration(
310
+ Layout storage $,
311
+ address account,
312
+ bytes32 appId,
313
+ uint48 newDuration
314
+ ) view returns (uint48) {
315
+ uint48 currentExpiration = $.appById[account][appId].expiration;
316
+ if (currentExpiration > block.timestamp) {
317
+ return currentExpiration + newDuration;
318
+ } else {
319
+ return uint48(block.timestamp) + newDuration;
320
+ }
321
+ }
@@ -0,0 +1,226 @@
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 {IAccountHub} from "./IAccountHub.sol";
8
+ import {IExecutionModule, ExecutionManifest, ManifestExecutionFunction, ManifestExecutionHook} from "@erc6900/reference-implementation/interfaces/IExecutionModule.sol";
9
+ import {IExecutionHookModule} from "@erc6900/reference-implementation/interfaces/IExecutionHookModule.sol";
10
+ import {IAppAccount} from "../../../spaces/facets/account/IAppAccount.sol";
11
+
12
+ // libraries
13
+ import "./AccountHubMod.sol" as AccountHub;
14
+ import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
15
+ import {Validator} from "../../../utils/libraries/Validator.sol";
16
+
17
+ // contracts
18
+ import {ModuleBase} from "modular-account/src/modules/ModuleBase.sol";
19
+ import {Facet} from "@towns-protocol/diamond/src/facets/Facet.sol";
20
+ import {OwnableBase} from "@towns-protocol/diamond/src/facets/ownable/OwnableBase.sol";
21
+ import {ReentrancyGuardTransient} from "solady/utils/ReentrancyGuardTransient.sol";
22
+
23
+ contract AccountHubFacet is
24
+ IAccountHub,
25
+ IModule,
26
+ IExecutionModule,
27
+ IExecutionHookModule,
28
+ ModuleBase,
29
+ OwnableBase,
30
+ ReentrancyGuardTransient,
31
+ Facet
32
+ {
33
+ using CustomRevert for bytes4;
34
+
35
+ /// @notice Initializes the facet when added to a Diamond
36
+ function __AccountHubFacet_init(
37
+ address spaceFactory,
38
+ address appRegistry
39
+ ) external onlyInitializing {
40
+ _addInterface(type(IModule).interfaceId);
41
+ _addInterface(type(IAccountHub).interfaceId);
42
+ _addInterface(type(IValidationModule).interfaceId);
43
+ _addInterface(type(IExecutionModule).interfaceId);
44
+ __AccountHubFacet_init_unchained(spaceFactory, appRegistry);
45
+ }
46
+
47
+ function __AccountHubFacet_init_unchained(address spaceFactory, address appRegistry) internal {
48
+ Validator.checkAddress(spaceFactory);
49
+ Validator.checkAddress(appRegistry);
50
+ AccountHub.Layout storage $ = AccountHub.getStorage();
51
+ ($.spaceFactory, $.appRegistry) = (spaceFactory, appRegistry);
52
+ }
53
+
54
+ /// @inheritdoc IModule
55
+ function onInstall(bytes calldata data) external override nonReentrant {
56
+ address account = abi.decode(data, (address));
57
+ AccountHub.installAccount(account);
58
+ }
59
+
60
+ /// @inheritdoc IModule
61
+ function onUninstall(bytes calldata data) external override nonReentrant {
62
+ address account = abi.decode(data, (address));
63
+ AccountHub.uninstallAccount(account);
64
+ }
65
+
66
+ function setSpaceFactory(address spaceFactory) external onlyOwner {
67
+ AccountHub.setSpaceFactory(spaceFactory);
68
+ }
69
+
70
+ function setAppRegistry(address appRegistry) external onlyOwner {
71
+ AccountHub.setAppRegistry(appRegistry);
72
+ }
73
+
74
+ function getSpaceFactory() external view returns (address) {
75
+ return AccountHub.getStorage().spaceFactory;
76
+ }
77
+
78
+ function getAppRegistry() external view returns (address) {
79
+ return AccountHub.getStorage().appRegistry;
80
+ }
81
+
82
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
83
+ /* ERC-6900 MODULE */
84
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
85
+
86
+ /// @inheritdoc IModule
87
+ function moduleId() external pure returns (string memory) {
88
+ return "towns.account-module.1.0.0";
89
+ }
90
+
91
+ function executionManifest() external pure returns (ExecutionManifest memory) {
92
+ bool allowGlobalValidation = false;
93
+ bool skipRuntimeValidation = true;
94
+
95
+ ManifestExecutionFunction[] memory executionFunctions = new ManifestExecutionFunction[](11);
96
+ executionFunctions[0] = ManifestExecutionFunction({
97
+ executionSelector: IAppAccount.onInstallApp.selector,
98
+ skipRuntimeValidation: skipRuntimeValidation,
99
+ allowGlobalValidation: allowGlobalValidation
100
+ });
101
+
102
+ executionFunctions[1] = ManifestExecutionFunction({
103
+ executionSelector: IAppAccount.onUninstallApp.selector,
104
+ skipRuntimeValidation: skipRuntimeValidation,
105
+ allowGlobalValidation: allowGlobalValidation
106
+ });
107
+
108
+ executionFunctions[2] = ManifestExecutionFunction({
109
+ executionSelector: IAppAccount.onRenewApp.selector,
110
+ skipRuntimeValidation: skipRuntimeValidation,
111
+ allowGlobalValidation: allowGlobalValidation
112
+ });
113
+
114
+ executionFunctions[3] = ManifestExecutionFunction({
115
+ executionSelector: IAppAccount.onUpdateApp.selector,
116
+ skipRuntimeValidation: skipRuntimeValidation,
117
+ allowGlobalValidation: allowGlobalValidation
118
+ });
119
+
120
+ executionFunctions[4] = ManifestExecutionFunction({
121
+ executionSelector: IAppAccount.enableApp.selector,
122
+ skipRuntimeValidation: skipRuntimeValidation,
123
+ allowGlobalValidation: allowGlobalValidation
124
+ });
125
+
126
+ executionFunctions[5] = ManifestExecutionFunction({
127
+ executionSelector: IAppAccount.disableApp.selector,
128
+ skipRuntimeValidation: skipRuntimeValidation,
129
+ allowGlobalValidation: allowGlobalValidation
130
+ });
131
+
132
+ executionFunctions[6] = ManifestExecutionFunction({
133
+ executionSelector: IAppAccount.isAppInstalled.selector,
134
+ skipRuntimeValidation: skipRuntimeValidation,
135
+ allowGlobalValidation: allowGlobalValidation
136
+ });
137
+
138
+ executionFunctions[7] = ManifestExecutionFunction({
139
+ executionSelector: IAppAccount.getAppId.selector,
140
+ skipRuntimeValidation: skipRuntimeValidation,
141
+ allowGlobalValidation: allowGlobalValidation
142
+ });
143
+
144
+ executionFunctions[8] = ManifestExecutionFunction({
145
+ executionSelector: IAppAccount.getAppExpiration.selector,
146
+ skipRuntimeValidation: skipRuntimeValidation,
147
+ allowGlobalValidation: allowGlobalValidation
148
+ });
149
+
150
+ executionFunctions[9] = ManifestExecutionFunction({
151
+ executionSelector: IAppAccount.isAppEntitled.selector,
152
+ skipRuntimeValidation: skipRuntimeValidation,
153
+ allowGlobalValidation: allowGlobalValidation
154
+ });
155
+
156
+ executionFunctions[10] = ManifestExecutionFunction({
157
+ executionSelector: IAppAccount.getInstalledApps.selector,
158
+ skipRuntimeValidation: skipRuntimeValidation,
159
+ allowGlobalValidation: allowGlobalValidation
160
+ });
161
+
162
+ ManifestExecutionHook[] memory executionHooks = new ManifestExecutionHook[](4);
163
+
164
+ executionHooks[0] = ManifestExecutionHook({
165
+ executionSelector: IAppAccount.onInstallApp.selector,
166
+ entityId: 1,
167
+ isPreHook: true,
168
+ isPostHook: false
169
+ });
170
+
171
+ executionHooks[1] = ManifestExecutionHook({
172
+ executionSelector: IAppAccount.onUninstallApp.selector,
173
+ entityId: 2,
174
+ isPreHook: true,
175
+ isPostHook: false
176
+ });
177
+
178
+ executionHooks[2] = ManifestExecutionHook({
179
+ executionSelector: IAppAccount.onRenewApp.selector,
180
+ entityId: 3,
181
+ isPreHook: true,
182
+ isPostHook: false
183
+ });
184
+
185
+ executionHooks[3] = ManifestExecutionHook({
186
+ executionSelector: IAppAccount.onUpdateApp.selector,
187
+ entityId: 4,
188
+ isPreHook: true,
189
+ isPostHook: false
190
+ });
191
+
192
+ bytes4[] memory interfaceIds = new bytes4[](1);
193
+ interfaceIds[0] = type(IAppAccount).interfaceId;
194
+
195
+ return
196
+ ExecutionManifest({
197
+ executionFunctions: executionFunctions,
198
+ executionHooks: executionHooks,
199
+ interfaceIds: interfaceIds
200
+ });
201
+ }
202
+
203
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
204
+ /* ACCOUNT MODULE */
205
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
206
+
207
+ function isInstalled(address account) external view returns (bool) {
208
+ return AccountHub.isInstalled(account);
209
+ }
210
+
211
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
212
+ /* MODULE HOOKS */
213
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
214
+
215
+ function preExecutionHook(
216
+ uint32,
217
+ address sender,
218
+ uint256,
219
+ bytes calldata
220
+ ) external view returns (bytes memory) {
221
+ AccountHub.onlyRegistry(sender);
222
+ return "";
223
+ }
224
+
225
+ function postExecutionHook(uint32, bytes calldata) external {}
226
+ }
@@ -0,0 +1,136 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.29;
3
+
4
+ // libraries
5
+ import {CustomRevert} from "../../../utils/libraries/CustomRevert.sol";
6
+ import {Validator} from "../../../utils/libraries/Validator.sol";
7
+
8
+ // types
9
+ using CustomRevert for bytes4;
10
+
11
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
12
+ /* EVENTS */
13
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
14
+
15
+ /// @notice Emitted when the space factory is set
16
+ /// @param spaceFactory The address of the space factory
17
+ event SpaceFactorySet(address spaceFactory);
18
+
19
+ /// @notice Emitted when the app registry is set
20
+ /// @param appRegistry The address of the app registry
21
+ event AppRegistrySet(address appRegistry);
22
+
23
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
24
+ /* ERRORS */
25
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
26
+
27
+ /// @notice Reverted when the sender is invalid
28
+ /// @param sender The address of the sender
29
+ error AccountHub__InvalidSender(address sender);
30
+
31
+ /// @notice Reverted when the account is already initialized
32
+ /// @param account The address of the account
33
+ error AccountHub__AlreadyInitialized(address account);
34
+
35
+ /// @notice Reverted when the account is invalid
36
+ /// @param account The address of the account
37
+ error AccountHub__InvalidAccount(address account);
38
+
39
+ /// @notice Reverted when the account is not installed
40
+ /// @param account The address of the account
41
+ error AccountHub__NotInstalled(address account);
42
+
43
+ /// @notice Reverted when the sender is not the registry
44
+ /// @param sender The address of the sender
45
+ error AccountHub__InvalidCaller(address sender);
46
+
47
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
48
+ /* STORAGE */
49
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
50
+
51
+ // keccak256(abi.encode(uint256(keccak256("towns.account.hub.storage")) - 1)) & ~bytes32(uint256(0xff))
52
+ bytes32 constant STORAGE_SLOT = 0x71d4dc86d61a3ac91d71fb32ada3a4c5ccb69a82d3979318701e6840c1db0a00;
53
+
54
+ /// @notice Storage layout for the AccountHubMod
55
+ /// @custom:storage-location erc7201:towns.account.hub.storage
56
+ struct Layout {
57
+ /// @notice Space factory
58
+ address spaceFactory;
59
+ /// @notice App registry
60
+ address appRegistry;
61
+ /// @notice Installed accounts
62
+ mapping(address account => bool installed) installed;
63
+ }
64
+
65
+ /// @notice Returns the storage layout for the AccountHubMod
66
+ function getStorage() pure returns (Layout storage $) {
67
+ assembly {
68
+ $.slot := STORAGE_SLOT
69
+ }
70
+ }
71
+
72
+ /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
73
+ /* FUNCTIONS */
74
+ /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
75
+
76
+ /// @notice Installs an account
77
+ /// @param account The address of the account
78
+ function installAccount(address account) {
79
+ Validator.checkAddress(account);
80
+ if (account != msg.sender) AccountHub__InvalidAccount.selector.revertWith(account);
81
+
82
+ Layout storage $ = getStorage();
83
+ if ($.installed[account]) AccountHub__AlreadyInitialized.selector.revertWith(account);
84
+ $.installed[account] = true;
85
+ }
86
+
87
+ /// @notice Uninstalls an account
88
+ /// @param account The address of the account
89
+ function uninstallAccount(address account) {
90
+ Validator.checkAddress(account);
91
+ if (account != msg.sender) AccountHub__InvalidAccount.selector.revertWith(account);
92
+ Layout storage $ = getStorage();
93
+ if (!$.installed[account]) AccountHub__NotInstalled.selector.revertWith(account);
94
+ delete $.installed[account];
95
+ }
96
+
97
+ /// @notice Sets the space factory
98
+ /// @param spaceFactory The address of the space factory
99
+ function setSpaceFactory(address spaceFactory) {
100
+ Validator.checkAddress(spaceFactory);
101
+ getStorage().spaceFactory = spaceFactory;
102
+ emit SpaceFactorySet(spaceFactory);
103
+ }
104
+
105
+ /// @notice Sets the app registry
106
+ /// @param appRegistry The address of the app registry
107
+ function setAppRegistry(address appRegistry) {
108
+ Validator.checkAddress(appRegistry);
109
+ getStorage().appRegistry = appRegistry;
110
+ emit AppRegistrySet(appRegistry);
111
+ }
112
+
113
+ /// @notice Checks if an account is installed
114
+ /// @param account The address of the account
115
+ /// @return True if the account is installed, false otherwise
116
+ function isInstalled(address account) view returns (bool) {
117
+ return getStorage().installed[account];
118
+ }
119
+
120
+ /// @notice Gets the space factory
121
+ /// @return spaceFactory The address of the space factory
122
+ function getSpaceFactory() view returns (address) {
123
+ return getStorage().spaceFactory;
124
+ }
125
+
126
+ /// @notice Gets the app registry
127
+ /// @return appRegistry The address of the app registry
128
+ function getAppRegistry() view returns (address) {
129
+ return getStorage().appRegistry;
130
+ }
131
+
132
+ /// @notice Checks if the caller is the registry
133
+ /// @dev Guard function: reverts if the caller is not the registry
134
+ function onlyRegistry(address caller) view {
135
+ if (caller != getStorage().appRegistry) AccountHub__InvalidCaller.selector.revertWith(caller);
136
+ }