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

    bool public stopped = false;
    uint256 public goodwill;

    // if true, goodwill is not deducted
    mapping(address => bool) public feeWhitelist;

    // % share of goodwill (0-100 %)
    uint256 affiliateSplit;
    // restrict affiliates
    mapping(address => bool) public affiliates;
    // affiliate => token => amount
    mapping(address => mapping(address => uint256)) public affiliateBalance;
    // token => amount
    mapping(address => uint256) public totalAffiliateBalance;

    address
        private constant ETHAddress = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;

    uint256
        private constant deadline = 0xf000000000000000000000000000000000000000000000000000000000000000;

    IUniswapV2Router02 private constant uniswapV2Router = IUniswapV2Router02(
        0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
    );

    address private constant wethTokenAddress = address(
        0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
    );

    constructor(uint256 _goodwill, uint256 _affiliateSplit) public {
        goodwill = _goodwill;
        affiliateSplit = _affiliateSplit;
    }

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

    event zapOut(
        address sender,
        address pool,
        address token,
        uint256 tokensRec
    );

    /**
    @notice Zap out in a pair of tokens
    @param _FromUniPoolAddress The uniswap pair address to zapout
    @param _IncomingLP The amount of LP
    @param affiliate Affiliate address
    @return the amount of pair tokens received after zapout
     */
    function ZapOut2PairToken(
        address _FromUniPoolAddress,
        uint256 _IncomingLP,
        address affiliate
    )
        public
        nonReentrant
        stopInEmergency
        returns (uint256 amountA, uint256 amountB)
    {
        IUniswapV2Pair pair = IUniswapV2Pair(_FromUniPoolAddress);

        require(address(pair) != address(0), "Error: Invalid Unipool Address");

        // get reserves
        address token0 = pair.token0();
        address token1 = pair.token1();

        IERC20(_FromUniPoolAddress).safeTransferFrom(
            msg.sender,
            address(this),
            _IncomingLP
        );

        IERC20(_FromUniPoolAddress).safeApprove(
            address(uniswapV2Router),
            _IncomingLP
        );

        if (token0 == wethTokenAddress || token1 == wethTokenAddress) {
            address _token = token0 == wethTokenAddress ? token1 : token0;
            (amountA, amountB) = uniswapV2Router.removeLiquidityETH(
                _token,
                _IncomingLP,
                1,
                1,
                address(this),
                deadline
            );

            // subtract goodwill
            uint256 tokenGoodwill = _subtractGoodwill(
                _token,
                amountA,
                affiliate
            );
            uint256 ethGoodwill = _subtractGoodwill(
                ETHAddress,
                amountB,
                affiliate
            );

            // send tokens
            IERC20(_token).safeTransfer(msg.sender, amountA.sub(tokenGoodwill));
            Address.sendValue(msg.sender, amountB.sub(ethGoodwill));
        } else {
            (amountA, amountB) = uniswapV2Router.removeLiquidity(
                token0,
                token1,
                _IncomingLP,
                1,
                1,
                address(this),
                deadline
            );

            // subtract goodwill
            uint256 tokenAGoodwill = _subtractGoodwill(
                token0,
                amountA,
                affiliate
            );
            uint256 tokenBGoodwill = _subtractGoodwill(
                token1,
                amountB,
                affiliate
            );

            // send tokens
            IERC20(token0).safeTransfer(
                msg.sender,
                amountA.sub(tokenAGoodwill)
            );
            IERC20(token1).safeTransfer(
                msg.sender,
                amountB.sub(tokenBGoodwill)
            );
        }
        emit zapOut(msg.sender, _FromUniPoolAddress, token0, amountA);
        emit zapOut(msg.sender, _FromUniPoolAddress, token1, amountB);
    }

    /**
    @notice Zap out in a single token
    @param _ToTokenContractAddress The ERC20 token to zapout in (address(0x00) if ether)
    @param _FromUniPoolAddress The uniswap pair address to zapout from
    @param _IncomingLP The amount of LP to remove.
    @param _minTokensRec indicates the minimum amount of tokens to receive
    @param _swapTarget indicates the execution target for swap.
    @param swap1Data DEX swap data
    @param swap2Data DEX swap data
    @param affiliate Affiliate address 
    @return the amount of eth/tokens received after zapout
     */
    function ZapOut(
        address _ToTokenContractAddress,
        address _FromUniPoolAddress,
        uint256 _IncomingLP,
        uint256 _minTokensRec,
        address[] memory _swapTarget,
        bytes memory swap1Data,
        bytes memory swap2Data,
        address affiliate
    ) public nonReentrant stopInEmergency returns (uint256 tokenBought) {
        //transfer goodwill and reoves liquidity
        (uint256 amountA, uint256 amountB) = _removeLiquidity(
            _FromUniPoolAddress,
            _IncomingLP
        );

        //swaps tokens to token
        tokenBought = _swapTokens(
            _FromUniPoolAddress,
            amountA,
            amountB,
            _ToTokenContractAddress,
            _swapTarget,
            swap1Data,
            swap2Data
        );
        require(tokenBought >= _minTokensRec, "High slippage");

        emit zapOut(
            msg.sender,
            _FromUniPoolAddress,
            _ToTokenContractAddress,
            tokenBought
        );

        uint256 totalGoodwillPortion;

        // transfer toTokens to sender
        if (_ToTokenContractAddress == address(0)) {
            totalGoodwillPortion = _subtractGoodwill(
                ETHAddress,
                tokenBought,
                affiliate
            );

            msg.sender.transfer(tokenBought.sub(totalGoodwillPortion));
        } else {
            totalGoodwillPortion = _subtractGoodwill(
                _ToTokenContractAddress,
                tokenBought,
                affiliate
            );

            IERC20(_ToTokenContractAddress).safeTransfer(
                msg.sender,
                tokenBought.sub(totalGoodwillPortion)
            );
        }

        return tokenBought.sub(totalGoodwillPortion);
    }

    /**
    @notice Zap out in a pair of tokens with permit
    @param _FromUniPoolAddress indicates the liquidity pool
    @param _IncomingLP indicates the amount of LP to remove from pool
    @param affiliate Affiliate address to share fees
    @param _permitData indicates the encoded permit data, which contains owner, spender, value, deadline, v,r,s values. 
    @return  amountA - indicates the amount received in token0, amountB - indicates the amount received in token1 
    */
    function ZapOut2PairTokenWithPermit(
        address _FromUniPoolAddress,
        uint256 _IncomingLP,
        address affiliate,
        bytes calldata _permitData
    ) external stopInEmergency returns (uint256 amountA, uint256 amountB) {
        // permit
        (bool success, ) = _FromUniPoolAddress.call(_permitData);
        require(success, "Could Not Permit");

        (amountA, amountB) = ZapOut2PairToken(
            _FromUniPoolAddress,
            _IncomingLP,
            affiliate
        );
    }

    /**
    @notice Zap out in a signle token with permit
    @param _ToTokenContractAddress indicates the toToken address to which tokens to convert.
    @param _FromUniPoolAddress indicates the liquidity pool
    @param _IncomingLP indicates the amount of LP to remove from pool
    @param _minTokensRec indicatest the minimum amount of toTokens to receive
    @param _permitData indicates the encoded permit data, which contains owner, spender, value, deadline, v,r,s values. 
    @param _swapTarget indicates the execution target for swap.
    @param swap1Data DEX swap data
    @param swap2Data DEX swap data
    @param affiliate Affiliate address to share fees
    */
    function ZapOutWithPermit(
        address _ToTokenContractAddress,
        address _FromUniPoolAddress,
        uint256 _IncomingLP,
        uint256 _minTokensRec,
        bytes memory _permitData,
        address[] memory _swapTarget,
        bytes memory swap1Data,
        bytes memory swap2Data,
        address affiliate
    ) public stopInEmergency returns (uint256) {
        // permit
        (bool success, ) = _FromUniPoolAddress.call(_permitData);
        require(success, "Could Not Permit");

        return (
            ZapOut(
                _ToTokenContractAddress,
                _FromUniPoolAddress,
                _IncomingLP,
                _minTokensRec,
                _swapTarget,
                swap1Data,
                swap2Data,
                affiliate
            )
        );
    }

    function _removeLiquidity(address _FromUniPoolAddress, uint256 _IncomingLP)
        internal
        returns (uint256 amountA, uint256 amountB)
    {
        IUniswapV2Pair pair = IUniswapV2Pair(_FromUniPoolAddress);

        require(address(pair) != address(0), "Error: Invalid Unipool Address");

        //get pair tokens
        address token0 = pair.token0();
        address token1 = pair.token1();

        IERC20(_FromUniPoolAddress).safeTransferFrom(
            msg.sender,
            address(this),
            _IncomingLP
        );

        IERC20(_FromUniPoolAddress).safeApprove(
            address(uniswapV2Router),
            _IncomingLP
        );

        //remove liquidity
        (amountA, amountB) = uniswapV2Router.removeLiquidity(
            token0,
            token1,
            _IncomingLP,
            1,
            1,
            address(this),
            deadline
        );
        require(amountA > 0 && amountB > 0, "Insufficient Liquidity");
    }

    function _swapTokens(
        address _FromUniPoolAddress,
        uint256 _amountA,
        uint256 _amountB,
        address _toToken,
        address[] memory _swapTarget,
        bytes memory swap1Data,
        bytes memory swap2Data
    ) internal returns (uint256 tokensBought) {
        require(_swapTarget.length == 2, "Invalid data for 0x swap");

        address token0 = IUniswapV2Pair(_FromUniPoolAddress).token0();
        address token1 = IUniswapV2Pair(_FromUniPoolAddress).token1();

        //swap token0 to toToken
        if (token0 == _toToken) {
            tokensBought = tokensBought.add(_amountA);
        } else {
            //swap token using 0x swap
            tokensBought = tokensBought.add(
                _fillQuote(
                    token0,
                    _toToken,
                    _amountA,
                    _swapTarget[0],
                    swap1Data
                )
            );
        }

        //swap token1 to toToken
        if (token1 == _toToken) {
            tokensBought = tokensBought.add(_amountB);
        } else {
            //swap token using 0x swap
            tokensBought = tokensBought.add(
                _fillQuote(
                    token1,
                    _toToken,
                    _amountB,
                    _swapTarget[1],
                    swap2Data
                )
            );
        }
    }

    function _fillQuote(
        address _fromTokenAddress,
        address _toToken,
        uint256 _amount,
        address _swapTarget,
        bytes memory swapData
    ) internal returns (uint256) {
        uint256 valueToSend;
        if (_fromTokenAddress == address(0)) {
            valueToSend = _amount;
        } else {
            IERC20 fromToken = IERC20(_fromTokenAddress);
            fromToken.safeApprove(address(_swapTarget), 0);
            fromToken.safeApprove(address(_swapTarget), _amount);
        }

        uint256 initialBalance = _toToken == address(0)
            ? address(this).balance
            : IERC20(_toToken).balanceOf(address(this));

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

        uint256 finalBalance = _toToken == address(0)
            ? (address(this).balance).sub(initialBalance)
            : IERC20(_toToken).balanceOf(address(this)).sub(initialBalance);

        require(finalBalance > 0, "Swapped to Invalid Intermediate");

        return finalBalance;
    }

    /**
    @notice this method returns the amount of tokens received in underlying tokens after removal of liquidity.
    @param _FromUniPoolAddress indicates the liquidity pool.
    @param _tokenA indicates the tokenA of pool
    @param _tokenB indicates the tokenB of pool
    @param _liquidity indicates the amount of liquidity to remove.
    @return  amountA - indicates the amount removed in token0, amountB - indicates the amount removed in token1 
    */
    function removeLiquidityReturn(
        address _FromUniPoolAddress,
        address _tokenA,
        address _tokenB,
        uint256 _liquidity
    ) external view returns (uint256 amountA, uint256 amountB) {
        IUniswapV2Pair pair = IUniswapV2Pair(_FromUniPoolAddress);

        (uint256 amount0, uint256 amount1) = _getBurnAmount(
            _FromUniPoolAddress,
            pair,
            _tokenA,
            _tokenB,
            _liquidity
        );

        (address token0, ) = _sortTokens(_tokenA, _tokenB);

        (amountA, amountB) = _tokenA == token0
            ? (amount0, amount1)
            : (amount1, amount0);

        require(amountA >= 1, "UniswapV2Router: INSUFFICIENT_A_AMOUNT");
        require(amountB >= 1, "UniswapV2Router: INSUFFICIENT_B_AMOUNT");
    }

    function _sortTokens(address tokenA, address tokenB)
        internal
        pure
        returns (address token0, address token1)
    {
        require(tokenA != tokenB, "UniswapV2Library: IDENTICAL_ADDRESSES");
        (token0, token1) = tokenA < tokenB
            ? (tokenA, tokenB)
            : (tokenB, tokenA);
        require(token0 != address(0), "UniswapV2Library: ZERO_ADDRESS");
    }

    function _getBurnAmount(
        address _FromUniPoolAddress,
        IUniswapV2Pair pair,
        address _token0,
        address _token1,
        uint256 _liquidity
    ) internal view returns (uint256 amount0, uint256 amount1) {
        uint256 balance0 = IERC20(_token0).balanceOf(_FromUniPoolAddress);
        uint256 balance1 = IERC20(_token1).balanceOf(_FromUniPoolAddress);

        uint256 _totalSupply = pair.totalSupply();

        amount0 = _liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
        amount1 = _liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
        require(
            amount0 > 0 && amount1 > 0,
            "UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED"
        );
    }

    function _subtractGoodwill(
        address token,
        uint256 amount,
        address affiliate
    ) internal returns (uint256 totalGoodwillPortion) {
        bool whitelisted = feeWhitelist[msg.sender];
        if (!whitelisted && goodwill > 0) {
            totalGoodwillPortion = SafeMath.div(
                SafeMath.mul(amount, goodwill),
                10000
            );

            if (affiliates[affiliate]) {
                uint256 affiliatePortion = totalGoodwillPortion
                    .mul(affiliateSplit)
                    .div(100);
                affiliateBalance[affiliate][token] = affiliateBalance[affiliate][token]
                    .add(affiliatePortion);
                totalAffiliateBalance[token] = totalAffiliateBalance[token].add(
                    affiliatePortion
                );
            }
        }
    }

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

    function set_new_goodwill(uint256 _new_goodwill) public onlyOwner {
        require(
            _new_goodwill >= 0 && _new_goodwill <= 100,
            "GoodWill Value not allowed"
        );
        goodwill = _new_goodwill;
    }

    function set_feeWhitelist(address zapAddress, bool status)
        external
        onlyOwner
    {
        feeWhitelist[zapAddress] = status;
    }

    function set_new_affiliateSplit(uint256 _new_affiliateSplit)
        external
        onlyOwner
    {
        require(
            _new_affiliateSplit <= 100,
            "Affiliate Split Value not allowed"
        );
        affiliateSplit = _new_affiliateSplit;
    }

    function set_affiliate(address _affiliate, bool _status)
        external
        onlyOwner
    {
        affiliates[_affiliate] = _status;
    }

    function ownerWithdrawTokens(address[] calldata tokens) external onlyOwner {
        // withdraw goodwill share + extra tokens if any sent
        // prevent owner from withdrawing affiliate share

        for (uint256 i = 0; i < tokens.length; i++) {
            uint256 qty;

            if (tokens[i] == ETHAddress) {
                qty = address(this).balance.sub(
                    totalAffiliateBalance[tokens[i]]
                );
                Address.sendValue(Address.toPayable(owner()), qty);
            } else {
                qty = IERC20(tokens[i]).balanceOf(address(this)).sub(
                    totalAffiliateBalance[tokens[i]]
                );
                IERC20(tokens[i]).safeTransfer(owner(), qty);
            }
        }
    }

    function affilliateWithdraw(address[] calldata tokens) external {
        uint256 tokenBal;
        for (uint256 i = 0; i < tokens.length; i++) {
            tokenBal = affiliateBalance[msg.sender][tokens[i]];
            affiliateBalance[msg.sender][tokens[i]] = 0;
            totalAffiliateBalance[tokens[i]] = totalAffiliateBalance[tokens[i]]
                .sub(tokenBal);

            if (tokens[i] == ETHAddress) {
                Address.sendValue(msg.sender, tokenBal);
            } else {
                IERC20(tokens[i]).safeTransfer(msg.sender, tokenBal);
            }
        }
    }

    function() external payable {
        require(msg.sender != tx.origin, "Do not send ETH directly");
    }
}
function ZapOut2PairTokenWithPermit(
        address _FromUniPoolAddress,
        uint256 _IncomingLP,
        address affiliate,
        bytes calldata _permitData
    ) external stopInEmergency returns (uint256 amountA, uint256 amountB) {
        // permit
        (bool success, ) = _FromUniPoolAddress.call(_permitData);
        require(success, "Could Not Permit");

        (amountA, amountB) = ZapOut2PairToken(
            _FromUniPoolAddress,
            _IncomingLP,
            affiliate
        );
    }
/**
 @notice Zap out in a pair of tokens with permit
 @param _FromUniPoolAddress indicates the liquidity pool
 @param _IncomingLP indicates the amount of LP to remove from pool
 @param affiliate Affiliate address to share fees
 @param _permitData indicates the encoded permit data, which contains owner, spender, value, deadline, v,r,s values. 
 @return  amountA - indicates the amount received in token0, amountB - indicates the amount received in token1 
 */