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]:
| Version byte | Defined in | Description |
|---|---|---|
0x00 | ERC-191 | Data with intended validator |
0x01 | EIP-712 | Structured data |
0x45 | ERC-191 | personal_sign messages |
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].
| EOA | Contract wallet (ERC-1271) |
|---|---|
| One private key signs[6] | No private key; the contract's own logic decides[6] |
Verifier recovers the address with ecrecover[1] | Verifier calls isValidSignature(hash, signature) on the wallet[6] |
A malformed signature makes ecrecover return the zero address, so the verifier must reject address(0)[10] | Valid only when the call returns 0x1626ba7e[6] |
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 witheth_sign, with 4 added tov[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
approveHashon-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,
spendercan move up tovalueof your tokens withtransferFrom[11]. A wrongspenderor an oversizedvalueis 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
deadlinelimits that window; adeadlineofuint(-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_SEPARATORcontains thechainIdbut 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.
- Is it typed data? Prefer EIP-712 (
0x01) requests. Apersonal_sign(0x45) request carries no domain or types[5], so you can't read what it authorises. - Does
chainIdmatch the network? The wallet should refuse a mismatch, and you should too[2]. - Is
verifyingContractthe 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]. - Do
nameandversionmatch the protocol? A new version means a new domain[2]. - Who is the
spender? The spender can pull up tovaluewithtransferFrom[11]. Match it to a published address. - Is
valuethe amount the mandate allows? Reject an unlimitedvalueunless the mandate requires one. The allowance is set to exactly that value[8]. - Is
deadlinenear? A near deadline limits how long a relayer holds the option[9]. - Is there a nonce, and is it the current one? A permit is valid only when its nonce
equals
nonces(owner)[8]. - 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].
- 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
EIP712Domainstruct. 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
0x19byte and a version byte at the start of signed data. It keeps signed data from being a valid transaction[5]. - ERC-1271
-
The standard
isValidSignaturemethod that lets a contract accept or reject a signature[6]. - Magic value
-
0x1626ba7e, thebytes4thatisValidSignaturereturns for a valid signature[6]. - Permit
-
An ERC-2612 signature that sets an ERC-20 allowance without an
approvetransaction 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.
- 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.
- EIP-712: Typed structured data hashing and signing. Definition of domainSeparator, the EIP712Domain fields. Accessed Oct 24, 2026.
- EIP-712: Typed structured data hashing and signing. Abstract and Motivation. Accessed Oct 24, 2026.
- EIP-712: Typed structured data hashing and signing. Security Considerations, Replay attacks and Frontrunning attacks. Accessed Oct 24, 2026.
- ERC-191: Signed Data Standard. Specification and Registry of version bytes. Accessed Oct 24, 2026.
- ERC-1271: Standard Signature Validation Method for Contracts. Abstract, Motivation and Specification. Accessed Oct 24, 2026.
- ERC-1271: Standard Signature Validation Method for Contracts. Security Considerations. Accessed Oct 24, 2026.
- ERC-2612: Permit Extension for EIP-20 Signed Approvals. Specification, permit(), nonces() and DOMAIN_SEPARATOR(). Accessed Oct 24, 2026.
- ERC-2612: Permit Extension for EIP-20 Signed Approvals. Motivation and Rationale, relayers and deadline. Accessed Oct 24, 2026.
- ERC-2612: Permit Extension for EIP-20 Signed Approvals. Security Considerations. Accessed Oct 24, 2026.
- ERC-20: Token Standard. Methods, approve and transferFrom. Accessed Oct 24, 2026.
- EIP-1: EIP Purpose and Guidelines. What is an EIP?, EIP Types and EIP numbers. Accessed Oct 24, 2026.
- Safe documentation: Safe Smart Account concepts. Threshold, Signatures and Signature verification. Accessed Oct 24, 2026.
- Safe documentation: Signatures. Encoding, ECDSA, eth_sign, contract signature (EIP-1271) and pre-validated signatures. Accessed Oct 24, 2026.
- Safe documentation: getTransactionHash. Smart account reference, the hash "that can be signed by the Safe owners". Accessed Oct 24, 2026.
- Safe documentation: domainSeparator. Smart account reference, "as defined in the EIP-712 standard". Accessed Oct 24, 2026.
- 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.
- Safe documentation: Fallback handler. CompatibilityFallbackHandler, ERC-1271 isValidSignature. Accessed Oct 24, 2026.
- 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.
- 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.