diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 1bf9984c..058f211e 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.23; /* solhint-disable no-inline-assembly */ import "../interfaces/IAccount.sol"; +import "../interfaces/IAccountExecute.sol"; import "../interfaces/IPaymaster.sol"; import "../interfaces/IEntryPoint.sol"; @@ -78,12 +79,33 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuard, (uint256 collected) { uint256 preGas = gasleft(); bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset); - - try this.innerHandleOp(userOp.callData, opInfo, context) returns ( - uint256 _actualGasCost - ) { - collected = _actualGasCost; - } catch { + uint saveFreePtr; + assembly { + saveFreePtr := mload(0x40) + } + bytes calldata callData = userOp.callData; + bytes memory innerCall; + bytes4 methodSig; + assembly { + let len := callData.length + if gt(len,3) { + methodSig := calldataload(callData.offset) + } + } + if (methodSig == IAccountExecute.executeUserOp.selector) { + bytes memory executeUserOp = abi.encodeCall(IAccountExecute.executeUserOp, (userOp, opInfo.userOpHash)); + innerCall = abi.encodeCall(this.innerHandleOp, (executeUserOp, opInfo, context)); + } else + { + innerCall = abi.encodeCall(this.innerHandleOp, (callData, opInfo, context)); + } + bool success; + assembly { + success := call(gas(), address(), 0, add(innerCall, 0x20), mload(innerCall), 0, 32) + collected := mload(0) + mstore(0x40, saveFreePtr) + } + if (!success) { bytes32 innerRevertCode; assembly { let len := returndatasize() diff --git a/contracts/interfaces/IAccountExecute.sol b/contracts/interfaces/IAccountExecute.sol new file mode 100644 index 00000000..440bd4b0 --- /dev/null +++ b/contracts/interfaces/IAccountExecute.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.12; + +import "./UserOperation.sol"; + +interface IAccountExecute { + /** + * Account may implement this execute method. + * passing this methodSig at the beginning of callData will cause the entryPoint to pass the full UserOp (and hash) + * to the account. + * The account should skip the methodSig, and use the callData (and optionally, other UserOp fields) + * + * @param userOp - The operation that was just validated. + * @param userOpHash - Hash of the user's request data. + */ + function executeUserOp( + UserOperation calldata userOp, + bytes32 userOpHash + ) external; +} diff --git a/contracts/test/TestExecAccount.sol b/contracts/test/TestExecAccount.sol new file mode 100644 index 00000000..9a624f03 --- /dev/null +++ b/contracts/test/TestExecAccount.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0 + +/* solhint-disable one-contract-per-file */ +/* solhint-disable avoid-low-level-calls */ +pragma solidity ^0.8.15; + +import "@openzeppelin/contracts/utils/Create2.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import "../samples/SimpleAccount.sol"; +import "../interfaces/IAccountExecute.sol"; + +/** + * a sample account with execUserOp. + * Note that this account does nothing special with the userop, just extract + * call to execute. In theory, such account can reference the signature, the hash, etc. + */ +contract TestExecAccount is SimpleAccount, IAccountExecute { + + constructor(IEntryPoint anEntryPoint) SimpleAccount(anEntryPoint){ + } + + event Executed(UserOperation userOp, bytes innerCallRet); + + function executeUserOp(UserOperation calldata userOp, bytes32 /*userOpHash*/) external { + _requireFromEntryPointOrOwner(); + + // read from the userOp.callData, but skip the "magic" prefix (executeUserOp sig), + // which caused it to call this method. + bytes calldata innerCall = userOp.callData[4 :]; + + bytes memory innerCallRet; + if (innerCall.length > 0) { + (address target, bytes memory data) = abi.decode(innerCall, (address, bytes)); + bool success; + (success, innerCallRet) = target.call(data); + require(success, "inner call failed"); + } + + emit Executed(userOp, innerCallRet); + } +} + +contract TestExecAccountFactory { + TestExecAccount public immutable accountImplementation; + + constructor(IEntryPoint _entryPoint) { + accountImplementation = new TestExecAccount(_entryPoint); + } + + function createAccount(address owner, uint256 salt) public returns (address ret) { + address addr = getAddress(owner, salt); + uint codeSize = addr.code.length; + if (codeSize > 0) { + return addr; + } + ret = address(new ERC1967Proxy{salt: bytes32(salt)}( + address(accountImplementation), + abi.encodeCall(SimpleAccount.initialize, (owner)) + )); + } + + /** + * calculate the counterfactual address of this account as it would be returned by createAccount() + */ + function getAddress(address owner, uint256 salt) public view returns (address) { + return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked( + type(ERC1967Proxy).creationCode, + abi.encode( + address(accountImplementation), + abi.encodeCall(SimpleAccount.initialize, (owner)) + ) + ))); + } +} diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 210d20be..645af213 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -12,44 +12,44 @@ ║ │ │ │ (delta for │ (compared to ║ ║ │ │ │ one UserOp) │ account.exec()) ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 1 │ 81905 │ │ ║ +║ simple │ 1 │ 81925 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 44177 │ 15198 ║ +║ simple - diff from previous │ 2 │ │ 44209 │ 15230 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 479663 │ │ ║ +║ simple │ 10 │ 479909 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 44165 │ 15186 ║ +║ simple - diff from previous │ 11 │ │ 44252 │ 15273 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 88043 │ │ ║ +║ simple paymaster │ 1 │ 88087 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 43021 │ 14042 ║ +║ simple paymaster with diff │ 2 │ │ 43065 │ 14086 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 475497 │ │ ║ +║ simple paymaster │ 10 │ 475793 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 43080 │ 14101 ║ +║ simple paymaster with diff │ 11 │ │ 43070 │ 14091 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 182962 │ │ ║ +║ big tx 5k │ 1 │ 182994 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 144676 │ 19452 ║ +║ big tx - diff from previous │ 2 │ │ 144708 │ 19484 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1485271 │ │ ║ +║ big tx 5k │ 10 │ 1485525 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 144737 │ 19513 ║ +║ big tx - diff from previous │ 11 │ │ 144752 │ 19528 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 89761 │ │ ║ +║ paymaster+postOp │ 1 │ 89808 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 44771 │ 15792 ║ +║ paymaster+postOp with diff │ 2 │ │ 44812 │ 15833 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 492869 │ │ ║ +║ paymaster+postOp │ 10 │ 493129 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 44831 │ 15852 ║ +║ paymaster+postOp with diff │ 11 │ │ 44857 │ 15878 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 148295 │ │ ║ +║ token paymaster │ 1 │ 148331 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 72950 │ 43971 ║ +║ token paymaster with diff │ 2 │ │ 73002 │ 44023 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 805344 │ │ ║ +║ token paymaster │ 10 │ 805648 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 73021 │ 44042 ║ +║ token paymaster with diff │ 11 │ │ 72998 │ 44019 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/test/testExecAccount.test.ts b/test/testExecAccount.test.ts new file mode 100644 index 00000000..4a1e5035 --- /dev/null +++ b/test/testExecAccount.test.ts @@ -0,0 +1,56 @@ +import { before } from 'mocha' +import { + EntryPoint, + TestExecAccount, + TestExecAccount__factory, + TestExecAccountFactory__factory +} from '../typechain' +import { createAccountOwner, deployEntryPoint, fund, objdump } from './testutils' +import { fillAndSign } from './UserOp' +import { Signer, Wallet } from 'ethers' +import { ethers } from 'hardhat' +import { defaultAbiCoder, hexConcat, hexStripZeros } from 'ethers/lib/utils' +import { expect } from 'chai' + +describe('IAccountExecute', () => { + let ethersSigner: Signer + let entryPoint: EntryPoint + let account: TestExecAccount + let owner: Wallet + before(async () => { + const provider = ethers.provider + ethersSigner = provider.getSigner() + entryPoint = await deployEntryPoint() + const factory = await new TestExecAccountFactory__factory(ethersSigner).deploy(entryPoint.address) + owner = createAccountOwner() + await factory.createAccount(owner.getAddress(), 0) + const accountAddress = await factory.callStatic.createAccount(owner.getAddress(), 0) + account = TestExecAccount__factory.connect(accountAddress, provider) + await fund(accountAddress) + }) + + it('should execute ', async () => { + const execSig = account.interface.getSighash('executeUserOp') + // innerCall, as TestExecAccount.executeUserOp will try to decode it: + const innerCall = defaultAbiCoder.encode(['address', 'bytes'], [ + account.address, + account.interface.encodeFunctionData('entryPoint') + ]) + + const userOp = await fillAndSign({ + sender: account.address, + callGasLimit: 100000, // normal estimate also chokes on this callData + callData: hexConcat([execSig, innerCall]) + }, owner, entryPoint) + + await entryPoint.handleOps([userOp], ethersSigner.getAddress()) + + const e = + await account.queryFilter(account.filters.Executed()) + + expect(e.length).to.eq(1, "didn't call inner execUserOp (no Executed event)") + console.log(e[0].event, objdump(e[0].args)) + // validate we retrieved the return value of the called "entryPoint()" function: + expect(hexStripZeros(e[0].args.innerCallRet)).to.eq(hexStripZeros(entryPoint.address)) + }) +})