// Contract: PoolStakeFactory
// Target function: function addLiquidity(uint _term, uint _multiplier, uint _id) external uTokenVet(_id) payable
.length >= 2, 'UniswapV2Library: INVALID_PATH');
        amounts = new uint[](path.length);
        amounts[amounts.length - 1] = amountOut;
        for (uint i = path.length - 1; i > 0; i--) {
            (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
            amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
        }
    }
}

interface IUniswapV2Pair {
    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

    function name() external pure returns (string memory);
    function symbol() external pure returns (string memory);
    function decimals() external pure returns (uint8);
    function totalSupply() external view returns (uint);
    function balanceOf(address owner) external view returns (uint);
    function allowance(address owner, address spender) external view returns (uint);

    function approve(address spender, uint value) external returns (bool);
    function transfer(address to, uint value) external returns (bool);
    function transferFrom(address from, address to, uint value) external returns (bool);

    function DOMAIN_SEPARATOR() external view returns (bytes32);
    function PERMIT_TYPEHASH() external pure returns (bytes32);
    function nonces(address owner) external view returns (uint);

    function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;

    event Mint(address indexed sender, uint amount0, uint amount1);
    event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
    event Swap(address indexed sender, uint amount0In, uint amount1In, uint amount0Out, uint amount1Out, address indexed to);
    event Sync(uint112 reserve0, uint112 reserve1);

    function MINIMUM_LIQUIDITY() external pure returns (uint);
    function factory() external view returns (address);
    function token0() external view returns (address);
    function token1() external view returns (address);
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
    function price0CumulativeLast() external view returns (uint);
    function price1CumulativeLast() external view returns (uint);
    function kLast() external view returns (uint);

    function mint(address to) external returns (uint liquidity);
    function burn(address to) external returns (uint amount0, uint amount1);
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
    function skim(address to) external;
    function sync() external;

    function initialize(address, address) external;
}

library Address {
    /**
     * @dev Returns true if `account` is a contract.
     *
     * [IMPORTANT]
     * ====
     * It is unsafe to assume that an address for which this function returns
     * false is an externally-owned account (EOA) and not a contract.
     *
     * Among others, `isContract` will return false for the following
     * types of addresses:
     *
     *  - an externally-owned account
     *  - a contract in construction
     *  - an address where a contract will be created
     *  - an address where a contract lived, but was destroyed
     * ====
     */
    function isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize, which returns 0 for contracts in
        // construction, since the code is only stored at the end of the
        // constructor execution.

        uint256 size;
        // solhint-disable-next-line no-inline-assembly
        assembly { size := extcodesize(account) }
        return size > 0;
    }
}

