KyberSwap Exploit 2023-11-23

From WEB3 Vulnerapedia
Jump to navigation Jump to search

On 23rd November 2023, KyberSwap, a decentralized trading platform, was exploited on it's liquidity pools, resulting in a loss of over $48M with KyberSwap announcing it on Twitter swiftly.. These losses were spread out across multiple chains utilizing the same exploitation methodology.

On-chain message from the exploiter to the KyberSwap team just after the attack

The attack was identified by Twitter user @spreekaway showcasing transactions details of multiple chains being drained. From the on-chain messages, the exploiter left notes detailing the exploitation process, mentioning that the vulnerability was about rounding in the wrong direction, and even leaving a "ty" at the end of exploit. After the exploit, the exploiter sent an on-chain message to start negotiations with the KyberSwap team after being fully rested.

Following the message and after a week, the exploiter stated an exchange of returning of funds for complete executive control over KyberSwap and surrender of all assets to the exploiter. This demand was has not yet been accepted despite already past the deadline of the 10th of December.

Around $5.7M worth of funds from KyberSwap pools on Polygon and Avalanche were frontrunned by bots and KyberSwap has been working to negotiate with them on returning those funds for users, about $4.7M worth of funds have been returned so far.

KyberSwap has underwent multiple security audits, ones that applied to the Elastic pool were by ChainSecurity and a Sherlock contest.

Background Knowledge

KyberSwap is an on-chain decentralized automated market maker (CLAMM) platform. With a demand of concentrated liquidity, KyberSwap launched it's liquidity optimization model called KyberSwap Elastic based on Uniswap V3, which allows LPs to designate liquidity at custom price ranges. The main difference between KyberSwap Elastic and Uniswap V3 is that KyberSwap includes a reinvestment curve which enables auto-compounding of liquidity provision yields. This implementation added more complexity to the computeSwapStep function for KyberSwap, and was one of the reasons the exploit was possible.

Due to the sophisticated nature of this exploit, it is easier to understand it with familiarity of how ticks work in Uniswap V3. While the exploit deals with Uniswap V3 mechanisms, the exploit itself is native to Kyber's implementation of concentrated liquidity, so it's highly unlikely this exploit will apply to other concentrated liquidity dexes.

Root cause of vulnerability

The calcReachAmount function that's responsible of calculating the amount of tokens needed for exchange at the scale boundary when both base liquidity and reinvestment liquidity are listed as available to the pool. This function resulted in a higher than expected amount, and since the pool uses an inequality to check sqrtP, it led to the protocol not updating liquidity and crossing the tick as expected through _updateLiquidityAndCrossTick.

This is known as flawed tick calculation, and was 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, leading to the protocol thinking it has more liquidity available than reality.

Background

Ticks

Since LPs can dictate how what prices their liquidity can be traded at, these price ranges which are also known as "ticks". Both Uniswap v3 and KyberSwap (https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic) provide detailed documentation explaining this concept.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").

Active liquidity visualized

Once the price of a certain asset changes, it will cross over certain ticks depending on the set ticks by liquidity providers. There are mechanisms that update the total liquidity available to the pool at any given time.

An example detailing this would be:

Alice having 10 ETH deposited as liquidity, and specifying a tick range of $1000 - $1500.

Bob having 12 ETH deposited as liquidity, and a tick range of $900 - $1300.

The LP at a given tick depends on the tick range specified, so assuming the tick to be at $1100, both Alice's and Bob's liquidity are available to the pool, which is 22 ETH in total.

On the other hand, if the tick is at $980, only Bob's 12 ETH is available to the pool, as once the price drops below $1000, Alice's ETH are no longer included as active liquidity.

This tick changes are important to keep track of, making sure liquidity is added in and removed accordingly is crucial to the protocol, if not, an exploit like double paying would happen, like in this case.

Vulnerability Analysis

Attack Transactions

Exploiter address 1 (ARB, OP, ETH, MATIC, BASE, AVAX): 0x50275e0b7261559ce1644014d4b78d4aa63be836

Exploiter address 2 (ARB, OP, ETH, MATIC, BASE, AVAX): 0xc9b826bad20872eb29f9b1d8af4befe8460b50c6

Example attack tx (ETH): 0x485e08dc2b6a4b3aeadcb89c3d18a37666dc7d9424961a2091d6b3696792f0f3

Steps of the attack in detail

Step
1. Attacker borrows 2000 WETH via flash loan from AAVE.
Flash loan for 2000 WETH
2. Swap WETH for frxETH in the KyberSwap Pool.

Unlike most flash loans, the goal isn't to manipulate the price oracle, instead it's pushing frxETH's price to exceed to range of all LP's positions, making the price range to be in a location where no other liquidity is present except the exploitor's own liquidity. In this case, it was swapping 6.850 WETH for 6.371 frxETH, along with the subsequent steps.

