diff --git a/contracts/hooks/EIP4337Hook.sol b/contracts/hooks/EIP4337Hook.sol new file mode 100644 index 00000000..51a24873 --- /dev/null +++ b/contracts/hooks/EIP4337Hook.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.18; + +import {IEIP4337Hook, IAccount} from './interfaces/IEIP4337Hook.sol'; +import {IERC1271Wallet} from '../interfaces/IERC1271Wallet.sol'; +import {IModuleCalls} from '../modules/commons/interfaces/IModuleCalls.sol'; +import {ModuleNonce} from '../modules/commons/ModuleNonce.sol'; +import {LibOptim} from '../utils/LibOptim.sol'; + +contract EIP4337Hook is IEIP4337Hook, ModuleNonce { + address public immutable entrypoint; + uint256 private constant SIG_VALIDATION_FAILED = 1; + bytes4 private constant ERC1271_SELECTOR = 0x1626ba7e; + + /** + * Create the EIP-4337 hook for the given entrypoint. + */ + constructor(address _entrypoint) { + entrypoint = _entrypoint; + } + + modifier onlyEntrypoint() { + require(msg.sender == entrypoint, 'EIP4337Hook: only 4337 or self'); //FIXME error obj + _; + } + + /** + * Allow the EIP-4337 entrypoint to execute a transaction on the wallet. + * @dev This function does not validate as the entrypoint is trusted to have called validateUserOp. + * @dev Failure handling done by ModuleCalls. + */ + function eip4337SelfExecute(IModuleCalls.Transaction[] calldata txs) external payable onlyEntrypoint { + // Self execute + (bool success, ) = payable(address(this)).call{value: msg.value}( + abi.encodeWithSelector(IModuleCalls.selfExecute.selector, txs) + ); + //FIXME Required? selfexecute should revert if it fails + require(success, 'call failed'); // FIXME Better error? + } + + /** + * Validate and pay for user op. + * @dev This must be called by the entrypoint. + * @dev GAS opcode is banned during this call, thus max uint256 is used. + */ + function validateUserOp( + IAccount.UserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external onlyEntrypoint returns (uint256 validationData) { + // Check nonce + _validateNonce(userOp.nonce); //FIXME Sequence space encoding is diff to EIP-4337 encoding + + // Check signature + bytes32 ethHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", userOpHash)); + (bool sigSuccess, bytes memory data) = address(this).call{gas: type(uint256).max}( + abi.encodeWithSelector(ERC1271_SELECTOR, ethHash, userOp.signature) + ); + if (!sigSuccess || bytes4(data) != ERC1271_SELECTOR) { + // Failed to validate signature + return SIG_VALIDATION_FAILED; + } + + // Pay entrypoint + if (missingAccountFunds != 0) { + (bool success,) = payable(msg.sender).call{value: missingAccountFunds, gas: type(uint256).max}(''); + (success); + } + + // Success + return 0; + } +} diff --git a/contracts/hooks/interfaces/IAccount.sol b/contracts/hooks/interfaces/IAccount.sol new file mode 100644 index 00000000..ba2b3154 --- /dev/null +++ b/contracts/hooks/interfaces/IAccount.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.18; + +interface IAccount { + struct UserOperation { + address sender; + uint256 nonce; + bytes initCode; + bytes callData; + uint256 callGasLimit; + uint256 verificationGasLimit; + uint256 preVerificationGas; + uint256 maxFeePerGas; + uint256 maxPriorityFeePerGas; + bytes paymasterAndData; + bytes signature; + } + + function validateUserOp( + UserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256 validationData); +} diff --git a/contracts/hooks/interfaces/IEIP4337Hook.sol b/contracts/hooks/interfaces/IEIP4337Hook.sol new file mode 100644 index 00000000..cd40b450 --- /dev/null +++ b/contracts/hooks/interfaces/IEIP4337Hook.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.18; + +import "../../modules/commons/interfaces/IModuleCalls.sol"; +import "./IAccount.sol"; + +/** + * An extension to EIP-4337 that includes a self execute function. + */ +interface IEIP4337Hook is IAccount { + + /** + * @notice Allow wallet owner to execute an action. + * @param _txs Transactions to process + * @notice This functions is only callable by the Entrypoint. + */ + function eip4337SelfExecute( + IModuleCalls.Transaction[] calldata _txs + ) external payable; + +} diff --git a/foundry_test/hooks/EIP4337Hook.t.sol b/foundry_test/hooks/EIP4337Hook.t.sol new file mode 100644 index 00000000..062f89b2 --- /dev/null +++ b/foundry_test/hooks/EIP4337Hook.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.18; + +import 'contracts/hooks/EIP4337Hook.sol'; +import 'contracts/modules/commons/ModuleHooks.sol'; +import 'contracts/modules/MainModule.sol'; +import 'contracts/modules/MainModuleUpgradable.sol'; +import 'contracts/Factory.sol'; + +import 'foundry_test/base/AdvTest.sol'; + +contract EIP4337HookTest is AdvTest { + MainModule private template; + EIP4337Hook private wallet; + + address private constant ENTRYPOINT = address(uint160(uint256(keccak256('entrypoint')))); + + function setUp() external { + Factory factory = new Factory(); + address upgradeable = address(new MainModuleUpgradable()); + template = new MainModule(address(factory), upgradeable); + ModuleHooks walletMod = ModuleHooks(payable(factory.deploy(address(template), bytes32(0)))); // Add hook below + vm.label(address(walletMod), "wallet"); + EIP4337Hook hook = new EIP4337Hook(ENTRYPOINT); + + // Fund wallet + vm.deal(address(walletMod), 10 ether); + + // Add hooks + vm.startPrank(address(walletMod)); + walletMod.addHook(IAccount.validateUserOp.selector, address(hook)); + walletMod.addHook(IEIP4337Hook.eip4337SelfExecute.selector, address(hook)); + vm.stopPrank(); + + wallet = EIP4337Hook(address(walletMod)); + } + + struct ToVal { + address target; + uint256 value; + } + + function test_execute_sendEth(ToVal memory sendTx) external { + vm.assume(sendTx.target.code.length == 0); // Non contract + uint256 walletBal = address(wallet).balance; + vm.assume(sendTx.value <= walletBal); + + IModuleCalls.Transaction[] memory txs = new IModuleCalls.Transaction[](1); + txs[0] = IModuleCalls.Transaction({ + delegateCall: false, + revertOnError: false, + gasLimit: 0, + target: sendTx.target, + value: sendTx.value, + data: '' + }); + + vm.prank(ENTRYPOINT); + wallet.eip4337SelfExecute(txs); + + assertEq(address(wallet).balance, walletBal - sendTx.value); + assertEq(sendTx.target.balance, sendTx.value); + } + + function test_validateUserOp(ToVal[] memory sendTx) external {} +}