Безопасность Solidity: нестандартное поведение токена
У внешней функции мы разделили успешное исполнение и предметный bool. Для ERC-20 есть ещё один слой: фактически полученное количество. Токен может вернуть true, но удержать комиссию, изменить балансы по собственному правилу или исполнять дополнительный код. Приложение должно знать, какой договор токенного поведения поддерживает.
Для учебной библиотеки введём отдельный MeasuredCreditInbox с записью admitted каждого отправителя. Он принимает только один выбранный токен и разрешает учесть депозит, если баланс inbox увеличился ровно на amount. Это демонстрация измерения, не готовый финансовый сервис: маршрута возврата и политики пользовательского хранения здесь нет.
Точный прирост баланса
// 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 MeasuredCreditInbox is ReentrancyGuard {
using SafeERC20 for IERC20;
error InvalidInput();
error UnexpectedReceipt();
IERC20 public immutable token;
mapping(address => uint256) public admitted;
constructor(address token_) {
if (token_ == address(0)) revert InvalidInput();
token = IERC20(token_);
}
function deposit(uint256 amount) external nonReentrant {
if (amount == 0) revert InvalidInput();
uint256 beforeBalance = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount);
uint256 afterBalance = token.balanceOf(address(this));
if (afterBalance < beforeBalance || afterBalance - beforeBalance != amount)
revert UnexpectedReceipt();
admitted[msg.sender] += amount;
}
}
Это полный собственный исходник со специально узкой политикой. Начальный адрес token является доверенной настройкой. IERC20 не проверяет, соответствует ли выбранный код желаемым свойствам, а SafeERC20 решает форму результата передачи, не подтверждая честность всех его балансов.
Короткое условие сначала исключает уменьшение баланса. Благодаря короткому вычислению || разность не считается при after<before. Поэтому отрицательный предметный прирост классифицируется нашей ошибкой, а не случайным checked-underflow при попытке вычесть. Такое расположение проверок делает причину результата понятной.
Обычный токен
Пусть исходный баланс inbox равен 20, пользователь передаёт 7, а выбранный обычный токен без комиссии увеличивает баланс до 27. Разность равна 7, admitted пользователя увеличивается на 7. Смысл admitted — успешно учтённые депозиты данной модели, не balance ERC-20 пользователя после оплаты gas.
Для transferFrom заранее требуется подходящий allowance на адрес inbox. Подключение кошелька или подпись входа на сайт не назначает это разрешение. Ошибка недостаточного allowance происходит в токенном маршруте, а не доказывает, что пользователь не является редактором CourseCatalog.
Комиссия при передаче
Теперь собственная лабораторная фикстура токена заявляет transfer amount 7, но получатель получает 6. SafeERC20 может успешно принять предусмотренный return, однако наша измерительная проверка обнаружит разницу 6!=7. Deposit делает revert вместе с вложенной передачей, если её эффекты находятся в общей транзакции.
Другой продукт мог бы сознательно учитывать фактически полученные 6. Но тогда admitted, обязательства возврата, события и интерфейс должны использовать этот договор. Нельзя заменить exact check на «любой неотрицательный прирост» и продолжить обещать пользователю семь учтённых единиц.
Политика exact также не является универсальной поддержкой всех существующих ERC-20. Она намеренно исключает часть вариантов поведения. Лучше назвать ограничение явно, чем объявить полную совместимость и обнаружить её отсутствие на пользовательском депозите.
Балансы могут иметь другое правило
Rebasing-модель способна изменить наблюдаемый баланс независимо от простой передачи выбранного amount. Даже если одна deposit дала ожидаемую разность, будущий баланс обеспечения может измениться. Наш admitted не пересчитывается сам по себе вслед за внешним токеном.
Поэтому разовая проверка delta не доказывает долговременный инвариант обеспечения. Если сервис хранит обязательства, нужно определить поддерживаемый токен, его обновляемость, комиссии и изменения единиц. Для образовательного примера достаточно показать предел, не представлять его как законченную кастодиальную архитектуру.
Злонамеренная зависимость может возвращать выдуманный balanceOf. Тогда измерение сравнит две неправдивые величины и формально пройдёт. Проверка является полезной частью договора с известным токеном, а не независимым доказательством поведения произвольного кода по адресу.
Внешний код и повторный вход
safeTransferFrom обращается к контракту токена. Даже когда типовой ERC-20 не имеет стандартного callback получателя, произвольная реализация может вызывать другой код по собственным правилам. Поэтому модель взаимодействия определяется фактической зависимостью, а не названием IERC20.
NonReentrant ограничивает повторный вход в deposit данного inbox. Если позже добавится другая функция изменения admitted, она войдёт в тот же анализ общего ресурса. Наследование guard не автоматически закрывает все будущие методы, как мы уже увидели в предыдущей главе.
balanceOf является view интерфейса и обычно вызывается с соответствующей статической границей. Это не основание считать returned number правдивым. Даже когда запись состояния запрещена в таком пути, зависимость может читать и сообщать нежелательный результат. Честность данных и запрет изменения — разные свойства.
Десятичный масштаб
Amount, beforeBalance и afterBalance выражены в базовых целых единицах токена. Decimals используется клиентом для отображения, но контракт в этом расчёте не делит amount на 10ⁿ. Если интерфейс передаст семь вместо семи миллионов для токена с шестью decimals, ledger учтёт другую сумму без нарушения собственных типов.
Поэтому спецификация формы фиксирует способ разбора десятичной строки и преобразование в целое. Число с дробью не передаётся в uint256 напрямую. Следующие главы разберут входные пределы и округление, которые так же не исправляются одним успешным transfer.
Подготовка сценариев
Для будущей локальной проверки выберите собственные модели обычного, false-returning, fee-on-transfer и некорректного balance токена. Укажите, какой результат ожидает SafeERC20 и какой наша delta-политика. Не нужны чужие экземпляры или реальные депозиты.
В самостоятельном разборе задайте before=20 и три after: 27,26,19. Объясните outcome deposit(7), состояние admitted и rollback. Затем оставьте after=27, но разрешите будущий rebase до 10 и сформулируйте, какое долгосрочное обещание наш код не обеспечивает. Все результаты здесь ожидаемые, без выполненного исполнения.
Если один пользователь сначала имеет admitted=5, а новый допустимый deposit добавляет 7, итог становится 12. Третья отклонённая передача не должна увеличивать его до 19. Сравнивайте последовательность успешных предметных учётов с фактическим балансом, а не только последнее событие ERC20. Это помогает увидеть, какой слой ошибочно записал обещание при неподходящем внешнем результате.
Для самостоятельного уточнения: SafeERC20.sol (5.4.0), eip-20, ReentrancyGuard.sol (5.4.0).
Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.