DeFi Security AllianceRequest an audit
Menu

EVM patterns

ERC1271 Signature Validation: Wallet State and Response Evidence

Contract wallets decide whether a signature is authorized in the state where they are checked. ERC1271 signature validation requires the expected return value, current wallet policy and a separately enforced application action.

White glass gate beside a blue wax seal and a key, representing state-dependent contract wallet signature checks.

Key facts

Original response grid
24 distinct call-outcome/payload pairs from 30 labeled combinations
Pinned helper
2 accepted pairs and 22 rejected pairs in OpenZeppelin v5.4.0
Prefix comparison
7 accepted pairs, including 5 outside the pinned helper predicate
Evidence boundary
Synthetic predicates, no EVM or signature cryptography executed

Establish what the signature proves

A successful wallet check is an observation about a particular contract state.

That distinction is the starting point for ERC1271 signature validation. The same message and signature bytes can produce a different answer after the wallet changes its authorization policy. OpenZeppelin's fixed v5.4.0 verifier explicitly documents that possibility in either direction. A stored success flag does not become permanent permission to settle an order.

The signer is the wallet contract. An underlying owner may supply cryptographic material, but recovering that owner's address does not by itself prove that the wallet currently authorizes the action. Its policy may require additional signers, a permitted session or some other state. An application that replaces the wallet's answer with its own owner-address comparison has changed the authorization system.

ERC1271 defines the contract signature interface isValidSignature(bytes32,bytes). Successful validation returns the magic value 0x1626ba7e. The function must not modify state and must allow external calls. The standard leaves the actual signing policy to the implementation. It does not prescribe a universal signature length, a wallet ownership model or the business meaning of the supplied hash.

Evidence required before treating a wallet signature as authorization
ObservationWhat it establishesWhat remains unresolved
Recovered owner addressA cryptographic signer under the chosen schemeThe wallet's current authorization policy
Successful contract checkWallet acceptance in the observed execution contextApplication replay and action constraints
Previously cached acceptanceA historical verification resultAcceptance at settlement
Expected return bytesThe verifier's response predicate passedWhether the intended message was hashed

Consider an order created before an owner rotation. The application may still possess the original bytes and an earlier positive result. The relevant question at fulfillment is whether the current wallet accepts that exact order hash under the intended policy. This is a constructed integration scenario, not a report of a particular exploit. It identifies the evidence that would be needed to approve settlement.

The ERC1271 specification defines the interface. Our signature replay analysis addresses a separate problem: preventing repeated or unintended use of an otherwise accepted message. Both boundaries belong in the review. Neither can substitute for the other.

Original response grid for a fixed verifier

Method

We reproduced the return-data predicate in OpenZeppelin Contracts v5.4.0 using a complete finite set of supplied call outcomes. This experiment asks which response bytes that particular helper accepts. It does not execute a wallet or establish what every ERC1271 implementation should return through an arbitrary low-level adapter.

Source
Fixed SignatureChecker.sol from OpenZeppelin Contracts v5.4.0, fetched on .
Population construction
Both call-success flags, return lengths 0, 4, 31, 32 and 64 bytes, and canonical magic, wrong-selector or dirty-padding word shapes.
Exclusion rule
Equal payloads produced by different labels at short lengths were deduplicated. The initial 30 labeled combinations became 12 distinct payloads and 24 distinct success/payload pairs.
Artifacts
seo/research/integration-boundaries-2026-10-11/support-signatures/measurements.py, results.json and erc1271-result-grid.csv. These are repository artifacts, not public downloads.

OpenZeppelin's helper performs a low-level staticcall, requires successful execution and checks that at least a complete ABI word was returned. It then compares the first word with the selector extended by zero padding. Merely seeing the selector at the start of a short byte string is a different acceptance policy.

Results

Complete response grid for the pinned helper, measured
PredicateAccepted pairsPairs outside pinned acceptance
Pinned helper2 of 240
Successful call alone12 of 2410
Successful four-byte prefix match7 of 245

Accepted responses had canonical first words and lengths of 32 or 64 bytes. Extra trailing data did not change this first-word predicate. A raw four-byte selector did not pass it. Nonzero padding inside the first ABI word also failed the full-word comparison. These are implementation-specific outcomes; the table is not an estimate of wallet compatibility or malformed responses on a network.

Limits

  • No EVM or signature algorithm ran. Call outcomes and returned bytes were supplied as artificial inputs.
  • The grid covers the declared response shapes, not every possible byte sequence or adapter.
  • Acceptance says nothing about message semantics, replay protection or the wallet's internal authorization logic.

A separate complete Boolean observation grid contains 4 historical/current pairs. Reusing the historical answer creates one modeled false acceptance and one modeled false rejection when the answers differ. That arithmetic illustrates why observation time belongs in the record. It does not count owner changes, measure block transitions or estimate their frequency.

Preserve the wallet state behind acceptance

A verification record needs enough context to distinguish a wallet-policy change from a broken encoding. Record the chain, wallet address, exact hash, signature bytes, verifier version and block reference. Preserve the raw call outcome separately from the interpretation. If a provider could not obtain historical state, a result from the latest block must not be labeled as a historical result.

