Back to Blog

~$39,5 млн потеряно: Allbridge, Wanchain и другие | Еженедельник BlockSec

Code Auditing
July 30, 2026
17 min read
Key Insights

За прошедшую неделю (2026/07/20 - 2026/07/26) представлены следующие 8 значимых инцидентов безопасности с общими потерями около $39,5 млн.

Дата Инцидент Тип Оценочные потери
2026/07/19* Allbridge Некорректная проверка входных данных ~$1,65M
2026/07/20 Zilliqa Некорректная генерация нонса ~$400K
2026/07/20 Wanchain Некорректное кодирование сообщений ~$500K
2026/07/22 42DAO Компрометация закрытого ключа ~$900K
2026/07/22 AFX Trade Компрометация закрытого ключа ~$24,15M
2026/07/22 B² Network Компрометация закрытого ключа ~$3,8M
2026/07/23 Verus Компрометация закрытого ключа ~$7,6M
2026/07/24 Lien Finance Некорректная логика проверки ~$542K

*Инцидент с Allbridge произошёл 19 июля (17:51 UTC) и не был освещён в отчёте прошлой недели. Он включён сюда для полноты картины.

  • Zilliqa был выбран, поскольку обнажил долгое время не выявлявшуюся уязвимость в реализации кошелька вне блокчейна, которая с 2019 года подвергала пользователей устаревших нативных ZIL-аккаунтов риску компрометации закрытых ключей.
  • Allbridge был выбран, поскольку демонстрирует уязвимость подмены аккаунтов (account aliasing), обусловленную позиционной моделью аккаунтов Solana. Уязвимость пришлось восстанавливать из двоичного файла развёрнутой программы, так как исходный код был недоступен.
  • Wanchain был выбран, поскольку демонстрирует, как мост можно опустошить без компрометации криптографии. Подписи были действительны и корректно проверены, однако неинъективное кодирование сообщений без разделителей позволило интерпретировать авторизацию на 3 097,56 токена в сети Cardano как 203 001 692,164714 токена.
  • Lien Finance был выбран, поскольку иллюстрирует, как проверку соответствия количества элементов без контроля идентичности и кратности совпавших элементов можно обойти.

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

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

Главное событие недели: Allbridge Core

Этот инцидент выделен, поскольку паттерн подмены аккаунтов применим к любой программе на Solana, принимающей один и тот же изменяемый аккаунт в двух ролях одной инструкции. Лежащий в основе механизм — два изменяемых представления одного и того же состояния, где второя запись молча перезаписывает первую — имеет ту же структуру, что и классическая ошибка самоперевода ERC-20 (transferFrom где from == to). Пост-мортем проекта [1] даёт высокоуровневое резюме, но не описывает механизм на уровне кода. Приведённый ниже анализ был восстановлен из двоичного файла развёрнутой программы для восполнения этого пробела.

19 июля 2026 года (17:51 UTC) Allbridge Core, протокол межсетевого моста на Solana, был взломан приблизительно на $1,65 млн (~$1,12 млн в USDC и ~$539K в USDT). Первопричина заключалась в том, что инструкция обмена принимала один и тот же аккаунт Pool в роли отправителя и получателя, не требуя их различия. При передаче одного и того же аккаунта дважды обновления внутреннего учёта в одной роли молча перезаписывались другой, тогда как реальные переводы токенов уже были выполнены. Пять самообменов исказили записанное состояние Pool настолько, что атакующий смог конвертировать небольшой ввод USDT в ~$2,24 млн USDC.

Предыстория

Allbridge Core — это межсетевой мост, маршрутизирующий обмены через внутреннюю расчётную единицу vUSD. Обмен состоит из двух учётных этапов:

исходный токен -- swap_to_v_usd(send_pool) --> vUSD
vUSD           -- swap_from_v_usd(receive_pool) --> целевой токен

