Back to Blog

Мелочи при округлении — огромные убытки: глубокий анализ недавнего инцидента с Balancer

Code Auditing
September 14, 2023
12 min read

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

С точки зрения безопасности этот отчет показывает, что существовало две ошибки. Первая — это ошибка округления в меньшую сторону, которую мы обсуждали в нашем отчете, а вторая — «сброс ставки при нулевом предложении» (resets rate on 0 supply), которая произошла на этапах атаки 3.6 и 3.7, как описано в нашем документе. В отчете Balancer вторая проблема рассматривается как наиболее критическая, а первая — как сопутствующая. Однако мы считаем, что обе ошибки одинаково важны для прибыльной эксплуатации:

  1. Первая ошибка используется для «накачки» курса токена, являясь первопричиной получения прибыли. Без нее получение прибыли было бы неосуществимо.

  2. Вторая ошибка делает эксплойт возможным за счет балансировки долга bb-a-tokens. Без нее атака провалилась бы из-за низкой ликвидности bb-a-tokens, учитывая отсутствие других источников получения этих токенов (если только злоумышленнику не удается получить их каким-либо иным способом).

22 августа 2023 года Balancer публично объявил о наличии критической уязвимости, затрагивающей несколько пулов ликвидности (boosted pools), и призвал пользователей немедленно вывести средства из затронутых пулов. Balancer инициировал процедуры экстренного реагирования, чтобы обезопасить большую часть TVL, однако некоторые средства оставались под угрозой. К сожалению, 27 августа, пять дней спустя, мы заметили несколько атак в основной сети. С тех пор было похищено активов на сумму более $2,12 млн.

На момент написания этого отчета (более чем через три недели после объявления, когда мы считаем, что это безопасно) проект Balancer не выпустил никакого углубленного анализа этой уязвимости. В данном отчете мы стремимся предоставить всесторонний анализ, основанный главным образом на одной из транзакций атаки.

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

  • Наше расследование указывает на то, что первопричиной является манипулирование ценой в результате логики округления в меньшую сторону (rounding down) в linear пуле. Это, в свою очередь, негативно влияет на кэшированный курс токена, используемый соответствующим boosted пулом.
  • Этот инцидент подчеркивает критическую необходимость оперативного уведомления проектов, которые сделали форк на основе уязвимого исходного кода, что действительно представляет собой серьезную проблему для всего сообщества.
  • Многочисленные продолжающиеся атаки подтверждают необходимость проактивного предотвращения угроз, что неизбежно поможет в смягчении потенциальных убытков.

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

0x1 Общие сведения о Balancer

Balancer V2 [1] — это децентрализованный протокол автоматизированного маркет-мейкера (AMM), представляющий собой гибкий строительный блок для программируемой ликвидности. В отличие от других AMM, где учет токенов связан с логикой пула, Balancer отделяет учет и управление токенами от логики пула, что позволяет повысить эффективность свопов за счет сокращения большого количества перемещений токенов.

Balancer поддерживает различные типы пулов. Каждый пул связан с LP-токеном, называемым BPT (Balancer Pool Token). По сути, стоимость BPT рассчитывается на основе общей стоимости всех базовых токенов.

Balancer поддерживает многоходовые свопы, также известные как batch swaps, которые используют лучшие цены из всех пулов, зарегистрированных в хранилище (Vault). В частности, Vault предоставляет функцию batchSwap для облегчения многоходовых обменов.

Flash swap в пулах Balancer устраняет необходимость удерживать какой-либо из входных токенов, традиционно требуемых для выполнения свопа. Вместо этого, обнаружив дисбаланс, вы можете дать указание Vault выполнить обмен и впоследствии получить вознаграждение.

0x1.1 Различные пулы Balancer

Ниже мы кратко представим некоторые концепции пулов, которые имеют отношение к этой уязвимости.

  • Linear Pools (Линейные пулы): Linear пулы [2] — это пулы Balancer, которые облегчают обмен актива и его обернутого аналога, приносящего доход, по известному обменному курсу. Как следует из названия, Linear пулы используют линейную математику. linear пул содержит три токена, включая:

    • два актива, т.е. main (основной) и wrapped (обернутый) токены, которые имеют равную стоимость подлежащего актива;
    • соответствующий BPT (Balancer Pool Token). Обратите внимание, что BPT являются токенами ERC-20.
  • Вложенные линейные пулы (Nesting Linear Pools): BPT линейного пула могут быть вложены в другой пул. Это создает простой путь batchSwap между базовыми активами и токенами во внешнем пуле, поскольку пользователи могут обменивать BPT на один из базовых токенов линейного пула.

  • Composable Stable Pools (Композитные стабильные пулы): Эти пулы [3] предназначены для активов, которые, как ожидается, будут торговаться consistently (последовательно) почти по паритету или по известному обменному курсу. Composable Stable Pools используют стабильную математику, которая позволяет совершать свопы значительного объема без существенного влияния на цену, значительно повышая эффективность капитала для однородных и коррелированных активов.

    Пул является композитным, когда он позволяет совершать обмен с и на собственный LP-токен. Помещение его LP-токена в другие пулы (или "вложение") позволяет легко совершать batchSwap из токенов вложенного пула на токены внешнего пула.

  • Boosted Pools (Усиленные пулы): Boosted пулы [4] разработаны для повышения эффективности капитала простаивающей ликвидности для крупных пулов. Boosted пулы на самом деле являются подклассом других пулов. Например, boosted пул может быть построен поверх linear пулов.

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

