Команда BlockSec (@BlockSecTeam)

На прошлой неделе в протоколе Compound была обнаружена ошибка, которая «случайно» отправляла пользователям большое количество токенов COMP. Причиной этой ошибки (ошибка 2 в этом блоге) стало неверное исправление другой ошибки (ошибка 1 в этом блоге), обнаруженной ранее.
В этом блоге мы подробно разберем первопричину первой ошибки и объясним, почему исправление первой ошибки привело ко второй.
Предыстория
Протокол Compound основан на техническом документе (Whitepaper) Compound. Через контракты cToken учетные записи в блокчейне предоставляют капитал (Ether или токены ERC-20), чтобы получать cToken или заимствовать активы из протокола (используя другие активы в качестве залога). Контракты cToken протокола Compound отслеживают эти балансы и алгоритмически устанавливают процентные ставки для заемщиков.
Чтобы стимулировать пользователей, те, кто обеспечивает ликвидность Compound (предоставляет капитал), могут получать проценты. В частности, пользователи предоставляют активы (например, Ether или другие токены ERC20) в Compound и получают соответствующие cToken. Когда cToken возвращаются в Compound, базовые активы (Ether или токены ERC20) и проценты возвращаются пользователю, если у него нет задолженности в Compound. Например, если у пользователя есть 1000 Ether, он может внести этот актив в Compound через cEth.mint(1000), чтобы получить cToken.

cToken представляет собой базовые активы, заблокированные в Compound. Пользователь может в дальнейшем использовать cToken в качестве залога для заимствования других активов. Например, пользователь может внести 1000 Ether через ceth.mint(1000), а затем использовать полученные cToken для заимствования x Dai на сумму 75 Ether (сверхзалоговое обеспечение — это число зависит от коэффициента обеспечения) через cDai.borrow(x).
Основная логика реализована в контракте Comptroller. Он поддерживает состояния пользователя, например, сколько токенов было внесено пользователем в Compound, сколько токенов было заимствовано и может ли пользователь заимствовать дополнительные токены. Функции, вызываемые в этом процессе, включают getHypotheticalAccountLiquidityInternal(), borrowAllowed(), mintAllowed() и другие.
В Compound также есть токен управления под названием COMP. Токен COMP можно использовать для голосования по предложениям. Кроме того, токен COMP торгуется на биржах. В настоящее время цена COMP составляет около $300.
Ошибка 1
31 сентября 2021 года в Compound DAO было внесено новое предложение (Предложение 62), целью которого было исправление ошибки в Comptroller.

Ошибка связана с CompSpeed, который представляет собой количество токенов COMP, распределяемых пользователям в каждом блоке.
Поток функции mint
Далее мы будем использовать функцию mint, чтобы описать причину этой ошибки. Цепочка вызовов функции mint: mint → mintInternal → mintFresh.

В функции mintFresh вызывается mintAllowed, а затем обновляется баланс cToken пользователя.

В функции mintAllowed сначала вызывается updateCompSupplyIndex, а затем distributeSupplierComp, чтобы 1) обновить compSupplyState рынка и 2) распределить токены COMP среди пользователей.
updateCompSupplyIndex

Функция updateCompSupplyIndex обновляет статус каждого рынка, главным образом compSupplyState[cToken].

В структуре CompMarketState записывается номер блока (block) этого обновления и индекс бонуса (index), который влияет на количество токенов COMP, подлежащих распределению среди пользователей (владельцев cToken).
Что такое индекс бонуса (index) для каждого токена? Это значение, накапливаемое с течением времени (показано в следующей формуле).

Это показывает количество COMP, которое должно быть распределено среди пользователей (для каждого cToken, которым владеет пользователь).
distributeSupplierComp
Другая функция, distributeSupplierComp, отвечает за запись количества токенов COMP, которые должны быть распределены пользователю (поставщику) в compAccrued[supplier].

В частности, она обновляет глобальный бонусный индекс в compSupplyState (в функции updateCompSupplyIndex). Затем в функции distributeSupplierComp значение supplyIndex записывает текущий бонусный индекс, а supplierIndex показывает последний бонусный индекс для пользователя (поставщика). Дельта-значение (supplyIndex - supplierIndex) * баланс cToken пользователя показывает количество токенов COMP, которые должны быть распределены пользователю.
Причина ошибки 1
Существует еще одна функция setCompSpeed для настройки supplySpeed рынка (compSpeeds[address[cToken]]).

Это связано с тем, что если мы установим CompSpeed для рынка на ноль, это будет означать, что токен COMP не будет распределяться среди пользователей на этом рынке. Поэтому, если мы хотим сначала отключить распределение COMP для рынка, а затем снова его включить, мы можем выполнить следующие шаги:
- Шаг I: Установите
CompSpeed[cToken]равным нулю, чтобы отключить распределение токенов COMP. - Шаг II: Вызовите функцию
setCompSpeed, чтобы установитьCompSpeed[cToken]на ненулевое значение.

Шаг I: Для рынков, для которых было отключено распределение токенов COMP на шаге I (supplySpeed == 0), блок не равен нулю, так как блок постоянно обновляется в updateCompSupplyIndex (else if (deltaBlocks > 0)).

