Back to Blog

Потеряно ~23 млн $: эксплойты Cosmos EVM и Moonwell | BlockSec Weekly

Code Auditing
3 сентября 2026 г.
25 min read
Key Insights
  • Пять инцидентов за эту неделю привели к убыткам на сумму примерно 22,7 млн долларов США в сетях Ethereum, Solana, Base, Cronos и ещё в шести цепочках Cosmos EVM, таких как TAC.

  • Общий код превратил два инцидента в события, затронувшие несколько протоколов. Уязвимость в обработке балансов Cosmos EVM была использована на шести цепочках Cosmos EVM в течение той же недели, включая TAC Chain; устаревший контракт карты Rain затронул Avici, Tria и другие карточные программы. Один дефект в общей инфраструктуре может распространиться на все уязвимые downstream-компоненты, использующие её.

  • Манипуляция ценой была самым распространённым типом атак, лежащим за двумя из пяти инцидентов и значительной частью убытков за неделю. Эксплойты Moonwell и Tectonic имели одинаковую форму на рынках в стиле Compound: каждый из них искажал как цену оракула, так и обменный курс токена-квитанции рынка, после чего осуществлялось заимствование под завышенную стоимость обеспечения.

За отчётный период (2026/08/22 - 2026/08/30) мы зафиксировали 5 инцидентов безопасности с общим оценочным ущербом около $22.7M.

Дата Инцидент Тип Оценочный ущерб
2026/08/20* Серия эксплойтов Cosmos EVM (MANTRA, TAC, KiiChain, +3) Арифметическое переполнение/антипереполнение ~$5.7M*
2026/08/27 Moonwell Манипуляция ценой ~$9.1M
2026/08/28 Ajna Некорректная бизнес-логика ~$775K
2026/08/28 Серия эксплойтов Rain Card Contract (Avici, Tria и другие) Обход проверки подписи ~$1.1M†
2026/08/30 Tectonic Манипуляция ценой ~$6M‡

*Серия эксплойтов Cosmos EVM началась 20.08 (MANTRA), до начала отчётного периода этой недели, и не была освещена в прошлом отчёте; она включена сюда для полноты картины. Указанные ~$5.7M — это то, что атакующие реализовали по всем шести затронутым сетям (~$2.87M через DEX и ~$2.85M через централизованные биржи, с тех пор заморожено) по ценам на 19 августа, согласно официальному пост-мортему Cosmos. Номинальные потери были выше там, где известна сумма токенов (TAC ~3 млрд TAC, ~$7.5M; MANTRA 720.9 млн токенов, ~$3.6M; KiiChain 148.3 млн KII), но большинство токенов остались непроданными, замороженными или восстановимыми в цепочке.

†~$1.1M — это оценочная совокупная сумма по всем программам, поддерживаемым Rain, затронутым через общий контракт; Avici и Tria — два крупнейших случая, раскрывшие соответственно ~$500,859 (1,685 пользователей) и ~$431,945 (636 пользователей).

‡~$6M — это реализованный ущерб, переведённый через мост в Ethereum до отката. Оценки общей суммы, выведенной из системы, варьируются от ~$74M (отслежено в кошельках атакующего) до ~$119.5M (валовой отток с рынка); основная часть осталась в сети Cronos и была аннулирована, когда валидаторы откатили сеть до состояния до эксплойта. Ни Tectonic, ни Cronos пока не подтвердили окончательную сумму ущерба.

Лучший аудитор безопасности для Web3

Проверьте архитектуру, код и бизнес-логику перед запуском

Главное событие недели: Серия эксплойтов Cosmos EVM (прослежено в сети TAC)

Эта серия была выбрана из-за того, что она демонстрирует в отношении раскрытия информации об общей инфраструктуре: поскольку уязвимость была ошибочно расценена как не представляющая серьёзной угрозы безопасности, её исправление было выпущено в виде тихого публичного патча, а не через скоординированное частное распространение, а затем сторонний форк открыто описал путь эксплуатации, раскрыв все непропатченные цепочки, всё ещё использующие этот модуль.

Между 20.08.2026 и 25.08.2026 атакующие использовали единую цепочку эксплуатации, объединяющую две уязвимости в общем модуле cosmos/evm в шести сетях Cosmos EVM. Обе ошибки находились в коде, который сверяет состояние EVM с реестром Cosmos x/bank: антипереполнение баланса и соответствующее переполнение, связанные в одной транзакции, нейтральной по отношению к объёму эмиссии.

Согласно официальному пост-мортему Cosmos, об ошибке было сообщено через программу баг-баунти в апреле, и изначально она была ошибочно расценена как не угрожающая реальным средствам, поэтому она прошла через процесс тихого публичного патча и была выпущена в версиях v0.6.2 и v0.7.2 19 августа. 20 августа сторонний форк публично описал путь эксплуатации, и уже через несколько часов начались первые выводы средств: сначала пострадала MANTRA (20.08), затем TAC и KiiChain (22.08) [1][2]. По всем шести цепочкам атакующие реализовали примерно $5.7M по ценам на 19 августа (около $2.87M продано через DEX и ~$2.85M через централизованные биржи, с тех пор заморожено) [1].

