From 004c4cd40266a0001f3f22dee84aa45c7d742137 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless0x@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:44:34 +0200 Subject: [PATCH 1/9] fix(eip7702): validate factory field with eip7702 requirements - Add validation for 0x7702 factory address in both short and padded forms - Require eip7702Auth when factory is set to 0x7702 - Add comprehensive test coverage for both factory address formats --- src/rpc/rpcHandler.ts | 14 ++++- test/e2e/tests/eth_sendUserOperation.test.ts | 60 ++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index 635ca7f5..159c35e1 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -246,6 +246,17 @@ export class RpcHandler { [boolean, string] > { if (!userOp.eip7702Auth) { + if ( + isVersion07(userOp) && + (userOp.factory === "0x7702" || + userOp.factory === + "0x7702000000000000000000000000000000000000") + ) { + return [ + false, + "Invalid EIP-7702 userOperation: factory is 0x7702 but eip7702Auth is missing." + ] + } return [true, ""] } @@ -341,11 +352,12 @@ export class RpcHandler { if ( isVersion07(userOp) && userOp.factory !== "0x7702" && + userOp.factory !== "0x7702000000000000000000000000000000000000" && userOp.factory !== null ) { return [ false, - "Invalid EIP-7702 authorization: UserOperation cannot contain factory that is neither null or 0x7702." + "Invalid EIP-7702 authorization: factory must be null or 0x7702." ] } diff --git a/test/e2e/tests/eth_sendUserOperation.test.ts b/test/e2e/tests/eth_sendUserOperation.test.ts index 42f6088f..c24cb69d 100644 --- a/test/e2e/tests/eth_sendUserOperation.test.ts +++ b/test/e2e/tests/eth_sendUserOperation.test.ts @@ -533,6 +533,66 @@ describe.each([ expect(receipt.success) }) + test.each([ + { + testName: "short form (0x7702)", + factory: "0x7702" + }, + { + testName: + "zero-padded form (0x7702000000000000000000000000000000000000)", + factory: "0x7702000000000000000000000000000000000000" + } + ])( + "Should reject userOp with factory $testName but no eip7702Auth", + async ({ factory }) => { + // Skip for v0.6 - doesn't have factory field + if (entryPointVersion === "0.6") { + return + } + + const client = await getSmartAccountClient({ + entryPointVersion, + anvilRpc, + altoRpc + }) + + const op = (await client.prepareUserOperation({ + calls: [ + { + to: TO_ADDRESS, + value: VALUE, + data: "0x" + } + ] + })) as UserOperation + + // Set factory to 0x7702 without providing eip7702Auth + op.factory = factory + op.factoryData = undefined + + op.signature = await client.account.signUserOperation(op) + + try { + await client.sendUserOperation(op) + expect.fail("Must throw") + } catch (err) { + expect(err).toBeInstanceOf(BaseError) + const error = err as BaseError + + expect(error.details).toMatch( + /factory is 0x7702 but eip7702Auth is not provided/i + ) + + const rpcError = error.walk( + (e) => e instanceof RpcRequestError + ) as RpcRequestError + expect(rpcError).toBeDefined() + expect(rpcError.code).toBe(ERC7769Errors.InvalidFields) + } + } + ) + test.each([ { testName: "r is zero", From ece928c829bc47bc48da744234059b933a28c0f4 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless0x@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:42:55 +0100 Subject: [PATCH 2/9] fix(rpc): add eip7702Auth validation for 0x7702 factory - Add early validation check for EIP-7702 userOps with 0x7702 factory - Ensure eip7702Auth is provided when factory is 0x7702 - Update error message for consistency --- src/rpc/rpcHandler.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index 159c35e1..18154c67 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -160,6 +160,20 @@ export class RpcHandler { return [false, paymasterSignatureError] } + // Reject userOps with factory 0x7702 but no eip7702Auth + if ( + isVersion07(userOp) && + !userOp.eip7702Auth && + (userOp.factory === "0x7702" || + userOp.factory === + "0x7702000000000000000000000000000000000000") + ) { + return [ + false, + "Invalid EIP-7702 userOperation: factory is 0x7702 but eip7702Auth is not provided." + ] + } + if ( this.config.legacyTransactions && userOp.maxFeePerGas !== userOp.maxPriorityFeePerGas @@ -254,7 +268,7 @@ export class RpcHandler { ) { return [ false, - "Invalid EIP-7702 userOperation: factory is 0x7702 but eip7702Auth is missing." + "Invalid EIP-7702 userOperation: factory is 0x7702 but eip7702Auth is not provided." ] } return [true, ""] From 9e11b9055a681cae6a1754baea0258101f920988 Mon Sep 17 00:00:00 2001 From: mouseless0x <97399882+mouseless0x@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:52:16 +0000 Subject: [PATCH 3/9] chore: format --- src/rpc/rpcHandler.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index 18154c67..0223d02b 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -165,8 +165,7 @@ export class RpcHandler { isVersion07(userOp) && !userOp.eip7702Auth && (userOp.factory === "0x7702" || - userOp.factory === - "0x7702000000000000000000000000000000000000") + userOp.factory === "0x7702000000000000000000000000000000000000") ) { return [ false, From 333f84217d7db813f2a0ceda3a079d9d2646f348 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless0x@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:36:18 +0100 Subject: [PATCH 4/9] fix(rpc): remove incorrect eip7702 factory validation - Removed validation check that incorrectly rejected userOps with factory 0x7702 but no eip7702Auth - This check was overly restrictive and not aligned with EIP-7702 specification --- src/rpc/rpcHandler.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index 0223d02b..d97c3db7 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -160,19 +160,6 @@ export class RpcHandler { return [false, paymasterSignatureError] } - // Reject userOps with factory 0x7702 but no eip7702Auth - if ( - isVersion07(userOp) && - !userOp.eip7702Auth && - (userOp.factory === "0x7702" || - userOp.factory === "0x7702000000000000000000000000000000000000") - ) { - return [ - false, - "Invalid EIP-7702 userOperation: factory is 0x7702 but eip7702Auth is not provided." - ] - } - if ( this.config.legacyTransactions && userOp.maxFeePerGas !== userOp.maxPriorityFeePerGas From 0e3b3f181327ef487f124efa497ba2935a0305f4 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless0x@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:23:07 +0100 Subject: [PATCH 5/9] feat(gas-estimation): add userOp field validation for estimation - Add early userOp field validation in eth_estimateUserOperationGas - Add isEstimation flag to skip gas checks during estimation - Consolidate EIP-7702 factory validation and remove duplication --- .../methods/eth_estimateUserOperationGas.ts | 14 ++++++++ src/rpc/rpcHandler.ts | 34 ++++++++++++------- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/rpc/methods/eth_estimateUserOperationGas.ts b/src/rpc/methods/eth_estimateUserOperationGas.ts index 8ffc1091..fad85135 100644 --- a/src/rpc/methods/eth_estimateUserOperationGas.ts +++ b/src/rpc/methods/eth_estimateUserOperationGas.ts @@ -284,6 +284,20 @@ export const ethEstimateUserOperationGasHandler = createMethodHandler({ chainType } = rpcHandler.config + // Validate userOp fields (sync - fail fast before async checks) + const [fieldsValid, fieldsError] = + rpcHandler.validateUserOpFields({ + userOp, + entryPoint, + isEstimation: true + }) + if (!fieldsValid) { + throw new RpcError( + fieldsError, + ERC7769Errors.InvalidFields + ) + } + // Execute multiple async operations in parallel const [ [validEip7702Auth, validEip7702AuthError], diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index d97c3db7..b09930bd 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -145,12 +145,33 @@ export class RpcHandler { validateUserOpFields({ userOp, entryPoint, - isBoosted = false + isBoosted = false, + isEstimation = false }: { userOp: UserOperation entryPoint: Address isBoosted?: boolean + isEstimation?: boolean }): [boolean, string] { + // Check for 0x7702 factory without eip7702Auth + if ( + !userOp.eip7702Auth && + isVersion07(userOp) && + (userOp.factory === "0x7702" || + userOp.factory === + "0x7702000000000000000000000000000000000000") + ) { + return [ + false, + "Invalid EIP-7702 userOperation: factory is 0x7702 but eip7702Auth is not provided." + ] + } + + // Skip gas/field checks during estimation + if (isEstimation) { + return [true, ""] + } + // Validate paymaster signature for EntryPoint 0.9 const paymasterSignatureError = validatePaymasterSignature({ userOp, @@ -246,17 +267,6 @@ export class RpcHandler { [boolean, string] > { if (!userOp.eip7702Auth) { - if ( - isVersion07(userOp) && - (userOp.factory === "0x7702" || - userOp.factory === - "0x7702000000000000000000000000000000000000") - ) { - return [ - false, - "Invalid EIP-7702 userOperation: factory is 0x7702 but eip7702Auth is not provided." - ] - } return [true, ""] } From 1e22224e3040a2854a4abfaa566d1ffdcc9d20d5 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless0x@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:23:45 +0100 Subject: [PATCH 6/9] style(rpc): fix line wrapping in factory validation --- src/rpc/rpcHandler.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rpc/rpcHandler.ts b/src/rpc/rpcHandler.ts index b09930bd..b02aba12 100644 --- a/src/rpc/rpcHandler.ts +++ b/src/rpc/rpcHandler.ts @@ -158,8 +158,7 @@ export class RpcHandler { !userOp.eip7702Auth && isVersion07(userOp) && (userOp.factory === "0x7702" || - userOp.factory === - "0x7702000000000000000000000000000000000000") + userOp.factory === "0x7702000000000000000000000000000000000000") ) { return [ false, From bb6b91a25c875cad9c91374b2c6cc4da94a9e985 Mon Sep 17 00:00:00 2001 From: mouseless0x <97399882+mouseless0x@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:26:55 +0000 Subject: [PATCH 7/9] chore: format --- src/rpc/methods/eth_estimateUserOperationGas.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/rpc/methods/eth_estimateUserOperationGas.ts b/src/rpc/methods/eth_estimateUserOperationGas.ts index fad85135..4581ff58 100644 --- a/src/rpc/methods/eth_estimateUserOperationGas.ts +++ b/src/rpc/methods/eth_estimateUserOperationGas.ts @@ -285,17 +285,13 @@ export const ethEstimateUserOperationGasHandler = createMethodHandler({ } = rpcHandler.config // Validate userOp fields (sync - fail fast before async checks) - const [fieldsValid, fieldsError] = - rpcHandler.validateUserOpFields({ - userOp, - entryPoint, - isEstimation: true - }) + const [fieldsValid, fieldsError] = rpcHandler.validateUserOpFields({ + userOp, + entryPoint, + isEstimation: true + }) if (!fieldsValid) { - throw new RpcError( - fieldsError, - ERC7769Errors.InvalidFields - ) + throw new RpcError(fieldsError, ERC7769Errors.InvalidFields) } // Execute multiple async operations in parallel From 0932cc1ed2690222c96f61d37d587f071e78ee44 Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless0x@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:39:56 +0100 Subject: [PATCH 8/9] test(e2e): add ERC-7702 factory validation test - Add test cases for rejecting estimation with 0x7702 factory without eip7702Auth - Test both short form (0x7702) and zero-padded form validation - Skip test for v0.6 which doesn't support factory field --- .../eth_estimateUserOperationGas.test.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/e2e/tests/eth_estimateUserOperationGas.test.ts b/test/e2e/tests/eth_estimateUserOperationGas.test.ts index 576e9594..cb2dad96 100644 --- a/test/e2e/tests/eth_estimateUserOperationGas.test.ts +++ b/test/e2e/tests/eth_estimateUserOperationGas.test.ts @@ -379,6 +379,63 @@ describe.each([ ) }) + test.each([ + { + testName: "short form (0x7702)", + factory: "0x7702" + }, + { + testName: + "zero-padded form (0x7702000000000000000000000000000000000000)", + factory: "0x7702000000000000000000000000000000000000" + } + ])( + "Should reject estimation with factory $testName but no eip7702Auth", + async ({ factory }) => { + // Skip for v0.6 - doesn't have factory field + if (entryPointVersion === "0.6") { + return + } + + const client = await getSmartAccountClient({ + entryPointVersion, + anvilRpc, + altoRpc + }) + + const op = (await client.prepareUserOperation({ + calls: [ + { + to: zeroAddress, + data: "0x" + } + ] + })) as UserOperation + + // Set factory to 0x7702 without providing eip7702Auth + op.factory = factory + op.factoryData = undefined + + try { + await client.estimateUserOperationGas(op) + expect.fail("Must throw") + } catch (err) { + expect(err).toBeInstanceOf(BaseError) + const error = err as BaseError + + expect(error.details).toMatch( + /factory is 0x7702 but eip7702Auth is not provided/i + ) + + const rpcError = error.walk( + (e) => e instanceof RpcRequestError + ) as RpcRequestError + expect(rpcError).toBeDefined() + expect(rpcError.code).toBe(ERC7769Errors.InvalidFields) + } + } + ) + test("Should throw AA25 when estimating userOp with nonce + 1", async () => { const smartAccountClient = await getSmartAccountClient({ entryPointVersion, From 455071013774aadafbb67575f02b2b3e8ce9ef1b Mon Sep 17 00:00:00 2001 From: mouseless <97399882+mouseless0x@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:56:06 +0100 Subject: [PATCH 9/9] test(eth_estimateUserOperationGas): use bundler client for gas estimation - Separate bundler client creation for gas estimation calls - Rename smartAccountClient for clarity - Pass entryPointAddress explicitly to estimateUserOperationGas --- .../e2e/tests/eth_estimateUserOperationGas.test.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/e2e/tests/eth_estimateUserOperationGas.test.ts b/test/e2e/tests/eth_estimateUserOperationGas.test.ts index cb2dad96..07913280 100644 --- a/test/e2e/tests/eth_estimateUserOperationGas.test.ts +++ b/test/e2e/tests/eth_estimateUserOperationGas.test.ts @@ -397,13 +397,18 @@ describe.each([ return } - const client = await getSmartAccountClient({ + const bundlerClient = createBundlerClient({ + chain: foundry, + transport: http(altoRpc) + }) + + const smartAccountClient = await getSmartAccountClient({ entryPointVersion, anvilRpc, altoRpc }) - const op = (await client.prepareUserOperation({ + const op = (await smartAccountClient.prepareUserOperation({ calls: [ { to: zeroAddress, @@ -417,7 +422,10 @@ describe.each([ op.factoryData = undefined try { - await client.estimateUserOperationGas(op) + await bundlerClient.estimateUserOperationGas({ + ...op, + entryPointAddress: entryPoint + }) expect.fail("Must throw") } catch (err) { expect(err).toBeInstanceOf(BaseError)