Skip to content

Commit 8654ffc

Browse files
jxomampcode-com
andauthored
fix: correct paymasterSignature encoding in toPackedUserOperation (#4209)
Amp-Thread-ID: https://ampcode.com/threads/T-019b8bfe-b21d-7031-aeb1-7ecdcd531c14 Co-authored-by: Amp <[email protected]>
1 parent 2b3b177 commit 8654ffc

File tree

4 files changed

+116
-3
lines changed

4 files changed

+116
-3
lines changed

.changeset/bright-dogs-swim.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"viem": patch
3+
---
4+
5+
Fixed encoding of `paymasterSignature` in `toPackedUserOperation` to use the correct ERC-4337 format with magic suffix and length prefix.

src/account-abstraction/utils/userOperation/getUserOperationHash.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ describe('entryPoint: 0.9', () => {
399399
},
400400
}),
401401
).toMatchInlineSnapshot(
402-
`"0x9a538c5deb10298bcc09baa188099e5a3935ec20d92f211a1d838ab214b260ba"`,
402+
`"0x802c25af5d98cd349b2118227faa93172fb791c4130a482272095cec45c4fc6e"`,
403403
)
404404
})
405405

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { describe, expect, test } from 'vitest'
2+
3+
import { toPackedUserOperation } from './toPackedUserOperation.js'
4+
import type { UserOperation } from '../../types/userOperation.js'
5+
6+
const paymasterSignatureMagic = '0x22e325a297439656'
7+
8+
describe('toPackedUserOperation', () => {
9+
const baseUserOp: UserOperation<'0.9'> = {
10+
callData: '0x',
11+
callGasLimit: 6942069n,
12+
maxFeePerGas: 69420n,
13+
maxPriorityFeePerGas: 69n,
14+
nonce: 0n,
15+
preVerificationGas: 6942069n,
16+
sender: '0x1234567890123456789012345678901234567890',
17+
signature: '0x',
18+
verificationGasLimit: 6942069n,
19+
}
20+
21+
test('default', () => {
22+
const packed = toPackedUserOperation(baseUserOp)
23+
expect(packed.paymasterAndData).toBe('0x')
24+
})
25+
26+
test('paymaster without signature', () => {
27+
const userOp: UserOperation<'0.9'> = {
28+
...baseUserOp,
29+
paymaster: '0x1234567890123456789012345678901234567890',
30+
paymasterVerificationGasLimit: 6942069n,
31+
paymasterPostOpGasLimit: 6942069n,
32+
paymasterData: '0xdeadbeef',
33+
}
34+
const packed = toPackedUserOperation(userOp)
35+
expect(packed.paymasterAndData).toMatchInlineSnapshot(
36+
`"0x12345678901234567890123456789012345678900000000000000000000000000069ed750000000000000000000000000069ed75deadbeef"`,
37+
)
38+
})
39+
40+
describe('paymasterSignature encoding', () => {
41+
const userOpWithPaymasterSig: UserOperation<'0.9'> = {
42+
...baseUserOp,
43+
paymaster: '0x1234567890123456789012345678901234567890',
44+
paymasterVerificationGasLimit: 6942069n,
45+
paymasterPostOpGasLimit: 6942069n,
46+
paymasterData: '0xdeadbeef',
47+
paymasterSignature: '0xcafebabe',
48+
}
49+
50+
test('forHash: true - uses magic only', () => {
51+
const packed = toPackedUserOperation(userOpWithPaymasterSig, {
52+
forHash: true,
53+
})
54+
expect(packed.paymasterAndData).toMatchInlineSnapshot(
55+
`"0x12345678901234567890123456789012345678900000000000000000000000000069ed750000000000000000000000000069ed75deadbeef22e325a297439656"`,
56+
)
57+
expect(packed.paymasterAndData.endsWith(paymasterSignatureMagic.slice(2))).toBe(true)
58+
})
59+
60+
test('forHash: false - includes signature + length + magic', () => {
61+
const packed = toPackedUserOperation(userOpWithPaymasterSig, {
62+
forHash: false,
63+
})
64+
expect(packed.paymasterAndData).toMatchInlineSnapshot(
65+
`"0x12345678901234567890123456789012345678900000000000000000000000000069ed750000000000000000000000000069ed75deadbeefcafebabe000422e325a297439656"`,
66+
)
67+
expect(packed.paymasterAndData.endsWith(paymasterSignatureMagic.slice(2))).toBe(true)
68+
expect(packed.paymasterAndData).toContain('cafebabe')
69+
expect(packed.paymasterAndData).toContain('0004')
70+
})
71+
72+
test('default (no options) - includes signature + length + magic', () => {
73+
const packed = toPackedUserOperation(userOpWithPaymasterSig)
74+
expect(packed.paymasterAndData).toMatchInlineSnapshot(
75+
`"0x12345678901234567890123456789012345678900000000000000000000000000069ed750000000000000000000000000069ed75deadbeefcafebabe000422e325a297439656"`,
76+
)
77+
})
78+
79+
test('longer signature encodes correct length', () => {
80+
const longSig =
81+
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef00'
82+
const userOp: UserOperation<'0.9'> = {
83+
...baseUserOp,
84+
paymaster: '0x1234567890123456789012345678901234567890',
85+
paymasterVerificationGasLimit: 1n,
86+
paymasterPostOpGasLimit: 1n,
87+
paymasterSignature: longSig,
88+
}
89+
const packed = toPackedUserOperation(userOp)
90+
expect(packed.paymasterAndData).toContain('0041')
91+
})
92+
})
93+
})

src/account-abstraction/utils/userOperation/toPackedUserOperation.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { concat } from '../../../utils/data/concat.js'
22
import { pad } from '../../../utils/data/pad.js'
3+
import { size } from '../../../utils/data/size.js'
34
import { numberToHex } from '../../../utils/index.js'
45
import type {
56
PackedUserOperation,
67
UserOperation,
78
} from '../../types/userOperation.js'
89
import { getInitCode } from './getInitCode.js'
910

11+
/** Magic suffix for paymaster signature encoding (keccak256("PaymasterSignature")[:8]) */
12+
const paymasterSignatureMagic = '0x22e325a297439656' as const
13+
1014
export type ToPackedUserOperationOptions = {
1115
/** Prepare the packed user operation for hashing. */
1216
forHash?: boolean | undefined
@@ -43,7 +47,10 @@ export function toPackedUserOperation(
4347
])
4448
const nonce = userOperation.nonce ?? 0n
4549

46-
// For v0.9, paymasterSignature can be provided separately and appended after paymasterData
50+
// For v0.9, paymasterSignature can be provided separately and appended after paymasterData.
51+
// The encoding uses a magic suffix and length prefix as per ERC-4337 spec:
52+
// - forHash: just append the magic (signature is not part of hash)
53+
// - !forHash: append signature + length (2 bytes) + magic
4754
const paymasterAndData = paymaster
4855
? concat([
4956
paymaster,
@@ -54,7 +61,15 @@ export function toPackedUserOperation(
5461
size: 16,
5562
}),
5663
paymasterData || '0x',
57-
...(paymasterSignature ? [paymasterSignature as `0x${string}`] : []),
64+
...(paymasterSignature
65+
? options.forHash
66+
? [paymasterSignatureMagic]
67+
: [
68+
paymasterSignature as `0x${string}`,
69+
pad(numberToHex(size(paymasterSignature)), { size: 2 }),
70+
paymasterSignatureMagic,
71+
]
72+
: []),
5873
])
5974
: '0x'
6075
const preVerificationGas = userOperation.preVerificationGas ?? 0n

0 commit comments

Comments
 (0)