Kyber Network Smart Contract

Yaron Velner
Kyber Network
Published in
10 min readJan 9, 2018

--

Over the last few weeks, we have focused our efforts on adding features to our MVP smart contracts and making them production-ready, as we inch ever closer towards mainnet pilot launch. In this blog post, we provide a summary of the entire smart contract system, beginning with a high-level overview of the functionality of the contracts, followed by a detailed discussion of the functionality and security model, and finally a description of key technical details and optimizations.

High-level overview

Kyber Network’s smart contract allows users to send one type of token (e.g. GNT) and receive a different token in return (e.g. ETH) according to market conversion rates. This all happens in a single transaction so at no point does the contract hold users’ funds. A conversion between ETH and GNT is depicted in the diagram below.

The conversion is possible thanks to reserve contracts that hold inventories of tokens and provide conversion rates to Kyber Network. The inventory of every reserve, along with its price feed, is managed by a reserve manager, namely, a person or an off-chain automated system that queries market prices and buys/sells inventory from the open market.

The user does not pay any fees. Platform fees are paid by the reserve that executes the exchange. The platform fees are eventually burned. In addition, some fees might be paid for the website/mobile application that directed the user to Kyber Network smart contract.

Detailed overview

Trust and security model

Who should the user trust? In general, the user need not trust Kyber Network with their funds. They can provide a minimal conversion rate, which guarantees that their exchange is either executed at this specified rate or at a better one, otherwise the entire transaction is reverted. While user funds are not at risk in such cases, they do lose some gas fees as dishonest behaviors of Kyber Network reserves (e.g. large, frequent changes in offered rates) could potentially increase gas fee losses for the user.

Who should Kyber Network trust? At this point, for Kyber Network to be fully functional, there needs to be at least one reserve running at all times. Kyber Network already operates a reserve of its own, so one can safely assume smooth, uninterrupted operations. The network admin should trust (i.e. read) the code of KyberReserve.sol and the listed ERC20 tokens.

Who should the reserve manger trust? At this point, the reserves must trust the honest behavior of Kyber Network administrators. While reserve funds are not at risk, Kyber Network administrators have the ability to halt the reserve operations within the platform. In addition, the reserve manager could be affected by extreme market conditions like flash crashes or from sub-optimal inventory management (e.g. setting wrong prices or from large exposure to risky tokens).

Smart contract architecture

Every contract in our system has three permission groups namely, admin, operators, and alerters. The admin account is unique (usually cold wallet) and handles infrequent, manual operations like listing new tokens in the exchange. The operator account is a hot wallet and is used for frequent updates like setting reserve rates and withdrawing funds from the reserve to certain destinations (e.g. when selling excess tokens in the open market). The alerter account is also a hot wallet and is used to alert the admin of consistencies in the system (e.g., strange conversion rates). In such cases, the reserve operation is halted and can be resumed only by the admin account.

Conceptually, the two main components in the system are KyberNetwork.sol and KyberReserve.sol which together implement Kyber network and the reserve(s). However, additional auxiliary contracts are needed to create upgradable components that might require frequent updating, and also because solidity contract size is limited to 25 Kb. In such cases, we split the contract functionality for multiple contracts, shown below:

KyberNetwork.sol

The user has two endpoints in the contract. First endpoint is to query conversion rate.

function getExpectedRate(ERC20 source, ERC20 dest, uint srcQuantity)
public view
returns (uint expectedRate, uint slippageRate)

which returns to the user the conversion rate, and an expected worst-case conversion rate (i.e. price update could happen before the user transaction is confirmed). It is recommended that the user will use slippagePrice as minConversionRate (in the trade function). The logic for slippagePrice depends on many parameters like average exchange volume and token price volatility. Hence, the logic is implemented in an upgradable contract (ExpectedRate.sol). The logic can be heavy as this is typically called off chain by an eth_call RPC.

The user endpoint to performs an exchange with the function

function trade(
ERC20 source,
uint srcAmount,
ERC20 dest,
address destAddress,
uint maxDestAmount,
uint minConversionRate,
address walletId
)

which converts source token into dest token and sends it to destination address (as a convention a special constant 0xeeee…eee denotes ETH). The conversion is done at a rate that is at least min conversion rate. Some fees are provided to the wallet id address, provided it is part of Kyber Network’s affiliation program.

When a trade is executed the contract queries the rates from all of the reserves (i.e., calls KyberReserve.sol). From the conversion rate, the ETH amount of the exchange is estimated (at this point, the system supports only ETH to/from token conversion). With the ETH amount, the contract checks if the exchange amount extends user cap. User cap might be set according to multiple criteria, e.g, users who did KYC will get higher cap. This logic could change over time, and is thus implemented in an auxiliary upgradable contract called KyberWhiteList.sol.

After user eligibility is determined, the network moves user funds to the reserve with the best rate, and in return gets the destination token from the reserve and sends it to the destination address. After the exchange, the contract burns the platform fees and transfers some of the fees to the affiliated wallet. This is done by calling the upgradable contract FeeBurner.sol. In the current implementation, to reduce user gas costs, the fees are not burned immediately, instead we maintain a counter that records the amount to be burned. The actual burn is done in a different function call, which can be called by anyone.

The exchange flow is depicted in the figure below:

ETH to GNT exchange overview

KyberReserve.sol

