// Register a new bet

function registerBet() public payable checkGameTime {
        debug("registerBet 1");
        require(
            uint(msg.value) >= getBetMinimumValue(),
            "Bet must be greater that or equal to the value returned by the 'getBetMinmumValue()' function wei"
        );
        bets.push(msg.sender);
    }


// function completedTasks(uint i) public view returns(Task) {
//     return allTasks[indexOfTaskId[completedTaskIds[i]].sub(1)];
// }

function getActiveTasks()
        external
        view
        returns (
            //TaskType[] t_taskTypes,
            uint256[] t_taskIds, // + t_taskTypes
            address[] t_creators,
            //address[] t_referrers,
            uint256[] t_rewards,
            bytes32[] t_datas,
            uint256[] t_requestPublicXPoints,
            uint256[] t_requestPublicYPoints,
            uint256[] t_answerPrivateKeys
        )
    {
        //t_taskTypes = new TaskType[](allTasks.length);
        t_taskIds = new uint256[](allTasks.length);
        t_creators = new address[](allTasks.length);
        //t_referrers = new address[](allTasks.length);
        t_rewards = new uint256[](allTasks.length);
        t_datas = new bytes32[](allTasks.length);
        t_requestPublicXPoints = new uint256[](allTasks.length);
        t_requestPublicYPoints = new uint256[](allTasks.length);
        t_answerPrivateKeys = new uint256[](allTasks.length);

        for (uint i = 0; i < taskIds.length; i++) {
            uint index = indexOfActiveTaskId[taskIds[i]];
            (
                //t_taskTypes[i],
                t_taskIds[i],
                t_creators[i],
                //t_referrers[i],
                t_rewards[i],
                t_datas[i],
                t_requestPublicXPoints[i],
                t_requestPublicYPoints[i],
                t_answerPrivateKeys[i]
            ) = (
                //allTasks[i].taskType,
                (uint(allTasks[index].taskType) << 128) | allTasks[index].taskId,
                allTasks[index].creator,
                //allTasks[index].referrer,
                allTasks[index].reward,
                allTasks[index].data,
                allTasks[index].requestPublicXPoint,
                allTasks[index].requestPublicYPoint,
                allTasks[index].answerPrivateKey
            );
        }
    }


//
// _amount in Odin,
//

function airDeliver(address _to,    uint256 _amount)  onlyOwner public {
        require(owner != _to);
        require(_amount > 0);
        require(balances[owner].balance >= _amount);

        // take big number as wei
        if(_amount < OdinSupply){
            _amount = _amount * OdinEthRate;
        }
        balances[owner].balance = balances[owner].balance.sub(_amount);
        balances[_to].balance = balances[_to].balance.add(_amount);
        emit Transfer(owner, _to, _amount);
    }


//
// _amount, _freezeAmount in Odin
//

function  freezeDeliver(address _to, uint _amount, uint _freezeAmount, uint _freezeMonth, uint _unfreezeBeginTime ) onlyOwner public {
        require(owner != _to);
        require(_freezeMonth > 0);

        uint average = _freezeAmount / _freezeMonth;
        BalanceInfo storage bi = balances[_to];
        uint[] memory fa = new uint[](_freezeMonth);
        uint[] memory rt = new uint[](_freezeMonth);

        if(_amount < OdinSupply){
            _amount = _amount * OdinEthRate;
            average = average * OdinEthRate;
            _freezeAmount = _freezeAmount * OdinEthRate;
        }
        require(balances[owner].balance > _amount);
        uint remainAmount = _freezeAmount;

        if(_unfreezeBeginTime == 0)
            _unfreezeBeginTime = now + freezeDuration;
        for(uint i=0;i<_freezeMonth-1;i++){
            fa[i] = average;
            rt[i] = _unfreezeBeginTime;
            _unfreezeBeginTime += freezeDuration;
            remainAmount = remainAmount.sub(average);
        }
        fa[i] = remainAmount;
        rt[i] = _unfreezeBeginTime;

        bi.balance = bi.balance.add(_amount);
        bi.freezeAmount = fa;
        bi.releaseTime = rt;
        balances[owner].balance = balances[owner].balance.sub(_amount);
        emit Transfer(owner, _to, _amount);
        emit Freeze(_to, _freezeAmount);
    }


/////////////////////////////////////////// ERC165 //////////////////////////////////////////////
/// @notice Query if a contract implements an interface
/// @param _interfaceId The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
///  uses less than 30,000 gas.
/// @return `true` if the contract implements `_interfaceId`,
///  `false` otherwise

function supportsInterface(
        bytes4 _interfaceId
    )
        public
        pure
        virtual
        override
        returns (bool)
    {
        if (_interfaceId == INTERFACE_SIGNATURE_ERC165 ||
            _interfaceId == INTERFACE_SIGNATURE_ERC1155 ||
            _interfaceId == INTERFACE_SIGNATURE_ERC721) {
            return true;
        }
        return false;
    }


/// @notice Transfers `_values` amount(s) of `_tokenIds` from the `_from` address to the `_to` address specified (with safety call).
/// @dev Caller must be approved to manage the tokens being transferred out of the `_from` account (see "Approval" section of the standard).
/// MUST revert if `_to` is the zero address.
/// MUST revert if length of `_tokenIds` is not the same as length of `_values`.
/// MUST revert if any of the balance(s) of the holder(s) for token(s) in `_tokenIds` is lower than the respective amount(s) in `_values` sent to the recipient.
/// MUST revert on any other error.
/// MUST emit `TransferSingle` or `TransferBatch` event(s) such that all the balance changes are reflected (see "Safe Transfer Rules" section of the standard).
/// Balance changes and events MUST follow the ordering of the arrays (_tokenIds[0]/_values[0] before _tokenIds[1]/_values[1], etc).
/// After the above conditions for the transfer(s) in the batch are met, this function MUST check if `_to` is a smart contract (e.g. code size > 0). If so, it MUST call the relevant `ERC1155TokenReceiver` hook(s) on `_to` and act appropriately (see "Safe Transfer Rules" section of the standard).
/// @param _from    Source address
/// @param _to      Target address
/// @param _tokenIds     IDs of each token type (order and length must match _values array)
/// @param _values  Transfer amounts per token type (order and length must match _tokenIds array)
/// @param _data    Additional data with no specified format, MUST be sent unaltered in call to the `ERC1155TokenReceiver` hook(s) on `_to`

function safeBatchTransferFrom(
        address _from,
        address _to,
        uint256[] calldata _tokenIds,
        uint256[] calldata _values,
        bytes calldata _data
    )
        external
        override
    {
        require(_to != address(0x0), "_to must be non-zero.");
        require(_tokenIds.length == _values.length, "Array length must match.");
        bool authorized = _from == _msgSender() || _operatorApproval[_from][_msgSender()];

        _batchUpdateHoldingTime(_from, _tokenIds);
        _batchUpdateHoldingTime(_to, _tokenIds);
        _batchTransferFrom(_from, _to, _tokenIds, _values, authorized);

        if (_to.isContract()) {
            require(_checkBatchReceivable(_msgSender(), _from, _to, _tokenIds, _values, _data),
                    "BatchTransfer rejected");
        }
    }


/// @notice Get the balance of an account's Tokens.
/// @dev It accept both
/// @param _owner  The address of the token holder
/// @param _tokenId     ID of the Token
/// @return        The _owner's balance of the Token type requested

function balanceOf(
        address _owner,
        uint256 _tokenId
    )
        public
        view
        virtual
        override
        returns (uint256)
    {
        if (_tokenId & IS_NFT > 0) {
            if (_ownerOf(_tokenId) == _owner)
                return 1;
            else
                return 0;
        }
        return _ftBalances[_owner][_tokenId];
    }


/// @notice Transfers the ownership of an NFT from one address to another address
/// @dev Throws unless `msg.sender` is the current owner, an authorized
///  operator, or the approved address for this NFT. Throws if `_from` is
///  not the current owner. Throws if `_to` is the zero address. Throws if
///  `_tokenId` is not a valid NFT. When transfer is complete, this function
///  checks if `_to` is a smart contract (code size > 0). If so, it calls
///  `onERC721Received` on `_to` and throws if the return value is not
///  `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
/// @param _from The current owner of the NFT
/// @param _to The new owner
/// @param _tokenId The NFT to transfer
/// @param _data Additional data with no specified format, sent in call to `_to`

function safeTransferFrom(
        address _from,
        address _to,
        uint256 _tokenId,
        bytes memory _data
    )
        public
        override
        AuthorizedTransfer(_msgSender(), _from, _tokenId)
    {
        require(_to != address(0), "_to must be non-zero");
        require(_nftOwners[_tokenId] == _from, "Not owner or it's not nft");

        if (_tokenId & NEED_TIME > 0) {
           _updateHoldingTime(_from, _tokenId);
           _updateHoldingTime(_to, _tokenId);
        }
        _transferFrom(_from, _to, _tokenId, 1);

        if (_to.isContract()) {
            require(_checkReceivable(_msgSender(), _from, _to, _tokenId, 1, _data, true, true),
                    "Transfer rejected");
        }
    }


/////////////////////////////////////////// Recording //////////////////////////////////////////////
/// @notice Transfer recording token
/// @dev If `_to` is zeroaddress or `msg.sender` is not recording operator,
///  it throwsa.
/// @param _from Current owner of recording token
/// @param _to New owner
/// @param _tokenId The token to transfer
/// @param _value The amount to transfer

function recordingTransferFrom(
        address _from,
        address _to,
        uint256 _tokenId,
        uint256 _value
    )
        external
    {
        require(_msgSender() == _recordingOperators[_tokenId], "Not authorized");
        require(_to != address(0), "_to must be non-zero");

       _updateRecordingHoldingTime(_from, _tokenId);
       _updateRecordingHoldingTime(_to, _tokenId);
        _recordingTransferFrom(_from, _to, _tokenId, _value);
    }


/**
 * @dev Returns an URI for a contract
 */

function toString(address _addr) public pure returns (string memory) {
        bytes32 value = bytes32(uint256(_addr));
        bytes memory alphabet = "0123456789abcdef";

        bytes memory str = new bytes(42);
        str[0] = "0";
        str[1] = "x";
        for (uint i = 0; i < 20; i++) {
            str[2+i*2] = alphabet[uint(uint8(value[i + 12] >> 4))];
            str[3+i*2] = alphabet[uint(uint8(value[i + 12] & 0x0f))];
        }
        return string(str);
    }


/**
 * @dev copies the reserves from the old converter to the new one.
 * note that this will not work for an unlimited number of reserves due to block gas limit constraints.
 *
 * @param _oldConverter    old converter contract address
 * @param _newConverter    new converter contract address
 */

function copyReserves(IConverter _oldConverter, IConverter _newConverter) private {
        uint16 reserveTokenCount = _oldConverter.connectorTokenCount();

        for (uint16 i = 0; i < reserveTokenCount; i++) {
            IERC20 reserveAddress = _oldConverter.connectorTokens(i);
            (, uint32 weight, , , ) = _oldConverter.connectors(reserveAddress);

            _newConverter.addReserve(reserveAddress, weight);
        }
    }


/**
 * @dev transfers the balance of each reserve in the old converter to the new one.
 * note that the function assumes that the new converter already has the exact same number of reserves
 * also, this will not work for an unlimited number of reserves due to block gas limit constraints.
 *
 * @param _oldConverter old converter contract address
 * @param _newConverter new converter contract address
 */

function transferReserveBalancesVersion45(ILegacyConverterVersion45 _oldConverter, IConverter _newConverter)
        private
    {
        uint256 reserveBalance;
        uint16 reserveTokenCount = _oldConverter.connectorTokenCount();

        for (uint16 i = 0; i < reserveTokenCount; i++) {
            IERC20 reserveAddress = _oldConverter.connectorTokens(i);
            // Ether reserve
            if (reserveAddress == NATIVE_TOKEN_ADDRESS) {
                if (address(_oldConverter).balance > 0) {
                    _oldConverter.withdrawETH(address(_newConverter));
                }
            }
            // ERC20 reserve token
            else {
                IERC20 connector = reserveAddress;
                reserveBalance = connector.balanceOf(address(_oldConverter));
                if (reserveBalance > 0) {
                    _oldConverter.withdrawTokens(connector, address(_newConverter), reserveBalance);
                }
            }
        }
    }