Этот отчёт подробно анализирует сеть TAC Chain в качестве разбираемого примера; это была наиболее пострадавшая цепочка в серии, с номинальным убытком пула стейкинга около $7.5M [3].

Предпосылки

TAC Chain работает как с Cosmos SDK, так и с EVM. Адрес tac1... и адрес 0x... имеют одни и те же лежащие в основе 20 байт, поэтому один и тот же адрес может быть сначала создан как аккаунт вестинга Cosmos, а затем по нему может быть развёрнут EVM-контракт. Это не два отдельных аккаунта: один и тот же адрес одновременно несёт в себе состояние вестинга и код контракта. Оба работают параллельно, и TAC предоставляет вызывающим сторонам EVM доступ к нативным действиям Cosmos, таким как стейкинг, через прекомпилированные контракты по фиксированным адресам, поэтому EVM-контракт может вызывать их так же, как и любой другой вызов.

Общий баланс Bank аккаунта вестинга включает заблокированные токены. Заблокированную часть нельзя передать до её вестинга, в то время как доступный (spendable) баланс — это то, что остаётся после вычитания заблокированной суммы из общего баланса. Когда EVM загружает аккаунт, он инициализирует баланс StateDB из доступного баланса, и переводы контракта во время выполнения оперируют именно этим балансом.

В конце транзакции изменения баланса в StateDB фиксируются обратно в x/bank, авторитетный реестр, и только то, что зафиксировано там, является реальным, передаваемым TAC. TAC работает на линии v0.7.x, которая записывает баланс EVM напрямую в x/bank, но только если значение переживает преобразование из uint256 в int256, поэтому баланс, близкий к 2^256, вообще не может быть зафиксирован.

Заблокированные токены нельзя передать, но их всё ещё можно делегировать для стейкинга. Делегирование проверяет общий баланс Bank, который включает заблокированные токены, и затем сохраняет ограничение вестинга через DelegatedVesting. Для полностью заблокированного аккаунта с total=1, делегирование этой единицы снижает общий баланс Bank до 0 и обновляет DelegatedVesting, при этом доступный баланс корректно остаётся 0 как до, так и после.

Анализ уязвимости

Проблемный компонент — синхронизация баланса в общем обработчике cosmos/evm, который запускается при каждом стейтфул-вызове прекомпайла стейкинга, доступного через прекомпайл стейкинга по адресу 0x0000...0800 и реализованного в [4]. Он сверяет балансы EVM с реестром Cosmos через непроверяемую арифметику uint256, и это открывает два взаимодополняющих дефекта: антипереполнение баланса на пути обратной записи стейкинга и соответствующее переполнение на пути обычного сложения. Ни один из них не опасен сам по себе; риск возникает от их сочетания на общем арифметическом пути.

Первый дефект — антипереполнение при обратной записи баланса. Когда выполняется стейтфул-вызов прекомпайла стейкинга, нативное действие Cosmos выполняется между хуками BeforeBalanceChange и AfterBalanceChange, и AfterBalanceChange отвечает за отражение изменения нативного баланса обратно в StateDB EVM.

Делегирование проверяется по общему балансу Bank, поэтому полностью заблокированный аккаунт может пройти проверку делегирования, при этом его доступный баланс остаётся 0. Нативное делегирование вычитает делегированную сумму из общего баланса Bank и генерирует событие coin_spent. Дефект заключается в том, как AfterBalanceChange обрабатывает это событие: вместо того чтобы перезагрузить текущий доступный баланс аккаунта и присвоить его, он воспроизводит сумму из coin_spent как stateDB.SubBalance(spender, amount), вычитая её из существующего баланса EVM.

Поскольку SubBalance выполняет вычитание с непроверяемой арифметикой uint256, любой аккаунт, чей баланс EVM меньше суммы события, подвергается антипереполнению. Для аккаунта с балансом EVM 0 и суммой coin_spent, равной 1, 0 - 1 переходит в MAX_UINT256.

Второй дефект — соответствующее переполнение на пути сложения. Каждый перевод баланса выполняет SubBalance для отправителя и AddBalance для получателя через один и тот же StateDB, поэтому оба направления используют один арифметический путь.

Оба сводятся к одним и тем же примитивам stateObject, где AddBalance и SubBalance вычисляют new(uint256.Int).Add(s.Balance(), amount) и .Sub(s.Balance(), amount) без какой-либо проверки диапазона. Точно так же, как вычитание приводит к антипереполнению ниже 0, достаточно большое сложение приводит к переполнению выше MAX_UINT256 и переходит обратно вниз.

Эти два дефекта взаимодополняют друг друга. Сам по себе, антипереполнение создаёт баланс MAX_UINT256, который бездействует, поскольку значение, близкое к 2^256, не может пережить преобразование из uint256 в int256, требуемое для фиксации в x/bank. Непроверяемое сложение является контрпартией: это единственный путь, который может вернуть такой чрезмерно большой баланс обратно ниже этого предела фиксации.

Анализ атаки