vUSD — не SPL-токен. Он существует только как поле в данных каждого Pool на блокчейне. Каждый Pool отслеживает token_balance, v_usd_balance и reserves. Этап отправки переводит исходный токен от пользователя в хранилище моста, увеличивает учёт на стороне токена Pool отправителя и рассчитывает вывод vUSD. Этап получения добавляет этот vUSD в Pool получателя, уменьшает его учёт на стороне токена и переводит целевой токен из хранилища моста получателя пользователю.

Программы Solana получают свои аккаунты в виде позиционного массива от вызывающей стороны. Обработчик инструкции программы указывает, какие позиции массива соответствуют каким ролям (send_pool, receive_pool, send_mint и т.д.), но среда выполнения не препятствует передаче одного и того же адреса аккаунта в двух позициях. Когда программа десериализует аккаунты на двух разных позициях, каждая десериализация создаёт отдельный объект в памяти. Изменения одного объекта не влияют на другой. Если один и тот же аккаунт передаётся дважды, оба объекта начинают с одних и тех же данных на блокчейне, но расходятся, как только один из них изменяется.

Когда обработчик завершает работу, логика выхода программы (например, AccountsExit в Anchor) сериализует каждый объект аккаунта обратно в данные аккаунта в фиксированном порядке. Если два объекта ссылаются на один и тот же аккаунт, вторая сериализация полностью перезаписывает первую.

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

Точный исходный код затронутого развёртывания Allbridge Core был недоступен. Анализ был восстановлен из 1 770 736-байтового ELF-файла, хранящегося в ProgramData (SHA-256: 40f776...346bb6). Последний слот развёртывания (204 727 029) предшествует слоту атаки (433 941 722).

Уязвимая программа — BrdgN2...ceWB. В инструкциях Swap транзакции атаки (инструкции с 3 по 7) четыре пары аккаунтов отправителя и получателя были идентичны:

Роли Аккаунт
send_mint / receive_mint Es9vMF...wNYB
send_pool / receive_pool DW4a2E...wCX
send_bridge_token / receive_bridge_token 2xY9TD...vohV
send_user_token / receive_user_token 817UdW...CVct

Парсер содержит 16 восстановленных 32-байтовых сравнений, привязывающих каждую роль к ожидаемому минту, хранилищу, владельцу, PDA, авторитету или программе Token. Однако ни одно сравнение не требует send_pool.key() != receive_pool.key(). Обе роли Pool десериализуются независимо до запуска обработчика.

Развёрнутый ELF показывает две конструкции Pool, расчёты отправки и получения, и два выхода Pool в фиксированном порядке:

L54249-L54252  function_9881(accounts[5])     -> send_pool локальный
L54294-L54297  function_9881(accounts[6])     -> receive_pool локальный

L74177-L74195  function_12623(send_pool)      -> swap_to_v_usd
L74368-L74389  function_12940(receive_pool)   -> swap_from_v_usd

L55932-L55936  function_8300(send_pool)       -> полная сериализация Pool
L55940-L55944  function_8300(receive_pool)    -> полная сериализация Pool

Каждый вызов function_9881 выделяет отдельный 176-байтовый локальный объект Pool и копирует в него декодированные поля. Когда ключи аккаунтов равны, оба объекта начинают с одних и тех же данных, но изменение объекта отправителя не меняет объект получателя.

Реконструированный псевдо-Rust (поведение развёрнутой программы, не восстановленный исходный код) [2] [3]:

let mut send_pool = Account::<Pool>::try_from(&accounts[5])?;
let mut receive_pool = Account::<Pool>::try_from(&accounts[6])?;

require_keys_eq!(send_pool.mint, send_mint.key());
require_keys_eq!(receive_pool.mint, receive_mint.key());

// Привязки хранилища, владельца, PDA, авторитета и программы Token проверяются.
// ОТСУТСТВУЕТ: require_keys_neq!(send_pool.key(), receive_pool.key());

