Skip to content

Commit

Permalink
Merge pull request #90 from rhinestonewtf/account/safe7579-fix-init
Browse files Browse the repository at this point in the history
🔥 Changing initAccount to only use validators for ERC4337 compliance
  • Loading branch information
zeroknots authored Apr 23, 2024
2 parents ec3760e + 0a2f09b commit 7066672
Show file tree
Hide file tree
Showing 53 changed files with 5,619 additions and 4,484 deletions.
32 changes: 32 additions & 0 deletions accounts/safe7579/.gas-snapshot
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
BaseTest:test_checkVersion() (gas: 21951)
BaseTest:test_foo() (gas: 3049)
ERC1271Test:test_WhenForwardingERC1271(bytes4) (runs: 258, μ: 207522, ~: 215621)
ERC1271Test:test_checkVersion() (gas: 21973)
ERC1271Test:test_foo() (gas: 3094)
ERC4337Test:test_WhenCallingValidateFunction() (gas: 122)
ERC4337Test:test_WhenEncodingAnInvalidValidatorModule() (gas: 144)
ERC7579ComplianceTest:test_exec4337() (gas: 98)
Executions4337Test:test_WhenExecutingOnValidTarget() (gas: 201167)
Executions4337Test:test_WhenExecutingSingleOnInvalidTarget() (gas: 38154)
Executions4337Test:test_WhenTryExecutingOnValidTarget() (gas: 66317)
Executions4337Test:test_checkVersion() (gas: 21984)
Executions4337Test:test_foo() (gas: 3127)
ExecutionsModuleTest:test_WhenExecutingOnValidTarget() (gas: 211917)
ExecutionsModuleTest:test_WhenExecutingSingleOnInvalidTarget() (gas: 45375)
ExecutionsModuleTest:test_WhenTryExecutingOnValidTarget() (gas: 76680)
ExecutionsModuleTest:test_checkVersion() (gas: 22013)
ExecutionsModuleTest:test_foo() (gas: 3094)
HookTest:test_WhenExecutingVia4337() (gas: 166)
HookTest:test_WhenExecutingViaModule() (gas: 122)
HookTest:test_WhenHookReverts() (gas: 144)
HookTest:test_WhenInstallingModule() (gas: 188)
LaunchpadBase:test_foo() (gas: 3071)
ModuleManagementTest:test_WhenInstallingModules() (gas: 232018)
ModuleManagementTest:test_checkVersion() (gas: 21973)
ModuleManagementTest:test_foo() (gas: 3094)
Safe7579Test:test_execBatch() (gas: 169250)
Safe7579Test:test_execBatchFromExecutor() (gas: 91544)
Safe7579Test:test_execSingle() (gas: 156580)
Safe7579Test:test_execViaExecutor() (gas: 73709)
Safe7579Test:test_fallback() (gas: 265186)
Safe7579Test:test_foo() (gas: 3071)
192 changes: 192 additions & 0 deletions accounts/safe7579/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
## How Safe7579 works

Safe7579 provides full `ERC4337` and `ERC7579` compliance to Safe accounts by serving as the Safe's `FallbackHandler` and an enabled module. This setup allows Safe accounts to utilize all `ERC7579` modules. A launchpad is developed to facilitate the setup of new safes with Safe7579 using the EntryPoint factory.

## How does the Launchpad work

1. **Creation by Factory:**

- Bundler informs `Entrypoint` to handleUserOps.
- Entrypoint calls `SenderCreator` to call `SafeProxyFactory`
- `SenderCreator` requests safeProxy creation from `SafeProxyFactory` using createProxyWithNonce.
- `SafeProxyFactory` creates a new `SafeProxy` using `create2`.
- `SafeProxy` is created with a singleton address set to `Launchpad` (!)
- `InitHash` is stored in the `SafeProxy` storage

2. **Validation Phase:**

