contract GoodCompoundStaking is SimpleStaking {
	using UniswapV2SwapHelper for IHasRouter;

	// Address of the TOKEN/USD oracle from chainlink
	address public tokenUsdOracle;
	//Address of the COMP/USD oracle from chianlink
	address public compUsdOracle;

	// Gas cost to collect interest from this staking contract
	uint32 public collectInterestGasCost = 250000;
	// Gas cost to collect COMP rewards
	uint32 public compCollectGasCost = 150000;

	address[] public tokenToDaiSwapPath;

	ERC20 public comp;

	Uniswap public uniswapContract;

	/**
	 * @param _token Token to swap DEFI token
	 * @param _iToken DEFI token address
	 * @param _ns Address of the NameService
	 * @param _tokenName Name of the staking token which will be provided to staker for their staking share
	 * @param _tokenSymbol Symbol of the staking token which will be provided to staker for their staking share
	 * @param _maxRewardThreshold Determines blocks to pass for 1x Multiplier
	 * @param _tokenUsdOracle address of the TOKEN/USD oracle
	 * @param _compUsdOracle address of the COMP/USD oracle
	 * @param _tokenToDaiSwapPath the uniswap path to swap token to DAI, should be empty if token is DAI
	 */
	function init(
		address _token,
		address _iToken,
		INameService _ns,
		string memory _tokenName,
		string memory _tokenSymbol,
		uint64 _maxRewardThreshold,
		address _tokenUsdOracle,
		address _compUsdOracle,
		address[] memory _tokenToDaiSwapPath
	) public {
		initialize(
			_token,
			_iToken,
			_ns,
			_tokenName,
			_tokenSymbol,
			_maxRewardThreshold
		);

		address dai = nameService.getAddress("DAI");
		require(
			_token == dai ||
				(_tokenToDaiSwapPath[0] == _token &&
					_tokenToDaiSwapPath[_tokenToDaiSwapPath.length - 1] == dai),
			"invalid path"
		);

		//above  initialize going  to revert on second call, so this is safe
		compUsdOracle = _compUsdOracle;
		tokenUsdOracle = _tokenUsdOracle;
		tokenToDaiSwapPath = _tokenToDaiSwapPath;
		comp = ERC20(nameService.getAddress("COMP"));
		uniswapContract = Uniswap(nameService.getAddress("UNISWAP_ROUTER"));

		_approveTokens();
	}

	/**
	 * @dev stake some Token
	 * @param _amount of Token to stake
	 */
	function mintInterestToken(uint256 _amount) internal override {
		require(
			cERC20(address(iToken)).mint(_amount) == 0,
			"Minting cToken failed, funds returned"
		);
	}

	/**
	 * @dev redeem Token from compound
	 * @param _amount of token to redeem in Token
	 */
	function redeem(uint256 _amount) internal override {
		require(
			cERC20(address(iToken)).redeemUnderlying(_amount) == 0,
			"Failed to redeem cToken"
		);
	}

	/**
	 * @dev Function to redeem cToken + reward COMP for DAI, so reserve knows how to handle it. (reserve can handle dai or cdai)
	 * @dev _amount of token in iToken
	 * @dev _recipient recipient of the DAI
	 * @return actualTokenGains amount of token redeemed for dai,
			actualRewardTokenGains amount of reward token redeemed for dai,
			daiAmount total dai received
	 */
	function redeemUnderlyingToDAI(uint256 _amount, address _recipient)
		internal
		override
		returns (
			uint256 actualTokenGains,
			uint256 actualRewardTokenGains,
			uint256 daiAmount
		)
	{
		uint256 compBalance = comp.balanceOf(address(this));

		uint256 redeemedDAI;

		if (compBalance > 0) {
			address[] memory compToDaiSwapPath = new address[](3);
			compToDaiSwapPath[0] = address(comp);
			compToDaiSwapPath[1] = uniswapContract.WETH();
			compToDaiSwapPath[2] = nameService.getAddress("DAI");
			actualRewardTokenGains = IHasRouter(this).maxSafeTokenAmount(
				address(comp),
				uniswapContract.WETH(),
				compBalance,
				maxLiquidityPercentageSwap
			);

			redeemedDAI = IHasRouter(this).swap(
				compToDaiSwapPath,
				actualRewardTokenGains,
				0,
				_recipient
			);
		}
		//in case of cdai there's no need to swap to DAI, we send cdai to reserve directly
		actualTokenGains = iTokenWorthInToken(_amount);
		if (address(iToken) == nameService.getAddress("CDAI")) {
			require(iToken.transfer(_recipient, _amount), "collect transfer failed");
			return (
				actualTokenGains,
				actualRewardTokenGains,
				actualTokenGains + redeemedDAI
			); // If iToken is cDAI then just return cDAI
		}

		//out of requested interests to withdraw how much is it safe to swap
		uint256 safeAmount = IHasRouter(this).maxSafeTokenAmount(
			address(token),
			tokenToDaiSwapPath[1],
			actualTokenGains,
			maxLiquidityPercentageSwap
		);

		if (actualTokenGains > safeAmount) {
			actualTokenGains = safeAmount;
			//recalculate how much iToken to redeem
			_amount = tokenWorthIniToken(actualTokenGains);
		}

		require(
			cERC20(address(iToken)).redeem(_amount) == 0,
			"Failed to redeem cToken"
		);

		actualTokenGains = token.balanceOf(address(this));

		if (actualTokenGains > 0) {
			redeemedDAI += IHasRouter(this).swap(
				tokenToDaiSwapPath,
				actualTokenGains,
				0,
				_recipient
			);
		}

		return (actualTokenGains, actualRewardTokenGains, redeemedDAI);
	}

	/**
	 * @dev returns decimals of token.
	 */
	function tokenDecimal() internal view override returns (uint256) {
		ERC20 token = ERC20(address(token));
		return uint256(token.decimals());
	}

	/**
	 * @dev returns decimals of interest token.
	 */
	function iTokenDecimal() internal view override returns (uint256) {
		ERC20 cToken = ERC20(address(iToken));
		return uint256(cToken.decimals());
	}

	/**
	 * @dev Function that calculates current interest gains of this staking contract
	 * @param _returnTokenBalanceInUSD determine return token balance of staking contract in USD
	 * @param _returnTokenGainsInUSD determine return token gains of staking contract in USD
	 * @return  iTokenGains gains in itoken, tokenGains gains in token, tokenBalance total locked Tokens, balanceInUsd locked tokens worth in USD, tokenGainsInUSD token Gains in USD
	 */
	function currentGains(
		bool _returnTokenBalanceInUSD,
		bool _returnTokenGainsInUSD
	)
		public
		view
		override
		returns (
			uint256 iTokenGains,
			uint256 tokenGains,
			uint256 tokenBalance,
			uint256 balanceInUSD,
			uint256 tokenGainsInUSD
		)
	{
		tokenBalance = iTokenWorthInToken(iToken.balanceOf(address(this)));
		balanceInUSD = _returnTokenBalanceInUSD
			? getTokenValueInUSD(tokenUsdOracle, tokenBalance, token.decimals())
			: 0;
		uint256 compValueInUSD = _returnTokenGainsInUSD
			? getTokenValueInUSD(
				compUsdOracle,
				comp.balanceOf(address(this)),
				18 // COMP is in 18 decimal
			)
			: 0;
		if (tokenBalance <= totalProductivity) {
			return (0, 0, tokenBalance, balanceInUSD, compValueInUSD);
		}

		tokenGains = tokenBalance - totalProductivity;
		tokenGainsInUSD = _returnTokenGainsInUSD
			? getTokenValueInUSD(tokenUsdOracle, tokenGains, token.decimals()) +
				compValueInUSD
			: 0;

		iTokenGains = tokenWorthIniToken(tokenGains);
	}

	/**
	 * @dev Function to get interest transfer cost for this particular staking contract
	 */
	function getGasCostForInterestTransfer()
		external
		view
		override
		returns (uint32)
	{
		uint256 compBalance = comp.balanceOf(address(this));
		if (compBalance > 0) return collectInterestGasCost + 200000; // need to make more check for this value

		return collectInterestGasCost;
	}

	/**
	 * @dev Calculates worth of given amount of iToken in Token
	 * @param _amount Amount of token to calculate worth in Token
	 * @return Worth of given amount of token in Token
	 */
	function iTokenWorthInToken(uint256 _amount)
		public
		view
		override
		returns (uint256)
	{
		cERC20 cToken = cERC20(address(iToken));
		uint256 er = cToken.exchangeRateStored();
		(uint256 decimalDifference, bool caseType) = tokenDecimalPrecision();
		uint256 mantissa = 18 + tokenDecimal() - iTokenDecimal();
		uint256 tokenWorth = caseType == true
			? (_amount * (10**decimalDifference) * er) / 10**mantissa
			: ((_amount / (10**decimalDifference)) * er) / 10**mantissa; // calculation based on https://compound.finance/docs#protocol-math
		return tokenWorth;
	}

	/**
	 * @dev Calculates worth of given amount of token in iToken
	 * @param _amount Amount of iToken to calculate worth in token
	 * @return tokenWorth Worth of given amount of token in iToken
	 */
	function tokenWorthIniToken(uint256 _amount)
		public
		view
		returns (uint256 tokenWorth)
	{
		uint256 er = cERC20(address(iToken)).exchangeRateStored();
		(uint256 decimalDifference, bool caseType) = tokenDecimalPrecision();
		uint256 mantissa = 18 + tokenDecimal() - iTokenDecimal();
		tokenWorth = caseType == true
			? ((_amount / (10**decimalDifference)) * 10**mantissa) / er
			: ((_amount * (10**decimalDifference)) * 10**mantissa) / er; // calculation based on https://compound.finance/docs#protocol-math
	}

	/**
	 * @dev Set Gas cost to interest collection for this contract
	 * @param _collectInterestGasCost Gas cost to collect interest
	 * @param _rewardTokenCollectCost gas cost to collect reward tokens
	 */
	function setcollectInterestGasCostParams(
		uint32 _collectInterestGasCost,
		uint32 _rewardTokenCollectCost
	) external {
		_onlyAvatar();
		collectInterestGasCost = _collectInterestGasCost;
		compCollectGasCost = _rewardTokenCollectCost;
	}

	function _approveTokens() internal override {
		address uniswapRouter = address(uniswapContract);
		comp.approve(uniswapRouter, type(uint256).max);
		token.approve(uniswapRouter, type(uint256).max);
		token.approve(address(iToken), type(uint256).max); // approve the transfers to defi protocol as much as possible in order to save gas
	}
}
/**
 * @title Staking contract that donates earned interest to the DAO
 * allowing stakers to deposit Token
 * or withdraw their stake in Token
 * the contracts buy cToken and can transfer the daily interest to the  DAO
 */
function iTokenWorthInToken(uint256 _amount)
	public
	view
	override
	returns (uint256)
{
	cERC20 cToken = cERC20(address(iToken));
	uint256 er = cToken.exchangeRateStored();
	(uint256 decimalDifference, bool caseType) = tokenDecimalPrecision();
	uint256 mantissa = 18 + tokenDecimal() - iTokenDecimal();
	uint256 tokenWorth = caseType == true
		? (_amount * (10**decimalDifference) * er) / 10**mantissa
		: ((_amount / (10**decimalDifference)) * er) / 10**mantissa; // calculation based on https://compound.finance/docs#protocol-math
	return tokenWorth;
}

/**
 	 * @dev Calculates worth of given amount of iToken in Token
 	 * @param _amount Amount of token to calculate worth in Token
 	 * @return Worth of given amount of token in Token
 	 */