Skip to content

Commit

Permalink
Spec update 6 (#18)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
fangting-alchemy authored Dec 1, 2023
1 parent 98bc1d7 commit 0cb269d
Show file tree
Hide file tree
Showing 43 changed files with 1,806 additions and 798 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: ERC6900 RI Test CI
name: ERC-6900 RI Test CI

on: [pull_request, workflow_dispatch]

Expand Down Expand Up @@ -45,9 +45,9 @@ jobs:

- name: "Lint the contracts"
run: "pnpm lint"
test:
name: Run Forge Tests

test-optimized-test-deep:
name: Run Forge Tests (optimized-test-deep)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand All @@ -63,13 +63,13 @@ jobs:
run: forge install

- name: Build project
run: forge build
run: FOUNDRY_PROFILE=optimized-build forge build

- name: Run tests
run: FOUNDRY_PROFILE=deep forge test -vvv
run: FOUNDRY_PROFILE=optimized-test-deep forge test -vvv

test-lite:
name: Run Forge Tests [lite build]
test-default:
name: Run Forge Tests (default)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand All @@ -85,7 +85,7 @@ jobs:
run: forge install

- name: Build project
run: FOUNDRY_PROFILE=lite forge build
run: forge build

- name: Run tests
run: FOUNDRY_PROFILE=lite forge test -vvv
run: forge test -vvv
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Foundry build and cache directories
out/
out-optimized/
cache/
node_modules/

# Coverage
report/
lcov.info
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ERC-6900 Ref Implementation
# ERC-6900 Reference Implementation

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

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

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

### Build
### Testing

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.

```bash
forge build

# or use the lite profile to reduce compilation time
FOUNDRY_PROFILE=lite forge build
forge test -vvv
```

### Test
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.

```bash
forge test -vvv

# or use the lite profile to reduce compilation time
FOUNDRY_PROFILE=lite forge test -vvv
FOUNDRY_PROFILE=optimized-build forge build
FOUNDRY_PROFILE=optimized-test forge test -vvv
```
32 changes: 23 additions & 9 deletions foundry.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
[profile.default]
solc = '0.8.19'
via_ir = true
via_ir = false
src = 'src'
out = 'out'
test = 'test'
libs = ['lib']
out = 'out'
optimizer = true
optimizer_runs = 10_000
ignored_error_codes = [3628]
fs_permissions = [
{ access = "read", path = "./out-optimized" }
]

[fuzz]
runs = 500
Expand All @@ -17,12 +20,23 @@ runs=500
fail_on_revert = true
depth = 10

[profile.lite]
solc = '0.8.19'
via_ir = false
optimizer = true
optimizer_runs = 10_000
ignored_error_codes = [3628]
[profile.optimized-build]
via_ir = true
test = 'src'
out = 'out-optimized'

[profile.optimized-test]
src = 'test'

[profile.optimized-test-deep]
src = 'test'

[profile.optimized-test-deep.fuzz]
runs = 10000

[profile.optimized-test-deep.invariant]
runs = 5000
depth = 32

[profile.deep.fuzz]
runs = 10000
Expand All @@ -43,4 +57,4 @@ goerli = "${RPC_URL_GOERLI}"
mainnet = { key = "${ETHERSCAN_API_KEY}" }
goerli = { key = "${ETHERSCAN_API_KEY}" }

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
148 changes: 148 additions & 0 deletions src/account/AccountLoupe.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;

import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import {IAccountLoupe} from "../interfaces/IAccountLoupe.sol";
import {IPluginManager} from "../interfaces/IPluginManager.sol";
import {IStandardExecutor} from "../interfaces/IStandardExecutor.sol";
import {
AccountStorage,
getAccountStorage,
getPermittedCallKey,
HookGroup,
toFunctionReferenceArray
} from "../libraries/AccountStorage.sol";
import {FunctionReference} from "../libraries/FunctionReferenceLib.sol";

abstract contract AccountLoupe is IAccountLoupe {
using EnumerableMap for EnumerableMap.Bytes32ToUintMap;
using EnumerableSet for EnumerableSet.AddressSet;

error ManifestDiscrepancy(address plugin);

/// @inheritdoc IAccountLoupe
function getExecutionFunctionConfig(bytes4 selector)
external
view
returns (ExecutionFunctionConfig memory config)
{
AccountStorage storage _storage = getAccountStorage();

if (
selector == IStandardExecutor.execute.selector || selector == IStandardExecutor.executeBatch.selector
|| selector == UUPSUpgradeable.upgradeTo.selector
|| selector == UUPSUpgradeable.upgradeToAndCall.selector
|| selector == IPluginManager.installPlugin.selector
|| selector == IPluginManager.uninstallPlugin.selector
) {
config.plugin = address(this);
} else {
config.plugin = _storage.selectorData[selector].plugin;
}

config.userOpValidationFunction = _storage.selectorData[selector].userOpValidation;

config.runtimeValidationFunction = _storage.selectorData[selector].runtimeValidation;
}

/// @inheritdoc IAccountLoupe
function getExecutionHooks(bytes4 selector) external view returns (ExecutionHooks[] memory execHooks) {
execHooks = _getHooks(getAccountStorage().selectorData[selector].executionHooks);
}

/// @inheritdoc IAccountLoupe
function getPermittedCallHooks(address callingPlugin, bytes4 selector)
external
view
returns (ExecutionHooks[] memory execHooks)
{
bytes24 key = getPermittedCallKey(callingPlugin, selector);
execHooks = _getHooks(getAccountStorage().permittedCalls[key].permittedCallHooks);
}

/// @inheritdoc IAccountLoupe
function getPreValidationHooks(bytes4 selector)
external
view
returns (
FunctionReference[] memory preUserOpValidationHooks,
FunctionReference[] memory preRuntimeValidationHooks
)
{
preUserOpValidationHooks =
toFunctionReferenceArray(getAccountStorage().selectorData[selector].preUserOpValidationHooks);
preRuntimeValidationHooks =
toFunctionReferenceArray(getAccountStorage().selectorData[selector].preRuntimeValidationHooks);
}

/// @inheritdoc IAccountLoupe
function getInstalledPlugins() external view returns (address[] memory pluginAddresses) {
pluginAddresses = getAccountStorage().plugins.values();
}

function _getHooks(HookGroup storage hooks) internal view returns (ExecutionHooks[] memory execHooks) {
uint256 preExecHooksLength = hooks.preHooks.length();
uint256 postOnlyExecHooksLength = hooks.postOnlyHooks.length();
uint256 maxExecHooksLength = postOnlyExecHooksLength;

// There can only be as many associated post hooks to run as there are pre hooks.
for (uint256 i = 0; i < preExecHooksLength;) {
(, uint256 count) = hooks.preHooks.at(i);
unchecked {
maxExecHooksLength += (count + 1);
++i;
}
}

// Overallocate on length - not all of this may get filled up. We set the correct length later.
execHooks = new ExecutionHooks[](maxExecHooksLength);
uint256 actualExecHooksLength;

for (uint256 i = 0; i < preExecHooksLength;) {
(bytes32 key,) = hooks.preHooks.at(i);
FunctionReference preExecHook = FunctionReference.wrap(bytes21(key));

uint256 associatedPostExecHooksLength = hooks.associatedPostHooks[preExecHook].length();
if (associatedPostExecHooksLength > 0) {
for (uint256 j = 0; j < associatedPostExecHooksLength;) {
execHooks[actualExecHooksLength].preExecHook = preExecHook;
(key,) = hooks.associatedPostHooks[preExecHook].at(j);
execHooks[actualExecHooksLength].postExecHook = FunctionReference.wrap(bytes21(key));

unchecked {
++actualExecHooksLength;
++j;
}
}
} else {
execHooks[actualExecHooksLength].preExecHook = preExecHook;

unchecked {
++actualExecHooksLength;
}
}

unchecked {
++i;
}
}

for (uint256 i = 0; i < postOnlyExecHooksLength;) {
(bytes32 key,) = hooks.postOnlyHooks.at(i);
execHooks[actualExecHooksLength].postExecHook = FunctionReference.wrap(bytes21(key));

unchecked {
++actualExecHooksLength;
++i;
}
}

// Trim the exec hooks array to the actual length, since we may have overallocated.
assembly ("memory-safe") {
mstore(execHooks, actualExecHooksLength)
}
}
}
1 change: 1 addition & 0 deletions src/account/AccountStorageInitializable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.19;

import {Address} from "@openzeppelin/contracts/utils/Address.sol";

import {AccountStorage, getAccountStorage} from "../libraries/AccountStorage.sol";

abstract contract AccountStorageInitializable {
Expand Down
Loading

0 comments on commit 0cb269d

Please sign in to comment.