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

    TokenTransferProxy private _tokenTransferProxy;

    bool private _paused;

    IWhitelisted private _whitelisted;
    address private _gasToken;

    IPartnerRegistry private _partnerRegistry;
    address payable private _feeWallet;

    event Paused();
    event Unpaused();

    event Swapped(
        address indexed beneficiary,
        address indexed srcToken,
        address indexed destToken,
        uint256 srcAmount,
        uint256 receivedAmount,
        uint256 expectedAmount,
        uint256 fee,
        string referrer
    );

    event FeeTaken(
        uint256 fee,
        uint256 partnerShare,
        uint256 paraswapShare
    );

    /**
     * @dev Modifier to make a function callable only when the contract is not paused.
     */
    modifier whenNotPaused() {
        require(!_paused, "Pausable: paused");
        _;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is paused.
     */
    modifier whenPaused() {
        require(_paused, "Pausable: not paused");
        _;
    }

    constructor(
        address whitelist,
        address gasToken,
        address partnerRegistry,
        address payable feeWallet
    )
        public
    {

        _partnerRegistry = IPartnerRegistry(partnerRegistry);
        _tokenTransferProxy = new TokenTransferProxy();
        _whitelisted = IWhitelisted(whitelist);
        _gasToken = gasToken;
        _feeWallet = feeWallet;
    }

    /**
    * @dev Fallback method to allow exchanges to transfer back ethers for a particular swap
    * It will only allow contracts to send funds to it
    */
    function() external payable whenNotPaused {
        address account = msg.sender;
        require(
            account.isContract(),
            "Sender is not a contract"
        );
    }
    
    function getPartnerRegistry() external view returns(address) {
        return address(_partnerRegistry);
    }

    function getWhitelistAddress() external view returns(address) {
        return address(_whitelisted);
    }

    function getFeeWallet() external view returns(address) {
        return _feeWallet;
    }

    function setFeeWallet(address payable feeWallet) external onlyOwner {
        require(feeWallet != address(0), "Invalid address");
        _feeWallet = feeWallet;
    }

    function setPartnerRegistry(address partnerRegistry) external onlyOwner {
        require(partnerRegistry != address(0), "Invalid address");
        _partnerRegistry = IPartnerRegistry(partnerRegistry);
    }

    function setWhitelistAddress(address whitelisted) external onlyOwner {
        require(whitelisted != address(0), "Invalid whitelist address");
        _whitelisted = IWhitelisted(whitelisted);
    }

    function getTokenTransferProxy() external view returns (address) {
        return address(_tokenTransferProxy);
    }

    /**
     * @dev Returns true if the contract is paused, and false otherwise.
     */
    function paused() external view returns (bool) {
        return _paused;
    }

    /**
     * @dev Called by a pauser to pause, triggers stopped state.
     */
    function pause() external onlyOwner whenNotPaused {
        _paused = true;
        emit Paused();
    }

    /**
     * @dev Called by a pauser to unpause, returns to normal state.
     */
    function unpause() external onlyOwner whenPaused {
        _paused = false;
        emit Unpaused();
    }

    /**
    * @dev Allows owner of the contract to transfer tokens any tokens which are assigned to the contract
    * This method is for saftey if by any chance tokens or ETHs are assigned to the contract by mistake
    * @dev token Address of the token to be transferred
    * @dev destination Recepient of the token
    * @dev amount Amount of tokens to be transferred
    */
    function ownerTransferTokens(
        address token,
        address payable destination,
        uint256 amount
    )
    external
    onlyOwner
    {
        Utils.transferTokens(token, destination, amount);
    }

    /**
    * @dev Allows owner of the contract to mint more gas tokens
    * @param amount Amount of gas tokens to mint
    */
    function mintGasTokens(uint256 amount) external onlyOwner {
        Utils.mintGasTokens(_gasToken, amount);
    }

    /**
   * @dev The function which performs the multi path swap.
   * It is a nonreentrant function
   * @param fromToken Address of the source token
   * @param toToken Address of the destination token
   * @param fromAmount Amount of source tokens to be swapped
   * @param toAmount Minimum destination token amount expected out of this swap
   * @param expectedAmount Expected amount of destination tokens without slippage
   * @param path Route to be taken for this swap to take place
   * @param mintPrice Price of gas at the time of minting of gas tokens, if any. In wei
   * @param beneficiary Beneficiary address
   * @param referrer referral id
   */
    function multiSwap(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 fromAmount,
        uint256 toAmount,
        uint256 expectedAmount,
        Utils.Path[] memory path,
        uint256 mintPrice,
        address payable beneficiary,
        string memory referrer
    )
        public
        payable
        whenNotPaused
        nonReentrant
        returns (uint256)
    {
        require(bytes(referrer).length > 0, "Invalid referrer");

        uint256 receivedAmount = performSwap(
            fromToken,
            toToken,
            fromAmount,
            toAmount,
            path,
            mintPrice
        );

        uint256 remainingAmount = receivedAmount;
        uint256 fee = 0;
        if (receivedAmount > toAmount) {
            fee = _takeFee(
                toToken,
                receivedAmount,
                referrer
            );

            remainingAmount = receivedAmount.sub(fee);
        }

        if (beneficiary == address(0)){
            Utils.transferTokens(address(toToken), msg.sender, remainingAmount);
        }
        else {
            Utils.transferTokens(address(toToken), beneficiary, remainingAmount);
        }

        //Contract should not have any remaining balance after entire execution
        require(
            Utils.tokenBalance(address(toToken), address(this)) == 0,
            "Destination tokens are stuck"
        );

        emit Swapped(
            msg.sender,
            address(fromToken),
            address(toToken),
            fromAmount,
            receivedAmount,
            expectedAmount,
            fee,
            referrer
        );

        return receivedAmount;
    }

    function performSwap(
        IERC20 fromToken,
        IERC20 toToken,
        uint256 fromAmount,
        uint256 toAmount,
        Utils.Path[] memory path,
        uint256 mintPrice
    )
        private
        returns(uint256)
    {
        uint initialGas = gasleft();

        uint _fromAmount = fromAmount;

        require(path.length > 0, "Path noth provided for swap");
        require(path[0].from == address(fromToken), "First from token does not match");

        //if fromToken is not ETH then transfer tokens from user to this contract
        if (address(fromToken) != Utils.ethAddress()) {
            _tokenTransferProxy.transferFrom(
                address(fromToken),
                msg.sender,
                address(this),
                fromAmount
            );
        }

        for (uint i = 0; i < path.length; i++) {
            IERC20 _fromToken = IERC20(path[i].from);
            IERC20 _toToken = IERC20(path[i].to);

            for (uint j = 0; j < path[i].routes.length; j++) {
                Utils.Route memory route = path[i].routes[j];

                //Calculating tokens to be passed to the relevant exchange
                //percentage should be 200 for 2%
                uint fromAmountSlice = _fromAmount.mul(route.percent).div(10000);
                uint256 value = route.networkFee;

                if (j == path[i].routes.length.sub(1)) {
                    uint256 remBal = Utils.tokenBalance(address(_fromToken), address(this));

                    fromAmountSlice = remBal;

                    if (address(_fromToken) == Utils.ethAddress()) {
                        fromAmountSlice = fromAmountSlice.sub(value);
                    }
                }

                //Check if exchange is supported
                require(_whitelisted.isWhitelisted(route.exchange), "Exchange not whitelisted");

                IExchange dex = IExchange(route.exchange);

                Utils.approve(route.exchange, address(_fromToken));

                //Call to the exchange
                if (address(_fromToken) == Utils.ethAddress()) {
                    value = value.add(fromAmountSlice);

                    dex.swap.value(value)(_fromToken, _toToken, fromAmountSlice, 1, route.targetExchange, route.payload);
                }
                else {
                    _fromToken.safeTransfer(route.exchange, fromAmountSlice);

                    dex.swap.value(value)(_fromToken, _toToken, fromAmountSlice, 1, route.targetExchange, route.payload);
                }
            }

            _fromAmount = Utils.tokenBalance(address(_toToken), address(this));

            //Contract should not have any remaining balance after execution
            require(
                Utils.tokenBalance(address(_fromToken), address(this)) == 0,
                "From tokens are stuck"
            );
        }

        uint256 receivedAmount = Utils.tokenBalance(address(toToken), address(this));
        require(
            receivedAmount >= toAmount,
            "Received amount of tokens are less then expected"
        );

        if (mintPrice > 0) {
            Utils.refundGas(_gasToken, initialGas, mintPrice);
        }
        return receivedAmount;
    }

    function _takeFee(
        IERC20 toToken,
        uint256 receivedAmount,
        string memory referrer
    )
        private
        returns(uint256)
    {
        address partnerContract = _partnerRegistry.getPartnerContract(referrer);
        if (partnerContract == address(0)) {
            return 0;
        }

        uint256 feePercent = IPartner(partnerContract).getFee();
        uint256 partnerSharePercent = IPartner(partnerContract).getPartnerShare();
        address payable partnerFeeWallet = IPartner(partnerContract).getFeeWallet();

        uint256 fee = receivedAmount.mul(feePercent).div(10000);
        uint256 partnerShare = fee.mul(partnerSharePercent).div(10000);
        uint256 paraswapShare = fee.sub(partnerShare);

        Utils.transferTokens(address(toToken), partnerFeeWallet, partnerShare);
        Utils.transferTokens(address(toToken), _feeWallet, paraswapShare);

        emit FeeTaken(fee, partnerShare, paraswapShare);
        return fee;
    }
}
function() external payable whenNotPaused {
    address account = msg.sender;
    require(
        account.isContract(),
        "Sender is not a contract"
    );
}

/**
 * @dev Fallback method to allow exchanges to transfer back ethers for a particular swap
 * It will only allow contracts to send funds to it
 */