contract CoolBitETFUSDTAndCompound is WhiteListToken{
    using SafeMath for uint256;

    uint256 public baseRatio;
    string public name = "X-Saving Certificate";
    string public constant symbol = "XSCert";
    uint8 public decimals;

    // USDT token contract
    ITetherERC20 public StableToken;
    SimpleOracleAccruedRatioUSD public oracle;
    // Defi contract
    CERC20 public cToken;

    // Roles
    address public bincentiveHot; // i.e., Platform Owner
    address public bincentiveCold;
    address[] public investors;
    mapping(address => bool) public isInInvestorList;

    uint256 public numAUMDistributedInvestors; // i.e., number of investors that already received AUM

    // Contract(Fund) Status
    // 0: not initialized
    // 1: initialized
    // 2: not enough fund came in in time
    // 3: fundStarted
    // 4: running
    // 5: stoppped
    // 6: closed
    // 7: suspended
    uint256 public fundStatus;

    // Money
    mapping(address => uint256) public investorDepositUSDTAmount;  // denominated in stable token
    uint256 public currentInvestedAmount;  // denominated in stable token

    // Fund Parameters
    uint256 public investPaymentDueTime;  // deadline for deposit which comes in before fund starts running
    uint256 public percentageOffchainFund;  // percentage of fund that will be transfered off-chain
    uint256 public percentageMinimumFund;  // minimum percentage of fund required to keep the fund functioning
    uint256 public minimumFund;  // minimum amounf required to keep the fund functioning
    uint256 public minPenalty;  // a minimum 100 USDT penalty

    // Events
    event Deposit(address indexed investor, uint256 investAmount, uint256 mintedAmount);
    event UserInfo(bytes32 indexed uuid, string referralCode);
    event StartFund(uint256 timeStamp, uint256 num_investors, uint256 totalInvestedAmount, uint256 totalMintedTokenAmount);
    event Withdraw(address indexed investor, uint256 tokenAmount, uint256 USDTAmount, uint256 ToBincentiveColdUSDTAmount);
    event MidwayQuit(address indexed investor, uint256 tokenAmount, uint256 USDTAmount);
    event ReturnAUM(uint256 StableTokenAmount);
    event DistributeAUM(address indexed to, uint256 tokenAmount, uint256 StableTokenAmount);
    // Admin Events
    event NewBincentiveCold(address newBincentiveCold);
    // Defi Events
    event MintcUSDT(uint USDTAmount);
    event RedeemcUSDT(uint RedeemcUSDTAmount);

    // Modifiers
    modifier initialized() {
        require(fundStatus == 1);
        _;
    }

    // modifier fundStarted() {
    //     require(fundStatus == 3);
    //     _;
    // }

    modifier running() {
        require(fundStatus == 4);
        _;
    }

    modifier runningOrSuspended() {
        require((fundStatus == 4) || (fundStatus == 7));
        _;
    }

    modifier stoppedOrSuspended() {
        require((fundStatus == 5) || (fundStatus == 7));
        _;
    }

    modifier runningOrStoppedOrSuspended() {
        require((fundStatus == 4) || (fundStatus == 5) || (fundStatus == 7));
        _;
    }

    modifier closedOrAbortedOrSuspended() {
        require((fundStatus == 6) || (fundStatus == 2) || (fundStatus == 7));
        _;
    }

    modifier isBincentive() {
        require(
            (msg.sender == bincentiveHot) || (msg.sender == bincentiveCold)
        );
        _;
    }

    modifier isBincentiveCold() {
        require(msg.sender == bincentiveCold);
        _;
    }

    modifier isInvestor() {
        // bincentive is not investor
        require(msg.sender != bincentiveHot);
        require(msg.sender != bincentiveCold);
        require(balances[msg.sender] > 0);
        _;
    }


    // Transfer functions for USDT
    function checkBalanceTransfer(address to, uint256 amount) internal {
        uint256 balanceBeforeTransfer = StableToken.balanceOf(to);
        uint256 balanceAfterTransfer;
        StableToken.transfer(to, amount);
        balanceAfterTransfer = StableToken.balanceOf(to);
        require(balanceAfterTransfer == balanceBeforeTransfer.add(amount));
    }

    function checkBalanceTransferFrom(address from, address to, uint256 amount) internal {
        uint256 balanceBeforeTransfer = StableToken.balanceOf(to);
        uint256 balanceAfterTransfer;
        StableToken.transferFrom(from, to, amount);
        balanceAfterTransfer = StableToken.balanceOf(to);
        require(balanceAfterTransfer == balanceBeforeTransfer.add(amount));
    }


    // Getter Functions

    // Get the balance of an investor, denominated in stable token
    function getBalanceValue(address investor) public view returns(uint256) {
        uint256 accruedRatioUSDT = oracle.query();
        return balances[investor].mul(accruedRatioUSDT).div(baseRatio);
    }

    // Defi Functions

    function querycUSDTAmount() internal returns(uint256) {
        return cToken.balanceOf(address(this));
    }

    function querycExgRate() internal returns(uint256) {
        return cToken.exchangeRateCurrent();
    }

    function mintcUSDT(uint USDTAmount) public isBincentive {

        StableToken.approve(address(cToken), USDTAmount); // approve the transfer
        assert(cToken.mint(USDTAmount) == 0);

        emit MintcUSDT(USDTAmount);
    }

    function redeemcUSDT(uint RedeemcUSDTAmount) public isBincentive {

        require(cToken.redeem(RedeemcUSDTAmount) == 0, "something went wrong");

        emit RedeemcUSDT(RedeemcUSDTAmount);
    }


    // Investor Deposit
    // It can either be called by investor directly or by bincentive accounts.
    // Only the passed in argument `investor` would be treated as the real investor.
    function deposit(address investor, uint256 depositUSDTAmount, bytes32 uuid, string referralCode) initialized public {
        require(now < investPaymentDueTime, "Deposit too late");
        require((investor != bincentiveHot) && (investor != bincentiveCold), "Investor can not be bincentive accounts");
        require(depositUSDTAmount > 0, "Deposited stable token amount should be greater than zero");

        // Transfer Stable Token to this contract
        checkBalanceTransferFrom(msg.sender, address(this), depositUSDTAmount);

        // Add investor to investor list if not present in the record before
        if(isInInvestorList[investor] == false) {
            investors.push(investor);
            isInInvestorList[investor] = true;
        }
        currentInvestedAmount = currentInvestedAmount.add(depositUSDTAmount);
        investorDepositUSDTAmount[investor] = investorDepositUSDTAmount[investor].add(depositUSDTAmount);

        // Query Oracle for current stable token ratio
        uint256 accruedRatioUSDT = oracle.query();
        // Mint and distribute tokens to investors
        uint256 mintedTokenAmount;
        mintedTokenAmount = depositUSDTAmount.mul(baseRatio).div(accruedRatioUSDT);
        _mint(investor, mintedTokenAmount);

        emit Deposit(investor, depositUSDTAmount, mintedTokenAmount);
        emit UserInfo(uuid, referralCode);
    }

    // Start Investing
    // Send part of the funds offline
    // and calculate the minimum amount of fund needed to keep the fund functioning
    // and calculate the maximum amount of fund allowed to be withdrawn per period.
    function start() initialized isBincentive public {
        // Send some USDT offline
        uint256 amountSentOffline = currentInvestedAmount.mul(percentageOffchainFund).div(100);
        checkBalanceTransfer(bincentiveCold, amountSentOffline);

        minimumFund = totalSupply().mul(percentageMinimumFund).div(100);
        // Start the contract
        fundStatus = 4;
        emit StartFund(now, investors.length, currentInvestedAmount, totalSupply());
    }

    function amountWithdrawable() public view returns(uint256) {
        return totalSupply().sub(minimumFund);
    }

    function isAmountWithdrawable(address investor, uint256 tokenAmount) public view returns(bool) {
        require(tokenAmount > 0, "Withdrawn amount must be greater than zero");
        require(balances[investor] >= tokenAmount, "Not enough token to be withdrawn");
        require(totalSupply().sub(tokenAmount) >= minimumFund, "Amount of fund left would be less than minimum fund threshold after withdrawal");

        return true;
    }

    function withdraw(address investor, uint256 tokenAmount) running isBincentive public {
        require(tokenAmount > 0, "Withdrawn amount must be greater than zero");
        require(balances[investor] >= tokenAmount, "Not enough token to be withdrawn");
        require(totalSupply().sub(tokenAmount) >= minimumFund, "Amount of fund left would be less than minimum fund threshold after withdrawal");

        uint256 investorBalanceBeforeWithdraw = balances[investor];
        // Substract withdrawing amount from investor's balance
        _burn(investor, tokenAmount);

        uint256 depositUSDTAmount = investorDepositUSDTAmount[investor];

        // Query Oracle for current stable token ratio
        uint256 accruedRatioUSDT = oracle.query();
        uint256 principle;
        uint256 interest;
        uint256 amountUSDTToWithdraw;
        uint256 amountUSDTForInvestor;
        uint256 amountUSDTToBincentiveCold;

        amountUSDTToWithdraw = tokenAmount.mul(accruedRatioUSDT).div(baseRatio);
        principle = depositUSDTAmount.mul(tokenAmount).div(investorBalanceBeforeWithdraw);
        interest = amountUSDTToWithdraw.sub(principle);
        amountUSDTForInvestor = principle.mul(99).div(100).add(interest.div(2));
        amountUSDTToBincentiveCold = amountUSDTToWithdraw.sub(amountUSDTForInvestor);

        // Check if `amountUSDTToBincentiveCold >= penalty`
        if (amountUSDTToBincentiveCold < minPenalty) {
            uint256 dif = minPenalty.sub(amountUSDTToBincentiveCold);
            require(dif <= amountUSDTForInvestor, "Withdraw amount is not enough to cover minimum penalty");
            amountUSDTForInvestor = amountUSDTForInvestor.sub(dif);
            amountUSDTToBincentiveCold = minPenalty;
        }

        investorDepositUSDTAmount[investor] = investorDepositUSDTAmount[investor].sub(principle);

        checkBalanceTransfer(investor, amountUSDTForInvestor);
        checkBalanceTransfer(bincentiveCold, amountUSDTToBincentiveCold);

        emit Withdraw(investor, tokenAmount, amountUSDTForInvestor, amountUSDTToBincentiveCold);

        // Suspend the contract if not enough fund remained
        if(totalSupply() == minimumFund) {
            fundStatus = 7;
        }
    }

    // Return AUM
    // Transfer the fund back to the contract
    function returnAUM(uint256 stableTokenAmount) runningOrSuspended isBincentiveCold public {
        // Option 1: contract transfer AUM directly from bincentiveCold
        checkBalanceTransferFrom(bincentiveCold, address(this), stableTokenAmount);

        emit ReturnAUM(stableTokenAmount);

        // If fund is running, stop the fund after AUM is returned
        if(fundStatus == 4) fundStatus = 5;
    }

    // Add an overlay on top of underlying token transfer
    // because token receiver should also be added to investor list to be able to receive AUM.
    function transfer(address _to, uint256 _value) public returns (bool){
        uint256 tokenBalanceBeforeTransfer = balances[msg.sender];
        bool success = super.transfer(_to, _value);

        if(success == true) {
            if(isInInvestorList[_to] == false) {
                investors.push(_to);
                isInInvestorList[_to] = true;
            }
            // Also transfer the deposited USDT so the receiver can withdraw
            uint256 USDTAmountToTransfer = investorDepositUSDTAmount[msg.sender].mul(_value).div(tokenBalanceBeforeTransfer);
            investorDepositUSDTAmount[msg.sender] = investorDepositUSDTAmount[msg.sender].sub(USDTAmountToTransfer);
            investorDepositUSDTAmount[_to] = investorDepositUSDTAmount[_to].add(USDTAmountToTransfer);
        }
        return success;
    }

    // Add an overlay on top of underlying token transferFrom
    // because token receiver should also be added to investor list to be able to receive AUM.
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool){
        uint256 tokenBalanceBeforeTransfer = balances[_from];
        bool success = super.transferFrom(_from, _to, _value);

        if(success == true) {
            if(isInInvestorList[_to] == false) {
                investors.push(_to);
                isInInvestorList[_to] = true;
            }
            // Also transfer the deposited USDT so the receiver can withdraw
            uint256 USDTAmountToTransfer = investorDepositUSDTAmount[_from].mul(_value).div(tokenBalanceBeforeTransfer);
            investorDepositUSDTAmount[_from] = investorDepositUSDTAmount[_from].sub(USDTAmountToTransfer);
            investorDepositUSDTAmount[_to] = investorDepositUSDTAmount[_to].add(USDTAmountToTransfer);
        }
        return success;
    }

    function update_investor(address _old_address, address _new_address) public isBincentive {
        require((_new_address != bincentiveHot) && (_new_address != bincentiveCold), "Investor can not be bincentive accounts");
        require(isInInvestorList[_old_address] == true, "Investor does not exist");

        uint256 balance = balances[_old_address];
        balances[_old_address] = balances[_old_address].sub(balance);
        balances[_new_address] = balances[_new_address].add(balance);
        emit Transfer(_old_address, _new_address, balance);
        if(isInInvestorList[_new_address] == false) {
            investors.push(_new_address);
            isInInvestorList[_new_address] = true;
        }
        uint256 USDTAmountToTransfer = investorDepositUSDTAmount[_old_address];
        investorDepositUSDTAmount[_old_address] = investorDepositUSDTAmount[_old_address].sub(USDTAmountToTransfer);
        investorDepositUSDTAmount[_new_address] = investorDepositUSDTAmount[_new_address].add(USDTAmountToTransfer);
    }

    // Distribute AUM
    // Dispense the fund returned to each investor according to his portion of the token he possessed.
    // All withdraw requests should be processed before calling this function.
    // Since there might be too many investors, each time this function is called,
    // a parameter `numInvestorsToDistribute` is passed in to indicate how many investors to process this time.
    function distributeAUM(uint256 numInvestorsToDistribute) stoppedOrSuspended isBincentive public {
        require(numAUMDistributedInvestors.add(numInvestorsToDistribute) <= investors.length, "Distributing to more than total number of investors");

        // Query Oracle for current stable token ratio
        uint256 accruedRatioUSDT = oracle.query();

        uint256 stableTokenDistributeAmount;
        address investor;
        uint256 investor_amount;
        // Distribute Stable Token to investors
        for(uint i = numAUMDistributedInvestors; i < (numAUMDistributedInvestors.add(numInvestorsToDistribute)); i++) {
            investor = investors[i];
            investor_amount = balances[investor];
            if(investor_amount == 0) continue;
            _burn(investor, investor_amount);

            stableTokenDistributeAmount = investor_amount.mul(accruedRatioUSDT).div(baseRatio);
            checkBalanceTransfer(investor, stableTokenDistributeAmount);

            emit DistributeAUM(investor, investor_amount, stableTokenDistributeAmount);
        }

        numAUMDistributedInvestors = numAUMDistributedInvestors.add(numInvestorsToDistribute);
        // If all investors have received AUM, then close the fund.
        if(numAUMDistributedInvestors >= investors.length) {
            currentInvestedAmount = 0;
            // If fund is stopped, close the fund
            if(fundStatus == 5) fundStatus = 6;
        }
    }

    function claimWronglyTransferredFund() closedOrAbortedOrSuspended isBincentive public {
        // withdraw leftover funds from Defi
        uint256 totalcUSDTAmount;
        totalcUSDTAmount = querycUSDTAmount();
        redeemcUSDT(totalcUSDTAmount);

        uint256 leftOverAmount = StableToken.balanceOf(address(this));
        if(leftOverAmount > 0) {
            checkBalanceTransfer(bincentiveCold, leftOverAmount);
        }
    }

    function updateBincentiveColdAddress(address _newBincentiveCold) public isBincentiveCold {
        require(_newBincentiveCold != address(0), "New BincentiveCold address can not be zero");

        bincentiveCold = _newBincentiveCold;
        emit NewBincentiveCold(_newBincentiveCold);
    }

    constructor(
        address _oracle,
        address _StableToken,
        address _cToken,
        address _bincentiveHot,
        address _bincentiveCold,
        uint256 _investPaymentPeriod,
        uint256 _percentageOffchainFund,
        uint256 _percentageMinimumFund) WhiteListToken(_bincentiveCold) public {

        oracle = SimpleOracleAccruedRatioUSD(_oracle);
        bincentiveHot = _bincentiveHot;
        bincentiveCold = _bincentiveCold;
        StableToken = ITetherERC20(_StableToken);
        cToken = CERC20(_cToken);

        decimals = StableToken.decimals();
        minPenalty = 100 * (10 ** uint256(decimals));  // a minimum 100 USDT penalty
        baseRatio = oracle.query();
        require(baseRatio > 0, "baseRatio should always greater than zero");

        // Set parameters
        investPaymentDueTime = now.add(_investPaymentPeriod);
        percentageOffchainFund = _percentageOffchainFund;
        percentageMinimumFund = _percentageMinimumFund;

        // Initialized the contract
        fundStatus = 1;
    }
}
function querycUSDTAmount() internal returns(uint256) {
    return cToken.balanceOf(address(this));
}

// Defi Functions