Перейти к основному содержимому
Блокчейн15ГЛАВА 15Готовый ERC20: OpenZeppelin и что он делает за вас@openzeppelinis ERC20_update3670 байт против 3976чужой код

Готовый ERC20: OpenZeppelin и что он делает за вас

Прочитано0%
Квизы0/4
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

Контракт, который выдают на аудит в модуле В, начинается ровно с этого:

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token are ERC20("CryptoMonster", "CMON") { … }

Опечатку в третьей строке мы уже разбирали. А вот вторую строку — подключение библиотеки — нет. Чтобы прочитать этот контракт, надо знать, что именно приезжает из библиотеки и чего в нём поэтому нет: все балансы, переводы, разрешения и события лежат там.

Библиотека называется OpenZeppelin, актуальная версия — 5.6.1. Большинство уроков в интернете описывают версию 4.x, а между ними сломанная совместимость: конструкторы, тексты ошибок и точки расширения изменились. Разберём и то, и другое.

Одна строка импорта и одно слово is​

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract OzToken is ERC20 {
constructor(uint256 supply) ERC20("Oz", "OZT") {
_mint(msg.sender, supply);
}
}
Нажми на часть выражения

Всё, что мы писали руками в прошлой главе, здесь приезжает готовым: шесть обязательных функций, три поля, два события, а также внутренние _transfer, _mint, _burn, _approve.

Библиотека оказалась меньше рукописного токена​

Первая модель ставит рядом три контракта — токен из прошлой главы, тот же токен со своими ошибками вместо строк, и наследника библиотеки:

── один и тот же токен: руками против готовой библиотеки ──
байткод MiniToken: 3976 байт
байткод MiniErrToken: 3733 байт (тот же код, но кастомные ошибки вместо строк)
байткод OzToken: 3670 байт
газ развёртывания MiniToken: 1019899
газ развёртывания OzToken: 966009
перевод на пустой баланс: MiniToken 52379 | OzToken 52150
перевод повторно: MiniToken 35279 | OzToken 35050

── текст отката: у anna на балансе 200, она отправляет 500 ──
MiniToken: reverted with reason string 'Недостаточно средств'
MiniErrToken: reverted with custom error 'NotEnough("0x7099…79C8", 200, 500)'
OzToken: reverted with custom error 'ERC20InsufficientBalance("0x7099…79C8", 200, 500)'

── что готовый ERC20 проверяет сам ──
decimals(): 18
перевод на address(0): 'ERC20InvalidReceiver("0x0000…0000")'
transferFrom без approve: 'ERC20InsufficientAllowance("0x7099…79C8", 0, 1)'
ГОТОВЫЙ ТОКЕН ОКАЗАЛСЯ МЕНЬШЕ РУКОПИСНОГОнаписан руками, строковые сообщения3 976 байттот же код, но свои ошибки вместо строк3 733 байтанаследник библиотечного ERC203 670 байт243 байта из 306 съедали тексты сообщений: свои ошибки короче строкразвёртывание: 1 019 899 газа против 966 009; перевод дешевле на 229ERC20InsufficientBalance(«0x7099…79C8», 200, 500) — вместо текста три числа
Разницу в 306 байт объясняет средний контракт: 243 из них съедали тексты сообщений

Ожидание было обратным: готовая библиотека — значит «толще». На деле она меньше и дешевле. Средний контракт объясняет почему: стоит заменить строковые сообщения на свои ошибки, и рукописный токен худеет на 243 байта из 306. Тексты живут в байткоде; коды ошибок — нет.

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

Интересный факт

Имена ошибок вроде ERC20InsufficientBalance — не выдумка библиотеки, а отдельный стандарт на тексты ошибок токенов. Поэтому интерфейс, написанный под одну библиотеку, поймёт ошибки другой.

Что изменилось в версии 5​

Разница между 4.x и 5.x — главная причина, по которой урок из интернета не собирается. Четыре изменения, которые встретятся почти сразу:

Владелец теперь передаётся в конструктор. Раньше владельцем становился тот, кто развернул контракт; теперь адрес передают явно:

contract FeeToken is ERC20, Ownable {
constructor(address initialOwner) ERC20("Fee", "FEE") Ownable(initialOwner) {}
}

Забыли аргумент — ошибка про то, что базовому конструктору не переданы значения.

Строковые сообщения заменены своими ошибками. Вместо текста — имя ошибки и числа.

Библиотека безопасной арифметики удалена. Файла просто нет: с версии языка 0.8 переполнение проверяет компилятор. Подключение даёт ошибку «файл не найден».

Два хука заменены одним. Раньше было «до перевода» и «после перевода»; теперь единственная точка — _update.

_update — единственная дверь​

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