This is an evidence practice derived from state-dependent validation. It is not an extra ERC1271 interface requirement. The goal is to let a reviewer reproduce the question the application actually asked, then compare that observation with the state in which it plans to use the answer.

Historical acceptance and current authorization are separate recordsAn exact message enters a fixed-block wallet check. A later settlement must consult its own execution state. The historical observation is retained as evidence and cannot replace the settlement check.Exact messageObserved wallet stateSettlement stateHistorical evidence is retained
The later action needs authorization in its own context, while the earlier result remains an auditable observation.
Message record
The exact encoded action and the hash supplied to the wallet.
Observation record
The block reference, low-level outcome and raw response from the earlier check.
Consumption record
The authorization and application constraints enforced when the action executes.

A chain reorganization creates another reason to retain the block hash alongside a human-readable block number. An application should decide when an observation is sufficiently stable for its own workflow. ERC1271 does not supply a universal finality rule. The record can state that a result is provisional without inventing certainty about a chain's settlement conditions.

Caching still has a role. A user interface can reuse a recent observation to explain why an order looked acceptable, provided it names that observation and the remaining check. A settlement contract should enforce the intended current authorization. If the product deliberately grants durable application permission after a wallet check, the review must examine that separate grant and its revocation mechanism.

Wallet upgrades belong in the same change log as owner or policy changes. The address alone may remain constant while the validation implementation changes. Capture the relevant implementation and configuration evidence according to the actual wallet architecture; do not assume that every contract signer is a proxy.

A verification service also needs to state whose execution context it reproduced. A wallet policy that consults external contracts can encounter different surrounding state even when its own address and bytecode have not changed. Archive the call parameters and the assumptions behind the observation. If reproducing the required context is impossible, describe the missing evidence rather than translating a transport error into an invalid-signature verdict.

For a reconciliation example, imagine two services reporting opposite answers for the same bytes. Compare their chain identifiers and block references before examining signature parsing. Then compare the hash and the deployed verifier version. An earlier positive answer and a later negative answer may both be accurate observations. The product should investigate the policy transition, not select whichever service produces the preferred answer.

Bind the signed action before consuming it

A wallet can correctly approve the wrong hash. The application is responsible for deciding what the hash means. EIP712 provides structured message hashing and domain fields, but explicitly leaves application replay protection outside its scope. A valid contract response therefore cannot rescue an action schema that omits its intended recipient, amount, operation or freshness conditions.

Start with the business action, then derive the message. For a settlement authorization, identify which contract consumes the order, who receives value, which assets move and what bounds the user approved. The exact fields depend on the protocol. A reusable login message and a token-transfer authorization have different consequences and should not share an ambiguous purpose.

  1. Write the action's permitted effect in ordinary language before choosing its encoded fields.
  2. Compare the frontend's displayed values with the hash recomputed by the consuming contract. Include every field that the execution relies on.
  3. Check the domain against the intended chain and verifying contract, using the selected signing standard's actual encoding.
  4. Define how the application consumes or invalidates authorization and how a previously accepted message is rejected after that event.
  5. Test a neighboring action with one changed material field. Keep a valid control case so an unrelated revert cannot be mistaken for correct binding.

The EIP712 source describes domain separation. It does not require every application to adopt an identical nonce structure. Ordered nonces, unordered invalidation and state-dependent one-time effects can support different workflows. Review the implemented mechanism and its race conditions instead of assuming that the presence of a field named nonce is sufficient.

Do not shorten an arbitrary contract signature to fit an EOA-only format. ERC1271 uses a variable-length byte array so that the contract can interpret its own scheme. A wallet may use multisignature evidence or a signature policy that does not map to the conventional recovery input. Compatibility requires calling the correct verifier with the original bytes.

Undeployed wallets add a distinct boundary. ERC6492 describes wrapped verification for counterfactual contract wallets. A code-length branch alone cannot discover that policy before deployment. Supporting that case requires an explicit verifier design and its own review, rather than silently falling back to ordinary recovery. This article's response grid does not implement ERC6492 deployment or wrapper verification.

Keep login authorization separate from asset authorization in the acceptance record. A service may create a session after checking a wallet message; the later session permissions are then an application policy. Their lifetime, revocation and allowed operations need their own controls. Rechecking ERC1271 is one possible design choice, but the standard does not automatically revoke a session that another system already created.

Similarly, an off-chain order index can retain an order that the wallet no longer accepts. Its database entry is discovery evidence, not settlement authority. A useful interface can mark the last observed status and the time of that observation while the consuming contract performs its current check. This makes the indexer's responsibility legible without promising that an earlier validation will survive every later state change.

Test rejection without losing wallet compatibility

A test that only expects failure is weak evidence. The wallet may fail because the test used the wrong message encoding, lacked required state or exhausted a manually chosen gas budget. Each intended rejection needs a corresponding accepted fixture that reaches the relevant check. Keep the expected raw response and the application's final decision as separate assertions.

