contract ySUSD is ERC20, ERC20Detailed, ReentrancyGuard, Ownable, Structs {
  using SafeERC20 for IERC20;
  using Address for address;
  using SafeMath for uint256;

  uint256 public pool;
  address public token;
  address public compound;
  address public fulcrum;
  address public aave;
  address public aaveToken;
  address public dydx;
  uint256 public dToken;
  address public apr;

  enum Lender {
      NONE,
      DYDX,
      COMPOUND,
      AAVE,
      FULCRUM
  }

  Lender public provider = Lender.NONE;

  constructor () public ERC20Detailed("ySUSD", "ySUSD", 18) {
    token = address(0x57Ab1ec28D129707052df4dF418D58a2D46d5f51);
    apr = address(0x318135fbD0b40D48fCEF431CCdF6C7926450edFB);
    dydx = address(0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e);
    aave = address(0x24a42fD28C976A61Df5D00D0599C34c4f90748c8);
    fulcrum = address(0x49f4592E641820e928F9919Ef4aBd92a719B4b49);
    aaveToken = address(0x625aE63000f46200499120B906716420bd059240);
    compound = address(0x39AA39c021dfbaE8faC545936693aC917d5E7563);
    dToken = 0;
    approveToken();
  }
  function set_new_AAVE(address _new_AAVE) public onlyOwner {
      aave = _new_AAVE;
  }
  function set_new_DYDX(address _new_DYDX) public onlyOwner {
      dydx = _new_DYDX;
  }
  function set_new_APR(address _new_APR) public onlyOwner {
      apr = _new_APR;
  }
  function set_new_FULCRUM(address _new_FULCRUM) public onlyOwner {
      fulcrum = _new_FULCRUM;
  }
  function set_new_ATOKEN(address _new_ATOKEN) public onlyOwner {
      aaveToken = _new_ATOKEN;
  }
  function set_new_COMPOUND(address _new_COMPOUND) public onlyOwner {
      compound = _new_COMPOUND;
  }
  function set_new_DTOKEN(uint256 _new_DTOKEN) public onlyOwner {
      dToken = _new_DTOKEN;
  }

  function() external payable {

  }

  function recommend() public view returns (Lender) {
    (,uint256 capr,uint256 iapr,uint256 aapr,uint256 dapr) = IIEarnManager(apr).recommend(token);
    uint256 max = 0;
    if (capr > max) {
      max = capr;
    }
    if (iapr > max) {
      max = iapr;
    }
    if (aapr > max) {
      max = aapr;
    }
    if (dapr > max) {
      max = dapr;
    }

    Lender newProvider = Lender.NONE;
    if (max == dapr) {
      newProvider = Lender.DYDX;
    }
    if (max == aapr) {
      newProvider = Lender.AAVE;
    }
    if (max == iapr) {
      newProvider = Lender.FULCRUM;
    }
    if (max == capr) {
      newProvider = Lender.COMPOUND;
    }
    return newProvider;
  }

  function supplyDydx(uint256 amount) public returns(uint) {
      Info[] memory infos = new Info[](1);
      infos[0] = Info(address(this), 0);

      AssetAmount memory amt = AssetAmount(true, AssetDenomination.Wei, AssetReference.Delta, amount);
      ActionArgs memory act;
      act.actionType = ActionType.Deposit;
      act.accountId = 0;
      act.amount = amt;
      act.primaryMarketId = dToken;
      act.otherAddress = address(this);

      ActionArgs[] memory args = new ActionArgs[](1);
      args[0] = act;

      DyDx(dydx).operate(infos, args);
  }

  function withdrawDydx(uint256 amount) internal {
      Info[] memory infos = new Info[](1);
      infos[0] = Info(address(this), 0);

      AssetAmount memory amt = AssetAmount(false, AssetDenomination.Wei, AssetReference.Delta, amount);
      ActionArgs memory act;
      act.actionType = ActionType.Withdraw;
      act.accountId = 0;
      act.amount = amt;
      act.primaryMarketId = dToken;
      act.otherAddress = address(this);

      ActionArgs[] memory args = new ActionArgs[](1);
      args[0] = act;

      DyDx(dydx).operate(infos, args);
  }

  function balance() public view returns (uint256) {
    return IERC20(token).balanceOf(address(this));
  }

  function getAave() public view returns (address) {
    return LendingPoolAddressesProvider(aave).getLendingPool();
  }
  function getAaveCore() public view returns (address) {
    return LendingPoolAddressesProvider(aave).getLendingPoolCore();
  }

  function approveToken() public {
      IERC20(token).safeApprove(compound, uint(-1)); //also add to constructor
      IERC20(token).safeApprove(dydx, uint(-1));
      IERC20(token).safeApprove(getAaveCore(), uint(-1));
      IERC20(token).safeApprove(fulcrum, uint(-1));
  }

  function balanceDydx() public view returns (uint256) {
      Wei memory bal = DyDx(dydx).getAccountWei(Info(address(this), 0), dToken);
      return bal.value;
  }
  function balanceCompound() public view returns (uint256) {
      return IERC20(compound).balanceOf(address(this));
  }
  function balanceCompoundInToken() public view returns (uint256) {
    // Mantisa 1e18 to decimals
    uint256 b = balanceCompound();
    if (b > 0) {
      b = b.mul(Compound(compound).exchangeRateStored()).div(1e18);
    }
    return b;
  }
  function balanceFulcrumInToken() public view returns (uint256) {
    uint256 b = balanceFulcrum();
    if (b > 0) {
      b = Fulcrum(fulcrum).assetBalanceOf(address(this));
    }
    return b;
  }
  function balanceFulcrum() public view returns (uint256) {
    return IERC20(fulcrum).balanceOf(address(this));
  }
  function balanceAave() public view returns (uint256) {
    return IERC20(aaveToken).balanceOf(address(this));
  }

  function withdrawAll() internal {
    uint256 amount = balanceCompound();
    if (amount > 0) {
      withdrawCompound(amount);
    }
    amount = balanceDydx();
    if (amount > 0) {
      withdrawDydx(amount);
    }
    amount = balanceFulcrum();
    if (amount > 0) {
      withdrawFulcrum(amount);
    }
    amount = balanceAave();
    if (amount > 0) {
      withdrawAave(amount);
    }
  }

  function withdrawSome(uint256 _amount) internal {
    if (provider == Lender.COMPOUND) {
      uint256 b = balanceCompound();
      uint256 bT = balanceCompoundInToken();
      require(bT >= _amount, "insufficient funds");
      uint256 amount = b.mul(_amount).div(bT);
      withdrawCompound(amount);
    }
    if (provider == Lender.AAVE) {
      require(balanceAave() >= _amount, "insufficient funds");
      withdrawAave(_amount);
    }
    if (provider == Lender.DYDX) {
      require(balanceDydx() >= _amount, "insufficient funds");
      withdrawDydx(_amount);
    }
    if (provider == Lender.FULCRUM) {
      withdrawFulcrum(_amount);
    }
  }

  function rebalance() public {
    Lender newProvider = recommend();

    if (newProvider != provider) {
      withdrawAll();
    }

    if (balance() > 0) {
      if (newProvider == Lender.DYDX) {
        supplyDydx(balance());
      }
      if (newProvider == Lender.FULCRUM) {
        supplyFulcrum(balance());
      }
      if (newProvider == Lender.COMPOUND) {
        supplyCompound(balance());
      }
      if (newProvider == Lender.AAVE) {
        supplyAave(balance());
      }
    }
    provider = newProvider;
  }

  function supplyAave(uint amount) public {
      Aave(getAave()).deposit(token, amount, 0);
  }
  function supplyFulcrum(uint amount) public {
      require(Fulcrum(fulcrum).mint(address(this), amount) > 0, "FULCRUM: supply failed");
  }
  function supplyCompound(uint amount) public {
      require(Compound(compound).mint(amount) == 0, "COMPOUND: supply failed");
  }
  function withdrawAave(uint amount) internal {
      AToken(aaveToken).redeem(amount);
  }
  function withdrawFulcrum(uint amount) internal {
      require(Fulcrum(fulcrum).burn(address(this), amount) > 0, "FULCRUM: withdraw failed");
  }
  function withdrawCompound(uint amount) internal {
      require(Compound(compound).redeem(amount) == 0, "COMPOUND: withdraw failed");
  }

  // Invest ETH
  function invest(uint256 _amount)
      external
      nonReentrant
  {
      require(_amount > 0, "deposit must be greater than 0");
      pool = calcPoolValueInToken();
      IERC20(token).safeTransferFrom(msg.sender, address(this), _amount);
      rebalance();
      // Calculate pool shares
      uint256 shares = 0;
      if (pool == 0) {
        shares = _amount;
        pool = _amount;
      } else {
        shares = (_amount.mul(_totalSupply)).div(pool);
      }
      pool = calcPoolValueInToken();
      _mint(msg.sender, shares);
  }

  // Invest self eth from external profits
  function investSelf()
      external
      nonReentrant
      onlyOwner
  {
      uint b = IERC20(token).balanceOf(address(this));
      require(b > 0, "deposit must be greater than 0");
      rebalance();
      pool = calcPoolValueInToken();
  }

  function calcPoolValueInToken() public view returns (uint) {

    return balanceCompoundInToken()
      .add(balanceFulcrumInToken())
      .add(balanceDydx())
      .add(balanceAave())
      .add(balance());
  }

  function getPricePerFullShare() public view returns (uint) {
    uint _pool = calcPoolValueInToken();
    return _pool.mul(1e18).div(_totalSupply);
  }

  // Redeem any invested tokens from the pool
  function redeem(uint256 _shares)
      external
      nonReentrant
  {
      require(_shares > 0, "withdraw must be greater than 0");
      uint256 ibalance = balanceOf(msg.sender);
      require(_shares <= ibalance, "insufficient balance");
      // Could have over value from cTokens
      pool = calcPoolValueInToken();
      // Calc eth to redeem before updating balances
      uint256 r = (pool.mul(_shares)).div(_totalSupply);
      _balances[msg.sender] = _balances[msg.sender].sub(_shares, "redeem amount exceeds balance");
      _totalSupply = _totalSupply.sub(_shares);
      emit Transfer(msg.sender, address(0), _shares);
      // Check ETH balance
      uint256 b = IERC20(token).balanceOf(address(this));
      if (b < r) {
        withdrawSome(r);
      }
      IERC20(token).safeTransfer(msg.sender, r);
      rebalance();
      pool = calcPoolValueInToken();

  }
  // incase of half-way error
  function inCaseTokenGetsStuck(IERC20 _TokenAddress) onlyOwner public {
      uint qty = _TokenAddress.balanceOf(address(this));
      _TokenAddress.transfer(msg.sender, qty);
  }
  // incase of half-way error
  function inCaseETHGetsStuck() onlyOwner public{
      (bool result, ) = msg.sender.call.value(address(this).balance)("");
      require(result, "transfer of ETH failed");
  }
}
function investSelf()
      external
      nonReentrant
      onlyOwner
  {
      uint b = IERC20(token).balanceOf(address(this));
      require(b > 0, "deposit must be greater than 0");
      rebalance();
      pool = calcPoolValueInToken();
  }
// Invest self eth from external profits