๐โโ๏ธVulnerabilities in Initial Pool Creation - Liquidity Manipulation Attacks
Deciphering Initial Liquidity Vulnerabilities and Uniswap's Countermeasure
Introduction
The Decentralized Exchange (DEX) landscape relies heavily on liquidity pools, and the initialization phase of these pools is critically vulnerable to exploitation. Uniswap V2 deploys a mechanism to mitigate this risk, one that centers around the burning of initial liquidity pool shares. This article seeks to elaborate and clarify this strategy in depth.
How Liquidity Pools Generally Work
Initial Liquidity: When a liquidity pool is first created, it has zero assets and zero pool shares (liquidity tokens).
First Deposit: The first liquidity provider sets the initial value of these pool shares by depositing assets into the pool. In return, they get an equivalent number of pool shares (often calculated as the geometric mean of the deposited amounts).
Subsequent Deposits: Future liquidity providers deposit assets and receive pool shares based on the current value set by the pool.
Problem Statement: Initializing Liquidity Token Supply
When someone wants to become a liquidity provider (LP) by depositing tokens into an existing Uniswap pair (or forked DEX), the formula to calculate how many liquidity tokens (also known as pool shares) they receive involves dividing the amount deposited by the starting amount of tokens already in the pool.
However, this formula doesn't work if the person is the first to deposit because the starting amount of tokens would be zero, causing a division by zero problem.
2. Uniswap V1's Approach
In Uniswap V1, the initial supply of liquidity tokens was set to be equal to the amount of ETH deposited. While this was an acceptable approximation, it had limitations:
The value of one liquidity token would depend on how liquidity was initially added, which can be arbitrary.
Uniswap V2 allows for arbitrary pairs, not just those involving ETH, making the V1 approach less applicable.
3. Uniswap V2's Solution: The Geometric Mean
Advantages:
It ensures the value of one liquidity pool share doesn't depend on how liquidity was initially added.
It minimizes rounding errors.
4. Risk of Share Value Growth
Over time, the value of a liquidity pool share could grow either by accumulating trading fees or through direct donations. If it grows too much, small liquidity providers could find it too costly to join the pool.
5. Mitigation: Burning Initial Shares
How an Attacker Can Raise the Value of Liquidity Pool Shares: A Detailed Explanation with Visualization
The Setup:
Let's say an attacker aims to manipulate a liquidity pool for a token pair ABC/XYZ to make it infeasible for smaller investors to participate. The attacker's strategy is to make 1 liquidity pool share extremely valuable by contributing a large amount of one token (or both) and a very small amount of the other.
The Attack:
Small Investor Deterrence: A small investor who can only afford to deposit 1 XYZ will receive far less than 1 pool share, making it practically impossible for them to participate.
Visualization:
Imagine each liquidity pool share as a "block" that represents a proportional slice of the total pool assets. In a well-balanced pool, these blocks are relatively small and easily divisible:
In our attack scenario, the attacker has made each block (pool share) represent a large amount of the pool's assets:
The size of each block (or the value of each liquidity pool share) is now so large that a small investor can only afford a tiny sliver, represented by a small fraction of a block:
Counter-Measure: Uniswap V2's Initial Share Burning
Summary:
Uniswap V2's mechanism of burning initial liquidity pool shares serves as a protective barrier against this type of manipulation. It keeps the door open for small investors while putting up a prohibitively expensive wall for malicious actors aiming to distort the pool's economics.
Example vulnerabilities of DEXs that didnt migitage this issue
Impact
A user who joins the systems first (stakes first) can steal everybody's tokens by sending tokens to the system externally. This attack is possible because you enable staking a small amount of tokens.
Proof of Concept
See the following attack:
the first user (user A) who enters the system stake 1 token
another user (user B) is about to stake X tokens
user A frontrun and transfer X tokens to the system via
ERC20.transfer
user B stakes X tokens, and the shares he receives is:
shares = (_amount * totalStakeShares_) / (totalTokenBalanceStakers() - _amount);
shares = (X * 1) / (X + 1 + X - X) = X/(X+1) = 0
meaning all the tokens he staked got him no shares, and those tokens are now a part of the single share that user A holdsuser A can now redeem his shares and get the 1 token he staked, the X tokens user B staked, and the X tokens he
ERC20.transfer
to the system because all the money in the system is in a single share that user A holds.
In general, since there is only a single share, for any user who is going to stake X tokens, if the system has X+1 tokens in its balance, the user won't get any shares and all the money will go to the attacker.
Recommended Mitigation Steps
Force users to stake at least some amount in the system (Uniswap forces users to pay at least 1e18
)
That way the amount the attacker will need to ERC20.transfer to the system will be at least X*1e18
instead of X
which is unrealistic
Similar to issue above
Code4rena High bug bounty payout
In the current implementation, the first liquidity
takes the nativeDeposit
amount and uses it directly.
However, since this number (totalLiquidityUnits
) will later be used for computing the liquidity
issued for future addLiquidity
using calculateLiquidityUnits
.
A malicious user can add liquidity with only 1 wei
USDV and making it nearly impossible for future users to add liquidity to the pool.
Recommendation
Uni v2 solved this problem by sending the first 1000 tokens to the zero address.
The same should work here, i.e., on first mint (totalLiquidityUnits == 0), lock some of the first minter's tokens by minting ~1% of the initial amount to the zero address instead of to the first minter.
First lp provider received liquidity amount same as the nativeDeposit amount and decides the rate. If the first lp sets the pool's rate to an extreme value no one can deposit to the pool afterward. (please refer to the proof of concept section)
A malicious attacker can DOS the system by back-running the setTokenSupport
and setting the pools' price to the extreme.
Pool
is created in function createPoolADD
. The price (rate) of the token is determined in this function. Since the address is deterministic, the attacker can front-run the createPoolADD
transaction and sends tokens to Pool's address. This would make the pool start with an extreme price and create a huge arbitrage space.
I assume pools would be created by the deployer rather than DAO at the early stage of the protocol.
If the deployer calls createPoolADD
and addCuratedPool
at the same time then an attacker/arbitrager could actually get (huge) profit by doing this.
Assume that the deployer wants to create a BNB pool at an initial rate of 10000:300 and then make it a curated pool. An arbitrager can send 2700 BNB to the (precomputed) pool address and make iBNB 10x cheaper. The arbitrager can mint the synth at a 10x cheaper price before the price becomes normal.
However, if a user first directly transfers Joe
tokens to the contract before the first updatePool
call, the block.timestamp - lastRewardTimestamp = block.timestamp
will be a large timestamp value and lots of rJoe
will be minted (but not distributed to users).
Even though they are not distributed to the users, inflating the rJoe
total supply might not be desired.
Last updated