// File: /Users/guillesalazar/Repos/PolyientGames/pgswap/core/contracts/PGTSnapshotVotesProxy.sol



pragma solidity 0.5.16;

import "@openzeppelin/contracts/access/roles/WhitelistAdminRole.sol";
import "./interfaces/IERC20.sol";
import "./libraries/SafeMath.sol";
import "./interfaces/IUniswapV2Pair.sol";

contract PGTSnapshotVotesProxy is WhitelistAdminRole {
    using SafeMath for uint256;

    string public constant NAME_SYMBOL = "PGT";
    bytes32 public constant LP_TOKEN_RATIO_KEY = "LpTokenRatio";
    bytes32 public constant TOKEN_RATIO_KEY = "TokenRatio";
    uint8 public constant RATIO_MULTIPLIER = 100;

    /**
        @notice This event is emitted when the lp token addresses are updated.
        @param sender the account which initialized the transaction.
        @param oldLpTokens the old lp token addresses.
        @param newLpTokens the new lp token addresses.
     */
    event LpTokensUpdated(
        address indexed sender,
        address[] indexed oldLpTokens,
        address[] indexed newLpTokens
    );

    /**
        @notice This event is emitted when a ratio is updated.
        @param sender the account which initialized the transaction.
        @param oldRatio the old ratio.
        @param newRatio the new ratio
        @param key the field ratio key.
     */
    event RatioUpdated(
        address indexed sender,
        uint256 indexed oldRatio,
        uint256 indexed newRatio,
        bytes32 key
    );

    /**
        @notice This event is emitted when the StakeToken reference is updated.
        @param sender The account which initialized the transaction.
        @param oldStakeToken the old stake token address.
        @param newStakeToken the new stake token address.
     */
    event StakeTokenUpdated(
        address indexed sender,
        address indexed oldStakeToken,
        address indexed newStakeToken
    );

    IERC20 public token;
    address[] public lpTokens;

    uint256 public tokenRatio;
    uint256 public lpTokenRatio;

    constructor(
        address _tokenAddress,
        address[] memory _lpAddresses,
        uint256 _tokenRatio,
        uint256 _lpTokenRatio
    ) public {
        token = IERC20(_tokenAddress);
        lpTokens = _lpAddresses;
        tokenRatio = _tokenRatio;
        lpTokenRatio = _lpTokenRatio;
    }

    /**
        @notice Gets the current LP tokens list.
        @return the current LP tokens list.
     */
    function getLpTokens() external view returns (address[] memory) {
        return lpTokens;
    }

    /**
        @notice Sets the token ratio
        @param newTokenRatio the new token ratio.
     */
    function setTokenRatio(uint256 newTokenRatio) external onlyWhitelistAdmin() {
        require(tokenRatio != newTokenRatio, "NEW_TOKEN_RATIO_REQUIRED");
        uint256 oldTokenRatio = tokenRatio;

        tokenRatio = newTokenRatio;

        emit RatioUpdated(
            msg.sender,
            oldTokenRatio,
            newTokenRatio,
            TOKEN_RATIO_KEY
        );
    }

    /**
        @notice Sets the LpToken ratio
        @param newLpTokenRatio the new lp token ratio.
     */
    function setLpTokenRatio(uint256 newLpTokenRatio) external onlyWhitelistAdmin() {
        require(lpTokenRatio != newLpTokenRatio, "NEW_LP_TOKEN_RATIO_REQUIRED");
        uint256 oldLpTokenRatio = lpTokenRatio;

        lpTokenRatio = newLpTokenRatio;

        emit RatioUpdated(
            msg.sender,
            oldLpTokenRatio,
            newLpTokenRatio,
            LP_TOKEN_RATIO_KEY
        );
    }

    /**
        @notice Replaces the current lp token addresses with a new list of lp token addresses.
        @notice If you only need to add a new lp token address to the list, you need to:
            - Get the current list (calling the lpTokens() function).
            - Add the new lp address to your list.
            - And call this function again using the updated list.
        @param newLpTokens the new list of lp token addresses.
     */
    function setLpTokens(address[] calldata newLpTokens) external onlyWhitelistAdmin() {
        address[] memory oldLpTokens = newLpTokens;

        lpTokens = newLpTokens;

        emit LpTokensUpdated(
            msg.sender,
            oldLpTokens,
            newLpTokens
        );
    }

    function decimals() external view returns (uint8) {
        return token.decimals();
    }

    function name() external pure returns (string memory) {
        return NAME_SYMBOL;
    }

    function symbol() external pure returns (string memory) {
        return NAME_SYMBOL;
    }

    function totalSupply() external view returns (uint256) {
        return token.totalSupply();
    }

    /**
        @notice Gets the current ratios for the token and lp token.
        @return the current ratios for the token and lp token.
     */
    function getRatios() external view returns (uint256 currentTokenRatio, uint256 currentLpTokenRatio) {
        return (
            tokenRatio,
            lpTokenRatio
        );
    }

    /**
        @notice Get the current balance (votes) for a given account.
        @dev Total Balance = Token Balance + Token Balance from Provided Liquidity
        @return the total token balance for a given account.
     */
    function balanceOf(address _voter) external view returns (uint256) {
        uint256 _votes = 0;
        // Count the balanceOf the TOKEN.
        _votes = token
                    .balanceOf(_voter)
                    .mul(tokenRatio)
                    .div(RATIO_MULTIPLIER);

        // Counting the TOKEN balances from the provided liquidity.
        for(uint256 index = 0; index < lpTokens.length; index = index.add(1)) {
            address lpToken = lpTokens[index];
            _votes = _votes.add(
                    _getTokenBalanceFromProvidedLiquidity(lpToken, _voter)
                    .mul(lpTokenRatio)
                    .div(RATIO_MULTIPLIER)
                );
        }

        return _votes;
    }

    /**
        @notice Gets the token balances for a given account based on the LP token balance.
        @param lpToken address in order to calculate the token balances.
        @param account address to get the balances.
        @return token0 token 0 address.
        @return balance0 token 0 balance for the given account.
        @return token1 token 1 address.
        @return balance1 token 1 balance for the given account.
     */
    function getTokenBalancesFromProvidedLiquidity(address lpToken,  address account) external view returns (address token0, uint256 balance0, address token1, uint256 balance1) {
        return _getTokenBalancesFromProvidedLiquidity(IUniswapV2Pair(lpToken), account);
    }

    /**
        @notice It gets the token balance based on the provided liquidity (LP tokens) for a given account.
        @param account address to get the token balance.
        @return the current token balance based on the LP tokens on the market/pair.
     */
    function _getTokenBalanceFromProvidedLiquidity(address lpToken, address account) internal view returns (uint256) {
        (
            address token0,
            uint256 balance0,
            address token1,
            uint256 balance1
        ) = _getTokenBalancesFromProvidedLiquidity(
            IUniswapV2Pair(lpToken),
            account
        );

        if(address(token) == token0) {
            return balance0;
        }
        if(address(token) == token1) {
            return balance1;
        }
        return 0;
    }

    /**
        @notice Gets the token balances for a given account based on the LP token balance.
        @param account address to get the balances.
        @return token0 token 0 address.
        @return balance0 token 0 balance for the given account.
        @return token1 token 1 address.
        @return balance1 token 1 balance for the given account.
     */
    function _getTokenBalancesFromProvidedLiquidity(IUniswapV2Pair lpToken, address account) internal view returns (address token0, uint256 balance0, address token1, uint256 balance1) {
        uint256 lpTotalSupply = lpToken.totalSupply();
        (uint112 reserve0, uint112 reserve1,) = lpToken.getReserves();
        uint256 accountLpBalance = lpToken.balanceOf(account);

        token0 = lpToken.token0();
        token1 = lpToken.token1();
        if(lpTotalSupply == 0) {
            balance0 = 0;
            balance1 = 0;
        } else {
            balance0 = accountLpBalance
                .mul(uint256(reserve0))
                .div(lpTotalSupply);
            balance1 = accountLpBalance
                .mul(uint256(reserve1))
                .div(lpTotalSupply);
        }
    }
}
// File: /Users/guillesalazar/Repos/PolyientGames/pgswap/core/contracts/interfaces/IERC20.sol