0x1.2 Конкретный пример уязвимых Boosted пулов: Balancer Boosted Aave USD

Balancer Boosted Aave USD (символ: bb-a-USD) — это Composable Stable Pool, который облегчает обмен между тремя стейблкоинами (USDC, USDT и DAI), одновременно отправляя простаивающую ликвидность в Aave. Базовыми linear пулами являются:

  • bb-a-USDC (состоит из USDC и обернутого aUSDC)
  • bb-a-USDT (состоит из USDT и обернутого aUSDT)
  • bb-a-DAI (состоит из DAI и обернутого aDAI)

В частности, bb-a-USD представляет собой совокупность одного Composable Stable Pool, который содержит токены трех различных linear пулов, и каждый из этих linear пулов имеет связанный стабильный токен: DAI, USDC и USDT. Рисунок ниже, предоставленный официальной документацией [5], показывает структуру bb-a-USD:

0x1.3 Как рассчитывается цена BPT

Важный вопрос, который возникает естественным образом: как определить цену BPT при обмене определенного количества (т.е. amountIn) BPT на определенное количество (т.е. amountOut) другого токена.

Balancer предоставляет подробное описание математических формул, которые они приняли [6, 7] для различных пулов. Для простоты здесь мы абстрагируем и резюмируем наиболее важные концепции.

Возьмем в качестве примера linear пул; цена BPT рассчитывается в функции onSwap контракта LinearPool.

Расчет можно резюмировать следующим образом:

amountOut=amountIntokenRateamountOut=amountIn*tokenRate

Здесь tokenRate рассчитывается по следующей формуле:

_INITIAL_BPT_SUPPLY\_INITIAL\_BPT\_SUPPLY — это константа: 211212^{112} - 1.

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

Стоит отметить, что балансы всех участвующих токенов должны быть нормализованы перед выполнением расчета, поскольку разные токены могут иметь разное количество знаков после запятой (decimals). В частности, необработанный баланс заданного токена будет умножен на соответствующий коэффициент масштабирования (upscale factor), который определяется функцией _scalingFactors.

(1) Коэффициенты масштабирования linear пулов

И BPT, и основной токен (main) имеют обычный константный коэффициент масштабирования.

(2) Коэффициенты масштабирования boosted пулов, таких как bb-a-USD

Расчет для boosted пула немного сложнее. В частности, возвращаемый коэффициент масштабирования — это произведение необработанного коэффициента (например, 1e18) и курса токена, который получается из кэшированного курса токена, если он имеется.

Откуда берется кэшированный курс токена? Существует приватная функция под названием _updateTokenRateCache. Очевидно, что эта функция сначала получает курс, вызывая функцию getRate этого токена, а затем кэширует его.

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

Обратите внимание, что существует три возможных пути, которые могут вызвать функцию _updateTokenRateCache:

Кроме того, предусмотрена проверка истечения срока действия при выполнении обновлений для путей через функцию onSwap:

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

Первопричина кроется в манипулировании ценой, вызванном логикой округления в меньшую сторону (rounding down) внутри функции onSwap линейного пула (linear pool). Это, в свою очередь, ненадлежащим образом влияет на кэшированный курс токена, используемый boosted пулом.

В частности, amountOut округляется в меньшую сторону при вызове функции _downscaleDown. Таким образом, если существует значительная разница в величинах между amountOut и scalingFactors[indexOut], возвращаемое значение функции _downscaleDown может быть равно нулю.

Например, если мы используем bb-a-USDC (в качестве BPT) для обмена USDC (в качестве main токена) в пуле bb-a-USDC, то когда amountOut меньше 1 000 000 000 000, возвращаемое значение всегда будет округляться до нуля. Это приведет к увеличению баланса bb-a-USDC, так как это может быть расценено как одностороннее добавление ликвидности bb-a-USDC.

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

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

Транзакция атаки состоит из следующих этапов:

  1. Заимствование 300 000 USDC с помощью флэш-кредита (Flashloan) у Aave.
  2. Обмен 1,067753 USDC на 0,970495 aUSDC в пуле bb-a-USDC.
  3. Выполнение batchSwap в пулах bb-a-USDC и bb-a-USD, т.е. сбор 15 628 bb-a-USDC, 139 431 bb-a-DAI и 248 868 bb-a-USDT с помощью 42 203 USDC. Подробные шаги суммированы в следующей таблице (с учетом знаков после запятой):
  1. Обмен LP-токенов на соответствующие базовые стабильные токены:
  • 139 431 bb-a-DAI -> 141 127 DAI в пуле bb-a-DAI
  • 15 628 bb-a-USDC -> 15 685 USDC в пуле bb-a-USDC
  • 248 868 bb-a-USDT -> 253 461 USDT в пуле bb-a-USDT
  1. Возврат флэш-кредита, и итоговая прибыль составляет:
  • 114 324 DAI
  • 253 461 USDT
  • 0,970495 aUSDC

