Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ERC: Minimal Batch Executor Interface #726

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3658226
Add ERC
Vectorized Nov 21, 2024
5f7f65b
Edit
Vectorized Nov 21, 2024
e9ccf92
Edit
Vectorized Nov 21, 2024
1c4d117
Edit
Vectorized Nov 21, 2024
5eef39b
Fix code
Vectorized Nov 21, 2024
4f72d55
Normify code example
Vectorized Nov 21, 2024
35943d6
Rename to opData
Vectorized Nov 21, 2024
dd14d7d
Tidy
Vectorized Nov 21, 2024
da0b5cc
Edit to use 7579 style
Vectorized Nov 21, 2024
54e2dbe
Nit
Vectorized Nov 21, 2024
1aff3f6
Normify
Vectorized Nov 21, 2024
5423d22
Edit
Vectorized Nov 21, 2024
8b9f9d2
Fix grammar
Vectorized Nov 21, 2024
43c4269
Update and rename erc-9999.md to erc-7821.md
xinbenlv Nov 21, 2024
05bc842
Edit
Vectorized Nov 21, 2024
d82eac8
Merge branch 'minimal-batch-executor-erc' of https://github.com/Vecto…
Vectorized Nov 21, 2024
580b8a7
Update mode magic number
Vectorized Nov 21, 2024
2650252
Merge branch 'master' into minimal-batch-executor-erc
Vectorized Nov 21, 2024
f9fa123
Revert back to initial proposal
Vectorized Nov 21, 2024
c682383
Merge branch 'minimal-batch-executor-erc' of https://github.com/Vecto…
Vectorized Nov 21, 2024
c94285a
Edit
Vectorized Nov 21, 2024
aede948
Edit
Vectorized Nov 21, 2024
ad51da0
Typo
Vectorized Nov 21, 2024
64d91a3
Update code
Vectorized Nov 21, 2024
4e47631
Smooth out grammar
Vectorized Nov 21, 2024
d2258cf
Update ERCS/erc-7821.md
Vectorized Nov 22, 2024
27c86b3
Update with address(0) replacement
Vectorized Nov 22, 2024
bd2de27
Merge branch 'minimal-batch-executor-erc' of https://github.com/Vecto…
Vectorized Nov 22, 2024
ba2d6c9
Switch back to ERC7579 style
Vectorized Nov 23, 2024
29dcc7b
Edit ERC code
Vectorized Nov 25, 2024
98b456d
Edit ERC code
Vectorized Nov 25, 2024
2cc663c
Update ERCS/erc-7821.md
Vectorized Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 270 additions & 0 deletions ERCS/erc-7821.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
---
eip: 7821
title: Minimal Batch Executor Interface
description: A minimal batch executor interface for delegations
author: Vectorized (@Vectorized), Jake Moxey (@jxom)
discussions-to: https://ethereum-magicians.org/t/erc-7821-minimal-batch-executor-interface/21776
status: Draft
type: Standards Track
category: ERC
created: 2024-11-21
requires: 7579
---

## Abstract

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 batched executions to be prepared in a standardized way.

## Motivation

With the advent of [EIP-7702](./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. To enable frontends to detect and prepare a vendor-agnostic batched transaction, we will need a standardized interface for batched executions.

In the absence of such a standard, vendors may choose to create their own proprietary implementations, causing fragmentation. Imagine visiting your favorite decentralized exchange and realizing that their frontend is still incompatible with your EOA delegation. The infamous approve and swap workflow cannot be fixed just by EIP-7702 alone.

We need a standardized batch executor interface.

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:

```solidity
/// @dev Interface for minimal batch executor.
interface IERC7821 {
/// @dev Call struct for the `execute` function.
struct Call {
address target; // Replaced with `address(this)` if `address(0)`.
uint256 value; // Amount of native currency (i.e. Ether) to send.
bytes data; // Calldata to send with the call.
}
/// @dev Executes the `calls` in `executionData` and returns the results.
/// The `results` are the returned data from each call.
/// Reverts and bubbles up error if any call fails.
///
/// `executionData` encoding:
/// - 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
///
/// Supported modes:
/// - `bytes32(0x01000000000000000000...)`: does not support optional `opData`.
/// - `bytes32(0x01000000000078210001...)`: supports optional `opData`.
///
/// 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.
///
/// `opData` may be used to store additional data for authentication,
/// paymaster data, gas limits, etc.
function execute(Call[] calldata calls, bytes calldata opData)
Vectorized marked this conversation as resolved.
Show resolved Hide resolved
external
payable
returns (bytes[] memory results);
/// @dev This function is provided for frontends to detect support.
/// Only returns true for:
/// - `bytes32(0x01000000000000000000...)`: does not support optional `opData`.
/// - `bytes32(0x01000000000078210001...)`: supports optional `opData`.
function supportsExecutionMode(bytes32 mode) external view returns (uint256);
}
```

### Recommendations

To support the approve + swap workflow on EOAs with delegations, frontends SHOULD:

1. Query `supportsExecutionMode(bytes32(0x0100000000000000000000000000000000000000000000000000000000000000))`, ensuring that it returns true.

2. Perform `execute(bytes32(0x0100000000000000000000000000000000000000000000000000000000000000), abi.encode(calls))`.

## Rationale

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](./eip-7579.md) 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](./eip-165.md) and the `executeFromExecutor` function in [ERC-7579](./eip-7579.md).

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

