Подписи EIP-712 и permit: как защитить контракт от повтора подписи
Подпись EIP-712 — это подпись структурированных данных, которую пользователь делает в кошельке без отправки транзакции, а контракт потом проверяет. На ней построен permit (EIP-2612): разрешение на списание токенов выдаётся подписью вместо отдельной транзакции approve. Главный риск — повтор подписи: одну и ту же подпись используют дважды, в другой сети или в другом контракте. Защита обязательна и состоит из пяти элементов: nonce, срок действия, домен с chainId и адресом контракта, проверка подписанта и корректная обработка ошибок.
Как устроена подпись EIP-712
EIP-712 задаёт, как превратить структуру данных в хеш, который кошелёк покажет пользователю в читаемом виде. Хеш собирается из двух частей. Домен описывает, где подпись действительна: название и версия приложения, chainId сети и адрес проверяющего контракта. Сообщение — сами данные: кому, сколько, до какого времени, с каким номером.
| Поле домена | От чего защищает |
|---|---|
name, version | От повтора в другом приложении или старой версии контракта |
chainId | От повтора в другой сети, где развёрнут такой же контракт |
verifyingContract | От повтора в другом контракте той же сети |
salt (необязательно) | Дополнительное разделение, если первых полей недостаточно |
В OpenZeppelin за домен отвечает контракт EIP712. Он вычисляет разделитель домена при деплое и пересчитывает его, если chainId изменился, например после разделения сети. С версии 5.7 название и версия в EIP712 должны укладываться в 31 байт, иначе конструктор откатится, — учитывайте это при выборе длинного названия токена.
Permit: approve без транзакции
Обычный сценарий работы с токеном требует двух транзакций: approve, затем вызов контракта, который списывает токены. EIP-2612 добавляет функцию permit(owner, spender, value, deadline, v, r, s). Пользователь подписывает разрешение в кошельке, а контракт вызывает permit и списание в одной транзакции. В OpenZeppelin это расширение ERC20Permit, оно уже содержит nonce и проверку срока.
Пять защит от повтора
- Nonce. У каждого подписанта счётчик, который увеличивается при каждом использовании подписи. Использованная подпись больше не пройдёт. В OpenZeppelin это контракт
Noncesи функция_useNonce(owner). - Срок действия. Поле
deadlineв сообщении и проверкаif (block.timestamp > deadline) revert Expired();. Подпись без срока живёт вечно. - Полный домен.
chainIdиverifyingContractв домене, а не собственная «упрощённая» схема хеширования. - Проверка подписанта. Восстановленный адрес сравнивается с ожидаемым, а нулевой адрес отклоняется.
- Все значимые поля в подписи. Получатель, сумма, токен, назначение. Всё, что не подписано, может подставить тот, кто отправляет транзакцию.
Учебный пример проверки
Учебный пример контракта, который исполняет заявку пользователя по подписи. Структура: Order(address owner,address to,uint256 amount,uint256 nonce,uint256 deadline).
bytes32 constant ORDER_TYPEHASH = keccak256("Order(address owner,address to,uint256 amount,uint256 nonce,uint256 deadline)");if (block.timestamp > o.deadline) revert Expired();bytes32 structHash = keccak256(abi.encode(ORDER_TYPEHASH, o.owner, o.to, o.amount, _useNonce(o.owner), o.deadline));bytes32 digest = _hashTypedDataV4(structHash);— домен добавляется автоматически.if (!SignatureChecker.isValidSignatureNow(o.owner, digest, signature)) revert BadSignature();
_useNonce возвращает текущий номер и сразу увеличивает его, поэтому вторая попытка с той же подписью не совпадёт с хешем. SignatureChecker проверяет и обычные подписи кошельков, и подписи смарт-кошельков по ERC-1271.
Тонкие места
Перехват permit
Подпись permit видна в мемпуле. Кто угодно может отправить её первым и вызвать permit напрямую. Средства он не получит, ведь разрешение выдаётся указанному spender, но nonce израсходуется, и транзакция пользователя упадёт. Документация OpenZeppelin рекомендует оборачивать вызов permit в try/catch и продолжать, если разрешение уже установлено.
Смарт-кошельки и EIP-7702
Всё больше пользователей работают через смарт-кошельки, а после Pectra (EIP-7702) у обычного адреса может появиться делегированный код. Проверка только через ECDSA.recover не примет подпись смарт-кошелька. Используйте SignatureChecker, если ваша аудитория может подписывать через контракт.
Подпись в нескольких сетях
Если один и тот же контракт развёрнут в нескольких сетях по одинаковому адресу, например через CREATE2, домен без chainId сделает подпись из одной сети действительной в другой. Это одна из причин не писать собственное хеширование, а использовать EIP712 из OpenZeppelin. Тест, который проверяет отказ подписи после смены chainId через vm.chainId(...) в Foundry, стоит включить в набор обязательных.
Подписи, которые пользователь не понимает
Подпись permit с неограниченной суммой и далёким сроком — частый инструмент фишинга: пользователь подписывает «вход на сайт», а на деле выдаёт разрешение на все токены. Делайте интерфейс честным: показывайте сумму, получателя и срок, запрашивайте ровно необходимое, а срок ставьте коротким.
Где ещё используют подписи
| Сценарий | Что подписывает пользователь или сервер | Главный риск |
|---|---|---|
| Вайтлист NFT | Сервер подписывает адрес и лимит минта | Подпись без лимита и срока, утечка ключа сервера |
| Раздача токенов | Сервер подписывает получателя и сумму | Повтор подписи, если нет учёта уже выданного |
| Мета-транзакции | Пользователь подписывает вызов, газ платит ретранслятор | Подмена параметров, которых нет в подписи |
| Ордера на обмен | Пользователь подписывает условия сделки | Исполнение по устаревшим условиям без срока |
| Голосование по подписи | Голос за предложение | Повтор голоса в другом предложении |
Когда подписывает сервер, а не пользователь, ключ сервера становится таким же ценным, как ключ администратора контракта. Его хранят отдельно от приложения, в защищённом хранилище, с журналом использования и возможностью быстро заменить. Если ключ утёк, контракт должен позволять сменить адрес доверенного подписанта, а старые подписи — перестать работать. Общие правила обращения с ключами и журналами мы разбирали в статье о токенах и паролях в журналах приложения.
Таблица проверки
| Вопрос | Что должно быть |
|---|---|
| Есть ли nonce и расходуется ли он до внешних вызовов? | Да, _useNonce или эквивалент |
| Есть ли срок действия? | Да, проверка deadline |
Входят ли chainId и адрес контракта в домен? | Да, через EIP712 из OpenZeppelin |
| Подписаны ли все поля, которые влияют на результат? | Получатель, сумма, токен, назначение |
| Как проверяется подпись? | ECDSA или SignatureChecker, не голый ecrecover |
| Что будет при перехвате permit? | try/catch и проверка текущего разрешения |
| Можно ли отозвать подпись? | Функция, которая увеличивает nonce владельца |
Подписи часто используют в вайтлистах NFT, раздачах токенов и мета-транзакциях. Для NFT-продаж с подписанным вайтлистом смотрите решение NFT-коллекции, а интерфейс, который показывает пользователю понятное содержимое подписи, мы делаем в рамках веб-интерфейса dApp. Перед запуском проверьте контракт по чек-листу перед аудитом или обратитесь к команде разработки смарт-контрактов.
Вопросы и ответы
Что такое EIP-712?
Это стандарт подписи структурированных данных в Ethereum и совместимых сетях. Кошелёк показывает пользователю понятные поля, а контракт проверяет подпись с учётом сети, адреса контракта и версии приложения.
Что такое permit в ERC-20?
Это функция из EIP-2612, которая выдаёт разрешение на списание токенов по подписи владельца, без отдельной транзакции approve. Это экономит одну транзакцию и газ.
Как защититься от повтора подписи?
Использовать nonce, срок действия, домен с chainId и адресом контракта, проверять подписанта через библиотеку ECDSA или SignatureChecker и подписывать все значимые поля.
Можно ли отменить выданную подпись?
Только если контракт это предусматривает: обычно через функцию, которая увеличивает nonce владельца. Тогда все ещё не использованные подписи с прежним номером перестают работать.
Опасно ли подписывать сообщения в кошельке?
Да, если не понимаете, что подписываете. Подпись permit может дать разрешение на списание токенов. Проверяйте сумму, получателя, срок и адрес сайта.
Читайте также
Отмена подписки и удаление аккаунта: как разделить сценарии
РазработкаПодпись вебхука: что проверить до изменения заказа
РазработкаПрограмма подписана, а SmartScreen всё равно предупреждает
БизнесПовторная аренда или продление: как считать возврат клиентов
ПоддержкаPush сайта не приходят: разрешение и подписка браузера
РазработкаРасписание сдвигается на час: как хранить повторы