Стоит отметить, что злоумышленник истощил aUSDC за счет USDC из пула bb-a-USDC на этапе 2, что сделало манипулирование ценой на этапе 3 намного проще, т.е. злоумышленнику нужно было сосредоточиться только на USDC и bb-a-USDC.

Здесь этап 3 играет ключевую роль. Теперь давайте углубимся в детали этого этапа, чтобы понять, почему злоумышленник смог получить прибыль. В частности:

  • Шаги 3.1 используются для истощения USDC с помощью bb-a-USDC из пула bb-a-USDC;
  • Шаги 3.3 и 3.4 используются для обмена bb-a-USDC на bb-a-DAI, в то время как шаг 3.5 используется для обмена bb-a-USDC на bb-a-USDT.
  • Шаг 3.7 используется для обмена USDC на bb-a-USDC из пула bb-a-USDC.

Здесь шаги 3.2 и 3.6 не обменивают обратно никакие целевые токены (т.е. USDC) из-за округления в меньшую сторону, обсуждавшегося ранее, поэтому балансы целевых токенов остаются неизменными после обмена, что можно рассматривать как добавление дополнительной ликвидности bb-a-USDC в пул bb-a-USDC.

Очевидно, что аномальные свопы в основном происходят на шагах 3.4, 3.5 и 3.7. Ниже мы подробно разберем каждый из этих шагов по очереди.

(1) bb-a-USDC -> bb-a-DAI

На шаге 3.3 обменный курс между bb-a-USDC и bb-a-DAI почти равен 1, в то время как на шаге 3.4 обменный курс становится 19:

  • Шаг 3.3: 1 000 339 378 515 783 699 / 1 000 000 000 000 000 000 = 1.00
  • Шаг 3.4: 139 430 482 942 020 211 267 110 / 7 300 000 000 000 000 000 000 = 19.10

Вспоминая логику кода, которую мы обсуждали ранее: на шаге 3.3, после возврата предопределенного кэшированного курса токена (1 012 181 365 780 643 700) для расчета коэффициента масштабирования, функция обновляет курс для вычисления нового значения (40 240 000 000 000 000 000). Это обновленное значение затем используется на шаге 3.4 в качестве нового коэффициента масштабирования. Поскольку исходные коэффициенты масштабирования остаются неизменными (т.е. 1e18), это означает, что новый курс примерно в 40 раз больше старого.

Однако, откуда берется этот значительный рост? Давайте вернемся к формуле расчета tokenRate. Поскольку баланс aUSDC был истощен на этапе 2, расчет tokenRate можно упростить следующим образом:

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

(2) bb-a-USDC -> bb-a-USDT

Шаг 3.5 использует ту же уловку, чтобы получить больше bb-a-USDT, и обменный курс между bb-a-USDC и bb-a-USDT составляет более 12:

  • 248 868 905 733 352 246 491 156 / 20 000 000 000 000 000 000 000 = 12.44

(3) USDC -> bb-a-USDC

Кроме того, bptBalance увеличивается на этапе 3.6, затем bptSupply становится равным нулю на этапе 3.7. Поступая таким образом, можно обменять USDC на bb-a-USDC по обменному курсу, который почти равен 1:1.

0x4 Сводка атак и прибылей

На момент написания этой статьи мы наблюдали десятки атак в основной сети, что привело к потерям, превышающим $2,12 млн. В целом, эти атаки были осуществлены с трех разных аккаунтов, следующим образом:

Balancer понес общие убытки в размере ~$1 млн из-за этой уязвимости. Менее чем через 12 часов после первоначальной атаки на Balancer его форк-протокол, Beethoven X, подвергся аналогичным атакам, что привело к предполагаемым убыткам в размере ~$1,1 млн. Beethoven X понес даже большие убытки, чем Balancer! Совокупный ущерб от этого инцидента безопасности составил ~$2,12 млн.

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

Некоторые наблюдения об атакующих

Анализируя транзакции, инициированные в каждой сети, мы обнаружили значительное расхождение в следах транзакций атаки в сети Fantom по сравнению с теми, что были в сетях Ethereum и Optimism.

В частности, помимо заметных различий в ключевых функциях, злоумышленник в сети Fantom использовал две уникальные уловки, чтобы избежать фронтраннинга со стороны MEV-ботов. Более того, средства, использованные для атаки в сети Fantom, были подготовлены за 163 дня до атаки.

Из наблюдений, подробно описанных выше, мы можем сделать вывод:

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

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

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

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

Ссылки

Best Security Auditor for Web3

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

BlockSec Audit