Back to Blog

Еженедельный обзор инцидентов безопасности Web3 | 9–15 марта 2026 г.

March 18, 2026
20 min read

За прошедшую неделю (09.03.2026 - 15.03.2026) компания BlockSec обнаружила и проанализировала восемь инцидентов, связанных с атаками, с общим оценочным ущербом ~$1,66 млн. В таблице ниже приведена сводка этих инцидентов, а подробный анализ каждого случая представлен в следующих подразделах.

Дата Инцидент Тип Оценочный ущерб
09.03.2026 Инцидент EtherFreakers Ошибка бизнес-логики ~$25 тыс.
10.03.2026 Инцидент Alkemi Ошибка бизнес-логики ~$89 тыс.
10.03.2026 Инцидент MT Ошибка бизнес-логики ~$242 тыс.
11.03.2026 Инцидент с ликвидацией AAVE Ошибка конфигурации ~$1,01 млн
11.03.2026 Инцидент Planet Finance Ошибка бизнес-логики ~$10 тыс.
12.03.2026 Инцидент AM Ошибка бизнес-логики ~$131 тыс.
12.03.2026 Инцидент DBXen Ошибка бизнес-логики ~$149 тыс.
15.03.2026 Инцидент Goose Finance Ошибка бизнес-логики ~$8 тыс.

Инцидент EtherFreakers

Краткое резюме

9 марта 2026 года игра EtherFreakers (NFT на базе Ethereum) подверглась атаке из-за неправильного двойного учета, что привело к убыткам в размере ~$25 тыс. Каждый NFT в игре имеет выводимый баланс ETH (так называемую «энергию»). По игровой механике игроки могут использовать функцию attack(), чтобы один NFT захватил другой и присвоил его энергию. Однако контракт выплачивает баланс цели, но переводит NFT до завершения учета. Хук перевода считывает устаревшие данные до выплаты и возвращает часть из них в глобальный пул дивидендов, увеличивая его без обеспечения новым ETH. Злоумышленник зациклил эту механику захвата, чтобы «раздуть» глобальный индекс, а затем вывел инфляционный баланс из партии NFT.

Предыстория

EtherFreakers — это ончейн NFT-игра, где каждый NFT (называемый «Freaker») имеет выводимый баланс ETH под названием energy. Система работает как пул дивидендов: при определенных действиях часть ETH распределяется между всеми Freaker'ами пропорционально. ETH, доступный для получения каждым Freaker'ом, отслеживается глобальным аккумулятором freakerIndex в сочетании с весовым коэффициентом доли fortune для каждого токена.

Формула учета выглядит следующим образом: energyOf = basic + (freakerIndex - index) * fortune. Значение freakerIndex увеличивается при выполнении _dissipateEnergyIntoPool(amount), распределяя 80% от amount среди всех Freaker'ов и 20% — создателям. Прямые депозиты через charge() увеличивают только basic, не затрагивая freakerIndex. Следовательно, рост freakerIndex всегда должен быть обеспечен реальным Ether, поступающим в систему. Если freakerIndex растет без соответствующего притока ETH, Freaker'ы могут получить больше Ether, чем фактически хранится в контракте.

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

Первопричиной является неверный порядок выполнения команд в контракте EtherFreak (0x3A27...c0f33). При успешном захвате функция attack() выполняет следующие шаги по порядку:

  1. Строка 237: Выплата targetCharge (полная энергия целевого NFT) защитнику как прямой перевод ETH. Энергия теперь потрачена.
  2. Строка 240: Вызов _transfer(defender, capturer, targetId) для перемещения NFT. Внутри _transfer() вызывается хук ERC-721 _beforeTokenTransfer(), который вызывает _dissipateEnergyIntoPool() с 0,1% от energyOf(targetId). Это первый вызов _dissipateEnergyIntoPool(), он считывает устаревшее значение, так как шаг 5 еще не выполнен.
  3. Строка 241: Явный вызов _dissipateEnergyIntoPool(sourceSpent). Это второй вызов, часть нормальной игровой логики.
  4. Строки 244-251: Обновление energyBalances для sourceId и targetId.

