From bd5453ea97dd703269827f05d81e5b9ac4dbd008 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:24:57 +0100 Subject: [PATCH 1/4] fix(pimlico): send EIP-7702 authorization as separate field per ERC-7769 The authorization was incorrectly encoded in factoryData. Per ERC-7769, eip7702Auth must be a separate field in the UserOperation JSON. - Add eip7702Auth field to UserOperationV07 interface - Send authorization as separate eip7702Auth field instead of factoryData - Set factoryData to '0x' (used for storage init, not auth) --- .../evm/paymaster/pimlico-bundler.service.ts | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts b/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts index 7f31a89a29..f2d839b144 100644 --- a/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts +++ b/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts @@ -56,6 +56,15 @@ interface UserOperationV07 { paymasterPostOpGasLimit: Hex; paymasterData: Hex; signature: Hex; + // EIP-7702 authorization - separate field per ERC-7769 + eip7702Auth?: { + chainId: Hex; + address: Address; + nonce: Hex; + r: Hex; + s: Hex; + yParity: Hex; + }; } @Injectable() @@ -234,11 +243,19 @@ export class PimlicoBundlerService { // 2. Encode the execute() call for MetaMask Delegator (ERC-7821 format) const callData = this.encodeExecuteCall(token.chainId as Address, transferData); - // 3. Encode the EIP-7702 authorization as factoryData - const factoryData = this.encodeAuthorizationAsFactoryData(authorization); - - // 4. Build the UserOperation - const userOp = await this.buildUserOperation(userAddress as Address, callData, factoryData, pimlicoUrl); + // 3. Build the UserOperation with eip7702Auth as separate field (per ERC-7769) + // factoryData is '0x' for EIP-7702 - it's passed to sender for storage init, not for auth + const userOp = await this.buildUserOperation(userAddress as Address, callData, '0x' as Hex, pimlicoUrl); + + // 4. Add EIP-7702 authorization as separate field + userOp.eip7702Auth = { + chainId: toHex(authorization.chainId), + address: authorization.address as Address, + nonce: toHex(authorization.nonce), + r: authorization.r as Hex, + s: authorization.s as Hex, + yParity: toHex(authorization.yParity), + }; // 5. Sponsor the UserOperation via Pimlico Paymaster const sponsoredUserOp = await this.sponsorUserOperation(userOp, pimlicoUrl); From 58fdf1c5e412367318aebd9c06dce938d860fa26 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Wed, 7 Jan 2026 19:05:27 +0100 Subject: [PATCH 2/4] fix(pimlico): update EIP-7702 UserOperation format for Pimlico bundler - Use EntryPoint v0.8 (required for EIP-7702) - Use correct EIP-7702 factory marker (0x7702... right-padded) - Pass eip7702Auth as separate field with 'contractAddress' key - Remove factoryData param (not needed for EIP-7702) --- .../evm/paymaster/pimlico-bundler.service.ts | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts b/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts index f2d839b144..b9e2b25a51 100644 --- a/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts +++ b/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts @@ -13,11 +13,13 @@ import { EVM_CHAIN_CONFIG, getEvmChainConfig, isEvmBlockchainSupported } from '. // Source: https://github.com/MetaMask/delegation-framework const METAMASK_DELEGATOR_ADDRESS = '0x63c0c19a282a1b52b07dd5a65b58948a07dae32b' as Address; -// ERC-4337 EntryPoint v0.7 - canonical address on all chains -const ENTRY_POINT_V07 = '0x0000000071727De22E5E9d8BAf0edAc6f37da032' as Address; +// ERC-4337 EntryPoint v0.8 - required for EIP-7702 support +// v0.8 address: 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 +const ENTRY_POINT_V08 = '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108' as Address; // EIP-7702 factory marker - signals to bundler that this is an EIP-7702 UserOperation -const EIP7702_FACTORY = '0x0000000000000000000000000000000000007702' as Address; +// MUST be right-padded: 0x7702 + 18 trailing zeros (NOT leading zeros!) +const EIP7702_FACTORY = '0x7702000000000000000000000000000000000000' as Address; // MetaMask Delegator ABI - ERC-7821 BatchExecutor interface const DELEGATOR_ABI = parseAbi(['function execute((bytes32 mode, bytes executionData) execution) external payable']); @@ -43,8 +45,8 @@ export interface GaslessTransferResult { interface UserOperationV07 { sender: Address; nonce: Hex; - factory: Address; - factoryData: Hex; + factory?: Address | null; + factoryData?: Hex; callData: Hex; callGasLimit: Hex; verificationGasLimit: Hex; @@ -57,9 +59,10 @@ interface UserOperationV07 { paymasterData: Hex; signature: Hex; // EIP-7702 authorization - separate field per ERC-7769 + // Pimlico expects 'contractAddress' not 'address' eip7702Auth?: { + contractAddress: Address; chainId: Hex; - address: Address; nonce: Hex; r: Hex; s: Hex; @@ -243,14 +246,14 @@ export class PimlicoBundlerService { // 2. Encode the execute() call for MetaMask Delegator (ERC-7821 format) const callData = this.encodeExecuteCall(token.chainId as Address, transferData); - // 3. Build the UserOperation with eip7702Auth as separate field (per ERC-7769) - // factoryData is '0x' for EIP-7702 - it's passed to sender for storage init, not for auth - const userOp = await this.buildUserOperation(userAddress as Address, callData, '0x' as Hex, pimlicoUrl); + // 3. Build the UserOperation (factory is left null for EIP-7702) + const userOp = await this.buildUserOperation(userAddress as Address, callData, pimlicoUrl); // 4. Add EIP-7702 authorization as separate field + // Pimlico expects 'contractAddress' not 'address' userOp.eip7702Auth = { + contractAddress: authorization.address as Address, chainId: toHex(authorization.chainId), - address: authorization.address as Address, nonce: toHex(authorization.nonce), r: authorization.r as Hex, s: authorization.s as Hex, @@ -339,12 +342,12 @@ export class PimlicoBundlerService { } /** - * Build UserOperation v0.7 structure + * Build UserOperation v0.8 structure for EIP-7702 + * Note: factory is intentionally left null/undefined - Pimlico expects this for EIP-7702 */ private async buildUserOperation( sender: Address, callData: Hex, - factoryData: Hex, pimlicoUrl: string, ): Promise { // Get current gas prices from Pimlico @@ -356,8 +359,8 @@ export class PimlicoBundlerService { const userOp: UserOperationV07 = { sender, nonce: toHex(nonce), - factory: EIP7702_FACTORY, - factoryData, + // For EIP-7702, do NOT set factory - Pimlico expects it to be null/undefined + // The eip7702Auth field is added separately after building callData, callGasLimit: toHex(200000n), verificationGasLimit: toHex(500000n), @@ -384,7 +387,7 @@ export class PimlicoBundlerService { * Sponsor UserOperation via Pimlico Paymaster */ private async sponsorUserOperation(userOp: UserOperationV07, pimlicoUrl: string): Promise { - const response = await this.jsonRpc(pimlicoUrl, 'pm_sponsorUserOperation', [userOp, ENTRY_POINT_V07]); + const response = await this.jsonRpc(pimlicoUrl, 'pm_sponsorUserOperation', [userOp, ENTRY_POINT_V08]); return { ...userOp, @@ -402,7 +405,7 @@ export class PimlicoBundlerService { * Submit UserOperation to Pimlico Bundler */ private async sendUserOperation(userOp: UserOperationV07, pimlicoUrl: string): Promise { - return this.jsonRpc(pimlicoUrl, 'eth_sendUserOperation', [userOp, ENTRY_POINT_V07]); + return this.jsonRpc(pimlicoUrl, 'eth_sendUserOperation', [userOp, ENTRY_POINT_V08]); } /** @@ -452,7 +455,7 @@ export class PimlicoBundlerService { try { const response = await this.jsonRpc(pimlicoUrl, 'eth_call', [ { - to: ENTRY_POINT_V07, + to: ENTRY_POINT_V08, data: encodeFunctionData({ abi: parseAbi(['function getNonce(address sender, uint192 key) view returns (uint256)']), functionName: 'getNonce', @@ -475,7 +478,7 @@ export class PimlicoBundlerService { pimlicoUrl: string, ): Promise<{ callGasLimit: Hex; verificationGasLimit: Hex; preVerificationGas: Hex }> { try { - const response = await this.jsonRpc(pimlicoUrl, 'eth_estimateUserOperationGas', [userOp, ENTRY_POINT_V07]); + const response = await this.jsonRpc(pimlicoUrl, 'eth_estimateUserOperationGas', [userOp, ENTRY_POINT_V08]); return { callGasLimit: response.callGasLimit, verificationGasLimit: response.verificationGasLimit, From b2c92422979093a33126462d0888a513bac64493 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:52:04 +0100 Subject: [PATCH 3/4] fix(pimlico): remove unused EIP7702_FACTORY constant --- .../shared/evm/paymaster/pimlico-bundler.service.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts b/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts index b9e2b25a51..3424a27b8a 100644 --- a/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts +++ b/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts @@ -14,13 +14,8 @@ import { EVM_CHAIN_CONFIG, getEvmChainConfig, isEvmBlockchainSupported } from '. const METAMASK_DELEGATOR_ADDRESS = '0x63c0c19a282a1b52b07dd5a65b58948a07dae32b' as Address; // ERC-4337 EntryPoint v0.8 - required for EIP-7702 support -// v0.8 address: 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 const ENTRY_POINT_V08 = '0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108' as Address; -// EIP-7702 factory marker - signals to bundler that this is an EIP-7702 UserOperation -// MUST be right-padded: 0x7702 + 18 trailing zeros (NOT leading zeros!) -const EIP7702_FACTORY = '0x7702000000000000000000000000000000000000' as Address; - // MetaMask Delegator ABI - ERC-7821 BatchExecutor interface const DELEGATOR_ABI = parseAbi(['function execute((bytes32 mode, bytes executionData) execution) external payable']); @@ -345,11 +340,7 @@ export class PimlicoBundlerService { * Build UserOperation v0.8 structure for EIP-7702 * Note: factory is intentionally left null/undefined - Pimlico expects this for EIP-7702 */ - private async buildUserOperation( - sender: Address, - callData: Hex, - pimlicoUrl: string, - ): Promise { + private async buildUserOperation(sender: Address, callData: Hex, pimlicoUrl: string): Promise { // Get current gas prices from Pimlico const gasPrice = await this.getGasPrice(pimlicoUrl); From e286fbc4a4b558645824c58d230a64e442d9aebf Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:24:51 +0100 Subject: [PATCH 4/4] fix(pimlico): improve EIP-7702 UserOperation structure - Remove dead code: encodeAuthorizationAsFactoryData (no longer used) - Rename UserOperationV07 to UserOperationV08 for clarity - Include eip7702Auth BEFORE gas estimation (was added after) - This ensures Pimlico bundler can properly estimate gas with the authorization context --- .../evm/paymaster/pimlico-bundler.service.ts | 76 +++++++------------ 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts b/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts index 3424a27b8a..3f06f4aafe 100644 --- a/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts +++ b/src/integration/blockchain/shared/evm/paymaster/pimlico-bundler.service.ts @@ -37,7 +37,7 @@ export interface GaslessTransferResult { userOpHash: string; } -interface UserOperationV07 { +interface UserOperationV08 { sender: Address; nonce: Hex; factory?: Address | null; @@ -241,27 +241,17 @@ export class PimlicoBundlerService { // 2. Encode the execute() call for MetaMask Delegator (ERC-7821 format) const callData = this.encodeExecuteCall(token.chainId as Address, transferData); - // 3. Build the UserOperation (factory is left null for EIP-7702) - const userOp = await this.buildUserOperation(userAddress as Address, callData, pimlicoUrl); - - // 4. Add EIP-7702 authorization as separate field - // Pimlico expects 'contractAddress' not 'address' - userOp.eip7702Auth = { - contractAddress: authorization.address as Address, - chainId: toHex(authorization.chainId), - nonce: toHex(authorization.nonce), - r: authorization.r as Hex, - s: authorization.s as Hex, - yParity: toHex(authorization.yParity), - }; + // 3. Build the UserOperation with EIP-7702 authorization + // Authorization is included before gas estimation for accurate estimates + const userOp = await this.buildUserOperation(userAddress as Address, callData, authorization, pimlicoUrl); - // 5. Sponsor the UserOperation via Pimlico Paymaster + // 4. Sponsor the UserOperation via Pimlico Paymaster const sponsoredUserOp = await this.sponsorUserOperation(userOp, pimlicoUrl); - // 6. Submit the UserOperation via Pimlico Bundler + // 5. Submit the UserOperation via Pimlico Bundler const userOpHash = await this.sendUserOperation(sponsoredUserOp, pimlicoUrl); - // 7. Wait for the transaction to be mined + // 6. Wait for the transaction to be mined const txHash = await this.waitForUserOperation(userOpHash, pimlicoUrl); return { txHash, userOpHash }; @@ -312,46 +302,26 @@ export class PimlicoBundlerService { return encoded; } - /** - * Encode EIP-7702 authorization as factoryData for UserOperation - * - * When factory = 0x7702, the bundler expects factoryData to contain - * the signed EIP-7702 authorization that delegates the smart account - * implementation to the EOA. - */ - private encodeAuthorizationAsFactoryData(authorization: Eip7702Authorization): Hex { - // factoryData format for EIP-7702: - // abi.encodePacked(address delegatee, uint256 nonce, bytes signature) - // where signature = abi.encodePacked(r, s, yParity) - const signature = concat([ - authorization.r as Hex, - authorization.s as Hex, - toHex(authorization.yParity, { size: 1 }), - ]); - - return concat([ - authorization.address as Hex, // delegatee (MetaMask Delegator) - pad(toHex(BigInt(authorization.nonce)), { size: 32 }), // nonce - signature, // signature (r, s, yParity) - ]); - } - /** * Build UserOperation v0.8 structure for EIP-7702 * Note: factory is intentionally left null/undefined - Pimlico expects this for EIP-7702 */ - private async buildUserOperation(sender: Address, callData: Hex, pimlicoUrl: string): Promise { + private async buildUserOperation( + sender: Address, + callData: Hex, + authorization: Eip7702Authorization, + pimlicoUrl: string, + ): Promise { // Get current gas prices from Pimlico const gasPrice = await this.getGasPrice(pimlicoUrl); // Get sender nonce from EntryPoint const nonce = await this.getSenderNonce(sender, pimlicoUrl); - const userOp: UserOperationV07 = { + const userOp: UserOperationV08 = { sender, nonce: toHex(nonce), // For EIP-7702, do NOT set factory - Pimlico expects it to be null/undefined - // The eip7702Auth field is added separately after building callData, callGasLimit: toHex(200000n), verificationGasLimit: toHex(500000n), @@ -363,9 +333,19 @@ export class PimlicoBundlerService { paymasterPostOpGasLimit: toHex(0n), paymasterData: '0x' as Hex, signature: '0x' as Hex, // Will be filled by sponsorship or left empty for EIP-7702 + // EIP-7702 authorization must be included BEFORE gas estimation + // Pimlico expects 'contractAddress' not 'address' + eip7702Auth: { + contractAddress: authorization.address as Address, + chainId: toHex(authorization.chainId), + nonce: toHex(authorization.nonce), + r: authorization.r as Hex, + s: authorization.s as Hex, + yParity: toHex(authorization.yParity), + }, }; - // Estimate gas limits + // Estimate gas limits (now includes eip7702Auth) const estimated = await this.estimateUserOperationGas(userOp, pimlicoUrl); userOp.callGasLimit = estimated.callGasLimit; userOp.verificationGasLimit = estimated.verificationGasLimit; @@ -377,7 +357,7 @@ export class PimlicoBundlerService { /** * Sponsor UserOperation via Pimlico Paymaster */ - private async sponsorUserOperation(userOp: UserOperationV07, pimlicoUrl: string): Promise { + private async sponsorUserOperation(userOp: UserOperationV08, pimlicoUrl: string): Promise { const response = await this.jsonRpc(pimlicoUrl, 'pm_sponsorUserOperation', [userOp, ENTRY_POINT_V08]); return { @@ -395,7 +375,7 @@ export class PimlicoBundlerService { /** * Submit UserOperation to Pimlico Bundler */ - private async sendUserOperation(userOp: UserOperationV07, pimlicoUrl: string): Promise { + private async sendUserOperation(userOp: UserOperationV08, pimlicoUrl: string): Promise { return this.jsonRpc(pimlicoUrl, 'eth_sendUserOperation', [userOp, ENTRY_POINT_V08]); } @@ -465,7 +445,7 @@ export class PimlicoBundlerService { * Estimate gas for UserOperation */ private async estimateUserOperationGas( - userOp: UserOperationV07, + userOp: UserOperationV08, pimlicoUrl: string, ): Promise<{ callGasLimit: Hex; verificationGasLimit: Hex; preVerificationGas: Hex }> { try {