Environment
- Package:
@zerodev/weighted-validator@5.5.1
- SDK:
@zerodev/sdk@5.5.10, permissionless@0.3.5, viem@2.49.3
- Kernel v0.3.1, EntryPoint v0.7
- Config:
threshold: 1, signers: [{ publicKey: eoaAddress, weight: 1 }], V0_0_2_PATCHED
- Chain: verified on Base, Arbitrum, Ethereum mainnet
Summary
The vanilla 1-of-1 weighted-validator setup crashes on the SA's first outgoing UserOp with two distinct bugs. Both surface only when the SA hasn't been deployed yet (i.e. the very first sendUserOperation on a freshly-derived SA).
Bug 1 — getStubSignature crashes when userOperation.signature is undefined
In toWeightedValidatorPlugin.ts (currently line ~230):
async getStubSignature(userOperation) {
let signatures = []
if (userOperation.signature !== "0x") {
console.log(userOperation.signature) // <-- also a stray debug log
signatures = decodeSignatures(userOperation.signature)
}
...
}
On the first call from prepareUserOperation, userOperation.signature is undefined, not "0x". The !== "0x" guard passes through, and decodeSignatures(undefined) → decodeAbiParameters(schema, undefined) → createCursor(undefined) → undefined.buffer throws:
TypeError: undefined is not an object (evaluating 'bytes.buffer')
at createCursor (viem/utils/cursor.js)
at decodeAbiParameters (viem/utils/abi/decodeAbiParameters.js)
at decodeSignatures (@zerodev/weighted-validator/utils.js)
at getStubSignature (@zerodev/weighted-validator/toWeightedValidatorPlugin.js:230)
at prepareUserOperation (viem/account-abstraction)
Fix:
if (userOperation.signature && userOperation.signature !== "0x") {
signatures = decodeSignatures(userOperation.signature)
}
Also please drop the console.log on the line above.
Bug 2 — signUserOperation appends real sig to stub instead of replacing
In the same file (currently line ~210):
async signUserOperation(userOperation) {
let signatures = []
if (userOperation.signature !== "0x") {
signatures = decodeSignatures(userOperation.signature)
}
// ... hash + sign ...
return encodeSignatures([...signatures, lastSignature])
}
For solo-signer flows (or any flow where sendUserOperation on the kernelClient does prepare + sign in one shot), userOperation.signature at this point holds the stub from getStubSignature, not signatures from other co-signers. The code decodes the stub and appends the real sig, so the tx broadcasts with [stubSig, realSig] — two signatures both from the same signer index.
On-chain validateUserOp reverts:
The custom-error selector suggests the WeightedValidator contract rejects the duplicate signer / oversized signature array.
Proposed fix: for the solo path, don't decode existing sigs. Multi-party collaboration should use the separate sendUserOperationWithSignatures action (which is what the docs at multisig / advanced describe). Simplest patch:
async signUserOperation(userOperation) {
const userOpHash = getUserOperationHash({
userOperation: { ...userOperation, signature: "0x" } as UserOperation<entryPointVersion>,
entryPointAddress: entryPoint.address,
entryPointVersion: entryPoint.version,
chainId: chainId
})
const lastSignature = await account.signMessage({ message: { raw: userOpHash } })
return encodeSignatures([lastSignature])
}
If you want to preserve multi-party collaboration through this same code path, the guard needs to distinguish "sig field contains our own stub" from "sig field contains a co-signer's real sig" — the current code has no way to tell them apart.
Reproduction
Any 1-of-1 setup:
const validator = await createWeightedValidator(publicClient, {
signer: await toECDSASigner({ signer: privateKeyToAccount(pk) }),
config: { threshold: 1, signers: [{ publicKey: owner.address, weight: 1 }] },
entryPoint: { address: ENTRY_POINT_V07, version: '0.7' },
kernelVersion: KERNEL_V3_1,
validatorContractVersion: WeightedValidatorContractVersion.V0_0_2_PATCHED,
})
const account = await createKernelAccount(publicClient, {
plugins: { sudo: validator },
entryPoint: { address: ENTRY_POINT_V07, version: '0.7' },
kernelVersion: KERNEL_V3_1,
})
const client = createKernelAccountClient({ account, chain, bundlerTransport, paymaster })
await client.sendUserOperation({ callData: /* any call */ }) // <-- crashes
Workaround
We're running a patch-package patch against 5.5.1 with both fixes above until upstream ships. Happy to open a PR if that's helpful.
Also worth clarifying (bonus, links to #254)
Docs at https://docs.zerodev.app/sdk/advanced/multisig import from @zerodev/weighted-validator, but getUpdateConfigCall() only exists in @zerodev/weighted-ecdsa-validator. See #254 — same confusion pointed us at @zerodev/weighted-validator and then this bug set. If the two packages are meant to converge, prioritizing that would prevent new users from tripping the wrong one.
Environment
@zerodev/weighted-validator@5.5.1@zerodev/sdk@5.5.10,permissionless@0.3.5,viem@2.49.3threshold: 1, signers: [{ publicKey: eoaAddress, weight: 1 }],V0_0_2_PATCHEDSummary
The vanilla 1-of-1 weighted-validator setup crashes on the SA's first outgoing UserOp with two distinct bugs. Both surface only when the SA hasn't been deployed yet (i.e. the very first
sendUserOperationon a freshly-derived SA).Bug 1 —
getStubSignaturecrashes whenuserOperation.signatureis undefinedIn
toWeightedValidatorPlugin.ts(currently line ~230):On the first call from
prepareUserOperation,userOperation.signatureisundefined, not"0x". The!== "0x"guard passes through, anddecodeSignatures(undefined)→decodeAbiParameters(schema, undefined)→createCursor(undefined)→undefined.bufferthrows:Fix:
Also please drop the
console.logon the line above.Bug 2 —
signUserOperationappends real sig to stub instead of replacingIn the same file (currently line ~210):
For solo-signer flows (or any flow where
sendUserOperationon the kernelClient does prepare + sign in one shot),userOperation.signatureat this point holds the stub fromgetStubSignature, not signatures from other co-signers. The code decodes the stub and appends the real sig, so the tx broadcasts with[stubSig, realSig]— two signatures both from the same signer index.On-chain
validateUserOpreverts:The custom-error selector suggests the WeightedValidator contract rejects the duplicate signer / oversized signature array.
Proposed fix: for the solo path, don't decode existing sigs. Multi-party collaboration should use the separate
sendUserOperationWithSignaturesaction (which is what the docs at multisig / advanced describe). Simplest patch:If you want to preserve multi-party collaboration through this same code path, the guard needs to distinguish "sig field contains our own stub" from "sig field contains a co-signer's real sig" — the current code has no way to tell them apart.
Reproduction
Any 1-of-1 setup:
Workaround
We're running a
patch-packagepatch against 5.5.1 with both fixes above until upstream ships. Happy to open a PR if that's helpful.Also worth clarifying (bonus, links to #254)
Docs at https://docs.zerodev.app/sdk/advanced/multisig import from
@zerodev/weighted-validator, butgetUpdateConfigCall()only exists in@zerodev/weighted-ecdsa-validator. See #254 — same confusion pointed us at@zerodev/weighted-validatorand then this bug set. If the two packages are meant to converge, prioritizing that would prevent new users from tripping the wrong one.