diff --git a/script/DeployEIP7702BatchDeleGator.s.sol b/script/DeployEIP7702BatchDeleGator.s.sol new file mode 100644 index 00000000..077bc765 --- /dev/null +++ b/script/DeployEIP7702BatchDeleGator.s.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; +import { IEntryPoint } from "@account-abstraction/interfaces/IEntryPoint.sol"; + +import { EIP7702BatchDeleGator } from "../src/EIP7702/EIP7702BatchDeleGator.sol"; +import { EIP7702BatchDeleGatorBeacon } from "../src/EIP7702/EIP7702BatchDeleGatorBeacon.sol"; +import { EIP7702BatchDeleGatorProxy } from "../src/EIP7702/EIP7702BatchDeleGatorProxy.sol"; +import { DeleGatorBatchRelayCoordinator } from "../src/DeleGatorBatchRelayCoordinator.sol"; +import { IDelegationManager } from "../src/interfaces/IDelegationManager.sol"; + +/** + * @title DeployEIP7702BatchDeleGator + * @notice Deploys EIP7702BatchDeleGator, optional beacon/proxy, and the batch relay coordinator. + * @dev Does not broadcast by default. Run with `--broadcast` only when ready to deploy. + * @dev Required env: + * - SALT + * - ENTRYPOINT_ADDRESS + * - DELEGATION_MANAGER_ADDRESS + * @dev Optional env: + * - BEACON_OWNER (defaults to deployer) + * - DEPLOY_PROXY=true|false (defaults to true) + */ +contract DeployEIP7702BatchDeleGator is Script { + bytes32 internal salt; + IEntryPoint internal entryPoint; + IDelegationManager internal delegationManager; + address internal deployer; + address internal beaconOwner; + bool internal deployProxy; + + function setUp() public { + salt = bytes32(abi.encodePacked(vm.envString("SALT"))); + entryPoint = IEntryPoint(vm.envAddress("ENTRYPOINT_ADDRESS")); + delegationManager = IDelegationManager(vm.envAddress("DELEGATION_MANAGER_ADDRESS")); + deployer = msg.sender; + beaconOwner = vm.envOr("BEACON_OWNER", deployer); + deployProxy = vm.envOr("DEPLOY_PROXY", true); + + console2.log("~~~ DeployEIP7702BatchDeleGator ~~~"); + console2.log("Deployer: %s", deployer); + console2.log("Entry Point: %s", address(entryPoint)); + console2.log("Delegation Manager: %s", address(delegationManager)); + console2.log("Beacon Owner: %s", beaconOwner); + console2.log("Deploy Proxy: %s", deployProxy); + console2.log("Salt:"); + console2.logBytes32(salt); + } + + function run() public { + vm.startBroadcast(); + + address implementation = address( + new EIP7702BatchDeleGator{ salt: salt }(delegationManager, entryPoint) + ); + console2.log("EIP7702BatchDeleGatorImpl: %s", implementation); + + address authorizationTarget = implementation; + address beacon; + address proxy; + + if (deployProxy) { + beacon = address(new EIP7702BatchDeleGatorBeacon{ salt: salt }(implementation, beaconOwner)); + console2.log("EIP7702BatchDeleGatorBeacon: %s", beacon); + + proxy = address(new EIP7702BatchDeleGatorProxy{ salt: salt }(beacon)); + console2.log("EIP7702BatchDeleGatorProxy: %s", proxy); + + authorizationTarget = proxy; + } + + address coordinator = address(new DeleGatorBatchRelayCoordinator{ salt: salt }()); + console2.log("DeleGatorBatchRelayCoordinator: %s", coordinator); + + console2.log("~~~ Release Metadata ~~~"); + console2.log("authorizationTarget: %s", authorizationTarget); + console2.log("implementation: %s", implementation); + console2.log("beacon: %s", beacon); + console2.log("proxy: %s", proxy); + console2.log("coordinator: %s", coordinator); + console2.log("eip712Name: EIP7702BatchDeleGator"); + console2.log("eip712Version: 1"); + console2.log("contractVersion: %s", EIP7702BatchDeleGator(payable(implementation)).VERSION()); + + vm.stopBroadcast(); + } +} diff --git a/src/DeleGatorBatchRelayCoordinator.sol b/src/DeleGatorBatchRelayCoordinator.sol new file mode 100644 index 00000000..709f73b0 --- /dev/null +++ b/src/DeleGatorBatchRelayCoordinator.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IDeleGatorBatchRelayCoordinator } from "./interfaces/IDeleGatorBatchRelayCoordinator.sol"; +import { IEIP7702BatchDeleGator } from "./interfaces/IEIP7702BatchDeleGator.sol"; + +/** + * @title DeleGatorBatchRelayCoordinator + * @notice Permissionless multi-account coordinator for signed batch DeleGator relay execution. + * @dev Non-atomic by default: a failed account row is recorded and later rows still execute. + * @dev Does not forward ETH and does not authorize child account execution by itself. + */ +contract DeleGatorBatchRelayCoordinator is IDeleGatorBatchRelayCoordinator { + uint256 internal constant MAX_REVERT_DATA = 256; + + /// @dev Emitted for each coordinator row after execution attempt. + event BatchRowExecuted(uint256 indexed index, address indexed account, bool success, bytes revertData); + + /// @inheritdoc IDeleGatorBatchRelayCoordinator + function executeBatches(AccountBatch[] calldata batches) external { + uint256 len = batches.length; + for (uint256 i = 0; i < len;) { + AccountBatch calldata batch = batches[i]; + + (bool success, bytes memory revertData) = address(batch.account).call( + abi.encodeWithSelector(IEIP7702BatchDeleGator.executeBatch.selector, batch.mode, batch.executionData) + ); + + if (!success && revertData.length > MAX_REVERT_DATA) { + revertData = abi.encodePacked(keccak256(revertData)); + } + + emit BatchRowExecuted(i, batch.account, success, success ? bytes("") : revertData); + + unchecked { + ++i; + } + } + } +} diff --git a/src/EIP7702/EIP7702BatchDeleGator.sol b/src/EIP7702/EIP7702BatchDeleGator.sol new file mode 100644 index 00000000..604c510f --- /dev/null +++ b/src/EIP7702/EIP7702BatchDeleGator.sol @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { IEntryPoint } from "@account-abstraction/interfaces/IEntryPoint.sol"; +import { Execution } from "@erc7579/interfaces/IERC7579Account.sol"; + +import { EIP7702DeleGatorCore } from "./EIP7702DeleGatorCore.sol"; +import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; +import { IEIP7702BatchDeleGator } from "../interfaces/IEIP7702BatchDeleGator.sol"; +import { ERC1271Lib } from "../libraries/ERC1271Lib.sol"; +import { BatchAuthorizationLib } from "../libraries/BatchAuthorizationLib.sol"; + +/** + * @title EIP7702BatchDeleGator + * @notice Stateful EIP-7702 DeleGator with ERC-7821 signed relay batches and unordered nonce replay protection. + * @dev Standard ERC-7579 execution remains on inherited `execute(ModeCode,bytes)` with EntryPoint/self access control. + * @dev Signed relay batches use the child-only `executeBatch(bytes32,bytes)` entrypoint. + */ +contract EIP7702BatchDeleGator is EIP7702DeleGatorCore, IEIP7702BatchDeleGator { + using BatchAuthorizationLib for Execution[]; + + ////////////////////////////// Constants ////////////////////////////// + + /// @dev The name of the contract used in the EIP-712 domain. + string public constant NAME = "EIP7702BatchDeleGator"; + + /// @dev The version used in the domainSeparator for EIP712. + string public constant DOMAIN_VERSION = "1"; + + /// @dev The semantic version of the contract. + string public constant VERSION = "1.0.0"; + + /// @dev Single batch, revert on failure — `abi.encode(Execution[])` only. + bytes32 public constant MODE_BATCH_SIMPLE = + bytes32(uint256(0x0100000000000000000000000000000000000000000000000000000000000000)); + + /// @dev Single batch with optional `opData` — `abi.encode(Execution[], bytes)`. + bytes32 public constant MODE_BATCH_WITH_OPDATA = + bytes32(uint256(0x0100000000007821000100000000000000000000000000000000000000000000)); + + /// @dev Nested signed batches — `abi.encode(bytes[])`. + bytes32 public constant MODE_BATCH_OF_BATCHES = + bytes32(uint256(0x0100000000007821000200000000000000000000000000000000000000000000)); + + /// @custom:storage-location erc7201:DeleGator.EIP7702BatchDeleGator.nonce + bytes32 private constant NONCE_STORAGE_LOCATION = + 0x1093877edb0cc0e2b2ea60a70fdf07c1dd8a109e13f7d461cf4b95c014189900; + + ////////////////////////////// Storage ////////////////////////////// + + struct NonceStorage { + /// @dev Bitmap of used relay nonces. Nonce word is `nonce >> 8`; bit is `uint8(nonce)`. + mapping(uint256 word => uint256 bitmap) nonceBitmap; + } + + ////////////////////////////// Events ////////////////////////////// + + event NonceInvalidated(uint256 indexed nonce); + event NoncesInvalidated(uint256 indexed word, uint256 mask); + + ////////////////////////////// Errors ////////////////////////////// + + error UnsupportedBatchExecutionMode(); + error UnauthorizedBatchExecuteCaller(); + error UnauthorizedRelayer(); + error InvalidBatchSignature(); + error BatchAuthorizationExpired(); + error NonceAlreadyUsed(); + + ////////////////////////////// Constructor ////////////////////////////// + + /** + * @notice Constructor for the EIP7702Batch DeleGator. + * @param _delegationManager Address of the trusted DelegationManager contract. + * @param _entryPoint Address of the EntryPoint contract. + */ + constructor(IDelegationManager _delegationManager, IEntryPoint _entryPoint) + EIP7702DeleGatorCore(_delegationManager, _entryPoint, NAME, DOMAIN_VERSION) + { } + + ////////////////////////////// External Methods ////////////////////////////// + + /// @inheritdoc IEIP7702BatchDeleGator + function executeBatch(bytes32 mode, bytes calldata executionData) external payable { + _routeBatchCalldata(mode, executionData); + } + + /// @inheritdoc IEIP7702BatchDeleGator + function supportsBatchExecutionMode(bytes32 mode) external pure returns (bool) { + return _batchExecutionModeId(mode) != 0; + } + + /// @inheritdoc IEIP7702BatchDeleGator + function hashBatchAuthorizationWithNonce( + Execution[] calldata executions, + uint256 nonce, + uint256 deadline, + address relayer + ) + external + view + returns (bytes32) + { + return _hashBatchAuthorizationWithNonce(executions, nonce, deadline, relayer); + } + + /// @inheritdoc IEIP7702BatchDeleGator + function isNonceUsed(uint256 nonce) external view returns (bool) { + (uint256 word, uint256 mask) = _nonceWordAndMask(nonce); + return _nonceStorage().nonceBitmap[word] & mask != 0; + } + + /// @inheritdoc IEIP7702BatchDeleGator + function nonceBitmap(uint256 word) external view returns (uint256 bitmap) { + return _nonceStorage().nonceBitmap[word]; + } + + /// @inheritdoc IEIP7702BatchDeleGator + function invalidateNonce(uint256 nonce) external onlyEntryPointOrSelf { + _consumeNonce(nonce); + emit NonceInvalidated(nonce); + } + + /// @inheritdoc IEIP7702BatchDeleGator + function invalidateNonces(uint256 word, uint256 mask) external onlyEntryPointOrSelf { + _nonceStorage().nonceBitmap[word] |= mask; + emit NoncesInvalidated(word, mask); + } + + ////////////////////////////// Internal Methods ////////////////////////////// + + /** + * @notice Verifies relay signatures against the delegated EOA address. + * @param _hash The data signed. + * @param _signature A 65-byte signature produced by the EIP7702 EOA. + */ + function _isValidSignature(bytes32 _hash, bytes calldata _signature) internal view override returns (bytes4) { + if (ECDSA.recover(_hash, _signature) == address(this)) return ERC1271Lib.EIP1271_MAGIC_VALUE; + + return ERC1271Lib.SIG_VALIDATION_FAILED; + } + + /// @dev Mode id: 0 invalid, 1 simple batch, 2 batch + optional opData, 3 batch-of-batches. + function _batchExecutionModeId(bytes32 mode) internal pure returns (uint256 id) { + /// @solidity memory-safe-assembly + assembly { + let m := and(shr(mul(22, 8), mode), 0xffff00000000ffffffff) + id := eq(m, 0x01000000000000000000) + id := or(shl(1, eq(m, 0x01000000000078210001)), id) + id := or(mul(3, eq(m, 0x01000000000078210002)), id) + } + } + + function _routeBatchCalldata(bytes32 mode, bytes calldata executionData) internal { + uint256 id = _batchExecutionModeId(mode); + if (id == 0) revert UnsupportedBatchExecutionMode(); + + if (id == 3) { + mode ^= bytes32(uint256(3 << (22 * 8))); + bytes[] memory batches = abi.decode(executionData, (bytes[])); + uint256 n = batches.length; + for (uint256 i = 0; i < n;) { + _routeBatchMemory(mode, batches[i]); + unchecked { + ++i; + } + } + return; + } + + Execution[] memory executions; + bytes memory opData; + + if (id == 2) { + (executions, opData) = abi.decode(executionData, (Execution[], bytes)); + } else { + executions = abi.decode(executionData, (Execution[])); + opData = ""; + } + + _authorizeAndExecuteBatch(executions, opData); + } + + function _routeBatchMemory(bytes32 mode, bytes memory executionData) internal { + uint256 id = _batchExecutionModeId(mode); + if (id == 0) revert UnsupportedBatchExecutionMode(); + + if (id == 3) { + mode ^= bytes32(uint256(3 << (22 * 8))); + bytes[] memory batches = abi.decode(executionData, (bytes[])); + uint256 n = batches.length; + for (uint256 i = 0; i < n;) { + _routeBatchMemory(mode, batches[i]); + unchecked { + ++i; + } + } + return; + } + + Execution[] memory executions; + bytes memory opData; + + if (id == 2) { + (executions, opData) = abi.decode(executionData, (Execution[], bytes)); + } else { + executions = abi.decode(executionData, (Execution[])); + opData = ""; + } + + _authorizeAndExecuteBatch(executions, opData); + } + + function _authorizeAndExecuteBatch(Execution[] memory executions, bytes memory opData) internal { + if (opData.length != 0) { + _verifyBatchAuthorization(executions, opData); + } else if (msg.sender != address(this)) { + revert UnauthorizedBatchExecuteCaller(); + } + + _executeExecutions(executions); + } + + function _verifyBatchAuthorization(Execution[] memory executions, bytes memory opData) internal { + (uint256 nonce, uint256 deadline, address relayer, bytes memory signature) = + abi.decode(opData, (uint256, uint256, address, bytes)); + + if (block.timestamp > deadline) revert BatchAuthorizationExpired(); + if (relayer != address(0) && relayer != msg.sender) revert UnauthorizedRelayer(); + + (uint256 word, uint256 mask) = _nonceWordAndMask(nonce); + NonceStorage storage $ = _nonceStorage(); + uint256 bitmap = $.nonceBitmap[word]; + if (bitmap & mask != 0) revert NonceAlreadyUsed(); + + bytes32 callsDigest = BatchAuthorizationLib.executionsDigest(executions); + bytes32 structHash = BatchAuthorizationLib.batchAuthorizationWithNonceStructHash(callsDigest, nonce, deadline, relayer); + bytes32 digest = _hashTypedDataV4(structHash); + + address recovered = ECDSA.recover(digest, signature); + if (recovered != address(this)) revert InvalidBatchSignature(); + + $.nonceBitmap[word] = bitmap | mask; + } + + function _hashBatchAuthorizationWithNonce( + Execution[] calldata executions, + uint256 nonce, + uint256 deadline, + address relayer + ) + internal + view + returns (bytes32) + { + bytes32 callsDigest = BatchAuthorizationLib.executionsDigestCalldata(executions); + bytes32 structHash = BatchAuthorizationLib.batchAuthorizationWithNonceStructHash(callsDigest, nonce, deadline, relayer); + return _hashTypedDataV4(structHash); + } + + function _executeExecutions(Execution[] memory executions) internal { + uint256 n = executions.length; + for (uint256 i = 0; i < n;) { + Execution memory execution = executions[i]; + address target = execution.target == address(0) ? address(this) : execution.target; + bytes memory callData = execution.callData; + bool ok; + + /// @solidity memory-safe-assembly + assembly { + ok := call(gas(), target, mload(add(execution, 0x20)), add(callData, 0x20), mload(callData), 0, 0) + if iszero(ok) { + let ptr := mload(0x40) + let size := returndatasize() + returndatacopy(ptr, 0, size) + revert(ptr, size) + } + } + + unchecked { + ++i; + } + } + } + + function _nonceWordAndMask(uint256 nonce) internal pure returns (uint256 word, uint256 mask) { + word = nonce >> 8; + mask = 1 << uint8(nonce); + } + + function _consumeNonce(uint256 nonce) internal { + (uint256 word, uint256 mask) = _nonceWordAndMask(nonce); + NonceStorage storage $ = _nonceStorage(); + uint256 bitmap = $.nonceBitmap[word]; + if (bitmap & mask != 0) revert NonceAlreadyUsed(); + $.nonceBitmap[word] = bitmap | mask; + } + + function _nonceStorage() private pure returns (NonceStorage storage $) { + /// @solidity memory-safe-assembly + assembly { + $.slot := NONCE_STORAGE_LOCATION + } + } +} diff --git a/src/EIP7702/EIP7702BatchDeleGatorBeacon.sol b/src/EIP7702/EIP7702BatchDeleGatorBeacon.sol new file mode 100644 index 00000000..9a29ec65 --- /dev/null +++ b/src/EIP7702/EIP7702BatchDeleGatorBeacon.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; + +/// @notice Upgrade beacon for EIP7702BatchDeleGator implementations. +contract EIP7702BatchDeleGatorBeacon is UpgradeableBeacon { + constructor(address implementation_, address initialOwner) UpgradeableBeacon(implementation_, initialOwner) { } +} diff --git a/src/EIP7702/EIP7702BatchDeleGatorProxy.sol b/src/EIP7702/EIP7702BatchDeleGatorProxy.sol new file mode 100644 index 00000000..9fe66370 --- /dev/null +++ b/src/EIP7702/EIP7702BatchDeleGatorProxy.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IBeacon } from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; +import { Proxy } from "@openzeppelin/contracts/proxy/Proxy.sol"; + +/** + * @title EIP7702BatchDeleGatorProxy + * @notice Stable EIP-7702 delegation target for EIP7702BatchDeleGator implementations. + * @dev Users authorize this proxy address once. The beacon address is immutable bytecode data, + * not account storage, so the proxy can safely run from an EIP-7702 delegated EOA. + */ +contract EIP7702BatchDeleGatorProxy is Proxy { + IBeacon private immutable _beacon; + + error InvalidBeacon(address beacon); + error InvalidBeaconImplementation(address implementation); + + constructor(address beacon_) payable { + if (beacon_.code.length == 0) revert InvalidBeacon(beacon_); + + address implementation_ = IBeacon(beacon_).implementation(); + if (implementation_.code.length == 0) revert InvalidBeaconImplementation(implementation_); + + _beacon = IBeacon(beacon_); + } + + /// @notice Returns the immutable beacon used by this delegation proxy. + function beacon() external view returns (address) { + return address(_beacon); + } + + /// @notice Returns the implementation currently selected by the beacon. + function implementation() external view returns (address) { + return _implementation(); + } + + receive() external payable virtual { + _fallback(); + } + + function _implementation() internal view override returns (address implementation_) { + implementation_ = _beacon.implementation(); + if (implementation_.code.length == 0) revert InvalidBeaconImplementation(implementation_); + } +} diff --git a/src/interfaces/IDeleGatorBatchRelayCoordinator.sol b/src/interfaces/IDeleGatorBatchRelayCoordinator.sol new file mode 100644 index 00000000..67e8995f --- /dev/null +++ b/src/interfaces/IDeleGatorBatchRelayCoordinator.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +/** + * @title IDeleGatorBatchRelayCoordinator + * @notice Multi-account coordinator for signed EIP7702BatchDeleGator relay batches. + * @dev Not an EIP-4337 paymaster. Child accounts still require valid signed batches. + */ +interface IDeleGatorBatchRelayCoordinator { + struct AccountBatch { + address account; + bytes32 mode; + bytes executionData; + } + + /// @notice Executes each signed batch row on its delegated account. + function executeBatches(AccountBatch[] calldata batches) external; +} diff --git a/src/interfaces/IEIP7702BatchDeleGator.sol b/src/interfaces/IEIP7702BatchDeleGator.sol new file mode 100644 index 00000000..feada431 --- /dev/null +++ b/src/interfaces/IEIP7702BatchDeleGator.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Execution } from "@erc7579/interfaces/IERC7579Account.sol"; + +/** + * @title IEIP7702BatchDeleGator + * @notice Relay-only ERC-7821 batch execution surface for EIP7702BatchDeleGator. + * @dev Signed batches are submitted through `executeBatch`, not inherited `execute(ModeCode,bytes)`. + */ +interface IEIP7702BatchDeleGator { + /// @dev Single batch with optional `opData` — `abi.encode(Execution[], bytes)`. + function MODE_BATCH_WITH_OPDATA() external view returns (bytes32); + + /// @dev Nested signed batches — `abi.encode(bytes[])`. + function MODE_BATCH_OF_BATCHES() external view returns (bytes32); + + /** + * @notice Executes a signed ERC-7821 batch after authorization checks. + * @param mode Relay mode constant (`MODE_BATCH_WITH_OPDATA` or `MODE_BATCH_OF_BATCHES`). + * @param executionData Encoded batch payload for the selected mode. + */ + function executeBatch(bytes32 mode, bytes calldata executionData) external payable; + + /** + * @notice Returns whether `mode` is supported by the relay entrypoint. + * @dev Relay-only modes are intentionally excluded from inherited `supportsExecutionMode`. + */ + function supportsBatchExecutionMode(bytes32 mode) external view returns (bool); + + /** + * @notice EIP-712 digest for replay-protected relayed execution. + * @param executions Executions authorized by the signature. + * @param nonce Unordered nonce to consume if the batch executes. + * @param deadline Last timestamp at which the authorization is valid. + * @param relayer Optional authorized relayer; use `address(0)` to allow any relayer. + */ + function hashBatchAuthorizationWithNonce( + Execution[] calldata executions, + uint256 nonce, + uint256 deadline, + address relayer + ) + external + view + returns (bytes32); + + /// @notice Returns whether an unordered relay nonce has already been consumed or invalidated. + function isNonceUsed(uint256 nonce) external view returns (bool); + + /// @notice Returns the used-nonce bitmap for `word`. + function nonceBitmap(uint256 word) external view returns (uint256 bitmap); + + /// @notice Invalidates one relay nonce. + function invalidateNonce(uint256 nonce) external; + + /// @notice Invalidates any nonce bits in `word` where `mask` has a 1 bit. + function invalidateNonces(uint256 word, uint256 mask) external; +} diff --git a/src/libraries/BatchAuthorizationLib.sol b/src/libraries/BatchAuthorizationLib.sol new file mode 100644 index 00000000..45eb6f04 --- /dev/null +++ b/src/libraries/BatchAuthorizationLib.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Execution } from "@erc7579/interfaces/IERC7579Account.sol"; + +/** + * @title BatchAuthorizationLib + * @notice Shared helpers for EIP-712 batch authorization digests. + */ +library BatchAuthorizationLib { + bytes32 internal constant BATCH_AUTH_WITH_NONCE_TYPEHASH = + keccak256("BatchAuthorizationWithNonce(bytes32 callsDigest,uint256 nonce,uint256 deadline,address relayer)"); + + /// @notice Computes the ordered digest over `(target, value, keccak256(callData))` for each execution. + function executionsDigest(Execution[] memory executions) internal pure returns (bytes32 digest) { + uint256 len = executions.length; + bytes memory encoded = _newExecutionsDigestBuffer(len); + + for (uint256 i = 0; i < len;) { + Execution memory execution = executions[i]; + bytes32 dataHash = keccak256(execution.callData); + + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, mload(execution)) + mstore(add(ptr, 0x20), mload(add(execution, 0x20))) + mstore(add(ptr, 0x40), dataHash) + mstore(add(add(encoded, 0x60), shl(5, i)), keccak256(ptr, 0x60)) + } + + unchecked { + ++i; + } + } + + /// @solidity memory-safe-assembly + assembly { + digest := keccak256(add(encoded, 0x20), mload(encoded)) + } + } + + /// @notice Computes the ordered digest over calldata executions. + function executionsDigestCalldata(Execution[] calldata executions) internal pure returns (bytes32 digest) { + uint256 len = executions.length; + bytes memory encoded = _newExecutionsDigestBuffer(len); + + for (uint256 i = 0; i < len;) { + Execution calldata execution = executions[i]; + address target = execution.target; + uint256 value = execution.value; + bytes32 dataHash = keccak256(execution.callData); + + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, target) + mstore(add(ptr, 0x20), value) + mstore(add(ptr, 0x40), dataHash) + mstore(add(add(encoded, 0x60), shl(5, i)), keccak256(ptr, 0x60)) + } + + unchecked { + ++i; + } + } + + /// @solidity memory-safe-assembly + assembly { + digest := keccak256(add(encoded, 0x20), mload(encoded)) + } + } + + function batchAuthorizationWithNonceStructHash( + bytes32 callsDigest, + uint256 nonce, + uint256 deadline, + address relayer + ) + internal + pure + returns (bytes32 structHash) + { + bytes32 typeHash = BATCH_AUTH_WITH_NONCE_TYPEHASH; + + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, typeHash) + mstore(add(ptr, 0x20), callsDigest) + mstore(add(ptr, 0x40), nonce) + mstore(add(ptr, 0x60), deadline) + mstore(add(ptr, 0x80), relayer) + structHash := keccak256(ptr, 0xa0) + } + } + + function _newExecutionsDigestBuffer(uint256 len) private pure returns (bytes memory encoded) { + uint256 encodedLen; + unchecked { + encodedLen = 0x40 + (len << 5); + } + encoded = new bytes(encodedLen); + + /// @solidity memory-safe-assembly + assembly { + mstore(add(encoded, 0x20), 0x20) + mstore(add(encoded, 0x40), len) + } + } +} diff --git a/test/DeleGatorBatchRelayCoordinatorTest.t.sol b/test/DeleGatorBatchRelayCoordinatorTest.t.sol new file mode 100644 index 00000000..6a1cf1e9 --- /dev/null +++ b/test/DeleGatorBatchRelayCoordinatorTest.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Test, Vm } from "forge-std/Test.sol"; + +import { DeleGatorBatchRelayCoordinator } from "../src/DeleGatorBatchRelayCoordinator.sol"; +import { EIP7702BatchDeleGator } from "../src/EIP7702/EIP7702BatchDeleGator.sol"; +import { IDeleGatorBatchRelayCoordinator } from "../src/interfaces/IDeleGatorBatchRelayCoordinator.sol"; +import { Execution } from "../src/utils/Types.sol"; +import { Counter } from "./utils/Counter.t.sol"; +import { BaseTest } from "./utils/BaseTest.t.sol"; + +contract DeleGatorBatchRelayCoordinatorTest is BaseTest { + DeleGatorBatchRelayCoordinator internal coordinator; + EIP7702BatchDeleGator internal implementation; + + uint256 internal accountAPk = 0xA110; + uint256 internal accountBPk = 0xB110; + address internal accountAAddress; + address internal accountBAddress; + + EIP7702BatchDeleGator internal accountA; + EIP7702BatchDeleGator internal accountB; + + Counter internal counterA; + Counter internal counterB; + + uint256 internal constant DEFAULT_DEADLINE = 1 days; + + function setUp() public override { + super.setUp(); + + coordinator = new DeleGatorBatchRelayCoordinator(); + implementation = new EIP7702BatchDeleGator(delegationManager, entryPoint); + + accountAAddress = vm.addr(accountAPk); + accountBAddress = vm.addr(accountBPk); + vm.etch(accountAAddress, address(implementation).code); + vm.etch(accountBAddress, address(implementation).code); + + accountA = EIP7702BatchDeleGator(payable(accountAAddress)); + accountB = EIP7702BatchDeleGator(payable(accountBAddress)); + + counterA = new Counter(accountAAddress); + counterB = new Counter(accountBAddress); + } + + function test_executeBatches_anyCaller() public { + IDeleGatorBatchRelayCoordinator.AccountBatch[] memory batches = + _singleSignedBatch(accountA, accountAPk, counterA, 1, address(coordinator)); + + vm.prank(address(0xBEEF)); + coordinator.executeBatches(batches); + + assertEq(counterA.count(), 1); + } + + function test_executeBatches_unsignedChildRow_recordsFailureAndContinues() public { + IDeleGatorBatchRelayCoordinator.AccountBatch[] memory batches = new IDeleGatorBatchRelayCoordinator.AccountBatch[](2); + + Execution[] memory unsignedCalls = _incrementsFor(counterA, 1); + batches[0] = IDeleGatorBatchRelayCoordinator.AccountBatch({ + account: accountAAddress, + mode: accountA.MODE_BATCH_SIMPLE(), + executionData: abi.encode(unsignedCalls) + }); + + Execution[] memory signedCalls = _incrementsFor(counterB, 2); + batches[1] = IDeleGatorBatchRelayCoordinator.AccountBatch({ + account: accountBAddress, + mode: accountB.MODE_BATCH_WITH_OPDATA(), + executionData: abi.encode( + signedCalls, + _signedOpData(accountB, accountBPk, signedCalls, 1, address(coordinator)) + ) + }); + + vm.recordLogs(); + coordinator.executeBatches(batches); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertEq(logs.length, 2); + assertEq(logs[0].topics[1], bytes32(uint256(0))); + assertEq(address(uint160(uint256(logs[0].topics[2]))), accountAAddress); + assertFalse(abi.decode(logs[0].data, (bool))); + assertEq(logs[1].topics[1], bytes32(uint256(1))); + assertTrue(abi.decode(logs[1].data, (bool))); + + assertEq(counterA.count(), 0); + assertEq(counterB.count(), 2); + } + + function test_executeBatches_signedRows() public { + IDeleGatorBatchRelayCoordinator.AccountBatch[] memory batches = new IDeleGatorBatchRelayCoordinator.AccountBatch[](2); + + Execution[] memory callsA = _incrementsFor(counterA, 1); + batches[0] = IDeleGatorBatchRelayCoordinator.AccountBatch({ + account: accountAAddress, + mode: accountA.MODE_BATCH_WITH_OPDATA(), + executionData: abi.encode( + callsA, _signedOpData(accountA, accountAPk, callsA, 10, address(coordinator)) + ) + }); + + Execution[] memory callsB = _incrementsFor(counterB, 3); + batches[1] = IDeleGatorBatchRelayCoordinator.AccountBatch({ + account: accountBAddress, + mode: accountB.MODE_BATCH_WITH_OPDATA(), + executionData: abi.encode( + callsB, _signedOpData(accountB, accountBPk, callsB, 20, address(coordinator)) + ) + }); + + coordinator.executeBatches(batches); + + assertEq(counterA.count(), 1); + assertEq(counterB.count(), 3); + } + + function test_executeBatches_wrongPinnedRelayer_recordsFailure() public { + Execution[] memory calls = _incrementsFor(counterA, 1); + IDeleGatorBatchRelayCoordinator.AccountBatch[] memory batches = new IDeleGatorBatchRelayCoordinator.AccountBatch[](1); + batches[0] = IDeleGatorBatchRelayCoordinator.AccountBatch({ + account: accountAAddress, + mode: accountA.MODE_BATCH_WITH_OPDATA(), + executionData: abi.encode(calls, _signedOpData(accountA, accountAPk, calls, 5, address(0xCAFE))) + }); + + vm.recordLogs(); + coordinator.executeBatches(batches); + + Vm.Log[] memory logs = vm.getRecordedLogs(); + assertEq(logs.length, 1); + assertFalse(abi.decode(logs[0].data, (bool))); + + assertEq(counterA.count(), 0); + } + + function _singleSignedBatch( + EIP7702BatchDeleGator account, + uint256 pk, + Counter counter, + uint256 nonce, + address pinnedRelayer + ) + internal + view + returns (IDeleGatorBatchRelayCoordinator.AccountBatch[] memory batches) + { + Execution[] memory calls = _incrementsFor(counter, 1); + batches = new IDeleGatorBatchRelayCoordinator.AccountBatch[](1); + batches[0] = IDeleGatorBatchRelayCoordinator.AccountBatch({ + account: address(account), + mode: account.MODE_BATCH_WITH_OPDATA(), + executionData: abi.encode(calls, _signedOpData(account, pk, calls, nonce, pinnedRelayer)) + }); + } + + function _incrementsFor(Counter counter, uint256 n) internal pure returns (Execution[] memory executions) { + executions = new Execution[](n); + for (uint256 i = 0; i < n; ++i) { + executions[i] = Execution({ + target: address(counter), value: 0, callData: abi.encodeCall(Counter.unsafeIncrement, ()) + }); + } + } + + function _signedOpData( + EIP7702BatchDeleGator account, + uint256 pk, + Execution[] memory executions, + uint256 nonce, + address pinnedRelayer + ) + internal + view + returns (bytes memory) + { + uint256 deadline = block.timestamp + DEFAULT_DEADLINE; + bytes32 digest = account.hashBatchAuthorizationWithNonce(executions, nonce, deadline, pinnedRelayer); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, digest); + return abi.encode(nonce, deadline, pinnedRelayer, abi.encodePacked(r, s, v)); + } +} diff --git a/test/EIP7702BatchDeleGatorTest.t.sol b/test/EIP7702BatchDeleGatorTest.t.sol new file mode 100644 index 00000000..4b3a760a --- /dev/null +++ b/test/EIP7702BatchDeleGatorTest.t.sol @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Test } from "forge-std/Test.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { IEntryPoint } from "@account-abstraction/interfaces/IEntryPoint.sol"; + +import { EIP7702BatchDeleGator } from "../src/EIP7702/EIP7702BatchDeleGator.sol"; +import { EIP7702BatchDeleGatorBeacon } from "../src/EIP7702/EIP7702BatchDeleGatorBeacon.sol"; +import { EIP7702BatchDeleGatorProxy } from "../src/EIP7702/EIP7702BatchDeleGatorProxy.sol"; +import { EIP7702DeleGatorCore } from "../src/EIP7702/EIP7702DeleGatorCore.sol"; +import { IDelegationManager } from "../src/interfaces/IDelegationManager.sol"; +import { BatchAuthorizationLib } from "../src/libraries/BatchAuthorizationLib.sol"; +import { Execution, ModeCode } from "../src/utils/Types.sol"; +import { Counter } from "./utils/Counter.t.sol"; +import { BaseTest } from "./utils/BaseTest.t.sol"; +import { StorageUtilsLib } from "./utils/StorageUtilsLib.t.sol"; + +interface IVersionedBatchDeleGator { + function version() external view returns (uint256); +} + +contract RevertingTarget { + error AlwaysReverts(); + + function boom() external pure { + revert AlwaysReverts(); + } +} + +contract EIP7702BatchDeleGatorTest is BaseTest { + using ModeLib for ModeCode; + + EIP7702BatchDeleGator internal implementation; + EIP7702BatchDeleGator internal account; + Counter internal counter; + + uint256 internal accountPk = 0xA11CE; + address internal accountAddress; + address internal relayer = address(0xBEEF); + + uint256 internal constant DEFAULT_DEADLINE = 1 days; + uint256 internal constant DEFAULT_NONCE = 42; + + bytes32 internal modeBatchSimple; + bytes32 internal modeBatchWithOpData; + bytes32 internal modeBatchOfBatches; + + function setUp() public override { + super.setUp(); + + accountAddress = vm.addr(accountPk); + implementation = new EIP7702BatchDeleGator(delegationManager, entryPoint); + vm.etch(accountAddress, address(implementation).code); + account = EIP7702BatchDeleGator(payable(accountAddress)); + counter = new Counter(accountAddress); + + modeBatchSimple = implementation.MODE_BATCH_SIMPLE(); + modeBatchWithOpData = implementation.MODE_BATCH_WITH_OPDATA(); + modeBatchOfBatches = implementation.MODE_BATCH_OF_BATCHES(); + } + + function test_supportsBatchExecutionModes() public { + assertTrue(account.supportsBatchExecutionMode(account.MODE_BATCH_SIMPLE())); + assertTrue(account.supportsBatchExecutionMode(account.MODE_BATCH_WITH_OPDATA())); + assertTrue(account.supportsBatchExecutionMode(account.MODE_BATCH_OF_BATCHES())); + } + + function test_inheritedSupportsExecutionMode_excludesRelayOnlyModes() public { + assertFalse(account.supportsExecutionMode(ModeCode.wrap(account.MODE_BATCH_WITH_OPDATA()))); + assertFalse(account.supportsExecutionMode(ModeCode.wrap(account.MODE_BATCH_OF_BATCHES()))); + } + + function test_executeBatch_unsigned_selfBatch() public { + Execution[] memory executions = _twoIncrements(); + vm.prank(accountAddress); + account.executeBatch(modeBatchSimple, abi.encode(executions)); + assertEq(counter.count(), 2); + } + + function test_executeBatch_unsigned_externalCaller_reverts() public { + Execution[] memory executions = _twoIncrements(); + vm.prank(relayer); + vm.expectRevert(EIP7702BatchDeleGator.UnauthorizedBatchExecuteCaller.selector); + account.executeBatch(modeBatchSimple, abi.encode(executions)); + } + + function test_executeBatch_signed_relay() public { + Execution[] memory executions = _twoIncrements(); + bytes memory opData = _signedOpData(executions, DEFAULT_NONCE, block.timestamp + DEFAULT_DEADLINE, address(0)); + + vm.prank(relayer); + account.executeBatch(modeBatchWithOpData, abi.encode(executions, opData)); + assertEq(counter.count(), 2); + assertTrue(account.isNonceUsed(DEFAULT_NONCE)); + } + + function test_executeBatch_signed_throughInheritedExecute_revertsForRelayer() public { + Execution[] memory executions = _twoIncrements(); + bytes memory opData = _signedOpData(executions, DEFAULT_NONCE, block.timestamp + DEFAULT_DEADLINE, address(0)); + bytes memory executionData = abi.encode(executions, opData); + + vm.prank(relayer); + vm.expectRevert(EIP7702DeleGatorCore.NotEntryPointOrSelf.selector); + account.execute(ModeCode.wrap(modeBatchWithOpData), executionData); + } + + function test_executeBatch_signed_wrongSigner_reverts() public { + Execution[] memory executions = _twoIncrements(); + bytes32 digest = account.hashBatchAuthorizationWithNonce( + executions, DEFAULT_NONCE, block.timestamp + DEFAULT_DEADLINE, address(0) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(0xBAD, digest); + bytes memory opData = abi.encode(DEFAULT_NONCE, block.timestamp + DEFAULT_DEADLINE, address(0), abi.encodePacked(r, s, v)); + + vm.prank(relayer); + vm.expectRevert(EIP7702BatchDeleGator.InvalidBatchSignature.selector); + account.executeBatch(modeBatchWithOpData, abi.encode(executions, opData)); + } + + function test_executeBatch_signed_expiredDeadline_reverts() public { + Execution[] memory executions = _twoIncrements(); + bytes memory opData = _signedOpData(executions, DEFAULT_NONCE, block.timestamp - 1, address(0)); + + vm.prank(relayer); + vm.expectRevert(EIP7702BatchDeleGator.BatchAuthorizationExpired.selector); + account.executeBatch(modeBatchWithOpData, abi.encode(executions, opData)); + } + + function test_executeBatch_signed_wrongRelayer_reverts() public { + Execution[] memory executions = _twoIncrements(); + bytes memory opData = _signedOpData(executions, DEFAULT_NONCE, block.timestamp + DEFAULT_DEADLINE, relayer); + + vm.prank(address(0xCAFE)); + vm.expectRevert(EIP7702BatchDeleGator.UnauthorizedRelayer.selector); + account.executeBatch(modeBatchWithOpData, abi.encode(executions, opData)); + } + + function test_executeBatch_signed_replay_reverts() public { + Execution[] memory executions = _twoIncrements(); + bytes memory opData = _signedOpData(executions, DEFAULT_NONCE, block.timestamp + DEFAULT_DEADLINE, address(0)); + bytes memory executionData = abi.encode(executions, opData); + + vm.startPrank(relayer); + account.executeBatch(modeBatchWithOpData, executionData); + vm.expectRevert(EIP7702BatchDeleGator.NonceAlreadyUsed.selector); + account.executeBatch(modeBatchWithOpData, executionData); + vm.stopPrank(); + } + + function test_executeBatch_failedExecution_revertsNonceConsumption() public { + RevertingTarget revertingTarget = new RevertingTarget(); + Execution[] memory executions = new Execution[](1); + executions[0] = Execution({ + target: address(revertingTarget), value: 0, callData: abi.encodeCall(RevertingTarget.boom, ()) + }); + bytes memory opData = _signedOpData(executions, DEFAULT_NONCE, block.timestamp + DEFAULT_DEADLINE, address(0)); + + vm.prank(relayer); + vm.expectRevert(RevertingTarget.AlwaysReverts.selector); + account.executeBatch(modeBatchWithOpData, abi.encode(executions, opData)); + + assertFalse(account.isNonceUsed(DEFAULT_NONCE)); + } + + function test_invalidateNonce_blocksLaterUse() public { + vm.prank(accountAddress); + account.invalidateNonce(DEFAULT_NONCE); + + Execution[] memory executions = _twoIncrements(); + bytes memory opData = _signedOpData(executions, DEFAULT_NONCE, block.timestamp + DEFAULT_DEADLINE, address(0)); + + vm.prank(relayer); + vm.expectRevert(EIP7702BatchDeleGator.NonceAlreadyUsed.selector); + account.executeBatch(modeBatchWithOpData, abi.encode(executions, opData)); + } + + function test_invalidateNonces_blocksMaskedNonces() public { + uint256 word = DEFAULT_NONCE >> 8; + uint256 mask = 1 << uint8(DEFAULT_NONCE); + + vm.prank(accountAddress); + account.invalidateNonces(word, mask); + + Execution[] memory executions = _twoIncrements(); + bytes memory opData = _signedOpData(executions, DEFAULT_NONCE, block.timestamp + DEFAULT_DEADLINE, address(0)); + + vm.prank(relayer); + vm.expectRevert(EIP7702BatchDeleGator.NonceAlreadyUsed.selector); + account.executeBatch(modeBatchWithOpData, abi.encode(executions, opData)); + } + + function test_executeBatch_ofBatches() public { + Execution[] memory executionsA = _twoIncrements(); + Execution[] memory executionsB = _oneIncrement(); + + bytes memory innerA = abi.encode(executionsA, _signedOpData(executionsA, 1, block.timestamp + DEFAULT_DEADLINE, address(0))); + bytes memory innerB = abi.encode(executionsB, _signedOpData(executionsB, 2, block.timestamp + DEFAULT_DEADLINE, address(0))); + + bytes[] memory innerBatches = new bytes[](2); + innerBatches[0] = innerA; + innerBatches[1] = innerB; + + vm.prank(relayer); + account.executeBatch(modeBatchOfBatches, abi.encode(innerBatches)); + + assertEq(counter.count(), 3); + assertTrue(account.isNonceUsed(1)); + assertTrue(account.isNonceUsed(2)); + } + + function test_hashBatchAuthorizationWithNonce_matchesManualDigest() public { + Execution[] memory executions = _twoIncrements(); + uint256 nonce = 7; + uint256 deadline = block.timestamp + DEFAULT_DEADLINE; + + bytes32 callsDigest = BatchAuthorizationLib.executionsDigest(executions); + bytes32 structHash = + BatchAuthorizationLib.batchAuthorizationWithNonceStructHash(callsDigest, nonce, deadline, relayer); + bytes32 expected = MessageHashUtils.toTypedDataHash(account.getDomainHash(), structHash); + + assertEq(account.hashBatchAuthorizationWithNonce(executions, nonce, deadline, relayer), expected); + } + + function test_beaconProxyUpgrade_preservesNonceBitmap() public { + EIP7702BatchDeleGator implementationV1 = new EIP7702BatchDeleGator(delegationManager, entryPoint); + EIP7702BatchDeleGatorV2Mock implementationV2 = new EIP7702BatchDeleGatorV2Mock(delegationManager, entryPoint); + EIP7702BatchDeleGatorBeacon beacon = new EIP7702BatchDeleGatorBeacon(address(implementationV1), address(this)); + EIP7702BatchDeleGatorProxy proxy = new EIP7702BatchDeleGatorProxy(address(beacon)); + + vm.etch(accountAddress, address(proxy).code); + account = EIP7702BatchDeleGator(payable(accountAddress)); + + Execution[] memory executions = _twoIncrements(); + bytes memory opData = _signedOpData(executions, DEFAULT_NONCE, block.timestamp + DEFAULT_DEADLINE, address(0)); + vm.prank(relayer); + account.executeBatch(modeBatchWithOpData, abi.encode(executions, opData)); + assertTrue(account.isNonceUsed(DEFAULT_NONCE)); + + bytes32 nonceSlot = StorageUtilsLib.getStorageLocation("DeleGator.EIP7702BatchDeleGator.nonce"); + uint256 word = DEFAULT_NONCE >> 8; + bytes32 bitmapSlot = keccak256(abi.encode(word, nonceSlot)); + bytes32 bitmapBefore = vm.load(accountAddress, bitmapSlot); + + beacon.upgradeTo(address(implementationV2)); + assertEq(IVersionedBatchDeleGator(accountAddress).version(), 2); + assertEq(vm.load(accountAddress, bitmapSlot), bitmapBefore); + + uint256 nextNonce = DEFAULT_NONCE + 1; + bytes memory nextOpData = + _signedOpData(executions, nextNonce, block.timestamp + DEFAULT_DEADLINE, address(0)); + vm.prank(relayer); + account.executeBatch(modeBatchWithOpData, abi.encode(executions, nextOpData)); + assertEq(counter.count(), 4); + } + + function test_inheritedStandardBatchMode_stillWorks() public { + Execution memory execution = Execution({ + target: address(counter), + value: 0, + callData: abi.encodeCall(Counter.unsafeIncrement, ()) + }); + Execution[] memory executions = new Execution[](1); + executions[0] = execution; + + vm.prank(accountAddress); + account.execute(ModeLib.encodeSimpleBatch(), ExecutionLib.encodeBatch(executions)); + assertEq(counter.count(), 1); + } + + function _twoIncrements() internal view returns (Execution[] memory executions) { + executions = new Execution[](2); + executions[0] = Execution({ + target: address(counter), value: 0, callData: abi.encodeCall(Counter.unsafeIncrement, ()) + }); + executions[1] = Execution({ + target: address(counter), value: 0, callData: abi.encodeCall(Counter.unsafeIncrement, ()) + }); + } + + function _oneIncrement() internal view returns (Execution[] memory executions) { + executions = new Execution[](1); + executions[0] = Execution({ + target: address(counter), value: 0, callData: abi.encodeCall(Counter.unsafeIncrement, ()) + }); + } + + function _signedOpData( + Execution[] memory executions, + uint256 nonce, + uint256 deadline, + address authorizedRelayer + ) + internal + view + returns (bytes memory) + { + bytes32 digest = account.hashBatchAuthorizationWithNonce(executions, nonce, deadline, authorizedRelayer); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(accountPk, digest); + return abi.encode(nonce, deadline, authorizedRelayer, abi.encodePacked(r, s, v)); + } +} + +contract EIP7702BatchDeleGatorV2Mock is EIP7702BatchDeleGator { + constructor(IDelegationManager delegationManager_, IEntryPoint entryPoint_) + EIP7702BatchDeleGator(delegationManager_, entryPoint_) + { } + + function version() external pure returns (uint256) { + return 2; + } +}