Oracle Manipulation 101 (Math edition)

12/26/22

Author: Parti

Intro

This article is divided into two.

In the first part, we will present a quick refresher on how Uniswap works specifically tailored to the needs of computing manipulation costs. It'll explore how to move the spot price in an AMM to the desired target for Uniswap v2 and v3.

The second part will show how we obtained the results from the "Oracle Manipulation 101" article. To do so, we will present step-by-step an attack of a lending protocol in DeFi. This case can be later generalized to different types of markets.

You can follow along with the simulations provided in this colab.

Uniswap Math

There are many great resources on the topic of Uniswap. We will show some definitions and then present new results for price manipulation in Uniswap v3.

Full Range and Uniswap v2

Let's begin by looking at Uniswap v2 AMMs, equivalent to a Full Range position in Uniswap v3. Note that we'll confirm this below.

Basic equations

Two different tokens' balance forms each "pool". Users can add balance to these pools and later remove it. Let's call xx the balance of token A and yy the balance of token B. The following equation gives the relationship among these balances:

xy=L2(1)x*y=L^2 \hspace{1cm}(1)

where LL is called Liquidity. This LL is modified only when someone adds or removes the token balance and is constant otherwise.

img/blog-posts/oracle-manipulation-101-math/1.png

Anyone can swap token A for token B or vice versa on this pool, modifying the balances xx and yy in the pool according to (1)(1). You can visualize this behaviour in the Figure (source).

We can compute the price of token A relative to token B at which anyone buys tokens from any AMM (not only constant product type) as

PA/B=dydx(2)P_{A/B}=-\frac{dy}{dx} \hspace{1cm}(2)

The way to think about this formula is to picture a user selling a quantity dxdx of token A to the pool. The pool reserve will increase dxdx for token A while decreasing dy-dy for token B. The ratio will yield the price at which the user sold their token A amount. This paper explains how Eq. (2)(2) and an invariant are all we need to build an AMM.

From (1)(1) we have y=L2xy=\frac{L^2}{x}. Then

PA/B=ddx(L2x)=L2x2=yx(3)P_{A/B}=-\frac{d}{dx}(\frac{L^2}{x})=\frac{L^2}{x^2} =\frac{y}{x} \hspace{1cm} (3)

The price for Uniswap v2 pools can be computed as the ratio among the balances of the two tokens. So, for instance, if the WETH/USDC pool has 22 WETH and 40004000 USDC, the price of WETH can be computed as PWETH/USDC=40002=2000P_{WETH/USDC} = \frac{4000}{2}=2000. This expression is, of course, symmetrical to the exchange of labelling, so to compute the price of USDC relative to WETH in the previous example, we invert the equation (3)(3):

PUSDC/WETH=24000=0.0005=1PWETH/USDCP_{USDC/WETH} = \frac{2}{4000}=0.0005=\frac{1}{P_{WETH/USDC}}

Notice we can also describe the pool using liquidity and price instead of token balances. Using (3)(3):

P=yx=L2x2x=LPP = \frac{y}{x} = \frac{L^2}{x^2}\rightarrow x = \frac{L}{\sqrt{P}}

P=yx=y2L2y=LPP=\frac{y}{x}=\frac{y^2}{L^2}\rightarrow y = L\sqrt{P}\hspace{1cm}

Price manipulation

Let's make a swap and see how the price moves. Suppose Alice wants to buy token B from the AMM with liquidity LL with Δx\Delta x token A. Then:

(x+Δx)(yΔy)=L2=xy(x+\Delta x)(y-\Delta y)=L^2=xy

Alice gets an amount Δy\Delta y of token B in return:

Δy=y(Δxx+Δx)\Delta y = y(\frac{\Delta x}{x+\Delta x})

The final price of token A relative to token B is:

Pf=yΔyx+Δx=L2(x+Δx)2P_f = \frac{y-\Delta y}{x+\Delta x}= \frac{L^2}{(x+\Delta x)^2}

From where can we recover

Δx=LPfx=L(1Pf1Pi)(4)\Delta x = \frac{L}{\sqrt{P_f}}-x = L(\frac{1}{\sqrt{P_f}}-\frac{1}{\sqrt{P_i}}) \hspace{1cm} (4)

where PiP_i is the initial price of token A relative to token B. Eq. (4)(4) tells us exactly how much Δx\Delta x Alice needs to move the spot price from PiP_i to PfP_f given the liquidity.

Similarly, if Alice wanted to buy token A with Δy\Delta y token B:

(xΔx)(y+Δy)=L2=xyΔx=x(Δyy+Δy)(x-\Delta x)(y+\Delta y)=L^2=xy \rightarrow \Delta x = x(\frac{\Delta y}{y+\Delta y})

