Разработка системы meta-транзакций (EIP-2771)
Пользователь установил приложение, получил NFT или токены, хочет что-то сделать — и натыкается на «нужно ETH для газа». На этом шаге теряется от 30 до 60% новых пользователей в зависимости от аудитории. Meta-транзакции — механизм, который убирает это препятствие: пользователь подписывает намерение, приложение оплачивает газ.
EIP-2771 стандартизировал архитектуру: trusted forwarder — контракт, которому целевой контракт доверяет пересылать вызовы с сохранением оригинального msg.sender.
Как работает EIP-2771
Без meta-транзакций: user → (напрямую) → Contract. msg.sender в контракте — это адрес пользователя.
С meta-транзакциями: user → (подписанный запрос) → Relayer → Forwarder → Contract. msg.sender в контракте — это адрес Forwarder. Контракт не знает настоящего отправителя.
Решение — контракт проверяет, что msg.sender является доверенным forwarder'ом, и тогда читает настоящий адрес из последних 20 байт calldata:
// OpenZeppelin ERC2771Context
function _msgSender() internal view virtual override returns (address) {
if (isTrustedForwarder(msg.sender) && msg.data.length >= 20) {
return address(bytes20(msg.data[msg.data.length - 20:]));
}
return super._msgSender();
}
Все msg.sender в бизнес-логике контракта нужно заменить на _msgSender(). Это единственное изменение в существующем контракте — если он наследует ERC2771Context от OpenZeppelin.
Компоненты системы
Trusted Forwarder
Валидирует подписи пользователей (EIP-712 typed data), проверяет nonce (защита от replay), пересылает вызов в целевой контракт, добавляя адрес пользователя в конец calldata.
OpenZeppelin MinimalForwarder — простая реализация, подходит для начала. Для production рекомендуем OpenGSN Forwarder или собственный с дополнительными проверками: deadline, domain separator, address whitelisting.
struct ForwardRequest {
address from; // пользователь
address to; // целевой контракт
uint256 value; // ETH (обычно 0)
uint256 gas; // лимит газа
uint256 nonce; // защита от replay
bytes data; // calldata
}
Подпись EIP-712
Пользователь подписывает структурированные данные, а не сырой хэш. Это позволяет MetaMask и другим кошелькам показывать человекочитаемое содержимое запроса перед подписанием.
// Клиент: подготовка подписи
const domain = {
name: "MyForwarder",
version: "1",
chainId: await signer.getChainId(),
verifyingContract: forwarderAddress,
};
const signature = await signer.signTypedData(domain, types, request);
Relayer
Принимает подписанный запрос, проверяет его валидность, отправляет транзакцию за пользователя, оплачивая газ. Может быть:
- Централизованным — собственный backend, самый простой вариант
- OpenGSN — децентрализованная сеть relayer'ов
- Biconomy — managed сервис с dashboard и analytics
- Gelato Network — для автоматизации и условных вызовов
Для большинства проектов на старте — централизованный relayer на собственном backend. Это проще, быстрее и дешевле, пока TPS небольшой. Децентрализация нужна, когда централизованный relayer становится точкой отказа с реальными последствиями.
Ключевые уязвимости и защиты
Replay attack. Подписанный запрос без nonce или с предсказуемым nonce может быть исполнен несколько раз. Forwarder должен хранить nonce per-user и инкрементировать после каждого успешного вызова.
Gas griefing. Пользователь указывает минимальный gas в запросе, relayer отправляет транзакцию с этим лимитом — контракт падает с out-of-gas, но газ потрачен. Решение: relayer проверяет, что у него достаточно газа для выполнения + overhead на forwarder logic.
Forwarder spoofing. Если контракт принимает любой forwarder как доверенный — атакующий может подделать msg.sender. Список доверенных forwarder'ов должен быть фиксированным или изменяемым только через multisig.
_msgSender() vs msg.sender. Самая распространённая ошибка при интеграции EIP-2771 — использование msg.sender там, где должен быть _msgSender(). Статический анализ через Slither ловит часть таких случаев, но не все.
Интеграция с существующим контрактом
Если контракт уже в production без поддержки EIP-2771 — его нельзя изменить (без upgrade proxy). Есть обходной путь: meta-транзакции через EIP-1271 (contract signatures), где пользователь деплоит собственный аккаунт-контракт. Но это сложнее и дороже для пользователя.
Вывод: если meta-транзакции нужны, закладывать поддержку ERC2771Context нужно на этапе первоначальной разработки, не после.
Процесс и сроки
| Компонент | Срок |
|---|---|
| Интеграция ERC2771Context в контракт + тесты | 1 день |
| MinimalForwarder деплой + конфигурация | 0.5 дня |
| Backend relayer (Node.js + ethers.js) | 1-2 дня |
| Frontend интеграция (wagmi + signTypedData) | 1 день |
| Тесты end-to-end сценариев | 1 день |
Итого для полной системы с централизованным relayer: 3-5 рабочих дней. С Biconomy или OpenGSN вместо собственного backend: 2-3 дня (меньше backend-работы, больше конфигурации). Стоимость зависит от состояния существующего контракта и выбора инфраструктуры relayer.