Ошибка заключается в шаге 2: поскольку energyBalances[targetId] еще не обновлен, хук по-прежнему видит баланс до выплаты и отправляет часть уже потраченной энергии в пул дивидендов. Прямая выплата ETH на шаге 1 и ввод в пул на шаге 2 используют одну и ту же энергию, «раздувая» freakerIndex без обеспечения новым ETH.

freakerIndex растет всякий раз, когда вызывается _dissipateEnergyIntoPool():

Анализ атаки

Анализ основан на транзакции 0x89e24d...9abd2942.

  • Шаг 1: Заем 1 700 WETH через флеш-кредит.

  • Шаг 2: Минтинг двух новых Freaker'ов (токены 590 и 591) на подконтрольные адреса.

  • Шаг 3: Многократный вызов игровой функции attack(590, 591) с сохранением успешных захватов.

  • Шаг 4: После каждого успеха токен 591 передается обратно помощнику, чтобы ту же пару можно было использовать снова.

  • Шаг 5: Каждый успешный цикл увеличивает freakerIndex сверх реального количества Ether, сохраненного системой.

  • Шаг 6: Как только индекс становится достаточно высоким, выполняется сброс («discharge») партии ранее контролируемых Freaker'ов. Токены с 496 по 520 были сброшены по 0,278052246002402082 Ether каждый.

  • Шаг 7: Обертывание выведенного Ether в WETH, погашение флеш-кредита в 1 700 WETH и получение прибыли в размере около 7,498 WETH.

Заключение

Причина кроется в потоке успешного захвата attack(): EtherFreakers выплачивает targetCharge до того, как состояние энергии токена цели будет определено. Затем _transfer() инициирует _beforeTokenTransfer(), который считывает устаревший баланс energyOf(targetId) до выплаты и рассеивает его часть в пул. Это увеличивает freakerIndex без обеспечения новым Ether, поэтому одна и та же энергия цели засчитывается одновременно как выплата и как ввод в пул. Это ошибка инфляции бизнес-логики, а не ошибка повторного входа (reentrancy).

Чтобы снизить подобные риски в будущем:

  • Избегайте пересчета экономических значений из изменяемого состояния внутри хуков передачи, пока транзакция еще не завершена.

  • Если хук передачи считывает переменную состояния, убедитесь, что порядок выполнения не влияет на результат (например, зафиксируйте состояние до запуска хука, а не после).


Инцидент Alkemi

Краткое резюме

10 марта 2026 года протокол Alkemi на Ethereum подвергся атаке, что привело к убыткам в размере ~$89 тыс. Первопричиной стала ошибка учета и дефектная бизнес-логика. Неисправная логика ликвидации позволяла любому пользователю ликвидировать свою собственную позицию в рамках одной транзакции и получать от этого прибыль. Кроме того, ошибка учета приводила к перезаписи суммы вычета собственного залога атакующего при ликвидации, что позволяло получать награды за ликвидацию без несения соответствующих расходов.

Предыстория

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

  1. Текущий баланс займа заемщика (currentBorrowBalance_TargetUnderwaterAsset).
  2. Максимальное погашение, которое может покрыть залог заемщика после применения скидки при ликвидации (calculateDiscountedBorrowDenominatedCollateral()).
  3. Сумма погашения, необходимая для приведения счета обратно к границе ликвидации (calculateDiscountedRepayToEvenAmount()), проверяется только тогда, когда актив isSupported.

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

Первопричиной являются дефектная бизнес-логика и ошибка учета в протоколе Alkemi (0x4822...a888). Поскольку currentBorrowBalance_TargetUnderwaterAsset обязательно больше 0, пока у заемщика есть непогашенный долг, а значение, возвращаемое calculateDiscountedBorrowDenominatedCollateral(), также обязательно больше 0 при наличии залога, протокол AlkemiEarnPublic эффективно полагается на calculateDiscountedRepayToEvenAmount() для определения возможности ликвидации. В этой функции сумма долга, подлежащая ликвидации, должна рассчитываться на основе переменной accountShortfall_TargetUser.

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

