Skip to content

fix: Safe WebAuthn owner matching, init-signature sorting, and estimation-path input handling - #216

Merged
Sednaoui merged 5 commits into
devfrom
fix/safe-webauthn-owners
Jul 23, 2026
Merged

fix: Safe WebAuthn owner matching, init-signature sorting, and estimation-path input handling#216
Sednaoui merged 5 commits into
devfrom
fix/safe-webauthn-owners

Conversation

@sherifahmed990

@sherifahmed990 sherifahmed990 commented Jul 22, 2026

Copy link
Copy Markdown
Member

Fixes for WebAuthn owner handling in SafeAccount.

  • Init UserOperations failed with GS026 for ~50% of WebAuthn key pairs: during account init the signature encoder substitutes the WebAuthn shared-signer address for a WebauthnPublicKey owner, but sortSignatures keyed on the per-owner verifier-proxy address. Whenever the two sorted differently relative to the other owners, the encoded signatures were out of ascending owner order and the Safe rejected the op — after gas estimation had already succeeded. Sorting now keys on the address that actually gets encoded.
  • WebAuthn owners could never be swapped/removed: createWebAuthnSignerVerifierAddress returned a lowercase address while getOwners() returns checksummed ones, so the lookups in createSwapOwnerMetaTransactions/createRemoveOwnerMetaTransaction always threw "not a current owner". The derived address is now checksummed and owner lookups are case-insensitive (lowercase EOA inputs work too).
  • Estimation-path overrides: user-supplied dummySignerSignaturePairs containing raw WebauthnPublicKey signers threw "Must define isInit parameter" with no way to supply the flag through the overrides API — in both createUserOperation and the public baseEstimateUserOperationGas. The init flag is now derived from the operation’s own init fields at both call sites, and the WebAuthn verifier-config overrides are forwarded to the signature encoder so deployed accounts with custom verifier setups encode the correct owner address instead of one derived from the defaults. The PAYMASTER_SIG_MAGIC check and its entrypoint-v0.9 gate also compared case-sensitively, spuriously rejecting uppercase hex paymasterData and lowercase entrypoint addresses.
  • Docs: clarify that getSignerLowerCaseAddress derives owner addresses (WebAuthn → verifier address), not just lowercases input.

Tests added in test/safe/webauthnInitSignatureSort.test.js and test/safe/webauthnDummyPairsEstimation.test.js; full offline suite passes.

Summary by CodeRabbit

  • Bug Fixes
    • Improved WebAuthn dummy-signature handling for user operation gas estimation, including correct “init” vs “deployed” owner resolution.
    • Fixed WebAuthn signer/verifier address formatting and ensured consistent sorting behavior across initialization and regular flows.
    • Improved compatibility for paymaster signature validation and EntryPoint checks with case-insensitive address handling.
    • Made Safe owner swap/remove operations reliable regardless of address capitalization.
  • Tests
    • Added regression coverage for WebAuthn signature sorting during initialization.
    • Added regression coverage for gas estimation with user-supplied dummy signer signature pairs containing raw WebAuthn signers.

sherifahmed990 and others added 4 commits July 22, 2026 12:40
…gets encoded

During account init the signature encoder substitutes the WebAuthn
shared-signer address for a WebauthnPublicKey owner, but sortSignatures
keyed on the per-owner verifier-proxy address. Whenever the two sort
differently relative to the other owners (~50% of key pairs) the encoded
signatures were not in ascending owner order, so the Safe rejected the
init UserOperation with GS026 — after gas estimation had succeeded,
since the dummy-signature path resolves the shared signer before sorting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sitively

createWebAuthnSignerVerifierAddress returned a lowercase address while
getOwners() returns checksummed ones, so the indexOf lookups in
createSwapOwnerMetaTransactions/createRemoveOwnerMetaTransaction never
matched a WebAuthn owner and always threw 'not a current owner'.
Checksum the derived address (matching createProxyAddress) and make the
owner lookups case-insensitive so lowercase EOA inputs work too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…agic casing

- user-supplied dummySignerSignaturePairs containing raw
  WebauthnPublicKey signers threw 'Must define isInit parameter' with no
  way to supply the flag through the overrides API; the init flag is now
  derived from the factory presence and passed to the signature
  formatter, matching the expectedSigners path.