Следующий анализ основан на транзакции 0xae4e9b...da46fc.

  • Шаг 1: В транзакции 0x4da591...df1af7 атакующий развернул фабрику CREATE2, чтобы адрес атакующего контракта 0x5711...c978 можно было вычислить до развёртывания контракта.

  • Шаг 2: В транзакции 95F43742...6A885BA атакующий использовал MsgCreateVestingAccount, чтобы передать и заблокировать одну базовую единицу (1utac) по этому будущему адресу контракта, дав ему общий баланс Bank, который мог пройти проверку делегирования, сохраняя при этом доступный баланс на уровне 0.

  • Шаг 3: В транзакции 0x2400f8...c57c81 атакующий использовал CREATE2 для развёртывания атакующего контракта по тому же адресу 0x5711...c978, сделав его одновременно аккаунтом вестинга Cosmos и EVM-контрактом, чтобы внешний аккаунт мог оплачивать газ, при этом адрес контракта оставался делегатором с spendable=0.
  • Шаг 4: Атакующий контракт делегировал заблокированную базовую единицу через прекомпайл стейкинга. Процесс стейкинга принял делегирование против общего баланса Bank, и последующая обратная запись баланса привела к переходу баланса EVM контракта в MAX_UINT256.

  • Шаг 5: Переполненный баланс MAX_UINT256 не мог быть зафиксирован обратно в реестр Cosmos как есть, поэтому атакующий контракт сначала снизил его до значения, поддающегося фиксации. Он отправил почти весь баланс в bonded_tokens_pool, крупнейший аккаунт цепочки и тот, что хранит весь застейканный TAC, выбрав сумму так, чтобы баланс пула переполнился при сложении и перешёл в 0. Поскольку эта сумма была вычтена из собственного MAX_UINT256 атакующего контракта, у контракта осталось ровно столько, сколько был прежний баланс пула, без создания новой эмиссии. Обнуление пула забирает весь его баланс — максимум, который могло дать это переполнение, поэтому атакующий изначально нацелился именно на крупнейший аккаунт сети.

  • Шаг 6: Затем атакующий контракт перевёл этот баланс, 2,985,651,403.40 TAC, на адрес атакующего.

Наш анализ на уровне транзакций совпадает с официальным пост-мортемом Cosmos, который описывает две связанные уязвимости: антипереполнение выше создаёт аномальный баланс, а переполнение на Шаге 5 анализа атаки превращает его в реальные средства пула [1]. Более ранний пост-мортем KiiChain, опубликованный до этого официального отчёта, утверждал, что были задействованы как минимум три вышестоящих дефекта и что исправлено было только антипереполнение [2]. Независимое освещение в прессе резюмировало то же расхождение в оценке того, сколько вышестоящих дефектов остаётся [5].

Заключение

Первопричиной серии эксплойтов Cosmos EVM было несоответствие в том, как слои EVM и Cosmos учитывали один и тот же баланс, в сочетании с арифметикой, которая никогда не проверялась на выход за границы допустимых значений. Когда два рантайма разделяют один реестр, они должны согласовывать семантику баланса вплоть до каждого отдельного аккаунта, и каждое изменение баланса должно проверяться на переполнение и антипереполнение. Поскольку изъян находился в общем модуле, а не в коде какой-либо одной цепочки, один дефект открыл уязвимость сразу во всех цепочках, использующих этот модуль, что и превратило единственную ошибку в мультицепочечное событие. Помимо кода, этот инцидент — урок в оценке серьёзности. Уязвимость изначально была расценена как не угрожающая реальным средствам, поэтому её исправление вышло в виде тихого публичного патча; к тому моменту, когда эта оценка была скорректирована, патч уже был публичным, а раскрытие пути эксплуатации третьей стороной превратило эту ошибочную оценку в шесть эксплуатированных цепочек. Ошибка в общей инфраструктуре, способная перемещать реальные средства, с самого начала требует частного, скоординированного распространения исправления, а не тихого публичного патча, который может расшифровать кто угодно.

Начните работу с Phalcon Explorer

Погрузитесь в транзакции, чтобы действовать разумно

Попробовать бесплатно

Другие инциденты на этой неделе


Moonwell

28 августа 2026 года (27.08.2026) Moonwell на Base был атакован на сумму около $9.1M путём сочетания инфляции учёта залога с манипуляцией оракульной ценой MAMO, актива с низкой ликвидностью, включённого в его Core Market. Помимо повышения цены MAMO, атакующий перевёл MAMO напрямую в контракт рынка mMAMO без чеканки долей, что повысило обеспечение каждой доли и увеличило стоимость залога сверх изменения цены. Против вдвойне завышенного залога атакующий взял примерно $11.03M валовых займов по cbBTC, WETH, USDC и wstETH, оставив около $9.13M остаточных обязательств после ликвидаций [6][7].

Анализ уязвимости

Core Market Moonwell работает на коде Compound v2, где одна доля рынка (mMAMO) стоит (cash + totalBorrows - totalReserves) / totalSupply. Здесь сочетаются две слабости. Во-первых, лимит эмиссии MAMO проверяет только формальный путь чеканки, поэтому прямой перевод MAMO в контракт mMAMO добавляет средства в кассу рынка без чеканки долей; это повышает вычисляемый обменный курс (exchangeRateStored()) и поднимает стоимость залога каждой существующей доли, полностью обходя лимит. Во-вторых, MAMO был включён в список залоговых активов с коэффициентом залога 50%, несмотря на низкую ликвидность, поэтому его оракульную цену можно сдвинуть скромным капиталом. Поскольку стоимость залога вычисляется как доли, умноженные на обменный курс, умноженные на оракульную цену, обе эти поверхности — и обменный курс, и цена — поддаются манипуляции, а их совместное завышение многократно увеличивает возможности заимствования против гораздо более ликвидных активов.

