Skip to content

Commit 8129b24

Browse files
committed
fix: use dynamic typehash with raw userOpHashes for smart session signatures
1 parent 911bfe0 commit 8129b24

3 files changed

Lines changed: 73 additions & 16 deletions

File tree

src/modules/quotes/constants.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { keccak256, toHex } from "viem";
2+
13
export enum MeeSignatureType {
24
OFF_CHAIN = "0x177eee00",
35
ON_CHAIN = "0x177eee01",
@@ -42,3 +44,25 @@ export const SUPERTX_MEEUSEROP_STRUCT_TYPEHASH =
4244
// keccak256("MeeUserOp(bytes32 userOpHash,uint256 lowerBoundTimestamp,uint256 upperBoundTimestamp)");
4345
export const MEE_USER_OP_TYPEHASH =
4446
"0x15a3822da13714219f4ba907e3daf8f006f6903616b4e7918e84eb2b8faf733d";
47+
48+
/**
49+
* Builds a dynamic SuperTx typehash for smart sessions.
50+
*
51+
* Smart sessions go through validateSignatureForOwner which receives the raw
52+
* userOpHash from SmartSession (no timestamp wrapping). Using the MeeUserOp[]
53+
* array typehash would cause compareAndGetFinalHash to fail because:
54+
* raw userOpHash ≠ keccak256(MEE_USER_OP_TYPEHASH || userOpHash || lower || upper)
55+
*
56+
* Instead we build a generic struct typehash with plain bytes32 fields:
57+
* "SuperTx(bytes32 op0,bytes32 op1,...,bytes32 opN)"
58+
*
59+
* This takes the `else` branch in HashLib.compareAndGetFinalHash which does:
60+
* structHash = keccak256(abi.encodePacked(outerTypeHash, itemHashes))
61+
*/
62+
export function buildSessionSuperTxTypeHash(opCount: number): `0x${string}` {
63+
const fields = Array.from(
64+
{ length: opCount },
65+
(_, i) => `bytes32 op${i}`,
66+
).join(",");
67+
return keccak256(toHex(`SuperTx(${fields})`));
68+
}

src/modules/quotes/quotes.service.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
MEE_SIGNATURE_TYPE_OFFSET,
6969
MeeSignatureType,
7070
SUPERTX_MEEUSEROP_STRUCT_TYPEHASH,
71+
buildSessionSuperTxTypeHash,
7172
} from "./constants";
7273
import {
7374
type ExecuteQuoteOptions,
@@ -858,6 +859,20 @@ export class QuotesService {
858859
({ meeUserOpHash }) => meeUserOpHash,
859860
);
860861

862+
// For smart sessions: use raw userOpHashes (without timestamp wrapping).
863+
// The SmartSession module passes raw userOpHash to validateSignatureForOwner,
864+
// so itemHashes must contain raw hashes to match at HashLib.compareAndGetFinalHash.
865+
const sessionItemHashes = packedMeeUserOps.map(
866+
({ userOpHash }) => userOpHash,
867+
);
868+
const sessionSuperTxTypeHash = isSessionExists
869+
? buildSessionSuperTxTypeHash(
870+
isEIP712TrustedSponsorshipSupported && isTrustedSponsorship
871+
? sessionItemHashes.length - 1
872+
: sessionItemHashes.length,
873+
)
874+
: undefined;
875+
861876
const signedPackedMeeUserOps: SignedPackedMeeUserOp[] =
862877
packedMeeUserOps.map((packedMeeUserOp, index) => {
863878
const { lowerBoundTimestamp, upperBoundTimestamp, userOp } =
@@ -894,28 +909,29 @@ export class QuotesService {
894909
case "simple": {
895910
if (isSessionExists) {
896911
if (isEIP712SupportedMeeVersion) {
912+
// Smart sessions: use dynamic generic typehash with raw userOpHashes.
913+
// validateSignatureForOwner receives raw userOpHash from SmartSession,
914+
// so we must use raw hashes (not MeeUserOp EIP-712 hashes with timestamps).
915+
const sessionHashes =
916+
isEIP712TrustedSponsorshipSupported && isTrustedSponsorship
917+
? sessionItemHashes.slice(1)
918+
: sessionItemHashes;
897919
signature = concatHex([
898920
signatureType, // Simple signature type
899921
encodeAbiParameters(
900922
[
901-
{ type: "bytes32" }, // stxStructTypeHash
923+
{ type: "bytes32" }, // stxStructTypeHash (dynamic, session-specific)
902924
{ type: "uint256" }, // userOp index
903-
{ type: "bytes32[]" }, // meeUserOpHashes - array of hashes
925+
{ type: "bytes32[]" }, // raw userOpHashes
904926
{ type: "bytes" }, // superTxSignature
905927
],
906928
[
907-
SUPERTX_MEEUSEROP_STRUCT_TYPEHASH,
908-
// For trusted sponsorship, as we're ignoring the payment userOp, index should be sub by 1 to account for that
909-
// The index will be always greater than 1 if its sponsorship mode so sub by 1 is not a problem
929+
sessionSuperTxTypeHash!,
910930
isEIP712TrustedSponsorshipSupported &&
911931
isTrustedSponsorship
912932
? BigInt(index - 1)
913933
: BigInt(index),
914-
// If it is a trusted sponsorship, the payment userop can be skipped because it is not going to be executed at all.
915-
isEIP712TrustedSponsorshipSupported &&
916-
isTrustedSponsorship
917-
? meeUserOpHashes.slice(1)
918-
: meeUserOpHashes,
934+
sessionHashes,
919935
signatureData,
920936
],
921937
),

src/modules/simulator/simulation.service.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ import {
6363
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
6464
import { multicall } from "viem/actions";
6565
// Don't import this from "@quotes" alone, it is causing the circular dependency issue
66-
import { SUPERTX_MEEUSEROP_STRUCT_TYPEHASH } from "../quotes/constants";
66+
import {
67+
SUPERTX_MEEUSEROP_STRUCT_TYPEHASH,
68+
buildSessionSuperTxTypeHash,
69+
} from "../quotes/constants";
6770
import {
6871
type CustomOverride,
6972
type EIP712DomainReturn,
@@ -106,6 +109,7 @@ export class SimulationService {
106109
sessionDetails: GrantPermissionResponseType;
107110
smartSessionMode: "ENABLE_AND_USE" | "USE";
108111
},
112+
rawUserOpHashes?: Hex[],
109113
) {
110114
// For trusted sponsorship, as we're ignoring the payment userOp, index should be sub by 1 to account for that
111115
// for all the userOps except the payment userOp itself
@@ -121,20 +125,29 @@ export class SimulationService {
121125
"0xcae0d1955b99d4832aef73ed0a2237045fad91a738ee5b96ba76b9a12ffc6f824ab2ecaeeeb903b50704fdf4a5d64216adf7d6aaaca0ed2a67b86d8525c4b4bb1c" as Hex;
122126

123127
if (sessionInfo?.sessionDetails) {
128+
// Smart sessions: use dynamic generic typehash with raw userOpHashes.
129+
// validateSignatureForOwner receives raw userOpHash from SmartSession,
130+
// so we must use raw hashes (not MeeUserOp EIP-712 hashes with timestamps).
131+
const sessionHashes = rawUserOpHashes
132+
? (isTrustedSponsorship ? rawUserOpHashes.slice(1) : rawUserOpHashes)
133+
: (isTrustedSponsorship ? meeUserOpHashes.slice(1) : meeUserOpHashes);
134+
const sessionTypeHash = rawUserOpHashes
135+
? buildSessionSuperTxTypeHash(sessionHashes.length)
136+
: SUPERTX_MEEUSEROP_STRUCT_TYPEHASH;
137+
124138
const dummySignature = concatHex([
125139
"0x177eee00", // Simple signature type
126140
encodeAbiParameters(
127141
[
128-
{ type: "bytes32" }, // stxStructTypeHash
142+
{ type: "bytes32" }, // stxStructTypeHash (dynamic, session-specific)
129143
{ type: "uint256" }, // userOp index
130-
{ type: "bytes32[]" }, // meeUserOpHashes - array of hashes
144+
{ type: "bytes32[]" }, // raw userOpHashes
131145
{ type: "bytes" }, // superTxSignature
132146
],
133147
[
134-
SUPERTX_MEEUSEROP_STRUCT_TYPEHASH,
148+
sessionTypeHash,
135149
BigInt(index),
136-
// If it is a trusted sponsorship, the payment userop can be skipped because it is not going to be executed at all.
137-
isTrustedSponsorship ? meeUserOpHashes.slice(1) : meeUserOpHashes,
150+
sessionHashes,
138151
dummyStxSig,
139152
],
140153
),
@@ -537,6 +550,7 @@ export class SimulationService {
537550
const upperBoundTimestamp = Math.floor(Date.now() / 1000) + 300; // 5 mins
538551

539552
const meeUserOpHashes: Hex[] = [];
553+
const rawUserOpHashes: Hex[] = [];
540554

541555
for (const {
542556
packedUserOp,
@@ -558,6 +572,8 @@ export class SimulationService {
558572
{ generateRandomHash: isTrustedPaymentUserOp },
559573
);
560574

575+
rawUserOpHashes.push(userOpHash);
576+
561577
const meeUserOpHash = getMeeUserOpHashEip712(
562578
userOpHash,
563579
lowerBoundTimestamp,
@@ -660,6 +676,7 @@ export class SimulationService {
660676
isTrustedSponsorship,
661677
userOpIndex,
662678
sessionDetails ? { sessionDetails, smartSessionMode } : undefined,
679+
sessionDetails ? rawUserOpHashes : undefined,
663680
);
664681
}
665682

0 commit comments

Comments
 (0)