- the parallelPaymasterInitValues PAYMASTER_SIG_MAGIC check and its
  entrypoint-v0.9 gate compared case-sensitively, spuriously rejecting
  uppercase hex paymasterData (which the EIP-712 trimmer and the
  MultiChain subclass accept) and lowercase entrypoint addresses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…just casing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5dd02686-6b30-45ff-bbb8-c1d2f7d94a74

📥 Commits

Reviewing files that changed from the base of the PR and between 2979d98 and 635ab78.

📒 Files selected for processing (2)
  • src/account/Safe/SafeAccount.ts
  • test/safe/webauthnDummyPairsEstimation.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/account/Safe/SafeAccount.ts

📝 Walkthrough

Walkthrough

SafeAccount now propagates initialization state into WebAuthn dummy-signature encoding, normalizes verifier and address comparisons, and adds regression coverage for initialization, deployed, and overridden WebAuthn signer flows.

Changes

SafeAccount behavior updates

Layer / File(s) Summary
WebAuthn initialization signature flow
src/account/Safe/SafeAccount.ts, test/safe/webauthnInitSignatureSort.test.js, test/safe/webauthnDummyPairsEstimation.test.js
Gas-estimation and placeholder signatures now pass isInit and verifier overrides, resolving shared versus per-owner WebAuthn addresses correctly. Verifier addresses are checksummed, and regression tests cover sorting and estimation cases.
Parallel paymaster compatibility checks
src/account/Safe/SafeAccount.ts
Paymaster magic-suffix and EntryPoint address comparisons are now case-insensitive.
Case-insensitive owner operations
src/account/Safe/SafeAccount.ts
Owner swap and removal lookups now compare addresses after lowercasing.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SafeAccount
  participant SignatureFormatter
  participant WebAuthnSigner
  participant Bundler
  SafeAccount->>SafeAccount: derive isInit from factory fields
  SafeAccount->>SignatureFormatter: format dummy signature with WebAuthn overrides
  SignatureFormatter->>WebAuthnSigner: resolve shared or verifier-proxy owner
  WebAuthnSigner-->>SignatureFormatter: return owner address
  SignatureFormatter-->>SafeAccount: encode userOperation.signature
  SafeAccount->>Bundler: submit gas-estimation request
Loading

Possibly related PRs

Suggested reviewers: andrewwahid, sednaoui

Poem

A bunny signs where passkeys glow,
Shared keys guide the bytes below.
Paymaster checks now heed case,
Owners find their proper place.
Hop, hop—the Safe flows right! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main Safe WebAuthn changes in the PR.
Description check ✅ Passed It covers the main changes and testing, though it does not follow the template headings exactly or include a dedicated Risk / Compatibility section.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@Sednaoui Sednaoui left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. baseEstimateUserOperationGas() still formats raw WebAuthn dummy pairs without isInit.
  2. The updated path doesn’t forward custom WebAuthn contract overrides.

@Sednaoui
Sednaoui dismissed their stale review July 22, 2026 18:25

Withdrawn.

@Sednaoui
Sednaoui self-requested a review July 22, 2026 18:35

@Sednaoui Sednaoui left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see previous comments

webAuthnSharedSigner,
// needed when user-supplied dummySignerSignaturePairs contain raw
// WebauthnPublicKey signers — the encoder requires the init flag
isInit,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For isInit: false, it also needs the verifier, factory, singleton, and proxy creation code values. Without them, it derives the owner using defaults rather than the caller’s custom configuration

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

The previous fix covered createUserOperation's formatter call only, and
only passed the init flag and shared signer:

- baseEstimateUserOperationGas still formatted user-supplied
  dummySignerSignaturePairs without isInit, so raw WebauthnPublicKey
  signers threw 'Must define isInit parameter' for direct callers. The
  init flag is now derived from the operation's own initCode/factory
  field (as the expectedSigners branch already did) and passed along
  with the WebAuthn verifier config to both formatter calls.
- createUserOperation's formatter call omitted the five verifier-config
  overrides, so deployed accounts (isInit=false) with a custom WebAuthn
  verifier setup derived the owner address from the DEFAULT_* constants
  — wrong owner, wrong sort order. The in-scope config values are now
  forwarded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sherifahmed990
sherifahmed990 requested a review from Sednaoui July 23, 2026 09:53
@Sednaoui
Sednaoui merged commit 2d02d2c into dev Jul 23, 2026
3 checks passed
@Sednaoui
Sednaoui deleted the fix/safe-webauthn-owners branch July 23, 2026 10:09
@Sednaoui Sednaoui mentioned this pull request Jul 27, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants