ERC-8211: Smart Batching

ERC: Smart Batching — Runtime-Resolved Parameters and Predicate-Gated Execution for Smart Accounts

Abstract

Smart Batching is a batch encoding standard where each parameter declares how to obtain its value at execution time and what conditions that value must satisfy. Parameters can be literals, live staticcall results, or balance queries — each independently resolved on-chain and validated against inline constraints before being assembled into the call.

Critically, a batch is no longer just a sequence of steps — it can include inline assertions: conditions on chain state that must hold for execution to proceed. A user doesn’t merely say “do A, then B”; they say “do A, assert the result is acceptable, then B.” Steps and assertions are peers in the same batch encoding, turning a blind transaction list into a verifiable program with built-in safety guarantees.

This eliminates the fundamental limitation of static batching (ERC-4337, EIP-5792): every parameter is frozen at signing time, blind to on-chain state at execution. If a swap returns fewer tokens than estimated, gas costs shift, or a bridge delivers with unexpected slippage — the batch reverts. The only workaround today is deploying custom smart contracts for each multi-step flow, or relying on protocol-level slippage parameters that not every target contract exposes.

Pull Request: Add ERC: Smart Batching by oxshaman · Pull Request #1638 · ethereum/ERCs · GitHub

Full Specification: ERCs/ERCS/erc-xxxx.md at smart-batching · oxshaman/ERCs · GitHub


Motivation

Real-world DeFi flows produce dynamic, unpredictable outputs. A swap yields a variable token amount. A withdrawal from a lending vault returns a variable share-to-asset conversion. A bridge delivers tokens after an unpredictable delay with variable fees. Static batching forces two bad choices: hardcode optimistic amounts (risking reverts) or underestimate conservatively (leaving value stranded).

Smart Batching resolves parameters at execution time. Instead of pre-encoding a static calldata blob, the user signs a batch where each parameter specifies how to obtain its value — as a literal, a staticcall, or a balance query. The execution logic resolves each parameter and constructs the calldata from scratch during the transaction.

But dynamic resolution alone is half the story. The other half is assertions — the ability for a batch to declare conditions that must be true at any point during execution. Today’s batches are flat lists of actions: “call A, call B, call C.” If the outcome of call A is unfavorable, you find out only after the entire batch reverts (or worse, succeeds with a bad result). With Smart Batching, assertions sit between steps as first-class entries, letting the user express: “call A, verify the output is at least X, then call B.” The batch becomes a program with embedded safety checks, not a hopeful script.

STATIC BATCHING (current model)
  Step 1: swap(100 USDC)           → OK
  Step 2: supply(0.05 WETH)        → REVERT (actual output was 0.0495)
  Problem: "0.05" was a guess at signature time.
  No way to express "only proceed if output ≥ threshold."

SMART BATCHING (this standard)
  Step 1: swap(100 USDC)           → OK, returns 0.0495
  Assert: BALANCE(WETH, account) ≥ 0.04    ✓ (user-defined safety floor)
  Step 2: supply(amount)           → amount = BALANCE(WETH, account) = 0.0495 ✓
  Parameters resolve on-chain. Assertions guard each transition.

Key Design Decisions

1. Calldata Construction, Not Placeholder Patching

Rather than pre-encoding calldata with sentinel bytes at known offsets and patching them, Smart Batching builds calldata from scratch — each InputParam is self-contained with its fetcher type, routing destination, and constraints. No offset arithmetic, no knowledge of the target function’s ABI layout required.

2. Encoding-First, Account-Standard-Agnostic

The standard defines encoding schemes and interfaces — not a specific module. The same ComposableExecution[] encoding works as:

  • An ERC-7579 executor module
  • An ERC-6900 plugin
  • A native account method
  • An ERC-7702 delegation target

One wire format. One interface (IComposableExecution). Thin adapters handle installation and permissions; the core logic is shared.

3. Assertions as First-Class Batch Entries

In every existing batching standard, a batch entry means “execute this call.” Smart Batching introduces a second kind of entry: the assertion — a condition on chain state that must hold for the batch to continue. Assertions are not a bolt-on; they use the same InputParam resolution and constraint mechanism as action entries, just with no call target.

This matters for three reasons:

Safety without custom contracts. Today, if you want to enforce “only supply to Aave if my swap output exceeds a threshold,” you need a bespoke contract (or a protocol that happens to expose a minAmountOut parameter). With assertions, the safety check is part of the batch itself — any user can express arbitrary conditions on any resolved value, against any protocol, without deploying anything.

Protection against MEV and state manipulation. Assertions let users guard against sandwich attacks, oracle manipulation, and unfavorable execution conditions. A batch can assert a price feed is within an expected range, a pool’s reserves haven’t been distorted, or a balance meets a minimum — all evaluated atomically within the same transaction. If any assertion fails, the entire batch reverts before value is lost.

Composable intent expression. Assertions transform batches from imperative scripts (“do X, do Y”) into declarative programs (“do X, require P, do Y”). Users express what outcomes they consider acceptable, not just what actions to take. This is a fundamentally different model — the batch encodes intent, and the EVM enforces it.

4. Emergent Predicates via Constraints

Each resolved value can carry inline constraints (GTE, LTE, EQ, IN). An entry with no call target (address(0)) becomes a pure boolean gate on chain state — a predicate entry. No separate predicate mechanism needed.

This produces cross-chain orchestration for free: relayers simulate batches via eth_call, submit when predicates are satisfied. Multi-chain flows execute as a single signed program, each step gated by verifiable on-chain predicates — agnostic to the interoperability layer (native bridges, ERC-7683, ERC-7786, any messaging protocol).