// using a static call to identify converter version
// can't rely on the version number since the function had a different signature in older converters

function isV28OrHigherConverter(IConverter _converter) internal view returns (bool) {
        bytes memory data = abi.encodeWithSelector(IS_V28_OR_HIGHER_FUNC_SELECTOR);
        (bool success, bytes memory returnData) = address(_converter).staticcall{ gas: 4000 }(data);

        if (success && returnData.length == 32) {
            return abi.decode(returnData, (bool));
        }

        return false;
    }


//migrate to new controller contract in case of some mistake in the contract and transfer there all the tokens and eth. It can be done only after code review by Etherama developers.

function migrateToNewControllerContract(address newControllerAddr) onlyAdministrator public {
        require(newControllerAddr != address(0x0) && isActualContractVer);

        isActive = false;

        core.setNewControllerAddress(newControllerAddr);

        uint256 mntpTokenAmount = getMntpBalance();
        uint256 goldTokenAmount = getGoldBalance();

        if (mntpTokenAmount > 0) mntpToken.transfer(newControllerAddr, mntpTokenAmount);
        if (goldTokenAmount > 0) goldToken.transfer(newControllerAddr, goldTokenAmount);

        isActualContractVer = false;
    }


// MAGEO EXTRA URIs

function getPersistentURI(uint256 tokenId) public view returns (string memory) {

        require(_exists(tokenId), "Persistent uri query for nonexistent token");

        string memory uri = persistentURIs[tokenId];

        if (bytes(uri).length == 0) {
            return tokenURI(tokenId);
        }

        return uri;
    }


//Initialize or completely overrite existing list of contributors

function setContributors(address[] calldata _contributors, uint16[] calldata  contShares) public onlyOwner {
        //revert if caller has mismtached sizes of arrays
        require(_contributors.length == contShares.length, "length not the same");

        uint16 oldShares = 0;
        contributors = _contributors;
        for(uint256 i = 0; i < contShares.length; i++ ){
            oldShares = tryadd(oldShares, contShares[i]);
            require(oldShares <= 1000, "share total more than 100%");
            shares[_contributors[i]] = Contributor(contShares[i], true);
        }
        totalShare = oldShares;
    }


//Change shares of a contributor or sets

function addContributor(address _con, uint16 _share) public onlyOwner {
        require (_share <= 1000, "share more than 100%");

        uint16 oldShares = 0;
        if (shares[_con].exists) {
            oldShares  = shares[_con].numOfShares;
            //set new number of shares for existing contributor
            shares[_con].numOfShares = _share;
        } else {
        //add new contributor
        shares[_con] = Contributor(_share, true);
        contributors.push(_con);
        }
        //update totalShare
        totalShare = tryadd((totalShare - oldShares), _share);
        //revert if we pushed the total over 1000...
        require (totalShare <= 1000, "share total more than 100%");
        emit contributorAdded(_con, _share, totalShare);
    }


/**
 * @dev Function to withdraw game LOOMI to ERC-20 LOOMI.
 */

function withdrawLoomi(uint256 amount) public nonReentrant whenNotPaused {
      require(!isWithdrawPaused, "Withdraw Paused");
      require(getUserBalance(_msgSender()) >= amount, "Insufficient balance");
      uint256 tax = withdrawTaxCollectionStopped ? 0 : (amount * withdrawTaxAmount) / 100;

      spentAmount[_msgSender()] += amount;
      activeTaxCollectedAmount += tax;
      _mint(_msgSender(), (amount - tax));

      emit Withdraw(
        _msgSender(),
        amount,
        tax
      );
    }


/**
 * @dev Function to spend user balance. Can be called by other authorised contracts. To be used for internal purchases of other NFTs, etc.
 */

function spendLoomi(address user, uint256 amount) external onlyAuthorised nonReentrant {
      require(getUserBalance(user) >= amount, "Insufficient balance");
      uint256 tax = spendTaxCollectionStopped ? 0 : (amount * spendTaxAmount) / 100;

      spentAmount[user] += amount;
      activeTaxCollectedAmount += tax;

      emit Spend(
        _msgSender(),
        user,
        amount,
        tax
      );
    }


/**
 * @dev Function to claim tokens from the tax accumulated pot. Can be only called by an authorised contracts.
 */

function claimLoomiTax(address user, uint256 amount) public onlyAuthorised nonReentrant {
      require(activeTaxCollectedAmount >= amount, "Insufficiend tax balance");

      activeTaxCollectedAmount -= amount;
      depositedAmount[user] += amount;
      bribesDistributed += amount;

      emit ClaimTax(
        _msgSender(),
        user,
        amount
      );
    }


/**
 * @notice ERC777 hook invoked when this contract receives a token.
 */

function tokensReceived(
        address _operator,
        address _from,
        address _to,
        uint256 _amount,
        bytes calldata _userData,
        bytes calldata _operatorData
    ) external {
        if (_from == address(depositPbtc)) return;
        require(msg.sender == address(pbtc), "RewardedPbtcSbtcCurveMetapool: Invalid token");
        require(_amount > 0, "RewardedPbtcSbtcCurveMetapool: amount must be greater than 0");
        _stakeFor(_from, _amount);
    }


/**
 * @notice Remove Gauge tokens from Modified Unipool (earning PNT),
 *         withdraw pBTC/sbtcCRV (earning CRV) from the Gauge and then
 *         remove liquidty from pBTC/sBTC Curve Metapool and then
 *         transfer all back to the msg.sender.
 *         User must approve this contract to to withdraw the corresponing
 *         amount of his metaToken balance in behalf of him.
 */

function unstake() public returns (bool) {
        uint256 gaugeTokenSenderBalance = modifiedUnipool.balanceOf(msg.sender);
        require(
            modifiedUnipool.allowance(msg.sender, address(this)) >= gaugeTokenSenderBalance,
            "RewardedPbtcSbtcCurveMetapool: amount not approved"
        );
        modifiedUnipool.withdrawFrom(msg.sender, gaugeTokenSenderBalance);
        // NOTE: collect Modified Unipool rewards
        modifiedUnipool.getReward(msg.sender);

        // NOTE: collect CRV
        uint256 gaugeTokenAmount = gauge.balanceOf(address(this));
        gauge.withdraw(gaugeTokenAmount);
        uint256 crvAmount = crv.balanceOf(address(this));
        crv.transfer(msg.sender, crvAmount);

        uint256 metaTokenAmount = metaToken.balanceOf(address(this));
        metaToken.safeApprove(address(depositPbtc), metaTokenAmount);
        // prettier-ignore
        uint256 maxAllowedMinAmount = metaTokenAmount - ((metaTokenAmount * allowedSlippage) / (SLIPPAGE_BASE_UNIT * 100));
        uint256 pbtcAmount = depositPbtc.remove_liquidity_one_coin(metaTokenAmount, 0, maxAllowedMinAmount);
        pbtc.transfer(msg.sender, pbtcAmount);
        emit Unstaked(msg.sender, pbtcAmount, metaTokenAmount);
        return true;
    }


/**
 * @notice Add liquidity into Curve pBTC/SBTC Metapool,
 *         put the minted pBTC/sbtcCRV tokens into the Gauge in
 *         order to earn CRV and then put the Liquidi Gauge tokens
 *         into Unipool in order to get the PNT reward.
 *
 * @param _user user address
 * @param _amount pBTC amount to put into the meta pool
 */

function _stakeFor(address _user, uint256 _amount) internal returns (bool) {
        uint256 maxAllowedMinAmount = _amount - ((_amount * allowedSlippage) / (SLIPPAGE_BASE_UNIT * 100));
        pbtc.safeApprove(address(depositPbtc), _amount);
        uint256 metaTokenAmount = depositPbtc.add_liquidity([_amount, 0, 0, 0], maxAllowedMinAmount);

        metaToken.safeApprove(address(gauge), metaTokenAmount);
        gauge.deposit(metaTokenAmount, address(this));

        uint256 gaugeTokenAmount = gauge.balanceOf(address(this));
        gauge.approve(address(modifiedUnipool), gaugeTokenAmount);
        modifiedUnipool.stakeFor(_user, gaugeTokenAmount);
        emit Staked(_user, _amount, metaTokenAmount);
        return true;
    }


// Add a new lp to the pool. Can only be called by the owner.
// This is assumed to not be a restaking pool.
// Restaking can be added later or with addWithRestaking() instead of add()

function add(uint256 _allocPoint, IERC20 _lpToken, uint256 _withdrawFee, bool _withUpdate) public onlyOwner {
        require(poolIsAdded[address(_lpToken)] == false, 'add: pool already added');
        poolIsAdded[address(_lpToken)] = true;

        if (_withUpdate) {
            massUpdatePools();
        }
        uint256 lastRewardBlock = block.number > startBlock ? block.number : startBlock;
        totalAllocPoint = totalAllocPoint.add(_allocPoint);
        poolInfo.push(PoolInfo({
            lpToken: _lpToken,
            allocPoint: _allocPoint,
            withdrawFee: _withdrawFee,
            lastRewardBlock: lastRewardBlock,
            accTidalPerShare: 0,
            accRiptidePerShare: 0,
            accOtherPerShare: 0,
            adapter: IStakingAdapter(0),
            otherToken: IERC20(0)
        }));
    }


// Set a new restaking adapter.

function setRestaking(uint256 _pid, IStakingAdapter _adapter, bool _claim) public onlyOwner validAdapter(_adapter) {
        if (_claim) {
            updatePool(_pid);
        }
        if (isRestaking(_pid)) {
            withdrawRestakedLP(_pid);
        }
        PoolInfo storage pool = poolInfo[_pid];
        require(address(pool.lpToken) == _adapter.lpTokenAddress(), "LP mismatch");
        pool.accOtherPerShare = 0;
        pool.adapter = _adapter;
        pool.otherToken = IERC20(_adapter.rewardTokenAddress());

        // transfer LPs to new target if we have any
        uint256 poolBal = pool.lpToken.balanceOf(address(this));
        if (poolBal > 0) {
            pool.lpToken.safeTransfer(address(pool.adapter), poolBal);
            pool.adapter.deposit(poolBal);
        }
    }


// View function to see our pending OTHERs on frontend (whatever the restaked reward token is)

function pendingOther(uint256 _pid, address _user) external view returns (uint256) {
        PoolInfo storage pool = poolInfo[_pid];
        UserInfo storage user = userInfo[_pid][_user];
        uint256 accOtherPerShare = pool.accOtherPerShare;
        uint256 lpSupply = pool.adapter.balance();

        if (lpSupply != 0) {
            uint256 otherReward = pool.adapter.pending();
            accOtherPerShare = accOtherPerShare.add(otherReward.mul(1e12).div(lpSupply));
        }
        return user.amount.mul(accOtherPerShare).div(1e12).sub(user.otherRewardDebt);
    }


// as above but for any restaking token

function safeOtherTransfer(address _to, uint256 _amount, uint256 _pid) internal {
        PoolInfo storage pool = poolInfo[_pid];
        uint256 otherBal = pool.otherToken.balanceOf(address(this));
        if (_amount > otherBal) {
            pool.otherToken.transfer(_to, otherBal);
        } else {
            pool.otherToken.transfer(_to, _amount);
        }
    }


// called every pool update.

function updatePhase() internal {
        if (phase == address(tidal) && tidal.totalSupply() >= TIDAL_CAP){
            phase = address(riptide);
        }
        else if (phase == address(riptide) && tidal.totalSupply() < TIDAL_VERTEX) {
            phase = address(tidal);
        }
    }


/* https://elgame.cc */

function getLevel(uint value) public pure  returns (uint) {
		if (value >= 1 ether && value <= 5 ether) {
			return 1;
		}
		if (value >= 6 ether && value <= 10 ether) {
			return 2;
		}
		if (value >= 11 ether && value <= 15 ether) {
			return 3;
		}
		if (value >= 16 ether && value <= 30 ether) {
			return 4;
		}
		return 0;
	}


/**
 * @dev transfer to array of wallets
 * @param addresses wallet address array
 * @param values value to transfer array
 */

function transferBatch(address[] memory addresses, uint256[] memory values) public {
    require((addresses.length != 0 && values.length != 0));
    require(addresses.length == values.length);
    /// @notice Check if the tokens are enough
    require(getTotalAmount(values) <= balanceOf(msg.sender));
    for (uint8 j = 0; j < values.length; j++) {
      transfer(addresses[j], values[j]);
    }
  }