The reserve’s role is to execute exchanges and provide rates for Kyber Network. The contract has no direct interaction with the end users (the only interaction with them is via the network platform). Its main interaction is with the reserve operator who manages the token inventory and feeds exchange rates every few minutes. The contract has two auxiliary contracts, Pricing.sol and SanityPricing.sol. The first contract provides logic to maintain simple on-chain adjustment to the prices and an optimized cheap (w.r.t gas consumption) interface to enter rate feeds. The second contract provides sanity price feeds. If there are large inconsistencies between the sanity prices and the accurate prices, then user exchanges are disabled. The sanity module protects both parties from bugs in the accurate pricing logic and both from hacks into the accurate pricing system. Having it in a different contract gives rise to two different operators, each having access to only one of the contracts. Thus, the sanity pricing module mitigates the damage of a potential hack to the reserve operator account.

For on-chain pricing adjustments, rates are determined according to market rate and according to the inventory state. For example, if a reserve manager aims towards having holdings of 1000 OMG (some of which could be outside the contract), but his current holdings are 900 OMG, then he would offer additional OMGs at a rate that is worse than market rate. Most of this logic is handled off-chain by the reserve manager system (which runs on a server) who feeds the relevant data every few minutes. However, this system might not be able to respond in real time, as several transactions can be processed in a single block. Hence, the pricing module (via VolumeImbalanceRecorde.sol) records the inventory imbalance that was generated since the last price update in the current block. ccording to the recent imbalance, small adjustments to the price are made in the contract. The contract stops serving requests if the imbalance is too big, waiting until the reserve manager sets new price feeds which take into account the current imbalance. This serves two purposes, the first is to give the reserve manager a chance to respond before the imbalance becomes too big. The second is to avoid wild speculation of shifts in the market price as a result of sudden big imbalances. In this case, the contract bounds the maximal imbalance between price updates and gives the reserve manager time to respond. Another consideration in pricing is the required quantity. The market price of 100 tokens is usually better than the market price of 1000 tokens, since low quantity of tokens can be more easily purchased in the exchanges. For this purpose, the contract supports a step function that offers different rates according to different required quantities.

The exchange process, from KyberReserve.sol point of view is depicted in the figure below:

An exchange from reserve contract point of view

Technical details

  1. Rate definition and resolution: A rate is defined between source token and dest token. The amount of dest quantity is determined according to
dest_amount = src_amount * rate / PRECISION

where PRECISION is 10¹⁸. For example if rate is 2*10¹⁸, then for every source token, two dest tokens are given. It is important to note that this formula holds for a complete token. For basic token units, a.k.a, token wei (twei), the decimals of every token should be taken into account. For example if source token decimals is 8, and destination is 18, then for every 10⁸ twei of source, 2*10¹⁸ twei are given.

2. Trust between KyberNetwork.sol and KyberReserve.sol. At this point, every reserve manager will have to use KyberReserve.sol smart contract. However, even if the KyberReserve.sol code is not trustable, the worse that could happen from the user’s perspective is a loss of gas fees. On the other hand, the reserve must trust/understand KyberNetwork.sol code, as it feeds it the conversion rate. This will save an additional calculation of rates — a gas-consuming process.

3. Transferring tokens between KyberNetwork.sol and KyberReserve.sol: To save gas fees, KyberNetwork.sol gives infinite allowance of tokens to the KyberReserve.sol contract. This is reasonable as the network balance should always be 0. Hence, when user sends token, the network simply passes it to its possession (using transferFrom), and the chosen reserve can just take it from the network with another call to transferFrom. In the converse direction, the reserve simply sends tokens to the network without the approve/transferFrom sequence, and network sends them to the user. This is acceptable as the network balance is 0; hence, if the reserve did not send the tokens, the operation will fail.

4. Efficient price feeds: as price updates are expected to happen every few minutes, it is imperative that prices are represented in a compact data structure (on-chain storage operations are expensive). For this purpose, we store rates in the following form: a base price consists of 256 bits, and an 8 bits compact price that represents the delta in 0.1% units from the base price. We note that the 8 bits stand for signed integer, and thus the price difference can be in the order of +-12.8%. When the market price deviates in greater amounts, the base price is also updated.

5. Volume recording algorithm: to reduce gas costs we aim to make only one storage write to maintain the imbalance since the last price update. Since a price update that was broadcast at block 1000 could only have been confirmed at block 1002, it is necessary to take into account the imbalance that was generated during blocks 1000 and 1001, and potentially a part of block 1002. For this purpose we keep a window of size 5 (where 5 blocks serve as an upper bound for transaction confirmation time). To prevent the need for a counter update (which is a storage operation), whenever imbalance is fetched, we read all the 5 units and deduce the imbalance from that. When imbalance is added, we simply record it in current_block_number % 5

6. User gas price limit: to mitigate flash crashes or sudden price changes, we limit the user exchange gas price. This allows the reserve operator to send price updates with higher gas prices and make sure user exchange is done at market price.

7. Bytes tricks: We wanted to maintain a structure with several fields that consume in total 32 bytes in two locations. However, to our surprise, when looking at the generated assembly code, the solidity compiler produced a code that was using multiple SSTORE op codes to update the structure content. Hence, we were forced to maintain a 32 bytes integral type (bytes32 our uint256) and extract the needed bytes with byte operations.

8. Breaking contract functionality into multiple contracts: we were forced to split the Pricing.sol contract from KyberReserve.sol since the code size was too big to deploy in a single transaction. For the same reason we also had to split FeeBurner.sol from KyberNetwork.sol contract. Splitting FeeBurner.sol also allows the reserve manager to mitigate bugs in the fee calculation. The reserve manager can limit the KNC allowance it gives, and thus bound the total amount of KNC that can be burned. On the other hand, this could give rise to reserve managers avoiding payment of fees (simply by resetting the allowance). We have decided that this risk is tolerable and manageable at this point in time.

9. The following contract are likely to be updated frequently in the first months of Kyber Network exchange operation: (i) ExpectedRate.sol (ii) KyberWhiteList.sol (iii) SanityRates.sol

--

--