Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/user-operation-7702-init-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ox": patch
---

Fixed `UserOperation.toPacked` and `UserOperation.toInitCode` to keep the padded EIP-7702 `0x7702` initCode marker expected by EntryPoint 0.8 and 0.9, using the delegate address only with the new `forHash` option.
33 changes: 30 additions & 3 deletions src/erc4337/UserOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,12 +704,24 @@ export declare namespace hash {
* @param userOperation - The user operation to convert.
* @returns The init code.
*/
export function toInitCode(userOperation: Partial<UserOperation>): Hex.Hex {
export function toInitCode(
userOperation: Partial<UserOperation>,
options: toInitCode.Options = {},
): Hex.Hex {
const { forHash } = options
const { authorization, factory, factoryData } = userOperation
if (
factory === '0x7702' ||
factory === '0x7702000000000000000000000000000000000000'
) {
// Onchain, the EntryPoint detects EIP-7702 by the 20-byte `0x7702` marker
// and reads the initialization data from `initCode[20:]`.
if (!forHash)
return Hex.concat(
'0x7702000000000000000000000000000000000000',
factoryData ?? '0x',
)
// For the hash, the EntryPoint replaces the marker with the delegate.
if (!authorization) return '0x7702000000000000000000000000000000000000'
const delegation = authorization.address
return Hex.concat(delegation, factoryData ?? '0x')
Expand All @@ -718,6 +730,18 @@ export function toInitCode(userOperation: Partial<UserOperation>): Hex.Hex {
return Hex.concat(factory, factoryData ?? '0x')
}

export declare namespace toInitCode {
type Options = {
/**
* Prepares the init code for hashing: replaces the EIP-7702 `0x7702`
* marker with the delegate address.
*/
forHash?: boolean | undefined
}

type ErrorType = Hex.concat.ErrorType | Errors.GlobalErrorType
}

/**
* Transforms a User Operation into "packed" format.
*
Expand Down Expand Up @@ -766,7 +790,7 @@ export function toPacked(
verificationGasLimit || 0n,
callGasLimit || 0n,
)
const initCode = toInitCode(userOperation)
const initCode = toInitCode(userOperation, { forHash: options.forHash })
const gasFees = packUint128Pair(
maxPriorityFeePerGas || 0n,
maxFeePerGas || 0n,
Expand Down Expand Up @@ -806,7 +830,10 @@ export function toPacked(
export declare namespace toPacked {
/** Packing options. */
export type Options = {
/** Omits the paymaster signature while retaining its marker. */
/**
* Prepares for hashing: omits the paymaster signature while retaining its
* marker, and replaces the EIP-7702 `initCode` marker with the delegate.
*/
forHash?: boolean | undefined
}

Expand Down
167 changes: 132 additions & 35 deletions src/erc4337/_test/UserOperation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,62 @@ describe('toPacked', () => {
).toMatchInlineSnapshot(`"0x004122e325a297439656"`)
})
})

describe('eip7702', () => {
const userOperation = {
authorization: {
address: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
chainId: 1,
nonce: 69n,
yParity: 0,
r: '0x0000000000000000000000000000000000000000000000000000000000000001',
s: '0x0000000000000000000000000000000000000000000000000000000000000002',
},
callData: '0xdeadbeef',
callGasLimit: 6942069n,
factory: '0x7702',
factoryData: '0xcafebabe',
maxFeePerGas: 69420n,
maxPriorityFeePerGas: 69n,
nonce: 0n,
preVerificationGas: 6942069n,
sender: '0x1234567890123456789012345678901234567890',
signature: '0x',
verificationGasLimit: 6942069n,
} as const satisfies UserOperation.UserOperation<'0.8', true>

test('default: keeps the EntryPoint `0x7702` initCode marker', () => {
const packed = UserOperation.toPacked(userOperation)
expect(packed).toMatchInlineSnapshot(`
{
"accountGasLimits": "0x0000000000000000000000000069ed750000000000000000000000000069ed75",
"callData": "0xdeadbeef",
"gasFees": "0x0000000000000000000000000000004500000000000000000000000000010f2c",
"initCode": "0x7702000000000000000000000000000000000000cafebabe",
"nonce": 0n,
"paymasterAndData": "0x",
"preVerificationGas": 6942069n,
"sender": "0x1234567890123456789012345678901234567890",
"signature": "0x",
}
`)
const { factory, factoryData } = UserOperation.fromPacked(packed)
expect({ factory, factoryData }).toMatchInlineSnapshot(`
{
"factory": "0x7702000000000000000000000000000000000000",
"factoryData": "0xcafebabe",
}
`)
})

test('options: forHash', () => {
expect(
UserOperation.toPacked(userOperation, { forHash: true }).initCode,
).toMatchInlineSnapshot(
`"0x9f1fdab6458c5fc642fa0f4c5af7473c46837357cafebabe"`,
)
})
})
})

