An ERC-7579 adapter for Safe accounts
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.
The Safe7579 adapter can be added to existing Safe accounts, enabling them to interact with ERC7579
modules. The adapter is installed as a module on the Safe account, allowing the Safe to interact with external contracts and modules.
Furthermore, this repository includes a launchpad that allows the creation of new Safe accounts with Safe7579. The launchpad is a factory that creates Safe accounts with Safe7579 and initializes them with the required modules.
Safe7579 allows the installation of validator modules to validate user operations. Validator modules can be selected by encoding the module address in the userOp.nonce key. If a validator module is not installed, the Safe's checkSignature
is used as a fallback.
Safe7579 allows the installation of fallback modules to handle user operations when no validator module is selected. Fallback modules can be selected by encoding the module address in the userOp.nonce key. If a fallback module is not installed, the Safe's checkSignature
is used as a fallback.
Safe7579 supports the usage of the IERC7579Hooks
interface to allow modules to hook into the 7579 execution flow.
To enable flexiblity, Safe7579 implements global an function selector specific hooks. global hooks always run, selector hooks only run if specific function is called.
-
Creation by Factory:
- Bundler informs
Entrypoint
to handleUserOps. - Entrypoint calls
SenderCreator
to callSafeProxyFactory
SenderCreator
requests safeProxy creation fromSafeProxyFactory
using createProxyWithNonce.SafeProxyFactory
creates a newSafeProxy
usingcreate2
.SafeProxy
is created with a singleton address set toLaunchpad
(!)InitHash
is stored in theSafeProxy
storage
- Bundler informs
-
Validation Phase:
Entrypoint
validates user operations inSafeProxy
viavalidateUserOp
.SafeProxy
delegates validation toLaunchpad
.Launchpad
ensures the presence of initHash from phase 1 and callsSafe7579.launchpadValidators
ValidatorModule
gets installed byLaunchpad
ValidatorModule
validates user operations and returnspackedValidationData
Launchpad
returns packedValidationData toSafeProxy
,SafeProxy
returns toEntrypoint
-
Execution Phase:
Entrypoint
triggerslaunchpad.setupSafe()
inSafeProxy
SafeProxy
delegates the setup toLaunchpad
LaunchPad
upgradresSafeStorage.singleton
toSafeSingleton
LaunchPad
callsSafeProxy.setup()
to initializeSafeSingleton
- Setup function in
SafeProxy.setup()
delegatecalls tolauchpad.initSafe7579
initSafe7579()
initilaziesSafe7579
with executors, fallbacks, hooks,IERC7484
registry
This detailed sequence outlines the creation, validation, and execution phases in the system's operation.
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), who came up with this technique
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.
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
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.
Thanks to the following people who have contributed to this project:
zeroknots (rhinestone) 💻 |
Konrad (rhinestone) 📝 |
Nicholas Rodrigues Lordello (Safe) 📝 |
Special Thanks to the Safe Team for their support and guidance in the development of Safe7579.