This proposal defines a minimal batch executor interface for delegations. A delegation is a smart contract that implements logic which other smart contracts can delegate to. This allows atomic batched executions to be prepared in a standardized way.
Motivation
With the advent of EIP-7702, it is possible for Externally Owned Accounts (EOAs) to perform atomic batched executions.
We anticipate that there will be multiple EIP-7702 delegations from multiple major vendors. A standard for the execution interface will enable better interoperability. EIP-7702 delegation is a risky procedure which should be done sparingly — it should not be performed upon each time a user switches websites. Also, EIP-7702 delegations are transactions that cost gas, making frequent delegation switching uneconomical. A standardized execution interface will reduce the need to switch delegations.
This standard complements the wallet_sendCalls API in EIP-5792. It enables the detection of atomic batch execution capabilities on EOAs and the preparation of the calldata for atomic batch executions on EOAs.
Using atomic batched executions reduces total latency and total tranaction costs, making it preferable over sequential transaction sending for EOAs.
Hence the utmost motivation for this proposal, which has been crafted for maximal simplicity, extensibility, performance and compatibility.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Overview
The minimal batch executor interface is defined as follows:
/// @dev Interface for minimal batch executor.
interfaceIERC7821{/// @dev Call struct for the `execute` function.
structCall{addresstarget;// Replaced with `address(this)` if `address(0)`.
uint256value;// Amount of native currency (i.e. Ether) to send.
bytesdata;// Calldata to send with the call.
}/// @dev Executes the calls in `executionData`.
/// Reverts and bubbles up error if any call fails.
///
/// `executionData` encoding (single batch):
/// - If `opData` is empty, `executionData` is simply `abi.encode(calls)`.
/// - Else, `executionData` is `abi.encode(calls, opData)`.
/// See: https://eips.ethereum.org/EIPS/eip-7579
///
/// `executionData` encoding (batch of batches):
/// - `executionData` is `abi.encode(bytes[])`, where each element in `bytes[]`
/// is an `executionData` for a single batch.
///
/// Supported modes:
/// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`.
/// - `0x01000000000078210001...`: Single batch. Supports optional `opData`.
/// - `0x01000000000078210002...`: Batch of batches. The mode is optional.
///
/// For the "batch of batches" mode, each batch will be recursively passed into
/// `execute` internally with mode `0x01000000000078210001...`.
/// Useful for passing in batches signed by different signers.
///
/// Authorization checks:
/// - If `opData` is empty, the implementation SHOULD require that
/// `msg.sender == address(this)`.
/// - If `opData` is not empty, the implementation SHOULD use the signature
/// encoded in `opData` to determine if the caller can perform the execution.
/// - If `msg.sender` is an authorized entry point, then `execute` MAY accept
/// calls from the entry point, and MAY use `opData` for specialized logic.
///
/// `opData` may be used to store additional data for authentication,
/// paymaster data, gas limits, etc.
///
/// For calldata compression efficiency, if a Call.target is `address(0)`,
/// it will be replaced with `address(this)`.
functionexecute(bytes32mode,bytescalldataexecutionData)externalpayable;/// @dev Provided for execution mode support detection.
/// Only returns true for:
/// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`.
/// - `0x01000000000078210001...`: Single batch. Supports optional `opData`.
/// - `0x01000000000078210002...`: Batch of batches. The mode is optional.
functionsupportsExecutionMode(bytes32mode)externalviewreturns(bool);}
Support for batch of batches mode is OPTIONAL. If it is not supported, the contract MUST return false for any mode starting with 0x01000000000078210002 in supportsExecutionMode.
Recommendations
To support the approve + swap workflow on EOAs with delegations, frontends SHOULD:
Query supportsExecutionMode(bytes32(0x0100000000000000000000000000000000000000000000000000000000000000)), ensuring that it returns true.
We aim for radical minimalism to keep the standard as left-curved as possible. Simplicity is the key to adoption. Our North Star is to get every decentralized exchange to support the approve + swap workflow for EOAs with delegations as soon as possible.
execute and supportsExecutionMode
We have opted to use the execute and supportsExecutionMode functions in ERC-7579 for better compatibility with the existing smart account ecosystem.
While radical minimalism is the goal, some compromises have to be made in the pursuit for better adoption.
For minimalism, this standard does not require implementing ERC-165 and the executeFromExecutor function in ERC-7579.
Optional encoding of opData in executionData
The opData bytes parameter can be optionally included in executionData by either doing abi.encode(calls) or abi.encode(calls, opData).
Replacing address(0) with address(this)
For calldata compression optimization.
Optional batch of batches mode
The opData may be used to provide authentication data for a single batch. Having a batch of batches mode will enable a single transaction to be able to submit batches signed by different signers, without the need for an authorized entry point. This mode is kept optional because the same functionality can still be achieved with the use of an authorized entry point. It is included for developer experience.
Backwards Compatibility
No backwards compatibility issues.
Reference Implementation
// SPDX-License-Identifier: CC0-1.0
pragmasolidity^0.8.4;/// @notice Minimal batch executor mixin.
abstractcontractERC7821{/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*//* STRUCTS *//*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*//// @dev Call struct for the `execute` function.
structCall{addresstarget;// Replaced with `address(this)` if `address(0)`.
uint256value;// Amount of native currency (i.e. Ether) to send.
bytesdata;// Calldata to send with the call.
}/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*//* ERRORS *//*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*//// @dev The execution mode is not supported.
errorUnsupportedExecutionMode();/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*//* EXECUTION OPERATIONS *//*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*//// @dev Executes the calls in `executionData`.
/// Reverts and bubbles up error if any call fails.
///
/// `executionData` encoding (single batch):
/// - If `opData` is empty, `executionData` is simply `abi.encode(calls)`.
/// - Else, `executionData` is `abi.encode(calls, opData)`.
/// See: https://eips.ethereum.org/EIPS/eip-7579
///
/// `executionData` encoding (batch of batches):
/// - `executionData` is `abi.encode(bytes[])`, where each element in `bytes[]`
/// is an `executionData` for a single batch.
///
/// Supported modes:
/// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`.
/// - `0x01000000000078210001...`: Single batch. Supports optional `opData`.
/// - `0x01000000000078210002...`: Batch of batches. The mode is optional.
///
/// For the "batch of batches" mode, each batch will be recursively passed into
/// `execute` internally with mode `0x01000000000078210001...`.
/// Useful for passing in batches signed by different signers.
///
/// Authorization checks:
/// - If `opData` is empty, the implementation SHOULD require that
/// `msg.sender == address(this)`.
/// - If `opData` is not empty, the implementation SHOULD use the signature
/// encoded in `opData` to determine if the caller can perform the execution.
/// - If `msg.sender` is an authorized entry point, then `execute` MAY accept
/// calls from the entry point, and MAY use `opData` for specialized logic.
///
/// `opData` may be used to store additional data for authentication,
/// paymaster data, gas limits, etc.
///
/// For calldata compression efficiency, if a Call.target is `address(0)`,
/// it will be replaced with `address(this)`.
functionexecute(bytes32mode,bytesmemoryexecutionData)publicpayablevirtual{uint256id=_executionModeId(mode);if(id==3){mode^=bytes32(uint256(3<<(22*8)));bytes[]memorybatches=abi.decode(executionData,(bytes[]));for(uint256i;i<batches.length;++i){execute(mode,batches[i]);}return;}if(id==uint256(0))revertUnsupportedExecutionMode();booltryWithOpData;/// @solidity memory-safe-assembly
assembly{lett:=gt(mload(add(executionData,0x20)),0x3f)letexecutionDataLength:=mload(executionData)tryWithOpData:=and(eq(id,2),and(gt(executionDataLength,0x3f),t))}Call[]memorycalls;bytesmemoryopData;if(tryWithOpData){(calls,opData)=abi.decode(executionData,(Call[],bytes));}else{calls=abi.decode(executionData,(Call[]));}_execute(calls,opData);}/// @dev Provided for execution mode support detection.
/// Only returns true for:
/// - `0x01000000000000000000...`: Single batch. Does not support optional `opData`.
/// - `0x01000000000078210001...`: Single batch. Supports optional `opData`.
/// - `0x01000000000078210002...`: Batch of batches. The mode is optional.
functionsupportsExecutionMode(bytes32mode)publicviewvirtualreturns(boolresult){return_executionModeId(mode)!=0;}/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*//* INTERNAL HELPERS *//*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*//// @dev 0: invalid mode, 1: no `opData` support, 2: with `opData` support, 3: batch of batches.
function_executionModeId(bytes32mode)internalviewvirtualreturns(uint256id){// Only supports atomic batched executions.
// For the encoding scheme, see: https://eips.ethereum.org/EIPS/eip-7579
// Bytes Layout:
// - [0] ( 1 byte ) `0x01` for batch call.
// - [1] ( 1 byte ) `0x00` for revert on any failure.
// - [2..5] ( 4 bytes) Reserved by ERC7579 for future standardization.
// - [6..9] ( 4 bytes) `0x00000000` or `0x78210001` or `0x78210002`.
// - [10..31] (22 bytes) Unused. Free for use.
uint256m=(uint256(mode)>>(22*8))&0xffff00000000ffffffff;if(m==0x01000000000078210002)id=3;if(m==0x01000000000078210001)id=2;if(m==0x01000000000000000000)id=1;}/// @dev Executes the calls and returns the results.
/// Reverts and bubbles up error if any call fails.
function_execute(Call[]memorycalls,bytesmemoryopData)internalvirtual{// Very basic auth to only allow this contract to be called by itself.
// Override this function to perform more complex auth with `opData`.
if(opData.length==uint256(0)){require(msg.sender==address(this));// Remember to return `_execute(calls)` when you override this function.
return_execute(calls);}revert();// In your override, replace this with logic to operate on `opData`.
}/// @dev Executes the calls.
/// Reverts and bubbles up error if any call fails.
function_execute(Call[]memorycalls)internalvirtual{for(uint256i;i<calls.length;++i){Callmemoryc=calls[i];addresstarget=c.target==address(0)?address(this):c.target;_execute(target,c.value,c.data);}}/// @dev Executes the call.
/// Reverts and bubbles up error if the call fails.
function_execute(addresstarget,uint256value,bytesmemorydata)internalvirtual{(boolsuccess,bytesmemoryresult)=target.call{value:value}(data);if(success)return;/// @solidity memory-safe-assembly
assembly{// Bubble up the revert if the call reverts.
revert(add(result,0x20),mload(result))}}}
Security Considerations
Access controls for execute
Implementations should ensure that execute have the proper access controls.