contract SmartWalletVault is Initializable, ReentrancyGuardEmber, WhitelistedRoleMeta {
    using SafeMath for uint256;

    // Nonces to prevent replay attacks
    mapping(uint256 => bool) private nonces;

    uint16 public constant version = 3;

    // Gas token saver
    ChiGasSaver public constant chiGasSaver = ChiGasSaver(0x0000000000004946c0e9F43F4Dee607b0eF1fA1c);

    modifier discountChiGasSaver {
      uint256 gasStart = gasleft();
      _;
      uint256 gasSpent = 21000 + gasStart - gasleft() + 16 *
                         msg.data.length;
      uint256 allowance = chiGasSaver.allowance(msg.sender, address(this));
      uint256 chiTokensToSpend = (gasSpent + 14154) / 41947;
      if (allowance < chiTokensToSpend) {
        chiGasSaver.approve(address(this), chiTokensToSpend);
      }
      chiGasSaver.freeFromUpTo(msg.sender, chiTokensToSpend);
    }

    mapping(address => address) public depositTokenAddressToYVault;
    mapping(address => address) public yVaultToDepositTokenAddress;

    /**
     * Initialize the smart contract with an owner.
     */
    function initialize(address _owner) public initializer {
      WhitelistedRoleMeta.initialize(_owner);
      ReentrancyGuardEmber.initialize();

      // Add more deposit to vault mapping to support more vaults
      // USDT 0xdAC17F958D2ee523a2206206994597C13D831ec7
      // yEarn USDT 0x2f08119C6f07c006695E079AAFc638b8789FAf18
      depositTokenAddressToYVault[0xdAC17F958D2ee523a2206206994597C13D831ec7] = 0x2f08119C6f07c006695E079AAFc638b8789FAf18;
      yVaultToDepositTokenAddress[0x2f08119C6f07c006695E079AAFc638b8789FAf18] = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
    }

    function() external payable { }

    // Prod addresses
    // address constant public transfersProxy = 0x882d80D3a191859d64477eb78Cca46599307ec1C;
    // address payable constant public rebalancingSetExchangeIssuanceModule = 0xde697AC56b1bD9C0987c08911020f2341e274813;
    // address payable constant public rebalancingSetIssuanceModule = 0xDA6786379FF88729264d31d472FA917f5E561443;
    // address payable constant public cTokenaddressToAddressWhiteList = 0x5BA020a8835ed40936941562bdF42D66F65fc00f;

    function getTransfersProxy() public view returns (address) {
      return 0x882d80D3a191859d64477eb78Cca46599307ec1C;
    }

    function getRebalancingSetExchangeIssuanceModule() public view returns (address payable) {
      return 0xde697AC56b1bD9C0987c08911020f2341e274813;
    }

    function getRebalancingSetIssuanceModule() public view returns (address payable) {
      return 0xDA6786379FF88729264d31d472FA917f5E561443;
    }

    function getCTokenaddressToAddressWhiteList() public view returns (address payable) {
      return 0x5BA020a8835ed40936941562bdF42D66F65fc00f;
    }

    function getWrappedEthInstance() public view returns (IWETH) {
      return IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    }

    /**
     * Withdraw all ERC20 tokens from this contract to the caller address. Only callable by the smart contract owner.
     */
    function withdrawAllAdmin(address [] calldata _tokenAddresses)
      external
      nonReentrant
      onlyWhitelistAdmin
    {
      for (uint i=0; i<_tokenAddresses.length; i++) {
          uint256 tokenBalance = ERC20Wrapper.balanceOf(_tokenAddresses[i], address(this));
          ERC20Wrapper.transfer(
              _tokenAddresses[i],
              msg.sender,
              tokenBalance
          );
      }
    }

    /**
     * Withdraw an ERC20 token to the owner's account.
     */
    function withdrawERC20(address _tokenAddress, address _to, uint256 _withdrawalAmount)
      external
      nonReentrant
      onlyWhitelisted
    {
      uint256 tokenBalance = ERC20Wrapper.balanceOf(_tokenAddress, address(this));
      require(tokenBalance >= _withdrawalAmount, "Withdrawal amount is too high");
      require(this.isWhitelistAdmin(_to), "You can only withdraw to an owner address");
      ERC20Wrapper.transfer(
          _tokenAddress,
          _to,
          _withdrawalAmount
      );
    }

    /**
     * Withdraw ETH to the owner's account.
     */
    function withdrawETH(address payable _to, uint256 _withdrawalAmount)
      external
      nonReentrant
      onlyWhitelisted
    {
      uint256 ethBalance = address(this).balance;
      require(ethBalance >= _withdrawalAmount, "Withdrawal amount is too high");
      require(this.isWhitelistAdmin(_to), "You can only withdraw to an owner address");
      _to.transfer(_withdrawalAmount);
    }

    function _checkReplayAttack(
       uint256 _nonce
    )
      private
      nonReentrant
    {
      require(!nonces[_nonce], "Replay attack detected.");
      nonces[_nonce] = true;
    }

    /**
     * Get the signer of vault deposit.
     */
    function getVaultDepositSigner(address _vaultAddress, address _tokenDepositAddress, uint256 _nonce, bytes memory _signature) private view returns (address) {
      bytes32 msgHash = OpenZeppelinUpgradesECDSA.toEthSignedMessageHash(
      keccak256(
          abi.encodePacked(
            _vaultAddress,
            _tokenDepositAddress,
            _nonce,
            address(this)
          )
        )
      );
      return OpenZeppelinUpgradesECDSA.recover(msgHash, _signature);
    }

    function vaultDeposit(
      address _vaultAddress,
      address _tokenDepositAddress,
      uint256 _amount,
      uint256 _nonce,
      bytes memory _signature
    )
        public
        nonReentrant
        discountChiGasSaver
    {

      _checkReplayAttack(_nonce);
      require(depositTokenAddressToYVault[_tokenDepositAddress] == _vaultAddress, "Deposit token and vault not supported");
      address signer = getVaultDepositSigner(_vaultAddress, _tokenDepositAddress, _nonce, _signature);
      require(signer != address(0), "Invalid signature");
      require(isWhitelistAdmin(signer), "signer is not an admin");

      uint256 tokenDepositBalance = ERC20Wrapper.balanceOf(_tokenDepositAddress, address(this));
      require(tokenDepositBalance >= _amount, "Token deposit balance is too low");

      // Approve the vault to spend the SMA's owner's deposit token's
      ERC20Wrapper.approve(_tokenDepositAddress, _vaultAddress, _amount);
      // Deposit tokens to the vault
      IVault(_vaultAddress).deposit(_amount);
    }


    /**
     * Get the signer of a vault withdrawal.
     */
    function getVaultWithdrawalSigner(address _vaultAddress, address payable _to, uint256 _nonce, bytes memory _signature) private view returns (address) {
      bytes32 msgHash = OpenZeppelinUpgradesECDSA.toEthSignedMessageHash(
      keccak256(
          abi.encodePacked(
            _vaultAddress,
            _to,
            _nonce,
            address(this)
          )
        )
      );
      return OpenZeppelinUpgradesECDSA.recover(msgHash, _signature);
    }

    function vaultWithdraw(
      address _vaultAddress,
      uint shares,
      address payable _to,
      uint256 _nonce,
      bytes memory _signature
    )
        public
        nonReentrant
        discountChiGasSaver
    {

      _checkReplayAttack(_nonce);
      address depositTokenAddress = yVaultToDepositTokenAddress[_vaultAddress];
      require(depositTokenAddress != address(0), "Vault not supported");

      address signer = getVaultWithdrawalSigner(_vaultAddress, _to, _nonce, _signature);
      require(signer != address(0), "Invalid signature");
      require(isWhitelistAdmin(signer), "signer is not an admin");

      // Get current balance of the deposit token before withdrawal
      uint256 depositTokenBalanceBeforeWithdrawal = ERC20Wrapper.balanceOf(depositTokenAddress, address(this));
      // Withdraw shares of the vault to deposit token
      IVault(_vaultAddress).withdraw(shares);

      // Compute how many deposit tokens were withdrawn
      uint256 depositTokenWithdrawn = ERC20Wrapper.balanceOf(depositTokenAddress, address(this)).sub(depositTokenBalanceBeforeWithdrawal);
      require(depositTokenWithdrawn > 0, "No token withdrawn");

      // Transfer withdrawn tokens to address passed as parameter
      ERC20Wrapper.transfer(
          depositTokenAddress,
          _to,
          depositTokenWithdrawn
      );

    }

}
/**
 * @title SmartWallet
 * @author Ember Fund
 *
 * Implementation of the Ember Fund smart wallet adding meta transaction capabilities for issuing and redeeming rebalancing sets.
 * The smart wallet is owned by an EOA that can white list accounts facilitating the management of any ERC20 tokens stored in the smart wallet.
 */
function initialize(address _owner) public initializer {
  WhitelistedRoleMeta.initialize(_owner);
  ReentrancyGuardEmber.initialize();

  // Add more deposit to vault mapping to support more vaults
  // USDT 0xdAC17F958D2ee523a2206206994597C13D831ec7
  // yEarn USDT 0x2f08119C6f07c006695E079AAFc638b8789FAf18
  depositTokenAddressToYVault[0xdAC17F958D2ee523a2206206994597C13D831ec7] = 0x2f08119C6f07c006695E079AAFc638b8789FAf18;
  yVaultToDepositTokenAddress[0x2f08119C6f07c006695E079AAFc638b8789FAf18] = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
}

/**
 * Initialize the smart contract with an owner.
 */