DeFi Security AllianceRequest an audit
Menu

EVM patterns

ERC6492 Signature Verification and the Prepare Retry

A wrapped smart account proof can run a deployment or preparation call before the wallet answers. ERC6492 signature verification needs a state check as well as a verdict when the caller can commit writes.

A blue wireframe wallet in a glass chamber beside a solid wallet with a blue latch, illustrating temporary setup and surviving state.

Key facts

Local EVM grid
36 cases across 3 routes with 12 inputs each
Reference isValidSig
3 of 12 calls left an observed state change, including 2 false results
Guard comparator
0 of 12 calls left an observed change under the audit-derived condition
State boundary
The outer caller inspected code presence, readiness and the factory counter after the subcall
Reference scope
The published ERC6492 Solidity excerpt compiled with fixture wallets and proof bytes

How a signature can carry an executable step

A user tries to sign in with a wallet before its first transaction. The application knows its predicted address. The wallet will be deployed later, so a direct signature query has no contract to answer it yet. ERC6492 signature verification lets the application evaluate the wallet's signature policy by supplying the setup call needed to make that policy available.

The wrapper changes what the verifier receives. A plain ERC1271 contract signature goes to the wallet's isValidSignature method. An ERC-6492 wrapper can first send a call elsewhere. The wallet's decision comes after that call, so a caller needs evidence about the final answer and about the state left behind.

Claimed signer and digest
The application passes the address and message hash it wants to verify. Keep those values tied to the original request throughout the operation.
Wrapper suffix
A fixed 32-byte marker distinguishes the wrapped format. Check it before ordinary contract verification and ecrecover.
Target and calldata
Decode the prefix to obtain the address to call and the bytes to send. For an undeployed wallet the call can deploy it. For an existing wallet it can prepare it to verify.
Inner proof
Pass the original signature bytes to the wallet. ERC-1271 uses the exact success value 0x1626ba7e.

A predictable address makes the opening scenario possible. With CREATE2, the address calculation includes the deployer, salt and hash of the initialization code. Keep the address derivation separate from the security review of the initialized wallet. A predicted address tells an application where to check. The wallet's code and state determine which proof it accepts there.

An existing wallet creates a second path through the same format. The wrapper might have been created before deployment, so the verifier can ask the live wallet to check its inner proof first. A rejection can lead to the preparation step. This is where the existing code check and the decision to run payload bytes come apart.

What persisted in 36 local EVM cases

Method

We compiled the Solidity reference excerpt from the captured ERC-6492 specification and exercised both public verification entrypoints in a local EVM. A separate comparator changed one rollback condition to match the recommendation in the published Hunter audit. Each case had a fresh factory and signer fixture. The harness inspected the state after the verifier subcall returned or reverted.

Reference
The UniversalSigValidator excerpt at 8dd085d159cb123f545c272c0d871a5339550e79. The source and audit were retrieved on .
Population and denominator
Enumerate 3 routes, 3 initial wallet states, 2 proof values and 2 payload behaviors. All 36 cases ran with zero exclusions.
Wallet states
Absent code, deployed and ready or deployed and dormant. The fixture accepted its designated proof for the fixed digest once it was ready.
Payload control
A successful fixture call deployed or prepared the signer and incremented a factory counter. Its failure counterpart attempted a counter write and reverted.
Persistence criterion
A change in code presence, readiness or the factory counter after the subcall. The outer harness retained control so it could inspect the state on either return path.
Reproduction
Run measurement.py with --verify in seo/research/erc6492-signature-verification-2026-11-06/ using the recorded Forge and Solidity versions. The script recompiles SignatureMatrix.t.sol and compares the observed rows with measurement.json. These artifacts are retained in the repository.

Results

Verifier outcomes and surviving state changes on . Each route ran the same 12 input combinations.
RouteReturned trueReturned falseRevertedState change survivedChange survived after false
Reference isValidSig43532
Reference isValidSigWithSideEffects43553
Guard comparator isValidSig43500

The three routes returned the same outcome for each corresponding input in the grid. Read the state next. With the reference isValidSig entrypoint, both successful payloads for a dormant wallet left it ready. One of those signatures returned false. A ready wallet with a mismatched proof also returned false after the prepare attempt incremented the factory counter.

Selected cases from the reference isValidSig route
Initial walletInner proofPayload behavior if calledVerifier resultObserved state after the call
AbsentMatchingDeploytrueCode absent again. Counter restored.
DormantMismatchedPreparefalseWallet ready. Counter increased.
ReadyMatchingReverttrueReadiness and counter unchanged. Payload skipped.
DormantMatchingRevertRevertedWallet still dormant. Counter restored.