Анализ атаки

Следующий анализ основан на транзакции 0x09687d...395593e. Операция была изначально запущена с примерно $1.947M (799 ETH, конвертированные в USDC и переведённые через мост в Base); валовой объём покупки MAMO достиг примерно $7.50M, как только заёмные активы были переработаны.

  • Шаг 1: Атакующий формально предоставил MAMO, чтобы отчеканить mMAMO, затем перевёл 53,393,290 MAMO напрямую в контракт mMAMO в двух транзакциях, которые не генерировали событие Mint, повысив вычисляемый обменный курс рынка примерно в 3.68 раза и переоценив доли mMAMO, которые атакующий только что отчеканил, наряду с долями всех остальных держателей.

  • Шаг 2: Атакующий скупал MAMO через пулы DEX, пока ликвидность была низкой, поднимая курс MAMO/USD с примерно $0.0106 до примерно $0.43.

  • Шаг 3: С завышенными и обменным курсом, и ценой, атакующий совершил 18 займов по cbBTC, WETH, USDC и wstETH (примерно $11.03M валовых), затем конвертировал и консолидировал средства, переведя через мост CCTP примерно 8.729M USDC в Ethereum и конвертировав их в примерно 8.728M DAI.

Заключение

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

Ajna

28 августа 2026 года Ajna была атакована на сумму около $775K по семи пулам в Ethereum через изъян бизнес-логики в её пути ликвидации. Ajna — это кредитный протокол, не использующий внешний оракул; он оценивает позиции через LUP (Lowest Utilized Price, наименьшую используемую цену), выведенную из ликвидности бакетов. Атакующий сначала манипулировал LUP, чтобы создать серьёзно неплатёжеспособную позицию, а затем заставил протокол ликвидировать контролируемую позицию по цене голландского аукциона, которую протокол всё ещё удерживал намного выше рыночной, так что требования на депозиты бакета были потрачены по их номинальной стоимости в котируемом токене для погашения долга, в то время как атакующий получил взамен реальный залог [8].

Предпосылки

Ajna — это разрешительный протокол, где любой может создать пул из пары токенов для предоставления и заимствования, аналогично пулу Uniswap. Каждый пул разделён на buckets (бакеты), аналогично ценовым тикам в Uniswap V3, где каждый тик представляет фиксированную сумму котируемого токена на единицу залога. Кредиторы выбирают бакет в соответствии с предпочитаемым соотношением займа к стоимости и предоставляют туда ликвидность, получая взамен LP (право требования на депозиты этого бакета). Поскольку внешнего оракула не существует, LUP выводится напрямую из распределения депозитов бакетов и общего долга.

Позиция становится подлежащей ликвидации, как только её threshold price (пороговая цена, её долг, делённый на её залог) поднимается выше LUP. Любой может затем kick (перевести) её в ликвидацию, внеся залог (bond), что открывает голландский аукцион. Цена аукциона не привязана к спотовому рынку: она начинается в 32 раза выше опорной цены и уменьшается вдвое каждый час, и удерживается неизменной первый час (период отсрочки), прежде чем начнёт снижаться.

Выставленный на аукцион залог можно забрать двумя способами. Вызывающий может take() (забрать) залог, заплатив котируемым токеном по текущей цене аукциона, либо вызвать bucketTake(), который использует депозиты котируемого токена из выбранного бакета для покупки выставленного на аукцион залога и погашения долга заёмщика. При bucketTake() залог зачисляется в этот бакет, поэтому именно его кредиторы становятся приобретателями залога; при арбитражном взятии бакета вызывающему дополнительно выплачивается вознаграждение LP (право требования на этот бакет) за спред между ценой бакета и ценой аукциона в качестве стимула для инициирования ликвидации. Замысел состоит в том, что тейкер вмешивается только тогда, когда цена аукциона опустилась до уровня или ниже реальной стоимости залога: без оракула этот убывающий аукцион — это то, как Ajna определяет справедливую цену, и кредиторы бакета получают прибыль, приобретая залог ниже собственной цены своего бакета. Сам аукцион завершается только тогда, когда долг заёмщика погашен или заём снова становится обеспеченным; отдельный путь урегулирования разрешает аукцион после 72-часового льготного периода, либо раньше, если залог не остался, поглощая любой остаточный плохой долг и возвращая оставшийся залог заёмщику.

Три технические детали этого механизма определяют показатели, которые даёт взятие. Во-первых, цена аукциона не выводится из рынка; она снижается как 32 * max(kickMomp, neutralPrice) * 2^(-max(elapsedHours - 1, 0)), поэтому остаётся неизменной в течение первого часа периода отсрочки и только затем начинает падать, оставаясь около 32-кратной опорной цены вскоре после этого периода.

Во-вторых, kick принимается только тогда, когда заёмщик уже недообеспечен при своём долге до штрафа: _kick() выполняет проверку _isCollateralized() и откатывается с BorrowerOk(), прежде чем добавить штраф в размере трёхмесячных процентов, так что этот штраф увеличивает зафиксированный пост-kick долг, не помогая позиции пройти квалификацию.

