contract Lottery is ERC20, CoreUniLotterySettings
{
    // ===================== Events ===================== //

    // After initialize() function finishes.
    event LotteryInitialized();

    // Emitted when lottery active stage ends (Mining Stage starts),
    // on Mining Stage Step 1, after transferring profits to their
    // respective owners (pool and OWNER_ADDRESS).
    event LotteryEnd(
        uint128 totalReturn,
        uint128 profitAmount
    );

    // Emitted when on final finish, we call Randomness Provider
    // to callback us with random value.
    event RandomnessProviderCalled();

    // Requirements for finishing stage start have been reached - 
    // finishing stage has started.
    event FinishingStageStarted();

    // We were currently on the finishing stage, but some requirement
    // is no longer met. We must stop the finishing stage.
    event FinishingStageStopped();

    // New Referral ID has been generated.
    event ReferralIDGenerated(
        address referrer,
        uint256 id
    );

    // New referral has been registered with a valid referral ID.
    event ReferralRegistered(
        address referree,
        address referrer,
        uint256 id
    );

    // Fallback funds received.
    event FallbackEtherReceiver(
        address sender,
        uint value
    );


    // ======================  Structs & Enums  ====================== //

    // Lottery Stages. 
    // Described in more detail above, on contract's main doc.
    enum STAGE
    {
        // Initial stage - before the initialize() function is called.
        INITIAL,

        // Active Stage: On this stage, all token trading occurs.
        ACTIVE,

        // Finishing stage:
        // This is when all finishing criteria are met, and for every
        // transfer, we're rolling a pseudo-random number to determine
        // if we should end the lottery (move to Ending stage).
        FINISHING,

        // Ending - Mining Stage:
        // This stage starts after we lottery is no longer active,
        // finishing stage ends. On this stage, Miners perform the
        // Ending Algorithm and other operations.
        ENDING_MINING,

        // Lottery is completed - this is set after the Mining Stage ends.
        // In this stage, Lottery Winners can claim their prizes.
        COMPLETION,

        // DISABLED stage. Used when we want a lottery contract to be
        // absolutely disabled - so no state-modifying functions could
        // be called.
        // This is used in DelegateCall scenarios, where state-contract
        // delegate-calls code contract, to save on deployment costs.
        DISABLED
    }


    // Ending algorithm types enum.
    enum EndingAlgoType
    {
        // 1. Mined Winner Selection Algorithm.
        //  This algorithm is executed by a Lottery Miner in a single
        //  transaction, on Mining Step 2.
        //
        //  On that single transaction, all ending scores for all
        //  holders are computed, and a sorted winner array is formed,
        //  which is written onto the LotteryStorage state.
        //  Thus, it's gas expensive, and suitable only for small
        //  holder numbers (up to 300).
        //
        // Pros:
        //  + Guaranteed deterministically specifiable winner prize
        //    distribution - for example, if we specify that there
        //    must be 2 winners, of which first gets 60% of prize funds,
        //    and second gets 40% of prize funds, then it's
        //    guarateed that prize funds will be distributed just
        //    like that.
        //
        //  + Low gas cost of prize claims - only ~ 40,000 gas for
        //    claiming a prize.
        //
        // Cons:
        //  - Not scaleable - as the Winner Selection Algorithm is
        //    executed in a single transaction, it's limited by 
        //    block gas limit - 12,500,000 on the MainNet.
        //    Thus, the lottery is limited to ~300 holders, and
        //    max. ~200 winners of those holders.
        //    So, it's suitable for only express-lotteries, where
        //    a lottery runs only until ~300 holders are reached.
        //
        //  - High mining costs - if lottery has 300 holders,
        //    mining transaction takes up whole block gas limit.
        //
        MinedWinnerSelection,

        // 2. Winner Self-Validation Algorithm.
        //
        //  This algorithm does no operations during the Mining Stage
        //  (except for setting up a Random Seed in Lottery Storage) -
        //  the winner selection (obtaining a winner rank) is done by
        //  the winners themselves, when calling the prize claim
        //  functions.
        //
        //  This algorithm relies on a fact that by the time that
        //  random seed is obtained, all data needed for winner selection
        //  is already there - the holder scores of the Active Stage
        //  (ether contributed, time factors, token balance), and
        //  the Random Data (random seed + nonce (holder's address)),
        //  so, there is no need to compute and sort the scores for the
        //  whole holder array.
        //
        //  It's done like this: the holder checks if he's a winner, using
        //  a view-function off-chain, and if so, he calls the 
        //  claimWinnerPrize() function, which obtains his winner rank
        //  on O(n) time, and does no writing to contract states,
        //  except for prize transfer-related operations.
        //
        //  When computing the winner's rank on LotteryStorage,
        //  O(n) time is needed, as we loop through the holders array,
        //  computing ending scores for each holder, using already-known
        //  data. 
        //  However that means that for every prize claim, all scores of
        //  all holders must be re-computed.
        //  Computing a score for a single holder takes roughly 1500 gas
        //  (400 for 3 slots SLOAD, and ~300 for arithmetic operations).
        //
        //  So, this algorithm makes prize claims more expensive for
        //  every lottery holder.
        //  If there's 1000 holders, prize claim takes up 1,500,000 gas,
        //  so, this algorithm is not suitable for small prizes,
        //  because gas fee would be higher than the prize amount won.
        //
        // Pros:
        //  + Guaranteed deterministically specifiable winner prize
        //    distribution (same as for algorithm 1).
        //
        //  + No mining costs for winner selection algorithm.
        //
        //  + More scalable than algorithm 1.
        //
        // Cons:
        //  - High gas costs of prize claiming, rising with the number
        //    of lottery holders - 1500 for every lottery holder.
        //    Thus, suitable for only large prize amounts.
        //
        WinnerSelfValidation,

        // 3. Rolled-Randomness algorithm.
        //
        //  This algorithm is the most cheapest in terms of gas, but
        //  the winner prize distribution is non-deterministic.
        //
        //  This algorithm doesn't employ miners (no mining costs),
        //  and doesn't require to compute scores for every holder
        //  prior to getting a winner's rank, thus is the most scalable.
        //
        //  It works like this: a holder checks his winner status by
        //  computing only his own randomized score (rolling a random
        //  number from the random seed, and multiplying it by holder's
        //  Active Stage score), and computing this randomized-score's
        //  ratio relative to maximum available randomized score.
        //  The higher the ratio, the higher the winner rank is.
        //
        //  However, many players can roll very high or low scores, and
        //  get the same prizes, so it's difficult to make a fair and
        //  efficient deterministic prize distribution mechanism, so
        //  we have to fallback to specific heuristic workarounds.
        //
        // Pros:
        //  + Scalable: O(1) complexity for computing a winner rank,
        //      so there can be an unlimited amount of lottery holders,
        //      and gas costs for winner selection and prize claim would
        //      still be constant & low.
        //
        //  + Gas-efficient: gas costs for all winner-related operations
        //      are constant and low, because only single holder's score
        //      is computed.
        //
        //  + Doesn't require mining - even more gas savings.
        //
        // Cons:
        //  + Hard to make a deterministic and fair prize distribution
        //      mechanism, because of un-known environment - as only
        //      single holder's score is compared to max-available
        //      random score, not taking into account other holder
        //      scores.
        //
        RolledRandomness
    }


    /**
     *  Gas-efficient, minimal config, which specifies only basic,
     *  most-important and most-used settings.
     */
    struct LotteryConfig
    {
        // ================ Misc Settings =============== //

        // --------- Slot --------- //

        // Initial lottery funds (initial market cap).
        // Specified by pool, and is used to check if initial funds 
        // transferred to fallback are correct - equal to this value.
        uint initialFunds;


        // --------- Slot --------- //

        // The minimum ETH value of lottery funds, that, once
        // reached on an exchange liquidity pool (Uniswap, or our
        // contract), must be guaranteed to not shrink below this value.
        // 
        // This is accomplished in _transfer() function, by denying 
        // all sells that would drop the ETH amount in liquidity pool
        // below this value.
        // 
        // But on initial lottery stage, before this minimum requirement
        // is reached for the first time, all sells are allowed.
        //
        // This value is expressed in ETH - total amount of ETH funds
        // that we own in Uniswap liquidity pair.
        //
        // So, if initial funds were 10 ETH, and this is set to 100 ETH,
        // after liquidity pool's ETH value reaches 100 ETH, all further
        // sells which could drop the liquidity amount below 100 ETH,
        // would be denied by require'ing in _transfer() function
        // (transactions would be reverted).
        //
        uint128 fundRequirement_denySells;

        // ETH value of our funds that we own in Uniswap Liquidity Pair,
        // that's needed to start the Finishing Stage.
        uint128 finishCriteria_minFunds;


        // --------- Slot --------- //

        // Maximum lifetime of a lottery - maximum amount of time 
        // allowed for lottery to stay active.
        // By default, it's two weeks.
        // If lottery is still active (hasn't returned funds) after this
        // time, lottery will stop on the next token transfer.
        uint32 maxLifetime;

        // Maximum prize claiming time - for how long the winners
        // may be able to claim their prizes after lottery ending.
        uint32 prizeClaimTime;

        // Token transfer burn rates for buyers, and a default rate for
        // sells and non-buy-sell transfers.
        uint32 burn_buyerRate;
        uint32 burn_defaultRate;

        // Maximum amount of tokens (in percentage of initial supply)
        // to be allowed to own by a single wallet.
        uint32 maxAmountForWallet_percentageOfSupply;

        // The required amount of time that must pass after
        // the request to Randomness Provider has been made, for
        // external actors to be able to initiate alternative
        // seed generation algorithm.
        uint32 REQUIRED_TIME_WAITING_FOR_RANDOM_SEED;
        
        
        // ================ Profit Shares =============== //

        // "Mined Uniswap Lottery" ending Ether funds, which were obtained
        // by removing token liquidity from Uniswap, are transfered to
        // these recipient categories:
        //
        //  1. The Main Pool:   Initial funds, plus Pool's profit share.
        //  2. The Owner:       Owner's profit share.
        //
        //  3. The Miners:      Miner rewards for executing the winner
        //      selection algorithm stages.
        //      The more holders there are, the more stages the 
        //      winner selection algorithm must undergo.
        //      Each Miner, who successfully completed an algorithm
        //      stage, will get ETH reward equal to:
        //      (minerProfitShare / totalAlgorithmStages).
        //
        //  4. The Lottery Winners:     All remaining funds are given to
        //      Lottery Winners, which were determined by executing
        //      the Winner Selection Algorithm at the end of the lottery
        //      (Miners executed it).
        //      The Winners can claim their prizes by calling a 
        //      dedicated function in our contract.
        //
        //  The profit shares of #1 and #2 have controlled value ranges 
        //  specified in CoreUniLotterySettings.
        //
        //  All these shares are expressed as percentages of the
        //  lottery profit amount (totalReturn - initialFunds).
        //  Percentages are expressed using the PERCENT constant, 
        //  defined in CoreUniLotterySettings.
        //
        //  Here we specify profit shares of Pool, Owner, and the Miners.
        //  Winner Prize Fund is all that's left (must be more than 50%
        //  of all profits).
        //

        uint32 poolProfitShare;
        uint32 ownerProfitShare;

        // --------- Slot --------- //

        uint32 minerProfitShare;
        
        
        // =========== Lottery Finish criteria =========== //

        // Lottery finish by design is a whole soft stage, that
        // starts when criteria for holders and fund gains are met.
        // During this stage, for every token transfer, a pseudo-random
        // number will be rolled for lottery finish, with increasing 
        // probability.
        //
        // There are 2 ways that this probability increase is 
        // implemented:
        // 1. Increasing on every new holder.
        // 2. Increasing on every transaction after finish stage
        //    was initiated.
        //
        // On every new holder, probability increases more than on
        // new transactions.
        //
        // However, if during this stage some criteria become 
        // no-longer-met, the finish stage is cancelled.
        // This cancel can be implemented by setting finish probability
        // to zero, or leaving it as it was, but pausing the finishing
        // stage.
        // This is controlled by finish_resetProbabilityOnStop flag -
        // if not set, probability stays the same, when the finishing
        // stage is discontinued. 

        // ETH value of our funds that we own in Uniswap Liquidity Pair,
        // that's needed to start the Finishing Stage.
        //
        // LOOK ABOVE - arranged for tight-packing.

        // Minimum number of token holders required to start the
        // finishing stage.
        uint32 finishCriteria_minNumberOfHolders;

        // Minimum amount of time that lottery must be active.
        uint32 finishCriteria_minTimeActive;

        // Initial finish probability, when finishing stage was
        // just initiated.
        uint32 finish_initialProbability;

        // Finishing probability increase steps, for every new 
        // transaction and every new holder.
        // If holder number decreases, probability decreases.
        uint32 finish_probabilityIncreaseStep_transaction;
        uint32 finish_probabilityIncreaseStep_holder;


        // =========== Winner selection config =========== //

        // Winner selection algorithm settings.
        //
        // Algorithm is based on score, which is calculated for 
        // every holder on lottery finish, and is comprised of
        // the following parts.
        // Each part is normalized to range ( 0 - scorePoints ), 
        // from smallest to largest value of each holder;
        //
        // After scores are computed, they are multiplied by 
        // holder count factor (holderCount / holderCountDivisor),
        // and finally, multiplied by safely-generated random values,
        // to get end winning scores.
        // The top scorers win prizes.
        //
        // By default setting, max score is 40 points, and it's
        // comprised of the following parts:
        //
        // 1. Ether contributed (when buying from Uniswap or contract). 
        //    Gets added when buying, and subtracted when selling.
        //      Default: 10 points.
        //
        // 2. Amount of lottery tokens holder has on finish.
        //      Default: 5 points.
        //
        // 3. Ether contributed, multiplied by the relative factor
        //      of time - that is/*, "block.timestamp" */minus "lotteryStartTime".
        //      This way, late buyers can get more points even if
        //      they get little tokens and don't spend much ether.
        //      Default: 5 points.
        //
        // 4. Refferrer bonus. For every player that joined with
        //      your referral ID, you get (that player's score) / 10 
        //      points! This goes up to specified max score.
        //      Also, every player who provides a valid referral ID,
        //      gets 2 points for free!
        //      Default max bonus: 20 points.
        //
        int16 maxPlayerScore_etherContributed;
        int16 maxPlayerScore_tokenHoldingAmount;
        int16 maxPlayerScore_timeFactor;
        int16 maxPlayerScore_refferalBonus;

        // --------- Slot --------- //

        // Score-To-Random ration data (as a rational ratio number).
        // For example if 1:5, then scorePart = 1, and randPart = 5.
        uint16 randRatio_scorePart;
        uint16 randRatio_randPart;

        // Time factor divisor - interval of time, in seconds, after
        // which time factor is increased by one.
        uint16 timeFactorDivisor;

        // Bonus score a player should get when registering a valid
        // referral code obtained from a referrer.
        int16 playerScore_referralRegisteringBonus;


        // Are we resetting finish probability when finishing stage
        // stops, if some criteria are no longer met?
        bool finish_resetProbabilityOnStop;


        // =========== Winner Prize Fund Settings =========== //

        // There are 2 available modes that we can use to distribute
        // winnings: a computable sequence (geometrical progression),
        // or an array of winner prize fund share percentages.

        // More gas efficient is to use a computable sequence, 
        // where each winner gets a share equal to (factor * fundsLeft).
        // Factor is in range [0.01 - 1.00] - simulated as [1% - 100%].
        //
        // For example:
        // Winner prize fund is 100 ethers, Factor is 1/4 (25%), and 
        // there are 5 winners total (winnerCount), and sequenced winner
        // count is 2 (sequencedWinnerCount).
        //
        // So, we pre-compute the upper shares, till we arrive to the
        // sequenced winner count, in a loop:
        // - Winner 1: 0.25 * 100 = 25 eth; 100 - 25 = 75 eth left.
        // - Winner 2: 0.25 * 75 ~= 19 eth; 75  - 19 = 56 eth left.
        //
        // Now, we compute the left-over winner shares, which are
        // winners that get their prizes from the funds left after the
        // sequence winners.
        //
        // So, we just divide the leftover funds (56 eth), by 3,
        // because winnerCount - sequencedWinnerCount = 3.
        // - Winner 3 = 56 / 3 = 18 eth;
        // - Winner 4 = 56 / 3 = 18 eth;
        // - Winner 5 = 56 / 3 = 18 eth;
        //

        // If this value is 0, then we'll assume that array-mode is
        // to be used.
        uint32 prizeSequenceFactor;

        // Maximum number of winners that the prize sequence can yield,
        // plus the leftover winners, which will get equal shares of
        // the remainder from the first-prize sequence.
        
        uint16 prizeSequence_winnerCount;

        // How many winners would get sequence-computed prizes.
        // The left-over winners
        // This is needed because prizes in sequence tend to zero, so
        // we need to limit the sequence to avoid very small prizes,
        // and to avoid the remainder.
        uint16 prizeSequence_sequencedWinnerCount;

        // Initial token supply (without decimals).
        uint48 initialTokenSupply;

        // Ending Algorithm type.
        // More about the 3 algorithm types above.
        uint8 endingAlgoType;


        // --------- Slot --------- //

        // Array mode: The winner profit share percentages array. 
        // For example, lottery profits can be distributed this way:
        //
        // Winner profit shares (8 winners):
        // [ 20%, 15%, 10%, 5%, 4%, 3%, 2%, 1% ] = 60% of profits.
        // Owner profits: 10%
        // Pool profits:  30%
        //
        // Pool profit share is not defined explicitly in the config, so
        // when we internally validate specified profit shares, we 
        // assume the pool share to be the left amount until 100% ,
        // but we also make sure that this amount is at least equal to
        // MIN_POOL_PROFITS, defined in CoreSettings.
        //
        uint32[] winnerProfitShares;

    }


    // ========================= Constants ========================= //


    // The Miner Profits - max/min values.
    // These aren't defined in Core Settings, because Miner Profits
    // are only specific to this lottery type.

    uint32 constant MIN_MINER_PROFITS = 1 * PERCENT;
    uint32 constant MAX_MINER_PROFITS = 10 * PERCENT;


    // Uniswap Router V2 contract instance.
    // Address is the same for MainNet, and all public testnets.
    IUniswapRouter constant uniswapRouter = IUniswapRouter(
        address( 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D ) );


    // Public-accessible ERC20 token specific constants.
    string constant public name = "UniLottery Token";
    string constant public symbol = "ULT";
    uint256 constant public decimals = 18;


    // =================== State Variables =================== //

    // ------- Initial Slots ------- //

    // The config which is passed to constructor.
    LotteryConfig internal cfg;

    // ------- Slot ------- //

    // The Lottery Storage contract, which stores all holder data,
    // such as scores, referral tree data, etc.
    LotteryStorage public lotStorage;

    // ------- Slot ------- //

    // Pool address. Set on constructor from msg.sender.
    address payable public poolAddress;

    // ------- Slot ------- //
    
    // Randomness Provider address.
    address public randomnessProvider;

    // ------- Slot ------- //

    // Exchange address. In Uniswap mode, it's the Uniswap liquidity 
    // pair's address, where trades execute.
    address public exchangeAddress;

    // Start date.
    uint32 public startDate;

    // Completion (Mining Phase End) date.
    uint32 public completionDate;
    
    // The date when Randomness Provider was called, requesting a
    // random seed for the lottery finish.
    // Also, when this variable becomes Non-Zero, it indicates that we're
    // on Ending Stage Part One: waiting for the random seed.
    uint32 finish_timeRandomSeedRequested;

    // ------- Slot ------- //

    // WETH address. Set by calling Router's getter, on constructor.
    address WETHaddress;

    // Is the WETH first or second token in our Uniswap Pair?
    bool uniswap_ethFirst;

    // If we are, or were before, on finishing stage, this is the
    // probability of lottery going to Ending Stage on this transaction.
    uint32 finishProbablity;
    
    // Re-Entrancy Lock (Mutex).
    // We protect for reentrancy in the Fund Transfer functions.
    bool reEntrancyMutexLocked;
    
    // On which stage we are currently.
    uint8 public lotteryStage;
    
    // Indicator for whether the lottery fund gains have passed a 
    // minimum fund gain requirement.
    // After that time point (when this bool is set), the token sells
    // which could drop the fund value below the requirement, would
    // be denied.
    bool fundGainRequirementReached;
    
    // The current step of the Mining Stage.
    uint16 miningStep;

    // If we're currently on Special Transfer Mode - that is, we allow
    // direct transfers between parties even in NON-ACTIVE state.
    bool specialTransferModeEnabled;


    // ------- Slot ------- //
    
    // Per-Transaction Pseudo-Random hash value (transferHashValue).
    // This value is computed on every token transfer, by keccak'ing
    // the last (current) transferHashValue, msg.sender, block.timestamp, and 
    // transaction count.
    //
    // This is used on Finishing Stage, as a pseudo-random number,
    // which is used to check if we should end the lottery (move to
    // Ending Stage).
    uint256 transferHashValue;

    // ------- Slot ------- //

    // On lottery end, get & store the lottery total ETH return
    // (including initial funds), and profit amount.
    uint128 public ending_totalReturn;
    uint128 public ending_profitAmount;

    // ------- Slot ------- //

    // The mapping that contains TRUE for addresses that already claimed
    // their lottery winner prizes.
    // Used only in COMPLETION, on claimWinnerPrize(), to check if
    // msg.sender has already claimed his prize.
    mapping( address => bool ) public prizeClaimersAddresses;


    // ============= Private/internal functions ============= //


    // Pool Only modifier.
    modifier poolOnly {
        require( msg.sender == poolAddress/*,
                 "Function can be called only by the pool!" */);
        _;
    }

    // Only randomness provider allowed modifier.
    modifier randomnessProviderOnly {
        require( msg.sender == randomnessProvider/*,
                 "Function can be called only by the UniLottery"
                 " Randomness Provider!" */);
        _;
    }

    // Execute function only on specific lottery stage.
    modifier onlyOnStage( STAGE _stage ) 
    {
        require( lotteryStage == uint8( _stage )/*,
                 "Function cannot be called on current stage!" */);
        _;
    }

    // Modifier for protecting the function from re-entrant calls,
    // by using a locked Re-Entrancy Lock (Mutex).
    modifier mutexLOCKED
    {
        require( ! reEntrancyMutexLocked/*, 
                    "Re-Entrant Calls are NOT ALLOWED!" */);

        reEntrancyMutexLocked = true;
        _;
        reEntrancyMutexLocked = false;
    }


    // Check if we're currently on a specific stage.
    function onStage( STAGE _stage )
                                                internal view
    returns( bool )
    {
        return ( lotteryStage == uint8( _stage ) );
    }


    /**
     *  Check if token transfer to specific wallet won't exceed 
     *  maximum token amount allowed to own by a single wallet.
     *
     *  @return true, if holder's balance with "amount" added,
     *      would exceed the max allowed single holder's balance
     *      (by default, that is 5% of total supply).
     */
    function transferExceedsMaxBalance( 
            address holder, uint amount )
                                                internal view
    returns( bool )
    {
        uint maxAllowedBalance = 
            ( totalSupply() * cfg.maxAmountForWallet_percentageOfSupply ) /
            ( _100PERCENT );

        return ( ( balanceOf( holder ) + amount ) > maxAllowedBalance );
    }


    /**
     *  Update holder data.
     *  This function is called by _transfer() function, just before
     *  transfering final amount of tokens directly from sender to
     *  receiver.
     *  At this point, all burns/mints have been done, and we're sure
     *  that this transfer is valid and must be successful.
     *
     *  In all modes, this function is used to update the holder array.
     *
     *  However, on external exchange modes (e.g. on Uniswap mode),
     *  it is also used to track buy/sell ether value, to update holder
     *  scores, when token buys/sells cannot be tracked directly.
     *
     *  If, however, we use Standalone mode, we are the exchange,
     *  so on _transfer() we already know the ether value, which is
     *  set to currentBuySellEtherValue variable.
     *
     *  @param amountSent - the token amount that is deducted from
     *      sender's balance. This includes burn, and owner fee.
     *
     *  @param amountReceived - the token amount that receiver 
     *      actually receives, after burns and fees.
     *
     *  @return holderCountChanged - indicates whether holder count
     *      changes during this transfer - new holder joins or leaves
     *      (true), or no change occurs (false).
     */
    function updateHolderData_preTransfer(
            address sender,
            address receiver,
            uint256 amountSent,
            uint256 amountReceived )
                                                internal
    returns( bool holderCountChanged )
    {
        // Update holder array, if new token holder joined, or if
        // a holder transfered his whole balance.
        holderCountChanged = false;

        // Sender transferred his whole balance - no longer a holder.
        if( balanceOf( sender ) == amountSent ) 
        {
            lotStorage.removeHolder( sender );
            holderCountChanged = true;
        }

        // Receiver didn't have any tokens before - add it to holders.
        if( balanceOf( receiver ) == 0 && amountReceived > 0 )
        {
            lotStorage.addHolder( receiver );
            holderCountChanged = true;
        }

        // Update holder score factors: if buy/sell occured, update
        // etherContributed and timeFactors scores,
        // and also propagate the scores through the referral chain
        // to the parent referrers (this is done in Storage contract).

        // This lottery operates only on external exchange (Uniswap)
        // mode, so we have to find out the buy/sell Ether value by 
        // calling the external exchange (Uniswap pair) contract.

        // Temporary variable to store current transfer's buy/sell
        // value in Ethers.
        int buySellValue;

        // Sender is an exchange - buy detected.
        if( sender == exchangeAddress && receiver != exchangeAddress ) 
        {
            // Use the Router's functionality.
            // Set the exchange path to WETH -> ULT
            // (ULT is Lottery Token, and it's address is our address).
            address[] memory path = new address[]( 2 );
            path[ 0 ] = WETHaddress;
            path[ 1 ] = address(this);

            uint[] memory ethAmountIn = uniswapRouter.getAmountsIn(
                amountSent,     // uint amountOut, 
                path            // address[] path
            );

            buySellValue = int( ethAmountIn[ 0 ] );
            
            // Compute time factor value for the current ether value.
            // buySellValue is POSITIVE.
            // When computing Time Factors, leave only 2 ether decimals.
            int timeFactorValue = ( buySellValue / (1 ether / 100) ) * 
                int( (block.timestamp - startDate) / cfg.timeFactorDivisor );

            if( timeFactorValue == 0 )
                timeFactorValue = 1;

            // Update and propagate the buyer (receiver) scores.
            lotStorage.updateAndPropagateScoreChanges(
                    receiver,
                    int80( buySellValue ),
                    int80( timeFactorValue ),
                    int80( amountReceived ) );
        }

        // Receiver is an exchange - sell detected.
        else if( sender != exchangeAddress && receiver == exchangeAddress )
        {
            // Use the Router's functionality.
            // Set the exchange path to ULT -> WETH
            // (ULT is Lottery Token, and it's address is our address).
            address[] memory path = new address[]( 2 );
            path[ 0 ] = address(this);
            path[ 1 ] = WETHaddress;

            uint[] memory ethAmountOut = uniswapRouter.getAmountsOut(
                amountReceived,     // uint amountIn
                path                // address[] path
            );

            // It's a sell (ULT -> WETH), so set value to NEGATIVE.
            buySellValue = int( -1 ) * int( ethAmountOut[ 1 ] );
            
            // Compute time factor value for the current ether value.
            // buySellValue is NEGATIVE.
            int timeFactorValue = ( buySellValue / (1 ether / 100) ) * 
                int( (block.timestamp - startDate) / cfg.timeFactorDivisor );

            if( timeFactorValue == 0 )
                timeFactorValue = -1;

            // Update and propagate the seller (sender) scores.
            lotStorage.updateAndPropagateScoreChanges(
                    sender,
                    int80( buySellValue ),
                    int80( timeFactorValue ),
                    -1 * int80( amountSent ) );
        }

        // Neither Sender nor Receiver are exchanges - default transfer.
        // Tokens just got transfered between wallets, without 
        // exchanging for ETH - so etherContributed_change = 0. 
        // On this case, update both sender's & receiver's scores.
        //
        else {
            buySellValue = 0;

            lotStorage.updateAndPropagateScoreChanges( sender, 0, 0, 
                                            -1 * int80( amountSent ) );

            lotStorage.updateAndPropagateScoreChanges( receiver, 0, 0, 
                                            int80( amountReceived ) );
        }

        // Check if lottery liquidity pool funds have already
        // reached a minimum required ETH value.
        uint ethFunds = getCurrentEthFunds();

        if( !fundGainRequirementReached &&
            ethFunds >= cfg.fundRequirement_denySells )
        {
            fundGainRequirementReached = true;
        }

        // Check whether this token transfer is allowed if it's a sell
        // (if buySellValue is negative):
        //
        // If we've already reached the minimum fund gain requirement,
        // and this sell would shrink lottery liquidity pool's ETH funds
        // below this requirement, then deny this sell, causing this 
        // transaction to fail.

        if( fundGainRequirementReached &&
            buySellValue < 0 &&
            ( uint( -1 * buySellValue ) >= ethFunds ||
              ethFunds - uint( -1 * buySellValue ) < 
                cfg.fundRequirement_denySells ) )
        {
            require( false/*, "This sell would drop the lottery ETH funds"
                            "below the minimum requirement threshold!" */);
        }
    }
    
    
    /**
     *  Check for finishing stage start conditions.
     *  - If some conditions are met, start finishing stage!
     *    Do it by setting "onFinishingStage" bool.
     *  - If we're currently on finishing stage, and some condition
     *    is no longer met, then stop the finishing stage.
     */
    function checkFinishingStageConditions()
                                                    internal
    {
        // Firstly, check if lottery hasn't exceeded it's maximum lifetime.
        // If so, don't check anymore, just set finishing stage, and
        // end the lottery on further call of checkForEnding().
        if( (block.timestamp - startDate) > cfg.maxLifetime ) 
        {
            lotteryStage = uint8( STAGE.FINISHING );
            return;
        }

        // Compute & check the finishing criteria.

        // Notice that we adjust the config-specified fund gain
        // percentage increase to uint-mode, by adding 100 percents,
        // because we don't deal with negative percentages, and here
        // we represent loss as a percentage below 100%, and gains
        // as percentage above 100%.
        // So, if in regular gains notation, it's said 10% gain,
        // in uint mode, it's said 110% relative increase.
        //
        // (Also, remember that losses are impossible in our lottery
        //  working scheme).

        if( lotStorage.getHolderCount() >= cfg.finishCriteria_minNumberOfHolders
            &&
            getCurrentEthFunds() >= cfg.finishCriteria_minFunds
            &&
            (block.timestamp - startDate) >= cfg.finishCriteria_minTimeActive )
        {
            if( onStage( STAGE.ACTIVE ) )
            {
                // All conditions are met - start the finishing stage.
                lotteryStage = uint8( STAGE.FINISHING );

                emit FinishingStageStarted();
            }
        }

        else if( onStage( STAGE.FINISHING ) )
        {
            // However, what if some condition was not met, but we're
            // already on the finishing stage?
            // If so, we must stop the finishing stage.
            // But what to do with the finishing probability?
            // Config specifies if it should be reset or maintain it's
            // value until the next time finishing stage is started.

            lotteryStage = uint8( STAGE.ACTIVE );

            if( cfg.finish_resetProbabilityOnStop )
                finishProbablity = cfg.finish_initialProbability;

            emit FinishingStageStopped();
        }
    }


    /**
     *  We're currently on finishing stage - so let's check if
     *  we should end the lottery block.timestamp!
     *
     *  This function is called from _transfer(), only if we're sure
     *  that we're currently on finishing stage (onFinishingStage
     *  variable is set).
     *
     *  Here, we compute the pseudo-random number from hash of
     *  current message's sender, block.timestamp, and other values,
     *  and modulo it to the current finish probability.
     *  If it's equal to 1, then we end the lottery!
     *
     *  Also, here we update the finish probability according to
     *  probability update criteria - holder count, and tx count.
     *
     *  @param holderCountChanged - indicates whether Holder Count
     *      has changed during this transfer (new holder joined, or
     *      a holder sold all his tokens).
     */
    function checkForEnding( bool holderCountChanged )
                                                            internal
    {
        // At first, check if lottery max lifetime is exceeded.
        // If so, start ending procedures right block.timestamp.
        if( (block.timestamp - startDate) > cfg.maxLifetime )
        {
            startEndingStage();
            return;
        }

        // Now, we know that lottery lifetime is still OK, and we're
        // currently on Finishing Stage (because this function is
        // called only when onFinishingStage is set).
        //
        // Now, check if we should End the lottery, by computing
        // a modulo on a pseudo-random number, which is a transfer
        // hash, computed for every transfer on _transfer() function.
        //
        // Get the modulo amount according to current finish 
        // probability.
        // We use precision of 0.01% - notice the "10000 *" before
        // 100 PERCENT.
        // Later, when modulo'ing, we'll check if value is below 10000.
        //
        uint prec = 10000;
        uint modAmount = (prec * _100PERCENT) / finishProbablity;

        if( ( transferHashValue % modAmount ) <= prec )
        {
            // Finish probability is met! Commence lottery end - 
            // start Ending Stage.
            startEndingStage();
            return;
        }

        // Finish probability wasn't met.
        // Update the finish probability, by increasing it!

        // Transaction count criteria.
        // As we know that this function is called on every new 
        // transfer (transaction), we don't check if transactionCount
        // increased or not - we just perform probability update.

        finishProbablity += cfg.finish_probabilityIncreaseStep_transaction;

        // Now, perform holder count criteria update.
        // Finish probability increases, no matter if holder count
        // increases or decreases.
        if( holderCountChanged )
            finishProbablity += cfg.finish_probabilityIncreaseStep_holder;
    }


    /**
     *  Start the Ending Stage, by De-Activating the lottery,
     *  to deny all further token transfers (excluding the one when
     *  removing liquidity from Uniswap), and transition into the 
     *  Mining Phase - set the lotteryStage to MINING.
     */
    function startEndingStage()
                                                internal
    {
        lotteryStage = uint8( STAGE.ENDING_MINING );
    }


    /**
     *  Execute the first step of the Mining Stage - request a 
     *  Random Seed from the Randomness Provider.
     *
     *  Here, we call the Randomness Provider, asking for a true random seed
     *  to be passed to us into our callback, named 
     *  "finish_randomnessProviderCallback()".
     *
     *  When that callback will be called, our storage's random seed will
     *  be set, and we'll be able to start the Ending Algorithm on
     *  further mining steps.
     *
     *  Notice that Randomness Provider must already be funded, to
     *  have enough Ether for Provable fee and the gas costs of our
     *  callback function, which are quite high, because of winner
     *  selection algorithm, which is computationally expensive.
     *
     *  The Randomness Provider is always funded by the Pool,
     *  right before the Pool deploys and starts a new lottery, so
     *  as every lottery calls the Randomness Provider only once,
     *  the one-call-fund method for every lottery is sufficient.
     *
     *  Also notice, that Randomness Provider might fail to call
     *  our callback due to some unknown reasons!
     *  Then, the lottery profits could stay locked in this 
     *  lottery contract forever ?!!
     *
     *  No! We've thought about that - we've implemented the
     *  Alternative Ending mechanism, where, if specific time passes 
     *  after we've made a request to Randomness Provider, and
     *  callback hasn't been called yet, we allow external actor to
     *  execute the Alternative ending, which basically does the
     *  same things as the default ending, just that the Random Seed
     *  will be computed locally in our contract, using the
     *  Pseudo-Random mechanism, which could compute a reasonably
     *  fair and safe value using data from holder array, and other
     *  values, described in more detail on corresponding function's
     *  description.
     */
    function mine_requestRandomSeed()
                                                internal
    {
        // We're sure that the Randomness Provider has enough funds.
        // Execute the random request, and get ready for Ending Algorithm.

        IRandomnessProvider( randomnessProvider )
            .requestRandomSeedForLotteryFinish();

        // Store the time when random seed has been requested, to
        // be able to alternatively handle the lottery finish, if
        // randomness provider doesn't call our callback for some
        // reason.
        finish_timeRandomSeedRequested = uint32( block.timestamp );

        // Emit appropriate events.
        emit RandomnessProviderCalled();
    }


    /** PAYABLE [ OUT ] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
     *
     *  Transfer the Owner & Pool profit shares, when lottery ends.
     *  This function is the first one that's executed on the Mining
     *  Stage.
     *  This is the first step of Mining. So, the Miner who executes this
     *  function gets the mining reward.
     *
     *  This function's job is to Gather the Profits & Initial Funds,
     *  and Transfer them to Profiters - that is, to The Pool, and
     *  to The Owner.
     *
     *  The Miners' profit share and Winner Prize Fund stay in this
     *  contract.
     *
     *  On this function, we (in this order):
     *
     *  1. Remove all liquidity from Uniswap (if using Uniswap Mode),
     *      pulling it to our contract's wallet.
     *
     *  2. Transfer the Owner and the Pool ETH profit shares to
     *      Owner and Pool addresses.
     *
     *  * This function transfers Ether out of our contract:
     *      - We transfer the Profits to Pool and Owner addresses.
     */
    function mine_removeUniswapLiquidityAndTransferProfits()
                                                                internal
                                                                mutexLOCKED
    {
        // We've already approved our token allowance to Router.
        // Now, approve Uniswap liquidity token's Router allowance.
        ERC20( exchangeAddress ).approve( address(uniswapRouter), uint(-1) );

        // Enable the SPECIAL-TRANSFER mode, to allow Uniswap to transfer
        // the tokens from Pair to Router, and then from Router to us.
        specialTransferModeEnabled = true;

        // Remove liquidity!
        uint amountETH = uniswapRouter
            .removeLiquidityETHSupportingFeeOnTransferTokens(
                address(this),          // address token,
                ERC20( exchangeAddress ).balanceOf( address(this) ),
                0,                      // uint amountTokenMin,
                0,                      // uint amountETHMin,
                address(this),          // address to,
                (block.timestamp + 10000000)        // uint deadline
            );

        // Tokens are transfered. Disable the special transfer mode.
        specialTransferModeEnabled = false;

        // Check that we've got a correct amount of ETH.
        require( address(this).balance >= amountETH &&
                 address(this).balance >= cfg.initialFunds/*,
                 "Incorrect amount of ETH received from Uniswap!" */);


        // Compute the Profit Amount (current balance - initial funds).
        ending_totalReturn = uint128( address(this).balance );
        ending_profitAmount = ending_totalReturn - uint128( cfg.initialFunds );

        // Compute, and Transfer Owner's profit share and 
        // Pool's profit share to their respective addresses.

        uint poolShare = ( ending_profitAmount * cfg.poolProfitShare ) /
                         ( _100PERCENT );

        uint ownerShare = ( ending_profitAmount * cfg.ownerProfitShare ) /
                          ( _100PERCENT );

        // To pool, transfer it's profit share plus initial funds.
        IUniLotteryPool( poolAddress ).lotteryFinish
            { value: poolShare + cfg.initialFunds }
            ( ending_totalReturn, ending_profitAmount );

        // Transfer Owner's profit share.
        OWNER_ADDRESS.transfer( ownerShare );

        // Emit ending event.
        emit LotteryEnd( ending_totalReturn, ending_profitAmount );
    }


    /**
     *  Executes a single step of the Winner Selection Algorithm
     *  (the Ending Algorithm).
     *  The algorithm itself is being executed in the Storage contract.
     *
     *  On current design, whole algorithm is executed in a single step.
     *
     *  This function is executed only in the Mining stage, and
     *  accounts for most of the gas spent during mining.
     */
    function mine_executeEndingAlgorithmStep()
                                                            internal
    {
        // Launch the winner algorithm, to execute the next step.
        lotStorage.executeWinnerSelectionAlgorithm();
    }



    // =============== Public functions =============== //


    /**
     *  Constructor of this delegate code contract.
     *  Here, we set OUR STORAGE's lotteryStage to DISABLED, because
     *  we don't want anybody to call this contract directly.
     */
    constructor()
    {
        lotteryStage = uint8( STAGE.DISABLED );
    }


    /**
     *  Construct the lottery contract which is delegating it's
     *  call to us.
     *
     *  @param config - LotteryConfig structure to use in this lottery.
     *
     *      Future approach: ABI-encoded Lottery Config 
     *      (different implementations might use different config 
     *      structures, which are ABI-decoded inside the implementation).
     *
     *      Also, this "config" includes the ABI-encoded temporary values, 
     *      which are not part of persisted LotteryConfig, but should
     *      be used only in constructor - for example, values to be
     *      assigned to storage variables, such as ERC20 token's
     *      name, symbol, and decimals.
     *
     *  @param _poolAddress - Address of the Main UniLottery Pool, which
     *      provides initial funds, and receives it's profit share.
     *
     *  @param _randomProviderAddress - Address of a Randomness Provider,
     *      to use for obtaining random seeds.
     *
     *  @param _storageAddress  - Address of a Lottery Storage.
     *      Storage contract is a separate contract which holds all 
     *      lottery token holder data, such as intermediate scores.
     *
     */
    function construct( 
            LotteryConfig memory config,
            address payable _poolAddress,
            address _randomProviderAddress,
            address _storageAddress )
                                                        external
    {
        // Check if contract wasn't already constructed!
        require( poolAddress == address( 0 )/*,
                 "Contract is already constructed!" */);

        // Set the Pool's Address - notice that it's not the
        // msg.sender, because lotteries aren't created directly
        // by the Pool, but by the Lottery Factory!
        poolAddress = _poolAddress;

        // Set the Randomness Provider address.
        randomnessProvider = _randomProviderAddress;


        // Check the minimum & maximum requirements for config
        // profit & lifetime parameters.

        require( config.maxLifetime <= MAX_LOTTERY_LIFETIME/*,
                 "Lottery maximum lifetime is too high!" */);

        require( config.poolProfitShare >= MIN_POOL_PROFITS &&
                 config.poolProfitShare <= MAX_POOL_PROFITS/*,
                 "Pool profit share is invalid!" */);

        require( config.ownerProfitShare >= MIN_OWNER_PROFITS &&
                 config.ownerProfitShare <= MAX_OWNER_PROFITS/*,
                 "Owner profit share is invalid!" */);

        require( config.minerProfitShare >= MIN_MINER_PROFITS &&
                 config.minerProfitShare <= MAX_MINER_PROFITS/*,
                 "Miner profit share is invalid!" */);

        // Check if time factor divisor is higher than 2 minutes.
        // That's because int40 wouldn't be able to handle precisions
        // of smaller time factor divisors.
        require( config.timeFactorDivisor >= 2 minutes /*,
                 "Time factor divisor is lower than 2 minutes!"*/ );

        // Check if winner profit share is good.
        uint32 totalWinnerShare = 
            (_100PERCENT) - config.poolProfitShare
                            - config.ownerProfitShare
                            - config.minerProfitShare;

        require( totalWinnerShare >= MIN_WINNER_PROFIT_SHARE/*,
                 "Winner profit share is too low!" */);

        // Check if ending algorithm params are good.
        require( config.randRatio_scorePart != 0    &&
                 config.randRatio_randPart  != 0    &&
                 ( config.randRatio_scorePart + 
                   config.randRatio_randPart    ) < 10000/*,
                 "Random Ratio params are invalid!" */);

        require( config.endingAlgoType == 
                    uint8( EndingAlgoType.MinedWinnerSelection ) ||
                 config.endingAlgoType == 
                    uint8( EndingAlgoType.WinnerSelfValidation ) ||
                 config.endingAlgoType == 
                    uint8( EndingAlgoType.RolledRandomness )/*,
                 "Wrong Ending Algorithm Type!" */);

        // Set the number of winners (winner count).
        // If using Computed Sequence winner prize shares, set that
        // value, and if it's zero, then we're using the Array-Mode
        // prize share specification.
        if( config.prizeSequence_winnerCount == 0 &&
            config.winnerProfitShares.length != 0 )
            config.prizeSequence_winnerCount = 
                uint16( config.winnerProfitShares.length );


        // Setup our Lottery Storage - initialize, and set the
        // Algorithm Config.

        LotteryStorage _lotStorage = LotteryStorage( _storageAddress );

        // Setup a Winner Score Config for the winner selection algo,
        // to be used in the Lottery Storage.
        LotteryStorage.WinnerAlgorithmConfig memory winnerConfig;

        // Algorithm type.
        winnerConfig.endingAlgoType = config.endingAlgoType;

        // Individual player max score parts.
        winnerConfig.maxPlayerScore_etherContributed =
            config.maxPlayerScore_etherContributed;

        winnerConfig.maxPlayerScore_tokenHoldingAmount =
            config.maxPlayerScore_tokenHoldingAmount;

        winnerConfig.maxPlayerScore_timeFactor =
            config.maxPlayerScore_timeFactor;

        winnerConfig.maxPlayerScore_refferalBonus =
            config.maxPlayerScore_refferalBonus;

        // Score-To-Random ratio parts.
        winnerConfig.randRatio_scorePart = config.randRatio_scorePart;
        winnerConfig.randRatio_randPart = config.randRatio_randPart;

        // Set winner count (no.of winners).
        winnerConfig.winnerCount = config.prizeSequence_winnerCount;


        // Initialize the storage (bind it to our contract).
        _lotStorage.initialize( winnerConfig );

        // Set our immutable variable.
        lotStorage = _lotStorage;


        // Now, set our config to the passed config.
        cfg = config;

        // Might be un-needed (can be replaced by Constant on the MainNet):
        WETHaddress = uniswapRouter.WETH();
    }


    /** PAYABLE [ IN  ] <<<<<<<<<<<<<<<<<<<<<<<<<<<<
     *
     *  Fallback Receive Ether function.
     *  Used to receive ETH funds back from Uniswap, on lottery's end,
     *  when removing liquidity.
     */
    receive()       external payable
    {
        emit FallbackEtherReceiver( msg.sender, msg.value );
    }



    /** PAYABLE [ IN  ] <<<<<<<<<<<<<<<<<<<<<<<<<<<<
     *  PAYABLE [ OUT ] >>>>>>>>>>>>>>>>>>>>>>>>>>>>
     *
     *  Initialization function.
     *  Here, the most important startup operations are made - 
     *  such as minting initial token supply and transfering it to
     *  the Uniswap liquidity pair, in exchange for UNI-v2 tokens.
     *
     *  This function is called by the pool, when transfering
     *  initial funds to this contract.
     *
     *  What's payable?
     *  - Pool transfers initial funds to our contract.
     *  - We transfer that initial fund Ether to Uniswap liquidity pair
     *    when creating/providing it.
     */
    function initialize()   
                                        external
                                        payable
                                        poolOnly
                                        mutexLOCKED
                                        onlyOnStage( STAGE.INITIAL )
    {
        // Check if pool transfered correct amount of funds.
        require( address( this ).balance == cfg.initialFunds/*,
                 "Invalid amount of funds transfered!" */);

        // Set start date.
        startDate = uint32( block.timestamp );

        // Set the initial transfer hash value.
        transferHashValue = uint( keccak256( 
                abi.encodePacked( msg.sender, block.timestamp ) ) );

        // Set initial finish probability, to be used when finishing
        // stage starts.
        finishProbablity = cfg.finish_initialProbability;
        
        
        // ===== Active operations - mint & distribute! ===== //

        // Mint full initial supply of tokens to our contract address!
        _mint( address(this), 
               uint( cfg.initialTokenSupply ) * (10 ** decimals) );

        // Now - prepare to create a new Uniswap Liquidity Pair,
        // with whole our total token supply and initial funds ETH
        // as the two liquidity reserves.
        
        // Approve Uniswap Router to allow it to spend our tokens.
        // Set maximum amount available.
        _approve( address(this), address( uniswapRouter ), uint(-1) );

        // Provide liquidity - the Router will automatically
        // create a new Pair.
        
        uniswapRouter.addLiquidityETH 
        { value: address(this).balance }
        (
            address(this),          // address token,
            totalSupply(),          // uint amountTokenDesired,
            totalSupply(),          // uint amountTokenMin,
            address(this).balance,  // uint amountETHMin,
            address(this),          // address to,
            (block.timestamp + 1000)            // uint deadline
        );

        // Get the Pair address - that will be the exchange address.
        exchangeAddress = IUniswapFactory( uniswapRouter.factory() )
            .getPair( WETHaddress, address(this) );

        // We assume that the token reserves of the pair are good,
        // and that we own the full amount of liquidity tokens.

        // Find out which of the pair tokens is WETH - is it the 
        // first or second one. Use it later, when getting our share.
        if( IUniswapPair( exchangeAddress ).token0() == WETHaddress )
            uniswap_ethFirst = true;
        else
            uniswap_ethFirst = false;


        // Move to ACTIVE lottery stage.
        // Now, all token transfers will be allowed.
        lotteryStage = uint8( STAGE.ACTIVE );

        // Lottery is initialized. We're ready to emit event.
        emit LotteryInitialized();
    }


    // Return this lottery's initial funds, as were specified in the config.
    //
    function getInitialFunds()          external view
    returns( uint )
    {
        return cfg.initialFunds;
    }

    // Return active (still not returned to pool) initial fund value.
    // If no-longer-active, return 0 (default) - because funds were 
    // already returned back to the pool.
    //
    function getActiveInitialFunds()    external view
    returns( uint )
    {
        if( onStage( STAGE.ACTIVE ) )
            return cfg.initialFunds;
        return 0;
    }


    /**
     *  Get current Exchange's Token and ETH reserves.
     *  We're on Uniswap mode, so get reserves from Uniswap.
     */
    function getReserves() 
                                                        external view
    returns( uint _ethReserve, uint _tokenReserve )
    {
        // Use data from Uniswap pair contract.
        ( uint112 res0, uint112 res1, ) = 
            IUniswapPair( exchangeAddress ).getReserves();

        if( uniswap_ethFirst )
            return ( res0, res1 );
        else
            return ( res1, res0 );
    }


    /**
     *  Get our share (ETH amount) of the Uniswap Pair ETH reserve,
     *  of our Lottery tokens ULT-WETH liquidity pair.
     */
    function getCurrentEthFunds()
                                                        public view
    returns( uint ethAmount )
    {
        IUniswapPair pair = IUniswapPair( exchangeAddress );
        
        ( uint112 res0, uint112 res1, ) = pair.getReserves();
        uint resEth = uint( uniswap_ethFirst ? res0 : res1 );

        // Compute our amount of the ETH reserve, based on our
        // percentage of our liquidity token balance to total supply.
        uint liqTokenPercentage = 
            ( pair.balanceOf( address(this) ) * (_100PERCENT) ) /
            ( pair.totalSupply() );

        // Compute and return the ETH reserve.
        return ( resEth * liqTokenPercentage ) / (_100PERCENT);
    }


    /**
     *  Get current finish probability.
     *  If it's ACTIVE stage, return 0 automatically.
     */
    function getFinishProbability()
                                                        external view
    returns( uint32 )
    {
        if( onStage( STAGE.FINISHING ) )
            return finishProbablity;
        return 0;
    }


    
    /**
     *  Generate a referral ID for msg.sender, who must be a token holder.
     *  Referral ID is used to refer other wallets into playing our
     *  lottery.
     *  - Referrer gets bonus points for every wallet that bought 
     *    lottery tokens and specified his referral ID.
     *  - Referrees (wallets who got referred by registering a valid
     *    referral ID, corresponding to some referrer), get some
     *    bonus points for specifying (registering) a referral ID.
     *
     *  Referral ID is a uint256 number, which is generated by
     *  keccak256'ing the holder's address, holder's current
     *  token ballance, and current time.
     */
    function generateReferralID()
                                        external
                                        onlyOnStage( STAGE.ACTIVE )
    {
        uint256 refID = lotStorage.generateReferralID( msg.sender );

        // Emit approppriate events.
        emit ReferralIDGenerated( msg.sender, refID );
    }


    /**
     *  Register a referral for a msg.sender (must be token holder),
     *  using a valid referral ID got from a referrer.
     *  This function is called by a referree, who obtained a
     *  valid referral ID from some referrer, who previously
     *  generated it using generateReferralID().
     *
     *  You can only register a referral once!
     *  When you do so, you get bonus referral points!
     */
    function registerReferral( 
            uint256 referralID )
                                        external
                                        onlyOnStage( STAGE.ACTIVE )
    {
        address referrer = lotStorage.registerReferral( 
                msg.sender,
                cfg.playerScore_referralRegisteringBonus,
                referralID );

        // Emit approppriate events.
        emit ReferralRegistered( msg.sender, referrer, referralID );
    }


    /**
     *  The most important function of this contract - Transfer Function.
     *
     *  Here, all token burning, intermediate score tracking, and 
     *  finish condition checking is performed, according to the 
     *  properties specified in config.
     */
    function _transfer( address sender,
                        address receiver,
                        uint256 amount )
                                            internal
                                            override
    {
        // Check if transfers are allowed in current state.
        // On Non-Active stage, transfers are allowed only from/to
        // our contract.
        // As we don't have Standalone Mode on this lottery variation,
        // that means that tokens to/from our contract are travelling
        // only when we transfer them to Uniswap Pair, and when
        // Uniswap transfers them back to us, on liquidity remove.
        //
        // On this state, we also don't perform any burns nor
        // holding trackings - just transfer and return.

        if( !onStage( STAGE.ACTIVE )    &&
            !onStage( STAGE.FINISHING ) &&
            ( sender == address(this) || receiver == address(this) ||
              specialTransferModeEnabled ) )
        {
            super._transfer( sender, receiver, amount );
            return;
        }

        // Now, we know that we're NOT on special mode.
        // Perform standard checks & brecks.
        require( ( onStage( STAGE.ACTIVE ) || 
                   onStage( STAGE.FINISHING ) )/*,
                 "Token transfers are only allowed on ACTIVE stage!" */);
                 
        // Can't transfer zero tokens, or use address(0) as sender.
        require( amount != 0 && sender != address(0)/*,
                 "Amount is zero, or transfering from zero address." */);


        // Compute the Burn Amount - if buying tokens from an exchange,
        // we use a lower burn rate - to incentivize buying!
        // Otherwise (if selling or just transfering between wallets),
        // we use a higher burn rate.
        uint burnAmount;

        // It's a buy - sender is an exchange.
        if( sender == exchangeAddress )
            burnAmount = ( amount * cfg.burn_buyerRate ) / (_100PERCENT);
        else
            burnAmount = ( amount * cfg.burn_defaultRate ) / (_100PERCENT);
        
        // Now, compute the final amount to be gotten by the receiver.
        uint finalAmount = amount - burnAmount;

        // Check if receiver's balance won't exceed the max-allowed!
        // Receiver must not be an exchange.
        if( receiver != exchangeAddress )
        {
            require( !transferExceedsMaxBalance( receiver, finalAmount )/*,
                "Receiver's balance would exceed maximum after transfer!"*/);
        }

        // Now, update holder data array accordingly.
        bool holderCountChanged = updateHolderData_preTransfer( 
                sender, 
                receiver, 
                amount,             // Amount Sent (Pre-Fees)
                finalAmount         // Amount Received (Post-Fees).
        );

        // All is ok - perform the burn and token transfers block.timestamp.

        // Burn token amount from sender's balance.
        super._burn( sender, burnAmount );

        // Finally, transfer the final amount from sender to receiver.
        super._transfer( sender, receiver, finalAmount );


        // Compute new Pseudo-Random transfer hash, which must be
        // computed for every transfer, and is used in the
        // Finishing Stage as a pseudo-random unique value for 
        // every transfer, by which we determine whether lottery
        // should end on this transfer.
        //
        // Compute it like this: keccak the last (current) 
        // transferHashValue, msg.sender, sender, receiver, amount.

        transferHashValue = uint( keccak256( abi.encodePacked(
            transferHashValue, msg.sender, sender, receiver, amount ) ) );


        // Check if we should be starting a finishing stage block.timestamp.
        checkFinishingStageConditions();

        // If we're on finishing stage, check for ending conditions.
        // If ending check is satisfied, the checkForEnding() function
        // starts ending operations.
        if( onStage( STAGE.FINISHING ) )
            checkForEnding( holderCountChanged );
    }


    /**
     *  Callback function, which is called from Randomness Provider,
     *  after it obtains a random seed to be passed to us, after
     *  we have initiated The Ending Stage, on which random seed
     *  is used to generate random factors for Winner Selection
     *  algorithm.
     */ 
    function finish_randomnessProviderCallback(
            uint256 randomSeed,
            uint256 /*callID*/ )
                                                external
                                                randomnessProviderOnly
    {
        // Set the random seed in the Storage Contract.
        lotStorage.setRandomSeed( randomSeed );

        // If algo-type is not Mined Winner Selection, then by block.timestamp
        // we assume lottery as COMPL3T3D.
        if( cfg.endingAlgoType != uint8(EndingAlgoType.MinedWinnerSelection) )
        {
            lotteryStage = uint8( STAGE.COMPLETION );
            completionDate = uint32( block.timestamp );
        }
    }


    /**
     *  Function checks if we can initiate Alternative Seed generation.
     *
     *  Alternative approach to Lottery Random Seed is used only when
     *  Randomness Provider doesn't work, and doesn't call the
     *  above callback.
     *
     *  This alternative approach can be initiated by Miners, when
     *  these conditions are met:
     *  - Lottery is on Ending (Mining) stage.
     *  - Request to Randomness Provider was made at least X time ago,
     *    and our callback hasn't been called yet.
     *
     *  If these conditions are met, we can initiate the Alternative
     *  Random Seed generation, which generates a seed based on our
     *  state.
     */
    function alternativeSeedGenerationPossible()
                                                        internal view
    returns( bool )
    {
        return ( onStage( STAGE.ENDING_MINING ) &&
                 ( (block.timestamp - finish_timeRandomSeedRequested) >
                   cfg.REQUIRED_TIME_WAITING_FOR_RANDOM_SEED ) );
    }


    /**
     *  Return this lottery's config, using ABIEncoderV2.
     */
    /*function getLotteryConfig()
                                                    external view
    returns( LotteryConfig memory ourConfig )
    {
        return cfg;
    }*/


    /**
     *  Checks if Mining is currently available.
     */
    function isMiningAvailable()
                                                    external view
    returns( bool )
    {
        return onStage( STAGE.ENDING_MINING ) && 
               ( miningStep == 0 || 
                 ( miningStep == 1 && 
                   ( lotStorage.getRandomSeed() != 0 ||
                     alternativeSeedGenerationPossible() )
                 ) );
    }


    /** PAYABLE [ OUT ] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
     *
     *  Mining function, to be executed on Ending (Mining) stage.
     *
     *  "Mining" approach is used in this lottery, to use external
     *  actors for executing the gas-expensive Ending Algorithm,
     *  and other ending operations, such as profit transfers.
     *
     *  "Miners" can be any external actors who call this function.
     *  When Miner successfully completes a Mining Step, he gets 
     *  a Mining Reward, which is a certain portion of lottery's profit
     *  share, dedicated to Miners.
     *
     *  NOT-IMPLEMENTED APPROACH:
     *
     *  All these operations are divided into "mining steps", which are
     *  smaller components, which fit into reasonable gas limits.
     *  All "steps" are designed to take up similar amount of gas.
     *
     *  For example, if total lottery profits (total ETH got from
     *  pulling liquidity out of Uniswap, minus initial funds),
     *  is 100 ETH, Miner Profit Share is 10%, and there are 5 mining
     *  steps total, then for a singe step executed, miner will get:
     *
     *  (100 * 0.1) / 5 = 2 ETH.
     *
     *  ---------------------------------
     *
     *  CURRENTLY IMPLEMENTED APPROACH:
     *
     *  As the above-defined approach would consume very much gas for
     *  inter-step intermediate state storage, we have thought that
     *  for block.timestamp, it's better to have only 2 mining steps, the second of
     *  which performs the whole Winner Selection Algorithm.
     *
     *  This is because performing the whole algorithm at once would save
     *  us up to 10x more gas in total, than executing it in steps.
     *
     *  However, this solution is not scalable, because algorithm has
     *  to fit into block gas limit (10,000,000 gas), so we are limited
     *  to a certain safe maximum number of token holders, which is
     *  empirically determined during testing, and defined in the
     *  MAX_SAFE_NUMBER_OF_HOLDERS constant, which is checked against the
     *  config value "finishCriteria_minNumberOfHolders" in constructor.
     *
     *  So, in this approach, there are only 2 mining steps:
     *
     *  1. Remove liquidity from Uniswap, transfer profit shares to
     *      the Pool and the Owner Address, and request Random Seed
     *      from the Randomness Provider.
     *      Reward: 25% of total Mining Rewards.
     *
     *  2. Perform the whole Winner Selection Algorithm inside the
     *      Lottery Storage contract.
     *      Reward: 75% of total Mining Rewards.
     *
     *  * Function transfers Ether out of our contract:
     *    - Transfers the current miner's reward to msg.sender.
     */
    function mine()
                                external
                                onlyOnStage( STAGE.ENDING_MINING )
    {
        uint currentStepReward;

        // Perform different operations on different mining steps.

        // Step 0:  Remove liquidity from Uniswap, transfer profits to
        //          Pool and Owner addresses. Also, request a Random Seed
        //          from the Randomness Provider.
        if( miningStep == 0 )
        {
            mine_requestRandomSeed();
            mine_removeUniswapLiquidityAndTransferProfits();

            // Compute total miner reward amount, then compute this 
            // step's reward later.
            uint totalMinerRewards = 
                ( ending_profitAmount * cfg.minerProfitShare ) / 
                ( _100PERCENT );

            // Step 0 reward is 10% for Algo type 1.
            if( cfg.endingAlgoType == uint8(EndingAlgoType.MinedWinnerSelection) )
            {
                currentStepReward = ( totalMinerRewards * (10 * PERCENT) ) /
                                    ( _100PERCENT );
            }
            // If other algo-types, second step is not normally needed,
            // so here we take 80% of miner rewards.
            // If Randomness Provider won't give us a seed after
            // specific amount of time, we'll initiate a second step,
            // with remaining 20% of miner rewords.
            else
            {
                currentStepReward = ( totalMinerRewards * (80 * PERCENT) ) /
                                    ( _100PERCENT );
            }

            require( currentStepReward <= totalMinerRewards/*, "BUG 1694" */);
        }

        // Step 1:
        //  If we use MinedWinnerSelection algo-type, then execute the 
        //  winner selection algorithm.
        //  Otherwise, check if Random Provider hasn't given us a
        //  random seed long enough, so that we have to generate a
        //  seed locally.
        else
        {
            // Check if we can go into this step when using specific
            // ending algorithm types.
            if( cfg.endingAlgoType != uint8(EndingAlgoType.MinedWinnerSelection) )
            {
                require( lotStorage.getRandomSeed() == 0 &&
                         alternativeSeedGenerationPossible()/*,
                         "Second Mining Step is not available for "
                         "current Algo-Type on these conditions!" */);
            }

            // Compute total miner reward amount, then compute this 
            // step's reward later.
            uint totalMinerRewards = 
                ( ending_profitAmount * cfg.minerProfitShare ) / 
                ( _100PERCENT );

            // Firstly, check if random seed is already obtained.
            // If not, check if we should generate it locally.
            if( lotStorage.getRandomSeed() == 0 )
            {
                if( alternativeSeedGenerationPossible() )
                {
                    // Set random seed inside the Storage Contract,
                    // but using our contract's transferHashValue as the
                    // random seed.
                    // We believe that this hash has enough randomness
                    // to be considered a fairly good random seed,
                    // because it has beed chain-computed for every
                    // token transfer that has occured in ACTIVE stage.
                    //
                    lotStorage.setRandomSeed( transferHashValue );

                    // If using Non-Mined algorithm types, reward for this
                    // step is 20% of miner funds.
                    if( cfg.endingAlgoType != 
                        uint8(EndingAlgoType.MinedWinnerSelection) )
                    {
                        currentStepReward = 
                            ( totalMinerRewards * (20 * PERCENT) ) /
                            ( _100PERCENT );
                    }
                }
                else
                {
                    // If alternative seed generation is not yet possible
                    // (not enough time passed since the rand.provider
                    // request was made), then mining is not available
                    // currently.
                    require( false/*, "Mining not yet available!" */);
                }
            }

            // Now, we know that  Random Seed is obtained.
            // If we use this algo-type, perform the actual
            // winner selection algorithm.
            if( cfg.endingAlgoType == uint8(EndingAlgoType.MinedWinnerSelection) )
            {
                mine_executeEndingAlgorithmStep();

                // Set the prize amount to SECOND STEP prize amount (90%).
                currentStepReward = ( totalMinerRewards * (90 * PERCENT) ) /
                                    ( _100PERCENT );
            }

            // Now we've completed both Mining Steps, it means MINING stage
            // is finally completed!
            // Transition to COMPLETION stage, and set lottery completion
            // time to NOW.

            lotteryStage = uint8( STAGE.COMPLETION );
            completionDate = uint32( block.timestamp );

            require( currentStepReward <= totalMinerRewards/*, "BUG 2007" */);
        }

        // Now, transfer the reward to miner!
        // Check for bugs too - if the computed amount doesn't exceed.

        // Increment the mining step - move to next step (if there is one).
        miningStep++;

        // Check & Lock the Re-Entrancy Lock for transfers.
        require( ! reEntrancyMutexLocked/*, "Re-Entrant call detected!" */);
        reEntrancyMutexLocked = true;

        // Finally, transfer the reward to message sender!
        msg.sender.transfer( currentStepReward );

        // UnLock ReEntrancy Lock.
        reEntrancyMutexLocked = false;
    }


    /**
     *  Function computes winner prize amount for winner at rank #N.
     *  Prerequisites: Must be called only on STAGE.COMPLETION stage,
     *  because we use the final profits amount here, and that value
     *  (ending_profitAmount) is known only on COMPLETION stage.
     *
     *  @param rankingPosition - ranking position of a winner.
     *  @return finalPrizeAmount - prize amount, in Wei, of this winner.
     */
    function getWinnerPrizeAmount(
            uint rankingPosition )
                                                        public view
    returns( uint finalPrizeAmount )
    {
        // Calculate total winner prize fund profit percentage & amount.
        uint winnerProfitPercentage = 
            (_100PERCENT) - cfg.poolProfitShare - 
            cfg.ownerProfitShare - cfg.minerProfitShare;

        uint totalPrizeAmount =
            ( ending_profitAmount * winnerProfitPercentage ) /
            ( _100PERCENT );


        // We compute the prize amounts differently for the algo-type
        // RolledRandomness, because distribution of these prizes is
        // non-deterministic - multiple holders could fall onto the
        // same ranking position, due to randomness of rolled score.
        //
        if( cfg.endingAlgoType == uint8(EndingAlgoType.RolledRandomness) )
        {
            // Here, we'll use Prize Sequence Factor approach differently.
            // We'll use the prizeSequenceFactor value not to compute
            // a geometric progression, but to compute an arithmetic
            // progression, where each ranking position will get a
            // prize equal to 
            // "totalPrizeAmount - rankingPosition * singleWinnerShare"
            //
            // singleWinnerShare is computed as a value corresponding
            // to single-winner's share of total prize amount.
            //
            // Using such an approach, winner at rank 0 would get a
            // prize equal to whole totalPrizeAmount, but, as the
            // scores are rolled using random factor, it's very unlikely
            // to get a such high score, so most likely such prize
            // won't ever be claimed, but it is a possibility.
            //
            // Most of the winners in this approach are likely to
            // roll scores in the middle, so would get prizes equal to
            // 1-10% of total prize funds.

            uint singleWinnerShare = totalPrizeAmount / 
                                     cfg.prizeSequence_winnerCount;

            return totalPrizeAmount - rankingPosition * singleWinnerShare;
        }

        // Now, we know that ending algorithm is normal (deterministic).
        // So, compute the prizes in a standard way.

        // If using Computed Sequence: loop for "rankingPosition"
        // iterations, while computing the prize shares.
        // If "rankingPosition" is larger than sequencedWinnerCount,
        // then compute the prize from sequence-leftover amount.
        if( cfg.prizeSequenceFactor != 0 )
        {
            require( rankingPosition < cfg.prizeSequence_winnerCount/*,
                     "Invalid ranking position!" */);

            // Leftover: If prizeSequenceFactor is 25%, it's 75%.
            uint leftoverPercentage = 
                (_100PERCENT) - cfg.prizeSequenceFactor;

            // Loop until the needed iteration.
            uint loopCount = ( 
                rankingPosition >= cfg.prizeSequence_sequencedWinnerCount ?
                cfg.prizeSequence_sequencedWinnerCount :
                rankingPosition
            );

            for( uint i = 0; i < loopCount; i++ )
            {
                totalPrizeAmount = 
                    ( totalPrizeAmount * leftoverPercentage ) /
                    ( _100PERCENT );
            }

            // Get end prize amount - sequenced, or leftover.
            // Leftover-mode.
            if( loopCount == cfg.prizeSequence_sequencedWinnerCount &&
                cfg.prizeSequence_winnerCount > 
                cfg.prizeSequence_sequencedWinnerCount )
            {
                // Now, totalPrizeAmount equals all leftover-group winner
                // prize funds.
                // So, just divide it by number of leftover winners.
                finalPrizeAmount = 
                    ( totalPrizeAmount ) /
                    ( cfg.prizeSequence_winnerCount -
                      cfg.prizeSequence_sequencedWinnerCount );
            }
            // Sequenced-mode
            else
            {
                finalPrizeAmount = 
                    ( totalPrizeAmount * cfg.prizeSequenceFactor ) /
                    ( _100PERCENT );
            }
        }

        // Else, if we're using Pre-Specified Array of winner profit
        // shares, just get the share at the corresponding index.
        else
        {
            require( rankingPosition < cfg.winnerProfitShares.length );

            finalPrizeAmount = 
                ( totalPrizeAmount *
                  cfg.winnerProfitShares[ rankingPosition ] ) /
                ( _100PERCENT );
        }
    }


    /**
     *  After lottery has completed, this function returns if msg.sender
     *  is one of lottery winners, and the position in winner rankings.
     *  
     *  Function must be used to obtain the ranking position before
     *  calling claimWinnerPrize().
     *
     *  @param addr - address whose status to check.
     */
    function getWinnerStatus( address addr )
                                                        external view
    returns( bool isWinner, uint32 rankingPosition, 
             uint prizeAmount )
    {
        if( !onStage( STAGE.COMPLETION ) || balanceOf( addr ) == 0 )
            return (false , 0, 0);

        ( isWinner, rankingPosition ) =
            lotStorage.getWinnerStatus( addr );

        if( isWinner )
        {
            prizeAmount = getWinnerPrizeAmount( rankingPosition );
            if( prizeAmount > address(this).balance )
                prizeAmount = address(this).balance;
        }
    }


    /**
     *  Compute the intermediate Active Stage player score.
     *  This score is Player Score, not randomized.
     *  @param addr - address to check.
     */
    function getPlayerIntermediateScore( address addr )
                                                        external view
    returns( uint )
    {
        return lotStorage.getPlayerActiveStageScore( addr );
    }


    /** PAYABLE [ OUT ] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
     *
     *  Claim the winner prize of msg.sender, if he is one of the winners.
     *
     *  This function must be provided a ranking position of msg.sender,
     *  which must be obtained using the function above.
     *  
     *  The Lottery Storage then just checks if holder address in the
     *  winner array element at position rankingPosition is the same
     *  as msg.sender's.
     *
     *  If so, then claim request is valid, and we can give the appropriate
     *  prize to that winner.
     *  Prize can be determined by a computed factor-based sequence, or
     *  from the pre-specified winner array.
     *
     *  * This function transfers Ether out of our contract:
     *    - Sends the corresponding winner prize to the msg.sender.
     *
     *  @param rankingPosition - the position of Winner Array, that
     *      msg.sender says he is in (obtained using getWinnerStatus).
     */
    function claimWinnerPrize(
            uint32 rankingPosition )
                                    external
                                    onlyOnStage( STAGE.COMPLETION )
                                    mutexLOCKED
    {
        // Check if msg.sender hasn't already claimed his prize.
        require( ! prizeClaimersAddresses[ msg.sender ]/*,
                 "msg.sender has already claimed his prize!" */);

        // msg.sender must have at least some of UniLottery Tokens.
        require( balanceOf( msg.sender ) != 0/*,
                 "msg.sender's token balance can't be zero!" */);

        // Check if there are any prize funds left yet.
        require( address(this).balance != 0/*,
                 "All prize funds have already been claimed!" */);

        // If using Mined Selection Algo, check if msg.sender is 
        // really on that ranking position - algo was already executed.
        if( cfg.endingAlgoType == uint8(EndingAlgoType.MinedWinnerSelection) )
        {
            require( lotStorage.minedSelection_isAddressOnWinnerPosition(
                            msg.sender, rankingPosition )/*,
                     "msg.sender is not on specified winner position!" */);
        }
        // For other algorithms, get ranking position by executing
        // a specific algorithm of that algo-type.
        else
        {
            bool isWinner;
            ( isWinner, rankingPosition ) =
                lotStorage.getWinnerStatus( msg.sender );

            require( isWinner/*, "msg.sender is not a winner!" */);
        }

        // Compute the prize amount, using our internal function.
        uint finalPrizeAmount = getWinnerPrizeAmount( rankingPosition ); 

        // If prize is small and computation precision errors occured,
        // leading it to be larger than our balance, fix it.
        if( finalPrizeAmount > address(this).balance )
            finalPrizeAmount = address(this).balance;


        // Transfer the Winning Prize to msg.sender!
        msg.sender.transfer( finalPrizeAmount );


        // Mark msg.sender as already claimed his prize.
        prizeClaimersAddresses[ msg.sender ] = true;
    }


    /** PAYABLE [ OUT ] >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
     *
     *  Transfer the leftover Winner Prize Funds of this contract to the
     *  Main UniLottery Pool, if prize claim deadline has been exceeded.
     *
     *  Function can only be called from the Main Pool, and if some
     *  winners haven't managed to claim their prizes on time, their
     *  prizes will go back to UniLottery Pool.
     *
     *  * Function transfers Ether out of our contract:
     *    - Transfer the leftover funds to the Pool (msg.sender).
     */
    function getUnclaimedPrizes()
                                        external
                                        poolOnly
                                        onlyOnStage( STAGE.COMPLETION )
                                        mutexLOCKED
    {
        // Check if prize claim deadline has passed.
        require( completionDate != 0 &&
                 ( block.timestamp - completionDate ) > cfg.prizeClaimTime/*,
                 "Prize claim deadline not reached yet!" */);

        // Just transfer it all to the Pool.
        poolAddress.transfer( address(this).balance );
    }

}
/**
 *  Simple, gas-efficient lottery, which uses Uniswap as liquidity provider,
 *  and determines the lottery winners through a 3 different approaches
 *  (explained in detail on EndingAlgoType documentation).
 *
 *  This contract contains all code of the lottery contract, and
 *  lotteries themselves are just storage-container stubs, which use
 *  DelegateCall mechanism to execute this actual lottery code on
 *  their behalf.
 *
 *  Lottery workflow consists of these consecutive stages:
 *
 *  1. Initialization stage: Main Pool deploys this lottery contract,
 *      and calls initialize() with initial Ether funds provided.
 *      Lottery mints initial token supply and provides the liquidity
 *      to Uniswap, with whole token supply and initial Ether funds.
 *      Then, lottery becomes Active - trading tokens becomes allowed.
 *
 *  2. Active Stage:    Token transfers occurs on this stage, and
 *      finish probability is Zero. Our ETH funds in Uniswap increases
 *      in this stage.
 *      When certain criteria of holder count and fund gains are met,
 *      the Finishing stage begins.
 *
 *  3. Finishing Stage:     It can be considered a second part of
 *      an Active stage, because all token transfers and Uniswap trading
 *      are still allowed and occur actively.
 *      However, on this stage, for every transfer, a pseudo-random
 *      number is rolled, and if that rolled number is below a specific
 *      threshold, lottery de-activates, and Ending stage begins, when
 *      token transfers are denied.
 *      The threshold is determined by Finish Probability, which
 *      increases on every transfer on this stage.
 *
 *      However, notice that if Finishing Criteria (holder count and
 *      fund gains) are no-longer met, Finishing Stage pauses, and
 *      we get back to Active Stage.
 *
 *  4. Ending-Mining Stage - Step One:
 *      On this stage, we Remove our contract's liquidity share
 *      from Uniswap, then transfer the profits to the Pool and
 *      the Owner addresses.
 *
 *      Then, we call the Randomness Provider, requesting the Random Seed,
 *      which later should be passed to us by calling our callback
 *      (finish_randomnessProviderCallback).
 *
 *      Miner, who completes this step, gets portion of Mining Rewards,
 *      which are a dedicated profit share to miners.
 *
 *  5. Ending-Mining Stage - Step Two:  On this stage, if  *
 *      However, if Randomness Provider hasn't given us a seed after
 *      specific amount of time, on this step, before starting the
 *      Winner Selection Algorithm, an Alternative Seed Generation
 *      is performed, where the pseudo-random seed is generated based
 *      on data in our and Storage contracts (transfer hash, etc.).
 *
 *      If we're using MinedWinnerSelection ending algorithm type, then
 *      on this step the miner performs the gas-intensive Winner Selection
 *      Algorithm, which involves complex score calculations in a loop, and
 *      then sorting the selected winners array.
 *
 *      Miner who successfully completes this step, gets a portion of
 *      the Mining Rewards.
 *
 *  6. Completion Stage (Winner Prize Claiming stage):  On this stage,
 *      the Lottery Winners can finally claim their Lottery Prizes,
 *      by calling a prize claim function on our contract.
 *
 *      If we're using WinnerSelfValidation ending algorithm, winner
 *      computes and validates his final score on this function by
 *      himself, so the prize claim transaction can be gas-costly.
 *
 *      However, is RolledRandomness or MinedWinnerSelection algorithms
 *      are used, this function is cheap in terms of gas.
 *
 *      However, if some of winners fail to claim their prizes after
 *      a specific amount of time (specified in config), then those
 *      prizes can then be claimed by Lottery Main Pool too.
 */
