Notional Finance был взломан на сумму 1,73 млн долларов из-за уязвимости переполнения целых чисел

iconMetaEra
Поделиться
AI summary iconСводка
1,73 миллиона долларов США были похищены у Notional Finance 4 сентября 2026 года, когда злоумышленники воспользовались переполнением целых чисел в контракте Escrow. Уязвимость в функции ExchangeRate._convertToETH позволила создавать позиции fCash без достаточного обеспечения. Инцидент подчеркивает важность высокого соотношения риска и вознаграждения в стратегиях DeFi. Трейдерам, использующим технический анализ для криптовалют, следует оставаться бдительными, поскольку такие уязвимости могут быстро изменить рыночную динамику.

Автор: Девять девять

Редактировать: 77

Контекст

4 сентября 2026 года известная децентрализованная платформа кредитования Notional Finance подверглась атаке, в результате которой были утеряны примерно 1,73 миллиона долларов США. Ниже приводится подробный анализ события командой безопасности SlowMist:

Предварительные знания

В Notional Finance V1 fCash можно понимать как денежное требование с указанной датой погашения. Получатель наличных получает платеж в срок погашения, а плательщик наличных производит платеж в срок погашения. Протокол фиксирует обе позиции в Portfolio и оценивает финансовую устойчивость аккаунта на предмет возможности открытия позиции с помощью проверки свободного залога.

В ERC1155Trade функция safeTransferFrom выполняет функцию учета активов. Когда передаваемый тип актива относится к получателю денежных средств, контракт вызывает Portfolios.mintfCashPair, одновременно фиксируя обязательство для плательщика и равнозначное требование для получателя. Этот вызов выглядит как перевод ERC1155, но на самом деле представляет собой парное создание fCash.

После записи новой позиции в Portfolio необходимо выполнить расчет и проверку свободного обеспечения. Смарт-контракт RiskFramework сначала суммирует fCash-обязательства плательщика в виде знакового int256, а затем конвертирует остатки по каждой валюте в ETH для итогового суммирования. Отрицательные значения обозначают денежные обязательства к оплате, положительные — наличные средства или кредитные права на счете. Результат этого расчета определяет, разрешена ли сделка.

После истечения срока позиции Portfolio вызывает функцию portfolioSettleCash в Escrow, чтобы преобразовать истекший fCash в денежный баланс. Затем счет использует функцию вывода для извлечения соответствующих активов DAI или USDC из Escrow.

Основная причина

Основная уязвимость этой атаки заключается в ExchangeRate._convertToETH, используемой в контракте Escrow, где один фрагмент кода напрямую преобразует абсолютное значение знакового целого числа в uint128.

Злоумышленник сначала открыл две позиции с долгом у одного и того же плательщика, суммы которых составляют 1 и uint128.max. Сумма этих двух долгов точно равна 2^128. Поскольку платежи плательщика в расчете риска учитываются как отрицательные значения, ключевой параметр, передаваемый в convertBalancesToETH Escrow, равен -2^128.

Однако версия Solidity, используемая в этом контракте, — 0.6.x, поэтому абсолютное значение 2^128 после приведения к uint128 переполняется и становится 0, из-за чего эта обязательство не попадает в результат, выраженный в ETH.

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

Анализ шагов атаки

1. В предварительной транзакции (0xe1589a19…d60a) злоумышленник сначала создал несколько вспомогательных смарт-контрактов и вызвал функцию setApprovalForAll смарт-контракта ERC1155Trade для предоставления разрешений вспомогательным контрактам; кроме того, он заранее запросил даты погашения двух CashMarket и рассчитал значения трех параметров AssetId, необходимые для последующих операций.

2. Затем злоумышленник вызывает функцию safeTransferFrom контракта ERC1155Trade для создания пары fCash номиналом 1, соответствующей облигационному активу с cashGroupId = 2 и временной меткой истечения 1788480000 (4 сентября 2026 года, 8:00). При этом вызывается внутренняя функция _upsertAsset для обновления обязательств и ожидаемого дохода для адреса from (злоумышленный контракт) и адреса to (контракт-получатель 1).

После завершения чеканки пары Portfolios немедленно проверяют свободное залоговое обеспечение атакующего контракта, и поскольку количество первой чеканки слишком мало, при текущем обменном курсе и точности оно округляется до нуля при переводе в ETH, поэтому первая проверка проходит успешно.

