contract Zapper_Swap_General_V1_1 is ReentrancyGuard, Ownable {
    using SafeMath for uint256;
    using Address for address;
    using SafeERC20 for IERC20;

    IUniswapRouter02 private constant uniswapRouter = IUniswapRouter02(
        0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
    );

    IAaveLendingPoolAddressesProvider
        private constant lendingPoolAddressProvider = IAaveLendingPoolAddressesProvider(
        0x24a42fD28C976A61Df5D00D0599C34c4f90748c8
    );

    IBFactory private constant BalancerFactory = IBFactory(
        0x9424B1412450D0f8Fc2255FAf6046b98213B76Bd
    );

    address private constant renBTCCurveSwapContract = address(
        0x93054188d876f558f4a66B2EF1d97d16eDf0895B
    );

    address private constant sBTCCurveSwapContract = address(
        0x7fC77b5c7614E1533320Ea6DDc2Eb61fa00A9714
    );

    IWETH private constant wethContract = IWETH(
        0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    );

    address private constant ETHAddress = address(
        0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE
    );

    uint256
        private constant deadline = 0xf000000000000000000000000000000000000000000000000000000000000000;

    mapping(address => address) public cToken;
    mapping(address => address) public yToken;
    mapping(address => address) public aToken;

    constructor() public {
        //mapping for cETH
        cToken[address(
            0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5
        )] = ETHAddress;
    }

    /**
		@notice This function adds c token addresses to a mapping
		@dev For cETH token, mapping is already added in constructor
    @param _cToken token address of c-token for given underlying asset
		 */
    function addCToken(address[] memory _cToken) public onlyOwner {
        for (uint256 i = 0; i < _cToken.length; i++) {
            cToken[_cToken[i]] = ICompound(_cToken[i]).underlying();
        }
    }

    /**
		@notice This function adds y token addresses to a mapping
    @param _yToken token address of y-token
		*/
    function addYToken(address[] memory _yToken) public onlyOwner {
        for (uint256 i = 0; i < _yToken.length; i++) {
            yToken[_yToken[i]] = IIearn(_yToken[i]).token();
        }
    }

    /**
		@notice This function adds a token addresses to a mapping
    @param _aToken token address of a-token for given underlying asset
		 */
    function addAToken(address[] memory _aToken) public onlyOwner {
        for (uint256 i = 0; i < _aToken.length; i++) {
            aToken[_aToken[i]] = IAToken(_aToken[i]).underlyingAssetAddress();
        }
    }

    /**
    @notice This function is used swap tokens using multiple exchanges
    @param toWhomToIssue address to which tokens should be sent after swap
		@param path token addresses indicating the conversion path
		@param amountIn amount of tokens to swap
    @param minTokenOut min amount of expected tokens
    @param withPool indicates the exchange and its sequence we want to swap from
    @param poolData pool or token addresses needed for swapping tokens according to the exchange
		@param starts indicates the index of poolData array for each swap
    @return amount of tokens received after swap
     */
    function MultiExchangeSwap(
        address payable toWhomToIssue,
        address[] calldata path,
        uint256 amountIn,
        uint256 minTokenOut,
        uint8[] calldata starts,
        uint8[] calldata withPool,
        address[] calldata poolData
    ) external payable nonReentrant returns (uint256 tokensBought) {
        require(toWhomToIssue != address(0), "Invalid receiver address");
        require(path[0] != path[path.length - 1], "Cannot swap same tokens");

        tokensBought = _swap(
            path,
            _getTokens(path[0], amountIn),
            starts,
            withPool,
            poolData
        );

        require(tokensBought >= minTokenOut, "High Slippage");
        _sendTokens(toWhomToIssue, path[path.length - 1], tokensBought);
    }

    //swap function
    function _swap(
        address[] memory path,
        uint256 tokensToSwap,
        uint8[] memory starts,
        uint8[] memory withPool,
        address[] memory poolData
    ) internal returns (uint256) {
        address _to;
        uint8 poolIndex = 0;
        address[] memory _poolData;
        address _from = path[starts[0]];

        for (uint256 index = 0; index < withPool.length; index++) {
            uint256 endIndex = index == withPool.length.sub(1)
                ? path.length - 1
                : starts[index + 1];

            _to = path[endIndex];

            {
                if (withPool[index] == 2) {
                    _poolData = _getPath(path, starts[index], endIndex + 1);
                } else {
                    _poolData = new address[](1);
                    _poolData[0] = poolData[poolIndex++];
                }
            }

            tokensToSwap = _swapFromPool(
                _from,
                _to,
                tokensToSwap,
                withPool[index],
                _poolData
            );

            _from = _to;
        }
        return tokensToSwap;
    }

    /**
    @notice This function is used swap tokens using multiple exchanges
    @param fromToken token addresses to swap from
		@param toToken token addresses to swap into
		@param amountIn amount of tokens to swap
    @param withPool indicates the exchange we want to swap from
    @param poolData pool or token addresses needed for swapping tokens according to the exchange
		@return amount of tokens received after swap
     */
    function _swapFromPool(
        address fromToken,
        address toToken,
        uint256 amountIn,
        uint256 withPool,
        address[] memory poolData
    ) internal returns (uint256) {
        require(fromToken != toToken, "Cannot swap same tokens");
        require(withPool <= 3, "Invalid Exchange");

        if (withPool == 1) {
            return
                _swapWithBalancer(poolData[0], fromToken, toToken, amountIn, 1);
        } else if (withPool == 2) {
            return
                _swapWithUniswapV2(fromToken, toToken, poolData, amountIn, 1);
        } else if (withPool == 3) {
            return _swapWithCurve(poolData[0], fromToken, toToken, amountIn, 1);
        }
    }

    /**
		@notice This function returns part of the given array 
    @param addresses address array to copy from
		@param _start start index
		@param _end end index
    @return addressArray copied from given array
		 */
    function _getPath(
        address[] memory addresses,
        uint256 _start,
        uint256 _end
    ) internal pure returns (address[] memory addressArray) {
        uint256 len = _end.sub(_start);
        require(len > 1, "ERR_UNIV2_PATH");
        addressArray = new address[](len);

        for (uint256 i = 0; i < len; i++) {
            if (
                addresses[_start + i] == address(0) ||
                addresses[_start + i] == ETHAddress
            ) {
                addressArray[i] = address(wethContract);
            } else {
                addressArray[i] = addresses[_start + i];
            }
        }
    }

    function _sendTokens(
        address payable toWhomToIssue,
        address token,
        uint256 amount
    ) internal {
        if (token == ETHAddress || token == address(0)) {
            toWhomToIssue.transfer(amount);
        } else {
            IERC20(token).safeTransfer(toWhomToIssue, amount);
        }
    }

    function _swapWithBalancer(
        address bpoolAddress,
        address fromToken,
        address toToken,
        uint256 amountIn,
        uint256 minTokenOut
    ) internal returns (uint256 tokenBought) {
        require(BalancerFactory.isBPool(bpoolAddress), "Invalid balancer pool");

        IBpool bpool = IBpool(bpoolAddress);
        require(bpool.isPublicSwap(), "Swap not allowed for this pool");

        address _to = toToken;
        if (fromToken == address(0)) {
            wethContract.deposit.value(amountIn)();
            fromToken = address(wethContract);
        } else if (toToken == address(0)) {
            _to = address(wethContract);
        }
        require(bpool.isBound(fromToken), "From Token not bound");
        require(bpool.isBound(_to), "To Token not bound");

        //approve it to exchange address
        IERC20(fromToken).safeApprove(bpoolAddress, amountIn);

        //swap tokens
        (tokenBought, ) = bpool.swapExactAmountIn(
            fromToken,
            amountIn,
            _to,
            minTokenOut,
            uint256(-1)
        );

        if (toToken == address(0)) {
            wethContract.withdraw(tokenBought);
        }
    }

    function _swapWithUniswapV2(
        address fromToken,
        address toToken,
        address[] memory path,
        uint256 amountIn,
        uint256 minTokenOut
    ) internal returns (uint256 tokenBought) {
        //unwrap & approve it to router contract
        uint256 tokensUnwrapped = amountIn;
        address _fromToken = fromToken;
        if (fromToken != address(0)) {
            (tokensUnwrapped, _fromToken) = _unwrap(fromToken, amountIn);
            IERC20(_fromToken).safeApprove(
                address(uniswapRouter),
                tokensUnwrapped
            );
        }

        //swap and transfer tokens
        if (fromToken == address(0)) {
            tokenBought = uniswapRouter.swapExactETHForTokens.value(
                tokensUnwrapped
            )(minTokenOut, path, address(this), deadline)[path.length - 1];
        } else if (toToken == address(0)) {
            tokenBought = uniswapRouter.swapExactTokensForETH(
                tokensUnwrapped,
                minTokenOut,
                path,
                address(this),
                deadline
            )[path.length - 1];
        } else {
            tokenBought = uniswapRouter.swapExactTokensForTokens(
                tokensUnwrapped,
                minTokenOut,
                path,
                address(this),
                deadline
            )[path.length - 1];
        }
    }

    function _swapWithCurve(
        address curveExchangeAddress,
        address fromToken,
        address toToken,
        uint256 amountIn,
        uint256 minTokenOut
    ) internal returns (uint256 tokenBought) {
        require(
            curveExchangeAddress != address(0),
            "ERR_Invaid_curve_exchange"
        );
        ICurve curveExchange = ICurve(curveExchangeAddress);

        (uint256 tokensUnwrapped, address _fromToken) = _unwrap(
            fromToken,
            amountIn
        );

        //approve it to exchange address
        IERC20(_fromToken).safeApprove(curveExchangeAddress, tokensUnwrapped);

        int128 i;
        int128 j;

        //swap tokens
        if (
            curveExchangeAddress == renBTCCurveSwapContract ||
            curveExchangeAddress == sBTCCurveSwapContract
        ) {
            int128 length = (curveExchangeAddress == renBTCCurveSwapContract)
                ? 2
                : 3;

            for (int128 index = 0; index < length; index++) {
                if (curveExchange.coins(index) == _fromToken) {
                    i = index;
                } else if (curveExchange.coins(index) == toToken) {
                    j = index;
                }
            }

            curveExchange.exchange(i, j, tokensUnwrapped, minTokenOut);
        } else {
            address compCurveSwapContract = address(
                0xA2B47E3D5c44877cca798226B7B8118F9BFb7A56
            );
            address usdtCurveSwapContract = address(
                0x52EA46506B9CC5Ef470C5bf89f17Dc28bB35D85C
            );

            int128 length = 4;
            if (curveExchangeAddress == compCurveSwapContract) {
                length = 2;
            } else if (curveExchangeAddress == usdtCurveSwapContract) {
                length = 3;
            }

            for (int128 index = 0; index < length; index++) {
                if (curveExchange.underlying_coins(index) == _fromToken) {
                    i = index;
                } else if (curveExchange.underlying_coins(index) == toToken) {
                    j = index;
                }
            }

            curveExchange.exchange_underlying(
                i,
                j,
                tokensUnwrapped,
                minTokenOut
            );
        }

        if (toToken == ETHAddress || toToken == address(0)) {
            tokenBought = address(this).balance;
        } else {
            tokenBought = IERC20(toToken).balanceOf(address(this));
        }
    }

    function unwrap(
        address payable _toWhomToIssue,
        address _FromTokenContractAddress,
        uint256 tokens2Trade,
        uint256 minTokens
    ) public returns (uint256 tokensUnwrapped, address toToken) {
        require(_toWhomToIssue != address(0), "Invalid receiver address");
        uint256 tokensToSwap = _getTokens(
            _FromTokenContractAddress,
            tokens2Trade
        );

        (tokensUnwrapped, toToken) = _unwrap(
            _FromTokenContractAddress,
            tokensToSwap
        );

        require(tokensUnwrapped >= minTokens, "High Slippage");

        //transfer
        _sendTokens(_toWhomToIssue, toToken, tokensUnwrapped);
    }

    function _unwrap(address _FromTokenContractAddress, uint256 tokens2Trade)
        internal
        returns (uint256 tokensUnwrapped, address toToken)
    {
        uint256 initialEthbalance = address(this).balance;
        if (cToken[_FromTokenContractAddress] != address(0)) {
            require(
                ICompoundToken(_FromTokenContractAddress).redeem(
                    tokens2Trade
                ) == 0,
                "Error in unwrapping"
            );
            toToken = cToken[_FromTokenContractAddress];
            if (toToken == ETHAddress) {
                tokensUnwrapped = address(this).balance;
                tokensUnwrapped = tokensUnwrapped.sub(initialEthbalance);
            } else {
                tokensUnwrapped = IERC20(toToken).balanceOf(address(this));
            }
        } else if (yToken[_FromTokenContractAddress] != address(0)) {
            IIearn(_FromTokenContractAddress).withdraw(tokens2Trade);
            toToken = IIearn(_FromTokenContractAddress).token();
            tokensUnwrapped = IERC20(toToken).balanceOf(address(this));
        } else if (aToken[_FromTokenContractAddress] != address(0)) {
            IAToken(_FromTokenContractAddress).redeem(tokens2Trade);
            toToken = IAToken(_FromTokenContractAddress)
                .underlyingAssetAddress();
            if (toToken == ETHAddress) {
                tokensUnwrapped = address(this).balance;
                tokensUnwrapped = tokensUnwrapped.sub(initialEthbalance);
            } else {
                tokensUnwrapped = IERC20(toToken).balanceOf(address(this));
            }
        } else {
            toToken = _FromTokenContractAddress;
            tokensUnwrapped = tokens2Trade;
        }
    }

    function wrap(
        address payable _toWhomToIssue,
        address _FromTokenContractAddress,
        address _ToTokenContractAddress,
        uint256 tokens2Trade,
        uint256 minTokens,
        uint256 _wrapInto
    ) public payable returns (uint256 tokensWrapped) {
        require(_toWhomToIssue != address(0), "Invalid receiver address");
        require(_wrapInto <= 3, "Invalid to Token");
        uint256 tokensToSwap = _getTokens(
            _FromTokenContractAddress,
            tokens2Trade
        );

        tokensWrapped = _wrap(
            _FromTokenContractAddress,
            _ToTokenContractAddress,
            tokensToSwap,
            _wrapInto
        );

        require(tokensWrapped >= minTokens, "High Slippage");

        //transfer tokens
        _sendTokens(_toWhomToIssue, _ToTokenContractAddress, tokensWrapped);
    }

    function _wrap(
        address _FromTokenContractAddress,
        address _ToTokenContractAddress,
        uint256 tokens2Trade,
        uint256 _wrapInto
    ) internal returns (uint256 tokensWrapped) {
        //weth
        if (_wrapInto == 0) {
            require(
                _FromTokenContractAddress == address(0),
                "Cannot wrap into WETH"
            );
            require(
                _ToTokenContractAddress == address(wethContract),
                "Invalid toToken"
            );

            wethContract.deposit.value(tokens2Trade)();
            return tokens2Trade;
        } else if (_wrapInto == 1) {
            //Compound
            if (_FromTokenContractAddress == address(0)) {
                ICompoundEther(_ToTokenContractAddress).mint.value(
                    tokens2Trade
                )();
            } else {
                IERC20(_FromTokenContractAddress).safeApprove(
                    address(_ToTokenContractAddress),
                    tokens2Trade
                );
                ICompoundToken(_ToTokenContractAddress).mint(tokens2Trade);
            }
        } else if (_wrapInto == 2) {
            //IEarn
            IERC20(_FromTokenContractAddress).safeApprove(
                address(_ToTokenContractAddress),
                tokens2Trade
            );
            IIearn(_ToTokenContractAddress).deposit(tokens2Trade);
        } else {
            // Aave
            if (_FromTokenContractAddress == address(0)) {
                IAaveLendingPool(lendingPoolAddressProvider.getLendingPool())
                    .deposit
                    .value(tokens2Trade)(ETHAddress, tokens2Trade, 0);
            } else {
                //approve lending pool core
                IERC20(_FromTokenContractAddress).safeApprove(
                    address(lendingPoolAddressProvider.getLendingPoolCore()),
                    tokens2Trade
                );

                //get lending pool and call deposit
                IAaveLendingPool(lendingPoolAddressProvider.getLendingPool())
                    .deposit(_FromTokenContractAddress, tokens2Trade, 0);
            }
        }
        tokensWrapped = IERC20(_ToTokenContractAddress).balanceOf(
            address(this)
        );
    }

    function _getTokens(address token, uint256 amount)
        internal
        returns (uint256)
    {
        if (token == address(0)) {
            require(msg.value > 0, "No eth sent");
            return msg.value;
        }
        require(amount > 0, "Invalid token amount");
        require(msg.value == 0, "Eth sent with token");

        //transfer token
        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
        return amount;
    }

    function() external payable {
        require(msg.sender != tx.origin, "Do not send ETH directly");
    }
}
function _swapFromPool(
    address fromToken,
    address toToken,
    uint256 amountIn,
    uint256 withPool,
    address[] memory poolData
) internal returns (uint256) {
    require(fromToken != toToken, "Cannot swap same tokens");
    require(withPool <= 3, "Invalid Exchange");

    if (withPool == 1) {
        return
            _swapWithBalancer(poolData[0], fromToken, toToken, amountIn, 1);
    } else if (withPool == 2) {
        return
            _swapWithUniswapV2(fromToken, toToken, poolData, amountIn, 1);
    } else if (withPool == 3) {
        return _swapWithCurve(poolData[0], fromToken, toToken, amountIn, 1);
    }
}

/**
 @notice This function is used swap tokens using multiple exchanges
 @param fromToken token addresses to swap from
 		@param toToken token addresses to swap into
 		@param amountIn amount of tokens to swap
 @param withPool indicates the exchange we want to swap from
 @param poolData pool or token addresses needed for swapping tokens according to the exchange
 		@return amount of tokens received after swap
 */