contract VaultUSDC is ERC20, Ownable {
  using SafeERC20 for IERC20;
  using Address for address;
  using SafeMath for uint256;

  event Withdraw(address indexed beneficiary, uint256 amount);
  event Deposit(address indexed beneficiary, uint256 amount);
  event Invest(uint256 amount);
  event NewStratCandidate(address implementation);
  event UpgradeStrat(address implementation);

  struct StratCandidate {
    address implementation;
    uint256 proposedTime;
  }
  
  // The last proposed strategy to switch to.
  StratCandidate public stratCandidate;
  
  // The strategy currently in use by the vault.
  address public strategy;

  // The token the vault accepts and looks to maximize.
  address public underlying;

  // The minimum time it has to pass before a strat candidate can be approved.
  uint256 public immutable approvalDelay;

  uint256 public underlyingUnit;
  uint256 public fractionToInvestNumerator = 96;
  uint256 public fractionToInvestDenominator = 100;
  
  constructor(
    address _underlying,
    string memory _name,
    string memory _symbol,
    uint256 _approvalDelay
  ) public ERC20(_name, _symbol) {
    underlying = _underlying;
    approvalDelay = _approvalDelay;
    underlyingUnit = 10 ** uint256(ERC20(underlying).decimals());
  }

  modifier whenStrategyDefined() {
    require(strategy != address(0), "Strategy must be defined");
    _;
  }
  
  /**
  * Chooses the best strategy and re-invests. If the strategy did not change, it just calls
  * doHardWork on the current strategy. Call this to claim hard rewards.
  */
  function doHardWork() whenStrategyDefined external {
    // ensure that new funds are invested too
    invest();
    IStrategy(strategy).doHardWork();
  }

  /*
  * Returns the cash balance across all users in this contract.
  */
  function underlyingBalanceInVault() view public returns (uint256) {
    return IERC20(underlying).balanceOf(address(this));
  }

  /* Returns the current underlying (e.g., DAI's) balance together with
   * the invested amount (if DAI is invested elsewhere by the strategy).
  */
  function underlyingBalanceWithInvestment() view public returns (uint256) {
    if (strategy == address(0)) {
      // initial state, when not set
      return underlyingBalanceInVault();
    }
    return underlyingBalanceInVault().add(IStrategy(strategy).investedUnderlyingBalance());
  }

  function getPricePerFullShare() public view returns (uint256) {
    return totalSupply() == 0
        ? underlyingUnit
        : underlyingUnit.mul(underlyingBalanceWithInvestment()).div(totalSupply());
  }

  /* get the user's share (in underlying)
  */
  function underlyingBalanceWithInvestmentForHolder(address holder) view external returns (uint256) {
    if (totalSupply() == 0) {
      return 0;
    }
    return underlyingBalanceWithInvestment()
        .mul(balanceOf(holder))
        .div(totalSupply());
  }

    /**
     * @dev Sets the candidate for the new strat to use with this vault.
     * @param _implementation The address of the candidate strategy.
     */
    function proposeStrat(address _implementation) public onlyOwner {
        stratCandidate = StratCandidate({
            implementation: _implementation,
            proposedTime: block.timestamp
        });

        emit NewStratCandidate(_implementation);
    }

    /**
     * @dev It switches the active strat for the strat candidate. After upgrading, the
     * candidate implementation is set to the 0x00 address, and proposedTime to a time
     * happening in +100 years for safety.
     */

    function upgradeStrat() public onlyOwner {
        require(
            stratCandidate.implementation != address(0),
            "There is no candidate"
        );
        require(
            stratCandidate.proposedTime.add(approvalDelay) < block.timestamp,
            "Delay has not passed"
        );
        
        if (strategy != stratCandidate.implementation) {
          require(IStrategy(stratCandidate.implementation).underlying() == underlying, "Vault underlying must match Strategy underlying");
          require(IStrategy(stratCandidate.implementation).vault() == address(this), "the strategy does not belong to this vault");
        
          if (strategy != address(0)) { // if the original strategy (no underscore) is defined
            IERC20(underlying).safeApprove(strategy, 0);
            IStrategy(strategy).withdrawAllToVault();
          }
          
          strategy = stratCandidate.implementation;
          stratCandidate.implementation = address(0);
          stratCandidate.proposedTime = 0;
          
          IERC20(underlying).safeApprove(address(strategy), 0);
          IERC20(underlying).safeApprove(address(strategy), uint256(~0));
          
          emit UpgradeStrat(stratCandidate.implementation);
        }
    }

  function setVaultFractionToInvest(uint256 numerator, uint256 denominator) external onlyOwner {
    require(denominator > 0, "denominator must be greater than 0");
    require(numerator <= denominator, "denominator must be greater than or equal to the numerator");
    fractionToInvestNumerator = numerator;
    fractionToInvestDenominator = denominator;
  }

  function rebalance() external onlyOwner {
    withdrawAll();
    invest();
  }

  function availableToInvestOut() public view returns (uint256) {
    uint256 wantInvestInTotal = underlyingBalanceWithInvestment()
        .mul(fractionToInvestNumerator)
        .div(fractionToInvestDenominator);
    uint256 alreadyInvested = IStrategy(strategy).investedUnderlyingBalance();
    if (alreadyInvested >= wantInvestInTotal) {
      return 0;
    } else {
      uint256 remainingToInvest = wantInvestInTotal.sub(alreadyInvested);
      return remainingToInvest <= underlyingBalanceInVault()
        // TODO: we think that the "else" branch of the ternary operation is not
        // going to get hit
        ? remainingToInvest : underlyingBalanceInVault();
    }
  }

  function invest() internal whenStrategyDefined {
    uint256 availableAmount = availableToInvestOut();
    if (availableAmount > 0) {
      IERC20(underlying).safeTransfer(strategy, availableAmount);
      emit Invest(availableAmount);
    }
  }

  /*
  * Allows for depositing the underlying asset in exchange for shares.
  * Approval is assumed.
  */
  function deposit(uint256 amount) external {
    _deposit(amount, msg.sender, msg.sender);
  }

  /*
  * Allows for depositing the underlying asset in exchange for shares
  * assigned to the holder.
  * This facilitates depositing for someone else (using DepositHelper)
  */
  function depositFor(uint256 amount, address holder) public {
    _deposit(amount, msg.sender, holder);
  }

  function withdrawAll() public onlyOwner whenStrategyDefined {
    IStrategy(strategy).withdrawAllToVault();
  }

  function withdraw(uint256 numberOfShares) external {
    require(totalSupply() > 0, "Vault has no shares");
    require(numberOfShares > 0, "numberOfShares must be greater than 0");
    uint256 totalSupply = totalSupply();
    _burn(msg.sender, numberOfShares);

    uint256 underlyingAmountToWithdraw = underlyingBalanceWithInvestment()
        .mul(numberOfShares)
        .div(totalSupply);
    if (underlyingAmountToWithdraw > underlyingBalanceInVault()) {
      // withdraw everything from the strategy to accurately check the share value
      if (numberOfShares == totalSupply) {
        IStrategy(strategy).withdrawAllToVault();
      } else {
        uint256 missing = underlyingAmountToWithdraw.sub(underlyingBalanceInVault());
        IStrategy(strategy).withdrawToVault(missing);
      }
      // recalculate to improve accuracy
      underlyingAmountToWithdraw = Math.min(underlyingBalanceWithInvestment()
          .mul(numberOfShares)
          .div(totalSupply), underlyingBalanceInVault());
    }

    IERC20(underlying).safeTransfer(msg.sender, underlyingAmountToWithdraw);

    // update the withdrawal amount for the holder
    emit Withdraw(msg.sender, underlyingAmountToWithdraw);
  }

  function _deposit(uint256 amount, address sender, address beneficiary) internal {
    require(amount > 0, "Cannot deposit 0");
    require(beneficiary != address(0), "holder must be defined");

    if (address(strategy) != address(0)) {
      require(IStrategy(strategy).depositArbCheck(), "Too much arb");
    }

    uint256 toMint = totalSupply() == 0
        ? amount
        : amount.mul(totalSupply()).div(underlyingBalanceWithInvestment());
    _mint(beneficiary, toMint);

    IERC20(underlying).safeTransferFrom(sender, address(this), amount);

    // update the contribution amount for the beneficiary
    emit Deposit(beneficiary, amount);
  }
}
function underlyingBalanceWithInvestmentForHolder(address holder) view external returns (uint256) {
  if (totalSupply() == 0) {
    return 0;
  }
  return underlyingBalanceWithInvestment()
      .mul(balanceOf(holder))
      .div(totalSupply());
}

/* get the user's share (in underlying)
 */