By: Jiujiu & Kong & Lisa
background
According to the SlowMist security team, on November 23, 2023, the decentralized trading platform KyberSwap was attacked, and the attacker made a profit of approximately US$54.7 million. The SlowMist security team immediately intervened in the analysis and shared the results as follows:
root cause
Due to the Reinvestment Curve feature of KyberSwap Elastic, when the base liquidity and reinvested liquidity are calculated as actual liquidity, the pool calculates the number of tokens required for redemption at the scale boundary through the calcReachAmount function, which is greater than expected, causing the next price sqrtP to exceed the sqrtP of the boundary scale, and the pool uses an inequality sign to check sqrtP, resulting in the protocol not updating liquidity through _updateLiquidityAndCrossTick as expected.
Prerequisites
Before we begin the analysis, we need to understand some key knowledge about KyberSwap in order to understand the content of this analysis.
KyberSwap is an on-chain decentralized trading platform with a new liquidity optimization model, KyberSwap Elastic. The centralized liquidity market maker mechanism adopted by this model enables LPs to allocate liquidity to customized price ranges, and introduces a reinvestment curve to automatically compound the idle liquidity fees in the pool for LPs.
First of all, what is a centralized liquidity market maker (CLMM)? Similar to Uniswap v3, liquidity providers can provide liquidity with their funds within a custom price range, and their liquidity will only be used when the price falls within this range. Uniswap v3 (https://blog.uniswap.org/uniswap-v3) and KyberSwap (https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic) both provide detailed documentation for explanations. Here we use a simple ETH/USDC pool diagram to illustrate what you need to know to read this article:
From: KyberSwap Elastic Liquidity Concept (https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic/concepts/concentrated-liquidity#liquidity-tracking-lp-contributions-at-a-specific-price)
The above is an ETH/USDC pool with three identical liquidity positions, and the current price is 1995. In CLMM, the price range is called tick-range. The tick range of position 1 liquidity is 1960-2020, the tick range of position 2 liquidity is 1980-2000, and the tick range of position 3 liquidity is 1990-2000.
It can be seen from the figure that the current range is the range with the best liquidity, and the liquidity of the three positions overlaps on tick 1990-2000. When the ETH price drops to 1985, its price will move to the left and cross the range of tick 1990. At this time, it will leave the liquidity range of position 3, but it is still in the liquidity range of positions 1 and 2. Therefore, the liquidity in the new range will be updated, and the liquidity of position 3 will be excluded. When the ETH price rises to 2005, its price will move to the right and cross the range of tick 2000. At this time, the liquidity of positions 2 and 3 will be excluded, but it is still in the range of position 1. That is, when the price crosses the liquidity boundary, the liquidity will be updated, either increasing or decreasing.
Unlike Uniswap v3, KyberSwap Elastic innovatively introduces a new feature - Reinvestment Curve. This is an additional AMM pool that accumulates the fees collected by users in the pool, and its curve supports a price range from 0 to infinity. KyberSwap Elastic aggregates the reinvestment curve with the original price curve (that is, the curve is separated, but the funds are still in the same pool), so that LP's fees can be compounded even when the price exceeds the range of their positions.
After a brief understanding of the mechanism of KyberSwap Elastic, we will analyze the attack steps.
Attack Steps Analysis
Here we take the attack transaction 0x485...0f3 as an example for analysis:
1. The attacker first flash-loans 2,000 WETH from AAVE, and uses 6.8496 WETH to exchange for frxETH in the KyberSwap pool, causing the price of frxETH to exceed the position range of all liquidity providers. At this time, the current price value sqrtP (the current price multiplied by the square root of 2^96) is pulled up to 20282409603651670423947251286016, which is located on tick 110909.
2. Next, the attacker added 0.006948 frxETH and 0.1078 WETH as liquidity in the specified price range [110909,111310], and then removed some of the liquidity, and finally controlled the liquidity value in the price range to 74692747583654757908 so that the liquidity meets the amount required for subsequent attack calculations. At this time, the attacker is the only one with liquidity in the tick [110909,111310] interval, and the price value sqrtP of tick 111310 is 20693058119558072255662180724088.
3. The attacker then exchanged 387.17 WETH for 0.005789 frxETH at the current price scale of 110909. This large-scale exchange raised the current price value sqrtP to 20693058119558072255665971001964, exceeding the sqrtP on the boundary scale of 111310.
4. Finally, the attacker used 0.005868 frxETH to reversely exchange 396.2 WETH from the sqrtP slightly larger than the price scale of 111310. After the exchange, the price fell back to the scale range of [110909,111310]. At this time, the attacker has made a profit, and the reverse exchange has exchanged about 9 more WETH than the forward exchange.
Why can such a simple attack step exchange more funds than expected? This is closely related to the reinvestment curve of KyberSwap Elastic. Let’s analyze it in detail to reveal its profit method.
Analysis of the attack principle
Through the above steps, we know that more funds than expected were exchanged in the last step of reverse exchange. When the exchange was performed, the current sqrtP was 20693058119558072255665971001964, which is greater than the price of tickUpper 111310 when the attacker added liquidity. We use a scale chart to indicate its position.
Since it exceeds the range of the liquidity scale [110909,111310] added by the attacker, the current position of sqrtP is theoretically illiquid. During the exchange process, it can only cross the 111310 scale to the left to obtain effective liquidity for exchange. We will follow up to see whether it is exchanged as expected.
As shown in the figure below, when we check the liquidity of the current scale where sqrtP is located, we find that without considering the reinvestment curve, there is an unexpectedly large amount of liquidity in the range where there should be 0 liquidity, which is much larger than the liquidity of the reinvestment curve, and the liquidity amount is consistent with the scale range [110909,111310].
This allows for a valid token swap to take place at tick 111310 when a swap is made, as shown in the image below.
After the effective exchange at tick 111310, sqrtP will cross this tick and enter the range [110909,111310] to exchange the remaining tokens. Looking back at the previous knowledge, we know that liquidity will be updated when crossing the liquidity position range. In KyberSwap Elastic Pool, the liquidity in the range [110909,111310] will be added to the curve through the _updateLiquidityAndCrossTick function to participate in the token exchange, as shown in the figure below.
This will cause the effective liquidity within the scale range [110909,111310] to be added to the extra false liquidity to the right of scale 111310, resulting in the total effective liquidity when exchanging within the scale range [110909,111310] to be much greater than expected. As shown in the figure below, the effective liquidity has doubled than expected.
Because the increase in liquidity in the current scale range makes the pool deeper than expected, the attacker can obtain more funds than expected. These additional funds come from the liquidity of other scale ranges in the pool.
Why is there unexpected liquidity on the right side of tick 111310, and the amount of liquidity is the same as the liquidity in the range [110909,111310]? The only explanation is that the pool did not perform the liquidity update operation as expected during the previous exchange. As shown in the figure below, in theory, when the previous exchange crosses tick 111310, the _updateLiquidityAndCrossTick function should also be called to update the liquidity after sqrtP enters the right side of tick 111310.
Let's actually analyze this exchange process. When performing an exchange, the Pool will calculate the actual amount used for exchange, as well as the exchange fee and the new sqrtP price value through the computeSwapStep function. In theory, when crossing the liquidity range, the calculated sqrtP result will be the sqrtP of scale 111310 that falls on the boundary of the range. But in fact, the new sqrtP has exceeded the sqrtP of scale 111310. As shown in the figure below, the sqrtP of scale 111310 is 20693058119558072255662180724088, but the actual sqrtP is 20693058119558072255665971001964.
Since the new sqrtP does not fall on the sqrtP of the boundary scale 111310, that is, swapData.sqrtP != swapData.nextSqrtP, the pool will think that the current sqrtP is still in the range of [110909,111310], so the liquidity check operation will be skipped and the _updateLiquidityAndCrossTick function will not be triggered for liquidity update!
But why does the new nextSqrtP not fall on the boundary? By analyzing the calculation of calcReachAmount, we can find that the amount of attack exchange 387170294533119999999 is just less than the amount of liquidity in the current range 387170294533120000000.
This means that nextSqrtP will not be assigned the value of targetSqrtP, but will remain 0, so it will directly perform the final sqrtP calculation through the calcFinalPrice function, which makes its calculation result greater than the sqrtP of the scale 111310.
Therefore, the calcReachAmount function is the key, which is used to calculate the number of tokens required in the exchange from currentSqrtP to targetSqrtP. By analyzing its calculation formula, we can know that its calculation result mainly depends on the current liquidity L, which is the sum of the basic liquidity and reinvestment liquidity.
We all know that there is no reinvestment curve feature in Uniswap v3, so is it because of the addition of reinvestment liquidity that the calculated result of calcReachAmount is larger than expected?
Through testing, without including reinvestment liquidity, the result of calcReachAmount is 387160697969657129472, which is less than the amount swapQty 387170294533119999999.
Without including reinvestment liquidity, the sqrtP calculated by computeSwapStep falls exactly on the scale of 111310:
Therefore, the truth is revealed. Due to the Reinvestment Curve feature of KyberSwap Elastic, the number of tokens required to calculate the exchange from the current sqrtP to the scale boundary sqrtP using the basic liquidity and reinvestment liquidity will be greater than expected, which causes the sqrtP after the exchange to exceed the sqrtP of the scale boundary, making the protocol believe that the liquidity within the current scale range has met the exchange requirements, and then stop the operation of updating liquidity across the boundary scale.
MistTrack Analysis
KyberSwap Exploit 1:0x50275e0b7261559ce1644014d4b78d4aa63be836
KyberSwap Exploiter 2:0xc9b826bad20872eb29f9b1d8af4befe8460b50c6
KyberSwap Exploiter 3:0xae7e16cAa7a4d572FfF09924Bf077a89485850Cb
KyberSwap Exploiter 4:0xd01896e3D4F130Ffd6f6a5A9d6780bbd7008d71d
According to MistTrack analysis, KyberSwap attackers made a total profit of more than US$54.7 million, involving Ethereum, BSC, Arbitrum, Optimism, Polygon, BASE, Scroll, and Avalanche chains.
On Ethereum, the initial funding for KyberSwap Exploiter 1 came from 20 ETH transferred from Tornado Cash. Of this, 0.1 ETH was transferred to KyberSwap Exploiter 2, 2 ETH was transferred to FixedFloat, and 6.5 ETH was transferred to Arbitrum, Optimism, Scroll, and Base chains respectively. KyberSwap Exploiter 2 made a profit of over $7.58 million worth of tokens, including USDC, WETH, KNC, etc., which have not yet been transferred.
On BSC, KyberSwap Exploiter 1 received 4.2678 BNB transferred from FixedFloat, which has not been transferred yet as a balance.
On Arbitrum, KyberSwap Exploiter 2 made profits of tokens worth more than $20.29 million, including WBTC, WETH, ARB, DAI, etc., of which 500 WETH was transferred to 0x98d69d3ea5f7e03098400a5bedfbe49f2b0b88d3, and this address transferred 300 WETH across chains to Ethereum, but has not yet been transferred. It is worth noting that KyberSwap Exploiter 2 transferred 1,000 WETH to the address of Indexed Finance Exploiter 0x84e66f86c28502c0fc8613e1d9cbbed806f7adb4.
On Optimism, KyberSwap Exploiter 2 has made a profit of over 15.64 million USD in tokens, including wstETH, WETH, OP, DAI, etc., which have not been transferred yet.
On Polygon, KyberSwap Exploiter 1’s initial funding came from 2,666.1243 MATIC transferred from FixedFloat, and then 100 MATIC was transferred to KyberSwap Exploiter 2. The current balance of Exploiter 1 is 2,564.0016 MATIC; KyberSwap Exploiter 2 made a profit of more than 2.93 million US dollars in tokens, including WBTC, WETH, DAI, etc., which have not been transferred yet; KyberSwap Exploiter 3 made a profit of more than 5.75 million US dollars in tokens, including wstETH, USDT, USDC, etc., and transferred most of the tokens to the address 0xa4c92d7482066878bb1e2c0510f42b20d79a7ea9.
On BASE, KyberSwap Exploiter 2 made a profit of over 1.95 million USD in tokens, including USDC, WETH, etc., which have not been transferred yet.
On Avalanche, KyberSwap Exploiter 1’s initial funding came from 49 AVAX transferred from FixedFloat; KyberSwap Exploiter 2 made a profit of over $23,500 in tokens, including 293.0756 WAVAX and 17,316.0305 USDC, which have not been transferred yet; KyberSwap Exploiter 4 made a profit of over $565,000 in tokens, including WAVAX, USDC, etc., and transferred USDC to the address 0x9296fa3246f478e32b05d4dde35176d927be703f.
The SlowMist security team has blacklisted the relevant addresses, and most of the funds have not yet been transferred. We will continue to monitor the movement of funds.
in conclusion
The root cause of this attack is that in the calculation of the exchange from the current price to the boundary scale price, the number of tokens required will be increased by the compound interest of the handling fee due to the reinvestment curve of KyberSwap Elastic, resulting in a larger calculation result than expected, which can cover the user's exchange needs, but the actual price has crossed the boundary scale, making the protocol believe that the liquidity within the current scale range has met the exchange needs, so no liquidity update is performed. Ultimately, the liquidity increased twice when the reverse exchange crossed the boundary scale, allowing the attacker to obtain more tokens than expected.
The SlowMist security team recommends that when designing an economic model, boundary conditions should be fully tested and liquidity and price should be strictly judged rather than using inequality signs for checking.
reference
Attacker address: 0x50275e0b7261559ce1644014d4b78d4aa63be836
Attack contract: 0xaf2acf3d4ab78e4c702256d214a3189a874cdc13
Related attack transactions:
0x485e08dc2b6a4b3aeadcb89c3d18a37666dc7d9424961a2091d6b3696792f0f3
0x09a3a12d58b0bb80e33e3fb8e282728551dc430c65d1e520fe0009ec519d75e8
0x396a83df7361519416a6dc960d394e689dd0f158095cbc6a6c387640716f5475