token::transfer(send_user_token, send_bridge_token, amount)?;
let v_usd = swap_to_v_usd(&mut send_pool, amount)?;
let receive_amount =
    swap_from_v_usd(&mut receive_pool, v_usd, receive_amount_min)?;
token::transfer(receive_bridge_token, receive_user_token, receive_amount)?;

// AccountsExit после возврата обработчика:
send_pool.exit(program_id)?;     // первая запись
receive_pool.exit(program_id)?;  // вторая запись, перезаписывает первую

function_8300 начинает запись с позиции 0 и сериализует все 131 определённый байт Pool. Первый выход записывает send_pool; второй выход записывает receive_pool поверх тех же байт. Итоговое состояние аккаунта — это устаревшее значение на стороне получателя, а не объединение двух локальных обновлений.

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

Ключевой изъян состоит в том, что программа не проверяет, что send_pool и receive_pool — это разные аккаунты. Все остальные проверки (привязка минта, владение хранилищем, вывод PDA) проходят, поскольку обе роли легитимно принадлежат одному и тому же Pool.

Анализ атаки

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

Эксплойт был выполнен в одной транзакции, содержащей флэш-займ Kamino, семь инструкций Allbridge Swap и погашение долга Kamino.

  • Шаг 1: Атакующий взял взаймы ~1,12 млн USDC у Kamino и обменял их через Allbridge на ~949K USDT (инструкции 1-2). Этот первый обмен обеспечил USDT, необходимый для последующих самообменов, и изменил состояния Pool USDC и USDT.
  • Шаг 2: Атакующий выполнил пять самообменов на ~100K USDT, используя идентичные аккаунты минта, Pool, хранилища и токенов пользователя для отправки и получения (инструкции 3-7). Поскольку Pool получателя сериализовывался последним при каждом вызове, сохранённый учёт фиксировал уменьшение на стороне получателя, но не увеличение на стороне отправителя. Наблюдаемые выводы уменьшались при каждом вызове по мере накопления искажения:
Самообмен Ввод Записанный vUSD Цена (vUSD/USDT) Вывод USDT
1 ~100K ~162K ~5,24 ~47,8K
2 ~100K ~256K ~16,6 ~25,4K
3 ~100K ~471K ~57,2 ~12,5K
4 ~100K ~918K ~190 ~5,73K
5 ~100K ~1,82M ~563 ~2,36K

Пять вызовов перевели ~500K USDT в хранилище и вернули ~93,8K USDT. Записанный token_balance Pool падал с каждым вызовом, тогда как реальный баланс хранилища рос, расширяя разрыв, который эксплуатируется на следующем шаге.

  • Шаг 3: Атакующий внёс лишь ~3,99K USDT (инструкция 8). Искажённый Pool USDT произвёл ~2,24 млн vUSD, которые Pool USDC конвертировал в ~2,24 млн USDC. Завышенный вывод vUSD стал возможен, потому что записанный баланс токенов Pool был намного ниже фактического баланса хранилища после пяти раундов отброшенных обновлений на стороне отправителя.
  • Шаг 4: Атакующий погасил флэш-займ Kamino на ~1,12 млн USDC и комиссию ~11,2 USDC (инструкция 9). Итоговые балансы атакующего составили ~1,12 млн USDC и ~539K USDT.

Заключение

Первопричиной этого инцидента стала отсутствующая проверка того, что send_pool и receive_pool ссылаются на разные аккаунты. Один изменяемый Pool был десериализован в два локальных учётных объекта, и вторая полная сериализация перезаписала первую после того, как программа Token уже выполнила переводы в хранилище. Это разделило записанный учёт Pool и реальный баланс его хранилища. Псевдонимы аккаунтов, поток управления ELF, порядок сериализации, журналы транзакций и балансы хранилищ — всё это подтверждает один и тот же механизм [1].

