From 51fd2d9a4f1917b97749fcaee128c086be9a25d3 Mon Sep 17 00:00:00 2001 From: hanzel98 Date: Mon, 17 Aug 2026 21:37:58 -0600 Subject: [PATCH] feat: add EIP-7702 multi-manager delegator --- documents/EIP7702DeleGator.md | 22 +- .../DeployEIP7702MultiManagerDeleGator.s.sol | 42 ++ script/verification/verify-contract.sh | 13 + src/EIP7702/EIP7702MultiManagerDeleGator.sol | 50 ++ .../EIP7702MultiManagerDeleGatorCore.sol | 464 +++++++++++++++ test/EIP7702MultiManagerDeleGatorTest.t.sol | 543 ++++++++++++++++++ test/utils/BaseTest.t.sol | 15 + test/utils/Types.t.sol | 4 +- 8 files changed, 1140 insertions(+), 13 deletions(-) create mode 100644 script/DeployEIP7702MultiManagerDeleGator.s.sol create mode 100644 src/EIP7702/EIP7702MultiManagerDeleGator.sol create mode 100644 src/EIP7702/EIP7702MultiManagerDeleGatorCore.sol create mode 100644 test/EIP7702MultiManagerDeleGatorTest.t.sol diff --git a/documents/EIP7702DeleGator.md b/documents/EIP7702DeleGator.md index 0ba86c3d..8219e14c 100644 --- a/documents/EIP7702DeleGator.md +++ b/documents/EIP7702DeleGator.md @@ -2,22 +2,22 @@ ## Overview -This document provides an overview of the implemented EIP-7702-compatible contracts. These contracts use a different upgrade mechanism than the previous UUPS proxy-based architecture (as implemented in DeleGatorCore) and instead follow the EIP-7702 standard. +These contracts let an EOA delegate its code to a DeleGator implementation under EIP-7702. EIP-7702 changes code through an EOA authorization and has no initializer. -Under EIP-7702, an Externally Owned Account (EOA) can submit an authorization to map the contract code of an existing contract to that EOA. Unlike UUPS proxy-based contracts, this approach neither supports contract initialization nor relies on UUPS-related code. +## Production Stateless account -## Contracts +`EIP7702DeleGatorCore` and `EIP7702StatelessDeleGator` provide the single-`DelegationManager` account. They support ERC-4337 and require ERC-1271 and UserOperation signatures to recover to the EOA whose address hosts the delegated code. -### 1. EIP7702DeleGatorCore.sol +## EIP7702MultiManagerDeleGator -**EIP7702DeleGatorCore** serves as the foundational contract for EIP-7702-compatible delegator functionality with ERC-7710. It acts as the primary interface for interactions under EIP-7702 and implements the EIP-7821 interface, which provides a method to execute calls in different modes (e.g., single or batch). These methods can be invoked either through the privileged ERC-4337 EntryPoint or directly via the EOA address. +`EIP7702MultiManagerDeleGator` is a non-ERC-4337 account with two immutable default `DelegationManager` contracts and optional additional approved `DelegationManager` contracts. -Future implementations may introduce additional features, such as signature validation, as outlined in EIP-7821. +See the [implementation](../src/EIP7702/EIP7702MultiManagerDeleGator.sol) and [core](../src/EIP7702/EIP7702MultiManagerDeleGatorCore.sol) source for API and implementation behavior. -This contract also integrates OpenZeppelin’s EIP712 functionality. The name and version used in the EIP712 constructor are limited to a maximum of 31 bytes. Exceeding this limit causes those variables to be stored in the contract state without namespace storage, leading to conflicts. Restricting the name and version size helps ensure that **EIP7702DeleGatorCore** remains stateless by avoiding additional storage. +### Security considerations -### 2. EIP7702StatelessDeleGator.sol +> **Warning:** Every default or approved `DelegationManager` is a root authority over the account. It can execute arbitrary calls, self-call the account, and modify mutable manager approvals. -**EIP7702StatelessDeleGator** does not maintain signer data within the contract state. Instead, control is granted to the EOA that shares the same address, in accordance with EIP-7702. The contract can be invoked either through the privileged ERC-4337 EntryPoint or directly via the EOA address. The signature is verified via the `isValidSignature()` function. - -This stateless design offers a lightweight and secure approach to delegator functionality under the EIP-7702 standard. +- Unknown, unofficial, unaudited, compromised, or upgradeable `DelegationManager` contracts can compromise the account. Users and integrators must carefully verify and trust each one before configuring or approving it. +- The two default `DelegationManager` contracts cannot be revoked for this implementation. +- Under EIP-7702, changing the delegated implementation does not clear the EOA's storage. Mutable approvals use the ERC-7201 namespace `DeleGator.EIP7702MultiManager.v1`; a replacement implementation should use a different namespace unless it intentionally adopts the same approvals. There is no on-chain enumeration or revoke-all operation. diff --git a/script/DeployEIP7702MultiManagerDeleGator.s.sol b/script/DeployEIP7702MultiManagerDeleGator.s.sol new file mode 100644 index 00000000..2aced783 --- /dev/null +++ b/script/DeployEIP7702MultiManagerDeleGator.s.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { Script, console2 } from "forge-std/Script.sol"; + +import { EIP7702MultiManagerDeleGator } from "../src/EIP7702/EIP7702MultiManagerDeleGator.sol"; +import { IDelegationManager } from "../src/interfaces/IDelegationManager.sol"; + +/** + * @title DeployEIP7702MultiManagerDeleGator + * @notice Deterministically deploys the EIP-7702 multi-DelegationManager implementation. + * @dev Requires SALT, DEFAULT_DELEGATION_MANAGER_1, and DEFAULT_DELEGATION_MANAGER_2 environment variables. + */ +contract DeployEIP7702MultiManagerDeleGator is Script { + bytes32 internal salt; + IDelegationManager internal defaultDelegationManager1; + IDelegationManager internal defaultDelegationManager2; + + /// @notice Loads deterministic deployment inputs from the environment. + function setUp() public { + salt = bytes32(abi.encodePacked(vm.envString("SALT"))); + defaultDelegationManager1 = IDelegationManager(vm.envAddress("DEFAULT_DELEGATION_MANAGER_1")); + defaultDelegationManager2 = IDelegationManager(vm.envAddress("DEFAULT_DELEGATION_MANAGER_2")); + + console2.log("~~~"); + console2.log("Deployer: %s", msg.sender); + console2.log("Default DelegationManager 1: %s", address(defaultDelegationManager1)); + console2.log("Default DelegationManager 2: %s", address(defaultDelegationManager2)); + console2.log("Salt:"); + console2.logBytes32(salt); + } + + /// @notice Deploys the implementation with CREATE2. + /// @return implementation_ The deployed multi-DelegationManager implementation. + function run() public returns (EIP7702MultiManagerDeleGator implementation_) { + vm.startBroadcast(); + implementation_ = new EIP7702MultiManagerDeleGator{ salt: salt }(defaultDelegationManager1, defaultDelegationManager2); + vm.stopBroadcast(); + + console2.log("EIP7702MultiManagerDeleGatorImpl: %s", address(implementation_)); + } +} diff --git a/script/verification/verify-contract.sh b/script/verification/verify-contract.sh index c70ddc92..646f4167 100755 --- a/script/verification/verify-contract.sh +++ b/script/verification/verify-contract.sh @@ -76,6 +76,19 @@ add_contract \ "0x0000000071727De22E5E9d8BAf0edAc6f37da032")" \ "" +# EIP7702MultiManagerDeleGator +# No address is committed until an actual deployment exists. Set all three variables to include it. +if [[ -n "${EIP7702_MULTI_MANAGER_ADDRESS:-}" ]]; then + add_contract \ + "EIP7702MultiManagerDeleGator" \ + "src/EIP7702/EIP7702MultiManagerDeleGator.sol" \ + "$EIP7702_MULTI_MANAGER_ADDRESS" \ + "$(encode_args "constructor(address,address)" \ + "$DEFAULT_DELEGATION_MANAGER_1" \ + "$DEFAULT_DELEGATION_MANAGER_2")" \ + "" +fi + # NativeTokenPaymentEnforcer add_contract \ "NativeTokenPaymentEnforcer" \ diff --git a/src/EIP7702/EIP7702MultiManagerDeleGator.sol b/src/EIP7702/EIP7702MultiManagerDeleGator.sol new file mode 100644 index 00000000..607cc15a --- /dev/null +++ b/src/EIP7702/EIP7702MultiManagerDeleGator.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +import { EIP7702MultiManagerDeleGatorCore } from "./EIP7702MultiManagerDeleGatorCore.sol"; +import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; +import { ERC1271Lib } from "../libraries/ERC1271Lib.sol"; + +/** + * @title EIP7702MultiManagerDeleGator + * @notice EIP-7702 account with two permanent default DelegationManagers and mutable additional DelegationManagers. + * @dev Every default or approved additional DelegationManager has full root execution authority. + */ +contract EIP7702MultiManagerDeleGator is EIP7702MultiManagerDeleGatorCore { + ////////////////////////////// State ////////////////////////////// + + /// @dev The name of the implementation. + string public constant NAME = "EIP7702MultiManagerDeleGator"; + + /// @dev The implementation version. + string public constant VERSION = "1.0.0"; + + ////////////////////////////// Constructor ////////////////////////////// + + /** + * @notice Initializes the implementation with two equal, permanent default DelegationManagers. + * @param _defaultDelegationManager1 The first permanent DelegationManager. + * @param _defaultDelegationManager2 The second permanent DelegationManager. + */ + constructor( + IDelegationManager _defaultDelegationManager1, + IDelegationManager _defaultDelegationManager2 + ) + EIP7702MultiManagerDeleGatorCore(_defaultDelegationManager1, _defaultDelegationManager2) + { } + + ////////////////////////////// Internal Methods ////////////////////////////// + + /** + * @notice Verifies a signature from the EOA whose address hosts this delegated code. + * @param _hash The signed hash. + * @param _signature The ECDSA signature. + * @return The ERC-1271 magic value for a valid signature, otherwise the failure value. + */ + 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; + } +} diff --git a/src/EIP7702/EIP7702MultiManagerDeleGatorCore.sol b/src/EIP7702/EIP7702MultiManagerDeleGatorCore.sol new file mode 100644 index 00000000..fbc9c08c --- /dev/null +++ b/src/EIP7702/EIP7702MultiManagerDeleGatorCore.sol @@ -0,0 +1,464 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { ExecutionHelper } from "@erc7579/core/ExecutionHelper.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import { IDeleGatorCore } from "../interfaces/IDeleGatorCore.sol"; +import { IDelegationManager } from "../interfaces/IDelegationManager.sol"; +import { IERC7821 } from "../interfaces/IERC7821.sol"; +import { CALLTYPE_BATCH, CALLTYPE_SINGLE, EXECTYPE_DEFAULT, EXECTYPE_TRY, MODE_DEFAULT } from "../utils/Constants.sol"; +import { CallType, Delegation, Execution, ExecType, ModeCode, ModePayload, ModeSelector } from "../utils/Types.sol"; + +/** + * @title EIP7702MultiManagerDeleGatorCore + * @notice EIP-7702 account core with two permanent default DelegationManagers and mutable additional DelegationManagers. + * @dev Every approved DelegationManager has full root execution authority, including authority to self-call this account. + * @dev Unknown, unofficial, unaudited, or upgradeable DelegationManagers can compromise the account. Users and + * integrators must carefully verify and trust each DelegationManager before configuring or approving it. + * @dev Any child contract adding state variables must use namespaced storage for safe EIP-7702 implementation changes. + */ +abstract contract EIP7702MultiManagerDeleGatorCore is + ExecutionHelper, + IERC165, + IERC7821, + IDeleGatorCore, + IERC721Receiver, + IERC1155Receiver +{ + using ExecutionLib for bytes; + using ModeLib for ModeCode; + using ModeLib for ModeSelector; + + ////////////////////////////// Structs ////////////////////////////// + + /// @custom:storage-location erc7201:DeleGator.EIP7702MultiManager.v1 + struct MultiManagerStorage { + mapping(address delegationManager => bool approved) isApprovedDelegationManager; + } + + ////////////////////////////// State ////////////////////////////// + + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address private immutable __self = address(this); + + /// @dev ERC-7201 slot for erc7201:DeleGator.EIP7702MultiManager.v1. + bytes32 private constant MULTI_MANAGER_STORAGE_LOCATION = 0x4ffe8763039a0c1b60d7504fab186e7e570dca76b49af35bcd94b7cdae58b300; + + /// @notice The first permanent DelegationManager. + IDelegationManager public immutable defaultDelegationManager1; + + /// @notice The second permanent DelegationManager. + IDelegationManager public immutable defaultDelegationManager2; + + ////////////////////////////// Events ////////////////////////////// + + /** + * @notice Emitted when an additional DelegationManager is approved. + * @param delegationManager The approved DelegationManager. + */ + event ApprovedDelegationManager(IDelegationManager indexed delegationManager); + + /** + * @notice Emitted when an additional DelegationManager is revoked. + * @param delegationManager The revoked DelegationManager. + */ + event RevokedDelegationManager(IDelegationManager indexed delegationManager); + + ////////////////////////////// Errors ////////////////////////////// + + /// @dev Error thrown when the caller is not this contract. + error NotSelf(); + + /// @dev The zero address cannot be configured as a DelegationManager. + error InvalidDelegationManager(); + + /// @dev A DelegationManager must have deployed code. + error DelegationManagerHasNoCode(IDelegationManager delegationManager); + + /// @dev The two default DelegationManagers must be distinct. + error DuplicateDefaultDelegationManager(); + + /// @dev The DelegationManager is already approved. + error DelegationManagerAlreadyApproved(IDelegationManager delegationManager); + + /// @dev The DelegationManager is not approved. + error DelegationManagerNotApproved(IDelegationManager delegationManager); + + /// @dev A permanent default DelegationManager cannot be revoked. + error DefaultDelegationManagerCannotBeRevoked(IDelegationManager delegationManager); + + /// @dev The call is from an unauthorized context. + error UnauthorizedCallContext(); + + /// @dev Error thrown when an execution with an unsupported CallType was made. + error UnsupportedCallType(CallType callType); + + /// @dev Error thrown when an execution with an unsupported ExecType was made. + error UnsupportedExecType(ExecType execType); + + ////////////////////////////// Modifiers ////////////////////////////// + + /** + * @dev Prevents direct calls to the implementation. + * @dev Under EIP-7702 the delegated account runs in the EOA context, so `address(this) != __self`. + */ + modifier onlyProxy() { + if (address(this) == __self) revert UnauthorizedCallContext(); + _; + } + + /// @notice Requires the function call to come from the EIP-7702 account itself. + modifier onlySelf() { + if (msg.sender != address(this)) revert NotSelf(); + _; + } + + /// @notice Requires the caller to be a default or approved additional DelegationManager. + modifier onlyDelegationManager() { + if (!_isApprovedDelegationManager(msg.sender)) { + revert DelegationManagerNotApproved(IDelegationManager(msg.sender)); + } + _; + } + + /// @dev Requires the selected DelegationManager to be a default or approved additional DelegationManager. + modifier onlyApprovedDelegationManager(IDelegationManager _delegationManager) { + if (!_isApprovedDelegationManager(address(_delegationManager))) { + revert DelegationManagerNotApproved(_delegationManager); + } + _; + } + + ////////////////////////////// Constructor ////////////////////////////// + + /** + * @notice Configures the two permanent default DelegationManagers. + * @param _defaultDelegationManager1 The first permanent DelegationManager. + * @param _defaultDelegationManager2 The second permanent DelegationManager. + */ + constructor(IDelegationManager _defaultDelegationManager1, IDelegationManager _defaultDelegationManager2) { + _validateDelegationManager(_defaultDelegationManager1); + _validateDelegationManager(_defaultDelegationManager2); + if (_defaultDelegationManager1 == _defaultDelegationManager2) revert DuplicateDefaultDelegationManager(); + + defaultDelegationManager1 = _defaultDelegationManager1; + defaultDelegationManager2 = _defaultDelegationManager2; + } + + ////////////////////////////// External Methods ////////////////////////////// + + /// @notice Allows this contract to receive the chain's native token. + receive() external payable { } + + /** + * @notice Approves an additional DelegationManager with full root execution authority. + * @dev A direct EOA self-transaction or DelegationManager-driven self-call may call this function. + * @param _delegationManager The additional DelegationManager to approve. + */ + function approveDelegationManager(IDelegationManager _delegationManager) external onlySelf { + _validateDelegationManager(_delegationManager); + if (_isApprovedDelegationManager(address(_delegationManager))) { + revert DelegationManagerAlreadyApproved(_delegationManager); + } + + _getMultiManagerStorage().isApprovedDelegationManager[address(_delegationManager)] = true; + emit ApprovedDelegationManager(_delegationManager); + } + + /** + * @notice Revokes an additional DelegationManager. + * @dev Permanent default DelegationManagers cannot be revoked. + * @param _delegationManager The additional DelegationManager to revoke. + */ + function revokeDelegationManager(IDelegationManager _delegationManager) external onlySelf { + if (_isDefaultDelegationManager(address(_delegationManager))) { + revert DefaultDelegationManagerCannotBeRevoked(_delegationManager); + } + + MultiManagerStorage storage multiManagerStorage_ = _getMultiManagerStorage(); + if (!multiManagerStorage_.isApprovedDelegationManager[address(_delegationManager)]) { + revert DelegationManagerNotApproved(_delegationManager); + } + + multiManagerStorage_.isApprovedDelegationManager[address(_delegationManager)] = false; + emit RevokedDelegationManager(_delegationManager); + } + + /** + * @notice Redeems delegations through a selected approved DelegationManager. + * @param _delegationManager The DelegationManager through which to redeem. + * @param _permissionContexts Delegation chains ordered from leaf to root. + * @param _modes Execution modes corresponding to each chain. + * @param _executionCallDatas Encoded executions corresponding to each chain. + */ + function redeemDelegations( + IDelegationManager _delegationManager, + bytes[] calldata _permissionContexts, + ModeCode[] calldata _modes, + bytes[] calldata _executionCallDatas + ) + external + onlySelf + onlyApprovedDelegationManager(_delegationManager) + { + _delegationManager.redeemDelegations(_permissionContexts, _modes, _executionCallDatas); + } + + /** + * @notice Executes a single call from this account. + * @param _execution The execution to perform. + */ + function execute(Execution calldata _execution) external payable onlySelf { + _execute(_execution.target, _execution.value, _execution.callData); + } + + /** + * @notice Executes calls from this account using an ERC-7579 execution mode. + * @param _mode The execution mode. + * @param _executionCalldata The encoded execution data. + */ + function execute(ModeCode _mode, bytes calldata _executionCalldata) external payable onlySelf { + (CallType callType_, ExecType execType_) = _validateExecutionMode(_mode); + + if (callType_ == CALLTYPE_BATCH) { + Execution[] calldata executions_ = _executionCalldata.decodeBatch(); + if (execType_ == EXECTYPE_DEFAULT) _execute(executions_); + else if (execType_ == EXECTYPE_TRY) _tryExecute(executions_); + else revert UnsupportedExecType(execType_); + } else if (callType_ == CALLTYPE_SINGLE) { + (address target_, uint256 value_, bytes calldata callData_) = _executionCalldata.decodeSingle(); + if (execType_ == EXECTYPE_DEFAULT) { + _execute(target_, value_, callData_); + } else if (execType_ == EXECTYPE_TRY) { + bytes[] memory returnData_ = new bytes[](1); + bool success_; + (success_, returnData_[0]) = _tryExecute(target_, value_, callData_); + if (!success_) emit TryExecuteUnsuccessful(0, returnData_[0]); + } else { + revert UnsupportedExecType(execType_); + } + } else { + revert UnsupportedCallType(callType_); + } + } + + /** + * @inheritdoc IDeleGatorCore + * @dev Every default or approved additional DelegationManager has full root authority through this function. + */ + function executeFromExecutor( + ModeCode _mode, + bytes calldata _executionCalldata + ) + external + payable + onlyDelegationManager + returns (bytes[] memory returnData_) + { + (CallType callType_, ExecType execType_) = _validateExecutionMode(_mode); + + if (callType_ == CALLTYPE_BATCH) { + Execution[] calldata executions_ = _executionCalldata.decodeBatch(); + if (execType_ == EXECTYPE_DEFAULT) returnData_ = _execute(executions_); + else if (execType_ == EXECTYPE_TRY) returnData_ = _tryExecute(executions_); + else revert UnsupportedExecType(execType_); + } else if (callType_ == CALLTYPE_SINGLE) { + (address target_, uint256 value_, bytes calldata callData_) = _executionCalldata.decodeSingle(); + returnData_ = new bytes[](1); + bool success_; + if (execType_ == EXECTYPE_DEFAULT) { + returnData_[0] = _execute(target_, value_, callData_); + } else if (execType_ == EXECTYPE_TRY) { + (success_, returnData_[0]) = _tryExecute(target_, value_, callData_); + if (!success_) emit TryExecuteUnsuccessful(0, returnData_[0]); + } else { + revert UnsupportedExecType(execType_); + } + } else { + revert UnsupportedCallType(callType_); + } + } + + /** + * @inheritdoc IERC1271 + * @notice Verifies a signature from the EOA whose address hosts this delegated code. + */ + function isValidSignature( + bytes32 _hash, + bytes calldata _signature + ) + external + view + override + onlyProxy + returns (bytes4 magicValue_) + { + return _isValidSignature(_hash, _signature); + } + + /// @inheritdoc IERC721Receiver + function onERC721Received(address, address, uint256, bytes memory) external view override onlyProxy returns (bytes4) { + return this.onERC721Received.selector; + } + + /// @inheritdoc IERC1155Receiver + function onERC1155Received(address, address, uint256, uint256, bytes memory) external view override onlyProxy returns (bytes4) { + return this.onERC1155Received.selector; + } + + /// @inheritdoc IERC1155Receiver + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) + external + view + override + onlyProxy + returns (bytes4) + { + return this.onERC1155BatchReceived.selector; + } + + /** + * @notice Disables a delegation through a selected approved DelegationManager. + * @param _delegationManager The DelegationManager that stores the disabled state. + * @param _delegation The delegation to disable. + */ + function disableDelegation( + IDelegationManager _delegationManager, + Delegation calldata _delegation + ) + external + onlySelf + onlyApprovedDelegationManager(_delegationManager) + { + _delegationManager.disableDelegation(_delegation); + } + + /** + * @notice Enables a delegation through a selected approved DelegationManager. + * @param _delegationManager The DelegationManager that stores the disabled state. + * @param _delegation The delegation to enable. + */ + function enableDelegation( + IDelegationManager _delegationManager, + Delegation calldata _delegation + ) + external + onlySelf + onlyApprovedDelegationManager(_delegationManager) + { + _delegationManager.enableDelegation(_delegation); + } + + /** + * @notice Returns whether a DelegationManager is a permanent default or approved additional DelegationManager. + * @param _delegationManager The DelegationManager to query. + * @return Whether the DelegationManager is approved. + */ + function isApprovedDelegationManager(IDelegationManager _delegationManager) external view returns (bool) { + return _isApprovedDelegationManager(address(_delegationManager)); + } + + /** + * @notice Returns whether a delegation is disabled in a selected approved DelegationManager. + * @param _delegationManager The DelegationManager to query. + * @param _delegationHash The delegation hash to query. + * @return Whether the delegation is disabled. + */ + function isDelegationDisabled( + IDelegationManager _delegationManager, + bytes32 _delegationHash + ) + external + view + onlyApprovedDelegationManager(_delegationManager) + returns (bool) + { + return _delegationManager.disabledDelegations(_delegationHash); + } + + /** + * @notice Returns whether an ERC-7579 execution mode is supported. + * @param _mode The mode to validate. + * @return Whether the mode is supported. + */ + function supportsExecutionMode(ModeCode _mode) external view virtual override returns (bool) { + (CallType callType_, ExecType execType_, ModeSelector modeSelector_, ModePayload modePayload_) = _mode.decode(); + + return ((callType_ == CALLTYPE_SINGLE || callType_ == CALLTYPE_BATCH) + && (execType_ == EXECTYPE_DEFAULT || execType_ == EXECTYPE_TRY) && (modeSelector_ == MODE_DEFAULT) + && (ModePayload.unwrap(modePayload_) == bytes22(0))); + } + + /** + * @inheritdoc IERC165 + * @dev Supports IDeleGatorCore, IERC721Receiver, IERC1155Receiver, IERC165, IERC1271, and IERC7821. + */ + function supportsInterface(bytes4 _interfaceId) public view virtual override(IERC165) onlyProxy returns (bool) { + return _interfaceId == type(IDeleGatorCore).interfaceId || _interfaceId == type(IERC721Receiver).interfaceId + || _interfaceId == type(IERC1155Receiver).interfaceId || _interfaceId == type(IERC165).interfaceId + || _interfaceId == type(IERC1271).interfaceId || _interfaceId == type(IERC7821).interfaceId; + } + + ////////////////////////////// Internal Methods ////////////////////////////// + + /** + * @notice Verifies a signature according to the implementing contract's signature scheme. + * @param _hash The signed hash. + * @param _signature The signature. + * @return The ERC-1271 validation result. + */ + function _isValidSignature(bytes32 _hash, bytes calldata _signature) internal view virtual returns (bytes4); + + ////////////////////////////// Private Methods ////////////////////////////// + + /// @dev Returns whether an address is one of the two permanent default DelegationManagers. + function _isDefaultDelegationManager(address _delegationManager) private view returns (bool) { + return _delegationManager == address(defaultDelegationManager1) || _delegationManager == address(defaultDelegationManager2); + } + + /// @dev Returns whether an address is a default or approved additional DelegationManager. + function _isApprovedDelegationManager(address _delegationManager) private view returns (bool) { + return _isDefaultDelegationManager(_delegationManager) + || _getMultiManagerStorage().isApprovedDelegationManager[_delegationManager]; + } + + /// @dev Validates a DelegationManager configured in the constructor or mutable approval mapping. + function _validateDelegationManager(IDelegationManager _delegationManager) private view { + address delegationManager_ = address(_delegationManager); + if (delegationManager_ == address(0)) revert InvalidDelegationManager(); + if (delegationManager_.code.length == 0) revert DelegationManagerHasNoCode(_delegationManager); + } + + /// @dev Validates mode selector and payload before execution and returns its call and execution types. + function _validateExecutionMode(ModeCode _mode) private pure returns (CallType callType_, ExecType execType_) { + ModeSelector modeSelector_; + ModePayload modePayload_; + (callType_, execType_, modeSelector_, modePayload_) = _mode.decode(); + if ( + ModeSelector.unwrap(modeSelector_) != ModeSelector.unwrap(MODE_DEFAULT) + || ModePayload.unwrap(modePayload_) != bytes22(0) + ) { + revert UnsupportedCallType(callType_); + } + } + + /// @dev Returns the namespaced mutable additional-DelegationManager storage. + function _getMultiManagerStorage() private pure returns (MultiManagerStorage storage multiManagerStorage_) { + bytes32 location_ = MULTI_MANAGER_STORAGE_LOCATION; + assembly { + multiManagerStorage_.slot := location_ + } + } +} diff --git a/test/EIP7702MultiManagerDeleGatorTest.t.sol b/test/EIP7702MultiManagerDeleGatorTest.t.sol new file mode 100644 index 00000000..52f7a537 --- /dev/null +++ b/test/EIP7702MultiManagerDeleGatorTest.t.sol @@ -0,0 +1,543 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; +import { CALLTYPE_DELEGATECALL, ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { IERC1271 } from "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import { IERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; +import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; + +import { BaseTest } from "./utils/BaseTest.t.sol"; +import { Counter } from "./utils/Counter.t.sol"; +import { SigningUtilsLib } from "./utils/SigningUtilsLib.t.sol"; +import { StorageUtilsLib } from "./utils/StorageUtilsLib.t.sol"; +import { Implementation, SignatureType } from "./utils/Types.t.sol"; +import { DeployEIP7702MultiManagerDeleGator } from "../script/DeployEIP7702MultiManagerDeleGator.s.sol"; +import { DelegationManager } from "../src/DelegationManager.sol"; +import { EIP7702MultiManagerDeleGator } from "../src/EIP7702/EIP7702MultiManagerDeleGator.sol"; +import { EIP7702MultiManagerDeleGatorCore } from "../src/EIP7702/EIP7702MultiManagerDeleGatorCore.sol"; +import { IDelegationManager } from "../src/interfaces/IDelegationManager.sol"; +import { IERC7821 } from "../src/interfaces/IERC7821.sol"; +import { EncoderLib } from "../src/libraries/EncoderLib.sol"; +import { ERC1271Lib } from "../src/libraries/ERC1271Lib.sol"; +import { CALLTYPE_BATCH, CALLTYPE_SINGLE, EXECTYPE_DEFAULT, MODE_DEFAULT } from "../src/utils/Constants.sol"; +import { Caveat, Delegation, Execution, ExecType, ModeCode, ModePayload, ModeSelector } from "../src/utils/Types.sol"; + +contract EIP7702MultiManagerDeleGatorTest is BaseTest { + DelegationManager internal additionalDelegationManager; + Counter internal counter; + + constructor() { + IMPLEMENTATION = Implementation.EIP7702MultiManager; + SIGNATURE_TYPE = SignatureType.EOA; + } + + function setUp() public override { + super.setUp(); + additionalDelegationManager = new DelegationManager(makeAddr("Additional DelegationManager Owner")); + counter = new Counter(address(users.alice.deleGator)); + } + + function test_defaultsAndMetadata() public { + EIP7702MultiManagerDeleGator account_ = _account(); + assertEq(account_.NAME(), "EIP7702MultiManagerDeleGator"); + assertEq(account_.VERSION(), "1.0.0"); + assertEq(address(account_.defaultDelegationManager1()), address(delegationManager)); + assertEq(address(account_.defaultDelegationManager2()), address(defaultDelegationManager2)); + assertTrue(account_.isApprovedDelegationManager(delegationManager)); + assertTrue(account_.isApprovedDelegationManager(defaultDelegationManager2)); + assertFalse(account_.isApprovedDelegationManager(additionalDelegationManager)); + } + + function test_constructorRejectsInvalidDefaults() public { + IDelegationManager zero_ = IDelegationManager(address(0)); + IDelegationManager noCode_ = IDelegationManager(makeAddr("No code")); + + vm.expectRevert(EIP7702MultiManagerDeleGatorCore.InvalidDelegationManager.selector); + new EIP7702MultiManagerDeleGator(zero_, defaultDelegationManager2); + + vm.expectRevert(abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.DelegationManagerHasNoCode.selector, noCode_)); + new EIP7702MultiManagerDeleGator(noCode_, defaultDelegationManager2); + + vm.expectRevert(EIP7702MultiManagerDeleGatorCore.InvalidDelegationManager.selector); + new EIP7702MultiManagerDeleGator(delegationManager, zero_); + + vm.expectRevert(abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.DelegationManagerHasNoCode.selector, noCode_)); + new EIP7702MultiManagerDeleGator(delegationManager, noCode_); + + vm.expectRevert(EIP7702MultiManagerDeleGatorCore.DuplicateDefaultDelegationManager.selector); + new EIP7702MultiManagerDeleGator(delegationManager, delegationManager); + } + + function test_deploymentScriptDryRun() public { + vm.setEnv("SALT", "eip7702-multi-manager-test"); + vm.setEnv("DEFAULT_DELEGATION_MANAGER_1", vm.toString(address(delegationManager))); + vm.setEnv("DEFAULT_DELEGATION_MANAGER_2", vm.toString(address(defaultDelegationManager2))); + DeployEIP7702MultiManagerDeleGator deployScript_ = new DeployEIP7702MultiManagerDeleGator(); + deployScript_.setUp(); + + EIP7702MultiManagerDeleGator implementation_ = deployScript_.run(); + assertEq(address(implementation_.defaultDelegationManager1()), address(delegationManager)); + assertEq(address(implementation_.defaultDelegationManager2()), address(defaultDelegationManager2)); + } + + function test_defaultsCannotBeApprovedOrRevoked() public { + EIP7702MultiManagerDeleGator account_ = _account(); + vm.startPrank(address(account_)); + + vm.expectRevert( + abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.DelegationManagerAlreadyApproved.selector, delegationManager) + ); + account_.approveDelegationManager(delegationManager); + + vm.expectRevert( + abi.encodeWithSelector( + EIP7702MultiManagerDeleGatorCore.DelegationManagerAlreadyApproved.selector, defaultDelegationManager2 + ) + ); + account_.approveDelegationManager(defaultDelegationManager2); + + vm.expectRevert( + abi.encodeWithSelector( + EIP7702MultiManagerDeleGatorCore.DefaultDelegationManagerCannotBeRevoked.selector, delegationManager + ) + ); + account_.revokeDelegationManager(delegationManager); + + vm.expectRevert( + abi.encodeWithSelector( + EIP7702MultiManagerDeleGatorCore.DefaultDelegationManagerCannotBeRevoked.selector, defaultDelegationManager2 + ) + ); + account_.revokeDelegationManager(defaultDelegationManager2); + vm.stopPrank(); + } + + function test_directSelfAdministersAdditionalDelegationManager() public { + EIP7702MultiManagerDeleGator account_ = _account(); + + vm.expectEmit(true, false, false, true); + emit EIP7702MultiManagerDeleGatorCore.ApprovedDelegationManager(additionalDelegationManager); + vm.prank(address(account_)); + account_.approveDelegationManager(additionalDelegationManager); + assertTrue(account_.isApprovedDelegationManager(additionalDelegationManager)); + + vm.expectEmit(true, false, false, true); + emit EIP7702MultiManagerDeleGatorCore.RevokedDelegationManager(additionalDelegationManager); + vm.prank(address(account_)); + account_.revokeDelegationManager(additionalDelegationManager); + assertFalse(account_.isApprovedDelegationManager(additionalDelegationManager)); + } + + function test_exactERC7201SlotStoresMutableApproval() public { + bytes32 storageLocation_ = StorageUtilsLib.getStorageLocation("DeleGator.EIP7702MultiManager.v1"); + assertEq(storageLocation_, 0x4ffe8763039a0c1b60d7504fab186e7e570dca76b49af35bcd94b7cdae58b300); + + _approveAdditional(); + bytes32 approvalSlot_ = keccak256(abi.encode(address(additionalDelegationManager), storageLocation_)); + assertEq(vm.load(address(_account()), approvalSlot_), bytes32(uint256(1))); + + vm.prank(address(_account())); + _account().revokeDelegationManager(additionalDelegationManager); + assertEq(vm.load(address(_account()), approvalSlot_), bytes32(0)); + } + + function test_rejectsInvalidDuplicateAbsentAndUnauthorizedAdministration() public { + EIP7702MultiManagerDeleGator account_ = _account(); + IDelegationManager noCode_ = IDelegationManager(makeAddr("No code")); + + vm.expectRevert(EIP7702MultiManagerDeleGatorCore.NotSelf.selector); + account_.approveDelegationManager(additionalDelegationManager); + + vm.startPrank(address(account_)); + vm.expectRevert(EIP7702MultiManagerDeleGatorCore.InvalidDelegationManager.selector); + account_.approveDelegationManager(IDelegationManager(address(0))); + + vm.expectRevert(abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.DelegationManagerHasNoCode.selector, noCode_)); + account_.approveDelegationManager(noCode_); + + account_.approveDelegationManager(additionalDelegationManager); + vm.expectRevert( + abi.encodeWithSelector( + EIP7702MultiManagerDeleGatorCore.DelegationManagerAlreadyApproved.selector, additionalDelegationManager + ) + ); + account_.approveDelegationManager(additionalDelegationManager); + account_.revokeDelegationManager(additionalDelegationManager); + + vm.expectRevert( + abi.encodeWithSelector( + EIP7702MultiManagerDeleGatorCore.DelegationManagerNotApproved.selector, additionalDelegationManager + ) + ); + account_.revokeDelegationManager(additionalDelegationManager); + vm.stopPrank(); + } + + function test_defaultAndAdditionalExecutorPaths() public { + EIP7702MultiManagerDeleGator account_ = _account(); + _approveAdditional(); + bytes memory execution_ = ExecutionLib.encodeSingle(address(counter), 0, abi.encodeCall(Counter.unsafeIncrement, ())); + + vm.prank(address(delegationManager)); + account_.executeFromExecutor(singleDefaultMode, execution_); + vm.prank(address(defaultDelegationManager2)); + account_.executeFromExecutor(singleDefaultMode, execution_); + vm.prank(address(additionalDelegationManager)); + account_.executeFromExecutor(singleDefaultMode, execution_); + assertEq(counter.count(), 3); + + vm.prank(address(account_)); + account_.revokeDelegationManager(additionalDelegationManager); + vm.expectRevert( + abi.encodeWithSelector( + EIP7702MultiManagerDeleGatorCore.DelegationManagerNotApproved.selector, additionalDelegationManager + ) + ); + vm.prank(address(additionalDelegationManager)); + account_.executeFromExecutor(singleDefaultMode, execution_); + } + + function test_delegationManagerSelfCallAdministersMutableSet() public { + EIP7702MultiManagerDeleGator account_ = _account(); + bytes memory approveCall_ = ExecutionLib.encodeSingle( + address(account_), 0, abi.encodeCall(account_.approveDelegationManager, (additionalDelegationManager)) + ); + + vm.prank(address(delegationManager)); + account_.executeFromExecutor(singleDefaultMode, approveCall_); + assertTrue(account_.isApprovedDelegationManager(additionalDelegationManager)); + + bytes memory revokeCall_ = ExecutionLib.encodeSingle( + address(account_), 0, abi.encodeCall(account_.revokeDelegationManager, (additionalDelegationManager)) + ); + vm.prank(address(additionalDelegationManager)); + account_.executeFromExecutor(singleDefaultMode, revokeCall_); + assertFalse(account_.isApprovedDelegationManager(additionalDelegationManager)); + } + + function test_delegationManagerCannotSelfCallRevokeDefault() public { + EIP7702MultiManagerDeleGator account_ = _account(); + bytes memory revokeDefaultCall_ = ExecutionLib.encodeSingle( + address(account_), 0, abi.encodeCall(account_.revokeDelegationManager, (delegationManager)) + ); + + vm.expectRevert( + abi.encodeWithSelector( + EIP7702MultiManagerDeleGatorCore.DefaultDelegationManagerCannotBeRevoked.selector, delegationManager + ) + ); + vm.prank(address(defaultDelegationManager2)); + account_.executeFromExecutor(singleDefaultMode, revokeDefaultCall_); + assertTrue(account_.isApprovedDelegationManager(delegationManager)); + } + + function test_batchRollbackRevertsManagerAdministration() public { + EIP7702MultiManagerDeleGator account_ = _account(); + Execution[] memory executions_ = new Execution[](2); + executions_[0] = + Execution(address(account_), 0, abi.encodeCall(account_.approveDelegationManager, (additionalDelegationManager))); + executions_[1] = Execution(address(new Reverter()), 0, abi.encodeCall(Reverter.fail, ())); + + vm.expectRevert(); + vm.prank(address(delegationManager)); + account_.executeFromExecutor(batchDefaultMode, ExecutionLib.encodeBatch(executions_)); + assertFalse(account_.isApprovedDelegationManager(additionalDelegationManager)); + } + + function test_batchTryCommitsApprovalBeforeLaterFailure() public { + EIP7702MultiManagerDeleGator account_ = _account(); + Execution[] memory executions_ = new Execution[](2); + executions_[0] = + Execution(address(account_), 0, abi.encodeCall(account_.approveDelegationManager, (additionalDelegationManager))); + executions_[1] = Execution(address(new Reverter()), 0, abi.encodeCall(Reverter.fail, ())); + + vm.prank(address(delegationManager)); + bytes[] memory returnData_ = account_.executeFromExecutor(batchTryMode, ExecutionLib.encodeBatch(executions_)); + + assertEq(returnData_.length, 2); + assertTrue(account_.isApprovedDelegationManager(additionalDelegationManager)); + } + + function test_approvedReentrantDelegationManagerCanAdminister() public { + EIP7702MultiManagerDeleGator account_ = _account(); + ReentrantDelegationManager reentrant_ = new ReentrantDelegationManager(account_, singleDefaultMode); + vm.prank(address(account_)); + account_.approveDelegationManager(IDelegationManager(address(reentrant_))); + + bytes memory outer_ = ExecutionLib.encodeSingle( + address(reentrant_), + 0, + abi.encodeCall(ReentrantDelegationManager.approve, (IDelegationManager(address(additionalDelegationManager)))) + ); + vm.prank(address(delegationManager)); + account_.executeFromExecutor(singleDefaultMode, outer_); + assertTrue(account_.isApprovedDelegationManager(additionalDelegationManager)); + } + + function test_managerSpecificRedeemEnableDisableAndStatus() public { + EIP7702MultiManagerDeleGator account_ = _account(); + _approveAdditional(); + Delegation memory delegation_ = _signedDelegation(additionalDelegationManager); + bytes32 delegationHash_ = EncoderLib._getDelegationHash(delegation_); + + vm.prank(address(account_)); + account_.disableDelegation(additionalDelegationManager, delegation_); + assertTrue(account_.isDelegationDisabled(additionalDelegationManager, delegationHash_)); + assertFalse(account_.isDelegationDisabled(delegationManager, delegationHash_)); + + vm.prank(address(account_)); + account_.enableDelegation(additionalDelegationManager, delegation_); + assertFalse(account_.isDelegationDisabled(additionalDelegationManager, delegationHash_)); + + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + bytes[] memory contexts_ = new bytes[](1); + contexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = singleDefaultMode; + bytes[] memory executions_ = new bytes[](1); + executions_[0] = ExecutionLib.encodeSingle(address(counter), 0, abi.encodeCall(Counter.unsafeIncrement, ())); + + vm.prank(users.bob.addr); + additionalDelegationManager.redeemDelegations(contexts_, modes_, executions_); + assertEq(counter.count(), 1); + + Delegation memory defaultDelegation_ = _signedDelegation(delegationManager); + defaultDelegation_.delegate = address(account_); + defaultDelegation_.signature = ""; + bytes32 defaultTypedDataHash_ = MessageHashUtils.toTypedDataHash( + delegationManager.getDomainHash(), EncoderLib._getDelegationHash(defaultDelegation_) + ); + defaultDelegation_.signature = SigningUtilsLib.signHash_EOA(users.alice.privateKey, defaultTypedDataHash_); + delegations_[0] = defaultDelegation_; + contexts_[0] = abi.encode(delegations_); + vm.prank(address(account_)); + account_.redeemDelegations(delegationManager, contexts_, modes_, executions_); + assertEq(counter.count(), 2); + } + + function test_unapprovedDelegationManagerRoutingAndStatusRevert() public { + EIP7702MultiManagerDeleGator account_ = _account(); + bytes[] memory contexts_ = new bytes[](0); + ModeCode[] memory modes_ = new ModeCode[](0); + bytes[] memory executions_ = new bytes[](0); + bytes memory expectedError_ = abi.encodeWithSelector( + EIP7702MultiManagerDeleGatorCore.DelegationManagerNotApproved.selector, additionalDelegationManager + ); + + vm.expectRevert(expectedError_); + vm.prank(address(account_)); + account_.redeemDelegations(additionalDelegationManager, contexts_, modes_, executions_); + + vm.expectRevert(expectedError_); + account_.isDelegationDisabled(additionalDelegationManager, bytes32(0)); + } + + function test_unapprovedDelegationManagerDirectRedemptionRevertsAtAccount() public { + Delegation memory delegation_ = _signedDelegation(additionalDelegationManager); + Delegation[] memory delegations_ = new Delegation[](1); + delegations_[0] = delegation_; + bytes[] memory contexts_ = new bytes[](1); + contexts_[0] = abi.encode(delegations_); + ModeCode[] memory modes_ = new ModeCode[](1); + modes_[0] = singleDefaultMode; + bytes[] memory executions_ = new bytes[](1); + executions_[0] = ExecutionLib.encodeSingle(address(counter), 0, abi.encodeCall(Counter.unsafeIncrement, ())); + + vm.prank(users.bob.addr); + vm.expectRevert( + abi.encodeWithSelector( + EIP7702MultiManagerDeleGatorCore.DelegationManagerNotApproved.selector, additionalDelegationManager + ) + ); + additionalDelegationManager.redeemDelegations(contexts_, modes_, executions_); + assertEq(counter.count(), 0); + } + + function test_allSupportedExecutionModes() public { + EIP7702MultiManagerDeleGator account_ = _account(); + bytes memory success_ = ExecutionLib.encodeSingle(address(counter), 0, abi.encodeCall(Counter.unsafeIncrement, ())); + bytes memory failure_ = ExecutionLib.encodeSingle(address(new Reverter()), 0, abi.encodeCall(Reverter.fail, ())); + Execution[] memory batchSuccess_ = new Execution[](2); + batchSuccess_[0] = Execution(address(counter), 0, abi.encodeCall(Counter.unsafeIncrement, ())); + batchSuccess_[1] = batchSuccess_[0]; + Execution[] memory batchTry_ = new Execution[](2); + batchTry_[0] = batchSuccess_[0]; + batchTry_[1] = Execution(address(new Reverter()), 0, abi.encodeCall(Reverter.fail, ())); + + vm.startPrank(address(delegationManager)); + account_.executeFromExecutor(singleDefaultMode, success_); + account_.executeFromExecutor(singleTryMode, failure_); + account_.executeFromExecutor(batchDefaultMode, ExecutionLib.encodeBatch(batchSuccess_)); + account_.executeFromExecutor(batchTryMode, ExecutionLib.encodeBatch(batchTry_)); + vm.stopPrank(); + assertEq(counter.count(), 4); + + vm.startPrank(address(account_)); + account_.execute(Execution(address(counter), 0, abi.encodeCall(Counter.unsafeIncrement, ()))); + account_.execute(singleDefaultMode, success_); + account_.execute(singleTryMode, failure_); + account_.execute(batchDefaultMode, ExecutionLib.encodeBatch(batchSuccess_)); + account_.execute(batchTryMode, ExecutionLib.encodeBatch(batchTry_)); + vm.stopPrank(); + assertEq(counter.count(), 9); + } + + function test_fullModeValidationMatchesSupportsExecutionMode() public { + EIP7702MultiManagerDeleGator account_ = _account(); + bytes memory execution_ = ExecutionLib.encodeSingle(address(counter), 0, abi.encodeCall(Counter.unsafeIncrement, ())); + ModeCode selectorMode_ = + ModeLib.encode(CALLTYPE_SINGLE, EXECTYPE_DEFAULT, ModeSelector.wrap(0x01020304), ModePayload.wrap(0)); + ModeCode payloadMode_ = + ModeLib.encode(CALLTYPE_SINGLE, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(bytes22(uint176(1)))); + ModeCode execMode_ = ModeLib.encode(CALLTYPE_SINGLE, ExecType.wrap(0x02), MODE_DEFAULT, ModePayload.wrap(0)); + ModeCode batchExecMode_ = ModeLib.encode(CALLTYPE_BATCH, ExecType.wrap(0x02), MODE_DEFAULT, ModePayload.wrap(0)); + ModeCode invalidCallMode_ = ModeLib.encode(CALLTYPE_DELEGATECALL, EXECTYPE_DEFAULT, MODE_DEFAULT, ModePayload.wrap(0)); + + assertFalse(account_.supportsExecutionMode(selectorMode_)); + assertFalse(account_.supportsExecutionMode(payloadMode_)); + assertFalse(account_.supportsExecutionMode(execMode_)); + assertFalse(account_.supportsExecutionMode(invalidCallMode_)); + + vm.startPrank(address(delegationManager)); + vm.expectRevert(abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.UnsupportedCallType.selector, CALLTYPE_SINGLE)); + account_.executeFromExecutor(selectorMode_, execution_); + vm.expectRevert(abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.UnsupportedCallType.selector, CALLTYPE_SINGLE)); + account_.executeFromExecutor(payloadMode_, execution_); + vm.expectRevert(abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.UnsupportedExecType.selector, ExecType.wrap(0x02))); + account_.executeFromExecutor(execMode_, execution_); + vm.expectRevert(abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.UnsupportedExecType.selector, ExecType.wrap(0x02))); + account_.executeFromExecutor(batchExecMode_, ExecutionLib.encodeBatch(new Execution[](0))); + vm.expectRevert( + abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.UnsupportedCallType.selector, CALLTYPE_DELEGATECALL) + ); + account_.executeFromExecutor(invalidCallMode_, execution_); + vm.stopPrank(); + + vm.startPrank(address(account_)); + vm.expectRevert(abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.UnsupportedCallType.selector, CALLTYPE_SINGLE)); + account_.execute(selectorMode_, execution_); + vm.expectRevert(abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.UnsupportedExecType.selector, ExecType.wrap(0x02))); + account_.execute(execMode_, execution_); + vm.expectRevert(abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.UnsupportedExecType.selector, ExecType.wrap(0x02))); + account_.execute(batchExecMode_, ExecutionLib.encodeBatch(new Execution[](0))); + vm.expectRevert( + abi.encodeWithSelector(EIP7702MultiManagerDeleGatorCore.UnsupportedCallType.selector, CALLTYPE_DELEGATECALL) + ); + account_.execute(invalidCallMode_, execution_); + vm.stopPrank(); + } + + function test_erc1271InterfacesAndTokenReceivers() public { + EIP7702MultiManagerDeleGator account_ = _account(); + bytes32 hash_ = keccak256("multi-DelegationManager"); + bytes memory signature_ = SigningUtilsLib.signHash_EOA(users.alice.privateKey, hash_); + assertEq(account_.isValidSignature(hash_, signature_), ERC1271Lib.EIP1271_MAGIC_VALUE); + assertEq( + account_.isValidSignature(hash_, SigningUtilsLib.signHash_EOA(users.bob.privateKey, hash_)), + ERC1271Lib.SIG_VALIDATION_FAILED + ); + + assertTrue(account_.supportsInterface(type(IERC165).interfaceId)); + assertTrue(account_.supportsInterface(type(IERC1271).interfaceId)); + assertTrue(account_.supportsInterface(type(IERC721Receiver).interfaceId)); + assertTrue(account_.supportsInterface(type(IERC1155Receiver).interfaceId)); + assertTrue(account_.supportsInterface(type(IERC7821).interfaceId)); + assertEq(account_.onERC721Received(address(this), address(this), 1, ""), IERC721Receiver.onERC721Received.selector); + assertEq(account_.onERC1155Received(address(this), address(this), 1, 1, ""), IERC1155Receiver.onERC1155Received.selector); + assertEq( + account_.onERC1155BatchReceived(address(this), address(this), new uint256[](0), new uint256[](0), ""), + IERC1155Receiver.onERC1155BatchReceived.selector + ); + + vm.expectRevert(EIP7702MultiManagerDeleGatorCore.UnauthorizedCallContext.selector); + eip7702MultiManagerDeleGatorImpl.isValidSignature(hash_, signature_); + } + + function test_sameImplementationStoragePersistsAcrossCodeChanges() public { + EIP7702MultiManagerDeleGator account_ = _account(); + _approveAdditional(); + vm.etch(address(account_), ""); + vm.etch(address(account_), bytes.concat(hex"ef0100", abi.encodePacked(eip7702MultiManagerDeleGatorImpl))); + + assertTrue(account_.isApprovedDelegationManager(delegationManager)); + assertTrue(account_.isApprovedDelegationManager(defaultDelegationManager2)); + assertTrue(account_.isApprovedDelegationManager(additionalDelegationManager)); + } + + function testFuzz_additionalDelegationManagerTransition(address _delegationManager) public { + EIP7702MultiManagerDeleGator account_ = _account(); + vm.assume( + uint160(_delegationManager) > 0xff && _delegationManager != address(account_) + && _delegationManager != address(delegationManager) && _delegationManager != address(defaultDelegationManager2) + ); + vm.etch(_delegationManager, hex"00"); + IDelegationManager delegationManager_ = IDelegationManager(_delegationManager); + + vm.prank(address(account_)); + account_.approveDelegationManager(delegationManager_); + assertTrue(account_.isApprovedDelegationManager(delegationManager_)); + vm.prank(address(account_)); + account_.revokeDelegationManager(delegationManager_); + assertFalse(account_.isApprovedDelegationManager(delegationManager_)); + } + + function testFuzz_malformedCalldataReverts(bytes calldata _calldata) public { + vm.assume(_calldata.length < 52); + vm.expectRevert(); + vm.prank(address(delegationManager)); + _account().executeFromExecutor(singleDefaultMode, _calldata); + } + + function _account() internal view returns (EIP7702MultiManagerDeleGator) { + return EIP7702MultiManagerDeleGator(payable(address(users.alice.deleGator))); + } + + function _approveAdditional() internal { + EIP7702MultiManagerDeleGator account_ = _account(); + vm.prank(address(account_)); + account_.approveDelegationManager(additionalDelegationManager); + } + + function _signedDelegation(DelegationManager _delegationManager) internal view returns (Delegation memory delegation_) { + delegation_ = Delegation({ + delegate: users.bob.addr, + delegator: address(_account()), + authority: _delegationManager.ROOT_AUTHORITY(), + caveats: new Caveat[](0), + salt: 0, + signature: "" + }); + bytes32 typedDataHash_ = + MessageHashUtils.toTypedDataHash(_delegationManager.getDomainHash(), EncoderLib._getDelegationHash(delegation_)); + delegation_.signature = SigningUtilsLib.signHash_EOA(users.alice.privateKey, typedDataHash_); + } + + receive() external payable { } +} + +contract ReentrantDelegationManager { + EIP7702MultiManagerDeleGator internal immutable account; + ModeCode internal immutable mode; + + constructor(EIP7702MultiManagerDeleGator _account, ModeCode _mode) { + account = _account; + mode = _mode; + } + + function approve(IDelegationManager _delegationManager) external { + account.executeFromExecutor( + mode, + ExecutionLib.encodeSingle(address(account), 0, abi.encodeCall(account.approveDelegationManager, (_delegationManager))) + ); + } +} + +contract Reverter { + function fail() external pure { + revert(); + } +} diff --git a/test/utils/BaseTest.t.sol b/test/utils/BaseTest.t.sol index 268d5301..bcbb6fca 100644 --- a/test/utils/BaseTest.t.sol +++ b/test/utils/BaseTest.t.sol @@ -30,6 +30,7 @@ import { DelegationManager } from "../../src/DelegationManager.sol"; import { DeleGatorCore } from "../../src/DeleGatorCore.sol"; import { HybridDeleGator } from "../../src/HybridDeleGator.sol"; import { MultiSigDeleGator } from "../../src/MultiSigDeleGator.sol"; +import { EIP7702MultiManagerDeleGator } from "../../src/EIP7702/EIP7702MultiManagerDeleGator.sol"; import { EIP7702StatelessDeleGator } from "../../src/EIP7702/EIP7702StatelessDeleGator.sol"; import "forge-std/Test.sol"; @@ -54,11 +55,13 @@ abstract contract BaseTest is Test { // Delegation Manager DelegationManager public delegationManager; + DelegationManager public defaultDelegationManager2; // DeleGator Implementations HybridDeleGator public hybridDeleGatorImpl; MultiSigDeleGator public multiSigDeleGatorImpl; EIP7702StatelessDeleGator public eip7702StatelessDeleGatorImpl; + EIP7702MultiManagerDeleGator public eip7702MultiManagerDeleGatorImpl; // Users TestUsers internal users; @@ -88,6 +91,8 @@ abstract contract BaseTest is Test { // DelegationManager delegationManager = new DelegationManager(makeAddr("DelegationManager Owner")); vm.label(address(delegationManager), "Delegation Manager"); + defaultDelegationManager2 = new DelegationManager(makeAddr("Default DelegationManager 2 Owner")); + vm.label(address(defaultDelegationManager2), "Default DelegationManager 2"); // Set constant values for easy access ROOT_AUTHORITY = delegationManager.ROOT_AUTHORITY(); @@ -107,6 +112,9 @@ abstract contract BaseTest is Test { eip7702StatelessDeleGatorImpl = new EIP7702StatelessDeleGator(delegationManager, entryPoint); vm.label(address(eip7702StatelessDeleGatorImpl), "EIP7702Stateless DeleGator"); + eip7702MultiManagerDeleGatorImpl = new EIP7702MultiManagerDeleGator(delegationManager, defaultDelegationManager2); + vm.label(address(eip7702MultiManagerDeleGatorImpl), "EIP7702 MultiManager DeleGator"); + // Create users users = _createUsers(); @@ -437,6 +445,8 @@ abstract contract BaseTest is Test { return deployDeleGator_MultiSig(_user); } else if (_implementation == Implementation.EIP7702Stateless) { return deployDeleGator_EIP7702Stateless(_user); + } else if (_implementation == Implementation.EIP7702MultiManager) { + return deployDeleGator_EIP7702MultiManager(_user.addr); } else { revert("Invalid Implementation"); } @@ -492,6 +502,11 @@ abstract contract BaseTest is Test { return _eoaAddress; } + function deployDeleGator_EIP7702MultiManager(address _eoaAddress) public returns (address) { + vm.etch(_eoaAddress, bytes.concat(hex"ef0100", abi.encodePacked(eip7702MultiManagerDeleGatorImpl))); + return _eoaAddress; + } + // Name is the seed used to generate the address, private key, and DeleGator. function createUser(string memory _name) public returns (TestUser memory user_) { (address addr_, uint256 privateKey_) = makeAddrAndKey(_name); diff --git a/test/utils/Types.t.sol b/test/utils/Types.t.sol index a93d5c9e..38ae7b1d 100644 --- a/test/utils/Types.t.sol +++ b/test/utils/Types.t.sol @@ -29,8 +29,8 @@ struct TestUsers { enum Implementation { MultiSig, // MultiSigDeleGator is a DeleGator that is owned by a set of EOA addresses. Hybrid, // HybridDeleGator is a DeleGator that is owned by a set of P256 Keys and EOA - EIP7702Stateless // EIP7702Stateless is a DeleGator that is owned by the EIP7702 EOA - + EIP7702Stateless, // EIP7702Stateless is a DeleGator that is owned by the EIP7702 EOA + EIP7702MultiManager // EIP7702MultiManager has two defaults and mutable additional DelegationManagers } /**