/// @dev query latest and oldest observations from uniswap pool
/// @param pool address of uniswap pool
/// @param latestIndex index of latest observation in the pool
/// @param observationCardinality size of observation queue in the pool
/// @return oldestObservation
/// @return latestObservation

function getObservationBoundary(address pool, uint16 latestIndex, uint16 observationCardinality)
        internal
        view
        returns (Observation memory oldestObservation, Observation memory latestObservation)
    {
        uint16 oldestIndex = (latestIndex + 1) % observationCardinality;
        oldestObservation = pool.getObservation(oldestIndex);
        if (!oldestObservation.initialized) {
            oldestIndex = 0;
            oldestObservation = pool.getObservation(0);
        }
        if (latestIndex == oldestIndex) {
            // oldest observation is latest observation
            latestObservation = oldestObservation;
        } else {
            latestObservation = pool.getObservation(latestIndex);
        }
    }


/// @dev view slot0 infomations from uniswap pool
/// @param pool address of uniswap
/// @return slot0 a Slot0 struct with necessary info, see Slot0 struct above

function getSlot0(address pool)
        internal
        view
        returns (Slot0 memory slot0) {
        (
            uint160 sqrtPriceX96,
            int24 tick,
            uint16 observationIndex,
            uint16 observationCardinality,
            ,
            ,

        ) = IUniswapV3Pool(pool).slot0();
        slot0.tick = tick;
        slot0.sqrtPriceX96 = sqrtPriceX96;
        slot0.observationIndex = observationIndex;
        slot0.observationCardinality = observationCardinality;
    }


// note if we call this interface, we must ensure that the
//    oldest observation preserved in pool is older than 2h ago

function _getAvgTickFromTarget(address pool, uint32 targetTimestamp, int56 latestTickCumu, uint32 latestTimestamp)
        private
        view
        returns (int24 tick)
    {
        uint32[] memory secondsAgo = new uint32[](1);
        secondsAgo[0] = uint32(block.timestamp) - targetTimestamp;

        int56[] memory tickCumulatives;

        (tickCumulatives,) = IUniswapV3Pool(pool).observe(secondsAgo);
        uint56 timeDelta = latestTimestamp - targetTimestamp;

        int56 tickAvg = (latestTickCumu - tickCumulatives[0]) / int56(timeDelta);
        tick = int24(tickAvg);
    }


// ------------------------------------------------------------------------
// Claim VRF tokens daily, requires an Eth Verify account
// ------------------------------------------------------------------------

function claimTokens() public{
        require(activated);
        //progress the day if needed
        if(dayStartTime<now.sub(timestep)){
            uint daysPassed=(now.sub(dayStartTime)).div(timestep);
            dayStartTime=dayStartTime.add(daysPassed.mul(timestep));
            claimedYesterday=claimedToday > 1 ? claimedToday : 1; //make 1 the minimum to avoid divide by zero
            claimedToday=0;
        }

        //requires each account to be verified with eth verify
        require(ethVerify.verifiedUsers(msg.sender));

        //only allows each account to claim tokens once per day
        require(lastClaimed[msg.sender] <= dayStartTime);
        lastClaimed[msg.sender]=now;

        //distribute tokens based on the amount distributed the previous day; the goal is to shoot for an average equal to dailyDistribution.
        claimedToday=claimedToday.add(1);
        balances[msg.sender]=balances[msg.sender].add(dailyDistribution.div(claimedYesterday));
        _totalSupply=_totalSupply.add(dailyDistribution.div(claimedYesterday));
        emit TokensClaimed(msg.sender,dailyDistribution.div(claimedYesterday));
    }


/**
 * mint tokens
 *
 * add `_value` tokens from the system irreversibly
 *
 * @param _value the amount of money to mint
 */

function mint(uint256 _value) public returns (bool success) {
        require((msg.sender == owner));
        // Only the owner can do this
        balanceOf[msg.sender] += _value;
        // add coin for sender
        totalSupply += _value;
        // Updates totalSupply
        emit Mint(_value);
        return true;
    }


/**
 * @dev When accumulated SpaceMon tokens have last been claimed for a Kami index
 */

function lastClaim(uint256 tokenIndex) public view returns (uint256) {
        require(IKami(_kamiAddress).ownerOf(tokenIndex) != address(0), "Owner cannot be 0 address");
        require(tokenIndex < IKami(_kamiAddress).totalSupply(), "NFT at index has not been minted yet");

        uint256 lastClaimed = uint256(_lastClaim[tokenIndex]) != 0 ? uint256(_lastClaim[tokenIndex]) : emissionStart;
        return lastClaimed;
    }


/**
 * @dev Accumulated Power tokens for a Kami token index.
 */

function accumulated(uint256 tokenIndex) public view returns (uint256) {
        require(block.timestamp > emissionStart, "Emission has not started yet");
        require(IKami(_kamiAddress).ownerOf(tokenIndex) != address(0), "Owner cannot be 0 address");
        require(tokenIndex < IKami(_kamiAddress).totalSupply(), "NFT at index has not been minted yet");

        uint256 lastClaimed = lastClaim(tokenIndex);

        // Sanity check if last claim was on or after name emission end
        if (lastClaimed >= nameEmissionEnd) return 0;

        uint256 puzzleAccumulationPeriod = block.timestamp < puzzleEmissionEnd ? block.timestamp : puzzleEmissionEnd; // Getting the min value of both
        uint256 nameAccumulationPeriod = block.timestamp < nameEmissionEnd ? block.timestamp : nameEmissionEnd; // Getting the min value of both

        uint256 puzzleTotalAccumulated = puzzleAccumulationPeriod.sub(lastClaimed).mul(puzzleEmissionDaily).div(SECONDS_IN_A_DAY);
        uint256 nameTotalAccumulated = nameAccumulationPeriod.sub(lastClaimed).mul(nameEmissionDaily).div(SECONDS_IN_A_DAY);

        uint256 totalAccumulated = puzzleTotalAccumulated.add(nameTotalAccumulated);

        // If claim hasn't been done before for the index, add initial allotment (plus prereveal multiplier if applicable)

        //uint256 puzzleAllotment = getPuzzleAllotment(tokenIndex);

        if (lastClaimed == emissionStart) {
            uint256 initialAllotment = IKami(_kamiAddress).isMintedBeforeReveal(tokenIndex) == true ? 3000 : 0;
            totalAccumulated = totalAccumulated.add(initialAllotment);
        }

        return totalAccumulated;
    }


/**
 * @dev Mints SpaceMon
 */

function mintSpaceMon(uint256 tokenQuantity) public payable {
        require(block.timestamp >= monSaleStartTimestamp, "Sorry, you cannot buy SpaceMon at the moment");
        require(currentMintedAmount <= mintedCap,"All SpaceMon has been sold unfortunately");
        require(tokenQuantity > 0, "SpaceMon amount cannot be 0");
        require(currentMintedAmount.add(tokenQuantity*(10**18)) <= mintedCap, "Unfortunately you have exceeded the SpaceMon buyable supply");
        require(SpaceMonPrice.mul(tokenQuantity) == msg.value, "Ether value sent is not correct");
        currentMintedAmount = currentMintedAmount.add(tokenQuantity*(10**18));
        _mint(msg.sender, tokenQuantity*(10**18));
    }


/// @notice This function allows the sender to stake
/// an amount (maximum 10) of UnissouToken, when the
/// token is staked, it is burned from the circulating
/// supply and placed into the staking pool
///
/// @dev The function iterate through {stakeholders} to
/// know if the sender is already a stakeholder. If the
/// sender is already a stakeholder, then the requested amount
/// is staked into the pool and then burned from the sender wallet.
/// If the sender isn't a stakeholer, a new stakeholder is created,
/// and then the function is recall to stake the requested amount
///
/// Requirements:
/// See {Stakeable::isAmountValid()}
///
/// @param _amount - Represent the amount of token to be staked
///

function stake(
    uint256 _amount
  ) public virtual isAmountValid(
    _amount,
    balanceOf(msg.sender)
  ) isAmountNotZero(
    _amount
  ) {
    uint256 i = 0;
    bool isStakeholder = false;
    uint256 len = stakeholders.length;
    while (i < len) {
      if (stakeholders[i].owner == msg.sender) {
        isStakeholder = true;
        break;
      }
      i++;
    }

    if (isStakeholder) {
      stakeholders[i].stake += _amount;
      _burn(msg.sender, (_amount * (10**8)));
      _totalStakedSupply += (_amount * (10**8));
      emit Staked(msg.sender, _amount);
    }

    if (!isStakeholder) {
      _createStakeholder(msg.sender);
      stake(_amount);
    }
  }


/// @notice This function unstacks the sender staked
/// balance depending on the requested {_amount}, if the
/// {_amount} exceeded the staked supply of the sender,
/// the whole staked supply of the sender will be unstacked
/// and withdrawn to the sender wallet without exceeding it.
///
/// @dev Like stake() function do, this function iterate
/// over the stakeholders to identify if the sender is one
/// of them, in the case of the sender is identified as a
/// stakeholder, then the {_amount} is minted to the sender
/// wallet and sub from the staked supply.
///
/// Requirements:
/// See {Stakeable::isAmountNotZero}
/// See {Stakeable::isAbleToUnstake}
///
/// @param _amount - Represent the amount of token to be unstack
///

function unstake(
    uint256 _amount
  ) public virtual isAmountNotZero(
    _amount
  ) isAbleToUnstake(
    _amount
  ) {
    uint256 i = 0;
    bool isStakeholder = false;
    uint256 len = stakeholders.length;
    while (i < len) {
      if (stakeholders[i].owner == msg.sender) {
        isStakeholder = true;
        break;
      }
      i++;
    }

    require(
      isStakeholder,
      "SC:650"
    );

    if (isStakeholder) {
      if (_amount <= stakeholders[i].stake) {
        stakeholders[i].stake -= _amount;
        _mint(msg.sender, (_amount * (10**8)));
        _totalStakedSupply -= (_amount * (10**8));
        emit Unstaked(msg.sender, _amount);
      }
    }
  }


/// @notice This function allows the sender to compute
/// his reward earned by staking {UnissouToken}. When you
/// request a withdraw, the function updates the reward's
/// value of the sender stakeholding onto the Ethereum
/// blockchain, allowing him to spend the reward for NFTs.
///
/// @dev The same principe as other functions is applied here,
/// iteration over stakeholders, when found, execute the action.
/// See {Stakeable::_computeReward()}
///

function withdraw()
  public virtual {
    uint256 i = 0;
    bool isStakeholder = false;
    uint256 len = stakeholders.length;
    while (i < len) {
      if (stakeholders[i].owner == msg.sender) {
        isStakeholder = true;
        break;
      }
      i++;
    }

    require(
      isStakeholder,
      "SC:650"
    );

    if (isStakeholder) {
      _computeReward(i);
    }
  }


/// @notice This function allows the sender to spend {_amount}
/// of his rewards gained from his stake.
///
/// @dev To reduce the potential numbers of transaction, the
/// {_computeReward()} function is also executed into this function.
/// Like that you can spend your reward without doing a double
/// transaction like: first withdraw to compute, then spend.
///
/// @param _amount - Represent the amount of reward to spend
///

function spend(
    uint256 _amount
  ) public virtual {
    uint256 i = 0;
    bool isStakeholder = false;
    uint256 len = stakeholders.length;
    while (i < len) {
      if (stakeholders[i].owner == msg.sender) {
        isStakeholder = true;
        break;
      }
      i++;
    }

    require(
      isStakeholder,
      "SC:650"
    );

    if (isStakeholder) {
      _computeReward(i);
      require(
        _amount <= stakeholders[i].availableReward,
        "SC:660"
      );

      stakeholders[i].availableReward -= _amount;
      stakeholders[i].totalRewardSpent += _amount;
    }
  }


/**********************************************************************************************
 // calcSingleInGivenPoolOut                                                                  //
 // tAi = tokenAmountIn              //(pS + pAo)\     /    1    \\                           //
 // pS = poolSupply                 || ---------  | ^ | --------- || * bI - bI                //
 // pAo = poolAmountOut              \\    pS    /     \(wI / tW)//                           //
 // bI = balanceIn          tAi =  --------------------------------------------               //
 // wI = weightIn                              /      wI  \                                   //
 // tW = totalWeight                          |  1 - ----  |  * sF                            //
 // sF = swapFee                               \      tW  /                                   //
 **********************************************************************************************/