3. Затем злоумышленник снова вызвал функцию safeTransferFrom контракта ERC1155Trade для создания пары fCash с суммой, равной uint128.max; при этом cashGroupId соответствующего долгового актива был равен 2, но временная метка погашения составляла 1796256000 (3 декабря 2026 года, 8:00).

Здесь есть одна деталь: атакующий создал облигационные активы с разными датами погашения для двух разных адресов. Это связано с тем, что при обновлении обязательств для адреса from, если облигационные активы были одинаковыми, они бы просто суммировались, что привело бы к переполнению при сложении и откату (контракт использует библиотеку SafeMath).

4. После обновления обязательств и ожидаемого дохода для адреса from (атакующий смарт-контракт) и адреса to (приемный смарт-контракт 2), функция mintfCashPair вызывает функцию freeCollateral для расчета чистой позиции залога по адресу from и проверки, что она больше или равна нулю, что указывает на здоровое состояние.

Сначала в функции getRequirement контракта RiskFramework будет произведено сложение общей суммы обязательств по двум чеканкам адреса from:

В результате получается -2^128, после чего вызывается функция convertBalancesToETH контракта Escrow для конвертации этого значения в ETH, а функция convertBalancesToETH, в свою очередь, вызывает функцию _convertToETH библиотеки ExchangeRate для вычислений:

При переходе в функцию _convertToETH можно заметить, что сначала берется абсолютное значение balance, а затем с помощью uint128 происходит преобразование 256-битного значения в 128-битное. Суммарная задолженность по параметру from равна -2^128; после взятия абсолютного значения 2^128 при принудительном преобразовании через uint128() приводит к переполнению и обнулению. Это означает, что протокол ошибочно считает, что свободная позиция залога адреса from равна 0 и является здоровой, что позволяет пройти финальную проверку в функции mintfCashPair.

5. Затем контракт 2 получил контракт и разделил свою позицию между двумя вспомогательными контрактами; оба safeTransferFrom по-прежнему направляются в функцию mintfCashPair. Однако, поскольку теперь плательщиком является контракт 2, полученный на втором этапе майнинга, который владеет огромной позицией ожидаемого дохода, полученной на предыдущем этапе, расчет риска рассматривает его как положительное требование, и поэтому он проходит проверку залога. В результате два вспомогательных контракта получают receiver fCash, который можно обменять на наличные после истечения срока.

6. Затем злоумышленник инициировал вторую официальную прибыльную сделку, в которой были закрыты соответствующие позиции по вспомогательным контрактам предыдущей сделки, увеличены денежные остатки соответствующих активов, после чего был вызван функция withdraw контракта Escrow для вывода активов и получения прибыли.

Сводка

Ключевым моментом этой атаки стало то, что злоумышленник сначала использовал два отдельных положения, чтобы сумма долгов плательщика составила 2^128, а затем заставил Escrow перевести этот долг в ETH. Используя уязвимость переполнения при преобразовании типов, он обрезал результат до нуля, из-за чего проверки риска прошли без препятствий.

Команда безопасности SlowMist рекомендует проектным командам обязательно выполнять проверку диапазона перед преобразованием типов: результаты, превышающие type(uint128).max, должны отклоняться напрямую. В то же время все границы, связанные с подписанными суммами, масштабированием точности и номиналом активов, должны проходить тестирование на предельные значения, особенно для входных данных: uint128.max, uint128.max + 1 и абсолютные значения отрицательных чисел. Рекомендуется использовать или ориентироваться на библиотеку SafeCast от OpenZeppelin для обработки преобразований.

Отказ от ответственности: Информация на этой странице может быть получена от третьих лиц и не обязательно отражает взгляды или мнения KuCoin. Данный контент предоставляется исключительно в общих информационных целях, без каких-либо заверений или гарантий, а также не может быть истолкован как финансовый или инвестиционный совет. KuCoin не несет ответственности за ошибки или упущения, а также за любые результаты, полученные в результате использования этой информации. Инвестиции в цифровые активы могут быть рискованными. Пожалуйста, тщательно оценивайте риски, связанные с продуктом, и свою устойчивость к риску, исходя из собственных финансовых обстоятельств. Для получения более подробной информации, пожалуйста, ознакомьтесь с нашими Условиями использования и Уведомлением о риске.