This guide is for information only. It isn't security, legal or investment advice. It summarises public Ethereum standards and Safe documentation. Confirm the signing policy for your institution with your own security and legal teams.

Before EIP-712, a wallet showed most signature requests as "an opaque hex string displayed to the user with little context"[3]. EIP-712 encodes the data together with its structure, so the wallet can display it "to the user for verification when signing"[3]. The structure helps only if someone reads it. This guide is about what to read.

What typed data commits to

An EIP-712 signature covers one 32-byte digest. The specification builds it from three parts: the bytes 0x19 0x01, a domain separator, and the hash of the message struct[1].

digest = keccak256( 0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct(message) )

domainSeparator = hashStruct(EIP712Domain{ name, version, chainId, verifyingContract })

The 0x19 0x01 prefix

ERC-191 defines the signed-data format 0x19 <1 byte version> <version specific data> <data to sign>. The leading 0x19 means that signed data can never be a valid Ethereum transaction[5]. The version byte tells the verifier how to read the rest[5]:

EIP-712 fixes the version byte to 0x01. The version-specific data is the 32-byte domain separator, and the data to sign is the 32-byte hash of the message[1]. A personal_sign request (0x45) prepends "\x19Ethereum Signed Message:\n" and the message length to a plain message[5]. It carries no EIP712Domain and no types, so the wallet has no fields to decode for you.

The domain separator

The domain separator is the hash of an EIP712Domain struct. Protocol designers "only need to include the fields that make sense for their signing domain", so every field is optional[2]:

  • name: the user-readable name of the signing domain, such as the protocol.
  • version: the current major version. Signatures from different versions aren't compatible.
  • chainId: the EIP-155 chain id. The wallet "should refuse signing if it does not match the currently active chain".
  • verifyingContract: the address of the contract that will verify the signature.
  • salt: a disambiguating value, "a domain separator of last resort".

These definitions come from the EIP-712 specification[2]. chainId and verifyingContract bind the signature to one chain and one contract. ERC-2612 states the purpose directly: the domain separator "should be unique to the contract and chain to prevent replay attacks from other domains"[8]. A domain that leaves out either field gives that protection up.

What the wallet shows and what gets hashed

The request below is the ERC-2612 Permit type as a dApp passes it to the wallet. The field names and types come from the permit typehash[8]; the values in angle brackets are placeholders.

{
  "domain": {
    "name": "<token name>",
    "version": "1",
    "chainId": 1,
    "verifyingContract": "<token contract address>"
  },
  "primaryType": "Permit",
  "types": {
    "Permit": [
      { "name": "owner",    "type": "address" },
      { "name": "spender",  "type": "address" },
      { "name": "value",    "type": "uint256" },
      { "name": "nonce",    "type": "uint256" },
      { "name": "deadline", "type": "uint256" }
    ]
  },
  "message": {
    "owner": "<your address>",
    "spender": "<address that receives the allowance>",
    "value": "<amount in the token's smallest unit>",
    "nonce": "<nonces(owner) on the token>",
    "deadline": "<unix time after which permit() reverts>"
  }
}

The wallet hashes every field of domain and message into the digest, and those fields are what bind the signer. A label that the dApp shows next to the request, such as "Sign in" or "Confirm deposit", sits outside the digest.

What EIP-712 leaves to the application

EIP-712 "does not include replay protection"[3]. Its Security Considerations put two duties on the contract that accepts the signature[4]:

  • Replay. The application must behave correctly "when it sees the same signed message twice", for example by rejecting the repeat.
  • Frontrunning. An attacker "intercepts the signature and submits it to the contract before the original intended use takes place". The application must still produce the effect the signer intended, or reject it.

ERC-2612, for example, keeps a nonces mapping "for replay protection"[9], and the two domain fields above bind the signature to one chain and one contract. Check that both are present before you sign.

EOA signatures and ERC-1271 contract signatures

An externally owned account (EOA) is controlled by one private key. It "can sign messages with" that key[6]. A verifying contract recovers the signer's address from the digest and the signature with Solidity's ecrecover, and compares it with the expected owner[1][10].

A contract wallet such as a Safe can't do this, because "contracts do not possess a private key"[6]. ERC-1271 adds one view function to the wallet. The verifier calls it with the hash and the signature bytes, and the wallet decides whether the signature is valid[6]:

