// File: contracts/Governance.sol



pragma solidity 0.6.12;

import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
import "./interfaces/IRAMv1Router.sol";

contract Governance {
  using SafeMath for uint256;

  IERC20 public YGYToken;
  IRAMv1Router public RAMRouter;

  uint256 public weightedNumber; // Number 1-8 weighted by total user numbers
  uint256 public votingShares; // Includes voting shares generated fromimelocked YGY

  uint256 public lastRAMRouterUpdateTime; // Last time the regenerator tax on the router was updated
  bool public updateStagingMode;
  uint256 public updateStagingReadyTime;
  address public owner;

  struct User {
    uint256 number; // Number from 1-8 indicating the desired LGE regenerator tax %
    uint256 timelockedYGY;
    // The timelocks are stack data structure implemented via hashmaps,
    // there's a stack at each level (1-4)
    mapping(uint256 => mapping(uint256 => TimeLock)) timelocks; // mapping(level => timelock ID => timelock object)
    mapping(uint256 => uint256) timelockTop; // mapping (level => top of stack at this level)
    mapping(uint256 => uint256) timelockCount; // mapping (level => current number timelocks at this level)
  }

  struct TimeLock {
    uint256 multipliedAmount;
    uint256 level;
    uint256 unlockTime;
  }

  mapping(address => User) public users;

  constructor(address _YGYToken, address _RAMRouter) public {
    YGYToken = IERC20(_YGYToken);
    RAMRouter = IRAMv1Router(_RAMRouter);
    weightedNumber = 1; // start at 1%
    owner = msg.sender;
  }

  function updateRouter(address _RAMRouter) external {
    require(msg.sender == owner, "!Owner");
    RAMRouter = IRAMv1Router(_RAMRouter);
  }

  function hasTimeLockAtLevel(address user, uint256 level)
    external
    view
    returns (
      bool _hasTimelock,
      uint256 _level,
      uint256 _unlockTime
    )
  {
    User storage userMem = users[user];
    uint256 timeLocks = userMem.timelockCount[level];
    if (timeLocks > 0) {
      uint256 top = userMem.timelockTop[level];
      uint256 unlockTime = userMem.timelocks[level][top].unlockTime;
      return (true, level, unlockTime);
    } else {
      return (false, level, 0);
    }
  }

  function setUserNumber(uint256 _number) public {
    require(_number >= 1 && _number <= 8, "Number must be in range 1-8");
    User storage user = users[msg.sender];
    user.number = _number;

    calcWeightedNumber(msg.sender);
  }

  function enterRegeneratorUpdateStagingMode() public {
    // 1 day mandatory wait time after last router regenerator tax update
    require(block.timestamp >= lastRAMRouterUpdateTime.add(1 days), "Must wait 1 day since last update");
    updateStagingMode = true;
    updateStagingReadyTime = block.timestamp.add(10 minutes);
  }

  function updateRAMRouterRegeneratorTax() public {
    require(updateStagingMode, "Must be in update staging mode");
    require(block.timestamp >= updateStagingReadyTime, "Must wait 10 minutes since update staged");
    updateStagingMode = false;
    lastRAMRouterUpdateTime = block.timestamp;

    // Update the RAM router's regenerator tax
    RAMRouter.setRegeneratorTax(weightedNumber);
  }

  // users can lock YGY for time durations to get multipliers on their YGY
  function timelockYGY(
    uint256 _amount,
    uint256 _level,
    uint256 _number
  ) public {
    require(_number >= 1 && _number <= 8, "Number must be in range 1-8");
    require(YGYToken.transferFrom(msg.sender, address(this), _amount), "Have tokens been approved?");

    User storage user = users[msg.sender];

    // Calculate effective voting power and create new timelock
    uint256 effectiveAmount = _amount.mul(getMultiplierForLevel(_level)).div(100);
    TimeLock memory timelock =
      TimeLock({ multipliedAmount: effectiveAmount, level: _level, unlockTime: block.timestamp.add(getDurationForLevel(_level)) });

    if (user.timelockTop[_level] == 0) {
      user.timelockTop[_level] = user.timelockTop[_level].add(1);
    }

    uint256 newTimelockCount = user.timelockCount[_level].add(1);
    user.timelocks[_level][newTimelockCount] = timelock;
    user.timelockCount[_level] = newTimelockCount;

    // Add the new voting power to user and the total voting power
    user.timelockedYGY = user.timelockedYGY.add(effectiveAmount);
    votingShares = votingShares.add(effectiveAmount);

    // Update number and calc new weighted number
    user.number = _number;
    calcWeightedNumber(msg.sender);
  }

  // User unlocks their oldest timelock, receiving all the YGY tokens directly to their address
  function unlockOldestTimelock(uint256 _level) public {
    User storage user = users[msg.sender];
    uint256 levelTimelockTop = user.timelockTop[_level];
    TimeLock storage timelock = user.timelocks[_level][levelTimelockTop];
    require(block.timestamp >= timelock.unlockTime, "Tokens are still timelocked");

    // Update user's timelocked balances and the total YGY balance
    user.timelockedYGY = user.timelockedYGY.sub(timelock.multipliedAmount);
    votingShares = votingShares.sub(timelock.multipliedAmount);

    // Send underlying amount of tokens to user
    uint256 underlyingAmount = timelock.multipliedAmount.div(getMultiplierForLevel(timelock.level).div(100));
    YGYToken.transfer(msg.sender, underlyingAmount);

    // Delete the timelock and update user's timelock stack
    delete user.timelocks[_level][levelTimelockTop];
    user.timelockTop[_level] = levelTimelockTop.add(1);
    user.timelockCount[_level] = user.timelockCount[_level].sub(1);

    calcWeightedNumber(msg.sender);
  }

  function calcWeightedNumber(address addr) internal {
    User storage user = users[addr];

    // Calculate the sum of all weights
    uint256 otherTotalYGY = votingShares.sub(user.timelockedYGY);

    // Calculate the sum of all weighing factors
    uint256 userWeighingFactor = user.timelockedYGY.mul(user.number);
    uint256 otherWeighingFactor = otherTotalYGY.mul(weightedNumber);
    uint256 sumOfWeighingFactors = userWeighingFactor.add(otherWeighingFactor);

    // Weighted average = (sum weighing factors / sum of weight)
    if (votingShares > 0 && user.timelockedYGY > 0) {
      weightedNumber = sumOfWeighingFactors.div(votingShares);
    }
  }

  function getDurationForLevel(uint256 _level) public pure returns (uint256) {
    if (_level == 1) {
      return 2 weeks;
    } else if (_level == 2) {
      return 4 weeks;
    } else if (_level == 3) {
      return 12 weeks;
    } else if (_level == 4) {
      return 24 weeks;
    }
    return 2 weeks;
  }

  function getMultiplierForLevel(uint256 _level) public pure returns (uint256) {
    if (_level == 1) {
      return 150; // 1.5x
    } else if (_level == 2) {
      return 300; // 3x
    } else if (_level == 3) {
      return 1000; // 10x
    } else if (_level == 4) {
      return 2500; // 25x
    } else {
      return 150;
    }
  }
}

