---
title: "ERC-20 с защитными механизмами"
description: "Как помочь людям избежать глупых ошибок"
author: "Ори Померанц"
lang: ru
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. В этой статье мы будем использовать [OpenZeppelin Contracts Wizard](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` токена ERC-20. В целом, явные определения намного лучше с точки зрения безопасности, чем неявные — вы не сможете забыть, что сделали что-то, если это находится прямо перед вами. Это также причина, по которой нам нужно указать, какой именно суперкласс `_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);
```

Это синтаксис для создания объекта контракта, когда мы получаем адрес. Мы можем сделать это, потому что у нас есть определение для токенов ERC-20 как часть исходного кода (см. строку 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/).