One useful counterexample uses the mismatched proof for an absent signer. Both reference entrypoints return false when the deployment payload succeeds. Under the guarded entrypoint the address has no code again. With side effects allowed, the same input leaves a deployed wallet and an incremented counter. The verdict comparison alone would miss the difference between the two observations.

A readiness check alone would also miss a change. In the ready-wallet case with a mismatched proof, preparation can set the ready flag to its existing value while the factory keeps a write of its own. This is why the harness reads both contracts. An integration test can apply the same idea to each contract that participates in its actual preparation path.

Limits

  • Matching and mismatched inputs are fixture byte strings. The wallet compares a fixed digest and proof hash.
  • We read code presence, readiness and one counter. Other storage, token balances and callbacks fall outside the sample.
  • The compiled code ran locally with a Cancun execution target. These observations describe the captured excerpt and the one-line comparator.
  • Persistence means visibility after the verifier subcall. A later outer revert can still undo those changes.

Why an existing wallet can reach the prepare branch

Follow the dormant wallet with the mismatched proof. Its code is present, so the first pass skips the payload. After a nonmagic return, the reference tries again with tryPrepare enabled. On this pass the factory runs and changes the wallet's readiness. The wallet can now evaluate the proof in its prepared state and still reject it.

At that point the code reads two decisions with different predicates. A payload can run when the wallet starts with no code or when the prepare flag is set. The rollback condition in the captured reference checks for initial code absence. With a deployed wallet, the second predicate stays false even though the first allowed a write. The caller receives the boolean after those writes have survived the subcall.

Rollback predicates compared in the experiment
// Predicate in the published reference excerpt
    contractCodeLen == 0 && isCounterfactual && !allowSideEffects

    // Predicate in the audit-derived comparator
    (contractCodeLen == 0 || tryPrepare) && isCounterfactual && !allowSideEffects

The rollback path deliberately reverts the inner call with a result byte. An outer wrapper catches it and converts it to a boolean. That sequence lets the caller receive a successful verdict even when the temporary deployment has disappeared.

A normal false return leaves rollback to the caller or verifier logic.

Preparation and rollback in a wrapped signature check Both paths receive a wrapped proof. The absent wallet is created temporarily and the reference rolls back the inner call. The existing wallet rejects its first check, then the prepare branch runs. The reference predicate lets that second path return with its changes intact. Wrapped proof Wallet absent Existing wallet rejects Deploy and check Prepare and retry Revert inner frame Writes can survive
The reference can execute a payload on either path. Initial code absence determines whether its guarded result takes the rollback route.
Absent signer
Deployment runs inside the call. The rollback result reaches the wrapper with code presence restored to its original value.
Deployed signer that rejects
The retry allows preparation. If the payload succeeds, the captured reference can leave writes visible to the caller.

Hunter documented this condition in its review and marked the fix resolved in the reviewed code. The narrower predicate remains in the captured specification excerpt. Our comparator applies the audit's proposed condition to that excerpt. A release review should identify which implementation the application actually executes.

Choose the execution context for verification

A backend checking a login request can evaluate the wallet through eth_call. Ethereum's RPC executes the request without creating a blockchain transaction. The reference deployless helper creates a validator during that execution and returns a result byte. A successful simulation can use temporary state created in the same call.

On-chain static execution has a different boundary. STATICCALL prevents state-changing operations in its call tree, including creation and storage writes. Wrapping the same preparation logic in a static call therefore changes what it can execute. This distinction matters for a caller that wants to check an undeployed signer from a contract while enforcing a static boundary.

Execution contexts for the same application request
ContextWhat can executeEvidence to retain
Backend simulationTemporary deployment or preparation within the RPC executionChain, block reference, signer and exact verifier input
Static contract checkThe live wallet's read-only validation pathWallet code and the expected return value at that state
Transaction with preparationA stateful external call before the application consumes the verdictAllowed targets and call data, required final state and caller rollback behavior

A simulation result describes one execution against a selected chain state. Save the request inputs before asking a second provider to reproduce it. If the providers use different block references, a change in wallet code or permissions can explain their disagreement. Match the block before comparing the verifier. A transport failure also needs a separate status from an executed wallet rejection, since the application may need to retry the check before it can make an authorization decision.

