contract StrategyCvxLINKV1 is StrategyBase {
    // want token
    address public constant link = 0x514910771AF9Ca656af840dff83E8264EcF986CA;
    uint8 public want_decimals = 18;

    // convex staking constants
    address public stakingPool = 0xF403C135812408BFbE8713b5A23a04b3D48AAE31;
    uint256 public stakingPoolId = 30;
    address public constant rewardTokenCRV = 0xD533a949740bb3306d119CC777fa900bA034cd52; 
    address public constant rewardTokenCVX = 0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B;
    address public rewardPool = 0x9700152175dc22E7d1f3245fE3c1D2cfa3602548;
	
    // curve constants
    address public constant curvePool = 0xF178C0b5Bb7e7aBF4e12A4838C7b7c5bA2C623c0;
    address public constant linkcrv = 0xcee60cFa923170e4f8204AE08B4fA6A3F5656F3a;
		
    // slippage protection for one-sided ape in/out
    uint256 public slippageProtectionIn = 50; // max 0.5%
    uint256 public slippageProtectionOut = 50; // max 0.5%
    uint256 public constant DENOMINATOR = 10000;

    constructor(
        address _governance,
        address _strategist,
        address _controller,
        address _timelock
    )
        public StrategyBase(link, _governance, _strategist, _controller, _timelock)
    {		        
        IERC20(link).safeApprove(curvePool, uint256(-1));
        IERC20(linkcrv).safeApprove(curvePool, uint256(-1));
        IERC20(linkcrv).safeApprove(stakingPool, uint256(-1));
        want_decimals = ERC20(link).decimals();
    }

    // **** Views **** //
    
    function setSlippageProtection(uint256 _in, uint256 _out) public{
        require(msg.sender == governance, "!governance");
        require(_in < DENOMINATOR && _in > 0, "!_in");
        require(_out < DENOMINATOR && _out > 0, "!_out");
        slippageProtectionIn = _in;
        slippageProtectionOut = _out;
    }

    function setStakingPoolId(uint256 _poolId) public{
        require(msg.sender == governance, "!governance");
        stakingPoolId = _poolId;
    }

    function setStakingPool(address _pool) public{
        require(msg.sender == governance, "!governance");
        stakingPool = _pool;
        IERC20(linkcrv).safeApprove(stakingPool, uint256(-1));
    }

    function setRewardPool(address _pool) public{
        require(msg.sender == governance, "!governance");
        rewardPool = _pool;
    }

    function _convertWantToBuyback(uint256 _lpAmount) internal override returns (address, uint256){
        require(_lpAmount > 0, '!_lpAmount');
		
        _swapUniswap(want, weth, _lpAmount);
        uint256 _weth = IERC20(weth).balanceOf(address(this));
        return (weth, _weth);
    }

    function harvest() public override onlyBenignCallers {

        // Collects reward tokens
        ICvxBaseRewardPool(rewardPool).getReward(address(this), true);
		
        uint256 _rewardCRV = IERC20(rewardTokenCRV).balanceOf(address(this));
        uint256 _rewardCVX = IERC20(rewardTokenCVX).balanceOf(address(this));

        if (_rewardCRV > 0) {
            address[] memory _swapPath = new address[](2);
            _swapPath[0] = rewardTokenCRV;
            _swapPath[1] = weth;
            _swapUniswapWithDetailConfig(rewardTokenCRV, weth, _rewardCRV, 1, _swapPath, sushiRouter);
        }

        if (_rewardCVX > 0) {
            address[] memory _swapPath = new address[](2);
            _swapPath[0] = rewardTokenCVX;
            _swapPath[1] = weth;
            _swapUniswapWithDetailConfig(rewardTokenCVX, weth, _rewardCVX, 1, _swapPath, sushiRouter);
        }
		
        uint256 _wethAmount = IERC20(weth).balanceOf(address(this));
        if (_wethAmount > 0){	
            uint256 _buybackLpAmount = _wethAmount.mul(performanceFee).div(performanceMax);
            if (buybackEnabled == true && _buybackLpAmount > 0){
                buybackAndNotify(weth, _buybackLpAmount);
            }
             
            _swapUniswap(weth, want, IERC20(weth).balanceOf(address(this)));
            uint256 _wantBal = IERC20(want).balanceOf(address(this));
            if (_wantBal > 0){
                lastHarvestBlock = block.number;
                lastHarvestInWant = _wantBal;
                deposit();
            }
        }
    }

    function deposit() public override {
        uint256 _wantAmt = IERC20(want).balanceOf(address(this));
        uint256 _expectedOut = _wantAmt.mul(1e18).div(virtualPriceToWant());
        uint256 _maxSlip = _expectedOut.mul(DENOMINATOR.sub(slippageProtectionIn)).div(DENOMINATOR);
        if (_wantAmt > 0 && checkSlip(_wantAmt, _maxSlip)) {
            uint256[2] memory amounts = [_wantAmt, 0];
            ICurveFi_2(curvePool).add_liquidity(amounts, _maxSlip);
        }
		
        uint256 _lpAmt = IERC20(linkcrv).balanceOf(address(this));
        require(_lpAmt > 0, "!_lpAmt");
        ICvxBooster(stakingPool).depositAll(stakingPoolId, true);
    }

    function _withdrawSome(uint256 _amount) internal override returns (uint256) {
        if (_amount == 0){
            return 0;
        }
		
        uint256 _wantBefore = IERC20(want).balanceOf(address(this));
	
        uint256 requiredWant = estimateRequiredLP(_amount);
        requiredWant = requiredWant.mul(DENOMINATOR.add(slippageProtectionOut)).div(DENOMINATOR);// try to remove bit more
		
        uint256 _lpAmount = IERC20(linkcrv).balanceOf(address(this));
        uint256 _withdrawFromStaking = _lpAmount < requiredWant? requiredWant.sub(_lpAmount) : 0;
			
        if (_withdrawFromStaking > 0){
            uint256 maxInStaking = ICvxBaseRewardPool(rewardPool).balanceOf(address(this));
            uint256 _toWithdraw = maxInStaking < _withdrawFromStaking? maxInStaking : _withdrawFromStaking;		
            ICvxBaseRewardPool(rewardPool).withdrawAndUnwrap(_toWithdraw, false);			
        }
		    	
        _lpAmount = IERC20(linkcrv).balanceOf(address(this));
        if (_lpAmount > 0){
            requiredWant = requiredWant > _lpAmount?  _lpAmount : requiredWant;

            uint256 maxSlippage = requiredWant.mul(DENOMINATOR.sub(slippageProtectionOut)).div(DENOMINATOR);

            if (want_decimals < 18) {
                maxSlippage = maxSlippage.div(10**(uint256(uint8(18) - want_decimals)));
            }
            ICurveFi_2(curvePool).remove_liquidity_one_coin(requiredWant, 0, maxSlippage);
        }
		
        uint256 _wantAfter = IERC20(want).balanceOf(address(this));		
        return _wantAfter.sub(_wantBefore);
    }

    // Controller only function for creating additional rewards from dust
    function withdraw(IERC20 _asset) external override returns (uint256 balance) {
        require(address(_asset) != want, 'wantToken');
        require(address(_asset) != linkcrv, 'lpToken');
        balance = _withdrawNonWantAsset(_asset);
    }
	
    function virtualPriceToWant() public view returns (uint256) {
        if (want_decimals < 18){
            return ICurveFi_2(curvePool).get_virtual_price().div(10 ** (uint256(uint8(18) - want_decimals)));
        }else{
            return ICurveFi_2(curvePool).get_virtual_price();
        }
    }
	
    function estimateRequiredLP(uint256 _wantAmt) public view returns (uint256) {
        return _wantAmt.mul(1e18).div(virtualPriceToWant());
    }
	
    function checkSlip(uint256 _wantAmt, uint256 _maxSlip) public view returns (bool){
        uint256[2] memory amounts = [_wantAmt, 0];
        return ICurveFi_2(curvePool).calc_token_amount(amounts, true) >= _maxSlip;
    }
	
    function crvLPToWant(uint256 _lpAmount) public view returns (uint256) {
        if (_lpAmount == 0){
            return 0;
        }
        uint256 virtualOut = virtualPriceToWant().mul(_lpAmount).div(1e18);
        return virtualOut; 
    }
	
    function balanceOfPool() public override view returns (uint256){
        uint256 lpAmt = ICvxBaseRewardPool(rewardPool).balanceOf(address(this));
        lpAmt = lpAmt.add(IERC20(linkcrv).balanceOf(address(this)));
        return crvLPToWant(lpAmt);
    }

    // only include CRV earned
    function getHarvestable() public view returns (uint256) {
        return ICvxBaseRewardPool(rewardPool).earned(address(this));
    }
	
    // https://etherscan.io/address/0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b#code#L1091
    function mintableCVX(uint256 _amount) public view returns (uint256) {
        uint256 _toMint = 0;
        uint256 supply = IERC20(rewardTokenCVX).totalSupply();
        uint256 cliff = supply.div(ICvxMinter(rewardTokenCVX).reductionPerCliff());
        uint256 totalCliffs = ICvxMinter(rewardTokenCVX).totalCliffs();
        if (cliff < totalCliffs){
            uint256 reduction = totalCliffs.sub(cliff);
            _amount = _amount.mul(reduction).div(totalCliffs);
            uint256 amtTillMax = ICvxMinter(rewardTokenCVX).maxSupply().sub(supply);
            if (_amount > amtTillMax){
                _amount = amtTillMax;
            }
            _toMint = _amount;
        }
        return _toMint;
    }

    function getHarvestableCVX() public view returns (uint256) {
        uint256 _crvEarned = getHarvestable();
        return mintableCVX(_crvEarned);
    }
}
function withdraw(IERC20 _asset) external override returns (uint256 balance) {
    require(address(_asset) != want, 'wantToken');
    require(address(_asset) != linkcrv, 'lpToken');
    balance = _withdrawNonWantAsset(_asset);
}

// Controller only function for creating additional rewards from dust