diff --git a/packages/bundler/src/BundlerServer.ts b/packages/bundler/src/BundlerServer.ts index 492904d8..372f1d84 100644 --- a/packages/bundler/src/BundlerServer.ts +++ b/packages/bundler/src/BundlerServer.ts @@ -95,8 +95,7 @@ export class BundlerServer { callGasLimit: 0, maxFeePerGas: 0, maxPriorityFeePerGas: 0, - signature: '0x', - authorizationList: [] + signature: '0x' } // await EntryPoint__factory.connect(this.config.entryPoint,this.provider).callStatic.addStake(0) try { diff --git a/packages/bundler/src/MethodHandlerERC4337.ts b/packages/bundler/src/MethodHandlerERC4337.ts index 751f80f6..f8b97857 100644 --- a/packages/bundler/src/MethodHandlerERC4337.ts +++ b/packages/bundler/src/MethodHandlerERC4337.ts @@ -24,7 +24,7 @@ import { requireCond, simulationRpcParams, tostr, - unpackUserOp + unpackUserOp, getAuthorizationList } from '@account-abstraction/utils' import { BundlerConfig } from './BundlerConfig' @@ -124,7 +124,7 @@ export class MethodHandlerERC4337 { entryPointInput: string, stateOverride?: StateOverride ): Promise { - if (!this.config.eip7702Support && userOp1.authorizationList != null && userOp1.authorizationList.length !== 0) { + if (!this.config.eip7702Support && userOp1.eip7702auth != null) { throw new Error('EIP-7702 tuples are not supported') } const userOp: UserOperation = { @@ -162,6 +162,7 @@ export class MethodHandlerERC4337 { preOpGas } = returnInfo + const authorizationList = getAuthorizationList(userOp) // todo: use simulateHandleOp for this too... let callGasLimit = await this.provider.send( 'eth_estimateGas', [ @@ -170,7 +171,7 @@ export class MethodHandlerERC4337 { to: userOp.sender, data: userOp.callData, // @ts-ignore - authorizationList: userOp.authorizationList + authorizationList: authorizationList.length === 0 ? null : authorizationList } ] ).then(b => toNumber(b)).catch(err => { @@ -192,12 +193,12 @@ export class MethodHandlerERC4337 { } async sendUserOperation (userOp: UserOperation, entryPointInput: string): Promise { - if (!this.config.eip7702Support && userOp.authorizationList != null && userOp.authorizationList.length !== 0) { + if (!this.config.eip7702Support && userOp.eip7702auth != null) { throw new Error('EIP-7702 tuples are not supported') } await this._validateParameters(userOp, entryPointInput) - debug(`UserOperation: Sender=${userOp.sender} Nonce=${tostr(userOp.nonce)} EntryPoint=${entryPointInput} Paymaster=${userOp.paymaster ?? ''} EIP-7702TuplesSize=${userOp.authorizationList?.length}`) + debug(`UserOperation: Sender=${userOp.sender} Nonce=${tostr(userOp.nonce)} EntryPoint=${entryPointInput} Paymaster=${userOp.paymaster ?? ''} ${userOp.eip7702auth != null ? 'eip-7702 auth' : ''}`) await this.execManager.sendUserOperation(userOp, entryPointInput, false) return await this.entryPoint.getUserOpHash(packUserOp(userOp)) } diff --git a/packages/bundler/src/modules/BundleManager.ts b/packages/bundler/src/modules/BundleManager.ts index b968c1c6..e1ee580d 100644 --- a/packages/bundler/src/modules/BundleManager.ts +++ b/packages/bundler/src/modules/BundleManager.ts @@ -23,7 +23,7 @@ import { getEip7702AuthorizationSigner, mergeStorageMap, packUserOp, - getUserOpHash + getUserOpHash, getAuthorizationList } from '@account-abstraction/utils' import { EventsManager } from './EventsManager' @@ -228,38 +228,36 @@ export class BundleManager implements IBundleManager { const common = new Common({ chain, eips: [2718, 2929, 2930, 7702] }) const authorizationList: AuthorizationList = eip7702Tuples.map(it => { - const res = { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion,@typescript-eslint/no-base-to-string - chainId: `0x${parseInt(it.chainId.toString()).toString(16)}` as PrefixedHexString, - address: it.address as PrefixedHexString, - nonce: toRlpHex(it.nonce as PrefixedHexString), - yParity: toRlpHex(it.yParity as PrefixedHexString), - r: it.r as PrefixedHexString, - s: it.s as PrefixedHexString + return { + chainId: toRlpHex(it.chainId), + address: toRlpHex(it.address), + nonce: toRlpHex(it.nonce), + yParity: toRlpHex(it.yParity), + r: toRlpHex(it.r), + s: toRlpHex(it.s) } - return res }) const txData: EOACode7702TxData = { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - nonce: `0x${tx.nonce!.toString(16)}`, + nonce: hexlify(tx.nonce!) as PrefixedHexString, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - to: tx.to!.toString() as PrefixedHexString, + to: hexlify(tx.to!) as PrefixedHexString, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion value: '0x0', // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - data: tx.data!.toString() as PrefixedHexString, + data: hexlify(tx.data!) as PrefixedHexString, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - chainId: `0x${tx.chainId!.toString(16)}`, + chainId: hexlify(tx.chainId!) as PrefixedHexString, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - maxPriorityFeePerGas: tx.maxPriorityFeePerGas!.toHexString() as PrefixedHexString, + maxPriorityFeePerGas: hexlify(tx.maxPriorityFeePerGas!) as PrefixedHexString, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - maxFeePerGas: tx.maxPriorityFeePerGas!.toHexString() as PrefixedHexString, + maxFeePerGas: hexlify(tx.maxPriorityFeePerGas!) as PrefixedHexString, accessList: [], authorizationList } // TODO: not clear why but 'eth_estimateGas' gives an 'execution reverted' error // txData.gasLimit = await this.provider.send('eth_estimateGas', [txData]) - txData.gasLimit = `0x${(10000000).toString(16)}` + txData.gasLimit = 10_000_000 const objectTx = new EOACode7702Transaction(txData, { common }) const privateKey = Buffer.from( // @ts-ignore @@ -463,17 +461,15 @@ export class BundleManager implements IBundleManager { * @return {boolean} - Returns `true` if the authorizations were successfully merged, otherwise `false`. */ mergeEip7702Authorizations (entry: MempoolEntry, authList: EIP7702Authorization[]): boolean { - for (const eip7702Authorization of entry.userOp.authorizationList ?? []) { + const authorizationList = getAuthorizationList(entry.userOp) + for (const eip7702Authorization of authorizationList) { const existingAuthorization = authList - .find(it => { - return getEip7702AuthorizationSigner(it) === getEip7702AuthorizationSigner(eip7702Authorization) - }) - if (existingAuthorization != null && existingAuthorization.address.toLowerCase() !== eip7702Authorization.address.toLowerCase()) { + .find(it => getEip7702AuthorizationSigner(it) === getEip7702AuthorizationSigner(eip7702Authorization)) + if (existingAuthorization == null) { + authList.push(eip7702Authorization) + } else if (existingAuthorization.address.toLowerCase() !== eip7702Authorization.address.toLowerCase()) { return false } - if (existingAuthorization == null && entry.userOp.authorizationList != null) { - authList.push(...entry.userOp.authorizationList) - } } return true } diff --git a/packages/bundler/test/BundlerManager.test.ts b/packages/bundler/test/BundlerManager.test.ts index 44d46298..a855f33c 100644 --- a/packages/bundler/test/BundlerManager.test.ts +++ b/packages/bundler/test/BundlerManager.test.ts @@ -78,8 +78,7 @@ describe('#BundlerManager', () => { verificationGasLimit: 7, maxFeePerGas: 8, maxPriorityFeePerGas: 9, - preVerificationGas: 10, - authorizationList: [] + preVerificationGas: 10 } const hash = await entryPoint.getUserOpHash(packUserOp(userOp)) @@ -161,8 +160,7 @@ describe('#BundlerManager', () => { verificationGasLimit: '0x50000', maxFeePerGas: '0x0', maxPriorityFeePerGas: '0x0', - preVerificationGas: '0x50000', - authorizationList: [] + preVerificationGas: '0x50000' } const userOp1: UserOperation = { ...cEmptyUserOp, diff --git a/packages/bundler/test/UserOpMethodHandler.test.ts b/packages/bundler/test/UserOpMethodHandler.test.ts index ec319d68..84d44af8 100644 --- a/packages/bundler/test/UserOpMethodHandler.test.ts +++ b/packages/bundler/test/UserOpMethodHandler.test.ts @@ -345,8 +345,7 @@ describe('UserOpMethodHandler', function () { preVerificationGas: 50000, maxFeePerGas: 1e6, maxPriorityFeePerGas: 1e6, - signature: Buffer.from('emit-msg'), - authorizationList: [] + signature: Buffer.from('emit-msg') } await entryPoint.depositTo(acc.address, { value: parseEther('1') }) // await signer.sendTransaction({to:acc.address, value: parseEther('1')}) diff --git a/packages/bundler/test/ValidateManager.test.ts b/packages/bundler/test/ValidateManager.test.ts index 4d7bc48f..129869bf 100644 --- a/packages/bundler/test/ValidateManager.test.ts +++ b/packages/bundler/test/ValidateManager.test.ts @@ -45,8 +45,7 @@ const cEmptyUserOp: UserOperation = { verificationGasLimit: 50000, maxFeePerGas: 0, maxPriorityFeePerGas: 0, - preVerificationGas: 0, - authorizationList: [] + preVerificationGas: 0 } describe('#ValidationManager', () => { diff --git a/packages/sdk/test/0-utils.test.ts b/packages/sdk/test/0-utils.test.ts index c687b50f..68c4be02 100644 --- a/packages/sdk/test/0-utils.test.ts +++ b/packages/sdk/test/0-utils.test.ts @@ -63,8 +63,7 @@ describe('utils', () => { callData: '333', maxFeePerGas: 5, maxPriorityFeePerGas: 6, - signature: '777', - authorizationList: [] + signature: '777' })).to.eql({ sender: 'a', nonce: '0x01', @@ -95,8 +94,7 @@ describe('utils', () => { paymaster, paymasterVerificationGasLimit: 8, paymasterPostOpGasLimit: 9, - paymasterData: '0xcafebabe', - authorizationList: [] + paymasterData: '0xcafebabe' })).to.eql({ sender: 'a', nonce: '0x01', diff --git a/packages/sdk/test/1-SimpleAccountAPI.test.ts b/packages/sdk/test/1-SimpleAccountAPI.test.ts index 03eadf3c..ed6a052d 100644 --- a/packages/sdk/test/1-SimpleAccountAPI.test.ts +++ b/packages/sdk/test/1-SimpleAccountAPI.test.ts @@ -53,8 +53,7 @@ describe('SimpleAccountAPI', () => { preVerificationGas: 7, maxFeePerGas: 8, maxPriorityFeePerGas: 9, - signature: '0xbbbb', - authorizationList: [] + signature: '0xbbbb' } const hash = await api.getUserOpHash(userOp) const epHash = await entryPoint.getUserOpHash(packUserOp(userOp)) diff --git a/packages/utils/src/ERC4337Utils.ts b/packages/utils/src/ERC4337Utils.ts index b1b9e0d1..76a1328c 100644 --- a/packages/utils/src/ERC4337Utils.ts +++ b/packages/utils/src/ERC4337Utils.ts @@ -174,8 +174,7 @@ export function unpackUserOp (packed: PackedUserOperation): UserOperation { callGasLimit, maxFeePerGas, maxPriorityFeePerGas, - signature: packed.signature, - authorizationList: [] + signature: packed.signature } if (packed.initCode != null && packed.initCode.length > 2) { const factory = hexDataSlice(packed.initCode, 0, 20) @@ -201,7 +200,7 @@ export function unpackUserOp (packed: PackedUserOperation): UserOperation { /** * abi-encode the userOperation - * @param op a PackedUserOp + * @param op1 a PackedUserOp * @param forSignature "true" if the hash is needed to calculate the getUserOpHash() * "false" to pack entire UserOp, for calculating the calldata cost of putting it on-chain. */ diff --git a/packages/utils/src/Utils.ts b/packages/utils/src/Utils.ts index 887e8e29..ded57377 100644 --- a/packages/utils/src/Utils.ts +++ b/packages/utils/src/Utils.ts @@ -10,6 +10,7 @@ import { PackedUserOperationStruct } from './soltypes' import { UserOperation } from './interfaces/UserOperation' import { OperationBase } from './interfaces/OperationBase' import { OperationRIP7560 } from './interfaces/OperationRIP7560' +import { EIP7702Authorization } from './interfaces/EIP7702Authorization' export interface SlotMap { [slot: string]: string @@ -236,3 +237,12 @@ export function getPackedNonce (userOp: OperationBase): BigNumber { const bigNumberNonce = BigNumber.from(packed) return bigNumberNonce } + +export function getAuthorizationList (op: OperationBase): EIP7702Authorization[] { + const userOp = op as UserOperation + if (userOp.eip7702auth != null) { + return [userOp.eip7702auth] + } else { + return (op as OperationRIP7560).authorizationList ?? [] + } +} diff --git a/packages/utils/src/interfaces/OperationBase.ts b/packages/utils/src/interfaces/OperationBase.ts index ec540598..0523c30d 100644 --- a/packages/utils/src/interfaces/OperationBase.ts +++ b/packages/utils/src/interfaces/OperationBase.ts @@ -22,5 +22,4 @@ export interface OperationBase { verificationGasLimit: BigNumberish paymasterVerificationGasLimit?: BigNumberish paymasterPostOpGasLimit?: BigNumberish - authorizationList?: EIP7702Authorization[] } diff --git a/packages/utils/src/interfaces/OperationRIP7560.ts b/packages/utils/src/interfaces/OperationRIP7560.ts index c05161de..4bd7dd9c 100644 --- a/packages/utils/src/interfaces/OperationRIP7560.ts +++ b/packages/utils/src/interfaces/OperationRIP7560.ts @@ -1,5 +1,6 @@ import { OperationBase } from './OperationBase' import { BigNumberish, BytesLike } from 'ethers' +import { EIP7702Authorization } from './EIP7702Authorization' export interface OperationRIP7560 extends OperationBase { chainId: BigNumberish @@ -12,4 +13,6 @@ export interface OperationRIP7560 extends OperationBase { // todo: we discussed using 'nonceKey' in the JSON schema for ERC-4337 as well but we did not finalize this decision nonceKey: BigNumberish + + authorizationList?: EIP7702Authorization[] } diff --git a/packages/utils/src/interfaces/UserOperation.ts b/packages/utils/src/interfaces/UserOperation.ts index f7bc0fff..8309db7a 100644 --- a/packages/utils/src/interfaces/UserOperation.ts +++ b/packages/utils/src/interfaces/UserOperation.ts @@ -1,5 +1,6 @@ import { BigNumberish, BytesLike } from 'ethers' import { OperationBase } from './OperationBase' +import { EIP7702Authorization } from './EIP7702Authorization' export interface UserOperation extends OperationBase { // these fields have same meaning but different names between ERC-4337 and RIP-7560/RIP-7712 @@ -8,4 +9,5 @@ export interface UserOperation extends OperationBase { nonce: BigNumberish preVerificationGas: BigNumberish + eip7702auth?: EIP7702Authorization } diff --git a/packages/validation-manager/src/ValidationManager.ts b/packages/validation-manager/src/ValidationManager.ts index 6416936a..c6e92d8a 100644 --- a/packages/validation-manager/src/ValidationManager.ts +++ b/packages/validation-manager/src/ValidationManager.ts @@ -11,6 +11,7 @@ import { EIP7702Authorization, IEntryPoint, IEntryPointSimulations__factory, + OperationBase, ReferencedCodeHashes, RpcError, StakeInfo, @@ -27,8 +28,7 @@ import { packUserOp, requireAddressAndFields, requireCond, - runContractScript, - OperationBase, SenderCreator__factory, IEntryPoint__factory, IPaymaster__factory + runContractScript, getAuthorizationList, SenderCreator__factory, IEntryPoint__factory, IPaymaster__factory } from '@account-abstraction/utils' import { tracerResultParser } from './TracerResultParser' @@ -56,12 +56,14 @@ const entryPointSimulations = IEntryPointSimulations__factory.createInterface() * (relevant only if unsafe=false) */ export class ValidationManager implements IValidationManager { + private readonly provider: JsonRpcProvider constructor ( readonly entryPoint: IEntryPoint, readonly unsafe: boolean, readonly preVerificationGasCalculator: PreVerificationGasCalculator, readonly providerForTracer?: JsonRpcProvider ) { + this.provider = this.entryPoint.provider as JsonRpcProvider } _getDebugConfiguration (): { @@ -118,8 +120,7 @@ export class ValidationManager implements IValidationManager { } } try { - const provider = this.entryPoint.provider as JsonRpcProvider - const simulationResult = await provider.send('eth_call', [tx, 'latest', stateOverride]) + const simulationResult = await this.provider.send('eth_call', [tx, 'latest', stateOverride]) const [res] = entryPointSimulations.decodeFunctionResult('simulateValidation', simulationResult) as ValidationResultStructOutput[] return this.parseValidationResult(userOp, res) @@ -237,7 +238,22 @@ export class ValidationManager implements IValidationManager { addresses: [], hash: '' } - const stateOverrideForEip7702 = await this.getAuthorizationsStateOverride(userOp.authorizationList ?? []) + const authorizationList = getAuthorizationList(userOp) + if (authorizationList.length > 0) { + // relevant only for RIP-7560... + requireCond(authorizationList.length === 1, 'Only one authorization is supported', ValidationErrors.InvalidFields) + + const chainId = await this.provider.getNetwork().then(n => n.chainId) + + // list is required to be of size=1. for completeness, we still scan it as a list. + for (const authorization of authorizationList) { + const authChainId = BigNumber.from(authorization.chainId) + requireCond(authChainId.eq(BigNumber.from(0)) || + authChainId.eq(chainId), 'Invalid chainId in authorization', ValidationErrors.InvalidFields) + requireCond(getEip7702AuthorizationSigner(authorizationList[0]).toLowerCase() === userOp.sender.toLowerCase(), 'Authorization signer is not sender', ValidationErrors.InvalidFields) + } + } + const stateOverrideForEip7702 = await this.getAuthorizationsStateOverride(authorizationList) let storageMap: StorageMap = {} if (!this.unsafe) { let tracerResult: BundlerTracerResult @@ -301,17 +317,15 @@ export class ValidationManager implements IValidationManager { authorizations: EIP7702Authorization[] = [] ): Promise<{ [address: string]: { code: string } }> { const stateOverride: { [address: string]: { code: string } } = {} - // TODO: why don't we have 'provider' as a member in here? - const provider = this.entryPoint.provider as JsonRpcProvider for (const authorization of authorizations) { const authSigner = getEip7702AuthorizationSigner(authorization) - const nonce = await provider.getTransactionCount(authSigner) + const nonce = await this.provider.getTransactionCount(authSigner) const authNonce: any = authorization.nonce if (nonce !== BigNumber.from(authNonce.replace(/0x$/, '0x0')).toNumber()) { continue } - const currentDelegateeCode = await provider.getCode(authSigner) - const newDelegateeCode = await provider.getCode(authorization.address) + const currentDelegateeCode = await this.provider.getCode(authSigner) + const newDelegateeCode = await this.provider.getCode(authorization.address) // TODO should be: hexConcat(['0xef0100', authorization.address]) const noCurrentDelegation = currentDelegateeCode.length <= 2 // TODO: do not send such authorizations to 'handleOps' as it is a waste of gas