Skip to content

lifinance/catapultar

Repository files navigation

Catapultar

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.

Design Goal

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 Account Tradeoffs

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

EIP-7702

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.

Catapultar Usage Note

  • To simulate dual mode transaction, mode 0x01000000000078210001 transactions can be submitted to the relevant proxy using the context of msg.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).

Key Features

  • 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).

Account Deployment

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

Execution Modes (ERC-7821)

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

opData

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:

  1. abi.encodePacked(bytes32(nonce), bytes(signature)) whenever the account is called externally.
  2. 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.

Execution Models

  • 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).

Account Signature Validation (ERC-1271)

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.

Nonce Management

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).

Events Reference

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].

Integration Example

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.

Development

Build

$ forge build

Test

$ forge test

Coverage

$ forge coverage --no-match-coverage "(script|test)" [--report lcov]

Deploy

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 --broadcast

License Notice

This 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:

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.

About

Batch execution smart accounts Solady ERC7821.sol

Resources

License

Stars

Watchers

Forks