Skip to content

Commit 0cb269d

Browse files
Spec update 6 (#18)
Change log: * Rename Execution to Call, and update IStandardExecutor * Update IPluginExecutor NatSpec * update IPluginManager * update name IPluginLoupe to IAccountLoupe * update IAccountLoupe interface to spec update 6 * update BaseModularAccount to PluginManagerInternals and clean it up * update IPlugin to match spec update 6 * add canSpendNativeToken support in code * re-arrange imports and inheritance * Remove uninstall field checking * Move exec hooks and permitted call hooks to hook group * perf: precompile contracts for faster test runs * Add post-only hooks and related tests * Update to use optimized test * Fix pre exec hook data in executeFromPlugin * Fix via-IR build & refactor * feat: allow overlapping hooks * feat: update ordering of associated post hook executions
1 parent 98bc1d7 commit 0cb269d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1806
-798
lines changed

.github/workflows/test.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: ERC6900 RI Test CI
1+
name: ERC-6900 RI Test CI
22

33
on: [pull_request, workflow_dispatch]
44

@@ -45,9 +45,9 @@ jobs:
4545

4646
- name: "Lint the contracts"
4747
run: "pnpm lint"
48-
49-
test:
50-
name: Run Forge Tests
48+
49+
test-optimized-test-deep:
50+
name: Run Forge Tests (optimized-test-deep)
5151
runs-on: ubuntu-latest
5252
steps:
5353
- uses: actions/checkout@v3
@@ -63,13 +63,13 @@ jobs:
6363
run: forge install
6464

6565
- name: Build project
66-
run: forge build
66+
run: FOUNDRY_PROFILE=optimized-build forge build
6767

6868
- name: Run tests
69-
run: FOUNDRY_PROFILE=deep forge test -vvv
69+
run: FOUNDRY_PROFILE=optimized-test-deep forge test -vvv
7070

71-
test-lite:
72-
name: Run Forge Tests [lite build]
71+
test-default:
72+
name: Run Forge Tests (default)
7373
runs-on: ubuntu-latest
7474
steps:
7575
- uses: actions/checkout@v3
@@ -85,7 +85,7 @@ jobs:
8585
run: forge install
8686

8787
- name: Build project
88-
run: FOUNDRY_PROFILE=lite forge build
88+
run: forge build
8989

9090
- name: Run tests
91-
run: FOUNDRY_PROFILE=lite forge test -vvv
91+
run: forge test -vvv

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# Foundry build and cache directories
22
out/
3+
out-optimized/
34
cache/
45
node_modules/
6+
7+
# Coverage
8+
report/
9+
lcov.info

README.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ERC-6900 Ref Implementation
1+
# ERC-6900 Reference Implementation
22

33
Reference implementation for [ERC-6900](https://eips.ethereum.org/EIPS/eip-6900). It is an early draft implementation.
44

@@ -13,20 +13,18 @@ The implementation includes an upgradable modular account with two plugins (`Sin
1313

1414
Anyone is welcome to submit feedback and/or PRs to improve code or add Plugins.
1515

16-
### Build
16+
### Testing
17+
18+
The default Foundry profile can be used to compile (without IR) and test the entire project. The default profile should be used when generating coverage and debugging.
1719

1820
```bash
1921
forge build
20-
21-
# or use the lite profile to reduce compilation time
22-
FOUNDRY_PROFILE=lite forge build
22+
forge test -vvv
2323
```
2424

25-
### Test
25+
Since IR compilation generates different bytecode, it's useful to test against the contracts compiled via IR. Since compiling the entire project (including the test suite) takes a long time, special profiles can be used to precompile just the source contracts, and have the tests deploy the relevant contracts using those artifacts.
2626

2727
```bash
28-
forge test -vvv
29-
30-
# or use the lite profile to reduce compilation time
31-
FOUNDRY_PROFILE=lite forge test -vvv
28+
FOUNDRY_PROFILE=optimized-build forge build
29+
FOUNDRY_PROFILE=optimized-test forge test -vvv
3230
```

foundry.toml

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
[profile.default]
22
solc = '0.8.19'
3-
via_ir = true
3+
via_ir = false
44
src = 'src'
5-
out = 'out'
65
test = 'test'
76
libs = ['lib']
7+
out = 'out'
88
optimizer = true
99
optimizer_runs = 10_000
1010
ignored_error_codes = [3628]
11+
fs_permissions = [
12+
{ access = "read", path = "./out-optimized" }
13+
]
1114

1215
[fuzz]
1316
runs = 500
@@ -17,12 +20,23 @@ runs=500
1720
fail_on_revert = true
1821
depth = 10
1922

20-
[profile.lite]
21-
solc = '0.8.19'
22-
via_ir = false
23-
optimizer = true
24-
optimizer_runs = 10_000
25-
ignored_error_codes = [3628]
23+
[profile.optimized-build]
24+
via_ir = true
25+
test = 'src'
26+
out = 'out-optimized'
27+
28+
[profile.optimized-test]
29+
src = 'test'
30+
31+
[profile.optimized-test-deep]
32+
src = 'test'
33+
34+
[profile.optimized-test-deep.fuzz]
35+
runs = 10000
36+
37+
[profile.optimized-test-deep.invariant]
38+
runs = 5000
39+
depth = 32
2640

2741
[profile.deep.fuzz]
2842
runs = 10000
@@ -43,4 +57,4 @@ goerli = "${RPC_URL_GOERLI}"
4357
mainnet = { key = "${ETHERSCAN_API_KEY}" }
4458
goerli = { key = "${ETHERSCAN_API_KEY}" }
4559

46-
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
60+
# See more config options https://github.com/foundry-rs/foundry/tree/master/config

src/account/AccountLoupe.sol

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity ^0.8.19;
3+
4+
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
5+
import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
6+
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
7+
8+
import {IAccountLoupe} from "../interfaces/IAccountLoupe.sol";
9+
import {IPluginManager} from "../interfaces/IPluginManager.sol";
10+
import {IStandardExecutor} from "../interfaces/IStandardExecutor.sol";
11+
import {
12+
AccountStorage,
13+
getAccountStorage,
14+
getPermittedCallKey,
15+
HookGroup,
16+
toFunctionReferenceArray
17+
} from "../libraries/AccountStorage.sol";
18+
import {FunctionReference} from "../libraries/FunctionReferenceLib.sol";
19+
20+
abstract contract AccountLoupe is IAccountLoupe {
21+
using EnumerableMap for EnumerableMap.Bytes32ToUintMap;
22+
using EnumerableSet for EnumerableSet.AddressSet;
23+
24+
error ManifestDiscrepancy(address plugin);
25+
26+
/// @inheritdoc IAccountLoupe
27+
function getExecutionFunctionConfig(bytes4 selector)
28+
external
29+
view
30+
returns (ExecutionFunctionConfig memory config)
31+
{
32+
AccountStorage storage _storage = getAccountStorage();
33+
34+
if (
35+
selector == IStandardExecutor.execute.selector || selector == IStandardExecutor.executeBatch.selector
36+
|| selector == UUPSUpgradeable.upgradeTo.selector
37+
|| selector == UUPSUpgradeable.upgradeToAndCall.selector
38+
|| selector == IPluginManager.installPlugin.selector
39+
|| selector == IPluginManager.uninstallPlugin.selector
40+
) {
41+
config.plugin = address(this);
42+
} else {
43+
config.plugin = _storage.selectorData[selector].plugin;
44+
}
45+
46+
config.userOpValidationFunction = _storage.selectorData[selector].userOpValidation;
47+
48+
config.runtimeValidationFunction = _storage.selectorData[selector].runtimeValidation;
49+
}
50+
51+
/// @inheritdoc IAccountLoupe
52+
function getExecutionHooks(bytes4 selector) external view returns (ExecutionHooks[] memory execHooks) {
53+
execHooks = _getHooks(getAccountStorage().selectorData[selector].executionHooks);
54+
}
55+
56+
/// @inheritdoc IAccountLoupe
57+
function getPermittedCallHooks(address callingPlugin, bytes4 selector)
58+
external
59+
view
60+
returns (ExecutionHooks[] memory execHooks)
61+
{
62+
bytes24 key = getPermittedCallKey(callingPlugin, selector);
63+
execHooks = _getHooks(getAccountStorage().permittedCalls[key].permittedCallHooks);
64+
}
65+
66+
/// @inheritdoc IAccountLoupe
67+
function getPreValidationHooks(bytes4 selector)
68+
external
69+
view
70+
returns (
71+
FunctionReference[] memory preUserOpValidationHooks,
72+
FunctionReference[] memory preRuntimeValidationHooks
73+
)
74+
{
75+
preUserOpValidationHooks =
76+
toFunctionReferenceArray(getAccountStorage().selectorData[selector].preUserOpValidationHooks);
77+
preRuntimeValidationHooks =
78+
toFunctionReferenceArray(getAccountStorage().selectorData[selector].preRuntimeValidationHooks);
79+
}
80+
81+
/// @inheritdoc IAccountLoupe
82+
function getInstalledPlugins() external view returns (address[] memory pluginAddresses) {
83+
pluginAddresses = getAccountStorage().plugins.values();
84+
}
85+
86+
function _getHooks(HookGroup storage hooks) internal view returns (ExecutionHooks[] memory execHooks) {
87+
uint256 preExecHooksLength = hooks.preHooks.length();
88+
uint256 postOnlyExecHooksLength = hooks.postOnlyHooks.length();
89+
uint256 maxExecHooksLength = postOnlyExecHooksLength;
90+
91+
// There can only be as many associated post hooks to run as there are pre hooks.
92+
for (uint256 i = 0; i < preExecHooksLength;) {
93+
(, uint256 count) = hooks.preHooks.at(i);
94+
unchecked {
95+
maxExecHooksLength += (count + 1);
96+
++i;
97+
}
98+
}
99+
100+
// Overallocate on length - not all of this may get filled up. We set the correct length later.
101+
execHooks = new ExecutionHooks[](maxExecHooksLength);
102+
uint256 actualExecHooksLength;
103+
104+
for (uint256 i = 0; i < preExecHooksLength;) {
105+
(bytes32 key,) = hooks.preHooks.at(i);
106+
FunctionReference preExecHook = FunctionReference.wrap(bytes21(key));
107+
108+
uint256 associatedPostExecHooksLength = hooks.associatedPostHooks[preExecHook].length();
109+
if (associatedPostExecHooksLength > 0) {
110+
for (uint256 j = 0; j < associatedPostExecHooksLength;) {
111+
execHooks[actualExecHooksLength].preExecHook = preExecHook;
112+
(key,) = hooks.associatedPostHooks[preExecHook].at(j);
113+
execHooks[actualExecHooksLength].postExecHook = FunctionReference.wrap(bytes21(key));
114+
115+
unchecked {
116+
++actualExecHooksLength;
117+
++j;
118+
}
119+
}
120+
} else {
121+
execHooks[actualExecHooksLength].preExecHook = preExecHook;
122+
123+
unchecked {
124+
++actualExecHooksLength;
125+
}
126+
}
127+
128+
unchecked {
129+
++i;
130+
}
131+
}
132+
133+
for (uint256 i = 0; i < postOnlyExecHooksLength;) {
134+
(bytes32 key,) = hooks.postOnlyHooks.at(i);
135+
execHooks[actualExecHooksLength].postExecHook = FunctionReference.wrap(bytes21(key));
136+
137+
unchecked {
138+
++actualExecHooksLength;
139+
++i;
140+
}
141+
}
142+
143+
// Trim the exec hooks array to the actual length, since we may have overallocated.
144+
assembly ("memory-safe") {
145+
mstore(execHooks, actualExecHooksLength)
146+
}
147+
}
148+
}

src/account/AccountStorageInitializable.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.19;
33

44
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
5+
56
import {AccountStorage, getAccountStorage} from "../libraries/AccountStorage.sol";
67

78
abstract contract AccountStorageInitializable {

0 commit comments

Comments
 (0)