В мае 2021 года мы стали свидетелями нескольких взломов, направленных на продукты BSC DeFi. В частности, была использована лазейка, связанная с майнингом вознаграждений в агрегаторе доходности PancakeBunny, что позволило добыть ~7 млн токенов BUNNY из ничего, что привело к огромным финансовым потерям в размере $45 млн. После кровавого взлома три форка проектов - AutoShark, Merlin Labs и PancakeHunny - были атакованы с помощью аналогичных методов. Команда Amber Group по безопасности блокчейна, возглавляемая доктором Чиачи Ву, подробно описывает лазейку и дает пошаговый отчет об эксплойте, воспроизводя атаку на PancakeBunny.
Скрытая поверхность атаки: balanceOf()
Многие считают, что композитность имеет решающее значение для успеха DeFi. Контракты на токены (например, ERC20s) играют важную роль на нижнем слое лего DeFi. Однако разработчики могут упустить некоторые неконтролируемые и непредсказуемые условия при интеграции ERC20s в свои проекты DeFi. Например, вы не можете предсказать, когда и сколько токенов вы получите при восстановлении текущего баланса токенов. Эта неопределенность создает скрытую поверхность для атак.
Во многих случаях смарт-контракты ссылаются на остатки ERC20 в своей бизнес-логике. Например, когда пользователь вносит в смарт-контракт некоторое количество токенов XYZ, вызывается функция XYZ.balanceOf(), чтобы проверить, сколько денег получено. Если вы знакомы с кодовой базой Uniswap, то наверняка знаете, что контракт UniswapV2Pair содержит множество вызовов balanceOf().

В фрагменте кода UniswapV2Pair.mint() использует текущие балансы (balance0, balance1) и данные бухгалтерского учета (amount0, amount1) для получения сумм, внесенных пользователем(amount0, amount1). Однако если плохой игрок переводит некоторые активы (токен1 или токен2) непосредственно перед вызовом mint(), жертва предоставит больше ликвидности, чем ожидалось, то есть будет отчеканено больше токенов LP. Если вознаграждение рассчитывается на основе количества токенов LP, плохой игрок может получить прибыль, когда вознаграждение превысит расходы.
UniswapV2Pair.burn() имеет аналогичный риск. Вызывающий функцию mint() может подвергнуть себя опасности без глубокого понимания связанных с ней рисков. Именно это произошло в случае с PancakeBunny.

В приведенном выше фрагменте кода строка 140 извлекает баланс токена LP с помощью функции balanceOf() и сохраняет его в ликвидности. В строках 144-145 часть общего количества токенов LP, принадлежащих UniswapV2Pair (т.е. ликвидность из _totalSupply), используется для получения (amount0, amount1) с текущими остатками (balance0, balance1) двух активов (т.е. токена0 и токена1). В дальнейшем (amount0, amount1) двух активов передаются по адресу в строках 148-149.
Здесь плохой агент может манипулировать (balance0, balance1) и ликвидностью, отправляя токен0+token1 или токен LP в контракт UniswapV2Pair прямо перед вызовом функции mint(), чтобы заставить вызывающего получить больше токена0+token1. Мы проведем вас через исходный код PancakeBunny и покажем, как плохой игрок может извлечь выгоду из этого.
Анализ лазеек: BunnyMinterV2

В исходном коде PancakeBunny функция BunnyMinterV2.mintForV2() отвечает за чеканку токенов BUNNY в качестве вознаграждения. В частности, количество, которое нужно добыть (т.е. mintBunny), определяется из входных параметров _withdrawalFees и _performanceFee. Вычисления связаны с тремя функциями: _zapAssetsToBunnyBNB() (строка 213), priceCalculator.valueOfAsset() (строка 219) и amountBunnyToMint() (строка 221). Поскольку плохой игрок может чеканить большое количество BUNNY, проблема кроется в одной из трех вышеупомянутых функций.

Начнем с функции _zapAssetsToBunnyBNB(). Когда передаваемый актив является Cake-LP (строка 267), определенное количество токенов LP используется для снятия ликвидности и взятия (amountToken0, amountToken1) из (token0, token1) пула ликвидности (строка 278). С помощью контракта zapBSC эти активы обмениваются на токены BUNNY-BNB LP (строки 287-288). Затем соответствующее количество токенов BUNNY-BNB LP возвращается вызывающей стороне (строка 298). Здесь мы сталкиваемся с проблемой. Соответствует ли сумма количеству жетонов LP, которое вы предполагаете сжечь?

В реализации PancakeV2Router.removeLiquidity() ликвидность в виде LP-токенов (сумма в zapAssetsToBunnyBNB()) будет отправлена в контракт PancakePair (строка 500) и будет вызвана функция PancakePair.burn(). Если текущий баланс LP-токенов в PancakePair больше 0, то фактическая сумма для сжигания будет больше суммы, что косвенно увеличит количество BUNNY для майнинга.
Еще одной проблемой в _zapAssetsToBunnyBNB() является вызов zapBSC.zapInToken(). Логика заключается в том, чтобы обменять два актива, собранные с помощью функции removeLiquidity(), на токены BUNNY-BNB LP. Поскольку zapBSC обменивает активы через PancakeSwap, плохой агент может использовать флэш-кредиты, чтобы манипулировать количеством обмениваемых BUNNY-BNB.
Возвращаясь к BunnyMinterV2.mintForV2(), сумма bunnyBNBAmount, возвращаемая zapAssetsToBunnyBNB(), передается в priceCalculator.valueOfAsset() для котировки стоимости на основе BNB (т.е. vauleInBNB), аналогично механизму оракула.