Pf=y+ΔyxΔx=(y+Δy)2L2P_f = \frac{y+\Delta y}{x-\Delta x}=\frac{(y+\Delta y)^2}{L^2}

And we get to

Δy=LPfy=L(PfPi)(5)\Delta y = L\sqrt{P_f}-y = L(\sqrt{P_f}-\sqrt{P_i})\hspace{1cm} (5)

You can play around with these formulas in this link.

Uniswap v3

Uniswap v3 revolutionized how AMMs work. The whitepaper goes in-depth on it, but the key takeaway is that liquidity can now be deployed over particular price ranges, unlocking a new level of capital efficiency.

Basic equations

Eq. (1)(1) gets replaced by the following formula

(x+xoffset)(y+yoffset)=L2,xoffset=Lpupper,yoffset=Lplower(6)(x+x_{offset})(y+y_{offset})=L^2,\quad\quad x_{offset}=\frac{L}{\sqrt{p_{upper}}},\quad y_{offset}=L\sqrt{p_{lower}}\hspace{1cm}(6)

The equation is misleading, as liquidity can change for different price ranges. Eq. (6)(6) is valid for each range where liquidity is constant and adjusts accordingly each time the price moves to a different liquidity region. We will dig deeper into it below.

Notice first that a Full Range liquidity position corresponds to taking the limits plower0p_{lower}\rightarrow 0 and pupperp_{upper}\rightarrow \infty. Doing so would eliminate the offsets and make (6)(6) equal to (1)(1). That's why a Full Range position in Uniswap v3 is equivalent to a Uniswap v2 position. Uniswap v3 does not reach prices of zero and infinity, but it's an excellent approximation.

We can solve (6)(6) for xx and yy:

x=L2y+yoffsetxoffset,y=L2x+xoffsetyoffset(7)x=\frac{L^2}{y+y_{offset}}-x_{offset},\quad y=\frac{L^2}{x+x_{offset}}-y_{offset} \hspace{1cm}(7)

We can compute the price as

P=dydx=L2(x+xoffset)2P=Lx+xoffset=y+yoffsetL(8)P=-\frac{dy}{dx}=\frac{L^2}{(x+x_{offset})^2}\rightarrow \sqrt{P}=\frac{L}{x+x_{offset}}=\frac{y+y_{offset}}{L} \hspace{1cm}(8)

As a corollary, we can also obtain the following interesting result.

y=LPyoffsetL=dydPy=L\sqrt{P}-y_{offset}\rightarrow L=\frac{dy}{d\sqrt{P}}

Heuristic approach

When dealing with Uniswap v3, it's easy to get lost in equations and forget the big picture. It's essential to have a clear image of what's going on to develop the "Unintuition".

One way of understanding Uniswap v3 is as a union of different Uniswap v2 pools for different price ranges. Analogously, we can think of it as a single Uniswap v2 pool but where liquidity is a split function such that:

