Перейти к основному содержанию

ERC-20 с мерами предосторожности

erc-20
Beginner
Ori Pomerantz
15 августа 2022 г.
8 минута прочтения

Введение

Одно из замечательных качеств Ethereum — это отсутствие центрального органа, который мог бы изменять или отменять ваши транзакции. Одна из больших проблем Ethereum заключается в том, что нет центрального органа, уполномоченного исправлять ошибки пользователей или отменять незаконные транзакции. В этой статье вы узнаете о некоторых распространенных ошибках, которые пользователи совершают с токенами ERC-20, а также о том, как создавать контракты ERC-20, которые помогают пользователям избежать этих ошибок или которые предоставляют центральному органу некоторые полномочия (например, для замораживания аккаунтов).

Обратите внимание, что, хотя мы будем использовать контракт токена ERC-20 от OpenZeppelin (opens in a new tab), в этой статье он не объясняется в мельчайших подробностях. Эту информацию можно найти здесь.

Если вы хотите увидеть полный исходный код:

  1. Откройте Remix IDE (opens in a new tab).
  2. Нажмите на значок клонирования github (значок клонирования github).
  3. Клонируйте репозиторий github https://github.com/qbzzt/20220815-erc20-safety-rails.
  4. Откройте contracts > erc20-safety-rails.sol.

Создание контракта ERC-20

Прежде чем мы сможем добавить функциональность мер предосторожности, нам понадобится контракт ERC-20. В этой статье мы воспользуемся Мастером контрактов OpenZeppelin (opens in a new tab). Откройте его в другом браузере и следуйте этим инструкциям:

  1. Выберите ERC20.

  2. Введите следующие настройки:

    ПараметрЗначение
    ИмяSafetyRailsToken
    СимволSAFE
    Premint1000
    ФункцииНет
    Контроль доступаOwnable
    Возможность обновленияНет
  3. Прокрутите вверх и нажмите Открыть в Remix (для Remix) или Загрузить, чтобы использовать другую среду. Я буду исходить из того, что вы используете Remix. Если вы используете что-то другое, просто внесите соответствующие изменения.

  4. Теперь у нас есть полнофункциональный контракт ERC-20. Вы можете развернуть .deps > npm, чтобы увидеть импортированный код.

  5. Скомпилируйте, разверните и поработайте с контрактом, чтобы убедиться, что он функционирует как контракт ERC-20. Если вам нужно научиться пользоваться Remix, воспользуйтесь этим руководством (opens in a new tab).

Распространенные ошибки

Ошибки

Иногда пользователи отправляют токены на неверный адрес. Хотя мы не можем читать их мысли, чтобы знать, что они хотели сделать, есть два типа ошибок, которые случаются часто и легко обнаруживаются:

  1. Отправка токенов на собственный адрес контракта. Например, токену OP от Optimism (opens in a new tab) удалось накопить более 120 000 (opens in a new tab) токенов OP менее чем за два месяца. Это представляет собой значительное состояние, которое, по-видимому, люди просто потеряли.

  2. Отправка токенов на пустой адрес, который не соответствует внешнему аккаунту или умному контракту. Хотя у меня нет статистики о том, как часто это происходит, один инцидент мог стоить 20 000 000 токенов (opens in a new tab).

Предотвращение переводов

Контракт ERC-20 от OpenZeppelin включает перехватчик (hook) _beforeTokenTransfer (opens in a new tab), который вызывается перед передачей токена. По умолчанию этот перехватчик ничего не делает, но мы можем «повесить» на него собственную функциональность, например, проверки, которые отменяют транзакцию в случае возникновения проблемы.

Чтобы использовать перехватчик, добавьте эту функцию после конструктора:

1 function _beforeTokenTransfer(address from, address to, uint256 amount)
2 internal virtual
3 override(ERC20)
4 {
5 super._beforeTokenTransfer(from, to, amount);
6 }

Некоторые части этой функции могут быть новыми, если вы не очень хорошо знакомы с Solidity:

1 internal virtual

Ключевое слово virtual означает, что так же, как мы унаследовали функциональность от ERC20 и переопределили эту функцию, другие контракты могут наследоваться от нас и переопределять эту функцию.

1 override(ERC20)

Мы должны явно указать, что мы переопределяем (opens in a new tab) определение _beforeTokenTransfer для токена ERC20. В целом, с точки зрения безопасности, явные определения намного лучше, чем неявные — вы не можете забыть, что что-то сделали, если это прямо перед вами. Это также причина, по которой нам нужно указать, чей _beforeTokenTransfer суперкласса мы переопределяем.

1 super._beforeTokenTransfer(from, to, amount);

Эта строка вызывает функцию _beforeTokenTransfer контракта или контрактов, от которых мы унаследовали и в которых она есть. В данном случае это только ERC20, у Ownable этого перехватчика нет. Несмотря на то, что в настоящее время ERC20._beforeTokenTransfer ничего не делает, мы вызываем его на случай, если в будущем будет добавлена функциональность (и мы затем решим переразвернуть контракт, потому что контракты не меняются после развертывания).

Программирование требований

Мы хотим добавить в функцию следующие требования:

  • Адрес to не может быть равен address(this), то есть адресу самого контракта ERC-20.
  • Адрес to не может быть пустым, он должен быть либо:
    • Внешний аккаунт (EOA). Мы не можем напрямую проверить, является ли адрес EOA, но мы можем проверить баланс ETH по этому адресу. EOA почти всегда имеют баланс, даже если они больше не используются — трудно очистить их до последнего wei.
    • Умный контракт. Проверить, является ли адрес умным контрактом, немного сложнее. Существует код операции (opcode), который проверяет длину внешнего кода, он называется EXTCODESIZE (opens in a new tab), но он недоступен напрямую в Solidity. Для этого мы должны использовать Yul (opens in a new tab), который является ассемблером EVM. Есть и другие значения, которые мы могли бы использовать из Solidity (<address>.code и <address>.codehash (opens in a new tab)), но они стоят дороже.

