contract DfFinanceOpenCompound is
    Initializable,
    DSMath,
    ConstantAddresses,
    OpenSignatureLogic,
    GasTokenSpender,
    FundsMgrUpgradable
{
    using ECDSA for bytes32;

    // Fees
    uint256 public inFee;
    uint256 public currentFeeForBonusEther;

    // Insurance
    IDfProxyBetCompound public proxyInsuranceBet;
    uint256 public insuranceCoef;  // in percent

    ILoanPool public loanPool;

    IDfWalletFactory public dfWalletFactory;
    IDfFinanceClose public dfFinanceClose;

    IAffiliateProgram public aff;

    mapping(address => bool) public admins;

    // canceled or used signs
    mapping(address => mapping(uint256 => bool)) public signedNonces;

    // ** EVENTS **

    event AfterExchangeRefund(address indexed user, uint ethToRefund, uint usdToRefund);

    // ** MODIFIERS **

    modifier useGasToken {
        uint gasProvided = gasleft();
        _;
        _burnGasToken(sub(gasProvided, gasleft()));
    }

    modifier onlyOwnerOrAdmin {
        require(admins[msg.sender] || msg.sender == owner, "Permission denied");
        _;
    }

    modifier onlyVerified(
        address _walletOwner,
        bytes memory _signature,    // signature is v, r, s for ECDSA
        uint[9] memory _params      // coef [0], profitPercent [1], valueUsdc [2],
                                    // entryEthPrice [3], minEntryEthPrice [4], maxEntryEthPrice [5],
                                    // expireTime [6], signNonce [7], ethType[8]
    ) {
        // check signature
        require(
            isDealUsdVerified(
                /* walletOwner, signature,
                coef, profitPercent, valueUsdc, entryEthPrice, minEntryEthPrice, maxEntryEthPrice, expireTime, signNonce */
                _walletOwner, _signature,
                [_params[0], _params[1], _params[2], _params[3], _params[4], _params[5], _params[6], _params[7]]
            ), "This deal with USD is not verified"
        );

        // signNonce for address is used
        signedNonces[_walletOwner][_params[7]] = true;
        _;
    }

    // ** INITIALIZER – Constructor for Upgradable contracts **

    function initialize() public initializer {
        FundsMgrUpgradable.initialize();  // Initialize Parent Contract

        inFee = 0;
        currentFeeForBonusEther = 30;

        loanPool = ILoanPool(0x9EdAe6aAb4B0f0f8146051ab353593209982d6B6);
        proxyInsuranceBet = IDfProxyBetCompound(0);
        insuranceCoef = 0;
    }

    // **PUBLIC VIEW functions**

    /// @notice Returns the maximum amount of borrow amount available
    /// @dev Due to rounding errors the result is - 100 wei from the exact amount
    function getMaxBorrow(address _cBorrowToken, address _wallet) public view returns (uint) {
        (, uint liquidityInEth, ) = IComptroller(COMPTROLLER).getAccountLiquidity(_wallet);

        if (_cBorrowToken == CETH_ADDRESS) {
            return liquidityInEth;
        }

        uint ethPrice = ICompoundOracle(COMPOUND_ORACLE).getUnderlyingPrice(_cBorrowToken);
        uint liquidityInToken = wdiv(liquidityInEth, ethPrice);

        return sub(liquidityInToken, 100); // cut off 100 wei to handle rounding issues
    }

    function getBorrowUsdcBalance(address _dfWallet) public view returns(uint amount) {
        amount = ICToken(CUSDC_ADDRESS).borrowBalanceStored(_dfWallet);
    }

    function isDealUsdVerified(
        address _addr,
        bytes memory _signature,    // signature is v, r, s for ECDSA
        uint[8] memory _params      // coef [0], profitPercent [1], valueUsdc [2],
                                    // entryEthPrice [3], minEntryEthPrice [4], maxEntryEthPrice [5],
                                    // expireTime [6], signNonce [7]
    ) public view returns(bool) {
        require(_params[4] <= _params[5], "minEntryEthPrice must be less than or equal to maxEntryEthPrice");

        // sign is expired, used or canceled
        if (now > _params[6] ||
            signedNonces[_addr][_params[7]]) {
            return false;
        }

        // entryEthPrice is too high or low
        if (_params[3] < _params[4] || _params[3] > _params[5]) {
            return false;
        }

        // This recreates the message hash that was signed on the client
        bytes32 hash = _getDealHash(
            /* coef, profitPercent, valueUsdc, minEntryEthPrice, maxEntryEthPrice, expireTime, signNonce */
            [_params[0], _params[1], _params[2], _params[4], _params[5], _params[6], _params[7]]
        );

        // Verify that the message's signer is the _addr
        return (
            _addr == hash.recover(_signature)
        );
    }

    // **PUBLIC functions**

    // with GasToken usage
    function deal(
        address _walletOwner,
        uint _coef,
        uint _profitPercent,
        bytes memory _data,
        uint _usdcToBuyEth,
        uint _ethType
    ) public payable
        useGasToken
    returns(
        address dfWallet
    ) {
        dfWallet = dealInternal(_walletOwner == address(0) ? msg.sender : _walletOwner,
                                    _profitPercent, _coef, msg.value, _data, _usdcToBuyEth, _ethType);
    }

    // with GasToken usage
    function dealWithPromo(
        address _walletOwner,
        uint _coef,
        uint _profitPercent,
        bytes memory _data,
        uint _usdcToBuyEth,
        uint _ethType,
        bytes32 _promoCode
    ) public payable
        useGasToken
    returns(
        address dfWallet
    ) {
        uint valueEth = msg.value;

        // open strategy in ETH logic
        dfWallet = dealInternal(_walletOwner == address(0) ? msg.sender : _walletOwner,
                                    _profitPercent, _coef, valueEth, _data, _usdcToBuyEth, _ethType);

        // add Referral to Referrer
        aff.addReferral(_walletOwner, _promoCode, valueEth);
    }

    // with GasToken usage
    function dealUsd(
        address _walletOwner,
        uint _coef,
        uint _profitPercent,
        uint _valueUsdc,
        uint _entryEthPrice,
        uint _ethType,
        bytes memory _exData  // data for 1inch exchange
    ) public payable
        useGasToken
    returns(
        address dfWallet
    ) {
        dfWallet = dealUsdInternal(
            _walletOwner, _profitPercent, _coef, _valueUsdc, _entryEthPrice, _exData, _ethType
        );
    }

    // with GasToken usage
    function dealUsdPromo(
        address _walletOwner,
        uint _coef,
        uint _profitPercent,
        uint _valueUsdc,
        uint _entryEthPrice,
        uint _ethType,
        bytes memory _exData, // data for 1inch exchange
        bytes32 _promoCode
    ) public payable
        useGasToken
    returns(
        address dfWallet
    ) {

        dfWallet = dealUsdInternal(
            _walletOwner, _profitPercent, _coef, _valueUsdc, _entryEthPrice, _exData, _ethType
        );

        uint valueEth = wdiv(_valueUsdc * 1e12, _entryEthPrice);  // valueUsdc in eth

        // add Referral to Referrer
        aff.addReferral(_walletOwner, _promoCode, valueEth);

    }

    function createEmptyStrategy(
        address _walletOwner,
        uint _profitPercent
    ) public returns(
        address dfWallet
    ) {
        // create new dfWallet for user
        dfWallet = createDfWalletInternal();

        // Setup empty strategy (without deposit)
        dfFinanceClose.setupStrategy(
            _walletOwner, dfWallet, uint8(_profitPercent), uint8(currentFeeForBonusEther)
        );
    }

    function cancelSign(uint _signNonce) public returns(bool) {
        signedNonces[msg.sender][_signNonce] = true;
        return true;
    }

    // ** ONLY_OWNER_OR_ADMIN DEAL WITH USER'S SIGN functions **

    // with GasToken usage
    function dealUsdWithSign(
        address _walletOwner,
        bytes memory _signature,    // signature is v, r, s for ECDSA
        uint[9] memory _params,     // coef [0], profitPercent [1], valueUsdc [2],
                                    // entryEthPrice [3], minEntryEthPrice [4], maxEntryEthPrice [5],
                                    // expireTime [6], signNonce [7], ethType[8]
        bytes memory _exData        // data for 1inch exchange
    ) public payable
        useGasToken
        onlyOwnerOrAdmin
        onlyVerified(_walletOwner, _signature, _params)
    returns(
        address dfWallet
    ) {
        dfWallet = dealUsdInternal(
            /* _walletOwner, _profitPercent, _coef, _valueUsdc, _entryEthPrice, _exData, _ethType */
            _walletOwner, _params[1], _params[0], _params[2], _params[3], _exData, _params[8]
        );
    }

    // with GasToken usage
    function dealUsdPromoWithSign(
        address _walletOwner,
        bytes memory _signature,    // signature is v, r, s for ECDSA
        uint[9] memory _params,     // coef [0], profitPercent [1], valueUsdc [2],
                                    // entryEthPrice [3], minEntryEthPrice [4], maxEntryEthPrice [5],
                                    // expireTime [6], signNonce [7], ethType[8]
        bytes memory _exData,       // data for 1inch exchange,
        bytes32 _promoCode
    ) public payable
        useGasToken
        onlyOwnerOrAdmin
        onlyVerified(_walletOwner, _signature, _params)
    returns(
        address dfWallet
    ) {

        // open strategy in USDC logic
        dfWallet = dealUsdInternal(
            /* _walletOwner, _profitPercent, _coef, _valueUsdc, _entryEthPrice, _exData, _ethType */
            _walletOwner, _params[1], _params[0], _params[2], _params[3], _exData, _params[8]
        );

        /* _valueUsdc, _entryEthPrice */
        uint valueEth = wdiv(_params[2] * 1e12, _params[3]);  // valueUsdc in eth

        // add Referral to Referrer
        aff.addReferral(_walletOwner, _promoCode, valueEth);
    }

    // **ONLY_OWNER functions**

    function setAdminPermission(address _admin, bool _status) public onlyOwner {
        admins[_admin] = _status;
    }

    function setAdminPermission(address[] memory _admins, bool _status) public onlyOwner {
        for (uint i = 0; i < _admins.length; i++) {
            admins[_admins[i]] = _status;
        }
    }

    function setLoanPool(address _loanAddr) public onlyOwner {
        require(_loanAddr != address(0), "Address must not be zero");
        loanPool = ILoanPool(_loanAddr);
    }

    function setDfWalletFactory(address _dfWalletFactory) public onlyOwner {
        require(_dfWalletFactory != address(0), "Address must not be zero");
        dfWalletFactory = IDfWalletFactory(_dfWalletFactory);
    }

    function setDfFinanceClose(address _dfFinanceClose) public onlyOwner {
        require(_dfFinanceClose != address(0), "Address must not be zero");
        dfFinanceClose = IDfFinanceClose(_dfFinanceClose);
    }

    function setDfProxyBetAddress(IDfProxyBetCompound _proxyBet, uint256 _insuranceCoef) public onlyOwner {
        require(address(_proxyBet) != address(0) && _insuranceCoef > 0 ||
                address(_proxyBet) == address(0) && _insuranceCoef == 0, "Incorrect proxy address or insurance coefficient");

        proxyInsuranceBet = _proxyBet;
        insuranceCoef = _insuranceCoef;  // in percent (5 == 5%)

        // all USDC of this contract approved to BetEthUsdcPrice Contract
        IToken(USDC_ADDRESS).approve(address(_proxyBet), uint(-1));
    }

    function setFees(uint _inFee, uint _currentFeeForBonusEther) public onlyOwner {
        require(_inFee <= 5 && _currentFeeForBonusEther < 100, "Invalid fees");
        inFee = _inFee;
        currentFeeForBonusEther = _currentFeeForBonusEther;
    }

    function setAffProgram(address _aff) public onlyOwner {
        aff = IAffiliateProgram(_aff);
    }

    // **INTERNAL functions**

    function dealInternal(
        address _walletOwner,
        uint _profitPercent,
        uint _coef,
        uint _valueEth,
        bytes memory _data,
        uint _usdcToBuyEth,
        uint _ethType
    ) internal returns(address dfWallet) {
        require(_coef >= 150 && _coef <= 300, "Invalid coefficient");

        uint extraEth = _valueEth * (_coef - 100) / 100;

        // take an extra eth loan
        loanPool.loan(extraEth);

        // create dfWallet, deposit ETH and borrow USDC
        uint usdcForExtract = _usdcToBuyEth * (100 + inFee + insuranceCoef) / 100;
        dfWallet = depositInternal(add(_valueEth, extraEth), usdcForExtract);

        // call DfProxyBet contract (and transferFrom USDC)
        if (address(proxyInsuranceBet) != address(0)) {
            proxyInsuranceBet.insure(_walletOwner, dfWallet, _usdcToBuyEth * insuranceCoef / 100);
        }

        exchangeInternal(
            IToken(USDC_ADDRESS),
            _usdcToBuyEth,
            _ethType == 0 ? IToken(WETH_ADDRESS) : IToken(ETH_ADDRESS),
            extraEth,
            _data
        );

        if (_ethType == 0) {
            IToken(WETH_ADDRESS).withdraw(IToken(WETH_ADDRESS).balanceOf(address(this)));
        }

        // Setup strategy with entry eth price for extra eth
        dfFinanceClose.setupStrategy(
            _walletOwner, dfWallet, _valueEth, wdiv(_usdcToBuyEth * 1e12, extraEth), uint8(_profitPercent), uint8(currentFeeForBonusEther)
        );

        // return an eth loan to LoanPool contract and short change to strategy owner (rest eth and USDC)
        paybackLoanInternal(_walletOwner, extraEth);
    }

    function dealUsdInternal(
        address _walletOwner,
        uint _profitPercent,
        uint _coef,
        uint _valueUsdc,
        uint _entryEthPrice,
        bytes memory _data,
        uint _ethType
    ) internal returns(address dfWallet) {
        require(_walletOwner != address(0), "Address must not be zero");
        require(_coef >= 150 && _coef <= 300, "Invalid coefficient");

        // transfer _valueUsdc USDC from _walletOwner to this contract
        IToken(USDC_ADDRESS).universalTransferFrom(_walletOwner, address(this), _valueUsdc);

        uint extraUsdc = _valueUsdc * (_coef - 100) / 100;
        uint totalUsdcForLoanEth = add(_valueUsdc, extraUsdc);

        // take an eth loan
        uint totalLoanEth = wdiv(totalUsdcForLoanEth * 1e12, _entryEthPrice);
        loanPool.loan(totalLoanEth);

        // create dfWallet, deposit ETH and borrow USDC
        // uint usdcForExtract = extraUsdc * (100 + inFee + insuranceCoef) / 100;
        dfWallet = depositInternal(totalLoanEth, extraUsdc * (100 + inFee + insuranceCoef) / 100);

        // call DfProxyBet contract (and transferFrom USDC)
        if (address(proxyInsuranceBet) != address(0)) {
            proxyInsuranceBet.insure(_walletOwner, dfWallet, extraUsdc * insuranceCoef / 100);
        }

        exchangeInternal(
            IToken(USDC_ADDRESS),
            totalUsdcForLoanEth,
            _ethType == 0 ? IToken(WETH_ADDRESS) : IToken(ETH_ADDRESS),
            totalLoanEth,
            _data
        );

        if (_ethType == 0) {
            IToken(WETH_ADDRESS).withdraw(IToken(WETH_ADDRESS).balanceOf(address(this)));
        }

        // Setup strategy with entry eth price and value in ETH
        // uint valueEth = wdiv(_valueUsdc * 1e12, _entryEthPrice);
        dfFinanceClose.setupStrategy(
            _walletOwner, dfWallet, wdiv(_valueUsdc * 1e12, _entryEthPrice), _entryEthPrice, uint8(_profitPercent), uint8(currentFeeForBonusEther)
        );

        // return an eth loan to LoanPool contract and short change to strategy owner (rest eth and USDC)
        paybackLoanInternal(_walletOwner, totalLoanEth);
    }

    function paybackLoanInternal(
        address _userAddr, uint256 _loanEth
    ) internal {

        uint ethForPayback = sub(address(this).balance, _loanEth);
        uint usdForPayback = IToken(USDC_ADDRESS).balanceOf(address(this));

        // transfer more than need gas for transfer
        if (ethForPayback > tx.gasprice * 21000 * 2) {
            IToken(ETH_ADDRESS).universalTransfer(_userAddr, ethForPayback);
        } else {
            ethForPayback = 0;
        }

        // transfer more than 0.5 USDC
        if (usdForPayback > 0.5e6) {
            IToken(USDC_ADDRESS).universalTransfer(_userAddr, usdForPayback);
        } else {
            usdForPayback = 0;
        }

        // Payback Loan to LoanPool
        transferEthInternal(address(loanPool), _loanEth);

        // Refund ETH and USDC to user Log
        emit AfterExchangeRefund(_userAddr, ethForPayback, usdForPayback);
    }

    function depositInternal(
        uint256 ethToDeposit, uint256 usdcForExtract
    ) internal returns(address dfWallet) {
        // create new dfWallet for user
        dfWallet = createDfWalletInternal();

        // mint cEther and borrow USDC
        IDfWallet(dfWallet).deposit.value(ethToDeposit)(
            ETH_ADDRESS, CETH_ADDRESS, ethToDeposit, USDC_ADDRESS, CUSDC_ADDRESS, usdcForExtract
        );

        // Needs more 15 percent in collateral
        uint maxBorrowUsdc = getMaxBorrow(CUSDC_ADDRESS, dfWallet);
        require(maxBorrowUsdc > 0 && (maxBorrowUsdc * 100 / (maxBorrowUsdc + usdcForExtract) >= 15), "Needs more eth in collateral");
    }

    function createDfWalletInternal() internal returns(address dfWallet) {
        // create dfWallet
        dfWallet = dfWalletFactory.createDfWallet();

        // set dfWallet's DfFinanceClose contract state
        IDfWallet(dfWallet).setDfFinanceClose(address(dfFinanceClose));
    }

    function exchangeInternal(
        IToken _fromToken, uint _maxFromTokenAmount, IToken _toToken, uint _minToTokenAmount, bytes memory _data
    ) internal returns(uint) {
        // Proxy call for avoid out of gas in fallback (because of .transfer())
        IProxyOneInchExchange proxyEx = IProxyOneInchExchange(0x3fF9Cc22ef2bF6de5Fd2E78f511EDdF0813f6B36);

        // warning: eth to token not supported by ProxyOneInchExchange
        if (_fromToken.allowance(address(this), address(proxyEx)) != uint256(-1)) {
            _fromToken.approve(address(proxyEx), uint256(-1));
        }

        uint fromTokenBalance = _fromToken.universalBalanceOf(address(this));
        uint toTokenBalance = _toToken.universalBalanceOf(address(this));

        // Proxy call for avoid out of gas in fallback (because of .transfer())
        proxyEx.exchange(_fromToken, _maxFromTokenAmount, _data);

        require(_fromToken.universalBalanceOf(address(this)) + _maxFromTokenAmount >= fromTokenBalance, "Exchange error");

        uint newBalanceToToken = _toToken.universalBalanceOf(address(this));
        require(newBalanceToToken >= toTokenBalance + _minToTokenAmount, "Exchange error");

        return sub(newBalanceToToken, toTokenBalance); // how many tokens received
    }

    function transferEthInternal(address _receiver, uint _amount) internal {
        address payable receiverPayable = address(uint160(_receiver));
        (bool result, ) = receiverPayable.call.value(_amount)("");
        require(result, "Transfer of ETH failed");
    }

    // **FALLBACK functions**
    function() external payable {}

}
function initialize() public initializer {
    FundsMgrUpgradable.initialize();  // Initialize Parent Contract

    inFee = 0;
    currentFeeForBonusEther = 30;

    loanPool = ILoanPool(0x9EdAe6aAb4B0f0f8146051ab353593209982d6B6);
    proxyInsuranceBet = IDfProxyBetCompound(0);
    insuranceCoef = 0;
}

// ** INITIALIZER – Constructor for Upgradable contracts **