// File: @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol



pragma solidity ^0.6.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `recipient`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address recipient, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `sender` to `recipient` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

// File: @openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol



pragma solidity ^0.6.0;

/**
 * @dev Wrappers over Solidity's arithmetic operations with added overflow
 * checks.
 *
 * Arithmetic operations in Solidity wrap on overflow. This can easily result
 * in bugs, because programmers usually assume that an overflow raises an
 * error, which is the standard behavior in high level programming languages.
 * `SafeMath` restores this intuition by reverting the transaction when an
 * operation overflows.
 *
 * Using this library instead of the unchecked operations eliminates an entire
 * class of bugs, so it's recommended to use it always.
 */
library SafeMath {
    /**
     * @dev Returns the addition of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `+` operator.
     *
     * Requirements:
     * - Addition cannot overflow.
     */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting with custom message on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        uint256 c = a - b;

        return c;
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `*` operator.
     *
     * Requirements:
     * - Multiplication cannot overflow.
     */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }

    /**
     * @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) {
        // Solidity only automatically asserts when dividing by 0
        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;
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * Reverts when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. 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 mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return mod(a, b, "SafeMath: modulo by zero");
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * Reverts with custom message when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. 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 mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b != 0, errorMessage);
        return a % b;
    }
}

// File: contracts/interfaces/IRAMv1Router.sol



// SPDX-License-Identifier: MIT
pragma solidity 0.6.12;

interface IRAMv1Router {
    function setRegeneratorTax(uint256 _regeneratorTax) external;
}