Давайте рассмотрим новый код построчно:

1 require(to != address(this), "Нельзя отправлять токены на адрес контракта");

Это первое требование, проверка того, что to и this(address) — это не одно и то же.

1 bool isToContract;
2 assembly {
3 isToContract := gt(extcodesize(to), 0)
4 }

Вот как мы проверяем, является ли адрес контрактом. Мы не можем получать выходные данные напрямую из Yul, поэтому вместо этого мы определяем переменную для хранения результата (в данном случае isToContract). Yul работает так, что каждый код операции (opcode) считается функцией. Итак, сначала мы вызываем EXTCODESIZE (opens in a new tab), чтобы получить размер контракта, а затем используем GT (opens in a new tab), чтобы проверить, что он не равен нулю (мы имеем дело с беззнаковыми целыми числами, поэтому, конечно, он не может быть отрицательным). Затем мы записываем результат в isToContract.

1 require(to.balance != 0 || isToContract, "Нельзя отправлять токены на пустой адрес");

И, наконец, у нас есть фактическая проверка на пустые адреса.

Административный доступ

Иногда полезно иметь администратора, который может исправлять ошибки. Чтобы уменьшить вероятность злоупотреблений, этот администратор может быть мультиподписным кошельком (multisig) (opens in a new tab), чтобы для выполнения действия требовалось согласие нескольких человек. В этой статье мы рассмотрим две административные функции:

  1. Замораживание и размораживание аккаунтов. Это может быть полезно, например, когда аккаунт может быть скомпрометирован.

  2. Очистка активов.

    Иногда мошенники отправляют поддельные токены на контракт настоящего токена, чтобы завоевать доверие. Например, смотрите здесь (opens in a new tab). Настоящий контракт ERC-20 — это 0x4200....0042 (opens in a new tab). Мошеннический контракт, который выдает себя за него, — это 0x234....bbe (opens in a new tab).

    Также возможно, что люди по ошибке отправят настоящие токены ERC-20 на наш контракт, и это еще одна причина, по которой стоит иметь способ их оттуда вывести.

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

Для простоты в этой статье мы используем Ownable.

Замораживание и размораживание счетов

Для замораживания и размораживания счетов требуется несколько изменений:

  • Сопоставление (mapping) (opens in a new tab) адресов с логическими значениями (booleans) (opens in a new tab) для отслеживания замороженных адресов. Все значения изначально равны нулю, что для логических значений интерпретируется как false. Это то, что нам нужно, потому что по умолчанию аккаунты не заморожены.

    1 mapping(address => bool) public frozenAccounts;
  • События (events) (opens in a new tab), информирующие всех заинтересованных лиц о замораживании или размораживании аккаунта. Технически говоря, события не требуются для этих действий, но они помогают коду вне блокчейна (offchain) прослушивать эти события и знать, что происходит. Считается хорошим тоном, когда умный контракт генерирует их, когда происходит что-то, что может быть важно для кого-то еще.

    События индексируются, поэтому можно будет найти все случаи замораживания или размораживания аккаунта.

    1 // Когда аккаунты замораживаются или размораживаются
    2 event AccountFrozen(address indexed _addr);
    3 event AccountThawed(address indexed _addr);
  • Функции для замораживания и размораживания аккаунтов. Эти две функции почти идентичны, поэтому мы рассмотрим только функцию заморозки.

    1 function freezeAccount(address addr)
    2 public
    3 onlyOwner

    Функции, помеченные как public (opens in a new tab), могут вызываться из других умных контрактов или напрямую через транзакцию.

    1 {
    2 require(!frozenAccounts[addr], "Аккаунт уже заморожен");
    3 frozenAccounts[addr] = true;
    4 emit AccountFrozen(addr);
    5 } // freezeAccount

    Если аккаунт уже заморожен, отменить транзакцию. В противном случае заморозьте его и сгенерируйте (emit) событие.

  • Измените _beforeTokenTransfer, чтобы предотвратить перемещение денег с замороженного аккаунта. Обратите внимание, что деньги все еще можно переводить на замороженный аккаунт.

    1 require(!frozenAccounts[from], "Аккаунт заморожен");

Очистка активов

Чтобы высвободить токены ERC-20, хранящиеся в этом контракте, нам нужно вызвать функцию в контракте токена, которому они принадлежат, — либо transfer (opens in a new tab), либо approve (opens in a new tab). В данном случае нет смысла тратить газ на разрешения (allowances), мы можем просто перевести их напрямую.

1 function cleanupERC20(
2 address erc20,
3 address dest
4 )
5 public
6 onlyOwner
7 {
8 IERC20 token = IERC20(erc20);

Это синтаксис для создания объекта для контракта, когда мы получаем адрес. Мы можем это сделать, потому что у нас есть определение для токенов ERC20 как часть исходного кода (см. строку 4), и этот файл включает определение для IERC20 (opens in a new tab), интерфейс для контракта ERC-20 от OpenZeppelin.

1 uint balance = token.balanceOf(address(this));
2 token.transfer(dest, balance);
3 }

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

Заключение

Это не идеальное решение — идеального решения для проблемы «пользователь совершил ошибку» не существует. Однако использование подобных проверок может по крайней мере предотвратить некоторые ошибки. Возможность замораживать аккаунты, хотя и является опасной, может быть использована для ограничения ущерба от некоторых взломов, лишая хакера украденных средств.

Больше моих работ смотрите здесь (opens in a new tab).

Последнее обновление страницы: 4 сентября 2025 г.

Было ли это руководство полезным?