Impact of manipulations of the Uniswap pool price
In this section, we discuss the impact of manipulations of the Uniswap pool price by a possible attacker within a single transaction, that is without manipulating the time-weighted average price. Let us denote the two tokens by and . We assume that the time-weighted average price is (this is in units of ), and in the positive direction, the onlyCalmPeriods
modifier checks that the current price at the time of invocation satisfies for some constant . We will sketch an attack scenario in which the attacker attempts to use price manipulations in order to steal value from the vault/strategy contracts, and we will attempt to estimate (making some simplifications along the way) under what conditions, in particular what value of , the attacker can make a profit. We assume the price before the attacker begins is and that the value of the strategy contract's position is (in units of ).
The attacker takes the following actions with funds of tokens and potentially obtained from a flash loan, and paid back at the end. We will expand on the precise amounts later.
They buy a large amount of in the Uniswap pool with their , moving the price to .
They deposit some tokens into the vault.
They sell a large amount of in the Uniswap pool, moving the price back to .
They withdraw their share of the vault.
Step 1: Manipulating the price
To simplify our calculations, we assume that the liquidity provided by the strategy to the Uniswap pool is centered around the tick corresponding to the price , with a very small width, so that we can ignore the price change while trading within that position for the purposes of our estimate. This position, valued in total, is composed of and so that half of the value is in each of the two tokens, so it consists of tokens and tokens .
The attackers buying converts the strategy's position entirely to token , so the attacker will pay in for this and receive in . Beyond that, let us assume that the rest of the liquidity in the Uniswap pool amounts to having to pay in and obtain in to move the price to .
The above was without fees. Let us assume the fee rate for the Uniswap pool is , for example (i.e., 0.05%). Then after Step 1, the attacker has paid of token and received of token .
Step 2: Depositing tokens
The attacker now deposits tokens . Note that due to this step, we must restrict our price manipulation in such a way as to pass the onlyCalmPeriods
check.
The first thing that will happen is that the strategy removes its liquidity. Ignoring the minor amount of fees, this results in the entire value being returned in the form of token . The price of the tokens contributed by the attacker is . If the total amount of shares before was , then the attacker thus receives .
The strategy contract will then provide liquidity to the Uniswap pool around the current price, providing of token and of token .
Step 3: Returning the price to normal
To trade back the price in the Uniswap pool to where it was before, the attacker first needs to trade through the strategy contract's position, which means converting the tokens to , at a price of (we again ignore the price changes due to the width of the position). As the strategy contract's position consisted of of token , the attacker must pay of token for this without fees, and they receive of token in return.
After that, the attacker has to pay in and receive in , reverting the trade regarding the remaining liquidity in the pool between the prices.
Taking into account Uniswap fees, in total the attacker has to pay in and receives in token .
Step 4: Withdrawing the shares
When withdrawing the shares, the strategy will first withdraw its liquidity, which amounts to of token . We can rewrite this as follows.
The attacker holds of a total of shares, and so they receive the following amount of tokens . They thus receive the following amount of tokens .
End result for the vault
At the end, the vault is left with only tokens . Let us calculate their worth in terms of token in order to compare with the original worth of .
According to the above formula, the vault/strategy contracts lose value, with loss increasing the bigger is. For very small , this loss might be offset by the fees collected during the attack. For the first step, the fees collected will be about in token . The fees collected when trading back will be about in token . The total worth of the fees is thus, in ,
We can easily check that these results are plausible: the strategy traded its entire value from one token to the other at the unfavorable price of instead of , so is as one would expect. Similarly, fees collected come from trading half of the liquidity at the start, and then the entire liquidity, but at the unfavorable price, so is as expected as well.
Conditions under which the vault makes a loss
For what will the fees we just estimated offset the loss? For the vault not losing value on this attack, we obtain the following, assuming :
Let us calculate this for a concrete value for . Uniswap V3 pools can have different fees. A lower fee will be worse for the vault/strategy here, so to be conservative, let us consider a low fee of 0.01%, for which there for example exists a DAI/USDC pool with significant activity. Using this script,
gamma = 0.0001
num = 1 + (gamma / (1-gamma))
denom = 1 - (gamma / 2*(1 - gamma))
result = num/denom - 1
print(result)
we obtain that we must have roughly to not make a loss, amounting to a relative price change from the time-weighted average price of at most 0.015%. It thus might not always be feasible to prevent this kind of attack by choosing in such a way that the vault/strategy will never make a loss; prevention must hinge on the attacker being unable to make a profit instead (due to having to trade through the rest of the liquidity providers' liquidity as well).
End result for the attacker
Let us calculate the net amount of token and the attacker gained. We obtain the following.
Let us convert the total value to token . To simplify, we assume that the liquidity in the Uniswap pool was concentrated around the price , so that we can estimate . Then we obtain
We can check plausibility of this formula: the part comes from the attacker trading with the strategy's positions. If we set , this would be , which corresponds to buying the strategy's entire balance while only needing to pay of the fair price for it, which is exactly what the attack is doing, so this is exactly as we would expect. The occurence of lowers the value due to losses due to fees. The part is then from the fees required to trade through the other liquidity provider's liquidity twice.
Conditions under which the attack is profitable
Given a certain ratio between and , say , what values of allow the attacker to make a profit? We obtain
There are two cases here. If the left-hand side is smaller than or equal to zero, then the attacker will not make a profit no matter what is. Otherwise we get
Examples for profitability
Using the above formulas, we arrive at the following table for when the attack will be profitable.
Requirement for | |||
---|---|---|---|
% | |||
% | |||
% | |||
% | |||
% | |||
% | |||
% | |||
Not possible | |||
% | |||
% | |||
Not possible | |||
Not possible |
We can see that, for example, for a reasonable ratio of 1%, and a fee of 0.01% (), a value of % suffices to make the attack profitable.
Code used to generate the profitability table
The following Sage code was used to generate the table above.
def lhs(gamma, r):
a = 1/2
b = (1 - 2*gamma) / (2 - 2*gamma)
c = (r**(-1)) * ((2*gamma) / (1 - gamma))
inner = a + b - c
outer = (1 - gamma) * inner
return outer
def calc(gamma, r):
return (lhs(gamma, r)**(-1)) - 1
print('| `l!$\gamma$` | `l!$r$` | requirement for `l!$d$` |')
print('| --- | --- | --- |')
for gamma in (0.0001, 0.001, 0.01):
for r in (1, 0.1, 0.01, 0.001):
if lhs(gamma, r) <= 0:
result = 'not possible'
else:
result = f'`l!$d \geq {float(calc(gamma, r)*100):.2f}$`%'
print(f'| `l!${float(gamma)}$` | `l!${float(r)}$` | {result} |')
Disclaimer
The above estimates are a best effort based on simplifying assumptions (such as ticks being continuous real numbers rather than discrete, tick spacing and position width infinitesimal, etc.), so the numbers obtained may not be precise boundaries that are safe.