Взлом Notional Finance на $1,73 млн

Введение
Notional Finance — это протокол для фиксированного кредитования в DeFi. Пользователи могут давать и брать в долг криптовалюту под заранее известную процентную ставку.
Escrow-контракт — это смарт-контракт-хранилище, в котором заморожены средства пользователей до выполнения определённых условий. В Notional он хранит реальные активы, которые пользователи могут получить при погашении долговых обязательств.
fCash — это внутренние долговые обязательства протокола Notional. Положительная fCash-позиция означает право получить деньги в будущем. Отрицательная — долг, который нужно погасить.
Ход инцидента
1️⃣ В старой версии протокола функция перевода fCash-позиций направляла вызов в функцию
mintfCashPair(). Эта функция одновременно создавала долговую позицию у одного адреса (payer — заёмщика) и положительную fCash-позицию у другого адреса (receiver — получателя права требования).2️⃣ После создания позиций контракт проверял только обеспеченность заёмщика. Если у него достаточно залога — операция разрешалась.
3️⃣ Злоумышленник создал для одного и того же заёмщика две отдельные долговые позиции. Сначала — с номиналом 1. Затем — с номиналом 2^128 − 1 (это максимальное число, которое помещается в специальный тип данных).
4️⃣ При проверке обеспеченности система сложила оба долга: 1 + (2^128 − 1) = 2^128.
5️⃣ В коде проверки использовалось преобразование к типу uint128 — это формат хранения чисел, который вмещает только значения от 0 до 2^128 − 1. Когда система попыталась записать число 2^128 в этот формат, произошло сужающее приведение: старший бит отбросился, и вместо огромного долга система увидела ноль.
6️⃣ Проверка обеспеченности посчитала, что у заёмщика нет долга, и пропустила операцию. Сам долг при этом никуда не делся, но система его больше не учитывала.
7️⃣ На адресах-получателях остались крупные положительные fCash-позиции. Злоумышленник использовал их для получения реальных активов из Escrow-контракта. Было выведено 69 242 DAI и 1 658 423 USDC, всего примерно $1,73 млн.
Почему это стало возможным
Проблема заключалась в функции
_convertToETH(), которая проверяла обеспеченность. В ней использовалось преобразование:uint128 absBalance = uint128(balance.abs());Тип uint128 может хранить числа только до 2^128 − 1. Каждый из двух созданных долгов по отдельности помещался в этот предел, поэтому контракт принимал их без проблем. Но после сложения совокупный долг стал равен ровно 2^128.
При сужающем преобразовании в Solidity старшие биты числа отбрасываются. Число 2^128 в двоичном виде — это единица, за которой следует 128 нулей. При отбрасывании старшего бита остаётся 128 нулей — то есть 0.
Детали расследования: движение средств
Похищенные активы поступили на адрес злоумышленника:
Оттуда средства в том же виде были переведены на адрес:
Далее оба токена (DAI и USDC) были обменяны на ETH и отправлены на адрес:
После этого вся сумма в виде ETH была отправлена на Tornado Cash — сервис, который разбивает и смешивает транзакции, чтобы скрыть их источник.
Остаток в размере 0,0112674 ETH был переведён на нелицензированный обменник CCE Cash.
Заключение
Инцидент в Notional Finance показал, что математические особенности компьютерных систем — в данном случае ограничения на размер чисел и правила их преобразования — могут стать инструментом для кражи. Создание двух отдельных позиций, которые по отдельности выглядели безопасно, при сложении привели к переполнению формата и обнулению долга. Отсутствие проверки на критическое значение перед преобразованием сделало всю систему обеспеченности бесполезной. Команда «КоинКит» продолжает анализ связанных адресов и методов отмывания средств.


