Back to Blog

Глубокий анализ: взлом Balancer V2

Code Auditing
November 5, 2025
7 min read

Обновлено 6 ноября 2025 г.: Balancer опубликовал официальный предварительный отчет [6], который подтверждает первопричину, выявленную в нашем анализе.

3 ноября 2025 г. Composable Stable Pools протокола Balancer V2, а также несколько форкнутых проектов в различных блокчейн-сетях подверглись скоординированной атаке, которая привела к общим убыткам на сумму более 125 миллионов долларов. Компания BlockSec выпустила предупреждение сразу же после обнаружения [1] и впоследствии опубликовала первоначальный анализ [2].

Это была крайне изощренная атака. Наше расследование показывает, что первопричиной стало манипулирование ценой в результате потери точности при вычислении инварианта, что, в свою очередь, исказило расчет цены BPT (Balancer Pool Token). Эта манипуляция инвариантом позволила злоумышленнику получить прибыль от конкретного стабильного пула с помощью одного пакетного обмена (batch swap). Хотя некоторые исследователи предложили свое видение ситуации, ряд интерпретаций вводит в заблуждение, а первопричина и процесс атаки до сих пор не были полностью прояснены. Цель этой статьи — представить всесторонний и точный технический анализ инцидента.

Основные выводы (TL;DR)

Первопричина: несоответствие правил округления и потеря точности

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

Исполнение эксплойта

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

Операционное влияние и последствия

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

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

0x1 Справочная информация

Composable Stable Pool протокола Balancer V2

Компонентом, затронутым в ходе этой атаки, был Composable Stable Pool [4] протокола Balancer V2. Эти пулы предназначены для активов, которые, как ожидается, будут поддерживать паритет 1:1 (или торговаться по известному обменному курсу), и позволяют проводить крупные свопы с минимальным влиянием на цену, что значительно повышает эффективность капитала для однородных или коррелирующих активов. Каждый пул имеет собственный токен пула Balancer (BPT), который представляет долю поставщика ликвидности в пуле, а также соответствующие базовые активы.

  • В этом пуле используется Stable Math (основанная на модели StableSwap от Curve), где инвариант D представляет виртуальную общую стоимость пула.
  • Цену BPT можно приблизительно выразить как:

Из приведенной выше формулы видно, что если D можно сделать меньше «на бумаге» (даже без фактической потери средств), цена BPT будет казаться ниже.

batchSwap() и onSwap()

Balancer V2 предоставляет функцию batchSwap(), которая позволяет выполнять многоходовые обмены в рамках Vault [5]. Существует два типа обмена, определяемых параметром, передаваемым в эту функцию:

  • GIVEN_IN ("Given In"): вызывающая сторона указывает точное количество входного токена, а пул рассчитывает соответствующее выходное количество.
  • GIVEN_OUT ("Given Out"): вызывающая сторона указывает желаемое выходное количество, а пул вычисляет требуемый входной объем.

Обычно batchSwap() состоит из нескольких обменов токенов, выполняемых через функцию onSwap(). Ниже приведен путь исполнения, когда SwapRequest назначается тип обмена GIVEN_OUT (обратите внимание, что ComposableStablePool наследуется от BaseGeneralPool):

Ниже показан расчет amount_in для типа обмена GIVEN_OUT, который включает в себя инвариант D.

Масштабирование и округление

Для нормализации расчетов между различными балансами токенов Balancer выполняет две следующие операции:

  • Масштабирование вверх (Upscaling): масштабирование балансов и сумм до унифицированной внутренней точности перед выполнением расчетов.
  • Масштабирование вниз (Downscaling): преобразование результатов обратно к их исходной точности с применением направленного округления (например, входные суммы обычно округляются вверх, чтобы гарантировать, что с пула не возьмут меньше положенного, в то время как выходные суммы часто округляются вниз).
Очевидно, что масштабирование вверх и вниз теоретически являются парными операциями — умножением и делением соответственно. Однако в реализации этих двух операций существует несоответствие. В частности, операция масштабирования вниз имеет два варианта или направления: divUp и divDown. Напротив, операция масштабирования вверх имеет только одно направление, а именно mulDown.

Причина этого несоответствия неясна. Согласно комментарию в функции _upscale(), разработчики считают, что влияние округления в одном направлении минимально.

// Округление при масштабировании вверх не обязательно всегда должно идти в одном направлении: например, в свопе баланс входящего // токена должен быть округлен вверх, а выходящего — вниз. Это единственное место, где мы округляем все суммы // в одном направлении, так как ожидается, что влияние этого округления минимально (и ошибки округления не возникнет, если _scalingFactor() не переопределен).

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

Основная проблема возникает из-за операции округления вниз, выполняемой во время масштабирования вверх в функции BaseGeneralPool._swapGivenOut(). В частности, _swapGivenOut() некорректно округляет вниз swapRequest.amount через функцию _upscale(). Полученное округленное значение впоследствии используется как amountOut при расчете amountIn через _onSwapGivenOut(). Такое поведение противоречит стандартной практике, согласно которой округление должно применяться таким образом, чтобы это было выгодно протоколу.

