// Contract address: 0xaBeBf8931C4fFE1b996398d6cB3e86510726E713 // Contract name: ExpectedRate // Etherscan link: https://etherscan.io/address/0xaBeBf8931C4fFE1b996398d6cB3e86510726E713#code pragma solidity 0.4.18; 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); } 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 } } interface FeeBurnerInterface { function handleFees (uint tradeWeiAmount, address reserve, address wallet) public returns(bool); } 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); } contract WhiteListInterface { function getUserCapInWei(address user) external view returns (uint userCapWei); } contract PermissionGroups { address public admin; address public pendingAdmin; mapping(address=>bool) internal operators; mapping(address=>bool) internal alerters; address[] internal operatorsGroup; address[] internal alertersGroup; 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; } 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. 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. 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; } } } } 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); } } interface ExpectedRateInterface { function getExpectedRate(ERC20 src, ERC20 dest, uint srcQty) public view returns (uint expectedRate, uint slippageRate); } contract ExpectedRate is Withdrawable, ExpectedRateInterface { KyberNetwork public kyberNetwork; uint public quantityFactor = 2; uint public minSlippageFactorInBps = 50; function ExpectedRate(KyberNetwork _kyberNetwork, address _admin) public { require(_admin != address(0)); require(_kyberNetwork != address(0)); kyberNetwork = _kyberNetwork; admin = _admin; } event QuantityFactorSet (uint newFactor, uint oldFactor, address sender); function setQuantityFactor(uint newFactor) public onlyOperator { QuantityFactorSet(quantityFactor, newFactor, msg.sender); quantityFactor = newFactor; } event MinSlippageFactorSet (uint newMin, uint oldMin, address sender); function setMinSlippageFactor(uint bps) public onlyOperator { MinSlippageFactorSet(bps, minSlippageFactorInBps, msg.sender); minSlippageFactorInBps = bps; } function getExpectedRate(ERC20 src, ERC20 dest, uint srcQty) public view returns (uint expectedRate, uint slippageRate) { require(quantityFactor != 0); uint bestReserve; uint minSlippage; (bestReserve, expectedRate) = kyberNetwork.findBestRate(src, dest, srcQty); (bestReserve, slippageRate) = kyberNetwork.findBestRate(src, dest, (srcQty * quantityFactor)); minSlippage = ((10000 - minSlippageFactorInBps) * expectedRate) / 10000; if (slippageRate >= minSlippage) { slippageRate = minSlippage; } return (expectedRate, slippageRate); } } contract KyberNetwork is Withdrawable, Utils { uint public negligibleRateDiff = 10; // basic rate steps will be in 0.01% KyberReserveInterface[] public reserves; mapping(address=>bool) public isReserve; WhiteListInterface public whiteListContract; ExpectedRateInterface public expectedRateContract; FeeBurnerInterface public feeBurnerContract; uint public maxGasPrice = 50 * 1000 * 1000 * 1000; // 50 gwei bool public enabled = false; // network is enabled mapping(address=>mapping(bytes32=>bool)) public perReserveListedPairs; function KyberNetwork(address _admin) public { require(_admin != address(0)); admin = _admin; } event EtherReceival(address indexed sender, uint amount); /* solhint-disable no-complex-fallback */ function() public payable { require(isReserve[msg.sender]); EtherReceival(msg.sender, msg.value); } /* solhint-enable no-complex-fallback */ event ExecuteTrade(address indexed sender, ERC20 src, ERC20 dest, uint actualSrcAmount, uint actualDestAmount); /// @notice use token address ETH_TOKEN_ADDRESS for ether /// @dev makes a trade between src and dest token and send dest token to destAddress /// @param src Src token /// @param srcAmount amount of src tokens /// @param dest Destination token /// @param destAddress Address to send tokens to /// @param maxDestAmount A limit on the amount of dest tokens /// @param minConversionRate The minimal conversion rate. If actual rate is lower, trade is canceled. /// @param walletId is the wallet ID to send part of the fees /// @return amount of actual dest tokens function trade( ERC20 src, uint srcAmount, ERC20 dest, address destAddress, uint maxDestAmount, uint minConversionRate, address walletId ) public payable returns(uint) { require(enabled); uint userSrcBalanceBefore; uint userSrcBalanceAfter; uint userDestBalanceBefore; uint userDestBalanceAfter; userSrcBalanceBefore = getBalance(src, msg.sender); if (src == ETH_TOKEN_ADDRESS) userSrcBalanceBefore += msg.value; userDestBalanceBefore = getBalance(dest, destAddress); uint actualDestAmount = doTrade(src, srcAmount, dest, destAddress, maxDestAmount, minConversionRate, walletId ); require(actualDestAmount > 0); userSrcBalanceAfter = getBalance(src, msg.sender); userDestBalanceAfter = getBalance(dest, destAddress); require(userSrcBalanceAfter <= userSrcBalanceBefore); require(userDestBalanceAfter >= userDestBalanceBefore); require((userDestBalanceAfter - userDestBalanceBefore) >= calcDstQty((userSrcBalanceBefore - userSrcBalanceAfter), getDecimals(src), getDecimals(dest), minConversionRate)); return actualDestAmount; } event AddReserveToNetwork(KyberReserveInterface reserve, bool add); /// @notice can be called only by admin /// @dev add or deletes a reserve to/from the network. /// @param reserve The reserve address. /// @param add If true, the add reserve. Otherwise delete reserve. function addReserve(KyberReserveInterface reserve, bool add) public onlyAdmin { if (add) { require(!isReserve[reserve]); reserves.push(reserve); isReserve[reserve] = true; AddReserveToNetwork(reserve, true); } else { isReserve[reserve] = false; // will have trouble if more than 50k reserves... for (uint i = 0; i < reserves.length; i++) { if (reserves[i] == reserve) { reserves[i] = reserves[reserves.length - 1]; reserves.length--; AddReserveToNetwork(reserve, false); break; } } } } event ListReservePairs(address reserve, ERC20 src, ERC20 dest, bool add); /// @notice can be called only by admin /// @dev allow or prevent a specific reserve to trade a pair of tokens /// @param reserve The reserve address. /// @param src Src token /// @param dest Destination token /// @param add If true then enable trade, otherwise delist pair. function listPairForReserve(address reserve, ERC20 src, ERC20 dest, bool add) public onlyAdmin { (perReserveListedPairs[reserve])[keccak256(src, dest)] = add; if (src != ETH_TOKEN_ADDRESS) { if (add) { src.approve(reserve, 2**255); // approve infinity } else { src.approve(reserve, 0); } } setDecimals(src); setDecimals(dest); ListReservePairs(reserve, src, dest, add); } function setParams( WhiteListInterface _whiteList, ExpectedRateInterface _expectedRate, FeeBurnerInterface _feeBurner, uint _maxGasPrice, uint _negligibleRateDiff ) public onlyAdmin { require(_whiteList != address(0)); require(_feeBurner != address(0)); require(_expectedRate != address(0)); whiteListContract = _whiteList; expectedRateContract = _expectedRate; feeBurnerContract = _feeBurner; maxGasPrice = _maxGasPrice; negligibleRateDiff = _negligibleRateDiff; } function setEnable(bool _enable) public onlyAdmin { if (_enable) { require(whiteListContract != address(0)); require(feeBurnerContract != address(0)); require(expectedRateContract != address(0)); } enabled = _enable; } /// @dev returns number of reserves /// @return number of reserves function getNumReserves() public view returns(uint) { return reserves.length; } /// @notice should be called off chain with as much gas as needed /// @dev get an array of all reserves /// @return An array of all reserves function getReserves() public view returns(KyberReserveInterface[]) { return reserves; } /// @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); } /// @notice use token address ETH_TOKEN_ADDRESS for ether /// @dev best conversion rate for a pair of tokens, if number of reserves have small differences. randomize /// @param src Src token /// @param dest Destination token /* solhint-disable code-complexity */ function findBestRate(ERC20 src, ERC20 dest, uint srcQty) public view returns(uint, uint) { uint bestRate = 0; uint bestReserve = 0; uint numRelevantReserves = 0; uint numReserves = reserves.length; uint[] memory rates = new uint[](numReserves); uint[] memory reserveCandidates = new uint[](numReserves); for (uint i = 0; i < numReserves; i++) { //list all reserves that have this token. if (!(perReserveListedPairs[reserves[i]])[keccak256(src, dest)]) continue; rates[i] = reserves[i].getConversionRate(src, dest, srcQty, block.number); if (rates[i] > bestRate) { //best rate is highest rate bestRate = rates[i]; } } if (bestRate > 0) { uint random = 0; uint smallestRelevantRate = (bestRate * 10000) / (10000 + negligibleRateDiff); for (i = 0; i < numReserves; i++) { if (rates[i] >= smallestRelevantRate) { reserveCandidates[numRelevantReserves++] = i; } } if (numRelevantReserves > 1) { //when encountering small rate diff from bestRate. draw from relevant reserves random = uint(block.blockhash(block.number-1)) % numRelevantReserves; } bestReserve = reserveCandidates[random]; bestRate = rates[bestReserve]; } return (bestReserve, bestRate); } /* solhint-enable code-complexity */ function getExpectedRate(ERC20 src, ERC20 dest, uint srcQty) public view returns (uint expectedRate, uint slippageRate) { require(expectedRateContract != address(0)); return expectedRateContract.getExpectedRate(src, dest, srcQty); } function getUserCapInWei(address user) public view returns(uint) { return whiteListContract.getUserCapInWei(user); } function doTrade( ERC20 src, uint srcAmount, ERC20 dest, address destAddress, uint maxDestAmount, uint minConversionRate, address walletId ) internal returns(uint) { require(tx.gasprice <= maxGasPrice); require(validateTradeInput(src, srcAmount, destAddress)); uint reserveInd; uint rate; (reserveInd, rate) = findBestRate(src, dest, srcAmount); KyberReserveInterface theReserve = reserves[reserveInd]; require(rate > 0); require(rate < MAX_RATE); require(rate >= minConversionRate); uint actualSrcAmount = srcAmount; uint actualDestAmount = calcDestAmount(src, dest, actualSrcAmount, rate); if (actualDestAmount > maxDestAmount) { actualDestAmount = maxDestAmount; actualSrcAmount = calcSrcAmount(src, dest, actualDestAmount, rate); require(actualSrcAmount <= srcAmount); } // do the trade // verify trade size is smaller than user cap uint ethAmount; if (src == ETH_TOKEN_ADDRESS) { ethAmount = actualSrcAmount; } else { ethAmount = actualDestAmount; } require(ethAmount <= getUserCapInWei(msg.sender)); require(doReserveTrade( src, actualSrcAmount, dest, destAddress, actualDestAmount, theReserve, rate, true)); if ((actualSrcAmount < srcAmount) && (src == ETH_TOKEN_ADDRESS)) { msg.sender.transfer(srcAmount - actualSrcAmount); } require(feeBurnerContract.handleFees(ethAmount, theReserve, walletId)); ExecuteTrade(msg.sender, src, dest, actualSrcAmount, actualDestAmount); return actualDestAmount; } /// @notice use token address ETH_TOKEN_ADDRESS for ether /// @dev do one trade with a reserve /// @param src Src token /// @param amount amount of src tokens /// @param dest Destination token /// @param destAddress Address to send tokens to /// @param reserve Reserve to use /// @param validate If true, additional validations are applicable /// @return true if trade is successful function doReserveTrade( ERC20 src, uint amount, ERC20 dest, address destAddress, uint expectedDestAmount, KyberReserveInterface reserve, uint conversionRate, bool validate ) internal returns(bool) { uint callValue = 0; if (src == ETH_TOKEN_ADDRESS) { callValue = amount; } else { // take src tokens to this contract src.transferFrom(msg.sender, this, amount); } // reserve sends tokens/eth to network. network sends it to destination require(reserve.trade.value(callValue)(src, amount, dest, this, conversionRate, validate)); if (dest == ETH_TOKEN_ADDRESS) { destAddress.transfer(expectedDestAmount); } else { require(dest.transfer(destAddress, expectedDestAmount)); } return true; } 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); } /// @notice use token address ETH_TOKEN_ADDRESS for ether /// @dev checks that user sent ether/tokens to contract before trade /// @param src Src token /// @param srcAmount amount of src tokens /// @return true if input is valid function validateTradeInput(ERC20 src, uint srcAmount, address destAddress) internal view returns(bool) { if ((srcAmount >= MAX_QTY) || (srcAmount == 0) || (destAddress == 0)) return false; if (src == ETH_TOKEN_ADDRESS) { if (msg.value != srcAmount) return false; } else { if ((msg.value != 0) || (src.allowance(msg.sender, this) < srcAmount)) return false; } return true; } }
v0.4.18