# Yet Another Tragedy of Precision Loss: An In-Depth Analysis of the KyberSwap Incident

By BlockSec

Original Post:

On November 23, 2023, we observed a series of attacks targeting KyberSwap. These attacks resulted in a total loss of over $48M. Our initial analysis suggested that the exploit was due to tick manipulation and double liquidity counting. However, due to space constraints, we are unable to delve into the extensive details within that post. Despite subsequent insightful analyses by other security researchers, the root cause of the issue — precision loss — remained unexposed.

Intriguingly, the plot thickened several days later. On November 30, 2023, following multiple rounds of discussions with officials, the attacker sent a message that, to the outside world, appeared full of provocation, demanding complete control. Setting that aside, the attacker also revealed a crucial piece of information: the issue is indeed related to precision loss, as shown in the figure below. This revelation fortifies the evidence for our investigation. As such, our goal is to present a comprehensive analysis in this report.

# Key takeaways (TL;DR)

- Our investigation reveals that the fundamental issue originates from
. This subsequently leads to improper tick calculation and finally double liquidity counting.*incorrect rounding direction during KyberSwap’s reinvestment process* - This incident underscores
, presenting a substantial challenge to the entire community.*the complex and stealthy nature of precision loss issues within DeFi protocols* - The frequency of these attacks serves as a stark reminder of
, which could significantly help in reducing future losses.*the critical need for proactive threat prevention measures*

In the forthcoming sections, we will first offer some crucial background information about KyberSwap. Subsequently, we will conduct an in-depth analysis of the vulnerability and the associated attack.

# 0x1 Background

KyberSwap[1] is a decentralized automated market maker (CLAMM) platform. To meet the market demand of the concentrated liquidity, KyberSwap Elastic[3] is launched based on Uniswap V3[2], with several improvements including the reinvestment curve to enable the auto-compounding of the liquidity provision yields.

## 0x1.1 Tick and Square Root Price

`Tick`

in Uniswap V3-like CLAMMs is used to mark the price in a discrete manner so that the LPs can provide liquidity within a fixed range instead of the entire range (hence the term "concentrated")[4].

In order to enable LPs to specify liquidity positions with customized price intervals, the protocol needed a way to track the aggregated liquidity across various price points. Uniswap V3 achieved by partitioning the space of possible prices into discrete “ticks” whereby LPs could contribute liquidity between any two ticks.

According to [5], liquidity can be put in a range between any two ticks (which need not be adjacent), i.e., a pair of tick indices (a lower tick and an upper tick). Specifically, the price of each tick (at an integer index *i*) is defined as follows:

In practice, the ** square root price** (denoted as

`sqrtP`

or `sqrtPrice`

) is used:It is also possible to compute the current tick based on the current square root price:

Using the square root price along with liquidityLis a practical way to avoid simultaneous changes. Specifically, price changes when swapping within a tick; liquidity changes when crossing a tick, or when minting or burning liquidity. For a more detailed explanation, please refer to Uniswap V3’s whitepaper[5].

Obviously, ** while only a single square root price is calculated for a given tick, multiple square root prices may point to the same tick**.

## 0x1.2 Reinvestment Curve

Uniswap V3-based CLAMM suffers from the pool utilization of LP fees and significant gas fees required to reinvest. Hence, KyberSwap adopted ** reinvestment curve**[6] to address the problem:

The reinvestment curve was designed with the sole purpose of natively reinvesting otherwise un-utilized LP fees in the concentrated liquidity model. This meant LP fees for concentrated liquidity positions were automatically compounded without the gas nor the manual management overhead. Moreover, LPs still have the option to collect their auto-compounded fee earnings separately at any point in time.

The key to the reinvestment curve is that the fees collected in each swap is accumulated as additional liquidity in the pool as the *reinvestment liquidity* within an infinite range. The reinvestment tokens are minted to the LPs and the reinvestment liquidity accumulated is allocated to the LPs accordingly. Besides, the reinvestment liquidity also participates in the swap and price calculation process.

To be precise, instead of the constant product formula:

fees are accumulated into Δ*L* in each swap:

The calculation of Δ*L* can be simplified into (under the assumption that the price deviation is lower than a threshold):

Then, the swap amount and the final price can be derived from the modified constant product formula:

The corresponding code for the calculations introduced above is shown in the `computeSwapStep`

function in following code snippet of the corresponding pool.

It should be noticed that due to the reinvestment liquidity, the `liquidity`

in this function is a sum of two components: `baseL`

for the base liquidity, and `reinvestL`

for the accumulated liquidity for the reinvestment.

## 0x1.3 Swap in KyberSwap

The control flow of a swap in Uniswap V3 can be depicted as follows[5]:

Accordingly, the implementation of the `swap`