Кроме того, в функции liquidateBorrow() при совпадении адресов ликвидатора и заемщика переменные supplyBalance_TargetCollateralAsset и supplyBalance_LiquidatorCollateralAsset указывают на одну и ту же ячейку хранилища. Функция вычисляет «уменьшенный баланс» и «баланс с наградой» по отдельности на основе одного начального баланса, а затем последовательно записывает их обратно в одну и ту же ячейку. Поскольку уменьшенный баланс записывается первым, а баланс с наградой — вторым, эффект уменьшения перезаписывается и теряется, оставляя только результат с наградой. Это позволяет атакующему еще больше увеличить прибыль.

Анализ атаки

Анализ основан на транзакции 0xa170...6d9d.

  • Шаг 1: Атакующий берет флеш-кредит в 51e18 WETH у Balancer.

  • Шаг 2: Атакующий распаковывает 51e18 WETH в ETH и предоставляет их в протокол Alkemi.

  • Шаг 3: Атакующий занимает 39.5e18 ETH у Alkemi.

  • Шаг 4: Атакующий ликвидирует свою собственную позицию, используя 39.5395e18 ETH.

  • Шаг 5: Атакующий выводит 93.5e18 ETH из Alkemi.

  • Шаг 6: Атакующий погашает флеш-кредит и получает прибыль в 43.4e18 ETH.

Заключение

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

Чтобы снизить подобные риски в будущем:

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

Инцидент MT

Краткое резюме

10 марта 2026 года MT Token, дефляционный токен в сети BNB Chain, подвергся атаке с убытком ~$242 тыс. Первопричиной стала ошибочная логика торговых ограничений в сочетании с непоследовательной обработкой особых условий перевода. Во время дефляционной фазы контракт блокирует покупку, если резервы пула превышают фиксированный порог. Однако контракт обрабатывает переводы точных сумм (например, 2e17 MT) как действия по привязке реферала, позволяя атакующему обойти ограничение на покупку. Кроме того, логика ограничений полагается на неполное обнаружение пути (isBuy) и не охватывает косвенные маршруты обмена, такие как «Пар – Маршрутизатор» (Pair to Router), а белые списки дополнительно прерывают критические проверки. Атакующий накопил токены MT, не вызывая ограничений или комиссий, манипулировал pendingBurnAmount и заставил пул перейти в ненормальное состояние, при котором цена токена была искусственно завышена.

Предыстория

MT Token — дефляционный токен в BNB Chain со встроенными торговыми ограничениями. Во время дефляционной фазы контракт блокирует покупки, если резерв MT в пуле превышает 21 000e18. Как только резерв падает ниже этого порога, дефляционная фаза заканчивается и покупки возобновляются. MT Token также включает реферальный механизм: перевод ровно 2e17 MT или 1e17 MT рассматривается как действие по привязке реферала, а не как обычная торговля.

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

Первопричиной стал дефектный дизайн ограничения покупки в контракте MT (0x037E...b449). В нормальных условиях атакующие не должны иметь возможности приобрести MT в качестве стартового капитала во время ограниченной фазы. Однако контракт рассматривает перевод ровно 2e17 MT как действие по привязке, что позволяет купить 2e17 MT в обход ограничения.

Кроме того, торговое ограничение опирается на ветку isBuy для блокировки покупок, но оно не охватывает путь «Пар – Маршрутизатор». Поскольку и Маршрутизатор, и Пара являются адресами из белого списка, такие переводы проходят проверку белого списка и никогда не доходят до логики ограничения покупки, позволяя атакующему приобретать MT путем перенаправления покупок на Маршрутизатор и последующего извлечения токенов путем удаления ликвидности.

Анализ атаки