L(price)={L0,price<p0L1,p0<price<p1LN,pN<priceL(price)=\begin{cases}L_0,\quad price<p_0\\ L_1,\quad p_0<price<p_1\\ \vdots\\ L_N,\quad p_N<price \end{cases}

the terms xoffsetx_{offset} and yoffsety_{offset} are only centring the equation in the corresponding range, as you can see in the Figure from the whitepaper:

img/blog-posts/oracle-manipulation-101-math/2.png

Understanding Liquidity

The LP is composed of a single asset when providing liquidity outside the current price range. Once deployed, the position will remain idle until the pool price reaches the edges. Only when the price falls inside will the balance of each asset follow Eq. (6)(6). Single-sided positions are often considered sell or buy orders, as swapers will convert them to the other asset once the price moves over. This interpretation is not strictly right, as the bought asset would convert back if the LP did not remove the position and price returns. We can find a better analogy in options.

Liquidity is not straightforward to compute now, as its formula depends on the price range. If we call the current price PP and the edge prices of the position plowerp_{lower} and pupperp_{upper}, then

  1. When PplowerP\leq p_{lower}, position is fully token A (y=0y=0): (x+Lpupper)Lplower=L2(x+\frac{L}{\sqrt{p_{upper}}})L\sqrt{p_{lower}}=L^2 x=L(1plower1pupper)=Lpupperplowerplowerpupperx=L(\frac{1}{\sqrt{p_{lower}}}-\frac{1}{\sqrt{p_{upper}}})=L\frac{\sqrt{p_{upper}}-\sqrt{p_{lower}}}{\sqrt{p_{lower}}\sqrt{p_{upper}}} LxL=xplower.pupperpupperplowerL_x\equiv L=x\frac{\sqrt{p_{lower}.p_{upper}}}{\sqrt{p_{upper}}-\sqrt{p_{lower}}}
  2. When PpupperP\geq p_{upper}, all position is made of B (x=0x=0): Lpupper(y+Lplower)=L2\frac{L}{\sqrt{p_{upper}}}(y+L\sqrt{p_{lower}})=L^2 y=L(pupperplower)y=L(\sqrt{p_{upper}}-\sqrt{p_{lower}}) LyL=ypupperplowerL_y\equiv L=\frac{y}{\sqrt{p_{upper}}-\sqrt{p_{lower}}}
  3. When plower<P<pupperp_{lower}<P<p_{upper}, Uniswap calculates the minimum between LxL_x and LyL_y: L=min[Lx,Ly],Lx=xP.pupperpupperP,Ly=yPplowerL=min[L_x,L_y],\quad L_x=x\frac{\sqrt{P.p_{upper}}}{\sqrt{p_{upper}}-\sqrt{P}},\quad L_y=\frac{y}{\sqrt{P}-\sqrt{p_{lower}}} Here, LxL_x is the liquidity provided by asset xx in the range (P,pupper)(P,p_{upper}) and LyL_y is the liquidity provided by yy in the range (plower,P)(p_{lower},P). An optimal position in such a scenario corresponds to Lx=LyL_x=L_y: Lx=LyxP.pupperpupperP=yPplowerL_x=L_y\rightarrow x\frac{\sqrt{P.p_{upper}}}{\sqrt{p_{upper}}-\sqrt{P}}=\frac{y}{\sqrt{P}-\sqrt{p_{lower}}} which can be solved for x,y,P,plowerx, y, P, p_{lower} or pupperp_{upper} without speaking of liquidity.

You can find the full code implementation in this link. You can play around with different values here. Also, you can find examples here and here.

img/blog-posts/oracle-manipulation-101-math/3.jpg

Price Manipulation

We can do the same math we did for Full Range positions to figure out how much capital is required to manipulate a pool to a target spot price (Eqs. (4)(4) and (5)(5)). To do so, we have to divide the range over the edges where the underlying liquidity changes. We can arrange these edges in a vector pedge=[0,p1,,pN,)p_{edge}=[0,p_1,\dots,p_N,\infty) (00 and \infty are not exact but concept-approximations, as there are a minimum and a maximum allowed price in Uniswap v3). Let's assume we are on a price PP between edges pip_i and pi+1p_{i+1}.

  1. If moving the price of token A relative to token B to the upside: We have to dump token B. Here yiy_i are the initial reserves of token B in the active range. - We can first compute how much capital we need to move the price to the following edge pip_{i}. Using (8)(8): pi+1P=(yi+Δy1+yoffseti)2(yi+yoffseti)2\frac{p_{i+1}}{P}=\frac{(y_i+\Delta y_1+y_{offset\,i})^2}{(y_i+y_{offset\,i})^2} pi+1P=(yi+Δy1+yoffseti)(yi+yoffseti)\sqrt{\frac{p_{i+1}}{P}}=\frac{(y_i+\Delta y_1+y_{offset\,i})}{(y_i+y_{offset\,i})} Δy1=(yi+yoffseti)(pi+1P1)\Delta y_1 =(y_i+y_{offset\,i})(\sqrt{\frac{p_{i+1}}{P}}-1) - From here, we compute how much capital is required to reach the following edge pi+2p_{i+2}: Δy2=(yi+1+yoffseti+1)(pi+2pi+11)\Delta y_2=(y_{i+1}+y_{offset\,i+1})(\sqrt{\frac{p_{i+2}}{p_{i+1}}}-1) But yi+1y_{i+1} is the balance of token B when the position crosses the liquidity range, which equals zero. Hence Δy2=yoffseti+1(pi+2pi+11)\Delta y_2=y_{offset\,i+1}(\sqrt{\frac{p_{i+2}}{p_{i+1}}}-1) We can use this same procedure for the following edges - Finally, to reach the target price PfP_f from the last crossed edge pjp_j: Δyj=yoffseti+j1(Pfpi+j11)\Delta y_j=y_{offset\,i+j-1}(\sqrt{\frac{P_f}{p_{i+j-1}}}-1) Then we have: Δy=l=1jΔyl,Δyl={(yi+yoffseti)(pi+1Pi1)l=1yoffseti+l1(pi+lpi+l11)l1,ljyoffseti+j1(Pfpi+j11)l=j(9)\Delta y=\sum_{l=1}^j\Delta y_l,\quad \Delta y_l = \begin{cases}(y_i+y_{offset\,i})(\sqrt{\frac{p_{i+1}}{P_i}}-1) & l=1\\ y_{offset\, i+l-1}(\sqrt{\frac{p_{i+l}}{p_{i+l-1}}}-1)&l\neq 1,l\neq j\\ y_{offset\, i+j-1}(\sqrt{\frac{P_f}{p_{i+j-1}}}-1)&l=j \end{cases}\hspace{1cm} (9)

    where $j$ is a function of the final price and the liquidity edges¹.
    
  2. If moving the price of token B relative to token A to the downside: We have to dump token A. The reasoning is the same but in the other direction.

Δx=l=1jΔxl,Δxl={(xi+xoffseti)(1piP)l=1xoffsetil+1(1pil+1pil+2)l1,ljxoffsetij+1(1Pfpij+2)l=j(10)\Delta x=\sum_{l=1}^j\Delta x_l,\quad \Delta x_l = \begin{cases}(x_i+x_{offset\,i})(1-\sqrt{\frac{p_{i}}{P}}) & l=1\\ x_{offset\, i-l+1}(1-\sqrt{\frac{p_{i-l+1}}{p_{i-l+2}}})&l\neq 1,l\neq j\\ x_{offset\, i-j+1}(1-\sqrt{\frac{P_f}{p_{i-j+2}}})&l=j \end{cases}\hspace{1cm} (10)

It is possible to compute the cost of manipulation analytically. Still, to do so, it's necessary to keep track of every liquidity and its edges, which can be done via direct pool querying or using indexers like The Graph. An alternative to this is to use a brute-force search (Euler has built an excellent implementation of this approach).

1: Guillaume Lambert made me notice that for arbitrary Uni v3 positions, this can be taken to a closed form:Δy=PiPf((r1)/rPL(P))dP\Delta y = \int_{P_i}^{P_f}((r-1)/√r * √P * L(P)) dP, where r=PbPar=\sqrt{\frac{P_b}{P_a}} with PaP_a and PbP_b the edges of each position. This expression assumes each position was deployed centered around K=PaPbK=\sqrt{P_aP_b}

Concentrated liquidity and price manipulation

With high power comes high responsibility: compared to Full Range liquidity, concentrated liquidity is excellent at increasing the potential LP return, but it also means manipulating the price gets easier. The most straightforward way to think about it is that the concentrated LP sells its capital at a lower average price than a Full Range. This affirmation is not always true, but it is for sure when the position is in the active region.

It is possible to make manipulation more expensive through concentrated positions by placing them far away from the current price. Still, there are no incentives to do so (this position would receive zero fees).

On top of that, concentrated liquidity is more prone than Full-Range to move from pool to pool to seek a higher APY.

For all these reasons, when evaluating the safety of Uniswap-based oracles, Full Range positions are strongly encouraged and taken into account over concentrated positions (see here and here).

The problem is, not even Full Range positions can be considered secure with current models. Remember that oracle users often differ from protocols that provide liquidity for their token. A lending market that accepts a specific token as collateral cannot know if the available full-range position will stay and cannot assign higher tiers/LTVs to them. This issue is one of the main points that PRICE solves.

In the analysis, we will only consider the Full Range positions, as

  1. It's consistent with the above liquidity visibility and security claims.
  2. We will require committed Full-Range positions for the oracle to work. When adding concentrated positions on top, the manipulation cost will only increase so that the analysis will serve as a worst-case scenario.

But what about positions which are almost Full Range? Well, that's an exciting topic. When manipulating a price, no one takes the price to infinity or zero, and deploying only over this range will surely increase capital efficiency. This claim is valid but requires knowing at which price a manipulation stops being profitable, which is not universal: it's a function of each market's parameters and the attacker's available capital.

PRICE will likely incorporate semi Full Range positions in future versions, but always with a mandatory Full Range position below.

TWAPs

Uniswap knows its role as a decentralized on-chain price source and has built its contracts to accommodate this fact. With the current version, price queries return a mathematical object known as TWAPTWAP. An attacker must consider this when manipulating a price. But what is a TWAPTWAP? Let's dive in.

TWAPTWAP stands for time-weighted average price. It's a geometric average price for a pool over a fixed interval of time and is what we query from the current implementation of the Uniswap v3 Oracle library. It's also a standard trading tool, as seen in the red line in the Figure.

img/blog-posts/oracle-manipulation-101-math/4.png

ℹ️ Given 2 numbers, a1a_1 and a2a_2: Arithmetic mean: a1+a22\frac{a_1+a_2}{2} Geometric mean: a1a2\sqrt{a_1*a_2}

This can be easily generalized over NN numbers aia_i as Geometric mean: i=1NaiN\sqrt[N]{\prod_{i=1}^N a_i}

Each pool on Uni v3 keeps track of an array which stores historical information about the price (the "ticks" formally) and the time duration of it, together in the "accumulator" values (summation of the ticks times each elapsed time). The array has a fixed length (cardinality) and overwrites by "moving" to the right as new observations occur. The longer the cardinality, the more observations can be stored. Pools are initialized with a maximum of one stored observation but can be increased by paying gas to execute a function in the pool's contract.

The observation's array is updated on the block's first swap/liquidity provision (only if in the active range). Observations correspond to the values from the "accumulator" at the end of the previous block. Flash-loan manipulations are hence not recorded. To impact the TWAPTWAP, attackers would have to expose themselves to arbitrage for a long time, increasing the attack cost. Additionally, swappers and LPs are responsible for paying the gas cost of a price update, sustainably subsidizing the oracle. Truly a giga-brain design by the Uniswap team.

Many resources are available on how the Geometric mean works in Uniswap v3. The key equation to keep in mind is (5.2) from the whitepaper describing the value of the geometric mean time-weighted average price among times t1t_1 and t2t_2:

Pt1,t2=(i=t1t2Pi)1t2t1(11)P_{t_1,t_2} =\left(\prod_{i=t_1}^{t_2}P_i\right)^{\frac{1}{t_2-t_1}} \hspace{1cm}(11)

As stored observations are constant in intervales, we can model this equation:

Pt1,t2TWAPt1,t2=(i=1NPi)1NP_{t_1,t_2}\equiv TWAP_{t_1,t_2} = (\prod_{i=1}^N P_i)^{\frac{1}{N}}

For some discretization, where ii is the index of each discrete interval, PiP_i the price at each discrete interval, and NN is the number of discrete intervals t1t_1 and t2t_2 (for instance, if observations have a duration of 4s,2s4s, 2s and 3s3s, NN equal to 99 will be enough to make this formula exact).

It is possible to make an approximation where these constant intervals are Ethereum blocks: this is not exact because it assumes t1t_1 and t2t_2 fall exactly on the observations, which is most commonly not the case, but the error is bounded by the block duration (12.5s12.5s) on each end. The longer the interval, the more likely it is to have a better approximation, as each observation has less weight. If the price remains constant at P1P_1 for NMN-M blocks and moves to P2P_2 for MM blocks, then

TWAPt1,t2(P1(NM)P2M)1N(12)TWAP_{t_1,t_2} \simeq (P_1^{(N-M)}P_2^M)^\frac{1}{N} \hspace{1cm}(12)

From here, we can deduce a simple formula for P2P_2 as a spot price to achieve a desired TWAPTWAP value:

P2TWAPt1,t2NP1(NM)M(13)P_2 \simeq \sqrt[M]{\frac{TWAP_{t_1,t_2}^N}{P_1^{(N-M)}}} \hspace{1cm} (13)

You can gain more by playing around at this link. Notice the target price behaves similarly to an exponential function with N/MN/M as the exponent (this is not exact due to the divisor). So, as a takeaway:

  • Using longer TWAPs will make movements exponentially harder.
  • Moving the price over several blocks reduces the costs exponentially.

img/blog-posts/oracle-manipulation-101-math/5.png

ℹ️ To manipulate a TWAPTWAP to the desired price, an attacker needs to move the spot much more so that the average falls on target. The longer the TWAPTWAP length NN is relative to the attack's MM, the harder it is to manipulate. That is why longer TWAPsTWAPs are suggested for a safer query.

Conversely, taking longer TWAPs means having a laggier query and a less accurate price. This tradeoff between security and precision is one of the main critiques of using Uniswap as an oracle. At PRICE, we have reduced this tradeoff to the bare minimum, eliminating another user headache.

Oracle Manipulation 101 Math

In the Oracle Manipulation 101 article, we have presented a study case for an oracle manipulation analysis. In particular, we have explained how an attack on a lending market can become profitable. This section will show how many of the results we have presented were derived.

As a brief rundown, attacks will likely happen if the profit from manipulation exceeds the cost of manipulation. Understanding this is fundamental to setting the parameters that allow for capital efficiency without adding new risks.

The Cost of Manipulation refers to the capital used ti borrow + capital used to move an AMM's price to the desired target. The latter is what we deduced in the Uniswap Math section above, both for Uniswap v2 and v3. As mentioned in Oracle Manipulation 101, we will consider Full Range positions for this analysis, which is consistent with our previous claims about concentrated positions.

We will exclude trading fees for simplicity of reading, but you can trivially add them to the analysis.

Math for Attack Scheme pre PoS

img/blog-posts/oracle-manipulation-101-math/6.png

The regular scheme for attacking a lending market is the following:

  1. The attacker will use a total capital CC (measured in B): C=Ccolateral+CmanipulationC=C_{colateral}+C_{manipulation}
  2. With CcolateralC_{colateral}, purchase acolaterala_{colateral} of A at price PiP_i (relative to B): Ccolateral=acolateralPiC_{colateral}=a_{colateral}P_i. The attacker can do this over several blocks and liquidity markets to mitigate price impact. This step is not necessary if efficient arbitrage exists.
  3. With CmanipulationC_{manipulation}, manipulate the TWAPTWAP to TWAPfinalTWAP_{final}, by purchasing amanipulationa_{manipulation} of token A in the AMM. The spot price to manipulate depends on the length of the TWAPTWAP queried by the market.
  4. Deposit acolaterala_{colateral} as collateral and borrow asset B with borrowing capacity facolateralTWAPfinalf*a_{colateral}*TWAP_{final}. Notice the reserves cap this amount. If efficient arbitrage exists, then deposit amanipulationa_{manipulation} instead.
  5. If possible (no arbitrage), manipulate the price back to the starting value by selling back amanipulationa_{manipulation} (or what's left) of A and get at most CmanipulationC_{manipulation} back. This step makes a huge difference in cost.
  6. Default bad debt and profit. Notice the stolen capital B can be sold slowly over time to reduce price impact.

Then, the profit is

Profit=min[acolateralfTWAPfinal,Reserves+SoldBack]acPiCmanipulationProfit=min[a_{colateral}fTWAP_{final},Reserves+SoldBack]-a_cP_i-C_{manipulation}^*

Where CmanipulationC_{manipulation}^*, the final cost of manipulation depends on point 5, and SoldBackSoldBack is the exceeding capital sold back to the pool (only greater than zero if reserves were exhausted). Any attack with Profit>0Profit>0 is profitable (equivalent to f×TWAPfinal>1f\times TWAP_{final}>1 if no arbitrage). If arbitrage will happen (step 5 is impossible), then the most efficient strategy is to use the capital obtained from manipulation as collateral.

Before PoS, for relevant enough markets, manipulating the price back to the initial value was extremely unlikely for the attacker for healthy liquidities, as Uniswap v3 Oracle requires a one-block delay to update, exposing the arbitrage opportunity (an explanation on this on the Uniswap refresher). This paper showed that, if efficient arbitrage exists, a single-block attack becomes cheaper to execute than a multi-block attack (results are for Uniswap v2 TWAPTWAP, but are also valid for v3) for large enough manipulations.

Healthy liquidity

Suppose the attacker knows arbitrage will happen and the pool has a Full Range position with liquidity LL. In that case, the best plan is to borrow as much as possible (sell high) using the capital obtained from manipulation. What's missing should be swapped for a price close to PiP_i. The cost of this manipulation is

Cmanipulation=L(PfPi)C_{manipulation}^*=L(\sqrt{P_f}-\sqrt{P_i})

and the pool returns a token B amount

Δy=y(Δxx+Δx)\Delta y =y(\frac{\Delta x}{x+\Delta x})

The attacker will use a part from this Δy\Delta y to borrow (until emptying the reserves) and a part to sell back to the pool. Hence,

Δy=Δysell+Δyborrow\Delta y = \Delta y_{sell}+\Delta y_{borrow} Δyborrow=StolenfPf\Delta y_{borrow}=\frac{Stolen}{fP_f}

The number of reserves caps the Δyborrow\Delta y_{borrow}:

Δyborrowmax=ReservesfPf\Delta y_{borrow}^{max}=\frac{Reserves}{fP_f}

What's missing will be swapped back ΔxsellPiΔysell\Delta x_{sell} \simeq P_i\Delta y_{sell}.

Profit=ΔxoutΔxin=min[fPfy(LPfxLPf),Reserves+ΔysellPi]CmanipulationProfit = \Delta x_{out} - \Delta x_{in} = min[fP_fy(\frac{L\sqrt{P_f}-x}{L\sqrt{P_f}}), Reserves+ \Delta y_{sell}P_i]-C_{manipulation}^*

You can play around simulating the arbitrage scenario in this link. You can see in the Figure below that the optimal attack in this scenario will correspond to using all capital from the manipulation to borrow up to the available reserves (no Δysell\Delta y_{sell} left). It's possible to find this optimal price analytically as a function of the reserves, which LPs can use to define safe semi Full-Range positions. Notice this graph does not take TWAP into account and is only valid for markets which query the spot price.

img/blog-posts/oracle-manipulation-101-math/7.png

To include the TWAPTWAP parameters in the analysis, we should compute the Cost of Manipulation CmanipulationC_{manipulation}^* with the spot price added using Eq. (3)(3) while keeping the TWAPTWAP price to obtain the stolen amount. We can also simulate this and check that manipulation cost increase radically to the point where single-block attacks are never profitable. Notice that the TWAPTWAP is not an on-off switch and has different levels, which we can measure with the ratio LengthattackLengthTWAPMN\frac{Length_{attack}}{Length_{TWAP}}\simeq \frac{M}{N}, with NN the approximate number of blocks in the TWAPTWAP and MM the number of blocks the manipulation lasted.

As we mentioned, multi-block attacks are more expensive than single-block attacks from a specific price onwards if there is efficient arbitrage (here). The primary explanation is that, even though the price has to move to a lower value for each block, the total required capital adds a more significant amount. If we include the additional TWAPTWAP cost, as long as LengthattackLengthTWAP\frac{Length_{attack}}{Length_{TWAP}} is not extremely close to 1, we can practically discard these types of attacks as well.

✅ From CmanipulationC_{manipulation}^*, we see the cost of manipulation goes linear with the liquidity, but almost exponentially with the LengthTWAPLengthattack\frac{Length_{TWAP}}{Length_{attack}} ratio. Lending markets are safe against this scenario if they choose a long enough LengthTWAPLength_{TWAP}. For this same reason, it is fundamental to pay attention to the cardinality of the pool, which will limit the worst-case scenario for LengthTWAPLength_{TWAP}.

⚠️ Incredibly, there are still markets using spot price as an oracle. We can see this mainly outside of Ethereum. A recent example was the attack on Mango Markets.

Unhealthy liquidity

Two main factors can endanger TWAPTWAP-based oracle liquidity:

  1. Bad liquidity positions in Uniswap v3: as we mentioned, a pool is, in most cases, easier to manipulate when liquidity is concentrated rather than over the Full Range. Price manipulation costs zero over regions with no liquidity.

img/blog-posts/oracle-manipulation-101-math/8.png

  1. No liquidity in secondary markets: there is no way for arbitrage to close the trade effectively. As we mentioned, the absence of arbitrage makes manipulation back to the initial price possible (the attacker recovers capital used for price manipulation). It also unlocks multi-block attacks (requires less upfront capital).

Both issues are typical for small projects. This is, for instance, what happened to the stablecoin FLOAT in Rari (see the FLOAT incident in Rari here and here): liquidity was deployed only over the 1.16-1.74 USDC per FLOAT in Uniswap, which meant that manipulation cost was zero outside this range. As there was no liquidity in secondary markets, the attacker could wait for a few blocks and significantly impact the registered TWAPTWAP. Then, they proceeded to empty over $1M USD from the Pool 90 Fuse for only 10k FLOAT.

img/blog-posts/oracle-manipulation-101-math/9.jpg

⚠️ These attacks are the most common for small projects. Attacks in these contexts are hard to distinguish from rug pulls. A lending market can protect itself by reverting the borrowing if the difference between TWAPTWAP and spot price is large, but as time passes, the TWAPTWAP will get close, and basic checks will pass. Both users and lending markets should be aware of these risks when using or listing low-liquidity tokens. PRICE will include additional methods to mitigate this risk.

Math for Attack Scheme post-PoS

After the Merge, big stakers have a high chance of proposing multiple blocks in a row, which makes manipulation back to the initial price possible and significantly lowers the attack cost. It also makes TWAPs cheaper to move, as the attacker can maintain the manipulated price for longer.

img/blog-posts/oracle-manipulation-101-math/10.jpg

Suppose the validator has n>2n>2 consecutive blocks. In that case, the attacker can manipulate over n1n-1 blocks to reduce the initial capital required. In the final block nn, they can exercise partial manipulation back to the initial price (or near it). As we have shown in Eq. (1), the final spot price to manipulate a TWAPTWAP becomes closer to the initial price as the number of proposed blocks increases (MM in the equation). It's straightforward to show that the attack cost decreases enormously with this parameter. When protecting an oracle, we must be ready for the worst-case scenario, i.e. the post-PoS multi-block attack.

Suppose there is a delay of information (like Uniswap v3 TWAPTWAP) and no-arbitrage (PoS). In that case, some of the capital used for manipulation can be resold to the pool at the last block for higher values without altering the oracle price. Then, the remaining amount is collateral to default. Again, notice that borrowing to default is equivalent to selling at a diminished price due to ff, with no price impact.

How would an optimal attack scheme look post-PoS for a validator with nn consecutive blocks?

  1. Attack will use a total capital CC (measured in B): C=Ccolateral+CmanipulationC=C_{colateral}+C_{manipulation}
  2. With CcolateralC_{colateral}, purchase acolaterala_{colateral} of A at price PiP_i (relative to B): Ccolateral=acolateralPiC_{colateral}=a_{colateral}P_i. The attacker can do this over several blocks and liquidity markets to mitigate price impact.
  3. With CmanipulationC_{manipulation}, manipulate the TWAPTWAP to TWAPfinalTWAP_{final}, by purchasing amanipulationa_{manipulation} of A in the AMM. The spot price to manipulate depends on the length of the TWAP queried by the market and how many blocks the proposer has at disposition.
  4. Let n1n-1 block pass to register the new price.
  5. Manipulate the price back in the AMM to Pf=fTWAPfinalP_f'=f*TWAP_{final} by selling back abacka_{back}. The attacker still holds aleft=amanipulationabacka_{left} = a_{manipulation}-a_{back}. Notice alefta_{left} is equivalent to the amount out of manipulating the pool up to PfP_f'.
  6. In the same block, deposit acolateral+alefta_{colateral}+a_{left} as colateral and borrow asset B with borrowing capacity f(acolateral+aleft)TWAPfinalf(a_{colateral}+a_{left})TWAP_{final}. Notice the reserves cap this amount.
  7. Default bad debt and profit.

acolaterala_{colateral} and amanipulationa_{manipulation} should be chosen to maximize the reserves. Demanipulating further would be equivalent to selling for a lower price.

Profit=min[(acolateral+aleft)fTWAPfinal,Reserves+SoldBack]acolateralPiCmanipulationProfit = min[(a_{colateral}+a_{left})f*TWAP_{final},Reserves+SoldBack]-a_{colateral}P_i-C_{manipulation}'

where CmanipulationC_{manipulation}' corresponds to the cost of manipulating up to PfP_f' and SoldBackSoldBack the remaining capital sold back to the pool (only greater than zero if reserves were exhausted).

An attacker could also manipulate the TWAP without getting arbitraged if they propose several non-consecutive batches of blocks where they must sacrifice the final block of each batch to close the manipulation.

You can play around with a simulation for this attack here and generate plots for different variables. The absence of arbitrage in this scenario makes everything smoother from the attacker's perspective. This attack can reach profitability quite easily, even after considering the TWAP.

The equilibrium price is a function of acolaterala_{colateral}. The higher this capital, the lower the target TWAPTWAP (but also, the less profit). For significant enough price manipulations, alefta_{left} is sufficient to be profitable, and acolaterala_{colateral} might be unnecessary. This dependence with acolaterala_{colateral} complicates the use of almost Full Range positions as a more efficient alternative to Full Range positions.

img/blog-posts/oracle-manipulation-101-math/11.png

img/blog-posts/oracle-manipulation-101-math/12.png

This scheme requires an additional up-front capital abacka_{back} , which is trivially recovered by manipulating back, but it's also the heaviest capital. The up-front cost falls exponentially with the attack length (number of consecutive blocks to propose). The longer the LenghtTWAPLenght_{TWAP} the market uses relative to the attack length LengthattackLength_{attack}, the more serious this capital becomes.

⚠️ This attack can easily reach profitability. Increasing the TWAP parameters will only require the attacker to have a more significant up-front capital (redeemable after the attack).

⚡ So, we are in danger once again… unless we use PRICE 🧠

Conclusion

This article has gone through the basic definitions of Uniswap v2 and v3. We have computed the cost of manipulation in each case. We have also discussed the TWAPTWAP price and its parameters.

We then showed that previous to the PoS consensus, an attack on a lending market could be profitable only if the market used the spot price as an oracle or the liquidity was in an unhealthy shape, either by lousy deployment or absence in secondary markets.

With the recent switch to PoS, TWAP oracle manipulation has become profitable again, even for pools with healthy liquidity. Multi-block proposing allows attackers to filter away interactions with a specific pool during their proposing window. A new approach is necessary to keep using Uniswap TWAPs after the Merge, which PRICE brings to the table.

The following post will discuss the parameter selection used to design PRICE.

You can reach out to us on Twitter if you have any questions.

Price

Special thanks to Guillaume Lambert and Gaston Maffei for the review and feedback.