- `Entrypoint` validates user operations in `SafeProxy` via `validateUserOp`.
- `SafeProxy` delegates validation to `Launchpad`.
- `Launchpad` ensures the presence of initHash from phase 1 and calls `Safe7579.launchpadValidators`
- `ValidatorModule` gets installed by `Launchpad`
- `ValidatorModule` validates user operations and returns `packedValidationData`
- `Launchpad` returns packedValidationData to `SafeProxy`, `SafeProxy` returns to `Entrypoint`

3. **Execution Phase:**
- `Entrypoint` triggers `launchpad.setupSafe()` in `SafeProxy`
- `SafeProxy` delegates the setup to `Launchpad`
- `LaunchPad` upgradres `SafeStorage.singleton` to `SafeSingleton`
- `LaunchPad` calls `SafeProxy.setup()` to initialize `SafeSingleton`
- Setup function in `SafeProxy.setup()` delegatecalls to `lauchpad.initSafe7579`
- `initSafe7579()` initilazies `Safe7579` with executors, fallbacks, hooks, `IERC7484` registry

This detailed sequence outlines the creation, validation, and execution phases in the system's operation.

```mermaid
sequenceDiagram
participant Bundler
participant Entrypoint
participant SenderCreator
participant SafeProxyFactory
participant SafeProxy
participant SafeSingleton
participant Launchpad
participant Safe7579
participant Registry
participant EventEmitter
participant ValidatorModule
participant Executor
alt Creation by Factory
Bundler->>Entrypoint: handleUserOps
Entrypoint->>SenderCreator: create this initcode
SenderCreator->>+SafeProxyFactory: createProxyWithNonce(launchpad, intializer, salt)
SafeProxyFactory-->>SafeProxy: create2
SafeProxy-->Launchpad: singleton = launchpad
SafeProxyFactory->>+SafeProxy: preValidationSetup (initHash, to, preInit)
SafeProxy-->>+Launchpad: preValidationSetup (initHash, to, preInit) [delegatecall]
Note over Launchpad: sstore initHash
SafeProxy-->>SafeProxyFactory: created
SafeProxyFactory-->>Entrypoint: created sender
end
alt Validation Phase
Entrypoint->>+SafeProxy: validateUserOp
SafeProxy-->>Launchpad: validateUserOp [delegatecall]
Note right of Launchpad: only initializeThenUserOp.selector
Note over Launchpad: require inithash (sload)
Launchpad->>Safe7579: launchpadValidators() [call]
Note over Safe7579: write validator(s) to storage
loop
Launchpad ->> ValidatorModule: onInstall()
Note over Launchpad: emit ModuleInstalled (as SafeProxy)
end
Note over Launchpad: get validator module selection from userOp.nonce
Launchpad ->> ValidatorModule: validateUserOp(userOp, userOpHash)
ValidatorModule ->> Launchpad: packedValidationData
Launchpad-->>SafeProxy: packedValidationData
SafeProxy->>-Entrypoint: packedValidationData
end
alt Execution Phase
Entrypoint->>+SafeProxy: setupSafe
SafeProxy-->>Launchpad: setupSafe [delegatecall]
Note over SafeProxy, Launchpad: sstore safe.singleton == SafeSingleton
Launchpad->>SafeProxy: safe.setup() [call]
SafeProxy->>SafeSingleton: safe.setup() [delegatecall]
Note over SafeSingleton: setup function in Safe has a delegatecall
SafeSingleton-->>Launchpad: initSafe7579WithRegistry [delegatecall]
Launchpad->>SafeProxy: this.enableModule(safe7579)
SafeProxy-->>SafeSingleton: enableModule (safe7579) [delegatecall]
SafeSingleton->>Safe7579: initializeAccountWithRegistry
Note over Safe7579: msg.sender: SafeProxy
alt SetupRegistry
Safe7579-->SafeProxy: exec set attesters on registry
SafeProxy-->>SafeSingleton: exec set attesters on registry
SafeSingleton->>Registry: set attesters (attesters[], threshold)
end
loop installation of modules
Safe7579->>Registry: checkForAccount(SafeProxy, moduleaddr, moduleType)
Safe7579->>SafeProxy: exec call onInstall on module
SafeProxy-->>SafeSingleton: exec call onInstall on Module [delegatecall]
SafeSingleton->>Executor: onInstall() [call]
Safe7579->>SafeProxy: exec EventEmitter
SafeProxy-->>SafeSingleton: exec EventEmitter [delegatecall]
SafeSingleton-->>EventEmitter: emit ModuleInstalled() [delegatecall]
Note over EventEmitter: emit ModuleInstalled() as SafeProxy
end
Safe7579->>SafeProxy: exec done
SafeProxy->-Entrypoint: exec done
end
```

