From 9f5411845cabf993267b599bb8dfa6a3fd10b223 Mon Sep 17 00:00:00 2001 From: Filip Date: Mon, 6 Apr 2026 13:43:32 +0200 Subject: [PATCH 1/3] fix: use dynamic typehash with raw userOpHashes for smart session signatures --- src/modules/quotes/constants.ts | 24 ++++++++++++++ src/modules/quotes/quotes.service.ts | 36 +++++++++++++++------ src/modules/simulator/simulation.service.ts | 36 +++++++++++++++------ 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/src/modules/quotes/constants.ts b/src/modules/quotes/constants.ts index e36481c5..d4dd8182 100644 --- a/src/modules/quotes/constants.ts +++ b/src/modules/quotes/constants.ts @@ -1,3 +1,5 @@ +import { keccak256, toHex } from "viem"; + export enum MeeSignatureType { OFF_CHAIN = "0x177eee00", ON_CHAIN = "0x177eee01", @@ -42,3 +44,25 @@ export const SUPERTX_MEEUSEROP_STRUCT_TYPEHASH = // keccak256("MeeUserOp(bytes32 userOpHash,uint256 lowerBoundTimestamp,uint256 upperBoundTimestamp)"); export const MEE_USER_OP_TYPEHASH = "0x15a3822da13714219f4ba907e3daf8f006f6903616b4e7918e84eb2b8faf733d"; + +/** + * Builds a dynamic SuperTx typehash for smart sessions. + * + * Smart sessions go through validateSignatureForOwner which receives the raw + * userOpHash from SmartSession (no timestamp wrapping). Using the MeeUserOp[] + * array typehash would cause compareAndGetFinalHash to fail because: + * raw userOpHash ≠ keccak256(MEE_USER_OP_TYPEHASH || userOpHash || lower || upper) + * + * Instead we build a generic struct typehash with plain bytes32 fields: + * "SuperTx(bytes32 op0,bytes32 op1,...,bytes32 opN)" + * + * This takes the `else` branch in HashLib.compareAndGetFinalHash which does: + * structHash = keccak256(abi.encodePacked(outerTypeHash, itemHashes)) + */ +export function buildSessionSuperTxTypeHash(opCount: number): `0x${string}` { + const fields = Array.from( + { length: opCount }, + (_, i) => `bytes32 op${i}`, + ).join(","); + return keccak256(toHex(`SuperTx(${fields})`)); +} diff --git a/src/modules/quotes/quotes.service.ts b/src/modules/quotes/quotes.service.ts index 715de9c4..2d2d64e6 100644 --- a/src/modules/quotes/quotes.service.ts +++ b/src/modules/quotes/quotes.service.ts @@ -68,6 +68,7 @@ import { MEE_SIGNATURE_TYPE_OFFSET, MeeSignatureType, SUPERTX_MEEUSEROP_STRUCT_TYPEHASH, + buildSessionSuperTxTypeHash, } from "./constants"; import { type ExecuteQuoteOptions, @@ -858,6 +859,20 @@ export class QuotesService { ({ meeUserOpHash }) => meeUserOpHash, ); + // For smart sessions: use raw userOpHashes (without timestamp wrapping). + // The SmartSession module passes raw userOpHash to validateSignatureForOwner, + // so itemHashes must contain raw hashes to match at HashLib.compareAndGetFinalHash. + const sessionItemHashes = packedMeeUserOps.map( + ({ userOpHash }) => userOpHash, + ); + const sessionSuperTxTypeHash = isSessionExists + ? buildSessionSuperTxTypeHash( + isEIP712TrustedSponsorshipSupported && isTrustedSponsorship + ? sessionItemHashes.length - 1 + : sessionItemHashes.length, + ) + : undefined; + const signedPackedMeeUserOps: SignedPackedMeeUserOp[] = packedMeeUserOps.map((packedMeeUserOp, index) => { const { lowerBoundTimestamp, upperBoundTimestamp, userOp } = @@ -894,28 +909,29 @@ export class QuotesService { case "simple": { if (isSessionExists) { if (isEIP712SupportedMeeVersion) { + // Smart sessions: use dynamic generic typehash with raw userOpHashes. + // validateSignatureForOwner receives raw userOpHash from SmartSession, + // so we must use raw hashes (not MeeUserOp EIP-712 hashes with timestamps). + const sessionHashes = + isEIP712TrustedSponsorshipSupported && isTrustedSponsorship + ? sessionItemHashes.slice(1) + : sessionItemHashes; signature = concatHex([ signatureType, // Simple signature type encodeAbiParameters( [ - { type: "bytes32" }, // stxStructTypeHash + { type: "bytes32" }, // stxStructTypeHash (dynamic, session-specific) { type: "uint256" }, // userOp index - { type: "bytes32[]" }, // meeUserOpHashes - array of hashes + { type: "bytes32[]" }, // raw userOpHashes { type: "bytes" }, // superTxSignature ], [ - SUPERTX_MEEUSEROP_STRUCT_TYPEHASH, - // For trusted sponsorship, as we're ignoring the payment userOp, index should be sub by 1 to account for that - // The index will be always greater than 1 if its sponsorship mode so sub by 1 is not a problem + sessionSuperTxTypeHash as `0x${string}`, isEIP712TrustedSponsorshipSupported && isTrustedSponsorship ? BigInt(index - 1) : BigInt(index), - // If it is a trusted sponsorship, the payment userop can be skipped because it is not going to be executed at all. - isEIP712TrustedSponsorshipSupported && - isTrustedSponsorship - ? meeUserOpHashes.slice(1) - : meeUserOpHashes, + sessionHashes, signatureData, ], ), diff --git a/src/modules/simulator/simulation.service.ts b/src/modules/simulator/simulation.service.ts index 95dd44f7..e7d634f0 100644 --- a/src/modules/simulator/simulation.service.ts +++ b/src/modules/simulator/simulation.service.ts @@ -63,7 +63,10 @@ import { import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { multicall } from "viem/actions"; // Don't import this from "@quotes" alone, it is causing the circular dependency issue -import { SUPERTX_MEEUSEROP_STRUCT_TYPEHASH } from "../quotes/constants"; +import { + SUPERTX_MEEUSEROP_STRUCT_TYPEHASH, + buildSessionSuperTxTypeHash, +} from "../quotes/constants"; import { type CustomOverride, type EIP712DomainReturn, @@ -106,6 +109,7 @@ export class SimulationService { sessionDetails: GrantPermissionResponseType; smartSessionMode: "ENABLE_AND_USE" | "USE"; }, + rawUserOpHashes?: Hex[], ) { // For trusted sponsorship, as we're ignoring the payment userOp, index should be sub by 1 to account for that // for all the userOps except the payment userOp itself @@ -121,22 +125,30 @@ export class SimulationService { "0xcae0d1955b99d4832aef73ed0a2237045fad91a738ee5b96ba76b9a12ffc6f824ab2ecaeeeb903b50704fdf4a5d64216adf7d6aaaca0ed2a67b86d8525c4b4bb1c" as Hex; if (sessionInfo?.sessionDetails) { + // Smart sessions: use dynamic generic typehash with raw userOpHashes. + // validateSignatureForOwner receives raw userOpHash from SmartSession, + // so we must use raw hashes (not MeeUserOp EIP-712 hashes with timestamps). + const sessionHashes = rawUserOpHashes + ? isTrustedSponsorship + ? rawUserOpHashes.slice(1) + : rawUserOpHashes + : isTrustedSponsorship + ? meeUserOpHashes.slice(1) + : meeUserOpHashes; + const sessionTypeHash = rawUserOpHashes + ? buildSessionSuperTxTypeHash(sessionHashes.length) + : SUPERTX_MEEUSEROP_STRUCT_TYPEHASH; + const dummySignature = concatHex([ "0x177eee00", // Simple signature type encodeAbiParameters( [ - { type: "bytes32" }, // stxStructTypeHash + { type: "bytes32" }, // stxStructTypeHash (dynamic, session-specific) { type: "uint256" }, // userOp index - { type: "bytes32[]" }, // meeUserOpHashes - array of hashes + { type: "bytes32[]" }, // raw userOpHashes { type: "bytes" }, // superTxSignature ], - [ - SUPERTX_MEEUSEROP_STRUCT_TYPEHASH, - BigInt(index), - // If it is a trusted sponsorship, the payment userop can be skipped because it is not going to be executed at all. - isTrustedSponsorship ? meeUserOpHashes.slice(1) : meeUserOpHashes, - dummyStxSig, - ], + [sessionTypeHash, BigInt(index), sessionHashes, dummyStxSig], ), ]); @@ -537,6 +549,7 @@ export class SimulationService { const upperBoundTimestamp = Math.floor(Date.now() / 1000) + 300; // 5 mins const meeUserOpHashes: Hex[] = []; + const rawUserOpHashes: Hex[] = []; for (const { packedUserOp, @@ -558,6 +571,8 @@ export class SimulationService { { generateRandomHash: isTrustedPaymentUserOp }, ); + rawUserOpHashes.push(userOpHash); + const meeUserOpHash = getMeeUserOpHashEip712( userOpHash, lowerBoundTimestamp, @@ -660,6 +675,7 @@ export class SimulationService { isTrustedSponsorship, userOpIndex, sessionDetails ? { sessionDetails, smartSessionMode } : undefined, + sessionDetails ? rawUserOpHashes : undefined, ); } From cc85bde8a9dc226ffdfdbd681cf88e68418e40db Mon Sep 17 00:00:00 2001 From: Filip Date: Mon, 6 Apr 2026 20:24:10 +0200 Subject: [PATCH 2/3] fix: use NoMee fallback for smart session signatures (raw ECDSA, no SuperTx wrapping) --- src/modules/quotes/constants.ts | 24 --------- src/modules/quotes/quotes.service.ts | 54 +++++---------------- src/modules/simulator/simulation.service.ts | 39 ++------------- 3 files changed, 16 insertions(+), 101 deletions(-) diff --git a/src/modules/quotes/constants.ts b/src/modules/quotes/constants.ts index d4dd8182..e36481c5 100644 --- a/src/modules/quotes/constants.ts +++ b/src/modules/quotes/constants.ts @@ -1,5 +1,3 @@ -import { keccak256, toHex } from "viem"; - export enum MeeSignatureType { OFF_CHAIN = "0x177eee00", ON_CHAIN = "0x177eee01", @@ -44,25 +42,3 @@ export const SUPERTX_MEEUSEROP_STRUCT_TYPEHASH = // keccak256("MeeUserOp(bytes32 userOpHash,uint256 lowerBoundTimestamp,uint256 upperBoundTimestamp)"); export const MEE_USER_OP_TYPEHASH = "0x15a3822da13714219f4ba907e3daf8f006f6903616b4e7918e84eb2b8faf733d"; - -/** - * Builds a dynamic SuperTx typehash for smart sessions. - * - * Smart sessions go through validateSignatureForOwner which receives the raw - * userOpHash from SmartSession (no timestamp wrapping). Using the MeeUserOp[] - * array typehash would cause compareAndGetFinalHash to fail because: - * raw userOpHash ≠ keccak256(MEE_USER_OP_TYPEHASH || userOpHash || lower || upper) - * - * Instead we build a generic struct typehash with plain bytes32 fields: - * "SuperTx(bytes32 op0,bytes32 op1,...,bytes32 opN)" - * - * This takes the `else` branch in HashLib.compareAndGetFinalHash which does: - * structHash = keccak256(abi.encodePacked(outerTypeHash, itemHashes)) - */ -export function buildSessionSuperTxTypeHash(opCount: number): `0x${string}` { - const fields = Array.from( - { length: opCount }, - (_, i) => `bytes32 op${i}`, - ).join(","); - return keccak256(toHex(`SuperTx(${fields})`)); -} diff --git a/src/modules/quotes/quotes.service.ts b/src/modules/quotes/quotes.service.ts index 2d2d64e6..8b8a8605 100644 --- a/src/modules/quotes/quotes.service.ts +++ b/src/modules/quotes/quotes.service.ts @@ -68,7 +68,6 @@ import { MEE_SIGNATURE_TYPE_OFFSET, MeeSignatureType, SUPERTX_MEEUSEROP_STRUCT_TYPEHASH, - buildSessionSuperTxTypeHash, } from "./constants"; import { type ExecuteQuoteOptions, @@ -859,20 +858,6 @@ export class QuotesService { ({ meeUserOpHash }) => meeUserOpHash, ); - // For smart sessions: use raw userOpHashes (without timestamp wrapping). - // The SmartSession module passes raw userOpHash to validateSignatureForOwner, - // so itemHashes must contain raw hashes to match at HashLib.compareAndGetFinalHash. - const sessionItemHashes = packedMeeUserOps.map( - ({ userOpHash }) => userOpHash, - ); - const sessionSuperTxTypeHash = isSessionExists - ? buildSessionSuperTxTypeHash( - isEIP712TrustedSponsorshipSupported && isTrustedSponsorship - ? sessionItemHashes.length - 1 - : sessionItemHashes.length, - ) - : undefined; - const signedPackedMeeUserOps: SignedPackedMeeUserOp[] = packedMeeUserOps.map((packedMeeUserOp, index) => { const { lowerBoundTimestamp, upperBoundTimestamp, userOp } = @@ -909,33 +894,18 @@ export class QuotesService { case "simple": { if (isSessionExists) { if (isEIP712SupportedMeeVersion) { - // Smart sessions: use dynamic generic typehash with raw userOpHashes. - // validateSignatureForOwner receives raw userOpHash from SmartSession, - // so we must use raw hashes (not MeeUserOp EIP-712 hashes with timestamps). - const sessionHashes = - isEIP712TrustedSponsorshipSupported && isTrustedSponsorship - ? sessionItemHashes.slice(1) - : sessionItemHashes; - signature = concatHex([ - signatureType, // Simple signature type - encodeAbiParameters( - [ - { type: "bytes32" }, // stxStructTypeHash (dynamic, session-specific) - { type: "uint256" }, // userOp index - { type: "bytes32[]" }, // raw userOpHashes - { type: "bytes" }, // superTxSignature - ], - [ - sessionSuperTxTypeHash as `0x${string}`, - isEIP712TrustedSponsorshipSupported && - isTrustedSponsorship - ? BigInt(index - 1) - : BigInt(index), - sessionHashes, - signatureData, - ], - ), - ]); + // Smart sessions with MEE >= 2.2.0: use raw ECDSA signature (no MEE prefix). + // This routes through NoMeeFlowLib.validateSignatureForOwner on-chain, + // which does plain ECDSA recovery against the raw userOpHash. + // + // We cannot use the MEE simple mode (0x177eee00) because: + // - SmartSession calls K1MeeValidator.validateSignatureWithData externally + // - compareAndGetFinalHash calls hashTypedDataForAccount(msg.sender, ...) + // - msg.sender is SmartSession's address, not the account + // - SmartSession doesn't implement eip712Domain() → reverts + // + // Session expiry is handled by SmartSession's own policies. + signature = signatureData; } else { signature = concatHex([ signatureType, diff --git a/src/modules/simulator/simulation.service.ts b/src/modules/simulator/simulation.service.ts index e7d634f0..79a556df 100644 --- a/src/modules/simulator/simulation.service.ts +++ b/src/modules/simulator/simulation.service.ts @@ -63,10 +63,7 @@ import { import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { multicall } from "viem/actions"; // Don't import this from "@quotes" alone, it is causing the circular dependency issue -import { - SUPERTX_MEEUSEROP_STRUCT_TYPEHASH, - buildSessionSuperTxTypeHash, -} from "../quotes/constants"; +import { SUPERTX_MEEUSEROP_STRUCT_TYPEHASH } from "../quotes/constants"; import { type CustomOverride, type EIP712DomainReturn, @@ -109,7 +106,6 @@ export class SimulationService { sessionDetails: GrantPermissionResponseType; smartSessionMode: "ENABLE_AND_USE" | "USE"; }, - rawUserOpHashes?: Hex[], ) { // For trusted sponsorship, as we're ignoring the payment userOp, index should be sub by 1 to account for that // for all the userOps except the payment userOp itself @@ -125,32 +121,9 @@ export class SimulationService { "0xcae0d1955b99d4832aef73ed0a2237045fad91a738ee5b96ba76b9a12ffc6f824ab2ecaeeeb903b50704fdf4a5d64216adf7d6aaaca0ed2a67b86d8525c4b4bb1c" as Hex; if (sessionInfo?.sessionDetails) { - // Smart sessions: use dynamic generic typehash with raw userOpHashes. - // validateSignatureForOwner receives raw userOpHash from SmartSession, - // so we must use raw hashes (not MeeUserOp EIP-712 hashes with timestamps). - const sessionHashes = rawUserOpHashes - ? isTrustedSponsorship - ? rawUserOpHashes.slice(1) - : rawUserOpHashes - : isTrustedSponsorship - ? meeUserOpHashes.slice(1) - : meeUserOpHashes; - const sessionTypeHash = rawUserOpHashes - ? buildSessionSuperTxTypeHash(sessionHashes.length) - : SUPERTX_MEEUSEROP_STRUCT_TYPEHASH; - - const dummySignature = concatHex([ - "0x177eee00", // Simple signature type - encodeAbiParameters( - [ - { type: "bytes32" }, // stxStructTypeHash (dynamic, session-specific) - { type: "uint256" }, // userOp index - { type: "bytes32[]" }, // raw userOpHashes - { type: "bytes" }, // superTxSignature - ], - [sessionTypeHash, BigInt(index), sessionHashes, dummyStxSig], - ), - ]); + // Smart sessions with MEE >= 2.2.0: use raw ECDSA signature (no MEE prefix). + // Routes through NoMeeFlowLib.validateSignatureForOwner on-chain. + const dummySignature = dummyStxSig; const dummySessionDetails: GrantPermissionResponseType = { ...sessionInfo?.sessionDetails, @@ -549,7 +522,6 @@ export class SimulationService { const upperBoundTimestamp = Math.floor(Date.now() / 1000) + 300; // 5 mins const meeUserOpHashes: Hex[] = []; - const rawUserOpHashes: Hex[] = []; for (const { packedUserOp, @@ -571,8 +543,6 @@ export class SimulationService { { generateRandomHash: isTrustedPaymentUserOp }, ); - rawUserOpHashes.push(userOpHash); - const meeUserOpHash = getMeeUserOpHashEip712( userOpHash, lowerBoundTimestamp, @@ -675,7 +645,6 @@ export class SimulationService { isTrustedSponsorship, userOpIndex, sessionDetails ? { sessionDetails, smartSessionMode } : undefined, - sessionDetails ? rawUserOpHashes : undefined, ); } From e4477ae79834685164e9fe1c2369cd61e6e2a8b0 Mon Sep 17 00:00:00 2001 From: Venkatesh Date: Tue, 7 Apr 2026 13:32:33 +0530 Subject: [PATCH 3/3] changed the mee version default for EIP 712 sig --- src/modules/quotes/quotes.service.ts | 6 +++--- src/modules/quotes/schemas.ts | 1 - src/modules/simulator/simulation.service.ts | 4 ++-- src/modules/user-ops/userop.service.ts | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/modules/quotes/quotes.service.ts b/src/modules/quotes/quotes.service.ts index 8b8a8605..2632bef1 100644 --- a/src/modules/quotes/quotes.service.ts +++ b/src/modules/quotes/quotes.service.ts @@ -122,7 +122,7 @@ export class QuotesService { } = options; const isEIP712SupportedMeeVersion = this.isVersionConsistentAndAboveTarget( - "2.2.0", + "2.2.1", meeVersions, ); @@ -677,7 +677,7 @@ export class QuotesService { } = options; const isEIP712SupportedMeeVersion = this.isVersionConsistentAndAboveTarget( - "2.2.0", + "2.2.1", meeVersions, ); const isStxValidatorSupportedMeeVersion = @@ -894,7 +894,7 @@ export class QuotesService { case "simple": { if (isSessionExists) { if (isEIP712SupportedMeeVersion) { - // Smart sessions with MEE >= 2.2.0: use raw ECDSA signature (no MEE prefix). + // Smart sessions with MEE >= 2.2.1: use raw ECDSA signature (no MEE prefix). // This routes through NoMeeFlowLib.validateSignatureForOwner on-chain, // which does plain ECDSA recovery against the raw userOpHash. // diff --git a/src/modules/quotes/schemas.ts b/src/modules/quotes/schemas.ts index bacc8d0e..23adc690 100644 --- a/src/modules/quotes/schemas.ts +++ b/src/modules/quotes/schemas.ts @@ -23,7 +23,6 @@ export const meeVersionSchema = z.enum([ "3.0.0", "2.3.0", "2.2.1", - "2.2.0", "2.1.0", "2.0.0", "1.1.0", diff --git a/src/modules/simulator/simulation.service.ts b/src/modules/simulator/simulation.service.ts index 79a556df..6827670e 100644 --- a/src/modules/simulator/simulation.service.ts +++ b/src/modules/simulator/simulation.service.ts @@ -121,7 +121,7 @@ export class SimulationService { "0xcae0d1955b99d4832aef73ed0a2237045fad91a738ee5b96ba76b9a12ffc6f824ab2ecaeeeb903b50704fdf4a5d64216adf7d6aaaca0ed2a67b86d8525c4b4bb1c" as Hex; if (sessionInfo?.sessionDetails) { - // Smart sessions with MEE >= 2.2.0: use raw ECDSA signature (no MEE prefix). + // Smart sessions with MEE >= 2.2.1: use raw ECDSA signature (no MEE prefix). // Routes through NoMeeFlowLib.validateSignatureForOwner on-chain. const dummySignature = dummyStxSig; @@ -620,7 +620,7 @@ export class SimulationService { let meeVersion: MeeVersionsType = "2.0.0"; // From this version, the EIP712 signatures are being supported and the dummy signature needs to be different here - const eip712SignatureVersionTarget: MeeVersionsType = "2.2.0"; + const eip712SignatureVersionTarget: MeeVersionsType = "2.2.1"; if (meeVersionInfo) { meeVersion = meeVersionInfo.version.version; diff --git a/src/modules/user-ops/userop.service.ts b/src/modules/user-ops/userop.service.ts index 97d0bfca..3a7a1e52 100644 --- a/src/modules/user-ops/userop.service.ts +++ b/src/modules/user-ops/userop.service.ts @@ -458,14 +458,14 @@ export class UserOpService { let meeUserOpHash: Hex; if (isEIP712SupportedMeeVersion && isSimpleMode) { - // If the meeVersion >= 2.2.0, V2 meeUserOp hash will be generated for simple mode + // If the meeVersion >= 2.2.1, V2 meeUserOp hash will be generated for simple mode meeUserOpHash = getMeeUserOpHashEip712( userOpHash, lowerBoundTimestamp, upperBoundTimestamp, ); } else { - // If the meeVersion is less than 2.2.0, legacy meeUserOp hash will be generated + // If the meeVersion is less than 2.2.1, legacy meeUserOp hash will be generated meeUserOpHash = getMeeUserOpHash( userOpHash, lowerBoundTimestamp,