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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "root",
"private": true,
"version": "4.0.2",
"version": "4.0.3",
"engines": {
"node": ">=18.0.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { AddressZero } from "@skandha/params/lib/index.js";
import {
AddressZero,
INITCODE_EIP7702_MARKER,
} from "@skandha/params/lib/index.js";
import {
UserOperation,
PackedUserOperation,
Expand Down Expand Up @@ -176,7 +179,13 @@ export function packUserOp(op: UserOperation): PackedUserOperation {
op.paymasterData
);
}
const factory = pad(op.factory ?? "0x", { dir: "right", size: 20 });
// The EIP-7702 marker must stay 2 bytes: paymasters (e.g. Pimlico's singleton)
// sign over keccak256(initCode), so padding it to 20 bytes invalidates their
// signature and the op reverts with AA34 at bundle time.
const factory =
op.factory?.toLowerCase() === INITCODE_EIP7702_MARKER
? INITCODE_EIP7702_MARKER
: pad(op.factory ?? "0x", { dir: "right", size: 20 });
return {
sender: op.sender,
nonce: BigInt(op.nonce),
Expand Down
64 changes: 64 additions & 0 deletions packages/executor/test/unit/services/packedUserOpUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, it, expect } from "vitest";
import { INITCODE_EIP7702_MARKER } from "@skandha/params/lib";
import { packUserOp } from "../../../src/services/EntryPointService/utils/packedUserOpUtils";
import { UserOperation } from "@skandha/types/lib/contracts/UserOperation";

const baseOp: UserOperation = {
sender: "0x54E3d8E0F9800440581D6d82C7A85F1167090d98",
nonce: "0x01",
callData: "0x",
callGasLimit: "0x3e93f",
verificationGasLimit: "0x1aaf8",
preVerificationGas: "0x10a2b",
maxFeePerGas: "0xd59f80",
maxPriorityFeePerGas: "0x1e8480",
signature: "0x",
};

describe("packUserOp initCode packing", () => {
it("keeps the EIP-7702 marker 0x7702 unpadded so paymaster signatures over keccak256(initCode) stay valid", () => {
// Pimlico's singleton paymaster commits to keccak256(userOp.initCode) of the
// canonical 2-byte marker. Padding it to 20 bytes changes the digest and the
// op reverts with FailedOp("AA34 signature error") at bundle time.
const packed = packUserOp({
...baseOp,
factory: INITCODE_EIP7702_MARKER,
factoryData: "0x",
});
expect(packed.initCode).toEqual(INITCODE_EIP7702_MARKER);
});

it("keeps the EIP-7702 marker unpadded regardless of prefix casing", () => {
const packed = packUserOp({
...baseOp,
factory: "0X7702" as `0x${string}`,
factoryData: "0x",
});
expect(packed.initCode.toLowerCase()).toEqual(INITCODE_EIP7702_MARKER);
});

it("appends factoryData after the EIP-7702 marker", () => {
const packed = packUserOp({
...baseOp,
factory: INITCODE_EIP7702_MARKER,
factoryData: "0xdeadbeef",
});
expect(packed.initCode).toEqual("0x7702deadbeef");
});

it("still packs a real 20-byte factory address followed by factoryData", () => {
const packed = packUserOp({
...baseOp,
factory: "0x5803c076563C85799989d42Fc00292A8aE52fa9E",
factoryData: "0xcafe",
});
expect(packed.initCode.toLowerCase()).toEqual(
"0x5803c076563c85799989d42fc00292a8ae52fa9ecafe"
);
});

it("packs an empty initCode when there is no factory", () => {
const packed = packUserOp({ ...baseOp });
expect(packed.initCode).toEqual("0x");
});
});
Loading