function of the KyberSwap's pool discussed earlier can be abstracted as the below diagram:

The crucial logic pertaining to the tick calculation resides within the swapping while loop, as highlighted by the blue rectangle. Specifically, the principal logic involves the `computeSwapStep`

function and the `_updateLiquidityAndCrossTick`

function. The former calculates key states, such as input and output amounts for the given swap and `nextSqrtP`

, while the latter handles cases when a ** cross-tick** occurs.

Traditionally, when the price increases, we refer to this as shifting the tick right/upward; otherwise, we say the tick moves left/downward.

To better understand the vulnerability that will be discussed later, it’s essential that we explore the relevant code logic of the `computeSwapStep`

function, as illustrated in the following figure:

Firstly, from lines 50 to 57, the `calcReachAmount`

function is invoked to calculate the amount of input token required to reach the `targetSqrtP`

(next tick or user-specified target price).

Next, between lines 59 and 62, a test is conducted to determine whether the tick should be crossed or not.

Specifically, if the amount used (`usedAmount`

) is more than the user specified amount (`specifiedAmount`

) in exact input swap (the case used in the attack), it means that the tick should not be crossed, and the `nextSqrtP`

needs to be derived from the incremental liquidity (`deltaL`

, i.e., the delta liquidity).

- Subsequently, between lines 70 and 79, the Δ
*L*(`deltaL`

) is derived from the input amount, current liquidity and price using the`estimateIncrementalLiquidity`

function. Finally, the final price after the swap`nextSqrtP`

is calculated based on the`deltaL`

, input amount, current price and liquidity, using the`calcFinalPrice`

function.

Conversely, if the required amount is less than the user specified amount (which means `nextSqrtP`

> 0), the `deltaL`

is calculated using the current and target `sqrtP`

, and the `nextSqrtP`

is the `sqrtP`

on the next tick. The details are omitted because this branch is not used in the attack.

The steps outlined above make clear that if the tick is not crossed, the `nextSqrtP`

returned by `computeSwapStep`

should not be larger than the `sqrtP`

of the next tick. However, due to the dependency of the price on the liquidty (base liquidity and delta liquidity) and precision loss, the attackers is able to manipulate the `nextSqrtP`

to be larger while the tick is not crossed.

# 0x2 Vulnerability Analysis

The root cause lies in the flawed tick calculation caused by the incorrect rounding direction within the delta liquidity calculation (i.e., the `estimateIncrementalLiquidity`

function) of the `SwapMath`

contract (which is invoked by the `computeSwapStep`

function). This, in turn, improperly affects the tick calculation later.

Interestingly, upon examining the comment at line 188 (highlighted by the blue rectangle), we find that `deltaL`

is intended to be rounded up in order to round down the `nextSqrtP`

. However, `deltaL`

is mistakenly rounded down due to the use of the `mulDivFloor`

function at line 189. Consequently, `nextSqrtP`

is inaccurately rounded up.

# 0x3 Attack Analysis

The attackers initiated multiple attack transactions, with each transaction draining multiple pools. For the sake of simplicity, the following discussion is based on the first attack within the attack transaction.

The core attack logic consists of the following six steps:

1. Borrowing 2,000 WETH via flash loan from AAVE.

2. Swapping 6.850 WETH for 6.371 frxETH in the victim pool 0xfd7b. This step is used to push current tick and `currentSqrtP`

into a location where currently no liquidity is present.

`currentSqrtP`

seems to be randomly chosen by the attacker, and the swap stops at this price precisely.- Base liquidity (
`baseL`

) is zero after this step, but the reinvestment liquidity (`reinvestL`

) is non-zero.

3. Adding liquidity into the pool and then removing part of the liquidity. This step is used to control the range and total liquidity to a desired amount.

- The tick range is chosen based on the
`currentSqrtP`

. - The desired liquidity for the attack could be derived from the tick range, although the corresponding calculation logic requires further exploration.

4. Swapping 387.170 WETH for 0.06 frxETH in the pool. This step is used to manipulate the current tick so that **nextTick**** == **

.**currentTick**

- The input amount is chosen based on the liquidity and
`currentSqrtP`

.

5. Swapping 0.06 frxETH for 396.244 WETH in the pool. Note that the swapping direction is opposite compared to the previous step. In this step, liquidity is double counted to make the swap profitable and consequently drain the pool.

6. Repaying the flash loan, and harvesting 6.364WETH and 1.117 frxETH.

Obvisouly, the last two swaps (step 4 and step 5) are the key attack steps to manipulate the tick calculation and make the swap profitable to drain the pool. We will delve into the details in the following sub-sections.