Однако priceCalculator.valueOfAsset() ссылается на количество BNB и BUNNY (reserve0, reserve1) в BUNNY_BNB PancakePair в качестве ценового фида, что позволяет злоумышленнику использовать флэш-кредиты для манипуляции количеством майнинга токенов BUNNY.

Функция amountBunnyToMint() - это простой математический расчет. Вводимый вклад умножается на пять (bunnyPerProfitBNB = 5e18), что само по себе не имеет поверхности атаки, но усиление увеличивает манипуляции, упомянутые выше.
Готовьтесь к бою
Поскольку атака запускается функцией getReward(), нам нужно сначала получить право на вознаграждение.

Как показано на скриншоте Etherscan выше, хакер PancakeBunny вызвал функцию init() контракта exploit, чтобы обменять 1 токен WBNB на токены WBNB-USDT-LP и внести() их в контракт VaultFlipToFlip, чтобы получить вознаграждение, вызвав функцию getReward().

Как показано выше, с помощью функции Exp.prepare() мы воспроизвели вызов vaultFlipToFlip.deposit() (строка 62). Мы также использовали контракт ZapBSC для упрощения получения токенов LP (строки 54-57). Однако человек не может получить вознаграждение, пока хранитель PancakeBunny не выполнит следующий вызов harvest(). По этой причине хакер PancakeBunny не начинал атаку до первой транзакции harvest(), следующей за транзакцией init().

В нашей симуляции ни один хранитель не может вызвать команду harvest(). Поэтому мы используем функцию ETH-brownie, чтобы выдать себя за хранителя и вручную инициировать транзакцию harvest() (строка 25).
Рекурсивные флэш-кредиты
Для привлечения средств эксплуататор PancakeBunny использовал восемь различных пулов фондов, включая семь контрактов PancakePair и банк ForTube. Здесь команда Amber Group`s Blockchain Security использовала только следующие семь контрактов PancakePair с функцией флэш-обмена для предоставления займа в 2,3 млн WBNB:
адреса[7] пары = [ address(0x0eD7e52944161450477ee417DE9Cd3a859b14fD0), address(0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16), address(0x74E4716E431f45807DCF19f284c7aA99F18a4fbc), address(0x61EB789d75A95CAa3fF50ed7E47b96c132fEc082), address(0x9adc6Fb78CEFA07E13E9294F150C1E8C1Dd566c0), address(0xF3Bc6FC080ffCC30d93dF48BFA2aA14b869554bb), address(0xDd5bAd8f8b360d76d12FdA230F8BAF42fe0022CF) ];

Чтобы упростить вызовы flash-swap, мы упаковали два параметра в четвертый входной аргумент вызовов PancakePair.swap() (строка 72 или 74): level и asset. Переменная level указывает, на каком уровне вызова swap() мы находимся; переменная asset равна 0 или 1, что означает, что нам нужно взять токен0 или токен1.

Используя функцию обратного вызова pancakeCall(), мы рекурсивно вызываем PancakePair.swap() с уровнем+1, пока не достигнем седьмого уровня. На верхнем уровне мы вызываем shellcode() для выполнения реального действия в строке 98. Когда shellcode() возвращается, переменная asset возвращает заимствованный актив на каждом соответствующем уровне (строки 102-104).
Нажать на спусковой крючок

Функция shellcode(), вызываемая седьмым уровнем pancakeCall(), является собственно кодом эксплойта. Сначала мы сохраняем текущий баланс WBNB в wbnbAmount (строка 108), меняем 15 000 WBNB на токены WBNB-USDT-LP (строка 112) и отправляем их контракту, который чеканил эти токены LP (т.е. контракту PancakePair) в строке 113. Этот шаг направлен на манипулирование вызовом removeLiquidity() внутри функции _zapAssetsToBunnyBNB(), как анализировалось выше, что позволяет нам получить больше WBNB+USDT, чем ожидалось.

Второй шаг заключается в манипулировании ценой USDT, на которую ссылается _zapAssetsToBunnyBNB(), чтобы обменять USDT на WBNB. Поскольку _zapAssetsToBunnyBNB() использует WBNB-USDT PancakePair для обмена USDT на WBNB, мы могли бы обменять оставшуюся часть флэш-займа WBNB на USDT на PancakeSwap. Это сделает WBNB чрезвычайно дешевым, и _zapAssetsToBunnyBNB() получит непропорционально большое количество WBNB при обмене с USDT. Обратите внимание, что манипулирование ценами здесь происходит на пуле Pancake V1, а не на Pancake V2`s PancakePair, как в предыдущем шаге.

Последним шагом является вызов getReward(). Простой вызов контракта позволяет добыть 6,9 млн токенов BUNNY (строка 125). Затем токены BUNNY можно обменять на WBNB на PancakeSwap для погашения флэш-кредита.

О компании Amber Group
Amber Group - ведущий глобальный поставщик услуг криптофинансирования, работающий по всему миру и круглосуточно, с представительствами в Гонконге, Тайбэе, Сеуле и Ванкувере. Основанная в 2017 году, Amber Group обслуживает более 500 институциональных клиентов и в совокупности провела торговлю на сумму более $500 млрд на 100+ электронных биржах, имея в управлении активы на сумму более $1,5 млрд. В 2021 году Amber Group привлекла $100 млн в рамках серии B финансирования и стала последним единорогом в сфере Fintech стоимостью более $1 млрд. Для получения дополнительной информации, пожалуйста, посетите: www.ambergroup.io.
Источник