contract Curve_ZapOut_General_V2_1 is ReentrancyGuard, Ownable {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;
    bool public stopped = false;
    uint16 public goodwill = 0;
    address
        public zgoodwillAddress = 0x3CE37278de6388532C3949ce4e886F365B14fB56;

    IUniswapV2Factory
        private constant UniSwapV2FactoryAddress = IUniswapV2Factory(
        0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
    );
    IUniswapRouter02 private constant uniswapRouter = IUniswapRouter02(
        0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
    );

    ICurveRegistry public curveReg;

    address
        private constant wethToken = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
    address
        private constant wbtcToken = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
    address
        public intermediateStable = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    uint256
        private constant deadline = 0xf000000000000000000000000000000000000000000000000000000000000000;

    // circuit breaker modifiers
    modifier stopInEmergency {
        if (stopped) {
            revert("Temporarily Paused");
        } else {
            _;
        }
    }

    constructor(ICurveRegistry _curveRegistry) public {
        curveReg = _curveRegistry;
    }

    function ZapOut(
        address payable toWhomToIssue,
        address swapAddress,
        uint256 incomingCrv,
        address toToken,
        uint256 minToTokens
    ) external stopInEmergency returns (uint256 ToTokensBought) {
        address poolTokenAddress = curveReg.getTokenAddress(swapAddress);
        uint256 goodwillPortion;
        if (goodwill > 0) {
            goodwillPortion = SafeMath.div(
                SafeMath.mul(incomingCrv, goodwill),
                10000
            );
            IERC20(poolTokenAddress).safeTransferFrom(
                msg.sender,
                zgoodwillAddress,
                goodwillPortion
            );
        }
        IERC20(poolTokenAddress).safeTransferFrom(
            msg.sender,
            address(this),
            SafeMath.sub(incomingCrv, goodwillPortion)
        );

        (bool isUnderlying, uint8 underlyingIndex) = curveReg.isUnderlyingToken(
            swapAddress,
            toToken
        );

        if (isUnderlying) {
            ToTokensBought = _exitCurve(
                swapAddress,
                incomingCrv,
                underlyingIndex
            );
        } else if (curveReg.isMetaPool(swapAddress)) {
            address[4] memory poolTokens = curveReg.getPoolTokens(swapAddress);
            address intermediateSwapAddress;
            uint8 i;
            for (; i < 4; i++) {
                if (curveReg.metaPools(poolTokens[i]) != address(0)) {
                    intermediateSwapAddress = curveReg.metaPools(poolTokens[i]);
                    break;
                }
            }
            // _exitCurve to intermediateSwapAddress Token
            uint256 intermediateBought = _exitCurve(
                swapAddress,
                incomingCrv,
                i
            );
            // _performZapOut: fromPool = intermediateSwapAddress
            ToTokensBought = _performZapOut(
                intermediateSwapAddress,
                intermediateBought,
                toToken
            );
        } else {
            ToTokensBought = _performZapOut(swapAddress, incomingCrv, toToken);
        }

        require(ToTokensBought >= minToTokens, "High Slippage");
        if (toToken == address(0)) {
            Address.sendValue(toWhomToIssue, ToTokensBought);
        } else {
            IERC20(toToken).safeTransfer(toWhomToIssue, ToTokensBought);
        }
    }

    function _performZapOut(
        address swapAddress,
        uint256 incomingCrv,
        address toToken
    ) internal returns (uint256 ToTokensBought) {
        if (curveReg.isBtcPool(swapAddress)) {
            (, uint8 wbtcIndex) = curveReg.isUnderlyingToken(
                swapAddress,
                wbtcToken
            );
            uint256 intermediateBought = _exitCurve(
                swapAddress,
                incomingCrv,
                wbtcIndex
            );

            ToTokensBought = _token2Token(
                wbtcToken,
                toToken,
                intermediateBought
            );
        } else {
            (
                bool isUnderlyingIntermediate,
                uint8 intermediateStableIndex
            ) = curveReg.isUnderlyingToken(swapAddress, intermediateStable);

            require(
                isUnderlyingIntermediate,
                "Pool does not support intermediate"
            );

            uint256 intermediateBought = _exitCurve(
                swapAddress,
                incomingCrv,
                intermediateStableIndex
            );

            ToTokensBought = _token2Token(
                intermediateStable,
                toToken,
                intermediateBought
            );
        }
    }

    function _exitCurve(
        address swapAddress,
        uint256 incomingCrv,
        uint256 index
    ) internal returns (uint256 tokensReceived) {
        address exitTokenAddress = curveReg.getPoolTokens(swapAddress)[index];
        uint256 iniTokenBal = IERC20(exitTokenAddress).balanceOf(address(this));

        address tokenAddress = curveReg.getTokenAddress(swapAddress);
        IERC20(tokenAddress).safeApprove(swapAddress, 0);
        IERC20(tokenAddress).safeApprove(swapAddress, incomingCrv);
        ICurveSwap(swapAddress).remove_liquidity_one_coin(
            incomingCrv,
            int128(index),
            0
        );

        tokensReceived = (IERC20(exitTokenAddress).balanceOf(address(this)))
            .sub(iniTokenBal);
    }

    /**
    @notice This function is used to swap ETH/ERC20 <> ETH/ERC20
    @param fromToken The token address to swap from. (0x00 for ETH)
    @param toToken The token address to swap to. (0x00 for ETH)
    @param tokens2Trade The amount of tokens to swap
    @return tokenBought The quantity of tokens bought
    */
    function _token2Token(
        address fromToken,
        address toToken,
        uint256 tokens2Trade
    ) internal returns (uint256 tokenBought) {
        if (fromToken == toToken) {
            return tokens2Trade;
        }

        if (fromToken == address(0)) {
            if (toToken == wethToken) {
                IWETH(wethToken).deposit.value(tokens2Trade)();
                return tokens2Trade;
            }

            address[] memory path = new address[](2);
            path[0] = wethToken;
            path[1] = toToken;
            tokenBought = uniswapRouter.swapExactETHForTokens.value(
                tokens2Trade
            )(1, path, address(this), deadline)[path.length - 1];
        } else if (toToken == address(0)) {
            if (fromToken == wethToken) {
                IWETH(wethToken).withdraw(tokens2Trade);
                return tokens2Trade;
            }

            IERC20(fromToken).safeApprove(address(uniswapRouter), tokens2Trade);

            address[] memory path = new address[](2);
            path[0] = fromToken;
            path[1] = wethToken;
            tokenBought = uniswapRouter.swapExactTokensForETH(
                tokens2Trade,
                1,
                path,
                address(this),
                deadline
            )[path.length - 1];
        } else {
            IERC20(fromToken).safeApprove(address(uniswapRouter), tokens2Trade);

            if (fromToken != wethToken) {
                if (toToken != wethToken) {
                    // check output via tokenA -> tokenB
                    address pairA = UniSwapV2FactoryAddress.getPair(
                        fromToken,
                        toToken
                    );
                    address[] memory pathA = new address[](2);
                    pathA[0] = fromToken;
                    pathA[1] = toToken;
                    uint256 amtA;
                    if (pairA != address(0)) {
                        amtA = uniswapRouter.getAmountsOut(
                            tokens2Trade,
                            pathA
                        )[1];
                    }

                    // check output via tokenA -> weth -> tokenB
                    address[] memory pathB = new address[](3);
                    pathB[0] = fromToken;
                    pathB[1] = wethToken;
                    pathB[2] = toToken;

                    uint256 amtB = uniswapRouter.getAmountsOut(
                        tokens2Trade,
                        pathB
                    )[2];

                    if (amtA >= amtB) {
                        tokenBought = uniswapRouter.swapExactTokensForTokens(
                            tokens2Trade,
                            1,
                            pathA,
                            address(this),
                            deadline
                        )[pathA.length - 1];
                    } else {
                        tokenBought = uniswapRouter.swapExactTokensForTokens(
                            tokens2Trade,
                            1,
                            pathB,
                            address(this),
                            deadline
                        )[pathB.length - 1];
                    }
                } else {
                    address[] memory path = new address[](2);
                    path[0] = fromToken;
                    path[1] = wethToken;

                    tokenBought = uniswapRouter.swapExactTokensForTokens(
                        tokens2Trade,
                        1,
                        path,
                        address(this),
                        deadline
                    )[path.length - 1];
                }
            } else {
                address[] memory path = new address[](2);
                path[0] = wethToken;
                path[1] = toToken;
                tokenBought = uniswapRouter.swapExactTokensForTokens(
                    tokens2Trade,
                    1,
                    path,
                    address(this),
                    deadline
                )[path.length - 1];
            }
        }
        require(tokenBought > 0, "Error Swapping Tokens");
    }

    function updateCurveRegistry(ICurveRegistry newCurveRegistry)
        external
        onlyOwner
    {
        require(newCurveRegistry != curveReg, "Already using this Registry");
        curveReg = newCurveRegistry;
    }

    function inCaseTokengetsStuck(IERC20 _TokenAddress) external onlyOwner {
        uint256 qty = _TokenAddress.balanceOf(address(this));
        IERC20(_TokenAddress).safeTransfer(_owner, qty);
    }

    function set_new_goodwill(uint16 _new_goodwill) external onlyOwner {
        require(
            _new_goodwill >= 0 && _new_goodwill < 10000,
            "GoodWill Value not allowed"
        );
        goodwill = _new_goodwill;
    }

    function set_new_zgoodwillAddress(address _new_zgoodwillAddress)
        external
        onlyOwner
    {
        zgoodwillAddress = _new_zgoodwillAddress;
    }

    // - to Pause the contract
    function toggleContractActive() external onlyOwner {
        stopped = !stopped;
    }

    // - to withdraw any ETH balance sitting in the contract
    function withdraw() external onlyOwner {
        _owner.transfer(address(this).balance);
    }

    function updateIntermediateStable(address newIntermediate)
        external
        onlyOwner
    {
        require(
            newIntermediate != intermediateStable,
            "Already using this intermediate"
        );
        intermediateStable = newIntermediate;
    }

    function() external payable {
        require(msg.sender != tx.origin, "Do not send ETH directly");
    }
}
function _token2Token(
    address fromToken,
    address toToken,
    uint256 tokens2Trade
) internal returns (uint256 tokenBought) {
    if (fromToken == toToken) {
        return tokens2Trade;
    }

    if (fromToken == address(0)) {
        if (toToken == wethToken) {
            IWETH(wethToken).deposit.value(tokens2Trade)();
            return tokens2Trade;
        }

        address[] memory path = new address[](2);
        path[0] = wethToken;
        path[1] = toToken;
        tokenBought = uniswapRouter.swapExactETHForTokens.value(
            tokens2Trade
        )(1, path, address(this), deadline)[path.length - 1];
    } else if (toToken == address(0)) {
        if (fromToken == wethToken) {
            IWETH(wethToken).withdraw(tokens2Trade);
            return tokens2Trade;
        }

        IERC20(fromToken).safeApprove(address(uniswapRouter), tokens2Trade);

        address[] memory path = new address[](2);
        path[0] = fromToken;
        path[1] = wethToken;
        tokenBought = uniswapRouter.swapExactTokensForETH(
            tokens2Trade,
            1,
            path,
            address(this),
            deadline
        )[path.length - 1];
    } else {
        IERC20(fromToken).safeApprove(address(uniswapRouter), tokens2Trade);

        if (fromToken != wethToken) {
            if (toToken != wethToken) {
                // check output via tokenA -> tokenB
                address pairA = UniSwapV2FactoryAddress.getPair(
                    fromToken,
                    toToken
                );
                address[] memory pathA = new address[](2);
                pathA[0] = fromToken;
                pathA[1] = toToken;
                uint256 amtA;
                if (pairA != address(0)) {
                    amtA = uniswapRouter.getAmountsOut(
                        tokens2Trade,
                        pathA
                    )[1];
                }

                // check output via tokenA -> weth -> tokenB
                address[] memory pathB = new address[](3);
                pathB[0] = fromToken;
                pathB[1] = wethToken;
                pathB[2] = toToken;

                uint256 amtB = uniswapRouter.getAmountsOut(
                    tokens2Trade,
                    pathB
                )[2];

                if (amtA >= amtB) {
                    tokenBought = uniswapRouter.swapExactTokensForTokens(
                        tokens2Trade,
                        1,
                        pathA,
                        address(this),
                        deadline
                    )[pathA.length - 1];
                } else {
                    tokenBought = uniswapRouter.swapExactTokensForTokens(
                        tokens2Trade,
                        1,
                        pathB,
                        address(this),
                        deadline
                    )[pathB.length - 1];
                }
            } else {
                address[] memory path = new address[](2);
                path[0] = fromToken;
                path[1] = wethToken;

                tokenBought = uniswapRouter.swapExactTokensForTokens(
                    tokens2Trade,
                    1,
                    path,
                    address(this),
                    deadline
                )[path.length - 1];
            }
        } else {
            address[] memory path = new address[](2);
            path[0] = wethToken;
            path[1] = toToken;
            tokenBought = uniswapRouter.swapExactTokensForTokens(
                tokens2Trade,
                1,
                path,
                address(this),
                deadline
            )[path.length - 1];
        }
    }
    require(tokenBought > 0, "Error Swapping Tokens");
}

/**
 @notice This function is used to swap ETH/ERC20 <> ETH/ERC20
 @param fromToken The token address to swap from. (0x00 for ETH)
 @param toToken The token address to swap to. (0x00 for ETH)
 @param tokens2Trade The amount of tokens to swap
 @return tokenBought The quantity of tokens bought
 */