EIP-712 improved Ethereum’s message signing by replacing unreadable (by humans) hex data with structured data that people can understand. It standardizes what transactions get signed and how they’re displayed, making signatures readable across Ethereum-based decentralized applications (dApps).
In this article, we'll explain EIP-712, the problems it solves, how it works, how to implement it, and its broader impact.
EIP-712 is an Ethereum Improvement Proposal that defines a secure, structured way to hash and sign typed data on Ethereum. The traditional method for displaying transaction data was to treat all data as opaque byte strings, making signatures difficult to interpret and verify. EIP-712 retains the structure and meaning of the data while formalizing how it’s displayed to users, making it both machine and human-readable.
This is achieved using a format that mirrors Solidity structs. EIP-712 defines a deterministic hashing scheme with keccak256 developers implement that enables wallets to represent messages clearly. Messages are contract-compatible types such as address, uint256, or string, enabling seamless communication between wallets, users, and smart contracts.
EIP-712 addresses several challenges associated with traditional signature schemes:
Before EIP-712, wallets and hardware devices displayed transaction data in highly technical formats like hexadecimal strings, machine-encoded JSON, or raw Ethereum addresses. While machine readable, these formats lacked clarity for users, undermined usability and security by limiting user’s understanding of what they were signing, especially in dApps that use signed messages to transfer assets.
The example below illustrates what a user’s interface would display before EIP-712 if they wanted to send 0.1ETH to address “0x5409ED02…A631.” The display provides no context or explanation making it impossible for a user to verify what’s happening.

Replay attacks occur when a signature is reused to execute multiple transactions. This was possible because before EIP-712, transactions did not have a unique identifier. Without this, identical data could be “replayed” by intercepting a legitimately signed user action on a blockchain network, and authorizing unintended actions (signing transactions or messages).
EIP-712 introduced a domain separator to act as a transaction identifier. It is a unique hash tied to the signing context (app name, version, chain ID) that ensures a signature is only valid in the context of a specific signature or transaction.
At the core of an EIP-712 signature is the hashStruct, which is the keccak256 hash of the typed and structured data payload. The hashStruct ensures the data's integrity, enabling secure, verifiable signatures.
Combined with a domain separator, a nonce is a sequential counter that tracks the number of transactions sent from an externally owned account (EOA) or contracts created by a contract account.
This mechanism ensures that each signature is unique and valid only within the context of a specific signature or transaction, thereby preventing replay attacks.
Though meaningful strides had been made toward improving the signing experience for users, usability remained limited. For instance, EIP-191 laid the conceptual groundwork for EIP-712 but lacked structured data, often resulting in truncated, unreadable messages, and an elevated risk of blind signing.
EIP-712 enables wallets like MetaMask to display clear, context-rich messages that users can read and understand. While also improving interoperability for dApps by establishing a standardized format.
While EIP-191 improved human readability for signed messages, it lacked structure, making the data hard to interpret and more vulnerable to misinterpretation and implementation errors.
Before EIP-712, signing was done using the eth_sign method, which let users sign arbitrary byte strings or hashes. The problem with that model was that users had no idea what they were signing. Malicious dApps could disguise a transaction or permission request as something harmless, tricking users into unintentionally authorizing harmful operations.
EIP-712 introduced typed data structures for messages, enabling wallets to parse and display the data in a human-readable format. This structured presentation empowers users to understand and evaluate what they are signing. Typed data can consist of various data types like strings, integers, addresses, and arrays.
For example, a simple message structure might look like this:
struct Mail {
address from;
address to;
string contents;
}
EIP-712 introduced a set of signable messages that extends beyond basic transactions and raw byte strings to include typed, structured data.. This provides a consistent signing experience across different Ethereum applications. As a result, smart contracts can generate signatures that are both easy to interpret and cost-efficient to verify on-chain.

