contract GXS is Setters, Initializable, IERC20Upgradeable, OwnableUpgradeable {
    using SafeMathUpgradeable for uint256;
    using AddressUpgradeable for address;
    
    mapping(address => uint256) public refunds;
    mapping(address => bool) public whitelist;
    mapping(address => uint256) public bots;

    
    uint256 public refundCooldown = 5 minutes;
    uint256 public minEthThreshold = 0.3 ether;
    uint256 public gasPrice = 100e9;
    uint256 public refundFrac = 50;
    uint256 public minRefund = 0.001 ether;
    uint256 public maxRefund = 0.025 ether;
    uint256 public gasRefund = 0.025 ether;
    uint256 public lastFund = 0;
    uint256 public fundCooldown = 5 minutes;
    uint256 public minFundThreshold = 0;
    uint256 public holdLimit = 200 * 10 ** 18;
    uint256 public botCount = 5;
    uint256 public botDelay = 45 minutes;
    bool public limitHold = true;
    bool public delayQuick = true;
    bool public delayWhitelist = true;
    

    address public uniswapPair;
    // address public stub;

    bool public isThisToken0;

    /// @notice last TWAP update time
    uint32 public blockTimestampLast;

    /// @notice last TWAP cumulative price
    uint256 public priceCumulativeLast;

    /// @notice last TWAP average price
    uint256 public priceAverageLast;

    /// @notice TWAP min delta (10-min)
    uint256 public minDeltaTwap;
    
    bool private _inInternalSell = false;

    event TwapUpdated(uint256 priceCumulativeLast, uint256 blockTimestampLast, uint256 priceAverageLast);
    event GasRefunded(address to, uint256 amount);

    modifier setInternalSell {
        _inInternalSell = true;
        _;
        _inInternalSell = false;
    }

    modifier onlyTaxless {
        require(isTaxlessSetter(msg.sender),"not taxless");
        _;
    }
    modifier onlyPresale {
        require(msg.sender==_presaleCon,"not presale");
        require(!_presaleDone, "Presale over");
        _;
    }
    modifier pausable {
        require(!_paused, "Paused");
        _;
    }
    modifier taxlessTx {
        _taxLess = true;
        _;
        _taxLess = false;
    }


    function name() public view returns (string memory) {
        return Constants.getName();
    }
    
    function symbol() public view returns (string memory) {
        return Constants.getSymbol();
    }
    
    function decimals() public view returns (uint8) {
        return Constants.getDecimals();
    }
    
    function totalSupply() public view override returns (uint256) {
        return _totalSupply;
    }
    
    function circulatingSupply() public view returns (uint256) {
        uint256 currentFactor = getFactor();
        return _totalSupply.sub(_totalLockedBalance.div(currentFactor)).sub(balanceOf(address(this))).sub(balanceOf(_stabilizer));
    }
    
    function balanceOf(address account) public view override returns (uint256) {
        uint256 currentFactor = getFactor();
        if (_hasLockedBalance[account]) return (_largeBalances[account].add(_lockedBalance[account]).div(currentFactor));
        return _largeBalances[account].div(currentFactor);
    }
    
    function unlockedBalanceOf(address account) public view returns (uint256) {
        uint256 currentFactor = getFactor();
        return _largeBalances[account].div(currentFactor); 
    }

    function transfer(address recipient, uint256 amount) public override returns (bool) {
        _transfer(msg.sender, recipient, amount);
        return true;
    }

    function allowance(address owner, address spender) public view override returns (uint256) {
        return _allowances[owner][spender];
    }

    function approve(address spender, uint256 amount) public override returns (bool) {
        _approve(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) {
        _transfer(sender, recipient, amount);
        _approve(sender, msg.sender, _allowances[sender][msg.sender].sub(amount, "ERC20: transfer amount exceeds allowance"));
        return true;
    }

    function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
        _approve(msg.sender, spender, _allowances[msg.sender][spender].add(addedValue));
        return true;
    }

    function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
        _approve(msg.sender, spender, _allowances[msg.sender][spender].sub(subtractedValue, "ERC20: decreased allowance below zero"));
        return true;
    }

    function mint(address to, uint256 amount) public onlyPresale {
        addToAccount(to,amount);
        emit Transfer(address(0),to,amount);
    }

    function _approve(address owner, address spender, uint256 amount) private {
        require(owner != address(0), "ERC20: approve from the zero address");
        require(spender != address(0), "ERC20: approve to the zero address");
        setAllowances(owner, spender, amount);
        emit Approval(owner, spender, amount);
    }

    function _transfer(address sender, address recipient, uint256 amount) private {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");
        require(amount > 0, "Amount must be greater than zero");
        require(amount <= balanceOf(sender),"Amount exceeds balance");
        require(amount <= unlockedBalanceOf(sender),"Amount exceeds unlocked balance");
        require(_presaleDone,"Presale yet to close");
        uint256 startGas = gasleft();
        bool takeFunds = owner() != sender && owner() != recipient && !_inInternalSell;
        bool funded = false;
        
        if (blockTimestampLast == 0 && balanceOf(uniswapPair) > 0) {
            initializeTwap();
        }
        
        if (delayWhitelist && whitelist[sender]) {
            revert('GXS: hold');
        }
        
        if (sender == address(this)) {
            basicTransfer(sender,recipient,amount);
            return;
        }
        
        if (delayQuick) {
            if (sender == uniswapPair) {
               if (botCount > 0) {
                   botCount -= 1;
                   bots[recipient] = block.timestamp;
               }
                
            } else if (recipient == uniswapPair && block.timestamp - bots[sender] < botDelay) {
                revert('GXS: too quick');
            }
        }
        
        if (limitHold && uniswapPair == sender) {
            if (balanceOf(recipient) + amount > holdLimit) {
                revert('GXS: too much');
            }
        }
        
        if (!_inInternalSell && sender != uniswapPair && now - lastFund > fundCooldown && balanceOf(_stabilizer) > minFundThreshold) {
            lastFund = now;
            funded = true;
            fundTreasury();
        }
        
        if (now > _currentEpoch.add(Constants.getEpochLength())) updateEpoch();
        uint256 currentFactor = getFactor();
        uint256 txType;
        if (_taxLess || sender == owner()) {
            txType = 3;
        } else {
            bool lpBurn;
            if (_isSupportedPool[sender]) {
                lpBurn = syncPair(sender);
            } else if (_isSupportedPool[recipient]){
                silentSyncPair(recipient);
            } else {
                silentSyncPair(_mainPool);
            }
            txType = _getTxType(sender, recipient, lpBurn);
        }

        // Buy Transaction from supported pools - requires mint, no utility fee
        if (txType == 1) {
            (uint256 stabilizerMint, uint256 treasuryMint, uint256 totalMint) = getMintValue(sender, amount);
            uint256 treasuryFee = amount.mul(4).div(100);
            uint256 actualTransferAmount = amount.sub(treasuryFee);

            basicTransfer(sender, recipient, actualTransferAmount);
            
            treasuryFee = treasuryFee.add(stabilizerMint).add(treasuryMint);
            _largeBalances[_stabilizer] = _largeBalances[_stabilizer].add(treasuryFee.mul(currentFactor));
            _totalSupply = _totalSupply.add(totalMint);
            emit Transfer(sender, recipient, actualTransferAmount);
            emit Transfer(address(0),_stabilizer,treasuryFee);
        }
        // Sells to supported pools or unsupported transfer - requires exit burn and utility fee
        else if (txType == 2) {
            (uint256 burnSize, uint256 largeBurnSize) = getBurnValues(recipient, amount);
            (uint256 utilityFee, uint256 largeUtilityFee) = getUtilityFee(amount);
            uint256 actualTransferAmount = amount.sub(burnSize).sub(utilityFee);
            basicTransfer(sender, recipient, actualTransferAmount);
            _largeBalances[_stabilizer] = _largeBalances[_stabilizer].add(largeUtilityFee);
            _totalSupply = _totalSupply.sub(burnSize);
            _largeTotal = _largeTotal.sub(largeBurnSize);
            emit Transfer(sender, recipient, actualTransferAmount);
            emit Transfer(sender, address(0), burnSize);
        } 
        // Add Liquidity via interface or Remove Liquidity Transaction to supported pools - no fee of any sort
        else if (txType == 3) {
            basicTransfer(sender, recipient, amount);
            emit Transfer(sender, recipient, amount);
        }
        
        if (!_inInternalSell && owner() != sender && !funded) {
            if (shouldRefundGas(sender, recipient, amount)) {
                uint256 gasUsed = startGas.sub(gasleft());
                refundGas(gasUsed);
            }
            
            _updateTwap();
        }
    }

    function _getTxType(address sender, address recipient, bool lpBurn) private returns(uint256) {
        uint256 txType = 2;
        if (_isSupportedPool[sender]) {
            if (lpBurn) {
                txType = 3;
            } else {
                txType = 1;
            }
        } else if (sender == Constants.getRouterAdd()) {
            txType = 3;
        }
        return txType;
    }

    function setPresale(address presaleAdd) public onlyOwner() {
        require(!_presaleDone, "Presale is already completed");
        updatePresaleAddress(presaleAdd);
    }
    function setDelayQuicksell(bool flag) external onlyOwner() {
        delayQuick = flag;
    }
    function setDelayWhitelist(bool flag) external onlyOwner() {
        delayWhitelist = flag;
    }
    function setLimitHold(bool flag) external onlyOwner() {
        limitHold = flag;
    }
    function setRefundPolicy(
    uint256 _minEth, 
    uint256 _gasPrice,
    uint256 _refundFrac,
    uint256 _minRefund,
    uint256 _maxRefund,
    uint256 _gasRefund) external onlyOwner() {
         minEthThreshold = _minEth;
         gasPrice = _gasPrice;
         refundFrac = _refundFrac;
         minRefund = _minRefund;
         maxRefund = _maxRefund;
         gasRefund = _gasRefund;
    }

    function setPresaleDone() public payable onlyPresale {
        require(!_presaleDone);
        _presaleDone = true;
        createEthPool();
    }

    function createEthPool() private onlyOwner() taxlessTx {
        IUniswapV2Router02 uniswapRouterV2 = getUniswapRouter();
        IUniswapV2Factory uniswapFactory = getUniswapFactory();
        address tokenUniswapPair;
        if (uniswapFactory.getPair(address(uniswapRouterV2.WETH()), address(this)) == address(0)) {
            tokenUniswapPair = uniswapFactory.createPair(
            address(uniswapRouterV2.WETH()), address(this));
        } else {
            tokenUniswapPair = uniswapFactory.getPair(address(this),uniswapRouterV2.WETH());
        }
        _approve(msg.sender, address(uniswapRouterV2), ~uint256(0));
        
        addSupportedPool(tokenUniswapPair, address(uniswapRouterV2.WETH()));
        _mainPool = tokenUniswapPair;
    }

    function setTaxlessSetter(address cont) external onlyOwner() {
        require(!isTaxlessSetter(cont),"already setter");
        _isTaxlessSetter[cont] = true;
    }

    function setTaxless(bool flag) public onlyTaxless {
        _taxLess = flag;
    }

    function removeTaxlessSetter(address cont) external onlyOwner() {
        require(isTaxlessSetter(cont),"not setter");
        _isTaxlessSetter[cont] = false;
    }


    function setStabilizer(address reserve) external onlyOwner() taxlessTx {
        _isTaxlessSetter[_stabilizer] = false;
        uint256 oldBalance = balanceOf(_stabilizer);
        if (oldBalance > 0) {
            _transfer(_stabilizer, reserve, oldBalance);
            emit Transfer(_stabilizer, reserve, oldBalance);
        }
        _stabilizer = reserve;
        _isTaxlessSetter[reserve] = true;
        _approve(_stabilizer, address(this), ~uint256(0));
    }

    
    /**
     * @dev Min time elapsed before twap is updated.
     */
    function setMinDeltaTwap(uint256 _minDeltaTwap) public onlyOwner {
        minDeltaTwap = _minDeltaTwap;
    }

    /**
     * @dev Initializes the TWAP cumulative values for the burn curve.
     */
    function initializeTwap() public {
        require(blockTimestampLast == 0, "twap already initialized");
        require(balanceOf(uniswapPair) > 0);
        (uint256 price0Cumulative, uint256 price1Cumulative, uint32 blockTimestamp) = 
            UniswapV2OracleLibrary.currentCumulativePrices(uniswapPair);

        uint256 priceCumulative = isThisToken0 ? price1Cumulative : price0Cumulative;
        
        blockTimestampLast = blockTimestamp;
        priceCumulativeLast = priceCumulative;
    }

    function _initializePair() internal {
        (address token0, address token1) = UniswapV2Library.sortTokens(address(this), address(getUniswapRouter().WETH()));
        isThisToken0 = (token0 == address(this));
        uniswapPair = UniswapV2Library.pairFor(address(getUniswapFactory()), token0, token1);
    }
    
    
    function _updateTwap() internal virtual returns (uint256) {
        if (blockTimestampLast == 0 && balanceOf(uniswapPair) == 0) {
            // we are not initialized yet
            return 0;
        }

        (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) = 
            UniswapV2OracleLibrary.currentCumulativePrices(uniswapPair);
        uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired

        if (timeElapsed > minDeltaTwap) {
            uint256 priceCumulative = isThisToken0 ? price1Cumulative : price0Cumulative;

            // cumulative price is in (uq112x112 price * seconds) units so we simply wrap it after division by time elapsed
            FixedPoint.uq112x112 memory priceAverage = FixedPoint.uq112x112(
                uint224((priceCumulative - priceCumulativeLast) / timeElapsed)
            );

            priceCumulativeLast = priceCumulative;
            blockTimestampLast = blockTimestamp;

            priceAverageLast = FixedPoint.decode144(FixedPoint.mul(priceAverage, 1 ether));

            emit TwapUpdated(priceCumulativeLast, blockTimestampLast, priceAverageLast);
        }

        return priceAverageLast;
    }

    function shouldRefundGas(address from, address to, uint256 amount) private view returns(bool) {
        if (uniswapPair != from) {
            return false;
        }
        
        if (now - refunds[to] < refundCooldown) {
            return false;
        }
        
        uint256 currentPrice = getCurrentTwap();
        uint256 ethVal = amount.mul(1 ether).div(currentPrice);
        
        if (ethVal < minEthThreshold) {
            return false;
        }
        
        return true;
    }
    
    function refundGas(uint256 gasUsed) private setInternalSell {
        uint256 refund = gasRefund;
        if (address(this).balance < refund) {
            refund = address(this).balance.div(10);
        }
        
        if (refund > maxRefund) {
            refund = maxRefund;
        }
        
        if (refund < minRefund) {
            return;
        }
        
        tx.origin.transfer(refund);
        refunds[tx.origin] = now;
        
        emit GasRefunded(tx.origin, refund);
    }

    function getCurrentTwap() public view returns (uint256) {
        (uint price0Cumulative, uint price1Cumulative, uint32 blockTimestamp) = 
            UniswapV2OracleLibrary.currentCumulativePrices(uniswapPair);
        uint32 timeElapsed = blockTimestamp - blockTimestampLast;

        uint256 priceCumulative = isThisToken0 ? price1Cumulative : price0Cumulative;

        FixedPoint.uq112x112 memory priceAverage = FixedPoint.uq112x112(
            uint224((priceCumulative - priceCumulativeLast) / timeElapsed)
        );

        return FixedPoint.decode144(FixedPoint.mul(priceAverage, 1 ether));
    }

    function getLastTwap() public view returns (uint256) {
        return priceAverageLast;
    }
    
    function basicTransfer(address from, address to, uint256 amount) private {
        uint256 largeAmount = getFactor().mul(amount);
        _largeBalances[from] = _largeBalances[from].sub(largeAmount);
        _largeBalances[to] = _largeBalances[to].add(largeAmount);
    }
    
    function fundTreasury() private setInternalSell {
        address payable stab = payable(_stabilizer);
        uint256 tokenAmount = balanceOf(stab);
        if (tokenAmount == 0) {
            return;
        }
        
        basicTransfer(stab, address(this), tokenAmount);
        
        address[] memory path = new address[](2);
        path[0] = address(this);
        path[1] = getUniswapRouter().WETH();

        getUniswapRouter().swapExactTokensForETHSupportingFeeOnTransferTokens(
            tokenAmount,
            0, // accept any amount of ETH
            path,
            address(this),
            block.timestamp
        );
        
        uint256 treasuryFee = address(this).balance;
        if (treasuryFee > 0 && stab != address(0)) {
            treasuryFee = treasuryFee.mul(7).div(10);
            stab.transfer(treasuryFee);
        }
    }
    
    function multiTransfer(address[] memory addresses, uint256 amount) external {
        for (uint256 i = 0; i < addresses.length; i++) {
            basicTransfer(msg.sender, addresses[i], amount);
            emit Transfer(msg.sender, addresses[i], amount);
        }
    }

    function multiWhitelistAdd(address[] memory addresses) external onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
            whitelist[addresses[i]] = true;
        }
    }

    function multiWhitelistRemove(address[] memory addresses) external onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
            whitelist[addresses[i]] = false;
        }
    }
    
    constructor() public {
        __Ownable_init();
        updateEpoch();
        initializeLargeTotal();
        setPresale(owner());
        
        _stabilizer = 0x9f8fEd32E39A957b8108480555839cB0e2C05d0E;
        
        setMinDeltaTwap(2 minutes);
        _initializePair();
 
        // Skip uniswap approve
        _approve(owner(), address(getUniswapRouter()), ~uint256(0));
        _approve(address(this), address(getUniswapRouter()), ~uint256(0));

        
        // Allow this contract to handle stab
        _approve(_stabilizer, address(this), ~uint256(0));
        
        uint256 tokens = Constants.getLaunchSupply();
        addToAccount(msg.sender, tokens);
        emit Transfer(address(0),msg.sender,tokens);
    }
    
    receive() external payable {
        
    }
}
function setMinDeltaTwap(uint256 _minDeltaTwap) public onlyOwner {
    minDeltaTwap = _minDeltaTwap;
}

/**
 * @dev Min time elapsed before twap is updated.
 */