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

    address
        private constant ETHToken = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

    ICurveRegistry public curveReg;

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

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

    event zapIn(address sender, address pool, uint256 crvTokens);

    /**
    @notice This function adds liquidity to a Curve pool with ETH or ERC20 tokens
    @param _fromTokenAddress The token used for entry (address(0) if ether)
    @param _toTokenAddress The intermediate ERC20 token to swap to
    @notice use _fromTokenAddress for _toTokenAddress if entry token is already a member of the pool
    @param _swapAddress Curve swap address for the pool
    @param _incomingTokenQty The amount of fromToken to invest
    @param _minPoolTokens The minimum acceptable quantity of Curve LP to receive. Reverts otherwise
    @param _allowanceTarget Spender for the first swap
    @param _swapTarget Excecution target for the first swap
    @param _swapCallData DEX quote data
    @return crvTokensBought- Quantity of Curve LP tokens received
     */
    function ZapIn(
        address _fromTokenAddress,
        address _toTokenAddress,
        address _swapAddress,
        uint256 _incomingTokenQty,
        uint256 _minPoolTokens,
        address _allowanceTarget,
        address _swapTarget,
        bytes calldata _swapCallData
    )
        external
        payable
        stopInEmergency
        nonReentrant
        returns (uint256 crvTokensBought)
    {
        if (_fromTokenAddress == address(0)) {
            require(msg.value > 0, "Error: ETH not sent");
            _incomingTokenQty = msg.value;
            _fromTokenAddress = ETHToken;
        } else {
            require(msg.value == 0, "Error: ETH sent");
            require(_incomingTokenQty > 0, "Error: Invalid ERC amount");
            IERC20(_fromTokenAddress).safeTransferFrom(
                msg.sender,
                address(this),
                _incomingTokenQty
            );
        }

        //perform zapIn
        crvTokensBought = _performZapIn(
            _fromTokenAddress,
            _toTokenAddress,
            _swapAddress,
            _incomingTokenQty,
            _allowanceTarget,
            _swapTarget,
            _swapCallData
        );

        require(
            crvTokensBought > _minPoolTokens,
            "Received less than minPoolTokens"
        );

        address poolTokenAddress = curveReg.getTokenAddress(_swapAddress);
        uint256 goodwillPortion;

        emit zapIn(msg.sender, poolTokenAddress, crvTokensBought);

        //transfer goodwill
        if (goodwill > 0) {
            goodwillPortion = SafeMath.div(
                SafeMath.mul(crvTokensBought, goodwill),
                10000
            );
            IERC20(poolTokenAddress).safeTransfer(
                zgoodwillAddress,
                goodwillPortion
            );
        }

        //transfer crvTokens to msg.sender
        IERC20(poolTokenAddress).transfer(
            msg.sender,
            SafeMath.sub(crvTokensBought, goodwillPortion)
        );

        return SafeMath.sub(crvTokensBought, goodwillPortion);
    }

    function _performZapIn(
        address _fromTokenAddress,
        address _toTokenAddress,
        address _swapAddress,
        uint256 toInvest,
        address _allowanceTarget,
        address _swapTarget,
        bytes memory _swapCallData
    ) internal returns (uint256 crvTokensBought) {
        (bool isUnderlying, uint8 underlyingIndex) = curveReg.isUnderlyingToken(
            _swapAddress,
            _fromTokenAddress
        );

        if (isUnderlying) {
            crvTokensBought = _enterCurve(
                _swapAddress,
                toInvest,
                underlyingIndex
            );
        } else {
            //swap tokens using 0x swap
            uint256 tokensBought = _fillQuote(
                _fromTokenAddress,
                _toTokenAddress,
                toInvest,
                _allowanceTarget,
                _swapTarget,
                _swapCallData
            );

            //get underlying token index
            (isUnderlying, underlyingIndex) = curveReg.isUnderlyingToken(
                _swapAddress,
                _toTokenAddress
            );

            if (isUnderlying) {
                crvTokensBought = _enterCurve(
                    _swapAddress,
                    tokensBought,
                    underlyingIndex
                );
            } else {
                (uint256 tokens, uint8 metaIndex) = _enterMetaPool(
                    _swapAddress,
                    _toTokenAddress,
                    tokensBought
                );

                crvTokensBought = _enterCurve(_swapAddress, tokens, metaIndex);
            }
        }
    }

    /**
    @notice This function gets adds the liquidity for meta pools and returns the token index and swap tokens
    @param _swapAddress Curve swap address for the pool
    @param _toTokenAddress The ERC20 token to which from token to be convert
    @param  swapTokens amount of toTokens to invest
    @return tokensBought- quantity of curve LP acquired
    @return index- index of LP token in _swapAddress whose pool tokens were acquired
     */
    function _enterMetaPool(
        address _swapAddress,
        address _toTokenAddress,
        uint256 swapTokens
    ) internal returns (uint256 tokensBought, uint8 index) {
        address[4] memory poolTokens = curveReg.getPoolTokens(_swapAddress);
        for (uint8 i = 0; i < 4; i++) {
            address intermediateSwapAddress = curveReg.metaPools(poolTokens[i]);
            if (intermediateSwapAddress != address(0)) {
                (, index) = curveReg.isUnderlyingToken(
                    intermediateSwapAddress,
                    _toTokenAddress
                );

                tokensBought = _enterCurve(
                    intermediateSwapAddress,
                    swapTokens,
                    index
                );

                return (tokensBought, i);
            }
        }
    }

    function _fillQuote(
        address _fromTokenAddress,
        address _toTokenAddress,
        uint256 _amount,
        address _allowanceTarget,
        address _swapTarget,
        bytes memory _swapCallData
    ) internal returns (uint256 amountBought) {
        uint256 valueToSend;

        if (_fromTokenAddress == _toTokenAddress) {
            return _amount;
        }
        if (_fromTokenAddress == ETHToken) {
            valueToSend = _amount;
        } else {
            IERC20 fromToken = IERC20(_fromTokenAddress);

            require(
                fromToken.balanceOf(address(this)) >= _amount,
                "Insufficient Balance"
            );

            fromToken.safeApprove(address(_allowanceTarget), 0);
            fromToken.safeApprove(address(_allowanceTarget), _amount);
        }

        uint256 initialBalance = IERC20(_toTokenAddress).balanceOf(
            address(this)
        );

        (bool success, ) = _swapTarget.call.value(valueToSend)(_swapCallData);
        require(success, "Error Swapping Tokens");

        amountBought = IERC20(_toTokenAddress).balanceOf(address(this)).sub(
            initialBalance
        );

        require(amountBought > 0, "Swapped To Invalid Intermediate");
    }

    /**
    @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 crvTokensBought- the quantity of curve LP tokens received
    */
    function _enterCurve(
        address _swapAddress,
        uint256 amount,
        uint8 index
    ) internal returns (uint256 crvTokensBought) {
        address tokenAddress = curveReg.getTokenAddress(_swapAddress);
        uint256 initialBalance = IERC20(tokenAddress).balanceOf(address(this));
        address entryToken = curveReg.getPoolTokens(_swapAddress)[index];
        if (entryToken != ETHToken) {
            IERC20(entryToken).safeIncreaseAllowance(
                address(_swapAddress),
                amount
            );
        }
        uint256 numTokens = curveReg.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;
            if (isETHUnderlying(_swapAddress)) {
                ICurveEthSwap(_swapAddress).add_liquidity.value(msg.value)(
                    amounts,
                    0
                );
            } else {
                ICurveSwap(_swapAddress).add_liquidity(amounts, 0);
            }
        }
        crvTokensBought = (IERC20(tokenAddress).balanceOf(address(this))).sub(
            initialBalance
        );
    }

    function isETHUnderlying(address _swapAddress)
        internal
        view
        returns (bool)
    {
        address[4] memory poolTokens = curveReg.getPoolTokens(_swapAddress);
        for (uint8 i = 0; i < 4; i++) {
            if (poolTokens[i] == ETHToken) {
                return true;
            }
        }
        return false;
    }

    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 updateCurveRegistry(ICurveRegistry newCurveRegistry)
        external
        onlyOwner
    {
        require(newCurveRegistry != curveReg, "Already using this Registry");
        curveReg = newCurveRegistry;
    }

    // - 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 _enterCurve(
    address _swapAddress,
    uint256 amount,
    uint8 index
) internal returns (uint256 crvTokensBought) {
    address tokenAddress = curveReg.getTokenAddress(_swapAddress);
    uint256 initialBalance = IERC20(tokenAddress).balanceOf(address(this));
    address entryToken = curveReg.getPoolTokens(_swapAddress)[index];
    if (entryToken != ETHToken) {
        IERC20(entryToken).safeIncreaseAllowance(
            address(_swapAddress),
            amount
        );
    }
    uint256 numTokens = curveReg.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;
        if (isETHUnderlying(_swapAddress)) {
            ICurveEthSwap(_swapAddress).add_liquidity.value(msg.value)(
                amounts,
                0
            );
        } else {
            ICurveSwap(_swapAddress).add_liquidity(amounts, 0);
        }
    }
    crvTokensBought = (IERC20(tokenAddress).balanceOf(address(this))).sub(
        initialBalance
    );
}

/**
 @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 crvTokensBought- the quantity of curve LP tokens received
 */