-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e0c5382
commit e003d71
Showing
4 changed files
with
184 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
} |