В-третьих, первое взятие в аукционе добавляет штраф 7% к долгу заёмщика перед тем, как рассчитываются суммы погашения и залога для взятия, поэтому один bucketTake() не обязан сам по себе полностью погасить долг.

Анализ уязвимости

Проблемный контракт — это пул Ajna (0xad24...178e); описанная здесь логика ликвидации взята из этого верифицированного исходного кода. Внутри bucketTake() протокол использует требования на депозиты бакета по их номинальной стоимости в котируемом токене для погашения долга заёмщика на аукционе, а при арбитражном взятии бакета выплачивает тейкеру вознаграждение LP, основанное на спреде между ценой бакета и ценой аукциона.

Он никогда не проверяет реальную возместимую стоимость этих требований на депозиты или экономическую обоснованность цены аукциона; он просто предполагает, что требования могут быть впоследствии выкуплены по номинальной стоимости. Две цены, на которые он опирается, вычисляются независимо, и ни одна не проверяется против другой: LUP следует распределению депозитов бакетов и общему долгу пула, в то время как цена аукциона снижается исключительно на основе прошедшего времени от опорной цены, зафиксированной при kick. Таким образом, номинальная стоимость требования в цепочке может расходиться с тем, что оно может реально возместить, а bucketTake() всё равно урегулирует долг против этой номинальной стоимости.

Внутренне, bucketTake() проходит через _takeBucket() в _rewardBucketTake(), где при арбитражном взятии вознаграждение LP тейкера вычисляется как забранный залог, умноженный на спред между ценой бакета и ценой аукциона.

Анализ атаки

Следующее основано на анализе в цепочке пула cbETH, одного из семи затронутых, с использованием транзакций 0x8a8793...016e64 и 0x12dfde...14e4f5.

  • Шаг 1: В подготовительной транзакции атакующий добавил около 49.343 WETH котируемой ликвидности в бакет с индексом 2000, с ценой намного выше рыночной. Опираясь на этот завышенный бакет, атакующий открыл почти необеспеченную позицию, заложив около 0.001 cbETH, чтобы занять около 49.319 WETH. Эта позиция удерживает почти нулевой залог, поэтому она не является призом; она служит двум подготовительным целям. Во-первых, как неплатёжеспособный долг она тянет LUP вниз, как только рынок возвращается к норме. Во-вторых, поскольку почти весь объём в 49.343 WETH, внесённый в бакет 2000, теперь заимствован обратно, оставляя лишь ничтожную сумму, бакет 2000 остаётся с требованием на депозит, чья номинальная стоимость, около 49.343 WETH, значительно превышает то, что он может реально возместить; именно это ослабленное требование позже становится боеприпасом, который атакующий тратит по номинальной стоимости на Шаге 4.
  • Шаг 2: В той же подготовительной транзакции атакующий использовал второй контролируемый адрес, 0x02d329...6f5f, чтобы открыть позицию заёмщика по нормальной цене, заложив около 48.128 cbETH залога и заняв под него около 49.319 WETH. Это вернуло рыночную цену в нормальный диапазон, оставив первую позицию серьёзно неплатёжеспособной, в то время как вторая позиция оставалась чуть выше порога здоровья. Именно эта вторая позиция, которая удерживает реальный залог, является той, которую атакующий на самом деле намеревается опустошить; неплатёжеспособная первая позиция служит лишь рычагом, который сдвинул LUP.

  • Шаг 3: Затем атакующий перевёл в аукцион (kick) вторую позицию (открытую 0x02d329...6f5f), а не неплатёжеспособную первую. Kick принимается только в том случае, если позиция уже недообеспечена по своему долгу до штрафа относительно текущего LUP, а неплатёжеспособная первая позиция к тому моменту уже опустила LUP достаточно низко, чтобы накопленный долг второй позиции достиг этой планки. Только после проверки соответствия критериям протокол добавляет штраф kick в размере трёхмесячных процентов, поэтому зафиксированный пост-kick долг составляет около 49.362 WETH. Kick открыл голландский аукцион позиции.

  • Шаг 4: Атакующий подождал до момента сразу после часового периода отсрочки, когда цена аукциона только начала снижаться и всё ещё была близка к 32-кратной опорной цене. Вызвав bucketTake() для второй позиции в бакете 2000, том же бакете, который теперь удерживал ослабленное требование, атакующий ликвидировал её по этой всё ещё высокой, значительно выше рыночной цене. Поскольку это было первое взятие в данном аукционе, протокол добавил штраф 7% к долгу заёмщика, прежде чем рассчитывать суммы погашения и залога для этого взятия. Залог оценён по цене аукциона, поэтому расходование около 49.34 WETH депозита бакета 2000 погасило лишь часть этого долга и убрало лишь около 1.51 cbETH залога, оставив основную часть залога нетронутой, в то время как вызывающий получил крупное вознаграждение LP на спреде.
  • Шаг 5: Атакующий выкупил это вознаграждение LP через removeCollateral(), забрав часть прибыли в виде залога.

  • Шаг 6: В завершение атакующий взял флэш-займ через Balancer и вызвал take() Ajna, чтобы погасить остаточный долг, оставленный bucketTake(), на этот раз реальным котируемым токеном, пока долг заёмщика не достиг нуля и аукцион не завершился. Финальный вызов repayDebt() затем вывел теперь необременённый залог, около 46.51 cbETH, с quoteRepaid=0: за него не было возвращено ни одного котируемого токена. Прибыль обошлась пулу. Дефицит не исчез, он переместился: после закрытия второй позиции всё ещё не погашенный, почти не обеспеченный заём первой позиции остался в пуле как плохой долг, лежащий на других кредиторах.