Анализ основан на транзакции 0xfb57...fca6.

  • Шаг 1: Атакующий взял флеш-кредит на ~358 681e18 WBNB.

  • Шаг 2: Атакующий купил 2e17 MT, обойдя ограничение покупки.

  • Шаг 3: Атакующий предоставил 4e12 WBNB и 2e17 MT в пару для добавления ликвидности. Этот перевод обошел логику взимания комиссии по той же причине.

  • Шаг 4: Атакующий купил ~10 000 000e18 MT у пары до Маршрутизатора, тем самым обойдя ограничение покупки и логику комиссии.

  • Шаг 5: Атакующий удалил половину своей ликвидности, извлекая все токены MT, удерживаемые Маршрутизатором, а затем продал восстановленные MT за WBNB. На этом этапе pendingBurnAmount был манипулирован до приблизительно 9 000 000e18.

  • Шаг 6: Атакующий снова купил ~10 000 000e18 MT, снизив резерв MT в пуле до ~6 756 516e18, что было ниже pendingBurnAmount.

  • Шаг 7: Атакующий удалил остаток ликвидности, вывел купленные MT и вызвал distributeDailyRewards(), чтобы сжечь MT из пула. В результате резерв MT снизился до 21 000e18.

  • Шаг 8: Атакующий обменял все MT обратно на ~1 198e18 WBNB, погасил флеш-кредит и закрепил прибыль.

Заключение

Этот взлом был вызван некорректными торговыми ограничениями, которые позволили обойти запрет на покупку. После получения MT атакующий смог обойти логику комиссии и защиты, сначала добавив ликвидность, затем купив MT через маршрутизатор и, наконец, удалив ликвидность для извлечения токенов. Последующая манипуляция pendingBurnAmount привела пул в ненормальное состояние, позволив продать MT по завышенной цене.

Чтобы снизить подобные риски в будущем:

  • Обеспечьте строгое разделение между семантикой перевода и торговой логикой.

Инцидент с ликвидацией AAVE

Краткое резюме

11 марта 2026 года AAVE столкнулся с некорректными ликвидациями на $21 млн в сети Ethereum, что привело к убытку в ~$1,01 млн. Первопричиной стала неверная цена оракула для wstETH, из-за чего изначально здоровые позиции стали недостаточно обеспеченными. В результате позиции пользователей были ликвидированы, что привело к финансовым потерям.

Предыстория

AAVE использует адаптеры оракулов для оценки обернутых активов, таких как wstETH. Адаптер CAPO (Capped Price Oracle) выводит цену wstETH, умножая базовую цену ETH/USD на коэффициент конверсии (getRatio(), то есть сколько ETH стоит один wstETH). Чтобы предотвратить манипулирование коэффициентом, CAPO применяет ограничение роста на основе моментальных снимков:

maxRatio = snapshotRatio + maxGrowthPerSecond x (currentTime - snapshotTimestamp)

и «зажимает» (clamp) результат getRatio() при оценке (если текущий коэффициент > maxRatio, он использует maxRatio). Этот механизм эффективно ограничивает максимальный восходящий дрейф коэффициента и итоговой цены.

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

Первопричиной стало несоответствие времени и коэффициента в конфигурации привязки оракула CAPO (0xe1D9...61Ef): метка времени снимка и коэффициент были установлены, но коэффициент снимка был настроен ниже истинного соотношения wstETH/ETH. В результате вычисленный адаптером maxRatio оказался ниже реального коэффициента, что привело к занижению цены оракула wstETH/USD. Эта недооценка залога снизила коэффициент здоровья позиций, использующих wstETH в качестве обеспечения, из-за чего здоровые счета были ошибочно классифицированы как нездоровые и ликвидированы.

Анализ атаки

Анализ основан на транзакции 0x9064...8a9c.

  • Шаг 1: Ликвидатор взял флеш-кредит на ~6304e18 WETH и ликвидировал заемщика.

  • Шаг 2: Ликвидатор погасил флеш-кредит, завершив ликвидацию.

Заключение

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

Чтобы снизить подобные риски в будущем:

  • Обеспечьте проверку критических параметров перед каждым обновлением.

  • Добавьте в реализацию проверки, чтобы отклонять неверные параметры.


Инцидент Planet Finance

Краткое резюме

11 марта 2026 года протокол Planet Finance в сети BNB Chain был взломан, ущерб составил около $10 тыс. Первопричиной стало то, что протокол ошибочно учитывал рост записанного баланса займа как начисленные проценты, что позволяло атакующему неоднократно занимать средства и инициировать расчет скидки для занижения записанного долга.

Предыстория

Planet Finance — протокол кредитования, позволяющий заемщикам погашать долг со скидкой. Скидка является уровневой и определяется соотношением между ставками GAMMA пользователя и стоимостью его ставок в других активах: чем выше это соотношение, тем больше скидка. График скидок включает три уровня, от 0% (минимум) до 50% (максимум).

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