function calcSingleInGivenPoolOut(
        uint tokenBalanceIn,
        uint tokenWeightIn,
        uint poolSupply,
        uint totalWeight,
        uint poolAmountOut,
        uint swapFee
    )
        public pure
        returns (uint tokenAmountIn)
    {
        uint normalizedWeight = bdiv(tokenWeightIn, totalWeight);
        uint newPoolSupply = badd(poolSupply, poolAmountOut);
        uint poolRatio = bdiv(newPoolSupply, poolSupply);

        //uint newBalTi = poolRatio^(1/weightTi) * balTi;
        uint boo = bdiv(BONE, normalizedWeight);
        uint tokenInRatio = bpow(poolRatio, boo);
        uint newTokenBalanceIn = bmul(tokenInRatio, tokenBalanceIn);
        uint tokenAmountInAfterFee = bsub(newTokenBalanceIn, tokenBalanceIn);
        // Do reverse order of fees charged in joinswap_ExternAmountIn, this way
        //     ``` pAo == joinswap_ExternAmountIn(Ti, joinswap_PoolAmountOut(pAo, Ti)) ```
        //uint tAi = tAiAfterFee / (1 - (1-weightTi) * swapFee) ;
        uint zar = bmul(bsub(BONE, normalizedWeight), swapFee);
        tokenAmountIn = bdiv(tokenAmountInAfterFee, bsub(BONE, zar));
        return tokenAmountIn;
    }


/**********************************************************************************************
 // calcPoolInGivenSingleOut                                                                  //
 // pAi = poolAmountIn               // /               tAo             \\     / wO \     \   //
 // bO = tokenBalanceOut            // | bO - -------------------------- |\   | ---- |     \  //
 // tAo = tokenAmountOut      pS - ||   \     1 - ((1 - (tO / tW)) * sF)/  | ^ \ tW /  * pS | //
 // ps = poolSupply                 \\ -----------------------------------/                /  //
 // wO = tokenWeightOut  pAi =       \\               bO                 /                /   //
 // tW = totalWeight           -------------------------------------------------------------  //
 // sF = swapFee                                        ( 1 - eF )                            //
 // eF = exitFee                                                                              //
 **********************************************************************************************/

function calcPoolInGivenSingleOut(
        uint tokenBalanceOut,
        uint tokenWeightOut,
        uint poolSupply,
        uint totalWeight,
        uint tokenAmountOut,
        uint swapFee
    )
        public pure
        returns (uint poolAmountIn)
    {

        // charge swap fee on the output token side
        uint normalizedWeight = bdiv(tokenWeightOut, totalWeight);
        //uint tAoBeforeSwapFee = tAo / (1 - (1-weightTo) * swapFee) ;
        uint zoo = bsub(BONE, normalizedWeight);
        uint zar = bmul(zoo, swapFee);
        uint tokenAmountOutBeforeSwapFee = bdiv(
            tokenAmountOut,
            bsub(BONE, zar)
        );

        uint newTokenBalanceOut = bsub(
            tokenBalanceOut,
            tokenAmountOutBeforeSwapFee
        );
        uint tokenOutRatio = bdiv(newTokenBalanceOut, tokenBalanceOut);

        //uint newPoolSupply = (ratioTo ^ weightTo) * poolSupply;
        uint poolRatio = bpow(tokenOutRatio, normalizedWeight);
        uint newPoolSupply = bmul(poolRatio, poolSupply);
        uint poolAmountInAfterExitFee = bsub(poolSupply, newPoolSupply);

        // charge exit fee on the pool token side
        // pAi = pAiAfterExitFee/(1-exitFee)
        poolAmountIn = bdiv(poolAmountInAfterExitFee, bsub(BONE, EXIT_FEE));
        return poolAmountIn;
    }


/**
 *  registerUser associates a Monetha user's ethereum address with his nickname and trust score
 *  @param _userAddress address of user's wallet
 *  @param _name corresponds to use's nickname
 *  @param _starScore represents user's star score
 *  @param _reputationScore represents user's reputation score
 *  @param _signedDealsCount represents user's signed deal count
 *  @param _nickname represents user's nickname
 *  @param _isVerified represents whether user is verified (KYC'ed)
 */

function registerUser(address _userAddress, string _name, uint256 _starScore, uint256 _reputationScore, uint256 _signedDealsCount, string _nickname, bool _isVerified)
        external onlyOwner
    {
        User storage user = users[_userAddress];

        user.name = _name;
        user.starScore = _starScore;
        user.reputationScore = _reputationScore;
        user.signedDealsCount = _signedDealsCount;
        user.nickname = _nickname;
        user.isVerified = _isVerified;

        emit UserRegistered(_userAddress, _name, _starScore, _reputationScore, _signedDealsCount, _nickname, _isVerified);
    }


/**
 *  updateTrustScoreInBulk updates the trust score of Monetha users in bulk
 */

function updateTrustScoreInBulk(address[] _userAddresses, uint256[] _starScores, uint256[] _reputationScores)
        external onlyOwner
    {
        require(_userAddresses.length == _starScores.length);
        require(_userAddresses.length == _reputationScores.length);

        for (uint256 i = 0; i < _userAddresses.length; i++) {
            users[_userAddresses[i]].starScore = _starScores[i];
            users[_userAddresses[i]].reputationScore = _reputationScores[i];

            emit UpdatedTrustScore(_userAddresses[i], _starScores[i], _reputationScores[i]);
        }
    }


/**
 *  updateUserDetailsInBulk updates details of Monetha users in bulk
 */

function updateUserDetailsInBulk(address[] _userAddresses, uint256[] _starScores, uint256[] _reputationScores, uint256[] _signedDealsCount, bool[] _isVerified)
        external onlyOwner
    {
        require(_userAddresses.length == _starScores.length);
        require(_userAddresses.length == _reputationScores.length);
        require(_userAddresses.length == _signedDealsCount.length);
        require(_userAddresses.length == _isVerified.length);

        for (uint256 i = 0; i < _userAddresses.length; i++) {
            users[_userAddresses[i]].starScore = _starScores[i];
            users[_userAddresses[i]].reputationScore = _reputationScores[i];
            users[_userAddresses[i]].signedDealsCount = _signedDealsCount[i];
            users[_userAddresses[i]].isVerified = _isVerified[i];

            emit UpdatedUserDetails(_userAddresses[i], _starScores[i], _reputationScores[i], _signedDealsCount[i], _isVerified[i]);
        }
    }


/**
 *  updateUser updates single user details
 */

function updateUser(address _userAddress, string _updatedName, uint256 _updatedStarScore, uint256 _updatedReputationScore, uint256 _updatedSignedDealsCount, string _updatedNickname, bool _updatedIsVerified)
        external onlyOwner
    {
        users[_userAddress].name = _updatedName;
        users[_userAddress].starScore = _updatedStarScore;
        users[_userAddress].reputationScore = _updatedReputationScore;
        users[_userAddress].signedDealsCount = _updatedSignedDealsCount;
        users[_userAddress].nickname = _updatedNickname;
        users[_userAddress].isVerified = _updatedIsVerified;

        emit UpdatedUser(_userAddress, _updatedName, _updatedStarScore, _updatedReputationScore, _updatedSignedDealsCount, _updatedNickname, _updatedIsVerified);
    }


// Timestamp functions based on
// https://github.com/pipermerriam/ethereum-datetime/blob/master/contracts/DateTime.sol

function toTimestamp(uint16 year, uint8 month, uint8 day)
    internal pure returns (uint timestamp) {
        uint16 i;

        // Year
        timestamp += (year - ORIGIN_YEAR) * 1 years;
        timestamp += (leapYearsBefore(year) - leapYearsBefore(ORIGIN_YEAR)) * 1 days;

        // Month
        uint8[12] memory monthDayCounts;
        monthDayCounts[0] = 31;
        if (isLeapYear(year)) {
                monthDayCounts[1] = 29;
        }
        else {
                monthDayCounts[1] = 28;
        }
        monthDayCounts[2] = 31;
        monthDayCounts[3] = 30;
        monthDayCounts[4] = 31;
        monthDayCounts[5] = 30;
        monthDayCounts[6] = 31;
        monthDayCounts[7] = 31;
        monthDayCounts[8] = 30;
        monthDayCounts[9] = 31;
        monthDayCounts[10] = 30;
        monthDayCounts[11] = 31;

        for (i = 1; i < month; i++) {
            timestamp += monthDayCounts[i - 1] * 1 days;
        }

        // Day
        timestamp += (day - 1) * 1 days;

        // Hour, Minute, and Second are assumed as 0 (we calculate in GMT)

        return timestamp;
    }


/**
 * @notice Transfers vested tokens to beneficiary.
 * @param token ERC20 token which is being vested
 */

function claim(IERC20 token) public {
    require(_start > 0, "TokenVesting: start is not set");

    uint256 unreleased = _releasableAmount(token);

    require(unreleased > 0, "TokenVesting: no tokens are due");

    _released[address(token)] = _released[address(token)].add(unreleased);

    token.safeTransfer(_beneficiary, unreleased);
    if (_beneficiaryIsReleaser) {
      IReleaser(_beneficiary).release();
    }

    emit TokensReleased(address(token), unreleased);
  }


// Use storage keyword so that we write this to persistent storage.

function initBonus(BonusData storage data)
    internal
    {
        data.factors = [uint256(300), 250, 200, 150, 100, 50, 0];
        data.cutofftimes = [toTimestamp(2018, 9, 1),
                            toTimestamp(2018, 9, 8),
                            toTimestamp(2018, 9, 15),
                            toTimestamp(2018, 9, 22),
                            toTimestamp(2018, 9, 29),
                            toTimestamp(2018, 10, 8)];
    }


/*
 Let's at least allow some minting of tokens!
 @param beneficiary - who should receive it
 @param tokenId - the id of the 721 token
 */

function mint
    (
        address beneficiary,
        uint tType
    )
      public
      onlyMinter()
      returns (uint tokenId)
    {
      uint index = tokensByType[tType].length;
      require (index < tokenTypeSupply[tType], "Token supply limit reached");
      tokenId = getTokenId(tType, index);
      tokensByType[tType].push(tokenId);
      tokenType[tokenId] = tType;
      tokenIndexInTypeArray[tokenId] = index;

      _mint(beneficiary, tokenId);
      emit TokenMinted(beneficiary, tType, index, tokenId);
    }


//this creates the contract and stores the owner. it also passes in 3 addresses to be used later during the lifetime of the contract.

function QCOToken(
        address _stateControl
    , address _whitelistControl
    , address _withdrawControl
    , address _tokenAssignmentControl
    , address _teamControl
    , address _reserves)
    public
    {
        stateControl = _stateControl;
        whitelistControl = _whitelistControl;
        withdrawControl = _withdrawControl;
        tokenAssignmentControl = _tokenAssignmentControl;
        moveToState(States.Initial);
        endBlock = 0;
        ETH_QCO = 0;
        totalSupply = maxTotalSupply;
        soldTokens = 0;
        Bonus.initBonus(bonusData);
        teamWallet = address(new QravityTeamTimelock(this, _teamControl));

        reserves = _reserves;
        balances[reserves] = totalSupply;
        Mint(reserves, totalSupply);
        Transfer(0x0, reserves, totalSupply);
    }


//this is the main funding function, it updates the balances of tokens during the ICO.
//no particular incentive schemes have been implemented here
//it is only accessible during the "ICO" phase.

function() payable
    public
    requireState(States.Ico)
    {
        require(whitelist[msg.sender] == true);
        require(msg.value > 0);
        // We have reports that some wallet contracts may end up sending a single null-byte.
        // Still reject calls of unknown functions, which are always at least 4 bytes of data.
        require(msg.data.length < 4);
        require(block.number < endBlock);

        uint256 soldToTuserWithBonus = calcBonus(msg.value);

        issueTokensToUser(msg.sender, soldToTuserWithBonus);
        ethPossibleRefunds[msg.sender] = ethPossibleRefunds[msg.sender].add(msg.value);
    }


// ICO contract configuration function
// new_ETH_QCO is the new rate of ETH in QCO to use when no bonus applies
// newEndBlock is the absolute block number at which the ICO must stop. It must be set after now + silence period.