Для программ Solana: любая инструкция, принимающая один и тот же тип аккаунта в двух или более изменяемых ролях, должна либо обеспечивать уникальность (require_keys_neq!), либо объединять изменения перед выходом. Система ограничений #[account] фреймворка Anchor не обеспечивает уникальность между ролями по умолчанию, поэтому эта проверка должна быть добавлена явно. Общий паттерн — два изменяемых представления одного и того же состояния, где вторая запись молча перезаписывает первую — может встречаться везде, где программы управляют собственным порядком сериализации.

Ссылки

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

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

Попробуйте бесплатно

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

Zilliqa Ledger Wallet

20 июля 2026 года Zilliqa зафиксировала on-chain активность, соответствующую активной эксплуатации устаревших нативных ZIL-аккаунтов, с известными потерями около $400K. Первопричиной стал смещённый нонс в пути подписания EC-Schnorr приложения Zilliqa Ledger: код генерации нонса копировал неверные 32 байта из 40-байтового буфера после модульного сокращения, фиксируя 64 старших бита в ноль. Имея несколько публичных подписей от одного и того же аккаунта, атакующий мог восстановить закрытый ключ с помощью решёточных методов и опустошить аккаунт.

Предыстория

Нативные транзакции Zilliqa используют подписи EC-Schnorr на secp256k1. Для каждой подписи подписывающая сторона должна сгенерировать свежий, полноразрядный, непредсказуемый эфемерный нонс kk, где 0k<N0 \le k < N (порядок кривой secp256k1). Процесс подписания порождает обязательство Q=compress(kG)Q = \text{compress}(kG), вызов r=SHA256(QpublicKeymsg)Nr = \text{SHA256}(Q \| \text{publicKey} \| \text{msg}) \bmod N и ответ s=krxNs = k - r \cdot x \bmod N, где xx — закрытый ключ. После публикации пара (r,s)(r, s) становится общедоступной.

Линейное соотношение s=krxNs = k - rx \bmod N является критической точкой. Корректная реализация остаётся безопасной, поскольку каждый kk свежий и равномерно распределён. Если нонс смещён или слишком мал, каждая публичная подпись раскрывает информацию об одном и том же закрытом ключе.

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

Соответствующий путь генерации нонса был введён в приложении Zilliqa Ledger в коммите "Bug fixes based on Ledger team review". Предполагаемый поток был следующим: сгенерировать 40 байт случайных данных, выполнить сокращение по модулю порядка кривой NN и использовать полученный скаляр как kk. Генерация широкого случайного целого числа и сокращение по модулю порядка кривой сами по себе не являются проблемой. Изъян проявился при копировании в буфер нонса:

unsigned char nonce[size+8];
cx_rng(nonce, size+8);
cx_math_modm(nonce, size+8, (unsigned WIDE char *) PIC(domain->n), size);
os_memcpy(T->K, nonce, size);

После cx_math_modm 32-байтовый скалярный результат располагается с выравниванием по правому краю внутри 40-байтового буфера. os_memcpy(T->K, nonce, size) копирует первые size (32) байта, сохраняя ведущие 8 нулевых байт заполнения и отбрасывая последние 8 байт энтропии. Сгенерированный нонс поэтому удовлетворяет k<2192k < 2^{192}, а не k<N2256k < N \approx 2^{256}.

Это означает, что 64 бита каждого нонса фиксированы в ноль. Из уравнения ответа Schnorr ki=si+rixNk_i = s_i + r_i x \bmod N каждая подпись даёт публичное модульное линейное соотношение, где результат ограничен 21922^{192}. Это экземпляр задачи о скрытом числе (HNP). При наличии примерно четырёх или более затронутых подписей от одного ключа стандартный алгоритм решёточного сокращения восстанавливает закрытый ключ за секунды на обычном оборудовании [1].

Дефект присутствовал во всех выпущенных версиях приложения Zilliqa Ledger с 2019 года вплоть до обнаружения инцидента в июле 2026 года [2].

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

Заключение