Integration tests that distinguish authorization failure from unrelated failure
Changed conditionExpected observationRequired control
Wallet policy changesCurrent wallet answer governs consumptionSame message under the earlier authorized policy
Return word is malformedPinned response predicate rejects itCanonical return word from a successful call
Material action field changesOriginal authorization cannot approve that actionUnchanged approved action
Provider cannot read required stateObservation remains unknownSuccessful read at an available block

ERC1271's security discussion warns against hardcoding a validation gas amount because legitimate implementations can consume different amounts. This does not mean an off-chain service must accept unbounded work from arbitrary callers. A service can define operational limits, record timeouts and distinguish unsupported resource consumption from a cryptographic rejection. Its interface should preserve that distinction.

The contract-consuming path needs an equally explicit resource argument. If the implementation imposes a cap, assess the compatibility consequences and the denial-of-service boundary against the wallets it supports. Avoid publishing a universal gas recommendation unsupported by the standard or measured deployments. Our synthetic response study did not measure gas consumption.

Logs should also separate a call that reverted from a successful call that returned a nonmatching value. Both may produce a final negative decision, but they point to different investigations. Preserve enough return data for diagnosis within the application's storage and privacy policy. Do not expose private signing material or user secrets merely to make a debugging record convenient.

A broader transaction simulation review can help test the eventual consuming action. Simulation remains evidence about its chosen state and inputs. It cannot turn a cached signature result into an irrevocable grant or guarantee that a future execution will see the same wallet configuration.

Turn the response grid into explicit integration fixtures for the chosen adapter. Supply a successful raw selector response, a truncated word, canonical padding, dirty padding and an extra trailing word. Record the bytes and expected decision before running the integration. The permissive prefix predicate is useful as a comparison, but substituting it changes the pinned acceptance policy and needs a deliberate compatibility argument.

The helper's address overload also routes by deployed code presence. With no code it uses ECDSA recovery; with code it calls the contract branch. The inspected ECDSA bytes overload accepts a conventional 65-byte encoding, while that restriction does not define every contract signature. List the supported routes in the acceptance packet and explain what happens to an unsupported route instead of silently truncating its bytes.

Use distinct tests for wallet-policy change and application replay. In the first, retain the original hash and signature while changing the supported wallet's real authorization configuration. In the second, consume an accepted action, then submit it again under unchanged wallet policy. The wallet may still return acceptance while the application must enforce its own consumption rule. A single rejection test cannot establish both properties.

Preserve the route of the validation call when reproducing a disagreement. A direct query and a query through the consuming verifier can have different execution contexts under a context-dependent policy. That is a source-permitted scenario, not a claim that every wallet varies by caller. Reproduce the actual integration before declaring that a direct wallet query proves what the consuming contract would observe.

Close verification with a reproducible record

The closing artifact should answer a narrow question: which exact authorization did the application enforce, in which state, using which verifier? Attach the tested response policy, the message schema and the consumption rule. A positive result without those details is difficult to distinguish from an EOA-only recovery check or a permissive byte-prefix comparison.

Review the user-facing label as part of acceptance. A screen that says the wallet authorized an order should identify the action and observation it refers to. A label that instead implies permanent ownership or future execution goes beyond the interface's evidence. Correct the wording at the same time as the integration, so the public claim matches the implemented check.

For an external review, make the wallet types and supported verification routes explicit in the scope. The security firm directory and the Pharos Production member profile provide places to start evaluating a provider. The review request still needs its own technical acceptance criteria: policy changes, malformed responses, replay boundaries and current-state consumption are different test obligations.

Keep unresolved observations unresolved. A failed historical read does not establish that the wallet would have rejected the message. A passed low-level response predicate does not establish that the signed action had the right recipient. The complete ledger makes these limits visible instead of collapsing them into a single reassuring label.

Assign an owner to the evidence packet. That person should be able to retrieve the pinned code, reconstruct the supplied hash and explain why a test failed. Preserve accepted fixtures as well as rejected ones: removing the former makes a compatibility regression difficult to distinguish from stronger authorization. After a verifier upgrade, replay both populations and record which outcomes changed before approving the new integration.

The measured result here is precise: the pinned helper accepted 2 of 24 constructed response pairs. What would change the conclusion is equally precise: a different verifier, a different response population or execution evidence from the actual integration. Approval should name those dependencies and trigger a new review when one changes.

Frequently asked questions

Should an exchange support desk request a seed phrase to reproduce verification?

No. Ask for the public wallet address, exact message or hash, signature bytes and relevant block context. Signing secrets are not part of the public verification input and should not be collected for this investigation.

Does a public wallet check identify the human or organization behind that wallet?

The interface reports the contract's acceptance of a hash and signature. Establishing a legal or human identity requires separate evidence; do not turn wallet authorization into an identity claim.

Can one successful check authorize several application actions at once?

Only if the application's approved message and consumption policy deliberately grant that scope. A wallet's positive interface response does not create additional business permissions beyond the action the application defines.

Comments

0

    Leave a comment

    Share a question or observation about this article.

    10 to 3,000 characters.