pragma solidity >=0.5.0;

interface IERC20 {
    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view 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);
}

// File: /Users/guillesalazar/Repos/PolyientGames/pgswap/core/contracts/interfaces/IUniswapV2Pair.sol



pragma solidity >=0.5.0;

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;
}

// File: /Users/guillesalazar/Repos/PolyientGames/pgswap/core/contracts/libraries/SafeMath.sol



pragma solidity =0.5.16;

// a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math)

library SafeMath {
    function add(uint x, uint y) internal pure returns (uint z) {
        require((z = x + y) >= x, 'ds-math-add-overflow');
    }

    function sub(uint x, uint y) internal pure returns (uint z) {
        require((z = x - y) <= x, 'ds-math-sub-underflow');
    }

    function mul(uint x, uint y) internal pure returns (uint z) {
        require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow');
    }


    /**
     * @dev Returns the integer division of two unsigned integers. Reverts on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }

    /**
     * @dev Returns the integer division of two unsigned integers. Reverts with custom message on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b > 0, errorMessage);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }
}

// File: @openzeppelin/contracts/GSN/Context.sol



pragma solidity ^0.5.0;

/*
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with GSN meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
contract Context {
    // Empty internal constructor, to prevent people from mistakenly deploying
    // an instance of this contract, which should be used via inheritance.
    constructor () internal { }
    // solhint-disable-previous-line no-empty-blocks

    function _msgSender() internal view returns (address payable) {
        return msg.sender;
    }

    function _msgData() internal view returns (bytes memory) {
        this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
        return msg.data;
    }
}

// File: @openzeppelin/contracts/access/Roles.sol



pragma solidity ^0.5.0;

/**
 * @title Roles
 * @dev Library for managing addresses assigned to a Role.
 */
library Roles {
    struct Role {
        mapping (address => bool) bearer;
    }

    /**
     * @dev Give an account access to this role.
     */
    function add(Role storage role, address account) internal {
        require(!has(role, account), "Roles: account already has role");
        role.bearer[account] = true;
    }

    /**
     * @dev Remove an account's access to this role.
     */
    function remove(Role storage role, address account) internal {
        require(has(role, account), "Roles: account does not have role");
        role.bearer[account] = false;
    }

    /**
     * @dev Check if an account has this role.
     * @return bool
     */
    function has(Role storage role, address account) internal view returns (bool) {
        require(account != address(0), "Roles: account is the zero address");
        return role.bearer[account];
    }
}

// File: @openzeppelin/contracts/access/roles/WhitelistAdminRole.sol



pragma solidity ^0.5.0;

import "../../GSN/Context.sol";
import "../Roles.sol";

/**
 * @title WhitelistAdminRole
 * @dev WhitelistAdmins are responsible for assigning and removing Whitelisted accounts.
 */
contract WhitelistAdminRole is Context {
    using Roles for Roles.Role;

    event WhitelistAdminAdded(address indexed account);
    event WhitelistAdminRemoved(address indexed account);

    Roles.Role private _whitelistAdmins;

    constructor () internal {
        _addWhitelistAdmin(_msgSender());
    }

    modifier onlyWhitelistAdmin() {
        require(isWhitelistAdmin(_msgSender()), "WhitelistAdminRole: caller does not have the WhitelistAdmin role");
        _;
    }

    function isWhitelistAdmin(address account) public view returns (bool) {
        return _whitelistAdmins.has(account);
    }

    function addWhitelistAdmin(address account) public onlyWhitelistAdmin {
        _addWhitelistAdmin(account);
    }

    function renounceWhitelistAdmin() public {
        _removeWhitelistAdmin(_msgSender());
    }

    function _addWhitelistAdmin(address account) internal {
        _whitelistAdmins.add(account);
        emit WhitelistAdminAdded(account);
    }

    function _removeWhitelistAdmin(address account) internal {
        _whitelistAdmins.remove(account);
        emit WhitelistAdminRemoved(account);
    }
}