It’s important to note that step 3 is crucial for manipulating the liquidity. Due to the need for precise tick manipulation through the rounding operation, achieving the goal by directly adding liquidity is unfeasible. The liquidity removal is to precisely control the liquidity in the range as the attacker desired.

# 0x3.1 Step 4: manipualte the current tick and `currentSqrtP`

After previous steps (step 1 and 2), the attacker has prepared the tick range and liquidity for manipulation. Specifically:

`currentSqrtP`

is in a desired location- current tick = 110,909 and next tick = 111,310, surrounding the
`currentSqrtP`

This step swaps WETH for frxETH. In the `computeSwapStep`

function, we have the following execution trace:

As shown in the above figure, the amount to reach the target (i.e., the next tick) will be calculated by invoking the `calcReachAmount`

function:

`usedAmount`

=`calcReachAmount`

(`liquidity`

,`currentSqrtP`

,`targetSqrtP`

)

Notice that this calculation can be derived before the swap. By carefully chose the `specifiedAmount`

(`usedAmount`

= `specifiedAmount`

+ 1), the attacker controlled the swap so that the target (i.e., next tick 111,310) is not reached, resulting in that `nextSqrtP`

= 0.

In this situation, because the tick is not crossed, `nextSqrtP`

(i.e., the final price) needs to be derived from the delta liquidity (accumulated as swap fees).

First, the incremental liquidity `deltaL`

from the fees is calculated by:

`deltaL`

=`estimateIncrementalLiquidity`

(`absDelta`

,`currentSqrtP`

)

Then the final price `nextSqrtP`

:

`nextSqrtP`

=`calcFinalPrice`

(`absDelta`

,`liquidity`

,`deltaL`

,`currentSqrtP`

)

Revisiting the rounding direction error discussed in the previous section, here `deltaL`

is erroneously rounded down, leading to `nextSqrtP`

being rounded up. Specifically, in this case, based on the same `absDelta`

(387,170,294,533,119,999,999), the calculation results differ due to varying rounding directions:

Therefore, after the tick manipulation in step 4, the current states are summarized as follows:

`currentSqrtP`

is 20,693,058,119,558,072,255,665,971,001,964, slightly larger than the`sqrtP`

at tick 111,310 (sqrtP at 111,310 = 20,693,058,119,558,072,255,662,180,724,088).- current tick = 111,310 and next tick = 111,310

As illustrated in the figure above, ** the swap in step 4 cunningly deceives the pool into believing that the tick 111,310 is not crossed**. However,

*in reality, the**currentSqrtP*

*is indeed greater than the**sqrtP*

**.**

*of tick 111,310*# 0x3.2 Step 5: double the liquidity counting

Based on the manipulation in step 4, the attack logic in step 5 is reasonably straightforward. At this stage, the attacker orchestrated a reverse swap from frxETH to WETH, which would shift the tick and the `currentSqrtP`

towards the left. Specifically, the `computeSwapStep`

function is invoked twice within the loop, which ultimately triggers the double liquidity counting[7] in an unforeseen manner and consequently generates additional profits.

As shown in the above trace:

- In the first invocation of the
`computeSwapStep`

function, the`currentSqrtP`

was shifted to the`sqrtP`

of tick 111,310. This is a tiny swap that only uses*3 wei of frxETH*to actually reach tick 111,310. Subsequently, within the`_updateLiquidityAndCrossTick`

function, the current tick should cross tick 111,310 (moving left/downwards), even though it hasn't truly traversed tick 111,310 in a right/upward direction in step 4. This results in liquidity at tick 111,310 being**counted twice**. - In the second invocation of the
`computeSwapStep`

function, the previous double counting of liquidity can lead to the potential for additional profits. Specifically, by taking advantage of this liquidity double count, the swap price in the concluding step is skewed, leading to a larger amount of WETH being swapped out, thereby generating a profit.

# 0x4 Summary of Attacks and Profits

As of this writing, we have observed several attacks on different chains (including Ethereum, Optimism, Polygon, Arbitrum, Avalanche and Base) in the wild, causing losses in excess of **$48M**. These attacks were launchecd by different attackers, as follows:

A full list of these attack transactions has been collected in a document we prepared. Please refer to it for more detailed information.

# 0x5 Conclusion

In conclusion, this is a subtle vulnerability originating from improper rounding logic. The exploit is incredibly sophisticated. In fact, this year we’ve observed a series of security incidents related to precision loss issues, posing significant challenges for the community.

Once again, these continuous attacks demonstrate the importance of proactive threat prevention, a strategy that could effectively help mitigate potential losses.

# References

[1] https://docs.kyberswap.com/

[2] https://blog.uniswap.org/uniswap-v3

[3] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic

[4] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/tick-range-mechanism

[5] https://uniswap.org/whitepaper-v3.pdf

[6] https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/reinvestment-curve