Коллекция нестандартных ERC20-реализаций, нарушающих стандарт: токены без возврата значения из transfer(), с комиссией при переводе, с blocklist, pausable, reentrant, с изменяемым балансом. Незаменима для аудиторов DeFi-протоколов.
git clone https://github.com/d-xo/weird-erc20 cd weird-erc20 && forge install # Просмотр всех категорий weird токенов: ls src/ # TransferFee.sol, BlockList.sol, Pausable.sol, # ReentrantToken.sol, ReturnsFalse.sol, ... # Тестирование протокола против weird токенов forge test -vvv
Этот репозиторий содержит минимальные примеры реализации токенов ERC20 на языке Solidity, поведение которых может быть неожиданным или непредсказуемым. Все токены в этом репозитории основаны на реальных токенах, многие из которых ранее использовались для эксплуатации систем смарт-контрактов. Предполагается, что эти примеры реализаций будут полезны разработчикам и аудиторам.
Спецификация ERC20 настолько свободно определена, что сводится лишь к объявлению интерфейса, и даже несколько семантических требований, которые она накладывает, систематически нарушаются разработчиками токенов в реальном мире.
Это делает создание смарт-контрактов, взаимодействующих напрямую с токенами ERC20, сложной задачей, мягко говоря. Разработчикам смарт-контрактов, как правило, следует по умолчанию придерживаться следующих шаблонов при необходимости взаимодействия с внешним кодом:
В некоторых случаях вышеуказанные шаблоны непрактичны (например, в случае безразрешительного AMM, поддержание ончейн-списка разрешений потребует введения централизованного контроля или сложной системы управления). В таких случаях разработчикам следует проявлять большую осторожность, выполняя эти взаимодействия в highly defensive манере. Следует отметить, что даже если ончейн-список разрешений невозможен, внесписочный список в официальном пользовательском интерфейсе также может защитить неподготовленных пользователей от токенов, нарушающих ожидания контракта, при сохранении бесконтрактного уровня бесконтрольности.
Наконец, если вы создаете токен, настоятельно рекомендуется избегать следующих видов поведения.
Дополнительные ресурсы
Некоторые токены позволяют реентрантные вызовы при передаче (например, токены ERC777).
Это использовалось в реальном мире в нескольких случаях (например, пул imBTC в Uniswap был опустошен, lendf.me был опустошен).
пример: Reentrant.sol
Некоторые токены не возвращают bool (например, USDT, BNB, OMG) в методах ERC20. Смотрите здесь полный (хоть и несколько устаревший) список.
Некоторые токены (например, BNB) могут возвращать bool для некоторых методов, но не делать этого для других. Это привело к застреванию токенов BNB в Uniswap v1 (подробности).
Некоторые особенно патологические токены (например, Tether Gold) объявляют возврат bool, но затем возвращают false, даже если передача была успешной (код).
Хорошая абстракция безопасной передачи (пример) может в некоторой степени помочь, но учтите, что наличие Tether Gold делает невозможным правильную обработку возвращаемых значений для всех токенов.
Представлены два примера токенов:
MissingReturns: не возвращает bool ни для какой операции ERC20.ReturnsFalse: объявляет возврат bool, но затем возвращает false для каждой операции ERC20.пример: MissingReturns.sol пример: ReturnsFalse.sol
Некоторые токены берут комиссию за передачу (например, STA, PAXG), некоторые в настоящее время не берут комиссию, но могут сделать это в будущем (например, USDT, USDC).
Комиссия за передачу STA была использована для вывода $500k из нескольких пулов Balancer (подробнее).
пример: TransferFee.sol
Некоторые токены могут вносить произвольные изменения в баланс вне передач (например, токены с rebasing в стиле Ampleforth, airdrops токенов управления в стиле Compound, mintable/burnable токены).
Некоторые системы смарт-контрактов кэшируют балансы токенов (например, Balancer, Uniswap-V2), и произвольные модификации базовых балансов могут означать, что контракт работает с устаревшей информацией.
В случае Ampleforth, некоторые пулы Balancer и Uniswap имеют специальную обработку, чтобы гарантировать, что кэшированные балансы пула обновляются атомарно как часть процедуры rebasing (подробности).
пример: TODO: реализовать rebasing токен
Некоторые токены (например, USDC, USDT) обновляемы, что позволяет владельцам токенов вносить произвольные изменения в логику токена в любой момент времени.
Изменение семантики токена может сломать любой смарт-контракт, зависящий от прошлого поведения.
Разработчикам, интегрирующим обновляемые токены, следует рассмотреть возможность добавления логики, которая будет замораживать взаимодействие с соответствующим токеном в случае обнаружения обновления (например, адаптер TUSD, используемый MakerDAO).
пример: Upgradable.sol
Некоторые токены (например, DAI) позволяют так называемый "flash minting", который позволяет чеканить токены только на время одной транзакции при условии их возврата в контракт токена к концу транзакции.
Это похоже на flash loan, но не требует, чтобы токены, которые нужно занять, существовали до начала транзакции. Токен, который может быть отчеканен через flash minting, потенциально может иметь общую эмиссию, равную uint256.
Документация по модулю flash mint MakerDAO доступна здесь.
Некоторые токены (например, USDC, USDT) имеют управляющий администратором контракта список блокировки адресов. Если адрес заблокирован, передача на этот адрес и с него запрещена.
Злонамеренные или скомпрометированные владельцы токенов могут заморозить средства в контракте, добавив адрес контракта в список блокировки. Это может быть результатом регуляторных действий против самого контракта, против отдельного пользователя контракта (например, LP в Uniswap), или также может быть частью попытки шантажа пользователей заблокированного контракта.
пример: BlockList.sol
Некоторые токены могут быть приостановлены администратором (например, BNB, ZIL).
Аналогично проблеме со списком блокировки выше, функция приостановки под контролем администратора подвергает пользователей токена риску со стороны злонамеренного или скомпрометированного владельца токена.
пример: Pausable.sol
Некоторые токены (например, USDT, KNC) не позволяют одобрять сумму M > 0, когда уже одобрена существующая сумма N > 0. Это сделано для защиты от вектора атаки ERC20, описанного здесь.
Этот PR показывает некоторые проблемы, возникающие в реальном мире из-за этой проблемы.
пример: Approval.sol
Некоторые токены (например, OpenZeppelin) будут делать revert, если пытаться одобрить нулевой адрес для траты токенов (т.е. при вызове approve(address(0), amt)).
Интеграторам может потребоваться добавить специальные случаи для обработки этой логики при работе с таким токеном.
пример: ApprovalToZeroAddress.sol
Некоторые токены (например, BNB) делают revert при одобрении суммы с нулевым значением (т.е. при вызове approve(address, 0)).
Интеграторам может потребоваться добавить специальные случаи для обработки этой логики при работе с таким токеном.
пример: ApprovalWithZeroValue.sol
Некоторые токены (например, LEND) делают revert при передаче суммы с нулевым значением.
пример: RevertZero.sol
Некоторые проксированные токены имеют несколько адресов.
В качестве примера рассмотрите следующий фрагмент. Функция rescueFunds предназначена для того, чтобы позволить владельцу контракта вернуть токены, не относящиеся к пулу, которые случайно были отправлены на контракт. Однако она предполагает, что у токена только один адрес, и поэтому позволит владельцу украсть все средства в пуле.
mapping isPoolToken(address => bool);
constructor(address tokenA, address tokenB) public {
isPoolToken[tokenA] = true;
isPoolToken[tokenB] = true;
}
function rescueFunds(address token, uint amount) external nonReentrant onlyOwner {
require(!isPoolToken[token], "access denied");
token.transfer(msg.sender, amount);
}
пример: Proxied.sol
Некоторые токены имеют низкую точность (например, USDC имеет 6 знаков после запятой). Еще более крайний случай — некоторые токены, такие как Gemini USD, имеют всего 2 знака после запятой.
Это может привести к большей, чем ожидается, потере точности.
пример: LowDecimals.sol
Некоторые токены имеют более 18 знаков после запятой (например, YAM-V2 имеет 24).
Это может вызвать непредвиденные откаты из-за переполнения, создавая риск нарушения работоспособности контракта.
пример: HighDecimals.sol
transferFrom при src == msg.senderНекоторые реализации токенов (например, DSToken) не пытаются уменьшить allowance (разрешение) вызывающего, если отправитель совпадает с вызывающим. Это придает transferFrom ту же семантику, что и transfer, в данном случае. Другие реализации (например, OpenZeppelin, Uniswap-v2) пытаются уменьшить allowance вызывающего от отправителя в transferFrom, даже если вызывающий и отправитель — это один и тот же адрес, придавая transfer(dst, amt) и transferFrom(address(this), dst, amt) разную семантику в этом случае.
примеры:
Предоставлены примеры обеих семантик:
stringНекоторые токены (например,
MKR) имеют поля метаданных (name / symbol), закодированные как bytes32 вместо string, предписанного спецификацией ERC20.
Это может вызвать проблемы при попытке чтения метаданных этих токенов.
пример: Bytes32Metadata.sol
Некоторые токены (например, openzeppelin) откатываются при попытке перевода на address(0).
Это может нарушить системы, которые рассчитывают на возможность сжигания токенов переводом их на address(0).
пример: RevertToZero.sol
Некоторые токены не откатываются при ошибке, а вместо этого возвращают false (например,
ZRX, EURS).
Хотя технически это соответствует стандарту ERC20, это противоречит общепринятым практикам написания кода на solidity и может быть упущено разработчиками, которые забывают оборачивать вызовы transfer в require.
пример: NoRevert.sol
Некоторые токены (например, UNI, COMP) откатываются, если значение, переданное в approve или transfer, больше uint96.
Оба вышеуказанных токена имеют специальную логику в approve, которая устанавливает allowance в type(uint96).max, если сумма одобрения равна uint256(-1), что может вызвать проблемы в системах, которые ожидают, что значение, переданное в approve, будет отражено в отображении allowances.
пример: Uint96.sol
Было обнаружено, что некоторые вредоносные токены включают вредоносный javascript в свой атрибут name, позволяя злоумышленникам извлекать приватные ключи у пользователей, которые выбирают взаимодействовать с этими токенами через уязвимые фронтенды.
Это использовалось для эксплуатации пользователей etherdelta (ссылка).
Некоторые токены (DAI, RAI, GLM, STAKE, CHAI, HAKKA, USDFL, HNY) имеют реализацию permit(), которая не следует EIP2612. Токены, которые не поддерживают permit, могут не откатываться, что может привести к выполнению последующих строк кода в непредвиденных сценариях. Permit2 от Uniswap может предоставить более совместимую альтернативу.
пример: DaiPermit.sol
amountНекоторые токены (например, cUSDCv3) содержат специальный случай для amount == type(uint256).max в своих функциях перевода, который приводит к переводу только баланса пользователя.
Это может вызвать проблемы в системах, которые переводят пользовательскую сумму amount на свой контракт, а затем начисляют пользователю такое же значение в хранилище (например, системы типа Vault), не проверяя фактическую переведенную сумму.
Некоторые блокчейны имеют ERC-20 представление своей нативной валюты, чтобы избежать необходимости в отдельном контракте-обертке. При обзоре протоколов, которые взаимодействуют как с нативной валютой, так и с ERC-20 токенами, предназначенными для развертывания в этих конкретных блокчейнах, всегда учитывайте этот сценарий и оценивайте правильность функционирования протокола для защиты от проблем с двойным расходованием или других уязвимостей, связанных с этой особенностью.
Примеры таких блокчейнов включают:
* Celo с CELO (адрес 0x471EcE3750Da237f93B8E339c536989b8978a438).
* Polygon с POL (адрес 0x0000000000000000000000000000000000001010).
* zkSync Era с ETH (адрес 0x000000000000000000000000000000000000800A).
Это привело к критической уязвимости в Uniswap V4 в сети CELO.