diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 373fc10e7..4e07df422 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,30 +78,27 @@ jobs: forge test --fuzz-runs 5000 ) - # Disable for now until we can use `foundry.toml` setting to override the - # transaction gas limit. See: EIP-7825. - # - # tests-ithaca: - # name: Forge Ithaca Testing - # runs-on: ubuntu-latest - # strategy: - # matrix: - # profile: [post-cancun,post-cancun-via-ir] - # steps: - # - uses: actions/checkout@v5 - # - name: Install Foundry Nightly - # uses: foundry-rs/foundry-toolchain@v1 - # with: - # version: nightly - # - name: Install Dependencies - # run: forge install - # - name: Run Tests with ${{ matrix.profile }} - # run: | - # if [[ "${{ matrix.profile }}" == "post-cancun" ]]; then - # FOUNDRY_PROFILE=ithaca forge test --use 0.8.28 - # elif [[ "${{ matrix.profile }}" == "post-cancun-via-ir" ]]; then - # FOUNDRY_PROFILE=ithaca forge test --use 0.8.28 --via-ir - # fi + tests-ithaca: + name: Forge Ithaca Testing + runs-on: ubuntu-latest + strategy: + matrix: + profile: [post-cancun,post-cancun-via-ir] + steps: + - uses: actions/checkout@v5 + - name: Install Foundry Stable + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + - name: Install Dependencies + run: forge install + - name: Run Tests with ${{ matrix.profile }} + run: | + if [[ "${{ matrix.profile }}" == "post-cancun" ]]; then + FOUNDRY_PROFILE=ithaca forge test --use 0.8.28 + elif [[ "${{ matrix.profile }}" == "post-cancun-via-ir" ]]; then + FOUNDRY_PROFILE=ithaca forge test --use 0.8.28 --via-ir + fi codespell: runs-on: ${{ matrix.os }} diff --git a/foundry.toml b/foundry.toml index 07fbd0454..0c3f1b73a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -30,8 +30,10 @@ evm_version = "cancun" skip = [] [profile.ithaca] +gas_limit = 16_777_216 # To prevent EVM error. evm_version = "cancun" odyssey = true +test = "test/ext/ithaca" skip = [] [profile.solx] diff --git a/src/accounts/ext/ithaca/ERC7821.sol b/src/accounts/ext/ithaca/ERC7821.sol new file mode 100644 index 000000000..854c40818 --- /dev/null +++ b/src/accounts/ext/ithaca/ERC7821.sol @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {Receiver} from "../../Receiver.sol"; + +/// @notice Minimal batch executor mixin (Ithaca variant). +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/accounts/ext/ithaca/ERC7821.sol) +/// +/// @dev This contract can be inherited to create fully-fledged smart accounts. +/// If you merely want to combine approve-swap transactions into a single transaction +/// using [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702), you will need to implement basic +/// [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) `isValidSignature` functionality to +/// validate signatures with `ecrecover` against the EOA address. This is necessary because some +/// signature checks skip `ecrecover` if the signer has code. For a basic EOA batch executor, +/// please refer to [BEBE](https://github.com/vectorized/bebe), which inherits from this class. +contract ERC7821 is Receiver { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* STRUCTS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Call struct for the `execute` function. + struct Call { + address to; // Replaced as `address(this)` if `address(0)`. Renamed to `to` for Ithaca Porto. + uint256 value; // Amount of native currency (i.e. Ether) to send. + bytes data; // Calldata to send with the call. + } + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev The execution mode is not supported. + error UnsupportedExecutionMode(); + + /// @dev Cannot decode `executionData` as a batch of batches `abi.encode(bytes[])`. + error BatchOfBatchesDecodingError(); + + /// @dev Cannot decode the optimized batch as either `abi.encode(address, bytes[], bytes)`. + /// or `abi.encode(address, bytes[])`. + error OptimizedBatchDecodingError(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* 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. + /// - `0x01000000000078210003...`: Single batch with common `to` address and optional `opData`. + /// + /// 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. + function execute(bytes32 mode, bytes calldata executionData) public payable virtual { + uint256 id = _executionModeId(mode); + if (id == 3) return _executeBatchOfBatches(mode, executionData); + if (id == 4) return _executeOptimizedBatch(mode, executionData); + Call[] calldata calls; + bytes calldata opData; + + /// @solidity memory-safe-assembly + assembly { + if iszero(id) { + mstore(0x00, 0x7f181275) // `UnsupportedExecutionMode()`. + revert(0x1c, 0x04) + } + // Use inline assembly to extract the calls and optional `opData` efficiently. + opData.length := 0 + let o := add(executionData.offset, calldataload(executionData.offset)) + calls.offset := add(o, 0x20) + calls.length := calldataload(o) + // If the offset of `executionData` allows for `opData`, and the mode supports it. + if gt(eq(id, 2), gt(0x40, calldataload(executionData.offset))) { + let q := add(executionData.offset, calldataload(add(0x20, executionData.offset))) + opData.offset := add(q, 0x20) + opData.length := calldataload(q) + } + // Bounds checking for `executionData` is skipped here for efficiency. + // This is safe if it is only used as an argument to `execute` externally. + // If `executionData` used as an argument to other functions externally, + // please perform the bounds checks via `LibERC7579.decodeBatchAndOpData` + /// or `abi.decode` in the other functions for safety. + } + _execute(mode, executionData, calls, opData); + } + + /// @dev Provided for execution mode support detection. + 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, 3: batch of batches. + 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..9] ( 4 bytes) `0x00000000` or `0x78210001` or `0x78210002`. + // - [10..31] (22 bytes) Unused. Free for use. + /// @solidity memory-safe-assembly + assembly { + let m := and(shr(mul(22, 8), mode), 0xffff00000000ffffffff) + id := eq(m, 0x01000000000000000000) // 1. + id := or(shl(1, eq(m, 0x01000000000078210001)), id) // 2. + id := or(mul(3, eq(m, 0x01000000000078210002)), id) // 3. + id := or(mul(4, eq(m, 0x01000000000078210003)), id) // 4. + } + } + + /// @dev For execution of a batch of batches with a common `to` address. + /// Execution Data: `abi.encode(address to, bytes[] dataArr, bytes opData)` + function _executeOptimizedBatch(bytes32 mode, bytes calldata executionData) internal virtual { + address to; + bytes[] calldata dataArr; + bytes calldata opData; + + /// @solidity memory-safe-assembly + assembly { + // This line is needed to ensure that `opData` is valid in all code paths. + // Otherwise the compiler complains. + opData.length := 0 + + to := calldataload(executionData.offset) + + let b := calldataload(add(0x20, executionData.offset)) // Relative offset of `dataArr`. + let c := add(executionData.offset, b) // Absolute offset of `dataArr.length`. + dataArr.offset := add(c, 0x20) + dataArr.length := calldataload(c) + + let e := add(executionData.offset, executionData.length) // End of `executionData`. + + // Perform bounds checking, to ensure: + // - `to` is within `executionData`. + // - `dataArr` and all of its elements are within `executionData`. + // - `opData` is within `executionData`, if provided. + // As this is a non-standard ERC7821 mode, `LibERC7579` will not include the decoding + // functions with bound checks. So we simply do the checks here. + if or( + shr(64, or(b, or(executionData.length, executionData.offset))), + or(gt(dataArr.offset, e), lt(executionData.length, 0x40)) + ) { + mstore(0x00, 0x6f23de0b) // `OptimizedBatchDecodingError()`. + revert(0x1c, 0x04) + } + // If the relative offset of `dataArr` is 3 words or more, + // it indicates the possible presence of a relative `opData` offset. + if iszero(lt(b, 0x60)) { + let p := calldataload(add(0x40, executionData.offset)) // Relative offset of `opData`. + let q := add(executionData.offset, p) // Absolute offset of `opData.length`. + opData.offset := add(q, 0x20) + opData.length := calldataload(q) + if or(shr(64, or(opData.length, p)), gt(add(opData.length, opData.offset), e)) { + mstore(0x00, 0x6f23de0b) // `OptimizedBatchDecodingError()`. + revert(0x1c, 0x04) + } + } + if dataArr.length { + // Perform bounds checks on the decoded `dataArr`. + // Loop runs out-of-gas if `dataArr.length` is big enough to cause overflows. + for { let i := dataArr.length } 1 {} { + i := sub(i, 1) + let p := calldataload(add(dataArr.offset, shl(5, i))) // Relative offset of `dataArr[i]`. + let u := add(dataArr.offset, p) // Absolute offset of `dataArr[i].length`. + let l := calldataload(u) // `dataArr[i].length`. + if or(shr(64, or(p, l)), gt(add(l, add(0x20, u)), e)) { + mstore(0x00, 0x6f23de0b) // `OptimizedBatchDecodingError()`. + revert(0x1c, 0x04) + } + if iszero(i) { break } + } + } + } + + _executeOptimizedBatch(mode, executionData, to, dataArr, opData); + } + + /// @dev For execution of a batch of batches. + function _executeBatchOfBatches(bytes32 mode, bytes calldata executionData) internal virtual { + // Replace with `0x0100________78210001...` while preserving optional and reserved fields. + mode ^= bytes32(uint256(3 << (22 * 8))); // `2 XOR 3 = 1`. + (uint256 n, uint256 o, uint256 e) = (0, 0, 0); + /// @solidity memory-safe-assembly + assembly { + let j := calldataload(executionData.offset) + let t := add(executionData.offset, j) + n := calldataload(t) // `batches.length`. + o := add(0x20, t) // Offset of `batches[0]`. + e := add(executionData.offset, executionData.length) // End of `executionData`. + // Do the bounds check on `executionData` treating it as `abi.encode(bytes[])`. + // Not too expensive, so we will just do it right here right now. + if or(shr(64, j), or(lt(executionData.length, 0x20), gt(add(o, shl(5, n)), e))) { + mstore(0x00, 0x3995943b) // `BatchOfBatchesDecodingError()`. + revert(0x1c, 0x04) + } + } + unchecked { + for (uint256 i; i != n; ++i) { + bytes calldata batch; + /// @solidity memory-safe-assembly + assembly { + let j := calldataload(add(o, shl(5, i))) + let t := add(o, j) + batch.offset := add(t, 0x20) + batch.length := calldataload(t) + // Validate that `batches[i]` is not out-of-bounds. + if or(shr(64, j), gt(add(batch.offset, batch.length), e)) { + mstore(0x00, 0x3995943b) // `BatchOfBatchesDecodingError()`. + revert(0x1c, 0x04) + } + } + execute(mode, batch); + } + } + } + + /// @dev Executes the calls. + /// Reverts and bubbles up error if any call fails. + /// The `mode` and `executionData` are passed along in case there's a need to use them. + function _execute( + bytes32 mode, + bytes calldata executionData, + Call[] calldata calls, + bytes calldata opData + ) internal virtual { + // Silence compiler warning on unused variables. + mode = mode; + executionData = executionData; + // 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, extraData)` when you override this function. + return _execute(calls, bytes32(0)); + } + 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. + /// The `mode` and `executionData` are passed along in case there's a need to use them. + function _executeOptimizedBatch( + bytes32 mode, + bytes calldata executionData, + address to, + bytes[] calldata dataArr, + bytes calldata opData + ) internal virtual { + // Silence compiler warning on unused variables. + mode = mode; + executionData = executionData; + // 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 `_executeOptimizedBatch(to, dataArr, extraData)` + // when you override this function. + return _executeOptimizedBatch(to, dataArr, bytes32(0)); + } + 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. + /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). + function _execute(Call[] calldata calls, bytes32 extraData) internal virtual { + unchecked { + uint256 i; + if (calls.length == uint256(0)) return; + do { + (address to, uint256 value, bytes calldata data) = _get(calls, i); + _execute(to, value, data, extraData); + } while (++i != calls.length); + } + } + + /// @dev Executes the `dataArr`, with a common `to` address. + /// If any `to == address(0)`, it will be replaced with `address(this)`. + /// Value for all calls is zero. + /// Reverts and bubbles up error if any call fails. + /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). + function _executeOptimizedBatch(address to, bytes[] calldata dataArr, bytes32 extraData) + internal + virtual + { + unchecked { + uint256 i; + /// @solidity memory-safe-assembly + assembly { + let t := shr(96, shl(96, to)) + to := or(mul(address(), iszero(t)), t) + } + if (dataArr.length == uint256(0)) return; + do { + _execute(to, 0, _get(dataArr, i), extraData); + } while (++i != dataArr.length); + } + } + + /// @dev Executes the call. + /// Reverts and bubbles up error if any call fails. + /// `extraData` can be any supplementary data (e.g. a memory pointer, some hash). + function _execute(address to, uint256 value, bytes calldata data, bytes32 extraData) + internal + virtual + { + /// @solidity memory-safe-assembly + assembly { + extraData := extraData // Silence unused variable compiler warning. + let m := mload(0x40) // Grab the free memory pointer. + calldatacopy(m, data.offset, data.length) + if iszero(call(gas(), to, value, m, data.length, codesize(), 0x00)) { + // Bubble up the revert if the call reverts. + returndatacopy(m, 0x00, returndatasize()) + revert(m, returndatasize()) + } + } + } + + /// @dev Convenience function for getting `calls[i]`, without bounds checks. + function _get(Call[] calldata calls, uint256 i) + internal + view + virtual + returns (address to, uint256 value, bytes calldata data) + { + /// @solidity memory-safe-assembly + assembly { + let c := add(calls.offset, calldataload(add(calls.offset, shl(5, i)))) + // Replaces `to` with `address(this)` if `address(0)` is provided. + let t := shr(96, shl(96, calldataload(c))) + to := or(mul(address(), iszero(t)), t) + value := calldataload(add(c, 0x20)) + let o := add(c, calldataload(add(c, 0x40))) + data.offset := add(o, 0x20) + data.length := calldataload(o) + } + } + + /// @dev Convenience function for getting `dataArr[i]`, without bounds checks. + function _get(bytes[] calldata dataArr, uint256 i) + internal + view + virtual + returns (bytes calldata data) + { + /// @solidity memory-safe-assembly + assembly { + let c := add(dataArr.offset, calldataload(add(dataArr.offset, shl(5, i)))) + data.offset := add(c, 0x20) + data.length := calldataload(c) + } + } +} diff --git a/test/ext/ithaca/ERC7821.t.sol b/test/ext/ithaca/ERC7821.t.sol new file mode 100644 index 000000000..ab9236c26 --- /dev/null +++ b/test/ext/ithaca/ERC7821.t.sol @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "../../utils/SoladyTest.sol"; +import {ERC7821, MockERC7821} from "../../utils/mocks/ext/ithaca/MockERC7821.sol"; +import {LibClone} from "../../../src/utils/LibClone.sol"; + +contract ERC7821Test is SoladyTest { + error CustomError(); + + MockERC7821 mbe; + + address target; + + bytes32 internal constant _SUPPORTED_MODE = bytes10(0x01000000000078210001); + bytes32 internal constant _OPTIMIZED_BATCH_MODE = bytes10(0x01000000000078210003); + bytes[] internal _bytes; + + function setUp() public { + mbe = new MockERC7821(); + mbe.setAuthorizedCaller(address(this), true); + target = LibClone.clone(address(this)); + } + + function revertsWithCustomError() external payable { + revert CustomError(); + } + + function returnsBytes(bytes memory b) external payable returns (bytes memory) { + return b; + } + + function returnsHash(bytes memory b) external payable returns (bytes32) { + return keccak256(b); + } + + function testERC7821Gas() public { + vm.pauseGasMetering(); + vm.deal(address(this), 1 ether); + + ERC7821.Call[] memory calls = new ERC7821.Call[](2); + + calls[0].to = target; + calls[0].value = 123; + calls[0].data = abi.encodeWithSignature("returnsBytes(bytes)", "hehe"); + + calls[1].to = target; + calls[1].value = 789; + calls[1].data = abi.encodeWithSignature("returnsHash(bytes)", "lol"); + + bytes memory data = abi.encode(calls); + vm.resumeGasMetering(); + + mbe.execute{value: _totalValue(calls)}(_SUPPORTED_MODE, data); + } + + function testERC7821OptimizedBatchGas() public { + vm.pauseGasMetering(); + vm.deal(address(this), 1 ether); + + bytes[] memory dataArr = new bytes[](2); + + dataArr[0] = abi.encodeWithSignature("returnsBytes(bytes)", "hehe"); + dataArr[1] = abi.encodeWithSignature("returnsHash(bytes)", "lol"); + + bytes memory data = abi.encode(target, dataArr); + vm.resumeGasMetering(); + + mbe.execute(_OPTIMIZED_BATCH_MODE, data); + } + + function testERC7821(bytes memory opData) public { + vm.deal(address(this), 1 ether); + + ERC7821.Call[] memory calls = new ERC7821.Call[](2); + + calls[0].to = target; + calls[0].value = 123; + calls[0].data = abi.encodeWithSignature("returnsBytes(bytes)", "hehe"); + + calls[1].to = target; + calls[1].value = 789; + calls[1].data = abi.encodeWithSignature("returnsHash(bytes)", "lol"); + + mbe.execute{value: _totalValue(calls)}(_SUPPORTED_MODE, _encode(calls, opData)); + + assertEq(mbe.lastOpData(), opData); + } + + function testERC7821OptimizedBatch(bytes memory opData) public { + vm.deal(address(this), 1 ether); + + bytes[] memory dataArr = new bytes[](2); + dataArr[0] = abi.encodeWithSignature("returnsBytes(bytes)", "hehe"); + dataArr[1] = abi.encodeWithSignature("returnsHash(bytes)", "lol"); + + mbe.execute(_OPTIMIZED_BATCH_MODE, _encodeOptimizedBatch(target, dataArr, opData)); + + assertEq(mbe.lastOpData(), opData); + } + + function testERC7821ForRevert() public { + ERC7821.Call[] memory calls = new ERC7821.Call[](1); + calls[0].to = target; + calls[0].value = 0; + calls[0].data = abi.encodeWithSignature("revertsWithCustomError()"); + + vm.expectRevert(CustomError.selector); + mbe.execute{value: _totalValue(calls)}(_SUPPORTED_MODE, _encode(calls, "")); + } + + function testERC7821OptimizedBatchForRevert() public { + bytes[] memory dataArr = new bytes[](1); + dataArr[0] = abi.encodeWithSignature("revertsWithCustomError()"); + + vm.expectRevert(CustomError.selector); + mbe.execute(_OPTIMIZED_BATCH_MODE, _encodeOptimizedBatch(target, dataArr, "")); + } + + function _encode(ERC7821.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); + } + + function _encodeOptimizedBatch(address to, bytes[] memory dataArr, bytes memory opData) + internal + returns (bytes memory) + { + if (_randomChance(2) && opData.length == 0) return abi.encode(to, dataArr); + return abi.encode(to, dataArr, opData); + } + + struct Payload { + bytes data; + uint256 mode; + } + + function testERC7821(bytes32) public { + vm.deal(address(this), 1 ether); + + ERC7821.Call[] memory calls = new ERC7821.Call[](_randomUniform() & 3); + Payload[] memory payloads = new Payload[](calls.length); + + for (uint256 i; i < calls.length; ++i) { + calls[i].to = target; + calls[i].value = _randomUniform() & 0xff; + bytes memory data = _truncateBytes(_randomBytes(), 0x1ff); + payloads[i].data = data; + if (_randomChance(2)) { + payloads[i].mode = 0; + calls[i].data = abi.encodeWithSignature("returnsBytes(bytes)", data); + } else { + payloads[i].mode = 1; + calls[i].data = abi.encodeWithSignature("returnsHash(bytes)", data); + } + } + + mbe.executeDirect{value: _totalValue(calls)}(calls); + + if (calls.length != 0 && _randomChance(32)) { + calls[_randomUniform() % calls.length].data = + abi.encodeWithSignature("revertsWithCustomError()"); + vm.expectRevert(CustomError.selector); + mbe.executeDirect{value: _totalValue(calls)}(calls); + } + } + + function testERC7821OptimizedBatch(bytes32) public { + vm.deal(address(this), 1 ether); + + bytes[] memory dataArr = new bytes[](_randomUniform() & 3); + Payload[] memory payloads = new Payload[](dataArr.length); + + for (uint256 i; i < dataArr.length; ++i) { + bytes memory data = _truncateBytes(_randomBytes(), 0x1ff); + payloads[i].data = data; + if (_randomChance(2)) { + payloads[i].mode = 0; + dataArr[i] = abi.encodeWithSignature("returnsBytes(bytes)", data); + } else { + payloads[i].mode = 1; + dataArr[i] = abi.encodeWithSignature("returnsHash(bytes)", data); + } + } + + mbe.executeDirect(target, dataArr); + + if (dataArr.length != 0 && _randomChance(32)) { + dataArr[_randomUniform() % dataArr.length] = + abi.encodeWithSignature("revertsWithCustomError()"); + vm.expectRevert(CustomError.selector); + mbe.executeDirect(target, dataArr); + } + } + + function _totalValue(ERC7821.Call[] memory calls) internal pure returns (uint256 result) { + unchecked { + for (uint256 i; i < calls.length; ++i) { + result += calls[i].value; + } + } + } + + function testERC7821ExecuteBatchOfBatches() public { + bytes32 mode = bytes32(0x0100000000007821000200000000000000000000000000000000000000000000); + bytes[] memory batchBytes = new bytes[](3); + batchBytes[0] = hex"112233"; + batchBytes[1] = hex""; + batchBytes[2] = + hex"112233445566778899112233445566778899112233445566778899112233445566778899112233445566778899"; + bytes[] memory batches = new bytes[](batchBytes.length); + for (uint256 i; i < batches.length; ++i) { + batches[i] = _encodePushBytesBatch(batchBytes[i]); + } + mbe.execute(mode, abi.encode(batches)); + for (uint256 i; i < batches.length; ++i) { + assertEq(_bytes[i], batchBytes[i]); + } + assertEq(_bytes.length, batchBytes.length); + + // Test that batch of batches is executed with the correct `msg.sender`. + + address pranker = _randomUniqueHashedAddress(); + vm.startPrank(pranker); + + vm.expectRevert(MockERC7821.Unauthorized.selector); + mbe.execute(mode, abi.encode(batches)); + + mbe.setAuthorizedCaller(pranker, true); + mbe.execute(mode, abi.encode(batches)); + + assertEq(_bytes.length, batchBytes.length * 2); + + mbe.setAuthorizedCaller(pranker, false); + vm.expectRevert(MockERC7821.Unauthorized.selector); + mbe.execute(mode, abi.encode(batches)); + + assertEq(_bytes.length, batchBytes.length * 2); + + vm.stopPrank(); + } + + function _encodePushBytesBatch(bytes memory x) internal view returns (bytes memory) { + ERC7821.Call[] memory calls = new ERC7821.Call[](1); + calls[0].data = abi.encodeWithSignature("pushBytes(bytes)", x); + calls[0].to = address(this); + return abi.encode(calls); + } + + function pushBytes(bytes memory x) public { + _bytes.push(x); + } + + function testERC7821OptimizedBatchWithZeroAddress() public { + // Test that when to=address(0), it gets replaced with address(this) (the MockERC7821 contract) + // We'll call executeDirect which directly calls the internal _execute function + bytes[] memory dataArr = new bytes[](1); + dataArr[0] = + abi.encodeWithSignature("setAuthorizedCaller(address,bool)", address(0x123), true); + + // This should replace address(0) with address(mbe) and call setAuthorizedCaller on itself + mbe.executeDirect(address(0), dataArr); + + // Verify the call succeeded by checking that address(0x123) is now authorized + assertTrue(mbe.isAuthorizedCaller(address(0x123))); + } +} diff --git a/test/utils/mocks/ext/ithaca/MockERC7821.sol b/test/utils/mocks/ext/ithaca/MockERC7821.sol new file mode 100644 index 000000000..c60302bb1 --- /dev/null +++ b/test/utils/mocks/ext/ithaca/MockERC7821.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {ERC7821} from "../../../../../src/accounts/ext/ithaca/ERC7821.sol"; +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 MockERC7821 is ERC7821, Brutalizer { + bytes public lastOpData; + + mapping(address => bool) public isAuthorizedCaller; + + error Unauthorized(); + + function _execute(bytes32, bytes calldata, Call[] calldata calls, bytes calldata opData) + internal + virtual + override + { + lastOpData = opData; + _execute(calls, bytes32(0)); + } + + function _executeOptimizedBatch( + bytes32, + bytes calldata, + address to, + bytes[] calldata dataArr, + bytes calldata opData + ) internal virtual override { + lastOpData = opData; + _executeOptimizedBatch(to, dataArr, bytes32(0)); + } + + function execute(bytes32 mode, bytes calldata executionData) public payable virtual override { + if (!isAuthorizedCaller[msg.sender]) revert Unauthorized(); + super.execute(mode, executionData); + } + + function executeDirect(Call[] calldata calls) public payable virtual { + _misalignFreeMemoryPointer(); + _brutalizeMemory(); + _execute(calls, bytes32(0)); + _checkMemory(); + } + + function executeDirect(address to, bytes[] calldata dataArr) public payable virtual { + _misalignFreeMemoryPointer(); + _brutalizeMemory(); + _executeOptimizedBatch(to, dataArr, bytes32(0)); + _checkMemory(); + } + + function setAuthorizedCaller(address target, bool status) public { + isAuthorizedCaller[target] = status; + } +}