function updateHolderData_preTransfer(
        address sender,
        address receiver,
        uint256 amountSent,
        uint256 amountReceived )
                                            internal
returns( bool holderCountChanged )
{
    // Update holder array, if new token holder joined, or if
    // a holder transfered his whole balance.
    holderCountChanged = false;

    // Sender transferred his whole balance - no longer a holder.
    if( balanceOf( sender ) == amountSent ) 
    {
        lotStorage.removeHolder( sender );
        holderCountChanged = true;
    }

    // Receiver didn't have any tokens before - add it to holders.
    if( balanceOf( receiver ) == 0 && amountReceived > 0 )
    {
        lotStorage.addHolder( receiver );
        holderCountChanged = true;
    }

    // Update holder score factors: if buy/sell occured, update
    // etherContributed and timeFactors scores,
    // and also propagate the scores through the referral chain
    // to the parent referrers (this is done in Storage contract).

    // This lottery operates only on external exchange (Uniswap)
    // mode, so we have to find out the buy/sell Ether value by 
    // calling the external exchange (Uniswap pair) contract.

    // Temporary variable to store current transfer's buy/sell
    // value in Ethers.
    int buySellValue;

    // Sender is an exchange - buy detected.
    if( sender == exchangeAddress && receiver != exchangeAddress ) 
    {
        // Use the Router's functionality.
        // Set the exchange path to WETH -> ULT
        // (ULT is Lottery Token, and it's address is our address).
        address[] memory path = new address[]( 2 );
        path[ 0 ] = WETHaddress;
        path[ 1 ] = address(this);

        uint[] memory ethAmountIn = uniswapRouter.getAmountsIn(
            amountSent,     // uint amountOut, 
            path            // address[] path
        );

        buySellValue = int( ethAmountIn[ 0 ] );
            
        // Compute time factor value for the current ether value.
        // buySellValue is POSITIVE.
        // When computing Time Factors, leave only 2 ether decimals.
        int timeFactorValue = ( buySellValue / (1 ether / 100) ) * 
            int( (block.timestamp - startDate) / cfg.timeFactorDivisor );

        if( timeFactorValue == 0 )
            timeFactorValue = 1;

        // Update and propagate the buyer (receiver) scores.
        lotStorage.updateAndPropagateScoreChanges(
                receiver,
                int80( buySellValue ),
                int80( timeFactorValue ),
                int80( amountReceived ) );
    }

    // Receiver is an exchange - sell detected.
    else if( sender != exchangeAddress && receiver == exchangeAddress )
    {
        // Use the Router's functionality.
        // Set the exchange path to ULT -> WETH
        // (ULT is Lottery Token, and it's address is our address).
        address[] memory path = new address[]( 2 );
        path[ 0 ] = address(this);
        path[ 1 ] = WETHaddress;

        uint[] memory ethAmountOut = uniswapRouter.getAmountsOut(
            amountReceived,     // uint amountIn
            path                // address[] path
        );

        // It's a sell (ULT -> WETH), so set value to NEGATIVE.
        buySellValue = int( -1 ) * int( ethAmountOut[ 1 ] );
            
        // Compute time factor value for the current ether value.
        // buySellValue is NEGATIVE.
        int timeFactorValue = ( buySellValue / (1 ether / 100) ) * 
            int( (block.timestamp - startDate) / cfg.timeFactorDivisor );

        if( timeFactorValue == 0 )
            timeFactorValue = -1;

        // Update and propagate the seller (sender) scores.
        lotStorage.updateAndPropagateScoreChanges(
                sender,
                int80( buySellValue ),
                int80( timeFactorValue ),
                -1 * int80( amountSent ) );
    }

    // Neither Sender nor Receiver are exchanges - default transfer.
    // Tokens just got transfered between wallets, without 
    // exchanging for ETH - so etherContributed_change = 0. 
    // On this case, update both sender's & receiver's scores.
    //
    else {
        buySellValue = 0;

        lotStorage.updateAndPropagateScoreChanges( sender, 0, 0, 
                                        -1 * int80( amountSent ) );

        lotStorage.updateAndPropagateScoreChanges( receiver, 0, 0, 
                                        int80( amountReceived ) );
    }

    // Check if lottery liquidity pool funds have already
    // reached a minimum required ETH value.
    uint ethFunds = getCurrentEthFunds();

    if( !fundGainRequirementReached &&
        ethFunds >= cfg.fundRequirement_denySells )
    {
        fundGainRequirementReached = true;
    }

    // Check whether this token transfer is allowed if it's a sell
    // (if buySellValue is negative):
    //
    // If we've already reached the minimum fund gain requirement,
    // and this sell would shrink lottery liquidity pool's ETH funds
    // below this requirement, then deny this sell, causing this 
    // transaction to fail.

    if( fundGainRequirementReached &&
        buySellValue < 0 &&
        ( uint( -1 * buySellValue ) >= ethFunds ||
          ethFunds - uint( -1 * buySellValue ) < 
            cfg.fundRequirement_denySells ) )
    {
        require( false/*, "This sell would drop the lottery ETH funds"
                        "below the minimum requirement threshold!" */);
    }
}

/**
 *  Update holder data.
 *  This function is called by _transfer() function, just before
 *  transfering final amount of tokens directly from sender to
 *  receiver.
 *  At this point, all burns/mints have been done, and we're sure
 *  that this transfer is valid and must be successful.
 *
 *  In all modes, this function is used to update the holder array.
 *
 *  However, on external exchange modes (e.g. on Uniswap mode),
 *  it is also used to track buy/sell ether value, to update holder
 *  scores, when token buys/sells cannot be tracked directly.
 *
 *  If, however, we use Standalone mode, we are the exchange,
 *  so on _transfer() we already know the ether value, which is
 *  set to currentBuySellEtherValue variable.
 *
 *  @param amountSent - the token amount that is deducted from
 *      sender's balance. This includes burn, and owner fee.
 *
 *  @param amountReceived - the token amount that receiver
 *      actually receives, after burns and fees.
 *
 *  @return holderCountChanged - indicates whether holder count
 *      changes during this transfer - new holder joins or leaves
 *      (true), or no change occurs (false).
 */