Uniswap v3 is an unregulated automated market maker (AMM) implemented on the Ethereum Virtual Machine (EVM). Compared to previous versions, Uniswap v3 improves capital efficiency, gives liquidity providers more control, enhances the accuracy and convenience of the price oracle, and introduces a more flexible fee structure.
Automated market makers (AMMs) are agents that pool liquidity and make it available to traders based on algorithms. Constant function market makers (CFMMs), a common category of AMMs that includes Uniswap, have been widely adopted in decentralized finance. They are typically implemented as smart contracts for trading tokens on permissionless blockchains.
Most existing CFMMs suffer from low capital efficiency. In the constant product market maker formula used by Uniswap v1/v2, only a fraction of the pool's funds are used for making markets at any given price. This is inefficient, especially when tokens trade within a narrow price range.
Note: Take stablecoins as an example, the price fluctuation range of USDC/USDT is extremely small. According to the v2 formula, liquidity providers essentially distribute funds across the price range (0, ∞), even though these prices will almost never be utilized. Thus, in Uniswap v1/v2 versions, capital efficiency is low, also leading to relatively higher slippage.
Products like Curve and YieldSpace have attempted to address the issue of capital utilization by establishing pools using different functions to describe the relationship between tokens. This requires all liquidity providers in the pool to follow the same formula, leading to liquidity fragmentation if they wish to provide liquidity in different price ranges.
This paper introduces Uniswap v3, a new AMM that allows liquidity providers more control over the price ranges where their capital is used, reducing the impact of liquidity fragmentation and gas consumption. This design does not rely on any assumptions about token price behavior. Uniswap v3 still utilizes the constant function curve from previous versions (
-
Concentrated liquidity: Liquidity providers (LPs) can concentrate liquidity within any price range. This improves pool capital efficiency and allows LPs to simulate their preferred price curves while providing efficient aggregated liquidity with the remaining funds. This feature and its implementation are described in sections 2 and 6, respectively.
-
Flexible fees: Transaction fees are no longer fixed at 0.30%. Instead, the fee tier is set at the initialization of each pool, with multiple tiers (pools) available for each trading pair. Default supported fee tiers are 0.05%, 0.30%, and 1%. New fee tiers can be added through UNI governance.
Note: The 9th UNI proposal to introduce a new fee tier of 0.01% has been implemented. The 0.01% fee tier is suitable for stablecoin trading scenarios, offering lower slippage and allowing Uniswap to compete directly with market leaders like Curve in the stablecoin trading domain.
-
Protocol fee governance: UNI governance can flexibly set the protocol's share of transaction fees (see section 6.2.2).
-
Improved price oracle: Uniswap v3 provides a way to query recent cumulative prices, avoiding manual recording of cumulative prices at the start and end of the period for calculating TWAP (time-weighted average price).
-
Liquidity oracle: The contract offers a time-weighted average liquidity oracle (see section 5.3).
Uniswap v2 core contracts were designed to be non-upgradable, thus Uniswap v3 is implemented on a set of brand new contracts. Like v2, Uniswap v3 contracts are non-upgradable but allow some parameters to be modified by governance, as discussed in section 4.
The central idea of Uniswap v3 is concentrated liquidity: limiting liquidity to a specific price range.
In previous versions, liquidity was evenly distributed along the following curve:
where
Note: For instance, in the case of stablecoin trading pairs, the price fluctuates minimally most of the time. If, like in Uniswap v2, liquidity is dispersed across all price ranges
$(0, \infty)$ , it leads to low capital utilization since most of the liquidity's price range will never be utilized.
Considering this, allowing LPs to concentrate their liquidity within narrower price ranges, rather than
Note: A v3 pool's range can be thought of as a part of a v2 pool.
Specifically, a position only needs to hold enough of token
When the price moves out of the position's range, its liquidity becomes inactive, and it no longer earns fees. At this point, liquidity consists entirely of one token, as the other has been depleted. If the price re-enters the range, liquidity becomes active again.
Note: Figure 1 shows that the movement of market prices in a Uniswap constant function pool is achieved through the increase and decrease of the balances of the two tokens in the pool. When the price (too high or too low) leaves the position's range, it means one of the tokens has been completely replaced by the other, leaving only one token remaining in the range at that time.
The amount of liquidity can be measured by
This curve is a variation of equation 2.1, with positions only having the ability to pay out within their range (Figure 2).
Note: Below, we derive equation 2.2:
From Figure 2, the v3 liquidity curve (the real reserves curve in the diagram) is actually obtained by translating the v2 liquidity curve (the virtual reserves curve in the diagram). Assuming the token balances (reserves) at points
$a$ and$b$ are$(x_a, y_a)$ and$(x_b, y_b)$ , respectively, the v2 curve needs to be translated along the x-axis by$-x_b$ and along the y-axis by$-y_a$ .Given the v2 curve:
$$x \cdot y = k$$ The translated v3 curve is:
$$(x + x_b) \cdot (y + y_a) = k \tag{2.2.0}$$ The prices at points
$a$ and$b$ are:
$$p_a = \frac{y_a}{x_a} \tag{2.2.1}$$
$$p_b = \frac{y_b}{x_b} \tag{2.2.2}$$ Also, given:
$$x_a \cdot y_a = k \tag{2.2.3}$$
$$x_b \cdot y_b = k \tag{2.2.4}$$ Therefore, from equations 2.2.1 and 2.2.3, we get:
$$y^2_a = k \cdot p_a$$
$$y_a = \sqrt{k} \cdot \sqrt{p_a} = L \sqrt{p_a}$$ From equations 2.2.2 and 2.2.4, we get:
$$x^2_b = \frac{k}{p_b}$$
$$x_b = \frac{\sqrt{k}}{\sqrt{p_b}} = \frac{L}{\sqrt{p_b}}$$ Since
$L = \sqrt{k}$ , thus$k = L^2$ Substituting
$x_b$ and$y_a$ into equation 2.2.0, we get:
$$ (x + \frac{L}{\sqrt{p_b}})(y + L\sqrt{p_a}) = k = L^2 $$
Liquidity providers can freely create any number of positions, each with its price range. This way, LPs can simulate any liquidity distribution they deem necessary in the price space (Figure 3 illustrates some examples). Moreover, this approach allows the market to determine where liquidity should be allocated. Rational LPs can concentrate liquidity in narrow ranges near the current price to reduce capital costs, adjusting their positions as prices move to keep their liquidity active.
Positions in extremely narrow ranges resemble limit orders, transforming entirely from one asset to another (plus accumulated fees) if the price crosses the range. Range orders differ from traditional limit orders in two main ways:
-
There is a minimum limit to the range of a position. If the price is exactly within the range, the limit order might only be partially executed.
-
After being crossed, the position must be manually withdrawn. Otherwise, if the price returns to the range, the position will automatically trade in the reverse direction.
Note: If the price repeatedly crosses a range order, the asset holdings in the position will automatically change, transitioning completely from one asset to another and then reversing, in a cyclic manner. Unlike CEX limit orders, which once fully executed, remain so even if prices later revert, completed orders do not roll back.
Therefore, to achieve the effect of traditional exchange limit orders, liquidity providers need to manually perform a withdrawal operation after the price crosses the limit range, to fully obtain the other token. Alternatively, automatic withdrawal functions provided by third-party applications, such as Gelato, can be used to implement traditional limit order effects with Uniswap v3 range orders.
Uniswap v3 implements several architectural changes, some of which are necessary for concentrated liquidity, while others are independent improvements.
In Uniswap v1 and v2, each trading pair corresponded to a separate liquidity pool, uniformly charging a 0.30% fee for all trades. Although historical data suggests the default fee level was reasonable for most tokens, it might have been too high for some pools (e.g., stablecoin pools) and too low for others (e.g., high volatility or niche tokens).
Uniswap v3 introduces multiple pools for each trading pair, allowing different transaction fees to be set separately. All pools are created using the same factory contract. Three default fee tiers are allowed: 0.05%, 0.30%, and 1%. Additional fee tiers can be added through UNI governance.
Note: A new 0.01% fee tier has been introduced through voting, catering to stablecoin trading scenarios and offering lower slippage, allowing Uniswap to compete with leaders like Curve in the stablecoin trading market.
3.2.1 Non-compounding fees. In previous versions, fee income was continuously deposited into the pool as liquidity. This meant that even without actively depositing, the pool's liquidity would grow over time, and fees could compound.
In Uniswap v3, due to the non-fungibility of positions, compounding becomes impossible. Instead, fees are accumulated separately and held in the form of the fee-paying tokens (see section 3.2.1).
Note: Since each position in v3 has its price range, v3's liquidity no longer spreads across all price ranges like in v2, meaning v2's liquidity is fungible and thus can be represented by ERC-20 tokens. In contrast, v3's liquidity essentially becomes an NFT (non-fungible token), represented using ERC-721.
3.2.2 Removal of native liquidity tokens. In Uniswap v1 and v2, the pool contract itself was an ERC-20 contract, and its tokens represented the pool's held liquidity. Although this representation was convenient, it was somewhat inconsistent with Uniswap v2's philosophy that anything not necessary to the core contract should be placed in a periphery contract. Using a "standard" ERC-20 implementation in the core contract prevented the creation of optimized versions of ERC-20 tokens in the future. Ideally, the ERC-20 token implementation should be placed in a periphery contract, then wrapped in a core contract as a liquidity position.
Note: Since the ERC-20 implementation of the pair contract was in the core contract and was non-upgradable, if there was a bug in the ERC-20 implementation, it would affect the entire v2 liquidity. Therefore, a better approach is to place the ERC-20 implementation in the periphery contract and only have a wrapper reference in the core contract for future upgrades to new versions of the ERC-20 implementation.
The changes introduced in Uniswap v3 make fungible liquidity tokens impossible. With the feature of custom liquidity supply, fees are now collected and held by the pool in separate tokens, rather than automatically reinvested as the pool's liquidity.
Therefore, v3's pool contract does not implement the ERC-20 standard. Anyone can create an ERC-20 token contract in the periphery to make liquidity positions more interchangeable, but this requires additional logic to handle the distribution or reinvestment of fee income. Alternatively, anyone can create a periphery contract using an ERC-721 NFT token to represent individual liquidity positions (including accumulated fees).
The factory contract has an owner, initially controlled by UNI token holders. The owner has no authority to pause any operation of the core contract.
Note: On the ETH mainnet, the factory contract address is 0x1F98431c8aD98523631AE4a59f267346ea31F984, with the owner being a TimeLock contract at 0x1a9C8182C09F50C8318d769245beA52c32BE35BC.
Like Uniswap v2, Uniswap v3 also has a protocol fee that can be enabled by UNI governance. In Uniswap v3, UNI governance can set the protocol's share of transaction fees more flexibly, allowing the protocol fee to be set to
Note: Uniswap v2 could only set the protocol fee on a global basis, whereas Uniswap v3 allows it to be set for each pool individually.
UNI governance can add additional transaction fee tiers. When adding a fee tier, the corresponding tickSpacing parameter can also be defined (see section 6.1). Once a fee tier is added to the factory contract, it cannot be removed (nor can tickSpacing be modified). The initial fee tiers and tickSpacing are 0.05% (tickSpacing of 10, approximately 0.10% between two initialized ticks), 0.30% (tickSpacing of 60, approximately 0.60%), and 1% (tickSpacing of 200, approximately 2.02%).
Note: Regarding the concept of ticks and tick spacing, please refer to section 6.1.
Simply put, each tick (point) corresponds to a price. To aggregate the liquidity of different positions, the price space is divided into ticks that can be initialized, with only ticks divisible by tickSpacing allowed to be initialized. Inside a tick, the trading mechanism is similar to v2; once the liquidity of that tick is consumed, the price moves to the next tick and repeats the process. Thus, smaller tickSpacing means more continuous liquidity and smaller slippage but also higher gas consumption.
Therefore, each fee tier's tickSpacing is a trade-off value. Generally, higher fee tiers have larger tickSpacing because higher fees indicate greater volatility of the trading pair, and traders can tolerate larger slippage.
The current fee configuration on the chain can be checked through the factory contract: feeAmountTickSpacing, with the supported feeAmount and tickSpacing being:
{100: 1, 500: 10, 3000: 60, 10000: 200}
.As mentioned in section 6.1, the minimum price difference between two adjacent ticks is 0.01%.
Finally, UNI governance has the right to transfer ownership to another address.
Uniswap v2 introduced the Time-Weighted Average Price (TWAP) oracle feature. Uniswap v3's TWAP includes three significant changes.
The most important change is that Uniswap v3 does not require oracle users to record historical cumulative prices externally. Uniswap v2 required users to manually record cumulative prices at the start and end of the period for calculating TWAP. Uniswap v3 moves the cumulative checkpoints to the core contract, allowing external contracts to directly calculate the recent on-chain TWAP without additional record-keeping.
Another change is that Uniswap v3 no longer calculates the arithmetic mean TWAP using the sum of cumulative prices; instead, it records the sum of
Note: As mentioned in the "Dive into Uniswap v2 Whitepaper," compared to the arithmetic mean, the geometric mean is less influenced by extreme values and does not require separate cumulative price records for each token, as the geometric mean price of one token is the reciprocal of the other.
Lastly, in addition to price accumulators, Uniswap v3 adds a liquidity accumulator, accumulating
Similar to Uniswap v2, Uniswap v3 records cumulative prices at the start of each block, multiplied by the time (in seconds) since the last block.
Uniswap v2's pool only saves the latest value of cumulative prices, updated by the most recent transaction block. When calculating the average price in Uniswap v2, external callers are responsible for providing historical cumulative price data. If there are many external users, each must independently maintain a method to record historical cumulative prices or use a shared method to reduce costs. Moreover, there is no guarantee that every interactive block will affect the cumulative price.
In Uniswap v3, the pool saves a series of historical cumulative prices (and as described in section 5.3, including cumulative liquidity). During the first interaction with the pool in each block, the contract automatically records cumulative prices and cyclically overwrites the oldest value in the array with the new value, similar to a circular buffer. Although the array is initially allocated space for only one checkpoint, anyone can initialize additional storage slots to extend the array up to 65,536 checkpoints. Anyone extending the checkpoint space for a trading pair must pay a one-time gas cost to initialize additional storage slots for the array.
Note: Expanding the checkpoint space is a one-time operation, paid by the initiator. For example, if someone wants the Uniswap v3 ETH-USDC trading pair to provide more historical price checkpoints (more checkpoints mean the oracle price calculated using on-chain data will be more reliable, as the cost for an attacker to manipulate these prices increases), they would call the ETH-USDC pair contract interface to expand the checkpoint space and pay the gas cost, as this operation allocates additional EVM storage slots for the trading pair.
The pool not only provides users with an array of historical observation data but also encapsulates a convenient function for finding the cumulative price at any point in the observation period.
Uniswap v2 maintains two cumulative prices, one for the price of token0 in terms of token1, and the other for the price of token1 in terms of token0. Users can calculate the time-weighted arithmetic mean price for any period by subtracting the cumulative price at the end of the period from the cumulative price at the beginning and dividing by the time (in seconds). Note that cumulative prices for token0 and token1 are tracked separately because the two arithmetic mean prices are not reciprocals of each other.
Uniswap v3 uses a time-weighted geometric mean price, eliminating the need to maintain separate cumulative prices for two tokens. The geometric mean of a set of ratios is the reciprocal of the geometric mean of their reciprocals.
Note: Suppose the price of token0 in terms of token1 is
$x$ , then the price of token1 in terms of token0 is$\frac{1}{x}$ .The geometric mean price of token0:
$$P_0 = \sqrt[n]{x_1 \cdot x_2 \cdot ... \cdot x_n}$$ The geometric mean price of token1:
$$P_1 = \sqrt[n]{\frac{1}{x_1} \cdot \frac{1}{x_2} \cdot ... \cdot \frac{1}{x_n}} = \frac{1}{\sqrt[n]{x_1 \cdot x_2 \cdot ... \cdot x_n}} = \frac{1}{P_0}$$ Thus, the geometric mean prices of two tokens are reciprocals of each other, and Uniswap v3's contract only needs to save the cumulative price of one token.
Implementing geometric mean prices in Uniswap v3 is relatively straightforward thanks to the custom liquidity supply mechanism (see section 6). Additionally, the accumulator can be represented with fewer bits because it only records
Note: To represent all possible prices with acceptable precision, Uniswap v2 used a 224-bit fixed-point number for price representation. Uniswap v3 only needs a 24-bit signed integer to represent
$\log_{1.0001}{P}$ , which can detect a price change of one basis point, 0.01%.As mentioned earlier, market prices themselves are a type of random Brownian motion. Theoretically, using a geometric mean is more accurate in tracking average prices because arithmetic means are more susceptible to distortion by extreme values.
Uniswap v3 records the cumulative sum of the current tick number (
Note: Why can
$\log_{1.0001}P$ detect a precision of price change as 0.01% (one basis point)?Since Uniswap v3 uses int24 (24-bit signed integer) to represent ticks, suppose the current tick is
$i$ , corresponding to price$P_1$ ; the next closest tick is$i + 1$ , with corresponding price$P_2 = P_1 \cdot 1.0001$ , its relative change in price to$P_1$ is:
$$\frac{P_2 - P_1}{P_1} = \frac{P_1 \cdot 1.0001 - P_1}{P_1} = 1.0001 - 1 = 0.0001 = 0.01%$$
The geometric mean price (time-weighted average price)
Note: Here, we revisit the definition of the geometric mean:
Geometric Mean:
$$ G(x_1,...,x_n) = \sqrt[n]{x_1 ... x_n} $$ It's evident that
$P_{t_1,t_2}$ is the geometric mean price for the period from$t_1$ to$t_2$ .
To calculate this value, you can look at the cumulative price at moments
In addition to the weighted cumulative count of
This count can be used by external liquidity mining contracts to distribute rewards fairly. If an external contract wishes to distribute rewards at an average rate of
Note:
$s_{pl}(t)$ represents the total duration (in seconds) each unit of liquidity has persisted up to moment$t$ .
$(s_{pl}(t_1) - s_{pl}(t_0))$ represents the total duration (in seconds) each unit of liquidity has persisted during the period from$t_0$ to$t_1$ .
$R \cdot (s_{pl}(t_1) - s_{pl}(t_0))$ represents the reward for each unit of liquidity during the period from$t_0$ to$t_1$ .Since the active liquidity of that position is
$L$ , the reward for the period from$t_0$ to$t_1$ is:
$$R \cdot L \cdot (s_{pl}(t_1) - s_{pl}(t_0))$$
To extend this formula to only reward liquidity within a position's range, Uniswap v3 saves a checkpoint based on this value each time a tick is crossed, as described in section 6.3.
On-chain contracts can use this cumulative count to make their oracles more robust (e.g., to evaluate which fee tier pool is better suited as an oracle data source).
The remaining sections of this paper will discuss the implementation mechanism of concentrated liquidity supply and its implementation in contracts.
To implement custom liquidity supply, the possible price space is discretized into ticks. Liquidity providers can supply liquidity between any two ticks (not necessarily adjacent).
Each range can be defined by a pair of tick indices (signed integers): a lower tick (
Conceptually, a tick exists whenever the price
By definition, the precision of price movement between two adjacent ticks is 0.01% (one basis point).
Note: Refer to the derivation in section 5.2.
Due to technical reasons described in section 6.2.1, the pool contract actually tracks ticks using the square root of the price,
For example,
When liquidity is added to a range, if one or both ticks are not already used as boundaries by existing positions, that tick is initialized.
Not every tick can be initialized. Each pool pair is initialized with a parameter tickSpacing (
Whenever the price crosses an initialized tick, virtual liquidity is added or removed. The gas cost incurred by crossing an initialized tick is fixed and independent of the number of positions adding or removing virtual liquidity at that tick.
To ensure the correct amount of liquidity is added and removed when the price crosses a tick and to ensure position holders receive their proportionate share of fees while within a price range, the pool contract needs to perform some accounting. The contract uses storage variables to record global (per pool), tick-level, and position-level states.
The contract's global state includes seven storage variables related to swapping and liquidity supply. (It also has other variables for the oracle, as described in section 5.)
In Uniswap v2, each pool contract recorded the current token balances of the pool:
Note: In reality, v3 operates according to the constant function only within a specific price range.
The contract records two different values: liquidity (
Conversely, the virtual balances of the two tokens can also be calculated using these two values:
Using
You might notice that the liquidity formula (based on the virtual balances of the tokens) is similar to the formula Uniswap v2 used for initializing the quantity of liquidity tokens (when no fee income has yet been accumulated). Liquidity can be considered as virtual liquidity tokens.
Similarly, liquidity can also be regarded as the proportion of the change in the number of token1 (
Note: Based on equation 6.6, suppose at moments
$t_0$ and$t_1$ , the corresponding$y_0$ and$y_1$ are:
$y_0 = L \cdot \sqrt{P_0}$
$y_1 = L \cdot \sqrt{P_1}$ Thus:
$y_1 - y_0 = L \cdot (\sqrt{P_1} - \sqrt{P_0})$ We can get the equation 6.7:
$$ L = \frac{y_1 - y_0}{\sqrt{P_1} - \sqrt{P_0}} = \frac{\Delta{Y}}{\Delta{\sqrt{P}}} $$
We record
The global state records the current tick index as
Note: According to equation 6.2:
$\sqrt{p}(i) = \sqrt{1.0001}^i$ ,Thus:
$i = \log_{\sqrt{1.0001}} \sqrt{P}$ Since
$i$ is an integer, it needs to be rounded down.
Each trading pair pool is initialized with an immutable transaction fee (
Note: The default fee values are 500, 3000, and 10000, representing fees of 500 x 0.0001% = 0.05%, 3000 x 0.0001% = 0.30%, and 1000 x 0.0001% = 1%, respectively.
Another variable, the protocol fee (
Note: The protocol fee switch cannot be automatically turned on when creating the pair; it must be executed by UNI governance for specific pools and can be set differently for different pools.
The global state also records two values: feeGrowthGlobal0 (
Lastly, the global state records cumulative unclaimed protocol fees for each token: protocolFees0 (
For small trades that do not cause the price to cross a tick, the contract operates like an
Suppose
First, feeGrowthGlobal1 and protocolFees1 increase by:
Note:
$\phi$ is the protocol fee as a percentage of the transaction fee, so the protocol fee proportion is:$\gamma \cdot \phi$ , with the protocol fee income given by equation 6.10.The remaining fees are distributed to liquidity providers, which is the transaction fee minus the protocol fee, with its proportion being:
$\gamma \cdot (1 - \phi)$ , and the transaction fee income given by equation 6.9.
Using the virtual balances (
Note: Because within a tick, the trade conforms to the
$k$ constant function, i.e.,
$$x_{end} \cdot y_{end} = x \cdot y$$ Thus, it can be deduced:
$$x_{end} = \frac{x \cdot y}{y_{end}} = \frac{x \cdot y}{y + \Delta{y}}$$
However, please note, in v3, the contract uses liquidity (
Similarly, we can derive the relationship between
Note: According to equation 6.5, suppose at moments
$t_0$ and$t_1$ , the corresponding$x_0$ and$x_1$ are:
$x_0 = \frac{L}{\sqrt{P_0}}$
$x_1 = \frac{L}{\sqrt{P_1}}$ Thus:
$x_1 - x_0 = L \cdot (\frac{1}{\sqrt{P_1}} - \frac{1}{\sqrt{P_0}})$ Rearranging gives equation 6.16:
$$ \Delta{x} = L \cdot \Delta{\frac{1}{\sqrt{P}}} $$
When exchanging one token for another, the trading pair contract can first calculate the new square root price
For any trade, as long as the square root price
Ticks that are not used as boundaries for liquidity positions (i.e., not initialized) can be skipped during trades.
To efficiently find the next initialized tick, the contract uses a bitmap, tickBitmap, to record initialized ticks. If a tick is initialized, the bit corresponding to that tick index in the bitmap is set to 1; otherwise, it is 0.
When a new position uses a tick as a boundary and it is not used by any other liquidity, it is initialized, and the corresponding bit in the bitmap is set to 1. When all liquidity associated with that tick is removed, the initialized tick reverts to uninitialized, and the corresponding bit in the bitmap is set to 0.
To keep track of the total liquidity to be added and removed when a tick is fully crossed and the fees earned while above and below that tick, the contract needs to store additional information for each tick.
The contract maintains a mapping from each tick index to the following seven variables:
Each tick records
We want to be able to uninitialize a tick when there is no longer any liquidity referencing that tick. In addition to the net liquidity change
feeGrowthOutside{0, 1} tracks the total fees accumulated outside a given range. Since the formulas for collecting fees for token0 and token1 are the same, we will omit the (token0 and token1) subscript in the remaining formulas in this section.
Depending on whether the current price is inside the range, you can use one formula to calculate the fees accumulated above (
Note: First, let's recall what each variable means.
$f_g$ is the global accumulated fees per liquidity;$f_o(i)$ is the accumulated fees outside a specified tick$i$ , and it's important to note that this value changes its meaning of direction with the current tick$i_c$ .When
$i_c < i$ ,
$$ \underbrace{\overbrace{i_c, ..., i - 1}^{f_b(i) = f_g - f_o(i)}, i, \overbrace{i + 1, ...}^{f_a(i)=f_o(i)}}_{f_g} $$ When
$i_c \geq i$ ,
$$ \underbrace{\overbrace{..., i - 1}^{f_b(i) = f_o(i)}, i, \overbrace{i + 1, ..., i_c}^{f_a(i)=f_g - f_o(i)}}_{f_g} $$
Using the above functions, we can calculate the total fees accumulated for every liquidity unit between two ticks (the lower tick
Note: According to the above derivation, we can diagram several fee relationships as follows:
$$ \underbrace{\overbrace{..., i_l - 1}^{f_b(i_l)}, \overbrace{i_l, i_l + 1, ..., i_u - 1, i_u}^{f_r}, \overbrace{i_u + 1, ...}^{f_a(i_u)}}_{f_g} $$
Only ticks used as boundary points by at least one position need
Note that since
Lastly, the contract also saves for each tick secondsOutside (
These three variables are similar to the fee growth variables mentioned above. However, unlike feeGrowthOutside{0, 1} tracking feeGrowthGlobal{0, 1}, secondsOutside tracks the current timestamp, secondsPerLiquidityOutside tracks the cumulative count of
For example, for a given tick, depending on whether the current price is inside the range,
The duration a position is within a price range from
Like
Similar to
As described in section 6.2.3, when trading between initialized ticks, Uniswap v3 can operate like a
To record the fees accumulated inside the range while the tick serves as a boundary point during price movement (and the duration), the contract needs to update the tick's state. feeGrowthOutside{0, 1} and secondsOutside are updated to reflect current values when the direction of trade associated with the tick changes, updated as follows:
After a tick is crossed, as described in section 6.2.3, the trade continues until it encounters the next initialized tick.
The contract maintains a mapping from the combination of user address, lower tick (left boundary, a tick index of type int24), and upper tick (right boundary, a tick index of type int24) to the details of a specific position. Each position records three values:
liquidity (
The liquidity amount does not represent the fees accumulated since the last interaction with the contract; uncollected fees are used for this purpose. To calculate uncollected fees for a position, additional information is stored in the position, such as feeGrowthInside0Last (
The setPosition method allows liquidity providers to update their positions.
The setPosition parameters, lowerTick and upperTick, combined with the caller msg.sender, form the identifier for the position.
An additional parameter, liquidityDelta, specifies the amount of virtual liquidity the user wants to add (or remove, if negative).
First, the method calculates the position's uncollected fees (
To calculate the uncollected fees for a token, it is necessary to know how much fee income
Next, the method adds liquidityDelta to the position's liquidity. At the lower tick, it also adds liquidityDelta to liquidityNet (indicating adding liquidity as the tick moves from left to right); at the position's upper tick, it subtracts liquidityDelta from liquidityNet (indicating removing liquidity as the tick moves from right to left). If the pool's current price is within the position's range, the contract also adds the liquidity to the global liquidity.
Finally, depending on the amount of liquidity destroyed or created, the pool transfers tokens from the user (if liquidityDelta is negative, it transfers tokens to the user).
The amount of token0 (
- [1] Hayden Adams, Noah Zinsmeister, and Dan Robinson. 2020. Uniswap v2 Core. Retrieved Feb 24, 2021 from https://uniswap.org/whitepaper.pdf
- [2] Guillermo Angeris and Tarun Chitra. 2020. Improved Price Oracles: Constant Function Market Makers. In Proceedings of the 2nd ACM Conference on Advances in Financial Technologies (AFT ’20). Association for Computing Machinery, New York,NY,UnitedStates, 80–91. https://doi.org/10.1145/3419614.3423251
- [3] Michael Egorov. 2019. StableSwap - Efficient Mechanism for Stablecoin Liquidity. Retrieved Feb 24, 2021 from https://www.curve.fi/stableswap-paper.pdf
- [4] Allan Niemerg, Dan Robinson, and Lev Livnev. 2020. YieldSpace: An Automated Liquidity Provider for Fixed Yield Tokens. Retrieved Feb 24, 2021 from https://yield.is/YieldSpace.pdf
- [5] Abraham Othman. 2012. Automated Market Making: Theory and Practice. Ph.D. Dissertation. Carnegie Mellon University.