BSC Flash Loan Attack: PancakeBunny
July 23 2021 - 8:57AM
NEWSBTC
In May 2021, we witnessed multiple hacks targeting BSC DeFi
products. In particular, a loophole related to reward minting in
the yield aggregator, PancakeBunny, was exploited to mint ~7M BUNNY
tokens from nothing, leading to a whopping $45M financial loss.
After the bloody hack, three forked projects — AutoShark,
Merlin Labs, and PancakeHunny — were attacked with similar
techniques. Amber Group’s Blockchain Security team, led by Dr.
Chiachih Wu, elaborates on the loophole and gives a step-by-step
account of the exploit by reproducing the attack against
PancakeBunny. Hidden Attack Surface: balanceOf() Many people
believe that composability is crucial to the success of DeFi. Token
contracts (e.g., ERC20s) play an essential role on the bottom layer
of DeFi legos. However, developers may overlook some uncontrollable
and unpredictable conditions when integrating ERC20s into their
DeFi projects. For example, you can’t predict when and how many
tokens you will receive when you retrieve the current token
balance. This uncertainty creates a hidden attack surface. In many
cases, smart contracts reference the balances of ERC20s in their
business logic. For example, when a user deposits some XYZ tokens
into the smart contract, XYZ.balanceOf() is invoked to check how
much money is received. If you’re familiar with the Uniswap
codebase, you probably know that the UniswapV2Pair contract has
many balanceOf() calls. In the code snippet, UniswapV2Pair.mint()
uses the current balances (balance0, balance1) and the book-keeping
data (amount0, amount1) to derive the amounts deposited by the
user(amount0, amount1). However, if a bad actor transfers some
assets (token1 or token2) right before the mint() call, the victim
would provide more liquidity than expected, i.e., more LP tokens
are minted. If the rewards are calculated based on the amount of LP
tokens, the bad actor can profit when the rewards exceed the
expenses. The UniswapV2Pair.burn() has a similar risk. The mint()
function caller might jeopardize himself without a thorough
understanding of the risks involved. This is what happened in the
case of PancakeBunny. In the code snippet above, line 140 retrieves
the balance of LP token via balanceOf() and stores it into
liquidity. In lines 144–145, the portion of total LP tokens owned
by UniswapV2Pair (i.e., liquidity out of _totalSupply) is used to
derive (amount0, amount1) with the current balances (balance0,
balance1) of the two assets (i.e., token0 and token1). Later on,
(amount0, amount1) of the two assets are transferred to the address
in lines 148–149. Here, a bad actor could manipulate (balance0,
balance1) and the liquidity by sending some token0+token1 or the LP
token into the UniswapV2Pair contract right before the mint()
function is invoked to make the caller get more token0+token1 out.
We’ll walk you through the PancakeBunny source code and show you
how the bad actor can profit from doing this. Loophole Analysis:
BunnyMinterV2 In the PancakeBunny source code, the
BunnyMinterV2.mintForV2() function is in charge of minting BUNNY
tokens as rewards. Specifically, the amount to be minted (i.e.,
mintBunny)is derived from the input parameters, _withdrawalFees,
and _performanceFee. The computation is related to three functions:
_zapAssetsToBunnyBNB() (line 213), priceCalculator.valueOfAsset()
(line 219) and amountBunnyToMint() (line 221). Since the bad actor
can mint a large amount of BUNNY, the problem lies in one of the
three functions mentioned above. Let’s start from the
_zapAssetsToBunnyBNB() function. When the passed-in asset is a
Cake-LP (line 267), a certain amount of LP tokens is used to remove
liquidity and take (amountToken0, amountToken1) of (token0, token1)
from the liquidity pool (line 278). With the help of the zapBSC
contract, those assets are swapped for BUNNY-BNB LP tokens (lines
287–288). A corresponding amount of BUNNY-BNB LP tokens is then
returned to the caller (line 298). Here, we have a problem. Does
the amount match the amount of LP tokens you assume to be burned?
In the implementation of PancakeV2Router.removeLiquidity(),
liquidity of LP tokens (amount in zapAssetsToBunnyBNB()) would be
sent to the PancakePair contract (line 500) and PancakePair.burn()
would be invoked. If the current LP token balance of PancakePair is
greater than 0, the actual amount to be burned would be greater
than amount, which indirectly increases the BUNNY amount to be
minted. Another issue in _zapAssetsToBunnyBNB() is the
zapBSC.zapInToken() call. The logic behind this is to exchange the
two assets collected by the removeLiquidity() into BUNNY-BNB LP
tokens. Since zapBSC swaps assets through PancakeSwap, the bad
actor could use flash loans to manipulate the amount of swapped
BUNNY-BNB. Back to BunnyMinterV2.mintForV2(), the bunnyBNBAmount
returned by zapAssetsToBunnyBNB() would be passed into
priceCalculator.valueOfAsset() to quote the value based on BNB
(i.e., vauleInBNB), similar to an oracle mechanism. However,
priceCalculator.valueOfAsset() references the amount of BNB and
BUNNY (reserve0, reserve1) in the BUNNY_BNB PancakePair as the
price feed, which enables the bad actor to use flash loans to
manipulate the amount of BUNNY tokens minted. The
amountBunnyToMint() function is a simple math calculation. The
input contribution is multiplied by five (bunnyPerProfitBNB =
5e18), which itself has no attack surface, but the amplification
magnifies the manipulation mentioned above. Prepare for Combat
Since the attack is triggered by getReward(), we need to be
qualified for rewards first. As shown in the Etherscan screenshot
above, the PancakeBunny hacker invoked the init() function of the
exploit contract to exchange 1 WBNB to WBNB-USDT-LP tokens and
deposit() them into the VaultFlipToFlip contract, such that he
would get some rewards by invoking getReward(). As shown above,
using the Exp.prepare() function we reproduced the
vaultFlipToFlip.deposit() call (line 62). We also used the ZapBSC
contract to simplify obtaining LP tokens (lines 54-57). However,
one isn’t able to get rewards until the PancakeBunny keeper
triggers the next harvest() call. For this reason, the PancakeBunny
hacker didn’t trigger the attack until the first harvest()
transaction following the init() transaction. In our simulation, no
keeper can trigger the harvest(). Therefore, we leverage the
feature of eth-brownie to impersonate the keeper and manually
initiate the harvest() transaction (line 25). Recursive Flash Loans
To leverage funds, the PancakeBunny exploiter utilized eight
different fund pools including seven PancakePair contracts and the
ForTube Bank. Here, Amber Group’s Blockchain Security team only
used the following seven PancakePair contracts’ flash-swap feature
to loan 2.3M WBNB: address[7] pairs = [
address(0x0eD7e52944161450477ee417DE9Cd3a859b14fD0),
address(0x58F876857a02D6762E0101bb5C46A8c1ED44Dc16),
address(0x74E4716E431f45807DCF19f284c7aA99F18a4fbc),
address(0x61EB789d75A95CAa3fF50ed7E47b96c132fEc082),
address(0x9adc6Fb78CEFA07E13E9294F150C1E8C1Dd566c0),
address(0xF3Bc6FC080ffCC30d93dF48BFA2aA14b869554bb),
address(0xDd5bAd8f8b360d76d12FdA230F8BAF42fe0022CF) ]; To simplify
the flash-swap calls, we packed two parameters into the fourth
input argument of the PancakePair.swap() calls (line 72 or line
74): level and asset. The level variable indicates which level of
swap() call we’re in; the asset variable is 0 or 1, meaning we need
to borrow token0 or token1. Using the callback function
pancakeCall(), we recursively call PancakePair.swap() with level+1
until we reach the seventh level. At the top level, we invoke
shellcode() to perform the real action in line 98. When shellcode()
returns, the asset variable returns the borrowed asset in each
corresponding level (lines 102–104). Pull the Trigger The
shellcode() function invoked by the seventh level of pancakeCall()
is the actual exploit code. First, we keep the current balance of
WBNB in wbnbAmount (line 108), swap 15,000 WBNB into WBNB-USDT-LP
tokens (line 112), and send them to the contract which minted those
LP tokens (i.e., the PancakePair contract) in line 113. This step
aims to manipulate the removeLiquidity() call inside the
_zapAssetsToBunnyBNB() function as analyzed above, enabling us to
receive more WBNB+USDT than expected. The second step is to
manipulate the USDT price referenced by _zapAssetsToBunnyBNB() to
swap USDT into WBNB. Since _zapAssetsToBunnyBNB() uses WBNB-USDT
PancakePair to swap USDT to WBNB, we could swap the rest of the
flash loaned WBNB to USDT on PancakeSwap. Doing so would make WBNB
extremely cheap, and _zapAssetsToBunnyBNB() would receive a
disproportionately large amount of WBNB when swapped from USDT.
Note that the price manipulation here occurs on the Pancake V1
pool, not Pancake V2’s PancakePair as in the previous step. The
final step is the getReward() call. The simple contract call could
mint 6.9M BUNNY tokens (line 125). The BUNNY tokens could then be
swapped for WBNB on PancakeSwap to pay back the flash loan. In our
simulation, the bad actor pays 1 WBNB and walks away with 104k WBNB
+ 3.8M USDT (equivalent to ~$45M). About Amber Group Amber Group is
a leading global crypto finance service provider operating around
the world and around the clock with a presence in Hong Kong,
Taipei, Seoul, and Vancouver. Founded in 2017, Amber Group services
over 500 institutional clients and has cumulatively traded over
$500 billion across 100+ electronic exchanges, with over $1.5
billion in assets under management. In 2021, Amber Group raised
$100 million in Series B funding and became the latest FinTech
unicorn valued over $1 billion. For more information, please visit:
www.ambergroup.io.
Uniswap (COIN:UNIUSD)
Historical Stock Chart
From Mar 2024 to Apr 2024
Uniswap (COIN:UNIUSD)
Historical Stock Chart
From Apr 2023 to Apr 2024