Audit Four Replay Boundaries in Smart-Account Signatures
A smart account can validate a signature correctly and still accept it in the wrong place. The cryptography may recover the intended owner. The calldata may decode. The nonce may be unused. Yet the same signed request may also work for another account, on another chain, against another application verifier, or under a schema whose meaning has changed.
That is a replay-boundary failure. It is not found by demonstrating that the intended transaction succeeds once. It is found by keeping the signature fixed, changing one boundary at a time, and proving that every unintended variant is rejected.
This article defines four such boundaries: account, chain, verifier, and schema/version. It also turns them into a reusable test corpus for ERC-1271 and EIP-712 integrations. The goal is concrete: one approved request and four controlled mutations, each with an expected failure. The evidence identifies which layer rejected each replay.A valid signature is safe only inside the domain the signer actually reviewed.
The audit target is a rejection matrix
Begin with a single known-good authorization. Call it the control case. Record the account address, chain ID, application verifier, domain fields, primary type, complete message, nonce, deadline, signature bytes, observation block, wallet implementation, and the call used to validate it.
Then create four negative cases. Each negative case changes exactly one variable family while every other input remains fixed:
- Case A, account: smart account A to smart account B. Required result: reject.
- Case B, chain: chain X to chain Y. Required result: reject.
- Case C, verifier: application V1 to application V2. Required result: reject.
- Case D, schema/version: type and version S1 to type or version S2. Required result: reject.
This arrangement matters. If a test changes both account and chain, rejection does not reveal which boundary is effective. If a test generates a fresh signature after every mutation, it does not test replay at all. Preserve the original signature and mutate only the context in which somebody tries to use it.
The control case should pass through the same deployed path that consumes the authorization. A browser wallet saying signed is not the control result. A local library recovering an address is not enough either. For an ERC-1271 account, call the deployed account through the integration or a faithful fork and record whether isValidSignature(hash, signature) returns 0x1626ba7e. Then submit or simulate the consuming call so authorization checks outside ERC-1271 are covered.
The matrix should contain an explicit failure oracle. A revert, a non-magic ERC-1271 return value, a consumed-nonce result, an expired result, or an application-specific invalid-domain error can all be correct. An unexplained RPC error is not a security result. Preserve the revert data, trace, block reference, and decoded boundary that caused the failure.
Typed data needs application replay controls
EIP-712 defines how structured data becomes a digest. The digest combines a domain separator with a hash of the typed message. Common domain fields include name, version, chainId, and verifyingContract. The message type contributes its own typeHash and encoded fields.
These components make a strong vocabulary for separation, but the specification does not consume a nonce, expire a request, or remember that a digest has already been used. Its authors state the application obligation directly:
“The repeated message should be rejected or the authorized action should be idempotent.”
Remco Bloemen, Leonid Logvinov and Jacob Evans, authors of EIP-712: Typed structured data hashing and signing, Ethereum Improvement Proposals, September 12, 2017. Original specification.
Treat domain separation and replay consumption as complementary controls. Domain separation says where a signature belongs. A nonce, digest registry, order salt, bitmap, sequence, or idempotent state transition says whether it may be used again there. A deadline bounds how long the verifier may accept it. Removing one layer should not be described as coverage by another.
This distinction is especially important for smart accounts. Under ERC-1271, the account answers whether one hash and signature are valid under its current logic. The standard does not prescribe how an application constructs that hash. An application can pass a well-formed but under-scoped digest to a perfectly conforming account. The account may return the magic value, while the application has still failed to bind the signed intent to the context the user expected.
Before adding code, Pharos Production maps the relevant contract roles, trust boundaries, tests, and deployment controls in its published smart-contract engineering scope. For this problem, that process is useful because it turns a vague requirement such as prevent replay into four named rejection cases tied to deployed verifiers. This is a mapping to the public service process, not a claim about a particular client result.
Boundary 1: bind the signature to the smart account
Assume the same externally owned account controls smart accounts A and B. An application creates a digest that describes an action and recovers the shared owner. If neither the application digest nor the smart account's validation step commits to the account address, the signature intended for A may also validate through B.
This is a different problem from using a signature twice on A. A nonce stored in A can stop the second execution on A while B has a separate, unused nonce. Per-account nonce storage contains repetition inside one account; it does not prove that the message is cryptographically bound to that account.
ERC-7739, which is Draft as of September 18, 2026, specifies a defensive-rehashing approach for readable typed signatures from smart accounts. Its motivation is the case in which several accounts share an owner and an under-scoped digest can be replayed among them. The design nests the application's typed data inside an account-specific EIP-712 domain so the account address and its domain participate in the signed authorization.
Do not turn that pattern into a universal assumption. ERC-7739 is not part of ERC-1271 by default, and not every deployed wallet or integration implements it. Some accounts use another account-bound envelope. Some applications include the account address directly in their primary message. Others deliberately support portable signatures. The audit must identify the actual construction and test the deployed code.
Build the account-boundary test this way:
- Deploy or select accounts A and B with the same authorized owner and equivalent policy.
- Give them separate nonce state, but begin with the same unused nonce value.
- Produce one signature through the intended flow for account A.
- Validate and, if safe, simulate the action through A; record a passing control.
- Submit the identical digest and signature through B without signing again.
- Require B to reject because the authorization is account-bound, not because its balance, role, or unrelated state differs.
Equivalent policies matter. If B rejects only because it lacks a token balance, the account replay has not been disproved. If B has a different threshold, owner set, or implementation, the test mixes policy with account identity. Align those variables or use a deterministic pair of test accounts.
Record the account-bound component explicitly. It may be verifyingContract in the account's EIP-712 domain, an account address inside the message, a defensive rehash, or another unambiguous field. Also record the code version responsible for interpreting it. A proxy address can remain stable while an implementation upgrade changes validation semantics.
The exception is intentional portability. A protocol may want one owner signature to be usable through several accounts. In that design, account B accepting the signature is not automatically a defect. The security requirement becomes a positive allowlist of eligible accounts, an explicit portable action type, and a separate replay-consumption model. Document the choice and exclude privileged or value-moving operations that were never meant to be portable.
Boundary 2: bind the authorization to the chain
The same contract address can exist on multiple networks. CREATE2 deployments, deterministic factories, copied deployments, and common proxy addresses make visual similarity unsurprising. If a signature does not commit to a chain boundary, a request authorized on chain X may also be presented on chain Y.
EIP-712 provides chainId as a domain field. The important word is provides. The application must include the field and derive the domain correctly. It must also refuse an unexpected value. A wallet display that shows a network cannot repair a verifier that reconstructs the digest without that network.
Construct the chain test with comparable state:
- Use the same smart-account address and application-verifier address on chains X and Y where practical.
- Align the owner configuration, relevant balances or test fixtures, nonce value, and contract version.
- Sign the chain-X request once.
- Prove the control path on X.
- Replay the same signature and message on Y.
- Require rejection at domain reconstruction or another explicitly documented chain gate.
Do not allow a gas failure or missing deployment to carry the result. The test is strongest when the call would otherwise succeed on Y. If identical deployment state is unavailable, call the validation function directly and separately prove that the chain-specific domain changes the hash.
Cache behavior deserves attention. Some EIP-712 implementations cache a domain separator and rebuild it after a chain-ID change. A proxy can also delegate to implementation code whose constructor-era assumptions do not match proxy storage. Inspect the deployed code path rather than relying on a library name. Record the domain returned or reconstructed at the observation block.
Cross-chain protocols are another explicit exception. A bridge claim, solver order, or omnichain intent may be designed to authorize activity on more than one network. In that case, deleting chainId is not the only possible design, and blindly demanding chain-local rejection would contradict the protocol. The signed message should state the allowed source and destination set, route and asset semantics. It should also carry an expiration and one-time consumption rule. The audit then mutates a chain outside that signed set and expects rejection.
Boundary 3: bind the request to the application verifier
Two addresses participate in many smart-account flows. The smart account validates the signature under ERC-1271. A separate application contract consumes the authorized action. Confusing those roles leaves a gap.
Suppose lending verifier V1 and trading verifier V2 accept the same message shape. The smart account address is identical, the chain is identical, and the account says the owner's signature is valid for a digest. If the digest does not include the intended application or if both applications reconstruct the same under-scoped digest, V2 may accept a request meant only for V1.
EIP-712's verifyingContract usually identifies the application that interprets the typed message. ERC-7739-style account binding can add the smart account as another domain layer. These are not substitutes. One answers which application may consume the intent; the other answers which account may present the contract signature.
The verifier-boundary test needs two real consumers:
- Deploy V1 and V2 with matching interfaces and state sufficient for the action.
- Keep chain, account, message fields, nonce value, deadline, and signature fixed.
- Execute or simulate the approved V1 path.
- Present the identical signed request to V2.
- Require rejection because the application-verifier binding differs.
Use different addresses even if both proxies point to the same implementation. The signed boundary is normally the deployed consumer, not the source repository or implementation contract. If a router is the intended verifier and delegates to adapters, record the router as the stable boundary and test an unauthorized router. If multiple verifier contracts are intentionally equivalent, enumerate them inside a signed or governed trust set rather than depending on shared bytecode.
Check off-chain components too. A relayer may decode a message, call ERC-1271 once, and store an approved job for later dispatch. If the job record loses the verifier address, the relayer can route valid authorization to the wrong destination even when each on-chain contract has sound checks. Persist the full domain and compare it immediately before submission.
The published blockchain delivery process describes discovery, architecture, testing, security review, deployment, and monitoring as connected stages. Pharos Production applies that sequence here by rehearsing both the on-chain rejection and the relayer routing rule against the same corpus. The page supports the process description; it is not independent evidence that any unnamed deployment is protected.
Boundary 4: bind meaning to the schema and version
A signature authorizes encoded meaning, not a UI sentence. That meaning depends on the primary type, its field layout, nested types, and the domain name/version used to compute the digest. A schema change can therefore create replay risk while account and chain stay fixed, even if the verifier does too.
EIP-712 includes the type definition in hashStruct, so a correct change to typeHash produces a different digest. Trouble appears when systems reuse a type for a new meaning, omit a material field, accept several encodings without strict routing, or translate an old request into a new action after validation.
Consider an old Transfer message containing token, to, amount, nonce, and deadline. A later release adds a fee recipient, execution mode, or destination-chain interpretation. If the new verifier supplies an omitted value from mutable configuration or treats the old type as equivalent to the new operation, an old signature can gain authority the signer never reviewed.
The domain version field is useful when a release changes message semantics, but only when the verifier enforces the expected version. Incrementing an application package number without changing the signed domain has no cryptographic effect. Conversely, changing the domain version invalidates every signature under the previous version, which may be an intentional migration but needs a compatibility and cancellation plan.
Build two schema tests rather than one mixed mutation:
- Type mutation: keep the domain fixed, change a primary or nested type in a security-relevant way, and prove that the original signature does not validate for the new type.
- Version mutation: keep the logical fields fixed, change the domain version, and prove that the old signature is rejected under the new release.
Use canonical EIP-712 encoding. Avoid an extra packed-hash layer that can erase type boundaries or make different field combinations ambiguous. Record the exact JSON typed-data payload shown to the signer, the canonical type string, type hash, domain separator, message hash, final digest, and release commit or artifact that produced them.
The UI is part of the evidence, not the verifier. Capture what the wallet rendered, then compare it with the fields the contract actually hashed. A polished prompt can omit a field. A correct hash can be paired with a misleading label. The audit passes only when the displayed and encoded meanings agree with the executed action.
Schema migration should also test pending signatures. Decide whether they remain valid under an explicit legacy verifier, are canceled by nonce or epoch, or expire naturally inside an acceptable window. Do not let a generic decoder silently reinterpret them under the new schema.
Nonce and expiry form a fifth, orthogonal control plane
The four boundaries answer where and under what meaning a signature is valid. Nonces and deadlines answer whether that valid authorization is still live. Keep those questions separate in the corpus.
After every four-boundary case, test three lifecycle variants:
- Submit the same control request twice and require the second attempt to fail or be provably idempotent.
- Submit a request after its signed deadline and require rejection using the verifier's actual time source.
- Invalidate a selected nonce or epoch and require the pending request to fail without changing unrelated requests unless that broader invalidation is intended.
OWASP SCWE-055 identifies replay as reuse of a valid authorization in another transaction or context and lists unique nonces and timestamps among the core mitigations. Apply them at the consuming contract. A database flag in one relayer does not stop another party from submitting the same public signature.
Nonce design changes the blast radius. A single sequential nonce is easy to reason about but forces ordering and can invalidate later requests when one transaction wins. Keyed or unordered nonces allow parallel actions and selective cancellation but demand precise bit or key accounting. A signed salt helps distinguish requests, yet it provides no one-time guarantee unless the verifier records or derives its consumption.
Deadlines require equal precision. Define the time unit and whether the comparison is inclusive. Name the on-chain clock. Test immediately before the boundary, exactly at it, then immediately after it. A long deadline is not a replay defense during its valid window; it is only a future stop.
Build a portable four-boundary replay corpus
The reusable artifact should let another engineer rerun the audit after an account upgrade, application release, chain deployment, or schema migration. Store machine-readable fixtures beside a human review table.
For every case, capture:
- Case ID and the security boundary under test.
- Raw typed-data JSON plus a canonical normalized representation.
- Domain separator, type hash and message hash, followed by the final digest.
- Smart-account address and implementation reference, with owners and threshold.
- Chain ID, verifier address, code hash and observation block.
- Signature bytes and signature format.
- Nonce key/value, consumption state, deadline and time source.
- Expected and observed results, with return value or revert data and the trace.
- Tool version, RPC endpoint classification, fixture commit and reviewer.
Do not put live private keys, production session tokens, or unrestricted signatures in the corpus. Generate bounded test accounts and payloads whose target, amount, environment, and deadline cannot move production value. If a production signature must be examined during an incident, store its digest and decoded fields under the appropriate evidence controls rather than copying reusable authority into a general repository.
Name the rejection layer. ACCOUNT_DOMAIN_MISMATCH, CHAIN_DOMAIN_MISMATCH, VERIFIER_DOMAIN_MISMATCH, SCHEMA_MISMATCH, NONCE_USED, and EXPIRED are more useful than a single invalid-signature label. Stable application errors are ideal, but traces and decoded comparisons can supply the classification when the public interface intentionally returns only failure.Keep the signature fixed; mutate one boundary; require a named rejection.
Version the corpus with the deployment. An implementation upgrade, owner-policy change, domain-name change, new chain, proxy migration, or message-schema change should select the relevant rows for rerun. A passing result from a local library should not be carried forward after deployed validation code changes.
Run the audit against deployed state
Use a fork at a recorded block when the production path cannot safely be exercised. Preserve the proxy and implementation storage, account configuration, nonce state, application verifier, and chain ID. Impersonation can prepare balances or test roles, but it must not replace the signature check being evaluated.
Start by reproducing the control case. If it fails, stop. Negative results are uninterpretable when the known-good fixture is broken. Once the control passes, run each mutation from a clean snapshot so earlier nonce consumption cannot cause later false passes.
For ERC-1271, record both the direct account result and the consuming application's result. The account may accept the hash while the application rejects its nonce. That can be correct, but the evidence should show which layer supplies which guarantee. It also reveals integrations that cache a prior ERC-1271 success and fail to recheck state at execution time.
Repeat the decisive cases through every supported entry point: direct contract call, relayer, bundler or account-abstraction path, and any batch router. A safe on-chain verifier can be undermined by an off-chain service that reconstructs a different digest or submits to an unintended contract. Conversely, an on-chain rejection is still the final containment when a relayer is compromised.
Use the following stop conditions:
- The team cannot reproduce the exact digest from the displayed request.
- The account and application verifier roles are ambiguous.
- Any negative case succeeds without an explicitly approved portability rule.
- Rejection depends on missing funds, missing code, or unrelated state.
- A deployed implementation differs from the code used to generate fixtures.
- Nonce or deadline enforcement exists only in an off-chain component.
- A schema migration can reinterpret pending signatures without a bounded legacy path.
Do not average these conditions into a score. One successful cross-verifier replay can authorize the wrong application even when every other row passes.
The completion record should fit one page
The detailed corpus may be large, but the release decision needs a compact record. Identify the deployment and observation block, link the control fixture, list the four mutation case IDs, show expected and observed results, and state the nonce and deadline cases. Add the implementation and runtime-code references that make the results reproducible.
Record exceptions as design decisions. If an authorization is portable across a governed set of accounts or chains, name the set and signed constraint. If a legacy schema remains valid during migration, give its deadline and consumption mechanism. If a rejection could not be exercised safely, mark it unverified and assign an owner.
The final question is not whether the signature library is reputable. It is whether the same captured authorization fails everywhere the signer did not approve. One successful control and four isolated rejections provide that evidence. Add nonce consumption and expiry, rerun the corpus when the verifier changes, and smart-account signatures become a testable security boundary rather than an assumption hidden inside a wallet prompt.
More insights to read
- Smart Contract Release Audit: 8 Repository Artifacts
- Smart Contract Upgrade Authority: Timelocks, Multisigs, and Emergency Controls
- Upgradeable Solidity Smart Contracts. Part 1: Versioning
- Web3. Smart Contracts. Oracles. Part 1
- Smart Contracts. Their Potential and Real Limitations. Part 1
About the author
Dmytro Nasyrov. Photo supplied by the author.
Written by Dmytro Nasyrov PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.
