Что такое метатранзакции и как их создать
Идея проста: третья сторона (Relayer) отправляет транзакции пользователя и оплачивает стоимость газа.
Схема выглядит так:
1️⃣Юзер: подписывает метатранзакцию (то есть сообщение, содержащее информацию о транзакции, которую он хотел бы выполнить).
2️⃣Релаер: веб-сервер с кошельком, который подписывает действительную транзакцию и отправляет ее в блокчейн.
3️⃣Форвард: контракт, отвечающий за проверку подписи метатранзакции, которая перенаправляет запрос контракту-получателю.
4️⃣Получатель: контракт, который пользователь намеревался вызвать без оплаты платы за газ, этот контракт должен уметь определять личность пользователя, который первоначально запросил транзакцию.
В Ethereum такие ретрансляторы организованы в сеть, в так называемую Open Gas Station Network (OGSN (https://opengsn.org/)). Механизм OGSN такой же, но сеть ретрансляторов обеспечивает децентрализацию и многие другие преимущества.
Давайте попробуем воссоздать упрощенную схему метатранзакций без кассиров, хаба ретрансляторов и др. участников.
Перенаправляющий контракт
Отправной точкой для понимания основного механизма является реализация OpenZeppelin MinimalForwarder, который будет использоваться вместе с контрактом, совместимым с ERC2771, в качестве контракта получателя.
Давайте рассмотрим контракт MinimalForwarder.sol:
contract MinimalForwarder is EIP712
Контракт соответствует предложению по усовершенствованию EIP-712, стандарту хэширования и подписи типизированных структурированных данных, реализуя схему разделителя доменов для защиты от атак повторного воспроизведения при возможном форке чейна.
struct ForwardRequest { address from; address to; uint256 value; uint256 gas; uint256 nonce; bytes data; }
Он имеет тип struct, определяющий необходимые поля Meta Transaction.
mapping(address => uint256) private _nonces;
Мэппинг между адресами и одноразовым токеном доступа, это связано с тем, что мета-транзакции не регистрируются в блокчейне, поэтому контракт должен самостоятельно проверять количество мета-транзакций, отправленных с данного адреса, чтобы избежать атак повторного воспроизведения.
function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool)
Эта функция проверяет, что Мета-транзакция (ForwarderRequest) имеет валидную подпись Пользователя и ее nonce корректен.
function execute(ForwardRequest calldata req, bytes calldata signature) public payable returns (bool, bytes memory) {
После метода верификации эта функция обновляет nonce пользователя и пересылает мета-транзакцию контракту-получателю.
Контракт получателя
Теперь нам нужен пример контракта с получателем. Его иллюстрация на картинке ниже.
Этот контракт имеет в памяти объект флага, который изначально белый и не имеет владельца.
Любой пользователь может вызвать метод setFlagOwner контракта, заявить о своем праве собственности на флаг и покрасить его в тот цвет, который он предпочитает.
Контракт получателя должен работать как с прямыми, так и с перенаправленными транзакциями. Разница между ними заключается в значении msg.sender.
В перенаправленных транзакциях msg.sender — это адрес контракта форвардера. Таким образом, в этой ситуации контракт получателя должен получить фактический msg.sender из полезной нагрузки транзакции.
Это достигается путем расширения контракта @openzeppelin/contracts/metatx/ERC2771Context.sol
contract Recipient is ERC2771Context
Кроме того, контракт должен быть развернут с указанием трастового форвардера (единственного, которому разрешено пересылать транзакции):
constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}
и в коде контракта msg.sender должен быть заменен на метод _msgSender().
function setFlagOwner(string memory _color) external { address previousHolder = currentHolder; currentHolder = _msgSender(); color = _color; emit FlagCaptured(previousHolder, currentHolder, color); }
Ниже показано, как метод _msgSender() может получить предполагаемого отправителя сообщения как для прямых, так и для перенаправленных транзакций.
function _msgSender() internal view virtual override returns (address sender) { if (isTrustedForwarder(msg.sender)) { assembly { sender := shr(96, calldataload(sub(calldatasize(), 20))) } } else { return msg.sender; } }
Аналогичные подходы, которые следует изучить:
- https://wyvernprotocol.com/docs
- https://github.com/etherdelta/smart_contract
- https://protocol.0x.org/en/latest/index.html
- https://github.com/DexyProject/protocol
Пример
Работающий пример доступен на Vercel.
Веб-интерфейс доступен для пользователей, которые хотят вызвать метод setFlagOwner без оплаты за газ. Единственным требованием является наличие установленного в браузере плагина metamask для подписи сообщений, но кто-то все равно должен платить за газ. Плата за газ (разумеется, тестовый) будет оплачиваться демо-сервером Relayer.