BSC PancakeBunny Exploit Post Mortem

Background

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

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Forward me an email and I can log into your facebook account

Step-by-Step Guide for Configuring a Homelab VPN Server That Just Works

Cryptography: the difference between Encryption, Hashing, Encoding and Obfuscation

{UPDATE} Cure Hunters Hack Free Resources Generator

GDPR: Data subjects’ rights and organizations’ duties

{UPDATE} Air Battle Hack Free Resources Generator

COVID-19 Malware Analysis

The Phish In The Room: Human Emotions And Cybersecurity

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Christoph Michel

Christoph Michel

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

More from Medium

Dopple Finance’s $KUSD and Synthetic Assets Manual Minting Analysis

Multi-sig Deployment

Storage Locations in Ethereum vs. NEAR

DeFi Security Lecture 7 —Price Oracle Manipulation