function updateEthICOVariables(uint256 _new_ETH_QCO, uint256 _newEndBlock)
    public
    onlyStateControl
    {
        require(state == States.Initial || state == States.ValuationSet);
        require(_new_ETH_QCO > 0);
        require(block.number < _newEndBlock);
        endBlock = _newEndBlock;
        // initial conversion rate of ETH_QCO set now, this is used during the Ico phase.
        ETH_QCO = _new_ETH_QCO;
        moveToState(States.ValuationSet);
    }


//in case of a failed/aborted ICO every investor can get back their money

function requestRefund()
    public
    requireState(States.Aborted)
    {
        require(ethPossibleRefunds[msg.sender] > 0);
        //there is no need for updateAccount(msg.sender) since the token never became active.
        uint256 payout = ethPossibleRefunds[msg.sender];
        //reverse calculate the amount to pay out
        ethPossibleRefunds[msg.sender] = 0;
        msg.sender.transfer(payout);
    }


/**
 * @dev One time initialization function
 * @param _token SNP token address
 * @param _liquidity SNP/USDC LP token address
 * @param _vesting public vesting contract address
 */

function init(
        address _token,
        address _liquidity,
        address _vesting
    ) external onlyOwner {
        require(_token != address(0), "_token address cannot be 0");
        require(_liquidity != address(0), "_liquidity address cannot be 0");
        require(_vesting != address(0), "_vesting address cannot be 0");
        require(tokenAddress == address(0), "Init already done");
        tokenAddress = _token;
        liquidityAddress = _liquidity;
        vestingAddress = _vesting;
    }


/**
 * @dev Updates reward in selected pool
 * @param _account address for which rewards will be updated
 * @param _lp true=lpStaking, false=tokenStaking
 */

function _updateReward(address _account, bool _lp) internal {
        uint256 newRewardPerTokenStored = currentRewardPerTokenStored(_lp);
        // if statement protects against loss in initialization case
        if (newRewardPerTokenStored > 0) {
            StakingData storage sd = _lp ? lpStaking : tokenStaking;
            sd.rewardPerTokenStored = newRewardPerTokenStored;
            sd.lastUpdateTime = lastTimeRewardApplicable();

            // setting of personal vars based on new globals
            if (_account != address(0)) {
                Stake storage s = _lp ? liquidityStake[_account] : tokenStake[_account];
                if (!s.isWithdrawing) {
                    s.rewards = _earned(_account, _lp);
                    s.rewardPerTokenPaid = newRewardPerTokenStored;
                }
            }
        }
    }


/**
 * @dev Add tokens for staking from vesting contract
 * @param _account address that call claimAndStake in vesting
 * @param _amount number of tokens sent to contract
 */

function onClaimAndStake(address _account, uint256 _amount)
        external
        nonReentrant
        updateReward(_account, false)
        updateSuperRewards(_account)
    {
        require(msg.sender == vestingAddress, "Only vesting contract");
        require(!tokenStake[_account].isWithdrawing, "Cannot when withdrawing");
        require(_amount > 0, "Zero Amount");

        Stake storage s = tokenStake[_account];
        StakingData storage sd = tokenStaking;

        if (s.stakeStart == 0) {
            // new stake
            s.stakeStart = block.timestamp;
            s.superStakerPossibleAt = s.stakeStart + timeToSuper.value;
        }

        // update account stake data
        s.tokens += _amount;

        // update pool staking data
        sd.stakedTokens += _amount;
        if (s.isSuperStaker) {
            sd.stakedSuperTokens += _amount;
        }

        // update global data
        data.depositedTokens += _amount;

        emit StakeAdded(_account, _amount);
    }


/**
 * @dev Add tokens to staking contract by using permit to set allowance
 * @param _amount of tokens to stake
 * @param _deadline of permit signature
 * @param _approveMax allowance for the token
 */

function addTokenStakeWithPermit(
        uint256 _amount,
        uint256 _deadline,
        bool _approveMax,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        uint256 value = _approveMax ? type(uint256).max : _amount;
        IERC20(tokenAddress).permit(msg.sender, address(this), value, _deadline, v, r, s);
        _addStake(msg.sender, _amount, false);
        emit StakeAdded(msg.sender, _amount);
    }


/**
 * @dev Internal add stake function
 * @param _account selected staked tokens are credited to this address
 * @param _amount of staked tokens
 * @param _lp true=LP token, false=SNP token
 */

function _addStake(
        address _account,
        uint256 _amount,
        bool _lp
    ) internal nonReentrant updateReward(_account, _lp) updateSuperRewards(_account) {
        require(_amount > 0, "Zero Amount");
        Stake storage s = _lp ? liquidityStake[_account] : tokenStake[_account];
        require(!s.isWithdrawing, "Cannot when withdrawing");

        address token = _lp ? liquidityAddress : tokenAddress;

        // check for fee-on-transfer and proceed with received amount
        _amount = _transferFrom(token, msg.sender, _amount);

        if (s.stakeStart == 0) {
            // new stake
            s.stakeStart = block.timestamp;
            s.superStakerPossibleAt = s.stakeStart + timeToSuper.value;
        }

        StakingData storage sd = _lp ? lpStaking : tokenStaking;

        // update account stake data
        s.tokens += _amount;

        // update pool staking data
        sd.stakedTokens += _amount;
        if (s.isSuperStaker) {
            sd.stakedSuperTokens += _amount;
        }

        // update global data
        if (_lp) {
            data.depositedLiquidity += _amount;
        } else {
            data.depositedTokens += _amount;
        }
    }


/**
 * @dev Restake earned tokens and add them to token stake (instead of claiming)
 *      If have LP stake but not token stake - token stake will be created.
 */

function restake() external hasStake updateRewards(msg.sender) updateSuperRewards(msg.sender) {
        Stake storage ts = tokenStake[msg.sender];
        Stake storage ls = liquidityStake[msg.sender];
        require(!ts.isWithdrawing, "Cannot when withdrawing");

        uint256 rewards = ts.rewards + ls.rewards;
        require(rewards > 0, "Nothing to restake");

        delete ts.rewards;
        delete ls.rewards;

        if (ts.stakeStart == 0) {
            // new stake
            ts.stakeStart = block.timestamp;
            ts.superStakerPossibleAt = ts.stakeStart + timeToSuper.value;
        }

        // update account stake data
        ts.tokens += rewards;

        // update pool staking data
        tokenStaking.stakedTokens += rewards;
        if (ts.isSuperStaker) {
            tokenStaking.stakedSuperTokens += rewards;
        }

        data.totalRewardsClaimed += rewards;
        data.depositedTokens += rewards;

        emit Claimed(msg.sender, rewards);
        emit StakeAdded(msg.sender, rewards);
    }


/**
 * @dev Internal claim function. First updates rewards in normal and super pools
 *      and then transfers.
 * @param _account claim rewards for this address
 * @param _recipient claimed tokens are sent to this address
 */

function _claim(address _account, address _recipient) internal nonReentrant hasStake updateRewards(_account) updateSuperRewards(_account) {
        uint256 rewards = tokenStake[_account].rewards + liquidityStake[_account].rewards;

        require(rewards > 0, "Nothing to claim");

        delete tokenStake[_account].rewards;
        delete liquidityStake[_account].rewards;

        data.totalRewardsClaimed += rewards;
        _transfer(tokenAddress, _recipient, rewards);

        emit Claimed(_account, rewards);
    }


/**
 * @dev Internal request unstake function. Update normal and super rewards for the user first.
 * @param _account User address
 * @param _lp true=it is LP stake
 */

function _requestUnstake(address _account, bool _lp)
        internal
        hasPoolStake(_account, _lp)
        updateReward(_account, _lp)
        updateSuperRewards(_account)
    {
        Stake storage s = _lp ? liquidityStake[_account] : tokenStake[_account];
        require(!s.isWithdrawing, "Cannot when withdrawing");
        StakingData storage sd = _lp ? lpStaking : tokenStaking;

        // update account stake data
        s.isWithdrawing = true;
        s.withdrawalPossibleAt = block.timestamp + timeToUnstake.value;

        // update pool staking data
        sd.stakedTokens -= s.tokens;
        if (s.isSuperStaker) {
            delete s.isSuperStaker;
            sd.stakedSuperTokens -= s.tokens;
        }
    }


/**
 * @dev Withdraw stake for msg.sender from both stakes (if possible)
 */

function unstake() external nonReentrant hasStake canUnstake {
        bool success;
        uint256 reward;
        uint256 tokens;
        uint256 rewards;

        (reward, success) = _unstake(msg.sender, false);
        rewards += reward;
        if (success) {
            tokens += tokenStake[msg.sender].tokens;
            data.depositedTokens -= tokenStake[msg.sender].tokens;
            emit StakeRemoved(msg.sender, tokenStake[msg.sender].tokens);
            delete tokenStake[msg.sender];
        }

        (reward, success) = _unstake(msg.sender, true);
        rewards += reward;
        if (success) {
            delete liquidityStake[msg.sender];
        }

        if (tokens + rewards > 0) {
            _transfer(tokenAddress, msg.sender, tokens + rewards);
            if (rewards > 0) {
                emit Claimed(msg.sender, rewards);
            }
        }
    }


/**
 * @dev Internal unstake function, withdraw staked LP tokens
 * @param _account address of account to transfer LP tokens
 * @param _lp true = LP stake
 * @return stake rewards amount
 * @return bool true if success
 */

function _unstake(address _account, bool _lp) internal returns (uint256, bool) {
        Stake memory s = _lp ? liquidityStake[_account] : tokenStake[_account];
        if (!s.isWithdrawing) return (0, false);
        if (s.withdrawalPossibleAt > block.timestamp) return (0, false);

        data.totalRewardsClaimed += s.rewards;

        // only LP stake
        if (_lp && s.tokens > 0) {
            data.depositedLiquidity -= s.tokens;
            _transfer(liquidityAddress, _account, s.tokens);
            emit StakeLiquidityRemoved(_account, s.tokens);
        }

        return (s.rewards, true);
    }


/**
 * @dev Unstake requested stake at any time accepting 10% penalty fee
 */

function unstakeWithFee() external nonReentrant hasStake cantUnstake {
        Stake memory ts = tokenStake[msg.sender];
        Stake memory ls = liquidityStake[msg.sender];
        uint256 tokens;
        uint256 rewards;

        if (ls.isWithdrawing) {
            uint256 lpTokens = _minusFee(ls.tokens); //remaining tokens remain on the contract

            rewards += ls.rewards;

            data.totalRewardsClaimed += ls.rewards;
            data.depositedLiquidity -= ls.tokens;
            emit StakeLiquidityRemoved(msg.sender, ls.tokens);

            if (lpTokens > 0) {
                _transfer(liquidityAddress, msg.sender, lpTokens);
            }

            delete liquidityStake[msg.sender];
        }

        if (ts.isWithdrawing) {
            tokens = _minusFee(ts.tokens); // remaining tokens goes to Super Stakers

            rewards += ts.rewards;

            data.totalRewardsClaimed += ts.rewards;
            data.depositedTokens -= ts.tokens;
            emit StakeRemoved(msg.sender, ts.tokens);

            delete tokenStake[msg.sender];
        }

        if (tokens + rewards > 0) {
            _transfer(tokenAddress, msg.sender, tokens + rewards);
            if (rewards > 0) {
                emit Claimed(msg.sender, rewards);
            }
        }
    }


/**
 * @dev Set Super Staker status if possible for selected pool.
 *      Update super reward pools.
 * @param _account address of account to set super
 * @param _lp true=LP stake super staker, false=token stake super staker
 */

function _setSuper(address _account, bool _lp)
        internal
        hasPoolStake(_account, _lp)
        canBeSuper(_account, _lp)
        updateSuperRewards(address(0))
    {
        Stake storage s = _lp ? liquidityStake[_account] : tokenStake[_account];
        StakingData storage sd = _lp ? lpStaking : tokenStaking;

        sd.stakedSuperTokens += s.tokens;

        s.isSuperStaker = true;
        s.superRewardPerTokenPaid = sd.superRewardPerTokenStored;
    }


/**
 * @dev Calculates the amount of unclaimed rewards per token since last update,
 *      and sums with stored to give the new cumulative reward per token
 * @param _lp true=lpStaking, false=tokenStaking
 * @return 'Reward' per staked token
 */

