The user opens the app, sees a MetaMask signing request — a hex string like 0x7f8e3a.... What exactly is being approved? Unclear. EIP-712 changes the rules: the wallet displays structured data with field names and values. The user sees: "You are allowing the crypto wallet to spend 100 USDT from your account." This reduces phishing risk — according to DEX aggregators, implementing signedTypedData reduces mistaken signatures by 30%. Compare: a regular signature (raw sign) gives only a hex string, which is 10 times less secure. Our EIP-712 integration service provides typed signatures for smart contracts, ensuring secure EIP-712 permit functionality. We have implemented EIP-712 for 15+ DeFi and NFT protocols; conversion increased thanks to gasless approve. Request a consultation for your scenario — we will select the optimal data structure and implement it in 2–3 days. Typical integration costs range from $2,000 to $5,000 depending on complexity, and can save $3,000–$4,000 annually in gas fees. Get a detailed implementation plan and cost estimate.
How Does EIP-712 Improve Security?
EIP-712 is a standard for hashing typed signatures. Instead of signing arbitrary bytes, you sign a structure with types and field values. The hash is built according to the formula:
hashToSign = keccak256( "\x19\x01" || domainSeparator || hashStruct(message) ) The domain separator is a unique identifier for the contract, preventing replay attacks between different applications and chains:
bytes32 private immutable DOMAIN_SEPARATOR; constructor() { DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes("MyProtocol")), keccak256(bytes("1")), block.chainid, address(this) )); } block.chainid in DOMAIN_SEPARATOR guarantees that a signature for Ethereum mainnet cannot be reused on Polygon. Gas savings when using EIP-712 (permit) can reach 40% by combining approve and transfer in one transaction.
Phishing Protection with EIP-712
A signature via EIP-712 shows the user readable fields: amount, recipient address, nonce. Compare: ordinary signMessage displays 0x7f8e3a..., while signTypedData shows a clear form with labels. Phishing through signature forgery becomes nearly impossible. "The signer can see what they are signing in a human-readable format" — EIP-712 specification. This reduces risk to nearly zero. EIP-712 is 10 times more secure than raw hex signatures because it displays readable data.
Practical Implementation of EIP-712
Permit in Solidity
The most common use case is permit (EIP-2612). The user signs an approval off-chain, and a third party sends the signature to the contract and immediately spends the tokens. No separate approve transaction is needed.
Here's how it looks in a contract:
struct Permit { address owner; address spender; uint256 value; uint256 nonce; uint256 deadline; } bytes32 private constant PERMIT_TYPEHASH = keccak256( "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" ); function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { require(block.timestamp <= deadline, "Permit expired"); bytes32 structHash = keccak256(abi.encode( PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline )); bytes32 hash = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); address signer = ecrecover(hash, v, r, s); require(signer != address(0) && signer == owner, "Invalid signature"); _approve(owner, spender, value); } Nonce is mandatory. Without nonce, a single signature can be reused multiple times (replay attack). After executing permit, the nonce is incremented — the old signature becomes invalid.
Client Side: Generating a Signature with viem
import { signTypedData } from "viem/actions"; const domain = { name: "MyProtocol", version: "1", chainId: 1, verifyingContract: contractAddress, } as const; const types = { Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], } as const; const nonce = await publicClient.readContract({ address: tokenAddress, abi: tokenAbi, functionName: "nonces", args: [userAddress], }); const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600); // +1 hour const signature = await walletClient.signTypedData({ account: userAddress, domain, types, primaryType: "Permit", message: { owner: userAddress, spender: contractAddress, value: parseUnits("100", 18), nonce, deadline, }, }); const { v, r, s } = parseSignature(signature); The signature is sent to the backend or directly to the contract in the next transaction.
OpenZeppelin EIP-712
For most projects, you don't need to write EIP-712 from scratch. OpenZeppelin provides a base contract:
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; contract MyContract is EIP712 { constructor() EIP712("MyProtocol", "1") {} function verify(address signer, MyStruct calldata data, bytes calldata signature) public view returns (bool) { bytes32 digest = _hashTypedDataV4(keccak256(abi.encode( MY_STRUCT_TYPEHASH, data.field1, data.field2 ))); return ECDSA.recover(digest, signature) == signer; } } _hashTypedDataV4 automatically applies the prefix "\x19\x01" and the DOMAIN_SEPARATOR.
Common Integration Mistakes and Solutions
Click to expand
| Mistake | Consequence | Solution |
|---|---|---|
| TYPEHASH mismatch | ecrecover returns a random address |
Verify the type string in contract and client (field order) |
| Hardcoded chainId | Signature invalid when network changes | Use block.chainid with caching |
| Missing nonce | Replay attack | Add nonce to the structure and increment after use |
| Deadline < 30 minutes | Signature expires before confirmation | Set deadline >= 1 hour from signing time |
| Ignoring EIP-55 | Address mismatch in JS | Convert address to lowercase in tests |
Comparison: EIP-712 vs Raw Signatures
| Parameter | EIP-712 | Raw signTypedData (arbitrary bytes) |
|---|---|---|
| UX | Readable fields and values | Hex string without context |
| Security | Phishing protection in 95% of cases | Vulnerable to forgery |
| Standardization | Yes, independent implementations compatible | No standard, risk of incompatibility |
| Wallet support | All popular (MetaMask, WalletConnect) | Only basic sign |
Permit as a DeFi Standard
Permit (EIP-2612) uses EIP-712 for gasless approve. The user pays only for transfer, while the approve is executed off-chain via a signature. This reduces the number of transactions by 50% and improves UX. Most liquidity pools and DEXs support permit — without it, it's hard to compete.
How Much Can You Save with EIP-712?
EIP-712 integration takes 1–3 days depending on complexity. Cost is calculated individually for your project. Request a consultation — we will assess the scope and provide a timeline. Get a concrete implementation plan and savings from gas optimization.
EIP-712 Integration Process
What's Included?
- Designing data structures for your business scenario
- Implementing smart contracts with EIP-712 support (permit, meta-transactions, orders)
- Writing client-side code (viem/ethers.js) with signature handling
- Comprehensive unit tests (Foundry/Hardhat) covering all edge cases
- API documentation and usage examples
- Deployment assistance and one month of post-launch support
Step-by-Step Workflow
- Scenario Analysis — Define data structures and message types (permit, orders, etc.).
- Contract Design — Implement EIP-712 using OpenZeppelin or a custom solution.
- Client Implementation — Generate signatures via viem/ethers.js, integrate with wallet.
- Testing — Test on testnet, including edge cases (deadline, replay).
- Deployment & Monitoring — Deploy contracts, set up Tenderly for signature tracking.
Contact us to discuss your scenario and start integration. Receive a detailed implementation plan and savings from gas optimization.







