diff --git a/snapshots/BenchmarkTest.json b/snapshots/BenchmarkTest.json index f0523aef..88c1ccc8 100644 --- a/snapshots/BenchmarkTest.json +++ b/snapshots/BenchmarkTest.json @@ -16,10 +16,10 @@ "testERC20Transfer_ERC4337MinimalAccount": "171906", "testERC20Transfer_ERC4337MinimalAccount_AppSponsor": "168893", "testERC20Transfer_ERC4337MinimalAccount_ERC20SelfPay": "217488", - "testERC20Transfer_IthacaAccount": "129892", - "testERC20Transfer_IthacaAccountWithSpendLimits": "190799", - "testERC20Transfer_IthacaAccount_AppSponsor": "141312", - "testERC20Transfer_IthacaAccount_ERC20SelfPay": "147545", + "testERC20Transfer_IthacaAccount": "129938", + "testERC20Transfer_IthacaAccountWithSpendLimits": "190845", + "testERC20Transfer_IthacaAccount_AppSponsor": "141358", + "testERC20Transfer_IthacaAccount_ERC20SelfPay": "147591", "testERC20Transfer_Safe4337": "197515", "testERC20Transfer_Safe4337_AppSponsor": "191679", "testERC20Transfer_Safe4337_ERC20SelfPay": "238658", @@ -28,24 +28,24 @@ "testERC20Transfer_ZerodevKernel_ERC20SelfPay": "252683", "testERC20Transfer_batch100_AlchemyModularAccount": "10104466", "testERC20Transfer_batch100_AlchemyModularAccount_ERC20SelfPay": "11635798", - "testERC20Transfer_batch100_IthacaAccount": "7693196", - "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8392216", - "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7537848", + "testERC20Transfer_batch100_IthacaAccount": "7697796", + "testERC20Transfer_batch100_IthacaAccount_AppSponsor": "8396816", + "testERC20Transfer_batch100_IthacaAccount_ERC20SelfPay": "7542448", "testERC20Transfer_batch100_ZerodevKernel": "12626718", "testERC20Transfer_batch100_ZerodevKernel_ERC20SelfPay": "14176437", "testNativeTransfer_AlchemyModularAccount": "180829", "testNativeTransfer_CoinbaseSmartWallet": "178916", - "testNativeTransfer_IthacaAccount": "131294", - "testNativeTransfer_IthacaAccount_AppSponsor": "142757", - "testNativeTransfer_IthacaAccount_ERC20SelfPay": "156247", + "testNativeTransfer_IthacaAccount": "131340", + "testNativeTransfer_IthacaAccount_AppSponsor": "142803", + "testNativeTransfer_IthacaAccount_ERC20SelfPay": "156293", "testNativeTransfer_Safe4337": "198595", "testNativeTransfer_ZerodevKernel": "208635", "testUniswapV2Swap_AlchemyModularAccount": "238767", "testUniswapV2Swap_CoinbaseSmartWallet": "237571", "testUniswapV2Swap_ERC4337MinimalAccount": "231254", - "testUniswapV2Swap_IthacaAccount": "189178", - "testUniswapV2Swap_IthacaAccount_AppSponsor": "200586", - "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "211643", + "testUniswapV2Swap_IthacaAccount": "189224", + "testUniswapV2Swap_IthacaAccount_AppSponsor": "200632", + "testUniswapV2Swap_IthacaAccount_ERC20SelfPay": "211689", "testUniswapV2Swap_Safe4337": "257453", "testUniswapV2Swap_ZerodevKernel": "266487" } \ No newline at end of file diff --git a/src/GuardedExecutor.sol b/src/GuardedExecutor.sol index 4a3f96f5..6660d647 100644 --- a/src/GuardedExecutor.sol +++ b/src/GuardedExecutor.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.23; -import {ERC7821} from "solady/accounts/ERC7821.sol"; import {LibSort} from "solady/utils/LibSort.sol"; import {LibBytes} from "solady/utils/LibBytes.sol"; import {LibZip} from "solady/utils/LibZip.sol"; @@ -13,6 +12,7 @@ import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; import {DateTimeLib} from "solady/utils/DateTimeLib.sol"; import {ICallChecker} from "./interfaces/ICallChecker.sol"; +import {ERC7821Ithaca as ERC7821} from "./libraries/ERC7821Ithaca.sol"; /// @title GuardedExecutor /// @notice Mixin for spend limits and calldata execution guards. diff --git a/src/IthacaAccount.sol b/src/IthacaAccount.sol index 0e1115be..8f36115d 100644 --- a/src/IthacaAccount.sol +++ b/src/IthacaAccount.sol @@ -485,6 +485,38 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { return isMultichain ? _hashTypedDataSansChainId(structHash) : _hashTypedData(structHash); } + function computeDigest(CallSansTo[] calldata calls, address to, uint256 nonce) + public + view + virtual + returns (bytes32 result) + { + // If to is 0, it will be replaced with address(this) + assembly ("memory-safe") { + let t := shr(96, shl(96, to)) + to := or(mul(address(), iszero(t)), t) + } + + bytes32[] memory a = EfficientHashLib.malloc(calls.length); + for (uint256 i; i < calls.length; ++i) { + (uint256 value, bytes calldata data) = _get(calls, i); + a.set( + i, + EfficientHashLib.hash( + CALL_TYPEHASH, + bytes32(uint256(uint160(to))), + bytes32(value), + EfficientHashLib.hashCalldata(data) + ) + ); + } + bool isMultichain = nonce >> 240 == MULTICHAIN_NONCE_PREFIX; + bytes32 structHash = EfficientHashLib.hash( + uint256(EXECUTE_TYPEHASH), LibBit.toUint(isMultichain), uint256(a.hash()), nonce + ); + return isMultichain ? _hashTypedDataSansChainId(structHash) : _hashTypedData(structHash); + } + /// @dev Returns if the signature is valid, along with its `keyHash`. /// The `signature` is a wrapped signature, given by /// `abi.encodePacked(bytes(innerSignature), bytes32(keyHash), bool(prehash))`. @@ -675,6 +707,49 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { // ERC7821 //////////////////////////////////////////////////////////////////////// + function _execute( + bytes32, + bytes calldata, + address to, + CallSansTo[] calldata calls, + bytes calldata opData + ) internal virtual override { + // Orchestrator workflow. + if (msg.sender == ORCHESTRATOR) { + // opdata + // 0x00: keyHash + if (opData.length != 0x20) revert OpDataError(); + bytes32 _keyHash = LibBytes.loadCalldata(opData, 0x00); + + LibTStack.TStack(_KEYHASH_STACK_TRANSIENT_SLOT).push(_keyHash); + _execute(calls, to, _keyHash); + LibTStack.TStack(_KEYHASH_STACK_TRANSIENT_SLOT).pop(); + + return; + } + + // Simple workflow without `opData`. + if (opData.length == uint256(0)) { + if (msg.sender != address(this)) revert Unauthorized(); + return _execute(calls, to, bytes32(0)); + } + + // Simple workflow with `opData`. + if (opData.length < 0x20) revert OpDataError(); + uint256 nonce = uint256(LibBytes.loadCalldata(opData, 0x00)); + LibNonce.checkAndIncrement(_getAccountStorage().nonceSeqs, nonce); + emit NonceInvalidated(nonce); + + (bool isValid, bytes32 keyHash) = unwrapAndValidateSignature( + computeDigest(calls, to, nonce), LibBytes.sliceCalldata(opData, 0x20) + ); + + if (!isValid) revert Unauthorized(); + LibTStack.TStack(_KEYHASH_STACK_TRANSIENT_SLOT).push(keyHash); + _execute(calls, to, keyHash); + LibTStack.TStack(_KEYHASH_STACK_TRANSIENT_SLOT).pop(); + } + /// @dev For ERC7821. function _execute(bytes32, bytes calldata, Call[] calldata calls, bytes calldata opData) internal @@ -711,8 +786,6 @@ contract IthacaAccount is IIthacaAccount, EIP712, GuardedExecutor { computeDigest(calls, nonce), LibBytes.sliceCalldata(opData, 0x20) ); if (!isValid) revert Unauthorized(); - - // TODO: Figure out where else to add these operations, after removing delegate call. LibTStack.TStack(_KEYHASH_STACK_TRANSIENT_SLOT).push(keyHash); _execute(calls, keyHash); LibTStack.TStack(_KEYHASH_STACK_TRANSIENT_SLOT).pop(); diff --git a/src/libraries/ERC7821Ithaca.sol b/src/libraries/ERC7821Ithaca.sol new file mode 100644 index 00000000..5f726b42 --- /dev/null +++ b/src/libraries/ERC7821Ithaca.sol @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {Receiver} from "solady/accounts/Receiver.sol"; + +/// @notice Minimal batch executor mixin. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/accounts/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 ERC7821Ithaca 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. + } + + struct CallSansTo { + 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(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* 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 _executeBatchCommonTo(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. + /// @dev if to == address(0), it will be replaced with address(this) + /// Execution Data: abi.encode(address to, CallSansTo[] calls, bytes opData) + function _executeBatchCommonTo(bytes32 mode, bytes calldata executionData) internal virtual { + address to; + CallSansTo[] calldata calls; + bytes calldata opData; + + /// @solidity memory-safe-assembly + assembly { + to := calldataload(executionData.offset) + + let callOffset := + add(executionData.offset, calldataload(add(0x20, executionData.offset))) + calls.offset := add(callOffset, 0x20) + calls.length := calldataload(callOffset) + + // This line is needed to ensure that opdata is valid in all code paths. + // Otherwise the compiler complains. + opData.length := 0 + // If the offset of `executionData` allows for `opData`, and the mode supports it. + if gt(calldataload(add(0x20, executionData.offset)), 0x40) { + let opDataOffset := + add(executionData.offset, calldataload(add(0x40, executionData.offset))) + opData.offset := add(opDataOffset, 0x20) + opData.length := calldataload(opDataOffset) + } + } + + _execute(mode, executionData, to, calls, 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 _execute( + bytes32 mode, + bytes calldata executionData, + address to, + CallSansTo[] 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, to, 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 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(CallSansTo[] calldata calls, address to, bytes32 keyHash) internal virtual { + unchecked { + uint256 i; + // If `to` is address(0), it will be replaced with address(this) + /// @solidity memory-safe-assembly + assembly { + let t := shr(96, shl(96, to)) + to := or(mul(address(), iszero(t)), t) + } + if (calls.length == uint256(0)) return; + do { + (uint256 value, bytes calldata data) = _get(calls, i); + _execute(to, value, data, keyHash); + } while (++i != calls.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(CallSansTo[] calldata calls, uint256 i) + internal + view + virtual + returns (uint256 value, bytes calldata data) + { + /// @solidity memory-safe-assembly + assembly { + let c := add(calls.offset, calldataload(add(calls.offset, shl(5, i)))) + value := calldataload(c) + let o := add(c, calldataload(add(c, 0x20))) + data.offset := add(o, 0x20) + data.length := calldataload(o) + } + } + + /// @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) + } + } +} diff --git a/test/Account.t.sol b/test/Account.t.sol index c4e42a2f..602f72dd 100644 --- a/test/Account.t.sol +++ b/test/Account.t.sol @@ -68,6 +68,125 @@ contract AccountTest is BaseTest { } } + struct _TestExecuteWithCallSansToTemps { + TargetFunctionPayload[] targetFunctionPayloads; + ERC7821.CallSansTo[] calls; + address to; + uint256 n; + uint256 nonce; + bytes opData; + bytes executionData; + } + + function testExecuteWithCallSansTo(bytes32) public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + vm.deal(d.eoa, 100 ether); + + _TestExecuteWithCallSansToTemps memory t; + t.n = _bound(_randomUniform(), 1, 5); + t.targetFunctionPayloads = new TargetFunctionPayload[](t.n); + t.calls = new ERC7821.CallSansTo[](t.n); + t.to = address(this); + + for (uint256 i; i < t.n; ++i) { + uint256 value = _random() % 0.1 ether; + bytes memory data = _truncateBytes(_randomBytes(), 0xff); + t.calls[i] = + ERC7821.CallSansTo(value, abi.encodeWithSignature("targetFunction(bytes)", data)); + t.targetFunctionPayloads[i].value = value; + t.targetFunctionPayloads[i].data = data; + } + + t.nonce = d.d.getNonce(0); + bytes memory signature = _sig(d, d.d.computeDigest(t.calls, t.to, t.nonce)); + t.opData = abi.encodePacked(t.nonce, signature); + t.executionData = abi.encode(t.to, t.calls, t.opData); + + // Negative test: wrong signature (32/256 chance) + if (_randomChance(32)) { + bytes memory wrongSignature = + _sig(_randomEIP7702DelegatedEOA(), d.d.computeDigest(t.calls, t.to, t.nonce)); + t.opData = abi.encodePacked(t.nonce, wrongSignature); + t.executionData = abi.encode(t.to, t.calls, t.opData); + vm.expectRevert(bytes4(keccak256("Unauthorized()"))); + d.d.execute(_ERC7821_BATCH_SANS_TO_EXECUTION_MODE, t.executionData); + return; + } + + d.d.execute(_ERC7821_BATCH_SANS_TO_EXECUTION_MODE, t.executionData); + + assertEq(targetFunctionPayloads.length, t.n); + for (uint256 i; i < t.n; ++i) { + assertEq(targetFunctionPayloads[i].by, d.eoa); + assertEq(targetFunctionPayloads[i].value, t.targetFunctionPayloads[i].value); + assertEq(targetFunctionPayloads[i].data, t.targetFunctionPayloads[i].data); + } + } + + function testExecuteWithCallSansToWrongNonce() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + vm.deal(d.eoa, 100 ether); + + _TestExecuteWithCallSansToTemps memory t; + t.n = 1; + t.targetFunctionPayloads = new TargetFunctionPayload[](t.n); + t.calls = new ERC7821.CallSansTo[](t.n); + t.to = address(this); + + t.calls[0] = ERC7821.CallSansTo(0, abi.encodeWithSignature("targetFunction(bytes)", "test")); + + t.nonce = d.d.getNonce(0); + uint256 wrongNonce = t.nonce + 1; + bytes memory signature = _sig(d, d.d.computeDigest(t.calls, t.to, wrongNonce)); + t.opData = abi.encodePacked(wrongNonce, signature); + t.executionData = abi.encode(t.to, t.calls, t.opData); + + vm.expectRevert(); // Should revert due to invalid nonce + d.d.execute(_ERC7821_BATCH_SANS_TO_EXECUTION_MODE, t.executionData); + } + + function testExecuteWithCallSansToWrongDigest() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + vm.deal(d.eoa, 100 ether); + + _TestExecuteWithCallSansToTemps memory t; + t.n = 1; + t.targetFunctionPayloads = new TargetFunctionPayload[](t.n); + t.calls = new ERC7821.CallSansTo[](t.n); + t.to = address(this); + + t.calls[0] = ERC7821.CallSansTo(0, abi.encodeWithSignature("targetFunction(bytes)", "test")); + + t.nonce = d.d.getNonce(0); + // Sign with wrong to address + address wrongTo = address(0x123); + bytes memory signature = _sig(d, d.d.computeDigest(t.calls, wrongTo, t.nonce)); + t.opData = abi.encodePacked(t.nonce, signature); + t.executionData = abi.encode(t.to, t.calls, t.opData); + + vm.expectRevert(bytes4(keccak256("Unauthorized()"))); + d.d.execute(_ERC7821_BATCH_SANS_TO_EXECUTION_MODE, t.executionData); + } + + function testExecuteWithCallSansToInvalidOpData() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + vm.deal(d.eoa, 100 ether); + + _TestExecuteWithCallSansToTemps memory t; + t.n = 1; + t.calls = new ERC7821.CallSansTo[](t.n); + t.to = address(this); + + t.calls[0] = ERC7821.CallSansTo(0, abi.encodeWithSignature("targetFunction(bytes)", "test")); + + // Test with opData too short (less than 32 bytes for nonce) + t.opData = hex"1234"; // Only 2 bytes + t.executionData = abi.encode(t.to, t.calls, t.opData); + + vm.expectRevert(bytes4(keccak256("OpDataError()"))); + d.d.execute(_ERC7821_BATCH_SANS_TO_EXECUTION_MODE, t.executionData); + } + function testSignatureCheckerApproval(bytes32) public { DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); PassKey memory k = _randomSecp256k1PassKey(); @@ -377,4 +496,37 @@ contract AccountTest is BaseTest { uint256 keysCount137 = IthacaAccount(eoaAddress).keyCount(); assertEq(keysCount137, 2, "Keys should be added on chain 137"); } + + function testCommonToZeroAddressReplacement() public { + DelegatedEOA memory d = _randomEIP7702DelegatedEOA(); + vm.deal(d.eoa, 100 ether); + + // Test that address(0) gets replaced with address(this) by comparing digest computation + // First, create calls with explicit address(this) + ERC7821.CallSansTo[] memory calls = new ERC7821.CallSansTo[](1); + calls[0] = ERC7821.CallSansTo(1 ether, ""); + + uint256 nonce = d.d.getNonce(0); + + // Compute digest with explicit address(this) + bytes32 digestExplicit = d.d.computeDigest(calls, address(d.d), nonce); + + // Compute digest with address(0) - should be the same due to replacement + bytes32 digestZero = d.d.computeDigest(calls, address(0), nonce); + + // If address(0) replacement is working, these digests should be identical + assertEq( + digestExplicit, + digestZero, + "Digest with address(0) should equal digest with address(this)" + ); + + // Additionally, test that the execution works + bytes memory signature = _sig(d, digestZero); + bytes memory opData = abi.encodePacked(nonce, signature); + bytes memory executionData = abi.encode(address(0), calls, opData); + + // This should succeed without reverting (proving the replacement works in execution too) + d.d.execute(_ERC7821_BATCH_SANS_TO_EXECUTION_MODE, executionData); + } } diff --git a/test/Base.t.sol b/test/Base.t.sol index 1765a651..4e353a9a 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.4; import "./utils/SoladyTest.sol"; import {EIP7702Proxy} from "solady/accounts/EIP7702Proxy.sol"; import {LibEIP7702} from "solady/accounts/LibEIP7702.sol"; -import {ERC7821} from "solady/accounts/ERC7821.sol"; import {LibERC7579} from "solady/accounts/LibERC7579.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {EfficientHashLib} from "solady/utils/EfficientHashLib.sol"; @@ -24,6 +23,8 @@ import {IOrchestrator} from "../src/interfaces/IOrchestrator.sol"; import {Simulator} from "../src/Simulator.sol"; import {ICommon} from "../src/interfaces/ICommon.sol"; +import {ERC7821Ithaca as ERC7821} from "../src/libraries/ERC7821Ithaca.sol"; + contract BaseTest is SoladyTest { using LibRLP for LibRLP.List; @@ -55,6 +56,9 @@ contract BaseTest is SoladyTest { bytes32 internal constant _ERC7821_BATCH_EXECUTION_MODE = 0x0100000000007821000100000000000000000000000000000000000000000000; + bytes32 internal constant _ERC7821_BATCH_SANS_TO_EXECUTION_MODE = + 0x0100000000007821000300000000000000000000000000000000000000000000; + bytes32 internal constant _ERC7579_DELEGATE_CALL_MODE = 0xff00000000000000000000000000000000000000000000000000000000000000;