5. From Transactions to Programs

const batch = smartBatch([
  swap({ from: WETH, to: USDC, amount: fullBalance() }),
  assert({ balance: gte(USDC, account, 2500e6) }),     // abort if swap output too low
  supply({ protocol: "aave", token: USDC, amount: fullBalance() }),
  assert({ balance: gte(aUSDC, account, 2400e6) }),    // verify supply was credited
  stake({ token: aUSDC, amount: fullBalance() }),
]);

Steps and assertions interleave freely. Developers author multi-step, multi-chain programs in TypeScript — actions and safety conditions side by side — compiled to a standard on-chain encoding, signed once, and executed entirely by the EVM. No contract deployment. No audit cycles for new flows.

Core Primitives

Input Parameters — Each specifies two orthogonal concerns:

  • Where the value goes (TARGET, VALUE, CALL_DATA)
  • How the value is obtained (RAW_BYTES, STATIC_CALL, BALANCE)

Output Parameters — Capture return values to an external Storage contract for use by subsequent entries (EXEC_RESULT, STATIC_CALL).

Constraints — Inline predicates on resolved values (EQ, GTE, LTE, IN). If any constraint fails, the entire batch reverts.

Storage Contract — Namespaced key-value storage with per-account, per-caller isolation. Supports transient storage (EIP-1153) for gas efficiency.

Backwards Compatibility

Fully backwards compatible. The encoding is self-contained and additive — no existing smart account requires migration. Works alongside existing executeBatch operations, and forward-compatible with EIP-8141 Frame Transactions.

Reference Implementation

The reference implementation includes:

  • IComposableExecution.sol — Standard interface
  • ComposableExecutionLib.sol — Shared library with full resolution algorithm
  • Storage.sol — External namespaced storage contract
  • ComposableExecutionModule.sol — ERC-7579 adapter
  • ComposableExecutionBase.sol — Native account integration base

The reference implementation has been audited, with all findings remediated.


Authors: Mislav Javor (@oxshaman), Filip Dujmušić (@fichiokaku), Filipp Makarov (@filmakarov), Venkatesh Rajendran (@vr16x)

We welcome feedback on the specification, especially around:

  • The constraint mechanism and whether the four constraint types (EQ, GTE, LTE, IN) are sufficient
  • The Storage contract design and transient storage trade-offs
  • The predicate entry pattern for cross-chain orchestration
  • Integration considerations for different account standards (ERC-7579, ERC-6900, ERC-7702)
1 Like

Why not simply run an EVM interpreter in the smart contract, that’ll provide ultimate flexibility in batching/composing actions.

Hi @oxshaman, thanks for the proposal, it seems pretty interesting!

I left a review, but I’m also sharing few questions and observations after reading the spec:

▎ 1. Dynamic return types in EXEC_RESULT
EXEC_RESULT captures N consecutive 32-byte words starting at offset 0. This works for static return types (uint256, address, bytes32), but dynamic types (bytes, string, arrays) place an ABI offset pointer at word 0, not the data itself. A function returning bytes with returnValueCount: 1 would capture 0x0000...0020 (the offset pointer), not the content. Is the capture mechanism intentionally limited to static return types? If so, it would help to state that restriction explicitly.

▎ 2. Constraint comparison semantics for signed integers
GTE/LTE comparisons operate on bytes32, which implies unsigned ordering. For int256, this produces wrong results: -1 (0xffff...ffff) is greater than any positive number under unsigned comparison. Some DeFi applications may need constraints over signed values (e.g., oracle deltas, net position checks). Would the proposers consider whether the constraint type system should support signed comparisons, or at least explicitly restrict to unsigned types?

▎ 3. OR composition of predicates
Multiple constraints on an InputParam and multiple predicate entries in a batch are both AND-composed. There is no native OR. The only workaround I can see is deploying a helper contract that computes OR logic and returns a bool, then validating it with EQ(true). But doesn’t that reintroduce exactly the custom-contract deployment overhead that smart batching is meant to eliminate? Curious whether OR predicates came up in design discussions and, if so, what the reasoning was for leaving them out.

▎ 4. Merkle tree authorization
The cross-chain orchestration model relies on a companion “Merkle tree ERC” for authorization that isn’t linked or numbered anywhere in the spec. More broadly, Merkle trees for signature authorization add significant complexity that may not be worth the tradeoffs (especially, loosing user’s readability). I’ve done some prior research on this topic in ERC-7964, which might be relevant context for this proposal

Hey, one of the authors here.

I believe you’re suggesting going in the direction of weiroll? A turing complete interpreter makes it almost impossible to statically analyze calldata. Wallets, explorers, and bundlers on the other hand can easily parse ComposableExecution structs and show users all the effects of a transaction. Having a very constrained set of functions and interfaces improves readability and reduces attack surface

Yes this was intentional, and we should probably make that clearer in the spec.
We wanted to keep things simple and ensure the standard works with static values so that transactions can be clearly parsed and presented to the end user. We believe most common flows are covered by this reduced set of interfaces. This standard comes from our work with developers in the industry, where we addressed the most common pain points. We’d rather rely on a thin “lens” contract (stateless, read only) that can take arbitrary return values and extract specific items from more complex responses. Here’s our reference implementation composable-batch-erc/contracts/lens/ComposableLens.sol at main · bcnmy/composable-batch-erc · GitHub

Great points here , agreed on both. We have actually faced these limitations in real use cases. We’re currently working on a reference implementation that addresses them. The changes are relatively small, keep the standard simple, and make a lot of sense in practice