function isValidSignature(bytes32 _hash, bytes memory _signature)
  public view returns (bytes4 magicValue);

// returns 0x1626ba7e when the contract accepts the signature

The wallet "MUST return the bytes4 magic value 0x1626ba7e" for a valid signature, and the call must not modify state[6]. The rule behind "valid" belongs to the wallet. It can depend on time, on state, on the signers' authorisation level, or on the signature scheme, for example a multisig[6]. ERC-1271 therefore makes "each contract implementing this method" responsible for its own checks[7]. No gas limit is expected for the call, so a verifier shouldn't hardcode the gas it sends[7].

Worked case: a Safe multisig

A Safe keeps a threshold, "the minimum number of owners required to confirm a transaction before it gets executed"[13]. It checks that each signer is an owner and verifies each signature by its type[13]. If too few owners have signed, the transaction reverts[13].

Owners sign a Safe transaction hash[15]. The Safe's domain separator is "as defined in the EIP-712 standard"[16], and in the Safe contract the domain holds two fields, chainId and verifyingContract, where the verifying contract is the Safe itself[17]. The SafeTx struct covers the destination, value, call data, operation type, gas and refund fields, and the Safe's own nonce[17].

Each owner signature is 65 bytes, and the Safe reads its type byte to decide how to verify it[14]:

  • ECDSA (type above 26 and below 31): a signature from an EOA owner over the hash[14].
  • eth_sign (type above 30): an EOA signature made with eth_sign, with 4 added to v[14].
  • Contract signature (type 0): the owner is itself a contract, and the Safe checks it through ERC-1271[14].
  • Pre-validated hash (type 1): the owner called approveHash on-chain beforehand[14].

A Safe can also act as the signer for other contracts. Its compatibility fallback handler implements ERC-1271 through isValidSignature[18], so a protocol that accepts ERC-1271 signatures can accept a Safe's approval.

Permit approvals (ERC-2612) and their risk

An ERC-20 approval lets a spender withdraw from your account "multiple times, up to the _value amount"[11]. The classic approve is a transaction that you send and pay gas for. ERC-2612 adds a permit function that sets the same allowance from a signature:

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external
function nonces(address owner) external view returns (uint)
function DOMAIN_SEPARATOR() external view returns (bytes32)

A valid call sets allowance[owner][spender] to value, increments nonces[owner] and emits an Approval event. It reverts unless the block time is at or before deadline, the owner isn't the zero address, the nonce matches, and the signature is a valid secp256k1 signature from the owner[8].

Why a permit carries risk

  • Anyone can submit it. The definition never refers to msg.sender: "The caller of the permit function can be any address"[8]. The signer pays no gas, and a relayer commonly submits the permit for them[9]. Whoever holds the signature can turn it into an on-chain allowance.
  • The allowance is the payload. Once the permit lands, spender can move up to value of your tokens with transferFrom[11]. A wrong spender or an oversized value is a live allowance to the wrong party.
  • A far deadline keeps the option open. The relaying party "is essentially given a free option to submit or withhold the Permit". A near deadline limits that window; a deadline of uint(-1) creates permits "that effectively never expire"[9].
  • Front-running. Another party "can always front run this transaction and call permit before the intended party". ERC-2612 notes that the end result is the same for the signer[10].
  • Censorship. "Signed Permit messages are censorable": the relayer can choose not to submit. If you hold ETH, you can submit the permit yourself, which can make earlier signed permits invalid[10].
  • Approval race. The standard ERC-20 approval race condition (SWC-114) "applies to permit as well"[10].
  • Chain splits. If DOMAIN_SEPARATOR contains the chainId but is fixed at deployment, permits can replay across chains after a future chain split[10].

A review checklist before you sign