function _update(address from, address to, uint256 value) internal override {
require(!paused, unicode"Токен на паузе");
// from == 0 — это выпуск, to == 0 — это сжигание: комиссию с них не берём
if (from != address(0) && to != address(0)) {
uint256 fee = value / 100; // 1 %
if (fee > 0) {
super._update(from, owner(), fee); // сама комиссия — тоже движение
value -= fee;
}
}
super._update(from, to, value);
}

Вторая модель сравнивает этот способ с «наивным» — где то же самое написано в переопределённом transfer:

── FeeToken: _update переопределён ──
комиссия владельцу с 1000 при переводе:
transfer : 10
transferFrom : 10
mint : 0 (выпуск: from == address(0))
burn : 0 (сжигание: to == address(0))
после setPaused(true):
transfer : reverted with reason string 'Токен на паузе'
transferFrom : reverted with reason string 'Токен на паузе'
mint : reverted with reason string 'Токен на паузе'
burn : reverted with reason string 'Токен на паузе'

── NaiveFeeToken: переопределён только transfer ──
комиссия владельцу с 1000 при переводе:
transfer : 10
transferFrom : 0
после setPaused(true):
transfer : reverted with reason string 'Токен на паузе'
transferFrom : прошло
mint : прошло
burn : прошло

── Ownable 5.x: чужой вызывает функцию владельца ──
reverted with custom error 'OwnableUnauthorizedAccount("0x7099…79C8")'
ОДНА ДВЕРЬ ДЛЯ ЛЮБОГО ДВИЖЕНИЯ БАЛАНСАtransfertransferFrom_mint_burn_updateпереопределили _update· transfer: комиссия 10· transferFrom: комиссия 10· на паузе: все четыре стоятпереопределили только transfer· transfer: комиссия 10· transferFrom: комиссия 0· на паузе: transferFrom прошловыпуск отличается нулевым адресом отправителя, сжигание — нулевым получателемзащита, повешенная на один transfer, выглядит рабочей — и обходится вторым способом перевода
Комиссия и пауза, повешенные на _update, перехватывают все четыре операции; повешенные на transfer — только одну

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

Ровно так и обходят такие защиты: не ломают проверку, а идут другой дверью.

Важно

Внутри переопределения вызывают super._update(...), а не _update(...). Второе — вызов самого себя: бесконечная рекурсия, которая закончится тем, что транзакция сожжёт весь газ и откатится.

Отвечено вопросов: 0 из 2
Наследник библиотечного ERC20 оказался меньше рукописного токена на 306 байт. Почему?
Комиссию повесили на переопределённый transfer. Что пройдёт мимо неё?
Отвечено верно: 0 из 2

Расширения​

Рядом с базовым токеном в библиотеке лежат готовые дополнения. Самые полезные для задания:

РасширениеЧто даёт
Сжиганиепубличные функции сжечь свои токены или чужие по разрешению
Паузапроверка паузы внутри _update, готовая ошибка
Потолок выпусканельзя выпустить больше заданного количества
Разрешение по подписивыдать разрешение без отдельной транзакции

Подключаются наследованием: contract T is ERC20, ERC20Burnable, ERC20Pausable. При этом компилятор потребует явно перечислить, чью версию _update вы берёте, — тот самый список после override, который мы разбирали в главе про наследование.

Там же, в библиотеке, лежит и стандарт долей хранилища — про него следующая глава.

Безопасный перевод чужого токена​

Третья модель платит чужим токеном тремя способами — прямым вызовом, вызовом с проверкой признака и безопасным переводом из библиотеки:

── NoReturnToken — transfer без returns (bool) ──
без проверки : откат: function returned an unexpected amount of data
с проверкой : откат: function returned an unexpected amount of data
safeTransfer : ок, газ 39328, доехало 100

── LyingToken — transfer возвращает false ──
без проверки : ок, газ 27058, доехало 0
с проверкой : reverted with reason string 'Перевод не прошёл'
safeTransfer : reverted with custom error 'SafeERC20FailedOperation("0x9fE4…a6e0")'

── GoodToken — обычный ERC20 из библиотеки ──
без проверки : ок, газ 39648, доехало 100
с проверкой : ок, газ 39616, доехало 100
safeTransfer : ок, газ 39173, доехало 100

── почему прямой вызов откатывается на NoReturnToken ──
IERC20.transfer объявлен как returns (bool): вызов ждёт 32 байта ответа
NoReturnToken.transfer возвращает 0 байт — декодер откатывает всю транзакцию
SafeERC20 читает длину ответа: 0 байт при успешном вызове считаются успехом
ТРИ СПОСОБА ЗАПЛАТИТЬ ЧУЖИМ ТОКЕНОМбез проверкис проверкойsafeTransferтокен без возврата признакаоткатоткаток · 39 328токен возвращает falseок, доехало 0откатоткатобычный токенок · 39 648ок · 39 616ок · 39 173строка «ок, доехало 0» — токен соврал, а вызывающий записал себе успешную выплатуна обычном токене безопасный перевод не просто надёжнее — он ещё и дешевле ручной проверки
Безопасный перевод справляется со всеми тремя токенами — и на обычном оказывается дешевле ручной проверки

