contract Balancer_Unzap_V1_1 is ReentrancyGuard, Ownable {
    using SafeMath for uint256;
    using Address for address;
    bool private stopped = false;
    uint16 public goodwill;
    address public dzgoodwillAddress;
    uint256 public defaultSlippage;

    IuniswapFactory_Balancer_Unzap_V1_1
        public UniSwapFactoryAddress = IuniswapFactory_Balancer_Unzap_V1_1(
        0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95
    );
    IBFactory_Balancer_Unzap_V1_1 BalancerFactory = IBFactory_Balancer_Unzap_V1_1(
        0x9424B1412450D0f8Fc2255FAf6046b98213B76Bd
    );

    address wethTokenAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    event Zapout(
        address _toWhomToIssue,
        address _fromBalancerPoolAddress,
        address _toTokenContractAddress,
        uint256 _OutgoingAmount
    );

    constructor(
        uint16 _goodwill,
        address _dzgoodwillAddress,
        uint256 _slippage
    ) public {
        goodwill = _goodwill;
        dzgoodwillAddress = _dzgoodwillAddress;
        defaultSlippage = _slippage;
    }

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

    /**
    @notice This function is used for zapping out of balancer pools
    @param _ToTokenContractAddress The token in which we want zapout (for ethers, its zero address)
    @param _FromBalancerPoolAddress The address of balancer pool to zap out
    @param _IncomingBPT The quantity of balancer pool tokens
    @param slippage slippage user wants
    @return success or failure
    */
    function EasyZapOut(
        address _ToTokenContractAddress,
        address _FromBalancerPoolAddress,
        uint256 _IncomingBPT,
        uint256 slippage
    ) public payable nonReentrant stopInEmergency returns (uint256) {
        require(
            BalancerFactory.isBPool(_FromBalancerPoolAddress),
            "Invalid Balancer Pool"
        );

        uint256 withSlippage = slippage > 0 && slippage < 10000
            ? slippage
            : defaultSlippage;

        address _FromTokenAddress;
        if (
            IBPool_Balancer_Unzap_V1_1(_FromBalancerPoolAddress).isBound(
                _ToTokenContractAddress
            )
        ) {
            _FromTokenAddress = _ToTokenContractAddress;
        } else if (
            _ToTokenContractAddress == address(0) &&
            IBPool_Balancer_Unzap_V1_1(_FromBalancerPoolAddress).isBound(
                wethTokenAddress
            )
        ) {
            _FromTokenAddress = wethTokenAddress;
        } else {
            _FromTokenAddress = _getBestDeal(
                _FromBalancerPoolAddress,
                _IncomingBPT
            );
        }

        return (
            _performZapOut(
                msg.sender,
                _ToTokenContractAddress,
                _FromBalancerPoolAddress,
                _IncomingBPT,
                _FromTokenAddress,
                withSlippage
            )
        );
    }

    /**
    @notice In the case of user wanting to get out in ETH, the '_ToTokenContractAddress' it will be address(0x0)
    @param _toWhomToIssue is the address of user
    @param _ToTokenContractAddress is the address of the token to which you want to convert to
    @param _FromBalancerPoolAddress the address of the Balancer Pool from which you want to ZapOut
    @param _IncomingBPT is the quantity of Balancer Pool tokens that the user wants to ZapOut
    @param _IntermediateToken is the token to which the Balancer Pool should be Zapped Out
    @param slippage slippage user wants
    @notice this is only used if the outgoing token is not amongst the Balancer Pool tokens
    @return success or failure
    */
    function ZapOut(
        address payable _toWhomToIssue,
        address _ToTokenContractAddress,
        address _FromBalancerPoolAddress,
        uint256 _IncomingBPT,
        address _IntermediateToken,
        uint256 slippage
    ) public payable nonReentrant stopInEmergency returns (uint256) {
        uint256 withSlippage = slippage > 0 && slippage < 10000
            ? slippage
            : defaultSlippage;

        return (
            _performZapOut(
                _toWhomToIssue,
                _ToTokenContractAddress,
                _FromBalancerPoolAddress,
                _IncomingBPT,
                _IntermediateToken,
                withSlippage
            )
        );
    }

    /**
    @notice This method is called by ZapOut and EasyZapOut()
    @param _toWhomToIssue is the address of user
    @param _ToTokenContractAddress is the address of the token to which you want to convert to
    @param _FromBalancerPoolAddress the address of the Balancer Pool from which you want to ZapOut
    @param _IncomingBPT is the quantity of Balancer Pool tokens that the user wants to ZapOut
    @param _IntermediateToken is the token to which the Balancer Pool should be Zapped Out
    @notice this is only used if the outgoing token is not amongst the Balancer Pool tokens
    @return success or failure
    */
    function _performZapOut(
        address payable _toWhomToIssue,
        address _ToTokenContractAddress,
        address _FromBalancerPoolAddress,
        uint256 _IncomingBPT,
        address _IntermediateToken,
        uint256 slippage
    ) internal returns (uint256) {
        //transfer goodwill

        uint256 goodwillPortion = _transferGoodwill(
            _FromBalancerPoolAddress,
            _IncomingBPT,
            _toWhomToIssue
        );

        require(
            IERC20(_FromBalancerPoolAddress).transferFrom(
                _toWhomToIssue,
                address(this),
                SafeMath.sub(_IncomingBPT, goodwillPortion)
            )
        );

        if (
            IBPool_Balancer_Unzap_V1_1(_FromBalancerPoolAddress).isBound(
                _ToTokenContractAddress
            )
        ) {
            return (
                _directZapout(
                    _FromBalancerPoolAddress,
                    _ToTokenContractAddress,
                    _toWhomToIssue,
                    SafeMath.sub(_IncomingBPT, goodwillPortion)
                )
            );
        }

        //exit balancer
        uint256 _returnedTokens = _exitBalancer(
            _FromBalancerPoolAddress,
            _IntermediateToken,
            SafeMath.sub(_IncomingBPT, goodwillPortion)
        );

        if (_ToTokenContractAddress == address(0)) {
            uint256 ethBought = _token2Eth(
                _IntermediateToken,
                _returnedTokens,
                _toWhomToIssue,
                slippage
            );
            emit Zapout(
                _toWhomToIssue,
                _FromBalancerPoolAddress,
                _ToTokenContractAddress,
                ethBought
            );
            return ethBought;
        } else {
            uint256 tokenBought = _token2Token(
                _IntermediateToken,
                _toWhomToIssue,
                _ToTokenContractAddress,
                _returnedTokens,
                slippage
            );
            emit Zapout(
                _toWhomToIssue,
                _FromBalancerPoolAddress,
                _ToTokenContractAddress,
                tokenBought
            );
            return tokenBought;
        }
    }

    /**
    @notice This function is used for zapping out of balancer pool
    @param _FromBalancerPoolAddress The address of balancer pool to zap out
    @param _ToTokenContractAddress The token in which we want to zapout (for ethers, its zero address)
    @param _toWhomToIssue The address of user
    @param tokens2Trade The quantity of balancer pool tokens
    @return success or failure
    */
    function _directZapout(
        address _FromBalancerPoolAddress,
        address _ToTokenContractAddress,
        address _toWhomToIssue,
        uint256 tokens2Trade
    ) internal returns (uint256 returnedTokens) {
        returnedTokens = _exitBalancer(
            _FromBalancerPoolAddress,
            _ToTokenContractAddress,
            tokens2Trade
        );

        emit Zapout(
            _toWhomToIssue,
            _FromBalancerPoolAddress,
            _ToTokenContractAddress,
            returnedTokens
        );

        IERC20(_ToTokenContractAddress).transfer(
            _toWhomToIssue,
            returnedTokens
        );
    }

    /**
    @notice This function is used to calculate and transfer goodwill
    @param _tokenContractAddress Token address in which goodwill is deducted
    @param tokens2Trade The total amount of tokens to be zapped out
    @param _toWhomToIssue The address of user
    @return The amount of goodwill deducted
    */
    function _transferGoodwill(
        address _tokenContractAddress,
        uint256 tokens2Trade,
        address _toWhomToIssue
    ) internal returns (uint256 goodwillPortion) {
        goodwillPortion = SafeMath.div(
            SafeMath.mul(tokens2Trade, goodwill),
            10000
        );

        if (goodwillPortion == 0) {
            return 0;
        }

        require(
            IERC20(_tokenContractAddress).transferFrom(
                _toWhomToIssue,
                dzgoodwillAddress,
                goodwillPortion
            ),
            "Error in transferring BPT:1"
        );
        return goodwillPortion;
    }

    /**
    @notice This function finds best token from the final tokens of balancer pool
    @param _FromBalancerPoolAddress The address of balancer pool to zap out
    @param _IncomingBPT The amount of balancer pool token to covert
    @return The token address having max liquidity
     */
    function _getBestDeal(
        address _FromBalancerPoolAddress,
        uint256 _IncomingBPT
    ) internal view returns (address _token) {
        //get token list
        address[] memory tokens = IBPool_Balancer_Unzap_V1_1(
            _FromBalancerPoolAddress
        ).getFinalTokens();

        uint256 maxEth;

        for (uint256 index = 0; index < tokens.length; index++) {
            //get token for given bpt amount
            uint256 tokensForBPT = getBPT2Token(
                _FromBalancerPoolAddress,
                _IncomingBPT,
                tokens[index]
            );

            //get eth value for each token
            Iuniswap_Balancer_Unzap_V1_1 FromUniSwapExchangeContractAddress
            = Iuniswap_Balancer_Unzap_V1_1(
                UniSwapFactoryAddress.getExchange(tokens[index])
            );

            if (address(FromUniSwapExchangeContractAddress) == address(0)) {
                continue;
            }
            uint256 ethReturned = FromUniSwapExchangeContractAddress
                .getTokenToEthInputPrice(tokensForBPT);

            //get max eth value
            if (maxEth < ethReturned) {
                maxEth = ethReturned;
                _token = tokens[index];
            }
        }
    }

    /**
    @notice This function gives the amount of tokens on zapping out from given BPool
    @param _FromBalancerPoolAddress Address of balancer pool to zapout from
    @param _IncomingBPT The amount of BPT to zapout
    @param _toToken Address of token to zap out with
    @return Amount of ERC token
     */
    function getBPT2Token(
        address _FromBalancerPoolAddress,
        uint256 _IncomingBPT,
        address _toToken
    ) internal view returns (uint256 tokensReturned) {
        uint256 totalSupply = IBPool_Balancer_Unzap_V1_1(
            _FromBalancerPoolAddress
        ).totalSupply();
        uint256 swapFee = IBPool_Balancer_Unzap_V1_1(_FromBalancerPoolAddress)
            .getSwapFee();
        uint256 totalWeight = IBPool_Balancer_Unzap_V1_1(
            _FromBalancerPoolAddress
        ).getTotalDenormalizedWeight();
        uint256 balance = IBPool_Balancer_Unzap_V1_1(_FromBalancerPoolAddress)
            .getBalance(_toToken);
        uint256 denorm = IBPool_Balancer_Unzap_V1_1(_FromBalancerPoolAddress)
            .getDenormalizedWeight(_toToken);

        tokensReturned = IBPool_Balancer_Unzap_V1_1(_FromBalancerPoolAddress)
            .calcSingleOutGivenPoolIn(
            balance,
            denorm,
            totalSupply,
            totalWeight,
            _IncomingBPT,
            swapFee
        );
    }

    /**
    @notice This function is used to zap out of the given balancer pool
    @param _FromBalancerPoolAddress The address of balancer pool to zap out
    @param _ToTokenContractAddress The Token address which will be zapped out
    @param _amount The amount of token for zapout
    @return The amount of tokens received after zap out
     */
    function _exitBalancer(
        address _FromBalancerPoolAddress,
        address _ToTokenContractAddress,
        uint256 _amount
    ) internal returns (uint256 returnedTokens) {
        require(
            IBPool_Balancer_Unzap_V1_1(_FromBalancerPoolAddress).isBound(
                _ToTokenContractAddress
            ),
            "Token not bound"
        );

        returnedTokens = IBPool_Balancer_Unzap_V1_1(_FromBalancerPoolAddress)
            .exitswapPoolAmountIn(_ToTokenContractAddress, _amount, 1);

        require(returnedTokens > 0, "Error in exiting balancer pool");
    }

    /**
    @notice This function is used to swap tokens
    @param _FromTokenContractAddress The token address to swap from
    @param _ToWhomToIssue The address to transfer after swap
    @param _ToTokenContractAddress The token address to swap to
    @param tokens2Trade The quantity of tokens to swap
    @return The amount of tokens returned after swap
     */
    function _token2Token(
        address _FromTokenContractAddress,
        address _ToWhomToIssue,
        address _ToTokenContractAddress,
        uint256 tokens2Trade,
        uint256 slippage
    ) internal returns (uint256 tokenBought) {

        Iuniswap_Balancer_Unzap_V1_1 FromUniSwapExchangeContractAddress
        = Iuniswap_Balancer_Unzap_V1_1(
            UniSwapFactoryAddress.getExchange(_FromTokenContractAddress)
        );

        Iuniswap_Balancer_Unzap_V1_1 ToUniSwapExchangeContractAddress
        = Iuniswap_Balancer_Unzap_V1_1(
            UniSwapFactoryAddress.getExchange(_ToTokenContractAddress)
        );

        IERC20(_FromTokenContractAddress).approve(
            address(FromUniSwapExchangeContractAddress),
            tokens2Trade
        );

        uint256 minEthBought = FromUniSwapExchangeContractAddress
            .getTokenToEthInputPrice(tokens2Trade);
        minEthBought = SafeMath.div(
            SafeMath.mul(minEthBought, SafeMath.sub(10000, slippage)),
            10000
        );

        uint256 minTokenBought = ToUniSwapExchangeContractAddress
            .getEthToTokenInputPrice(minEthBought);
        minTokenBought = SafeMath.div(
            SafeMath.mul(minTokenBought, SafeMath.sub(10000, slippage)),
            10000
        );

        tokenBought = FromUniSwapExchangeContractAddress
            .tokenToTokenTransferInput(
            tokens2Trade,
            minTokenBought,
            minEthBought,
            SafeMath.add(now, 300),
            _ToWhomToIssue,
            _ToTokenContractAddress
        );
        require(tokenBought > 0, "Error in swapping ERC: 1");
    }

    /**
    @notice This function is used to swap tokens to eth
    @param _FromTokenContractAddress The token address to swap from
    @param tokens2Trade The quantity of tokens to swap
    @param _toWhomToIssue The address to transfer after swap
    @return The amount of ether returned after swap
     */
    function _token2Eth(
        address _FromTokenContractAddress,
        uint256 tokens2Trade,
        address payable _toWhomToIssue,
        uint256 slippage
    ) internal returns (uint256 ethBought) {
        if (_FromTokenContractAddress == wethTokenAddress) {
            IWETH(wethTokenAddress).withdraw(tokens2Trade);
            _toWhomToIssue.transfer(tokens2Trade);
            return tokens2Trade;
        }

        Iuniswap_Balancer_Unzap_V1_1 FromUniSwapExchangeContractAddress
        = Iuniswap_Balancer_Unzap_V1_1(
            UniSwapFactoryAddress.getExchange(_FromTokenContractAddress)
        );

        IERC20(_FromTokenContractAddress).approve(
            address(FromUniSwapExchangeContractAddress),
            tokens2Trade
        );

        uint256 minEthBought = FromUniSwapExchangeContractAddress
            .getTokenToEthInputPrice(tokens2Trade);
        minEthBought = SafeMath.div(
            SafeMath.mul(minEthBought, SafeMath.sub(10000, slippage)),
            10000
        );

        ethBought = FromUniSwapExchangeContractAddress.tokenToEthTransferInput(
            tokens2Trade,
            minEthBought,
            SafeMath.add(now, 300),
            _toWhomToIssue
        );
        require(ethBought > 0, "Error in swapping Eth: 1");
    }

    function updateSlippage(uint256 _newSlippage) public onlyOwner {
        require(
            _newSlippage > 0 && _newSlippage < 10000,
            "Slippage Value not allowed"
        );
        defaultSlippage = _newSlippage;
    }

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

    function set_new_dzgoodwillAddress(address _new_dzgoodwillAddress)
        public
        onlyOwner
    {
        dzgoodwillAddress = _new_dzgoodwillAddress;
    }

    function inCaseTokengetsStuck(IERC20 _TokenAddress) public onlyOwner {
        uint256 qty = _TokenAddress.balanceOf(address(this));
        _TokenAddress.transfer(owner(), qty);
    }

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

    // - to withdraw any ETH balance sitting in the contract
    function withdraw() public onlyOwner {
        uint256 contractBalance = address(this).balance;
        address payable _to = owner().toPayable();
        _to.transfer(contractBalance);
    }

    // - to kill the contract
    function destruct() public onlyOwner {
        address payable _to = owner().toPayable();
        selfdestruct(_to);
    }

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