Most items map to a clause in the sources. Work through it on the device that holds the key, against addresses your institution has published or verified.

  1. Is it typed data? Prefer EIP-712 (0x01) requests. A personal_sign (0x45) request carries no domain or types[5], so you can't read what it authorises.
  2. Does chainId match the network? The wallet should refuse a mismatch, and you should too[2].
  3. Is verifyingContract the contract you expect? Compare it character by character with the address your institution has on file. It names the contract that will verify the signature[2].
  4. Do name and version match the protocol? A new version means a new domain[2].
  5. Who is the spender? The spender can pull up to value with transferFrom[11]. Match it to a published address.
  6. Is value the amount the mandate allows? Reject an unlimited value unless the mandate requires one. The allowance is set to exactly that value[8].
  7. Is deadline near? A near deadline limits how long a relayer holds the option[9].
  8. Is there a nonce, and is it the current one? A permit is valid only when its nonce equals nonces(owner)[8].
  9. Can your wallet decode every field? If it shows only a hash or raw hex, refuse. That is blind signing, the opaque-hex problem EIP-712 exists to solve[3].
  10. Does the Safe transaction hash match on every device? Safe asks each signer to check the domain and message hashes that the hardware wallet shows before they confirm[19]. Every owner should see the same hash for the same SafeTx[17].

An analogy helps explain why items 2 and 3 matter. NIST SP 800-63B-4 calls an authenticator phishing-resistant when the protocol binds its output to the verifier, through channel binding or verifier name binding, "without relying on the vigilance of the claimant"[20]. NIST writes this for authentication, not for transaction signing. A typed-data domain gives a similar binding: the signature is valid only for the chainId and verifyingContract it names[2]. That check on the contract stays with you: the wallet doesn't decide which contract is legitimate.

Glossary

EIP
Ethereum Improvement Proposal: "a design document providing information to the Ethereum community, or describing a new feature"[12]. An EIP in the ERC category covers "application-level standards and conventions" and is written ERC-X, as with ERC-191, ERC-1271 and ERC-2612[12].
EOA
Externally owned account: an account that signs with its own private key[6].
Typed data
A message encoded with its structure, so a wallet can display its fields for verification before you sign[3].
Domain separator
The hash of the EIP712Domain struct. It separates signatures for one protocol, version, chain and contract from all others[2].
chainId
The EIP-155 chain id in the domain. The wallet should refuse to sign when it doesn't match the active chain[2].
verifyingContract
The address of the contract that will verify the signature[2].
Nonce
A per-owner counter. ERC-2612 uses nonces(owner) for replay protection and increments it on every successful permit[8][9].
EIP-191 prefix
The 0x19 byte and a version byte at the start of signed data. It keeps signed data from being a valid transaction[5].
ERC-1271
The standard isValidSignature method that lets a contract accept or reject a signature[6].
Magic value
0x1626ba7e, the bytes4 that isValidSignature returns for a valid signature[6].
Permit
An ERC-2612 signature that sets an ERC-20 allowance without an approve transaction from the owner[8].
Spender
The address an allowance lets withdraw from your account, up to the approved amount[11].
Blind signing
Signing a request the wallet shows only as opaque hex, with no readable fields[3].
Safe threshold
The minimum number of Safe owners who must confirm a transaction before it can execute[13].

Where Rekord fits

The custody pattern in this guide is the one behind the 9SicUSDC vault. Its assets sit in a 3/6 Safe multisig, and a public Zodiac Roles Modifier scopes outgoing transfers to a single whitelisted destination: the Fordefi settlement vault. Rekord Capital Markets (RCM) signs settlement transactions within the limits that the Safe and the Roles Modifier enforce. Vault status: Fully Allocated · View Only. The full control set is on How Rekord works.

Sources