contract Owned {
    
    //address of contract owner
    address public owner;

    //event for transfer of ownership
    event OwnershipTransferred(address indexed _from, address indexed _to);

    /**
     * @dev Initializes the contract setting the _owner as the initial owner.
     */
    constructor(address _owner) public {
        owner = _owner;
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner {
        require(msg.sender == owner, 'only owner allowed');
        _;
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`_newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address _newOwner) external onlyOwner {
        owner = _newOwner;
        emit OwnershipTransferred(owner, _newOwner);
    }
}

interface Multiplier {
    function updateLockupPeriod(address _user, uint _lockup) external returns(bool);
    function getMultiplierCeiling() external pure returns (uint);
    function balance(address user) external view returns (uint);
    function approvedContract(address _user) external view returns(address);
    function lockupPeriod(address user) external view returns (uint);
}

/* 
 * @dev PoolStakes contract for locking up liquidity to earn bonus rewards.
 */
contract PoolStake is Owned {
    //instantiate SafeMath library
    using SafeMath for uint;
    
    IERC20 internal weth;                       //represents weth.
    IERC20 internal token;                      //represents the project's token which should have a weth pair on uniswap
    IERC20 internal lpToken;                    //lpToken for liquidity provisioning
    
    address internal uToken1;                   //utility token
    address internal uToken2;                   //utility token for migration 
    address internal platformWallet;            //fee receiver
    uint internal scalar = 10**36;              //unit for scaling
    uint internal cap;                          //ETH limit that can be provided
    bool internal migratedToLQDY;
    
    Multiplier internal multiplier1;                        //Interface of Multiplier contract
    Multiplier internal multiplier2;                        //Interface of Multiplier contract
    UniswapV2Router internal uniswapRouter;                 //Interface of Uniswap V2 router
    IUniswapV2Factory internal iUniswapV2Factory;           //Interface of uniswap V2 factory
    
    //user struct
    struct User {
        uint start;                 //starting period
        uint release;               //release period
        uint tokenBonus;            //user token bonus
        uint wethBonus;             //user weth bonus
        uint tokenWithdrawn;        //amount of token bonus withdrawn
        uint wethWithdrawn;         //amount of weth bonus withdrawn
        uint liquidity;             //user liquidity gotten from uniswap
        uint period;                //identifies users' current term period
        bool migrated;              //if migrated to uniswap V3
    }
    
    mapping(address => User) internal _users;
    
    //term periods
    uint32 internal period1;
    uint32 internal period2;
    uint32 internal period3;
    uint32 internal period4;
    
    //12 hours represented in seconds
    uint32 internal constant _012_HOURS_IN_SECONDS = 43200;
    
    //mapping periods(in series of 1 - 4) to number of providers. 
    mapping(uint => uint) internal _providers;
    
    //return percentages for ETH and token                          1000 = 1% 
    uint internal period1RPWeth; 
    uint internal period2RPWeth;
    uint internal period3RPWeth;
    uint internal period4RPWeth;
    uint internal period1RPToken; 
    uint internal period2RPToken;
    uint internal period3RPToken;
    uint internal period4RPToken;
    
    //available bonuses rto be claimed
    uint internal _pendingBonusesWeth;
    uint internal _pendingBonusesToken;
    
    //data for analytics
    uint internal totalETHProvided;
    uint internal totalTokenProvided;
    uint internal totalProviders;
    
    //migration contract for Uniswap V3
    address public migrationContract;
    
    //events
    event BonusAdded(address indexed sender, uint ethAmount, uint tokenAmount);
    event BonusRemoved(address indexed sender, uint amount);
    event CapUpdated(address indexed sender, uint amount);
    event LPWithdrawn(address indexed sender, uint amount);
    event LiquidityAdded(address indexed sender, uint liquidity, uint amountETH, uint amountToken);
    event LiquidityWithdrawn(address indexed sender, uint liquidity, uint amountETH, uint amountToken);
    event MigratedToLQDY(address indexed sender, address uToken, address multiplier);
    event FeeReceiverUpdated(address oldFeeReceiver, address newFeeReceiver);
    event NewUToken(address indexed sender, address uToken2, address multiplier);
    event UserTokenBonusWithdrawn(address indexed sender, uint amount, uint fee);
    event UserETHBonusWithdrawn(address indexed sender, uint amount, uint fee);
    event VersionMigrated(address indexed sender, uint256 time, address to);
    event LiquidityMigrated(address indexed sender, uint amount, address to);
    
    /* 
     * @dev initiates a new PoolStake.
     * --------------------------------------------------------
     * @param _token    --> token offered for staking liquidity.
     * @param _Owner    --> address for the initial contract owner.
     */ 
    constructor(address _token, address _Owner) public Owned(_Owner) {
            
        require(_token != address(0), "can not deploy a zero address");
        token = IERC20(_token);
        weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); 
        
        iUniswapV2Factory = IUniswapV2Factory(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f);
        address _lpToken = iUniswapV2Factory.getPair(address(token), address(weth));
        require(_lpToken != address(0), "Pair must first be created on uniswap");
        lpToken = IERC20(_lpToken);
        
        uToken1 = 0x9Ed8e7C9604790F7Ec589F99b94361d8AAB64E5E;
        platformWallet = 0xa7A4d919202DFA2f4E44FFAc422d21095bF9770a;
        multiplier1 = Multiplier(0xbc962d7be33d8AfB4a547936D8CE6b9a1034E9EE);
        uniswapRouter = UniswapV2Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);  
    }
    
    modifier onlyPlatformWallet() {
        
        require(msg.sender == platformWallet, "only wallet can call");
        _;
    }
    
    modifier uTokenVet(uint _id) {
        
        if(uToken2 == address(0)) require(_id == 1, "currently accepts only uToken1");
        if(migratedToLQDY) require(_id != 1, "currently accepts only uToken2");
        _;
    }

    function newUToken(address _uToken2, address _multiplier2) external onlyPlatformWallet returns(bool) {
        
        require(uToken2 == address(0) && address(multiplier2) == address(0), "already migrated to LQDY");
        require(_uToken2 != address(0x0) && _multiplier2 != address(0x0), "cannot set the zero address");
        require(Address.isContract(_multiplier2), "multiplier must be a smart contract address");
        
        uToken2 = _uToken2;
        multiplier2 = Multiplier(_multiplier2);
        
        emit NewUToken(msg.sender, _uToken2, _multiplier2);
        return true;
    }
    
    function completeUTokenMerge() external onlyPlatformWallet returns(bool) {
        
        require(!migratedToLQDY, "already migrated to LQDY");
        
        migratedToLQDY = true;
        uToken1 = uToken2;
        
        address _multiplier2 = address(multiplier2);
        multiplier1 = Multiplier(_multiplier2);
        
        emit MigratedToLQDY(msg.sender, uToken2, address(multiplier2));
        return true;
    }
    
    function changeFeeReceiver(address _feeReceiver) external onlyPlatformWallet returns(bool) {
        
        platformWallet = _feeReceiver;
        
        emit FeeReceiverUpdated(msg.sender, _feeReceiver);
        return true;
    }
    
    /* 
     * @dev change the return percentage for locking up liquidity for ETH and Token (only Owner).
     * ------------------------------------------------------------------------------------
     * @param _period1RPETH - _period4RPToken --> the new return percentages.
     * ----------------------------------------------
     * returns whether successfully changed or not.
     */ 
    function changeReturnPercentages(
        uint _period1RPETH, uint _period2RPETH, uint _period3RPETH, uint _period4RPETH, 
        uint _period1RPToken, uint _period2RPToken, uint _period3RPToken, uint _period4RPToken
    ) external onlyOwner returns(bool) {
        
        period1RPWeth = _period1RPETH;
        period2RPWeth = _period2RPETH;
        period3RPWeth = _period3RPETH;
        period4RPWeth = _period4RPETH;
        
        period1RPToken = _period1RPToken;
        period2RPToken = _period2RPToken;
        period3RPToken = _period3RPToken;
        period4RPToken = _period4RPToken;
        
        return true;
    }
    
    /* 
     * @dev change the lockup periods (only Owner).
     * ------------------------------------------------------------------------------------
     * @param _firstTerm - _fourthTerm --> the new term periods.
     * ----------------------------------------------
     * returns whether successfully changed or not.
     */ 
    function changeTermPeriods(
        uint32 _firstTerm, uint32 _secondTerm, 
        uint32 _thirdTerm, uint32 _fourthTerm
    ) external onlyOwner returns(bool) {
        
        period1 = _firstTerm;
        period2 = _secondTerm;
        period3 = _thirdTerm;
        period4 = _fourthTerm;
        
        return true;
    }
    
    /* 
     * @dev change the maximum ETH that a user can enter with (only Owner).
     * ------------------------------------------------------------------------------------
     * @param _cap      --> the new cap value.
     * ----------------------------------------------
     * returns whether successfully changed or not.
     */ 
    function changeCap(uint _cap) external onlyOwner returns(bool) {
        
        cap = _cap;
        
        emit CapUpdated(msg.sender, _cap);
        return true;
    }
    
    /* 
     * @dev makes migration possible for uniswap V3 (only Owner).
     * ----------------------------------------------------------
     * @param _unistakeMigrationContract      --> the migration contract address.
     * -------------------------------------------------------------------------
     * returns whether successfully migrated or not.
     */ 
    function allowMigration(address _unistakeMigrationContract) external onlyOwner returns (bool) {
        
        require(_unistakeMigrationContract != address(0x0), "cannot migrate to a null address");
        migrationContract = _unistakeMigrationContract;
        
        emit VersionMigrated(msg.sender, now, migrationContract);
        return true;
    }
    
    /* 
     * @dev initiates migration for a user (only when migration is allowed).
     * -------------------------------------------------------------------
     * @param _unistakeMigrationContract      --> the migration contract address.
     * -------------------------------------------------------------------------
     * returns whether successfully migrated or not.
     */ 
    function startMigration(address _unistakeMigrationContract) external returns (bool) {
        
        require(_unistakeMigrationContract != address(0x0), "cannot migrate to a null address");
        require(migrationContract == _unistakeMigrationContract, "must confirm endpoint");
        require(!getUserMigration(msg.sender), "must not be migrated already");
        
        _users[msg.sender].migrated = true;
        
        uint256 liquidity = _users[msg.sender].liquidity;
        lpToken.transfer(migrationContract, liquidity);
        
        emit LiquidityMigrated(msg.sender, liquidity, migrationContract);
        return true;
    }
    
    /* 
     * @dev add more staking bonuses to the pool.
     * ----------------------------------------
     * @param              --> input value along with call to add ETH
     * @param _tokenAmount --> the amount of token to be added.
     * --------------------------------------------------------
     * returns whether successfully added or not.
     */ 
    function addBonus(uint _tokenAmount) external payable returns(bool) {
        
        require(_tokenAmount > 0 || msg.value > 0, "must send value");
        if (_tokenAmount > 0)
        require(token.transferFrom(msg.sender, address(this), _tokenAmount), "must approve smart contract");
        
        emit BonusAdded(msg.sender, msg.value, _tokenAmount);
        return true;
    }
    
    /* 
     * @dev remove staking bonuses to the pool. (only owner)
     * must have enough asset to be removed
     * ----------------------------------------
     * @param _amountETH   --> eth amount to be removed
     * @param _amountToken --> token amount to be removed.
     * --------------------------------------------------------
     * returns whether successfully added or not.
     */ 
    function removeETHAndTokenBouses(uint _amountETH, uint _amountToken) external onlyOwner returns (bool success) {
       
        require(_amountETH > 0 || _amountToken > 0, "amount must be larger than zero");
    
        if (_amountETH > 0) {
            require(_checkForSufficientStakingBonusesForETH(_amountETH), 'cannot withdraw above current ETH bonus balance');
            msg.sender.transfer(_amountETH);
            emit BonusRemoved(msg.sender, _amountETH);
        }
        
        if (_amountToken > 0) {
            require(_checkForSufficientStakingBonusesForToken(_amountToken), 'cannot withdraw above current token bonus balance');
            require(token.transfer(msg.sender, _amountToken), "error: token transfer failed");
            emit BonusRemoved(msg.sender, _amountToken);
        }
        
        return true;
    }