The captured viem implementation shows why a library function name alone provides little deployment evidence. Its ERC-6492 route can call a configured verifier address or supply deployless creation data. For a server integration, log which route you chose and the chain and block used to evaluate it. A reproduction needs the same inputs and a comparable execution context.

Use the transaction simulation guide when the same wallet flow later requests a real transaction. Save the block used to verify the sign-in proof. Submitting a transaction introduces its own state and execution checks.

Turn the state boundary into release criteria

Start from the state the application is willing to change. A verification-only operation may require the wallet to remain undeployed or dormant afterward. An onboarding transaction may explicitly authorize deployment. Treat those as separate release criteria. Put the decision at the caller boundary because a verifier return value alone cannot establish what the caller will commit.

  1. Identify the actual entrypoint and runtime implementation. Include the library revision, configured verifier address when present and the wrapper your protocol uses to receive a verdict.
  2. Track the effects of the payload as the verifier's caller would see them. Observe whether the signer gains code, whether readiness changes and whether another contract retains a write.
  3. Pair a known matching proof with a mismatched one on the same wallet state. Force the external payload to fail and keep that row separate from an ordinary rejection.
  4. Repeat with an existing wallet whose live check succeeds before preparation. A deliberately failing payload gives this case a useful control because the result should still succeed when the payload is skipped.
  5. Exercise the entire application call with the verdict handling it will use in production. A caller that reverts on rejection has a different final-state boundary from a caller that records failure and continues.

A requesting party can supply the wrapper's target and bytes. Decide which targets may run, what they may do and which identity they see as their caller. Keep the review scope open past the factory. A factory can call other contracts, and those calls can reach permissions the outer protocol did not intend to expose during verification.

Consider a verifier used by two callers. One asks for a login verdict and the other is part of a transaction that initializes an account. The first can isolate its check in an RPC execution. The second needs a precise statement of the operations it authorizes and where failure causes a revert. Document that difference next to the call sites so a maintainer can evaluate what changes when a common helper moves from one path to the other.

The smallest useful assertion names an actor and a final value. For example, take a snapshot of the setup contract's counter immediately before the verifier call and read it again after the caller handles rejection. Include an expected value for a legitimate preparation too. An implementation that skips every payload could pass a test that checks only for unchanged state.

Check reentry during the payload as its own test. The ERC identifies a reentrancy concern because setup uses a normal call. Our grid measures surviving writes after return. A callback test should additionally observe what the external target can reach while the caller is still processing the signature.

Record the target allowlist and state assertions in the audit scope builder. If a reviewer has already inspected the verifier, use the Pharos Production member profile to locate the provider context and ask for the reviewed revision and call path before relying on that work.

Bind the result to the request you authorize

Verification establishes the wallet's answer for the supplied digest at the selected state. The application still decides what that digest authorizes. For sign-in, include the intended domain, account, chain and fresh nonce in the request checks. A prior successful answer may become stale when the wallet changes its authentication policy.

Wallet onboarding adds an application decision after the verifier returns. Pharos Production's guide to smart account sign-in and wallet connections puts ERC-1271 and ERC-6492 in the wider dApp flow, alongside domain binding and fresh nonces for Sign-In with Ethereum. Use that context to decide which session the verifier result may open.

The replay boundary extends across networks. The same counterfactual address can be evaluated in a different chain context when its deployment inputs reproduce the address there. The ERC points out that changed authentication on one chain can leave an earlier proof valid in another. The application's request checks should follow the chain the user intended. The signature replay guide covers how to give each proof a concrete action and validity window.

Take the example one step further. A wallet was already deployed when the app received its wrapped proof. After the first rejection, the check prepared the wallet and received a successful result. That gives the app a reason to consider the request. Before-and-after observations tell it whether the wallet stays prepared for the next operation. Those observations belong in the release evidence even when the user experience is a single sign-in button.

Frequently asked questions

Does the factory return value choose the wallet to verify?

The captured reference uses the signer address passed to the verifier. The successful payload makes validation available at that address, which the verifier queries directly.

Can an unwrapped signature be shorter than the wrapper marker?

A short unwrapped proof can belong to a wallet-specific scheme. For an implementation that supports such inputs, check the byte length before reading the trailing 32-byte marker. The captured reference excerpt slices the tail directly.

Does every ERC6492 factory need the same function signature?

The wrapper carries the raw calldata for its target. That supports different factory interfaces. A protocol that restricts preparation can apply its own selector and argument rules to those bytes.

Comments

0

    Leave a comment

    Share a question or observation about this article.

    10 to 3,000 characters.