Первопричиной стало то, что при расчете скидки заемщика в changeUserBorrowDiscount() протокол (0x4c9E...F467) ошибочно принимал увеличение записанного баланса займа заемщика за новые начисленные проценты. В результате скидка, предназначенная только для начисленных процентов, неправильно применялась к основному телу займа, неоправданно уменьшая записанный долг. Атакующий мог неоднократно выполнять цикл borrow -> changeUserBorrowDiscount для накопления чрезмерных скидок, из-за чего ончейн-задолженность была постоянно ниже реальной суммы займа, что в конечном итоге позволяло извлекать прибыль.

Анализ атаки

Анализ основан на транзакции 0x5f45...5ec9.

  • Шаг 1: Атакующий взял флеш-кредит на 200 000e18 USDT.

  • Шаг 2: Атакующий использовал 5 000e18 USDT для покупки WBNB, а затем использовал их для покупки ~8 726 524e18 GAMMA.

  • Шаг 3: Атакующий сначала разместил всю GAMMA на рынке gGAMMA, затем предоставил оставшиеся USDT в качестве залога, что увеличило скидку на погашение до 5% и позволило последующие займы.

  • Шаг 4: Атакующий неоднократно вызывал borrow и updateUserDiscount для постоянного уменьшения записанного долга.

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

Заключение

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

Чтобы снизить подобные риски в будущем:

  • Проводите четкое различие между процентами и новыми займами в кредитном протоколе.

Инцидент AM

Краткое резюме

12 марта 2026 года AM Token, дефляционный токен в сети BNB Chain, подвергся атаке с убытком ~$131 тыс. AM Token реализует дефляционный механизм: каждая продажа инициирует дополнительное сжигание из пула ликвидности, навсегда удаляя токены. Однако сжигание не происходит мгновенно — вместо этого полная сумма продажи записывается как toBurnAmount, а фактическое сжигание откладывается до следующей продажи. Эта задержка создает окно между фиксацией и исполнением, в течение которого атакующий может выкупить AM, чтобы сократить резерв AM в пуле до toBurnAmount. Когда следующая продажа инициирует отложенное сжигание, весь резерв AM уничтожается, доводя цену до экстремального уровня и позволяя атакующему выгодно продать AM.

Предыстория

AM Token — дефляционный токен в BNB Chain. При каждой продаже контракт записывает сумму AM, участвующую в обмене, как toBurnAmount и сжигает её из пула при следующей продаже. По сути, продажи инициируют отложенное сжигание, которое сокращает резерв AM в пуле. Кроме того, перед исполнением сжигания протокол обменивает накопленные totalTokenFee на USDT и распределяет их согласно логике распределения комиссий.

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

Первопричиной стало то, что логика продажи токена (0x27f9...213f) накапливает полную сумму обменянных AM как toBurnAmount и выполняет сжигание только на следующей продаже путем удаления токенов из пары AM/USDT и вызова pair.sync(). Этот дизайн позволяет атакующему манипулировать резервами AM в пуле, искажать ончейн-цену и получать прибыль через арбитраж.

Анализ атаки