Заключение

Первопричина в том, что путь ликвидации Ajna урегулирует долг заёмщика на аукционе против требований на депозиты бакета по номинальной стоимости, без проверки их реальной возместимой суммы или экономической обоснованности цены аукциона. Именно отсутствие этой проверки позволило потратить искусственно созданное ослабленное требование по номинальной стоимости, чтобы опустошить реальный залог, оставив дефицит в пуле в виде плохого долга. Путь ликвидации должен оценивать требования на депозиты по их реальной возместимой сумме, а не по номинальной стоимости, подтверждать экономическую обоснованность цены аукциона перед завершением взятия и защищаться от расходования требований, которые уже были ослаблены существующим плохим долгом пула. Затронутые контракты пулов неизменяемы и не предоставляют административной паузы, поэтому, как только вывод средств начался, не было способа его остановить; пользователи могли только самостоятельно выходить из позиций.

Серия эксплойтов Rain Card Contract (прослежено на Avici)

28 августа 2026 года (по UTC) устаревшая версия общей солана-программы залогового обеспечения карт Rain была атакована через обход проверки подписи Ed25519. Заставив программу принять сфабрикованное административное одобрение, атакующий получил контроль над залоговыми аккаунтами пользователей и вывел токенные балансы, хранящиеся в них. Поскольку изъян находился в коде программы, общем для программ карт на базе Rain, единая ошибка одновременно затронула их все: эксплойт вывел оценочно ~$1.1M в общей сложности, причём Avici и Tria — два крупнейших случая, раскрывшие соответственно около $500,859 (1,685 пользователей) и $431,945 (636 пользователей) [9].

Приведённый ниже анализ использует Avici в качестве разбираемого примера.

Предпосылки

В Solana проверка подписей выполняется нативным прекомпайлом Ed25519 в рамках обработки транзакции: если указанная подпись недействительна, вся транзакция завершается неудачей. Бизнес-программа не получает этот результат напрямую; она проверяет другие инструкции в той же транзакции (через системную переменную Instructions) и полагается на то, что проверка прошла успешно. Когда пользователь пополняет карту на базе Rain (как в случае с Avici), баланс хранится в персональном залоговом аккаунте, управляемом общей программой Rain, и полномочия токена этого аккаунта выводятся из программы, поэтому администратор аккаунта может перемещать активы на этом аккаунте через поток перевода программы.

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

Инструкция проверки Ed25519 начинается с однобайтового счётчика проверяемых подписей и байта заполнения, за которыми следует по одной структуре Ed25519SignatureOffsets на каждую подпись. Каждая структура указывает не только байтовые смещения подписи, публичного ключа и сообщения, но и индекс инструкции, откуда следует читать каждый из них [10]. Эти индексы — намеренная функция: они позволяют одной проверке считывать свои входные данные из данных любой проиндексированной инструкции в транзакции, например, чтобы проверить подпись над данными другой инструкции без их копирования. Нативный верификатор просто читает всё, на что указывают смещения и индексы инструкций.

Анализ уязвимости

Изъян находится в залоговой программе (3zVB...yBzDuc): она доверяет публичному ключу, не подтверждая, что этот ключ действительно тот, который был проверен верификатором. Чтобы зафиксировать подписывающего, одобряющего изменение администратора, она считывает публичный ключ из фиксированной позиции внутри инструкции проверки Ed25519, которую она проверяет, а затем принимает сам факт прохождения проверки как доказательство того, что владелец этого ключа подписал сообщение об изменении администратора.

То, что она никогда не подтверждает — это куда на самом деле смотрела эта проверка. Поскольку эти поля индекса инструкции контролируются вызывающей стороной, а рантайм передаёт данные каждой инструкции нативному верификатору, инструкция проверки может содержать один публичный ключ в собственном теле, при этом её индексы инструкций направляют верификатор на другую инструкцию, чтобы проверить подпись под другим публичным ключом над отдельно взятым сообщением.

Таким образом, существуют два прочтения «ключа администратора», ничем не связанные друг с другом: программа доверяет ключу, находящемуся в инструкции, которую она проверяет, в то время как верификатор проверял лишь то, на что указывали индексы. Ключ администратора может присутствовать в транзакции в виде данных, даже если не была проверена ни одна действительная подпись под этим ключом. Именно это разделение является уязвимостью, а отсутствующая проверка — это связка, которая должна была бы принудительно требовать, чтобы публичный ключ и сообщение, которые реально проверил верификатор, совпадали с ключом и сообщением, которым доверяет программа.

Анализ атаки

