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

♻️ Convert MinimalBatchExecutor to 7579 style #1170

Merged
merged 10 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
75 changes: 62 additions & 13 deletions src/accounts/MinimalBatchExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,84 @@ abstract contract MinimalBatchExecutor {
bytes data;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* ERRORS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev The execution mode is not supported.
error UnsupportedExecutionMode();

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* FUNCTIONS TO OVERRIDE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev Ensures that `execute` can only be called by the correct caller or `authData`.
function _authorizeExecute(Call[] calldata calls, bytes calldata authData) internal virtual;
/// @dev Ensures that `execute` can only be called by the correct caller or `opData`.
function _authorizeExecute(Call[] calldata calls, bytes calldata opData) internal virtual;

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* EXECUTE */
/* EXECUTION OPERATIONS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev Executes the `calls` and returns the results.
/// @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.
function execute(Call[] calldata calls, bytes calldata authData)
///
/// `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
///
/// 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 results)
{
_authorizeExecute(calls, authData);
if (!supportsExecutionMode(mode)) {
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, 0x7f181275) // `UnsupportedExecutionMode()`.
revert(0x1c, 0x04)
}
}
Call[] calldata calls;
bytes calldata opData;
/// @solidity memory-safe-assembly
assembly {
opData.length := 0
let o := add(executionData.offset, calldataload(executionData.offset))
calls.offset := add(o, 0x20)
calls.length := calldataload(o)
if iszero(lt(calldataload(executionData.offset), 0x40)) {
let p := add(executionData.offset, calldataload(add(executionData.offset, 0x20)))
opData.length := calldataload(p)
opData.offset := add(p, 0x20)
}
}
_authorizeExecute(calls, opData);
return _execute(calls);
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* SIGNALING */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

/// @dev This function is provided for frontends to detect support.
function minimalBatchExecutorVersion() public pure virtual returns (uint256) {
return 1; // This number may change.
/// @dev Provided for execution mode support detection.
function supportsExecutionMode(bytes32 mode) public pure virtual returns (bool result) {
// 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..7] ( 2 bytes) `0x9999`.
// - [8..9] ( 2 bytes) Version in hex format.
// - [9..31] (22 bytes) Unused. Free for use.
return bytes10(mode) & 0xffff00000000ffffffff == 0x01000000000099990001;
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
Expand All @@ -66,6 +114,7 @@ abstract contract MinimalBatchExecutor {
bytes calldata data;
/// @solidity memory-safe-assembly
assembly {
// Directly extract `calls[i]` without bounds checks.
let c := add(calls.offset, calldataload(add(calls.offset, shl(5, i))))
target := calldataload(c)
value := calldataload(add(c, 0x20))
Expand Down
46 changes: 43 additions & 3 deletions test/MinimalBatchExecutor.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ contract MinimalBatchExecutorTest is SoladyTest {

address target;

bytes32 internal constant _SUPPORTED_MODE = bytes10(0x01000000000099990001);

function setUp() public {
mbe = new MockMinimalBatchExecutor();
target = LibClone.clone(address(this));
Expand All @@ -32,7 +34,34 @@ contract MinimalBatchExecutorTest is SoladyTest {
return keccak256(b);
}

function testMinimalBatchExecutor() public {
function testMinimalBatchExecutorGas() public {
vm.pauseGasMetering();
vm.deal(address(this), 1 ether);

MinimalBatchExecutor.Call[] memory calls = new MinimalBatchExecutor.Call[](2);

calls[0].target = target;
calls[0].value = 123;
calls[0].data = abi.encodeWithSignature("returnsBytes(bytes)", "hehe");

calls[1].target = target;
calls[1].value = 789;
calls[1].data = abi.encodeWithSignature("returnsHash(bytes)", "lol");

bytes memory data = abi.encode(calls);
vm.resumeGasMetering();

bytes[] memory results = mbe.execute{value: _totalValue(calls)}(_SUPPORTED_MODE, data);

vm.pauseGasMetering();

assertEq(results.length, 2);
assertEq(abi.decode(results[0], (bytes)), "hehe");
assertEq(abi.decode(results[1], (bytes32)), keccak256("lol"));
vm.resumeGasMetering();
}

function testMinimalBatchExecutor(bytes memory opData) public {
vm.deal(address(this), 1 ether);

MinimalBatchExecutor.Call[] memory calls = new MinimalBatchExecutor.Call[](2);
Expand All @@ -45,7 +74,10 @@ contract MinimalBatchExecutorTest is SoladyTest {
calls[1].value = 789;
calls[1].data = abi.encodeWithSignature("returnsHash(bytes)", "lol");

bytes[] memory results = mbe.execute{value: _totalValue(calls)}(calls, "");
bytes[] memory results =
mbe.execute{value: _totalValue(calls)}(_SUPPORTED_MODE, _encode(calls, opData));

assertEq(mbe.lastOpData(), opData);

assertEq(results.length, 2);
assertEq(abi.decode(results[0], (bytes)), "hehe");
Expand All @@ -59,7 +91,15 @@ contract MinimalBatchExecutorTest is SoladyTest {
calls[0].data = abi.encodeWithSignature("revertsWithCustomError()");

vm.expectRevert(CustomError.selector);
mbe.execute{value: _totalValue(calls)}(calls, "");
mbe.execute{value: _totalValue(calls)}(_SUPPORTED_MODE, _encode(calls, ""));
}

function _encode(MinimalBatchExecutor.Call[] memory calls, bytes memory opData)
internal
returns (bytes memory)
{
if (_randomChance(2) && opData.length == 0) return abi.encode(calls);
return abi.encode(calls, opData);
}

struct Payload {
Expand Down
10 changes: 5 additions & 5 deletions test/utils/mocks/MockMinimalBatchExecutor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {Brutalizer} from "../Brutalizer.sol";
/// @dev WARNING! This mock is strictly intended for testing purposes only.
/// Do NOT copy anything here into production code unless you really know what you are doing.
contract MockMinimalBatchExecutor is MinimalBatchExecutor, Brutalizer {
function _authorizeExecute(Call[] calldata calls, bytes calldata authData)
internal
virtual
override
{}
bytes public lastOpData;

function _authorizeExecute(Call[] calldata, bytes calldata opData) internal virtual override {
lastOpData = opData;
}

function executeDirect(Call[] calldata calls)
public
Expand Down