Анализ основан на транзакции 0xd0d1...f859.

  • Шаг 1: Атакующий взял флеш-кредит на ~27 265 119e18 USDC и ~361 710e18 WBNB, а затем обменял их на ~100 423 811e18 USDT.

  • Шаг 2: Атакующий обменял ~5 062e18 токенов AM на USDT, что манипулировало записанным контрактом toBurnAmount до ~4 303e18.

  • Шаг 3: Атакующий обменял USDT на AM Token, снизив резерв AM в пуле до ~4 303e18.

  • Шаг 4: Атакующий передал 6 wei AM в пул, инициируя логику сжигания. В результате контракт сжег весь баланс AM из пула, доведя резерв до 0. Обратите внимание: протокол сначала пытается обменять накопленные комиссии на USDT перед сжиганием. Этот путь конвертации комиссий также вызывает логику сжигания. После завершения сжигания и достижения резервом AM значения 0 обмен комиссий терпит неудачу. Поскольку он обернут в try/catch, ошибка не откатывает транзакцию. Вместо этого выполнение продолжается, а накопитель комиссий сбрасывается.

  • Шаг 5: Атакующий вызвал pool.sync() и передал оставшиеся USDT и 1 wei AM в пул. Поскольку оба токена были переданы одновременно, контракт воспринял это как addLiquidity, поэтому toBurnAmount не накопился. Резерв AM был обновлен до 7.

  • Шаг 6: Атакующий обменял оставшиеся AM на USDT. Во время этого обмена перевод AM в пару вызвал логику сжигания, снизив резерв AM до 1. Кроме того, так как totalFeeAmount был сброшен в шаге 4, конвертация комиссий больше не выполнялась, позволяя атакующему продать AM по искусственно завышенной цене.

  • Шаг 7: Атакующий погасил флеш-кредит и получил прибыль.

Заключение

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

Чтобы снизить подобные риски в будущем:

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

Инцидент DBXen

Краткое резюме

12 марта 2026 года протокол DBXen (burn-to-earn) в сетях Ethereum и BNB Chain был взломан с общим ущербом ~$149 тыс. Первопричиной стало несоответствие между _msgSender() и msg.sender. Когда burnBatch() вызывается через forwarder, сожженная сумма XEN записывается на _msgSender() (подконтрольный атакующему), но записи циклов обновляются на msg.sender (forwarder). Этот разрыв позволяет атакующему требовать награды и комиссии по устаревшим записям циклов, что приводит к аномально крупным выплатам.

Предыстория

DBXen — протокол типа burn-to-earn: пользователи сжигают XEN в обмен на награды DXN и долю накопленных комиссий протокола. Основная механика работает циклами. Когда пользователь вызывает burnBatch(), происходят две вещи: (1) сожженная сумма XEN записывается на адрес вызывающего (_msgSender()), и (2) контракт XEN вызывает DBXen через onTokenBurned(), чтобы обновить записи циклов вызывающего (текущий цикл сжигания и цикл последнего обновления комиссий).

Награды и комиссии рассчитываются через updateStats(). Награда пропорциональна доле пользователя в общем объеме сожженного XEN в цикле сжигания. Комиссия основана на накопленных комиссиях протокола, начисленных с момента последнего цикла пользователя. Оба расчета зависят от актуальности записей циклов.

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

Первопричина — дефектная бизнес-логика в протоколе DBXen (0xf5c8...2abd). Функция _msgSender() проверяет, является ли msg.sender forwarder'ом. Если да, она возвращает последние 20 байт calldata, и это значение может быть произвольно задано в контексте forwarder. Однако burnBatch() напрямую сжигает XEN, удерживаемый msg.sender. В результате атакующий может вызвать burnBatch() через forwarder, заставляя протокол сжечь XEN, удерживаемый forwarder'ом, и обновить записи циклов forwarder'а. При этом протокол записывает сожженную сумму на _msgSender().

Затем атакующий вызывает claimFees(), который вызывает updateStats(). Поскольку записи цикла адреса _msgSender() никогда не обновлялись (они остаются 0), updateStats() рассчитывает награды в текущем цикле и комиссии, накопленные с цикла 0, охватывая всю историю комиссий протокола. Атакующий получает прибыль вызовами claimFees() и claimRewards().

Анализ атаки

Анализ основан на транзакции 0x914a5a...b808bc37.

  • Шаг 1: Атакующий зарегистрировал домен в контракте Forwarder.

  • Шаг 2: Атакующий обменял 0.14e18 ETH на 13 900 000 000e18 XEN в пуле Uniswap V2.

  • Шаг 3: Атакующий перевел XEN в контракт Forwarder.

  • Шаг 4: Атакующий использовал Forwarder.execute(), чтобы разрешить DBXen тратить XEN контракта Forwarder.

  • Шаг 5: Атакующий вызвал DBXen.burnBatch() и сжег 13 900 000 000e18 XEN. Сумма была записана на адрес 0x425D3eC2DCeBE2c04bA1687504D43AFC6be7328d, в то время как записи циклов были обновлены на Forwarder.

  • Шаг 6: Атакующий вызвал DBXen.claimFees() и получил 65.36e18 ETH.

  • Шаг 7: Атакующий вызвал DBXen.claimRewards() и получил 2 305.4e18 DXN.