Таким образом, для данного пула (wstETH/rETH/cbETH) вычисленное amountIn недооценивает фактически требуемый входной объем. Это позволяет пользователю обменять меньшее количество одного базового актива (например, wstETH) на другой (например, cbETH), тем самым уменьшая инвариант D в результате снижения эффективной ликвидности. Следовательно, цена соответствующего BPT (wstETH/rETH/cbETH) становится заниженной, поскольку цена BPT = D / totalSupply.

0x3 Анализ атаки

Злоумышленник выполнил двухэтапную атаку, вероятно, для минимизации риска обнаружения:

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

Первый этап можно разделить на две фазы: расчет параметров и пакетный обмен. Ниже мы иллюстрируем эти фазы на примере транзакции атаки (TX) в сети Arbitrum.

Фаза расчета параметров

На этой фазе злоумышленник объединил офчейн-расчеты с ончейн-симуляциями, чтобы точно настроить параметры каждого «прыжка» в следующей фазе (пакетного обмена), основываясь на текущем состоянии Composable Stable Pool (включая коэффициенты масштабирования, коэффициент усиления, ставку BPT, комиссии за своп и другие параметры). Примечательно, что злоумышленник также развернул вспомогательный контракт для помощи в этих расчетах, что, возможно, было сделано для уменьшения вероятности фронтраннинга.

Вначале злоумышленник собирает базовую информацию о целевом пуле, включая коэффициенты масштабирования каждого токена, параметр усиления, ставку BPT и процент комиссии за своп. Затем они вычисляют ключевое значение под названием trickAmt — манипулируемое количество целевого токена, используемое для вызова потери точности.

Обозначая коэффициент масштабирования целевого токена как sF, расчет выглядит так:

Чтобы определить параметры, используемые на шаге 2 следующей фазы (пакетного обмена), злоумышленник выполнил последующие вызовы симуляции функции 0x524c9e20 вспомогательного контракта со следующими calldata:

uint256[] balances; // Балансы токенов пула (исключая BPT)
uint256[] scalingFactors; // Коэффициенты масштабирования для каждого токена пула
uint tokenIn; // Индекс входного токена для симуляции этого этапа
uint tokenOut; // Индекс выходного токена для симуляции этого этапа
uint256 amountOut; // Желаемое количество выходного токена
uint256 amp; // Параметр усиления (amplification) пула
uint256 fee; // Процент комиссии пула за своп

И возвращаемые данные:

uint256[] balances; // Балансы токенов пула (исключая BPT) после обмена

В частности, начальный баланс и количество итераций цикла были вычислены вне сети и переданы в качестве параметров контракту злоумышленника (сообщается о значениях 100 000 000 000 и 25 соответственно). Каждая итерация выполняет три свопа:

  • Своп 1: Доведение суммы целевого токена до trickAmt + 1, при условии, что направление свопа 0 → 1.
  • Своп 2: Продолжение вывода целевого токена с суммой trickAmt, что вызывает округление вниз при вызове _upscale().
  • Своп 3: Выполнение операции обратного обмена (1 → 0), где количество, подлежащее обмену, выводится из текущего баланса токена в пуле путем отсечения двух самых значимых десятичных цифр, то есть округления вниз до ближайшего значения, кратного 10d210^{d-2}, где d — количество десятичных цифр. Например, 324 816 -> 320 000.
    • Обратите внимание, что этот шаг иногда может завершаться неудачно из-за метода Ньютона-Рафсона, используемого в расчетах StableMath. Чтобы смягчить это, злоумышленник применяет две попытки повтора, каждая из которых использует 9/10 от исходного значения. Вспомогательный контракт злоумышленника основан на библиотеке StableMath от Balancer V2, что подтверждается наличием специфических сообщений об ошибках в стиле "BAL".

Фаза пакетного обмена (Batch Swap Phase)

Затем операцию batchSwap() можно разделить на три шага:

  • Шаг 1: Злоумышленник меняет BPT (wstETH/rETH/cbETH) на базовые активы, чтобы точно скорректировать баланс одного токена (cbETH) до границы округления (amount = 9). Это создает условия для потери точности на следующем шаге.

  • Шаг 2: Затем злоумышленник выполняет обмен между другим базовым активом (wstETH) и cbETH, используя подготовленную сумму (= 8). Из-за округления вниз при масштабировании суммы токенов, вычисленное Δx становится немного меньше (8,918 -> 8), что приводит к недооцененному Δy и, следовательно, к меньшему инварианту (D из модели StableSwap от Curve). Поскольку цена BPT = D / totalSupply, цена BPT искусственно занижается.

  • Шаг 3: Злоумышленник совершает обратный своп базовых активов обратно в BPT, восстанавливая баланс и получая прибыль от искусственно заниженной цены BPT.

0x4 Атаки и убытки

Мы суммировали атаки и соответствующие им убытки в таблице ниже, при этом общие убытки превысили 125 миллионов долларов.

0x5 Заключение

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

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

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

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

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

Ссылки

[1] https://x.com/Phalcon_xyz/status/1985262010347696312

[2] https://x.com/Phalcon_xyz/status/1985302779263643915

[3] https://x.com/Balancer/status/1985390307245244573

[4] https://docs-v2.balancer.fi/concepts/pools/composable-stable.html

[5] https://docs-v2.balancer.fi/reference/swaps/batch-swaps.html

[6] https://x.com/balancer/status/1986104426667401241

Best Security Auditor for Web3

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

BlockSec Audit