## Backwards Compatibility

No backwards compatibility issues.

## Reference Implementation

```solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.4;
/// @notice Minimal batch executor mixin.
abstract contract ERC7821 {
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STRUCTS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Call struct for the `execute` function.
struct Call {
address target; // Replaced with `address(this)` if `address(0)`.
uint256 value; // Amount of native currency (i.e. Ether) to send.
bytes data; // Calldata to send with the call.
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev The execution mode is not supported.
error UnsupportedExecutionMode();
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EXECUTION OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev Executes the `calls` in `executionData` and returns the results.
/// The `results` are the returned data from each call.
/// Reverts and bubbles up error if any call fails.
///
/// `executionData` encoding:
/// - 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
///
/// Supported modes:
/// - `bytes32(0x01000000000000000000...)`: does not support optional `opData`.
/// - `bytes32(0x01000000000078210001...)`: supports optional `opData`.
///
/// 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.
///
/// `opData` may be used to store additional data for authentication,
/// paymaster data, gas limits, etc.
function execute(bytes32 mode, bytes calldata executionData)
public
payable
virtual
returns (bytes[] memory)
{
uint256 id = _executionModeId(mode);
if (id == uint256(0)) revert UnsupportedExecutionMode();
bool tryWithOpData;
/// @solidity memory-safe-assembly
assembly {
let t := gt(calldataload(executionData.offset), 0x3f)
tryWithOpData := and(eq(id, 2), and(gt(executionData.length, 0x3f), t))
}
Call[] memory calls;
bytes memory opData;
if (tryWithOpData) {
(calls, opData) = abi.decode(executionData, (Call[], bytes));
} else {
calls = abi.decode(executionData, (Call[]));
}
return _execute(calls, opData);
}
/// @dev Provided for execution mode support detection.
/// Only returns true for:
/// - `bytes32(0x01000000000000000000...)`: does not support optional `opData`.
/// - `bytes32(0x01000000000078210001...)`: supports optional `opData`.
function supportsExecutionMode(bytes32 mode) public view virtual returns (bool result) {
return _executionModeId(mode) != 0;
}
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* INTERNAL HELPERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
/// @dev 0: invalid mode, 1: no `opData` support, 2: with `opData` support.
function _executionModeId(bytes32 mode) internal view virtual returns (uint256 id) {
// 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..8] ( 4 bytes) `0x78210001` or `0x00000000`.
// - [9..31] (22 bytes) Unused. Free for use.
uint256 m = (uint256(mode) >> (22 * 8)) & 0xffff00000000ffffffff;
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[] memory calls, bytes memory opData)
internal
virtual
returns (bytes[] memory)
{
// 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` and returns the results.
/// Reverts and bubbles up error if any call fails.
function _execute(Call[] memory calls) internal virtual returns (bytes[] memory results) {
results = new bytes[](calls.length);
for (uint256 i; i < calls.length; ++i) {
Call memory c = calls[i];
address target = c.target == address(0) ? address(this) : c.target;
results[i] = _execute(target, c.value, c.data);
}
}
/// @dev Executes the `calls` and returns the result.
/// Reverts and bubbles up error if any call fails.
function _execute(address target, uint256 value, bytes memory data)
internal
virtual
returns (bytes memory)
{
(bool success, bytes memory result) = target.call{value: value}(data);
if (success) return result;
/// @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.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
Loading