describe('toInitCode', () => {
Expand Down Expand Up @@ -965,55 +1021,96 @@ describe('toInitCode', () => {
).toBe('0x7702000000000000000000000000000000000000')
})

test('behavior: EIP-7702 factory with authorization and no factoryData', () => {
test('options: forHash: EIP-7702 factory with authorization and no factoryData', () => {
expect(
UserOperation.toInitCode({
factory: '0x7702',
factoryData: undefined,
authorization: {
address: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
chainId: 1,
nonce: 69n,
yParity: 0,
r: '0x0000000000000000000000000000000000000000000000000000000000000001',
s: '0x0000000000000000000000000000000000000000000000000000000000000002',
UserOperation.toInitCode(
{
factory: '0x7702',
factoryData: undefined,
authorization: {
address: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
chainId: 1,
nonce: 69n,
yParity: 0,
r: '0x0000000000000000000000000000000000000000000000000000000000000001',
s: '0x0000000000000000000000000000000000000000000000000000000000000002',
},
},
}),
{ forHash: true },
),
).toBe('0x9f1fdab6458c5fc642fa0f4c5af7473c46837357')
})

test('behavior: EIP-7702 factory with authorization and factoryData', () => {
test('options: forHash: EIP-7702 factory with authorization and factoryData', () => {
expect(
UserOperation.toInitCode({
factory: '0x7702',
factoryData: '0xdeadbeef',
authorization: {
address: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
chainId: 1,
nonce: 69n,
yParity: 0,
r: '0x0000000000000000000000000000000000000000000000000000000000000001',
s: '0x0000000000000000000000000000000000000000000000000000000000000002',
UserOperation.toInitCode(
{
factory: '0x7702',
factoryData: '0xdeadbeef',
authorization: {
address: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
chainId: 1,
nonce: 69n,
yParity: 0,
r: '0x0000000000000000000000000000000000000000000000000000000000000001',
s: '0x0000000000000000000000000000000000000000000000000000000000000002',
},
},
}),
{ forHash: true },
),
).toBe('0x9f1fdab6458c5fc642fa0f4c5af7473c46837357deadbeef')
})

test('behavior: EIP-7702 factory (full form) with authorization and factoryData', () => {
test('options: forHash: EIP-7702 factory (full form) with authorization and factoryData', () => {
expect(
UserOperation.toInitCode(
{
factory: '0x7702000000000000000000000000000000000000',
factoryData: '0xcafebabe',
authorization: {
address: '0x1234567890123456789012345678901234567890',
chainId: 1,
nonce: 42n,
yParity: 1,
r: '0x000000000000000000000000000000000000000000000000000000000000007b',
s: '0x00000000000000000000000000000000000000000000000000000000000001c8',
},
},
{ forHash: true },
),
).toBe('0x1234567890123456789012345678901234567890cafebabe')
})

test('behavior: EIP-7702 factory with authorization packs the 20-byte marker', () => {
const authorization = {
address: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
chainId: 1,
nonce: 69n,
yParity: 0,
r: '0x0000000000000000000000000000000000000000000000000000000000000001',
s: '0x0000000000000000000000000000000000000000000000000000000000000002',
} as const
expect(
UserOperation.toInitCode({
factory: '0x7702',
factoryData: undefined,
authorization,
}),
).toBe('0x7702000000000000000000000000000000000000')
expect(
UserOperation.toInitCode({
factory: '0x7702',
factoryData: '0xdeadbeef',
authorization,
}),
).toBe('0x7702000000000000000000000000000000000000deadbeef')
expect(
UserOperation.toInitCode({
factory: '0x7702000000000000000000000000000000000000',
factoryData: '0xcafebabe',
authorization: {
address: '0x1234567890123456789012345678901234567890',
chainId: 1,
nonce: 42n,
yParity: 1,
r: '0x000000000000000000000000000000000000000000000000000000000000007b',
s: '0x00000000000000000000000000000000000000000000000000000000000001c8',
},
factoryData: '0xdeadbeef',
authorization,
}),
).toBe('0x1234567890123456789012345678901234567890cafebabe')
).toBe('0x7702000000000000000000000000000000000000deadbeef')
})
})

Expand Down
Loading