@safe-global/safe-4337
TypeScript icon, indicating that this package has built-in type declarations

0.3.0 • Public • Published

Safe Module/Fallback handler for ERC-4337 Support

⚠️ This module MUST only be used with Safe 1.4.1 or newer ⚠️

For the supported EntryPoint version, please refer to the CHANGELOG. The module was tested and audited with the EntryPoint version mentioned in the changelog in mind; if you use a different version, you do so at your own risk.

Execution Flow

The diagram below outlines the flow triggered when a user operation is submitted to the entrypoint. Additionally, the gas overhead compared to a native implementation is mentioned.

sequenceDiagram
    actor B as Bundler
    participant E as Entry Point
    participant P as Safe Proxy
    participant S as Safe Singleton
    participant M as Safe 4337 Module
    actor T as Target

    B->>+E: Submit User Operations
    E->>+P: Validate User Operation
    P-->>S: Load Safe logic
    Note over P, M: Gas overhead for calls and storage access
    P->>+M: Forward validation
    Note over P, M: Load fallback handler ~2100 gas<br>Intital module access ~2600 gas
    M->>P: Check signatures
    P-->>S: Load Safe logic
    Note over P, M: Call to Safe Proxy ~100 gas<br>Load logic ~100 gas
    opt Pay required fee
        M->>P: Trigger fee payment
        P-->>S: Load Safe logic
        Note over P, M: Module check ~2100 gas<br>Call to Safe Proxy ~100 gas<br>Load logic ~100 gas
        P->>E: Perform fee payment
    end
    M-->>-P: Validation response
    P-->>-E: Validation response
    Note over P, M: Total gas overhead<br>Without fee payment ~4.900 gas<br>With fee payment ~7.200 gas

    E->>+P: Execute User Operation
    P-->>S: Load Safe logic
    P->>+M: Forward execution
    Note over P, M: Call to Safe Proxy ~100 gas<br>Call to fallback handler ~100 gas
    M->>P: Execute From Module
    P-->>S: Load Safe logic
    Note over P, M: Call to Safe Proxy ~100 gas<br>Module check ~100 gas
    P->>+T: Perform transaction
    opt Bubble up return data
        T-->>-P: Call Return Data
        P-->>M: Call Return Data
        M-->>-P: Call return data
        P-->>-E: Call return data
    end

The gas overhead is based on EIP-2929. It is possible to reduce the gas overhead by using access lists.

Note: The gas overhead is only the baseline using storage access and call costs. The actual gas overhead is higher as additional costs occur to handle the storage access and calls. The tests indicate an overhead of ~12.1k gas when a fee payment is required and ~10.3k gas otherwise. This shows that there is still room for optimization in the contracts used in the tests.

Setup Flow

If the Account that the Entry Point should interact with is not deployed yet, it is necessary to provide the required initialization data (initCode) to deploy it.

Important to note that ERC-4337 defined the initCode as the following:

The initCode field (if non-zero length) is parsed as a 20-byte address, followed by "calldata" to pass to this address.

To deploy a Safe with 4337 directly enabled, we require a setup contract that enables multiple modules (SafeModuleSetup). This is necessary because to enable a Module, the Safe has to do a call to itself. Before calling the setup method, we do not know the address of the Safe yet (as the address depends on the setup parameters) and using MultiSend will not result in the correct msg.sender for a selfcall.

The initCode for the Safe with a 4337 module enabled is composed in the following way:

/** Setup Safe **/
address[] memory modules = new address[](1);
{
    modules[0] = SAFE_4337_MODULE_ADDRESS;
}
bytes memory initializer = abi.encodeWithSignature(
    "setup(address[],uint256,address,bytes,address,address,uint256,address)",
    owners,
    threshold,
    SAFE_MODULE_SETUP_ADDRESS,
    abi.encodeWithSignature("enableModules(address[])", modules),
    SAFE_4337_MODULE_ADDRESS,
    // We do not want to use any refund logic; therefore, this is all set to 0
    address(0),
    0,
    address(0)
);

/** Deploy Proxy **/
bytes memory initCallData = abi.encodeWithSignature(
    "createProxyWithNonce(address,bytes,uint256)",
    SAFE_SINGLETON_ADDRESS,
    initializer,
    saltNonce
);

