contract VEGA_Pool is Ownable {

  uint256 public constant EXEPECTED_VEGA = 422000 ether;
  uint256 public constant EQUITY_RATIO = 2500;

  uint256 public assignSharesCutoff;
  uint256 public equityTokens;
  uint256 public equityTokensRedeemed;
  uint256 public preferentialTokens;
  uint256 public preferentialTokensRedeemed;

  address public preferentialAddress;

  bool public initialized = false;

  ISHP public shp;
  IERC20 public vega;
  IVegaVesting public vegaVesting;

  uint256 public referenceBlock;

  bool public voteComplete = false;
  bool public approveDistribution = false;

  mapping(address => uint256) public equityShares;
  mapping(address => bool) public permittedEquityHolders;
  mapping(uint256 => address) public equityHolders;
  mapping(address => int8) public distributionVotes;
  mapping(address => bool) public shpTokensRedeemed;

  uint256 public totalEquityHolders;
  uint256 public totalShares;
  uint256 public totalVotes;
  int256 public voteOutcome;
  uint256 public shpRedemptionCount;

  event VoteCast(int8 vote, address shareholder);
  event TokensClaimed(uint256 amount, address recipient);
  event ERC20TokenWithdrawn(uint256 amount, address tokenAddress);
  event EtherWithdrawn(uint256 amount);
  event EquityIssued(address holder, uint256 amount,
    uint256 totalEquityHolders, uint256 totalShares);
  event PreferentialTokensRedeemed(uint256 amount);
  event EquityTokensRedeemed(address recipient, uint256 amount);
  event ExcessTokensRedeemed(uint256 amount);
  event PermittedEquityHolderAdded(address holder);
  event VegaPoolInitialized(address vegaAddress, address vestingAddress,
    address preferentialAddress, uint256 assignSharesCutoff,
    uint256 referenceBlock, address shpTokenAddress);

  // This modifier makes sure the contract has been initialized
  modifier requireInitialized() {
     require(initialized, "Contract is not initialized.");
     _;
  }

  // This modifier makes sure the contract is not initialized
  modifier notInitialized() {
     require(!initialized, "Contract has been initialized.");
     _;
  }

  receive() external payable { }

  /**
  * This function allows equity holders to vote on whether tokens should
  * remain theirs, or whether they should be made available for redemption
  * by SHP token holders.
  *
  * If they vote to allow SHP token holders to redeem VEGA from the contract
  * then SHP token holders will be able to call the claimTokens function
  * and the amount of VEGA will be calculated based on their SHP holding
  * at the reference Ethereum block.
  *
  * Once the vote has been successfully completed, if the equity holders vote
  * AGAINST distrubiton, they will be able to redeem tokens by calling
  * redeemTokensViaEquity. If they vote FOR distribution they will not be
  * able to redeem any tokens. Instead SHP token holders will be able to
  * redeem tokens by calling claimTokens.
  *
  * _vote   the user's vote (1 = for, -1 = against)
  **/
  function castVote(int8 _vote) requireInitialized public {
    require(block.timestamp > assignSharesCutoff,
      "Cannot vote whilst shares can still be assigned.");
    require(distributionVotes[msg.sender] == 0,
      "You have already cast your vote.");
    require(_vote == 1 || _vote == -1,
      "Vote must be 1 or -1");
    require(voteComplete == false,
      "Voting has already concluded.");
    require(equityShares[msg.sender] > 0,
      "You cannot vote without equity shares.");
    int256 weight = int256(getUserEquity(msg.sender));
    distributionVotes[msg.sender] = _vote;
    totalVotes += 1;
    voteOutcome += (_vote * weight);
    if(totalVotes == totalEquityHolders) {
      voteComplete = true;
      approveDistribution = voteOutcome > 0;
    }
    emit VoteCast(_vote, msg.sender);
  }

  /**
  * This function withdraws any vested tokens and redeems the preferential
  * tokens if they have not already been redeemed.
  **/
  function syncTokens() requireInitialized internal {
    withdrawVestedTokens();
    if(preferentialTokens > preferentialTokensRedeemed) {
      redeemPreferentialTokens();
    }
  }

  /**
  * This function allows users that held SHP at the reference Ethereum block
  * to claim VEGA from the smart contract, provided the equity holders have
  * voted to permit them to do so.
  *
  * If permitted to do so, the equityTokens will be made available to users
  * in direct proportion to the SHP held (divided by total supply) at the
  * reference block.
  **/
  function claimTokens() requireInitialized public {
    require(approveDistribution, "Distribution is not approved");
    syncTokens();
    require(preferentialTokens == preferentialTokensRedeemed,
      "Cannot claim until preferential tokens are redeemed.");
    uint256 shpBalance = shp.balanceOfAt(msg.sender, referenceBlock);
    require(shpTokensRedeemed[msg.sender] == false,
      "SHP holder already claimed tokens.");
    uint256 vegaBalance = vega.balanceOf(address(this));
    require(shpRedemptionCount > 0 || vegaBalance >= equityTokens,
      "Cannot claim until all equity tokens are fully vested.");
    uint256 shpSupply = shp.totalSupplyAt(referenceBlock);
    uint256 mod = 1000000000000;
    uint256 tokenAmount = (((shpBalance * mod) / shpSupply) *
      equityTokens) / mod;
    vega.transfer(msg.sender, tokenAmount);
    equityTokensRedeemed += tokenAmount;
    shpTokensRedeemed[msg.sender] = true;
    shpRedemptionCount += 1;
    emit TokensClaimed(tokenAmount, msg.sender);
  }

  /**
  * This function allows the owner to withdraw any ERC20 which is not VEGA
  * from the contract at-will. This can be used to redeem staking rewards,
  * or other ERC20s which might end up in this contract by mistake, or by
  * something like an airdrop.
  *
  * _tokenAddress    the contract address for the ERC20
  **/
  function withdrawArbitraryTokens(
    address _tokenAddress
  ) requireInitialized onlyOwner public {
    require(_tokenAddress != address(vega),
      "VEGA cannot be withdrawn at-will.");
    IERC20 token = IERC20(_tokenAddress);
    uint256 amount = token.balanceOf(address(this));
    token.transfer(owner(), amount);
    emit ERC20TokenWithdrawn(amount, _tokenAddress);
  }

  /**
  * This function performs the same role as withdrawArbitraryTokens, except
  * it is used to withdraw ETH.
  **/
  function withdrawEther() requireInitialized onlyOwner public {
    uint256 amount = address(this).balance;
    payable(owner()).transfer(amount);
    emit EtherWithdrawn(amount);
  }

  /**
  * This function can be called by anybody and it withdraws unlocked
  * VEGA tokens from the vesting contract. The tokens are transferred
  * to this contract, which allows them to be redeemed by the rightful owner
  * when they call one of the redemption functions.
  **/
  function withdrawVestedTokens() requireInitialized internal {
    for(uint8 i = 1; i < vegaVesting.tranche_count(); i++) {
      if(vegaVesting.get_vested_for_tranche(address(this), i) > 0) {
        vegaVesting.withdraw_from_tranche(i);
      }
    }
  }

  /**
  * This function allows the owner to issue equity to new users. This is done
  * by assigning an absolute number of shares, which in turn dilutes all
  * existing share holders.
  *
  * _holder    the Ethereum address of the equity holder
  * _amount    the number of shares to be assigned to the holder
  **/
  function issueEquity(
    address _holder,
    uint256 _amount
  ) requireInitialized onlyOwner public {
    require(permittedEquityHolders[_holder],
      "The holder must be permitted to own equity.");
    require(assignSharesCutoff > block.timestamp,
      "The cutoff has passed for assigning shares.");
    if(equityShares[_holder] == 0) {
      equityHolders[totalEquityHolders] = _holder;
      totalEquityHolders += 1;
    }
    totalShares += _amount;
    equityShares[_holder] += _amount;
    emit EquityIssued(_holder, _amount, totalEquityHolders, totalShares);
  }

  /**
  * This function allows the preferential tokens to be distributed to the
  * rightful owner. This function can be called by anybody.
  **/
  function redeemPreferentialTokens() requireInitialized public {
    require(preferentialTokens > preferentialTokensRedeemed,
      "All preferntial tokens have been redeemed.");
    withdrawVestedTokens();
    uint256 availableTokens = preferentialTokens - preferentialTokensRedeemed;
    uint256 vegaBalance = vega.balanceOf(address(this));
    if(availableTokens > vegaBalance) {
      availableTokens = vegaBalance;
    }
    vega.transfer(preferentialAddress, availableTokens);
    preferentialTokensRedeemed += availableTokens;
    emit PreferentialTokensRedeemed(availableTokens);
  }

  /**
  * This function distributes tokens to equity holders based on the amount
  * of shares they own.
  *
  * Anybody can call this function in order to ensure all of the tokens are
  * distributed when it becomes eligible to do so.
  **/
  function redeemTokensViaEquity() requireInitialized public {
    require(totalShares > 0, "There are are no equity holders");
    require(assignSharesCutoff < block.timestamp,
      "Tokens cannot be redeemed whilst equity can still be assigned.");
    syncTokens();
    require(preferentialTokens == preferentialTokensRedeemed,
      "Cannot redeem via equity until all preferential tokens are collected.");
    require(voteComplete, "Cannot redeem via equity until vote is completed.");
    require(approveDistribution == false,
      "Tokens can only be redeemed by SHP holders.");
    uint256 availableTokens = equityTokens - equityTokensRedeemed;
    uint256 vegaBalance = vega.balanceOf(address(this));
    if(availableTokens > vegaBalance) {
      availableTokens = vegaBalance;
    }
    for(uint256 i = 0; i < totalEquityHolders; i++) {
      uint256 tokensToRedeem = (availableTokens *
        getUserEquity(equityHolders[i])) / 10000;
      vega.transfer(equityHolders[i], tokensToRedeem);
      equityTokensRedeemed += tokensToRedeem;
      emit EquityTokensRedeemed(equityHolders[i], tokensToRedeem);
    }
  }

  /**
  * This function allows anybody to redeem excess VEGA to the owner's wallet
  * provided the following conditions are met:
  *
  * 1) No equity shares exist, which happens under two scenarios:
  *      a) They are never issued in the first place
  *      b) They are burnt after redeeming VEGA
  * 2) The cut-off for assigning equity shares is in the past
  *
  * This function transfers the entire VEGA balance held by the
  * smart contract at execution time.
  **/
  function redeemExcessTokens() requireInitialized public {
    if(totalEquityHolders > 0) {
      require(equityTokens == equityTokensRedeemed,
        "Cannot redeem excess tokens until equity tokens are collected.");
    }
    require(preferentialTokens == preferentialTokensRedeemed,
      "Cannot redeem excess tokens until preferential tokens are collected.");
    withdrawVestedTokens();
    uint256 amount = vega.balanceOf(address(this));
    emit ExcessTokensRedeemed(amount);
    vega.transfer(owner(), amount);
  }

  /**
  * This function calculates the equity of the specified user
  *
  * _holder    the Ethereum address of the equity holder
  **/
  function getUserEquity(
    address _holder
  ) public view returns(uint256) {
    return (equityShares[_holder] * 10000) / totalShares;
  }

  /**
  * This function allows the contract to be initialized only once.
  * We do not use the constructor, because the Vega vesting contract needs to
  * know the address of this smart contract when it is deployed. Therefore,
  * this contract needs to be deployed, and then updated with the address of
  * the Vega vesting contract afterwards.
  *
  * _vegaAdress           the Ethereum address of the VEGA token contract
  * _vegaVestingAddress   the Ethereum address of Vega's vesting contract
  * _preferentialAddress  Ethereum address for preferential tokens
  * _holders              an array of permitted equity holders
  * _assignSharesCutoff   timestamp after which shares cannot be assigned
  * _referenceBlock       the Ethereum block to lookup SHP balances with
  * _shpTokenAddress      the Ethereum address for SHP token contract
  **/
  function initialize(
    address _vegaAddress,
    address _vegaVestingAddress,
    address _preferentialAddress,
    address[] memory _holders,
    uint256 _assignSharesCutoff,
    uint256 _referenceBlock,
    address _shpTokenAddress
  ) public onlyOwner notInitialized {
    vega = IERC20(_vegaAddress);
    shp = ISHP(_shpTokenAddress);
    vegaVesting = IVegaVesting(_vegaVestingAddress);
    uint256 totalTokens = vegaVesting.user_total_all_tranches(address(this));
    preferentialAddress = _preferentialAddress;
    assignSharesCutoff = _assignSharesCutoff;
    referenceBlock = _referenceBlock;
    require(totalTokens >= EXEPECTED_VEGA,
      "The balance at the vesting contract is too low.");
    for(uint8 x = 0; x < _holders.length; x++) {
      permittedEquityHolders[_holders[x]] = true;
      emit PermittedEquityHolderAdded(_holders[x]);
    }
    equityTokens = (totalTokens * EQUITY_RATIO) / 10000;
    preferentialTokens = totalTokens - equityTokens;
    initialized = true;
    emit VegaPoolInitialized(_vegaAddress, _vegaVestingAddress,
      _preferentialAddress, _assignSharesCutoff,
      _referenceBlock, _shpTokenAddress);
  }
}
function withdrawEther() requireInitialized onlyOwner public {
  uint256 amount = address(this).balance;
  payable(owner()).transfer(amount);
  emit EtherWithdrawn(amount);
}

/**
 * This function performs the same role as withdrawArbitraryTokens, except
 * it is used to withdraw ETH.
 **/