July 12, 2022

Что такое метатранзакции и как их создать

Идея проста: третья сторона (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;
 }
 }

Аналогичные подходы, которые следует изучить:

Пример

Работающий пример доступен на Vercel.

Веб-интерфейс доступен для пользователей, которые хотят вызвать метод setFlagOwner без оплаты за газ. Единственным требованием является наличие установленного в браузере плагина metamask для подписи сообщений, но кто-то все равно должен платить за газ. Плата за газ (разумеется, тестовый) будет оплачиваться демо-сервером Relayer.

Код контракта и демо-сервера.