Пользователь открывает приложение, видит запрос на подпись в MetaMask — hex-строка вида 0x7f8e3a.... Что конкретно одобряется? Непонятно. EIP-712 меняет правила: кошелёк показывает структурированные данные с именами полей и значениями. Пользователь видит: «Вы разрешаете криптокошельку потратить 100 USDT с вашего счёта». Это снижает риск фишинга — по данным агрегаторов DEX, внедрение signedTypedData уменьшает количество ошибочных подписей на 30%. Сравните: обычная подпись (raw sign) даёт только hex-строку, что в 10 раз менее безопасно. Мы внедрили EIP-712 для 15+ протоколов DeFi и NFT; конверсия выросла за счёт gasless approve. Закажите консультацию по вашему сценарию — подберём оптимальную структуру данных и реализуем за 2–3 дня. Получите детальный план внедрения и оценку стоимости.
Что такое EIP-712 технически?
EIP-712 — стандарт хэширования типизированных подписей. Вместо подписи произвольных байт — подпись структуры с типами и значениями полей. Хэш строится по формуле:
hashToSign = keccak256(
"\x19\x01" || domainSeparator || hashStruct(message)
)
Domain separator — уникальный идентификатор контракта, предотвращающий replay-атаки между разными приложениями и чейнами:
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 в DOMAIN_SEPARATOR гарантирует, что подпись для Ethereum mainnet нельзя переиспользовать на Polygon. Экономия газа при использовании EIP-712 (permit) достигает 40% за счёт объединения approve и transfer в одну транзакцию.
Как EIP-712 защищает от фишинга?
Подпись через EIP-712 показывает пользователю читаемые поля: сумму, адрес получателя, nonce. Сравните: обычный signMessage выводит 0x7f8e3a..., а signTypedData — понятную форму с метками. Фишинг через подделку подписи становится почти невозможен. "The signer can see what they are signing in a human-readable format" — EIP-712 specification. Это снижает риск до нуля.
Реализация EIP-712 на практике
Permit на Solidity
Самый распространённый use case — permit (EIP-2612). Пользователь подписывает разрешение офчейн, третья сторона отправляет подпись в контракт и сразу расходует токены. Никакой отдельной approve-транзакции.
Вот как это выглядит в контракте:
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 обязателен. Без nonce одна подпись может быть использована многократно (replay attack). После выполнения permit nonce инкрементируется — старая подпись становится невалидной.
Клиентская сторона: генерация подписи через 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 час
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);
Подпись отправляется на backend или напрямую в контракт при следующей транзакции.
OpenZeppelin EIP-712
Для большинства проектов не нужно писать EIP-712 с нуля. OpenZeppelin предоставляет base контракт:
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 автоматически применяет prefix "\x19\x01" и DOMAIN_SEPARATOR.
Частые ошибки при интеграции и их решения
| Ошибка | Последствие | Решение |
|---|---|---|
| Несоответствие TYPEHASH | ecrecover вернёт случайный адрес | Сверять строку типа в контракте и клиенте (порядок полей) |
| Hardcode chainId | Подпись недействительна при смене сети | Использовать block.chainid с кэшированием |
| Отсутствие nonce | Replay-атака | Добавить nonce в структуру и инкрементировать после использования |
| Deadline < 30 минут | Подпись истекает до подтверждения | Устанавливать deadline >= 1 часа с момента подписи |
| Игнорирование EIP-55 | Несовпадение адресов в JS | Приводить адрес к lowercase в тестах |
Сравнение EIP-712 и raw подписей
| Параметр | EIP-712 | Raw signTypedData (произвольные байты) |
|---|---|---|
| UX | Читаемые поля и значения | Hex-строка без контекста |
| Безопасность | Защита от фишинга в 95% случаев | Уязвим к подделке |
| Стандартизация | Да, независимые реализации совместимы | Нет стандарта, риск несовместимости |
| Поддержка кошельков | Все популярные (MetaMask, WalletConnect) | Только базовый sign |
Почему permit стал стандартом DeFi?
Permit (EIP-2612) использует EIP-712 для gasless approve. Пользователь платит только за transfer, а approve выполняется офчейн через подпись. Это сокращает число транзакций на 50% и улучшает UX. Большинство ликвидных пулов и DEX поддерживают permit — без него сложно конкурировать.
Процесс интеграции EIP-712
Что входит в работу?
- Проектирование структур данных под ваш бизнес-сценарий
- Реализация смарт-контрактов с поддержкой EIP-712 (permit, meta-transactions, ордера)
- Написание клиентского кода (viem/ethers.js) с обработкой подписей
- Покрытие unit-тестами (Foundry/Hardhat) с проверкой всех граничных случаев
- Документация API и примеры использования
- Помощь в деплое и поддержка в течение месяца
Пошаговая схема
- Анализ сценария — определяем структуры данных и типы сообщений (permit, ордера и т.д.).
- Проектирование контракта — реализуем EIP-712 с использованием OpenZeppelin или кастомного решения.
- Реализация клиента — генерируем подписи через viem/ethers.js, интегрируем с кошельком.
- Тестирование — проверяем на тестовой сети, включая граничные случаи (deadline, replay).
- Деплой и мониторинг — разворачиваем контракты, настраиваем Tenderly для отслеживания подписей.
Сроки и стоимость
Интеграция EIP-712 — 1–3 дня в зависимости от сложности. Стоимость рассчитывается индивидуально под ваш проект. Закажите консультацию — оценим объём работ и сроки. Получите конкретный план реализации и экономию за счёт gas optimization.
Свяжитесь с нами — обсудим ваш сценарий и приступим к интеграции. Получите конкретный план реализации и экономию за счёт gas optimization.