Шаг II: При выполнении операции на шаге II функция setCompSpeedInternal пройдет через оператор else if (compSpeed != 0) (строка 1083). Затем в строках 1088–1093 есть проверка if (compSupplyState[address(cToken)].index == 0 && compSupplyState[address(cToken)].block == 0) для инициализации index и block для нового рынка. Однако, поскольку мы повторно включаем распределение токенов COMP на существующем рынке (а не на новом), операторы в строках 1090 и 1091 не будут выполнены для инициализации index и block (поскольку compSupplyState[address(cToken)].block не равен нулю).
Таким образом, для отключенного в данный момент рынка index равен нулю. Однако block не равен нулю. Это означает, что когда мы включаем отключенный рынок, вызывая setCompSpeed для установки ненулевого значения CompSpeed[cToken], значение индекса НЕ будет повторно инициализировано значением CompInitialIndex (1e36) (строки 1090 и 1091 не выполняются).
Влияние ошибки 1
Мы глубже изучили функцию distributeSupplierComp, которая отвечает за распределение токенов COMP.

supplierIndex является compInitialIndex. Однако supplyIndex все еще равен нулю из-за ошибки, что приведет к переполнению (underflow) при вычислении Double memory deltaIndex = sub_(supplyIndex=0, supplierIndex=1e36).

Ошибка 2: Вызвана исправлением ошибки 1
Чтобы исправить ошибку, владелец проекта изменил логику кода. В частности, он немедленно инициализирует index значением compInitialIndex при инициализации нового рынка.

Поскольку глобальный бонусный индекс (index) был инициализирован значением compInitialIndex, индекс бонуса пользователя также должен быть инициализирован этим значением. Давайте взглянем на функцию distributeSupplierComp.

Условие if в строке 1234 не может быть выполнено, даже если supplierIndex == 0, поскольку supplyIndex равен (а не больше) compInitialIndex (1e36). Это приводит к тому, что supplierIndex НЕ инициализируется должным образом значением compInitialIndex (его значение равно 0). Тогда deltaIndex (supplyIndex - supplierIndex) будет равен compInitialIndex, а не нулю. Значение supplierTokens станет очень большим, если баланс cToken пользователя не равен нулю.
Таким образом, если пользователь случайно выполнил операцию mint до исправления ошибки 1, то у него есть cToken, и supplierIndex станет равным нулю (поскольку токен COMP был распределен). Затем после исправления ошибки 1 (которое привносит ошибку 2), когда пользователь снова вызывает функцию mint, он может получить большое количество токенов COMP (1e36 * ctoken.balanceOf(user)).
Реальная ситуация
Мы показываем затронутые рынки ниже:
0xF5DCe57282A584D2746FaF1593d3121Fcac444dC: cSAI
0x12392F67bdf24faE0AF363c24aC620a2f67DAd86: cTUSD
0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b: cMKR
0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7: cSUSHI
0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c: cAAVE
0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946: cYFI
Пользователь (0xa7b95d2a2d10028cc4450e453151181cbcac74fc) получает 4,466.542459954989867175 токенов COMP в этой транзакции (0x6416ed016c39ffa23694a70d8a386c613f005be18aa0048ded8094f6165e7308).

Дальнейшая отладка транзакции показывает, что из-за ошибки 2 deltaIndex равен 1e36, и у пользователя в этот момент оказались cToken.



Исправление ошибки 2
Исправление ошибки 2 простое. Оно меняет условие if в функции distributeSupplierComp.

Уроки
- Это ошибка, вызванная исправлением другой ошибки. Вопрос о том, как тщательно проверять изменения кода для крупных проектов, остается открытым.
- DAO может устранить риск централизации. Однако это также делает процесс реагирования на инциденты безопасности медленным.
- Известные DeFi-проекты могут внедрять передовые методы обеспечения безопасности, используемые в традиционном ПО, например, развертывание эффективной системы фаззинга с непрерывным процессом тестирования.
О компании BlockSec
BlockSec — это компания-новатор в сфере блокчейн-безопасности, основанная в 2021 году группой всемирно известных экспертов по безопасности. Компания стремится повысить безопасность и удобство использования для развивающегося мира Web3, чтобы способствовать его массовому внедрению. Для этого BlockSec предоставляет услуги по аудиту безопасности смарт-контрактов и сетей EVM, платформу Phalcon для разработки систем безопасности и проактивного блокирования угроз, платформу MetaSleuth для отслеживания и расследования транзакций, а также расширение MetaSuites для эффективной работы Web3-пользователей в криптомире.
На сегодняшний день компания обслужила более 300 уважаемых клиентов, таких как MetaMask, Uniswap Foundation, Compound, Forta и PancakeSwap, и привлекла десятки миллионов долларов США в двух раундах финансирования от ведущих инвесторов, включая Matrix Partners, Vitalbridge Capital и Fenbushi Capital.
Официальный сайт: https://blocksec.com/
Официальный аккаунт в Twitter: https://twitter.com/BlockSecTeam