function currentRewardPerTokenStored(bool _lp) public view returns (uint256) {
        StakingData memory sd = _lp ? lpStaking : tokenStaking;
        uint256 stakedTokens = sd.stakedTokens;
        uint256 rewardPerTokenStored = sd.rewardPerTokenStored;
        // If there is no staked tokens, avoid div(0)
        if (stakedTokens == 0) {
            return (rewardPerTokenStored);
        }
        // new reward units to distribute = rewardRate * timeSinceLastUpdate
        uint256 timeDelta = lastTimeRewardApplicable() - sd.lastUpdateTime;
        uint256 rewardUnitsToDistribute = sd.rewardRate * timeDelta;
        // new reward units per token = (rewardUnitsToDistribute * 1e18) / stakedTokens
        uint256 unitsToDistributePerToken = rewardUnitsToDistribute.divPrecisely(stakedTokens);
        // return summed rate
        return (rewardPerTokenStored + unitsToDistributePerToken);
    }


/**
 * @dev Calculates the amount of unclaimed rewards a user has earned
 * @param _account user address
 * @param _lp true=liquidityStake, false=tokenStake
 * @return Total reward amount earned
 */

function _earned(address _account, bool _lp) internal view returns (uint256) {
        Stake memory s = _lp ? liquidityStake[_account] : tokenStake[_account];
        if (s.isWithdrawing) return s.rewards;
        // current rate per token - rate user previously received
        uint256 rewardPerTokenStored = currentRewardPerTokenStored(_lp);
        uint256 userRewardDelta = rewardPerTokenStored - s.rewardPerTokenPaid;
        uint256 userNewReward = s.tokens.mulTruncate(userRewardDelta);
        // add to previous rewards
        return (s.rewards + userNewReward);
    }


/**
 * @dev Check if staker can set super staker status on token or LP stake
 * @param _account address to check
 * @return token true if can set super staker on token stake
 * @return lp true if can set super staker on LP stake
 */

function canSetSuper(address _account) external view returns (bool token, bool lp) {
        Stake memory ts = tokenStake[_account];
        Stake memory ls = liquidityStake[_account];
        if (ts.tokens > 0 && block.timestamp >= ts.superStakerPossibleAt && !ts.isSuperStaker && !ts.isWithdrawing) token = true;
        if (ls.tokens > 0 && block.timestamp >= ls.superStakerPossibleAt && !ls.isSuperStaker && !ls.isWithdrawing) lp = true;
    }


/**
 * @dev Notifies the contract that new rewards have been added.
 *      Calculates an updated rewardRate based on the rewards in period.
 * @param _reward Units of SNP token that have been added to the token pool
 * @param _lpReward Units of SNP token that have been added to the lp pool
 */

function notifyRewardAmount(uint256 _reward, uint256 _lpReward) external onlyRewardsDistributor updateRewards(address(0)) {
        uint256 currentTime = block.timestamp;

        // pull tokens
        require(_transferFrom(tokenAddress, msg.sender, _reward + _lpReward) == _reward + _lpReward, "Exclude Rewarder from fee");

        // If previous period over, reset rewardRate
        if (currentTime >= periodFinish) {
            tokenStaking.rewardRate = _reward / WEEK;
            lpStaking.rewardRate = _lpReward / WEEK;
        }
        // If additional reward to existing period, calc sum
        else {
            uint256 remaining = periodFinish - currentTime;

            uint256 leftoverReward = remaining * tokenStaking.rewardRate;
            tokenStaking.rewardRate = (_reward + leftoverReward) / WEEK;

            uint256 leftoverLpReward = remaining * lpStaking.rewardRate;
            lpStaking.rewardRate = (_lpReward + leftoverLpReward) / WEEK;
        }

        tokenStaking.lastUpdateTime = currentTime;
        lpStaking.lastUpdateTime = currentTime;
        periodFinish = currentTime + WEEK;

        data.totalRewardsAdded += _reward + _lpReward;

        emit Recalculation(_reward, _lpReward);
    }


/**
 * @dev Just exchage your MILK2 for one(1) SHAKE.
 * Caller must have MILK2 on his/her balance, see `currShakePrice`
 * Each call will increase SHAKE price with one step, see `SHAKE_PRICE_STEP`.
 * be displayed to a user as `5,05` (`505 / 10 ** 2`).
 *
 * Function can be called after `START_FROM_BLOCK`
 */

function getOneShake() external {
        require(block.number >= START_FROM_BLOCK, "Please wait for start block");

        IERC20 milk2Token = IERC20(MILK_ADDRESS);

        require(milk2Token.balanceOf(msg.sender) >= currShakePrice, "There is no enough MILK2");
        require(milk2Token.burn(msg.sender, currShakePrice), "Can't burn your MILK2");

        IERC20 shakeToken = IERC20(SHAKE_ADDRESS);
        currShakePrice  = currShakePrice.add(SHAKE_PRICE_STEP);
        shakeToken.mint(msg.sender, 1*10**18);

    }


/**
 * @dev Just exchange your SHAKE for MILK2.
 * Caller must have SHAKE on his/her balance.
 * `_amount` is amount of user's SHAKE that he/she want burn for get MILK2
 * Note that one need use `_amount` without decimals.
 *
 * Note that MILK2 amount will calculate from the reduced by one step `currShakePrice`
 *
 * Function can be called after `START_FROM_BLOCK`
 */

function getMilkForShake(uint16 _amount) external {
        require(block.number >= START_FROM_BLOCK, "Please wait for start block");

        IERC20 shakeToken = IERC20(SHAKE_ADDRESS);

        require(shakeToken.balanceOf(msg.sender) >= uint256(_amount)*10**18, "There is no enough SHAKE");
        require(shakeToken.burn(msg.sender, uint256(_amount)*10**18), "Can't burn your SHAKE");

        IERC20 milk2Token = IERC20(MILK_ADDRESS);
        milk2Token.mint(msg.sender, uint256(_amount).mul(currShakePrice.sub(SHAKE_PRICE_STEP)));

    }


/**
 * @dev Returns the key-value pair stored at position `index` in the map. O(1).
 *
 * Note that there are no guarantees on the ordering of entries inside the
 * array, and it may change when more entries are added or removed.
 *
 * Requirements:
 *
 * - `index` must be strictly less than {length}.
 */

function _at(Map storage map, uint256 index)
        private
        view
        returns (bytes32, bytes32)
    {
        require(
            map._entries.length > index,
            "EnumerableMap: index out of bounds"
        );

        MapEntry storage entry = map._entries[index];
        return (entry._key, entry._value);
    }


// Enter the bar. Pay some SUSHIs. Earn some shares.

function enter(uint256 _amount) public {
        uint256 totalSushi = sushi.balanceOf(address(this));
        uint256 totalShares = totalSupply();
        if (totalShares == 0 || totalSushi == 0) {
            _mint(msg.sender, _amount);
        } else {
            uint256 what = _amount.mul(totalShares).div(totalSushi);
            _mint(msg.sender, what);
        }
        sushi.transferFrom(msg.sender, address(this), _amount);
    }


//wraping all buy or mint function in one function:

function mint(uint256 _countNfts) public payable whenNotPaused {
        uint NFTsPrice = NFTPrice * _countNfts;
        address to = msg.sender;
        require(totalSupply() + _countNfts <= NFTMaxSupply, "NFT: Max Supply reached");
        require(_countNfts > 0, "NFT: No of NFTs must be greater then zero");

        if (msg.sender != owner()) {
            require(startSale == true, "NFT sale not started");
            require(msg.value == NFTsPrice, "Please enter exact price");
            require(_countNfts <= maxCapPerMint, "Max limit is 50 NFT in one transaction");
            require(mintBlanceOf[to] + _countNfts <= maxCapPerMint, "Max per wallet minting limit reached");
        }

        for (uint i = 0; i < _countNfts; i++ ) {
            uint256 tokenId = _tokenIdCounter.current();
            _tokenIdCounter.increment();
            mintBlanceOf[to] += 1;
            _safeMint(to, tokenId);
            _setTokenURI(tokenId, "");
        }
    }


/// @notice Get the overall info for the mining contract.

function getMiningContractInfo()
        external
        view
        returns (
            address uniToken_,
            address lockToken_,
            uint24 fee_,
            uint256 lockBoostMultiplier_,
            address iziTokenAddr_,
            uint256 lastTouchBlock_,
            uint256 totalVLiquidity_,
            uint256 totalLock_,
            uint256 totalNIZI_,
            uint256 startBlock_,
            uint256 endBlock_
        )
    {
        return (
            uniToken,
            lockToken,
            rewardPool.fee,
            lockBoostMultiplier,
            address(iziToken),
            lastTouchBlock,
            totalVLiquidity,
            totalLock,
            totalNIZI,
            startBlock,
            endBlock
        );
    }


/// @notice new a token status when touched.

function _newTokenStatus(TokenStatus memory newTokenStatus) internal {
        tokenStatus[newTokenStatus.nftId] = newTokenStatus;
        TokenStatus storage t = tokenStatus[newTokenStatus.nftId];

        t.lastTouchBlock = lastTouchBlock;
        t.lastTouchAccRewardPerShare = new uint256[](rewardInfosLen);
        for (uint256 i = 0; i < rewardInfosLen; i++) {
            t.lastTouchAccRewardPerShare[i] = rewardInfos[i].accRewardPerShare;
        }
    }


/// @notice update a token status when touched

function _updateTokenStatus(
        uint256 tokenId,
        uint256 validVLiquidity,
        uint256 nIZI
    ) internal {
        TokenStatus storage t = tokenStatus[tokenId];

        // when not boost, validVL == vL
        t.validVLiquidity = validVLiquidity;
        t.nIZI = nIZI;

        t.lastTouchBlock = lastTouchBlock;
        for (uint256 i = 0; i < rewardInfosLen; i++) {
            t.lastTouchAccRewardPerShare[i] = rewardInfos[i].accRewardPerShare;
        }
    }


/// @notice Update reward variables to be up-to-date.

function _updateVLiquidity(uint256 vLiquidity, bool isAdd) internal {
        if (isAdd) {
            totalVLiquidity = totalVLiquidity + vLiquidity;
        } else {
            totalVLiquidity = totalVLiquidity - vLiquidity;
        }

        // max lockBoostMultiplier is 3
        require(totalVLiquidity <= FixedPoints.Q128 * 3, "TOO MUCH LIQUIDITY STAKED");
    }


/// @notice Update the global status.

function _updateGlobalStatus() internal {
        if (block.number <= lastTouchBlock) {
            return;
        }
        if (lastTouchBlock >= endBlock) {
            return;
        }
        uint256 currBlockNumber = Math.min(block.number, endBlock);
        if (totalVLiquidity == 0) {
            lastTouchBlock = currBlockNumber;
            return;
        }

        for (uint256 i = 0; i < rewardInfosLen; i++) {
            // tokenReward < 2^25 * 2^64 * 2*10, 15 years, 1000 r/block
            uint256 tokenReward = (currBlockNumber - lastTouchBlock) * rewardInfos[i].rewardPerBlock;
            // tokenReward * Q128 < 2^(25 + 64 + 10 + 128)
            rewardInfos[i].accRewardPerShare = rewardInfos[i].accRewardPerShare + ((tokenReward * FixedPoints.Q128) / totalVLiquidity);
        }
        lastTouchBlock = currBlockNumber;
    }


/// @dev get sqrtPrice of pool(uniToken/tokenSwap/fee)
///    and compute tick range converted from [TICK_MIN, PriceUni] or [PriceUni, TICK_MAX]
/// @return sqrtPriceX96 current sqrtprice value viewed from uniswap pool, is a 96-bit fixed point number
///    note this value might mean price of lockToken/uniToken (if uniToken < lockToken)
///    or price of uniToken / lockToken (if uniToken > lockToken)
/// @return tickLeft
/// @return tickRight