EIP-712 enforced a structured hashing algorithm using keccak256, which cryptographically binds the message’s content, structure, and signing context into a single, tamper-proof hash. Keccak256 is designed to be resistant to various cryptographic attacks, including collision, ensuring the integrity and authenticity of messages.
The EIP-712 domain separator (DOMAIN_SEPARATOR) is used to identify the domain in which a message or transaction is being signed. It ensures that signatures are valid only within a specific operational context (or “domain”), and the EIP712Domain struct has the following fields, which define the signing context:
name: A string representing the name of the domain.version: A string representing the version of the domain.chainId: An unsigned integer representing the chain ID of the domain.verifyingContract: An address representing the contract that will verify the signature.salt: A bytes32 value representing a random salt value used to uniquely identify the domain. Salt is an optional component, often unused in most implementations.
A common domain separator structure in EIP-712 might look like the following:
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
bytes32 salt;
}
The domain separator is essential to preventing the reuse of valid signatures across contexts with structurally identical but unrelated data, ensuring cryptographic isolation between domains.
EIP-712 introduced a method for creating a unique hash of the typed data that the user signs, called “typed structured data hashing and signing”.
A full EIP-712 signature has the following structure:
0x19 0x01 <domainSeparator> <hashStruct(message)>
0x19: This prefix ensures the message cannot be misinterpreted as a raw Ethereum transaction. 0x01: Specifies that the message follows the EIP-712 format, enabling wallets and tools to parse and validate typed data appropriately.
The overall message hash contains two components:
domainSeparator: A hash of the EIP-712 domain, which binds the message to a specific dApp, contract, or environment.hashStruct: A hash of the specific message data being signed (e.g., the guest address and event ID).
The struct contains one or all of the following and is known as the EIP712Domain. The EIP-712 data can be rewritten as:
0x19 0x01 <hashStruct(eip712Domain)> <hashStruct(message)>
The hashStruct function is defined as
hashStruct(s : 𝕊) = keccak256(typeHash ‖ encodeData(s))
|| denotes byte-wise concatenation
Where: typeHash = keccak256(encodeType(typeOf(s)))
Computing the typeHash:
bytes32 constant EIP712DOMAIN_TYPEHASH =
keccak256("EIP712Domain(
string name,
string version,
uint256 chainId,
address verifyingContract
)");Encoding the domain values:
EIP712Domain domain = EIP712Domain({
name: "SignatureVerifier",
version: "1",
chainId: 1,
verifyingContract: address(this)
});The encodeType defines how the structure of the data is described, while encodeData serializes the actual field values into a byte string..
Final hashing of the encoded struct:
bytes32 domainSeparator = keccak256(
abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes(domain.name)),
keccak256(bytes(domain.version)),
domain.chainId,
domain.verifyingContract
)
);
For a sample message:
struct Message {
uint256 number;
}First, define the type hash:
bytes32 public constant MESSAGE_TYPEHASH = keccak256("Message(uint256 number)");Then compute the hash of the structured message:
bytes32 hashedMessage = keccak256(abi.encode(MESSAGE_TYPEHASH, Message({ number: message })));This layered hashing approach ensures each signature is cryptographically bound to both its content and context, preventing tampering or replay.
Here is a simple implementation of a function to recover the signer address from an EIP-712 signature. This implementation assumes the message is already typed and structured in a compliant EIP-712 format:
function getSignerEIP712(
uint256 message,
uint8 v,
bytes32 r,
bytes32 s
) public view returns (address) {
bytes1 prefix = 0x19;
bytes1 version = 0x01;
bytes32 domainSeparator = i_domain_separator; // assumed to be internal or inherited variable
// Hash the structured message
bytes32 hashedMessage = keccak256(
abi.encode(MESSAGE_TYPEHASH, message)
);
// Combine all parts into digest
bytes32 digest = keccak256(
abi.encodePacked(prefix, version, domainSeparator, hashedMessage)
);
// Recover the signer address
return ecrecover(digest, v, r, s);
}
verifySigner712(): Verify the signaturefunction verifySigner712(
uint256 message,
uint8 v,
bytes32 r,
bytes32 s,
address signer
) public view returns (bool) {
address recoveredSigner = getSignerEIP712(message, v, r, s);
require(signer == recoveredSigner, "Invalid signer");
return true;
}Where:
v is the recovery ID, which is typically the last byte of the signature (27 or 28).r is the first 32 bytes of the signatures is the second 32 bytes of the signature
Message signing improves the on-chain experience by allowing relayers to cover gas fees on behalf of users. This removes major onboarding hurdles, like installing wallets, setting them up, or acquiring ETH tokens. With EIP-712, users can interact with custom-built decentralized applications (dApps) immediately, while still maintaining full control of their accounts.
Additionally, it enables clear and human-readable message signing. Instead of raw hexadecimal blobs, users see structured signing prompts that include meaningful data, making actions easier to understand. This reduces confusion and improves cross-platform consistency across Ethereum wallets and applications.
EIP-712 structured format addresses longstanding security flaws in earlier signing methods. Traditional eth_sign requests lacked context, making them vulnerable to phishing and signature replay attacks. Attackers could trick users into signing one message and reuse the signature for unrelated actions.
In contrast, EIP-712 uses a structured format that tightly binds the signature to a specific domain: the application, the contract, and the network. Each signature becomes valid only within its intended context. This design reduced the risk of misuse, replay attacks, and impersonation.
Before EIP-712, developers relied on inconsistent and custom serialization methods to sign and verify off-chain data. These ad-hoc solutions often led to errors and interoperability issues. EIP-712 introduces a consistent framework for signing typed structured data. As a result, wallets, libraries, and smart contracts can now work together reliably.
EIP-712 provides a secure and standardized way of signing structured data, allowing for greater visibility into transaction details at the point of approval. EIP-712’s adoption has been accelerated by integrations with wallets like MetaMask and platforms like WalletConnect.
EIP-712 builds on the foundation of EIP-191, which introduced a safer format for message signing. It adds structured data support, making off-chain signing both transparent and reliable. This allows developers to build more secure and user-friendly applications