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.







