Summary
A critical vulnerability was identified and reported by whitehat @riproprip in the Raydium protocol on January 10, 2024.
This vulnerability, found in the increase_liquidity.rs function allowed an attacker to exploit the liquidity management functionality of the automated market maker (AMM) to drain funds from liquidity pools.
A bounty of $505,000 in RAY tokens was awarded to the whitehat for this discovery. Raydium, an AMM built on the Solana blockchain, quickly addressed and resolved the issue.
What is Raydium?
Raydium is an AMM with an integrated central order book system, currently in its V3 iteration. Users can provide liquidity, perform swaps on the exchange, and stake the RAY token for additional yield.
A fundamental aspect of Raydium is the Concentrated Liquidity Market Maker (CLMM).
Concentrated Liquidity Market Maker (CLMM)
Concentrated Liquidity Market Maker (CLMM) pools offer a more efficient way for liquidity providers to manage their capital.
Unlike traditional Automated Market Maker (AMM) pools, where liquidity is distributed along a broad price curve from 0 to ∞, CLMM pools allow providers to concentrate their liquidity within a specific price range.
Traditional AMMs work by putting a pair of tokens together to automatically set a price for trades. This method requires a large amount of tokens on both sides to maintain a stable price, which can lead to issues like high slippage and volatile prices due to limited liquidity.
CLMMs enhance liquidity efficiency by setting a specific price range for a pool to be in an “active” state. This allows a much smaller liquidity pool to provide the same low slippage trades as a much larger AMM pool. By concentrating liquidity within a targeted price range, CLMMs minimize the amount of capital required while maintaining stable prices and reducing slippage.
How Does CLMM Work?
In a CLMM setup, liquidity providers can choose a price range where their liquidity is active. This means they can focus their resources where they believe the most trading activity will occur, rather than spreading it thin across all possible prices.
To see CLMM in action, let’s look at Uniswap V3 as an example. In this system, liquidity providers can define a price range for their contributions. For instance, if an LP believes that the price of ETH will stay between $2,000 and $3,000, they can choose to concentrate their liquidity within this range.
- When traders operate within this range, the LP’s capital is used more intensively, earning them higher fees.
- If the price moves outside of this range, their liquidity is no longer active, prompting them to adjust their position as needed.
This is both a more efficient use of capital for liquidity providers and leads to potentially better prices and deeper liquidity for the market to execute large trades with less impact on the price.
Vulnerability Analysis
The vulnerability was located within the increase_liquidity
function of the Raydium protocol. This function is essential for adding liquidity to a position within a liquidity pool.
It conducts several critical operations, including pool status validation, token amount calculations based on user-provided maximums, fee updates, and the actual increase of liquidity in the position’s state.
What are Ticks?
Ticks represent discrete price points in a CLMM. Each tick corresponds to a specific price level, and the space between ticks represents the smallest possible price movement in the pool. By defining a range with tick_lower and tick_upper, an LP specifies the boundaries within which their liquidity will be active.
- tick_lower: This is the lower boundary of the price range where the LP’s liquidity will be active. It represents the lowest price point at which the LP is willing to provide liquidity.
- tick_upper: This is the upper boundary of the price range where the LP’s liquidity will be active. It represents the highest price point at which the LP is willing to provide liquidity.
The flaw was specifically in the conditional handling of the tickarray_bitmap_extension
:
The tickarray_bitmap_extension
plays a crucial role in managing the pool’s pricing at extreme boundaries (very high or low prices). It also acts as an extended index to manage a larger range of price ticks which helps track which ticks have been initialized (i.e., have non-zero liquidity) beyond the default capacity of the system.
The conditional logic decides whether an account from remaining_accounts
is needed as part of the operation. This account is presumably related to handling the tick array bitmap extension. This function is designed to increase liquidity in a specific position within a liquidity pool using the remaining_accounts
vector.
Here are a few details you should know about the remaining_accounts
function:
remaining_accounts
is a vector that contains all accounts that were passed into the instruction but are not declared in the Accounts struct. This is useful when you want your function to handle a variable amount of accounts, e.g. when initializing a game with a variable number of players.- While
remaining_accounts
provides significant flexibility, it also necessitates careful validation within the smart contract to ensure security. The contract must validate the accounts inremaining_accounts
to confirm they are of the expected type and have the correct permissions for the intended operations. - Please refer to the following documentation for more information on
remaining_accounts
: https://www.anchor-lang.com/docs/the-program-module#context
The vulnerability arises because the function fails to verify whether remaining_accounts[0]
is the accurate TickArrayBitmapExtension
account linked to the current state of the pool. This oversight permits an attacker to execute liquidity operations at arbitrary price boundaries as described in the vulnerability demonstration section below.
Normal Behavior
In normal operation, when a user adds liquidity:
- The
increase_liquidity
function validates the current state of the pool and the price range specified bytick_lower
andtick_upper
. - It calculates the token amounts needed based on the user’s input and current pool state.
- It updates the fee structure and increases the liquidity in the specified position.
- If the price range specified requires the
tickarray_bitmap_extension
(because it is at an extreme boundary), the function uses an account fromremaining_accounts
to manage this extension properly.
Here is the code snippet for the increase_liquidity
function:
Attack Behavior
Due to the bug, the function fails to verify whether remaining_accounts[0]
is the accurate TickArrayBitmapExtension
account linked to the current state of the pool.
This oversight permits an attacker to execute liquidity operations at arbitrary price boundaries.
Vulnerability Demonstration
The whitehat showed how to exploit the system by manipulating the tickarray_bitmap_extension
. This allowed them to bypass the intended removal of liquidity at specific price points, known as ticks.
Steps of the Attack:
- Create a Secondary Pool: The attacker sets up a secondary pool.
- Target a Tick: They open a position at a specific tick they want to exploit in the primary pool.
- Zero Out and Manipulate Liquidity: They reduce the liquidity at this tick to zero and then increase it in a way that incorrectly uses the
tickarray_bitmap
of the primary pool.
This manipulation lets the attacker “flip” a tick’s status in the bitmap.Transactions
that should have adjusted liquidity at certain prices bypass these checks, allowing unauthorized liquidity increases.
To clarify the exploit process using a practical example, consider price points A, B, and C, with A < B < C and both A and B within the lower range of the bitmap_array
.
The steps to exploit the vulnerability are as follows:
1. Identify a victim pool.
2. Execute a swap to shift the price to just above point A (A+1).
3. Create liquidity within the price range B to C.
4. Perform a swap across B without crossing C, which adds liquidity to the pool state due to the manipulated tickarray_bitmap
.
5. Execute the described attack to switch the tickarray_bitmap
status at B to DISABLED, allowing a swap at A+1 to skip over B without affecting liquidity.
6. Repeat the attack to re-enable the tickarray_bitmap
at B.
7. Perform another swap over B without crossing C, effectively doubling the liquidity erroneously.
8. This process can be repeated as many times as desired to amass an excessive amount of liquidity.
9. Following this procedure, swaps directed towards C will yield disproportionately high amounts of Token A, indicating the vulnerability has been fully exploited.
10. If the goal is to acquire Token B, the process can be replicated at the higher end of the price spectrum.
The core issue is the ability to “flip” a tick’s status within the tickarray_bitmap
, allowing liquidity to be added without proper checks. This leads to significant discrepancies in liquidity management and compromises the integrity of the liquidity pool.
Vulnerability Fix
The vulnerability was addressed by Raydium through a critical update, as documented in their GitHub commit:
The correction involved adding a security check to ensure the correct application of the tickarray_bitmap_extension
:
This fix introduces a validation step to confirm that the remaining_accounts[0]
is the correct TickArrayBitmapExtension
account associated with the pool’s current state.
This ensures the proper handling of liquidity operations involving extreme price boundaries, thereby preventing the exploitation of this vulnerability in the future.
Acknowledgements
We would like to thank the whitehat @riproprip for doing an amazing job in responsibly disclosing such an important bug. Big props also to the Raydium team who quickly responded to the report and patched the issue.
This bugfix review was written by Immunefi triager, Arbaz Hussain.
If you’re a developer or a whitehat considering a lucrative bug-hunting career in web3 — this message is for you. With 10–100x the rewards commonly found in web2, your efforts will pay off exponentially by switching to web3.
Check out the Web3 Security Library, and start earning rewards on Immunefi — the leading bug bounty platform for web3 with the world’s biggest payouts.