TL;DR

In early April, our whitehats izhuer (Purdue University) and Gwinhen (Purdue University) from Pwned No More (PNM) DAO reported a critical security vulnerability to the bug bounty program of Grizzly.fi. Hackers can leverage this vulnerability to forcibly mint a tremendous amount of HONEY token, the native token of Grizzly.fi, to themselves, rendering all the investment funds at risk. For this findings, we were rewarded $10,000, the maximum amount of offered bounties. The Grizzly team is highly responsive with the bugfix and also with the payout of the bounty.

Background

MasterChef

MasterChef contract serves for the token farming of Pancake Swap, the leading Decentralized Exchange (DEX) on BNB blockchain. Users can lock their liquidity (of Pancake Swap) into MasterChef contract and get rewards in CAKE tokens, namely staking. The amount of rewards depends on how many LP tokens are staked and how long they are locked. With a careful design, MasterChef contract guarantees that only a limited number of CAKE tokens would be generated per block.

Grizzly.fi

Grizzly.fi is a soon launching DeFi project which tries to make liquidity mining easier and more profitable. Besides depositing Pancake Swap LP tokens locked in Grizzly, users can select different investment strategies provided by Grizzly. Grizzly contract will further invest the locked liquidity based on the selected strategy and shoot for better profit. Grizzly's native token, i.e., HONEY token, is minted as rewards for users who stake Pancake Swap LP tokens into Grizzly contracts.

Vulnerability Analysis

The standard strategy, one of the Grizzly-provided automatic investment strategies, stakes all deposited LP token into the MasterChef contract and rewards HONEY tokens along with the received CAKE tokens. The amount of minted HONEY tokens is decided by the amount of rewarded CAKE tokens, and is hence guaranteed with a reasonable mint rate as well.

The following code snippet depicts the basic logic, for which we crafted a bit for discussion simplicity.

function deposit(uint256 amount) external {
    _stakeRewards();
    staked[msg.sender] += amount;
    totalStaked += amount;
    IERC20(lpToken).transferFrom(msg.sender, address(this), amount);
}

function withdraw(uint256 amount) external {
    require(amount <= staked[msg.sender]);
    
    _stakeReward();
    staked[msg.sender] -= amount;
    totalStaked -= amount;

    IERC20(lpToken).transferFrom(msg.sender, address(this), amount);
    IERC20(rewardToken).transferFrom(
        msg.sender, address(this), pendingReward[msg.sender]
    );
    IERC20(rewardToken).transferFrom(
        msg.sender, address(this), pendingHoney[msg.sender]
    );
}

function _stakeRewards() internal returns () {
    // Get rewards, i.e., CAKE token, from MasterChef
    StakingContract.deposit(PoolID, 0);
    uint256 currentRewards = RewardToken.balanceOf(address(this));

    if (currentRewards == 0) return (0, 0, 0, 0);

    ......
  
    // 30% of the equivalent amount of Honey (based on Honey-BNB price) is minted
    uint256 mintedHoney = mintTokens(
        (currentRewards * 30) / 100,
        beeEfficiencyLevel
    );

    ......
    
    rewardPerShare += currentRewards / totalStaked;
    honeyPerShare += mintedHoney / totalStaked;
    
    pendingReward[msg.sender] += 
        (rewardPerShare - currentRewardPerShare[msg.sender]) * staked[msg.sender];
    currentRewardPerShare[msg.sender] = rewardPerShare;
    
    pendingHoney[msg.sender] += 
        (honeyPerShare - currentHoneyPerShare[msg.sender]) * staked[msg.sender];
    currentHoneyPerShare[msg.sender] = honeyPerShare;   
    ......
}

The code follows a similar logic of MasterChef contract. Functions deposit and withdraw are easy to understand, where users can deposit LP tokens and withdraw their LP, CAKE, and HONEY tokens. The detailed explanation is therefore omitted.

Function _stakeRewards is invoked each time users deposit and withdraw tokens. StakingContract and RewardToken are MasterChef and CAKE token, respectively. The code first invokes StakingContract.deposit(PoolID, 0) to get rewarded CAKE tokens from MasterChef contract. It then checks its current CAKE balance, namely curentRewards, which further decides the number of minted HONEY tokens. Grizzly distributes the rewarded CAKE tokens and minted HONEY tokens to each user according to the proportion of his/her stakings w.r.t. the total staking amount. In short, the more you stake, the more rewards (in CAKE and HONEY tokens) you will get.

Vulnerability

The vulnerability lies in how Grizzly detects the amount of its rewarded CAKE tokens. Specifically, Grizzly directly invokes currentRewards = RewardToken.balanceOf(address(this)), which can be manipulated by attackers. If the attacker directly sends CAKE tokens to Grizzly contract via the ERC20 Transfer function, s/he can increase currentRewards as well as the minting rate of HONEY tokens.

To get back the her/his funds (i.e., CAKE tokens) and the extract minted HONEY tokens, the attacker needs to become the dominator of the staking pool. It is not difficult with the help of flash loans, by borrowing a large number of LP tokens (to become the dominator) and CAKE tokens (to dramatically increase the minting rate of HONEY tokens).

Attack Scenario

We provide a step-by-step guide on how to launch this attack is as follows: