pragma solidity 0.4.18;

// File: contracts/ERC20Interface.sol

// https://github.com/ethereum/EIPs/issues/20
interface ERC20 {
    function totalSupply() public view returns (uint supply);
    function balanceOf(address _owner) public view returns (uint balance);
    function transfer(address _to, uint _value) public returns (bool success);
    function transferFrom(address _from, address _to, uint _value) public returns (bool success);
    function approve(address _spender, uint _value) public returns (bool success);
    function allowance(address _owner, address _spender) public view returns (uint remaining);
    function decimals() public view returns(uint digits);
    event Approval(address indexed _owner, address indexed _spender, uint _value);
}

// File: contracts/KyberReserveInterface.sol

/// @title Kyber Reserve contract
interface KyberReserveInterface {

    function trade(
        ERC20 srcToken,
        uint srcAmount,
        ERC20 destToken,
        address destAddress,
        uint conversionRate,
        bool validate
    )
        public
        payable
        returns(bool);

    function getConversionRate(ERC20 src, ERC20 dest, uint srcQty, uint blockNumber) public view returns(uint);
}

// File: contracts/Utils.sol

/// @title Kyber constants contract
contract Utils {

    ERC20 constant internal ETH_TOKEN_ADDRESS = ERC20(0x00eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee);
    uint  constant internal PRECISION = (10**18);
    uint  constant internal MAX_QTY   = (10**28); // 10B tokens
    uint  constant internal MAX_RATE  = (PRECISION * 10**6); // up to 1M tokens per ETH
    uint  constant internal MAX_DECIMALS = 18;
    uint  constant internal ETH_DECIMALS = 18;
    mapping(address=>uint) internal decimals;

    function setDecimals(ERC20 token) internal {
        if (token == ETH_TOKEN_ADDRESS) decimals[token] = ETH_DECIMALS;
        else decimals[token] = token.decimals();
    }

    function getDecimals(ERC20 token) internal view returns(uint) {
        if (token == ETH_TOKEN_ADDRESS) return ETH_DECIMALS; // save storage access
        uint tokenDecimals = decimals[token];
        // technically, there might be token with decimals 0
        // moreover, very possible that old tokens have decimals 0
        // these tokens will just have higher gas fees.
        if(tokenDecimals == 0) return token.decimals();

        return tokenDecimals;
    }

    function calcDstQty(uint srcQty, uint srcDecimals, uint dstDecimals, uint rate) internal pure returns(uint) {
        require(srcQty <= MAX_QTY);
        require(rate <= MAX_RATE);

        if (dstDecimals >= srcDecimals) {
            require((dstDecimals - srcDecimals) <= MAX_DECIMALS);
            return (srcQty * rate * (10**(dstDecimals - srcDecimals))) / PRECISION;
        } else {
            require((srcDecimals - dstDecimals) <= MAX_DECIMALS);
            return (srcQty * rate) / (PRECISION * (10**(srcDecimals - dstDecimals)));
        }
    }

    function calcSrcQty(uint dstQty, uint srcDecimals, uint dstDecimals, uint rate) internal pure returns(uint) {
        require(dstQty <= MAX_QTY);
        require(rate <= MAX_RATE);
        
        //source quantity is rounded up. to avoid dest quantity being too low.
        uint numerator;
        uint denominator;
        if (srcDecimals >= dstDecimals) {
            require((srcDecimals - dstDecimals) <= MAX_DECIMALS);
            numerator = (PRECISION * dstQty * (10**(srcDecimals - dstDecimals)));
            denominator = rate;
        } else {
            require((dstDecimals - srcDecimals) <= MAX_DECIMALS);
            numerator = (PRECISION * dstQty);
            denominator = (rate * (10**(dstDecimals - srcDecimals)));
        }
        return (numerator + denominator - 1) / denominator; //avoid rounding down errors
    }
}

// File: contracts/Utils2.sol

contract Utils2 is Utils {

    /// @dev get the balance of a user.
    /// @param token The token type
    /// @return The balance
    function getBalance(ERC20 token, address user) public view returns(uint) {
        if (token == ETH_TOKEN_ADDRESS)
            return user.balance;
        else
            return token.balanceOf(user);
    }

    function getDecimalsSafe(ERC20 token) internal returns(uint) {

        if (decimals[token] == 0) {
            setDecimals(token);
        }

        return decimals[token];
    }

    function calcDestAmount(ERC20 src, ERC20 dest, uint srcAmount, uint rate) internal view returns(uint) {
        return calcDstQty(srcAmount, getDecimals(src), getDecimals(dest), rate);
    }

    function calcSrcAmount(ERC20 src, ERC20 dest, uint destAmount, uint rate) internal view returns(uint) {
        return calcSrcQty(destAmount, getDecimals(src), getDecimals(dest), rate);
    }

    function calcRateFromQty(uint srcAmount, uint destAmount, uint srcDecimals, uint dstDecimals)
        internal pure returns(uint)
    {
        require(srcAmount <= MAX_QTY);
        require(destAmount <= MAX_QTY);

        if (dstDecimals >= srcDecimals) {
            require((dstDecimals - srcDecimals) <= MAX_DECIMALS);
            return (destAmount * PRECISION / ((10 ** (dstDecimals - srcDecimals)) * srcAmount));
        } else {
            require((srcDecimals - dstDecimals) <= MAX_DECIMALS);
            return (destAmount * PRECISION * (10 ** (srcDecimals - dstDecimals)) / srcAmount);
        }
    }
}

// File: contracts/PermissionGroups.sol

contract PermissionGroups {

    address public admin;
    address public pendingAdmin;
    mapping(address=>bool) internal operators;
    mapping(address=>bool) internal alerters;
    address[] internal operatorsGroup;
    address[] internal alertersGroup;
    uint constant internal MAX_GROUP_SIZE = 50;

    function PermissionGroups() public {
        admin = msg.sender;
    }

    modifier onlyAdmin() {
        require(msg.sender == admin);
        _;
    }

    modifier onlyOperator() {
        require(operators[msg.sender]);
        _;
    }

    modifier onlyAlerter() {
        require(alerters[msg.sender]);
        _;
    }

    function getOperators () external view returns(address[]) {
        return operatorsGroup;
    }

    function getAlerters () external view returns(address[]) {
        return alertersGroup;
    }

    event TransferAdminPending(address pendingAdmin);

    /**
     * @dev Allows the current admin to set the pendingAdmin address.
     * @param newAdmin The address to transfer ownership to.
     */
    function transferAdmin(address newAdmin) public onlyAdmin {
        require(newAdmin != address(0));
        TransferAdminPending(pendingAdmin);
        pendingAdmin = newAdmin;
    }

    /**
     * @dev Allows the current admin to set the admin in one tx. Useful initial deployment.
     * @param newAdmin The address to transfer ownership to.
     */
    function transferAdminQuickly(address newAdmin) public onlyAdmin {
        require(newAdmin != address(0));
        TransferAdminPending(newAdmin);
        AdminClaimed(newAdmin, admin);
        admin = newAdmin;
    }

    event AdminClaimed( address newAdmin, address previousAdmin);

    /**
     * @dev Allows the pendingAdmin address to finalize the change admin process.
     */
    function claimAdmin() public {
        require(pendingAdmin == msg.sender);
        AdminClaimed(pendingAdmin, admin);
        admin = pendingAdmin;
        pendingAdmin = address(0);
    }

    event AlerterAdded (address newAlerter, bool isAdd);

    function addAlerter(address newAlerter) public onlyAdmin {
        require(!alerters[newAlerter]); // prevent duplicates.
        require(alertersGroup.length < MAX_GROUP_SIZE);

        AlerterAdded(newAlerter, true);
        alerters[newAlerter] = true;
        alertersGroup.push(newAlerter);
    }

    function removeAlerter (address alerter) public onlyAdmin {
        require(alerters[alerter]);
        alerters[alerter] = false;

        for (uint i = 0; i < alertersGroup.length; ++i) {
            if (alertersGroup[i] == alerter) {
                alertersGroup[i] = alertersGroup[alertersGroup.length - 1];
                alertersGroup.length--;
                AlerterAdded(alerter, false);
                break;
            }
        }
    }

    event OperatorAdded(address newOperator, bool isAdd);

    function addOperator(address newOperator) public onlyAdmin {
        require(!operators[newOperator]); // prevent duplicates.
        require(operatorsGroup.length < MAX_GROUP_SIZE);

        OperatorAdded(newOperator, true);
        operators[newOperator] = true;
        operatorsGroup.push(newOperator);
    }

    function removeOperator (address operator) public onlyAdmin {
        require(operators[operator]);
        operators[operator] = false;

        for (uint i = 0; i < operatorsGroup.length; ++i) {
            if (operatorsGroup[i] == operator) {
                operatorsGroup[i] = operatorsGroup[operatorsGroup.length - 1];
                operatorsGroup.length -= 1;
                OperatorAdded(operator, false);
                break;
            }
        }
    }
}

// File: contracts/Withdrawable.sol

/**
 * @title Contracts that should be able to recover tokens or ethers
 * @author Ilan Doron
 * @dev This allows to recover any tokens or Ethers received in a contract.
 * This will prevent any accidental loss of tokens.
 */
contract Withdrawable is PermissionGroups {

    event TokenWithdraw(ERC20 token, uint amount, address sendTo);

    /**
     * @dev Withdraw all ERC20 compatible tokens
     * @param token ERC20 The address of the token contract
     */
    function withdrawToken(ERC20 token, uint amount, address sendTo) external onlyAdmin {
        require(token.transfer(sendTo, amount));
        TokenWithdraw(token, amount, sendTo);
    }

    event EtherWithdraw(uint amount, address sendTo);

    /**
     * @dev Withdraw Ethers
     */
    function withdrawEther(uint amount, address sendTo) external onlyAdmin {
        sendTo.transfer(amount);
        EtherWithdraw(amount, sendTo);
    }
}

// File: contracts/reserves/bridgeReserves/uniswap/KyberUniswapReserve.sol

interface UniswapExchange {
        function ethToTokenSwapInput(
            uint256 min_tokens,
            uint256 deadline
        )
            external
            payable
            returns (uint256  tokens_bought);

        function tokenToEthSwapInput(
            uint256 tokens_sold,
            uint256 min_eth,
            uint256 deadline
        )
            external
            returns (uint256  eth_bought);

        function getEthToTokenInputPrice(
            uint256 eth_sold
        )
            external
            view
            returns (uint256 tokens_bought);

        function getTokenToEthInputPrice(
            uint256 tokens_sold
        )
            external
            view
            returns (uint256 eth_bought);
    }


    interface UniswapFactory {
        function getExchange(address token) external view returns (address exchange);
    }


    /*
     * A reserve that connects to Uniswap.
     *
     * This reserve makes use of an internal inventory for locally filling orders
     * using the reserve's inventory when certain conditions are met.
     * Conditions are:
     * - After trading the inventory will remain within defined limits.
     * - Uniswap prices do not display internal arbitrage.
     * - Uniswap ask and bid prices meet minimum spread requirements.
     *
     * An additional premium may be added to the converted price for optional
     * promotions.
     */
    contract KyberUniswapReserve is KyberReserveInterface, Withdrawable, Utils2 {
        // Parts per 10000
        uint public constant DEFAULT_FEE_BPS = 25;

        UniswapFactory public uniswapFactory;
        address public kyberNetwork;

        uint public feeBps = DEFAULT_FEE_BPS;

        // Uniswap exchange contract for every listed token
        // token -> exchange
        mapping (address => address) public tokenExchange;

        // Internal inventory balance limits
        // token -> limit
        mapping (address => uint) public internalInventoryMin;
        mapping (address => uint) public internalInventoryMax;

        // Minimum spread in BPS required for using internal inventory
        // token -> limit
        mapping (address => uint) public internalActivationMinSpreadBps;

        // Premium BPS added to internal price (making it better).
        // token -> limit
        mapping (address => uint) public internalPricePremiumBps;

        bool public tradeEnabled = true;

        /**
            Constructor
        */
        function KyberUniswapReserve(
            UniswapFactory _uniswapFactory,
            address _admin,
            address _kyberNetwork
        )
            public
        {
            require(address(_uniswapFactory) != 0);
            require(_admin != 0);
            require(_kyberNetwork != 0);

            uniswapFactory = _uniswapFactory;
            admin = _admin;
            kyberNetwork = _kyberNetwork;
        }

        function() public payable {
            // anyone can deposit ether
        }

        /**
            Returns dest quantity / source quantity.
            Last bit of the rate indicates whether to use internal inventory:
              0 - use uniswap
              1 - use internal inventory
        */
        function getConversionRate(
            ERC20 src,
            ERC20 dest,
            uint srcQty,
            uint blockNumber
        )
            public
            view
            returns(uint)
        {
            // This makes the UNUSED warning go away.
            blockNumber;
            if (!isValidTokens(src, dest)) return 0;
            if (!tradeEnabled) return 0;
            if (srcQty == 0) return 0;

            ERC20 token;
            if (src == ETH_TOKEN_ADDRESS) {
                token = dest;
            } else if (dest == ETH_TOKEN_ADDRESS) {
                token = src;
            } else {
                // Should never arrive here - isValidTokens requires one side to be ETH
                revert();
            }

            uint convertedQuantity;
            uint rateSrcDest;
            uint rateDestSrc;
            (convertedQuantity, rateSrcDest) = calcUniswapConversion(src, dest, srcQty);
            if (convertedQuantity == 0) return 0;
            (, rateDestSrc) = calcUniswapConversion(dest, src, convertedQuantity);

            uint quantityWithPremium = addPremium(token, convertedQuantity);

            bool useInternalInventory = shouldUseInternalInventory(
                src, /* srcToken */
                srcQty, /* srcAmount */
                dest, /* destToken */
                quantityWithPremium, /* destAmount */
                rateSrcDest, /* rateSrcDest */
                rateDestSrc /* rateDestSrc */
            );

            uint rate;
            if (useInternalInventory) {
                // If using internal inventory add premium to converted quantity
                rate = calcRateFromQty(
                    srcQty, /* srcAmount */
                    quantityWithPremium, /* destAmount */
                    getDecimals(src), /* srcDecimals */
                    getDecimals(dest) /* dstDecimals */
                );
            } else {
                // Use rate calculated from uniswap quantities after fees
                rate = rateSrcDest;
            }
            return applyInternalInventoryHintToRate(rate, useInternalInventory);
        }

        function applyInternalInventoryHintToRate(
            uint rate,
            bool useInternalInventory
        )
            internal
            pure
            returns(uint)
        {
            return rate % 2 == (useInternalInventory ? 1 : 0)
                ? rate
                : rate - 1;
        }


        event TradeExecute(
            address indexed sender,
            address src,
            uint srcAmount,
            address destToken,
            uint destAmount,
            address destAddress,
            bool useInternalInventory
        );

        /**
          conversionRate: expected conversion rate should be >= this value.
         */
        function trade(
            ERC20 srcToken,
            uint srcAmount,
            ERC20 destToken,
            address destAddress,
            uint conversionRate,
            bool validate
        )
            public
            payable
            returns(bool)
        {
            require(tradeEnabled);
            require(msg.sender == kyberNetwork);
            require(isValidTokens(srcToken, destToken));

            if (validate) {
                require(conversionRate > 0);
                if (srcToken == ETH_TOKEN_ADDRESS)
                    require(msg.value == srcAmount);
                else
                    require(msg.value == 0);
            }

            // Making sure srcAmount has been transfered to the reserve.
            // If srcToken is ETH the value has already been transfered by calling
            // the function.
            if (srcToken != ETH_TOKEN_ADDRESS)
                require(srcToken.transferFrom(msg.sender, address(this), srcAmount));

            uint expectedDestAmount = calcDestAmount(
                srcToken, /* src */
                destToken, /* dest */
                srcAmount, /* srcAmount */
                conversionRate /* rate */
            );

            bool useInternalInventory = conversionRate % 2 == 1;

            uint destAmount;
            UniswapExchange exchange;
            if (srcToken == ETH_TOKEN_ADDRESS) {
                if (!useInternalInventory) {
                    // Deduct fees (in ETH) before converting
                    uint quantity = deductFee(srcAmount);
                    exchange = UniswapExchange(tokenExchange[address(destToken)]);
                    destAmount = exchange.ethToTokenSwapInput.value(quantity)(
                        1, /* min_tokens: uniswap requires it to be > 0 */
                        2 ** 255 /* deadline */
                    );
                    require(destAmount >= expectedDestAmount);
                }

                // Transfer user-expected dest amount
                require(destToken.transfer(destAddress, expectedDestAmount));
            } else {
                if (!useInternalInventory) {
                    exchange = UniswapExchange(tokenExchange[address(srcToken)]);
                    destAmount = exchange.tokenToEthSwapInput(
                        srcAmount,
                        1, /* min_eth: uniswap requires it to be > 0 */
                        2 ** 255 /* deadline */
                    );
                    // Deduct fees (in ETH) after converting
                    destAmount = deductFee(destAmount);
                    require(destAmount >= expectedDestAmount);
                }

                // Transfer user-expected dest amount
                destAddress.transfer(expectedDestAmount);
            }

            TradeExecute(
                msg.sender, /* sender */
                srcToken, /* src */
                srcAmount, /* srcAmount */
                destToken, /* destToken */
                expectedDestAmount, /* destAmount */
                destAddress, /* destAddress */
                useInternalInventory /* useInternalInventory */
            );
            return true;
        }

        event FeeUpdated(
            uint bps
        );

        function setFee(
            uint bps
        )
            public
            onlyAdmin
        {
            require(bps <= 10000);

            feeBps = bps;

            FeeUpdated(bps);
        }

        event InternalActivationConfigUpdated(
            ERC20 token,
            uint minSpreadBps,
            uint premiumBps
        );

        function setInternalActivationConfig(
            ERC20 token,
            uint minSpreadBps,
            uint premiumBps
        )
            public
            onlyAdmin
        {
            require(tokenExchange[address(token)] != address(0));
            require(minSpreadBps <= 1000); // min spread <= 10%
            require(premiumBps <= 500); // premium <= 5%

            internalActivationMinSpreadBps[address(token)] = minSpreadBps;
            internalPricePremiumBps[address(token)] = premiumBps;

            InternalActivationConfigUpdated(token, minSpreadBps, premiumBps);
        }

        event InternalInventoryLimitsUpdated(
            ERC20 token,
            uint minBalance,
            uint maxBalance
        );

        function setInternalInventoryLimits(
            ERC20 token,
            uint minBalance,
            uint maxBalance
        )
            public
            onlyOperator
        {
            require(tokenExchange[address(token)] != address(0));

            internalInventoryMin[address(token)] = minBalance;
            internalInventoryMax[address(token)] = maxBalance;

            InternalInventoryLimitsUpdated(token, minBalance, maxBalance);
        }

        event TokenListed(
            ERC20 token,
            UniswapExchange exchange
        );

        function listToken(ERC20 token)
            public
            onlyAdmin
        {
            require(address(token) != 0);

            UniswapExchange uniswapExchange = UniswapExchange(
                uniswapFactory.getExchange(token)
            );
            tokenExchange[address(token)] = address(uniswapExchange);
            setDecimals(token);

            require(token.approve(uniswapExchange, 2 ** 255));

            // internal inventory disabled by default
            internalInventoryMin[address(token)] = 2 ** 255;
            internalInventoryMax[address(token)] = 0;
            internalActivationMinSpreadBps[address(token)] = 0;
            internalPricePremiumBps[address(token)] = 0;

            TokenListed(token, uniswapExchange);
        }

        event TokenDelisted(ERC20 token);

        function delistToken(ERC20 token)
            public
            onlyAdmin
        {
            require(tokenExchange[address(token)] != address(0));

            delete tokenExchange[address(token)];
            delete internalInventoryMin[address(token)];
            delete internalInventoryMax[address(token)];
            delete internalActivationMinSpreadBps[address(token)];
            delete internalPricePremiumBps[address(token)];

            TokenDelisted(token);
        }

        function isValidTokens(
            ERC20 src,
            ERC20 dest
        )
            public
            view
            returns(bool)
        {
            return (
                (
                    src == ETH_TOKEN_ADDRESS &&
                    tokenExchange[address(dest)] != address(0)
                ) ||
                (
                    tokenExchange[address(src)] != address(0) &&
                    dest == ETH_TOKEN_ADDRESS
                )
            );
        }

        event TradeEnabled(
            bool enable
        );

        function enableTrade()
            public
            onlyAdmin
            returns(bool)
        {
            tradeEnabled = true;
            TradeEnabled(true);
            return true;
        }

        function disableTrade()
            public
            onlyAlerter
            returns(bool)
        {
            tradeEnabled = false;
            TradeEnabled(false);
            return true;
        }

        event KyberNetworkSet(
            address kyberNetwork
        );

        function setKyberNetwork(
            address _kyberNetwork
        )
            public
            onlyAdmin
        {
            require(_kyberNetwork != 0);
            kyberNetwork = _kyberNetwork;
            KyberNetworkSet(kyberNetwork);
        }

        /*
         * Uses amounts and rates to check if the reserve's internal inventory can
         * be used directly.
         *
         * rateEthToToken and rateTokenToEth are in kyber rate format meaning
         * rate as numerator and 1e18 as denominator.
         */
        function shouldUseInternalInventory(
            ERC20 srcToken,
            uint srcAmount,
            ERC20 destToken,
            uint destAmount,
            uint rateSrcDest,
            uint rateDestSrc
        )
            public
            view
            returns(bool)
        {
            require(srcAmount < MAX_QTY);
            require(destAmount < MAX_QTY);

            // Check for internal inventory balance limitations
            ERC20 token;
            if (srcToken == ETH_TOKEN_ADDRESS) {
                token = destToken;
                uint tokenBalance = token.balanceOf(this);
                if (
                    tokenBalance < destAmount ||
                    tokenBalance - destAmount < internalInventoryMin[token]
                ) {
                    return false;
                }
            } else {
                token = srcToken;
                if (this.balance < destAmount) return false;
                if (token.balanceOf(this) + srcAmount > internalInventoryMax[token]) {
                    return false;
                }
            }

            uint normalizedDestSrc = 10 ** 36 / rateDestSrc;

            // Check for arbitrage
            if (rateSrcDest > normalizedDestSrc) return false;

            uint activationSpread = internalActivationMinSpreadBps[token];
            uint spread = uint(calculateSpreadBps(normalizedDestSrc, rateSrcDest));
            return spread >= activationSpread;
        }

        /*
         * Spread calculation is (ask - bid) / ((ask + bid) / 2).
         * We multiply by 10000 to get result in BPS.
         *
         * Note: if askRate > bidRate result will be negative indicating
         * internal arbitrage.
         */
        function calculateSpreadBps(
            uint _askRate,
            uint _bidRate
        )
            public
            pure
            returns(int)
        {
            int askRate = int(_askRate);
            int bidRate = int(_bidRate);
            return 10000 * 2 * (askRate - bidRate) / (askRate + bidRate);
        }

        function deductFee(
            uint amount
        )
            public
            view
            returns(uint)
        {
            return amount * (10000 - feeBps) / 10000;
        }

        function addPremium(
            ERC20 token,
            uint amount
        )
            public
            view
            returns(uint)
        {
            require(amount <= MAX_QTY);
            return amount * (10000 + internalPricePremiumBps[token]) / 10000;
        }

        function calcUniswapConversion(
            ERC20 src,
            ERC20 dest,
            uint srcQty
        )
            internal
            view
            returns(uint destQty, uint rate)
        {
            UniswapExchange exchange;
            if (src == ETH_TOKEN_ADDRESS) {
                exchange = UniswapExchange(tokenExchange[address(dest)]);
                destQty = exchange.getEthToTokenInputPrice(
                    deductFee(srcQty)
                );
            } else {
                exchange = UniswapExchange(tokenExchange[address(src)]);
                destQty = deductFee(
                    exchange.getTokenToEthInputPrice(srcQty)
                );
            }

            rate = calcRateFromQty(
                srcQty, /* srcAmount */
                destQty, /* destAmount */
                getDecimals(src), /* srcDecimals */
                getDecimals(dest) /* dstDecimals */
            );
        }
    }