Три вывода.

Токен без возврата признака ломает обычный вызов. Не потому, что перевод не прошёл, — а потому, что вызывающий ждал 32 байта ответа и не получил их. Такие токены существуют, и один из них — очень крупный.

Токен, который врёт, обманывает непроверенный вызов. «Ок, доехало 0» — это худший исход: транзакция успешна, учёт испорчен.

Безопасный перевод дешевле ручной проверки. На обычном токене — 39 173 против 39 616 газа. Он читает длину ответа и трактует её правильно: ноль байт при успешном вызове — это успех, 32 байта — надо смотреть на признак.

На площадке интернета нет​

Проверка занимает одну команду в терминале рабочего места: посмотреть версию установленного пакета и содержимое его папки. Если пакет есть — работаем с библиотекой, экономим час. Если нет — пишем токен руками, как в прошлой главе.

Важно

Это проверяют в первые минуты модуля, а не когда контракт наполовину написан под библиотеку. Переписывание с библиотеки на рукописный токен за час до сдачи — потерянный модуль.

Типичные ошибки​

are вместо is — та самая строка из контракта задания.

Владелец без адреса в конструкторе. В версии 5 аргумент обязателен.

Старый хук «до перевода». Его больше нет; точка расширения — _update.

Подключение удалённой библиотеки арифметики. Файл не найден: с версии языка 0.8 она не нужна.

decimals() без слова override. Компилятор потребует подтвердить переопределение.

Неполный список после override при нескольких расширениях: нужно перечислить всех родителей.

Вызов _update вместо super._update. Бесконечная рекурсия и сожжённый газ.

Защита, повешенная на transfer. Обходится списанием по разрешению.

Прямой вызов перевода на чужом токене. Может откатиться на разборе ответа или тихо вернуть неправду.

Слова главы​

  • Импорт из пакета — путь, начинающийся с имени пакета, а не с точки.
  • Свои ошибки токена — ERC20InsufficientBalance, ERC20InvalidReceiver, ERC20InsufficientAllowance.
  • Ошибка владельца — OwnableUnauthorizedAccount вместо строкового сообщения.
  • _update — единая точка любого движения баланса в версии 5.
  • super._update — вызов базовой реализации; без super получится рекурсия.
  • Расширения — сжигание, пауза, потолок выпуска, разрешение по подписи.
  • Безопасный перевод — обёртка, которая корректно понимает токены без возврата признака.
  • Совместимость 4.x → 5.x — конструктор владельца, свои ошибки, удалённые библиотеки, один хук вместо двух.

Тренажёр: набрать вслепую​

Цель: скорость ≥ 100 зн/мин, точность ≥ 88%

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

На чемпионате​

Модуль А, 37 баллов. Если библиотека на площадке есть, пять токенов задания пишутся за десять минут: пять наследников с разными именами и точностью. Если нет — работает шаблон из прошлой главы. Решение о том, каким путём идти, принимается в первые минуты, одной проверкой.

Модуль В, 14 баллов. Контракт для аудита наследует библиотечный токен, и это меняет то, что вы ищете: балансов и переводов в файле нет — значит, ошибки не в них, а в надстройке. Смотреть надо на выпуск без прав, на переопределения, повешенные не на ту функцию, на расчёты долей и на проверки.

Отдельный готовый пункт для отчёта по функционалу: перечислить, что именно контракт получает по наследству, — эксперт видит, что участник понимает, где заканчивается его код и начинается чужой.

Отвечено вопросов: 0 из 2
Контракт для аудита наследует библиотечный ERC20. Где искать ошибки?
Что нужно сделать в первые минуты модуля А?
Отвечено верно: 0 из 2

Что дальше​

Обычный токен считает балансы. Следующая глава — про токен, который считает доли: сколько вкладчик внёс относительно остальных и сколько заберёт, когда хранилище заработает. Это прямое требование задания к токену долей, и там же прячется уязвимость, из-за которой первый вкладчик может остаться ни с чем.

Отвечено вопросов: 0 из 1
Почему вызов transfer на чужом токене может откатиться, хотя перевод прошёл?
Отвечено верно: 0 из 1

Углубиться​

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

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

ВидеоСоздаем свой токен ERC 20 в Solidity с помощью библиотеки Openzeppelin. EthereumAlex Klimov
ВидеоОбновление ERC-20 токена с помощью OpenZeppelinnodejsprogrammer
ВидеоКак создать токен ERC20 и ERC721 и запустить на PolygonCRYPTUS
Видео3- Understanding the OpenZeppelin ERC20 Solidity CodeBlockEater
ВидеоSolidity Tutorial | Creating an ERC-20 Token in Solidity with OpenZeppelin (for Techmaker Studio

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.