Background
According to intelligence from the SlowMist Security Team, on November 23, 2023, the decentralized trading platform KyberSwap was attacked, resulting in the attacker stealing approximately $54.7 million. We immediately conducted an analysis of this incident and these were our findings.
Root Cause
Due to the Reinvestment Curve feature of KyberSwap Elastic pool, when both base liquidity and reinvestment liquidity are considered as actual liquidity, it calculates the amount of tokens needed for exchange at the scale boundary using the calcReachAmount function. This calculation resulted in a higher than expected amount, causing the next price sqrtP to exceed the boundary scale’s sqrtP. The pool, using an inequality to check sqrtP, led to the protocol not updating liquidity and crossing the tick as expected through _updateLiquidityAndCrossTick.
Preliminary Knowledge
Before diving into the analysis, it’s essential to understand some key concepts about KyberSwap to grasp the content of this analysis.
KyberSwap is an on-chain decentralized trading platform featuring a novel liquidity optimization model called KyberSwap Elastic. This model uses a Concentrated Liquidity Market Maker (CLMM) mechanism, allowing Liquidity Providers (LPs) to allocate liquidity to custom price ranges. It also introduces a reinvestment curve that automatically compounds idle liquidity fees in the pool for LPs.
Firstly, what is a Concentrated Liquidity Market Maker (CLMM)? Similar to Uniswap v3, liquidity providers can offer their funds within a custom price range, and their liquidity is only utilized when the price falls within this range. Both Uniswap v3 (https://blog.uniswap.org/uniswap-v3) and KyberSwap (https://docs.kyberswap.com/liquidity-solutions/kyberswap-elastic) provide detailed documentation explaining this concept. For the purpose of this article, let’s use a simplified ETH/USDC pool illustration to understand the necessary knowledge.
The illustration above represents an ETH/USDC pool with three identical liquidity positions, with the current price at 1995. In CLMM, the price range is referred to as a tick-range. Position 1’s liquidity is in the tick range of 1960–2020, position 2’s liquidity is in the tick range of 1980–2000, and position 3’s liquidity is in the tick range of 1990–2000.
From the diagram, it’s evident that the current range is where liquidity is most optimal, with all three positions overlapping in the tick range of 1990–2000. When the price of ETH falls to 1985, it will move left, crossing the tick range of 1990. This will cause it to exit the liquidity range of position 3 but remain within the ranges of positions 1 and 2, thus updating the liquidity within the new range and excluding the liquidity of position 3. Conversely, when the ETH price rises to 2005, it will move right, crossing the tick range of 2000, at which point it will exclude the liquidity of positions 2 and 3, while still being within the range of position 1. In other words, liquidity is updated whenever the price crosses a liquidity boundary, either increasing or decreasing.
Unlike Uniswap v3, KyberSwap Elastic introduces an innovative feature called the Reinvestment Curve. This is an additional AMM pool that accumulates fees charged from users’ swaps in the pool, with a curve that supports a price range from 0 to infinity. KyberSwap Elastic combines the reinvestment curve with the original price curve (i.e., the curves are separate, but the funds remain in the same pool), allowing LPs to compound their fees and earn returns even when the price exceeds their position range.
With a basic understanding of the KyberSwap Elastic mechanism, let’s analyze the steps of the attack.
Steps of the Attack
Here, we analyze the attack using transaction 0x485…0f3 as an example:
1. The attacker first borrows 2000 WETH via a flash loan from AAVE and uses 6.8496 WETH to swap for frxETH in the KyberSwap pool, causing the frxETH price to exceed the range of all liquidity providers’ positions. As a result, the current price value sqrtP (the square root of the current price multiplied by 2⁹⁶) is elevated to 20282409603651670423947251286016, located at tick 110909.
2. Next, the attacker adds liquidity of 0.006948 frxETH and 0.1078 WETH within the specified price range [110909,111310]. Following this, the attacker partially removes this liquidity, ultimately controlling the liquidity amount within this price range to be 74692747583654757908. This manipulation ensures that the liquidity amount aligns with the needs of the subsequent stages of the attack. At this point, the attacker is the sole provider of liquidity in the tick range [110909,111310], and the price value sqrtP at tick 111310 is 20693058119558072255662180724088.
3. Afterwards, the attacker uses 387.17 WETH to swap for 0.005789 frxETH at the current price tick 110909. This significant swap increases the current price value sqrtP to 20693058119558072255665971001964, surpassing the sqrtP at the boundary tick 111310.
4. Finally, the attacker uses 0.005868 frxETH to reverse swap for 396.2 WETH at a price slightly higher than the sqrtP of tick 111310. After the swap, the price falls back within the [110909,111310] tick range. At this point, the attacker has made a profit, as the reverse swap yields approximately 9 more WETH than what was exchanged in the forward swap.
Why did such seemingly simple attack steps result in more funds than expected? This is closely related to KyberSwap Elastic’s Reinvestment Curve. Let’s delve into a detailed analysis to uncover the method behind the attacker’s profit.
Analysis of the Attack Principle
From the steps described above, we know that the final reverse swap resulted in more funds than anticipated. At the time of this swap, the current sqrtP was 20693058119558072255665971001964, which is higher than the price at tickUpper 111310, where the attacker had added liquidity. Let’s use a scale diagram to illustrate the position of these values.
Because the current sqrtP exceeds the liquidity tick range [110909,111310] added by the attacker, theoretically, there should be no liquidity at its position. During the swapping process, the price needs to move left, crossing the 111310 tick, to find effective liquidity for the exchange. Let’s investigate whether the exchange proceeded as expected.
As illustrated in the following diagram, when examining the liquidity at the current sqrtP tick, we find an unexpected amount of liquidity in a range that should have been devoid of liquidity if the reinvestment curve is not considered. Moreover, the amount of liquidity present is significantly larger than what the reinvestment curve would suggest, and it matches the liquidity in the [110909,111310] range.
This means that the swap will be effectively executed at tick 111310, as shown in the following diagram.
After the effective exchange at tick 111310, sqrtP will cross this tick and enter the [110909,111310] range to exchange the remaining tokens. Recalling our preliminary knowledge, we know that crossing the liquidity position range triggers a liquidity update. In the KyberSwap Elastic Pool, this is done through the _updateLiquidityAndCrossTick function, which adds the liquidity within the [110909,111310] range to the curve to participate in the token exchange, as illustrated below.
This results in the effective liquidity within the [110909,111310] range being added to the unexpected additional liquidity to the right of tick 111310, leading to a total effective liquidity during the exchange in this range that is significantly higher than expected. The following diagram illustrates this, showing that the effective liquidity has doubled compared to the expected amount.
Due to the increase in liquidity within the current tick range, the pool depth is better than expected, allowing the attacker to obtain more funds than anticipated. These extra funds are sourced from the liquidity of other tick ranges in the pool.
The question arises as to why there is unexpected additional liquidity to the right of tick 111310, and why it matches the liquidity amount in the [110909,111310] range. The only explanation is that the pool did not update the liquidity as expected during the previous exchange process. The diagram below theoretically shows that the _updateLiquidityAndCrossTick function should have been called during the previous exchange when crossing tick 111310, updating the liquidity after sqrtP entered the right side of tick 111310.
In our actual analysis of this exchange process, the Pool calculates the actual amount used for the exchange, the exchange fees, and the new sqrtP price value using the computeSwapStep function. Theoretically, when crossing the liquidity range, the calculated sqrtP result should fall on the boundary tick 111310’s sqrtP. However, the actual new sqrtP exceeded the sqrtP of tick 111310. The diagram below shows that the sqrtP of tick 111310 is 20693058119558072255662180724088, but the actual sqrtP turned out to be 20693058119558072255665971001964.
The fact that the new sqrtP did not land on the boundary tick 111310’s sqrtP, meaning swapData.sqrtP != swapData.nextSqrtP, led the pool to believe that the current sqrtP was still within the [110909,111310] range. This resulted in the pool skipping the liquidity check operation and not triggering the _updateLiquidityAndCrossTick function for liquidity update!
But why did the new nextSqrtP not land on the boundary? Analysis of the calcReachAmount computation reveals that the swap amount of 387170294533119999999 was just slightly less than the liquidity amount within the current range of 387170294533120000000.
This resulted in nextSqrtP not being assigned the value of targetSqrtP but remaining at 0. Therefore, the final sqrtP calculation was done directly using the calcFinalPrice function, resulting in a calculation that exceeded the sqrtP at tick 111310.
Thus, the calcReachAmount function is crucial as it calculates the amount of tokens needed to reach from currentSqrtP to targetSqrtP in a swap. By analyzing its formula, we can see that the result primarily depends on the current liquidity L, which is the sum of the base liquidity and the reinvestment liquidity.
Given that Uniswap v3 does not have the feature of a reinvestment curve, could the inclusion of reinvestment liquidity be the reason why the calcReachAmount calculation resulted in a larger amount than expected?
Upon testing, it was found that without including reinvestment liquidity, the calcReachAmount calculation result was 387160697969657129472, which is less than the swap quantity of 387170294533119999999 used in the attack.
Moreover, without the reinvestment liquidity, the sqrtP calculated by the computeSwapStep function falls exactly on tick 111310:
Therefore, the truth becomes clear. Due to the Reinvestment Curve feature of KyberSwap Elastic, when calculating the amount of tokens needed for a swap from the current sqrtP to the boundary sqrtP using both base and reinvestment liquidity, the required amount of tokens is greater than expected. This causes the post-swap sqrtP to exceed the boundary sqrtP, leading the protocol to believe that the liquidity within the current tick range has met the swap requirements, thereby halting the liquidity update process for ticks crossed by the boundary.
MistTrack Analysis
KyberSwap Exploiter Addresses
1. KyberSwap Exploiter 1: 0x50275e0b7261559ce1644014d4b78d4aa63be836
2. KyberSwap Exploiter 2: 0xc9b826bad20872eb29f9b1d8af4befe8460b50c6
3. KyberSwap Exploiter 3: 0xae7e16cAa7a4d572FfF09924Bf077a89485850Cb
4. KyberSwap Exploiter 4: 0xd01896e3D4F130Ffd6f6a5A9d6780bbd7008d71d
Based on our analysis of our AML platform, MistTrack, the KyberSwap attackers collectively stole over $54.7 million, with activities spanning across multiple blockchain networks including Ethereum, BSC (Binance Smart Chain), Arbitrum, Optimism, Polygon, BASE, Scroll, and Avalanche.
On the Ethereum network, KyberSwap Exploiter 1’s initial funding was sourced from 20 ETH transferred from Tornado Cash. Of this, 0.1 ETH was transferred to KyberSwap Exploiter 2, and 2 ETH was moved to FixedFloat. Additionally, 6.5 ETH was split and transferred across various chains including Arbitrum, Optimism, Scroll, and BASE. Meanwhile, KyberSwap Exploiter 2 managed to steal over $7.58 million in tokens, including USDC, WETH, KNC, etc., which have not yet been moved.
On the Binance Smart Chain (BSC), KyberSwap Exploiter 1 received 4.2678 BNB from FixedFloat as a balance, which remains untouched.
On Arbitrum, the KyberSwap Exploiter 2 stole over $20.29 million in tokens, including WBTC, WETH, ARB, DAI, etc. Of these, 500 WETH was transferred to 0x98d69d3ea5f7e03098400a5bedfbe49f2b0b88d3, which it then moved 300 WETH crosschain to Ethereum and remains untouched. Notably, KyberSwap Exploiter 2 sent 1,000 WETH to the Indexed Finance Exploiter’s address, 0x84e66f86c28502c0fc8613e1d9cbbed806f7adb4.
On Optimism, KyberSwap Exploiter 2 stole over $15.64 million in tokens, including wstETH, WETH, OP, DAI, etc., which also has not been touched.
On Polygon, KyberSwap Exploiter 1’s initial funds came from 2,666.1243 MATIC transferred from FixedFloat. 100 MATIC was then sent to KyberSwap Exploiter 2, leaving a balance of 2,564.0016 MATIC for Exploiter 1. KyberSwap Exploiter 2 stole over $2.93 million in tokens, including WBTC, WETH, DAI, etc., and remain untouched as well. KyberSwap Exploiter 3 stole over $5.75 million in tokens, including wstETH, USDT, USDC, etc., and transferred most of these tokens to address 0xa4c92d7482066878bb1e2c0510f42b20d79a7ea9.
On BASE, KyberSwap Exploiter 2 stole over $1.95 million in tokens, including USDC, WETH, etc., and remains untouched.
On Avalanche, KyberSwap Exploiter 1’s initial funds came from 49 AVAX transferred from FixedFloat. KyberSwap Exploiter 2 stole over $23,500 in tokens, including 293.0756 WAVAX, 17,316.0305 USDC, which remains untouched. KyberSwap Exploiter 4 gained over $565,000 in tokens, including WAVAX, USDC, etc., and transferred the USDC to address 0x9296fa3246f478e32b05d4dde35176d927be703f.
The SlowMist Security Team has blacklisted the involved addresses, and most of the funds have not yet been moved. We will continue to monitor the fund movements and provide updates on this case.
Conclusion
The root cause of this attack lies in the calculation of the amount of tokens needed for an exchange from the current price to the boundary tick price. Due to KyberSwap Elastic’s Reinvestment Curve, the liquidity was inadvertently increased by the compounded fees, resulting in a calculated amount larger than expected. This excess covered the user’s exchange needs, but the actual price had already crossed the boundary tick. Consequently, the protocol mistakenly believed that the liquidity within the current tick range was sufficient for the exchange and therefore did not update the liquidity. This oversight led to a duplication of liquidity increase during the reverse exchange across the boundary tick, allowing the attacker to obtain more tokens than anticipated.
The SlowMist Security Team recommends that when designing economic models, boundary conditions should be thoroughly tested, and liquidity and price calculations should be strictly evaluated, rather than relying on inequality checks.
References
- Attacker’s Address: 0x50275e0b7261559ce1644014d4b78d4aa63be836
- Attack Contract: 0xaf2acf3d4ab78e4c702256d214a3189a874cdc13
Related Attack Transactions:
- 0x485e08dc2b6a4b3aeadcb89c3d18a37666dc7d9424961a2091d6b3696792f0f3
- 0x09a3a12d58b0bb80e33e3fb8e282728551dc430c65d1e520fe0009ec519d75e8
- 0x396a83df7361519416a6dc960d394e689dd0f158095cbc6a6c387640716f5475
About SlowMist
At SlowMist, we pride ourselves on being a frontrunner in blockchain security, dedicating years to mastering threat intelligence. Our expertise is grounded in providing comprehensive security audits and advanced anti-money laundering tracking to a diverse clientele. We’ve established a robust network for threat intelligence collaboration, positioning ourselves as a key player in the global blockchain security landscape. We offer tailor-made security solutions that span from identifying threats to implementing effective defense mechanisms. This holistic approach has garnered the trust of numerous leading and recognized projects worldwide, including names like Huobi, OKX, Binance, imToken, Crypto.com, Amber Group, Klaytn, EOS, 1inch, PancakeSwap, TUSD, Alpaca Finance, MultiChain, and Cheers UP. Our mission is to ensure the blockchain ecosystem is not only innovative but also secure and reliable.
SlowMist offers a variety of services that include but are not limited to security audits, threat information, defense deployment, security consultants, and other security-related services. They offer AML (Anti-money laundering) software, Vulpush (Vulnerability monitoring) , SlowMist Hacked (Crypto hack archives), FireWall.x (Smart contract firewall) , Safe Staking and other SaaS products. They have partnerships with domestic and international firms such as Akamai, BitDefender, FireEye, RC², TianJi Partners, IPIP, etc.
By delivering a comprehensive security solution customized to individual projects, they can identify risks and prevent them from occurring. Their team was able to find and publish several high-risk blockchain security flaws. By doing so, they could spread awareness and raise the security standards in the blockchain ecosystem.