SafeERC20 в Solidity: работа с чужим токеном
CreditInbox из предыдущего урока получает логический ответ transferFrom. Для нашего известного LibraryCredit это понятный маршрут. Однако не все используемые токены возвращают данные одинаково: встречаются false, откат и отсутствие обычного bool. SafeERC20 помогает правильно обработать некоторые такие формы ответа.
Название safe не означает, что произвольный токен стал честным или что его экономика совпала с нашим упражнением. Разберём это ограничение через новый ExactCreditInbox. Он принимает только точное количество по выбранной политике и явно отделяет успешный вызов от результата чтения баланса.
Какие ответы существуют
Высокоуровневый вызов IERC20.transferFrom ожидает форму ABI с bool. Если старый токен успешно выполнил перевод, но не вернул это значение, декодирование может стать проблемой. Напротив, полученный false нельзя молча трактовать как успех.
SafeERC20.safeTransferFrom вызывает операцию по предусмотренному механизму библиотеки. Откат передаётся как отказ. False также приводит к отказу. Для отсутствующего return предусмотрен отдельный маршрут, который включает проверку наличия кода цели. Это поведение закреплённой версии 5.4.0, а не обещание, что любой пустой ответ доказывает перевод.
Если вызов вернул true, но токен изменил баланс иначе, обёртка не угадает причину. Например, комиссия на перевод может уменьшить полученное количество. Если приложение обязано получить точную сумму, оно должно установить соответствующую политику и проверить наблюдаемое изменение.
Полный отдельный приёмник
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract ExactCreditInbox is ReentrancyGuard {
using SafeERC20 for IERC20;
error InvalidToken();
error InvalidAmount();
error UnexpectedReceived();
IERC20 public immutable credit;
event Received(address indexed reader, uint256 amount);
constructor(address credit_) {
if (credit_ == address(0)) revert InvalidToken();
credit = IERC20(credit_);
}
function deposit(uint256 amount) external nonReentrant {
if (amount == 0) revert InvalidAmount();
uint256 beforeBalance = credit.balanceOf(address(this));
credit.safeTransferFrom(msg.sender, address(this), amount);
uint256 afterBalance = credit.balanceOf(address(this));
if (afterBalance < beforeBalance
|| afterBalance - beforeBalance != amount) {
revert UnexpectedReceived();
}
emit Received(msg.sender, amount);
}
}
Перед нами самостоятельная учебная модель без вывода и без обязательств пользователю. Она не является готовым хранилищем реальных средств. Адрес токена фиксируется при создании. Использование using даёт удобную форму credit.safeTransferFrom; оно не меняет сам токен.
NonReentrant относится к входу deposit. Это ограничение повторного входа, а не проверка честности балансного ответа. Даже если обёртка и guard применены правильно, неизвестный код токена остаётся внешним источником, который может сообщать ложные значения.
Ожидаемое точное количество
Пусть перед вызовом у inbox пять миллионов единиц. Reader передаёт ещё два миллиона через allowance. Наш LibraryCredit должен дать семь миллионов. Разность равна amount, событие содержит два миллиона. Если наблюдается меньше, договор точного приёма отклоняет действие.
Проверка afterBalance < beforeBalance нужна, чтобы не трактовать уменьшение как обычную положительную разность. В показанном short-circuit-условии опасное вычитание при таком случае не выполняется. При этом код не пытается поддержать rebasing и другие изменяемые модели: они не входят в выбранную лабораторную политику.
При откате нашей верхней операции её журнал Received не становится успешной записью. Внешнее исполнение, связанное с этим вызовом, рассматривается по общей границе отката. Нельзя выдавать появившийся в памяти промежуточный afterBalance за окончательное принятие.
Политика комиссии
Другой проект мог бы намеренно учитывать только фактически полученную сумму. Тогда его событие и пользовательская бухгалтерия записывали бы measuredAmount, а не исходный amount. Такой договор допускает комиссию, но требует объяснить её и последствия для покупки.
Наш вариант сознательно не допускает недобор. Если курс стоит две отображаемые единицы, приложение не обещает доступ после получения 1,9. Но цену в этом контракте мы вообще не вводили: он демонстрирует приём, а не покупку. Нельзя объявить событие Received полноценной оплатой курса 17 без дополнительной модели заказа.
Также разность баланса не защищает от любого внешнего поведения. Токен может менять баланс других адресов, имеет административное обновление или нестандартное чтение. Выбор доверенного допустимого токена остаётся важным требованием. Точная арифметика не заменяет оценку зависимости.
Разрешение не исчезает
SafeTransferFrom использует существующее allowance. Оно не назначает разрешение от имени reader. Если approved другой экземпляр, операция не становится разрешённой только благодаря обёртке. Сайт по-прежнему показывает сеть, адрес credit и spender равный этому inbox.
После расходования конечный остаток проверяется по реализации токена. Для бесконечного лимита могут действовать отдельные правила. Не нужно говорить «SafeERC20 убрал разрешение» от имени этой библиотеки. Она выполняет вызов и обрабатывает результат, а state allowance находится в токене.
У библиотеки есть другие методы управления разрешением. Их назначение относится к allowance самого вызывающего контракта, а не произвольного читателя. Если inbox вызывает forceApprove, он устанавливает собственное разрешение следующему spender. Это не способ обойти необходимость пользовательского approve.
Ограничение обработчика повторного входа
Guard помогает не войти в deposit повторно через предусмотренный защищённый маршрут во время внешнего взаимодействия. Но система может иметь и другие функции, использующие общий ресурс. Если они не защищены согласованно, одного nonReentrant в красивом методе недостаточно.
В нашем приёмнике таких методов нет, и он не ведёт внутренний баланс обязательств. При добавлении возврата необходимо заново определить состояние, порядок effects и interactions, а также возможности получателя. Это новая работа, а не дописывание harmless кнопки.
Практика сравнения моделей
Сопоставьте CreditInbox и ExactCreditInbox. Первый рассчитывает на обычный bool собственного известного токена. Второй обрабатывает дополнительные формы ответа и требует точной наблюдаемой суммы. Оба ограничены учебным приёмом и не предоставляют готовое финансовое обслуживание.
Поменяйте условие на разрешение недобора и назовите, какое новое значение должно попасть в событие. Затем задайте неизвестный токен, который всегда возвращает true, но ничего не переводит. Проверка разности обнаружит нулевое изменение при честном balanceOf, а при полностью ложном источнике потребуется другой уровень доверия.
Мы получили инструмент нормализации вызова и точную границу его обещаний. Следующий урок изменит способ назначения allowance через подпись, сохранив отдельность перевода. SafeERC20 5.4.0 и ReentrancyGuard описывают использованные механизмы.
При разборе отклонения сохраните различие между TransferFailed, ошибкой самого токена и UnexpectedReceived. Для клиента они означают разные нарушенные условия. Скрытие всех случаев за сообщением «не хватает денег» помешает понять, была ли причина в allowance, несовместимом поведении или выбранной политике точного получения.
Для самостоятельного уточнения: SafeERC20.sol (5.4.0), ReentrancyGuard.sol (5.4.0).
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.