---
title: "Спонсирование комиссий за газ: как покрыть транзакционные издержки для ваших пользователей"
description: "Создать приватный ключ и адрес легко; это лишь вопрос запуска нужного программного обеспечения. Но в мире есть много мест, где получить ETH для отправки транзакций гораздо сложнее. В этом руководстве вы узнаете, как покрыть внутрисетевые затраты на газ для выполнения подписанных пользователем внесетевых структурированных данных в вашем смарт-контракте. Пользователь подписывает структуру, содержащую информацию о транзакции, которую ваш внесетевой код затем отправляет в блокчейн в виде транзакции."
author: "Ори Померанц"
tags: ["без газа", "Solidity", "EIP-712", "мета-транзакции"]
skill: intermediate
breadcrumb: "Спонсирование газа"
lang: ru
published: 2026-02-27
---

## Введение {#introduction}

Если мы хотим, чтобы Эфириум обслуживал [еще миллиард человек](https://blog.ethereum.org/category/next-billion), нам нужно устранить препятствия и сделать его максимально простым в использовании. Одним из источников таких препятствий является необходимость наличия ETH для оплаты комиссий за газ.

Если у вас есть децентрализованное приложение (dapp), которое приносит доход от пользователей, возможно, имеет смысл позволить им отправлять транзакции через ваш сервер и оплачивать транзакционные комиссии самостоятельно. Поскольку пользователи по-прежнему подписывают [сообщение авторизации EIP-712](https://eips.ethereum.org/EIPS/eip-712) в своих кошельках, они сохраняют гарантии целостности Эфириума. Доступность зависит от сервера, который ретранслирует транзакции, поэтому она более ограничена. Однако вы можете все настроить так, чтобы пользователи также могли напрямую обращаться к смарт-контракту (если у них есть ETH), и позволить другим настраивать свои собственные серверы, если они хотят спонсировать транзакции.

Метод, описанный в этом руководстве, работает только тогда, когда вы контролируете смарт-контракт. Существуют и другие методы, включая [абстракцию учетной записи](https://eips.ethereum.org/EIPS/eip-4337), которые позволяют спонсировать транзакции к другим смарт-контрактам, и я надеюсь рассмотреть их в будущем руководстве.

Примечание: Это _не_ код для рабочей среды (production). Он уязвим для серьезных атак и не имеет важных функций. Узнайте больше в [разделе об уязвимостях этого руководства](#vulnerabilities).

### Предварительные требования {#prerequisites}

Для понимания этого руководства вам необходимо быть знакомым с:

- Solidity
- JavaScript
- React и WAGMI. Если вы не знакомы с этими инструментами пользовательского интерфейса, [у нас есть руководство по ним](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/).

## Пример приложения {#sample-app}

Представленный здесь пример приложения является вариантом контракта `Greeter` от Hardhat. Вы можете посмотреть его [на GitHub](https://github.com/qbzzt/260301-gasless). Смарт-контракт уже развернут в сети [Sepolia](https://sepolia.dev/) по адресу [`0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA`](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA).

Чтобы увидеть его в действии, выполните следующие шаги.

1. Клонируйте репозиторий и установите необходимое программное обеспечение.

   ```sh
   git clone https://github.com/qbzzt/260301-gasless.git
   cd 260301-gasless/server
   npm install
   ```

2. Отредактируйте `.env`, чтобы установить для `PRIVATE_KEY` кошелек, на котором есть ETH в сети Sepolia. Если вам нужны Sepolia ETH, [используйте кран](/developers/docs/networks/#sepolia). В идеале этот приватный ключ должен отличаться от того, который находится в вашем браузерном кошельке.

3. Запустите сервер.

   ```sh
   npm run dev
   ```

4. Перейдите к приложению по URL-адресу [`http://localhost:5173`](http://localhost:5173).

5. Нажмите **Connect with Injected**, чтобы подключиться к кошельку. Одобрите действие в кошельке и при необходимости одобрите переключение на сеть Sepolia.

6. Напишите новое приветствие и нажмите **Update greeting via sponsor**.

7. Подпишите сообщение.

8. Подождите около 12 секунд (время блока в сети Sepolia). Во время ожидания вы можете посмотреть URL-адрес в консоли сервера, чтобы увидеть транзакцию.

9. Убедитесь, что приветствие изменилось, и что значение адреса последнего обновившего теперь является адресом вашего браузерного кошелька.

Чтобы понять, как это работает, нам нужно рассмотреть, как сообщение создается в пользовательском интерфейсе, как оно ретранслируется сервером и как смарт-контракт его обрабатывает.

### Пользовательский интерфейс {#ui-changes}

Пользовательский интерфейс основан на [WAGMI](https://wagmi.sh/); вы можете прочитать об этом [в этом руководстве](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/).

Вот как мы подписываем сообщение:

```js
const signGreeting = useCallback(
```

Хук React [`useCallback`](https://react.dev/reference/react/useCallback) позволяет нам повысить производительность за счет повторного использования одной и той же функции при перерисовке компонента.

```js
    async (greeting) => {
        if (!account) throw new Error("Wallet not connected")
```

Если аккаунт отсутствует, вызывается ошибка. Этого никогда не должно происходить, потому что кнопка пользовательского интерфейса, запускающая процесс, который вызывает `signGreeting`, в этом случае отключена. Однако будущие программисты могут удалить эту защиту, поэтому хорошей идеей будет проверить это условие и здесь.

```js
        const domain = {
            name: "Greeter",
            version: "1",
            chainId,
            verifyingContract: contractAddr,
        }
```

Параметры для [разделителя домена](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator). Это значение является константой, поэтому в более оптимизированной реализации мы могли бы вычислить его один раз, а не пересчитывать каждый раз при вызове функции.

- `name` — это удобочитаемое имя, например, название dapp, для которого мы создаем подписи.
- `version` — это версия. Разные версии несовместимы.
- `chainId` — это цепь, которую мы используем, как предоставлено [WAGMI](https://wagmi.sh/react/api/hooks/useChainId).
- `verifyingContract` — это адрес контракта, который будет проверять эту подпись. Мы не хотим, чтобы одна и та же подпись применялась к нескольким контрактам, на случай, если существует несколько контрактов `Greeter` и мы хотим, чтобы у них были разные приветствия.

```js

        const types = {
            GreetingRequest: [
                { name: "greeting", type: "string" },
            ],
        }
```

Тип данных, который мы подписываем. Здесь у нас есть единственный параметр, `greeting`, но в реальных системах их обычно больше.

```js
        const message = { greeting }
```

Фактическое сообщение, которое мы хотим подписать и отправить. `greeting` — это одновременно имя поля и имя переменной, которая его заполняет.

```js
        const signature = await signTypedDataAsync({
            domain,
            types,
            primaryType: "GreetingRequest",
            message,
        })
```

Фактическое получение подписи. Эта функция асинхронна, потому что пользователям требуется много времени (с точки зрения компьютера) для подписания данных.

```js
        const r = `0x${signature.slice(2, 66)}`
        const s = `0x${signature.slice(66, 130)}`
        const v = parseInt(signature.slice(130, 132), 16)

        return {
            req: { greeting },
            v,
            r,
            s,
        }
    },
```

Функция возвращает одно шестнадцатеричное значение. Здесь мы разделяем его на поля.

```js
    [account, chainId, contractAddr, signTypedDataAsync],
)
```

Если какая-либо из этих переменных изменится, создайте новый экземпляр функции. Параметры `account` и `chainId` могут быть изменены пользователем в кошельке. `contractAddr` является функцией идентификатора цепи. `signTypedDataAsync` не должен меняться, но мы импортируем его из [хука](https://wagmi.sh/react/api/hooks/useSignTypedData), поэтому мы не можем быть уверены, и лучше добавить его сюда.

Теперь, когда новое приветствие подписано, нам нужно отправить его на сервер.

```js
  const sponsoredGreeting = async () => {
    try {
```

Эта функция принимает подпись и отправляет ее на сервер.

```js
      const signedMessage = await signGreeting(newGreeting)
      const response = await fetch("/server/sponsor", {
```

Отправка по пути `/server/sponsor` на сервер, с которого мы пришли.

```js
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(signedMessage),
      })
```

Используйте `POST` для отправки информации в формате JSON.

```js
      const data = await response.json()
      console.log("Server response:", data)
    } catch (err) {
      console.error("Error:", err)
    }
  }
```

Вывод ответа. В рабочей системе мы бы также показали ответ пользователю.

### Сервер {#server}

Мне нравится использовать [Vite](https://vite.dev/) для фронтенда. Он автоматически обслуживает библиотеки React и обновляет браузер при изменении кода фронтенда. Однако Vite не включает инструменты для бэкенда.

Решение находится в [`index.js`](https://github.com/qbzzt/260301-gasless/blob/main/server/index.js).

```js
  app.post("/server/sponsor", async (req, res) => {
    ...
  })

  // Пусть Vite обработает все остальное
  const vite = await createViteServer({
    server: { middlewareMode: true }
  })

  app.use(vite.middlewares)
```

Сначала мы регистрируем обработчик для запросов, которые обрабатываем сами (`POST` к `/server/sponsor`). Затем мы создаем и используем сервер Vite для обработки всех остальных URL-адресов.

```js
  app.post("/server/sponsor", async (req, res) => {
    try {
      const signed = req.body

      const txHash = await sepoliaClient.writeContract({
        address: greeterAddr,
        abi: greeterABI,
        functionName: 'sponsoredSetGreeting',
        args: [signed.req, signed.v, signed.r, signed.s],
      })
    } ...
  })
```

Это просто стандартный вызов блокчейна через [viem](https://viem.sh/).

### Смарт-контракт {#smart-contract}

Наконец, [`Greeter.sol`](https://github.com/qbzzt/260301-gasless/blob/main/contracts/src/Greeter.sol) должен проверить подпись.

```solidity
    constructor(string memory _greeting) {
        greeting = _greeting;

        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                keccak256(
                    "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
                ),
                keccak256(bytes("Greeter")),
                keccak256(bytes("1")),
                block.chainid,
                address(this)
            )
        );
    }
```

Конструктор создает [разделитель домена](https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator), аналогично коду пользовательского интерфейса выше. Выполнение в блокчейне обходится гораздо дороже, поэтому мы вычисляем его только один раз.

```solidity
    struct GreetingRequest {
        string greeting;
    }
```

Это структура, которая подписывается. Здесь у нас только одно поле.

```solidity
    bytes32 private constant GREETING_TYPEHASH =
        keccak256("GreetingRequest(string greeting)");
```

Это [идентификатор структуры](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct). Он вычисляется каждый раз в пользовательском интерфейсе.

```solidity
    function sponsoredSetGreeting(
        GreetingRequest calldata req,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
```

Эта функция получает подписанный запрос и обновляет приветствие.

```solidity
        // Вычислить дайджест EIP-712
        bytes32 digest = keccak256(
            abi.encodePacked(
                "\x19\x01",
                DOMAIN_SEPARATOR,
                keccak256(
                    abi.encode(
                        GREETING_TYPEHASH,
                        keccak256(bytes(req.greeting))
                    )
                )
            )
        );
```

Создание дайджеста в соответствии с [EIP-712](https://eips.ethereum.org/EIPS/eip-712).

```solidity
        // Восстановить подписанта
        address signer = ecrecover(digest, v, r, s);
        require(signer != address(0), "Invalid signature");
```

Используйте [`ecrecover`](https://www.evm.codes/precompiled?fork=osaka#0x01), чтобы получить адрес подписавшего. Обратите внимание, что неверная подпись все равно может привести к действительному адресу, просто случайному.

```solidity
        // Применить приветствие так, как если бы его вызвал подписант
        greeting = req.greeting;
        emit SetGreeting(signer, req.greeting);
    }
```

Обновление приветствия.

## Уязвимости {#vulnerabilities}

Это _не_ код для рабочей среды. Он уязвим для серьезных атак и не имеет важных функций. Вот некоторые из них, а также способы их решения.

Чтобы увидеть некоторые из этих атак, нажимайте кнопки под заголовком _Attacks_ и смотрите, что происходит. Для кнопки **Invalid signature** проверьте консоль сервера, чтобы увидеть ответ на транзакцию.

### Отказ в обслуживании на сервере {#dos-on-server}

Самая простая атака — это атака типа [отказ в обслуживании](https://en.wikipedia.org/wiki/Denial-of-service_attack) (DoS) на сервер. Сервер получает запросы из любой точки Интернета и на основе этих запросов отправляет транзакции. Абсолютно ничто не мешает злоумышленнику выпустить кучу подписей, действительных или недействительных. Каждая из них вызовет транзакцию. В конечном итоге на сервере закончатся ETH для оплаты газа.

Одним из решений этой проблемы является ограничение скорости до одной транзакции на блок. Если цель состоит в том, чтобы показывать приветствия [внешним аккаунтам](/developers/docs/accounts/#key-differences), в любом случае не имеет значения, каким будет приветствие в середине блока.

Другое решение — отслеживать адреса и разрешать подписи только от действительных клиентов.

### Подписи с неверным приветствием {#wrong-greeting-sigs}

Когда вы нажимаете **Signature for wrong greeting**, вы отправляете действительную подпись для определенного адреса (`0xaA92c5d426430D4769c9E878C1333BDe3d689b3e`) и приветствия (`Hello`). Но она отправляется с другим приветствием. Это сбивает с толку `ecrecover`, который меняет приветствие, но имеет неверный адрес.

Чтобы решить эту проблему, добавьте адрес в [подписанную структуру](https://github.com/qbzzt/260301-gasless/blob/main/server/src/Greeter.jsx#L122-L124). Таким образом, случайный адрес `ecrecover` не совпадет с адресом в подписи, и смарт-контракт отклонит сообщение.

### Атаки повторного воспроизведения {#replay-attack}

Когда вы нажимаете **Replay attack**, вы отправляете ту же подпись «Я 0xaA92c5d426430D4769c9E878C1333BDe3d689b3e, и я хочу, чтобы приветствие было `Hello`», но с правильным приветствием. В результате смарт-контракт считает, что адрес (который не является вашим) изменил приветствие обратно на `Hello`. Информация для этого общедоступна в [информации о транзакции](https://eth-sepolia.blockscout.com/tx/0xa66afe4bbf886f59533e677a798c802ceab1ac0f9db6e83a4d4b59a45cf7c1b1).

Если это проблема, одним из решений является добавление [нонса](https://en.wikipedia.org/wiki/Cryptographic_nonce). Создайте [сопоставление (mapping)](https://docs.soliditylang.org/en/latest/types.html#mapping-types) между адресами и числами и добавьте поле нонса в подпись. Если поле нонса совпадает с сопоставлением для адреса, примите подпись и увеличьте значение в сопоставлении для следующего раза. Если нет, отклоните транзакцию.

Другое решение — добавить временную метку к подписанным данным и принимать подпись как действительную только в течение нескольких секунд после этой временной метки. Это проще и дешевле, но мы рискуем подвергнуться атакам повторного воспроизведения в пределах временного окна, а также сбоем легитимных транзакций, если временное окно будет превышено.

## Другие недостающие функции {#other-missing-features}

Есть дополнительные функции, которые мы бы добавили в рабочей среде.

### Доступ с других серверов {#other-servers}

В настоящее время мы разрешаем любому адресу отправлять `sponsorSetGreeting`. Возможно, это именно то, что нам нужно в интересах децентрализации. Или, может быть, мы хотим убедиться, что спонсируемые транзакции проходят через _наш_ сервер, и в этом случае мы бы проверяли `msg.sender` в смарт-контракте.

В любом случае, это должно быть осознанное архитектурное решение, а не просто результат того, что об этой проблеме не подумали.

### Обработка ошибок {#error-handling}

Пользователь отправляет приветствие. Может быть, оно обновится в следующем блоке. А может и нет. Ошибки невидимы. В рабочей системе пользователь должен иметь возможность различать эти случаи:

- Новое приветствие еще не отправлено
- Новое приветствие отправлено и находится в процессе обработки
- Новое приветствие было отклонено

## Заключение {#conclusion}

На данном этапе вы сможете реализовать безгазовое взаимодействие для пользователей вашего децентрализованного приложения (dapp) ценой некоторой централизации.

Однако это работает только со смарт-контрактами, которые поддерживают ERC-712. Например, для перевода токена ERC-20 необходимо, чтобы владелец подписал транзакцию, а не просто сообщение. Самое простое решение — сделать так, чтобы активы принадлежали не адресу EOA, а отдельному контракту (простая форма [абстракции учетной записи](/roadmap/account-abstraction/)). Вы можете прочитать об этом подробнее [в следующем руководстве](/developers/tutorials/gasless-token).

[Здесь вы можете ознакомиться с другими моими работами](https://cryptodocguy.pro/).