Этот инцидент был вызван изъяном реализации подписи вне блокчейна, а не ошибкой смарт-контракта. Приложение Zilliqa Ledger создавало подписи EC-Schnorr с нонсами, ограниченными k<2192k < 2^{192}, что после нескольких нативных транзакций с одного аккаунта раскрывало достаточно структурированной информации для восстановления закрытого ключа.

Основная мера защиты — вывод ключей из использования, а не просто обновление приложения Ledger. Исправленное приложение предотвращает создание новых ослабленных подписей, но не может стереть уже публичные подписи, записанные в блокчейне. Любой затронутый ключ, который произвёл достаточно уязвимых подписей, следует считать скомпрометированным [2].

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

Ссылки


Wanchain Cardano Bridge

20 июля 2026 года мост Wanchain Cardano потерял около 515,2 млн NIGHT (~$500K) из-за неинъективного кодирования сообщений в валидаторе Cardano TreasuryCheck на Plutus [1]. Пост-мортем проекта [2] описывает механизм на высоком уровне, но не предоставляет деталей кодирования на уровне байтов или кода; приведённый ниже анализ восстанавливает полную коллизию. Подписи нод моста были действительны и корректно проверены, однако конкатенация полей переменной длины без разделителей позволила атакующему сместить границы байт между двумя соседними числовыми полями, интерпретировав авторизацию на 3 097,56 NIGHT как вывод 203 001 692,164714 NIGHT.

Предыстория

Затронутый компонент — система межсетевого казначейства Wanchain на Cardano. Пользователь исходной сети сжигает или блокирует токены, ноды моста подписывают авторизационное сообщение, построенное из разобранных полей искупителя (redeemer), и подписанное доказательство отправляется в валидатор Cardano TreasuryCheck на Plutus для разблокировки активов из казначейства.

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

Валидатор TreasuryCheck проверяет подпись нод моста над сырой конкатенацией 14 разобранных полей искупителя:

hashRedeemer = sha3_256 $ mconcat
  [ toPkhPay, toPkhStk, policy, assetName
  , packInteger amount, packInteger adaAmount
  , txHash, packInteger index, packInteger mode
  , uniqueId, packInteger txType, packInteger ttl
  , packInteger outputCount, userData
  ]

Целочисленное кодирование, используемое packInteger, имеет переменную длину, а конкатенация не имеет префикса длины, разделителя типов или доменно-разделённой типизированной сериализации. Различные семантические кортежи могут поэтому производить одну и ту же подписанную строку байт.

В показательной атаке ноды моста подписали обычный вывод:

amount    = 3097560000     -> packInteger = b8a103c0
adaAmount = 1206800        -> packInteger = 126a10
combined                                  = b8a103c0126a10

Атакующий отправил искупитель Cardano, разобранный как:

amount    = 203001692164714 -> packInteger = b8a103c0126a
adaAmount = 16              -> packInteger = 10
combined                                   = b8a103c0126a10

Последовательности байт идентичны. Проверка подписи прошла, и контракт Cardano интерпретировал искупитель как вывод примерно в 65 000 раз больший, чем было авторизовано.

Анализ атаки

Следующий анализ ссылается на показательную транзакцию Cardano 0a4861...2ea1 и исходную транзакцию BSC 0xe90111...d5f26b.

  • Шаг 1: Атакующий создал обычно выглядящие запросы на мост в исходной сети. В показательном случае транзакция BSC сожгла 3 110 NIGHT, и API Wanchain зафиксировал ожидаемую сумму получения в Cardano 3 097,56 NIGHT [3].

  • Шаг 2: Ноды моста подписали хеш авторизации, построенный из сырых конкатенированных полей.

  • Шаг 3: Атакующий изменил границы полей искупителя Cardano между amount и adaAmount, сохранив подписанную байтовую последовательность неизменной.

  • Шаг 4: Валидатор Cardano TreasuryCheck разобрал искупитель как вывод с высоким значением и успешно проверил повторно использованную подпись. Транзакция выплатила атакующему 203 001 692,164714 NIGHT.

  • Шаг 5: Тот же паттерн был повторён в нескольких транзакциях Cardano, что привело к общему опустошению примерно на 515 206 545,426856 NIGHT (~$500K).

