Skip to content

Commit

Permalink
EIP-4337 hook first pass
Browse files Browse the repository at this point in the history
  • Loading branch information
ScreamingHawk committed Sep 11, 2023
1 parent e0c5382 commit e003d71
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 0 deletions.
73 changes: 73 additions & 0 deletions contracts/hooks/EIP4337Hook.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
24 changes: 24 additions & 0 deletions contracts/hooks/interfaces/IAccount.sol
Original file line number Diff line number Diff line change
@@ -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);
}
21 changes: 21 additions & 0 deletions contracts/hooks/interfaces/IEIP4337Hook.sol
Original file line number Diff line number Diff line change
@@ -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;

}
66 changes: 66 additions & 0 deletions foundry_test/hooks/EIP4337Hook.t.sol
Original file line number Diff line number Diff line change
@@ -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 {}
}

0 comments on commit e003d71

Please sign in to comment.