---
title: "ERC-20 із запобіжниками"
description: "Як допомогти людям уникати безглуздих помилок"
author: "Орі Померанц"
lang: uk
tags: ["erc-20"]
skill: beginner
breadcrumb: "Безпека ERC-20"
published: 2022-08-15
---

## Вступ {#introduction}

Одна з чудових рис Етеріуму полягає в тому, що не існує центрального органу, який міг би змінювати або скасовувати ваші транзакції. Одна з великих проблем Етеріуму полягає в тому, що не існує центрального органу, який мав би повноваження скасовувати помилки користувачів або незаконні транзакції. У цій статті ви дізнаєтеся про деякі поширені помилки, яких припускаються користувачі з токенами [ERC-20](/developers/docs/standards/tokens/erc-20/), а також про те, як створювати контракти ERC-20, які допомагають користувачам уникати цих помилок або надають центральному органу певні повноваження (наприклад, заморожувати акаунти).

Зверніть увагу, що хоча ми будемо використовувати [контракт токена ERC-20 від ОупенЗеппелін](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC20), ця стаття не пояснює його в усіх деталях. Ви можете знайти цю інформацію [тут](/developers/tutorials/erc20-annotated-code).

Якщо ви хочете переглянути повний вихідний код:

1. Відкрийте [Remix IDE](https://remix.ethereum.org/).
2. Натисніть значок клонування GitHub (![clone github icon](icon-clone.png)).
3. Склонуйте репозиторій GitHub `https://github.com/qbzzt/20220815-erc20-safety-rails`.
4. Відкрийте **contracts > erc20-safety-rails.sol**.

## Створення контракту ERC-20 {#creating-an-erc-20-contract}

Перш ніж ми зможемо додати функціональність запобіжників, нам потрібен контракт ERC-20. У цій статті ми будемо використовувати [Майстер контрактів ОупенЗеппелін](https://docs.openzeppelin.com/contracts/5.x/wizard). Відкрийте його в іншому браузері та дотримуйтесь цих інструкцій:

1. Виберіть **ERC20**.
2. Введіть ці налаштування:

   | Параметр | Значення |
   | -------------- | ---------------- |
   | Назва | SafetyRailsToken |
   | Символ | SAFE |
   | Попередній випуск (Premint) | 1000 |
   | Функції | Немає |
   | Контроль доступу | Ownable |
   | Можливість оновлення | Немає |

3. Прокрутіть вгору та натисніть **Open in Remix** (для Remix) або **Download**, щоб використовувати інше середовище. Я припускаю, що ви використовуєте Remix, якщо ви використовуєте щось інше, просто внесіть відповідні зміни.
4. Тепер у нас є повністю функціональний контракт ERC-20. Ви можете розгорнути `.deps` > `npm`, щоб побачити імпортований код.
5. Скомпілюйте, розгорніть та попрацюйте з контрактом, щоб переконатися, що він функціонує як контракт ERC-20. Якщо вам потрібно дізнатися, як користуватися Remix, [скористайтеся цим посібником](https://remix.ethereum.org/?#activate=udapp,solidity,LearnEth).

## Поширені помилки {#common-mistakes}

### Помилки {#the-mistakes}

Користувачі іноді надсилають токени на неправильну адресу. Хоча ми не можемо читати їхні думки, щоб дізнатися, що вони мали на увазі, існують два типи помилок, які трапляються часто і які легко виявити:

1. Надсилання токенів на власну адресу контракту. Наприклад, [токен OP від Optimism](https://optimism.mirror.xyz/qvd0WfuLKnePm1Gxb9dpGchPf5uDz5NSMEFdgirDS4c) зумів накопичити [понад 120 000](https://optimism.blockscout.com/address/0x4200000000000000000000000000000000000042) токенів OP менш ніж за два місяці. Це значна сума коштів, яку люди, ймовірно, просто втратили.

2. Надсилання токенів на порожню адресу, яка не відповідає [зовнішньому акаунту (EOA)](/developers/docs/accounts/#externally-owned-accounts-and-key-pairs) або [смарт-контракту](/developers/docs/smart-contracts). Хоча у мене немає статистики про те, як часто це трапляється, [один інцидент міг коштувати 20 000 000 токенів](https://gov.optimism.io/t/message-to-optimism-community-from-wintermute/2595).

### Запобігання переказам {#preventing-transfers}

Контракт ERC-20 від ОупенЗеппелін містить [хук `_beforeTokenTransfer`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L364-L368), який викликається перед переказом токена. За замовчуванням цей хук нічого не робить, але ми можемо прив'язати до нього власну функціональність, наприклад, перевірки, які скасовують транзакцію у разі виникнення проблеми.

Щоб використати хук, додайте цю функцію після конструктора:

```solidity
    function _beforeTokenTransfer(address from, address to, uint256 amount)
        internal virtual
        override(ERC20)
    {
        super._beforeTokenTransfer(from, to, amount);
    }
```

Деякі частини цієї функції можуть бути новими для вас, якщо ви не дуже добре знайомі з Solidity:

```solidity
        internal virtual
```

Ключове слово `virtual` означає, що так само, як ми успадкували функціональність від `ERC20` і перевизначили цю функцію, інші контракти можуть успадковувати від нас і перевизначати цю функцію.

```solidity
        override(ERC20)
```

Ми повинні явно вказати, що ми [перевизначаємо](https://docs.soliditylang.org/en/v0.8.15/contracts.html#function-overriding) визначення `_beforeTokenTransfer` токена ERC20. Загалом, явні визначення набагато кращі з точки зору безпеки, ніж неявні — ви не можете забути, що щось зробили, якщо це прямо перед вами. Це також причина, чому нам потрібно вказати, який саме `_beforeTokenTransfer` суперкласу ми перевизначаємо.

```solidity
        super._beforeTokenTransfer(from, to, amount);
```

Цей рядок викликає функцію `_beforeTokenTransfer` контракту або контрактів, від яких ми успадкували і які її мають. У цьому випадку це лише `ERC20`, оскільки `Ownable` не має цього хука. Хоча наразі `ERC20._beforeTokenTransfer` нічого не робить, ми викликаємо його на випадок, якщо функціональність буде додана в майбутньому (і ми тоді вирішимо повторно розгорнути контракт, оскільки контракти не змінюються після розгортання).

### Програмування вимог {#coding-the-requirements}

Ми хочемо додати до функції такі вимоги:

- Адреса `to` не може дорівнювати `address(this)`, адресі самого контракту ERC-20.
- Адреса `to` не може бути порожньою, вона має бути однією з таких:
  - Зовнішній акаунт (EOA). Ми не можемо безпосередньо перевірити, чи є адреса EOA, але ми можемо перевірити баланс ETH на адресі. EOA майже завжди мають баланс, навіть якщо вони більше не використовуються — їх важко очистити до останнього Wei.
  - Смарт-контракт. Перевірити, чи є адреса смарт-контрактом, трохи складніше. Існує опкод, який перевіряє довжину зовнішнього коду, що називається [`EXTCODESIZE`](https://www.evm.codes/#3b), але він недоступний безпосередньо в Solidity. Для цього нам потрібно використовувати [Yul](https://docs.soliditylang.org/en/v0.8.15/yul.html), який є асемблером EVM. Існують інші значення, які ми могли б використовувати з Solidity ([`<address>.code` та `<address>.codehash`](https://docs.soliditylang.org/en/v0.8.15/units-and-global-variables.html#members-of-address-types)), але вони коштують дорожче.

Давайте пройдемося по новому коду рядок за рядком:

```solidity
        require(to != address(this), "Can't send tokens to the contract address");
```

Це перша вимога: перевірити, що `to` та `this(address)` не є одним і тим самим.

```solidity
        bool isToContract;
        assembly {
           isToContract := gt(extcodesize(to), 0)
        }
```

Ось як ми перевіряємо, чи є адреса контрактом. Ми не можемо отримати вивід безпосередньо з Yul, тому замість цього ми визначаємо змінну для зберігання результату (у цьому випадку `isToContract`). Yul працює таким чином, що кожен опкод розглядається як функція. Тому спочатку ми викликаємо [`EXTCODESIZE`](https://www.evm.codes/#3b), щоб отримати розмір контракту, а потім використовуємо [`GT`](https://www.evm.codes/#11), щоб перевірити, що він не дорівнює нулю (ми маємо справу з цілими числами без знака, тому, звичайно, воно не може бути від'ємним). Потім ми записуємо результат у `isToContract`.

```solidity
        require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");
```

І нарешті, у нас є фактична перевірка на порожні адреси.

## Адміністративний доступ {#admin-access}

Іноді корисно мати адміністратора, який може скасовувати помилки. Щоб зменшити ймовірність зловживань, цим адміністратором може бути [мультипідпис](https://blog.logrocket.com/security-choices-multi-signature-wallets/), щоб кілька людей мали погодити дію. У цій статті ми розглянемо дві адміністративні функції:

1. Заморожування та розморожування акаунтів. Це може бути корисно, наприклад, коли акаунт може бути скомпрометований.
2. Очищення активів.

   Іноді шахраї надсилають шахрайські токени на контракт справжнього токена, щоб отримати легітимність. Наприклад, [дивіться тут](https://optimism.blockscout.com/token/0x2348B1a1228DDCd2dB668c3d30207c3E1852fBbe?tab=holders). Легітимний контракт ERC-20 — це [0x4200....0042](https://optimism.blockscout.com/token/0x4200000000000000000000000000000000000042). Шахрайський контракт, який видає себе за нього, — це [0x234....bbe](https://optimism.blockscout.com/token/0x2348B1a1228DDCd2dB668c3d30207c3E1852fBbe).

   Також можливо, що люди помилково надсилають легітимні токени ERC-20 на наш контракт, що є ще однією причиною мати спосіб їх вивести.

ОупенЗеппелін надає два механізми для забезпечення адміністративного доступу:

- Контракти [`Ownable`](https://docs.openzeppelin.com/contracts/5.x/access-control#ownership-and-ownable) мають одного власника. Функції, які мають [модифікатор](https://www.tutorialspoint.com/solidity/solidity_function_modifiers.htm) `onlyOwner`, можуть бути викликані лише цим власником. Власники можуть передати право власності комусь іншому або повністю відмовитися від нього. Права всіх інших акаунтів зазвичай ідентичні.
- Контракти [`AccessControl`](https://docs.openzeppelin.com/contracts/5.x/access-control#role-based-access-control) мають [контроль доступу на основі ролей (RBAC)](https://en.wikipedia.org/wiki/Role-based_access_control).

Задля простоти у цій статті ми використовуємо `Ownable`.

### Заморожування та розморожування контрактів {#freezing-and-thawing-contracts}

Заморожування та розморожування контрактів вимагає кількох змін:

- [Відображення (mapping)](https://www.tutorialspoint.com/solidity/solidity_mappings.htm) адрес на [логічні значення (booleans)](https://en.wikipedia.org/wiki/Boolean_data_type) для відстеження того, які адреси заморожені. Усі значення спочатку дорівнюють нулю, що для логічних значень інтерпретується як false. Це саме те, що нам потрібно, оскільки за замовчуванням акаунти не заморожені.

  ```solidity
      mapping(address => bool) public frozenAccounts;
  ```

- [Події](https://www.tutorialspoint.com/solidity/solidity_events.htm) для інформування всіх зацікавлених про те, коли акаунт заморожується або розморожується. Технічно кажучи, події не є обов'язковими для цих дій, але це допомагає позамережевому коду слухати ці події та знати, що відбувається. Вважається хорошим тоном для смарт-контракту генерувати їх, коли відбувається щось, що може бути важливим для когось іншого.

  Події індексуються, тому можна буде знайти всі випадки, коли акаунт був заморожений або розморожений.

  ```solidity
    // Коли акаунти заморожено або розморожено
    event AccountFrozen(address indexed _addr);
    event AccountThawed(address indexed _addr);
  ```

- Функції для заморожування та розморожування акаунтів. Ці дві функції майже ідентичні, тому ми розглянемо лише функцію заморожування.

  ```solidity
      function freezeAccount(address addr)
        public
        onlyOwner
  ```

  Функції, позначені як [`public`](https://www.tutorialspoint.com/solidity/solidity_contracts.htm), можуть бути викликані з інших смарт-контрактів або безпосередньо транзакцією.

  ```solidity
    {
        require(!frozenAccounts[addr], "Account already frozen");
        frozenAccounts[addr] = true;
        emit AccountFrozen(addr);
    }  // freezeAccount
  ```

  Якщо акаунт вже заморожений, скасувати транзакцію. В іншому випадку заморозити його та згенерувати (`emit`) подію.

- Змінити `_beforeTokenTransfer`, щоб запобігти переміщенню коштів із замороженого акаунта. Зверніть увагу, що кошти все ще можна переказувати на заморожений акаунт.

  ```solidity
       require(!frozenAccounts[from], "The account is frozen");
  ```

### Очищення активів {#asset-cleanup}

Щоб вивільнити токени ERC-20, які зберігаються на цьому контракті, нам потрібно викликати функцію в контракті токена, якому вони належать: або [`transfer`](https://eips.ethereum.org/EIPS/eip-20#transfer), або [`approve`](https://eips.ethereum.org/EIPS/eip-20#approve). У цьому випадку немає сенсу витрачати газ на дозволи (allowances), ми можемо зробити прямий переказ.

```solidity
    function cleanupERC20(
        address erc20,
        address dest
    )
        public
        onlyOwner
    {
        IERC20 token = IERC20(erc20);
```

Це синтаксис для створення об'єкта контракту, коли ми отримуємо адресу. Ми можемо це зробити, оскільки маємо визначення для токенів ERC20 як частину вихідного коду (див. рядок 4), і цей файл містить [визначення для IERC20](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol), інтерфейсу для контракту ERC-20 від ОупенЗеппелін.

```solidity
        uint balance = token.balanceOf(address(this));
        token.transfer(dest, balance);
    }
```

Це функція очищення, тому, ймовірно, ми не хочемо залишати жодних токенів. Замість того, щоб отримувати баланс від користувача вручну, ми можемо автоматизувати цей процес.

## Висновок {#conclusion}

Це не ідеальне рішення — не існує ідеального рішення для проблеми «користувач зробив помилку». Однак використання таких перевірок може принаймні запобігти деяким помилкам. Можливість заморожувати акаунти, хоч і є небезпечною, може бути використана для обмеження шкоди від певних зломів шляхом позбавлення хакера доступу до вкрадених коштів.

[Більше моїх робіт можна знайти тут](https://cryptodocguy.pro/).