BeanstalkFarms/Beanstalk

RFC: Chop Rate Change

Closed this issue · 1 comments

RFC: Chop Rate Change

Authors

Guy, Brean, Deadmanwalking, Ben Weintraub, Brendan Sanderson

Summary

Change the Chop Rate from (the percent recapitalization of Unripe assets * the percent debt repaid to Fertilizer) to (the percent recapitalization of Unripe assets)^2.

Problem

Currently, Beanstalk offers a conservative Chop Rate of ~1.03% (22.49% recapitalization * 4.5627% of debt repaid to Fertilizer) to Unripe holders. Although the current model is likely to result in the least volatility as Beanstalk is recapitalized (as a result of Unripe holders Chopping later on average), Beanstalk can be more aggressive.

Beanstalk is in a healthy position in terms of liquidity and >20% of Fertilizer has been sold. Given these circumstances, a Chop Rate of ~1% seems conservative.

Furthermore, Chopping is healthy for Beanstalk as its obligations and the amount needed to fully recapitalize Unripe assets decreases.

Solution

Change the Chop Rate from (the percent recapitalization of Unripe assets * the percent of of debt repaid to Fertilizer) to (the percent recapitalization of Unripe assets)^2.

At the current percent recapitalization of Unripe assets of 22.49%, the Chop Rate would be ~5.06% using the new formula.

Specification

Modify getPenalizedUnderlying in LibUnripe.sol to (1) remove the scalar based on the debt repaid to Fertilizer and (2) factor in the % recapitalized calculation. The recapitalization is defined in US dollar terms and expressed as: $${Total \ USD \ raised / Total \ USD \ needed \ to \ recapitalize}$$ A verbose explanation on the final formulas can be found in the implementation below.

    /**
     * @notice Calculates the the penalized amount of Ripe Tokens corresponding to 
     * the amount of Unripe Tokens that are Chopped according to the current Chop Rate.
     * The new chop rate is %Recapitalized^2.
     */
    function getPenalizedUnderlying(
        address unripeToken,
        uint256 amount,
        uint256 supply
    ) internal view returns (uint256 redeem) {
        require(isUnripe(unripeToken), "not vesting");
        AppStorage storage s = LibAppStorage.diamondStorage();
        // getTotalRecapDollarsNeeded() queries for the total urLP supply which is burned upon a chop
        // If the token being chopped is unripeLP, getting the current supply here is inaccurate due to the burn
        // Instead, we use the supply passed in as an argument to getTotalRecapDollarsNeeded since the supply variable
        // here is the total urToken supply queried before burnning the unripe token
	    uint256 totalUsdNeeded = unripeToken == C.UNRIPE_LP ? LibFertilizer.getTotalRecapDollarsNeeded(supply) 
            : LibFertilizer.getTotalRecapDollarsNeeded();
        // chop rate = total redeemable * (%DollarRecapitalized)^2 * share of unripe tokens
        // redeem = totalRipeUnderlying * (usdValueRaised/totalUsdNeeded)^2 * UnripeAmountIn/UnripeSupply;
        // But totalRipeUnderlying = CurrentUnderlying * totalUsdNeeded/usdValueRaised to get the total underlying
        // redeem = currentRipeUnderlying * (usdValueRaised/totalUsdNeeded) * UnripeAmountIn/UnripeSupply
        uint256 underlyingAmount = s.u[unripeToken].balanceOfUnderlying;
        redeem = underlyingAmount.mul(s.recapitalized).div(totalUsdNeeded).mul(amount).div(supply);
        // cap `redeem to `balanceOfUnderlying in the case that `s.recapitalized` exceeds `totalUsdNeeded`.
        // this can occur due to unripe LP chops.
        if(redeem > underlyingAmount) redeem = underlyingAmount;
    }

Isolate the calculation for the US dollars needed for recapitalization in a new function called getTotalRecapDollarsNeeded in LibFertlilizer.sol.

    /**
     * @dev Returns the total dollar amount needed to recapitalize Beanstalk
     * for the supply of Unripe LP.
     * @param urLPsupply The supply of Unripe LP.
     * @return totalDollars The total dollar amount.
     */
    function getTotalRecapDollarsNeeded(uint256 urLPsupply) internal pure returns(uint256) {
	    uint256 totalDollars = C
            .dollarPerUnripeLP()
            .mul(urLPsupply)
            .div(DECIMALS);
        totalDollars = totalDollars / 1e6 * 1e6; // round down to nearest USDC
        return totalDollars;
    }

Introduce a new view function to calculate the percent recapitalized in order for it to be used in the Unripe LP penalty calculation.

    /**
     * @notice returns the total percentage that beanstalk has recapitalized.
     * @dev this is calculated by the ratio of s.recapitalized and the total dollars the barnraise needs to raise.
     * returns the same precision as `getRecapPaidPercentAmount` (100% recapitalized = 1e6).
     */
    function getTotalRecapitalizedPercent() internal view returns (uint256 recapitalizedPercent) {
        AppStorage storage s = LibAppStorage.diamondStorage();
        uint256 totalUsdNeeded = LibFertilizer.getTotalRecapDollarsNeeded();
        if(totalUsdNeeded == 0) return 0;
        return s.recapitalized.mul(DECIMALS).div(totalUsdNeeded);
    }

Update getPercentPenalty in UnripeFacet to use getTotalRecapitalizedPercent when calculating the percent penalty for Unripe LP.

    /**
     * @notice Returns the % penalty of Chopping an Unripe Token into its Ripe Token.
     * @param unripeToken The address of the Unripe Token.
     * @return penalty The penalty % of Chopping derived from %Recapitalized^2.
     * @dev `address` parameter retained for backwards compatiability.
     */
    function getPercentPenalty(address unripeToken) external view returns (uint256 penalty) {
        if (unripeToken == C.UNRIPE_BEAN) { 
            return LibUnripe.getPenalizedUnderlying(
                unripeToken,
                LibUnripe.DECIMALS,
                IERC20(unripeToken).totalSupply()
            );
        }
        
        if (unripeToken == C.UNRIPE_LP) { 
            return LibUnripe.getTotalRecapitalizedPercent()
                .mul(LibUnripe.getTotalRecapitalizedPercent())
                .div(LibUnripe.DECIMALS);
        }
    }

#802 addresses this issue.