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).

Background

PancakeBunny is a yield aggregator accepting a variety of tokens, among them LP tokens from PancakeSwap. Stakers need to pay a 30% performance fee on the profits when withdrawing/claiming. However, they also receive BUNNY tokens in return — for every 1 BNB in fees collected, 3 BUNNY is rewarded to the depositor.

The Exploit

There’s an official post mortem but it lacks depth making it hard to understand. We will provide more details for the series of events:

  1. Attacker takes a flashloan
  2. 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.)
  3. 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.
  4. 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.
  5. Trades the BUNNY tokens to WBNB.
  6. Repays all flashloans.
// 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;
}
}
  1. 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);
IStakingRewards(BUNNY_POOL).notifyRewardAmount(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
}
// 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
}
}
}

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