Заключение

Криптография не была взломана. Подписывающие стороны создали действительные подписи, и валидатор TreasuryCheck корректно их проверил. Изъян заключался в конструкции сообщения: hashRedeemer складывал 14 полей переменной длины в одну строку байт без разделителя или префикса длины, делая кодирование неинъективным. Различные кортежи полей производят одинаковый прообраз, одинаковый хеш и одинаковую действительную подпись.

Исправление состоит в том, чтобы сделать подписанное кодирование инъективным, так чтобы ровно один кортеж полей мог породить любую заданную строку байт. Кодирование целых чисел фиксированной ширины или добавление префикса длины к каждому полю фиксирует границы, которые переместил атакующий. Стандартный структурированный кодировщик достигает того же результата и сложнее реализовать неправильно. Общее правило: подписывайте каноническую сериализацию, а не сырую конкатенацию. Это тот же класс ошибок, что и abi.encodePacked в Solidity с аргументами переменной длины, где различные входные кортежи могут производить идентичные байтовые последовательности. Это применимо к любому подписанному сообщению, составленному из полей переменной длины, и особенно актуально для мостов, где стороны, конструирующие и разбирающие сообщение, работают в разных системах.

Ссылки


Lien Finance

24 июля 2026 года Lien Finance, децентрализованный OTC-протокол облигаций на Ethereum, был взломан приблизительно на $542K в USDC. Первопричиной стал изъян проверки в функции обмена облигаций: она проверяла, что общее число общих облигаций совпадает между входной и выходной группами, но не отслеживала, какие именно облигации были сопоставлены. Это позволило атакующему обойти шаг сжигания для всех входных облигаций при одновременном выпуске необеспеченной облигации, которая затем была продана за реальный USDC через OTC-пулы протокола.

Предыстория

Lien Finance — это DeFi-протокол, выпускающий обеспеченные ETH облигации. Его контракт BondMakerCollateralizedEth позволяет пользователям блокировать ETH в качестве залога для выпуска токенов облигаций. Выплата по облигации определяется fnMap — кусочно-линейной функцией, отображающей цену залога при погашении на сумму выплаты по облигации.

Значимой единицей является группа облигаций. registerNewBondGroup() проверяет, что выплаты по всем облигациям в группе суммируются в цену ETH в каждой точке излома их объединённого fnMap. Группа, удовлетворяющая этому условию, в точности восстанавливает одну единицу залога. Вот почему группы взаимозаменяемы: любые две, удовлетворяющие этому условию, стоят одинаково, и путь конверсии exchangeEquivalentBonds() опирается на эту гарантию.

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

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

Уязвимые контракты — 0xDA6F...BEf0 и 0x8432...7de0.

Когда облигация появляется и во входной, и в выходной группе, её сжигание с немедленным перевыпуском — бесполезная работа. Параметр exceptionBonds называет эти облигации, чтобы обмен мог пропустить оба шага. Функция обеспечивает это единственным счётчиком exceptionCount: он увеличивается на единицу за каждое совпадение при сканировании входной группы и уменьшается на единицу за каждое совпадение при сканировании выходной группы, при этом никогда не фиксируя, какой bondID совпал.

Это означает, что входная группа [BondA, BondB] и выходная группа [BondC, BondA, BondA] с exceptionBonds = [BondA, BondB] проходят проверку. Счётчик достигает 2 на стороне ввода (по одному совпадению для BondA и BondB) и возвращается к 0 на стороне вывода, но оба декремента исходят от двух вхождений BondA. Обмен ничего не сжигает и выпускает только BondC: токены выходной группы создаются без потребления входных данных.

Анализ атаки