/** Encode for 4337 **/
bytes memory initCode = abi.encodePacked(SAFE_PROXY_FACTORY_ADDRESS, initCallData);

The diagram below outlines the flow triggered by the initialization data to deploy a Safe with the 4337 module enabled.

sequenceDiagram
    actor B as Bundler
    participant E as Entry Point
    participant F as Safe Proxy Factory
    participant P as Safe Proxy
    participant L as SafeModuleSetup
    B->>+E: Submit User Operations with `initCode`
    E->>+F: Deploy Proxy with `deployData`
    F->>+P: Create Proxy
    F->>P: Setup Proxy with `setupData` and 4337 module as a fallback handler
    P->>+L: Execute a delegatecall
    L->>P: Enable 4337 module
    deactivate L
    deactivate P
    F->>-E: Return Account Address

Signature encoding

The signatures are encoded as follows:

function encodeSignatures(uint48 validUntil, uint48 validAfter, bytes signatures) {
  return abi.encodePacked(validUntil, validAfter, signatures);
}

where validUntil and validAfter are two 48 bit timestamps for signature validity and signatures is a concatenated signatures bytes following the Safe encoding, defined here.

Usage

Install Requirements With NPM:

npm install

Run Hardhat Integration Tests:

npm test

Run End-to-end Tests:

End-to-end tests with the reference bundler implementation are also provided. These automated tests verify that no user operation validation rules are broken by the implementation. Docker is required to run these tests.

Note: Geth is used as the RPC because the reference bundler implementation relies on Geth-specific tracing APIs.

npm run test:e2e

Run Hardhat Integration and End-to-end Tests:

npm run test:all

Deployments

A collection of the different Safe 4337 modules deployments and their addresses can be found in the Safe module deployments repository.

To add support for a new network follow the steps of the Deploy section and create a PR in the Safe module deployments repository.

Deploy

⚠️ Make sure to use the correct commit when deploying the contracts. Any change (even comments) within the contract files will result in different addresses. The tagged versions used by the Safe team can be found in the releases.

This will deploy the contracts deterministically and verify the contracts on etherscan and sourcify.

Preparation:

  • Set MNEMONIC in .env
  • Set INFURA_KEY in .env
npm run deploy-all <network>

This will perform the following steps

npm run build
npx hardhat --network <network> deploy
npx hardhat --network <network> etherscan-verify
npx hardhat --network <network> local-verify

Run script

Preparation:

  • Set DEPLOYMENT_ENTRY_POINT_ADDRESS in .env. This should be the entry point supported by the 4337 bundler RPC endpoint you are connected to.
  • Deploy contracts (see Deploy section)
  • Set SCRIPT_* in .env
npx hardhat run scripts/runOp.ts --network goerli

Compiler settings

The project uses Solidity compiler version 0.8.21 with 10 million optimizer runs, as we want to optimize for the code execution costs. The EVM version is set to paris because not all our target networks support the opcodes introduced in the Shanghai EVM upgrade.

After careful consideration, we decided to enable the optimizer for the following reasons:

  • The most critical functionality, such as signature checks and replay protection, is handled by the Safe and Entrypoint contracts.
  • The entrypoint contract uses the optimizer.

Custom Networks

It is possible to use the NODE_URL env var to connect to any EVM-based network via an RPC endpoint. This connection can then be used with the custom network.

E.g. to deploy the Safe contract suite on that network, you would run yarn deploy-all custom.

The resulting addresses should be on all networks the same.

Note: The address will vary if the contract code changes or a different Solidity version is used.

Verify contract

This command will use the deployment artifacts to compile the contracts and compare them to the onchain code.

npx hardhat --network <network> local-verify

This command will upload the contract source to Etherscan.

npx hardhat --network <network> etherscan-verify

Documentation

Audits

Security and Liability

All contracts are WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

License

All smart contracts are released under LGPL-3.0.

Package Sidebar

Install

npm i @safe-global/safe-4337

Weekly Downloads

7

Version

0.3.0

License

GPL-3.0

Unpacked Size

13 MB

Total Files

714

Last publish

Collaborators

  • nlordell
  • remedcu
  • rmeissner
  • mikheevm