Skip to content

feat(account): accept raw EOA signatures for ERC-1271 in SMA7702 - #362

Closed
dancoombs wants to merge 14 commits into
v2.0.xfrom
danc/sma7702-raw-eoa-1271
Closed

dancoombs wants to merge 14 commits into
v2.0.xfrom
danc/sma7702-raw-eoa-1271

Conversation

@dancoombs

@dancoombs dancoombs commented Jul 29, 2026 •

Copy link
Copy Markdown

Context

Some applications dispatch signature verification exclusively on whether the signer has code. Permit2 is the
concrete affected integration: after an EOA delegates with EIP-7702, Permit2 routes the EOA's otherwise ordinary
signature through ERC-1271. SMA7702 previously decoded that bare signature as a modular validation locator and
could revert, surfacing to callers as an unrelated gas-estimation failure.

This is narrower than general ERC-1271 compatibility. A verifier does not need this path for formats it recovers
as ECDSA before trying ERC-1271. In the OpenZeppelin version vendored here,
SignatureChecker.isValidSignatureNow recovers 65-byte signatures first, while its bytes-based recovery does not
accept 64-byte ERC-2098 signatures and therefore routes that compact form to ERC-1271.

Contract changes

  • SemiModularAccount7702.isValidSignature accepts canonical 65-byte ECDSA and 64-byte ERC-2098 signatures
    directly over the caller-supplied digest.
  • ModularAccountBase.isValidSignature becomes public virtual, allowing SMA7702 to delegate all non-raw inputs
    to the unchanged standard modular decoder.
  • SemiModularAccountBase._checkSignature rejects CONTRACT_OWNER when the resolved owner is address(this).
    The delegated key must use the EOA type. This closes a pre-existing self-ERC-1271 recursion that could promote
    a signature-only validation to the native fallback's global UserOperation or deferred-action authority.
    CONTRACT_OWNER remains supported for a distinct contract fallback signer.
  • Account IDs are bumped from v1.0.0 to v1.1.0 for all changed SMA implementations: alchemy.sma-7702,
    alchemy.sma-bytecode, and alchemy.sma-storage.

Raw mode is active by default and remains active only while all three gates hold:

  1. fallback signing is enabled;
  2. the resolved fallback signer is address(this); and
  3. native FALLBACK_VALIDATION has no pre-validation hooks.
State Signature length Behavior
Raw mode active 64 or 65 Bare ECDSA over the supplied digest; returns ERC-1271 magic or failure and does not revert.
Raw mode active Any other length Standard modular decoding.
Raw mode inactive Any length Standard modular decoding; valid modular signatures may succeed, while bare or malformed inputs may fail or revert.

Configuration and integration behavior

There is deliberately no independent raw-signature flag. The EOA key already has root authority through direct
transactions and can change or clear its EIP-7702 delegation. Raw mode can instead be deactivated by:

  1. installing a pre-validation hook on native FALLBACK_VALIDATION;
  2. rotating the fallback signer away from address(this); or
  3. disabling fallback signing.

The first option retains fallback signing but is not free: it restores standard modular decoding for bare
signatures, runs the hook across fallback runtime/UserOperation/ERC-1271 validation, and makes the fallback
ineligible for deferred actions. A no-op opt-out hook must implement all three validation-hook entry points and
accept empty hook data. Removing all native fallback validation hooks can reactivate raw mode if the other two
gates still hold.

Execution hooks, hooks on other validation keys, and the native fallback's isGlobal, isSignatureValidation,
and isUserOpValidation flags do not control raw mode. Those flags are inert for native fallback validation
generally because the SMA fallback paths short-circuit the ordinary flag checks and treat it as global.

The bare-length dispatcher exists only in public ERC-1271 validation. Runtime validation, UserOperation
validation, and deferred-action validation retain their existing framing, selector checks, hooks, signer-state
checks, and path-specific digests. Direct top-level transactions from the delegated EOA still bypass validation
and validation-associated hooks; selector-associated execution hooks still run.

The README also records two existing configuration hazards rather than expanding this patch:

  • granting a limited validation access to executeWithRuntimeValidation is root-equivalent; access to
    executeBatch alone is not;
  • configuring SingleSignerValidationModule with signer == account and using that module's CONTRACT_OWNER
    encoding creates a separate circular ERC-1271 authority composition. It does not traverse the native fallback
    branch and is not closed by the self-owner guard.

No module production code is changed.

