12/26/22
Author: Parti
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.
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.
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.
Two different tokens' balance forms each "pool". Users can add balance to these pools and later remove it. Let's call the balance of token A and the balance of token B. The following equation gives the relationship among these balances:
where is called Liquidity. This is modified only when someone adds or removes the token balance and is constant otherwise.
Anyone can swap token A for token B or vice versa on this pool, modifying the balances and in the pool according to . 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
The way to think about this formula is to picture a user selling a quantity of token A to the pool. The pool reserve will increase for token A while decreasing for token B. The ratio will yield the price at which the user sold their token A amount. This paper explains how Eq. and an invariant are all we need to build an AMM.
From we have . Then
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 WETH and USDC, the price of WETH can be computed as . 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 :
Notice we can also describe the pool using liquidity and price instead of token balances. Using :
Let's make a swap and see how the price moves. Suppose Alice wants to buy token B from the AMM with liquidity with token A. Then:
Alice gets an amount of token B in return:
The final price of token A relative to token B is:
From where can we recover
where is the initial price of token A relative to token B. Eq. tells us exactly how much Alice needs to move the spot price from to given the liquidity.
Similarly, if Alice wanted to buy token A with token B:
And we get to
You can play around with these formulas in this link.
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.
Eq. gets replaced by the following formula
The equation is misleading, as liquidity can change for different price ranges. Eq. 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 and . Doing so would eliminate the offsets and make equal to . 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 for and :
We can compute the price as
As a corollary, we can also obtain the following interesting result.
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:
the terms and are only centring the equation in the corresponding range, as you can see in the Figure from the whitepaper:
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. . 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 and the edge prices of the position and , then
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.
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. and ). 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 ( and 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 between edges and .
If moving the price of token A relative to token B to the upside: We have to dump token B. Here 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 . Using : - From here, we compute how much capital is required to reach the following edge : But is the balance of token B when the position crosses the liquidity range, which equals zero. Hence We can use this same procedure for the following edges - Finally, to reach the target price from the last crossed edge : Then we have:
where $j$ is a function of the final price and the liquidity edges¹.
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.
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:, where with and the edges of each position. This expression assumes each position was deployed centered around
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
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.
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 . An attacker must consider this when manipulating a price. But what is a ? Let's dive in.
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.
ℹ️ Given 2 numbers, and : Arithmetic mean: Geometric mean:
This can be easily generalized over numbers as Geometric mean:
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 , 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 and :
As stored observations are constant in intervales, we can model this equation:
For some discretization, where is the index of each discrete interval, the price at each discrete interval, and is the number of discrete intervals and (for instance, if observations have a duration of and , equal to 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 and fall exactly on the observations, which is most commonly not the case, but the error is bounded by the block duration () 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 for blocks and moves to for blocks, then
From here, we can deduce a simple formula for as a spot price to achieve a desired value:
You can gain more by playing around at this link. Notice the target price behaves similarly to an exponential function with as the exponent (this is not exact due to the divisor). So, as a takeaway:
ℹ️ To manipulate a to the desired price, an attacker needs to move the spot much more so that the average falls on target. The longer the length is relative to the attack's , the harder it is to manipulate. That is why longer 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.
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.
The regular scheme for attacking a lending market is the following:
Then, the profit is
Where , the final cost of manipulation depends on point 5, and is the exceeding capital sold back to the pool (only greater than zero if reserves were exhausted). Any attack with is profitable (equivalent to 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 , but are also valid for v3) for large enough manipulations.
Suppose the attacker knows arbitrage will happen and the pool has a Full Range position with liquidity . 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 . The cost of this manipulation is
and the pool returns a token B amount
The attacker will use a part from this to borrow (until emptying the reserves) and a part to sell back to the pool. Hence,
The number of reserves caps the :
What's missing will be swapped back .
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 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.
To include the parameters in the analysis, we should compute the Cost of Manipulation with the spot price added using Eq. while keeping the 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 is not an on-off switch and has different levels, which we can measure with the ratio , with the approximate number of blocks in the and 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 cost, as long as is not extremely close to 1, we can practically discard these types of attacks as well.
✅ From , we see the cost of manipulation goes linear with the liquidity, but almost exponentially with the ratio. Lending markets are safe against this scenario if they choose a long enough . For this same reason, it is fundamental to pay attention to the cardinality of the pool, which will limit the worst-case scenario for .
⚠️ 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.
Two main factors can endanger -based oracle liquidity:
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 . Then, they proceeded to empty over $1M USD from the Pool 90 Fuse for only 10k FLOAT.
⚠️ 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 and spot price is large, but as time passes, the 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.
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.
Suppose the validator has consecutive blocks. In that case, the attacker can manipulate over blocks to reduce the initial capital required. In the final block , 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 becomes closer to the initial price as the number of proposed blocks increases ( 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 ) 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 , with no price impact.
How would an optimal attack scheme look post-PoS for a validator with consecutive blocks?
and should be chosen to maximize the reserves. Demanipulating further would be equivalent to selling for a lower price.
where corresponds to the cost of manipulating up to and 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 . The higher this capital, the lower the target (but also, the less profit). For significant enough price manipulations, is sufficient to be profitable, and might be unnecessary. This dependence with complicates the use of almost Full Range positions as a more efficient alternative to Full Range positions.
This scheme requires an additional up-front capital , 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 the market uses relative to the attack length , 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 🧠
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 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.
Special thanks to Guillaume Lambert and Gaston Maffei for the review and feedback.