Ethereum standards are cited from eips.ethereum.org, section by section. Safe behaviour is cited from the Safe documentation at docs.safe.global.

  1. EIP-712: Typed structured data hashing and signing. Specification, the encode() definition and its ERC-191 version byte; eth_signTypedData, the ecrecover example. Accessed Oct 24, 2026.
  2. EIP-712: Typed structured data hashing and signing. Definition of domainSeparator, the EIP712Domain fields. Accessed Oct 24, 2026.
  3. EIP-712: Typed structured data hashing and signing. Abstract and Motivation. Accessed Oct 24, 2026.
  4. EIP-712: Typed structured data hashing and signing. Security Considerations, Replay attacks and Frontrunning attacks. Accessed Oct 24, 2026.
  5. ERC-191: Signed Data Standard. Specification and Registry of version bytes. Accessed Oct 24, 2026.
  6. ERC-1271: Standard Signature Validation Method for Contracts. Abstract, Motivation and Specification. Accessed Oct 24, 2026.
  7. ERC-1271: Standard Signature Validation Method for Contracts. Security Considerations. Accessed Oct 24, 2026.
  8. ERC-2612: Permit Extension for EIP-20 Signed Approvals. Specification, permit(), nonces() and DOMAIN_SEPARATOR(). Accessed Oct 24, 2026.
  9. ERC-2612: Permit Extension for EIP-20 Signed Approvals. Motivation and Rationale, relayers and deadline. Accessed Oct 24, 2026.
  10. ERC-2612: Permit Extension for EIP-20 Signed Approvals. Security Considerations. Accessed Oct 24, 2026.
  11. ERC-20: Token Standard. Methods, approve and transferFrom. Accessed Oct 24, 2026.
  12. EIP-1: EIP Purpose and Guidelines. What is an EIP?, EIP Types and EIP numbers. Accessed Oct 24, 2026.
  13. Safe documentation: Safe Smart Account concepts. Threshold, Signatures and Signature verification. Accessed Oct 24, 2026.
  14. Safe documentation: Signatures. Encoding, ECDSA, eth_sign, contract signature (EIP-1271) and pre-validated signatures. Accessed Oct 24, 2026.
  15. Safe documentation: getTransactionHash. Smart account reference, the hash "that can be signed by the Safe owners". Accessed Oct 24, 2026.
  16. Safe documentation: domainSeparator. Smart account reference, "as defined in the EIP-712 standard". Accessed Oct 24, 2026.
  17. safe-global/safe-smart-account v1.4.1, contracts/Safe.sol. DOMAIN_SEPARATOR_TYPEHASH and SAFE_TX_TYPEHASH definitions (lines 48 to 56) and domainSeparator() (lines 365 to 367). Accessed Oct 24, 2026.
  18. Safe documentation: Fallback handler. CompatibilityFallbackHandler, ERC-1271 isValidSignature. Accessed Oct 24, 2026.
  19. Safe Help Center: How to verify Safe{Wallet} transactions on a hardware wallet. Steps 2 and 3, domain and message hashes. Accessed Oct 24, 2026.
  20. NIST SP 800-63B-4, Digital Identity Guidelines: Authentication and Authenticator Management (July 2025). Sec. 3.2.5, Phishing Resistance. Accessed Oct 24, 2026.

This guide is for information only and is not security or legal advice. Standards and their implementations change; read the current sources before you act.

Frequently asked questions

What does an EIP-712 signature commit to?

One 32-byte digest, built from three parts: the bytes 0x19 0x01, a domain separator and the hash of the message struct. Every field of the domain and of the message is hashed into the digest. A label that the dApp shows next to the request, such as "Confirm deposit", sits outside it.

Copy link
What do chainId and verifyingContract protect against?

chainId is the EIP-155 chain id and verifyingContract is the address of the contract that will verify the signature. Together they bind the signature to one chain and one contract, which prevents replay from other domains. A domain that leaves out either field gives that protection up.

Copy link
How does a Safe or another contract wallet prove a signature?

A contract has no private key, so ERC-1271 adds an isValidSignature function to the wallet. The verifier calls it with the hash and the signature bytes, and the wallet must return the bytes4 magic value 0x1626ba7e for a valid signature. A Safe checks that each signer is an owner and that enough owners signed to meet its threshold.

Copy link
Does EIP-712 include replay protection?

No. EIP-712 does not include replay protection. The contract that accepts the signature must behave correctly when it sees the same signed message twice. ERC-2612, for example, keeps a nonces mapping for replay protection.

Copy link
Why does an ERC-2612 permit carry risk?

A permit sets an ERC-20 allowance from a signature, and any address can submit it. Once it lands, the spender can move up to the approved value of your tokens with transferFrom. A far deadline gives the relaying party a free option to submit or withhold the permit.

Copy link
What should a signer check before approving a typed-data request?

That the request is typed data. That chainId matches the network, and that verifyingContract, name and version match the expected protocol. That the spender, value, deadline and nonce are what the mandate allows, and that the wallet decodes every field. On a Safe, every owner should see the same transaction hash.

Copy link