Security implications

  • The raw signer must recover to the delegated EOA itself, which already controls the account. This adds a
    compatibility encoding, not a new key or authority boundary.
  • The digest is intentionally not wrapped in the account's replay-safe domain. Chain, protocol, nonce, deadline,
    action, and revocation binding come from the calling application, matching an undelegated EOA for digest
    binding. Recovery is deliberately stricter than raw ecrecover: 65-byte signatures require low-s and
    v of 27 or 28; 64-byte signatures use ERC-2098.
  • A canonical signature produced before delegation or while raw mode was inactive can validate later if raw mode
    is active, the caller supplies the same digest, and the application's nonce/deadline/revocation state still
    allows it. Re-delegating an existing v1.0.0 EOA to v1.1.0 therefore requires migration review of still-live
    approvals for codesize-only verifiers.
  • While raw mode is active, total modular signature lengths 64 and 65 are reserved. With no hook segments, this
    collides with module payloads of 58/59 bytes under an entity locator and 42/43 bytes under a direct-call locator.
    The module is not called; the collision normally returns ERC-1271 failure.
    doc/SMA7702-Raw-ERC1271-Signatures.md publishes the complete length formula and current-state detection
    procedure.

Scope

The raw ERC-1271 dispatcher is SMA7702-only. The self-CONTRACT_OWNER guard lives in
SemiModularAccountBase, so deployed bytecode changes for all three SMA implementations:

  • SemiModularAccount7702
  • SemiModularAccountBytecode
  • SemiModularAccountStorageOnly

ModularAccount and production modules are unchanged. There is no module change and no forward port in this PR.
All three SMA implementations belong in the external audit and release/deployment scope. This work is outside the
December 2024 audit scope.

Verification

All checks below use the CI-pinned Foundry v1.3.6:

  • Default suite: 331/331
  • SMA7702 focused suite: 34/34
  • Optimized deep non-script suite: 322/322, including 10,000 fuzz runs and 5,000 invariant runs
  • Gas snapshot suite: 34/34
  • Default and gas-profile forge fmt --check
  • All four Solhint targets via pnpm lint
  • optimized-build, optimized-build-sma-storage, and optimized-build-standalone
  • git diff --check

Optimized deployed sizes:

Contract Size EIP-170 margin
SemiModularAccount7702 24,358 bytes 218 bytes
SemiModularAccountBytecode 24,261 bytes 315 bytes
SemiModularAccountStorageOnly (optimized-build-sma-storage) 24,206 bytes 370 bytes

Coverage includes Permit2 and both OpenZeppelin helper paths, 64/65 canonical behavior, high-s and invalid-v
rejection, all raw-mode gate combinations and lifecycle transitions, hook types and fallback flags, entity and
direct-call locator collisions, exact 63/64/65/66/67 boundaries, direct bare UserOperation rejection,
self-CONTRACT_OWNER rejection in UserOperation and deferred-action paths, exploit regression, and preservation
of distinct contract fallback signers.

Release requirements

  • Update off-chain consumers that string-match any of the three old v1.0.0 SMA account IDs.
  • Recompute initcode hashes and mine replacement CREATE2 salts/addresses for all three changed SMA implementations
    and dependent factory deployments before deployment. The existing deployment ledger is intentionally not
    changed until that release work is complete.
  • Foundry is pinned to v1.3.6 because v1.4.0 introduced formatter and build-time lint changes not applied to the
    v2.0.x release line. Existing Solhint jobs remain enforced.

Dan and others added 14 commits July 29, 2026 10:52
Contracts that route signature checks through ERC-1271 whenever the signer
address has code - Permit2, OpenZeppelin's SignatureChecker, Seaport - hand a
delegated account the plain ECDSA signature the EOA's wallet produced, since
the wallet has no way to know the address is delegated. SemiModularAccount7702
read the leading bytes of that signature as a validation locator, resolved an
uninstalled validation function, and reverted while parsing rather than
returning the ERC-1271 failure value. Uniswap surfaced this as an unrelated gas
estimation failure.

Accept bare 64- and 65-byte ECDSA signatures over the unwrapped digest,
verified against address(this). This matches Uniswap's Calibur, ZeroDev's
Kernel7702, MetaMask's stateless delegator, and eth-infinitism's
Simple7702Account.

No replay-safe wrapping is needed on this path: the recovered address is
compared against address(this), which binds the signature to this account just
as the domain separator would. The path is gated on the EOA still being the
account's active fallback signer, so the accepted signers are a subset of those
fallback validation already accepts. Only SemiModularAccount7702 is affected -
in the other variants the fallback signer is an independent key that may sign
for several accounts, so replay-safe hashing remains required there.

Scoped to the external isValidSignature entry point. User operation validation,
and with it deferred action authorization, is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Route 64- and 65-byte signatures through the standard validation pipeline whenever raw mode is inactive, including when fallback validation hooks are installed. Document the opt-out and replay implications and add regression coverage.

Co-Authored-By: Codex <noreply@openai.com>
Use the latest stable release compatible with the release branch's formatter, and format the SMA7702 additions with that toolchain.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Codex <noreply@openai.com>
@dancoombs dancoombs closed this Jul 31, 2026
@dancoombs
dancoombs deleted the danc/sma7702-raw-eoa-1271 branch July 31, 2026 13:28
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.

1 participant