diff --git a/.solcover.js b/.solcover.js index 851e37d61..133010a90 100644 --- a/.solcover.js +++ b/.solcover.js @@ -1,8 +1,8 @@ module.exports = { skipFiles: [ "test", - "samples/bls/lib", - "utils/Exec.sol" + "utils/Exec.sol", + "samples" ], configureYulOptimizer: true, }; diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol new file mode 100644 index 000000000..f86de87e2 --- /dev/null +++ b/contracts/core/Eip7702Support.sol @@ -0,0 +1,67 @@ +pragma solidity ^0.8; +// SPDX-License-Identifier: MIT +// solhint-disable no-inline-assembly + +import "../interfaces/PackedUserOperation.sol"; +import "../core/UserOperationLib.sol"; + +library Eip7702Support { + + // EIP-7702 code prefix before delegate address. + bytes3 internal constant EIP7702_PREFIX = 0xef0100; + + // EIP-7702 initCode marker, to specify this account is EIP-7702. + bytes2 internal constant INITCODE_EIP7702_MARKER = 0x7702; + + using UserOperationLib for PackedUserOperation; + + //get alternate InitCodeHash (just for UserOp hash) when using EIP-7702 + function _getEip7702InitCodeHashOverride(PackedUserOperation calldata userOp) internal view returns (bytes32) { + bytes calldata initCode = userOp.initCode; + if (!_isEip7702InitCode(initCode)) { + return 0; + } + address delegate = _getEip7702Delegate(userOp.getSender()); + if (initCode.length <= 20) + return keccak256(abi.encodePacked(delegate)); + else + return keccak256(abi.encodePacked(delegate, initCode[20 :])); + } + + // check if this initCode is EIP-7702: starts with INITCODE_EIP7702_MARKER. + function _isEip7702InitCode(bytes calldata initCode) internal pure returns (bool) { + + if (initCode.length < 2) { + return false; + } + bytes20 initCodeStart; + // non-empty calldata bytes are always zero-padded to 32-bytes, so can be safely casted to "bytes20" + assembly ("memory-safe") { + initCodeStart := calldataload(initCode.offset) + } + // make sure first 20 bytes of initCode are "0x7702" (padded with zeros) + return initCodeStart == bytes20(INITCODE_EIP7702_MARKER); + } + + /** + * get the EIP-7702 delegate from contract code. + * must only be used if _isEip7702InitCode(initCode) is true. + */ + function _getEip7702Delegate(address sender) internal view returns (address) { + + bytes32 senderCode; + + assembly ("memory-safe") { + extcodecopy(sender, 0, 0, 23) + senderCode := mload(0) + } + // To be a valid EIP-7702 delegate, the first 3 bytes are EIP7702_PREFIX + // followed by the delegate address + if (bytes3(senderCode) != EIP7702_PREFIX) { + // instead of just "not an EIP-7702 delegate", if some info. + require(sender.code.length > 0, "sender has no code"); + revert("not an EIP-7702 delegate"); + } + return address(bytes20(senderCode << 24)); + } +} diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 9220296f2..24fba3628 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -14,6 +14,7 @@ import "./NonceManager.sol"; import "./SenderCreator.sol"; import "./StakeManager.sol"; import "./UserOperationLib.sol"; +import "./Eip7702Support.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; @@ -381,8 +382,9 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT function getUserOpHash( PackedUserOperation calldata userOp ) public view returns (bytes32) { + bytes32 overrideInitCodeHash = Eip7702Support._getEip7702InitCodeHashOverride(userOp); return - MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash()); + MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCodeHash)); } /** @@ -444,6 +446,13 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT ) internal { if (initCode.length != 0) { address sender = opInfo.mUserOp.sender; + if ( Eip7702Support._isEip7702InitCode(initCode) ) { + if (initCode.length>20 ) { + //already validated it is an EIP-7702 delegate (and hence, already has code) + senderCreator().initEip7702Sender(sender, initCode[20:]); + } + return; + } if (sender.code.length != 0) revert FailedOp(opIndex, "AA10 sender already constructed"); address sender1 = senderCreator().createSender{ diff --git a/contracts/core/EntryPointSimulations.sol b/contracts/core/EntryPointSimulations.sol index 3dda963e0..0bf73ee4c 100644 --- a/contracts/core/EntryPointSimulations.sol +++ b/contracts/core/EntryPointSimulations.sol @@ -215,4 +215,7 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { return __domainSeparatorV4; } + function supportsInterface(bytes4) public view virtual override returns (bool) { + return false; + } } diff --git a/contracts/core/SenderCreator.sol b/contracts/core/SenderCreator.sol index 539760610..fa9e7510d 100644 --- a/contracts/core/SenderCreator.sol +++ b/contracts/core/SenderCreator.sol @@ -1,7 +1,11 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.23; +/* solhint-disable avoid-low-level-calls */ +/* solhint-disable no-inline-assembly */ import "../interfaces/ISenderCreator.sol"; +import "../interfaces/IEntryPoint.sol"; +import "../utils/Exec.sol"; /** * Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address, @@ -14,6 +18,8 @@ contract SenderCreator is ISenderCreator { entryPoint = msg.sender; } + uint256 private constant REVERT_REASON_MAX_LEN = 2048; + /** * Call the "initCode" factory to create and return the sender account address. * @param initCode - The initCode value from a UserOp. contains 20 bytes of factory address, @@ -23,13 +29,11 @@ contract SenderCreator is ISenderCreator { function createSender( bytes calldata initCode ) external returns (address sender) { - if (msg.sender != entryPoint) { - revert("AA97 should call from EntryPoint"); - } - address factory = address(bytes20(initCode[0:20])); - bytes memory initCallData = initCode[20:]; + require(msg.sender == entryPoint, "AA97 should call from EntryPoint"); + address factory = address(bytes20(initCode[0 : 20])); + + bytes memory initCallData = initCode[20 :]; bool success; - /* solhint-disable no-inline-assembly */ assembly ("memory-safe") { success := call( gas(), @@ -40,10 +44,23 @@ contract SenderCreator is ISenderCreator { 0, 32 ) - sender := mload(0) + if success { + sender := mload(0) + } } + } + + // use initCallData to initialize an EIP-7702 account + // caller (EntryPoint) already verified it is an EIP-7702 account. + function initEip7702Sender( + address sender, + bytes calldata initCallData + ) external { + require(msg.sender == entryPoint, "AA97 should call from EntryPoint"); + bool success = Exec.call(sender, 0, initCallData, gasleft()); if (!success) { - sender = address(0); + bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); + revert IEntryPoint.FailedOpWithRevert(0, "AA13 EIP7702 sender init failed", result); } } } diff --git a/contracts/core/UserOperationLib.sol b/contracts/core/UserOperationLib.sol index 2c6bde18f..ac7da8472 100644 --- a/contracts/core/UserOperationLib.sol +++ b/contracts/core/UserOperationLib.sol @@ -55,13 +55,15 @@ library UserOperationLib { /** * Pack the user operation data into bytes for hashing. * @param userOp - The user operation data. + * @param overrideInitCodeHash - If set, encode this instead of the initCode field in the userOp. */ function encode( - PackedUserOperation calldata userOp + PackedUserOperation calldata userOp, + bytes32 overrideInitCodeHash ) internal pure returns (bytes memory ret) { address sender = getSender(userOp); uint256 nonce = userOp.nonce; - bytes32 hashInitCode = calldataKeccak(userOp.initCode); + bytes32 hashInitCode = overrideInitCodeHash != 0 ? overrideInitCodeHash : calldataKeccak(userOp.initCode); bytes32 hashCallData = calldataKeccak(userOp.callData); bytes32 accountGasLimits = userOp.accountGasLimits; uint256 preVerificationGas = userOp.preVerificationGas; @@ -136,10 +138,12 @@ library UserOperationLib { /** * Hash the user operation data. * @param userOp - The user operation data. + * @param overrideInitCodeHash - If set, the initCode hash will be replaced with this value just for UserOp hashing. */ function hash( - PackedUserOperation calldata userOp + PackedUserOperation calldata userOp, + bytes32 overrideInitCodeHash ) internal pure returns (bytes32) { - return keccak256(encode(userOp)); + return keccak256(encode(userOp, overrideInitCodeHash)); } } diff --git a/contracts/interfaces/ISenderCreator.sol b/contracts/interfaces/ISenderCreator.sol index bd56051e7..95e0fd3c6 100644 --- a/contracts/interfaces/ISenderCreator.sol +++ b/contracts/interfaces/ISenderCreator.sol @@ -8,4 +8,7 @@ interface ISenderCreator { * @return sender Address of the newly created sender contract. */ function createSender(bytes calldata initCode) external returns (address sender); + + // call initCode to initialize an EIP-7702 account + function initEip7702Sender(address sender, bytes calldata initCode) external; } diff --git a/contracts/test/TestEip7702DelegateAccount.sol b/contracts/test/TestEip7702DelegateAccount.sol new file mode 100644 index 000000000..96b0c86a7 --- /dev/null +++ b/contracts/test/TestEip7702DelegateAccount.sol @@ -0,0 +1,53 @@ +pragma solidity ^0.8.23; +// SPDX-License-Identifier: MIT +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../core/BaseAccount.sol"; +import "../core/Eip7702Support.sol"; + +contract TestEip7702DelegateAccount is BaseAccount { + + IEntryPoint private immutable _entryPoint; + bool public testInitCalled; + + constructor(IEntryPoint anEntryPoint) { + _entryPoint = anEntryPoint; + } + + function testInit() public { + testInitCalled = true; + } + + function entryPoint() public view override virtual returns (IEntryPoint) { + return _entryPoint; + } + + // Require the function call went through EntryPoint or owner + function _requireFromEntryPointOrOwner() internal view { + require(msg.sender == address(this) || msg.sender == address(entryPoint()), "account: not Owner or EntryPoint"); + } + + /** + * execute a transaction (called directly from owner, or by entryPoint) + * @param dest destination address to call + * @param value the value to pass in this call + * @param func the calldata to pass in this call + */ + function execute(address dest, uint256 value, bytes calldata func) external { + _requireFromEntryPointOrOwner(); + (bool success,) = dest.call{value: value}(func); + require(success, "call failed"); + } + + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256 validationData) { + if (userOp.initCode.length > 20) { + require(testInitCalled, "testInit not called"); + } + if (ECDSA.recover(userOpHash, userOp.signature) == address(this)) { + return 0; + } + return 1; + } +} diff --git a/contracts/test/TestUtil.sol b/contracts/test/TestUtil.sol index 4c9f38c8f..129656086 100644 --- a/contracts/test/TestUtil.sol +++ b/contracts/test/TestUtil.sol @@ -2,13 +2,16 @@ pragma solidity ^0.8.23; import "../interfaces/PackedUserOperation.sol"; -import "../core/UserOperationLib.sol"; +import "../core/Eip7702Support.sol"; contract TestUtil { using UserOperationLib for PackedUserOperation; function encodeUserOp(PackedUserOperation calldata op) external pure returns (bytes memory){ - return op.encode(); + return op.encode(0); } + function isEip7702InitCode(bytes calldata initCode) external pure returns (bool) { + return Eip7702Support._isEip7702InitCode(initCode); + } } diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index d95403c86..807d1aed1 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -12,36 +12,36 @@ ║ │ │ │ (delta for │ (compared to ║ ║ │ │ │ one UserOp) │ account.exec()) ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 1 │ 77468 │ │ ║ +║ simple │ 1 │ 77824 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41604 │ 12345 ║ +║ simple - diff from previous │ 2 │ │ 41996 │ 12737 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 452160 │ │ ║ +║ simple │ 10 │ 455792 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 41677 │ 12418 ║ +║ simple - diff from previous │ 11 │ │ 42033 │ 12774 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83307 │ │ ║ +║ simple paymaster │ 1 │ 83675 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40156 │ 10897 ║ +║ simple paymaster with diff │ 2 │ │ 40524 │ 11265 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 444914 │ │ ║ +║ simple paymaster │ 10 │ 448582 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40177 │ 10918 ║ +║ simple paymaster with diff │ 11 │ │ 40593 │ 11334 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167233 │ │ ║ +║ big tx 5k │ 1 │ 167613 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 130871 │ 16169 ║ +║ big tx - diff from previous │ 2 │ │ 131215 │ 16513 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1345038 │ │ ║ +║ big tx 5k │ 10 │ 1348706 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 130896 │ 16194 ║ +║ big tx - diff from previous │ 11 │ │ 131228 │ 16526 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84690 │ │ ║ +║ paymaster+postOp │ 1 │ 85070 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41574 │ 12315 ║ +║ paymaster+postOp with diff │ 2 │ │ 41930 │ 12671 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 458869 │ │ ║ +║ paymaster+postOp │ 10 │ 462525 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41577 │ 12318 ║ +║ paymaster+postOp with diff │ 11 │ │ 41993 │ 12734 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/scripts/docker-gascalc.yml b/scripts/docker-gascalc.yml index a4a261a7c..ac6b05506 100644 --- a/scripts/docker-gascalc.yml +++ b/scripts/docker-gascalc.yml @@ -15,8 +15,9 @@ services: localgeth: ports: [ '8545:8545' ] image: ethereum/client-go:release-1.14 + # image: dtr22/geth7702 command: | - --verbosity 2 + --verbosity 1 --http -http.addr 0.0.0.0 --http.api 'eth,net,web3,debug' --http.port 8545 --http.vhosts '*,localhost,host.docker.internal' --dev --rpc.allow-unprotected-txs diff --git a/scripts/geth.sh b/scripts/geth.sh new file mode 100755 index 000000000..b82a0d2a7 --- /dev/null +++ b/scripts/geth.sh @@ -0,0 +1,7 @@ +#!/bin/sh +name=geth-$$ +trap "echo killing docker; docker kill $name 2> /dev/null" EXIT +port=$1 +shift +params="--http --http.api eth,net,web3,debug --rpc.allow-unprotected-txs --allow-insecure-unlock --dev --http.addr 0.0.0.0" +docker run --name $name --rm -p $port:8545 ethpandaops/geth:master $params diff --git a/src/Create2Factory.ts b/src/Create2Factory.ts index ba9076f3d..52b4640ae 100644 --- a/src/Create2Factory.ts +++ b/src/Create2Factory.ts @@ -61,8 +61,8 @@ export class Create2Factory { gasLimit = Math.floor(gasLimit * 64 / 63) } - const ret = await this.signer.sendTransaction({ ...deployTx, gasLimit }) - await ret.wait() + await this.signer.sendTransaction({ ...deployTx, gasLimit }).then(async tx => tx.wait()) + if (await this.provider.getCode(addr).then(code => code.length) === 2) { throw new Error('failed to deploy') } @@ -98,6 +98,7 @@ export class Create2Factory { if (await this._isFactoryDeployed()) { return } + await (signer ?? this.signer).sendTransaction({ to: Create2Factory.factoryDeployer, value: BigNumber.from(Create2Factory.factoryDeploymentFee) @@ -106,6 +107,7 @@ export class Create2Factory { await new Promise(resolve => setTimeout(resolve, 100)) await this.provider.sendTransaction(Create2Factory.factoryTx).then(async tx => tx.wait()) + if (!await this._isFactoryDeployed()) { throw new Error('fatal: failed to deploy deterministic deployer') } diff --git a/test/GethExecutable.ts b/test/GethExecutable.ts new file mode 100644 index 000000000..7c5a6e74f --- /dev/null +++ b/test/GethExecutable.ts @@ -0,0 +1,135 @@ +import { spawn, ChildProcess } from 'child_process' +import Debug from 'debug' +import { BigNumber, BigNumberish } from 'ethers' +import { JsonRpcProvider } from '@ethersproject/providers' +import { isBigNumber } from 'hardhat/common' +import { decodeRevertReason } from './testutils' + +const debug = Debug('aa.geth') + +// launcher scripts for executables. +// should use "trap" to kill launched process on exit. +// executed with single parameter: port to listen +const launchers = { + geth: './scripts/geth.sh', + anvil: './scripts/anvil.sh' +} + +export type LauncherName = keyof typeof launchers + +interface Eip7702Transaction { + to: string + data?: string + value?: BigNumberish + gas?: BigNumberish + authorizationList?: any +} + +export class GethExecutable { + gethFrom: string + provider: JsonRpcProvider + port = Math.floor(5000 + Math.random() * 10000) + + impl: string + constructor (private readonly implName: LauncherName = 'geth') { + this.impl = launchers[implName] + } + + private gethProcess: ChildProcess | null = null + + markerString = /HTTP server started|Listening on/ + + rpcUrl (): string { + return `http://localhost:${this.port}` + } + + async init (): Promise { + await this.initProcess() + this.provider = new JsonRpcProvider(this.rpcUrl()) + this.gethFrom = (await this.provider.send('eth_accounts', []))[0] + } + + async sendTx (tx: Eip7702Transaction): Promise { + // todo: geth is strict on values (e.g. leading hex zero digits not allowed) + // might need to add more cleanups here.. + const tx1 = { + from: this.gethFrom, + ...tx + } as any + for (const key of Object.keys(tx1)) { + if (typeof tx1[key] === 'number' || isBigNumber(tx1[key])) { + tx1[key] = BigNumber.from(tx1[key]).toHexString() + } + // ugly: numbers must not have leading zeros, but addresses must have 40 chars + if (typeof tx1[key] === 'string' && tx1[key].length < 42) { + tx1[key] = tx1[key].replace(/0x0\B/, '0x') + } + } + // console.log('tx=', await geth.provider.getTransactionReceipt(hash)) + + const hash = await this.provider.send('eth_sendTransaction', [tx1]).catch(e => { + throw new Error(decodeRevertReason(e.error.data) ?? e.error.message) + }) + while (await this.provider.getTransactionReceipt(hash) == null) { + await new Promise(resolve => setTimeout(resolve, 100)) + } + return hash + } + + // equivalent to provider.call, but supports 7702 authorization + async call (tx: Eip7702Transaction): Promise { + return await this.provider.send('eth_call', [{ + from: this.gethFrom, + ...tx + }]) + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + + async initProcess (): Promise { + return new Promise((resolve, reject) => { + console.log('spawning: ', this.impl, this.port) + this.gethProcess = spawn(this.impl, [this.port.toString()]) + + let allData = '' + if (this.gethProcess != null) { + const timeout = setTimeout(() => { + reject(new Error(`Timed out waiting for marker regex: ${this.markerString.toString()}\n: ${allData}`)) + }, 5000) + + this.gethProcess.stdout?.on('data', (data: string) => { + data = data.toString() + allData += data + debug('stdout:', data) + if (data.match(this.markerString) != null) { + clearTimeout(timeout) + resolve() + } + }) + this.gethProcess.stderr?.on('data', (data: string) => { + data = data.toString() + allData += data + debug('stderr:', data) + + if (data.match(this.markerString) != null) { + clearTimeout(timeout) + resolve() + } + }) + + this.gethProcess.on('exit', (code: number | null) => { + console.log(`${this.impl}: process exited with code ${code}`) + }) + } else { + reject(new Error('Failed to start geth process')) + } + }) + } + + done (): void { + if (this.gethProcess != null) { + debug('killing geth') + this.gethProcess.kill() + } + } +} diff --git a/test/UserOp.ts b/test/UserOp.ts index 2f984ebdf..75da30616 100644 --- a/test/UserOp.ts +++ b/test/UserOp.ts @@ -1,7 +1,7 @@ import { arrayify, - defaultAbiCoder, hexConcat, - hexDataSlice, + defaultAbiCoder, hexConcat, hexDataLength, + hexDataSlice, hexlify, keccak256 } from 'ethers/lib/utils' import { BigNumber, Contract, Signer, Wallet } from 'ethers' @@ -33,6 +33,8 @@ const DOMAIN_VERSION = '1' // Matched to UserOperationLib.sol: const PACKED_USEROP_TYPEHASH = keccak256(Buffer.from('PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData)')) +export const INITCODE_EIP7702_MARKER = '0x7702' + export function packUserOp (userOp: UserOperation): PackedUserOperation { const accountGasLimits = packAccountGasLimits(userOp.verificationGasLimit, userOp.callGasLimit) const gasFees = packAccountGasLimits(userOp.maxPriorityFeePerGas, userOp.maxFeePerGas) @@ -87,6 +89,33 @@ export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: n ])) } +export function isEip7702UserOp (op: UserOperation): boolean { + return op.initCode != null && hexlify(op.initCode).startsWith(INITCODE_EIP7702_MARKER) +} + +export function updateUserOpForEip7702Hash (op: UserOperation, delegate: string): UserOperation { + if (!isEip7702UserOp(op)) { + throw new Error('initCode should start with INITCODE_EIP7702_MARKER') + } + let initCode = hexlify(op.initCode) + if (hexDataLength(initCode) < 20) { + initCode = delegate + } else { + // replace address in initCode with delegate + initCode = hexConcat([delegate, hexDataSlice(initCode, 20)]) + } + return { + ...op, initCode + } +} + +// calculate UserOpHash, given "sender" contract code. +// (only used if initCode starts with prefix) +export function getUserOpHashWithEip7702 (op: UserOperation, entryPoint: string, chainId: number, delegate: string): string { + const op1 = updateUserOpForEip7702Hash(op, delegate) + return getUserOpHash(op1, entryPoint, chainId) +} + export const DefaultsForUserOp: UserOperation = { sender: AddressZero, nonce: 0, @@ -104,8 +133,16 @@ export const DefaultsForUserOp: UserOperation = { signature: '0x' } -export function signUserOp (op: UserOperation, signer: Wallet, entryPoint: string, chainId: number): UserOperation { - const message = getUserOpHash(op, entryPoint, chainId) +export function signUserOp (op: UserOperation, signer: Wallet, entryPoint: string, chainId: number, eip7702delegate?: string): UserOperation { + let message + if (isEip7702UserOp(op)) { + if (eip7702delegate == null) { + throw new Error('Must have eip7702delegate to sign') + } + message = getUserOpHashWithEip7702(op, entryPoint, chainId, eip7702delegate) + } else { + message = getUserOpHash(op, entryPoint, chainId) + } const sig = ecsign(Buffer.from(arrayify(message)), Buffer.from(arrayify(signer.privateKey))) // that's equivalent of: await signer.signTypedData(domain, types, packUserOp(op)); @@ -131,6 +168,15 @@ export function fillUserOpDefaults (op: Partial, defaults = Defau return filled } +// Options for fill/sign UserOperations functions +export interface FillUserOpOptions { + // account nonce function to call, if userOp doesn't contain nonce. defaults to "getNonce()" + getNonceFunction?: string + // eip7702 delegate. only needed if this is the creation UserOp (that is, a one that runs with the eip7702 authorization tuple). + // if the option is missing (and this is an EIP-7702 UserOp), the "fill" functions will read the value from the account's address. + eip7702delegate?: string +} + // helper to fill structure: // - default callGasLimit to estimate call from entryPoint to account (TODO: add overhead) // if there is initCode: @@ -143,36 +189,54 @@ export function fillUserOpDefaults (op: Partial, defaults = Defau // sender - only in case of construction: fill sender from initCode. // callGasLimit: VERY crude estimation (by estimating call to account, and add rough entryPoint overhead // verificationGasLimit: hard-code default at 100k. should add "create2" cost -export async function fillUserOp (op: Partial, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { +export async function fillUserOp (op: Partial, entryPoint?: EntryPoint, options?: FillUserOpOptions): Promise { + const getNonceFunction = options?.getNonceFunction ?? 'getNonce' const op1 = { ...op } const provider = entryPoint?.provider - if (op.initCode != null) { - const initAddr = hexDataSlice(op1.initCode!, 0, 20) - const initCallData = hexDataSlice(op1.initCode!, 20) - if (op1.nonce == null) op1.nonce = 0 - if (op1.sender == null) { - // hack: if the init contract is our known deployer, then we know what the address would be, without a view call - if (initAddr.toLowerCase() === Create2Factory.contractAddress.toLowerCase()) { - const ctr = hexDataSlice(initCallData, 32) - const salt = hexDataSlice(initCallData, 0, 32) - op1.sender = Create2Factory.getDeployedAddress(ctr, salt) - } else { - // console.log('\t== not our deployer. our=', Create2Factory.contractAddress, 'got', initAddr) + if (op1.initCode != null) { + if (isEip7702UserOp(op1 as UserOperation)) { + if (provider == null) { + throw new Error('must have provider to check eip7702 delegate') + } + const code = await provider.getCode(op1.sender!) + if (code.length === 2) { + if (options?.eip7702delegate == null) { + throw new Error('must have eip7702delegate') + } + } else if (code.length !== 23 * 2 + 2) { + throw new Error('sender is not an eip7702 delegate') + } + if (op1.nonce == null) { + op1.nonce = await provider.getTransactionCount(op1.sender!) + } + } else { + const initAddr = hexDataSlice(op1.initCode!, 0, 20) + const initCallData = hexDataSlice(op1.initCode!, 20) + if (op1.nonce == null) op1.nonce = 0 + if (op1.sender == null) { + // hack: if the init contract is our known deployer, then we know what the address would be, without a view call + if (initAddr.toLowerCase() === Create2Factory.contractAddress.toLowerCase()) { + const ctr = hexDataSlice(initCallData, 32) + const salt = hexDataSlice(initCallData, 0, 32) + op1.sender = Create2Factory.getDeployedAddress(ctr, salt) + } else { + // console.log('\t== not our deployer. our=', Create2Factory.contractAddress, 'got', initAddr) + if (provider == null) throw new Error('no entrypoint/provider') + op1.sender = await entryPoint!.callStatic.getSenderAddress(op1.initCode!).catch(e => e.errorArgs.sender) + } + } + if (op1.verificationGasLimit == null) { if (provider == null) throw new Error('no entrypoint/provider') - op1.sender = await entryPoint!.callStatic.getSenderAddress(op1.initCode!).catch(e => e.errorArgs.sender) + const senderCreator = await entryPoint?.senderCreator() + const initEstimate = await provider.estimateGas({ + from: senderCreator, + to: initAddr, + data: initCallData, + gasLimit: 10e6 + }) + op1.verificationGasLimit = BigNumber.from(DefaultsForUserOp.verificationGasLimit).add(initEstimate) } } - if (op1.verificationGasLimit == null) { - if (provider == null) throw new Error('no entrypoint/provider') - const senderCreator = await entryPoint?.senderCreator() - const initEstimate = await provider.estimateGas({ - from: senderCreator, - to: initAddr, - data: initCallData, - gasLimit: 10e6 - }) - op1.verificationGasLimit = BigNumber.from(DefaultsForUserOp.verificationGasLimit).add(initEstimate) - } } if (op1.nonce == null) { if (provider == null) throw new Error('must have entryPoint to autofill nonce') @@ -218,8 +282,8 @@ export async function fillUserOp (op: Partial, entryPoint?: Entry return op2 } -export async function fillAndPack (op: Partial, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { - return packUserOp(await fillUserOp(op, entryPoint, getNonceFunction)) +export async function fillAndPack (op: Partial, entryPoint?: EntryPoint, options?: FillUserOpOptions): Promise { + return packUserOp(await fillUserOp(op, entryPoint, options)) } export function getDomainSeparator (entryPoint: string, chainId: number): string { @@ -258,17 +322,52 @@ export function getErc4337TypedDataTypes (): { [type: string]: TypedDataField[] ] } } -export async function fillAndSign (op: Partial, signer: Wallet | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { - const provider = entryPoint?.provider - const op2 = await fillUserOp(op, entryPoint, getNonceFunction) +/** + * call eth_signTypedData_v4 to sign the UserOp + * @param op + * @param signer + * @param entryPoint + * @param eip7702delegate account's delegate. only needed if this is the creation UserOp (that is, a one that runs with the eip7702 authorization tuple). + * Otherwise, it will be obtained from the deployed account. + */ +export async function asyncSignUserOp (op: UserOperation, signer: Wallet | Signer, entryPoint?: EntryPoint, options?: FillUserOpOptions): Promise { + let eip7702delegate = options?.eip7702delegate + const provider = entryPoint?.provider const chainId = await provider!.getNetwork().then(net => net.chainId) const typedSigner: TypedDataSigner = signer as any - const packedUserOp = packUserOp(op2) + let userOpToSign = op + if (isEip7702UserOp(userOpToSign)) { + if (eip7702delegate == null) { + const senderCode = await provider!.getCode(userOpToSign.sender) + if (!senderCode.startsWith('0xef0100')) { + if (senderCode === '0x') { + throw new Error('sender contract not deployed. is this the first EIP-7702 message? add eip7702delegate to options') + } + throw new Error(`sender is not an eip7702 delegate: ${senderCode}`) + } + eip7702delegate = hexDataSlice(senderCode, 3) + } + userOpToSign = updateUserOpForEip7702Hash(userOpToSign, eip7702delegate) + } + + const packedUserOp = packUserOp(userOpToSign) - const signature = await typedSigner._signTypedData(getErc4337TypedDataDomain(entryPoint!.address, chainId), getErc4337TypedDataTypes(), packedUserOp) // .catch(e => e.toString()) + return await typedSigner._signTypedData(getErc4337TypedDataDomain(entryPoint!.address, chainId), getErc4337TypedDataTypes(), packedUserOp) // .catch(e => e.toString()) +} + +/** + * fill userop fields, and sign it + * @param op + * @param signer the account owner that should sign the userOpHash + * @param entryPoint account entrypoint. + * @param options - see @FillOptions + */ +export async function fillAndSign (op: Partial, signer: Wallet | Signer, entryPoint?: EntryPoint, options?: FillUserOpOptions): Promise { + const op2 = await fillUserOp(op, entryPoint, options) + const signature = await asyncSignUserOp(op2, signer, entryPoint, options) return { ...op2, @@ -276,8 +375,11 @@ export async function fillAndSign (op: Partial, signer: Wallet | } } -export async function fillSignAndPack (op: Partial, signer: Wallet | Signer, entryPoint?: EntryPoint, getNonceFunction = 'getNonce'): Promise { - const filledAndSignedOp = await fillAndSign(op, signer, entryPoint, getNonceFunction) +/** + * utility method: call fillAndSign, and then pack it to submit to handleOps. + */ +export async function fillSignAndPack (op: Partial, signer: Wallet | Signer, entryPoint?: EntryPoint, options?: FillUserOpOptions): Promise { + const filledAndSignedOp = await fillAndSign(op, signer, entryPoint, options) return packUserOp(filledAndSignedOp) } diff --git a/test/eip7702helpers.ts b/test/eip7702helpers.ts new file mode 100644 index 000000000..357a8e8fc --- /dev/null +++ b/test/eip7702helpers.ts @@ -0,0 +1,74 @@ +import { ecrecover, ecsign, PrefixedHexString, pubToAddress, toBuffer, toChecksumAddress } from 'ethereumjs-util' +import { BigNumber, BigNumberish, Wallet } from 'ethers' +import { arrayify, hexConcat, hexlify, keccak256, RLP } from 'ethers/lib/utils' +import { tostr } from './testutils' + +// from: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md +// authority = ecrecover(keccak(MAGIC || rlp([chain_id, address, nonce])), y_parity, r, s) + +const EIP7702_MAGIC = '0x05' + +export interface UnsignedEIP7702Authorization { + chainId: BigNumberish + address: string + nonce?: BigNumberish +} + +export interface EIP7702Authorization extends UnsignedEIP7702Authorization { + yParity: BigNumberish + r: BigNumberish + s: BigNumberish +} + +export function toRlpHex (s: any): PrefixedHexString { + if (BigNumber.isBigNumber(s) || typeof s === 'number') { + s = BigNumber.from(s).toHexString() + } + let ret = s.replace(/0x0*/, '0x') + // make sure hex string is not odd-length + if (ret.length % 2 === 1) { + ret = ret.replace('0x', '0x0') + } + return ret as PrefixedHexString +} + +export function eip7702DataToSign (authorization: UnsignedEIP7702Authorization): PrefixedHexString { + const rlpData = [ + toRlpHex(authorization.chainId), + toRlpHex(authorization.address), + toRlpHex(authorization.nonce) + ] + return keccak256(hexConcat([ + EIP7702_MAGIC, + RLP.encode(rlpData) + ])) +} + +export function getEip7702AuthorizationSigner (authorization: EIP7702Authorization, chainId?: number): string { + const yParity = BigNumber.from(authorization.yParity).toHexString() + // yParity = 28 + const r = toBuffer(tostr(authorization.r)) + const s = toBuffer(tostr(authorization.s)) + const dataToSign = toBuffer(eip7702DataToSign(authorization)) + const retRecover = pubToAddress(ecrecover(dataToSign, yParity, r, s)) + return toChecksumAddress(hexlify(retRecover)) +} + +// geth only accepts hex values with no leading zeroes (except for zero itself) +export function gethHex (n: BigNumberish): string { + return BigNumber.from(n).toHexString().replace(/0x0(.)/, '0x$1') +} + +export async function signEip7702Authorization (signer: Wallet, authorization: UnsignedEIP7702Authorization): Promise { + const nonce = authorization.nonce ?? await signer.getTransactionCount() + const dataToSign = toBuffer(eip7702DataToSign({ nonce, ...authorization })) + const sig = ecsign(dataToSign, arrayify(signer.privateKey) as any) + return { + address: authorization.address, + chainId: gethHex(authorization.chainId), + nonce: gethHex(nonce), + yParity: gethHex(sig.v - 27), + r: gethHex(sig.r), + s: gethHex(sig.s) + } +} diff --git a/test/entrypoint-7702.test.ts b/test/entrypoint-7702.test.ts new file mode 100644 index 000000000..4cc7f960b --- /dev/null +++ b/test/entrypoint-7702.test.ts @@ -0,0 +1,247 @@ +import './aa.init' +import { Wallet } from 'ethers' +import { expect } from 'chai' +import { + EntryPoint, + TestEip7702DelegateAccount, + TestEip7702DelegateAccount__factory, + TestUtil, + TestUtil__factory +} from '../typechain' +import { + callGetUserOpHashWithCode, + createAccountOwner, + createAddress, + decodeRevertReason, + deployEntryPoint +} from './testutils' +import { + INITCODE_EIP7702_MARKER, + fillAndSign, + fillSignAndPack, + fillUserOpDefaults, + getUserOpHash, + getUserOpHashWithEip7702, + packUserOp +} from './UserOp' +import { ethers } from 'hardhat' +import { hexConcat, parseEther } from 'ethers/lib/utils' +import { before } from 'mocha' +import { GethExecutable } from './GethExecutable' +import { getEip7702AuthorizationSigner, gethHex, signEip7702Authorization } from './eip7702helpers' + +describe('EntryPoint EIP-7702 tests', function () { + const ethersSigner = ethers.provider.getSigner() + + // use stateOverride to "inject" 7702 delegate code to check the generated UserOpHash + describe('userOpHash with eip-7702 account', () => { + const userop = fillUserOpDefaults({ + sender: createAddress(), + nonce: 1, + callData: '0xdead', + callGasLimit: 2, + verificationGasLimit: 3, + maxFeePerGas: 4 + }) + let chainId: number + + let entryPoint: EntryPoint + const mockDelegate = createAddress() + + const deployedDelegateCode = hexConcat(['0xef0100', mockDelegate]) + + before(async function () { + this.timeout(20000) + chainId = await ethers.provider.getNetwork().then(net => net.chainId) + entryPoint = await deployEntryPoint() + }) + + describe('#_isEip7702InitCode', () => { + let testUtil: TestUtil + before(async () => { + testUtil = await new TestUtil__factory(ethersSigner).deploy() + }); + + [1, 10, 20, 30].forEach(pad => + it(`should accept initCode with zero pad ${pad}`, async () => { + expect(await testUtil.isEip7702InitCode(INITCODE_EIP7702_MARKER + '00'.repeat(pad))).to.be.true + }) + ) + + it('should accept initCode with just prefix', async () => { + expect(await testUtil.isEip7702InitCode(INITCODE_EIP7702_MARKER)).to.be.true + }) + + it('should not accept EIP7702 if first 20 bytes contain non-zero', async () => { + const addr = INITCODE_EIP7702_MARKER + '0'.repeat(40 - INITCODE_EIP7702_MARKER.length) + '01' + expect(addr.length).to.eql(42) + expect(await testUtil.isEip7702InitCode(addr)).to.be.false + }) + }) + + describe('check 7702 utility functions helpers', () => { + // sample valid auth: + const authSigner = new Wallet('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80') + // created using "cast call --auth" + const authorizationList = [ + { + chainId: '0x539', + address: '0x5fbdb2315678afecb367f032d93f642f64180aa3', + nonce: '0x2', + yParity: '0x0', + r: '0x8812962756107260d0c7934e0ea656ede2f953f2250a406d34be2605499134b4', + s: '0x43a2f470a01de2b68f4e9b31d7bef91188f1ab81fb95c732958398b17c7af8f6' + } + ] + it('#getEip7702AuthorizationSigner', async () => { + const auth = authorizationList[0] + const signer = getEip7702AuthorizationSigner(auth) + expect(signer).to.eql(authSigner.address) + }) + + it('#signEip7702Authorization', async () => { + // deliberately remove previous signature... + const authToSign = { address: createAddress(), nonce: 12345, chainId: '0x0' } + const signed = await signEip7702Authorization(authSigner, authToSign) + expect(getEip7702AuthorizationSigner(signed)).to.eql(authSigner.address) + }) + }) + + it('calculate userophash with normal account', async () => { + expect(getUserOpHash(userop, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(packUserOp(userop))) + }) + + describe('#getUserOpHashWith7702', () => { + it('#getUserOpHashWith7702 just delegate', async () => { + const hash = getUserOpHash({ ...userop, initCode: mockDelegate }, entryPoint.address, chainId) + expect(getUserOpHashWithEip7702({ + ...userop, + initCode: INITCODE_EIP7702_MARKER + }, entryPoint.address, chainId, mockDelegate)).to.eql(hash) + }) + it('#getUserOpHashWith7702 with initcode', async () => { + const hash = getUserOpHash({ ...userop, initCode: mockDelegate + 'b1ab1a' }, entryPoint.address, chainId) + expect(getUserOpHashWithEip7702({ + ...userop, + initCode: INITCODE_EIP7702_MARKER.padEnd(42, '0') + 'b1ab1a' + }, entryPoint.address, chainId, mockDelegate)).to.eql(hash) + }) + }) + + describe('entryPoint getUserOpHash', () => { + it('should return the same hash as calculated locally', async () => { + const op1 = { ...userop, initCode: INITCODE_EIP7702_MARKER } + expect(await callGetUserOpHashWithCode(entryPoint, op1, deployedDelegateCode)).to.eql( + getUserOpHashWithEip7702(op1, entryPoint.address, chainId, mockDelegate)) + }) + + it('should fail getUserOpHash marked for eip-7702, without a delegate', async () => { + const op1 = { ...userop, initCode: INITCODE_EIP7702_MARKER } + await expect(callGetUserOpHashWithCode(entryPoint, op1, '0x' + '00'.repeat(23)).catch(e => { throw e.error ?? e.message })).to.revertedWith('not an EIP-7702 delegate') + }) + + it('should allow initCode with INITCODE_EIP7702_MARKER tailed with zeros only, ', async () => { + const op_zero_tail = { ...userop, initCode: INITCODE_EIP7702_MARKER + '00'.repeat(10) } + expect(await callGetUserOpHashWithCode(entryPoint, op_zero_tail, deployedDelegateCode)).to.eql( + getUserOpHashWithEip7702(op_zero_tail, entryPoint.address, chainId, mockDelegate)) + + op_zero_tail.initCode = INITCODE_EIP7702_MARKER + '00'.repeat(30) + expect(await callGetUserOpHashWithCode(entryPoint, op_zero_tail, deployedDelegateCode)).to.eql( + getUserOpHashWithEip7702(op_zero_tail, entryPoint.address, chainId, mockDelegate)) + }) + + describe('test with geth', () => { + // can't deploy coverage "entrypoint" on geth (contract too large) + if (process.env.COVERAGE != null) { + return + } + + let geth: GethExecutable + let delegate: TestEip7702DelegateAccount + const beneficiary = createAddress() + let eoa: Wallet + let entryPoint: EntryPoint + + before(async () => { + this.timeout(20000) + geth = new GethExecutable() + await geth.init() + eoa = createAccountOwner(geth.provider) + entryPoint = await deployEntryPoint(geth.provider) + delegate = await new TestEip7702DelegateAccount__factory(geth.provider.getSigner()).deploy(entryPoint.address) + console.log('\tdelegate addr=', delegate.address, 'len=', await geth.provider.getCode(delegate.address).then(code => code.length)) + await geth.sendTx({ to: eoa.address, value: gethHex(parseEther('1')) }) + }) + + it('should fail without sender delegate', async () => { + const eip7702userOp = await fillSignAndPack({ + sender: eoa.address, + nonce: 0, + initCode: INITCODE_EIP7702_MARKER // not init function, just delegate + }, eoa, entryPoint, { eip7702delegate: delegate.address }) + const handleOpCall = { + to: entryPoint.address, + data: entryPoint.interface.encodeFunctionData('handleOps', [[eip7702userOp], beneficiary]), + gasLimit: 1000000 + // authorizationList: [eip7702tuple] + } + expect(await geth.call(handleOpCall).catch(e => { + return e.error + })).to.match(/not an EIP-7702 delegate|sender has no code/) + }) + + it('should succeed with authorizationList', async () => { + const eip7702userOp = await fillAndSign({ + sender: eoa.address, + nonce: 0, + initCode: INITCODE_EIP7702_MARKER // not init function, just delegate + }, eoa, entryPoint, { eip7702delegate: delegate.address }) + const eip7702tuple = await signEip7702Authorization(eoa, { + address: delegate.address, + nonce: await geth.provider.getTransactionCount(eoa.address), + chainId: await geth.provider.getNetwork().then(net => net.chainId) + }) + + const handleOpCall = { + to: entryPoint.address, + data: entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(eip7702userOp)], beneficiary]), + gasLimit: 1000000, + authorizationList: [eip7702tuple] + } + + await geth.call(handleOpCall).catch(e => { + throw Error(decodeRevertReason(e)!) + }) + }) + + // skip until auth works. + it('should succeed and call initcode', async () => { + const eip7702userOp = await fillSignAndPack({ + sender: eoa.address, + nonce: 0, + initCode: hexConcat([INITCODE_EIP7702_MARKER + '0'.repeat(42 - INITCODE_EIP7702_MARKER.length), delegate.interface.encodeFunctionData('testInit')]) + }, eoa, entryPoint, { eip7702delegate: delegate.address }) + + const eip7702tuple = await signEip7702Authorization(eoa, { + address: delegate.address, + // nonce: await geth.provider.getTransactionCount(eoa.address), + chainId: await geth.provider.getNetwork().then(net => net.chainId) + }) + const handleOpCall = { + to: entryPoint.address, + data: entryPoint.interface.encodeFunctionData('handleOps', [[eip7702userOp], beneficiary]), + gasLimit: 1000000, + authorizationList: [eip7702tuple] + } + await geth.call(handleOpCall).catch(e => { + throw Error(decodeRevertReason(e)!) + }) + }) + + after(async () => { + geth.done() + }) + }) + }) + }) +}) diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index 78c03e1e2..db1b0cbe2 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -30,6 +30,19 @@ import { TestSignatureAggregator__factory, TestWarmColdAccount__factory } from '../typechain' +import { DefaultsForUserOp, fillAndSign, fillSignAndPack, getUserOpHash, packUserOp, simulateValidation } from './UserOp' +import { PackedUserOperation, UserOperation } from './UserOperation' +import { PopulatedTransaction } from 'ethers/lib/ethers' +import { ethers } from 'hardhat' +import { arrayify, defaultAbiCoder, hexZeroPad, parseEther } from 'ethers/lib/utils' +import { debugTransaction } from './debugTx' +import { BytesLike } from '@ethersproject/bytes' +import { toChecksumAddress } from 'ethereumjs-util' +import { getERC165InterfaceID } from '../src/Utils' +import { UserOperationEventEvent } from '../typechain/contracts/interfaces/IEntryPoint' +import { before } from 'mocha' + +import Debug from 'debug' import { AddressZero, calcGasUsage, @@ -53,25 +66,6 @@ import { TWO_ETH, unpackAccountGasFees } from './testutils' -import { - DefaultsForUserOp, - fillAndSign, - fillSignAndPack, - getUserOpHash, - packUserOp, - simulateValidation -} from './UserOp' -import { PackedUserOperation, UserOperation } from './UserOperation' -import { PopulatedTransaction } from 'ethers/lib/ethers' -import { ethers } from 'hardhat' -import { arrayify, defaultAbiCoder, hexZeroPad, parseEther } from 'ethers/lib/utils' -import { debugTransaction } from './debugTx' -import { BytesLike } from '@ethersproject/bytes' -import { toChecksumAddress } from 'ethereumjs-util' -import { getERC165InterfaceID } from '../src/Utils' -import { UserOperationEventEvent } from '../typechain/contracts/interfaces/IEntryPoint' - -import Debug from 'debug' const debug = Debug('entrypoint.test') @@ -558,7 +552,9 @@ describe('EntryPoint', function () { const rcpt = await entryPoint.handleOps([packUserOp(await createUserOpWithGas(minVerGas, minPmVerGas - 1, minCallGas))], beneficiary) .then(async r => r.wait()) - .catch((e: Error) => { throw new Error(decodeRevertReason(e, false) as any) }) + .catch((e: Error) => { + throw new Error(decodeRevertReason(e, false) as any) + }) expect(rcpt.events?.map(ev => ev.event)).to.eql([ 'BeforeExecution', 'PostOpRevertReason', @@ -1436,7 +1432,6 @@ describe('EntryPoint', function () { sender: testExpiryAccount.address }, expiredOwner, entryPoint) const ret = await simulateValidation(userOp, entryPoint.address) - // console.log(ret.returnInfo.accountValidationData.toHexString()) const validationData = parseValidationData(ret.returnInfo.accountValidationData) expect(validationData.validUntil).eql(now - 60) expect(validationData.validAfter).to.eql(123) diff --git a/test/entrypointsimulations.test.ts b/test/entrypointsimulations.test.ts index 80a77a38f..34c5d9ebd 100644 --- a/test/entrypointsimulations.test.ts +++ b/test/entrypointsimulations.test.ts @@ -295,7 +295,7 @@ describe('EntryPointSimulations', function () { describe(`compare to execution ${withPaymaster} paymaster`, () => { let execVgl: number let execPmVgl: number - const diff = 500 + const diff = 600 before(async () => { execPmVgl = withPaymaster === 'without' ? 0 : await findUserOpWithMin(async n => userOpWithGas(1e6, n), false, entryPoint, 1, 500000) execVgl = await findUserOpWithMin(async n => userOpWithGas(n, execPmVgl), false, entryPoint, 1, 500000) diff --git a/test/testExecAccount.test.ts b/test/testExecAccount.test.ts index a1577832f..47a341f67 100644 --- a/test/testExecAccount.test.ts +++ b/test/testExecAccount.test.ts @@ -50,6 +50,6 @@ describe('IAccountExecute', () => { expect(e.length).to.eq(1, "didn't call inner execUserOp (no Executed event)") // validate we retrieved the return value of the called "entryPoint()" function: - expect(hexStripZeros(e[0].args.innerCallRet)).to.eq(hexStripZeros(entryPoint.address)) + expect(hexStripZeros(e[0].args.innerCallRet)).to.eq(hexStripZeros(entryPoint.address).toLowerCase()) }) }) diff --git a/test/testutils.ts b/test/testutils.ts index 94c7b009f..41b9b7496 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -22,13 +22,14 @@ import { TestAggregatedAccountFactory, TestPaymasterRevertCustomError__factory, TestERC20__factory } from '../typechain' import { BytesLike, Hexable } from '@ethersproject/bytes' -import { JsonRpcProvider } from '@ethersproject/providers' +import { JsonRpcProvider, Provider } from '@ethersproject/providers' import { expect } from 'chai' import { Create2Factory } from '../src/Create2Factory' import { debugTransaction } from './debugTx' import { UserOperation } from './UserOperation' import { packUserOp, simulateValidation } from './UserOp' import Debug from 'debug' +import { toChecksumAddress } from 'ethereumjs-util' const debug = Debug('testutils') @@ -73,9 +74,9 @@ export async function getTokenBalance (token: IERC20, address: string): Promise< let counter = 0 // create non-random account, so gas calculations are deterministic -export function createAccountOwner (): Wallet { +export function createAccountOwner (provider: Provider = ethers.provider): Wallet { const privateKey = keccak256(Buffer.from(arrayify(BigNumber.from(++counter)))) - return new ethers.Wallet(privateKey, ethers.provider) + return new ethers.Wallet(privateKey, provider) // return new ethers.Wallet('0x'.padEnd(66, privkeyBase), ethers.provider); } @@ -281,7 +282,7 @@ export async function checkForBannedOps (txHash: string, checkPaymaster: boolean export async function deployEntryPoint (provider = ethers.provider): Promise { const create2factory = new Create2Factory(provider) - const addr = await create2factory.deploy(EntryPoint__factory.bytecode, process.env.SALT, process.env.COVERAGE != null ? 20e6 : 8e6) + const addr = toChecksumAddress(await create2factory.deploy(EntryPoint__factory.bytecode, process.env.SALT, process.env.COVERAGE != null ? 20e6 : 8e6)) return EntryPoint__factory.connect(addr, provider.getSigner()) } @@ -451,3 +452,20 @@ export async function findSimulationUserOpWithMin (f: (n: number) => Promise { + const stateOverride = senderCode == null + ? null + : { + [userop.sender]: { + code: senderCode + } + } + return await ethers.provider.send('eth_call', [ + { + to: entryPoint.address, + data: entryPoint.interface.encodeFunctionData('getUserOpHash', [packUserOp(userop)]) + }, 'latest', stateOverride + ]) +}