Атака разделена на две транзакции: шаг 1 происходит в 0xe8689a...284d0f, а шаги 2-5 — в 0xb96d57...48e0e7.

  • Шаг 1: Атакующий вызвал registerNewBond() без разрешений три раза, определив BondA, BondB и BondC с одним сроком погашения. BondA и BondB — это кредитные плечи, равномерно делящие залог ниже $3 200, где выплаты суммируются в цену ETH, как требует _assertBondGroup(). BondC — это глубоко вне денег LBT со страйком $3 200 при спот-цене $1 870: нулевая внутренняя стоимость, но всё же примерно $7,64/IMT временной стоимости как опцион.

  • Шаг 2: Атакующий зарегистрировал две группы облигаций. registerNewBondGroup([BondA, BondB]) вернул группу 36 (входная), а registerNewBondGroup([BondC, BondA, BondA]) вернул группу 37 (выходная). Нулевая плоская выплата BondC ничего не добавляет к суммам точек излома, поэтому её присутствие не нарушает условие эквивалентности. BondA указан дважды в группе 37, что и заставляет обмен проходить проверку.

  • Шаг 3: Атакующий вызвал exchangeEquivalentBonds(inputGroup = 36, outputGroup = 37, amount = 6968565865905, exceptionBonds = [BondA, BondB]). Сканирование входной группы: BondA и BondB каждый совпадают с исключением, поэтому оба пропускают сжигание, и exceptionCount достигает 2. Сканирование выходной группы: BondC ни с чем не совпадает и выпускается, затем два вхождения BondA каждое совпадает с исключением и уменьшает счётчик до 0.

  • Шаг 4: Атакующий продал выпущенный BondC за USDC через OTC-пулы протокола (GeneralizedDotc), получив 532 144 USDC.

  • Шаг 5: Атакующий повторил тот же паттерн против второго контракта BondMakerCollateralizedEth, доведя общую прибыль до приблизительно 542 144,63 USDC.

Заключение

Механизм exceptionBonds, предназначенный для пропуска перевыпуска общих облигаций, мог быть применён ко всем входным облигациям сразу путём дублирования bondID в выходной группе. Это нарушило инвариант, согласно которому токены облигаций создаются только против внесённого залога через issueNewBonds(). Исправление состоит в отслеживании того, какие именно bondID были сопоставлены (например, с использованием битовой маски или множества), обеспечивая однократное использование каждого исключения в обеих группах.

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

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

Попробуйте бесплатно

О BlockSec

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

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

Sign up for the latest updates
~$1,35 млн потеряно: BarnBridge, DeFiTuna | Еженедельник BlockSec
Security Insights

~$1,35 млн потеряно: BarnBridge, DeFiTuna | Еженедельник BlockSec

Еженедельный отчёт охватывает 2 инцидента (13–19 июля 2026 г.), общий ущерб ~$1,35 млн в сетях Ethereum и Solana. DeFiTuna потеряла ~$570K из-за ошибки проверки позиции; BarnBridge лишилась ~$776K через устаревшую систему управления.

~$800K потеряно: двойное списание в Hinkal | Еженедельник BlockSec
Security Insights

~$800K потеряно: двойное списание в Hinkal | Еженедельник BlockSec

Еженедельный отчёт охватывает 1 инцидент за 29 июня – 5 июля 2026 г.: ~$800K потерь в Ethereum. Протокол Hinkal был атакован через двойное списание из-за уязвимости в устаревшем формате нот, позволявшей извлекать несколько нуллификаторов из одного депозита.

Информационный бюллетень — июнь 2026 г.
Security Insights

Информационный бюллетень — июнь 2026 г.

Отчёт за июнь 2026: три крупнейших инцидента с общими потерями ~$22M. Honeypot атака опустошила MEV-бот на ~$15M. Уязвимости Aztec rollup привели к потере ~$4.35M. Ошибка Ed25519 в SecondFi раскрыла ключи 374 кошельков (~$2.4M).

Best Security Auditor for Web3

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

BlockSec Audit