Special thanks to [@nlordell (Safe)](https://github.com/nlordell), who came up with [this technique](https://github.com/safe-global/safe-modules/pull/184)

## How do validations and executions work

In order to call module logic or interact with external contracts, all calls have to be routed over the SafeProxy.
The Safe7579 adapter is an enabled Safe module on the Safe Account, and makes use for `execTransactionFromModule`, to call into external contracts as the Safe Account.

In order to select validator modules, the address of the validator module can be encoded in the userOp.nonce key. If an validator module that was not previously installed, or validator module with address(0) be selected, the Safe's `checkSignature` is used as a fallback.

```mermaid
sequenceDiagram
participant Bundler
participant Entrypoint
participant SafeProxy
participant SafeSingleton
participant Safe7579
participant ValidatorModule
participant ERC20
alt validation with Validator Module
Bundler->>Entrypoint: handleUserOps
Entrypoint->>SafeProxy: validateUserOp()
SafeProxy-->>SafeSingleton: validateUserOp [delegatecall]
Note over SafeSingleton: select Safe7579 as fallback handler
SafeSingleton->>Safe7579: validateUserOp
Note over Safe7579: validator module selection
alt Use Validator Module
Safe7579->>SafeProxy: execTransactionFromModule: call validation module
SafeProxy-->>SafeSingleton: execTransactionFromModule: call validation module [delegatecall]
SafeSingleton->>ValidatorModule: validateUserOp
end
alt Use Safe signatures
Safe7579->>SafeProxy: checkSignatures
SafeProxy-->>SafeSingleton: checkSignatures [delegatecall]
end
end
alt Execution
Bundler->>Entrypoint: handleUserOps
Entrypoint->>SafeProxy: execute()
SafeProxy-->>SafeSingleton: execute [delegatecall]
Note over SafeSingleton: select Safe7579 as fallback handler
SafeSingleton->>Safe7579: execute()
Safe7579->>SafeProxy: execTransactionFromModule: call ERC20.transfer [delegatecall]
SafeProxy-->>SafeSingleton: call ERC20.transfer
SafeSingleton->>ERC20: transfer()
end
```

## Batched Executions

Safe Account's `execTransactionFromModule` do not natively offer the ability to batch multiple calls into a single transaction. Yet one of the core feature of ERC7579 is the ability to validate and make batched executions.
To save Gas, instead of calling `execTransactionFromModule` n times, we are making use of a special multicall contract, that will be delegatecalled by the Safe account.
The same contract is also used, to emit events for onInstall / onUninstall of modules.

## Installation of Modules

## Authors / Credits✨

Thanks to the following people who have contributed to this project:

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://twitter.com/zeroknotsETH/"><img src="https://pbs.twimg.com/profile_images/1639062011387715590/bNmZ5Gpf_400x400.jpg" width="100px;" alt=""/><br /><sub><b>zeroknots (rhinestone)</b></sub></a><br /><a href="https://github.com/zeroknots" title="Code">💻</a></td>

<td align="center"><a href="https://twitter.com/abstractooor"><img src="https://avatars.githubusercontent.com/u/26718079" width="100px;" alt=""/><br /><sub><b>Konrad (rhinestone)</b></sub></a><br /><a href="https://github.com/kopy-kat" title="Spec">📝</a> </td>

<td align="center"><a href="https://twitter.com/NLordello"><img src="https://avatars.githubusercontent.com/u/4210206" width="100px;" alt=""/><br /><sub><b>Nicholas Rodrigues Lordello (Safe)
</b></sub></a><br /><a href="https://github.com/ nlordell" title="Review / Launchpad Idea">📝</a> </td>

</tr>
</table>

Special Thanks to the Safe Team for their support and guidance in the development of Safe7579.
2 changes: 1 addition & 1 deletion accounts/safe7579/foundry.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
libs = ["node_modules"]
fs_permissions = [{ access = "read", path = "out-optimized" }]
allow_paths = ["*", "/"]

Expand Down
18 changes: 9 additions & 9 deletions accounts/safe7579/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,23 @@
"url": "https://github.com/rhinestonewtf/modulekit/issues"
},
"devDependencies": {
"@rhinestone/modulekit": "workspace:*",
"@rhinestone/sessionkeymanager": "workspace:*",
"@safe-global/safe-contracts": "^1.4.1",
"@openzeppelin/contracts": "5.0.1",
"@ERC4337/account-abstraction": "github:kopy-kat/account-abstraction#develop",
"@ERC4337/account-abstraction-v0.6": "github:eth-infinitism/account-abstraction#v0.6.0",
"erc4337-validation": "github:rhinestonewtf/erc4337-validation",
"@openzeppelin/contracts": "5.0.1",
"@prb/math": "^4.0.2",
"forge-std": "github:foundry-rs/forge-std",
"@rhinestone/modulekit": "workspace:*",
"@rhinestone/sessionkeymanager": "workspace:*",
"@safe-global/safe-contracts": "^1.4.1",
"ds-test": "github:dapphub/ds-test",
"erc4337-validation": "github:rhinestonewtf/erc4337-validation",
"erc7579": "github:erc7579/erc7579-implementation",
"forge-std": "github:foundry-rs/forge-std",
"prettier": "^2.8.8",
"sentinellist": "github:zeroknots/sentinellist",
"solady": "github:vectorized/solady",
"solarray": "github:sablier-labs/solarray",
"solmate": "github:transmissions11/solmate",
"solhint": "^4.1.1",
"prettier": "^2.8.8"
"solhint": "^4.5.4",
"solmate": "github:transmissions11/solmate"
},
"files": [
"artifacts",
Expand Down
4 changes: 3 additions & 1 deletion accounts/safe7579/remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ account-abstraction-v0.6/=node_modules/@ERC4337/account-abstraction-v0.6/contrac
erc7579/=node_modules/erc7579/src/
sentinellist/=node_modules/sentinellist/src/
solmate/=node_modules/solmate/src/
solady/=node_modules/solady/
solady/=node_modules/solady/src/
solarray/=node_modules/solarray/src/
@rhinestone/modulekit/=node_modules/@rhinestone/modulekit/packages/modulekit/
@rhinestone/sessionkeymanager/=node_modules/@rhinestone/modulekit/packages/sessionkeymanager/
@rhinestone/=node_modules/@rhinestone/
@safe-global/=node_modules/@safe-global/
@ERC4337/=node_modules/@ERC4337/
Expand Down
1 change: 0 additions & 1 deletion accounts/safe7579/script/Deploy.s.sol

This file was deleted.

26 changes: 26 additions & 0 deletions accounts/safe7579/src/DataTypes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.25;

import { IERC7484 } from "./interfaces/IERC7484.sol";
import { CallType } from "./lib/ModeLib.sol";

struct FallbackHandler {
address handler;
CallType calltype;
}

enum HookType {
GLOBAL,
SIG
}

struct ModuleInit {
address module;
bytes initData;
}

struct RegistryInit {
IERC7484 registry;
address[] attesters;
uint8 threshold;
}
Loading

0 comments on commit 7066672

Please sign in to comment.