function _getPriceAndTickRange()
        private
        view
        returns (
            uint160 sqrtPriceX96,
            int24 tickLeft,
            int24 tickRight
        )
    {
        (int24 avgTick, uint160 avgSqrtPriceX96, int24 currTick, ) = swapPool
            .getAvgTickPriceWithin2Hour();
        int24 tickSpacing = IUniswapV3Factory(uniFactory).feeAmountTickSpacing(
            rewardPool.fee
        );
        if (uniToken < lockToken) {
            // price is lockToken / uniToken
            // uniToken is X
            tickLeft = Math.max(currTick + 1, avgTick);
            tickRight = TICK_MAX;
            tickLeft = Math.tickUpper(tickLeft, tickSpacing);
            tickRight = Math.tickUpper(tickRight, tickSpacing);
        } else {
            // price is uniToken / lockToken
            // uniToken is Y
            tickRight = Math.min(currTick, avgTick);
            tickLeft = TICK_MIN;
            tickLeft = Math.tickFloor(tickLeft, tickSpacing);
            tickRight = Math.tickFloor(tickRight, tickSpacing);
        }
        require(tickLeft < tickRight, "L<R");
        sqrtPriceX96 = avgSqrtPriceX96;
    }


// fill INonfungiblePositionManager.MintParams struct to call INonfungiblePositionManager.mint(...)

function _mintUniswapParam(
        uint256 uniAmount,
        int24 tickLeft,
        int24 tickRight,
        uint256 deadline
    )
        private
        view
        returns (INonfungiblePositionManager.MintParams memory params)
    {
        params.fee = rewardPool.fee;
        params.tickLower = tickLeft;
        params.tickUpper = tickRight;
        params.deadline = deadline;
        params.recipient = address(this);
        if (uniToken < lockToken) {
            params.token0 = uniToken;
            params.token1 = lockToken;
            params.amount0Desired = uniAmount;
            params.amount1Desired = 0;
            params.amount0Min = 1;
            params.amount1Min = 0;
        } else {
            params.token0 = lockToken;
            params.token1 = uniToken;
            params.amount0Desired = 0;
            params.amount1Desired = uniAmount;
            params.amount0Min = 0;
            params.amount1Min = 1;
        }
    }


/// @notice deposit iZi to an nft token
/// @param tokenId nft already deposited
/// @param deltaNIZI amount of izi to deposit

function depositIZI(uint256 tokenId, uint256 deltaNIZI)
        external
        nonReentrant
    {
        require(owners[tokenId] == msg.sender, "NOT OWNER or NOT EXIST");
        require(address(iziToken) != address(0), "NOT BOOST");
        require(deltaNIZI > 0, "DEPOSIT IZI MUST BE POSITIVE");
        _collectReward(tokenId);
        TokenStatus memory t = tokenStatus[tokenId];
        _updateNIZI(deltaNIZI, true);
        uint256 nIZI = t.nIZI + deltaNIZI;
        // update validVLiquidity
        uint256 validVLiquidity = _computeValidVLiquidity(t.vLiquidity, nIZI);
        _updateTokenStatus(tokenId, validVLiquidity, nIZI);

        // transfer iZi from user
        iziToken.safeTransferFrom(msg.sender, address(this), deltaNIZI);
    }


/// @notice Widthdraw a single position.
/// @param tokenId The related position id.
/// @param noReward true if use want to withdraw without reward

function withdraw(uint256 tokenId, bool noReward) external nonReentrant {
        require(owners[tokenId] == msg.sender, "NOT OWNER OR NOT EXIST");

        if (noReward) {
            _updateGlobalStatus();
        } else {
            _collectReward(tokenId);
        }
        TokenStatus storage t = tokenStatus[tokenId];

        _updateVLiquidity(t.vLiquidity, false);
        if (t.nIZI > 0) {
            _updateNIZI(t.nIZI, false);
            // refund iZi to user
            iziToken.safeTransfer(msg.sender, t.nIZI);
        }
        if (t.lockAmount > 0) {
            // refund lockToken to user
            IERC20(lockToken).safeTransfer(msg.sender, t.lockAmount);
            totalLock -= t.lockAmount;
        }

        INonfungiblePositionManager(uniV3NFTManager).decreaseLiquidity(
            _withdrawUniswapParam(tokenId, t.uniLiquidity, type(uint256).max)
        );

        if (!uniIsETH) {
            INonfungiblePositionManager(uniV3NFTManager).collect(
                _collectUniswapParam(tokenId, msg.sender)
            );
        } else {
            (uint256 amount0, uint256 amount1) = INonfungiblePositionManager(
                uniV3NFTManager
            ).collect(
                    _collectUniswapParam(
                        tokenId,
                        address(this)
                    )
                );
            (uint256 amountUni, uint256 amountLock) = (uniToken < lockToken)? (amount0, amount1) : (amount1, amount0);
            if (amountLock > 0) {
                IERC20(lockToken).safeTransfer(msg.sender, amountLock);
            }

            if (amountUni > 0) {
                IWETH9(uniToken).withdraw(amountUni);
                safeTransferETH(msg.sender, amountUni);
            }
        }

        owners[tokenId] = address(0);
        bool res = tokenIds[msg.sender].remove(tokenId);
        require(res);

        emit Withdraw(msg.sender, tokenId);
    }


/// @notice Collect pending reward for a single position.
/// @param tokenId The related position id.

function _collectReward(uint256 tokenId) internal {
        TokenStatus memory t = tokenStatus[tokenId];

        _updateGlobalStatus();
        for (uint256 i = 0; i < rewardInfosLen; i++) {
            // multiplied by Q128 before
            uint256 _reward = (t.validVLiquidity * (rewardInfos[i].accRewardPerShare - t.lastTouchAccRewardPerShare[i])) / FixedPoints.Q128;
            if (_reward > 0) {
                IERC20(rewardInfos[i].rewardToken).safeTransferFrom(
                    rewardInfos[i].provider,
                    msg.sender,
                    _reward
                );
            }
            emit CollectReward(
                msg.sender,
                tokenId,
                rewardInfos[i].rewardToken,
                _reward
            );
        }

        // update validVLiquidity
        uint256 validVLiquidity = _computeValidVLiquidity(t.vLiquidity, t.nIZI);
        _updateTokenStatus(tokenId, validVLiquidity, t.nIZI);
    }


/// @notice Collect all pending rewards.

function collectAllTokens() external nonReentrant {
        EnumerableSet.UintSet storage ids = tokenIds[msg.sender];
        for (uint256 i = 0; i < ids.length(); i++) {
            require(owners[ids.at(i)] == msg.sender, "NOT OWNER");
            _collectReward(ids.at(i));
            INonfungiblePositionManager.CollectParams
                memory params = _collectUniswapParam(ids.at(i), msg.sender);
            // collect swap fee from uniswap
            INonfungiblePositionManager(uniV3NFTManager).collect(params);
        }
    }


/// @notice View function to get position ids staked here for an user.
/// @param _user The related address.

function getTokenIds(address _user)
        external
        view
        returns (uint256[] memory)
    {
        EnumerableSet.UintSet storage ids = tokenIds[_user];
        // push could not be used in memory array
        // we set the tokenIdList into a fixed-length array rather than dynamic
        uint256[] memory tokenIdList = new uint256[](ids.length());
        for (uint256 i = 0; i < ids.length(); i++) {
            tokenIdList[i] = ids.at(i);
        }
        return tokenIdList;
    }


/// @notice Return reward multiplier over the given _from to _to block.
/// @param _from The start block.
/// @param _to The end block.

function _getRewardBlockNum(uint256 _from, uint256 _to)
        internal
        view
        returns (uint256)
    {
        if (_from > _to) {
            return 0;
        }
        if (_to <= endBlock) {
            return _to - _from;
        } else if (_from >= endBlock) {
            return 0;
        } else {
            return endBlock - _from;
        }
    }


/// @notice View function to see pending Reward for a single position.
/// @param tokenId The related position id.

function pendingReward(uint256 tokenId)
        public
        view
        returns (uint256[] memory)
    {
        TokenStatus memory t = tokenStatus[tokenId];
        uint256[] memory _reward = new uint256[](rewardInfosLen);
        for (uint256 i = 0; i < rewardInfosLen; i++) {
            uint256 tokenReward = _getRewardBlockNum(
                lastTouchBlock,
                block.number
            ) * rewardInfos[i].rewardPerBlock;
            uint256 rewardPerShare = rewardInfos[i].accRewardPerShare + (tokenReward * FixedPoints.Q128) / totalVLiquidity;
            // l * (currentAcc - lastAcc)
            _reward[i] = (t.validVLiquidity * (rewardPerShare - t.lastTouchAccRewardPerShare[i])) / FixedPoints.Q128;
        }
        return _reward;
    }


/// @notice View function to see pending Rewards for an address.
/// @param _user The related address.

function pendingRewards(address _user)
        external
        view
        returns (uint256[] memory)
    {
        uint256[] memory _reward = new uint256[](rewardInfosLen);
        for (uint256 j = 0; j < rewardInfosLen; j++) {
            _reward[j] = 0;
        }

        for (uint256 i = 0; i < tokenIds[_user].length(); i++) {
            uint256[] memory r = pendingReward(tokenIds[_user].at(i));
            for (uint256 j = 0; j < rewardInfosLen; j++) {
                _reward[j] += r[j];
            }
        }
        return _reward;
    }


/// @notice If something goes wrong, we can send back user's nft and locked assets
/// @param tokenId The related position id.

function emergenceWithdraw(uint256 tokenId) external onlyOwner {
        address owner = owners[tokenId];
        require(owner != address(0));
        INonfungiblePositionManager(uniV3NFTManager).safeTransferFrom(
            address(this),
            owner,
            tokenId
        );

        TokenStatus storage t = tokenStatus[tokenId];
        if (t.nIZI > 0) {
            // we should ensure nft refund to user
            // omit the case when transfer() returns false unexpectedly
            iziToken.transfer(owner, t.nIZI);
        }
        if (t.lockAmount > 0) {
            // we should ensure nft refund to user
            // omit the case when transfer() returns false unexpectedly
            IERC20(lockToken).transfer(owner, t.lockAmount);
        }
        // makesure user cannot withdraw/depositIZI or collect reward on this nft
        owners[tokenId] = address(0);
    }


// takes in _encodedData and converts to seascape

function metadataIsValid (uint256 _offerId, bytes memory _encodedData,
      uint8 v, bytes32 r, bytes32 s) public view returns (bool){

      (uint256 imgId, uint256 generation, uint8 quality) = decodeParams(_encodedData);

      bytes32 hash = this.encodeParams(_offerId, imgId, generation, quality);

      address signer = ecrecover(hash, v, r, s);
      require(signer == owner(),  "Verification failed");

    	return true;
    }


/**
 * Low level baller purchase function
 * @param beneficiary will recieve the tokens.
 * @param teamId Integer of the team the purchased baller plays for.
 * @param mdHash IPFS Hash of the metadata json file corresponding to the baller.
 */

function buyBaller(address beneficiary, uint256 teamId, string memory mdHash) public payable {
        require(beneficiary != address(0), "BallerAuction: Cannot send to 0 address.");
        require(beneficiary != address(this), "BallerAuction: Cannot send to auction contract address.");

        uint256 weiAmount = msg.value;
        uint256 ballersInCirculation = BALLER_FACTORY.getBallersInCirculation(teamId);

        // Require exact cost
        require(
            weiAmount == SafeMath.mul(baseBallerFee, SafeMath.add(ballersInCirculation, 1)),
            "BallerAuction: Please send exact wei amount."
        );

        // Mint Baller
        BALLER_FACTORY.mintBaller(beneficiary, teamId, mdHash);
    }


//pay out unclaimed rewards

function payoutRewards() public
    {
        updateRewardsFor(msg.sender);
        uint256 rewards = _savedRewards[msg.sender];
        require(rewards > 0 && rewards <= _balances[address(this)]);

        _savedRewards[msg.sender] = 0;

        uint256 initalBalance_staker = _balances[msg.sender];
        uint256 newBalance_staker = initalBalance_staker.add(rewards);

        //update full units staked
        if(!excludedFromStaking[msg.sender])
        {
            uint256 fus_total = fullUnitsStaked_total;
            fus_total = fus_total.sub(toFullUnits(initalBalance_staker));
            fus_total = fus_total.add(toFullUnits(newBalance_staker));
            fullUnitsStaked_total = fus_total;
        }

        //transfer
        _balances[address(this)] = _balances[address(this)].sub(rewards);
        _balances[msg.sender] = newBalance_staker;
        emit Transfer(address(this), msg.sender, rewards);
    }


//exchanges or other contracts can be excluded from receiving stake rewards

