From db002afac09ce2da10519e9c6e3760a8ad4c1284 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 1 Jan 2025 21:32:38 +0200 Subject: [PATCH 01/44] userOpHash as ERC-712 signature overhead: 269 gas per userop (down to 144 on 11th userop) increased (330) with TokenPaymaster - not sure why --- contracts/core/EntryPoint.sol | 30 +++++++++++++++++----- contracts/core/UserOperationLib.sol | 7 ++++- gascalc/GasChecker.ts | 4 ++- reports/gas-checker.txt | 40 ++++++++++++++--------------- test/UserOp.ts | 31 +++++++++++++++++----- test/entrypoint.test.ts | 17 +++++++++++- 6 files changed, 92 insertions(+), 37 deletions(-) diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 058d8eba7..8965d9acc 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -5,18 +5,19 @@ pragma solidity ^0.8.23; import "../interfaces/IAccount.sol"; import "../interfaces/IAccountExecute.sol"; -import "../interfaces/IPaymaster.sol"; import "../interfaces/IEntryPoint.sol"; +import "../interfaces/IPaymaster.sol"; import "../utils/Exec.sol"; -import "./StakeManager.sol"; -import "./SenderCreator.sol"; import "./Helpers.sol"; import "./NonceManager.sol"; +import "./SenderCreator.sol"; +import "./StakeManager.sol"; import "./UserOperationLib.sol"; -import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; /* * Account-Abstraction (EIP-4337) singleton EntryPoint implementation. @@ -24,12 +25,15 @@ import "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; */ /// @custom:security-contact https://bounty.ethereum.org -contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardTransient, ERC165 { +contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardTransient, ERC165, EIP712 { using UserOperationLib for PackedUserOperation; SenderCreator private immutable _senderCreator = new SenderCreator(); + constructor() EIP712("ERC4337", "v0.8") { + } + function senderCreator() public view virtual returns (ISenderCreator) { return _senderCreator; } @@ -359,12 +363,24 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT } } + function getPackedUserOpTypeHash() public pure returns (bytes32) { + return UserOperationLib._PACKED_USER_OPERATION; + } + + function getDomainSeparatorV4() public view returns (bytes32) { + return _domainSeparatorV4(); + } + /// @inheritdoc IEntryPoint function getUserOpHash( PackedUserOperation calldata userOp ) public view returns (bytes32) { - return - keccak256(abi.encode(userOp.hash(), address(this), block.chainid)); + return _hashTypedDataV4( + keccak256(abi.encodePacked( + UserOperationLib._PACKED_USER_OPERATION, + userOp.encode() + )) + ); } /** diff --git a/contracts/core/UserOperationLib.sol b/contracts/core/UserOperationLib.sol index dcf5740cc..2a3b7dc1b 100644 --- a/contracts/core/UserOperationLib.sol +++ b/contracts/core/UserOperationLib.sol @@ -47,6 +47,11 @@ library UserOperationLib { } } + bytes32 internal constant _PACKED_USER_OPERATION = + keccak256( + "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData)" + ); + /** * Pack the user operation data into bytes for hashing. * @param userOp - The user operation data. @@ -131,7 +136,7 @@ library UserOperationLib { * Hash the user operation data. * @param userOp - The user operation data. */ - function hash( + function hash1( PackedUserOperation calldata userOp ) internal pure returns (bytes32) { return keccak256(encode(userOp)); diff --git a/gascalc/GasChecker.ts b/gascalc/GasChecker.ts index ba4a11c87..9b9c6e140 100644 --- a/gascalc/GasChecker.ts +++ b/gascalc/GasChecker.ts @@ -13,7 +13,7 @@ import { } from '../typechain' import { BigNumberish, Wallet } from 'ethers' import hre from 'hardhat' -import { fillSignAndPack, fillUserOp, packUserOp, signUserOp } from '../test/UserOp' +import { fillSignAndPack, fillUserOp, initUserOpHashParams, packUserOp, signUserOp } from '../test/UserOp' import { TransactionReceipt } from '@ethersproject/abstract-provider' import { table, TableUserConfig } from 'table' import { Create2Factory } from '../src/Create2Factory' @@ -344,6 +344,8 @@ export class GasCheckCollector { this.entryPoint = EntryPoint__factory.connect(entryPointAddressOrTest, globalSigner) } + await initUserOpHashParams(this.entryPoint) + const tableHeaders = [ 'handleOps description ', 'count', diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 4ec9417c7..f1f89e195 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 │ 77464 │ │ ║ +║ simple │ 1 │ 77733 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41632 │ 12373 ║ +║ simple - diff from previous │ 2 │ │ 41870 │ 12611 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 452471 │ │ ║ +║ simple │ 10 │ 454926 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 41807 │ 12548 ║ +║ simple - diff from previous │ 11 │ │ 41951 │ 12692 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83295 │ │ ║ +║ simple paymaster │ 1 │ 83564 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40200 │ 10941 ║ +║ simple paymaster with diff │ 2 │ │ 40426 │ 11167 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 445222 │ │ ║ +║ simple paymaster │ 10 │ 447626 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40252 │ 10993 ║ +║ simple paymaster with diff │ 11 │ │ 40447 │ 11188 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167225 │ │ ║ +║ big tx 5k │ 1 │ 167485 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 130859 │ 16157 ║ +║ big tx - diff from previous │ 2 │ │ 131101 │ 16399 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1345223 │ │ ║ +║ big tx 5k │ 10 │ 1347631 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 130903 │ 16201 ║ +║ big tx - diff from previous │ 11 │ │ 131158 │ 16456 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84472 │ │ ║ +║ paymaster+postOp │ 1 │ 84740 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41353 │ 12094 ║ +║ paymaster+postOp with diff │ 2 │ │ 41580 │ 12321 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 456976 │ │ ║ +║ paymaster+postOp │ 10 │ 459337 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41410 │ 12151 ║ +║ paymaster+postOp with diff │ 11 │ │ 41701 │ 12442 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 121542 │ │ ║ +║ token paymaster │ 1 │ 121855 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 61133 │ 31874 ║ +║ token paymaster with diff │ 2 │ │ 61416 │ 32157 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 672064 │ │ ║ +║ token paymaster │ 10 │ 674884 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61201 │ 31942 ║ +║ token paymaster with diff │ 11 │ │ 61540 │ 32281 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/test/UserOp.ts b/test/UserOp.ts index 9720f201c..691ef3590 100644 --- a/test/UserOp.ts +++ b/test/UserOp.ts @@ -1,6 +1,6 @@ import { arrayify, - defaultAbiCoder, + defaultAbiCoder, hexConcat, hexDataSlice, keccak256 } from 'ethers/lib/utils' @@ -15,7 +15,7 @@ import { } from './testutils' import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util' import { - EntryPoint, EntryPointSimulations__factory + EntryPoint, EntryPointSimulations__factory, IEntryPoint } from '../typechain' import { PackedUserOperation, UserOperation } from './UserOperation' import { Create2Factory } from '../src/Create2Factory' @@ -66,12 +66,29 @@ export function encodeUserOp (userOp: UserOperation, forSignature = true): strin } } +let domainSeparator: string | undefined +let packedUserOpTypeHash: string | undefined + +export async function initUserOpHashParams (ep: IEntryPoint): Promise { + domainSeparator = await ep.getDomainSeparatorV4() + packedUserOpTypeHash = await ep.getPackedUserOpTypeHash() +} + export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: number): string { - const userOpHash = keccak256(encodeUserOp(op, true)) - const enc = defaultAbiCoder.encode( - ['bytes32', 'address', 'uint256'], - [userOpHash, entryPoint, chainId]) - return keccak256(enc) + if (domainSeparator == null || packedUserOpTypeHash == null) { + throw new Error('must call initUserOpHashParams(ep)') + } + const packed = hexConcat([packedUserOpTypeHash, encodeUserOp(op, true)]) + return keccak256(hexConcat([ + '0x1901', + domainSeparator, + keccak256(packed) + ])) + // const userOpHash = keccak256(encodeUserOp(op, true)) + // const enc = defaultAbiCoder.encode( + // ['bytes32', 'address', 'uint256'], + // [userOpHash, entryPoint, chainId]) + // return keccak256(enc) } export const DefaultsForUserOp: UserOperation = { diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index c1dcc732b..9f8e95c6f 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -50,7 +50,15 @@ import { getAggregatedAccountInitCode, decodeRevertReason, parseValidationData, findUserOpWithMin } from './testutils' -import { DefaultsForUserOp, fillAndSign, fillSignAndPack, getUserOpHash, packUserOp, simulateValidation } from './UserOp' +import { + DefaultsForUserOp, + fillAndSign, + fillSignAndPack, + getUserOpHash, + initUserOpHashParams, + packUserOp, + simulateValidation +} from './UserOp' import { PackedUserOperation, UserOperation } from './UserOperation' import { PopulatedTransaction } from 'ethers/lib/ethers' import { ethers } from 'hardhat' @@ -87,8 +95,15 @@ describe('EntryPoint', function () { } = await createAccount(ethersSigner, await accountOwner.getAddress(), entryPoint.address)) await fund(account) + await initUserOpHashParams(entryPoint) + // sanity: validate helper functions const sampleOp = await fillAndSign({ sender: account.address }, accountOwner, entryPoint) + console.log('epaddr=', entryPoint.address) + console.log('hash=', await entryPoint.getDomainSeparatorV4()) + console.log('typehash=', await entryPoint.getPackedUserOpTypeHash()) + expect(await entryPoint.getPackedUserOpTypeHash()).to.eql('0x29a0bca4af4be3421398da00295e58e6d7de38cb492214754cb6a47507dd6f8e') + const packedOp = packUserOp(sampleOp) expect(getUserOpHash(sampleOp, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(packedOp)) }) From edb2255c9532116b5b61032980a6841b2e15123d Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 2 Jan 2025 18:05:34 +0200 Subject: [PATCH 02/44] don't encode "domain" by default. --- contracts/core/EntryPoint.sol | 12 ++++----- contracts/core/UserOperationLib.sol | 1 + reports/gas-checker.txt | 40 ++++++++++++++--------------- test/UserOp.ts | 21 +++++++-------- test/entrypoint.test.ts | 4 --- 5 files changed, 35 insertions(+), 43 deletions(-) diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 8965d9acc..be2c26e6c 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -374,13 +374,11 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT /// @inheritdoc IEntryPoint function getUserOpHash( PackedUserOperation calldata userOp - ) public view returns (bytes32) { - return _hashTypedDataV4( - keccak256(abi.encodePacked( - UserOperationLib._PACKED_USER_OPERATION, - userOp.encode() - )) - ); + ) public view returns (bytes32 ret) { + //match encoding in ./test/UserOp.ts:82 (getUserOpHash) + ret = keccak256(userOp.encode()); + //wrap by EIP712 "domainSeparator": +// ret = _hashTypedDataV4(ret); } /** diff --git a/contracts/core/UserOperationLib.sol b/contracts/core/UserOperationLib.sol index 2a3b7dc1b..f8c79fd7a 100644 --- a/contracts/core/UserOperationLib.sol +++ b/contracts/core/UserOperationLib.sol @@ -69,6 +69,7 @@ library UserOperationLib { bytes32 hashPaymasterAndData = calldataKeccak(userOp.paymasterAndData); return abi.encode( + UserOperationLib._PACKED_USER_OPERATION, sender, nonce, hashInitCode, hashCallData, accountGasLimits, preVerificationGas, gasFees, diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index f1f89e195..eb9e3d2e1 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 │ 77733 │ │ ║ +║ simple │ 1 │ 77310 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41870 │ 12611 ║ +║ simple - diff from previous │ 2 │ │ 41458 │ 12199 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 454926 │ │ ║ +║ simple │ 10 │ 450604 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 41951 │ 12692 ║ +║ simple - diff from previous │ 11 │ │ 41519 │ 12260 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83564 │ │ ║ +║ simple paymaster │ 1 │ 83141 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40426 │ 11167 ║ +║ simple paymaster with diff │ 2 │ │ 40002 │ 10743 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 447626 │ │ ║ +║ simple paymaster │ 10 │ 443278 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40447 │ 11188 ║ +║ simple paymaster with diff │ 11 │ │ 40035 │ 10776 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167485 │ │ ║ +║ big tx 5k │ 1 │ 167069 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 131101 │ 16399 ║ +║ big tx - diff from previous │ 2 │ │ 130647 │ 15945 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1347631 │ │ ║ +║ big tx 5k │ 10 │ 1343266 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131158 │ 16456 ║ +║ big tx - diff from previous │ 11 │ │ 130660 │ 15958 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84740 │ │ ║ +║ paymaster+postOp │ 1 │ 84318 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41580 │ 12321 ║ +║ paymaster+postOp with diff │ 2 │ │ 41178 │ 11919 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 459337 │ │ ║ +║ paymaster+postOp │ 10 │ 455005 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41701 │ 12442 ║ +║ paymaster+postOp with diff │ 11 │ │ 41229 │ 11970 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 121855 │ │ ║ +║ token paymaster │ 1 │ 121420 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 61416 │ 32157 ║ +║ token paymaster with diff │ 2 │ │ 61002 │ 31743 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 674884 │ │ ║ +║ token paymaster │ 10 │ 670585 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61540 │ 32281 ║ +║ token paymaster with diff │ 11 │ │ 61000 │ 31741 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/test/UserOp.ts b/test/UserOp.ts index 691ef3590..2603e7f1b 100644 --- a/test/UserOp.ts +++ b/test/UserOp.ts @@ -15,7 +15,7 @@ import { } from './testutils' import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util' import { - EntryPoint, EntryPointSimulations__factory, IEntryPoint + EntryPoint, EntryPointSimulations__factory } from '../typechain' import { PackedUserOperation, UserOperation } from './UserOperation' import { Create2Factory } from '../src/Create2Factory' @@ -69,7 +69,7 @@ export function encodeUserOp (userOp: UserOperation, forSignature = true): strin let domainSeparator: string | undefined let packedUserOpTypeHash: string | undefined -export async function initUserOpHashParams (ep: IEntryPoint): Promise { +export async function initUserOpHashParams (ep: EntryPoint): Promise { domainSeparator = await ep.getDomainSeparatorV4() packedUserOpTypeHash = await ep.getPackedUserOpTypeHash() } @@ -79,16 +79,13 @@ export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: n throw new Error('must call initUserOpHashParams(ep)') } const packed = hexConcat([packedUserOpTypeHash, encodeUserOp(op, true)]) - return keccak256(hexConcat([ - '0x1901', - domainSeparator, - keccak256(packed) - ])) - // const userOpHash = keccak256(encodeUserOp(op, true)) - // const enc = defaultAbiCoder.encode( - // ['bytes32', 'address', 'uint256'], - // [userOpHash, entryPoint, chainId]) - // return keccak256(enc) + // match implementation in ./contracts/core/EntryPoint.sol:382: + return keccak256(packed) + // return keccak256(hexConcat([ + // '0x1901', + // domainSeparator, + // keccak256(packed) + // ])) } export const DefaultsForUserOp: UserOperation = { diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index 9f8e95c6f..e18398cc0 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -99,10 +99,6 @@ describe('EntryPoint', function () { // sanity: validate helper functions const sampleOp = await fillAndSign({ sender: account.address }, accountOwner, entryPoint) - console.log('epaddr=', entryPoint.address) - console.log('hash=', await entryPoint.getDomainSeparatorV4()) - console.log('typehash=', await entryPoint.getPackedUserOpTypeHash()) - expect(await entryPoint.getPackedUserOpTypeHash()).to.eql('0x29a0bca4af4be3421398da00295e58e6d7de38cb492214754cb6a47507dd6f8e') const packedOp = packUserOp(sampleOp) expect(getUserOpHash(sampleOp, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(packedOp)) From dc432856969f914544d88aa32c7dcc22934c333c Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Mon, 13 Jan 2025 15:02:27 +0200 Subject: [PATCH 03/44] working --- contracts/core/EntryPoint.sol | 14 ++-- contracts/core/EntryPointSimulations.sol | 24 +++++++ contracts/core/UserOperationLib.sol | 4 +- contracts/samples/SimpleAccount.sol | 5 +- contracts/test/TestExpiryAccount.sol | 3 +- test/UserOp.ts | 86 ++++++++++++++++-------- test/entrypoint.test.ts | 2 +- 7 files changed, 97 insertions(+), 41 deletions(-) diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index be2c26e6c..58073d853 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -31,7 +31,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT SenderCreator private immutable _senderCreator = new SenderCreator(); - constructor() EIP712("ERC4337", "v0.8") { + string constant internal DOMAIN_NAME = "ERC4337"; + string constant internal DOMAIN_VERSION = "1"; + + constructor() EIP712(DOMAIN_NAME, DOMAIN_VERSION) { } function senderCreator() public view virtual returns (ISenderCreator) { @@ -364,10 +367,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT } function getPackedUserOpTypeHash() public pure returns (bytes32) { - return UserOperationLib._PACKED_USER_OPERATION; + return UserOperationLib.PACKED_USEROP_TYPEHASH; } - function getDomainSeparatorV4() public view returns (bytes32) { + function getDomainSeparatorV4() public virtual view returns (bytes32) { return _domainSeparatorV4(); } @@ -375,10 +378,9 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT function getUserOpHash( PackedUserOperation calldata userOp ) public view returns (bytes32 ret) { - //match encoding in ./test/UserOp.ts:82 (getUserOpHash) - ret = keccak256(userOp.encode()); + bytes32 hash = keccak256(userOp.encode()); //wrap by EIP712 "domainSeparator": -// ret = _hashTypedDataV4(ret); + ret = MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), hash); } /** diff --git a/contracts/core/EntryPointSimulations.sol b/contracts/core/EntryPointSimulations.sol index a5debe8a8..8573125d4 100644 --- a/contracts/core/EntryPointSimulations.sol +++ b/contracts/core/EntryPointSimulations.sol @@ -16,10 +16,14 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { SenderCreator private _senderCreator; + bytes32 private _non_immutable_domainSeparatorV4; + function initSenderCreator() internal virtual { //this is the address of the first contract created with CREATE by this address. address createdObj = address(uint160(uint256(keccak256(abi.encodePacked(hex"d694", address(this), hex"01"))))); _senderCreator = SenderCreator(createdObj); + + initDomainSeparator(); } function senderCreator() public view virtual override(EntryPoint, IEntryPoint) returns (ISenderCreator) { @@ -191,4 +195,24 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { return verificationGasLimit - 300; } + + //copied from EIP712.sol + bytes32 private constant TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + function __buildDomainSeparator() private view returns (bytes32) { + bytes32 _hashedName = keccak256(bytes(DOMAIN_NAME)); + bytes32 _hashedVersion = keccak256(bytes(DOMAIN_VERSION)); + return keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); + } + + //can't rely on "immutable" (constructor-initialized) variables" in simulation + function initDomainSeparator() internal { + _non_immutable_domainSeparatorV4 = __buildDomainSeparator(); + } + + function getDomainSeparatorV4() public override view returns (bytes32) { + return _non_immutable_domainSeparatorV4; + } + } diff --git a/contracts/core/UserOperationLib.sol b/contracts/core/UserOperationLib.sol index f8c79fd7a..d641834f0 100644 --- a/contracts/core/UserOperationLib.sol +++ b/contracts/core/UserOperationLib.sol @@ -47,7 +47,7 @@ library UserOperationLib { } } - bytes32 internal constant _PACKED_USER_OPERATION = + bytes32 internal constant PACKED_USEROP_TYPEHASH = keccak256( "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData)" ); @@ -69,7 +69,7 @@ library UserOperationLib { bytes32 hashPaymasterAndData = calldataKeccak(userOp.paymasterAndData); return abi.encode( - UserOperationLib._PACKED_USER_OPERATION, + UserOperationLib.PACKED_USEROP_TYPEHASH, sender, nonce, hashInitCode, hashCallData, accountGasLimits, preVerificationGas, gasFees, diff --git a/contracts/samples/SimpleAccount.sol b/contracts/samples/SimpleAccount.sol index 61af6ca6b..924a3688f 100644 --- a/contracts/samples/SimpleAccount.sol +++ b/contracts/samples/SimpleAccount.sol @@ -104,8 +104,9 @@ contract SimpleAccount is BaseAccount, TokenCallbackHandler, UUPSUpgradeable, In /// implement template method of BaseAccount function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) internal override virtual returns (uint256 validationData) { - bytes32 hash = MessageHashUtils.toEthSignedMessageHash(userOpHash); - if (owner != ECDSA.recover(hash, userOp.signature)) + + //userOpHash can be generated using eth_signTypedData_v4 + if (owner != ECDSA.recover(userOpHash, userOp.signature)) return SIG_VALIDATION_FAILED; return SIG_VALIDATION_SUCCESS; } diff --git a/contracts/test/TestExpiryAccount.sol b/contracts/test/TestExpiryAccount.sol index 5e423575c..899fb039b 100644 --- a/contracts/test/TestExpiryAccount.sol +++ b/contracts/test/TestExpiryAccount.sol @@ -37,8 +37,7 @@ contract TestExpiryAccount is SimpleAccount { /// implement template method of BaseAccount function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) internal override view returns (uint256 validationData) { - bytes32 hash = MessageHashUtils.toEthSignedMessageHash(userOpHash); - address signer = ECDSA.recover(hash,userOp.signature); + address signer = ECDSA.recover(userOpHash,userOp.signature); uint48 _until = ownerUntil[signer]; uint48 _after = ownerAfter[signer]; diff --git a/test/UserOp.ts b/test/UserOp.ts index 2603e7f1b..0262f4cc5 100644 --- a/test/UserOp.ts +++ b/test/UserOp.ts @@ -5,6 +5,7 @@ import { keccak256 } from 'ethers/lib/utils' import { BigNumber, Contract, Signer, Wallet } from 'ethers' +import { TypedDataSigner, TypedDataDomain, TypedDataField } from '@ethersproject/abstract-signer' import { AddressZero, callDataCost, @@ -13,7 +14,7 @@ import { packPaymasterData, rethrow } from './testutils' -import { ecsign, toRpcSig, keccak256 as keccak256_buffer } from 'ethereumjs-util' +import { ecsign, toRpcSig } from 'ethereumjs-util' import { EntryPoint, EntryPointSimulations__factory } from '../typechain' @@ -25,6 +26,13 @@ import EntryPointSimulationsJson from '../artifacts/contracts/core/EntryPointSim import { ethers } from 'hardhat' import { IEntryPointSimulations } from '../typechain/contracts/core/EntryPointSimulations' +// Matched to domain name, version from EntryPoint.sol: +const DOMAIN_NAME = 'ERC4337' +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 function packUserOp (userOp: UserOperation): PackedUserOperation { const accountGasLimits = packAccountGasLimits(userOp.verificationGasLimit, userOp.callGasLimit) const gasFees = packAccountGasLimits(userOp.maxPriorityFeePerGas, userOp.maxFeePerGas) @@ -48,19 +56,23 @@ export function encodeUserOp (userOp: UserOperation, forSignature = true): strin const packedUserOp = packUserOp(userOp) if (forSignature) { return defaultAbiCoder.encode( - ['address', 'uint256', 'bytes32', 'bytes32', + ['bytes32', + 'address', 'uint256', 'bytes32', 'bytes32', 'bytes32', 'uint256', 'bytes32', 'bytes32'], - [packedUserOp.sender, packedUserOp.nonce, keccak256(packedUserOp.initCode), keccak256(packedUserOp.callData), + [PACKED_USEROP_TYPEHASH, + packedUserOp.sender, packedUserOp.nonce, keccak256(packedUserOp.initCode), keccak256(packedUserOp.callData), packedUserOp.accountGasLimits, packedUserOp.preVerificationGas, packedUserOp.gasFees, keccak256(packedUserOp.paymasterAndData)]) } else { // for the purpose of calculating gas cost encode also signature (and no keccak of bytes) return defaultAbiCoder.encode( - ['address', 'uint256', 'bytes', 'bytes', + ['bytes32', + 'address', 'uint256', 'bytes', 'bytes', 'bytes32', 'uint256', 'bytes32', 'bytes', 'bytes'], - [packedUserOp.sender, packedUserOp.nonce, packedUserOp.initCode, packedUserOp.callData, + [PACKED_USEROP_TYPEHASH, + packedUserOp.sender, packedUserOp.nonce, packedUserOp.initCode, packedUserOp.callData, packedUserOp.accountGasLimits, packedUserOp.preVerificationGas, packedUserOp.gasFees, packedUserOp.paymasterAndData, packedUserOp.signature]) } @@ -78,14 +90,15 @@ export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: n if (domainSeparator == null || packedUserOpTypeHash == null) { throw new Error('must call initUserOpHashParams(ep)') } - const packed = hexConcat([packedUserOpTypeHash, encodeUserOp(op, true)]) - // match implementation in ./contracts/core/EntryPoint.sol:382: - return keccak256(packed) - // return keccak256(hexConcat([ - // '0x1901', - // domainSeparator, - // keccak256(packed) - // ])) + const packed = encodeUserOp(op, true) + // console.log('offchain: ep addr=', entryPoint, 'domain=', domainSeparator) + // console.log('offchain: packed hash=', keccak256(packed)) + // return keccak256(packed) + return keccak256(hexConcat([ + '0x1901', + domainSeparator, + keccak256(packed) + ])) } export const DefaultsForUserOp: UserOperation = { @@ -107,14 +120,10 @@ export const DefaultsForUserOp: UserOperation = { export function signUserOp (op: UserOperation, signer: Wallet, entryPoint: string, chainId: number): UserOperation { const message = getUserOpHash(op, entryPoint, chainId) - const msg1 = Buffer.concat([ - Buffer.from('\x19Ethereum Signed Message:\n32', 'ascii'), - Buffer.from(arrayify(message)) - ]) - const sig = ecsign(keccak256_buffer(msg1), Buffer.from(arrayify(signer.privateKey))) - // that's equivalent of: await signer.signMessage(message); - // (but without "async" + const sig = ecsign(Buffer.from(arrayify(message)), Buffer.from(arrayify(signer.privateKey))) + // that's equivalent of: await signer.signTypedData(domain, types, packUserOp(op)); + // (but without "async") const signedMessage1 = toRpcSig(sig.v, sig.r, sig.s) return { ...op, @@ -227,20 +236,41 @@ export async function fillAndPack (op: Partial, entryPoint?: Entr return packUserOp(await fillUserOp(op, entryPoint, getNonceFunction)) } +export function getErc4337TypedDataDomain (entryPoint: EntryPoint, chainId: number): TypedDataDomain { + return { + name: DOMAIN_NAME, + version: DOMAIN_VERSION, + chainId: chainId, + verifyingContract: entryPoint.address + } +} + +export function getErc4337TypedDataTypes (): { [type: string]: TypedDataField[] } { + return { + PackedUserOperation: [ + { name: 'sender', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + { name: 'initCode', type: 'bytes' }, + { name: 'callData', type: 'bytes' }, + { name: 'accountGasLimits', type: 'bytes32' }, + { name: 'preVerificationGas', type: 'uint256' }, + { name: 'gasFees', type: 'bytes32' }, + { name: 'paymasterAndData', type: 'bytes' } + ] + } +} 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) const chainId = await provider!.getNetwork().then(net => net.chainId) - const message = arrayify(getUserOpHash(op2, entryPoint!.address, chainId)) - let signature - try { - signature = await signer.signMessage(message) - } catch (err: any) { - // attempt to use 'eth_sign' instead of 'personal_sign' which is not supported by Foundry Anvil - signature = await (signer as any)._legacySignMessage(message) - } + const typedSigner: TypedDataSigner = signer as any + + const packedUserOp = packUserOp(op2) + + const signature = await typedSigner._signTypedData(getErc4337TypedDataDomain(entryPoint!, chainId), getErc4337TypedDataTypes(), packedUserOp) // .catch(e => e.toString()) + return { ...op2, signature diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index e18398cc0..e61fe2cec 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -957,7 +957,7 @@ describe('EntryPoint', function () { callData: accountExecCounterFromEntryPoint.data, sender: account2.address, callGasLimit: 2e6, - verificationGasLimit: 76000 + verificationGasLimit: 80000 }, accountOwner2, entryPoint) await simulateValidation(op2, entryPoint.address) From fc0c4e7d69afb11795a3ee93275dc28ab6e1fe8a Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Mon, 13 Jan 2025 15:35:25 +0200 Subject: [PATCH 04/44] removed initUserOpHashParams (internal helper) --- gascalc/GasChecker.ts | 4 +--- test/UserOp.ts | 36 ++++++++++++++++++------------------ test/entrypoint.test.ts | 3 --- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/gascalc/GasChecker.ts b/gascalc/GasChecker.ts index 9b9c6e140..ba4a11c87 100644 --- a/gascalc/GasChecker.ts +++ b/gascalc/GasChecker.ts @@ -13,7 +13,7 @@ import { } from '../typechain' import { BigNumberish, Wallet } from 'ethers' import hre from 'hardhat' -import { fillSignAndPack, fillUserOp, initUserOpHashParams, packUserOp, signUserOp } from '../test/UserOp' +import { fillSignAndPack, fillUserOp, packUserOp, signUserOp } from '../test/UserOp' import { TransactionReceipt } from '@ethersproject/abstract-provider' import { table, TableUserConfig } from 'table' import { Create2Factory } from '../src/Create2Factory' @@ -344,8 +344,6 @@ export class GasCheckCollector { this.entryPoint = EntryPoint__factory.connect(entryPointAddressOrTest, globalSigner) } - await initUserOpHashParams(this.entryPoint) - const tableHeaders = [ 'handleOps description ', 'count', diff --git a/test/UserOp.ts b/test/UserOp.ts index 0262f4cc5..8d29175a3 100644 --- a/test/UserOp.ts +++ b/test/UserOp.ts @@ -78,25 +78,11 @@ export function encodeUserOp (userOp: UserOperation, forSignature = true): strin } } -let domainSeparator: string | undefined -let packedUserOpTypeHash: string | undefined - -export async function initUserOpHashParams (ep: EntryPoint): Promise { - domainSeparator = await ep.getDomainSeparatorV4() - packedUserOpTypeHash = await ep.getPackedUserOpTypeHash() -} - export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: number): string { - if (domainSeparator == null || packedUserOpTypeHash == null) { - throw new Error('must call initUserOpHashParams(ep)') - } const packed = encodeUserOp(op, true) - // console.log('offchain: ep addr=', entryPoint, 'domain=', domainSeparator) - // console.log('offchain: packed hash=', keccak256(packed)) - // return keccak256(packed) return keccak256(hexConcat([ '0x1901', - domainSeparator, + getDomainSeparator(entryPoint, chainId), keccak256(packed) ])) } @@ -236,12 +222,26 @@ export async function fillAndPack (op: Partial, entryPoint?: Entr return packUserOp(await fillUserOp(op, entryPoint, getNonceFunction)) } -export function getErc4337TypedDataDomain (entryPoint: EntryPoint, chainId: number): TypedDataDomain { +export function getDomainSeparator (entryPoint: string, chainId: number): string { + const domainData = getErc4337TypedDataDomain(entryPoint, chainId) + console.log('data=', domainData) + return keccak256(defaultAbiCoder.encode( + ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], + [ + keccak256(Buffer.from('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')), + keccak256(Buffer.from(domainData.name!)), + keccak256(Buffer.from(domainData.version!)), + domainData.chainId, + domainData.verifyingContract + ])) +} + +export function getErc4337TypedDataDomain (entryPoint: string, chainId: number): TypedDataDomain { return { name: DOMAIN_NAME, version: DOMAIN_VERSION, chainId: chainId, - verifyingContract: entryPoint.address + verifyingContract: entryPoint } } @@ -269,7 +269,7 @@ export async function fillAndSign (op: Partial, signer: Wallet | const packedUserOp = packUserOp(op2) - const signature = await typedSigner._signTypedData(getErc4337TypedDataDomain(entryPoint!, chainId), getErc4337TypedDataTypes(), packedUserOp) // .catch(e => e.toString()) + const signature = await typedSigner._signTypedData(getErc4337TypedDataDomain(entryPoint!.address, chainId), getErc4337TypedDataTypes(), packedUserOp) // .catch(e => e.toString()) return { ...op2, diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index e61fe2cec..ff95914b1 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -55,7 +55,6 @@ import { fillAndSign, fillSignAndPack, getUserOpHash, - initUserOpHashParams, packUserOp, simulateValidation } from './UserOp' @@ -95,8 +94,6 @@ describe('EntryPoint', function () { } = await createAccount(ethersSigner, await accountOwner.getAddress(), entryPoint.address)) await fund(account) - await initUserOpHashParams(entryPoint) - // sanity: validate helper functions const sampleOp = await fillAndSign({ sender: account.address }, accountOwner, entryPoint) From 263a2324cb4876b36829cd4f0c0ab6220cf74164 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Mon, 13 Jan 2025 15:36:28 +0200 Subject: [PATCH 05/44] gas cals --- contracts/core/EntryPointSimulations.sol | 7 +++-- reports/gas-checker.txt | 40 ++++++++++++------------ 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/contracts/core/EntryPointSimulations.sol b/contracts/core/EntryPointSimulations.sol index 8573125d4..4ae2e1fde 100644 --- a/contracts/core/EntryPointSimulations.sol +++ b/contracts/core/EntryPointSimulations.sol @@ -16,7 +16,8 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { SenderCreator private _senderCreator; - bytes32 private _non_immutable_domainSeparatorV4; + //non-immutable, as EntryPointSimulations is used with state-override, without a constructor + bytes32 private __domainSeparatorV4; function initSenderCreator() internal virtual { //this is the address of the first contract created with CREATE by this address. @@ -208,11 +209,11 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { //can't rely on "immutable" (constructor-initialized) variables" in simulation function initDomainSeparator() internal { - _non_immutable_domainSeparatorV4 = __buildDomainSeparator(); + __domainSeparatorV4 = __buildDomainSeparator(); } function getDomainSeparatorV4() public override view returns (bytes32) { - return _non_immutable_domainSeparatorV4; + return __domainSeparatorV4; } } diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index eb9e3d2e1..88b0cad36 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 │ 77310 │ │ ║ +║ simple │ 1 │ 77450 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41458 │ 12199 ║ +║ simple - diff from previous │ 2 │ │ 41586 │ 12327 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 450604 │ │ ║ +║ simple │ 10 │ 451956 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 41519 │ 12260 ║ +║ simple - diff from previous │ 11 │ │ 41671 │ 12412 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83141 │ │ ║ +║ simple paymaster │ 1 │ 83281 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40002 │ 10743 ║ +║ simple paymaster with diff │ 2 │ │ 40130 │ 10871 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 443278 │ │ ║ +║ simple paymaster │ 10 │ 444690 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40035 │ 10776 ║ +║ simple paymaster with diff │ 11 │ │ 40151 │ 10892 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167069 │ │ ║ +║ big tx 5k │ 1 │ 167209 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 130647 │ 15945 ║ +║ big tx - diff from previous │ 2 │ │ 130823 │ 16121 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1343266 │ │ ║ +║ big tx 5k │ 10 │ 1344690 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 130660 │ 15958 ║ +║ big tx - diff from previous │ 11 │ │ 130836 │ 16134 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84318 │ │ ║ +║ paymaster+postOp │ 1 │ 84458 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41178 │ 11919 ║ +║ paymaster+postOp with diff │ 2 │ │ 41306 │ 12047 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 455005 │ │ ║ +║ paymaster+postOp │ 10 │ 456477 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41229 │ 11970 ║ +║ paymaster+postOp with diff │ 11 │ │ 41357 │ 12098 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 121420 │ │ ║ +║ token paymaster │ 1 │ 121560 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 61002 │ 31743 ║ +║ token paymaster with diff │ 2 │ │ 61130 │ 31871 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 670585 │ │ ║ +║ token paymaster │ 10 │ 671877 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61000 │ 31741 ║ +║ token paymaster with diff │ 11 │ │ 61248 │ 31989 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ From 5b052aedb37ffd729ab90b6ac8679886a11c32ae Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Mon, 13 Jan 2025 16:13:45 +0200 Subject: [PATCH 06/44] undo gas limit change. --- test/entrypoint.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index ff95914b1..f5eba41d1 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -954,7 +954,7 @@ describe('EntryPoint', function () { callData: accountExecCounterFromEntryPoint.data, sender: account2.address, callGasLimit: 2e6, - verificationGasLimit: 80000 + verificationGasLimit: 76000 }, accountOwner2, entryPoint) await simulateValidation(op2, entryPoint.address) From 38d4714c24e05bc84744975b303d8851905d2de7 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 15 Jan 2025 16:55:43 +0200 Subject: [PATCH 07/44] pr review (typo hash1) --- contracts/core/EntryPoint.sol | 7 +++---- contracts/core/UserOperationLib.sol | 2 +- reports/gas-checker.txt | 28 ++++++++++++++-------------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 58073d853..6ded1a88d 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -377,10 +377,9 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT /// @inheritdoc IEntryPoint function getUserOpHash( PackedUserOperation calldata userOp - ) public view returns (bytes32 ret) { - bytes32 hash = keccak256(userOp.encode()); - //wrap by EIP712 "domainSeparator": - ret = MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), hash); + ) public view returns (bytes32) { + return + MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash()); } /** diff --git a/contracts/core/UserOperationLib.sol b/contracts/core/UserOperationLib.sol index d641834f0..2c6bde18f 100644 --- a/contracts/core/UserOperationLib.sol +++ b/contracts/core/UserOperationLib.sol @@ -137,7 +137,7 @@ library UserOperationLib { * Hash the user operation data. * @param userOp - The user operation data. */ - function hash1( + function hash( PackedUserOperation calldata userOp ) internal pure returns (bytes32) { return keccak256(encode(userOp)); diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 88b0cad36..70ab3a60e 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -14,42 +14,42 @@ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ simple │ 1 │ 77450 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41586 │ 12327 ║ +║ simple - diff from previous │ 2 │ │ 41598 │ 12339 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 451956 │ │ ║ +║ simple │ 10 │ 451980 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 41671 │ 12412 ║ +║ simple - diff from previous │ 11 │ │ 41659 │ 12400 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ simple paymaster │ 1 │ 83281 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40130 │ 10871 ║ +║ simple paymaster with diff │ 2 │ │ 40142 │ 10883 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 444690 │ │ ║ +║ simple paymaster │ 10 │ 444666 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40151 │ 10892 ║ +║ simple paymaster with diff │ 11 │ │ 40163 │ 10904 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ big tx 5k │ 1 │ 167209 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 130823 │ 16121 ║ +║ big tx - diff from previous │ 2 │ │ 130799 │ 16097 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1344690 │ │ ║ +║ big tx 5k │ 10 │ 1344654 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 130836 │ 16134 ║ +║ big tx - diff from previous │ 11 │ │ 130872 │ 16170 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp │ 1 │ 84458 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp with diff │ 2 │ │ 41306 │ 12047 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 456477 │ │ ║ +║ paymaster+postOp │ 10 │ 456393 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41357 │ 12098 ║ +║ paymaster+postOp with diff │ 11 │ │ 41381 │ 12122 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 121560 │ │ ║ +║ token paymaster │ 1 │ 121572 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ token paymaster with diff │ 2 │ │ 61130 │ 31871 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 671877 │ │ ║ +║ token paymaster │ 10 │ 671901 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61248 │ 31989 ║ +║ token paymaster with diff │ 11 │ │ 61200 │ 31941 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ From 5f0a6b74573aef40bdb56cb2df11904273c2ea1e Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Mon, 20 Jan 2025 15:05:29 +0200 Subject: [PATCH 08/44] remove unused comment. --- contracts/core/EntryPointSimulations.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/core/EntryPointSimulations.sol b/contracts/core/EntryPointSimulations.sol index 4ae2e1fde..2182fb54b 100644 --- a/contracts/core/EntryPointSimulations.sol +++ b/contracts/core/EntryPointSimulations.sol @@ -16,7 +16,6 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { SenderCreator private _senderCreator; - //non-immutable, as EntryPointSimulations is used with state-override, without a constructor bytes32 private __domainSeparatorV4; function initSenderCreator() internal virtual { From 68894e4f6c1ddf29ca130048e115673fa5e1e40a Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 22 Jan 2025 17:45:05 +0200 Subject: [PATCH 09/44] initial implementation --- contracts/core/Eip7702Support.sol | 54 ++++++++++++++++++ contracts/core/EntryPoint.sol | 10 +++- contracts/core/EntryPointSimulations.sol | 2 +- contracts/core/SenderCreator.sol | 24 +++++--- contracts/core/UserOperationLib.sol | 20 ++++++- contracts/interfaces/ISenderCreator.sol | 3 + reports/gas-checker.txt | 42 +++++++------- src/Create2Factory.ts | 4 +- test/UserOp.ts | 28 +++++++++- test/entrypoint.test.ts | 70 ++++++++++++++++++++++-- 10 files changed, 215 insertions(+), 42 deletions(-) create mode 100644 contracts/core/Eip7702Support.sol diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol new file mode 100644 index 000000000..291dfbcd8 --- /dev/null +++ b/contracts/core/Eip7702Support.sol @@ -0,0 +1,54 @@ +pragma solidity ^0.8; + +import "../interfaces/PackedUserOperation.sol"; +import "../core/UserOperationLib.sol"; +// SPDX-License-Identifier: MIT + +// EIP-7702 code prefix. Also, we use this prefix as a marker in the initCode. To specify this account is EIP-7702. +uint256 constant EIP7702_PREFIX = 0xef0100; + +using UserOperationLib for PackedUserOperation; + + //get alternate InitCode (just for hashing) when using EIP-7702 + function _getEip7702InitCodeOverride(PackedUserOperation calldata userOp) view returns (bytes memory) { + bytes calldata initCode = userOp.initCode; + if (! _isEip7702InitCode(initCode)) { + return ""; + } + address delegate = _getEip7702Delegate(userOp.getSender()); + if (initCode.length < 20) + return abi.encodePacked(delegate); + else + return abi.encodePacked(delegate, initCode[20 :]); + } + + + function _isEip7702InitCode(bytes calldata initCode) pure returns (bool) { + if (initCode.length < 3) { + return false; + } + uint256 initCodeStart; + assembly { + initCodeStart := calldataload(initCode.offset) + } + // make sure first 20 bytes of initCode are "0xff0100" (padded with zeros) + // initCode can be shorter (e.g. only 3), but then it is already zero-padded. + return (initCodeStart >> (256 - 160)) == ((EIP7702_PREFIX << (160 - 24))); + } + +/** + * get the EIP-7702 delegate from contract code. + * requires EXTCODECOPY pr: https://github.com/ethereum/EIPs/pull/9248 (not yet merged or implemented) + **/ + function _getEip7702Delegate(address sender) view returns (address) { + uint256 senderCode; + assembly { + extcodecopy(sender, 0, 0, 32) + senderCode := mload(0) + } + // senderCode is the first 32 bytes of the sender's code + // If it is an EIP-7702 delegate, then top 24 bits are the EIP7702_PREFIX + // next 160 bytes are the delegate address + require(senderCode >> (256 - 24) == EIP7702_PREFIX, "not an EIP-7702 delegate"); + return address(uint160(senderCode >> (256 - 160 - 24))); + } diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 6ded1a88d..0808ff790 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"; @@ -378,8 +379,9 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT function getUserOpHash( PackedUserOperation calldata userOp ) public view returns (bytes32) { + bytes memory overrideInitCode = _getEip7702InitCodeOverride(userOp); return - MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash()); + MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCode)); } /** @@ -441,6 +443,12 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT ) internal { if (initCode.length != 0) { address sender = opInfo.mUserOp.sender; + if ( _isEip7702InitCode(initCode) ) { + // validate account has an EIP7702 delegate + _getEip7702Delegate(sender); + 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 2182fb54b..27a9fc538 100644 --- a/contracts/core/EntryPointSimulations.sol +++ b/contracts/core/EntryPointSimulations.sol @@ -192,7 +192,7 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { //slightly stricter gas limit than the real EntryPoint function _getVerificationGasLimit(uint256 verificationGasLimit) internal pure virtual override returns (uint256) { - return verificationGasLimit - 300; + return verificationGasLimit - 350; } diff --git a/contracts/core/SenderCreator.sol b/contracts/core/SenderCreator.sol index 539760610..958929d15 100644 --- a/contracts/core/SenderCreator.sol +++ b/contracts/core/SenderCreator.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.23; import "../interfaces/ISenderCreator.sol"; +import "../utils/Exec.sol"; /** * Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address, @@ -23,10 +24,9 @@ contract SenderCreator is ISenderCreator { function createSender( bytes calldata initCode ) external returns (address sender) { - if (msg.sender != entryPoint) { - revert("AA97 should call from EntryPoint"); - } + 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 */ @@ -40,10 +40,20 @@ contract SenderCreator is ISenderCreator { 0, 32 ) - sender := mload(0) - } - if (!success) { - sender = address(0); + if success { + sender := mload(0) + } } } + + //use initcode to initialize an EIP-7702 account + function initEip7702Sender( + address sender, + bytes calldata initCode + ) external { + require(msg.sender == entryPoint, "AA97 should call from EntryPoint"); + bytes memory initCallData = initCode[20 :]; + bool success = Exec.call(sender, 0, initCallData, gasleft()); + require(success, "AA13: EIP7702 sender initialization failed"); + } } diff --git a/contracts/core/UserOperationLib.sol b/contracts/core/UserOperationLib.sol index 2c6bde18f..99973ef47 100644 --- a/contracts/core/UserOperationLib.sol +++ b/contracts/core/UserOperationLib.sol @@ -58,10 +58,22 @@ library UserOperationLib { */ function encode( PackedUserOperation calldata userOp + ) internal pure returns (bytes memory ret) { + return encode(userOp, ""); + } + + /** + * Pack the user operation data into bytes for hashing. + * @param userOp - The user operation data. + * @param overrideInitCode - If set, encode this instead of the initCode field in the userOp. + */ + function encode( + PackedUserOperation calldata userOp, + bytes memory overrideInitCode ) internal pure returns (bytes memory ret) { address sender = getSender(userOp); uint256 nonce = userOp.nonce; - bytes32 hashInitCode = calldataKeccak(userOp.initCode); + bytes32 hashInitCode = overrideInitCode.length==0 ? calldataKeccak(userOp.initCode) : keccak256(overrideInitCode); bytes32 hashCallData = calldataKeccak(userOp.callData); bytes32 accountGasLimits = userOp.accountGasLimits; uint256 preVerificationGas = userOp.preVerificationGas; @@ -136,10 +148,12 @@ library UserOperationLib { /** * Hash the user operation data. * @param userOp - The user operation data. + * @param overrideInitCode - If set, the initCode will be replaced with this value just for hashing. */ function hash( - PackedUserOperation calldata userOp + PackedUserOperation calldata userOp, + bytes memory overrideInitCode ) internal pure returns (bytes32) { - return keccak256(encode(userOp)); + return keccak256(encode(userOp, overrideInitCode)); } } 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/reports/gas-checker.txt b/reports/gas-checker.txt index 70ab3a60e..ecda5f16f 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -4,7 +4,7 @@ ╔══════════════════════════╤════════╗ ║ gas estimate "simple" │ 29259 ║ ╟──────────────────────────┼────────╢ -║ gas estimate "big tx 5k" │ 114702 ║ +║ gas estimate "big tx 5k" │ 114690 ║ ╚══════════════════════════╧════════╝ ╔════════════════════════════════╤═══════╤═══════════════╤════════════════╤═════════════════════╗ @@ -12,44 +12,44 @@ ║ │ │ │ (delta for │ (compared to ║ ║ │ │ │ one UserOp) │ account.exec()) ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 1 │ 77450 │ │ ║ +║ simple │ 1 │ 78894 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41598 │ 12339 ║ +║ simple - diff from previous │ 2 │ │ 43023 │ 13764 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 451980 │ │ ║ +║ simple │ 10 │ 466080 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 41659 │ 12400 ║ +║ simple - diff from previous │ 11 │ │ 43098 │ 13839 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83281 │ │ ║ +║ simple paymaster │ 1 │ 84943 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40142 │ 10883 ║ +║ simple paymaster with diff │ 2 │ │ 41785 │ 12526 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 444666 │ │ ║ +║ simple paymaster │ 10 │ 461056 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40163 │ 10904 ║ +║ simple paymaster with diff │ 11 │ │ 41785 │ 12526 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167209 │ │ ║ +║ big tx 5k │ 1 │ 168641 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 130799 │ 16097 ║ +║ big tx - diff from previous │ 2 │ │ 132237 │ 17547 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1344654 │ │ ║ +║ big tx 5k │ 10 │ 1358832 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 130872 │ 16170 ║ +║ big tx - diff from previous │ 11 │ │ 132228 │ 17538 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84458 │ │ ║ +║ paymaster+postOp │ 1 │ 86275 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41306 │ 12047 ║ +║ paymaster+postOp with diff │ 2 │ │ 43046 │ 13787 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 456393 │ │ ║ +║ paymaster+postOp │ 10 │ 474248 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41381 │ 12122 ║ +║ paymaster+postOp with diff │ 11 │ │ 43110 │ 13851 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 121572 │ │ ║ +║ token paymaster │ 1 │ 123633 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 61130 │ 31871 ║ +║ token paymaster with diff │ 2 │ │ 63148 │ 33889 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 671901 │ │ ║ +║ token paymaster │ 10 │ 692104 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61200 │ 31941 ║ +║ token paymaster with diff │ 11 │ │ 63234 │ 33975 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/src/Create2Factory.ts b/src/Create2Factory.ts index a0f778639..57d4cee2d 100644 --- a/src/Create2Factory.ts +++ b/src/Create2Factory.ts @@ -101,8 +101,8 @@ export class Create2Factory { await (signer ?? this.signer).sendTransaction({ to: Create2Factory.factoryDeployer, value: BigNumber.from(Create2Factory.factoryDeploymentFee) - }) - await this.provider.sendTransaction(Create2Factory.factoryTx) + }).then(async (t) => t.wait()) + await this.provider.sendTransaction(Create2Factory.factoryTx).then(async (t) => t.wait()) if (!await this._isFactoryDeployed()) { throw new Error('fatal: failed to deploy deterministic deployer') } diff --git a/test/UserOp.ts b/test/UserOp.ts index 8d29175a3..8e544ffa8 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 EIP7702_PREFIX = '0xef0100' + export function packUserOp (userOp: UserOperation): PackedUserOperation { const accountGasLimits = packAccountGasLimits(userOp.verificationGasLimit, userOp.callGasLimit) const gasFees = packAccountGasLimits(userOp.maxPriorityFeePerGas, userOp.maxFeePerGas) @@ -87,6 +89,27 @@ export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: n ])) } +// calculate UserOpHash, given "sender" contract code. +// (only used if initCode starts with prefix) +export function getUserOpHashWithEip7702 (op: UserOperation, entryPoint: string, chainId: number, senderCode: string): string { + let initCode = hexlify(op.initCode) + if (initCode.startsWith(EIP7702_PREFIX)) { + const delegate = hexDataSlice(senderCode, 3, 23) + if (hexDataLength(initCode) < 20) { + // its only prefix: + initCode = delegate + } else { + // replace address in initCode with delegate + initCode = hexConcat([delegate, hexDataSlice(initCode, 20)]) + } + op = { + ...op, + initCode: initCode + } + } + return getUserOpHash(op, entryPoint, chainId) +} + export const DefaultsForUserOp: UserOperation = { sender: AddressZero, nonce: 0, @@ -224,7 +247,6 @@ export async function fillAndPack (op: Partial, entryPoint?: Entr export function getDomainSeparator (entryPoint: string, chainId: number): string { const domainData = getErc4337TypedDataDomain(entryPoint, chainId) - console.log('data=', domainData) return keccak256(defaultAbiCoder.encode( ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], [ diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index f5eba41d1..b77f8518c 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -51,22 +51,23 @@ import { decodeRevertReason, parseValidationData, findUserOpWithMin } from './testutils' import { - DefaultsForUserOp, + DefaultsForUserOp, EIP7702_PREFIX, fillAndSign, - fillSignAndPack, - getUserOpHash, + fillSignAndPack, fillUserOpDefaults, + getUserOpHash, getUserOpHashWithEip7702, 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 { arrayify, defaultAbiCoder, hexConcat, 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' describe('EntryPoint', function () { let entryPoint: EntryPoint @@ -101,6 +102,67 @@ describe('EntryPoint', function () { expect(getUserOpHash(sampleOp, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(packedOp)) }) + // 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 + + const mockDelegate = createAddress() + + const deployedDelegateCode = hexConcat([EIP7702_PREFIX, mockDelegate]) + + before(async () => { + chainId = await ethers.provider.getNetwork().then(net => net.chainId) + }) + + 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: EIP7702_PREFIX }, entryPoint.address, chainId, deployedDelegateCode)).to.eql(hash) + }) + it('#getUserOpHashWith7702 with initcode', async () => { + const hash = getUserOpHash({ ...userop, initCode: mockDelegate + 'b1ab1a' }, entryPoint.address, chainId) + expect(getUserOpHashWithEip7702({ ...userop, initCode: '0xef0100'.padEnd(42, '0') + 'b1ab1a' }, entryPoint.address, chainId, deployedDelegateCode)).to.eql(hash) + }) + }) + + describe('entryPoint getUserOpHash', () => { + it('should return the same hash as calculated locally', async () => { + // call entryPoint.getUserOpHash, but use state-override to run it with specific code (e.g. delegate) on the sender's code. + async function callGetUserOpHashWithCode (userop: UserOperation, senderCode?: any): 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 + ]) + } + + userop.initCode = EIP7702_PREFIX + expect(await callGetUserOpHashWithCode(userop, deployedDelegateCode)).to.eql( + getUserOpHashWithEip7702(userop, entryPoint.address, chainId, deployedDelegateCode)) + }) + }) + }) + describe('Stake Management', () => { let addr: string before(async () => { From 317d26b4e6c391faaf42e74fd27c8639bda9c9b2 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 22 Jan 2025 19:43:47 +0200 Subject: [PATCH 10/44] memory-safe --- contracts/core/Eip7702Support.sol | 2 +- reports/gas-checker.txt | 40 +++++++++++++++---------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol index 291dfbcd8..76e5894ba 100644 --- a/contracts/core/Eip7702Support.sol +++ b/contracts/core/Eip7702Support.sol @@ -42,7 +42,7 @@ using UserOperationLib for PackedUserOperation; **/ function _getEip7702Delegate(address sender) view returns (address) { uint256 senderCode; - assembly { + assembly ("memory-safe") { extcodecopy(sender, 0, 0, 32) senderCode := mload(0) } diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index ecda5f16f..cb4f35b5a 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 │ 78894 │ │ ║ +║ simple │ 1 │ 77924 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 43023 │ 13764 ║ +║ simple - diff from previous │ 2 │ │ 42071 │ 12812 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 466080 │ │ ║ +║ simple │ 10 │ 456686 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 43098 │ 13839 ║ +║ simple - diff from previous │ 11 │ │ 42122 │ 12863 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 84943 │ │ ║ +║ simple paymaster │ 1 │ 83743 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 41785 │ 12526 ║ +║ simple paymaster with diff │ 2 │ │ 40627 │ 11368 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 461056 │ │ ║ +║ simple paymaster │ 10 │ 449410 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 41785 │ 12526 ║ +║ simple paymaster with diff │ 11 │ │ 40603 │ 11344 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 168641 │ │ ║ +║ big tx 5k │ 1 │ 167659 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 132237 │ 17547 ║ +║ big tx - diff from previous │ 2 │ │ 131285 │ 16595 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1358832 │ │ ║ +║ big tx 5k │ 10 │ 1349402 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 132228 │ 17538 ║ +║ big tx - diff from previous │ 11 │ │ 131324 │ 16634 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 86275 │ │ ║ +║ paymaster+postOp │ 1 │ 84931 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 43046 │ 13787 ║ +║ paymaster+postOp with diff │ 2 │ │ 41792 │ 12533 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 474248 │ │ ║ +║ paymaster+postOp │ 10 │ 461222 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 43110 │ 13851 ║ +║ paymaster+postOp with diff │ 11 │ │ 41772 │ 12513 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 123633 │ │ ║ +║ token paymaster │ 1 │ 122046 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 63148 │ 33889 ║ +║ token paymaster with diff │ 2 │ │ 61603 │ 32344 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 692104 │ │ ║ +║ token paymaster │ 10 │ 676648 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 63234 │ 33975 ║ +║ token paymaster with diff │ 11 │ │ 61677 │ 32418 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ From 2617f0319d3d917a7944b9adf16c822cdbee554d Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 22 Jan 2025 19:58:00 +0200 Subject: [PATCH 11/44] optimize overrideInitCode --- contracts/core/Eip7702Support.sol | 12 +++++---- contracts/core/EntryPoint.sol | 2 +- contracts/core/UserOperationLib.sol | 6 ++--- contracts/test/TestUtil.sol | 2 +- reports/gas-checker.txt | 42 ++++++++++++++--------------- 5 files changed, 33 insertions(+), 31 deletions(-) diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol index 76e5894ba..b579f4dc0 100644 --- a/contracts/core/Eip7702Support.sol +++ b/contracts/core/Eip7702Support.sol @@ -10,25 +10,26 @@ uint256 constant EIP7702_PREFIX = 0xef0100; using UserOperationLib for PackedUserOperation; //get alternate InitCode (just for hashing) when using EIP-7702 - function _getEip7702InitCodeOverride(PackedUserOperation calldata userOp) view returns (bytes memory) { + function _getEip7702InitCodeOverride(PackedUserOperation calldata userOp) view returns (bytes32) { bytes calldata initCode = userOp.initCode; if (! _isEip7702InitCode(initCode)) { - return ""; + return 0; } address delegate = _getEip7702Delegate(userOp.getSender()); if (initCode.length < 20) - return abi.encodePacked(delegate); + return keccak256(abi.encodePacked(delegate)); else - return abi.encodePacked(delegate, initCode[20 :]); + return keccak256(abi.encodePacked(delegate, initCode[20 :])); } function _isEip7702InitCode(bytes calldata initCode) pure returns (bool) { + if (initCode.length < 3) { return false; } uint256 initCodeStart; - assembly { + assembly ("memory-safe") { initCodeStart := calldataload(initCode.offset) } // make sure first 20 bytes of initCode are "0xff0100" (padded with zeros) @@ -42,6 +43,7 @@ using UserOperationLib for PackedUserOperation; **/ function _getEip7702Delegate(address sender) view returns (address) { uint256 senderCode; + assembly ("memory-safe") { extcodecopy(sender, 0, 0, 32) senderCode := mload(0) diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 0808ff790..60d9e3450 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -379,7 +379,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT function getUserOpHash( PackedUserOperation calldata userOp ) public view returns (bytes32) { - bytes memory overrideInitCode = _getEip7702InitCodeOverride(userOp); + bytes32 overrideInitCode = _getEip7702InitCodeOverride(userOp); return MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCode)); } diff --git a/contracts/core/UserOperationLib.sol b/contracts/core/UserOperationLib.sol index 99973ef47..3aaa6893a 100644 --- a/contracts/core/UserOperationLib.sol +++ b/contracts/core/UserOperationLib.sol @@ -69,11 +69,11 @@ library UserOperationLib { */ function encode( PackedUserOperation calldata userOp, - bytes memory overrideInitCode + bytes32 overrideInitCode ) internal pure returns (bytes memory ret) { address sender = getSender(userOp); uint256 nonce = userOp.nonce; - bytes32 hashInitCode = overrideInitCode.length==0 ? calldataKeccak(userOp.initCode) : keccak256(overrideInitCode); + bytes32 hashInitCode = overrideInitCode==0 ? calldataKeccak(userOp.initCode) : overrideInitCode; bytes32 hashCallData = calldataKeccak(userOp.callData); bytes32 accountGasLimits = userOp.accountGasLimits; uint256 preVerificationGas = userOp.preVerificationGas; @@ -152,7 +152,7 @@ library UserOperationLib { */ function hash( PackedUserOperation calldata userOp, - bytes memory overrideInitCode + bytes32 overrideInitCode ) internal pure returns (bytes32) { return keccak256(encode(userOp, overrideInitCode)); } diff --git a/contracts/test/TestUtil.sol b/contracts/test/TestUtil.sol index 4c9f38c8f..3f06c1a32 100644 --- a/contracts/test/TestUtil.sol +++ b/contracts/test/TestUtil.sol @@ -8,7 +8,7 @@ contract TestUtil { using UserOperationLib for PackedUserOperation; function encodeUserOp(PackedUserOperation calldata op) external pure returns (bytes memory){ - return op.encode(); + return op.encode(0); } } diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index cb4f35b5a..8c98d6763 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -4,7 +4,7 @@ ╔══════════════════════════╤════════╗ ║ gas estimate "simple" │ 29259 ║ ╟──────────────────────────┼────────╢ -║ gas estimate "big tx 5k" │ 114690 ║ +║ gas estimate "big tx 5k" │ 114702 ║ ╚══════════════════════════╧════════╝ ╔════════════════════════════════╤═══════╤═══════════════╤════════════════╤═════════════════════╗ @@ -12,44 +12,44 @@ ║ │ │ │ (delta for │ (compared to ║ ║ │ │ │ one UserOp) │ account.exec()) ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 1 │ 77924 │ │ ║ +║ simple │ 1 │ 77803 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 42071 │ 12812 ║ +║ simple - diff from previous │ 2 │ │ 41939 │ 12680 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 456686 │ │ ║ +║ simple │ 10 │ 455498 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 42122 │ 12863 ║ +║ simple - diff from previous │ 11 │ │ 42048 │ 12789 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83743 │ │ ║ +║ simple paymaster │ 1 │ 83634 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40627 │ 11368 ║ +║ simple paymaster with diff │ 2 │ │ 40495 │ 11236 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 449410 │ │ ║ +║ simple paymaster │ 10 │ 448184 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40603 │ 11344 ║ +║ simple paymaster with diff │ 11 │ │ 40552 │ 11293 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167659 │ │ ║ +║ big tx 5k │ 1 │ 167562 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 131285 │ 16595 ║ +║ big tx - diff from previous │ 2 │ │ 131164 │ 16462 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1349402 │ │ ║ +║ big tx 5k │ 10 │ 1348232 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131324 │ 16634 ║ +║ big tx - diff from previous │ 11 │ │ 131129 │ 16427 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84931 │ │ ║ +║ paymaster+postOp │ 1 │ 84799 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41792 │ 12533 ║ +║ paymaster+postOp with diff │ 2 │ │ 41683 │ 12424 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 461222 │ │ ║ +║ paymaster+postOp │ 10 │ 459983 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41772 │ 12513 ║ +║ paymaster+postOp with diff │ 11 │ │ 41746 │ 12487 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 122046 │ │ ║ +║ token paymaster │ 1 │ 121913 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 61603 │ 32344 ║ +║ token paymaster with diff │ 2 │ │ 61447 │ 32188 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 676648 │ │ ║ +║ token paymaster │ 10 │ 675275 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61677 │ 32418 ║ +║ token paymaster with diff │ 11 │ │ 61541 │ 32282 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ From 50523174cbc6afd4d1b7071c518dc075017cf7c9 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 23 Jan 2025 13:49:39 +0200 Subject: [PATCH 12/44] addeds: zero-tails, fail if not eip-7702 account. --- contracts/core/EntryPoint.sol | 2 +- contracts/core/SenderCreator.sol | 3 ++- test/entrypoint.test.ts | 38 +++++++++++++++----------------- test/testutils.ts | 17 ++++++++++++++ 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 60d9e3450..49fe8bd1e 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -444,7 +444,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT if (initCode.length != 0) { address sender = opInfo.mUserOp.sender; if ( _isEip7702InitCode(initCode) ) { - // validate account has an EIP7702 delegate + // validate it is an EIP7702 account _getEip7702Delegate(sender); senderCreator().initEip7702Sender(sender, initCode[20:]); return; diff --git a/contracts/core/SenderCreator.sol b/contracts/core/SenderCreator.sol index 958929d15..df7e46d7c 100644 --- a/contracts/core/SenderCreator.sol +++ b/contracts/core/SenderCreator.sol @@ -46,7 +46,8 @@ contract SenderCreator is ISenderCreator { } } - //use initcode to initialize an EIP-7702 account + // use initCode to initialize an EIP-7702 account + // caller (EntryPoint) already verified it is an EIP-7702 account. function initEip7702Sender( address sender, bytes calldata initCode diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index b77f8518c..964c35ce2 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -48,7 +48,7 @@ import { HashZero, createAccount, getAggregatedAccountInitCode, - decodeRevertReason, parseValidationData, findUserOpWithMin + decodeRevertReason, parseValidationData, findUserOpWithMin, callGetUserOpHashWithCode } from './testutils' import { DefaultsForUserOp, EIP7702_PREFIX, @@ -139,26 +139,24 @@ describe('EntryPoint', function () { describe('entryPoint getUserOpHash', () => { it('should return the same hash as calculated locally', async () => { - // call entryPoint.getUserOpHash, but use state-override to run it with specific code (e.g. delegate) on the sender's code. - async function callGetUserOpHashWithCode (userop: UserOperation, senderCode?: any): 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 - ]) - } + const op1 = { ...userop, initCode: EIP7702_PREFIX } + expect(await callGetUserOpHashWithCode(entryPoint, op1, deployedDelegateCode)).to.eql( + getUserOpHashWithEip7702(op1, entryPoint.address, chainId, deployedDelegateCode)) + }) + + it('should fail getUserOpHash marked for eip-7702, without a delegate', async () => { + const op1 = { ...userop, initCode: EIP7702_PREFIX } + await expect(callGetUserOpHashWithCode(entryPoint, op1, '0x608000')).to.revertedWith('not an EIP-7702 delegate') + }) + + it('should allow initCode with EIP7702_PREFIX tailed with zeros only, ', async () => { + const op_zero_tail = { ...userop, initCode: EIP7702_PREFIX + '00'.repeat(10) } + expect(await callGetUserOpHashWithCode(entryPoint, op_zero_tail, deployedDelegateCode)).to.eql( + getUserOpHashWithEip7702(op_zero_tail, entryPoint.address, chainId, deployedDelegateCode)) - userop.initCode = EIP7702_PREFIX - expect(await callGetUserOpHashWithCode(userop, deployedDelegateCode)).to.eql( - getUserOpHashWithEip7702(userop, entryPoint.address, chainId, deployedDelegateCode)) + op_zero_tail.initCode = EIP7702_PREFIX + '00'.repeat(30) + expect(await callGetUserOpHashWithCode(entryPoint, op_zero_tail, deployedDelegateCode)).to.eql( + getUserOpHashWithEip7702(op_zero_tail, entryPoint.address, chainId, deployedDelegateCode)) }) }) }) diff --git a/test/testutils.ts b/test/testutils.ts index 59205a3e1..481574ba8 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -444,3 +444,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 + ]) +} From b8a9d264b592d82232705587a63fb962a32469c1 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 23 Jan 2025 13:52:14 +0200 Subject: [PATCH 13/44] gas calcs --- reports/gas-checker.txt | 34 +++++++++++++++++----------------- src/Create2Factory.ts | 8 ++++++-- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 8c98d6763..020cd6b33 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 │ 77803 │ │ ║ +║ simple │ 1 │ 77779 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41939 │ 12680 ║ +║ simple - diff from previous │ 2 │ │ 41975 │ 12716 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 455498 │ │ ║ +║ simple │ 10 │ 455462 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 42048 │ 12789 ║ +║ simple - diff from previous │ 11 │ │ 42084 │ 12825 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83634 │ │ ║ +║ simple paymaster │ 1 │ 83598 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40495 │ 11236 ║ +║ simple paymaster with diff │ 2 │ │ 40471 │ 11212 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 448184 │ │ ║ +║ simple paymaster │ 10 │ 448052 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40552 │ 11293 ║ +║ simple paymaster with diff │ 11 │ │ 40516 │ 11257 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ big tx 5k │ 1 │ 167562 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 131164 │ 16462 ║ +║ big tx - diff from previous │ 2 │ │ 131176 │ 16474 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1348232 │ │ ║ +║ big tx 5k │ 10 │ 1348148 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131129 │ 16427 ║ +║ big tx - diff from previous │ 11 │ │ 131225 │ 16523 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84799 │ │ ║ +║ paymaster+postOp │ 1 │ 84787 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp with diff │ 2 │ │ 41683 │ 12424 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp │ 10 │ 459983 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41746 │ 12487 ║ +║ paymaster+postOp with diff │ 11 │ │ 41698 │ 12439 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 121913 │ │ ║ +║ token paymaster │ 1 │ 121925 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 61447 │ 32188 ║ +║ token paymaster with diff │ 2 │ │ 61483 │ 32224 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 675275 │ │ ║ +║ token paymaster │ 10 │ 675443 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61541 │ 32282 ║ +║ token paymaster with diff │ 11 │ │ 61529 │ 32270 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/src/Create2Factory.ts b/src/Create2Factory.ts index 57d4cee2d..9477a6bf6 100644 --- a/src/Create2Factory.ts +++ b/src/Create2Factory.ts @@ -101,8 +101,12 @@ export class Create2Factory { await (signer ?? this.signer).sendTransaction({ to: Create2Factory.factoryDeployer, value: BigNumber.from(Create2Factory.factoryDeploymentFee) - }).then(async (t) => t.wait()) - await this.provider.sendTransaction(Create2Factory.factoryTx).then(async (t) => t.wait()) + }) + //first tx.. can't "wait" for it. + await new Promise(resolve => setTimeout(resolve, 100)) + + await this.provider.sendTransaction(Create2Factory.factoryTx).then(tx => tx.wait()) + if (!await this._isFactoryDeployed()) { throw new Error('fatal: failed to deploy deterministic deployer') } From d9b3f7738f1bc0ce75ad299e10489a0bedab9f8f Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 23 Jan 2025 13:56:33 +0200 Subject: [PATCH 14/44] lints --- contracts/core/Eip7702Support.sol | 2 ++ contracts/core/SenderCreator.sol | 3 ++- src/Create2Factory.ts | 8 ++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol index b579f4dc0..26e7e56a6 100644 --- a/contracts/core/Eip7702Support.sol +++ b/contracts/core/Eip7702Support.sol @@ -29,6 +29,7 @@ using UserOperationLib for PackedUserOperation; return false; } uint256 initCodeStart; + // solhint-disable-next-line no-inline-assembly assembly ("memory-safe") { initCodeStart := calldataload(initCode.offset) } @@ -44,6 +45,7 @@ using UserOperationLib for PackedUserOperation; function _getEip7702Delegate(address sender) view returns (address) { uint256 senderCode; + // solhint-disable-next-line no-inline-assembly assembly ("memory-safe") { extcodecopy(sender, 0, 0, 32) senderCode := mload(0) diff --git a/contracts/core/SenderCreator.sol b/contracts/core/SenderCreator.sol index df7e46d7c..f134468c5 100644 --- a/contracts/core/SenderCreator.sol +++ b/contracts/core/SenderCreator.sol @@ -54,7 +54,8 @@ contract SenderCreator is ISenderCreator { ) external { require(msg.sender == entryPoint, "AA97 should call from EntryPoint"); bytes memory initCallData = initCode[20 :]; + // solhint-disable-next-line avoid-low-level-calls bool success = Exec.call(sender, 0, initCallData, gasleft()); - require(success, "AA13: EIP7702 sender initialization failed"); + require(success, "AA13 EIP7702 sender init failed"); } } diff --git a/src/Create2Factory.ts b/src/Create2Factory.ts index 9477a6bf6..02bcc3965 100644 --- a/src/Create2Factory.ts +++ b/src/Create2Factory.ts @@ -102,11 +102,11 @@ export class Create2Factory { to: Create2Factory.factoryDeployer, value: BigNumber.from(Create2Factory.factoryDeploymentFee) }) - //first tx.. can't "wait" for it. + // first tx.. can't "wait" for it. await new Promise(resolve => setTimeout(resolve, 100)) - - await this.provider.sendTransaction(Create2Factory.factoryTx).then(tx => tx.wait()) - + + await this.provider.sendTransaction(Create2Factory.factoryTx).then(async tx => tx.wait()) + if (!await this._isFactoryDeployed()) { throw new Error('fatal: failed to deploy deterministic deployer') } From fafeca8c30abb6d9cb54f19223ebed65cbc0f4aa Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 23 Jan 2025 14:06:18 +0200 Subject: [PATCH 15/44] removed extracheck. --- contracts/core/EntryPoint.sol | 3 +-- reports/gas-checker.txt | 40 +++++++++++++++++------------------ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 49fe8bd1e..3b99b5916 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -444,8 +444,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT if (initCode.length != 0) { address sender = opInfo.mUserOp.sender; if ( _isEip7702InitCode(initCode) ) { - // validate it is an EIP7702 account - _getEip7702Delegate(sender); + //already validated it is an EIP-7702 delegate (and hence, already has code) senderCreator().initEip7702Sender(sender, initCode[20:]); return; } diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 70ab3a60e..bf5d7e26d 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 │ 77450 │ │ ║ +║ simple │ 1 │ 77803 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41598 │ 12339 ║ +║ simple - diff from previous │ 2 │ │ 41951 │ 12692 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 451980 │ │ ║ +║ simple │ 10 │ 455510 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 41659 │ 12400 ║ +║ simple - diff from previous │ 11 │ │ 42036 │ 12777 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83281 │ │ ║ +║ simple paymaster │ 1 │ 83634 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40142 │ 10883 ║ +║ simple paymaster with diff │ 2 │ │ 40495 │ 11236 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 444666 │ │ ║ +║ simple paymaster │ 10 │ 448172 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40163 │ 10904 ║ +║ simple paymaster with diff │ 11 │ │ 40564 │ 11305 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167209 │ │ ║ +║ big tx 5k │ 1 │ 167550 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 130799 │ 16097 ║ +║ big tx - diff from previous │ 2 │ │ 131176 │ 16474 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1344654 │ │ ║ +║ big tx 5k │ 10 │ 1348196 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 130872 │ 16170 ║ +║ big tx - diff from previous │ 11 │ │ 131189 │ 16487 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84458 │ │ ║ +║ paymaster+postOp │ 1 │ 84811 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41306 │ 12047 ║ +║ paymaster+postOp with diff │ 2 │ │ 41647 │ 12388 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 456393 │ │ ║ +║ paymaster+postOp │ 10 │ 459935 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41381 │ 12122 ║ +║ paymaster+postOp with diff │ 11 │ │ 41698 │ 12439 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 121572 │ │ ║ +║ token paymaster │ 1 │ 121925 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 61130 │ 31871 ║ +║ token paymaster with diff │ 2 │ │ 61483 │ 32224 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 671901 │ │ ║ +║ token paymaster │ 10 │ 675455 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61200 │ 31941 ║ +║ token paymaster with diff │ 11 │ │ 61589 │ 32330 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ From f98cd9571897aa22339e2ef6c023c4ee52051bd6 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 23 Jan 2025 15:04:09 +0200 Subject: [PATCH 16/44] gascalc --- reports/gas-checker.txt | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index bf5d7e26d..a745dc7d3 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -2,7 +2,7 @@ the destination is "account.entryPoint()", which is known to be "hot" address used by this account it little higher than EOA call: its an exec from entrypoint (or account owner) into account contract, verifying msg.sender and exec to target) ╔══════════════════════════╤════════╗ -║ gas estimate "simple" │ 29259 ║ +║ gas estimate "simple" │ 29247 ║ ╟──────────────────────────┼────────╢ ║ gas estimate "big tx 5k" │ 114702 ║ ╚══════════════════════════╧════════╝ @@ -12,44 +12,44 @@ ║ │ │ │ (delta for │ (compared to ║ ║ │ │ │ one UserOp) │ account.exec()) ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 1 │ 77803 │ │ ║ +║ simple │ 1 │ 77779 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41951 │ 12692 ║ +║ simple - diff from previous │ 2 │ │ 41951 │ 12704 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 455510 │ │ ║ +║ simple │ 10 │ 455462 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 42036 │ 12777 ║ +║ simple - diff from previous │ 11 │ │ 41976 │ 12729 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ simple paymaster │ 1 │ 83634 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40495 │ 11236 ║ +║ simple paymaster with diff │ 2 │ │ 40483 │ 11236 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 448172 │ │ ║ +║ simple paymaster │ 10 │ 448196 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40564 │ 11305 ║ +║ simple paymaster with diff │ 11 │ │ 40528 │ 11281 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167550 │ │ ║ +║ big tx 5k │ 1 │ 167526 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 131176 │ 16474 ║ +║ big tx - diff from previous │ 2 │ │ 131212 │ 16510 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1348196 │ │ ║ +║ big tx 5k │ 10 │ 1348136 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131189 │ 16487 ║ +║ big tx - diff from previous │ 11 │ │ 131225 │ 16523 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp │ 1 │ 84811 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41647 │ 12388 ║ +║ paymaster+postOp with diff │ 2 │ │ 41671 │ 12424 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 459935 │ │ ║ +║ paymaster+postOp │ 10 │ 459971 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41698 │ 12439 ║ +║ paymaster+postOp with diff │ 11 │ │ 41734 │ 12487 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ token paymaster │ 1 │ 121925 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 61483 │ 32224 ║ +║ token paymaster with diff │ 2 │ │ 61483 │ 32236 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 675455 │ │ ║ +║ token paymaster │ 10 │ 675467 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61589 │ 32330 ║ +║ token paymaster with diff │ 11 │ │ 61565 │ 32318 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ From fb06bc3a18bc2d27be1180b349ced5889c6217e1 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 29 Jan 2025 19:37:35 +0200 Subject: [PATCH 17/44] tests passes, including 7702-enabled external geth --- contracts/core/Eip7702Support.sol | 21 +- contracts/core/EntryPoint.sol | 10 +- contracts/core/SenderCreator.sol | 9 +- contracts/core/UserOperationLib.sol | 12 +- contracts/test/TestEip7702DelegateAccount.sol | 53 +++++ contracts/test/TestUtil.sol | 5 +- src/Create2Factory.ts | 5 +- test/GethExecutable.ts | 77 +++++++ test/UserOp.ts | 124 +++++++++--- test/eip7702helpers.ts | 70 +++++++ test/entrypoint.test.ts | 188 +++++++++++++++++- 11 files changed, 509 insertions(+), 65 deletions(-) create mode 100644 contracts/test/TestEip7702DelegateAccount.sol create mode 100644 test/GethExecutable.ts create mode 100644 test/eip7702helpers.ts diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol index 26e7e56a6..98cf14b16 100644 --- a/contracts/core/Eip7702Support.sol +++ b/contracts/core/Eip7702Support.sol @@ -7,16 +7,16 @@ import "../core/UserOperationLib.sol"; // EIP-7702 code prefix. Also, we use this prefix as a marker in the initCode. To specify this account is EIP-7702. uint256 constant EIP7702_PREFIX = 0xef0100; -using UserOperationLib for PackedUserOperation; + using UserOperationLib for PackedUserOperation; - //get alternate InitCode (just for hashing) when using EIP-7702 - function _getEip7702InitCodeOverride(PackedUserOperation calldata userOp) view returns (bytes32) { +//get alternate InitCodeHash (just for UserOp hash) when using EIP-7702 + function _getEip7702InitCodeHashOverride(PackedUserOperation calldata userOp) view returns (bytes32) { bytes calldata initCode = userOp.initCode; - if (! _isEip7702InitCode(initCode)) { + if (!_isEip7702InitCode(initCode)) { return 0; } address delegate = _getEip7702Delegate(userOp.getSender()); - if (initCode.length < 20) + if (initCode.length <= 20) return keccak256(abi.encodePacked(delegate)); else return keccak256(abi.encodePacked(delegate, initCode[20 :])); @@ -25,7 +25,7 @@ using UserOperationLib for PackedUserOperation; function _isEip7702InitCode(bytes calldata initCode) pure returns (bool) { - if (initCode.length < 3) { + if (initCode.length < 2) { return false; } uint256 initCodeStart; @@ -43,6 +43,7 @@ using UserOperationLib for PackedUserOperation; * requires EXTCODECOPY pr: https://github.com/ethereum/EIPs/pull/9248 (not yet merged or implemented) **/ function _getEip7702Delegate(address sender) view returns (address) { + uint256 senderCode; // solhint-disable-next-line no-inline-assembly @@ -53,6 +54,12 @@ using UserOperationLib for PackedUserOperation; // senderCode is the first 32 bytes of the sender's code // If it is an EIP-7702 delegate, then top 24 bits are the EIP7702_PREFIX // next 160 bytes are the delegate address - require(senderCode >> (256 - 24) == EIP7702_PREFIX, "not an EIP-7702 delegate"); + if (senderCode >> (256 - 24) != EIP7702_PREFIX) { + // instead of just "not an EIP-7702 delegate", if some info. + require(sender.code.length > 0, "sender has no code"); + //temp: sanity check for current EIP-7702 implementation. + require(sender.code.length == 23, "EIP-7702 delegate-length"); + revert("not an EIP-7702 delegate"); + } return address(uint160(senderCode >> (256 - 160 - 24))); } diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 3b99b5916..29adb6701 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -379,9 +379,9 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT function getUserOpHash( PackedUserOperation calldata userOp ) public view returns (bytes32) { - bytes32 overrideInitCode = _getEip7702InitCodeOverride(userOp); + bytes32 overrideInitCodeHash = _getEip7702InitCodeHashOverride(userOp); return - MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCode)); + MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCodeHash)); } /** @@ -444,8 +444,10 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT if (initCode.length != 0) { address sender = opInfo.mUserOp.sender; if ( _isEip7702InitCode(initCode) ) { - //already validated it is an EIP-7702 delegate (and hence, already has code) - senderCreator().initEip7702Sender(sender, initCode[20:]); + 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) diff --git a/contracts/core/SenderCreator.sol b/contracts/core/SenderCreator.sol index f134468c5..9a1ce9dd8 100644 --- a/contracts/core/SenderCreator.sol +++ b/contracts/core/SenderCreator.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.23; import "../interfaces/ISenderCreator.sol"; import "../utils/Exec.sol"; +import {IEntryPoint} from "../interfaces/IEntryPoint.sol"; /** * Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address, @@ -50,12 +51,14 @@ contract SenderCreator is ISenderCreator { // caller (EntryPoint) already verified it is an EIP-7702 account. function initEip7702Sender( address sender, - bytes calldata initCode + bytes calldata initCallData ) external { require(msg.sender == entryPoint, "AA97 should call from EntryPoint"); - bytes memory initCallData = initCode[20 :]; // solhint-disable-next-line avoid-low-level-calls bool success = Exec.call(sender, 0, initCallData, gasleft()); - require(success, "AA13 EIP7702 sender init failed"); + if (!success) { + bytes memory result = Exec.getReturnData(2048); + revert IEntryPoint.FailedOpWithRevert(0,"AA13 EIP7702 sender init failed", result); + } } } diff --git a/contracts/core/UserOperationLib.sol b/contracts/core/UserOperationLib.sol index 3aaa6893a..5d23510eb 100644 --- a/contracts/core/UserOperationLib.sol +++ b/contracts/core/UserOperationLib.sol @@ -65,15 +65,15 @@ library UserOperationLib { /** * Pack the user operation data into bytes for hashing. * @param userOp - The user operation data. - * @param overrideInitCode - If set, encode this instead of the initCode field in the userOp. + * @param overrideInitCodeHash - If set, encode this instead of the initCode field in the userOp. */ function encode( PackedUserOperation calldata userOp, - bytes32 overrideInitCode + bytes32 overrideInitCodeHash ) internal pure returns (bytes memory ret) { address sender = getSender(userOp); uint256 nonce = userOp.nonce; - bytes32 hashInitCode = overrideInitCode==0 ? calldataKeccak(userOp.initCode) : overrideInitCode; + bytes32 hashInitCode = overrideInitCodeHash != 0 ? overrideInitCodeHash : calldataKeccak(userOp.initCode); bytes32 hashCallData = calldataKeccak(userOp.callData); bytes32 accountGasLimits = userOp.accountGasLimits; uint256 preVerificationGas = userOp.preVerificationGas; @@ -148,12 +148,12 @@ library UserOperationLib { /** * Hash the user operation data. * @param userOp - The user operation data. - * @param overrideInitCode - If set, the initCode will be replaced with this value just for hashing. + * @param overrideInitCodeHash - If set, the initCode hash will be replaced with this value just for UserOp hashing. */ function hash( PackedUserOperation calldata userOp, - bytes32 overrideInitCode + bytes32 overrideInitCodeHash ) internal pure returns (bytes32) { - return keccak256(encode(userOp, overrideInitCode)); + return keccak256(encode(userOp, overrideInitCodeHash)); } } 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 3f06c1a32..dca95883d 100644 --- a/contracts/test/TestUtil.sol +++ b/contracts/test/TestUtil.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.23; import "../interfaces/PackedUserOperation.sol"; -import "../core/UserOperationLib.sol"; +import "../core/Eip7702Support.sol"; contract TestUtil { using UserOperationLib for PackedUserOperation; @@ -11,4 +11,7 @@ contract TestUtil { return op.encode(0); } + function _isEip7702InitCode(bytes calldata initCode) external pure returns (bool) { + return _isEip7702InitCode(initCode); + } } diff --git a/src/Create2Factory.ts b/src/Create2Factory.ts index 02bcc3965..75d423ed8 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) diff --git a/test/GethExecutable.ts b/test/GethExecutable.ts new file mode 100644 index 000000000..6a53729a9 --- /dev/null +++ b/test/GethExecutable.ts @@ -0,0 +1,77 @@ +import { spawn, ChildProcess } from 'child_process' +import Debug from 'debug' + +const debug = Debug('aa.geth') + +const port = 54321 +export const gethLauncher = { + name: 'geth', + exec: './scripts/geth.sh', + args: `--http --http.api personal,eth,net,web3,debug --rpc.allow-unprotected-txs --allow-insecure-unlock --dev --http.port=${port}` +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const anvilLauncher = { + name: 'anvil', + exec: './scripts/anvil.sh', + args: `--hardfork prague --port=${port}` +} + +export class GethExecutable { + constructor (private readonly impl = gethLauncher) { + } + + private gethProcess: ChildProcess | null = null + + markerString = /HTTP server started|Listening on/ + + rpcUrl (): string { + return `http://localhost:${port}` + } + + async init (): Promise { + return new Promise((resolve, reject) => { + console.log('spawning: ', this.impl.exec, this.impl.args) + this.gethProcess = spawn(this.impl.exec, this.impl.args.split(' ')) + + 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.name} process exited with code ${code}`) + }) + } else { + reject(new Error('Failed to start geth process')) + } + }) + } + + done (): void { + if (this.gethProcess != null) { + this.gethProcess.kill() + } + } +} diff --git a/test/UserOp.ts b/test/UserOp.ts index 8e544ffa8..c289208aa 100644 --- a/test/UserOp.ts +++ b/test/UserOp.ts @@ -33,7 +33,7 @@ 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 EIP7702_PREFIX = '0xef0100' +export const EIP7702_PREFIX = '0xef01' export function packUserOp (userOp: UserOperation): PackedUserOperation { const accountGasLimits = packAccountGasLimits(userOp.verificationGasLimit, userOp.callGasLimit) @@ -89,25 +89,31 @@ export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: n ])) } -// calculate UserOpHash, given "sender" contract code. -// (only used if initCode starts with prefix) -export function getUserOpHashWithEip7702 (op: UserOperation, entryPoint: string, chainId: number, senderCode: string): string { +export function isEip7702UserOp (op: UserOperation): boolean { + return op.initCode != null && hexlify(op.initCode).startsWith(EIP7702_PREFIX) +} + +export function updateUserOpForEip7702Hash (op: UserOperation, delegate: string): UserOperation { + if (!isEip7702UserOp(op)) { + throw new Error('initCode should start with EIP7702_PREFIX') + } let initCode = hexlify(op.initCode) - if (initCode.startsWith(EIP7702_PREFIX)) { - const delegate = hexDataSlice(senderCode, 3, 23) - if (hexDataLength(initCode) < 20) { - // its only prefix: - initCode = delegate - } else { - // replace address in initCode with delegate - initCode = hexConcat([delegate, hexDataSlice(initCode, 20)]) - } - op = { - ...op, - initCode: initCode - } + if (hexDataLength(initCode) < 20) { + initCode = delegate + } else { + // replace address in initCode with delegate + initCode = hexConcat([delegate, hexDataSlice(initCode, 20)]) } - return getUserOpHash(op, entryPoint, chainId) + 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 = { @@ -127,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)); @@ -154,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: @@ -166,10 +189,11 @@ 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) { + if (op1.initCode != null && !isEip7702UserOp(op1 as UserOperation)) { const initAddr = hexDataSlice(op1.initCode!, 0, 20) const initCallData = hexDataSlice(op1.initCode!, 20) if (op1.nonce == null) op1.nonce = 0 @@ -241,8 +265,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 { @@ -281,17 +305,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) + + return await typedSigner._signTypedData(getErc4337TypedDataDomain(entryPoint!.address, chainId), getErc4337TypedDataTypes(), packedUserOp) // .catch(e => e.toString()) +} - const signature = 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, @@ -299,8 +358,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..94d78412d --- /dev/null +++ b/test/eip7702helpers.ts @@ -0,0 +1,70 @@ +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 EIP7702Authorization { + chainId: BigNumberish + address: string + nonce: BigNumberish + 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: Partial): 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 function signEip7702Authorization (signer: Wallet, authorization: Partial, chainId?: number): EIP7702Authorization { + const dataToSign = toBuffer(eip7702DataToSign(authorization)) + const sig = ecsign(dataToSign, arrayify(signer.privateKey) as any) + return { + address: authorization.address!, + chainId: gethHex(authorization.chainId!), + nonce: gethHex(authorization.nonce!), + yParity: gethHex(sig.v - 27), + r: gethHex(sig.r), + s: gethHex(sig.s) + } +} diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index 266c695f6..5e78ebc3b 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -16,6 +16,8 @@ import { TestAggregatedAccount__factory, TestCounter, TestCounter__factory, + TestEip7702DelegateAccount, + TestEip7702DelegateAccount__factory, TestExpirePaymaster, TestExpirePaymaster__factory, TestExpiryAccount, @@ -28,6 +30,8 @@ import { TestRevertAccount__factory, TestSignatureAggregator, TestSignatureAggregator__factory, + TestUtil, + TestUtil__factory, TestWarmColdAccount__factory } from '../typechain' import { @@ -69,6 +73,9 @@ import { toChecksumAddress } from 'ethereumjs-util' import { getERC165InterfaceID } from '../src/Utils' import { UserOperationEventEvent } from '../typechain/contracts/interfaces/IEntryPoint' import { before } from 'mocha' +import { GethExecutable } from './GethExecutable' +import { JsonRpcProvider } from '@ethersproject/providers' +import { getEip7702AuthorizationSigner, gethHex, signEip7702Authorization } from './eip7702helpers' describe('EntryPoint', function () { let entryPoint: EntryPoint @@ -117,12 +124,63 @@ describe('EntryPoint', function () { const mockDelegate = createAddress() - const deployedDelegateCode = hexConcat([EIP7702_PREFIX, mockDelegate]) + const deployedDelegateCode = hexConcat(['0xef0100', mockDelegate]) before(async () => { chainId = await ethers.provider.getNetwork().then(net => net.chainId) }) + 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(EIP7702_PREFIX + '00'.repeat(pad))).to.be.true + }) + ) + + it('should accept initCode with just prefix', async () => { + expect(await testUtil._isEip7702InitCode(EIP7702_PREFIX)).to.be.true + }) + + it('should not accept EIP7702 if first 20 bytes contain non-zero', async () => { + const addr = EIP7702_PREFIX + '0'.repeat(40 - EIP7702_PREFIX.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', () => { + // deliberately remove previous signature... + const authToSign = { address: createAddress(), nonce: 12345, chainId: '0x0' } + const signed = 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))) }) @@ -130,11 +188,17 @@ describe('EntryPoint', function () { describe('#getUserOpHashWith7702', () => { it('#getUserOpHashWith7702 just delegate', async () => { const hash = getUserOpHash({ ...userop, initCode: mockDelegate }, entryPoint.address, chainId) - expect(getUserOpHashWithEip7702({ ...userop, initCode: EIP7702_PREFIX }, entryPoint.address, chainId, deployedDelegateCode)).to.eql(hash) + expect(getUserOpHashWithEip7702({ + ...userop, + initCode: EIP7702_PREFIX + }, 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: '0xef0100'.padEnd(42, '0') + 'b1ab1a' }, entryPoint.address, chainId, deployedDelegateCode)).to.eql(hash) + expect(getUserOpHashWithEip7702({ + ...userop, + initCode: '0xef0100'.padEnd(42, '0') + 'b1ab1a' + }, entryPoint.address, chainId, mockDelegate)).to.eql(hash) }) }) @@ -142,22 +206,117 @@ describe('EntryPoint', function () { it('should return the same hash as calculated locally', async () => { const op1 = { ...userop, initCode: EIP7702_PREFIX } expect(await callGetUserOpHashWithCode(entryPoint, op1, deployedDelegateCode)).to.eql( - getUserOpHashWithEip7702(op1, entryPoint.address, chainId, deployedDelegateCode)) + getUserOpHashWithEip7702(op1, entryPoint.address, chainId, mockDelegate)) }) it('should fail getUserOpHash marked for eip-7702, without a delegate', async () => { const op1 = { ...userop, initCode: EIP7702_PREFIX } - await expect(callGetUserOpHashWithCode(entryPoint, op1, '0x608000')).to.revertedWith('not an EIP-7702 delegate') + 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 EIP7702_PREFIX tailed with zeros only, ', async () => { const op_zero_tail = { ...userop, initCode: EIP7702_PREFIX + '00'.repeat(10) } expect(await callGetUserOpHashWithCode(entryPoint, op_zero_tail, deployedDelegateCode)).to.eql( - getUserOpHashWithEip7702(op_zero_tail, entryPoint.address, chainId, deployedDelegateCode)) + getUserOpHashWithEip7702(op_zero_tail, entryPoint.address, chainId, mockDelegate)) op_zero_tail.initCode = EIP7702_PREFIX + '00'.repeat(30) expect(await callGetUserOpHashWithCode(entryPoint, op_zero_tail, deployedDelegateCode)).to.eql( - getUserOpHashWithEip7702(op_zero_tail, entryPoint.address, chainId, deployedDelegateCode)) + getUserOpHashWithEip7702(op_zero_tail, entryPoint.address, chainId, mockDelegate)) + }) + + describe('test with geth', () => { + let geth: GethExecutable + let prov: JsonRpcProvider + let delegate: TestEip7702DelegateAccount + let gethFrom: string + const beneficiary = createAddress() + const eoa = createAccountOwner() + let entryPoint: EntryPoint + + before(async () => { + this.timeout(20000) + + geth = new GethExecutable() + await geth.init() + prov = new JsonRpcProvider(geth.rpcUrl()) + entryPoint = await deployEntryPoint(prov) + delegate = await new TestEip7702DelegateAccount__factory(prov.getSigner()).deploy(entryPoint.address) + console.log('delegate addr=', delegate.address, 'len=', await prov.getCode(delegate.address).then(code => code.length)) + gethFrom = (await prov.send('eth_accounts', []))[0] + await prov.send('eth_sendTransaction', [{ from: gethFrom, to: eoa.address, value: gethHex(parseEther('1')) }]) + }) + + it('should fail without sender delegate', async () => { + const eip7702userOp = await fillSignAndPack({ + sender: eoa.address, + nonce: 0, + initCode: EIP7702_PREFIX // not init function, just delegate + }, eoa, entryPoint, { eip7702delegate: delegate.address }) + const handleOpCall = { + from: gethFrom, + to: entryPoint.address, + data: entryPoint.interface.encodeFunctionData('handleOps', [[eip7702userOp], beneficiary]), + gasLimit: 1000000 + // authorizationList: [eip7702tuple] + } + expect(await prov.send('eth_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: EIP7702_PREFIX // not init function, just delegate + }, eoa, entryPoint, { eip7702delegate: delegate.address }) + const eip7702tuple = signEip7702Authorization(eoa, { + address: delegate.address, + nonce: await prov.getTransactionCount(eoa.address), + chainId: await prov.getNetwork().then(net => net.chainId) + }) + + const handleOpCall = { + from: gethFrom, + to: entryPoint.address, + data: entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(eip7702userOp)], beneficiary]), + gasLimit: 1000000, + authorizationList: [eip7702tuple] + } + + await prov.send('eth_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([EIP7702_PREFIX + '0'.repeat(42 - EIP7702_PREFIX.length), delegate.interface.encodeFunctionData('testInit')]) + }, eoa, entryPoint, { eip7702delegate: delegate.address }) + + const eip7702tuple = signEip7702Authorization(eoa, { + address: delegate.address, + nonce: await prov.getTransactionCount(eoa.address), + chainId: await prov.getNetwork().then(net => net.chainId) + }) + const handleOpCall = { + from: gethFrom, + to: entryPoint.address, + data: entryPoint.interface.encodeFunctionData('handleOps', [[eip7702userOp], beneficiary]), + gasLimit: 1000000, + authorizationList: [eip7702tuple] + } + await prov.send('eth_call', [handleOpCall]).catch(e => { + throw Error(decodeRevertReason(e)!) + }) + }) + + after(async () => { + geth.done() + }) }) }) }) @@ -605,7 +764,9 @@ describe('EntryPoint', function () { const beneficiaryBalance = await ethers.provider.getBalance(beneficiary) 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', @@ -726,8 +887,14 @@ describe('EntryPoint', function () { const beneficiaryAddress = createAddress() // "warmup" userop, for better gas calculation, below - await entryPoint.handleOps([await fillSignAndPack({ sender: account.address, callData: accountExec.data }, accountOwner, entryPoint)], beneficiaryAddress) - await entryPoint.handleOps([await fillSignAndPack({ sender: account.address, callData: accountExec.data }, accountOwner, entryPoint)], beneficiaryAddress) + await entryPoint.handleOps([await fillSignAndPack({ + sender: account.address, + callData: accountExec.data + }, accountOwner, entryPoint)], beneficiaryAddress) + await entryPoint.handleOps([await fillSignAndPack({ + sender: account.address, + callData: accountExec.data + }, accountOwner, entryPoint)], beneficiaryAddress) const op1 = await fillSignAndPack({ sender: account.address, @@ -1378,7 +1545,6 @@ describe('EntryPoint', function () { sender: account.address }, expiredOwner, entryPoint) const ret = await simulateValidation(userOp, entryPoint.address) - console.log(ret.returnInfo.accountValidationData.toHexString()) const validationData = parseValidationData(ret.returnInfo.accountValidationData) console.log('validationdata=', validationData) expect(validationData.validUntil).eql(now - 60) From 69499d018076f56a0e07abdb63ca51ac0690702f Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 29 Jan 2025 20:07:43 +0200 Subject: [PATCH 18/44] gaschecks, geth docker. --- reports/gas-checker.txt | 42 ++++++++++++++++++++--------------------- scripts/geth.sh | 2 ++ 2 files changed, 23 insertions(+), 21 deletions(-) create mode 100755 scripts/geth.sh diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index a745dc7d3..c92b7b46c 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -2,7 +2,7 @@ the destination is "account.entryPoint()", which is known to be "hot" address used by this account it little higher than EOA call: its an exec from entrypoint (or account owner) into account contract, verifying msg.sender and exec to target) ╔══════════════════════════╤════════╗ -║ gas estimate "simple" │ 29247 ║ +║ gas estimate "simple" │ 29259 ║ ╟──────────────────────────┼────────╢ ║ gas estimate "big tx 5k" │ 114702 ║ ╚══════════════════════════╧════════╝ @@ -12,44 +12,44 @@ ║ │ │ │ (delta for │ (compared to ║ ║ │ │ │ one UserOp) │ account.exec()) ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 1 │ 77779 │ │ ║ +║ simple │ 1 │ 77818 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41951 │ 12704 ║ +║ simple - diff from previous │ 2 │ │ 41966 │ 12707 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 455462 │ │ ║ +║ simple │ 10 │ 455636 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 41976 │ 12729 ║ +║ simple - diff from previous │ 11 │ │ 42039 │ 12780 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83634 │ │ ║ +║ simple paymaster │ 1 │ 83649 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40483 │ 11236 ║ +║ simple paymaster with diff │ 2 │ │ 40498 │ 11239 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 448196 │ │ ║ +║ simple paymaster │ 10 │ 448346 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40528 │ 11281 ║ +║ simple paymaster with diff │ 11 │ │ 40555 │ 11296 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167526 │ │ ║ +║ big tx 5k │ 1 │ 167577 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 131212 │ 16510 ║ +║ big tx - diff from previous │ 2 │ │ 131191 │ 16489 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1348136 │ │ ║ +║ big tx 5k │ 10 │ 1348334 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131225 │ 16523 ║ +║ big tx - diff from previous │ 11 │ │ 131252 │ 16550 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84811 │ │ ║ +║ paymaster+postOp │ 1 │ 84826 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41671 │ 12424 ║ +║ paymaster+postOp with diff │ 2 │ │ 41674 │ 12415 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 459971 │ │ ║ +║ paymaster+postOp │ 10 │ 460073 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41734 │ 12487 ║ +║ paymaster+postOp with diff │ 11 │ │ 41749 │ 12490 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 1 │ 121925 │ │ ║ +║ token paymaster │ 1 │ 121940 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 2 │ │ 61483 │ 32236 ║ +║ token paymaster with diff │ 2 │ │ 61486 │ 32227 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster │ 10 │ 675467 │ │ ║ +║ token paymaster │ 10 │ 675629 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ token paymaster with diff │ 11 │ │ 61565 │ 32318 ║ +║ token paymaster with diff │ 11 │ │ 61544 │ 32285 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/scripts/geth.sh b/scripts/geth.sh new file mode 100755 index 000000000..28f76ebbc --- /dev/null +++ b/scripts/geth.sh @@ -0,0 +1,2 @@ +#!/bin/sh +docker run --rm -ti -p 54321:54321 dtr22/geth-7702-23 From bac31137f0810e17d7b1dec4f3b63f35c466bca7 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 29 Jan 2025 20:11:45 +0200 Subject: [PATCH 19/44] geth docker. --- scripts/geth.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/geth.sh b/scripts/geth.sh index 28f76ebbc..b951d5b34 100755 --- a/scripts/geth.sh +++ b/scripts/geth.sh @@ -1,2 +1,2 @@ #!/bin/sh -docker run --rm -ti -p 54321:54321 dtr22/geth-7702-23 +docker run --rm -p 54321:54321 dtr22/geth-7702-23 From e54c2a7501aa680385c7ddb1b626a43c2599c5d4 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 29 Jan 2025 20:46:44 +0200 Subject: [PATCH 20/44] fix geth script, coverage test --- contracts/core/EntryPointSimulations.sol | 2 +- scripts/geth.sh | 3 ++- test/GethExecutable.ts | 2 +- test/entrypoint.test.ts | 4 ++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/core/EntryPointSimulations.sol b/contracts/core/EntryPointSimulations.sol index 27a9fc538..2182fb54b 100644 --- a/contracts/core/EntryPointSimulations.sol +++ b/contracts/core/EntryPointSimulations.sol @@ -192,7 +192,7 @@ contract EntryPointSimulations is EntryPoint, IEntryPointSimulations { //slightly stricter gas limit than the real EntryPoint function _getVerificationGasLimit(uint256 verificationGasLimit) internal pure virtual override returns (uint256) { - return verificationGasLimit - 350; + return verificationGasLimit - 300; } diff --git a/scripts/geth.sh b/scripts/geth.sh index b951d5b34..22ad1e6f7 100755 --- a/scripts/geth.sh +++ b/scripts/geth.sh @@ -1,2 +1,3 @@ #!/bin/sh -docker run --rm -p 54321:54321 dtr22/geth-7702-23 +trap "echo killing docker; docker kill geth-7702-23" EXIT +docker run --name geth-7702-23 --rm -p 8545:8545 -p 54321:54321 dtr22/geth-7702-23 $* diff --git a/test/GethExecutable.ts b/test/GethExecutable.ts index 6a53729a9..053ad76fa 100644 --- a/test/GethExecutable.ts +++ b/test/GethExecutable.ts @@ -7,7 +7,7 @@ const port = 54321 export const gethLauncher = { name: 'geth', exec: './scripts/geth.sh', - args: `--http --http.api personal,eth,net,web3,debug --rpc.allow-unprotected-txs --allow-insecure-unlock --dev --http.port=${port}` + args: `--http --http.api personal,eth,net,web3,debug --rpc.allow-unprotected-txs --allow-insecure-unlock --dev --http.addr 0.0.0.0 --http.port=${port}` } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/test/entrypoint.test.ts b/test/entrypoint.test.ts index 5e78ebc3b..f9a35edc3 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -225,6 +225,10 @@ describe('EntryPoint', function () { }) describe('test with geth', () => { + if (process.env.COVERAGE != null) { + return + } + let geth: GethExecutable let prov: JsonRpcProvider let delegate: TestEip7702DelegateAccount From 146dbbe34336c2237211c3aedb7688ae5c16b9d7 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 29 Jan 2025 21:04:15 +0200 Subject: [PATCH 21/44] separate entrypoint-7702 tests into a separate test file --- test/entrypoint-7702.test.ts | 253 +++++++++++++++++++++++++++++++++ test/entrypoint.test.ts | 262 +++-------------------------------- 2 files changed, 270 insertions(+), 245 deletions(-) create mode 100644 test/entrypoint-7702.test.ts diff --git a/test/entrypoint-7702.test.ts b/test/entrypoint-7702.test.ts new file mode 100644 index 000000000..20261267d --- /dev/null +++ b/test/entrypoint-7702.test.ts @@ -0,0 +1,253 @@ +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 { + EIP7702_PREFIX, + 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 { JsonRpcProvider } from '@ethersproject/providers' +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 () => { + 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(EIP7702_PREFIX + '00'.repeat(pad))).to.be.true + }) + ) + + it('should accept initCode with just prefix', async () => { + expect(await testUtil._isEip7702InitCode(EIP7702_PREFIX)).to.be.true + }) + + it('should not accept EIP7702 if first 20 bytes contain non-zero', async () => { + const addr = EIP7702_PREFIX + '0'.repeat(40 - EIP7702_PREFIX.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', () => { + // deliberately remove previous signature... + const authToSign = { address: createAddress(), nonce: 12345, chainId: '0x0' } + const signed = 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: EIP7702_PREFIX + }, 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: '0xef0100'.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: EIP7702_PREFIX } + 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: EIP7702_PREFIX } + 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 EIP7702_PREFIX tailed with zeros only, ', async () => { + const op_zero_tail = { ...userop, initCode: EIP7702_PREFIX + '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 = EIP7702_PREFIX + '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', () => { + if (process.env.COVERAGE != null) { + return + } + + let geth: GethExecutable + let prov: JsonRpcProvider + let delegate: TestEip7702DelegateAccount + let gethFrom: string + const beneficiary = createAddress() + const eoa = createAccountOwner() + let entryPoint: EntryPoint + + before(async () => { + this.timeout(20000) + + geth = new GethExecutable() + await geth.init() + prov = new JsonRpcProvider(geth.rpcUrl()) + entryPoint = await deployEntryPoint(prov) + delegate = await new TestEip7702DelegateAccount__factory(prov.getSigner()).deploy(entryPoint.address) + console.log('delegate addr=', delegate.address, 'len=', await prov.getCode(delegate.address).then(code => code.length)) + gethFrom = (await prov.send('eth_accounts', []))[0] + await prov.send('eth_sendTransaction', [{ from: gethFrom, to: eoa.address, value: gethHex(parseEther('1')) }]) + }) + + it('should fail without sender delegate', async () => { + const eip7702userOp = await fillSignAndPack({ + sender: eoa.address, + nonce: 0, + initCode: EIP7702_PREFIX // not init function, just delegate + }, eoa, entryPoint, { eip7702delegate: delegate.address }) + const handleOpCall = { + from: gethFrom, + to: entryPoint.address, + data: entryPoint.interface.encodeFunctionData('handleOps', [[eip7702userOp], beneficiary]), + gasLimit: 1000000 + // authorizationList: [eip7702tuple] + } + expect(await prov.send('eth_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: EIP7702_PREFIX // not init function, just delegate + }, eoa, entryPoint, { eip7702delegate: delegate.address }) + const eip7702tuple = signEip7702Authorization(eoa, { + address: delegate.address, + nonce: await prov.getTransactionCount(eoa.address), + chainId: await prov.getNetwork().then(net => net.chainId) + }) + + const handleOpCall = { + from: gethFrom, + to: entryPoint.address, + data: entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(eip7702userOp)], beneficiary]), + gasLimit: 1000000, + authorizationList: [eip7702tuple] + } + + await prov.send('eth_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([EIP7702_PREFIX + '0'.repeat(42 - EIP7702_PREFIX.length), delegate.interface.encodeFunctionData('testInit')]) + }, eoa, entryPoint, { eip7702delegate: delegate.address }) + + const eip7702tuple = signEip7702Authorization(eoa, { + address: delegate.address, + nonce: await prov.getTransactionCount(eoa.address), + chainId: await prov.getNetwork().then(net => net.chainId) + }) + const handleOpCall = { + from: gethFrom, + to: entryPoint.address, + data: entryPoint.interface.encodeFunctionData('handleOps', [[eip7702userOp], beneficiary]), + gasLimit: 1000000, + authorizationList: [eip7702tuple] + } + await prov.send('eth_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 f9a35edc3..7e37607ef 100644 --- a/test/entrypoint.test.ts +++ b/test/entrypoint.test.ts @@ -12,12 +12,10 @@ import { SimpleAccountFactory, SimpleAccountFactory__factory, TestAggregatedAccount, - TestAggregatedAccountFactory__factory, TestAggregatedAccount__factory, + TestAggregatedAccountFactory__factory, TestCounter, TestCounter__factory, - TestEip7702DelegateAccount, - TestEip7702DelegateAccount__factory, TestExpirePaymaster, TestExpirePaymaster__factory, TestExpiryAccount, @@ -30,52 +28,41 @@ import { TestRevertAccount__factory, TestSignatureAggregator, TestSignatureAggregator__factory, - TestUtil, - TestUtil__factory, TestWarmColdAccount__factory } from '../typechain' import { AddressZero, + calcGasUsage, + checkForGeth, + createAccount, createAccountOwner, + createAddress, + decodeRevertReason, + deployEntryPoint, + findUserOpWithMin, fund, - checkForGeth, - rethrow, - tostr, + getAccountAddress, getAccountInitCode, - calcGasUsage, - ONE_ETH, - TWO_ETH, - deployEntryPoint, + getAggregatedAccountInitCode, getBalance, - createAddress, - getAccountAddress, HashZero, - createAccount, - getAggregatedAccountInitCode, - decodeRevertReason, parseValidationData, findUserOpWithMin, callGetUserOpHashWithCode + ONE_ETH, + parseValidationData, + rethrow, + tostr, + TWO_ETH } from './testutils' -import { - - DefaultsForUserOp, EIP7702_PREFIX, - fillAndSign, - fillSignAndPack, fillUserOpDefaults, - getUserOpHash, getUserOpHashWithEip7702, - packUserOp, - simulateValidation -} from './UserOp' +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, hexConcat, hexZeroPad, parseEther } from 'ethers/lib/utils' +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 { GethExecutable } from './GethExecutable' -import { JsonRpcProvider } from '@ethersproject/providers' -import { getEip7702AuthorizationSigner, gethHex, signEip7702Authorization } from './eip7702helpers' describe('EntryPoint', function () { let entryPoint: EntryPoint @@ -110,221 +97,6 @@ describe('EntryPoint', function () { expect(getUserOpHash(sampleOp, entryPoint.address, chainId)).to.eql(await entryPoint.getUserOpHash(packedOp)) }) - // 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 - - const mockDelegate = createAddress() - - const deployedDelegateCode = hexConcat(['0xef0100', mockDelegate]) - - before(async () => { - chainId = await ethers.provider.getNetwork().then(net => net.chainId) - }) - - 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(EIP7702_PREFIX + '00'.repeat(pad))).to.be.true - }) - ) - - it('should accept initCode with just prefix', async () => { - expect(await testUtil._isEip7702InitCode(EIP7702_PREFIX)).to.be.true - }) - - it('should not accept EIP7702 if first 20 bytes contain non-zero', async () => { - const addr = EIP7702_PREFIX + '0'.repeat(40 - EIP7702_PREFIX.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', () => { - // deliberately remove previous signature... - const authToSign = { address: createAddress(), nonce: 12345, chainId: '0x0' } - const signed = 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: EIP7702_PREFIX - }, 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: '0xef0100'.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: EIP7702_PREFIX } - 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: EIP7702_PREFIX } - 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 EIP7702_PREFIX tailed with zeros only, ', async () => { - const op_zero_tail = { ...userop, initCode: EIP7702_PREFIX + '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 = EIP7702_PREFIX + '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', () => { - if (process.env.COVERAGE != null) { - return - } - - let geth: GethExecutable - let prov: JsonRpcProvider - let delegate: TestEip7702DelegateAccount - let gethFrom: string - const beneficiary = createAddress() - const eoa = createAccountOwner() - let entryPoint: EntryPoint - - before(async () => { - this.timeout(20000) - - geth = new GethExecutable() - await geth.init() - prov = new JsonRpcProvider(geth.rpcUrl()) - entryPoint = await deployEntryPoint(prov) - delegate = await new TestEip7702DelegateAccount__factory(prov.getSigner()).deploy(entryPoint.address) - console.log('delegate addr=', delegate.address, 'len=', await prov.getCode(delegate.address).then(code => code.length)) - gethFrom = (await prov.send('eth_accounts', []))[0] - await prov.send('eth_sendTransaction', [{ from: gethFrom, to: eoa.address, value: gethHex(parseEther('1')) }]) - }) - - it('should fail without sender delegate', async () => { - const eip7702userOp = await fillSignAndPack({ - sender: eoa.address, - nonce: 0, - initCode: EIP7702_PREFIX // not init function, just delegate - }, eoa, entryPoint, { eip7702delegate: delegate.address }) - const handleOpCall = { - from: gethFrom, - to: entryPoint.address, - data: entryPoint.interface.encodeFunctionData('handleOps', [[eip7702userOp], beneficiary]), - gasLimit: 1000000 - // authorizationList: [eip7702tuple] - } - expect(await prov.send('eth_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: EIP7702_PREFIX // not init function, just delegate - }, eoa, entryPoint, { eip7702delegate: delegate.address }) - const eip7702tuple = signEip7702Authorization(eoa, { - address: delegate.address, - nonce: await prov.getTransactionCount(eoa.address), - chainId: await prov.getNetwork().then(net => net.chainId) - }) - - const handleOpCall = { - from: gethFrom, - to: entryPoint.address, - data: entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(eip7702userOp)], beneficiary]), - gasLimit: 1000000, - authorizationList: [eip7702tuple] - } - - await prov.send('eth_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([EIP7702_PREFIX + '0'.repeat(42 - EIP7702_PREFIX.length), delegate.interface.encodeFunctionData('testInit')]) - }, eoa, entryPoint, { eip7702delegate: delegate.address }) - - const eip7702tuple = signEip7702Authorization(eoa, { - address: delegate.address, - nonce: await prov.getTransactionCount(eoa.address), - chainId: await prov.getNetwork().then(net => net.chainId) - }) - const handleOpCall = { - from: gethFrom, - to: entryPoint.address, - data: entryPoint.interface.encodeFunctionData('handleOps', [[eip7702userOp], beneficiary]), - gasLimit: 1000000, - authorizationList: [eip7702tuple] - } - await prov.send('eth_call', [handleOpCall]).catch(e => { - throw Error(decodeRevertReason(e)!) - }) - }) - - after(async () => { - geth.done() - }) - }) - }) - }) - describe('Stake Management', () => { let addr: string before(async () => { From ee54899553cb03a98ac40f7594e8ab417189b91f Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 30 Jan 2025 10:30:10 +0200 Subject: [PATCH 22/44] test timeout --- test/entrypoint-7702.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/entrypoint-7702.test.ts b/test/entrypoint-7702.test.ts index 20261267d..7018f8d53 100644 --- a/test/entrypoint-7702.test.ts +++ b/test/entrypoint-7702.test.ts @@ -51,7 +51,8 @@ describe('EntryPoint EIP-7702 tests', function () { const deployedDelegateCode = hexConcat(['0xef0100', mockDelegate]) - before(async () => { + before(async function() { + this.timeout(20000) chainId = await ethers.provider.getNetwork().then(net => net.chainId) entryPoint = await deployEntryPoint() }) From 33b294a849266a0a8808f4318e19d562ddf8cb6b Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Tue, 4 Feb 2025 15:45:00 +0200 Subject: [PATCH 23/44] lints --- contracts/test/TestUtil.sol | 2 +- test/entrypoint-7702.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/test/TestUtil.sol b/contracts/test/TestUtil.sol index dca95883d..6e99bcc3a 100644 --- a/contracts/test/TestUtil.sol +++ b/contracts/test/TestUtil.sol @@ -11,7 +11,7 @@ contract TestUtil { return op.encode(0); } - function _isEip7702InitCode(bytes calldata initCode) external pure returns (bool) { + function isEip7702InitCode(bytes calldata initCode) external pure returns (bool) { return _isEip7702InitCode(initCode); } } diff --git a/test/entrypoint-7702.test.ts b/test/entrypoint-7702.test.ts index 7018f8d53..4195c54fa 100644 --- a/test/entrypoint-7702.test.ts +++ b/test/entrypoint-7702.test.ts @@ -51,7 +51,7 @@ describe('EntryPoint EIP-7702 tests', function () { const deployedDelegateCode = hexConcat(['0xef0100', mockDelegate]) - before(async function() { + before(async function () { this.timeout(20000) chainId = await ethers.provider.getNetwork().then(net => net.chainId) entryPoint = await deployEntryPoint() @@ -65,18 +65,18 @@ describe('EntryPoint EIP-7702 tests', function () { [1, 10, 20, 30].forEach(pad => it(`should accept initCode with zero pad ${pad}`, async () => { - expect(await testUtil._isEip7702InitCode(EIP7702_PREFIX + '00'.repeat(pad))).to.be.true + expect(await testUtil.isEip7702InitCode(EIP7702_PREFIX + '00'.repeat(pad))).to.be.true }) ) it('should accept initCode with just prefix', async () => { - expect(await testUtil._isEip7702InitCode(EIP7702_PREFIX)).to.be.true + expect(await testUtil.isEip7702InitCode(EIP7702_PREFIX)).to.be.true }) it('should not accept EIP7702 if first 20 bytes contain non-zero', async () => { const addr = EIP7702_PREFIX + '0'.repeat(40 - EIP7702_PREFIX.length) + '01' expect(addr.length).to.eql(42) - expect(await testUtil._isEip7702InitCode(addr)).to.be.false + expect(await testUtil.isEip7702InitCode(addr)).to.be.false }) }) From 8dafd94c869fe6407e60031429c3a6e20fc2fa84 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 6 Feb 2025 20:17:36 +0200 Subject: [PATCH 24/44] account --- contracts/samples/EIP7702Account.sol | 90 ++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 contracts/samples/EIP7702Account.sol diff --git a/contracts/samples/EIP7702Account.sol b/contracts/samples/EIP7702Account.sol new file mode 100644 index 000000000..01a6a154f --- /dev/null +++ b/contracts/samples/EIP7702Account.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: MIT +// based on: https://gist.github.com/frangio/e40305b9f99de290b73750dff5ebe50a +pragma solidity ^0.8; + +import "../interfaces/PackedUserOperation.sol"; +import "../core/Helpers.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../core/BaseAccount.sol"; + +/** + * EIP7702Account + * A minimal account to be used with EIP-7702 (for batching) and ERC-4337 (for gas sponsoring) + */ +contract EIP7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721Holder { + + // temporary address of entryPoint v0.8 + function entryPoint() public pure override returns (IEntryPoint) { + return IEntryPoint(0xe6562c192D185da05097BFFfD5ee12C878A21c33); + } + + /** + * Make this account callable through ERC-4337 EntryPoint. + * The UserOperation should be signed by this account's private key. + */ + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256 validationData) { + + if (address(this) != ECDSA.recover(userOpHash, userOp.signature)) { + return SIG_VALIDATION_FAILED; + } + return 0; + } + + function _requireFromSelfOrEntryPoint() internal view virtual { + require( + msg.sender == address(this) || + msg.sender == address(entryPoint()), + "not from self or EntryPoint" + ); + } + + struct Call { + address target; + uint256 value; + bytes data; + } + + function execute(Call[] calldata calls) external { + _requireFromSelfOrEntryPoint(); + + for (uint256 i = 0; i < calls.length; i++) { + Call calldata call = calls[i]; +// if (!Exec.call(gasleft(), call.target, call.value, call.data)) { +// assembly { +// returndatacopy(0, 0, returndatasize()) +// revert(0, returndatasize()) +// } +// } + (bool ok, bytes memory ret) = call.target.call{value: call.value}(call.data); + if (!ok) { + // solhint-disable-next-line no-inline-assembly + assembly { revert(add(ret, 32), mload(ret)) } + } + } + } + + function supportsInterface(bytes4 id) public override(ERC1155Holder, IERC165) pure returns (bool) { + return + id == type(IERC165).interfaceId || + id == type(IAccount).interfaceId || + id == type(IERC1271).interfaceId || + id == type(IERC1155Receiver).interfaceId || + id == type(IERC721Receiver).interfaceId; + } + + //deliberately return the same signature as returned by the EOA itself: This way, + // ERC-1271 can be used regardless if the account currently has this code or not. + function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) { + return ECDSA.recover(hash, signature) == address(this) ? this.isValidSignature.selector : bytes4(0); + } + + receive() external payable { + } +} From c026b2e19ecdc50d8f35038a619367d9047ecd51 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sun, 9 Feb 2025 14:12:08 +0200 Subject: [PATCH 25/44] added wallet tests --- scripts/docker-gascalc.yml | 3 +- scripts/geth.sh | 5 +- test/GethExecutable.ts | 53 +++++++++++++++++ test/eip7702-wallet.test.ts | 109 +++++++++++++++++++++++++++++++++++ test/eip7702helpers.ts | 9 ++- test/entrypoint-7702.test.ts | 30 ++++------ test/testutils.ts | 6 +- 7 files changed, 187 insertions(+), 28 deletions(-) create mode 100644 test/eip7702-wallet.test.ts 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 index 22ad1e6f7..a6f2a6adf 100755 --- a/scripts/geth.sh +++ b/scripts/geth.sh @@ -1,3 +1,4 @@ #!/bin/sh -trap "echo killing docker; docker kill geth-7702-23" EXIT -docker run --name geth-7702-23 --rm -p 8545:8545 -p 54321:54321 dtr22/geth-7702-23 $* +name=geth-$$ +trap "echo killing docker; docker kill $name 2> /dev/null" EXIT +docker run --name $name --rm -p 8545:8545 -p 54321:54321 dtr22/geth7702 $* diff --git a/test/GethExecutable.ts b/test/GethExecutable.ts index 053ad76fa..b11f85fe5 100644 --- a/test/GethExecutable.ts +++ b/test/GethExecutable.ts @@ -1,5 +1,8 @@ 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' const debug = Debug('aa.geth') @@ -17,7 +20,18 @@ export const anvilLauncher = { args: `--hardfork prague --port=${port}` } +interface Eip7702Transaction { + to: string + data?: string + value?: BigNumberish + gas?: BigNumberish + authorizationList?: any +} + export class GethExecutable { + gethFrom: string + provider: JsonRpcProvider + constructor (private readonly impl = gethLauncher) { } @@ -30,6 +44,44 @@ export class GethExecutable { } 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') + } + } + await this.provider.send('eth_sendTransaction', [tx1]).catch(e => { + // console.log(e) + throw new Error(e.error.message) + }) + } + + // 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.exec, this.impl.args) this.gethProcess = spawn(this.impl.exec, this.impl.args.split(' ')) @@ -71,6 +123,7 @@ export class GethExecutable { done (): void { if (this.gethProcess != null) { + debug('killing geth') this.gethProcess.kill() } } diff --git a/test/eip7702-wallet.test.ts b/test/eip7702-wallet.test.ts new file mode 100644 index 000000000..ec8cbe8f2 --- /dev/null +++ b/test/eip7702-wallet.test.ts @@ -0,0 +1,109 @@ +import { expect } from 'chai' + +import { EIP7702Account, EIP7702Account__factory, EntryPoint } from '../typechain' +import { createAccountOwner, createAddress, deployEntryPoint } from './testutils' +import { fillAndSign, packUserOp } from './UserOp' +import { hexConcat, parseEther } from 'ethers/lib/utils' +import { signEip7702Authorization } from './eip7702helpers' +import { GethExecutable } from './GethExecutable' +import { Wallet } from 'ethers' + +describe('EIP7702Account', function () { + let entryPoint: EntryPoint + + let eip7702delegate: EIP7702Account + let geth: GethExecutable + + before(async function () { + geth = new GethExecutable() + await geth.init() + + entryPoint = await deployEntryPoint(geth.provider) + + eip7702delegate = await new EIP7702Account__factory(geth.provider.getSigner()).deploy() + console.log('set eip7702delegate=', eip7702delegate.address) + }) + + after(() => { + geth.done() + }) + + describe('sanity: normal 7702 batching', () => { + let eoa: Wallet + before(async () => { + eoa = createAccountOwner(geth.provider) + + const auth = signEip7702Authorization(eoa, { + chainId: 0, + nonce: 0, + address: eip7702delegate.address + }) + const sendVal = parseEther('10') + const tx = { + to: eoa.address, + value: sendVal.toHexString(), + gas: 1e6, + authorizationList: [auth] + } + // console.log('tx=', tx) + await geth.sendTx(tx) + + expect(await geth.provider.getBalance(eoa.address)).to.equal(sendVal) + expect(await geth.provider.getCode(eoa.address)).to.equal(hexConcat(['0xef0100', eip7702delegate.address])) + }) + + it('should fail call from another account', async () => { + const wallet1 = EIP7702Account__factory.connect(eoa.address, geth.provider.getSigner()) + await expect(wallet1.execute([])).to.revertedWith('not from self or EntryPoint') + }) + + it('should succeed sending a batch', async () => { + // submit a batch + const wallet2 = EIP7702Account__factory.connect(eoa.address, eoa) + console.log('eoa balance=', await geth.provider.getBalance(eoa.address)) + + const addr1 = createAddress() + const addr2 = createAddress() + + await wallet2.execute([{ + target: addr1, value: 1, data: '0x' + }, { + target: addr2, value: 2, data: '0x' + }]) + expect(await geth.provider.getBalance(addr1)).to.equal(1) + expect(await geth.provider.getBalance(addr2)).to.equal(2) + }) + }) + + describe('use EntryPoint without paymaster', () => { + const eoa = createAccountOwner() + const addr1 = createAddress() + + it('init', async () => { + const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{ target: addr1, value: 1, data: '0x' }]]) + const userop = await fillAndSign({ + sender: eoa.address, + // initCode: '0xef01', + nonce: 0, + callData + }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) + + const auth = signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address }) + const beneficiary = createAddress() + await geth.sendTx({ + to: entryPoint.address, + data: '0x', + gas: 1000000, + authorizationList: [auth] + }) + console.log('delpoyed eoa code=', await geth.provider.getCode(eoa.address)) + const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary]) + const tx = { + to: entryPoint.address, + data: handleOps + // authorizationList: [auth] + } + await geth.sendTx(tx) + }) + }) +}) diff --git a/test/eip7702helpers.ts b/test/eip7702helpers.ts index 94d78412d..2f4f7307e 100644 --- a/test/eip7702helpers.ts +++ b/test/eip7702helpers.ts @@ -8,10 +8,13 @@ import { tostr } from './testutils' const EIP7702_MAGIC = '0x05' -export interface EIP7702Authorization { +export interface UnsignedEIP7702Authorization { chainId: BigNumberish address: string nonce: BigNumberish +} + +export interface EIP7702Authorization extends UnsignedEIP7702Authorization { yParity: BigNumberish r: BigNumberish s: BigNumberish @@ -29,7 +32,7 @@ export function toRlpHex (s: any): PrefixedHexString { return ret as PrefixedHexString } -export function eip7702DataToSign (authorization: Partial): PrefixedHexString { +export function eip7702DataToSign (authorization: UnsignedEIP7702Authorization): PrefixedHexString { const rlpData = [ toRlpHex(authorization.chainId), toRlpHex(authorization.address), @@ -56,7 +59,7 @@ export function gethHex (n: BigNumberish): string { return BigNumber.from(n).toHexString().replace(/0x0(.)/, '0x$1') } -export function signEip7702Authorization (signer: Wallet, authorization: Partial, chainId?: number): EIP7702Authorization { +export function signEip7702Authorization (signer: Wallet, authorization: UnsignedEIP7702Authorization, chainId?: number): EIP7702Authorization { const dataToSign = toBuffer(eip7702DataToSign(authorization)) const sig = ecsign(dataToSign, arrayify(signer.privateKey) as any) return { diff --git a/test/entrypoint-7702.test.ts b/test/entrypoint-7702.test.ts index 4195c54fa..1a1d8d19f 100644 --- a/test/entrypoint-7702.test.ts +++ b/test/entrypoint-7702.test.ts @@ -28,7 +28,6 @@ import { ethers } from 'hardhat' import { hexConcat, parseEther } from 'ethers/lib/utils' import { before } from 'mocha' import { GethExecutable } from './GethExecutable' -import { JsonRpcProvider } from '@ethersproject/providers' import { getEip7702AuthorizationSigner, gethHex, signEip7702Authorization } from './eip7702helpers' describe('EntryPoint EIP-7702 tests', function () { @@ -157,9 +156,7 @@ describe('EntryPoint EIP-7702 tests', function () { } let geth: GethExecutable - let prov: JsonRpcProvider let delegate: TestEip7702DelegateAccount - let gethFrom: string const beneficiary = createAddress() const eoa = createAccountOwner() let entryPoint: EntryPoint @@ -169,12 +166,10 @@ describe('EntryPoint EIP-7702 tests', function () { geth = new GethExecutable() await geth.init() - prov = new JsonRpcProvider(geth.rpcUrl()) - entryPoint = await deployEntryPoint(prov) - delegate = await new TestEip7702DelegateAccount__factory(prov.getSigner()).deploy(entryPoint.address) - console.log('delegate addr=', delegate.address, 'len=', await prov.getCode(delegate.address).then(code => code.length)) - gethFrom = (await prov.send('eth_accounts', []))[0] - await prov.send('eth_sendTransaction', [{ from: gethFrom, to: eoa.address, value: gethHex(parseEther('1')) }]) + entryPoint = await deployEntryPoint(geth.provider) + delegate = await new TestEip7702DelegateAccount__factory(geth.provider.getSigner()).deploy(entryPoint.address) + console.log('delegate 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 () => { @@ -184,13 +179,12 @@ describe('EntryPoint EIP-7702 tests', function () { initCode: EIP7702_PREFIX // not init function, just delegate }, eoa, entryPoint, { eip7702delegate: delegate.address }) const handleOpCall = { - from: gethFrom, to: entryPoint.address, data: entryPoint.interface.encodeFunctionData('handleOps', [[eip7702userOp], beneficiary]), gasLimit: 1000000 // authorizationList: [eip7702tuple] } - expect(await prov.send('eth_call', [handleOpCall]).catch(e => { + expect(await geth.call(handleOpCall).catch(e => { return e.error })).to.match(/not an EIP-7702 delegate|sender has no code/) }) @@ -203,19 +197,18 @@ describe('EntryPoint EIP-7702 tests', function () { }, eoa, entryPoint, { eip7702delegate: delegate.address }) const eip7702tuple = signEip7702Authorization(eoa, { address: delegate.address, - nonce: await prov.getTransactionCount(eoa.address), - chainId: await prov.getNetwork().then(net => net.chainId) + nonce: await geth.provider.getTransactionCount(eoa.address), + chainId: await geth.provider.getNetwork().then(net => net.chainId) }) const handleOpCall = { - from: gethFrom, to: entryPoint.address, data: entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(eip7702userOp)], beneficiary]), gasLimit: 1000000, authorizationList: [eip7702tuple] } - await prov.send('eth_call', [handleOpCall]).catch(e => { + await geth.call(handleOpCall).catch(e => { throw Error(decodeRevertReason(e)!) }) }) @@ -230,17 +223,16 @@ describe('EntryPoint EIP-7702 tests', function () { const eip7702tuple = signEip7702Authorization(eoa, { address: delegate.address, - nonce: await prov.getTransactionCount(eoa.address), - chainId: await prov.getNetwork().then(net => net.chainId) + nonce: await geth.provider.getTransactionCount(eoa.address), + chainId: await geth.provider.getNetwork().then(net => net.chainId) }) const handleOpCall = { - from: gethFrom, to: entryPoint.address, data: entryPoint.interface.encodeFunctionData('handleOps', [[eip7702userOp], beneficiary]), gasLimit: 1000000, authorizationList: [eip7702tuple] } - await prov.send('eth_call', [handleOpCall]).catch(e => { + await geth.call(handleOpCall).catch(e => { throw Error(decodeRevertReason(e)!) }) }) diff --git a/test/testutils.ts b/test/testutils.ts index cd3f62c00..4440f4220 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -22,7 +22,7 @@ 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' @@ -73,9 +73,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); } From 1442357c9d81f8604cb6164aa222d75ffef27044 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sun, 9 Feb 2025 15:02:55 +0200 Subject: [PATCH 26/44] test eip-7702 account --- contracts/core/EntryPointSimulations.sol | 3 ++ test/eip7702-wallet.test.ts | 57 ++++++++++++------------ test/entrypointsimulations.test.ts | 2 +- 3 files changed, 33 insertions(+), 29 deletions(-) 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/test/eip7702-wallet.test.ts b/test/eip7702-wallet.test.ts index ec8cbe8f2..7064f05bc 100644 --- a/test/eip7702-wallet.test.ts +++ b/test/eip7702-wallet.test.ts @@ -75,35 +75,36 @@ describe('EIP7702Account', function () { }) }) - describe('use EntryPoint without paymaster', () => { - const eoa = createAccountOwner() + it('should be able to use EntryPoint without paymaster', async () => { const addr1 = createAddress() - - it('init', async () => { - const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{ target: addr1, value: 1, data: '0x' }]]) - const userop = await fillAndSign({ - sender: eoa.address, - // initCode: '0xef01', - nonce: 0, - callData - }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) - - const auth = signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address }) - const beneficiary = createAddress() - await geth.sendTx({ - to: entryPoint.address, - data: '0x', - gas: 1000000, - authorizationList: [auth] - }) - console.log('delpoyed eoa code=', await geth.provider.getCode(eoa.address)) - const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary]) - const tx = { - to: entryPoint.address, - data: handleOps - // authorizationList: [auth] - } - await geth.sendTx(tx) + const eoa = createAccountOwner(geth.provider) + const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{ target: addr1, value: 1, data: '0x' }]]) + const userop = await fillAndSign({ + sender: eoa.address, + // initCode: '0xef01', + nonce: 0, + callData + }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) + + const auth = signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address }) + const beneficiary = createAddress() + await geth.sendTx({ + to: entryPoint.address, + data: '0x', + gas: 1000000, + authorizationList: [auth] }) + const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary]) + const tx = { + to: entryPoint.address, + data: handleOps, + gas: 1e6 + // authorizationList: [auth] + } + await geth.sendTx(tx) + }) + + it('should use EntryPoint with paymaster', () => { + }) }) 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) From b088f055181663f9e5a86b3b469e6c4ba6da978c Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sun, 9 Feb 2025 21:10:05 +0200 Subject: [PATCH 27/44] test 7702 with paymaster --- contracts/samples/EIP7702Account.sol | 2 +- scripts/geth.sh | 6 ++- test/GethExecutable.ts | 27 +++++++----- test/UserOp.ts | 65 ++++++++++++++++++---------- test/eip7702-wallet.test.ts | 62 ++++++++++++++++++++------ test/eip7702helpers.ts | 13 +++--- test/entrypoint-7702.test.ts | 17 ++++---- test/testExecAccount.test.ts | 2 +- test/testutils.ts | 3 +- 9 files changed, 131 insertions(+), 66 deletions(-) diff --git a/contracts/samples/EIP7702Account.sol b/contracts/samples/EIP7702Account.sol index 01a6a154f..2e78f4f54 100644 --- a/contracts/samples/EIP7702Account.sol +++ b/contracts/samples/EIP7702Account.sol @@ -19,7 +19,7 @@ contract EIP7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721 // temporary address of entryPoint v0.8 function entryPoint() public pure override returns (IEntryPoint) { - return IEntryPoint(0xe6562c192D185da05097BFFfD5ee12C878A21c33); + return IEntryPoint(0x690953a7e55E6cd6bD7192708bA1bBA0a511161D); } /** diff --git a/scripts/geth.sh b/scripts/geth.sh index a6f2a6adf..bfd084842 100755 --- a/scripts/geth.sh +++ b/scripts/geth.sh @@ -1,4 +1,6 @@ -#!/bin/sh +#!/bin/sh -xe name=geth-$$ trap "echo killing docker; docker kill $name 2> /dev/null" EXIT -docker run --name $name --rm -p 8545:8545 -p 54321:54321 dtr22/geth7702 $* +port=$1 +shift +docker run --name $name --rm -p $port:8545 dtr22/geth7702 $* diff --git a/test/GethExecutable.ts b/test/GethExecutable.ts index b11f85fe5..1edf3bf39 100644 --- a/test/GethExecutable.ts +++ b/test/GethExecutable.ts @@ -3,21 +3,21 @@ 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') -const port = 54321 export const gethLauncher = { name: 'geth', exec: './scripts/geth.sh', - args: `--http --http.api personal,eth,net,web3,debug --rpc.allow-unprotected-txs --allow-insecure-unlock --dev --http.addr 0.0.0.0 --http.port=${port}` + args: 'PORT --http --http.api personal,eth,net,web3,debug --rpc.allow-unprotected-txs --allow-insecure-unlock --dev --http.addr 0.0.0.0' } // eslint-disable-next-line @typescript-eslint/no-unused-vars export const anvilLauncher = { name: 'anvil', exec: './scripts/anvil.sh', - args: `--hardfork prague --port=${port}` + args: '--hardfork prague --port=PORT' } interface Eip7702Transaction { @@ -31,6 +31,7 @@ interface Eip7702Transaction { export class GethExecutable { gethFrom: string provider: JsonRpcProvider + port = Math.floor(5000 + Math.random() * 10000) constructor (private readonly impl = gethLauncher) { } @@ -40,7 +41,7 @@ export class GethExecutable { markerString = /HTTP server started|Listening on/ rpcUrl (): string { - return `http://localhost:${port}` + return `http://localhost:${this.port}` } async init (): Promise { @@ -49,7 +50,7 @@ export class GethExecutable { this.gethFrom = (await this.provider.send('eth_accounts', []))[0] } - async sendTx (tx: Eip7702Transaction): Promise { + 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 = { @@ -65,10 +66,15 @@ export class GethExecutable { tx1[key] = tx1[key].replace(/0x0\B/, '0x') } } - await this.provider.send('eth_sendTransaction', [tx1]).catch(e => { - // console.log(e) - throw new Error(e.error.message) + // 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 @@ -83,8 +89,9 @@ export class GethExecutable { async initProcess (): Promise { return new Promise((resolve, reject) => { - console.log('spawning: ', this.impl.exec, this.impl.args) - this.gethProcess = spawn(this.impl.exec, this.impl.args.split(' ')) + const args = this.impl.args.replace(/PORT/, this.port.toString()) + console.log('spawning: ', this.impl.exec, args) + this.gethProcess = spawn(this.impl.exec, args.split(' ')) let allData = '' if (this.gethProcess != null) { diff --git a/test/UserOp.ts b/test/UserOp.ts index c289208aa..c85c1130e 100644 --- a/test/UserOp.ts +++ b/test/UserOp.ts @@ -193,33 +193,50 @@ export async function fillUserOp (op: Partial, entryPoint?: Entry const getNonceFunction = options?.getNonceFunction ?? 'getNonce' const op1 = { ...op } const provider = entryPoint?.provider - if (op1.initCode != null && !isEip7702UserOp(op1 as UserOperation)) { - 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') diff --git a/test/eip7702-wallet.test.ts b/test/eip7702-wallet.test.ts index 7064f05bc..a3d2504c1 100644 --- a/test/eip7702-wallet.test.ts +++ b/test/eip7702-wallet.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' -import { EIP7702Account, EIP7702Account__factory, EntryPoint } from '../typechain' -import { createAccountOwner, createAddress, deployEntryPoint } from './testutils' +import { EIP7702Account, EIP7702Account__factory, EntryPoint, TestPaymasterAcceptAll__factory } from '../typechain' +import { createAccountOwner, createAddress, deployEntryPoint, fund } from './testutils' import { fillAndSign, packUserOp } from './UserOp' import { hexConcat, parseEther } from 'ethers/lib/utils' import { signEip7702Authorization } from './eip7702helpers' @@ -21,6 +21,7 @@ describe('EIP7702Account', function () { entryPoint = await deployEntryPoint(geth.provider) eip7702delegate = await new EIP7702Account__factory(geth.provider.getSigner()).deploy() + expect(await eip7702delegate.entryPoint()).to.equal(entryPoint.address, 'fix entryPoint in EIP7702Account') console.log('set eip7702delegate=', eip7702delegate.address) }) @@ -33,7 +34,7 @@ describe('EIP7702Account', function () { before(async () => { eoa = createAccountOwner(geth.provider) - const auth = signEip7702Authorization(eoa, { + const auth = await signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address @@ -45,9 +46,7 @@ describe('EIP7702Account', function () { gas: 1e6, authorizationList: [auth] } - // console.log('tx=', tx) await geth.sendTx(tx) - expect(await geth.provider.getBalance(eoa.address)).to.equal(sendVal) expect(await geth.provider.getCode(eoa.address)).to.equal(hexConcat(['0xef0100', eip7702delegate.address])) }) @@ -69,7 +68,7 @@ describe('EIP7702Account', function () { target: addr1, value: 1, data: '0x' }, { target: addr2, value: 2, data: '0x' - }]) + }]).then(async tx => tx.wait()) expect(await geth.provider.getBalance(addr1)).to.equal(1) expect(await geth.provider.getBalance(addr2)).to.equal(2) }) @@ -78,16 +77,23 @@ describe('EIP7702Account', function () { it('should be able to use EntryPoint without paymaster', async () => { const addr1 = createAddress() const eoa = createAccountOwner(geth.provider) - const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{ target: addr1, value: 1, data: '0x' }]]) + + const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{ + target: addr1, + value: 1, + data: '0x' + }]]) const userop = await fillAndSign({ sender: eoa.address, - // initCode: '0xef01', + initCode: '0xef01', nonce: 0, callData }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) - const auth = signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address }) + await geth.sendTx({ to: eoa.address, value: parseEther('1') }) + const auth = await signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address }) const beneficiary = createAddress() + // submit separate tx with tuple: geth's estimateGas doesn't work, and its easier to detect thrown errors.. await geth.sendTx({ to: entryPoint.address, data: '0x', @@ -97,14 +103,44 @@ describe('EIP7702Account', function () { const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary]) const tx = { to: entryPoint.address, - data: handleOps, - gas: 1e6 - // authorizationList: [auth] + data: handleOps } await geth.sendTx(tx) }) - it('should use EntryPoint with paymaster', () => { + it('should use EntryPoint with paymaster', async () => { + const addr1 = createAddress() + const eoa = createAccountOwner(geth.provider) + const paymaster = await new TestPaymasterAcceptAll__factory(geth.provider.getSigner()).deploy(entryPoint.address) + await paymaster.deposit({ value: parseEther('1') }) + const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{ + target: addr1, + value: 1, + data: '0x' + }]]) + const userop = await fillAndSign({ + sender: eoa.address, + paymaster: paymaster.address, + initCode: '0xef01', + nonce: 0, + callData + }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) + const auth = await signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address }) + const beneficiary = createAddress() + console.log('delegate=', eip7702delegate.address) + // submit separate tx with tuple: geth's estimateGas doesn't work, and its easier to detect thrown errors.. + await geth.sendTx({ + to: entryPoint.address, + data: '0x', + gas: 1000000, + authorizationList: [auth] + }) + const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary]) + const tx = { + to: entryPoint.address, + data: handleOps + } + await geth.sendTx(tx) }) }) diff --git a/test/eip7702helpers.ts b/test/eip7702helpers.ts index 2f4f7307e..357a8e8fc 100644 --- a/test/eip7702helpers.ts +++ b/test/eip7702helpers.ts @@ -11,7 +11,7 @@ const EIP7702_MAGIC = '0x05' export interface UnsignedEIP7702Authorization { chainId: BigNumberish address: string - nonce: BigNumberish + nonce?: BigNumberish } export interface EIP7702Authorization extends UnsignedEIP7702Authorization { @@ -59,13 +59,14 @@ export function gethHex (n: BigNumberish): string { return BigNumber.from(n).toHexString().replace(/0x0(.)/, '0x$1') } -export function signEip7702Authorization (signer: Wallet, authorization: UnsignedEIP7702Authorization, chainId?: number): EIP7702Authorization { - const dataToSign = toBuffer(eip7702DataToSign(authorization)) +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(authorization.nonce!), + 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 index 1a1d8d19f..c2fd88de7 100644 --- a/test/entrypoint-7702.test.ts +++ b/test/entrypoint-7702.test.ts @@ -16,6 +16,7 @@ import { deployEntryPoint } from './testutils' import { + asyncSignUserOp, EIP7702_PREFIX, fillAndSign, fillSignAndPack, @@ -99,10 +100,10 @@ describe('EntryPoint EIP-7702 tests', function () { expect(signer).to.eql(authSigner.address) }) - it('#signEip7702Authorization', () => { + it('#signEip7702Authorization', async () => { // deliberately remove previous signature... const authToSign = { address: createAddress(), nonce: 12345, chainId: '0x0' } - const signed = signEip7702Authorization(authSigner, authToSign) + const signed = await signEip7702Authorization(authSigner, authToSign) expect(getEip7702AuthorizationSigner(signed)).to.eql(authSigner.address) }) }) @@ -158,17 +159,17 @@ describe('EntryPoint EIP-7702 tests', function () { let geth: GethExecutable let delegate: TestEip7702DelegateAccount const beneficiary = createAddress() - const eoa = createAccountOwner() + 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('delegate addr=', delegate.address, 'len=', await geth.provider.getCode(delegate.address).then(code => code.length)) + 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')) }) }) @@ -195,7 +196,7 @@ describe('EntryPoint EIP-7702 tests', function () { nonce: 0, initCode: EIP7702_PREFIX // not init function, just delegate }, eoa, entryPoint, { eip7702delegate: delegate.address }) - const eip7702tuple = signEip7702Authorization(eoa, { + const eip7702tuple = await signEip7702Authorization(eoa, { address: delegate.address, nonce: await geth.provider.getTransactionCount(eoa.address), chainId: await geth.provider.getNetwork().then(net => net.chainId) @@ -221,9 +222,9 @@ describe('EntryPoint EIP-7702 tests', function () { initCode: hexConcat([EIP7702_PREFIX + '0'.repeat(42 - EIP7702_PREFIX.length), delegate.interface.encodeFunctionData('testInit')]) }, eoa, entryPoint, { eip7702delegate: delegate.address }) - const eip7702tuple = signEip7702Authorization(eoa, { + const eip7702tuple = await signEip7702Authorization(eoa, { address: delegate.address, - nonce: await geth.provider.getTransactionCount(eoa.address), + // nonce: await geth.provider.getTransactionCount(eoa.address), chainId: await geth.provider.getNetwork().then(net => net.chainId) }) const handleOpCall = { 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 4440f4220..41b9b7496 100644 --- a/test/testutils.ts +++ b/test/testutils.ts @@ -29,6 +29,7 @@ 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') @@ -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()) } From 6459a16960a196e20103b33420c3f673f3b1d567 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sun, 9 Feb 2025 21:10:54 +0200 Subject: [PATCH 28/44] lnts --- test/eip7702-wallet.test.ts | 2 +- test/entrypoint-7702.test.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/eip7702-wallet.test.ts b/test/eip7702-wallet.test.ts index a3d2504c1..ed2e6aba7 100644 --- a/test/eip7702-wallet.test.ts +++ b/test/eip7702-wallet.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { EIP7702Account, EIP7702Account__factory, EntryPoint, TestPaymasterAcceptAll__factory } from '../typechain' -import { createAccountOwner, createAddress, deployEntryPoint, fund } from './testutils' +import { createAccountOwner, createAddress, deployEntryPoint } from './testutils' import { fillAndSign, packUserOp } from './UserOp' import { hexConcat, parseEther } from 'ethers/lib/utils' import { signEip7702Authorization } from './eip7702helpers' diff --git a/test/entrypoint-7702.test.ts b/test/entrypoint-7702.test.ts index c2fd88de7..542ddf489 100644 --- a/test/entrypoint-7702.test.ts +++ b/test/entrypoint-7702.test.ts @@ -16,7 +16,6 @@ import { deployEntryPoint } from './testutils' import { - asyncSignUserOp, EIP7702_PREFIX, fillAndSign, fillSignAndPack, From 192be39f33cf10c5d9e5907dc6414b5678e6eafe Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Mon, 10 Feb 2025 02:25:47 +0200 Subject: [PATCH 29/44] coverage --- .solcover.js | 4 ++-- contracts/core/UserOperationLib.sol | 10 ---------- contracts/samples/EIP7702Account.sol | 2 +- scripts/geth.sh | 5 +++-- test/GethExecutable.ts | 28 +++++++++++++--------------- test/eip7702-wallet.test.ts | 5 +++++ test/entrypoint-7702.test.ts | 1 + 7 files changed, 25 insertions(+), 30 deletions(-) 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/UserOperationLib.sol b/contracts/core/UserOperationLib.sol index 5d23510eb..ac7da8472 100644 --- a/contracts/core/UserOperationLib.sol +++ b/contracts/core/UserOperationLib.sol @@ -52,16 +52,6 @@ library UserOperationLib { "PackedUserOperation(address sender,uint256 nonce,bytes initCode,bytes callData,bytes32 accountGasLimits,uint256 preVerificationGas,bytes32 gasFees,bytes paymasterAndData)" ); - /** - * Pack the user operation data into bytes for hashing. - * @param userOp - The user operation data. - */ - function encode( - PackedUserOperation calldata userOp - ) internal pure returns (bytes memory ret) { - return encode(userOp, ""); - } - /** * Pack the user operation data into bytes for hashing. * @param userOp - The user operation data. diff --git a/contracts/samples/EIP7702Account.sol b/contracts/samples/EIP7702Account.sol index 2e78f4f54..0cbdc6e18 100644 --- a/contracts/samples/EIP7702Account.sol +++ b/contracts/samples/EIP7702Account.sol @@ -19,7 +19,7 @@ contract EIP7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721 // temporary address of entryPoint v0.8 function entryPoint() public pure override returns (IEntryPoint) { - return IEntryPoint(0x690953a7e55E6cd6bD7192708bA1bBA0a511161D); + return IEntryPoint(0x6F4F5099a64044D69EB7419d66760fD4106fcE3C); } /** diff --git a/scripts/geth.sh b/scripts/geth.sh index bfd084842..483aa063b 100755 --- a/scripts/geth.sh +++ b/scripts/geth.sh @@ -1,6 +1,7 @@ -#!/bin/sh -xe +#!/bin/sh name=geth-$$ trap "echo killing docker; docker kill $name 2> /dev/null" EXIT port=$1 shift -docker run --name $name --rm -p $port:8545 dtr22/geth7702 $* +params="--http --http.api personal,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 dtr22/geth7702 $params diff --git a/test/GethExecutable.ts b/test/GethExecutable.ts index 1edf3bf39..7c5a6e74f 100644 --- a/test/GethExecutable.ts +++ b/test/GethExecutable.ts @@ -7,18 +7,15 @@ import { decodeRevertReason } from './testutils' const debug = Debug('aa.geth') -export const gethLauncher = { - name: 'geth', - exec: './scripts/geth.sh', - args: 'PORT --http --http.api personal,eth,net,web3,debug --rpc.allow-unprotected-txs --allow-insecure-unlock --dev --http.addr 0.0.0.0' +// 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' } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const anvilLauncher = { - name: 'anvil', - exec: './scripts/anvil.sh', - args: '--hardfork prague --port=PORT' -} +export type LauncherName = keyof typeof launchers interface Eip7702Transaction { to: string @@ -33,7 +30,9 @@ export class GethExecutable { provider: JsonRpcProvider port = Math.floor(5000 + Math.random() * 10000) - constructor (private readonly impl = gethLauncher) { + impl: string + constructor (private readonly implName: LauncherName = 'geth') { + this.impl = launchers[implName] } private gethProcess: ChildProcess | null = null @@ -89,9 +88,8 @@ export class GethExecutable { async initProcess (): Promise { return new Promise((resolve, reject) => { - const args = this.impl.args.replace(/PORT/, this.port.toString()) - console.log('spawning: ', this.impl.exec, args) - this.gethProcess = spawn(this.impl.exec, args.split(' ')) + console.log('spawning: ', this.impl, this.port) + this.gethProcess = spawn(this.impl, [this.port.toString()]) let allData = '' if (this.gethProcess != null) { @@ -120,7 +118,7 @@ export class GethExecutable { }) this.gethProcess.on('exit', (code: number | null) => { - console.log(`${this.impl.name} process exited with code ${code}`) + console.log(`${this.impl}: process exited with code ${code}`) }) } else { reject(new Error('Failed to start geth process')) diff --git a/test/eip7702-wallet.test.ts b/test/eip7702-wallet.test.ts index ed2e6aba7..2d9152b32 100644 --- a/test/eip7702-wallet.test.ts +++ b/test/eip7702-wallet.test.ts @@ -9,6 +9,11 @@ import { GethExecutable } from './GethExecutable' import { Wallet } from 'ethers' describe('EIP7702Account', function () { + // can't deploy coverage "entrypoint" on geth (contract too large) + if (process.env.COVERAGE != null) { + return + } + let entryPoint: EntryPoint let eip7702delegate: EIP7702Account diff --git a/test/entrypoint-7702.test.ts b/test/entrypoint-7702.test.ts index 542ddf489..fa12d02d2 100644 --- a/test/entrypoint-7702.test.ts +++ b/test/entrypoint-7702.test.ts @@ -151,6 +151,7 @@ describe('EntryPoint EIP-7702 tests', function () { }) describe('test with geth', () => { + // can't deploy coverage "entrypoint" on geth (contract too large) if (process.env.COVERAGE != null) { return } From 26d6a0b28bb0d1268769433803a79fc6f478a60d Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Mon, 10 Feb 2025 02:32:55 +0200 Subject: [PATCH 30/44] rename to Simple7702Account --- .../{EIP7702Account.sol => Simple7702Account.sol} | 10 ++-------- test/eip7702-wallet.test.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 15 deletions(-) rename contracts/samples/{EIP7702Account.sol => Simple7702Account.sol} (89%) diff --git a/contracts/samples/EIP7702Account.sol b/contracts/samples/Simple7702Account.sol similarity index 89% rename from contracts/samples/EIP7702Account.sol rename to contracts/samples/Simple7702Account.sol index 0cbdc6e18..475721105 100644 --- a/contracts/samples/EIP7702Account.sol +++ b/contracts/samples/Simple7702Account.sol @@ -12,10 +12,10 @@ import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import "../core/BaseAccount.sol"; /** - * EIP7702Account + * Simple7702Account.sol * A minimal account to be used with EIP-7702 (for batching) and ERC-4337 (for gas sponsoring) */ -contract EIP7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721Holder { +contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721Holder { // temporary address of entryPoint v0.8 function entryPoint() public pure override returns (IEntryPoint) { @@ -56,12 +56,6 @@ contract EIP7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721 for (uint256 i = 0; i < calls.length; i++) { Call calldata call = calls[i]; -// if (!Exec.call(gasleft(), call.target, call.value, call.data)) { -// assembly { -// returndatacopy(0, 0, returndatasize()) -// revert(0, returndatasize()) -// } -// } (bool ok, bytes memory ret) = call.target.call{value: call.value}(call.data); if (!ok) { // solhint-disable-next-line no-inline-assembly diff --git a/test/eip7702-wallet.test.ts b/test/eip7702-wallet.test.ts index 2d9152b32..529a1f567 100644 --- a/test/eip7702-wallet.test.ts +++ b/test/eip7702-wallet.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai' -import { EIP7702Account, EIP7702Account__factory, EntryPoint, TestPaymasterAcceptAll__factory } from '../typechain' +import { Simple7702Account, Simple7702Account__factory, EntryPoint, TestPaymasterAcceptAll__factory } from '../typechain' import { createAccountOwner, createAddress, deployEntryPoint } from './testutils' import { fillAndSign, packUserOp } from './UserOp' import { hexConcat, parseEther } from 'ethers/lib/utils' @@ -8,7 +8,7 @@ import { signEip7702Authorization } from './eip7702helpers' import { GethExecutable } from './GethExecutable' import { Wallet } from 'ethers' -describe('EIP7702Account', function () { +describe('Simple7702Account.sol', function () { // can't deploy coverage "entrypoint" on geth (contract too large) if (process.env.COVERAGE != null) { return @@ -16,7 +16,7 @@ describe('EIP7702Account', function () { let entryPoint: EntryPoint - let eip7702delegate: EIP7702Account + let eip7702delegate: Simple7702Account let geth: GethExecutable before(async function () { @@ -25,8 +25,8 @@ describe('EIP7702Account', function () { entryPoint = await deployEntryPoint(geth.provider) - eip7702delegate = await new EIP7702Account__factory(geth.provider.getSigner()).deploy() - expect(await eip7702delegate.entryPoint()).to.equal(entryPoint.address, 'fix entryPoint in EIP7702Account') + eip7702delegate = await new Simple7702Account__factory(geth.provider.getSigner()).deploy() + expect(await eip7702delegate.entryPoint()).to.equal(entryPoint.address, 'fix entryPoint in Simple7702Account.sol') console.log('set eip7702delegate=', eip7702delegate.address) }) @@ -57,13 +57,13 @@ describe('EIP7702Account', function () { }) it('should fail call from another account', async () => { - const wallet1 = EIP7702Account__factory.connect(eoa.address, geth.provider.getSigner()) + const wallet1 = Simple7702Account__factory.connect(eoa.address, geth.provider.getSigner()) await expect(wallet1.execute([])).to.revertedWith('not from self or EntryPoint') }) it('should succeed sending a batch', async () => { // submit a batch - const wallet2 = EIP7702Account__factory.connect(eoa.address, eoa) + const wallet2 = Simple7702Account__factory.connect(eoa.address, eoa) console.log('eoa balance=', await geth.provider.getBalance(eoa.address)) const addr1 = createAddress() From a747be14d30044fc4d5bae772cd4e821795e42d4 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Tue, 11 Feb 2025 14:06:55 +0200 Subject: [PATCH 31/44] gascalc --- reports/gas-checker.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index b664905fd..4ea66d5fa 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -16,17 +16,17 @@ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ simple - diff from previous │ 2 │ │ 41950 │ 12691 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 455488 │ │ ║ +║ simple │ 10 │ 455524 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 41999 │ 12740 ║ +║ simple - diff from previous │ 11 │ │ 42011 │ 12752 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83641 │ │ ║ +║ simple paymaster │ 1 │ 83629 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40502 │ 11243 ║ +║ simple paymaster with diff │ 2 │ │ 40514 │ 11255 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 448278 │ │ ║ +║ simple paymaster │ 10 │ 448242 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40511 │ 11252 ║ +║ simple paymaster with diff │ 11 │ │ 40559 │ 11300 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ big tx 5k │ 1 │ 167561 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ @@ -34,14 +34,14 @@ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ big tx 5k │ 10 │ 1348210 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131152 │ 16450 ║ +║ big tx - diff from previous │ 11 │ │ 131200 │ 16498 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84972 │ │ ║ +║ paymaster+postOp │ 1 │ 84984 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp with diff │ 2 │ │ 41844 │ 12585 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 461641 │ │ ║ +║ paymaster+postOp │ 10 │ 461665 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41967 │ 12708 ║ +║ paymaster+postOp with diff │ 11 │ │ 41859 │ 12600 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ From f7c16884405be6f342b57243e4a9514af734846a Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Tue, 11 Feb 2025 15:38:09 +0200 Subject: [PATCH 32/44] remove Simple7702Account, into a separate PR --- contracts/samples/Simple7702Account.sol | 84 ------------- test/eip7702-wallet.test.ts | 151 ------------------------ 2 files changed, 235 deletions(-) delete mode 100644 contracts/samples/Simple7702Account.sol delete mode 100644 test/eip7702-wallet.test.ts diff --git a/contracts/samples/Simple7702Account.sol b/contracts/samples/Simple7702Account.sol deleted file mode 100644 index 475721105..000000000 --- a/contracts/samples/Simple7702Account.sol +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: MIT -// based on: https://gist.github.com/frangio/e40305b9f99de290b73750dff5ebe50a -pragma solidity ^0.8; - -import "../interfaces/PackedUserOperation.sol"; -import "../core/Helpers.sol"; -import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import "@openzeppelin/contracts/interfaces/IERC1271.sol"; -import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; -import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "../core/BaseAccount.sol"; - -/** - * Simple7702Account.sol - * A minimal account to be used with EIP-7702 (for batching) and ERC-4337 (for gas sponsoring) - */ -contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721Holder { - - // temporary address of entryPoint v0.8 - function entryPoint() public pure override returns (IEntryPoint) { - return IEntryPoint(0x6F4F5099a64044D69EB7419d66760fD4106fcE3C); - } - - /** - * Make this account callable through ERC-4337 EntryPoint. - * The UserOperation should be signed by this account's private key. - */ - function _validateSignature( - PackedUserOperation calldata userOp, - bytes32 userOpHash - ) internal virtual override returns (uint256 validationData) { - - if (address(this) != ECDSA.recover(userOpHash, userOp.signature)) { - return SIG_VALIDATION_FAILED; - } - return 0; - } - - function _requireFromSelfOrEntryPoint() internal view virtual { - require( - msg.sender == address(this) || - msg.sender == address(entryPoint()), - "not from self or EntryPoint" - ); - } - - struct Call { - address target; - uint256 value; - bytes data; - } - - function execute(Call[] calldata calls) external { - _requireFromSelfOrEntryPoint(); - - for (uint256 i = 0; i < calls.length; i++) { - Call calldata call = calls[i]; - (bool ok, bytes memory ret) = call.target.call{value: call.value}(call.data); - if (!ok) { - // solhint-disable-next-line no-inline-assembly - assembly { revert(add(ret, 32), mload(ret)) } - } - } - } - - function supportsInterface(bytes4 id) public override(ERC1155Holder, IERC165) pure returns (bool) { - return - id == type(IERC165).interfaceId || - id == type(IAccount).interfaceId || - id == type(IERC1271).interfaceId || - id == type(IERC1155Receiver).interfaceId || - id == type(IERC721Receiver).interfaceId; - } - - //deliberately return the same signature as returned by the EOA itself: This way, - // ERC-1271 can be used regardless if the account currently has this code or not. - function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) { - return ECDSA.recover(hash, signature) == address(this) ? this.isValidSignature.selector : bytes4(0); - } - - receive() external payable { - } -} diff --git a/test/eip7702-wallet.test.ts b/test/eip7702-wallet.test.ts deleted file mode 100644 index 529a1f567..000000000 --- a/test/eip7702-wallet.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { expect } from 'chai' - -import { Simple7702Account, Simple7702Account__factory, EntryPoint, TestPaymasterAcceptAll__factory } from '../typechain' -import { createAccountOwner, createAddress, deployEntryPoint } from './testutils' -import { fillAndSign, packUserOp } from './UserOp' -import { hexConcat, parseEther } from 'ethers/lib/utils' -import { signEip7702Authorization } from './eip7702helpers' -import { GethExecutable } from './GethExecutable' -import { Wallet } from 'ethers' - -describe('Simple7702Account.sol', function () { - // can't deploy coverage "entrypoint" on geth (contract too large) - if (process.env.COVERAGE != null) { - return - } - - let entryPoint: EntryPoint - - let eip7702delegate: Simple7702Account - let geth: GethExecutable - - before(async function () { - geth = new GethExecutable() - await geth.init() - - entryPoint = await deployEntryPoint(geth.provider) - - eip7702delegate = await new Simple7702Account__factory(geth.provider.getSigner()).deploy() - expect(await eip7702delegate.entryPoint()).to.equal(entryPoint.address, 'fix entryPoint in Simple7702Account.sol') - console.log('set eip7702delegate=', eip7702delegate.address) - }) - - after(() => { - geth.done() - }) - - describe('sanity: normal 7702 batching', () => { - let eoa: Wallet - before(async () => { - eoa = createAccountOwner(geth.provider) - - const auth = await signEip7702Authorization(eoa, { - chainId: 0, - nonce: 0, - address: eip7702delegate.address - }) - const sendVal = parseEther('10') - const tx = { - to: eoa.address, - value: sendVal.toHexString(), - gas: 1e6, - authorizationList: [auth] - } - await geth.sendTx(tx) - expect(await geth.provider.getBalance(eoa.address)).to.equal(sendVal) - expect(await geth.provider.getCode(eoa.address)).to.equal(hexConcat(['0xef0100', eip7702delegate.address])) - }) - - it('should fail call from another account', async () => { - const wallet1 = Simple7702Account__factory.connect(eoa.address, geth.provider.getSigner()) - await expect(wallet1.execute([])).to.revertedWith('not from self or EntryPoint') - }) - - it('should succeed sending a batch', async () => { - // submit a batch - const wallet2 = Simple7702Account__factory.connect(eoa.address, eoa) - console.log('eoa balance=', await geth.provider.getBalance(eoa.address)) - - const addr1 = createAddress() - const addr2 = createAddress() - - await wallet2.execute([{ - target: addr1, value: 1, data: '0x' - }, { - target: addr2, value: 2, data: '0x' - }]).then(async tx => tx.wait()) - expect(await geth.provider.getBalance(addr1)).to.equal(1) - expect(await geth.provider.getBalance(addr2)).to.equal(2) - }) - }) - - it('should be able to use EntryPoint without paymaster', async () => { - const addr1 = createAddress() - const eoa = createAccountOwner(geth.provider) - - const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{ - target: addr1, - value: 1, - data: '0x' - }]]) - const userop = await fillAndSign({ - sender: eoa.address, - initCode: '0xef01', - nonce: 0, - callData - }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) - - await geth.sendTx({ to: eoa.address, value: parseEther('1') }) - const auth = await signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address }) - const beneficiary = createAddress() - // submit separate tx with tuple: geth's estimateGas doesn't work, and its easier to detect thrown errors.. - await geth.sendTx({ - to: entryPoint.address, - data: '0x', - gas: 1000000, - authorizationList: [auth] - }) - const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary]) - const tx = { - to: entryPoint.address, - data: handleOps - } - await geth.sendTx(tx) - }) - - it('should use EntryPoint with paymaster', async () => { - const addr1 = createAddress() - const eoa = createAccountOwner(geth.provider) - const paymaster = await new TestPaymasterAcceptAll__factory(geth.provider.getSigner()).deploy(entryPoint.address) - await paymaster.deposit({ value: parseEther('1') }) - const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{ - target: addr1, - value: 1, - data: '0x' - }]]) - const userop = await fillAndSign({ - sender: eoa.address, - paymaster: paymaster.address, - initCode: '0xef01', - nonce: 0, - callData - }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) - - const auth = await signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address }) - const beneficiary = createAddress() - console.log('delegate=', eip7702delegate.address) - // submit separate tx with tuple: geth's estimateGas doesn't work, and its easier to detect thrown errors.. - await geth.sendTx({ - to: entryPoint.address, - data: '0x', - gas: 1000000, - authorizationList: [auth] - }) - const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary]) - const tx = { - to: entryPoint.address, - data: handleOps - } - await geth.sendTx(tx) - }) -}) From 074844c0f094d9b0a3ebebd32ca7ca3e0325c8b9 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 12 Feb 2025 20:16:31 +0200 Subject: [PATCH 33/44] PR review: cleanup Eip7702Support assembly usage. --- contracts/core/Eip7702Support.sol | 29 +++++++++++++---------------- reports/gas-checker.txt | 28 ++++++++++++++-------------- scripts/geth.sh | 4 ++-- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol index 98cf14b16..18b740f17 100644 --- a/contracts/core/Eip7702Support.sol +++ b/contracts/core/Eip7702Support.sol @@ -1,11 +1,13 @@ pragma solidity ^0.8; +// SPDX-License-Identifier: MIT +// solhint-disable no-inline-assembly import "../interfaces/PackedUserOperation.sol"; import "../core/UserOperationLib.sol"; -// SPDX-License-Identifier: MIT + // EIP-7702 code prefix. Also, we use this prefix as a marker in the initCode. To specify this account is EIP-7702. -uint256 constant EIP7702_PREFIX = 0xef0100; +bytes3 constant EIP7702_PREFIX = 0xef0100; using UserOperationLib for PackedUserOperation; @@ -28,14 +30,13 @@ uint256 constant EIP7702_PREFIX = 0xef0100; if (initCode.length < 2) { return false; } - uint256 initCodeStart; - // solhint-disable-next-line no-inline-assembly + 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 "0xff0100" (padded with zeros) - // initCode can be shorter (e.g. only 3), but then it is already zero-padded. - return (initCodeStart >> (256 - 160)) == ((EIP7702_PREFIX << (160 - 24))); + return initCodeStart == bytes20(EIP7702_PREFIX); } /** @@ -44,22 +45,18 @@ uint256 constant EIP7702_PREFIX = 0xef0100; **/ function _getEip7702Delegate(address sender) view returns (address) { - uint256 senderCode; + bytes32 senderCode; - // solhint-disable-next-line no-inline-assembly assembly ("memory-safe") { - extcodecopy(sender, 0, 0, 32) + extcodecopy(sender, 0, 0, 23) senderCode := mload(0) } - // senderCode is the first 32 bytes of the sender's code - // If it is an EIP-7702 delegate, then top 24 bits are the EIP7702_PREFIX - // next 160 bytes are the delegate address - if (senderCode >> (256 - 24) != EIP7702_PREFIX) { + // 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"); - //temp: sanity check for current EIP-7702 implementation. - require(sender.code.length == 23, "EIP-7702 delegate-length"); revert("not an EIP-7702 delegate"); } - return address(uint160(senderCode >> (256 - 160 - 24))); + return address(bytes20(senderCode << 24)); } diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 4ea66d5fa..6b1ed469c 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 │ 77802 │ │ ║ +║ simple │ 1 │ 77790 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41950 │ 12691 ║ +║ simple - diff from previous │ 2 │ │ 41938 │ 12679 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 455524 │ │ ║ +║ simple │ 10 │ 455476 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 42011 │ 12752 ║ +║ simple - diff from previous │ 11 │ │ 42023 │ 12764 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ simple paymaster │ 1 │ 83629 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40514 │ 11255 ║ +║ simple paymaster with diff │ 2 │ │ 40490 │ 11231 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 448242 │ │ ║ +║ simple paymaster │ 10 │ 448182 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40559 │ 11300 ║ +║ simple paymaster with diff │ 11 │ │ 40523 │ 11264 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167561 │ │ ║ +║ big tx 5k │ 1 │ 167549 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 131175 │ 16473 ║ +║ big tx - diff from previous │ 2 │ │ 131163 │ 16461 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1348210 │ │ ║ +║ big tx 5k │ 10 │ 1348162 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131200 │ 16498 ║ +║ big tx - diff from previous │ 11 │ │ 131224 │ 16522 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84984 │ │ ║ +║ paymaster+postOp │ 1 │ 84972 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41844 │ 12585 ║ +║ paymaster+postOp with diff │ 2 │ │ 41856 │ 12597 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp │ 10 │ 461665 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41859 │ 12600 ║ +║ paymaster+postOp with diff │ 11 │ │ 41871 │ 12612 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/scripts/geth.sh b/scripts/geth.sh index 483aa063b..b82a0d2a7 100755 --- a/scripts/geth.sh +++ b/scripts/geth.sh @@ -3,5 +3,5 @@ name=geth-$$ trap "echo killing docker; docker kill $name 2> /dev/null" EXIT port=$1 shift -params="--http --http.api personal,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 dtr22/geth7702 $params +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 From fac94f7e42b2160fec319af5c549d8564c73eeb6 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 13 Feb 2025 16:13:41 +0200 Subject: [PATCH 34/44] update comments --- contracts/core/Eip7702Support.sol | 5 ++--- reports/gas-checker.txt | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol index 18b740f17..75e06fbe8 100644 --- a/contracts/core/Eip7702Support.sol +++ b/contracts/core/Eip7702Support.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8; import "../interfaces/PackedUserOperation.sol"; import "../core/UserOperationLib.sol"; - // EIP-7702 code prefix. Also, we use this prefix as a marker in the initCode. To specify this account is EIP-7702. bytes3 constant EIP7702_PREFIX = 0xef0100; @@ -24,7 +23,7 @@ bytes3 constant EIP7702_PREFIX = 0xef0100; return keccak256(abi.encodePacked(delegate, initCode[20 :])); } - +// check if this initCode is EIP-7702: starts with EIP7702_PREFIX. function _isEip7702InitCode(bytes calldata initCode) pure returns (bool) { if (initCode.length < 2) { @@ -41,7 +40,7 @@ bytes3 constant EIP7702_PREFIX = 0xef0100; /** * get the EIP-7702 delegate from contract code. - * requires EXTCODECOPY pr: https://github.com/ethereum/EIPs/pull/9248 (not yet merged or implemented) + * must only be used if _isEip7702InitCode(initCode) is true. **/ function _getEip7702Delegate(address sender) view returns (address) { diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 6b1ed469c..94752d9ef 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 │ 77790 │ │ ║ +║ simple │ 1 │ 77802 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41938 │ 12679 ║ +║ simple - diff from previous │ 2 │ │ 41926 │ 12667 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 455476 │ │ ║ +║ simple │ 10 │ 455440 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 42023 │ 12764 ║ +║ simple - diff from previous │ 11 │ │ 42047 │ 12788 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ simple paymaster │ 1 │ 83629 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40490 │ 11231 ║ +║ simple paymaster with diff │ 2 │ │ 40514 │ 11255 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 448182 │ │ ║ +║ simple paymaster │ 10 │ 448242 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40523 │ 11264 ║ +║ simple paymaster with diff │ 11 │ │ 40511 │ 11252 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167549 │ │ ║ +║ big tx 5k │ 1 │ 167561 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 131163 │ 16461 ║ +║ big tx - diff from previous │ 2 │ │ 131175 │ 16473 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1348162 │ │ ║ +║ big tx 5k │ 10 │ 1348222 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131224 │ 16522 ║ +║ big tx - diff from previous │ 11 │ │ 131164 │ 16462 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84972 │ │ ║ +║ paymaster+postOp │ 1 │ 84984 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41856 │ 12597 ║ +║ paymaster+postOp with diff │ 2 │ │ 41832 │ 12573 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 461665 │ │ ║ +║ paymaster+postOp │ 10 │ 461737 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41871 │ 12612 ║ +║ paymaster+postOp with diff │ 11 │ │ 41883 │ 12624 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ From 7ee78c42342fdb698b7dd813d7b4175edd8335d3 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Tue, 11 Feb 2025 15:45:25 +0200 Subject: [PATCH 35/44] AA-525 add Simple7702Account Sample ERC-4337 account that uses EIP-7702. Supports also direct (batched) calls from the EOA account itself. --- contracts/samples/Simple7702Account.sol | 84 +++++++++++++ test/eip7702-wallet.test.ts | 151 ++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 contracts/samples/Simple7702Account.sol create mode 100644 test/eip7702-wallet.test.ts diff --git a/contracts/samples/Simple7702Account.sol b/contracts/samples/Simple7702Account.sol new file mode 100644 index 000000000..475721105 --- /dev/null +++ b/contracts/samples/Simple7702Account.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +// based on: https://gist.github.com/frangio/e40305b9f99de290b73750dff5ebe50a +pragma solidity ^0.8; + +import "../interfaces/PackedUserOperation.sol"; +import "../core/Helpers.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/interfaces/IERC1271.sol"; +import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../core/BaseAccount.sol"; + +/** + * Simple7702Account.sol + * A minimal account to be used with EIP-7702 (for batching) and ERC-4337 (for gas sponsoring) + */ +contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721Holder { + + // temporary address of entryPoint v0.8 + function entryPoint() public pure override returns (IEntryPoint) { + return IEntryPoint(0x6F4F5099a64044D69EB7419d66760fD4106fcE3C); + } + + /** + * Make this account callable through ERC-4337 EntryPoint. + * The UserOperation should be signed by this account's private key. + */ + function _validateSignature( + PackedUserOperation calldata userOp, + bytes32 userOpHash + ) internal virtual override returns (uint256 validationData) { + + if (address(this) != ECDSA.recover(userOpHash, userOp.signature)) { + return SIG_VALIDATION_FAILED; + } + return 0; + } + + function _requireFromSelfOrEntryPoint() internal view virtual { + require( + msg.sender == address(this) || + msg.sender == address(entryPoint()), + "not from self or EntryPoint" + ); + } + + struct Call { + address target; + uint256 value; + bytes data; + } + + function execute(Call[] calldata calls) external { + _requireFromSelfOrEntryPoint(); + + for (uint256 i = 0; i < calls.length; i++) { + Call calldata call = calls[i]; + (bool ok, bytes memory ret) = call.target.call{value: call.value}(call.data); + if (!ok) { + // solhint-disable-next-line no-inline-assembly + assembly { revert(add(ret, 32), mload(ret)) } + } + } + } + + function supportsInterface(bytes4 id) public override(ERC1155Holder, IERC165) pure returns (bool) { + return + id == type(IERC165).interfaceId || + id == type(IAccount).interfaceId || + id == type(IERC1271).interfaceId || + id == type(IERC1155Receiver).interfaceId || + id == type(IERC721Receiver).interfaceId; + } + + //deliberately return the same signature as returned by the EOA itself: This way, + // ERC-1271 can be used regardless if the account currently has this code or not. + function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) { + return ECDSA.recover(hash, signature) == address(this) ? this.isValidSignature.selector : bytes4(0); + } + + receive() external payable { + } +} diff --git a/test/eip7702-wallet.test.ts b/test/eip7702-wallet.test.ts new file mode 100644 index 000000000..529a1f567 --- /dev/null +++ b/test/eip7702-wallet.test.ts @@ -0,0 +1,151 @@ +import { expect } from 'chai' + +import { Simple7702Account, Simple7702Account__factory, EntryPoint, TestPaymasterAcceptAll__factory } from '../typechain' +import { createAccountOwner, createAddress, deployEntryPoint } from './testutils' +import { fillAndSign, packUserOp } from './UserOp' +import { hexConcat, parseEther } from 'ethers/lib/utils' +import { signEip7702Authorization } from './eip7702helpers' +import { GethExecutable } from './GethExecutable' +import { Wallet } from 'ethers' + +describe('Simple7702Account.sol', function () { + // can't deploy coverage "entrypoint" on geth (contract too large) + if (process.env.COVERAGE != null) { + return + } + + let entryPoint: EntryPoint + + let eip7702delegate: Simple7702Account + let geth: GethExecutable + + before(async function () { + geth = new GethExecutable() + await geth.init() + + entryPoint = await deployEntryPoint(geth.provider) + + eip7702delegate = await new Simple7702Account__factory(geth.provider.getSigner()).deploy() + expect(await eip7702delegate.entryPoint()).to.equal(entryPoint.address, 'fix entryPoint in Simple7702Account.sol') + console.log('set eip7702delegate=', eip7702delegate.address) + }) + + after(() => { + geth.done() + }) + + describe('sanity: normal 7702 batching', () => { + let eoa: Wallet + before(async () => { + eoa = createAccountOwner(geth.provider) + + const auth = await signEip7702Authorization(eoa, { + chainId: 0, + nonce: 0, + address: eip7702delegate.address + }) + const sendVal = parseEther('10') + const tx = { + to: eoa.address, + value: sendVal.toHexString(), + gas: 1e6, + authorizationList: [auth] + } + await geth.sendTx(tx) + expect(await geth.provider.getBalance(eoa.address)).to.equal(sendVal) + expect(await geth.provider.getCode(eoa.address)).to.equal(hexConcat(['0xef0100', eip7702delegate.address])) + }) + + it('should fail call from another account', async () => { + const wallet1 = Simple7702Account__factory.connect(eoa.address, geth.provider.getSigner()) + await expect(wallet1.execute([])).to.revertedWith('not from self or EntryPoint') + }) + + it('should succeed sending a batch', async () => { + // submit a batch + const wallet2 = Simple7702Account__factory.connect(eoa.address, eoa) + console.log('eoa balance=', await geth.provider.getBalance(eoa.address)) + + const addr1 = createAddress() + const addr2 = createAddress() + + await wallet2.execute([{ + target: addr1, value: 1, data: '0x' + }, { + target: addr2, value: 2, data: '0x' + }]).then(async tx => tx.wait()) + expect(await geth.provider.getBalance(addr1)).to.equal(1) + expect(await geth.provider.getBalance(addr2)).to.equal(2) + }) + }) + + it('should be able to use EntryPoint without paymaster', async () => { + const addr1 = createAddress() + const eoa = createAccountOwner(geth.provider) + + const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{ + target: addr1, + value: 1, + data: '0x' + }]]) + const userop = await fillAndSign({ + sender: eoa.address, + initCode: '0xef01', + nonce: 0, + callData + }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) + + await geth.sendTx({ to: eoa.address, value: parseEther('1') }) + const auth = await signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address }) + const beneficiary = createAddress() + // submit separate tx with tuple: geth's estimateGas doesn't work, and its easier to detect thrown errors.. + await geth.sendTx({ + to: entryPoint.address, + data: '0x', + gas: 1000000, + authorizationList: [auth] + }) + const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary]) + const tx = { + to: entryPoint.address, + data: handleOps + } + await geth.sendTx(tx) + }) + + it('should use EntryPoint with paymaster', async () => { + const addr1 = createAddress() + const eoa = createAccountOwner(geth.provider) + const paymaster = await new TestPaymasterAcceptAll__factory(geth.provider.getSigner()).deploy(entryPoint.address) + await paymaster.deposit({ value: parseEther('1') }) + const callData = eip7702delegate.interface.encodeFunctionData('execute', [[{ + target: addr1, + value: 1, + data: '0x' + }]]) + const userop = await fillAndSign({ + sender: eoa.address, + paymaster: paymaster.address, + initCode: '0xef01', + nonce: 0, + callData + }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) + + const auth = await signEip7702Authorization(eoa, { chainId: 0, nonce: 0, address: eip7702delegate.address }) + const beneficiary = createAddress() + console.log('delegate=', eip7702delegate.address) + // submit separate tx with tuple: geth's estimateGas doesn't work, and its easier to detect thrown errors.. + await geth.sendTx({ + to: entryPoint.address, + data: '0x', + gas: 1000000, + authorizationList: [auth] + }) + const handleOps = entryPoint.interface.encodeFunctionData('handleOps', [[packUserOp(userop)], beneficiary]) + const tx = { + to: entryPoint.address, + data: handleOps + } + await geth.sendTx(tx) + }) +}) From f177e328f9f014b602f224fd2952cf0d16a24f45 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 12 Feb 2025 19:24:41 +0200 Subject: [PATCH 36/44] Update Simple7702Account.sol --- contracts/samples/Simple7702Account.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/samples/Simple7702Account.sol b/contracts/samples/Simple7702Account.sol index 475721105..1945760a1 100644 --- a/contracts/samples/Simple7702Account.sol +++ b/contracts/samples/Simple7702Account.sol @@ -73,8 +73,6 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC id == type(IERC721Receiver).interfaceId; } - //deliberately return the same signature as returned by the EOA itself: This way, - // ERC-1271 can be used regardless if the account currently has this code or not. function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) { return ECDSA.recover(hash, signature) == address(this) ? this.isValidSignature.selector : bytes4(0); } From 6fdff8389e07f224991e7dcb6cdd02a2347c70fc Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Wed, 12 Feb 2025 20:44:47 +0200 Subject: [PATCH 37/44] AA-521-ep-7702 --- contracts/samples/Simple7702Account.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/samples/Simple7702Account.sol b/contracts/samples/Simple7702Account.sol index 1945760a1..2536346e4 100644 --- a/contracts/samples/Simple7702Account.sol +++ b/contracts/samples/Simple7702Account.sol @@ -19,7 +19,7 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC // temporary address of entryPoint v0.8 function entryPoint() public pure override returns (IEntryPoint) { - return IEntryPoint(0x6F4F5099a64044D69EB7419d66760fD4106fcE3C); + return IEntryPoint(0xe5fDb4B271ef97F075cFDB9713f8cff6Dbf5F325); } /** From aa0309c3582e229596a9c781370d61a11a12ec8b Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 13 Feb 2025 16:20:09 +0200 Subject: [PATCH 38/44] added fallback --- contracts/samples/Simple7702Account.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contracts/samples/Simple7702Account.sol b/contracts/samples/Simple7702Account.sol index 2536346e4..679e8bb27 100644 --- a/contracts/samples/Simple7702Account.sol +++ b/contracts/samples/Simple7702Account.sol @@ -19,7 +19,7 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC // temporary address of entryPoint v0.8 function entryPoint() public pure override returns (IEntryPoint) { - return IEntryPoint(0xe5fDb4B271ef97F075cFDB9713f8cff6Dbf5F325); + return IEntryPoint(0x85fF6c675397CF96412c217e405F2175c249a6B2); } /** @@ -77,6 +77,10 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC return ECDSA.recover(hash, signature) == address(this) ? this.isValidSignature.selector : bytes4(0); } + // accept incoming calls (with our without value), to mimic an EOA. + fallback() external payable { + } + receive() external payable { } } From de69b3b1faa086a9686d422e8fc819260da65b8b Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sun, 16 Feb 2025 12:55:09 +0200 Subject: [PATCH 39/44] refactor library. split constants. --- contracts/core/Eip7702Support.sol | 32 ++++++++++++++++++------------- contracts/core/EntryPoint.sol | 4 ++-- contracts/core/SenderCreator.sol | 18 +++++++++-------- contracts/test/TestUtil.sol | 2 +- reports/gas-checker.txt | 30 ++++++++++++++--------------- test/UserOp.ts | 6 +++--- test/entrypoint-7702.test.ts | 28 +++++++++++++-------------- 7 files changed, 64 insertions(+), 56 deletions(-) diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol index 75e06fbe8..3f82ce848 100644 --- a/contracts/core/Eip7702Support.sol +++ b/contracts/core/Eip7702Support.sol @@ -5,13 +5,18 @@ pragma solidity ^0.8; import "../interfaces/PackedUserOperation.sol"; import "../core/UserOperationLib.sol"; -// EIP-7702 code prefix. Also, we use this prefix as a marker in the initCode. To specify this account is EIP-7702. -bytes3 constant EIP7702_PREFIX = 0xef0100; +library Eip7702Support { + + // EIP-7702 code prefix. Also, we use this prefix as a marker in the initCode. To specify this account is EIP-7702. + 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) view returns (bytes32) { + //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; @@ -23,8 +28,8 @@ bytes3 constant EIP7702_PREFIX = 0xef0100; return keccak256(abi.encodePacked(delegate, initCode[20 :])); } -// check if this initCode is EIP-7702: starts with EIP7702_PREFIX. - function _isEip7702InitCode(bytes calldata initCode) pure returns (bool) { + // check if this initCode is EIP-7702: starts with EIP7702_PREFIX. + function _isEip7702InitCode(bytes calldata initCode) internal pure returns (bool) { if (initCode.length < 2) { return false; @@ -34,15 +39,15 @@ bytes3 constant EIP7702_PREFIX = 0xef0100; assembly ("memory-safe") { initCodeStart := calldataload(initCode.offset) } - // make sure first 20 bytes of initCode are "0xff0100" (padded with zeros) - return initCodeStart == bytes20(EIP7702_PREFIX); + // 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) view returns (address) { + /** + * 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; @@ -59,3 +64,4 @@ bytes3 constant EIP7702_PREFIX = 0xef0100; } return address(bytes20(senderCode << 24)); } +} diff --git a/contracts/core/EntryPoint.sol b/contracts/core/EntryPoint.sol index 12f316a8d..7f8bbc3bf 100644 --- a/contracts/core/EntryPoint.sol +++ b/contracts/core/EntryPoint.sol @@ -379,7 +379,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT function getUserOpHash( PackedUserOperation calldata userOp ) public view returns (bytes32) { - bytes32 overrideInitCodeHash = _getEip7702InitCodeHashOverride(userOp); + bytes32 overrideInitCodeHash = Eip7702Support._getEip7702InitCodeHashOverride(userOp); return MessageHashUtils.toTypedDataHash(getDomainSeparatorV4(), userOp.hash(overrideInitCodeHash)); } @@ -443,7 +443,7 @@ contract EntryPoint is IEntryPoint, StakeManager, NonceManager, ReentrancyGuardT ) internal { if (initCode.length != 0) { address sender = opInfo.mUserOp.sender; - if ( _isEip7702InitCode(initCode) ) { + 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:]); diff --git a/contracts/core/SenderCreator.sol b/contracts/core/SenderCreator.sol index 9a1ce9dd8..fa9e7510d 100644 --- a/contracts/core/SenderCreator.sol +++ b/contracts/core/SenderCreator.sol @@ -1,9 +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"; -import {IEntryPoint} from "../interfaces/IEntryPoint.sol"; /** * Helper contract for EntryPoint, to call userOp.initCode from a "neutral" address, @@ -16,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, @@ -26,11 +30,10 @@ contract SenderCreator is ISenderCreator { bytes calldata initCode ) external returns (address sender) { require(msg.sender == entryPoint, "AA97 should call from EntryPoint"); - address factory = address(bytes20(initCode[0:20])); + address factory = address(bytes20(initCode[0 : 20])); - bytes memory initCallData = initCode[20:]; + bytes memory initCallData = initCode[20 :]; bool success; - /* solhint-disable no-inline-assembly */ assembly ("memory-safe") { success := call( gas(), @@ -47,18 +50,17 @@ contract SenderCreator is ISenderCreator { } } - // use initCode to initialize an EIP-7702 account + // 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"); - // solhint-disable-next-line avoid-low-level-calls bool success = Exec.call(sender, 0, initCallData, gasleft()); if (!success) { - bytes memory result = Exec.getReturnData(2048); - revert IEntryPoint.FailedOpWithRevert(0,"AA13 EIP7702 sender init failed", result); + bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN); + revert IEntryPoint.FailedOpWithRevert(0, "AA13 EIP7702 sender init failed", result); } } } diff --git a/contracts/test/TestUtil.sol b/contracts/test/TestUtil.sol index 6e99bcc3a..129656086 100644 --- a/contracts/test/TestUtil.sol +++ b/contracts/test/TestUtil.sol @@ -12,6 +12,6 @@ contract TestUtil { } function isEip7702InitCode(bytes calldata initCode) external pure returns (bool) { - return _isEip7702InitCode(initCode); + return Eip7702Support._isEip7702InitCode(initCode); } } diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 94752d9ef..2e6c06957 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -4,7 +4,7 @@ ╔══════════════════════════╤════════╗ ║ gas estimate "simple" │ 29259 ║ ╟──────────────────────────┼────────╢ -║ gas estimate "big tx 5k" │ 114702 ║ +║ gas estimate "big tx 5k" │ 114690 ║ ╚══════════════════════════╧════════╝ ╔════════════════════════════════╤═══════╤═══════════════╤════════════════╤═════════════════════╗ @@ -14,34 +14,34 @@ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ simple │ 1 │ 77802 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41926 │ 12667 ║ +║ simple - diff from previous │ 2 │ │ 41950 │ 12691 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 455440 │ │ ║ +║ simple │ 10 │ 455500 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 42047 │ 12788 ║ +║ simple - diff from previous │ 11 │ │ 42023 │ 12764 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83629 │ │ ║ +║ simple paymaster │ 1 │ 83617 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40514 │ 11255 ║ +║ simple paymaster with diff │ 2 │ │ 40466 │ 11207 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 448242 │ │ ║ +║ simple paymaster │ 10 │ 448206 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40511 │ 11252 ║ +║ simple paymaster with diff │ 11 │ │ 40547 │ 11288 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167561 │ │ ║ +║ big tx 5k │ 1 │ 167537 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 131175 │ 16473 ║ +║ big tx - diff from previous │ 2 │ │ 131175 │ 16485 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1348222 │ │ ║ +║ big tx 5k │ 10 │ 1348138 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131164 │ 16462 ║ +║ big tx - diff from previous │ 11 │ │ 131176 │ 16486 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp │ 1 │ 84984 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41832 │ 12573 ║ +║ paymaster+postOp with diff │ 2 │ │ 41844 │ 12585 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 461737 │ │ ║ +║ paymaster+postOp │ 10 │ 461653 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41883 │ 12624 ║ +║ paymaster+postOp with diff │ 11 │ │ 41907 │ 12648 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ diff --git a/test/UserOp.ts b/test/UserOp.ts index c85c1130e..75da30616 100644 --- a/test/UserOp.ts +++ b/test/UserOp.ts @@ -33,7 +33,7 @@ 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 EIP7702_PREFIX = '0xef01' +export const INITCODE_EIP7702_MARKER = '0x7702' export function packUserOp (userOp: UserOperation): PackedUserOperation { const accountGasLimits = packAccountGasLimits(userOp.verificationGasLimit, userOp.callGasLimit) @@ -90,12 +90,12 @@ export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: n } export function isEip7702UserOp (op: UserOperation): boolean { - return op.initCode != null && hexlify(op.initCode).startsWith(EIP7702_PREFIX) + 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 EIP7702_PREFIX') + throw new Error('initCode should start with INITCODE_EIP7702_MARKER') } let initCode = hexlify(op.initCode) if (hexDataLength(initCode) < 20) { diff --git a/test/entrypoint-7702.test.ts b/test/entrypoint-7702.test.ts index fa12d02d2..4cc7f960b 100644 --- a/test/entrypoint-7702.test.ts +++ b/test/entrypoint-7702.test.ts @@ -16,7 +16,7 @@ import { deployEntryPoint } from './testutils' import { - EIP7702_PREFIX, + INITCODE_EIP7702_MARKER, fillAndSign, fillSignAndPack, fillUserOpDefaults, @@ -64,16 +64,16 @@ describe('EntryPoint EIP-7702 tests', function () { [1, 10, 20, 30].forEach(pad => it(`should accept initCode with zero pad ${pad}`, async () => { - expect(await testUtil.isEip7702InitCode(EIP7702_PREFIX + '00'.repeat(pad))).to.be.true + 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(EIP7702_PREFIX)).to.be.true + 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 = EIP7702_PREFIX + '0'.repeat(40 - EIP7702_PREFIX.length) + '01' + 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 }) @@ -116,36 +116,36 @@ describe('EntryPoint EIP-7702 tests', function () { const hash = getUserOpHash({ ...userop, initCode: mockDelegate }, entryPoint.address, chainId) expect(getUserOpHashWithEip7702({ ...userop, - initCode: EIP7702_PREFIX + 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: '0xef0100'.padEnd(42, '0') + 'b1ab1a' + 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: EIP7702_PREFIX } + 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: EIP7702_PREFIX } + 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 EIP7702_PREFIX tailed with zeros only, ', async () => { - const op_zero_tail = { ...userop, initCode: EIP7702_PREFIX + '00'.repeat(10) } + 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 = EIP7702_PREFIX + '00'.repeat(30) + 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)) }) @@ -177,7 +177,7 @@ describe('EntryPoint EIP-7702 tests', function () { const eip7702userOp = await fillSignAndPack({ sender: eoa.address, nonce: 0, - initCode: EIP7702_PREFIX // not init function, just delegate + initCode: INITCODE_EIP7702_MARKER // not init function, just delegate }, eoa, entryPoint, { eip7702delegate: delegate.address }) const handleOpCall = { to: entryPoint.address, @@ -194,7 +194,7 @@ describe('EntryPoint EIP-7702 tests', function () { const eip7702userOp = await fillAndSign({ sender: eoa.address, nonce: 0, - initCode: EIP7702_PREFIX // not init function, just delegate + initCode: INITCODE_EIP7702_MARKER // not init function, just delegate }, eoa, entryPoint, { eip7702delegate: delegate.address }) const eip7702tuple = await signEip7702Authorization(eoa, { address: delegate.address, @@ -219,7 +219,7 @@ describe('EntryPoint EIP-7702 tests', function () { const eip7702userOp = await fillSignAndPack({ sender: eoa.address, nonce: 0, - initCode: hexConcat([EIP7702_PREFIX + '0'.repeat(42 - EIP7702_PREFIX.length), delegate.interface.encodeFunctionData('testInit')]) + 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, { From d4a28028d91453f2058fed19777539716e82b8f8 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sun, 16 Feb 2025 13:08:44 +0200 Subject: [PATCH 40/44] fix comment --- contracts/core/Eip7702Support.sol | 4 ++-- reports/gas-checker.txt | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol index 3f82ce848..ebf446601 100644 --- a/contracts/core/Eip7702Support.sol +++ b/contracts/core/Eip7702Support.sol @@ -7,10 +7,10 @@ import "../core/UserOperationLib.sol"; library Eip7702Support { - // EIP-7702 code prefix. Also, we use this prefix as a marker in the initCode. To specify this account is EIP-7702. + // EIP-7702 code prefix before delegate address. bytes3 internal constant EIP7702_PREFIX = 0xef0100; - // EIP-7702 initCode marker. To specify this account is EIP-7702. + // EIP-7702 initCode marker, to specify this account is EIP-7702. bytes2 internal constant INITCODE_EIP7702_MARKER = 0x7702; using UserOperationLib for PackedUserOperation; diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index 2e6c06957..fc9daa47b 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -4,7 +4,7 @@ ╔══════════════════════════╤════════╗ ║ gas estimate "simple" │ 29259 ║ ╟──────────────────────────┼────────╢ -║ gas estimate "big tx 5k" │ 114690 ║ +║ gas estimate "big tx 5k" │ 114702 ║ ╚══════════════════════════╧════════╝ ╔════════════════════════════════╤═══════╤═══════════════╤════════════════╤═════════════════════╗ @@ -14,34 +14,34 @@ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ simple │ 1 │ 77802 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41950 │ 12691 ║ +║ simple - diff from previous │ 2 │ │ 41902 │ 12643 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 455500 │ │ ║ +║ simple │ 10 │ 455476 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 42023 │ 12764 ║ +║ simple - diff from previous │ 11 │ │ 41999 │ 12740 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83617 │ │ ║ +║ simple paymaster │ 1 │ 83629 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40466 │ 11207 ║ +║ simple paymaster with diff │ 2 │ │ 40514 │ 11255 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 448206 │ │ ║ +║ simple paymaster │ 10 │ 448242 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40547 │ 11288 ║ +║ simple paymaster with diff │ 11 │ │ 40535 │ 11276 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167537 │ │ ║ +║ big tx 5k │ 1 │ 167561 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 131175 │ 16485 ║ +║ big tx - diff from previous │ 2 │ │ 131175 │ 16473 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1348138 │ │ ║ +║ big tx 5k │ 10 │ 1348174 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131176 │ 16486 ║ +║ big tx - diff from previous │ 11 │ │ 131188 │ 16486 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84984 │ │ ║ +║ paymaster+postOp │ 1 │ 84936 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp with diff │ 2 │ │ 41844 │ 12585 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 461653 │ │ ║ +║ paymaster+postOp │ 10 │ 461641 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 11 │ │ 41907 │ 12648 ║ +║ paymaster+postOp with diff │ 11 │ │ 41883 │ 12624 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ From 539f7b711616b709236295a6f2c4b636a53b3b09 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sun, 16 Feb 2025 13:17:27 +0200 Subject: [PATCH 41/44] merge --- contracts/core/Eip7702Support.sol | 2 +- reports/gas-checker.txt | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contracts/core/Eip7702Support.sol b/contracts/core/Eip7702Support.sol index ebf446601..f86de87e2 100644 --- a/contracts/core/Eip7702Support.sol +++ b/contracts/core/Eip7702Support.sol @@ -28,7 +28,7 @@ library Eip7702Support { return keccak256(abi.encodePacked(delegate, initCode[20 :])); } - // check if this initCode is EIP-7702: starts with EIP7702_PREFIX. + // 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) { diff --git a/reports/gas-checker.txt b/reports/gas-checker.txt index fc9daa47b..e92cc21d9 100644 --- a/reports/gas-checker.txt +++ b/reports/gas-checker.txt @@ -14,33 +14,33 @@ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ simple │ 1 │ 77802 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 2 │ │ 41902 │ 12643 ║ +║ simple - diff from previous │ 2 │ │ 41938 │ 12679 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple │ 10 │ 455476 │ │ ║ +║ simple │ 10 │ 455500 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple - diff from previous │ 11 │ │ 41999 │ 12740 ║ +║ simple - diff from previous │ 11 │ │ 42047 │ 12788 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 1 │ 83629 │ │ ║ +║ simple paymaster │ 1 │ 83641 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 2 │ │ 40514 │ 11255 ║ +║ simple paymaster with diff │ 2 │ │ 40490 │ 11231 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster │ 10 │ 448242 │ │ ║ +║ simple paymaster │ 10 │ 448254 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ simple paymaster with diff │ 11 │ │ 40535 │ 11276 ║ +║ simple paymaster with diff │ 11 │ │ 40547 │ 11288 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 1 │ 167561 │ │ ║ +║ big tx 5k │ 1 │ 167549 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 2 │ │ 131175 │ 16473 ║ +║ big tx - diff from previous │ 2 │ │ 131187 │ 16485 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx 5k │ 10 │ 1348174 │ │ ║ +║ big tx 5k │ 10 │ 1348222 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ big tx - diff from previous │ 11 │ │ 131188 │ 16486 ║ +║ big tx - diff from previous │ 11 │ │ 131176 │ 16474 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 1 │ 84936 │ │ ║ +║ paymaster+postOp │ 1 │ 84984 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp with diff │ 2 │ │ 41844 │ 12585 ║ +║ paymaster+postOp with diff │ 2 │ │ 41832 │ 12573 ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ -║ paymaster+postOp │ 10 │ 461641 │ │ ║ +║ paymaster+postOp │ 10 │ 461677 │ │ ║ ╟────────────────────────────────┼───────┼───────────────┼────────────────┼─────────────────────╢ ║ paymaster+postOp with diff │ 11 │ │ 41883 │ 12624 ║ ╚════════════════════════════════╧═══════╧═══════════════╧════════════════╧═════════════════════╝ From 7fe744c03d0109bb0467a4a084b0f867bd224c83 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sun, 16 Feb 2025 13:28:17 +0200 Subject: [PATCH 42/44] merge fixes --- contracts/samples/Simple7702Account.sol | 2 +- test/eip7702-wallet.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/samples/Simple7702Account.sol b/contracts/samples/Simple7702Account.sol index 679e8bb27..29545dad2 100644 --- a/contracts/samples/Simple7702Account.sol +++ b/contracts/samples/Simple7702Account.sol @@ -19,7 +19,7 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC // temporary address of entryPoint v0.8 function entryPoint() public pure override returns (IEntryPoint) { - return IEntryPoint(0x85fF6c675397CF96412c217e405F2175c249a6B2); + return IEntryPoint(0xE2b1C20D236ECe93b91f0656A9428C072e3F88Ad); } /** diff --git a/test/eip7702-wallet.test.ts b/test/eip7702-wallet.test.ts index 529a1f567..7579abc93 100644 --- a/test/eip7702-wallet.test.ts +++ b/test/eip7702-wallet.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai' import { Simple7702Account, Simple7702Account__factory, EntryPoint, TestPaymasterAcceptAll__factory } from '../typechain' import { createAccountOwner, createAddress, deployEntryPoint } from './testutils' -import { fillAndSign, packUserOp } from './UserOp' +import { fillAndSign, INITCODE_EIP7702_MARKER, packUserOp } from './UserOp' import { hexConcat, parseEther } from 'ethers/lib/utils' import { signEip7702Authorization } from './eip7702helpers' import { GethExecutable } from './GethExecutable' @@ -90,7 +90,7 @@ describe('Simple7702Account.sol', function () { }]]) const userop = await fillAndSign({ sender: eoa.address, - initCode: '0xef01', + initCode: INITCODE_EIP7702_MARKER, nonce: 0, callData }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) @@ -126,7 +126,7 @@ describe('Simple7702Account.sol', function () { const userop = await fillAndSign({ sender: eoa.address, paymaster: paymaster.address, - initCode: '0xef01', + initCode: INITCODE_EIP7702_MARKER, nonce: 0, callData }, eoa, entryPoint, { eip7702delegate: eip7702delegate.address }) From 35204f7187e4da4f1da27d582907acb5a978cc14 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sun, 16 Feb 2025 13:59:44 +0200 Subject: [PATCH 43/44] refactor --- contracts/samples/Simple7702Account.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/contracts/samples/Simple7702Account.sol b/contracts/samples/Simple7702Account.sol index 29545dad2..e33783aba 100644 --- a/contracts/samples/Simple7702Account.sol +++ b/contracts/samples/Simple7702Account.sol @@ -31,10 +31,7 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC bytes32 userOpHash ) internal virtual override returns (uint256 validationData) { - if (address(this) != ECDSA.recover(userOpHash, userOp.signature)) { - return SIG_VALIDATION_FAILED; - } - return 0; + return _checkSignature(userOpHash, userOp.signature) ? 0 : SIG_VALIDATION_FAILED; } function _requireFromSelfOrEntryPoint() internal view virtual { @@ -59,7 +56,7 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC (bool ok, bytes memory ret) = call.target.call{value: call.value}(call.data); if (!ok) { // solhint-disable-next-line no-inline-assembly - assembly { revert(add(ret, 32), mload(ret)) } + assembly {revert(add(ret, 32), mload(ret))} } } } @@ -74,7 +71,11 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC } function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) { - return ECDSA.recover(hash, signature) == address(this) ? this.isValidSignature.selector : bytes4(0); + return _checkSignature(hash, signature) ? this.isValidSignature.selector : bytes4(0); + } + + function _checkSignature(bytes32 hash, bytes memory signature) internal view returns (bool) { + return ECDSA.recover(hash, signature) == address(this); } // accept incoming calls (with our without value), to mimic an EOA. From d1318710debdd5f7423db15433c9de907e934ed6 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sun, 16 Feb 2025 14:54:31 +0200 Subject: [PATCH 44/44] refactor SimpleAccount --- contracts/samples/Simple7702Account.sol | 33 ++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/contracts/samples/Simple7702Account.sol b/contracts/samples/Simple7702Account.sol index e33783aba..0649f263a 100644 --- a/contracts/samples/Simple7702Account.sol +++ b/contracts/samples/Simple7702Account.sol @@ -1,14 +1,12 @@ // SPDX-License-Identifier: MIT -// based on: https://gist.github.com/frangio/e40305b9f99de290b73750dff5ebe50a pragma solidity ^0.8; -import "../interfaces/PackedUserOperation.sol"; -import "../core/Helpers.sol"; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "@openzeppelin/contracts/interfaces/IERC1271.sol"; import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../core/Helpers.sol"; import "../core/BaseAccount.sol"; /** @@ -17,6 +15,12 @@ import "../core/BaseAccount.sol"; */ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC721Holder { + struct Call { + address target; + uint256 value; + bytes data; + } + // temporary address of entryPoint v0.8 function entryPoint() public pure override returns (IEntryPoint) { return IEntryPoint(0xE2b1C20D236ECe93b91f0656A9428C072e3F88Ad); @@ -34,6 +38,14 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC return _checkSignature(userOpHash, userOp.signature) ? 0 : SIG_VALIDATION_FAILED; } + function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) { + return _checkSignature(hash, signature) ? this.isValidSignature.selector : bytes4(0); + } + + function _checkSignature(bytes32 hash, bytes memory signature) internal view returns (bool) { + return ECDSA.recover(hash, signature) == address(this); + } + function _requireFromSelfOrEntryPoint() internal view virtual { require( msg.sender == address(this) || @@ -42,11 +54,6 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC ); } - struct Call { - address target; - uint256 value; - bytes data; - } function execute(Call[] calldata calls) external { _requireFromSelfOrEntryPoint(); @@ -70,15 +77,7 @@ contract Simple7702Account is BaseAccount, IERC165, IERC1271, ERC1155Holder, ERC id == type(IERC721Receiver).interfaceId; } - function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) { - return _checkSignature(hash, signature) ? this.isValidSignature.selector : bytes4(0); - } - - function _checkSignature(bytes32 hash, bytes memory signature) internal view returns (bool) { - return ECDSA.recover(hash, signature) == address(this); - } - - // accept incoming calls (with our without value), to mimic an EOA. + // accept incoming calls (with or without value), to mimic an EOA. fallback() external payable { }