This project is a fork of the original pimlicolabs/alto repository. We would like to acknowledge and thank the original creators for their work and contribution. This fork has been modified to enable hosting on Railway with default settings configured for Arbitrum.
- Introduction
- Getting Started
- Features
- Configuration
- Alto Help
- Security Considerations
- UserOperation Mempool
- Acknowledgements
- License
In ERC-4337, a Bundler is the core infrastructure component that allows account abstraction to work on any EVM network. On the highest level, its purpose is to work with a mempool of User Operations to get the transaction to be included on-chain.
This project allows you to deploy the ALTO bundler application on Railway with support for Arbitrum. ALTO is a platform designed to provide decentralized orchestration for blockchain-based tasks, specifically functioning as a Bundler for ERC-4337.
Follow these steps to get the project up and running:
- Fork the Repository: Fork this repository to your own GitHub account.
- Clone the Repository: Clone the forked repository to your local machine.
git clone https://github.com/syphrpunk/alto.git
- Install Dependencies: Navigate to the project directory and install the required dependencies.
cd alto pnpm install
- Deploy to Railway: Follow the Railway Documentation to deploy the project. Or 1-click deploy
- Decentralized task orchestration
- Default settings configured for Arbitrum
- Easy deployment on Railway
By default, this project is configured to work with Arbitrum. To modify the configuration, edit the necessary environment variables and settings in the railway.toml
(and environment variables directly on railway).
You can edit the commands in railway.toml to include any of the below additional parameters. Below are the commands available in the ALTO CLI, obtained by running ./alto help
:
π Alto: TypeScript ERC-4337 Bundler.
* by Pimlico, 2024
./alto help
π Alto: TypeScript ERC-4337 Bundler.
* by Pimlico, 2024
Options:
-e, --entrypoints EntryPoint contract addresses split by commas [string] [required]
-c, --entrypoint-simulation-contract Address of the EntryPoint simulations contract [string]
-x, --executor-private-keys Private keys of the executor accounts split by commas [string] [required]
-u, --utility-private-key Private key of the utility account [string]
--max-executors Maximum number of executor accounts to use from the list of executor private keys [number]
--min-executor-balance Minimum balance required for each executor account (below which the utility account will refill) [string]
--executor-refill-interval Interval to refill the signer balance (seconds) [number] [required] [default: 1200]
--min-entity-stake Minimum stake required for a relay (in 10e18) [number] [required] [default: 1]
--min-entity-unstake-delay Minimum unstake delay (seconds) [number] [required] [default: 1]
--max-bundle-wait Maximum time to wait for a bundle to be submitted (ms) [number] [required] [default: 1000]
--max-bundle-size Maximum number of operations allowed in the mempool before a bundle is submitted [number] [required] [default: 10]
--safe-mode Enable safe mode (enforcing all ERC-4337 rules) [boolean] [required] [default: true]
--gas-price-bump Amount to multiply the gas prices fetched from the node [string] [default: "100"]
--gas-price-floor-percent The minimum percentage of incoming user operation gas prices compared to the gas price used by the bundler to submit bundles [number] [required] [default: 101]
--gas-price-expiry Maximum that the gas prices fetched using pimlico_getUserOperationGasPrice will be accepted for (seconds) [number] [default: 10]
--gas-price-multipliers Amount to multiply the gas prices fetched using pimlico_getUserOperationGasPrice (format: slow,standard,fast) [string] [default: "105,110,115"]
--mempool-max-parallel-ops Maximum amount of parallel user ops to keep in the meempool (same sender, different nonce keys) [number] [default: 10]
--mempool-max-queued-ops Maximum amount of sequential user ops to keep in the mempool (same sender and nonce key, different nonce values) [number] [default: 0]
--enforce-unique-senders-per-bundle Include user ops with the same sender in the single bundle [boolean] [default: true]
--max-gas-per-bundle Maximum amount of gas per bundle [string] [default: "5000000"]
--config Path to JSON config file
-h, --help Show help [boolean]
-v, --version Show version number [boolean]
Compatibility Options:
--chain-type Indicates weather the chain is a OP stack chain, arbitrum chain, or default EVM chain [string] [choices: "default", "op-stack", "arbitrum"] [default: "default"]
--legacy-transactions Send a legacy transactions instead of an EIP-1559 transactions [boolean] [required] [default: false]
--balance-override Override the sender native token balance during estimation [boolean] [required] [default: true]
--local-gas-limit-calculation Calculate the bundle transaction gas limits locally instead of using the RPC gas limit estimation [boolean] [required] [default: false]
--flush-stuck-transactions-during-startup Flush stuck transactions with old nonces during bundler startup [boolean] [required] [default: false]
--fixed-gas-limit-for-estimation Use a fixed value for gas limits during bundle transaction gas limit estimations [string]
--api-version API version (used for internal Pimlico versioning compatibility) [string] [required] [default: "v1,v2"]
--default-api-version Default API version [string] [default: "v1"]
--paymaster-gas-limit-multiplier Amount to multiply the paymaster gas limits fetched from simulations [string] [required] [default: "110"]
Server Options:
--port Port to listen on [number] [required] [default: 3000]
--timeout Timeout for incoming requests (in ms) [number]
--websocket-max-payload-size Maximum payload size for websocket messages in bytes (default to 1MB) [number]
--websocket Enable websocket server [boolean]
RPC Options:
-r, --rpc-url RPC url to connect to [string] [required]
--send-transaction-rpc-url RPC url to send transactions to (e.g. flashbots relay) [string]
--polling-interval Polling interval for querying for new blocks (ms) [number] [required] [default: 1000]
--max-block-range Max block range for getLogs calls [number]
--block-tag-support-disabled Disable sending block tag when sending eth_estimateGas call [boolean] [default: false]
Bundle Compression Options:
--bundle-bulker-address Address of the BundleBulker contract [string]
--per-op-inflator-address Address of the PerOpInflator contract [string]
Logging Options:
--json Log in JSON format [boolean] [required] [default: false]
--network-name Name of the network (used for metrics) [string] [required] [default: "localhost"]
--log-level Default log level [string] [required] [choices: "trace", "debug", "info", "warn", "error", "fatal"] [default: "info"]
--public-client-log-level Log level for the publicClient module [string] [choices: "trace", "debug", "info", "warn", "error", "fatal"]
--wallet-client-log-level Log level for the walletClient module [string] [choices: "trace", "debug", "info", "warn", "error", "fatal"]
--rpc-log-level Log level for the rpc module [string] [choices: "trace", "debug", "info", "warn", "error", "fatal"]
--mempool-log-level Log level for the mempool module [string] [choices: "trace", "debug", "info", "warn", "error", "fatal"]
--executor-log-level Log level for the executor module [string] [choices: "trace", "debug", "info", "warn", "error", "fatal"]
--reputation-manager-log-level Log level for the executor module [string] [choices: "trace", "debug", "info", "warn", "error", "fatal"]
--nonce-queuer-log-level Log level for the executor module [string] [choices: "trace", "debug", "info", "warn", "error", "fatal"]
Debug Options:
--bundle-mode Set if the bundler bundle user operations automatically or only when calling debug_bundler_sendBundleNow. [string] [required] [choices: "auto", "manual"] [default: "auto"]
--enable-debug-endpoints Enable debug endpoints [boolean] [required] [default: false]
--expiration-check Should the node make expiration checks [boolean] [required] [default: true]
--dangerous-skip-user-operation-validation Skip user operation validation, use with caution [boolean] [required] [default: false]
--deploy-simulations-contract Should the bundler deploy the simulations contract on startup [boolean] [required] [default: false]
--tenderly RPC url follows the tenderly format [boolean] [required] [default: false]
π For more information, check the our docs:
* https://docs.pimlico.io/
When reading the EIP specs, you'll notice that there are many rules a bundler must follow. Although the list of rules may seem long and complex, each one has been extensively debated and discussed by security researchers and builders within the Ethereum ecosystem.
One of the bundler's main jobs is to comply with these rules to prevent all possible DoS attack vectors. These include everything from basic sanity checks that make sure a User Operation is structurally sound to more in-depth tracing for banned opcodes and storage access to make sure bundles cannot be censored once submitted to the network.
Similar to Ethereum clients, all bundler implementations are expected to pass a test suite to ensure compliance and that it won't fragment the mempool.
Spec Tests: https://github.com/eth-infinitism/bundler-spec-tests
π‘ Stackup's bundler currently maintains 100% coverage of the test suite.
Although the spec is still a work in progress, all future iterations will strive to maintain full compliance coverage.
The canonical mempool for EIP-4337 is decentralized and is made up of a permissionless P2P network of independent bundlers. To maintain this requirement, it doesn't make any assumptions about which contracts are okay and which are not. All contracts must follow the same rules during validation.
However, there will be cases where some contracts are audited and proven to be safe even though they break some of the rules set by the canonical mempool. In this case, a group of bundlers can create alternative mempools for such exceptions. A common example of when this might be needed is in the case of a Deposit Paymaster that can abstract gas fees with any ERC-20 token.
Another role of the bundler is to maintain a connection to the canonical mempool and also any other alternative mempools it opts into. To read more about this topic, we highly recommend checking out this article.
When a bundler receives a UserOperation, it must first run some basic sanity checks, namely that:
- Either the sender is an existing contract, or the
initCode
is not empty (but not both). - If
initCode
is not empty, parse its first 20 bytes as a factory address. Record whether the factory is staked, in case the later simulation indicates that it needs to be. If the factory accesses global state, it must be staked. - The
verificationGasLimit
is sufficiently low (<=MAX_VERIFICATION_GAS
) and thepreVerificationGas
is sufficiently high (enough to pay for the calldata gas cost of serializing the UserOperation plusPRE_VERIFICATION_OVERHEAD_GAS
). - The
paymasterAndData
is either empty or starts with the paymaster address. The paymaster must (i) currently have nonempty code on chain, (ii) have a sufficient deposit to pay for the UserOperation, and (iii) not be currently banned. - The
callGas
is at least the cost of a CALL with non-zero value. - The
maxFeePerGas
andmaxPriorityFeePerGas
are above a configurable minimum value that the bundler is willing to accept. At the minimum, they are sufficiently high to be included with the current blockbasefee
. - The sender doesn't have another UserOperation already present in the pool (or it replaces an existing entry with the same sender and nonce, with higher
maxPriorityFeePerGas
and an equally increasedmaxFeePerGas
).
If the UserOperation object passes these sanity checks, the bundler must next run the first op simulation, and if the simulation succeeds, the bundler must add the op to the pool. A second simulation must also happen during bundling to make sure the UserOperation is still valid.
In order to add a UserOperation into the UserOp mempool (and later to add it into a bundle), we need to "simulate" its validation to make sure it is valid, and that it is capable of paying for its own execution. In addition, we need to verify that the same will hold true when executed on-chain. For this purpose, a UserOperation is not allowed to access any information that might change between simulation and execution, such as current block time, number, hash, etc.
A UserOperation is only allowed to access data related to this sender address: Multiple UserOperations should not access the same storage, so that it is impossible to invalidate a large number of UserOperations with a single state change.
There are three special contracts that interact with the account: the factory (initCode
) that deploys the contract, the paymaster that can pay for the gas, and the signature aggregator. Each of these contracts is also restricted in its storage access, to make sure UserOperation validations are isolated.
Special thanks to the original creators of the pimlico/alto project. This fork wouldn't be possible without their foundational work.
The ERC-4337 Team and the Ethereum community for their continued support and guidance in the development of the EIP-4337 specification.
This project is licensed under the GNU GENERAL PUBLIC LICENSE. See the LICENSE file for details.