BSC PancakeBunny Exploit Post Mortem

On May 19th 2021, PancakeBunny was exploited by an attacker abusing a wrong PancakeSwap LP price computation in Bunny’s PriceCalculatorBSCV1 contract to mint 6.97M BUNNY tokens which were then exchanged for a profit of 114,631 WBNB (~30M USD).


The Exploit

  1. The attacker deploys a contract, already acquires 9.2751 WBNB<>BUSD-T PancakeSwap LP tokens (380$), and deposits them to the VaultFlipToFlip contract in this first transaction.
  2. Attacker takes a flashloan
  3. Mints another 144,445.5921 WBNB<>BUSDT LP tokens to the pair contract itself. (The BunnyMinter will later receive these LP tokens when it calls the router.removeLiquidity function.)
  4. Swaps 2,315,631 WBNB to 3,826,047 BUSDT on the PancakeSwap V1 USDT/BNB pool, significantly increasing the WBNB in the pool’s reserves.
  5. Attacker withdraws the “profit” of the staked LP tokens from 1) by calling VaultFlipToFlip.getReward() plus 6,972,455 minted BUNNY tokens. The minting of large amounts of BUNNY tokens on the performance fee is due to a wrong LP price calculation.
  6. Trades the BUNNY tokens to WBNB.
  7. Repays all flashloans.

Most of the attack happens in step 5) and it’s best understood by looking at the code.

The entry-point is the call to the VaultFlipToFlip.getReward function which computes the performance fee on the small amount of LP tokens provided in step 1) and then calls BunnyMinter.mintForV2(to=attacker).

// VaultFlipToFlip 0xd415e6caa8af7cc17b7abd872a42d5f2c90838ea
function getReward() external override {
uint amount = earned(msg.sender); // returns 0.000521785526032378e18
// ...

amount = _withdrawTokenWithCorrection(amount); // withdraws same amount from MasterChef
uint depositTimestamp = _depositedAt[msg.sender];
uint performanceFee = canMint() ? _minter.performanceFee(amount) : 0; // 30% of earned
if (performanceFee > DUST) {
// important call that internally mints the BUNNY tokens
_minter.mintForV2(address(_stakingToken), 0, performanceFee, msg.sender, depositTimestamp);
amount = amount.sub(performanceFee);

_stakingToken.safeTransfer(msg.sender, amount); // withdraws 70% of earned LP tokens

function balance() public view override returns (uint amount) {
(amount,) = CAKE_MASTER_CHEF.userInfo(pid, address(this)); // total LP token balance of all depositors
function balanceOf(address account) public view override returns(uint) {
if (totalShares == 0) return 0;
return balance().mul(sharesOf(account)).div(totalShares);
function earned(address account) public view override returns (uint) {
if (balanceOf(account) >= principalOf(account) + DUST) {
return balanceOf(account).sub(principalOf(account));
} else {
return 0;

The _performanceFee amount determines the amount of BUNNY tokens to mint for the attacker (3 BUNNY per 1 WBNB) in mintForV2. But even with the manipulated PancakeSwap spot price, it would not make this attack profitable. Another amplifier is needed and this is where the LP provisioning from step 3) comes in: The fees (denoted in WBNB <> BUSDT LP tokens) are first traded for WBNB <> BUNNY LP tokens using the BunnyMinter._zapAssetsToBunnyBNB function which:

  1. First calls pancakeRouter.removeLiquidity() with the fee amount. Internally, the router calls the pair.burn function which burns the whole LP token balance of the contract. This includes the 144,445 LP tokens from step 3), returning large amounts of WBNB and BUSDT to the BunnyMinter.
  2. It then swaps these amounts to provide liquidity for the WBNB <> BUNNY pair and returns the minted LP tokens.
// BunnyMinterV2.sol 0x819eea71d3f93bb604816f1797d4828c90219b5d
function mintForV2(address asset /* LP token */, uint _withdrawalFee /* 0 */, uint _performanceFee /* 0.00015... */, address to /* attacker */, uint) external payable override onlyMinter {
uint feeSum = _performanceFee.add(_withdrawalFee);
_transferAsset(asset, feeSum); // transfers LP tokens from VaultFlipToFlip to this

// removes liquidity from WBNB <> BUSDT pool. Because of previously minted LPs returns
// 2,961,750 USDT and 7,744 WBNB
// then swaps these to WBNB and BUNNY and provides liquidity to the WBNB <> BUNNY pool
// returns these LP tokens as bunnyBNBAmount
uint bunnyBNBAmount = _zapAssetsToBunnyBNB(asset, feeSum, true);

if (bunnyBNBAmount == 0) return;

IBEP20(BUNNY_BNB).safeTransfer(BUNNY_POOL, bunnyBNBAmount);

(uint valueInBNB,) = priceCalculator.valueOfAsset(BUNNY_BNB, bunnyBNBAmount); // returns inflated value
uint contribution = valueInBNB.mul(_performanceFee).div(feeSum);
uint mintBunny = amountBunnyToMint(contribution); // multiplies by 3 (1 WBNB : 3 BUNNY)
if (mintBunny == 0) return;
_mint(mintBunny, to); // mints BUNNY for attacker

Finally, the already high amount of WBNB <> BUNNY LP tokens are multiplied by a manipulated price in priceCalculator.valueOfAsset(BUNNY_BNB, bunnyBNBAmount).

// PriceCalculatorBSCV1.sol 0x81ef2bc1e02fee5414e46accc6ae14d833eebba0
function valueOfAsset(address asset, uint amount) public view override returns (uint valueInBNB, uint valueInUSD) {
if (keccak256(abi.encodePacked(IPancakePair(asset).symbol())) == keccak256("Cake-LP")) {
(uint reserve0, uint reserve1, ) = IPancakePair(asset).getReserves();
if (IPancakePair(asset).token0() == WBNB) {
valueInBNB = amount.mul(reserve0).mul(2).div(IPancakePair(asset).totalSupply());
valueInUSD = valueInBNB.mul(priceOfBNB()).div(1e18);
} else if (IPancakePair(asset).token1() == WBNB) {
valueInBNB = amount.mul(reserve1).mul(2).div(IPancakePair(asset).totalSupply());
valueInUSD = valueInBNB.mul(priceOfBNB()).div(1e18);
} else {
// ... recursion on both, not relevant

The TVL of the pool is computed as two times the WBNB reserves which have been inflated in step 2). This does not work and has already been used in the hack.

The inflated value is then used to mint BUNNY tokens for the attacker.

Full Stack Software Engineer #javascript #EOS. Into Recreational Math / CS 🤯 Just message me about anything, my mind is open.