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

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

    IBalancer private BalWBTCPool = IBalancer(
        0x1efF8aF5D577060BA4ac8A29A13525bb0Ee2A3D5
    );

    address
        private constant wethToken = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    address
        private constant wbtcToken = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;

    address
        public intermediateStable = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;

    uint256
        private constant deadline = 0xf000000000000000000000000000000000000000000000000000000000000000;

    struct Pool {
        address swapAddress;
        address tokenAddress;
        address[4] poolTokens;
        bool isMetaPool;
    }

    mapping(address => Pool) public curvePools;
    mapping(address => address) private metaPools; //Token address => swap address

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

    /**
    @notice This function adds liquidity to a Curve pool with ETH or ERC20 tokens
    @param toWhomToIssue The address to return the Curve LP tokens to
    @param fromToken The ERC20 token used for investment (address(0x00) if ether)
    @param swapAddress Curve swap address for the pool
    @param incomingTokenQty The amount of fromToken to invest
    @param minPoolTokens The minimum acceptable quantity of tokens to receive. Reverts otherwise
    @return Amount of Curve LP tokens received
     */
    function ZapIn(
        address toWhomToIssue,
        address fromToken,
        address swapAddress,
        uint256 incomingTokenQty,
        uint256 minPoolTokens
    )
        external
        payable
        stopInEmergency
        nonReentrant
        returns (uint256 crvTokensBought)
    {
        uint256 toInvest;
        if (fromToken == address(0)) {
            require(msg.value > 0, "Error: ETH not sent");
            toInvest = msg.value;
        } else {
            require(msg.value == 0, "Error: ETH sent");
            require(incomingTokenQty > 0, "Error: Invalid ERC amount");
            IERC20(fromToken).safeTransferFrom(
                msg.sender,
                address(this),
                incomingTokenQty
            );
            toInvest = incomingTokenQty;
        }
        (bool isUnderlying, uint8 underlyingIndex) = _isUnderlyingToken(
            swapAddress,
            fromToken
        );
        if (isUnderlying) {
            crvTokensBought = _enterCurve(
                swapAddress,
                toInvest,
                underlyingIndex
            );
        } else {
            (uint256 tokensBought, uint8 index) = _getIntermediate(
                swapAddress,
                fromToken,
                toInvest
            );
            crvTokensBought = _enterCurve(swapAddress, tokensBought, index);
        }
        require(
            crvTokensBought > minPoolTokens,
            "Received less than minPoolTokens"
        );

        address poolTokenAddress = curvePools[swapAddress].tokenAddress;
        uint256 goodwillPortion;
        if (goodwill > 0) {
            goodwillPortion = SafeMath.div(
                SafeMath.mul(crvTokensBought, goodwill),
                10000
            );
            IERC20(poolTokenAddress).safeTransfer(
                zgoodwillAddress,
                goodwillPortion
            );
        }
        IERC20(poolTokenAddress).transfer(
            toWhomToIssue,
            SafeMath.sub(crvTokensBought, goodwillPortion)
        );
    }

    /**
    @notice This function swaps to an appropriate intermediate token to be used to add liquidity
    @param swapAddress Curve swap address for the pool
    @param fromToken The ERC20 token used for investment (address(0x00) if ether)
    @param amount The amount of fromToken to invest
    @return Amount of tokens (or LP) bought, token index for add_liquidity call
     */
    function _getIntermediate(
        address swapAddress,
        address fromToken,
        uint256 amount
    ) internal returns (uint256 tokensBought, uint8 index) {
        Pool memory pool2Enter = curvePools[swapAddress];
        address[4] memory poolTokens = pool2Enter.poolTokens;
        if (pool2Enter.isMetaPool) {
            for (uint8 i = 0; i < 4; i++) {
                if (metaPools[poolTokens[i]] != address(0)) {
                    address intermediateSwapAddress = metaPools[poolTokens[i]];
                    (
                        bool isUnderlying,
                        uint8 underlyingIndex
                    ) = _isUnderlyingToken(intermediateSwapAddress, fromToken);
                    if (isUnderlying) {
                        tokensBought = _enterCurve(
                            intermediateSwapAddress,
                            amount,
                            underlyingIndex
                        );
                        return (tokensBought, i);
                    }
                    uint256 intermediateTokenBought;
                    if (_isBtcPool(intermediateSwapAddress)) {
                        intermediateTokenBought = _token2Token(
                            fromToken,
                            wbtcToken,
                            amount
                        );
                        (, index) = _isUnderlyingToken(
                            intermediateSwapAddress,
                            wbtcToken
                        );
                    } else {
                        intermediateTokenBought = _token2Token(
                            fromToken,
                            intermediateStable,
                            amount
                        );
                        (, index) = _isUnderlyingToken(
                            intermediateSwapAddress,
                            intermediateStable
                        );
                    }
                    tokensBought = _enterCurve(
                        intermediateSwapAddress,
                        intermediateTokenBought,
                        index
                    );
                    return (tokensBought, i);
                }
            }
        } else {
            if (_isBtcPool(swapAddress)) {
                tokensBought = _token2Token(fromToken, wbtcToken, amount);
                (, index) = _isUnderlyingToken(swapAddress, wbtcToken);
                return (tokensBought, index);
            }
            tokensBought = _token2Token(fromToken, intermediateStable, amount);
            (, index) = _isUnderlyingToken(swapAddress, intermediateStable);
        }
    }

    /**
    @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 == address(0)) {
            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 {
            IERC20(fromToken).safeIncreaseAllowance(
                address(uniswapRouter),
                tokens2Trade
            );
            if (fromToken != 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] = 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 adds liquidity to a curve pool
    @param swapAddress Curve swap address for the pool
    @param amount The quantity of tokens being added as liquidity
    @param index The token index for the add_liquidity call
    @return tokenBought The quantity of curve LP tokens received
    */
    function _enterCurve(
        address swapAddress,
        uint256 amount,
        uint8 index
    ) internal returns (uint256 crvTokensBought) {
        address tokenAddress = curvePools[swapAddress].tokenAddress;
        uint256 iniTokenBal = IERC20(tokenAddress).balanceOf(address(this));
        address entryToken = curvePools[swapAddress].poolTokens[index];
        IERC20(entryToken).safeIncreaseAllowance(address(swapAddress), amount);
        uint256 numTokens = _getNumTokens(swapAddress);
        if (numTokens == 4) {
            uint256[4] memory amounts;
            amounts[index] = amount;
            ICurveSwap(swapAddress).add_liquidity(amounts, 0);
        } else if (numTokens == 3) {
            uint256[3] memory amounts;
            amounts[index] = amount;
            ICurveSwap(swapAddress).add_liquidity(amounts, 0);
        } else {
            uint256[2] memory amounts;
            amounts[index] = amount;
            ICurveSwap(swapAddress).add_liquidity(amounts, 0);
        }
        crvTokensBought = (IERC20(tokenAddress).balanceOf(address(this))).sub(
            iniTokenBal
        );
    }

    /**
    @notice This function checks if the curve pool contains WBTC
    @param swapAddress Curve swap address for the pool
    @return true if the pool contains WBTC, false otherwise
    */
    function _isBtcPool(address swapAddress) internal view returns (bool) {
        address[4] memory poolTokens = getPoolTokens(swapAddress);
        for (uint8 i = 0; i < 4; i++) {
            if (poolTokens[i] == wbtcToken) return true;
        }
        return false;
    }

    function _getNumTokens(address swapAddress)
        internal
        view
        returns (uint256 numTokens)
    {
        address[4] memory poolTokens = getPoolTokens(swapAddress);
        if (poolTokens[2] == address(0)) return 2;
        if (poolTokens[3] == address(0)) return 3;
        return 4;
    }

    function _isUnderlyingToken(
        address swapAddress,
        address fromTokenContractAddress
    ) internal view returns (bool, uint8) {
        address[4] memory poolTokens = getPoolTokens(swapAddress);
        for (uint8 i = 0; i < 4; i++) {
            if (poolTokens[i] == address(0)) return (false, 0);
            if (poolTokens[i] == fromTokenContractAddress) return (true, i);
        }
    }

    /**
    @notice This function adds a new supported pool
    @param swapAddress Curve swap address for the pool
    @param tokenAddress Curve token address for the pool
    @param poolTokens token (or LP) contract addresses of underlying tokens
    @dev poolTokens should be unwrapped tokens (e.g. DAI not yDAI)
    @dev poolTokens should use 0 address for pools with < 4 tokens
    @param isMetaPool true if pool contains a curve LP token as a pool token
    */
    function addPool(
        address swapAddress,
        address tokenAddress,
        address[4] calldata poolTokens,
        bool isMetaPool
    ) external onlyOwner {
        require(
            curvePools[swapAddress].swapAddress == address(0),
            "Pool exists"
        );
        Pool memory newPool = Pool(
            swapAddress,
            tokenAddress,
            poolTokens,
            isMetaPool
        );
        curvePools[swapAddress] = newPool;
        metaPools[tokenAddress] = swapAddress;
    }

    /**
    @notice This function updates an existing supported pool
    @param swapAddress Curve swap address for the pool
    @param tokenAddress Curve token address for the pool
    @param poolTokens token (or LP) contract addresses of underlying tokens
    @dev poolTokens should be unwrapped tokens (e.g. DAI not yDAI)
    @dev poolTokens should use 0 address for pools with < 4 tokens
    @param isMetaPool true if pool contains a curve LP token as a pool token
    */
    function updatePool(
        address swapAddress,
        address tokenAddress,
        address[4] calldata poolTokens,
        bool isMetaPool
    ) external onlyOwner {
        require(
            curvePools[swapAddress].swapAddress == swapAddress,
            "Pool doesn't exist"
        );
        Pool storage pool2Update = curvePools[swapAddress];
        pool2Update.tokenAddress = tokenAddress;
        pool2Update.poolTokens = poolTokens;
        pool2Update.isMetaPool = isMetaPool;
        metaPools[tokenAddress] = swapAddress;
    }

    /**
    @notice This function returns an array of underlying pool token addresses
    @param swapAddress Curve swap address for the pool
    @return returns a 4 element array containing the addresses of the pool tokens (0 address if pool contains < 4 tokens)
    */
    function getPoolTokens(address swapAddress)
        public
        view
        returns (address[4] memory poolTokens)
    {
        poolTokens = curvePools[swapAddress].poolTokens;
    }

    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;
    }

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

    // - 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() external payable {
        require(msg.sender != tx.origin, "Do not send ETH directly");
    }
}
function updatePool(
    address swapAddress,
    address tokenAddress,
    address[4] calldata poolTokens,
    bool isMetaPool
) external onlyOwner {
    require(
        curvePools[swapAddress].swapAddress == swapAddress,
        "Pool doesn't exist"
    );
    Pool storage pool2Update = curvePools[swapAddress];
    pool2Update.tokenAddress = tokenAddress;
    pool2Update.poolTokens = poolTokens;
    pool2Update.isMetaPool = isMetaPool;
    metaPools[tokenAddress] = swapAddress;
}

/**
 @notice This function updates an existing supported pool
 @param swapAddress Curve swap address for the pool
 @param tokenAddress Curve token address for the pool
 @param poolTokens token (or LP) contract addresses of underlying tokens
 @dev poolTokens should be unwrapped tokens (e.g. DAI not yDAI)
 @dev poolTokens should use 0 address for pools with < 4 tokens
 @param isMetaPool true if pool contains a curve LP token as a pool token
 */