Заключение

Причиной инцидента стало непоследовательное использование _msgSender() и msg.sender. Разрыв в данных позволил злоумышленнику использовать несоответствие в свою пользу.

Чтобы снизить подобные риски в будущем:

  • Используйте _msgSender() последовательно во всех путях логики или удостоверьтесь, что операции, зависящие от msg.sender и _msgSender(), всегда ссылаются на один и тот же адрес.

Инцидент Goose Finance

Краткое резюме

15 марта 2026 года Goose Finance (протокол доходности в BNB Chain) был взломан на $8 тыс. Первопричиной стала ошибка в порядке ценообразования долей (share pricing) в StrategyGooseEgg: deposit() выпускает доли до фиксации «намайненных» наград, поэтому знаменатель активов занижен. Это значит, что вкладчики получают больше долей, чем должны. Когда вызывается withdraw(), он инициирует сбор наград, которые увеличивают полные активы, делая каждую долю дороже. Зацикливая deposit и withdraw в одной транзакции, атакующий многократно минтил переоцененные доли и выкупал их по скорректированной (высшей) стоимости.

Предыстория

Goose Finance — протокол фарминга в BNB Chain, где средства пользователей направляются в стратегию, которая делает стейкинг в MasterChef для получения наград EGG.

Компоненты инцидента:

  • VaultChef: отслеживает позиции пользователей и пересылает капитал в StrategyGooseEgg.
  • StrategyGooseEgg: ведет учет на уровне стратегии с sharesTotal и wantLockedTotal.
  • MasterChef: принимает активы и выплачивает награды EGG.

Ожидание: ценообразование долей должно отражать полные активы стратегии (стейкинг + накопленные награды). В StrategyGooseEgg, однако, deposit() выпускает доли до того, как _farm() фиксирует награды в wantLockedTotal, а withdraw() может инициировать сбор наград из MasterChef.

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

Первопричина — рассинхронизация учета в StrategyGooseEgg (0x0980...b26b) между сбором наград и ценообразованием долей.

Ценообразование использует wantLockedTotal как знаменатель. Чтобы это было честно, он должен отражать все активы, включая праздные награды EGG. Однако deposit() минтит доли до того, как награды учитываются в wantLockedTotal. знаменатель исключает их, из-за чего вкладчик получает больше долей.

Более того, withdraw() вызывает MasterChef.withdraw(), который возвращает стейк плюс награды EGG в стратегию. Учет стратегии только вычитает запрашиваемую _wantAmt из wantLockedTotal, оставляя награды без учета, что делает последующее ценообразование deposit() еще более неточным.

Анализ атаки

Анализ основан на транзакции 0x86efdf...ce316223.

  • Шаг 1: Атакующий берет флеш-кредит EGG из двух пар Pancake.
  • Шаг 2: Первый депозит в VaultChef/StrategyGooseEgg (10 170 000e18 EGG).
  • Шаг 3: Первый вывод (12 593 884e18 EGG) собирает награды из MasterChef; 359 561e18 EGG остаются как бесхозная ценность.
  • Шаг 4: Второй депозит повторно использует выведенный капитал. Доли оцениваются до фиксации праздных наград — это этап переминтинга (over-mint).
  • Шаг 5: Второй вывод (12 826 027e18 EGG) реализует прибыль от завышенных долей (на 232 143 EGG больше депозита в шаге 4).
  • Шаг 6: Атакующий погашает свопы и оставляет прибыль.

Заключение

Инсцидент проистекает из изъяна в порядке ценообразования StrategyGooseEgg: выпуск долей опережает обновление wantLockedTotal, а сбор наград (withdraw()) оставляет их вне учета до следующего депозита.

Чтобы снизить подобные риски в будущем:

  • Фиксируйте награды и обновляйте учет до расчетов минтинга и сжигания долей.
  • Оценивайте доли относительно единого totalAssets (стейк + праздные активы) в момент расчета.

О BlockSec

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