@towns-protocol/contracts 0.0.440 → 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.
- package/package.json +3 -3
- package/scripts/deployments/diamonds/DeployAccountModules.s.sol +188 -0
- package/scripts/deployments/facets/DeployAccountHubFacet.s.sol +47 -0
- package/scripts/deployments/facets/DeployAccountTippingFacet.s.sol +37 -0
- package/scripts/deployments/facets/DeployAppManagerFacet.s.sol +40 -0
- package/src/account/facets/app/AppManagerFacet.sol +68 -0
- package/src/account/facets/app/AppManagerMod.sol +321 -0
- package/src/account/facets/hub/AccountHubFacet.sol +226 -0
- package/src/account/facets/hub/AccountHubMod.sol +136 -0
- package/src/account/facets/hub/IAccountHub.sol +25 -0
- package/src/account/facets/tipping/AccountTippingFacet.sol +72 -0
- package/src/account/facets/tipping/AccountTippingMod.sol +144 -0
- package/src/spaces/facets/account/AppAccountStorage.sol +0 -1
- package/src/spaces/facets/tipping/ITipping.sol +14 -19
- package/src/spaces/facets/tipping/TippingBase.sol +0 -1
|
@@ -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
|
+
}
|