---
title: "Спонсорування комісій за газ: як покрити витрати на транзакції для ваших користувачів"
description: "Створити приватний ключ та адресу легко; це лише питання запуску відповідного програмного забезпечення. Але у світі є багато місць, де отримати ETH для надсилання транзакцій набагато складніше. У цьому посібнику ви дізнаєтеся, як покрити витрати на газ у мережі для виконання підписаних користувачем структурованих даних поза ланцюгом у вашому смарт-контракті. Користувач підписує структуру, що містить інформацію про транзакцію, яку ваш код поза ланцюгом потім надсилає в блокчейн як транзакцію."
author: "Орі Померанц"
tags: ["без газу", "Solidity", "eip-712", "мета-транзакції"]
skill: intermediate
breadcrumb: "Спонсорування газу"
lang: uk
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), які дозволяють спонсорувати транзакції до інших смарт-контрактів, і я сподіваюся розглянути їх у майбутньому посібнику.

Примітка: це _не_ код виробничого рівня. Він вразливий до серйозних атак і не має основних функцій. Дізнайтеся більше в [розділі про вразливості цього посібника](#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) на сервер. Сервер отримує запити з будь-якої точки Інтернету і на основі цих запитів надсилає транзакції. Абсолютно нічого не заважає зловмиснику видати купу підписів, дійсних чи недійсних. Кожен з них викличе транзакцію. Зрештою на сервері закінчаться 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/).
