diff --git a/.changeset/calm-cats-sign.md b/.changeset/calm-cats-sign.md new file mode 100644 index 00000000..858d85ae --- /dev/null +++ b/.changeset/calm-cats-sign.md @@ -0,0 +1,5 @@ +--- +"@zerodev/weighted-validator": patch +--- + +Handle missing initial UserOperation signatures and replace the current signer's stub without dropping co-signer signatures. diff --git a/plugins/weighted-r1-k1/toWeightedValidatorPlugin.test.ts b/plugins/weighted-r1-k1/toWeightedValidatorPlugin.test.ts new file mode 100644 index 00000000..540c4e9c --- /dev/null +++ b/plugins/weighted-r1-k1/toWeightedValidatorPlugin.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, test } from "bun:test" +import { createClient, custom } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import { mainnet } from "viem/chains" +import { + WeightedValidatorContractVersion, + createWeightedValidator +} from "./toWeightedValidatorPlugin.js" +import { decodeSignatures, encodeSignatures } from "./utils.js" + +const owner = privateKeyToAccount( + "0x2827b876ee775816460ab6eb4481352a752101f950899831702ccead54000001" +) + +const entryPointAddress = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" + +const getValidator = async () => { + const signer = { + account: owner, + getPublicKey: () => owner.address, + getDummySignature: () => `0x${"00".repeat(65)}` as `0x${string}`, + type: "0x01" as never + } + const client = createClient({ + chain: mainnet, + transport: custom({ + request: async ({ method }) => { + if (method === "eth_chainId") return "0x1" + throw new Error(`Unexpected RPC method: ${method}`) + } + }) + }) + + return createWeightedValidator(client, { + entryPoint: { + address: entryPointAddress, + version: "0.7" + }, + kernelVersion: "0.3.1", + validatorContractVersion: + WeightedValidatorContractVersion.V0_0_2_PATCHED, + signer, + config: { + threshold: 1, + signers: [{ publicKey: owner.address, weight: 1 }] + } + }) +} + +const userOperation = { + sender: "0x0000000000000000000000000000000000000001", + nonce: 0n, + callData: "0x", + callGasLimit: 100_000n, + verificationGasLimit: 100_000n, + preVerificationGas: 50_000n, + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n, + signature: "0x" +} as const + +describe("createWeightedValidator", () => { + test("creates a stub signature when the UserOperation signature is undefined", async () => { + const validator = await getValidator() + + const signature = await validator.getStubSignature({ + ...userOperation, + signature: undefined + } as never) + + expect(decodeSignatures(signature)).toHaveLength(1) + }) + + test("replaces its stub while preserving co-signer signatures", async () => { + const validator = await getValidator() + const cosignerSignature = `0x01${"11".repeat(65)}` as `0x${string}` + const stubSignature = await validator.getStubSignature({ + ...userOperation, + signature: encodeSignatures([cosignerSignature]) + }) + + const signature = await validator.signUserOperation({ + ...userOperation, + signature: stubSignature + }) + + const signatures = decodeSignatures(signature) + expect(signatures).toHaveLength(2) + expect(signatures[0]).toBe(cosignerSignature) + expect(signatures[1]).not.toBe(decodeSignatures(stubSignature)[1]) + }) +}) diff --git a/plugins/weighted-r1-k1/toWeightedValidatorPlugin.ts b/plugins/weighted-r1-k1/toWeightedValidatorPlugin.ts index 2d407608..66035b36 100644 --- a/plugins/weighted-r1-k1/toWeightedValidatorPlugin.ts +++ b/plugins/weighted-r1-k1/toWeightedValidatorPlugin.ts @@ -207,8 +207,16 @@ export async function createWeightedValidator< // Sign a user operation async signUserOperation(userOperation) { let signatures: readonly Hex[] = [] - if (userOperation.signature !== "0x") { - signatures = decodeSignatures(userOperation.signature) + if (userOperation.signature && userOperation.signature !== "0x") { + const ownStubSignature = concatHex([ + toHex(getIndexOfSigner(), { size: 1 }), + await signer.getDummySignature() + ]) + signatures = decodeSignatures(userOperation.signature).filter( + (signature) => + signature.toLowerCase() !== + ownStubSignature.toLowerCase() + ) } // last signer signs for userOpHash const userOpHash = getUserOperationHash({ @@ -230,8 +238,7 @@ export async function createWeightedValidator< async getStubSignature(userOperation) { let signatures: readonly Hex[] = [] - if (userOperation.signature !== "0x") { - console.log(userOperation.signature) + if (userOperation.signature && userOperation.signature !== "0x") { signatures = decodeSignatures(userOperation.signature) }