On the 23rd of November 2023, KyberSwap was attacked on several chains. The attack was made tick manipulation flaw in the contracts. Over $45M dollars were stolen.
KyberSwap is a decentralized market maker. For more information, check out their website.
Vulnerability Analysis & Impact:
Attacker Address: 0xc9b826bad20872eb29f9b1d8af4befe8460b50c6
Victim Contract: 0x1694d7fabf3b28f11d65deeb9f60810daa26909a
The Root Cause:
- The root cause of this attack was a precise manipulation of liquidity math in the implementation of KyberSwap which tricked the pool into believing that it has more liquidity than it actually has.
- To understand the attack, let’s have a closer look on Concentrated Liquidity Market Makers (CLMMs) and ticks.
- In a standard Automated Market Maker (AMM), any liquidity added to a pool is available for traders irrespective of the price.
- However, in a Concentrated Liquidity Market Maker (CLMM), liquidity providers (LPs) contribute funds within specific price bands. These LP-provided tokens are only utilized when the current price falls within the defined narrow range.
- This approach offers two key benefits: increased liquidity utilization for enhanced capital efficiency, resulting in reduced price impact for traders, and LPs gain the ability to control the extent of impermanent loss they are willing to accept.
- Similar to how floating-point numbers discretely divide the infinite real number line, in a CLMM, the price range is discretely segmented into ticks.
- Each tick is represented by a numerical value, and for a given tick denoted as “t,” the corresponding price is calculated as 1.0001^t. Liquidity providers (LPs) can offer liquidity between two ticks, provided that these ticks are evenly spaced multiples of the tick size.
- For instance, with a tick spacing of 10, an LP could offer liquidity in the price range of $1.00 to $1.22 by providing liquidity within the tick range (0, 2000) since 1.0001^0 equals 1.00 and 1.0001^2000 is approximately 1.22.
- In a pool with two tokens, Token Zero and Token One, represented by liquidity depth in various tick ranges, LPs contribute liquidity in specific ranges (green, purple, and red). The depth is greatest where these ranges overlap.
- When LPs add liquidity (mint), the overall pool liquidity changes only if the tick range intersects the current price. Swaps by traders can cause the price to shift, crossing tick range boundaries. These swaps are then divided into sub-swaps, impacting the pool’s current liquidity based on whether the tick range boundary is leading or trailing. The impact of each sub-swap, per unit swapped, is influenced by the liquidity within the tick range.
- For example, if the price is at tick 250250 and 1000 units of Token One are swapped for Token Zero, the existing liquidity may not be sufficient. Swapping a portion causes the price to cross a tick range boundary, adjusting the pool’s liquidity. With enhanced liquidity, the remaining units can be swapped with a reduced price impact.
- When the boundaries of ticks are surpassed in Kyber, the updateLiquidityAndCrossTick function is called. This function is responsible for modifying the liquidity value of the curve by considering the LP range positions associated with that particular tick.
- But this was never called in the attack.
- So somehow, Preventing the invocation of the process when the LP position moves beyond a specified range would ensure that liquidity is consistently retained within the curve. In this scenario, the pool is deceived into believing it possesses greater liquidity than it actually holds.
- However, upon returning to the specified range, you ensure that it triggers again. Consequently, liquidity is reintroduced, even though it was never withdrawn initially. This results in a double-counting scenario, where the pool reflects the liquidity from the original LP position twice.
- To bypass this on the first step, we need to understand the mechanics of KyberSwap
- In concentrated liquid automated market makers (AMMs), swaps are computed through a sequence of steps. In each step, it is necessary to ascertain whether the swap will encounter a tick boundary or if the swap will be deleted.
- Kyber executes a swap step and verifies whether the concluding price of the step aligns with the subsequent tick price. If there is no match, it signifies that the swap has not reached the tick boundary, and as a result, the updateLiq function is not required to be invoked.
- Be aware that the check involves inequality, not a comparison of direction. If you manage to perform a swap step that results in the price ending up beyond the tick boundary, the check will fail, and the updateLiquidity function will not be triggered, even if you have crossed a tick boundary.
- Typically, this scenario should not occur due to the initial step in the computeSwapStep function. This step involves computing an upper limit for the amount that can be swapped before reaching the tick. If this calculated limit is less than the remaining amount of the swap, the function confidently predicts that the final price will not reach the tick.
- In this amusing scenario, calcReachAmount made a prediction that the swap quantity wouldn’t quite hit the tick boundary. However, a humorous twist occurred when the final price ended up just a smidge beyond the tick boundary. The culprit? The “reach quantity” upper limit for hitting the tick boundary was calculated as 22080000, while the sly exploiter sneakily set a swap quantity of 220799999.
- Kyber’s implementation involves slightly different arithmetic for quantity calculation (for the upper limit until a bounds is reached) and price change. In a meticulously controlled and precisely engineered scenario, the bounds check may indicate that any swap quantity below X will remain within the tick price. However, the parallel price change calculation, when applying X swap quantity, may result in going beyond the tick bound.
- The attack was started by a flash loan of 10k wstETH ( ~ $23 M )
- Then the attacker swapped 2800 wstETH (~ $6M ) into the pool. To move the price where existing liquidity is zero.
- The result was that the price of wstETH/ETH moved from 1.05 ETH to 0.0000152 which is just barely below the liquidity’s price range.
- All of this was done to move the price into the area into the concentrated part of the liquidity curve to the point of zero liquidity.
- Then, minted 3.4wstETH as liquidity was done at the price range was 0.0000146 to 0.0000153.
- Now, 2 swaps were executed around this price range.
- The first one was exploiter selling 1056 wstETH for 0.0157 ETH and pushing the price to 0.0000146
- The second and the opposite swap was buying back 3911 wstETH from the price to 0.06ETH and this pushed up the price to 0.00001637.
- Notice how the attacker received more money after the second swap ( 1056 vs 3911 ). This way, the attacker was able to take out more liquidity than he started with.
- This attack was repeatedly executed on several chains –
$7.5M on Mainnet
$315K on Base
$15M on Optimism
$2M on Polygon
$20M on Arbitrum
Flow of Funds:
Here is the fund flow during and after the exploit. You can see more details here.
Here is a snippet of attacker’s wallet
After the Exploit
- The Project acknowledged the hack via their Twitter.
(Nov-22-2023 10:58:23 PM +UTC) – The attacker started the attack after creating a malicious contract.
How could they have prevented the Exploit?
A proper validation is required of the code to check for edge cases like this.
Web3 security- Need of the hour
Why QuillAudits For Web3 Security? QuillAudits is well-equipped with tools and expertise to provide cybersecurity solutions, saving millions in funds.
Want more Such Security Blogs & Reports?
Connect with QuillAudits on :
Partner with QuillAudits :