Следующий анализ основан на транзакции ZmpBgn...mqWL.

  • Шаг 1: Атакующий построил транзакцию с двумя инструкциями проверки Ed25519, за которыми следовала SubmitSignatures. Инструкция 0 несла собственный публичный ключ атакующего (cafa…53db) и реальную подпись над сообщением об изменении администратора, обеспечивая транзакции одну действительно валидную проверку Ed25519.
  • Шаг 2: Инструкция 1 разместила публичный ключ протокольного администратора (a2fc…959a) в слоте публичного ключа, но заполнила слот подписи фейковыми байтами 0x09, так что эта инструкция провалилась бы, если бы её собственные данные были на самом деле проверены.

  • Шаг 3: Атакующий установил заголовок Ed25519 для инструкции 1 в значение 01003000000010000000700020000000. Только три поля индекса инструкции выполняют перенаправление, и при декодировании все они равны нулю, поэтому подпись, публичный ключ и сообщение все читаются из инструкции 0 (байтовые смещения по-прежнему указывают внутри данных этой инструкции):

    num_signatures      = 1,    padding = 0
    signature_offset    = 48,   signature_instruction_index   = 0
    public_key_offset   = 16,   public_key_instruction_index  = 0
    message_data_offset = 112,  message_data_size = 32,  message_instruction_index = 0

    Таким образом, вторая проверка повторно прочитала инструкцию 0 и повторно проверила собственную действительную подпись атакующего вместо недействительной полезной нагрузки 0x09 в инструкции 1.

  • Шаг 4: Атакующий вызвал SubmitSignatures. Программа увидела две успешные проверки Ed25519, но при фиксации второго подписывающего она прочитала публичный ключ протокольного администратора, встроенный в инструкцию 1, и таким образом приняла этот ключ администратора как одобренного второго подписывающего.

  • Шаг 5: С этим ложным одобрением, зафиксированным в системе, атакующий использовал поток изменения администратора, чтобы назначить себя администратором залогового аккаунта жертвы, превратив обход проверки в прямой контроль над этим аккаунтом.

  • Шаг 6: Подтвердив вызывающую сторону как администратора залога, программа вывела активы из токенного аккаунта залога карты пользователя через свой поток перевода, вызвав перевод с использованием собственного программно-выведенного адреса (PDA) в качестве полномочий этого токенного аккаунта.

Заключение

Первопричина серии эксплойтов Rain Card Contract заключается в том, что залоговая программа подтверждала, что нативная проверка Ed25519 была выполнена, но никогда не подтверждала, что публичный ключ и сообщение, которым она доверяла, были именно теми, которые верификатор реально проверил. Программа, полагающаяся на нативную проверку подписи, должна связывать данные авторизации, которые она использует, с этой проверкой: либо требовать самодостаточную структуру Ed25519, которая читает свои входные данные из текущей инструкции, либо разрешать каждую упомянутую инструкцию и сравнивать проверенный публичный ключ и сообщение, байт за байтом, с данными, на основе которых она действует. То, что тот же изъян работал непропатченным в множестве программ карт на базе Rain, и превратило одну ошибку в мультипрограммное событие.

Tectonic

30 августа 2026 года Tectonic, кредитный протокол в стиле Compound в сети Cronos, был атакован из-за того, что его токен управления TONIC с низкой ликвидностью был разрешён в качестве залога с коэффициентом залога 20%. Атакующий завысил залог, деноминированный в TONIC, сразу на двух поверхностях: манипуляцией обменного курса tTONIC через прямой перевод в сочетании с скупкой на DEX, поднявшей оракульную цену. Против вдвойне завышенного залога атакующий занимал средства на нескольких кредитных рынках. Около $6.29M (2,592 ETH) были переведены через мост в Ethereum и представляют собой реализованный ущерб; основная часть вывода осталась в сети Cronos и была аннулирована, когда валидаторы откатили сеть до состояния до эксплойта. Ни Tectonic, ни Cronos пока не подтвердили окончательную сумму ущерба [11].

Анализ уязвимости

Первопричиной стало включение TONIC, токена управления с низкой ликвидностью, в список залоговых активов с коэффициентом залога 20%. Несмотря на свою роль в управлении, TONIC имел крайне низкую ликвидность в цепочке, поэтому его оценку можно было резко сдвинуть ограниченным капиталом. Как и в эксплойте Moonwell, здесь одновременно поддавались манипуляции две поверхности: оракульная цена TONIC/USD и обменный курс токена-квитанции tTONIC, который простой перевод TONIC на рынок поднимает без аннулирования соответствующего долга [11]. Любой ненулевой коэффициент залога затем превращал эту завышенную оценку в возможность заимствования против гораздо более ликвидных активов.

Анализ атаки

