ERC-8211: Smart Batching
From transactions to programs.
A batch encoding where each parameter declares how to obtain its value at execution time and what conditions that value must satisfy. No Solidity required. No contract deployment. No audit cycles for new flows.
See it in action
Interactive demo of Smart Batching — runtime-resolved parameters and predicate-gated execution, live on-chain.
The problem with static batching
An Ethereum transaction is a single function call. ERC-4337 and EIP-5792 extended this with batch execution — multiple calls under one signature — but every parameter is static: frozen at signing time, blind to on-chain state at execution.
Real-world DeFi flows produce dynamic, unpredictable outputs. Static batching forces two bad choices: hardcode optimistic amounts (risking reverts) or underestimate conservatively (leaving value stranded).
swap(100 USDC → WETH) supply(0.05 WETH) withdraw(500 shares) bridge(500 USDC) bridge(100 USDC → Optimism) deposit(100 USDC) repay(debt on Aave) borrow(exact collateral ratio) transfer(1.0 ETH) account balance swap(1 ETH → USDC) supply($1,800 USDC) Parameters are locked when you sign. If on-chain state changes before execution, too bad.
No awareness of balances, oracle prices, or any live on-chain data at execution time.
One stale parameter in a multi-step flow causes the entire batch to revert. All or nothing.
What Smart Batching unlocks
Three primitives that turn a static batch into a programmable execution plan.
Runtime parameter injection
Each parameter declares a fetcher that resolves its value on-chain at execution — reading balances, oracles, or any contract state.
supply(0.05 WETH) supply(BALANCE(WETH)) Pre-and-post assertions
Every resolved value runs through inline constraints.
Entries with target = address(0) act as pure
predicate checks that revert the batch if conditions fail.
amount GTE min price LTE maxPrice token EQ expected Multi-transaction context
A shared Storage contract lets entries write output values that later entries — or even the next UserOperation — can read without custom contracts.
swap() Storage supply() Runtime parameter injection
Swap USDC → USDT on Uniswap, then supply to Aave. The swap output is variable — smart batching resolves the exact amount at execution time.
SwapRouter.exactInputSingle(USDC, USDT, 500, 1000e6, 0)AavePool.supply(USDT, runtime injectedResolved at execution timeFetcher typeBALANCEResolves viaIERC20(USDT).balanceOf(self)Returns997_420_000(997.42 USDT)ConstraintGTE(1)✓ pass, self, 0)How it works
Every parameter in a smart batch declares two orthogonal concerns: how the value is obtained (fetcher type) and where it goes (parameter routing). After resolution, inline constraints validate the value before it’s routed.
Fetcher types — how values are obtained
Each parameter specifies its resolution strategy independently.
RAW_BYTES Literal value, known at encoding STATIC_CALL Arbitrary on-chain state read BALANCE ERC-20 or native balance query Parameter routing — where values go
Each resolved value is directed to one of three destinations.
TARGET Call address VALUE ETH to forward CALL_DATA Appended to calldata Inline constraints — validate before routing
Each resolved value is checked against predicates. If any fails, the batch reverts.
EQ GTE LTE IN Exact match, bounds, range Execution pipeline
- 1 Resolve inputsEach InputParam fetcher fires: RAW_BYTES, STATIC_CALL, or BALANCE
- 2 Validate constraintsInline predicates check each resolved value — any failure reverts the batch
- 3 Route & assemble calldataValues direct to TARGET, VALUE, or CALL_DATA; calldata built from selector + parameters
- 4 Execute call
target.call{value} (calldata)— skipped if target isaddress(0)(predicate entry) - 5 Capture outputsReturn data optionally written to Storage contract for use by later entries
The pipeline is normative — all conforming implementations must follow this exact sequence. See the full specification for details.
Cross-chain orchestration
Rebalance a lending position from Morpho on Base to Aave on Ethereum — eight steps across two chains, signed once. The Ethereum batch is predicate-gated: a relayer simulates via eth_call and submits only when the bridged WETH has arrived.
From transactions to programs
Developers author multi-step, multi-chain programs in TypeScript.
The SDK compiles to ComposableExecution[] entries with fetchers,
constraints, and storage instructions. The user signs once. Relayers execute on-chain.
const batch = smartBatch([
swap({
from: WETH,
to: USDC,
amount: fullBalance()
}),
predicate({
balance: gte(USDC, account, 2500e6)
}),
supply({
protocol: "aave",
token: USDC,
amount: fullBalance()
}),
stake({
token: aUSDC,
amount: fullBalance()
}),
]);
This is a complete four-step DeFi flow: swap WETH for USDC, verify the balance
meets a minimum, supply to Aave, stake the receipt tokens. Every amount resolves
dynamically via fullBalance() — no hardcoded values, no leftover dust.
Write flows in TypeScript. The SDK compiles to standard on-chain encoding.
No new attack surface. No audit cycles when flows change.
One signature over a Merkle root. Relayers handle submission.
Universal compatibility
The standard is defined at the encoding and interface level — not as a specific
module. The same ComposableExecution[] encoding works with every smart
account architecture. One wire format, one interface, thin adapters.
ERC-7579 Executor Module
Wraps IComposableExecution as a standard executor module. Installs through
the ERC-7579 module lifecycle.
ERC-6900 Plugin
Registers executeComposable as an execution function via the ERC-6900 manifest.
Native Direct Inheritance
Smart accounts implement IComposableExecution directly. No module wrapper, no
cross-contract overhead.
ERC-7702 Delegation Target EOAs delegate to an implementation contract. Composable execution without a smart account deployment.
No existing smart account requires migration. The encoding format is self-contained and the interface is a single function — adapters for any account standard are minimal.