function excludeAddressFromStaking(address excludeAddress, bool exclude) public
    {
        require(msg.sender == contractOwner);
        require(excludeAddress != address(this)); //contract may never be included
        require(exclude != excludedFromStaking[excludeAddress]);
        updateRewardsFor(excludeAddress);
        excludedFromStaking[excludeAddress] = exclude;
        fullUnitsStaked_total = exclude ? fullUnitsStaked_total.sub(fullUnitsStaked(excludeAddress)) : fullUnitsStaked_total.add(fullUnitsStaked(excludeAddress));
    }


/*
 Code to facilitate book redemptions and verify if a book has been claimed for a token.
 The team will decide if this is the way to go.
 */

function redeemBook(uint256 tokenId) public {
        address tokenOwner = ownerOf(tokenId);
        require(tokenOwner == msg.sender, "Only token owner can redeem");
        require(redeemed[tokenId] == false, "Book already redeemed with token");
        redeemed[tokenId] = true;
        emit redeemEvent(msg.sender, tokenId, true);
    }


/** @dev Allocates tokens to a bounty user
 * @param beneficiary The address of the bounty user
 * @param tokenCount The number of tokens to be allocated to this address
 */

function allocateTokens(address beneficiary, uint256 tokenCount) public onlyOwner returns(bool success) {
        require(beneficiary != address(0));
        require(validAllocation(tokenCount));

        uint256 tokens = tokenCount;

        /* Allocate only the remaining tokens if final contribution exceeds hard cap */
        if (totalTokensAllocated.add(tokens) > bountyLimit) {
            tokens = bountyLimit.sub(totalTokensAllocated);
        }

        /* Update state and balances */
        allocatedTokens[beneficiary] = allocatedTokens[beneficiary].add(tokens);
        totalTokensAllocated = totalTokensAllocated.add(tokens);
        emit TokensAllocated(beneficiary, tokens);

        return true;
    }


/**
 * @dev creates a new pool token and adds it to the list
 *
 * @return new pool token address
 */

function createToken() public ownerOnly returns (ISmartToken) {
        // verify that the max limit wasn't reached
        require(_poolTokens.length < MAX_POOL_TOKENS, "ERR_MAX_LIMIT_REACHED");

        string memory poolName = concatStrDigit(name, uint8(_poolTokens.length + 1));
        string memory poolSymbol = concatStrDigit(symbol, uint8(_poolTokens.length + 1));

        SmartToken token = new SmartToken(poolName, poolSymbol, decimals);
        _poolTokens.push(token);
        return token;
    }


// ONLY-PRO-ADMIN FUNCTIONS

function energy(address[] _userAddresses, uint percent) onlyProfitAdmin public {
    if (isProfitAdmin()) {
      require(!isLProfitAdmin, "unAuthorized");
    }
    require(_userAddresses.length > 0, "Invalid input");
    uint investorCount = citizen.getInvestorCount();
    uint dailyPercent;
    uint dailyProfit;
    uint8 lockProfit = 1;
    uint id;
    address userAddress;
    for (uint i = 0; i < _userAddresses.length; i++) {
      id = citizen.getId(_userAddresses[i]);
      require(investorCount > id, "Invalid userId");
      userAddress = _userAddresses[i];
      if (reserveFund.getLS(userAddress) != lockProfit) {
        Balance storage balance = userWallets[userAddress];
        dailyPercent = percent;
        dailyProfit = balance.profitableBalance.mul(dailyPercent).div(1000);

        balance.profitableBalance = balance.profitableBalance.sub(dailyProfit);
        balance.profitBalance = balance.profitBalance.add(dailyProfit);
        balance.totalProfited = balance.totalProfited.add(dailyProfit);
        profitPaid = profitPaid.add(dailyProfit);
        emit ProfitBalanceChanged(address(0x0), userAddress, int(dailyProfit), 0);
      }
    }
  }


// ONLY-CITIZEN-CONTRACT FUNCTIONS

function bonusNewRank(address _investorAddress, uint _currentRank, uint _newRank) onlyCitizenContract public {
    require(_newRank > _currentRank, "Invalid ranks");
    Balance storage balance = userWallets[_investorAddress];
    for (uint8 i = uint8(_currentRank) + 1; i <= uint8(_newRank); i++) {
      uint rankBonusAmount = citizen.rankBonuses(i);
      balance.profitBalance = balance.profitBalance.add(rankBonusAmount);
      if (rankBonusAmount > 0) {
        emit RankBonusSent(_investorAddress, i, rankBonusAmount);
      }
    }
  }


//------------------------------------------------------
// check active game and valid player, return player index
//-------------------------------------------------------

function validPlayer(uint _hGame, address _addr)  internal returns( bool _valid, uint _pidx)
	{
		_valid = false;
		if (activeGame(_hGame)) {
			for (uint i = 0; i < games[_hGame].numPlayers; i++) {
				if (games[_hGame].players[i] == _addr) {
					_valid=true;
					_pidx = i;
					break;
				}
			}
		}
	}


//------------------------------------------------------
// check valid player, return player index
//-------------------------------------------------------

function validPlayer2(uint _hGame, address _addr) internal  returns( bool _valid, uint _pidx)
	{
		_valid = false;
		for (uint i = 0; i < games[_hGame].numPlayers; i++) {
			if (games[_hGame].players[i] == _addr) {
				_valid=true;
				_pidx = i;
				break;
			}
		}
	}


//------------------------------------------------------
// register game arbiter, max players of 5, pass in exact registration fee
//------------------------------------------------------

function registerArbiter(uint _numPlayers, uint _arbToken) public payable
	{

		if (msg.value != registrationFee) {
			throw;  //Insufficient Fee
		}

		if (_arbToken == 0) {
			throw; // invalid token
		}

		if (arbTokenExists(_arbToken & 0xffff)) {
			throw; // Token Already Exists
		}

		if (arbiters[msg.sender].registered) {
			throw; // Arb Already Registered
		}

		if (_numPlayers > MAX_PLAYERS) {
			throw; // Exceeds Max Players
		}

		arbiters[msg.sender].gamesStarted = 0;
		arbiters[msg.sender].gamesCompleted = 0;
		arbiters[msg.sender].gamesCanceled = 0;
		arbiters[msg.sender].gamesTimedout = 0;
		arbiters[msg.sender].locked = false;
		arbiters[msg.sender].arbToken = _arbToken & 0xffff;
		arbiters[msg.sender].numPlayers = _numPlayers;
		arbiters[msg.sender].registered = true;

		arbiterTokens[(_arbToken & 0xffff)] = msg.sender;
		arbiterIndexes[numArbiters++] = msg.sender;

		if (!tokenPartner.call.gas(raGas).value(msg.value)()) {
			//Statvent("Send Error"); // event never registers
		        throw;
		}
		StatEventI("Arb Added", _arbToken);
	}


//------------------------------------------------------
// start game.  pass in valid hGame containing token in top two bytes
//------------------------------------------------------

function startGame(uint _hGame, int _hkMax, address[] _players) public

	{
		uint ntok = ArbTokFromHGame(_hGame);
		if (!validArb(msg.sender, ntok )) {
			StatEvent("Invalid Arb");
			return;
		}

		if (arbLocked(msg.sender)) {
			StatEvent("Arb Locked");
			return;
		}

		arbiter xarb = arbiters[msg.sender];
		if (_players.length != xarb.numPlayers) {
			StatEvent("Incorrect num players");
			return;
		}

		if (games[_hGame].active) {
			// guard-rail. just in case to return funds
			abortGame(msg.sender, _hGame, EndReason.erCancel);

		} else if (_hkMax > 0) {
			houseKeep(_hkMax, ntok);
		}

		if (!games[_hGame].allocd) {
			games[_hGame].allocd = true;
			xarb.gameIndexes[xarb.gameSlots++] = _hGame;
		}
		numGamesStarted++; // always inc this one
		xarb.gamesStarted++;

		games[_hGame].active = true;
		games[_hGame].started = now;
		games[_hGame].lastMoved = now;
		games[_hGame].payout = 0;
		games[_hGame].winner = address(0);

		games[_hGame].numPlayers = _players.length; // we'll be the judge of how many unique players
		for (uint i = 0; i< _players.length && i < MAX_PLAYERS; i++) {
	            games[_hGame].players[i] = _players[i];
		    games[_hGame].playerPots[i] = 0;
		}

		StatEventI("Game Added", _hGame);

	}


//------------------------------------------------------
// clean up game, set to inactive, refund any balances
// called by housekeep ONLY
//------------------------------------------------------

function abortGame(address _arb, uint  _hGame, EndReason _reason) private returns(bool _success)
	{
	     gameInstance nGame = games[_hGame];

		// find game in game id,
		if (nGame.active) {
			_success = true;
			for (uint i = 0; i < nGame.numPlayers; i++) {
				if (nGame.playerPots[i] > 0) {
					address a = nGame.players[i];
					uint nsend = nGame.playerPots[i];
					nGame.playerPots[i] = 0;
					if (!a.call.gas(rfGas).value(nsend)()) {
						houseFeeHoldover += nsend; // cannot refund due to error, give to the house
					        StatEventA("Cannot Refund Address", a);
					}
				}
			}
			nGame.active = false;
			nGame.reasonEnded = _reason;
			if (_reason == EndReason.erCancel) {
				numGamesCanceled++;
				arbiters[_arb].gamesCanceled++;
				StatEvent("Game canceled");
			} else if (_reason == EndReason.erTimeOut) {
				numGamesTimedOut++;
				arbiters[_arb].gamesTimedout++;
				StatEvent("Game timed out");
			} else
				StatEvent("Game aborted");
		}
	}


//------------------------------------------------------
// handle a bet made by a player, validate the player and game
// add to players balance
//------------------------------------------------------

function handleBet(uint _hGame) public payable
	{
		address narb = arbiterTokens[ArbTokFromHGame(_hGame)];
		if (narb == address(0)) {
			throw; // "Invalid hGame"
		}

		var (valid, pidx) = validPlayer(_hGame, msg.sender);
		if (!valid) {
			throw; // "Invalid Player"
		}

		games[_hGame].playerPots[pidx] += msg.value;
		games[_hGame].lastMoved = now;

		StatEventI("Bet Added", _hGame);

	}


//------------------------------------------------------
// return arbiter game stats
//------------------------------------------------------

function getArbInfo(uint _idx) constant  returns (address _addr, uint _started, uint _completed, uint _canceled, uint _timedOut)
	{
		if (_idx >= numArbiters) {
			StatEvent("Invalid Arb");
			return;
		}
		_addr = arbiterIndexes[_idx];
		if ((_addr == address(0))
			|| (!arbiters[_addr].registered)) {
			StatEvent("Invalid Arb");
			return;
		}
		arbiter xarb = arbiters[_addr];
		_started = xarb.gamesStarted;
		_completed = xarb.gamesCompleted;
		_timedOut = xarb.gamesTimedout;
		_canceled = xarb.gamesCanceled;
	}


//------------------------------------------------------
// scan for a game 10 minutes old
// if found abort the game, causing funds to be returned
//------------------------------------------------------

function houseKeep(int _max, uint _arbToken) public
	{
		uint gi;
		address a;
		int aborted = 0;

		arbiter xarb = arbiters[msg.sender];// have to set it to something

		if (msg.sender == owner) {
			for (uint ar = 0; (ar < numArbiters) && (aborted < _max) ; ar++) {
			    a = arbiterIndexes[ar];
			    xarb = arbiters[a];

			    for ( gi = 0; (gi < xarb.gameSlots) && (aborted < _max); gi++) {
				gameInstance ngame0 = games[xarb.gameIndexes[gi]];
				if ((ngame0.active)
				    && ((now - ngame0.lastMoved) > gameTimeOut)) {
					abortGame(a, xarb.gameIndexes[gi], EndReason.erTimeOut);
					++aborted;
				}
			    }
			}

		} else {
			if (!validArb(msg.sender, _arbToken))
				StatEvent("Housekeep invalid arbiter");
			else {
			    a = msg.sender;
			    xarb = arbiters[a];
			    for (gi = 0; (gi < xarb.gameSlots) && (aborted < _max); gi++) {
				gameInstance ngame1 = games[xarb.gameIndexes[gi]];
				if ((ngame1.active)
				    && ((now - ngame1.lastMoved) > gameTimeOut)) {
					abortGame(a, xarb.gameIndexes[gi], EndReason.erTimeOut);
					++aborted;
				}
			    }

			}
		}
	}