Приведённая ниже реконструкция атаки основана на анализе в цепочке и подробной реконструкции по архивной ноде [11][12].

  • Шаг 1: Атакующий предоставил Tectonic около 5 млн USDC в качестве залога. Пока оракул TONIC/USD всё ещё оценивал токен примерно в $1.06e-8, один контролируемый аккаунт занял примерно 376.54 трлн TONIC и перевёл их на второй аккаунт.

  • Шаг 2: Второй аккаунт предоставил около 41.87 трлн TONIC обычным способом, получив взамен tTONIC (токен-квитанцию рынка).

  • Шаг 3: Атакующий перевёл большую часть оставшихся заёмных TONIC напрямую в контракт рынка tTONIC, не погасив первоначальный долг по TONIC, повысив обменный курс tTONIC и увеличив стоимость залога tTONIC, удерживаемого вторым аккаунтом.

  • Шаг 4: Используя завышенный залог tTONIC, атакующий занял 200,000 USDC и около 6.96 млн CRO, затем скупил ещё примерно 16.23 трлн TONIC по пулам TONIC/USDC, TONIC/WCRO и TONIC/VVS и вернул их обратно на рынок. Это ещё больше подняло обменный курс и подтолкнуло вверх спотовую цену на DEX; офчейн-фид Tectonic TONIC/USD затем принял серию быстро растущих котировок (с примерно $1.06e-8 в 12:19 UTC до примерно $2.08e-6 к 12:49 UTC). Это внешняя поверхность.

  • Шаг 5: Дальнейшие займы на примерно 3.31 млн USDC и 21.21 млн CRO позволили скупить ещё 7.68 трлн TONIC, снова направленных на рынок, ещё больше усиливая как завышенный обменный курс, так и манипулируемую цену непосредственно перед выводом средств.

  • Шаг 6: В финальной транзакции атакующий выбирал средства против вдвойне завышенного залога на нескольких кредитных рынках, выведя примерно 55.24 млн USDC, 45.65 млн USDT, 98 WBTC, 1,895 WETH, 16.75 млн CRO и другие активы. Около $6.29M было переведено через мост в Ethereum (~2,592 ETH), прежде чем валидаторы остановили цепочку. Производство блоков возобновилось 30.08.2026 в 23:49 UTC (объявлено 31.08) после того, как валидаторы откатили состояние до блока перед эксплойтом, аннулировав баланс в сети Cronos; только переведённые через мост ~$6.29M остаются как реализованный ущерб.

Заключение

Как и в случае с эксплойтом Moonwell ранее на этой неделе, это была атака с манипуляцией ценой на рынок в стиле Compound, принявший токен с низкой ликвидностью в качестве залога, завысив стоимость залога сразу на двух поверхностях: оракульной цене и обменном курсе токена-квитанции. Кредитным протоколам следует избегать включения активов с низкой ликвидностью в списки залоговых активов, применять строгие лимиты эмиссии и заимствования там, где такие активы всё же должны поддерживаться, и использовать средневзвешенное по времени или учитывающее ликвидность ценообразование, чтобы тонкий спотовый рынок не мог сдвинуть оракул. Поверхность обменного курса нуждается в собственной защите: учёт залога рынка должен исключать несанкционированные переводы базового актива, чтобы прямой перевод не мог завысить обменный курс токена-квитанции, пока соответствующий долг остаётся непогашенным. Мониторинг аномальной ценовой и заёмной активности в цепочке может дополнительно сократить окно реагирования до того, как стоимость будет выведена через мост.

Начните работу с Phalcon Security

Обнаруживайте каждую угрозу, оповещайте о важном и блокируйте атаки.

Попробовать бесплатно

Источники

О компании BlockSec

BlockSec — это провайдер полного цикла услуг в области блокчейн-безопасности и крипто-комплаенса. Мы создаём продукты и услуги, которые помогают клиентам проводить аудит кода (включая смарт-контракты, блокчейны и кошельки), пресекать атаки в реальном времени, анализировать инциденты, отслеживать незаконные средства и выполнять обязательства по AML/CFT на всех этапах жизненного цикла протоколов и платформ.

BlockSec опубликовала множество работ по безопасности блокчейнов на престижных конференциях, сообщила о нескольких атаках нулевого дня на DeFi-приложения, заблокировала множество взломов, спасая более 20 миллионов долларов, и обеспечила безопасность криптовалют на миллиарды долларов.

Sign up for the latest updates
Поверхности атаки Web3: обзор тестирования на проникновение

Поверхности атаки Web3: обзор тестирования на проникновение

Крипто-организации сохраняют все традиционные уязвимости и добавляют цепочку работы с деньгами. Статья описывает систему через четыре компонента: приложение, авторизацию и подпись, взаимодействие с блокчейном, инфраструктуру — с их зонами атак, а также пять специфичных для web3 областей: от эксплуатации и подписания до вывода средств и контрактов.

Правила взаимодействия и безопасности продакшена при пентесте институционального блокчейна

Правила взаимодействия и безопасности продакшена при пентесте институционального блокчейна

Пентест систем подписи, вывода средств и учёта готовится заранее: цель, область, ответственные, доступ, правила взаимодействия (RoE), критерии остановки, мониторинг, координация изменений. Завершается устранением недостатков и повторным тестированием.

Что такое пентестинг блокчейна? Определения и границы
Security Services

Что такое пентестинг блокчейна? Определения и границы

Общепринятого определения пентестинга блокчейна нет — его часто путают с аудитом, сканированием и bug bounty. Статья даёт рабочее определение и разбирает пять тестируемых возможностей связки офчейн-ончейн, направляя смежные задачи к аудиту кода, аудиту безопасности кошельков, web3-тестированию, сканированию и bug bounty.

Best Security Auditor for Web3

Validate design, code, and business logic before launch. Aligned with the highest industry security standards.

BlockSec Audit