Swapping WETH for frxETH to move the pool price to a curve where there is 0 existing liquidity.
3. Attacker adds liquidity by minting 0.006948 frxETH and 0.1078 WETH and specifies a price range between [110909,111310].
Minting 0.006948 frxETH of liquidity
4. Attacker partially removes this liquidity by burning it, controlling the liquidity amount within that price range to be 74692747583654757908.This ensures that the liquidity amount aligns with the needs of the subsequent stages of the attack.
Removal of liquidity
5. As the attacker is now the sole provider of liquidity in the tick range of [110909,111310] specified from before, the price value of sqrtP at tick 111310 is 20693058119558072255662180724088.
Liquidity when currentTick is at 110909
6. Attacker uses 387.170 WETH to swap for 0.005789 frxETH at the current price tick 110909. This swap increases the current price value sqrt to 20693058119558072255665971001964, surpassing the sqrtP at the boundary tick 111310. This makes the nextTick == currentTick.
Swapping 387.170 WETH for 0.005789 frxETH
Active liquidity when the currentTick is 111310
7. Attacker uses 0.005868 frxETH to reverse swap for 396.2 WETH at a price slightly higher than the sqrtP of tick 111310. This swap makes the price fall back within the [110909,111310] tick range. In this step, liquidity is double counted to make the swap profitable and consequently drain the pool, as the reverse swap yields approximately 9 more WETH than what was exchanged in the forward swap.
Swapping 0.005868 frxETH to 396.2 WETH

How the funds were drained in detail?

State Changes from the second swap

In the attack steps, on the 6th step, where the attacker's swap 387.170 WETH for 0.005789, he received more funds that he deposited compared to the first swap where he swapped 6.850 WETH for 6.371 frxETH.

A clue to this can be noticed on the state changes of the second swap, where the base liquidity baseL is 149385495167309515816. Supposedly, as the second swap ended outside of the attacker's liquidity price range, there should not be any liquidity there, however somehow the state reflects that the pool still thinks there is active liquidity despite the price range not being within one.

There is another difference in the first and second swap, where updateLiquidityAndCrossTick is called in the second but not the first. This function is crucial to adjust the Curve's liquidity value based on the LP's provided tick range, on an event that an LP position goes out of range, the function removes the liquidity from the curve, and vice versa.

In this case, since the updateLiquidityAndCrossTick was not called on the first swap, the pool doesn't remove the active liquidity despite already out of the price range, and when the price goes back into range, it updates and adds the amount into active liquidity, essentially double counting the same amount of liquidity.

Bypassing updataLiquidityAndCrossTick Checks

The attacker managed to bypass the call to updateLiquidity on the first swap through a bug that didn't call the updateLiquidity function by having the price tick move out of range by a small mathematical margin.

computeSwapStep prediction logic

This occured due to computeSwapStep calculates the upper limit of the amount that can be swapped before reaching the tick. If said amount is lesser than remainder of the swap, it confidently assumes the ending price will not reach the tick. But in this case, despite predicting that the tick boundary will not be crossed over, the ending price still went over the prediction.

This issue can be found in Kyberswap's SwapMath.sol library, where quantity calculations for the upper limit until a bound is hit and price change use a slightly different arithmetic.

The bounds check will make sure anything less than X swap quantity will keep you within the tick price, however parallel calculation price change calculation will apply that X swap quantity and wind up outside the tick bound.

Upper bound for reaching tick boundary in WETH/frxETH pool

In this specific case for the pool between WETH to frxETH, the upper bound for reaching the tick boundary was calculated as 6371028957698847497, where the exploiter set a swap quantity of 6371028957698847500 to bypass the updateLiquidity function from being called. By calculations, the check failed by <0.00000000001%.

Analysis of another pool (WETH/wstETH)

Impact

The losses were spread out as follows:

  • >$20M on Arbitrum
  • $15M on Optimism
  • $7.5M on Ethereum
  • $3M on Polygon
  • $2M on Base
  • $23k on Avalanche

Liquidity providers on KyberSwap Elastic pool are affected, resulting in all of their liquidity being drained in the process. Traders are safe as only liquidity is drained.

Fortunately, around $5.7M of funds were arbitraged by MEV bots and with the cooperation of them with KyberSwap authorities, the MEV bot returned 90% of the funds arbitraged.[1]

Mitigation methods

The vulnerability in this case is quite deep and sophisticated, which would be unlikely to be spot during time-based audits. However certain fuzzing parameters can be used to find out edge cases where exploitation is possible, a proof-of-concept utilizing this method can be found below.

From this direct case however, making sure the swap step accounts for even the smallest margins of difference.

Proof of Concept

Fuzz Test for the exploit: https://github.com/paco0x/kyber-exploit-example/blob/main/test/Kyber.t.sol

References