Catapultar is an optimised smart account primarily intended to be used as a batch executor. It consists of a base template — Catapultar.sol — and a proxy factory — CatapultarFactory.sol — to aid with the deployment of various versions of proxies.
It is based on Solady's ERC7821.sol for efficient portable batch execution.
A smart contract account that can be used to scale a transaction dispatch environment without relying on nonce spamming while still working as a minimal SCA for an end user. It should provide durable double spend protection ensuring dispatched transactions are not nefariously nor accidentally executed twice.
To scale a transaction dispatch environment, execution mode 0x01010000000078210001 can be used. It is a once callable, revert ignoring batch call. It allows a set of transactions to be executed in single call with no call blocking others.
To provide durable double spend protections, execution mode 0x01000000000078210001 can be used. It is a once executable, revert raising batch call. It allows a set of transaction to be executed conditionally.
Both execution modes can be combined with an outer signed 0x01010000000078210001 calling itself allowing for a one time callable batch with inner unsigned 0x01000000000078210001 allowing for safe re-tryable transactions. A transaction dispatch service can maintain a list of 0x01000000000078210001s. Once the transaction executor is available, all outstanding 0x01000000000078210001s can be executed through a single 0x01010000000078210001.
Smart accounts are built for efficiency; To be used on Ethereum gas costs have to be kept minimal. As a result, feature space of the account is limited. Below a table comparing popular smart accounts can be found:
| Feature | Catapultar | Ithaca account (Porto) | Biconomy Nexus | Zerodev Kernel |
|---|---|---|---|---|
| Multiple Keys | 1 | Yes | Yes (K1Validator) | With Module |
| Multiple Signatures | No | No | With Module | With Module |
| Call Batching | ERC-7821* | ERC-7821 | ERC-7821 | ERC-7821 |
| Call Batching (Ignore failures) | Yes | No | No | With Module |
| Nonces | Permit2 style | 4337 style | ERC-4337 | ERC-4337 |
| Embed action on deploy | Yes | Yes-ish | With Module | With Module |
| Supports EIP-7702 | No | Yes | Yes | Yes |
| Requires EIP-7702 | No | Yes | No | No |
| Full Passkey Support | Yes | Yes | With Module | With Module |
| Solady LibZip | Yes | No | No | With Module |
| Account Deploy | Factory | EIP-7702 Delegate | Factory | Factory |
| Permissionless chain deploy | Yes | Yes | Yes | Yes |
| Account Init | ~110k | EIP-7702 Delegate | More expensive | More expensive |
| Modular (ERC-7579) | No | No | Yes | Yes |
Catapular currently does not support EIP-7702, even though it would provide significant advantages. Using something like PREP or briefly generating a private key to initialize an account would substantially reduce account creation costs. However, this would require core changes to the codebase.
In general, there are two main approaches to implementing EIP-7702 support for smart account creation:
-
Disposed Private Key: Generate a private key that is immediately disposed of after signing an EIP-7702 authorization. This approach allows signing more than just the authorization—for example, an initial (embedded) transaction—without requiring user input. Additionally, the address can be determined before generating a passkey, allowing the passkey to be named according to the address.
-
Provably Rootless EIP-7702 Proxy: Create an EIP-7702 authorization and set the signature as the account initialization data. Most well-formed random signatures are valid for a corresponding account. This EIP-7702 authorization signature for a random account acts as a proxy for a specific implementation. Furthermore, the initialization call can be enforced by validating the signature on-chain. This results in significantly lower account deployment costs and a provably secure technique. Unfortunately, EIP-7702 is not yet supported on most networks.
- To simulate dual mode transaction, mode
0x01000000000078210001transactions can be submitted to the relevant proxy using the context ofmsg.sender === proxy. - Catapultar contains no gas controls. If dual mode transactions are used, gas controls should be handled off-chain. Gas-spending untrusted contracts should be executed individually.
- Catapultar contains no calldata manipulation. Injection of
erc20::balanceOf()or similar manipulations should be on external contracts. - Catapultar does not support external delegate calls. Delegate calls are dangourus, particularly for upgradeable contracts. They can change the owner of Catapultar but also the implementation of a proxy (ERC-1967).
- Batch Execution Modes:
- Conditional batch: All transactions succeed or all fail. Nonce is only spent on success.
- Individual batch: Each transaction in the batch is executed independently; failures do not block others. Nonce is always spent.
- Nested batches: Mix conditional and individual batches for complex workflows.
- Signature Validation:
- Supports ECDSA and ERC-1271 signatures.
- Implements replay protection: signatures are valid only for a specific account instance.
- Proxy Deployment Strategies:
- Minimal proxy (low cost, non-upgradeable).
- Upgradeable ERC-1967 proxy (ownership handover, upgradable logic).
Use the CatapultarFactory contract to deploy Catapultar proxies:
-
Minimal Proxy:
factory.deploy(owner, salt);Deploys a minimal proxy for batch execution.
-
Upgradeable Proxy:
factory.deployUpgradeable(owner, salt);Deploys an ERC1967 upgradeable proxy. Ownership can be transferred and logic upgraded.
For all deployments, the first 20 bytes of salt should be the owner address or zero. Use the predictDeploy* functions to precompute addresses before deployment.
| Feature | Minimal Proxy | Upgradeable Proxy |
|---|---|---|
| Upgradable | No | Yes |
| Ownership Transfer | Yes | Yes |
| Gas Cost | Lowest | Higher |
Catapultar is not ERC-7821 compatible but it follows ERC-7821 specification. It supports the following execution modes:
| Execution Mode | 0x0100....78210001 | 0x0101....78210001 | 0x0100....78210002 |
|---|---|---|---|
| Raise Revert | Yes | No | Yes |
| Consume Nonce on Revert | No | Yes | No |
| Batch of Batches | No | No | Yes |
| OpData Required | Yes | Yes | No |
To execute a transaction batch opData is required for execution. As a result, mode 0x01000000000000000000 is not supported. opData is expected to be formatted in one of two ways:
abi.encodePacked(bytes32(nonce), bytes(signature))whenever the account is called externally.abi.encodePacked(bytes32(nonce))if the account calls itself.
Since 0x01000000000078210002 does not execute a transaction batch but a batch of batches, it does not require opData.
-
0x01000000000078210001: Executing a set of conditional trasactions.
If 1 transaction in a set fails, the entire set should fail. This can allow for retrying the transaction at a later time since the nonce is not spent.
-
0x01010000000078210001: Executing a set of individual transactions.
If 1 or more transactions in a set fails, the remaining transactions in the set should be executed. The nonce is always spent.
-
0x01000000000078210001 inside 0x01010000000078210001: Executing a large set of individual transactions containing conditional transactions.
Each 0x01000000000078210001 batch can be retried in the future if it fails with each 0x01010000000078210001 only being executable once. This allows a batch executor to schedule a set of transaction to be executed. The entire set should be executed individually (0x01010000000078210001) but each sub-batch or transaction needs to be executed conditionally (0x01000000000078210001).
To validate ERC-1271 signatures against the account, the message hash needs to be rehashed for replay protection.
- Hash your payload as usual (e.g., EIP-712).
- Compute the replay-protected hash:
bytes32 replayHash = keccak256(abi.encode( keccak256(bytes("Replay(address account,bytes32 payload)")), address(account), payloadHash ));
- Sign and verify using
::isValidSignature.
Catapultar uses unordered nonces for replay protection. Nonces are stored in a 256 bit index using a 24 byte word: bytes24(word) | bytes8(index). For efficient nonce management, nonces should be spent in each word in its entirety.
Multiple nonces can be invalidated at one time using index masks: ::invalidateUnorderedNonces(word, mask).
Catapultar emits the following events:
UnorderedNonceInvalidation(uint256 wordPos, uint256 mask)— Nonce invalidation.CallReverted(bytes32 extraData, bytes revertData)— Transaction failure in batch execution.
extraData packs execution mode, nonce, and index for identifying failed calls into: bytes1(executionMode) | bytes23(nonce) | bytes8(index).
In particular, if an event is observed then its batch can be identified with the nonce as extraData[1:24] and the transaction's index in the batch can be identified using extraData[24:32].
Batch execution uses a Call struct defined as:
struct Call {
address to;
uint256 value;
bytes data;
}Each batch is an array of Call objects. The mode and nonce are provided as part of the calldata.
// Deploy
address proxy = factory.deploy(owner, salt);
// Prepare batch
Call[] memory calls = new Call[](2);
calls[0] = Call({to: addr1, value: 0, data: data1});
calls[1] = Call({to: addr2, value: 0, data: data2});
// Prepare opData (nonce + signature)
bytes memory opData = abi.encodePacked(nonce, signature);
// Execute (from proxy)
proxy.execute(mode, abi.encode(calls, opData));On chains where calldata is expensive, Catapultar supports Solady's LibZip::cdFallback() to compress calldata.
$ forge build$ forge test$ forge coverage --no-match-coverage "(script|test)" [--report lcov]To deploy the script, provide RPC urls in .env in the format of: RPC_URL_<name>. The name does not matter but when used in the below script, the name is accessed through the string[] array.
$ forge script deploy --sig "run(string[])" "[<chains>]" --multi --verify --broadcastThis project is licensed under the GNU Lesser General Public License v3.0 only (LGPL-3.0-only).
It also uses the following third-party libraries:
- Solady – Licensed under the MIT License
- Permit2 – Licensed under the MIT License
Each library is included under the terms of its respective license. Copies of the license texts can be found in their source files or original repositories.
When distributing this project, please ensure that all relevant license notices are preserved in accordance with their terms.