Livre blanc Uniswap v3
Overview
Uniswap v3 Core March 2021 Hayden Adams [email protected] Noah Zinsmeister [email protected] Moody Salem [email protected] River Keefer [email protected] Dan Robinson [email protected] ABSTRACT Uniswap v3 is a noncustodial automated market maker imple- mented for the Ethereum Virtual Machine. In comparison to earlier versions of the protocol, Uniswap v3 provides increased capital efficiency and fine-tuned control to liquidity providers, improves the accuracy and convenience of the price oracle, and has a more flexible fee structure. 1 INTRODUCTION Automated market makers (AMMs) are agents that pool liquidity and make it available to traders according to an algorithm [5]. Con- stant function market makers (CFMMs), a broad class of AMMs of which Uniswap is a member, have seen widespread use in the con- text of decentralized finance, where they are typically implemented as smart contracts that trade tokens on a permissionless blockchain [2]. CFMMs as they are implemented today are often capital inef- ficient. In the constant product market maker formula used by Uniswap v1 and v2, only a fraction of the assets in the pool are available at a given price. This is inefficient, particularly when assets are expected to trade close to a particular price at all times. Prior attempts to address this capital efficiency issue, such as Curve [3] and YieldSpace [4], have involved building pools that use different functions to describe the relation between reserves. This requires all liquidity providers in a given pool to adhere to a single formula, and could result in liquidity fragmentation if liquidity providers want to provide liquidity within different price ranges. In this paper, we present Uniswap v3, a novel AMM that gives liquidity providers more control over the price ranges in which their capital is used, with limited effect on liquidity fragmentation and gas inefficiency. This design does not depend on any shared assumption about the price behavior of the tokens. Uniswap v3 is based on the same constant product reserves curve as earlier versions [1], but offers several significant new features: โข Concentrated Liquidity: Liquidity providers (LPs) are given the ability to concentrate their liquidity by โbounding" it within an arbitrary price range. This improves the poolโs capital efficiency and allows LPs to approximate their pre- ferred reserves curve, while still being efficiently aggregated with the rest of the pool. We describe this feature in section 2 and its implementation in Section 6. โข Flexible Fees : The swap fee is no longer locked at 0.30%. Rather, the fee tier for each pool (of which there can be multiple per asset pair) is set on initialization (Section 3.1). The initially supported fee tiers are 0.05%, 0.30%, and 1%. UNI governance is able to add additional values to this set. โข Protocol Fee Governance: UNI governance has more flexibility in setting the fraction of swap fees collected by the protocol (Section 6.2.2). โข Improved Price Oracle: Uniswap v3 provides a way for users to query recent price accumulator values, thus avoiding the need to checkpoint the accumulator value at the exact be- ginning and end of the period for which a TWAP is being measured. (Section 5.1). 1
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson โข Liquidity Oracle: The contracts expose a time-weighted av- erage liquidity oracle (Section 5.3). The Uniswap v2 core contracts are non-upgradeable by de- sign, so Uniswap v3 is implemented as an entirely new set of contracts, available here. The Uniswap v3 core contracts are also non-upgradeable, with some parameters controlled by governance as described in Section 4. 2 CONCENTRATED LIQUIDITY The defining idea of Uniswap v3 is that of concentrated liquidity: liquidity bounded within some price range. In earlier versions, liquidity was distributed uniformly along the ๐ฅ ยท๐ฆ = ๐ (2.1) reserves curve, where ๐ฅ and ๐ฆ are the respective reserves of two assets X and Y, and ๐ is a constant [1]. In other words, earlier ver- sions were designed to provide liquidity across the entire price range (0, โ). This is simple to implement and allows liquidity to be efficiently aggregated, but means that much of the assets held in a pool are never touched. Having considered this, it seems reasonable to allow LPs to concentrate their liquidity to smaller price ranges than (0, โ). We call liquidity concentrated to a finite range a position. A position only needs to maintain enough reserves to support trading within its range, and therefore can act like a constant product pool with larger reserves (we call these the virtual reserves) within that range. ๐ ๐ ๐ ๐ฆreal ๐ฅreal X Reserves Y Reserves virtual reserves Figure 1: Simulation of Virtual Liquidity Specifically, a position only needs to hold enough of asset X to cover price movement to its upper bound, because upwards price movement1 corresponds to depletion of the X reserves. Similarly, it only needs to hold enough of asset Y to cover price movement to its lower bound. Fig. 1 depicts this relationship for a position on a range [๐๐, ๐๐ ] and a current price ๐๐ โ [ ๐๐, ๐๐ ]. ๐ฅreal and ๐ฆreal denote the positionโs real reserves. When the price exits a positionโs range, the positionโs liquidity is no longer active, and no longer earns fees. At that point, its 1We take asset Y to be the unit of account, which corresponds to token1 in our implementation. liquidity is composed entirely of a single asset, because the reserves of the other asset must have been entirely depleted. If the price ever reenters the range, the liquidity becomes active again. The amount of liquidity provided can be measured by the value ๐ฟ, which is equal to โ ๐. The real reserves of a position are described by the curve: (๐ฅ + ๐ฟโ๐๐ ) (๐ฆ + ๐ฟโ๐๐) = ๐ฟ2 (2.2) This curve is a translation of formula 2.1 such that the position is solvent exactly within its range (Fig. 2). ๐ ๐ X Reserves Y Reserves virtual reserves (2.1) real reserves (2.2) Figure 2: Real Reserves Liquidity providers are free to create as many positions as they see fit, each on its own price range. In this way, LPs can approximate any desired distribution of liquidity on the price space (see Fig. 3 for a few examples). Moreover, this serves as a mechanism to let the market decide where liquidity should be allocated. Rational LPs can reduce their capital costs by concentrating their liquidity in a narrow band around the current price, and adding or removing tokens as the price moves to keep their liquidity active. 2.1 Range Orders Positions on very small ranges act similarly to limit ordersโif the range is crossed, the position flips from being composed entirely of one asset, to being composed entirely of the other asset (plus accrued fees). There are two differences between this range order and a traditional limit order: โข There is a limit to how narrow a positionโs range can be. While the price is within that range, the limit order might be partially executed. โข When the position has been crossed, it needs to be with- drawn. If it is not, and the price crosses back across that range, the position will be traded back, effectively reversing the trade. 2
Uniswap v3 Core 0 โ Price Liquidity (I) Uniswap v2 ๐๐ ๐๐ Price Liquidity (II) A single position on [๐๐, ๐๐ ] Price Liquidity (III) A collection of custom positions Figure 3: Example Liquidity Distributions 3 ARCHITECTURAL CHANGES Uniswap v3 makes a number of architectural changes, some of which are necessitated by the inclusion of concentrated liquidity, and some of which are independent improvements. 3.1 Multiple Pools Per Pair In Uniswap v1 and v2, every pair of tokens corresponds to a single liquidity pool, which applies a uniform fee of 0.30% to all swaps. While this default fee tier historically worked well enough for many tokens, it is likely too high for some pools (such as pools between two stablecoins), and too low for others (such as pools that include highly volatile or rarely traded tokens). Uniswap v3 introduces multiple pools for each pair of tokens, each with a different swap fee. All pools are created by the same factory contract. The factory contract initially allows pools to be created at three fee tiers: 0.05%, 0.30%, and 1%. Additional fee tiers can be enabled by UNI governance. 3.2 Non-Fungible Liquidity 3.2.1 Non-Compounding Fees. Fees earned in earlier versions were continuously deposited in the pool as liquidity. This meant that liquidity in the pool would grow over time, even without explicit deposits, and that fee earnings compounded. In Uniswap v3, due to the non-fungible nature of positions, this is no longer possible. Instead, fee earnings are stored separately and held as the tokens in which the fees are paid (see Section 6.2.2). 3.2.2 Removal of Native Liquidity Tokens. In Uniswap v1 and v2, the pool contract is also an ERC-20 token contract, whose tokens represent liquidity held in the pool. While this is convenient, it actually sits uneasily with the Uniswap v2 philosophy that any- thing that does not need to be in the core contracts should be in the periphery, and blessing one โcanonical" ERC-20 implementation discourages the creation of improved ERC-20 token wrappers. Ar- guably, the ERC-20 token implementation should have been in the periphery, as a wrapper on a single liquidity position in the core contract. The changes made in Uniswap v3 force this issue by making completely fungible liquidity tokens impossible. Due to the custom liquidity provision feature, fees are now collected and held by the pool as individual tokens, rather than automatically reinvested as liquidity in the pool. As a result, in v3, the pool contract does not implement the ERC-20 standard. Anyone can create an ERC-20 token contract in the periphery that makes a liquidity position more fungible, but it will have to have additional logic to handle distribution of, or reinvestment of, collected fees. Alternatively, anyone could create a periphery contract that wraps an individual liquidity position (including collected fees) in an ERC-721 non-fungible token. 4 GOVERNANCE The factory has an owner, which is initially controlled by UNI tokenholders.2 The owner does not have the ability to halt the operation of any of the core contracts. As in Uniswap v2, Uniswap v3 has a protocol fee that can be turned on by UNI governance. In Uniswap v3, UNI governance has more flexibility in choosing the fraction of swap fees that go to the protocol, and is able to choose any fraction 1 ๐ where 4 โค ๐ โค 10, or 0. This parameter can be set on a per-pool basis. UNI governance also has the ability to add additional fee tiers. When it adds a new fee tier, it can also define the tickSpacing (see Section 6.1) corresponding to that fee tier. Once a fee tier is added to the factory, it cannot be removed (and the tickSpacing cannot be changed). The initial fee tiers and tick spacings supported are 0.05% (with a tick spacing of 10, approximately 0.10% between initializable ticks), 0.30% (with a tick spacing of 60, approximately 0.60% between initializable ticks), and 1% (with a tick spacing of 200, approximately 2.02% between ticks. Finally, UNI governance has the power to transfer ownership to another address. 5 ORACLE UPGRADES Uniswap v3includes three significant changes to the time-weighted average price (TWAP) oracle that was introduced by Uniswap v2. Most significantly, Uniswap v3 removes the need for users of the oracle to track previous values of the accumulator externally. Uniswap v2 requires users to checkpoint the accumulator value at both the beginning and end of the time period for which they 2Specifically, the owner will be initialized to the Timelock contract from UNI gover- nance, 0x1a9c8182c09f50c8318d769245bea52c32be35bc. 3
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson wanted to compute a TWAP. Uniswap v3 brings the accumulator checkpoints into core, allowing external contracts to compute on- chain TWAPs over recent periods without storing checkpoints of the accumulator value. Another change is that instead of accumulating the sum of prices, allowing users to compute the arithmetic mean TWAP,Uniswap v3 tracks the sum of log prices, allowing users to compute the geometric mean TWAP. Finally, Uniswap v3 adds a liquidity accumulator that is tracked alongside the price accumulator, which accumulates 1 ๐ฟ for each second. This liquidity accumulator is useful for external contracts that want to implement liquidity mining on top of Uniswap v3. It can also be used by other contracts to inform a decision on which of the pools corresponding to a pair (see section 3.1) will have the most reliable TWAP. 5.1 Oracle Observations As in Uniswap v2, Uniswap v3 tracks a running accumulator of the price at the beginning of each block, multiplied by the number of seconds since the last block. A pool in Uniswap v2 stores only the most recent value of this price accumulatorโthat is, the value as of the last block in which a swap occurred. When computing average prices in Uniswap v2, it is the responsibility of the external caller to provide the previous value of the price accumulator. With many users, each will have to provide their own methodology for checkpointing previous values of the accumulator, or coordinate on a shared method to reduce costs. And there is no way to guarantee that every block in which the pool is touched will be reflected in the accumulator. In Uniswap v3, the pool stores a list of previous values for the price accumulator (as well as the liquidity accumulator described in section 5.3). It does this by automatically checkpointing the accumulator value every time the pool is touched for the first time in a block, cycling through an array where the oldest checkpoint is eventually overwritten by a new one, similar to a circular buffer. While this array initially only has room for a single checkpoint, anyone can initialize additional storage slots to lengthen the array, extending to as many as 65,536 checkpoints. 3 This imposes the one-time gas cost of initializing additional storage slots for this array on whoever wants this pair to checkpoint more slots. The pool exposes the array of past observations to users, as well as a convenience function for finding the (interpolated) accumulator value at any historical timestamp within the checkpointed period. 5.2 Geometric Mean Price Oracle Uniswap v2 maintains two price accumulatorsโone for the price of token0 in terms of token1, and one for the price oftoken1 in terms of token0. Users can compute the time-weighted arithmetic mean of the prices over any period, by subtracting the accumulator value at the beginning of the period from the accumulator at the end of the period, then dividing the difference by the number of seconds in the period. Note that accumulators for token0 and token1 are tracked separately, since the time-weighted arithmetic mean price 3The maximum of 65,536 checkpoints allows fetching checkpoints for at least 9 days after they are written, assuming 13 seconds pass between each block and a checkpoint is written every block. of token0 is not equivalent to the reciprocal of the time-weighted arithmetic mean price of token1. Using the time-weighted geometric mean price, as Uniswap v3 does, avoids the need to track separate accumulators for these ratios. The geometric mean of a set of ratios is the reciprocal of the geometric mean of their reciprocals. It is also easy to implement in Uniswap v3 because of its implementation of custom liquidity provision, as described in section 6. In addition, the accumulator can be stored in a smaller number of bits, since it trackslog ๐ rather than ๐, and log ๐ can represent a wide range of prices with consistent precision.4 Finally, there is a theoretical argument that the time- weighted geometric mean price should be a truer representation of the average price.5 Instead of tracking the cumulative sum of the price ๐, Uniswap v3 accumulates the cumulative sum of the current tick index (๐๐๐1.0001๐, the logarithm of price for base 1.0001, which is precise up to 1 basis point). The accumulator at any given time is equal to the sum of ๐๐๐1.0001 (๐) for every second in the history of the contract: ๐๐ก = ๐กร ๐=1 log1.0001 (๐๐ ) (5.1) We want to estimate the geometric mean time-weighted average price (๐๐ก1,๐ก2) over any period ๐ก1 to ๐ก2. ๐๐ก1,๐ก2 = ยฉยญ ยซ ๐ก2ร ๐=๐ก1 ๐๐ ยชยฎ ยฌ 1 ๐ก2โ๐ก1 (5.2) To compute this, you can look at the accumulatorโs value at๐ก1 and at ๐ก2, subtract the first value from the second, divide by the number of seconds elapsed, and compute 1.0001๐ฅ to compute the time weighted geometric mean price. log1.0001 ๐๐ก1,๐ก2 = ร๐ก2 ๐=๐ก1 log1.0001 (๐๐ ) ๐ก2 โ ๐ก1 (5.3) log1.0001 ๐๐ก1,๐ก2 = ๐๐ก2 โ ๐๐ก1 ๐ก2 โ ๐ก1 (5.4) ๐๐ก1,๐ก2 = 1.0001 ๐๐ก2 โ๐๐ก1 ๐ก2โ๐ก1 (5.5) 5.3 Liquidity Oracle In addition to the seconds-weighted accumulator of log1.0001 ๐๐๐๐๐ , Uniswap v3 also tracks a seconds-weighted accumulator of 1 ๐ฟ (the reciprocal of the virtual liquidity currently in range) at the begin- ning of each block: secondsPerLiquidityCumulative (๐ ๐๐ ). This can be used by external liquidity mining contracts to fairly allocate rewards. If an external contract wants to distribute rewards at an even rate of ๐ tokens per second to all active liquidity in the 4In order to support tolerable precision across all possible prices, Uniswap v2 repre- sents each price as a 224-bit fixed-point number. Uniswap v3 only needs to represent ๐๐๐ 1.0001๐ as a signed 24-bit number, and still can detect price movements of one tick, or 1 basis point. 5While arithmetic mean TWAPs are much more widely used, they should theoretically be less accurate in measuring a geometric Brownian motion process (which is how price movements are usually modeled). The arithmetic mean of a geometric Brownian motion process will tend to overweight higher prices (where small percentage movements correspond to large absolute movements) relative to lower ones. 4
Uniswap v3 Core contract, and a position with ๐ฟ liquidity was active from ๐ก0 to ๐ก1, then its rewards for that period would be ๐ ยทLยท(๐ ๐๐ (๐ก1) โ ๐ ๐๐ (๐ก0)). In order to extend this so that concentrated liquidity is rewarded only when it is in range,Uniswap v3 stores a computed checkpoint based on this value every time a tick is crossed, as described in section 6.3. This accumulator can also be used by on-chain contracts to make their oracles stronger (such as by evaluating which fee-tier pool to use the oracle from). 6 IMPLEMENTING CONCENTRATED LIQUIDITY The rest of this paper describes how concentrated liquidity provi- sion works, and gives a high-level description of how it is imple- mented in the contracts. 6.1 Ticks and Ranges To implement custom liquidity provision, the space of possible prices is demarcated by discrete ticks. Liquidity providers can pro- vide liquidity in a range between any two ticks (which need not be adjacent). Each range can be specified as a pair of signed integertick indices: a lower tick (๐๐ ) and an upper tick ( ๐๐ข). Ticks represent prices at which the virtual liquidity of the contract can change. We will assume that prices are always expressed as the price of one of the tokensโcalled token0โin terms of the other tokenโ token1. The assignment of the two tokens to token0 and token1 is arbitrary and does not affect the logic of the contract (other than through possible rounding errors). Conceptually, there is a tick at every price ๐ that is an integer power of 1.0001. Identifying ticks by an integer index ๐, the price at each is given by: ๐ (๐) = 1.0001๐ (6.1) This has the desirable property of each tick being a .01% (1 basis point) price movement away from each of its neighboring ticks. For technical reasons explained in 6.2.1, however, pools actually track ticks at every square root price that is an integer power ofโ 1.0001. Consider the above equation, transformed into square root price space: โ๐ (๐) = โ 1.0001 ๐ = 1.0001 ๐ 2 (6.2) As an example,โ๐ (0)โthe square root price at tick 0โis 1, โ๐ (1) is โ 1.0001 โ 1.00005, and โ๐ (โ1) is 1โ 1.0001 โ 0.99995. When liquidity is added to a range, if one or both of the ticks is not already used as a bound in an existing position, that tick is initialized. Not every tick can be initialized. The pool is instantiated with a parameter, tickSpacing (๐ก๐ ); only ticks with indexes that are divisi- ble by tickSpacing can be initialized. For example, iftickSpacing is 2, then only even ticks (...-4, -2, 0, 2, 4...) can be initialized. Small choices for tickSpacing allow tighter and more precise ranges, but may cause swaps to be more gas-intensive (since each initialized tick that a swap crosses imposes a gas cost on the swapper). Whenever the price crosses an initialized tick, virtual liquidity is kicked in or out. The gas cost of an initialized tick crossing is constant, and is not dependent on the number of positions being kicked in or out at that tick. Ensuring that the right amount of liquidity is kicked in and out of the pool when ticks are crossed, and ensuring that each position earns its proportional share of the fees that were accrued while it was within range, requires some accounting within the pool. The pool contract uses storage variables to track state at a global (per-pool) level, at a per-tick level, and at a per-position level. 6.2 Global State The global state of the contract includes seven storage variables relevant to swaps and liquidity provision. (It has other storage variables that are used for the oracle, as described in section 5.) Type Variable Name Notation uint128 liquidity ๐ฟ uint160 sqrtPriceX96 โ ๐ int24 tick ๐๐ uint256 feeGrowthGlobal0X128 ๐๐,0 uint256 feeGrowthGlobal1X128 ๐๐,1 uint128 protocolFees.token0 ๐๐,0 uint128 protocolFees.token1 ๐๐,1 Table 1: Global State 6.2.1 Price and Liquidity. In Uniswap v2, each pool contract tracks the poolโs current reserves,๐ฅ and ๐ฆ. In Uniswap v3, the contract could be thought of as having virtual reservesโvalues for ๐ฅ and ๐ฆ that allow you to describe the contractโs behavior (between two adjacent ticks) as if it followed the constant product formula. Instead of tracking those virtual reserves, however, the pool contract tracks two different values:liquidity (๐ฟ) and sqrtPrice ( โ ๐). These could be computed from the virtual reserves with the following formulas: ๐ฟ = โ๐ฅ๐ฆ (6.3) โ ๐ = r๐ฆ ๐ฅ (6.4) Conversely, these values could be used to compute the virtual reserves: ๐ฅ = ๐ฟโ ๐ (6.5) ๐ฆ = ๐ฟ ยท โ ๐ (6.6) Using ๐ฟ and โ ๐ is convenient because only one of them changes at a time. Price (and thus โ ๐) changes when swapping within a tick; liquidity changes when crossing a tick, or when minting or burning liquidity. This avoids some rounding errors that could be encountered if tracking virtual reserves. You may notice that the formula for liquidity (based on virtual reserves) is similar to the formula used to initialize the quantity of liquidity tokens (based on actual reserves) in Uniswap v2. before 5
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson any fees have been earned. In some ways, liquidity can be thought of as virtual liquidity tokens. Alternatively, liquidity can be thought of as the amount that token1 reserves (either actual or virtual) changes for a given change in โ ๐: ๐ฟ = ฮ๐ ฮ โ ๐ (6.7) We track โ ๐ instead of ๐ to take advantage of this relationship, and to avoid having to take any square roots when computing swaps, as described in section 6.2.3. The global state also tracks the current tick index as tick (๐๐), a signed integer representing the current tick (more specifically, the nearest tick below the current price). This is an optimization (and a way of avoiding precision issues with logarithms), since at any time, you should be able to compute the current tick based on the current sqrtPrice. Specifically, at any given time, the following equation should be true: ๐๐ = j logโ 1.0001 โ ๐ k (6.8) 6.2.2 Fees. Each pool is initialized with an immutable value, fee (๐พ), representing the fee paid by swappers in units of hundredths of a basis point (0.0001%). It also tracks the current protocol fee, ๐ (which is initialized to zero, but can changed by UNI governance).6 This number gives you the fraction of the fees paid by swappers that currently goes to the protocol rather than to liquidity providers. ๐ only has a limited set of permitted values: 0, 1/4, 1/5, 1/6, 1/7, 1/8, 1/9, or 1/10. The global state also tracks two numbers: feeGrowthGlobal0 (๐๐,0) andfeeGrowthGlobal1 (๐๐,1). These represent the total amount of fees that have been earned per unit of virtual liquidity (๐ฟ), over the entire history of the contract. You can think of them as the total amount of fees that would have been earned by 1 unit of unbounded liquidity that was deposited when the contract was first initialized. They are stored as fixed-point unsigned 128x128 numbers. Note that in Uniswap v3, fees are collected in the tokens themselves rather than in liquidity, for reasons explained in section 3.2.1. Finally, the global state tracks the total accumulated uncollected protocol fee in each token,protocolFees0 (๐๐,0) andprotocolFees1 (๐๐,1). This is an unsigned uint128. The accumulated protocol fees can be collected by UNI governance, by calling thecollectProtocol function. 6.2.3 Swapping Within a Single Tick. For small enough swaps, that do not move the price past a tick, the contracts act like an ๐ฅ ยท๐ฆ = ๐ pool. Suppose ๐พ is the fee, i.e., 0.003, and ๐ฆ๐๐ as the amount of token1 sent in. First, feeGrowthGlobal1 and protocolFees1 are incremented: ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท (1 โ ๐) (6.9) ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท๐ (6.10) ฮ๐ฆ is the increase in ๐ฆ (after the fee is taken out). 6Technically, the storage variable called โprotocolFee" is the denominator of this fraction (or is zero, if ๐ is zero). ฮ๐ฆ = ๐ฆ๐๐ ยท (1 โ ๐พ) (6.11) If you used the computed virtual reserves (๐ฅ and ๐ฆ) for thetoken0 and token1 balances, then this formula could be used to find the amount of token0 sent out: ๐ฅ๐๐๐ = ๐ฅ ยท๐ฆ ๐ฆ + ฮ๐ฆ (6.12) But remember that inv3, the contract actually tracks liquidity (๐ฟ) and square root of price ( โ ๐) instead of ๐ฅ and ๐ฆ. We could compute ๐ฅ and ๐ฆ from those values, and then use those to calculate the execution price of the trade. But it turns out that there are simple formulas that describe the relationship between ฮ โ ๐ and ฮ๐ฆ, for a given ๐ฟ (which can be derived from formula 6.7): ฮ โ ๐ = ฮ๐ฆ ๐ฟ (6.13) ฮ๐ฆ = ฮ โ ๐ ยท๐ฟ (6.14) There are also simple formulas that describe the relationship between ฮ 1โ ๐ and ฮ๐ฅ: ฮ 1โ ๐ = ฮ๐ฅ ๐ฟ (6.15) ฮ๐ฅ = ฮ 1โ ๐ ยท๐ฟ (6.16) When swapping one token for the other, the pool contract can first compute the new โ ๐ using formula 6.13 or 6.15, and then can compute the amount of token0 or token1 to send out using formula 6.14 or 6.16. These formulas will work for any swap that does not push โ ๐ past the price of the next initialized tick. If the computed ฮ โ ๐ would cause โ ๐ to move past that next initialized tick, the contract must only cross up to that tickโusing up only part of the swapโand then cross the tick, as described in section 6.3.1, before continuing with the rest of the swap. 6.2.4 Initialized Tick Bitmap. If a tick is not used as the endpoint of a range with any liquidity in itโthat is, if the tick is uninitial- izedโthen that tick can be skipped during swaps. As an optimization to make finding the next initialized tick more efficient, the pool tracks a bitmap tickBitmap of initialized ticks. The position in the bitmap that corresponds to the tick index is set to 1 if the tick is initialized, and 0 if it is not initialized. When a tick is used as an endpoint for a new position, and that tick is not currently used by any other liquidity, the tick is initialized, and the corresponding bit in the bitmap is set to 1. An initialized tick can become uninitialized again if all of the liquidity for which it is an endpoint is removed, in which case that tickโs position on the bitmap is zeroed out. 6.3 Tick-Indexed State The contract needs to store information about each tick in order to track the amount of net liquidity that should be added or removed when the tick is crossed, as well as to track the fees earned above and below that tick. 6
Uniswap v3 Core Start S0. Check input S1. Swap within current interval S2. Is there remaining input or output? S4. Cross next tick Stop S5. Execute computed swap Pass Fail Yes No Figure 4: Swap Control Flow The contract stores a mapping from tick indexes (int24) to the following seven values: Type Variable Name Notation int128 liquidityNet ฮ๐ฟ uint128 liquidityGross ๐ฟ๐ uint256 feeGrowthOutside0X128 ๐๐,0 uint256 feeGrowthOutside1X128 ๐๐,1 uint256 secondsOutside ๐ ๐ uint256 tickCumulativeOutside ๐๐ uint256 secondsPerLiquidityOutsideX128 ๐ ๐๐ Table 2: Tick-Indexed State Each tick tracks ฮ๐ฟ, the total amount of liquidity that should be kicked in or out when the tick is crossed. The tick only needs to track one signed integer: the amount of liquidity added (or, if negative, removed) when the tick is crossed going left to right. This value does not need to be updated when the tick is crossed (but only when a position with a bound at that tick is updated). We want to be able to uninitialize a tick when there is no longer any liquidity referencing that tick. Since ฮ๐ฟ is a net value, itโs necessary to track a gross tally of liquidity referencing the tick, liquidityGross. This value ensures that even if net liquidity at a tick is 0, we can still know if a tick is referenced by at least one underlying position or not, which tells us whether to update the tick bitmap. feeGrowthOutside{0,1} are used to track how many fees were accumulated within a given range. Since the formulas are the same for the fees collected in token0 and token1, we will omit that sub- script for the rest of this section. You can compute the fees earned per unit of liquidity in token 0 above (๐๐) and below (๐๐) a tick ๐ with a formula that depends on whether the price is currently within or outside that rangeโthat is, whether the current tick index ๐๐ is greater than or equal to ๐: ๐๐ (๐) = ( ๐๐ โ ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ (๐) ๐๐ < ๐ (6.17) ๐๐ (๐) = ( ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ โ ๐๐ (๐) ๐๐ < ๐ (6.18) We can use these functions to compute the total amount of cumulative fees per share ๐๐ in the range between two ticksโa lower tick ๐๐ and an upper tick ๐๐ข: ๐๐ = ๐๐ โ ๐๐ (๐๐ ) โ ๐๐ (๐๐ข ) (6.19) ๐๐ needs to be updated each time the tick is crossed. Specifically, as a tick๐ is crossed in either direction, its๐๐ (for each token) should be updated as follows: ๐๐ (๐) := ๐๐ โ ๐๐ (๐) (6.20) ๐๐ is only needed for ticks that are used as either the lower or upper bound for at least one position. As a result, for efficiency,๐๐ is not initialized (and thus does not need to be updated when crossed) until a position is created that has that tick as one of its bounds. When ๐๐ is initialized for a tick ๐, the valueโby conventionโis chosen as if all of the fees earned to date had occurred below that tick: ๐๐ := ( ๐๐ ๐๐ โฅ ๐ 0 ๐๐ < ๐ (6.21) Note that since ๐๐ values for different ticks could be initialized at different times, comparisons of the ๐๐ values for different ticks are not meaningful, and there is no guarantee that values for ๐๐ will be consistent. This does not cause a problem for per-position accounting, since, as described below, all the position needs to know is the growth in ๐ within a given range since that position was last touched. 7
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson Finally, the contract also stores secondsOutside (๐ ๐), secondsPerLiquidityOutside, andtickCumulativeOutside for each tick. These values are not used within the contract, but are tracked for the benefit of external contracts that need more fine- grained information about the poolโs behavior (for purposes like liquidity mining). All three of these indexes work similarly to the fee growth in- dexes described above. But where the feeGrowthOutside{0,1} indexes track feeGrowthGlobal{0,1}, the secondsOutside index tracks seconds (that is, the current timestamp), secondsPerLiquidityOutside tracks the 1/๐ฟ accumulator (secondsPerLiquidityCumulative) described in section 5.3, and tickCumulativeOutside tracks the log1.0001 ๐ accumulator de- scribed in section 5.2. For example, the seconds spent above (๐ ๐) and below (๐ ๐) a given tick is computed differently based on whether the current price is within that range, and the seconds spent within a range (๐ ๐ ) can be computed using the values of ๐ ๐ and ๐ ๐: ๐ก๐ (๐) = ( ๐ก โ ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก๐ (๐) ๐๐ < ๐ (6.22) ๐ก๐ (๐) = ( ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก โ ๐ก๐ (๐) ๐๐ < ๐ (6.23) ๐ก๐ (๐๐ , ๐๐ข ) = ๐ก โ ๐ก๐ (๐๐ ) โ ๐ก๐ (๐๐ข ) (6.24) The number of seconds spent within a range between two times ๐ก1 and ๐ก2 can be computed by recording the value of ๐ ๐ (๐๐ , ๐๐ข ) at ๐ก1 and at ๐ก2, and subtracting the former from the latter. Like ๐๐, ๐ ๐ does not need to be tracked for ticks that are not on the edge of any position. Therefore, it is not initialized until a position is created that is bounded by that tick. By convention, it is initialized as if every second since the Unix timestamp 0 had been spent below that tick: ๐ก๐ (๐) := ( ๐ก ๐ ๐ โฅ ๐ 0 ๐๐ < ๐ (6.25) As with ๐๐ values, ๐ก๐ values are not meaningfully comparable across different ticks. ๐ก๐ is only meaningful in computing the num- ber of seconds that liquidity was within some particular range between some defined start time (which must be after ๐ก๐ was ini- tialized for both ticks) and some end time. 6.3.1 Crossing a Tick. As described in section 6.2.3, Uniswap v3 acts like it obeys the constant product formula when swapping between initialized ticks. When a swap crosses an initialized tick, however, the contract needs to add or remove liquidity, to ensure that no liquidity provider is insolvent. This means theฮ๐ฟ is fetched from the tick, and applied to the global ๐ฟ. The contract also needs to update the tickโs own state, in order to track the fees earned (and seconds spent) within ranges bounded by this tick. The feeGrowthOutside{0,1} and secondsOutside values are updated to both reflect current values, as well as the proper orientation relative to the current tick: ๐๐ := ๐๐ โ ๐๐ (6.26) ๐ก๐ := ๐ก โ ๐ก๐ (6.27) Once a tick is crossed, the swap can continue as described in section 6.2.3 until it reaches the next initialized tick. 6.4 Position-Indexed State The contract has a mapping from user (an address), lower bound (a tick index, int24), and upper bound (a tick index, int24) to a specific Position struct. Each Position tracks three values: Type Variable Name Notation uint128 liquidity ๐ uint256 feeGrowthInside0LastX128 ๐๐,0 (๐ก0) uint256 feeGrowthInside1LastX128 ๐๐,1 (๐ก0) Table 3: Position-Indexed State liquidity (๐) means the amount of virtual liquidity that the position represented the last time this position was touched. Specif- ically, liquidity could be thought of as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are the respective amounts of virtual token0 and virtual token1 that this liquidity contributes to the pool at any time that it is within range. Unlike pool shares in Uniswap v2 (where the value of each share grows over time), the units for liquidity do not change as fees are accumulated; it is always measured as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are quantities of token0 and token1, respectively. This liquidity number does not reflect the fees that have been accumulated since the contract was last touched, which we will call uncollected fees . Computing these uncollected fees requires additional stored values on the position, feeGrowthInside0Last (๐๐,0 (๐ก0)) and feeGrowthInside1Last (๐๐,1 (๐ก0)), as described be- low. 6.4.1 setPosition. The setPosition function allows a liquidity provider to update their position. Two of the arguments tosetPosition โ lowerTick and upperTickโ when combined with the msg.sender, together specify a position. The function takes one additional parameter, liquidityDelta, to specify how much virtual liquidity the user wants to add or (if negative) remove. First, the function computes the uncollected fees ( ๐๐ข) that the position is entitled to, in each token.7 The amount collected in fees is credited to the user and netted against the amount that they would send in or out for their virtual liquidity deposit. To compute uncollected fees of a token, you need to know how much ๐๐ for the positionโs range (calculated from the rangeโs ๐๐ and ๐๐ as described in section 6.3) has grown since the last time fees were collected for that position. The growth in fees in a given range per unit of liquidity over between times ๐ก0 and ๐ก1 is simply ๐๐ (๐ก1) โ ๐๐ (๐ก0) (where ๐๐ (๐ก0) is stored in the position as feeGrowthInside{0,1}Last, and ๐๐ (๐ก1) can be computed from the current state of the ticks). Multiplying this by the positionโs liquidity gives us the total uncollected fees in token 0 for this position: 7Since the formulas for computing uncollected fees in each token are the same, we will omit that subscript for the rest of this section. 8
Uniswap v3 Core ๐๐ข = ๐ ยท (๐๐ (๐ก1) โ ๐๐ (๐ก0)) (6.28) Then, the contract updates the positionโsliquidity by adding liquidityDelta. It also addsliquidityDelta to theliquidityNet value for the tick at the bottom end of the range, and subtracts it from the liquidityNet at the upper tick (to reflect that this new liquidity would be added when the price crosses the lower tick going up, and subtracted when the price crosses the upper tick going up). If the poolโs current price is within the range of this position, the contract also adds liquidityDelta to the contractโs global liquidity value. Finally, the pool transfers tokens from (or, if liquidityDelta is negative, to) the user, corresponding to the amount of liquidity burned or minted. The amount of token0 (ฮ๐ ) or token1 (ฮ๐ ) that needs to be deposited can be thought of as the amount that would be sold from the position if the price were to move from the current price (๐) to the upper tick or lower tick (for token0 or token1, respectively). These formulas can be derived from formulas 6.14 and 6.16, and depend on whether the current price is below, within, or above the range of the position: ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ 0 ๐๐ < ๐๐ ฮ๐ฟ ยท ( โ ๐ โ p ๐ (๐๐ )) ๐๐ โค ๐๐ < ๐๐ข ฮ๐ฟ ยท ( p ๐ (๐๐ข ) โ p ๐ (๐๐ )) ๐๐ โฅ ๐๐ข (6.29) ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ ฮ๐ฟ ยท ( 1โ ๐ (๐๐ ) โ 1โ ๐ (๐๐ข ) ) ๐๐ < ๐๐ ฮ๐ฟ ยท ( 1โ ๐ โ 1โ ๐ (๐๐ข ) ) ๐๐ โค ๐๐ < ๐๐ข 0 ๐๐ โฅ ๐๐ข (6.30) REFERENCES [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, United States, 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. DISCLAIMER This paper is for general information purposes only. It does not constitute investment advice or a recommendation or solicitation to buy or sell any investment and should not be used in the evaluation of the merits of making any investment decision. It should not be relied upon for accounting, legal or tax advice or investment rec- ommendations. This paper reflects current opinions of the authors and is not made on behalf of Uniswap Labs, Paradigm, or their affiliates and does not necessarily reflect the opinions of Uniswap Labs, Paradigm, their affiliates or individuals associated with them. The opinions reflected herein are subject to change without being updated. 9
Uniswap v3 Core March 2021 Hayden Adams [email protected] Noah Zinsmeister [email protected] Moody Salem [email protected] River Keefer [email protected] Dan Robinson [email protected] ABSTRACT Uniswap v3 is a noncustodial automated market maker imple- mented for the Ethereum Virtual Machine. In comparison to earlier versions of the protocol, Uniswap v3 provides increased capital efficiency and fine-tuned control to liquidity providers, improves the accuracy and convenience of the price oracle, and has a more flexible fee structure. 1 INTRODUCTION Automated market makers (AMMs) are agents that pool liquidity and make it available to traders according to an algorithm [5]. Con- stant function market makers (CFMMs), a broad class of AMMs of which Uniswap is a member, have seen widespread use in the con- text of decentralized finance, where they are typically implemented as smart contracts that trade tokens on a permissionless blockchain [2]. CFMMs as they are implemented today are often capital inef- ficient. In the constant product market maker formula used by Uniswap v1 and v2, only a fraction of the assets in the pool are available at a given price. This is inefficient, particularly when assets are expected to trade close to a particular price at all times. Prior attempts to address this capital efficiency issue, such as Curve [3] and YieldSpace [4], have involved building pools that use different functions to describe the relation between reserves. This requires all liquidity providers in a given pool to adhere to a single formula, and could result in liquidity fragmentation if liquidity providers want to provide liquidity within different price ranges. In this paper, we present Uniswap v3, a novel AMM that gives liquidity providers more control over the price ranges in which their capital is used, with limited effect on liquidity fragmentation and gas inefficiency. This design does not depend on any shared assumption about the price behavior of the tokens. Uniswap v3 is based on the same constant product reserves curve as earlier versions [1], but offers several significant new features: โข Concentrated Liquidity: Liquidity providers (LPs) are given the ability to concentrate their liquidity by โbounding" it within an arbitrary price range. This improves the poolโs capital efficiency and allows LPs to approximate their pre- ferred reserves curve, while still being efficiently aggregated with the rest of the pool. We describe this feature in section 2 and its implementation in Section 6. โข Flexible Fees : The swap fee is no longer locked at 0.30%. Rather, the fee tier for each pool (of which there can be multiple per asset pair) is set on initialization (Section 3.1). The initially supported fee tiers are 0.05%, 0.30%, and 1%. UNI governance is able to add additional values to this set. โข Protocol Fee Governance: UNI governance has more flexibility in setting the fraction of swap fees collected by the protocol (Section 6.2.2). โข Improved Price Oracle: Uniswap v3 provides a way for users to query recent price accumulator values, thus avoiding the need to checkpoint the accumulator value at the exact be- ginning and end of the period for which a TWAP is being measured. (Section 5.1). 1
Protocol Design
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson โข Liquidity Oracle: The contracts expose a time-weighted av- erage liquidity oracle (Section 5.3). The Uniswap v2 core contracts are non-upgradeable by de- sign, so Uniswap v3 is implemented as an entirely new set of contracts, available here. The Uniswap v3 core contracts are also non-upgradeable, with some parameters controlled by governance as described in Section 4. 2 CONCENTRATED LIQUIDITY The defining idea of Uniswap v3 is that of concentrated liquidity: liquidity bounded within some price range. In earlier versions, liquidity was distributed uniformly along the ๐ฅ ยท๐ฆ = ๐ (2.1) reserves curve, where ๐ฅ and ๐ฆ are the respective reserves of two assets X and Y, and ๐ is a constant [1]. In other words, earlier ver- sions were designed to provide liquidity across the entire price range (0, โ). This is simple to implement and allows liquidity to be efficiently aggregated, but means that much of the assets held in a pool are never touched. Having considered this, it seems reasonable to allow LPs to concentrate their liquidity to smaller price ranges than (0, โ). We call liquidity concentrated to a finite range a position. A position only needs to maintain enough reserves to support trading within its range, and therefore can act like a constant product pool with larger reserves (we call these the virtual reserves) within that range. ๐ ๐ ๐ ๐ฆreal ๐ฅreal X Reserves Y Reserves virtual reserves Figure 1: Simulation of Virtual Liquidity Specifically, a position only needs to hold enough of asset X to cover price movement to its upper bound, because upwards price movement1 corresponds to depletion of the X reserves. Similarly, it only needs to hold enough of asset Y to cover price movement to its lower bound. Fig. 1 depicts this relationship for a position on a range [๐๐, ๐๐ ] and a current price ๐๐ โ [ ๐๐, ๐๐ ]. ๐ฅreal and ๐ฆreal denote the positionโs real reserves. When the price exits a positionโs range, the positionโs liquidity is no longer active, and no longer earns fees. At that point, its 1We take asset Y to be the unit of account, which corresponds to token1 in our implementation. liquidity is composed entirely of a single asset, because the reserves of the other asset must have been entirely depleted. If the price ever reenters the range, the liquidity becomes active again. The amount of liquidity provided can be measured by the value ๐ฟ, which is equal to โ ๐. The real reserves of a position are described by the curve: (๐ฅ + ๐ฟโ๐๐ ) (๐ฆ + ๐ฟโ๐๐) = ๐ฟ2 (2.2) This curve is a translation of formula 2.1 such that the position is solvent exactly within its range (Fig. 2). ๐ ๐ X Reserves Y Reserves virtual reserves (2.1) real reserves (2.2) Figure 2: Real Reserves Liquidity providers are free to create as many positions as they see fit, each on its own price range. In this way, LPs can approximate any desired distribution of liquidity on the price space (see Fig. 3 for a few examples). Moreover, this serves as a mechanism to let the market decide where liquidity should be allocated. Rational LPs can reduce their capital costs by concentrating their liquidity in a narrow band around the current price, and adding or removing tokens as the price moves to keep their liquidity active. 2.1 Range Orders Positions on very small ranges act similarly to limit ordersโif the range is crossed, the position flips from being composed entirely of one asset, to being composed entirely of the other asset (plus accrued fees). There are two differences between this range order and a traditional limit order: โข There is a limit to how narrow a positionโs range can be. While the price is within that range, the limit order might be partially executed. โข When the position has been crossed, it needs to be with- drawn. If it is not, and the price crosses back across that range, the position will be traded back, effectively reversing the trade. 2
Uniswap v3 Core 0 โ Price Liquidity (I) Uniswap v2 ๐๐ ๐๐ Price Liquidity (II) A single position on [๐๐, ๐๐ ] Price Liquidity (III) A collection of custom positions Figure 3: Example Liquidity Distributions 3 ARCHITECTURAL CHANGES Uniswap v3 makes a number of architectural changes, some of which are necessitated by the inclusion of concentrated liquidity, and some of which are independent improvements. 3.1 Multiple Pools Per Pair In Uniswap v1 and v2, every pair of tokens corresponds to a single liquidity pool, which applies a uniform fee of 0.30% to all swaps. While this default fee tier historically worked well enough for many tokens, it is likely too high for some pools (such as pools between two stablecoins), and too low for others (such as pools that include highly volatile or rarely traded tokens). Uniswap v3 introduces multiple pools for each pair of tokens, each with a different swap fee. All pools are created by the same factory contract. The factory contract initially allows pools to be created at three fee tiers: 0.05%, 0.30%, and 1%. Additional fee tiers can be enabled by UNI governance. 3.2 Non-Fungible Liquidity 3.2.1 Non-Compounding Fees. Fees earned in earlier versions were continuously deposited in the pool as liquidity. This meant that liquidity in the pool would grow over time, even without explicit deposits, and that fee earnings compounded. In Uniswap v3, due to the non-fungible nature of positions, this is no longer possible. Instead, fee earnings are stored separately and held as the tokens in which the fees are paid (see Section 6.2.2). 3.2.2 Removal of Native Liquidity Tokens. In Uniswap v1 and v2, the pool contract is also an ERC-20 token contract, whose tokens represent liquidity held in the pool. While this is convenient, it actually sits uneasily with the Uniswap v2 philosophy that any- thing that does not need to be in the core contracts should be in the periphery, and blessing one โcanonical" ERC-20 implementation discourages the creation of improved ERC-20 token wrappers. Ar- guably, the ERC-20 token implementation should have been in the periphery, as a wrapper on a single liquidity position in the core contract. The changes made in Uniswap v3 force this issue by making completely fungible liquidity tokens impossible. Due to the custom liquidity provision feature, fees are now collected and held by the pool as individual tokens, rather than automatically reinvested as liquidity in the pool. As a result, in v3, the pool contract does not implement the ERC-20 standard. Anyone can create an ERC-20 token contract in the periphery that makes a liquidity position more fungible, but it will have to have additional logic to handle distribution of, or reinvestment of, collected fees. Alternatively, anyone could create a periphery contract that wraps an individual liquidity position (including collected fees) in an ERC-721 non-fungible token. 4 GOVERNANCE The factory has an owner, which is initially controlled by UNI tokenholders.2 The owner does not have the ability to halt the operation of any of the core contracts. As in Uniswap v2, Uniswap v3 has a protocol fee that can be turned on by UNI governance. In Uniswap v3, UNI governance has more flexibility in choosing the fraction of swap fees that go to the protocol, and is able to choose any fraction 1 ๐ where 4 โค ๐ โค 10, or 0. This parameter can be set on a per-pool basis. UNI governance also has the ability to add additional fee tiers. When it adds a new fee tier, it can also define the tickSpacing (see Section 6.1) corresponding to that fee tier. Once a fee tier is added to the factory, it cannot be removed (and the tickSpacing cannot be changed). The initial fee tiers and tick spacings supported are 0.05% (with a tick spacing of 10, approximately 0.10% between initializable ticks), 0.30% (with a tick spacing of 60, approximately 0.60% between initializable ticks), and 1% (with a tick spacing of 200, approximately 2.02% between ticks. Finally, UNI governance has the power to transfer ownership to another address. 5 ORACLE UPGRADES Uniswap v3includes three significant changes to the time-weighted average price (TWAP) oracle that was introduced by Uniswap v2. Most significantly, Uniswap v3 removes the need for users of the oracle to track previous values of the accumulator externally. Uniswap v2 requires users to checkpoint the accumulator value at both the beginning and end of the time period for which they 2Specifically, the owner will be initialized to the Timelock contract from UNI gover- nance, 0x1a9c8182c09f50c8318d769245bea52c32be35bc. 3
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson wanted to compute a TWAP. Uniswap v3 brings the accumulator checkpoints into core, allowing external contracts to compute on- chain TWAPs over recent periods without storing checkpoints of the accumulator value. Another change is that instead of accumulating the sum of prices, allowing users to compute the arithmetic mean TWAP,Uniswap v3 tracks the sum of log prices, allowing users to compute the geometric mean TWAP. Finally, Uniswap v3 adds a liquidity accumulator that is tracked alongside the price accumulator, which accumulates 1 ๐ฟ for each second. This liquidity accumulator is useful for external contracts that want to implement liquidity mining on top of Uniswap v3. It can also be used by other contracts to inform a decision on which of the pools corresponding to a pair (see section 3.1) will have the most reliable TWAP. 5.1 Oracle Observations As in Uniswap v2, Uniswap v3 tracks a running accumulator of the price at the beginning of each block, multiplied by the number of seconds since the last block. A pool in Uniswap v2 stores only the most recent value of this price accumulatorโthat is, the value as of the last block in which a swap occurred. When computing average prices in Uniswap v2, it is the responsibility of the external caller to provide the previous value of the price accumulator. With many users, each will have to provide their own methodology for checkpointing previous values of the accumulator, or coordinate on a shared method to reduce costs. And there is no way to guarantee that every block in which the pool is touched will be reflected in the accumulator. In Uniswap v3, the pool stores a list of previous values for the price accumulator (as well as the liquidity accumulator described in section 5.3). It does this by automatically checkpointing the accumulator value every time the pool is touched for the first time in a block, cycling through an array where the oldest checkpoint is eventually overwritten by a new one, similar to a circular buffer. While this array initially only has room for a single checkpoint, anyone can initialize additional storage slots to lengthen the array, extending to as many as 65,536 checkpoints. 3 This imposes the one-time gas cost of initializing additional storage slots for this array on whoever wants this pair to checkpoint more slots. The pool exposes the array of past observations to users, as well as a convenience function for finding the (interpolated) accumulator value at any historical timestamp within the checkpointed period. 5.2 Geometric Mean Price Oracle Uniswap v2 maintains two price accumulatorsโone for the price of token0 in terms of token1, and one for the price oftoken1 in terms of token0. Users can compute the time-weighted arithmetic mean of the prices over any period, by subtracting the accumulator value at the beginning of the period from the accumulator at the end of the period, then dividing the difference by the number of seconds in the period. Note that accumulators for token0 and token1 are tracked separately, since the time-weighted arithmetic mean price 3The maximum of 65,536 checkpoints allows fetching checkpoints for at least 9 days after they are written, assuming 13 seconds pass between each block and a checkpoint is written every block. of token0 is not equivalent to the reciprocal of the time-weighted arithmetic mean price of token1. Using the time-weighted geometric mean price, as Uniswap v3 does, avoids the need to track separate accumulators for these ratios. The geometric mean of a set of ratios is the reciprocal of the geometric mean of their reciprocals. It is also easy to implement in Uniswap v3 because of its implementation of custom liquidity provision, as described in section 6. In addition, the accumulator can be stored in a smaller number of bits, since it trackslog ๐ rather than ๐, and log ๐ can represent a wide range of prices with consistent precision.4 Finally, there is a theoretical argument that the time- weighted geometric mean price should be a truer representation of the average price.5 Instead of tracking the cumulative sum of the price ๐, Uniswap v3 accumulates the cumulative sum of the current tick index (๐๐๐1.0001๐, the logarithm of price for base 1.0001, which is precise up to 1 basis point). The accumulator at any given time is equal to the sum of ๐๐๐1.0001 (๐) for every second in the history of the contract: ๐๐ก = ๐กร ๐=1 log1.0001 (๐๐ ) (5.1) We want to estimate the geometric mean time-weighted average price (๐๐ก1,๐ก2) over any period ๐ก1 to ๐ก2. ๐๐ก1,๐ก2 = ยฉยญ ยซ ๐ก2ร ๐=๐ก1 ๐๐ ยชยฎ ยฌ 1 ๐ก2โ๐ก1 (5.2) To compute this, you can look at the accumulatorโs value at๐ก1 and at ๐ก2, subtract the first value from the second, divide by the number of seconds elapsed, and compute 1.0001๐ฅ to compute the time weighted geometric mean price. log1.0001 ๐๐ก1,๐ก2 = ร๐ก2 ๐=๐ก1 log1.0001 (๐๐ ) ๐ก2 โ ๐ก1 (5.3) log1.0001 ๐๐ก1,๐ก2 = ๐๐ก2 โ ๐๐ก1 ๐ก2 โ ๐ก1 (5.4) ๐๐ก1,๐ก2 = 1.0001 ๐๐ก2 โ๐๐ก1 ๐ก2โ๐ก1 (5.5) 5.3 Liquidity Oracle In addition to the seconds-weighted accumulator of log1.0001 ๐๐๐๐๐ , Uniswap v3 also tracks a seconds-weighted accumulator of 1 ๐ฟ (the reciprocal of the virtual liquidity currently in range) at the begin- ning of each block: secondsPerLiquidityCumulative (๐ ๐๐ ). This can be used by external liquidity mining contracts to fairly allocate rewards. If an external contract wants to distribute rewards at an even rate of ๐ tokens per second to all active liquidity in the 4In order to support tolerable precision across all possible prices, Uniswap v2 repre- sents each price as a 224-bit fixed-point number. Uniswap v3 only needs to represent ๐๐๐ 1.0001๐ as a signed 24-bit number, and still can detect price movements of one tick, or 1 basis point. 5While arithmetic mean TWAPs are much more widely used, they should theoretically be less accurate in measuring a geometric Brownian motion process (which is how price movements are usually modeled). The arithmetic mean of a geometric Brownian motion process will tend to overweight higher prices (where small percentage movements correspond to large absolute movements) relative to lower ones. 4
Uniswap v3 Core contract, and a position with ๐ฟ liquidity was active from ๐ก0 to ๐ก1, then its rewards for that period would be ๐ ยทLยท(๐ ๐๐ (๐ก1) โ ๐ ๐๐ (๐ก0)). In order to extend this so that concentrated liquidity is rewarded only when it is in range,Uniswap v3 stores a computed checkpoint based on this value every time a tick is crossed, as described in section 6.3. This accumulator can also be used by on-chain contracts to make their oracles stronger (such as by evaluating which fee-tier pool to use the oracle from). 6 IMPLEMENTING CONCENTRATED LIQUIDITY The rest of this paper describes how concentrated liquidity provi- sion works, and gives a high-level description of how it is imple- mented in the contracts. 6.1 Ticks and Ranges To implement custom liquidity provision, the space of possible prices is demarcated by discrete ticks. Liquidity providers can pro- vide liquidity in a range between any two ticks (which need not be adjacent). Each range can be specified as a pair of signed integertick indices: a lower tick (๐๐ ) and an upper tick ( ๐๐ข). Ticks represent prices at which the virtual liquidity of the contract can change. We will assume that prices are always expressed as the price of one of the tokensโcalled token0โin terms of the other tokenโ token1. The assignment of the two tokens to token0 and token1 is arbitrary and does not affect the logic of the contract (other than through possible rounding errors). Conceptually, there is a tick at every price ๐ that is an integer power of 1.0001. Identifying ticks by an integer index ๐, the price at each is given by: ๐ (๐) = 1.0001๐ (6.1) This has the desirable property of each tick being a .01% (1 basis point) price movement away from each of its neighboring ticks. For technical reasons explained in 6.2.1, however, pools actually track ticks at every square root price that is an integer power ofโ 1.0001. Consider the above equation, transformed into square root price space: โ๐ (๐) = โ 1.0001 ๐ = 1.0001 ๐ 2 (6.2) As an example,โ๐ (0)โthe square root price at tick 0โis 1, โ๐ (1) is โ 1.0001 โ 1.00005, and โ๐ (โ1) is 1โ 1.0001 โ 0.99995. When liquidity is added to a range, if one or both of the ticks is not already used as a bound in an existing position, that tick is initialized. Not every tick can be initialized. The pool is instantiated with a parameter, tickSpacing (๐ก๐ ); only ticks with indexes that are divisi- ble by tickSpacing can be initialized. For example, iftickSpacing is 2, then only even ticks (...-4, -2, 0, 2, 4...) can be initialized. Small choices for tickSpacing allow tighter and more precise ranges, but may cause swaps to be more gas-intensive (since each initialized tick that a swap crosses imposes a gas cost on the swapper). Whenever the price crosses an initialized tick, virtual liquidity is kicked in or out. The gas cost of an initialized tick crossing is constant, and is not dependent on the number of positions being kicked in or out at that tick. Ensuring that the right amount of liquidity is kicked in and out of the pool when ticks are crossed, and ensuring that each position earns its proportional share of the fees that were accrued while it was within range, requires some accounting within the pool. The pool contract uses storage variables to track state at a global (per-pool) level, at a per-tick level, and at a per-position level. 6.2 Global State The global state of the contract includes seven storage variables relevant to swaps and liquidity provision. (It has other storage variables that are used for the oracle, as described in section 5.) Type Variable Name Notation uint128 liquidity ๐ฟ uint160 sqrtPriceX96 โ ๐ int24 tick ๐๐ uint256 feeGrowthGlobal0X128 ๐๐,0 uint256 feeGrowthGlobal1X128 ๐๐,1 uint128 protocolFees.token0 ๐๐,0 uint128 protocolFees.token1 ๐๐,1 Table 1: Global State 6.2.1 Price and Liquidity. In Uniswap v2, each pool contract tracks the poolโs current reserves,๐ฅ and ๐ฆ. In Uniswap v3, the contract could be thought of as having virtual reservesโvalues for ๐ฅ and ๐ฆ that allow you to describe the contractโs behavior (between two adjacent ticks) as if it followed the constant product formula. Instead of tracking those virtual reserves, however, the pool contract tracks two different values:liquidity (๐ฟ) and sqrtPrice ( โ ๐). These could be computed from the virtual reserves with the following formulas: ๐ฟ = โ๐ฅ๐ฆ (6.3) โ ๐ = r๐ฆ ๐ฅ (6.4) Conversely, these values could be used to compute the virtual reserves: ๐ฅ = ๐ฟโ ๐ (6.5) ๐ฆ = ๐ฟ ยท โ ๐ (6.6) Using ๐ฟ and โ ๐ is convenient because only one of them changes at a time. Price (and thus โ ๐) changes when swapping within a tick; liquidity changes when crossing a tick, or when minting or burning liquidity. This avoids some rounding errors that could be encountered if tracking virtual reserves. You may notice that the formula for liquidity (based on virtual reserves) is similar to the formula used to initialize the quantity of liquidity tokens (based on actual reserves) in Uniswap v2. before 5
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson any fees have been earned. In some ways, liquidity can be thought of as virtual liquidity tokens. Alternatively, liquidity can be thought of as the amount that token1 reserves (either actual or virtual) changes for a given change in โ ๐: ๐ฟ = ฮ๐ ฮ โ ๐ (6.7) We track โ ๐ instead of ๐ to take advantage of this relationship, and to avoid having to take any square roots when computing swaps, as described in section 6.2.3. The global state also tracks the current tick index as tick (๐๐), a signed integer representing the current tick (more specifically, the nearest tick below the current price). This is an optimization (and a way of avoiding precision issues with logarithms), since at any time, you should be able to compute the current tick based on the current sqrtPrice. Specifically, at any given time, the following equation should be true: ๐๐ = j logโ 1.0001 โ ๐ k (6.8) 6.2.2 Fees. Each pool is initialized with an immutable value, fee (๐พ), representing the fee paid by swappers in units of hundredths of a basis point (0.0001%). It also tracks the current protocol fee, ๐ (which is initialized to zero, but can changed by UNI governance).6 This number gives you the fraction of the fees paid by swappers that currently goes to the protocol rather than to liquidity providers. ๐ only has a limited set of permitted values: 0, 1/4, 1/5, 1/6, 1/7, 1/8, 1/9, or 1/10. The global state also tracks two numbers: feeGrowthGlobal0 (๐๐,0) andfeeGrowthGlobal1 (๐๐,1). These represent the total amount of fees that have been earned per unit of virtual liquidity (๐ฟ), over the entire history of the contract. You can think of them as the total amount of fees that would have been earned by 1 unit of unbounded liquidity that was deposited when the contract was first initialized. They are stored as fixed-point unsigned 128x128 numbers. Note that in Uniswap v3, fees are collected in the tokens themselves rather than in liquidity, for reasons explained in section 3.2.1. Finally, the global state tracks the total accumulated uncollected protocol fee in each token,protocolFees0 (๐๐,0) andprotocolFees1 (๐๐,1). This is an unsigned uint128. The accumulated protocol fees can be collected by UNI governance, by calling thecollectProtocol function. 6.2.3 Swapping Within a Single Tick. For small enough swaps, that do not move the price past a tick, the contracts act like an ๐ฅ ยท๐ฆ = ๐ pool. Suppose ๐พ is the fee, i.e., 0.003, and ๐ฆ๐๐ as the amount of token1 sent in. First, feeGrowthGlobal1 and protocolFees1 are incremented: ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท (1 โ ๐) (6.9) ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท๐ (6.10) ฮ๐ฆ is the increase in ๐ฆ (after the fee is taken out). 6Technically, the storage variable called โprotocolFee" is the denominator of this fraction (or is zero, if ๐ is zero). ฮ๐ฆ = ๐ฆ๐๐ ยท (1 โ ๐พ) (6.11) If you used the computed virtual reserves (๐ฅ and ๐ฆ) for thetoken0 and token1 balances, then this formula could be used to find the amount of token0 sent out: ๐ฅ๐๐๐ = ๐ฅ ยท๐ฆ ๐ฆ + ฮ๐ฆ (6.12) But remember that inv3, the contract actually tracks liquidity (๐ฟ) and square root of price ( โ ๐) instead of ๐ฅ and ๐ฆ. We could compute ๐ฅ and ๐ฆ from those values, and then use those to calculate the execution price of the trade. But it turns out that there are simple formulas that describe the relationship between ฮ โ ๐ and ฮ๐ฆ, for a given ๐ฟ (which can be derived from formula 6.7): ฮ โ ๐ = ฮ๐ฆ ๐ฟ (6.13) ฮ๐ฆ = ฮ โ ๐ ยท๐ฟ (6.14) There are also simple formulas that describe the relationship between ฮ 1โ ๐ and ฮ๐ฅ: ฮ 1โ ๐ = ฮ๐ฅ ๐ฟ (6.15) ฮ๐ฅ = ฮ 1โ ๐ ยท๐ฟ (6.16) When swapping one token for the other, the pool contract can first compute the new โ ๐ using formula 6.13 or 6.15, and then can compute the amount of token0 or token1 to send out using formula 6.14 or 6.16. These formulas will work for any swap that does not push โ ๐ past the price of the next initialized tick. If the computed ฮ โ ๐ would cause โ ๐ to move past that next initialized tick, the contract must only cross up to that tickโusing up only part of the swapโand then cross the tick, as described in section 6.3.1, before continuing with the rest of the swap. 6.2.4 Initialized Tick Bitmap. If a tick is not used as the endpoint of a range with any liquidity in itโthat is, if the tick is uninitial- izedโthen that tick can be skipped during swaps. As an optimization to make finding the next initialized tick more efficient, the pool tracks a bitmap tickBitmap of initialized ticks. The position in the bitmap that corresponds to the tick index is set to 1 if the tick is initialized, and 0 if it is not initialized. When a tick is used as an endpoint for a new position, and that tick is not currently used by any other liquidity, the tick is initialized, and the corresponding bit in the bitmap is set to 1. An initialized tick can become uninitialized again if all of the liquidity for which it is an endpoint is removed, in which case that tickโs position on the bitmap is zeroed out. 6.3 Tick-Indexed State The contract needs to store information about each tick in order to track the amount of net liquidity that should be added or removed when the tick is crossed, as well as to track the fees earned above and below that tick. 6
Uniswap v3 Core Start S0. Check input S1. Swap within current interval S2. Is there remaining input or output? S4. Cross next tick Stop S5. Execute computed swap Pass Fail Yes No Figure 4: Swap Control Flow The contract stores a mapping from tick indexes (int24) to the following seven values: Type Variable Name Notation int128 liquidityNet ฮ๐ฟ uint128 liquidityGross ๐ฟ๐ uint256 feeGrowthOutside0X128 ๐๐,0 uint256 feeGrowthOutside1X128 ๐๐,1 uint256 secondsOutside ๐ ๐ uint256 tickCumulativeOutside ๐๐ uint256 secondsPerLiquidityOutsideX128 ๐ ๐๐ Table 2: Tick-Indexed State Each tick tracks ฮ๐ฟ, the total amount of liquidity that should be kicked in or out when the tick is crossed. The tick only needs to track one signed integer: the amount of liquidity added (or, if negative, removed) when the tick is crossed going left to right. This value does not need to be updated when the tick is crossed (but only when a position with a bound at that tick is updated). We want to be able to uninitialize a tick when there is no longer any liquidity referencing that tick. Since ฮ๐ฟ is a net value, itโs necessary to track a gross tally of liquidity referencing the tick, liquidityGross. This value ensures that even if net liquidity at a tick is 0, we can still know if a tick is referenced by at least one underlying position or not, which tells us whether to update the tick bitmap. feeGrowthOutside{0,1} are used to track how many fees were accumulated within a given range. Since the formulas are the same for the fees collected in token0 and token1, we will omit that sub- script for the rest of this section. You can compute the fees earned per unit of liquidity in token 0 above (๐๐) and below (๐๐) a tick ๐ with a formula that depends on whether the price is currently within or outside that rangeโthat is, whether the current tick index ๐๐ is greater than or equal to ๐: ๐๐ (๐) = ( ๐๐ โ ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ (๐) ๐๐ < ๐ (6.17) ๐๐ (๐) = ( ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ โ ๐๐ (๐) ๐๐ < ๐ (6.18) We can use these functions to compute the total amount of cumulative fees per share ๐๐ in the range between two ticksโa lower tick ๐๐ and an upper tick ๐๐ข: ๐๐ = ๐๐ โ ๐๐ (๐๐ ) โ ๐๐ (๐๐ข ) (6.19) ๐๐ needs to be updated each time the tick is crossed. Specifically, as a tick๐ is crossed in either direction, its๐๐ (for each token) should be updated as follows: ๐๐ (๐) := ๐๐ โ ๐๐ (๐) (6.20) ๐๐ is only needed for ticks that are used as either the lower or upper bound for at least one position. As a result, for efficiency,๐๐ is not initialized (and thus does not need to be updated when crossed) until a position is created that has that tick as one of its bounds. When ๐๐ is initialized for a tick ๐, the valueโby conventionโis chosen as if all of the fees earned to date had occurred below that tick: ๐๐ := ( ๐๐ ๐๐ โฅ ๐ 0 ๐๐ < ๐ (6.21) Note that since ๐๐ values for different ticks could be initialized at different times, comparisons of the ๐๐ values for different ticks are not meaningful, and there is no guarantee that values for ๐๐ will be consistent. This does not cause a problem for per-position accounting, since, as described below, all the position needs to know is the growth in ๐ within a given range since that position was last touched. 7
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson Finally, the contract also stores secondsOutside (๐ ๐), secondsPerLiquidityOutside, andtickCumulativeOutside for each tick. These values are not used within the contract, but are tracked for the benefit of external contracts that need more fine- grained information about the poolโs behavior (for purposes like liquidity mining). All three of these indexes work similarly to the fee growth in- dexes described above. But where the feeGrowthOutside{0,1} indexes track feeGrowthGlobal{0,1}, the secondsOutside index tracks seconds (that is, the current timestamp), secondsPerLiquidityOutside tracks the 1/๐ฟ accumulator (secondsPerLiquidityCumulative) described in section 5.3, and tickCumulativeOutside tracks the log1.0001 ๐ accumulator de- scribed in section 5.2. For example, the seconds spent above (๐ ๐) and below (๐ ๐) a given tick is computed differently based on whether the current price is within that range, and the seconds spent within a range (๐ ๐ ) can be computed using the values of ๐ ๐ and ๐ ๐: ๐ก๐ (๐) = ( ๐ก โ ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก๐ (๐) ๐๐ < ๐ (6.22) ๐ก๐ (๐) = ( ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก โ ๐ก๐ (๐) ๐๐ < ๐ (6.23) ๐ก๐ (๐๐ , ๐๐ข ) = ๐ก โ ๐ก๐ (๐๐ ) โ ๐ก๐ (๐๐ข ) (6.24) The number of seconds spent within a range between two times ๐ก1 and ๐ก2 can be computed by recording the value of ๐ ๐ (๐๐ , ๐๐ข ) at ๐ก1 and at ๐ก2, and subtracting the former from the latter. Like ๐๐, ๐ ๐ does not need to be tracked for ticks that are not on the edge of any position. Therefore, it is not initialized until a position is created that is bounded by that tick. By convention, it is initialized as if every second since the Unix timestamp 0 had been spent below that tick: ๐ก๐ (๐) := ( ๐ก ๐ ๐ โฅ ๐ 0 ๐๐ < ๐ (6.25) As with ๐๐ values, ๐ก๐ values are not meaningfully comparable across different ticks. ๐ก๐ is only meaningful in computing the num- ber of seconds that liquidity was within some particular range between some defined start time (which must be after ๐ก๐ was ini- tialized for both ticks) and some end time. 6.3.1 Crossing a Tick. As described in section 6.2.3, Uniswap v3 acts like it obeys the constant product formula when swapping between initialized ticks. When a swap crosses an initialized tick, however, the contract needs to add or remove liquidity, to ensure that no liquidity provider is insolvent. This means theฮ๐ฟ is fetched from the tick, and applied to the global ๐ฟ. The contract also needs to update the tickโs own state, in order to track the fees earned (and seconds spent) within ranges bounded by this tick. The feeGrowthOutside{0,1} and secondsOutside values are updated to both reflect current values, as well as the proper orientation relative to the current tick: ๐๐ := ๐๐ โ ๐๐ (6.26) ๐ก๐ := ๐ก โ ๐ก๐ (6.27) Once a tick is crossed, the swap can continue as described in section 6.2.3 until it reaches the next initialized tick. 6.4 Position-Indexed State The contract has a mapping from user (an address), lower bound (a tick index, int24), and upper bound (a tick index, int24) to a specific Position struct. Each Position tracks three values: Type Variable Name Notation uint128 liquidity ๐ uint256 feeGrowthInside0LastX128 ๐๐,0 (๐ก0) uint256 feeGrowthInside1LastX128 ๐๐,1 (๐ก0) Table 3: Position-Indexed State liquidity (๐) means the amount of virtual liquidity that the position represented the last time this position was touched. Specif- ically, liquidity could be thought of as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are the respective amounts of virtual token0 and virtual token1 that this liquidity contributes to the pool at any time that it is within range. Unlike pool shares in Uniswap v2 (where the value of each share grows over time), the units for liquidity do not change as fees are accumulated; it is always measured as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are quantities of token0 and token1, respectively. This liquidity number does not reflect the fees that have been accumulated since the contract was last touched, which we will call uncollected fees . Computing these uncollected fees requires additional stored values on the position, feeGrowthInside0Last (๐๐,0 (๐ก0)) and feeGrowthInside1Last (๐๐,1 (๐ก0)), as described be- low. 6.4.1 setPosition. The setPosition function allows a liquidity provider to update their position. Two of the arguments tosetPosition โ lowerTick and upperTickโ when combined with the msg.sender, together specify a position. The function takes one additional parameter, liquidityDelta, to specify how much virtual liquidity the user wants to add or (if negative) remove. First, the function computes the uncollected fees ( ๐๐ข) that the position is entitled to, in each token.7 The amount collected in fees is credited to the user and netted against the amount that they would send in or out for their virtual liquidity deposit. To compute uncollected fees of a token, you need to know how much ๐๐ for the positionโs range (calculated from the rangeโs ๐๐ and ๐๐ as described in section 6.3) has grown since the last time fees were collected for that position. The growth in fees in a given range per unit of liquidity over between times ๐ก0 and ๐ก1 is simply ๐๐ (๐ก1) โ ๐๐ (๐ก0) (where ๐๐ (๐ก0) is stored in the position as feeGrowthInside{0,1}Last, and ๐๐ (๐ก1) can be computed from the current state of the ticks). Multiplying this by the positionโs liquidity gives us the total uncollected fees in token 0 for this position: 7Since the formulas for computing uncollected fees in each token are the same, we will omit that subscript for the rest of this section. 8
Uniswap v3 Core ๐๐ข = ๐ ยท (๐๐ (๐ก1) โ ๐๐ (๐ก0)) (6.28) Then, the contract updates the positionโsliquidity by adding liquidityDelta. It also addsliquidityDelta to theliquidityNet value for the tick at the bottom end of the range, and subtracts it from the liquidityNet at the upper tick (to reflect that this new liquidity would be added when the price crosses the lower tick going up, and subtracted when the price crosses the upper tick going up). If the poolโs current price is within the range of this position, the contract also adds liquidityDelta to the contractโs global liquidity value. Finally, the pool transfers tokens from (or, if liquidityDelta is negative, to) the user, corresponding to the amount of liquidity burned or minted. The amount of token0 (ฮ๐ ) or token1 (ฮ๐ ) that needs to be deposited can be thought of as the amount that would be sold from the position if the price were to move from the current price (๐) to the upper tick or lower tick (for token0 or token1, respectively). These formulas can be derived from formulas 6.14 and 6.16, and depend on whether the current price is below, within, or above the range of the position: ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ 0 ๐๐ < ๐๐ ฮ๐ฟ ยท ( โ ๐ โ p ๐ (๐๐ )) ๐๐ โค ๐๐ < ๐๐ข ฮ๐ฟ ยท ( p ๐ (๐๐ข ) โ p ๐ (๐๐ )) ๐๐ โฅ ๐๐ข (6.29) ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ ฮ๐ฟ ยท ( 1โ ๐ (๐๐ ) โ 1โ ๐ (๐๐ข ) ) ๐๐ < ๐๐ ฮ๐ฟ ยท ( 1โ ๐ โ 1โ ๐ (๐๐ข ) ) ๐๐ โค ๐๐ < ๐๐ข 0 ๐๐ โฅ ๐๐ข (6.30) REFERENCES [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, United States, 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. DISCLAIMER This paper is for general information purposes only. It does not constitute investment advice or a recommendation or solicitation to buy or sell any investment and should not be used in the evaluation of the merits of making any investment decision. It should not be relied upon for accounting, legal or tax advice or investment rec- ommendations. This paper reflects current opinions of the authors and is not made on behalf of Uniswap Labs, Paradigm, or their affiliates and does not necessarily reflect the opinions of Uniswap Labs, Paradigm, their affiliates or individuals associated with them. The opinions reflected herein are subject to change without being updated. 9
Uniswap v3 Core March 2021 Hayden Adams [email protected] Noah Zinsmeister [email protected] Moody Salem [email protected] River Keefer [email protected] Dan Robinson [email protected] ABSTRACT Uniswap v3 is a noncustodial automated market maker imple- mented for the Ethereum Virtual Machine. In comparison to earlier versions of the protocol, Uniswap v3 provides increased capital efficiency and fine-tuned control to liquidity providers, improves the accuracy and convenience of the price oracle, and has a more flexible fee structure. 1 INTRODUCTION Automated market makers (AMMs) are agents that pool liquidity and make it available to traders according to an algorithm [5]. Con- stant function market makers (CFMMs), a broad class of AMMs of which Uniswap is a member, have seen widespread use in the con- text of decentralized finance, where they are typically implemented as smart contracts that trade tokens on a permissionless blockchain [2]. CFMMs as they are implemented today are often capital inef- ficient. In the constant product market maker formula used by Uniswap v1 and v2, only a fraction of the assets in the pool are available at a given price. This is inefficient, particularly when assets are expected to trade close to a particular price at all times. Prior attempts to address this capital efficiency issue, such as Curve [3] and YieldSpace [4], have involved building pools that use different functions to describe the relation between reserves. This requires all liquidity providers in a given pool to adhere to a single formula, and could result in liquidity fragmentation if liquidity providers want to provide liquidity within different price ranges. In this paper, we present Uniswap v3, a novel AMM that gives liquidity providers more control over the price ranges in which their capital is used, with limited effect on liquidity fragmentation and gas inefficiency. This design does not depend on any shared assumption about the price behavior of the tokens. Uniswap v3 is based on the same constant product reserves curve as earlier versions [1], but offers several significant new features: โข Concentrated Liquidity: Liquidity providers (LPs) are given the ability to concentrate their liquidity by โbounding" it within an arbitrary price range. This improves the poolโs capital efficiency and allows LPs to approximate their pre- ferred reserves curve, while still being efficiently aggregated with the rest of the pool. We describe this feature in section 2 and its implementation in Section 6. โข Flexible Fees : The swap fee is no longer locked at 0.30%. Rather, the fee tier for each pool (of which there can be multiple per asset pair) is set on initialization (Section 3.1). The initially supported fee tiers are 0.05%, 0.30%, and 1%. UNI governance is able to add additional values to this set. โข Protocol Fee Governance: UNI governance has more flexibility in setting the fraction of swap fees collected by the protocol (Section 6.2.2). โข Improved Price Oracle: Uniswap v3 provides a way for users to query recent price accumulator values, thus avoiding the need to checkpoint the accumulator value at the exact be- ginning and end of the period for which a TWAP is being measured. (Section 5.1). 1
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson โข Liquidity Oracle: The contracts expose a time-weighted av- erage liquidity oracle (Section 5.3). The Uniswap v2 core contracts are non-upgradeable by de- sign, so Uniswap v3 is implemented as an entirely new set of contracts, available here. The Uniswap v3 core contracts are also non-upgradeable, with some parameters controlled by governance as described in Section 4. 2 CONCENTRATED LIQUIDITY The defining idea of Uniswap v3 is that of concentrated liquidity: liquidity bounded within some price range. In earlier versions, liquidity was distributed uniformly along the ๐ฅ ยท๐ฆ = ๐ (2.1) reserves curve, where ๐ฅ and ๐ฆ are the respective reserves of two assets X and Y, and ๐ is a constant [1]. In other words, earlier ver- sions were designed to provide liquidity across the entire price range (0, โ). This is simple to implement and allows liquidity to be efficiently aggregated, but means that much of the assets held in a pool are never touched. Having considered this, it seems reasonable to allow LPs to concentrate their liquidity to smaller price ranges than (0, โ). We call liquidity concentrated to a finite range a position. A position only needs to maintain enough reserves to support trading within its range, and therefore can act like a constant product pool with larger reserves (we call these the virtual reserves) within that range. ๐ ๐ ๐ ๐ฆreal ๐ฅreal X Reserves Y Reserves virtual reserves Figure 1: Simulation of Virtual Liquidity Specifically, a position only needs to hold enough of asset X to cover price movement to its upper bound, because upwards price movement1 corresponds to depletion of the X reserves. Similarly, it only needs to hold enough of asset Y to cover price movement to its lower bound. Fig. 1 depicts this relationship for a position on a range [๐๐, ๐๐ ] and a current price ๐๐ โ [ ๐๐, ๐๐ ]. ๐ฅreal and ๐ฆreal denote the positionโs real reserves. When the price exits a positionโs range, the positionโs liquidity is no longer active, and no longer earns fees. At that point, its 1We take asset Y to be the unit of account, which corresponds to token1 in our implementation. liquidity is composed entirely of a single asset, because the reserves of the other asset must have been entirely depleted. If the price ever reenters the range, the liquidity becomes active again. The amount of liquidity provided can be measured by the value ๐ฟ, which is equal to โ ๐. The real reserves of a position are described by the curve: (๐ฅ + ๐ฟโ๐๐ ) (๐ฆ + ๐ฟโ๐๐) = ๐ฟ2 (2.2) This curve is a translation of formula 2.1 such that the position is solvent exactly within its range (Fig. 2). ๐ ๐ X Reserves Y Reserves virtual reserves (2.1) real reserves (2.2) Figure 2: Real Reserves Liquidity providers are free to create as many positions as they see fit, each on its own price range. In this way, LPs can approximate any desired distribution of liquidity on the price space (see Fig. 3 for a few examples). Moreover, this serves as a mechanism to let the market decide where liquidity should be allocated. Rational LPs can reduce their capital costs by concentrating their liquidity in a narrow band around the current price, and adding or removing tokens as the price moves to keep their liquidity active. 2.1 Range Orders Positions on very small ranges act similarly to limit ordersโif the range is crossed, the position flips from being composed entirely of one asset, to being composed entirely of the other asset (plus accrued fees). There are two differences between this range order and a traditional limit order: โข There is a limit to how narrow a positionโs range can be. While the price is within that range, the limit order might be partially executed. โข When the position has been crossed, it needs to be with- drawn. If it is not, and the price crosses back across that range, the position will be traded back, effectively reversing the trade. 2
Consensus and Security
Uniswap v3 Core 0 โ Price Liquidity (I) Uniswap v2 ๐๐ ๐๐ Price Liquidity (II) A single position on [๐๐, ๐๐ ] Price Liquidity (III) A collection of custom positions Figure 3: Example Liquidity Distributions 3 ARCHITECTURAL CHANGES Uniswap v3 makes a number of architectural changes, some of which are necessitated by the inclusion of concentrated liquidity, and some of which are independent improvements. 3.1 Multiple Pools Per Pair In Uniswap v1 and v2, every pair of tokens corresponds to a single liquidity pool, which applies a uniform fee of 0.30% to all swaps. While this default fee tier historically worked well enough for many tokens, it is likely too high for some pools (such as pools between two stablecoins), and too low for others (such as pools that include highly volatile or rarely traded tokens). Uniswap v3 introduces multiple pools for each pair of tokens, each with a different swap fee. All pools are created by the same factory contract. The factory contract initially allows pools to be created at three fee tiers: 0.05%, 0.30%, and 1%. Additional fee tiers can be enabled by UNI governance. 3.2 Non-Fungible Liquidity 3.2.1 Non-Compounding Fees. Fees earned in earlier versions were continuously deposited in the pool as liquidity. This meant that liquidity in the pool would grow over time, even without explicit deposits, and that fee earnings compounded. In Uniswap v3, due to the non-fungible nature of positions, this is no longer possible. Instead, fee earnings are stored separately and held as the tokens in which the fees are paid (see Section 6.2.2). 3.2.2 Removal of Native Liquidity Tokens. In Uniswap v1 and v2, the pool contract is also an ERC-20 token contract, whose tokens represent liquidity held in the pool. While this is convenient, it actually sits uneasily with the Uniswap v2 philosophy that any- thing that does not need to be in the core contracts should be in the periphery, and blessing one โcanonical" ERC-20 implementation discourages the creation of improved ERC-20 token wrappers. Ar- guably, the ERC-20 token implementation should have been in the periphery, as a wrapper on a single liquidity position in the core contract. The changes made in Uniswap v3 force this issue by making completely fungible liquidity tokens impossible. Due to the custom liquidity provision feature, fees are now collected and held by the pool as individual tokens, rather than automatically reinvested as liquidity in the pool. As a result, in v3, the pool contract does not implement the ERC-20 standard. Anyone can create an ERC-20 token contract in the periphery that makes a liquidity position more fungible, but it will have to have additional logic to handle distribution of, or reinvestment of, collected fees. Alternatively, anyone could create a periphery contract that wraps an individual liquidity position (including collected fees) in an ERC-721 non-fungible token. 4 GOVERNANCE The factory has an owner, which is initially controlled by UNI tokenholders.2 The owner does not have the ability to halt the operation of any of the core contracts. As in Uniswap v2, Uniswap v3 has a protocol fee that can be turned on by UNI governance. In Uniswap v3, UNI governance has more flexibility in choosing the fraction of swap fees that go to the protocol, and is able to choose any fraction 1 ๐ where 4 โค ๐ โค 10, or 0. This parameter can be set on a per-pool basis. UNI governance also has the ability to add additional fee tiers. When it adds a new fee tier, it can also define the tickSpacing (see Section 6.1) corresponding to that fee tier. Once a fee tier is added to the factory, it cannot be removed (and the tickSpacing cannot be changed). The initial fee tiers and tick spacings supported are 0.05% (with a tick spacing of 10, approximately 0.10% between initializable ticks), 0.30% (with a tick spacing of 60, approximately 0.60% between initializable ticks), and 1% (with a tick spacing of 200, approximately 2.02% between ticks. Finally, UNI governance has the power to transfer ownership to another address. 5 ORACLE UPGRADES Uniswap v3includes three significant changes to the time-weighted average price (TWAP) oracle that was introduced by Uniswap v2. Most significantly, Uniswap v3 removes the need for users of the oracle to track previous values of the accumulator externally. Uniswap v2 requires users to checkpoint the accumulator value at both the beginning and end of the time period for which they 2Specifically, the owner will be initialized to the Timelock contract from UNI gover- nance, 0x1a9c8182c09f50c8318d769245bea52c32be35bc. 3
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson wanted to compute a TWAP. Uniswap v3 brings the accumulator checkpoints into core, allowing external contracts to compute on- chain TWAPs over recent periods without storing checkpoints of the accumulator value. Another change is that instead of accumulating the sum of prices, allowing users to compute the arithmetic mean TWAP,Uniswap v3 tracks the sum of log prices, allowing users to compute the geometric mean TWAP. Finally, Uniswap v3 adds a liquidity accumulator that is tracked alongside the price accumulator, which accumulates 1 ๐ฟ for each second. This liquidity accumulator is useful for external contracts that want to implement liquidity mining on top of Uniswap v3. It can also be used by other contracts to inform a decision on which of the pools corresponding to a pair (see section 3.1) will have the most reliable TWAP. 5.1 Oracle Observations As in Uniswap v2, Uniswap v3 tracks a running accumulator of the price at the beginning of each block, multiplied by the number of seconds since the last block. A pool in Uniswap v2 stores only the most recent value of this price accumulatorโthat is, the value as of the last block in which a swap occurred. When computing average prices in Uniswap v2, it is the responsibility of the external caller to provide the previous value of the price accumulator. With many users, each will have to provide their own methodology for checkpointing previous values of the accumulator, or coordinate on a shared method to reduce costs. And there is no way to guarantee that every block in which the pool is touched will be reflected in the accumulator. In Uniswap v3, the pool stores a list of previous values for the price accumulator (as well as the liquidity accumulator described in section 5.3). It does this by automatically checkpointing the accumulator value every time the pool is touched for the first time in a block, cycling through an array where the oldest checkpoint is eventually overwritten by a new one, similar to a circular buffer. While this array initially only has room for a single checkpoint, anyone can initialize additional storage slots to lengthen the array, extending to as many as 65,536 checkpoints. 3 This imposes the one-time gas cost of initializing additional storage slots for this array on whoever wants this pair to checkpoint more slots. The pool exposes the array of past observations to users, as well as a convenience function for finding the (interpolated) accumulator value at any historical timestamp within the checkpointed period. 5.2 Geometric Mean Price Oracle Uniswap v2 maintains two price accumulatorsโone for the price of token0 in terms of token1, and one for the price oftoken1 in terms of token0. Users can compute the time-weighted arithmetic mean of the prices over any period, by subtracting the accumulator value at the beginning of the period from the accumulator at the end of the period, then dividing the difference by the number of seconds in the period. Note that accumulators for token0 and token1 are tracked separately, since the time-weighted arithmetic mean price 3The maximum of 65,536 checkpoints allows fetching checkpoints for at least 9 days after they are written, assuming 13 seconds pass between each block and a checkpoint is written every block. of token0 is not equivalent to the reciprocal of the time-weighted arithmetic mean price of token1. Using the time-weighted geometric mean price, as Uniswap v3 does, avoids the need to track separate accumulators for these ratios. The geometric mean of a set of ratios is the reciprocal of the geometric mean of their reciprocals. It is also easy to implement in Uniswap v3 because of its implementation of custom liquidity provision, as described in section 6. In addition, the accumulator can be stored in a smaller number of bits, since it trackslog ๐ rather than ๐, and log ๐ can represent a wide range of prices with consistent precision.4 Finally, there is a theoretical argument that the time- weighted geometric mean price should be a truer representation of the average price.5 Instead of tracking the cumulative sum of the price ๐, Uniswap v3 accumulates the cumulative sum of the current tick index (๐๐๐1.0001๐, the logarithm of price for base 1.0001, which is precise up to 1 basis point). The accumulator at any given time is equal to the sum of ๐๐๐1.0001 (๐) for every second in the history of the contract: ๐๐ก = ๐กร ๐=1 log1.0001 (๐๐ ) (5.1) We want to estimate the geometric mean time-weighted average price (๐๐ก1,๐ก2) over any period ๐ก1 to ๐ก2. ๐๐ก1,๐ก2 = ยฉยญ ยซ ๐ก2ร ๐=๐ก1 ๐๐ ยชยฎ ยฌ 1 ๐ก2โ๐ก1 (5.2) To compute this, you can look at the accumulatorโs value at๐ก1 and at ๐ก2, subtract the first value from the second, divide by the number of seconds elapsed, and compute 1.0001๐ฅ to compute the time weighted geometric mean price. log1.0001 ๐๐ก1,๐ก2 = ร๐ก2 ๐=๐ก1 log1.0001 (๐๐ ) ๐ก2 โ ๐ก1 (5.3) log1.0001 ๐๐ก1,๐ก2 = ๐๐ก2 โ ๐๐ก1 ๐ก2 โ ๐ก1 (5.4) ๐๐ก1,๐ก2 = 1.0001 ๐๐ก2 โ๐๐ก1 ๐ก2โ๐ก1 (5.5) 5.3 Liquidity Oracle In addition to the seconds-weighted accumulator of log1.0001 ๐๐๐๐๐ , Uniswap v3 also tracks a seconds-weighted accumulator of 1 ๐ฟ (the reciprocal of the virtual liquidity currently in range) at the begin- ning of each block: secondsPerLiquidityCumulative (๐ ๐๐ ). This can be used by external liquidity mining contracts to fairly allocate rewards. If an external contract wants to distribute rewards at an even rate of ๐ tokens per second to all active liquidity in the 4In order to support tolerable precision across all possible prices, Uniswap v2 repre- sents each price as a 224-bit fixed-point number. Uniswap v3 only needs to represent ๐๐๐ 1.0001๐ as a signed 24-bit number, and still can detect price movements of one tick, or 1 basis point. 5While arithmetic mean TWAPs are much more widely used, they should theoretically be less accurate in measuring a geometric Brownian motion process (which is how price movements are usually modeled). The arithmetic mean of a geometric Brownian motion process will tend to overweight higher prices (where small percentage movements correspond to large absolute movements) relative to lower ones. 4
Uniswap v3 Core contract, and a position with ๐ฟ liquidity was active from ๐ก0 to ๐ก1, then its rewards for that period would be ๐ ยทLยท(๐ ๐๐ (๐ก1) โ ๐ ๐๐ (๐ก0)). In order to extend this so that concentrated liquidity is rewarded only when it is in range,Uniswap v3 stores a computed checkpoint based on this value every time a tick is crossed, as described in section 6.3. This accumulator can also be used by on-chain contracts to make their oracles stronger (such as by evaluating which fee-tier pool to use the oracle from). 6 IMPLEMENTING CONCENTRATED LIQUIDITY The rest of this paper describes how concentrated liquidity provi- sion works, and gives a high-level description of how it is imple- mented in the contracts. 6.1 Ticks and Ranges To implement custom liquidity provision, the space of possible prices is demarcated by discrete ticks. Liquidity providers can pro- vide liquidity in a range between any two ticks (which need not be adjacent). Each range can be specified as a pair of signed integertick indices: a lower tick (๐๐ ) and an upper tick ( ๐๐ข). Ticks represent prices at which the virtual liquidity of the contract can change. We will assume that prices are always expressed as the price of one of the tokensโcalled token0โin terms of the other tokenโ token1. The assignment of the two tokens to token0 and token1 is arbitrary and does not affect the logic of the contract (other than through possible rounding errors). Conceptually, there is a tick at every price ๐ that is an integer power of 1.0001. Identifying ticks by an integer index ๐, the price at each is given by: ๐ (๐) = 1.0001๐ (6.1) This has the desirable property of each tick being a .01% (1 basis point) price movement away from each of its neighboring ticks. For technical reasons explained in 6.2.1, however, pools actually track ticks at every square root price that is an integer power ofโ 1.0001. Consider the above equation, transformed into square root price space: โ๐ (๐) = โ 1.0001 ๐ = 1.0001 ๐ 2 (6.2) As an example,โ๐ (0)โthe square root price at tick 0โis 1, โ๐ (1) is โ 1.0001 โ 1.00005, and โ๐ (โ1) is 1โ 1.0001 โ 0.99995. When liquidity is added to a range, if one or both of the ticks is not already used as a bound in an existing position, that tick is initialized. Not every tick can be initialized. The pool is instantiated with a parameter, tickSpacing (๐ก๐ ); only ticks with indexes that are divisi- ble by tickSpacing can be initialized. For example, iftickSpacing is 2, then only even ticks (...-4, -2, 0, 2, 4...) can be initialized. Small choices for tickSpacing allow tighter and more precise ranges, but may cause swaps to be more gas-intensive (since each initialized tick that a swap crosses imposes a gas cost on the swapper). Whenever the price crosses an initialized tick, virtual liquidity is kicked in or out. The gas cost of an initialized tick crossing is constant, and is not dependent on the number of positions being kicked in or out at that tick. Ensuring that the right amount of liquidity is kicked in and out of the pool when ticks are crossed, and ensuring that each position earns its proportional share of the fees that were accrued while it was within range, requires some accounting within the pool. The pool contract uses storage variables to track state at a global (per-pool) level, at a per-tick level, and at a per-position level. 6.2 Global State The global state of the contract includes seven storage variables relevant to swaps and liquidity provision. (It has other storage variables that are used for the oracle, as described in section 5.) Type Variable Name Notation uint128 liquidity ๐ฟ uint160 sqrtPriceX96 โ ๐ int24 tick ๐๐ uint256 feeGrowthGlobal0X128 ๐๐,0 uint256 feeGrowthGlobal1X128 ๐๐,1 uint128 protocolFees.token0 ๐๐,0 uint128 protocolFees.token1 ๐๐,1 Table 1: Global State 6.2.1 Price and Liquidity. In Uniswap v2, each pool contract tracks the poolโs current reserves,๐ฅ and ๐ฆ. In Uniswap v3, the contract could be thought of as having virtual reservesโvalues for ๐ฅ and ๐ฆ that allow you to describe the contractโs behavior (between two adjacent ticks) as if it followed the constant product formula. Instead of tracking those virtual reserves, however, the pool contract tracks two different values:liquidity (๐ฟ) and sqrtPrice ( โ ๐). These could be computed from the virtual reserves with the following formulas: ๐ฟ = โ๐ฅ๐ฆ (6.3) โ ๐ = r๐ฆ ๐ฅ (6.4) Conversely, these values could be used to compute the virtual reserves: ๐ฅ = ๐ฟโ ๐ (6.5) ๐ฆ = ๐ฟ ยท โ ๐ (6.6) Using ๐ฟ and โ ๐ is convenient because only one of them changes at a time. Price (and thus โ ๐) changes when swapping within a tick; liquidity changes when crossing a tick, or when minting or burning liquidity. This avoids some rounding errors that could be encountered if tracking virtual reserves. You may notice that the formula for liquidity (based on virtual reserves) is similar to the formula used to initialize the quantity of liquidity tokens (based on actual reserves) in Uniswap v2. before 5
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson any fees have been earned. In some ways, liquidity can be thought of as virtual liquidity tokens. Alternatively, liquidity can be thought of as the amount that token1 reserves (either actual or virtual) changes for a given change in โ ๐: ๐ฟ = ฮ๐ ฮ โ ๐ (6.7) We track โ ๐ instead of ๐ to take advantage of this relationship, and to avoid having to take any square roots when computing swaps, as described in section 6.2.3. The global state also tracks the current tick index as tick (๐๐), a signed integer representing the current tick (more specifically, the nearest tick below the current price). This is an optimization (and a way of avoiding precision issues with logarithms), since at any time, you should be able to compute the current tick based on the current sqrtPrice. Specifically, at any given time, the following equation should be true: ๐๐ = j logโ 1.0001 โ ๐ k (6.8) 6.2.2 Fees. Each pool is initialized with an immutable value, fee (๐พ), representing the fee paid by swappers in units of hundredths of a basis point (0.0001%). It also tracks the current protocol fee, ๐ (which is initialized to zero, but can changed by UNI governance).6 This number gives you the fraction of the fees paid by swappers that currently goes to the protocol rather than to liquidity providers. ๐ only has a limited set of permitted values: 0, 1/4, 1/5, 1/6, 1/7, 1/8, 1/9, or 1/10. The global state also tracks two numbers: feeGrowthGlobal0 (๐๐,0) andfeeGrowthGlobal1 (๐๐,1). These represent the total amount of fees that have been earned per unit of virtual liquidity (๐ฟ), over the entire history of the contract. You can think of them as the total amount of fees that would have been earned by 1 unit of unbounded liquidity that was deposited when the contract was first initialized. They are stored as fixed-point unsigned 128x128 numbers. Note that in Uniswap v3, fees are collected in the tokens themselves rather than in liquidity, for reasons explained in section 3.2.1. Finally, the global state tracks the total accumulated uncollected protocol fee in each token,protocolFees0 (๐๐,0) andprotocolFees1 (๐๐,1). This is an unsigned uint128. The accumulated protocol fees can be collected by UNI governance, by calling thecollectProtocol function. 6.2.3 Swapping Within a Single Tick. For small enough swaps, that do not move the price past a tick, the contracts act like an ๐ฅ ยท๐ฆ = ๐ pool. Suppose ๐พ is the fee, i.e., 0.003, and ๐ฆ๐๐ as the amount of token1 sent in. First, feeGrowthGlobal1 and protocolFees1 are incremented: ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท (1 โ ๐) (6.9) ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท๐ (6.10) ฮ๐ฆ is the increase in ๐ฆ (after the fee is taken out). 6Technically, the storage variable called โprotocolFee" is the denominator of this fraction (or is zero, if ๐ is zero). ฮ๐ฆ = ๐ฆ๐๐ ยท (1 โ ๐พ) (6.11) If you used the computed virtual reserves (๐ฅ and ๐ฆ) for thetoken0 and token1 balances, then this formula could be used to find the amount of token0 sent out: ๐ฅ๐๐๐ = ๐ฅ ยท๐ฆ ๐ฆ + ฮ๐ฆ (6.12) But remember that inv3, the contract actually tracks liquidity (๐ฟ) and square root of price ( โ ๐) instead of ๐ฅ and ๐ฆ. We could compute ๐ฅ and ๐ฆ from those values, and then use those to calculate the execution price of the trade. But it turns out that there are simple formulas that describe the relationship between ฮ โ ๐ and ฮ๐ฆ, for a given ๐ฟ (which can be derived from formula 6.7): ฮ โ ๐ = ฮ๐ฆ ๐ฟ (6.13) ฮ๐ฆ = ฮ โ ๐ ยท๐ฟ (6.14) There are also simple formulas that describe the relationship between ฮ 1โ ๐ and ฮ๐ฅ: ฮ 1โ ๐ = ฮ๐ฅ ๐ฟ (6.15) ฮ๐ฅ = ฮ 1โ ๐ ยท๐ฟ (6.16) When swapping one token for the other, the pool contract can first compute the new โ ๐ using formula 6.13 or 6.15, and then can compute the amount of token0 or token1 to send out using formula 6.14 or 6.16. These formulas will work for any swap that does not push โ ๐ past the price of the next initialized tick. If the computed ฮ โ ๐ would cause โ ๐ to move past that next initialized tick, the contract must only cross up to that tickโusing up only part of the swapโand then cross the tick, as described in section 6.3.1, before continuing with the rest of the swap. 6.2.4 Initialized Tick Bitmap. If a tick is not used as the endpoint of a range with any liquidity in itโthat is, if the tick is uninitial- izedโthen that tick can be skipped during swaps. As an optimization to make finding the next initialized tick more efficient, the pool tracks a bitmap tickBitmap of initialized ticks. The position in the bitmap that corresponds to the tick index is set to 1 if the tick is initialized, and 0 if it is not initialized. When a tick is used as an endpoint for a new position, and that tick is not currently used by any other liquidity, the tick is initialized, and the corresponding bit in the bitmap is set to 1. An initialized tick can become uninitialized again if all of the liquidity for which it is an endpoint is removed, in which case that tickโs position on the bitmap is zeroed out. 6.3 Tick-Indexed State The contract needs to store information about each tick in order to track the amount of net liquidity that should be added or removed when the tick is crossed, as well as to track the fees earned above and below that tick. 6
Uniswap v3 Core Start S0. Check input S1. Swap within current interval S2. Is there remaining input or output? S4. Cross next tick Stop S5. Execute computed swap Pass Fail Yes No Figure 4: Swap Control Flow The contract stores a mapping from tick indexes (int24) to the following seven values: Type Variable Name Notation int128 liquidityNet ฮ๐ฟ uint128 liquidityGross ๐ฟ๐ uint256 feeGrowthOutside0X128 ๐๐,0 uint256 feeGrowthOutside1X128 ๐๐,1 uint256 secondsOutside ๐ ๐ uint256 tickCumulativeOutside ๐๐ uint256 secondsPerLiquidityOutsideX128 ๐ ๐๐ Table 2: Tick-Indexed State Each tick tracks ฮ๐ฟ, the total amount of liquidity that should be kicked in or out when the tick is crossed. The tick only needs to track one signed integer: the amount of liquidity added (or, if negative, removed) when the tick is crossed going left to right. This value does not need to be updated when the tick is crossed (but only when a position with a bound at that tick is updated). We want to be able to uninitialize a tick when there is no longer any liquidity referencing that tick. Since ฮ๐ฟ is a net value, itโs necessary to track a gross tally of liquidity referencing the tick, liquidityGross. This value ensures that even if net liquidity at a tick is 0, we can still know if a tick is referenced by at least one underlying position or not, which tells us whether to update the tick bitmap. feeGrowthOutside{0,1} are used to track how many fees were accumulated within a given range. Since the formulas are the same for the fees collected in token0 and token1, we will omit that sub- script for the rest of this section. You can compute the fees earned per unit of liquidity in token 0 above (๐๐) and below (๐๐) a tick ๐ with a formula that depends on whether the price is currently within or outside that rangeโthat is, whether the current tick index ๐๐ is greater than or equal to ๐: ๐๐ (๐) = ( ๐๐ โ ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ (๐) ๐๐ < ๐ (6.17) ๐๐ (๐) = ( ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ โ ๐๐ (๐) ๐๐ < ๐ (6.18) We can use these functions to compute the total amount of cumulative fees per share ๐๐ in the range between two ticksโa lower tick ๐๐ and an upper tick ๐๐ข: ๐๐ = ๐๐ โ ๐๐ (๐๐ ) โ ๐๐ (๐๐ข ) (6.19) ๐๐ needs to be updated each time the tick is crossed. Specifically, as a tick๐ is crossed in either direction, its๐๐ (for each token) should be updated as follows: ๐๐ (๐) := ๐๐ โ ๐๐ (๐) (6.20) ๐๐ is only needed for ticks that are used as either the lower or upper bound for at least one position. As a result, for efficiency,๐๐ is not initialized (and thus does not need to be updated when crossed) until a position is created that has that tick as one of its bounds. When ๐๐ is initialized for a tick ๐, the valueโby conventionโis chosen as if all of the fees earned to date had occurred below that tick: ๐๐ := ( ๐๐ ๐๐ โฅ ๐ 0 ๐๐ < ๐ (6.21) Note that since ๐๐ values for different ticks could be initialized at different times, comparisons of the ๐๐ values for different ticks are not meaningful, and there is no guarantee that values for ๐๐ will be consistent. This does not cause a problem for per-position accounting, since, as described below, all the position needs to know is the growth in ๐ within a given range since that position was last touched. 7
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson Finally, the contract also stores secondsOutside (๐ ๐), secondsPerLiquidityOutside, andtickCumulativeOutside for each tick. These values are not used within the contract, but are tracked for the benefit of external contracts that need more fine- grained information about the poolโs behavior (for purposes like liquidity mining). All three of these indexes work similarly to the fee growth in- dexes described above. But where the feeGrowthOutside{0,1} indexes track feeGrowthGlobal{0,1}, the secondsOutside index tracks seconds (that is, the current timestamp), secondsPerLiquidityOutside tracks the 1/๐ฟ accumulator (secondsPerLiquidityCumulative) described in section 5.3, and tickCumulativeOutside tracks the log1.0001 ๐ accumulator de- scribed in section 5.2. For example, the seconds spent above (๐ ๐) and below (๐ ๐) a given tick is computed differently based on whether the current price is within that range, and the seconds spent within a range (๐ ๐ ) can be computed using the values of ๐ ๐ and ๐ ๐: ๐ก๐ (๐) = ( ๐ก โ ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก๐ (๐) ๐๐ < ๐ (6.22) ๐ก๐ (๐) = ( ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก โ ๐ก๐ (๐) ๐๐ < ๐ (6.23) ๐ก๐ (๐๐ , ๐๐ข ) = ๐ก โ ๐ก๐ (๐๐ ) โ ๐ก๐ (๐๐ข ) (6.24) The number of seconds spent within a range between two times ๐ก1 and ๐ก2 can be computed by recording the value of ๐ ๐ (๐๐ , ๐๐ข ) at ๐ก1 and at ๐ก2, and subtracting the former from the latter. Like ๐๐, ๐ ๐ does not need to be tracked for ticks that are not on the edge of any position. Therefore, it is not initialized until a position is created that is bounded by that tick. By convention, it is initialized as if every second since the Unix timestamp 0 had been spent below that tick: ๐ก๐ (๐) := ( ๐ก ๐ ๐ โฅ ๐ 0 ๐๐ < ๐ (6.25) As with ๐๐ values, ๐ก๐ values are not meaningfully comparable across different ticks. ๐ก๐ is only meaningful in computing the num- ber of seconds that liquidity was within some particular range between some defined start time (which must be after ๐ก๐ was ini- tialized for both ticks) and some end time. 6.3.1 Crossing a Tick. As described in section 6.2.3, Uniswap v3 acts like it obeys the constant product formula when swapping between initialized ticks. When a swap crosses an initialized tick, however, the contract needs to add or remove liquidity, to ensure that no liquidity provider is insolvent. This means theฮ๐ฟ is fetched from the tick, and applied to the global ๐ฟ. The contract also needs to update the tickโs own state, in order to track the fees earned (and seconds spent) within ranges bounded by this tick. The feeGrowthOutside{0,1} and secondsOutside values are updated to both reflect current values, as well as the proper orientation relative to the current tick: ๐๐ := ๐๐ โ ๐๐ (6.26) ๐ก๐ := ๐ก โ ๐ก๐ (6.27) Once a tick is crossed, the swap can continue as described in section 6.2.3 until it reaches the next initialized tick. 6.4 Position-Indexed State The contract has a mapping from user (an address), lower bound (a tick index, int24), and upper bound (a tick index, int24) to a specific Position struct. Each Position tracks three values: Type Variable Name Notation uint128 liquidity ๐ uint256 feeGrowthInside0LastX128 ๐๐,0 (๐ก0) uint256 feeGrowthInside1LastX128 ๐๐,1 (๐ก0) Table 3: Position-Indexed State liquidity (๐) means the amount of virtual liquidity that the position represented the last time this position was touched. Specif- ically, liquidity could be thought of as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are the respective amounts of virtual token0 and virtual token1 that this liquidity contributes to the pool at any time that it is within range. Unlike pool shares in Uniswap v2 (where the value of each share grows over time), the units for liquidity do not change as fees are accumulated; it is always measured as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are quantities of token0 and token1, respectively. This liquidity number does not reflect the fees that have been accumulated since the contract was last touched, which we will call uncollected fees . Computing these uncollected fees requires additional stored values on the position, feeGrowthInside0Last (๐๐,0 (๐ก0)) and feeGrowthInside1Last (๐๐,1 (๐ก0)), as described be- low. 6.4.1 setPosition. The setPosition function allows a liquidity provider to update their position. Two of the arguments tosetPosition โ lowerTick and upperTickโ when combined with the msg.sender, together specify a position. The function takes one additional parameter, liquidityDelta, to specify how much virtual liquidity the user wants to add or (if negative) remove. First, the function computes the uncollected fees ( ๐๐ข) that the position is entitled to, in each token.7 The amount collected in fees is credited to the user and netted against the amount that they would send in or out for their virtual liquidity deposit. To compute uncollected fees of a token, you need to know how much ๐๐ for the positionโs range (calculated from the rangeโs ๐๐ and ๐๐ as described in section 6.3) has grown since the last time fees were collected for that position. The growth in fees in a given range per unit of liquidity over between times ๐ก0 and ๐ก1 is simply ๐๐ (๐ก1) โ ๐๐ (๐ก0) (where ๐๐ (๐ก0) is stored in the position as feeGrowthInside{0,1}Last, and ๐๐ (๐ก1) can be computed from the current state of the ticks). Multiplying this by the positionโs liquidity gives us the total uncollected fees in token 0 for this position: 7Since the formulas for computing uncollected fees in each token are the same, we will omit that subscript for the rest of this section. 8
Uniswap v3 Core ๐๐ข = ๐ ยท (๐๐ (๐ก1) โ ๐๐ (๐ก0)) (6.28) Then, the contract updates the positionโsliquidity by adding liquidityDelta. It also addsliquidityDelta to theliquidityNet value for the tick at the bottom end of the range, and subtracts it from the liquidityNet at the upper tick (to reflect that this new liquidity would be added when the price crosses the lower tick going up, and subtracted when the price crosses the upper tick going up). If the poolโs current price is within the range of this position, the contract also adds liquidityDelta to the contractโs global liquidity value. Finally, the pool transfers tokens from (or, if liquidityDelta is negative, to) the user, corresponding to the amount of liquidity burned or minted. The amount of token0 (ฮ๐ ) or token1 (ฮ๐ ) that needs to be deposited can be thought of as the amount that would be sold from the position if the price were to move from the current price (๐) to the upper tick or lower tick (for token0 or token1, respectively). These formulas can be derived from formulas 6.14 and 6.16, and depend on whether the current price is below, within, or above the range of the position: ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ 0 ๐๐ < ๐๐ ฮ๐ฟ ยท ( โ ๐ โ p ๐ (๐๐ )) ๐๐ โค ๐๐ < ๐๐ข ฮ๐ฟ ยท ( p ๐ (๐๐ข ) โ p ๐ (๐๐ )) ๐๐ โฅ ๐๐ข (6.29) ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ ฮ๐ฟ ยท ( 1โ ๐ (๐๐ ) โ 1โ ๐ (๐๐ข ) ) ๐๐ < ๐๐ ฮ๐ฟ ยท ( 1โ ๐ โ 1โ ๐ (๐๐ข ) ) ๐๐ โค ๐๐ < ๐๐ข 0 ๐๐ โฅ ๐๐ข (6.30) REFERENCES [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, United States, 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. DISCLAIMER This paper is for general information purposes only. It does not constitute investment advice or a recommendation or solicitation to buy or sell any investment and should not be used in the evaluation of the merits of making any investment decision. It should not be relied upon for accounting, legal or tax advice or investment rec- ommendations. This paper reflects current opinions of the authors and is not made on behalf of Uniswap Labs, Paradigm, or their affiliates and does not necessarily reflect the opinions of Uniswap Labs, Paradigm, their affiliates or individuals associated with them. The opinions reflected herein are subject to change without being updated. 9
Uniswap v3 Core March 2021 Hayden Adams [email protected] Noah Zinsmeister [email protected] Moody Salem [email protected] River Keefer [email protected] Dan Robinson [email protected] ABSTRACT Uniswap v3 is a noncustodial automated market maker imple- mented for the Ethereum Virtual Machine. In comparison to earlier versions of the protocol, Uniswap v3 provides increased capital efficiency and fine-tuned control to liquidity providers, improves the accuracy and convenience of the price oracle, and has a more flexible fee structure. 1 INTRODUCTION Automated market makers (AMMs) are agents that pool liquidity and make it available to traders according to an algorithm [5]. Con- stant function market makers (CFMMs), a broad class of AMMs of which Uniswap is a member, have seen widespread use in the con- text of decentralized finance, where they are typically implemented as smart contracts that trade tokens on a permissionless blockchain [2]. CFMMs as they are implemented today are often capital inef- ficient. In the constant product market maker formula used by Uniswap v1 and v2, only a fraction of the assets in the pool are available at a given price. This is inefficient, particularly when assets are expected to trade close to a particular price at all times. Prior attempts to address this capital efficiency issue, such as Curve [3] and YieldSpace [4], have involved building pools that use different functions to describe the relation between reserves. This requires all liquidity providers in a given pool to adhere to a single formula, and could result in liquidity fragmentation if liquidity providers want to provide liquidity within different price ranges. In this paper, we present Uniswap v3, a novel AMM that gives liquidity providers more control over the price ranges in which their capital is used, with limited effect on liquidity fragmentation and gas inefficiency. This design does not depend on any shared assumption about the price behavior of the tokens. Uniswap v3 is based on the same constant product reserves curve as earlier versions [1], but offers several significant new features: โข Concentrated Liquidity: Liquidity providers (LPs) are given the ability to concentrate their liquidity by โbounding" it within an arbitrary price range. This improves the poolโs capital efficiency and allows LPs to approximate their pre- ferred reserves curve, while still being efficiently aggregated with the rest of the pool. We describe this feature in section 2 and its implementation in Section 6. โข Flexible Fees : The swap fee is no longer locked at 0.30%. Rather, the fee tier for each pool (of which there can be multiple per asset pair) is set on initialization (Section 3.1). The initially supported fee tiers are 0.05%, 0.30%, and 1%. UNI governance is able to add additional values to this set. โข Protocol Fee Governance: UNI governance has more flexibility in setting the fraction of swap fees collected by the protocol (Section 6.2.2). โข Improved Price Oracle: Uniswap v3 provides a way for users to query recent price accumulator values, thus avoiding the need to checkpoint the accumulator value at the exact be- ginning and end of the period for which a TWAP is being measured. (Section 5.1). 1
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson โข Liquidity Oracle: The contracts expose a time-weighted av- erage liquidity oracle (Section 5.3). The Uniswap v2 core contracts are non-upgradeable by de- sign, so Uniswap v3 is implemented as an entirely new set of contracts, available here. The Uniswap v3 core contracts are also non-upgradeable, with some parameters controlled by governance as described in Section 4. 2 CONCENTRATED LIQUIDITY The defining idea of Uniswap v3 is that of concentrated liquidity: liquidity bounded within some price range. In earlier versions, liquidity was distributed uniformly along the ๐ฅ ยท๐ฆ = ๐ (2.1) reserves curve, where ๐ฅ and ๐ฆ are the respective reserves of two assets X and Y, and ๐ is a constant [1]. In other words, earlier ver- sions were designed to provide liquidity across the entire price range (0, โ). This is simple to implement and allows liquidity to be efficiently aggregated, but means that much of the assets held in a pool are never touched. Having considered this, it seems reasonable to allow LPs to concentrate their liquidity to smaller price ranges than (0, โ). We call liquidity concentrated to a finite range a position. A position only needs to maintain enough reserves to support trading within its range, and therefore can act like a constant product pool with larger reserves (we call these the virtual reserves) within that range. ๐ ๐ ๐ ๐ฆreal ๐ฅreal X Reserves Y Reserves virtual reserves Figure 1: Simulation of Virtual Liquidity Specifically, a position only needs to hold enough of asset X to cover price movement to its upper bound, because upwards price movement1 corresponds to depletion of the X reserves. Similarly, it only needs to hold enough of asset Y to cover price movement to its lower bound. Fig. 1 depicts this relationship for a position on a range [๐๐, ๐๐ ] and a current price ๐๐ โ [ ๐๐, ๐๐ ]. ๐ฅreal and ๐ฆreal denote the positionโs real reserves. When the price exits a positionโs range, the positionโs liquidity is no longer active, and no longer earns fees. At that point, its 1We take asset Y to be the unit of account, which corresponds to token1 in our implementation. liquidity is composed entirely of a single asset, because the reserves of the other asset must have been entirely depleted. If the price ever reenters the range, the liquidity becomes active again. The amount of liquidity provided can be measured by the value ๐ฟ, which is equal to โ ๐. The real reserves of a position are described by the curve: (๐ฅ + ๐ฟโ๐๐ ) (๐ฆ + ๐ฟโ๐๐) = ๐ฟ2 (2.2) This curve is a translation of formula 2.1 such that the position is solvent exactly within its range (Fig. 2). ๐ ๐ X Reserves Y Reserves virtual reserves (2.1) real reserves (2.2) Figure 2: Real Reserves Liquidity providers are free to create as many positions as they see fit, each on its own price range. In this way, LPs can approximate any desired distribution of liquidity on the price space (see Fig. 3 for a few examples). Moreover, this serves as a mechanism to let the market decide where liquidity should be allocated. Rational LPs can reduce their capital costs by concentrating their liquidity in a narrow band around the current price, and adding or removing tokens as the price moves to keep their liquidity active. 2.1 Range Orders Positions on very small ranges act similarly to limit ordersโif the range is crossed, the position flips from being composed entirely of one asset, to being composed entirely of the other asset (plus accrued fees). There are two differences between this range order and a traditional limit order: โข There is a limit to how narrow a positionโs range can be. While the price is within that range, the limit order might be partially executed. โข When the position has been crossed, it needs to be with- drawn. If it is not, and the price crosses back across that range, the position will be traded back, effectively reversing the trade. 2
Uniswap v3 Core 0 โ Price Liquidity (I) Uniswap v2 ๐๐ ๐๐ Price Liquidity (II) A single position on [๐๐, ๐๐ ] Price Liquidity (III) A collection of custom positions Figure 3: Example Liquidity Distributions 3 ARCHITECTURAL CHANGES Uniswap v3 makes a number of architectural changes, some of which are necessitated by the inclusion of concentrated liquidity, and some of which are independent improvements. 3.1 Multiple Pools Per Pair In Uniswap v1 and v2, every pair of tokens corresponds to a single liquidity pool, which applies a uniform fee of 0.30% to all swaps. While this default fee tier historically worked well enough for many tokens, it is likely too high for some pools (such as pools between two stablecoins), and too low for others (such as pools that include highly volatile or rarely traded tokens). Uniswap v3 introduces multiple pools for each pair of tokens, each with a different swap fee. All pools are created by the same factory contract. The factory contract initially allows pools to be created at three fee tiers: 0.05%, 0.30%, and 1%. Additional fee tiers can be enabled by UNI governance. 3.2 Non-Fungible Liquidity 3.2.1 Non-Compounding Fees. Fees earned in earlier versions were continuously deposited in the pool as liquidity. This meant that liquidity in the pool would grow over time, even without explicit deposits, and that fee earnings compounded. In Uniswap v3, due to the non-fungible nature of positions, this is no longer possible. Instead, fee earnings are stored separately and held as the tokens in which the fees are paid (see Section 6.2.2). 3.2.2 Removal of Native Liquidity Tokens. In Uniswap v1 and v2, the pool contract is also an ERC-20 token contract, whose tokens represent liquidity held in the pool. While this is convenient, it actually sits uneasily with the Uniswap v2 philosophy that any- thing that does not need to be in the core contracts should be in the periphery, and blessing one โcanonical" ERC-20 implementation discourages the creation of improved ERC-20 token wrappers. Ar- guably, the ERC-20 token implementation should have been in the periphery, as a wrapper on a single liquidity position in the core contract. The changes made in Uniswap v3 force this issue by making completely fungible liquidity tokens impossible. Due to the custom liquidity provision feature, fees are now collected and held by the pool as individual tokens, rather than automatically reinvested as liquidity in the pool. As a result, in v3, the pool contract does not implement the ERC-20 standard. Anyone can create an ERC-20 token contract in the periphery that makes a liquidity position more fungible, but it will have to have additional logic to handle distribution of, or reinvestment of, collected fees. Alternatively, anyone could create a periphery contract that wraps an individual liquidity position (including collected fees) in an ERC-721 non-fungible token. 4 GOVERNANCE The factory has an owner, which is initially controlled by UNI tokenholders.2 The owner does not have the ability to halt the operation of any of the core contracts. As in Uniswap v2, Uniswap v3 has a protocol fee that can be turned on by UNI governance. In Uniswap v3, UNI governance has more flexibility in choosing the fraction of swap fees that go to the protocol, and is able to choose any fraction 1 ๐ where 4 โค ๐ โค 10, or 0. This parameter can be set on a per-pool basis. UNI governance also has the ability to add additional fee tiers. When it adds a new fee tier, it can also define the tickSpacing (see Section 6.1) corresponding to that fee tier. Once a fee tier is added to the factory, it cannot be removed (and the tickSpacing cannot be changed). The initial fee tiers and tick spacings supported are 0.05% (with a tick spacing of 10, approximately 0.10% between initializable ticks), 0.30% (with a tick spacing of 60, approximately 0.60% between initializable ticks), and 1% (with a tick spacing of 200, approximately 2.02% between ticks. Finally, UNI governance has the power to transfer ownership to another address. 5 ORACLE UPGRADES Uniswap v3includes three significant changes to the time-weighted average price (TWAP) oracle that was introduced by Uniswap v2. Most significantly, Uniswap v3 removes the need for users of the oracle to track previous values of the accumulator externally. Uniswap v2 requires users to checkpoint the accumulator value at both the beginning and end of the time period for which they 2Specifically, the owner will be initialized to the Timelock contract from UNI gover- nance, 0x1a9c8182c09f50c8318d769245bea52c32be35bc. 3
Network Operation
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson wanted to compute a TWAP. Uniswap v3 brings the accumulator checkpoints into core, allowing external contracts to compute on- chain TWAPs over recent periods without storing checkpoints of the accumulator value. Another change is that instead of accumulating the sum of prices, allowing users to compute the arithmetic mean TWAP,Uniswap v3 tracks the sum of log prices, allowing users to compute the geometric mean TWAP. Finally, Uniswap v3 adds a liquidity accumulator that is tracked alongside the price accumulator, which accumulates 1 ๐ฟ for each second. This liquidity accumulator is useful for external contracts that want to implement liquidity mining on top of Uniswap v3. It can also be used by other contracts to inform a decision on which of the pools corresponding to a pair (see section 3.1) will have the most reliable TWAP. 5.1 Oracle Observations As in Uniswap v2, Uniswap v3 tracks a running accumulator of the price at the beginning of each block, multiplied by the number of seconds since the last block. A pool in Uniswap v2 stores only the most recent value of this price accumulatorโthat is, the value as of the last block in which a swap occurred. When computing average prices in Uniswap v2, it is the responsibility of the external caller to provide the previous value of the price accumulator. With many users, each will have to provide their own methodology for checkpointing previous values of the accumulator, or coordinate on a shared method to reduce costs. And there is no way to guarantee that every block in which the pool is touched will be reflected in the accumulator. In Uniswap v3, the pool stores a list of previous values for the price accumulator (as well as the liquidity accumulator described in section 5.3). It does this by automatically checkpointing the accumulator value every time the pool is touched for the first time in a block, cycling through an array where the oldest checkpoint is eventually overwritten by a new one, similar to a circular buffer. While this array initially only has room for a single checkpoint, anyone can initialize additional storage slots to lengthen the array, extending to as many as 65,536 checkpoints. 3 This imposes the one-time gas cost of initializing additional storage slots for this array on whoever wants this pair to checkpoint more slots. The pool exposes the array of past observations to users, as well as a convenience function for finding the (interpolated) accumulator value at any historical timestamp within the checkpointed period. 5.2 Geometric Mean Price Oracle Uniswap v2 maintains two price accumulatorsโone for the price of token0 in terms of token1, and one for the price oftoken1 in terms of token0. Users can compute the time-weighted arithmetic mean of the prices over any period, by subtracting the accumulator value at the beginning of the period from the accumulator at the end of the period, then dividing the difference by the number of seconds in the period. Note that accumulators for token0 and token1 are tracked separately, since the time-weighted arithmetic mean price 3The maximum of 65,536 checkpoints allows fetching checkpoints for at least 9 days after they are written, assuming 13 seconds pass between each block and a checkpoint is written every block. of token0 is not equivalent to the reciprocal of the time-weighted arithmetic mean price of token1. Using the time-weighted geometric mean price, as Uniswap v3 does, avoids the need to track separate accumulators for these ratios. The geometric mean of a set of ratios is the reciprocal of the geometric mean of their reciprocals. It is also easy to implement in Uniswap v3 because of its implementation of custom liquidity provision, as described in section 6. In addition, the accumulator can be stored in a smaller number of bits, since it trackslog ๐ rather than ๐, and log ๐ can represent a wide range of prices with consistent precision.4 Finally, there is a theoretical argument that the time- weighted geometric mean price should be a truer representation of the average price.5 Instead of tracking the cumulative sum of the price ๐, Uniswap v3 accumulates the cumulative sum of the current tick index (๐๐๐1.0001๐, the logarithm of price for base 1.0001, which is precise up to 1 basis point). The accumulator at any given time is equal to the sum of ๐๐๐1.0001 (๐) for every second in the history of the contract: ๐๐ก = ๐กร ๐=1 log1.0001 (๐๐ ) (5.1) We want to estimate the geometric mean time-weighted average price (๐๐ก1,๐ก2) over any period ๐ก1 to ๐ก2. ๐๐ก1,๐ก2 = ยฉยญ ยซ ๐ก2ร ๐=๐ก1 ๐๐ ยชยฎ ยฌ 1 ๐ก2โ๐ก1 (5.2) To compute this, you can look at the accumulatorโs value at๐ก1 and at ๐ก2, subtract the first value from the second, divide by the number of seconds elapsed, and compute 1.0001๐ฅ to compute the time weighted geometric mean price. log1.0001 ๐๐ก1,๐ก2 = ร๐ก2 ๐=๐ก1 log1.0001 (๐๐ ) ๐ก2 โ ๐ก1 (5.3) log1.0001 ๐๐ก1,๐ก2 = ๐๐ก2 โ ๐๐ก1 ๐ก2 โ ๐ก1 (5.4) ๐๐ก1,๐ก2 = 1.0001 ๐๐ก2 โ๐๐ก1 ๐ก2โ๐ก1 (5.5) 5.3 Liquidity Oracle In addition to the seconds-weighted accumulator of log1.0001 ๐๐๐๐๐ , Uniswap v3 also tracks a seconds-weighted accumulator of 1 ๐ฟ (the reciprocal of the virtual liquidity currently in range) at the begin- ning of each block: secondsPerLiquidityCumulative (๐ ๐๐ ). This can be used by external liquidity mining contracts to fairly allocate rewards. If an external contract wants to distribute rewards at an even rate of ๐ tokens per second to all active liquidity in the 4In order to support tolerable precision across all possible prices, Uniswap v2 repre- sents each price as a 224-bit fixed-point number. Uniswap v3 only needs to represent ๐๐๐ 1.0001๐ as a signed 24-bit number, and still can detect price movements of one tick, or 1 basis point. 5While arithmetic mean TWAPs are much more widely used, they should theoretically be less accurate in measuring a geometric Brownian motion process (which is how price movements are usually modeled). The arithmetic mean of a geometric Brownian motion process will tend to overweight higher prices (where small percentage movements correspond to large absolute movements) relative to lower ones. 4
Uniswap v3 Core contract, and a position with ๐ฟ liquidity was active from ๐ก0 to ๐ก1, then its rewards for that period would be ๐ ยทLยท(๐ ๐๐ (๐ก1) โ ๐ ๐๐ (๐ก0)). In order to extend this so that concentrated liquidity is rewarded only when it is in range,Uniswap v3 stores a computed checkpoint based on this value every time a tick is crossed, as described in section 6.3. This accumulator can also be used by on-chain contracts to make their oracles stronger (such as by evaluating which fee-tier pool to use the oracle from). 6 IMPLEMENTING CONCENTRATED LIQUIDITY The rest of this paper describes how concentrated liquidity provi- sion works, and gives a high-level description of how it is imple- mented in the contracts. 6.1 Ticks and Ranges To implement custom liquidity provision, the space of possible prices is demarcated by discrete ticks. Liquidity providers can pro- vide liquidity in a range between any two ticks (which need not be adjacent). Each range can be specified as a pair of signed integertick indices: a lower tick (๐๐ ) and an upper tick ( ๐๐ข). Ticks represent prices at which the virtual liquidity of the contract can change. We will assume that prices are always expressed as the price of one of the tokensโcalled token0โin terms of the other tokenโ token1. The assignment of the two tokens to token0 and token1 is arbitrary and does not affect the logic of the contract (other than through possible rounding errors). Conceptually, there is a tick at every price ๐ that is an integer power of 1.0001. Identifying ticks by an integer index ๐, the price at each is given by: ๐ (๐) = 1.0001๐ (6.1) This has the desirable property of each tick being a .01% (1 basis point) price movement away from each of its neighboring ticks. For technical reasons explained in 6.2.1, however, pools actually track ticks at every square root price that is an integer power ofโ 1.0001. Consider the above equation, transformed into square root price space: โ๐ (๐) = โ 1.0001 ๐ = 1.0001 ๐ 2 (6.2) As an example,โ๐ (0)โthe square root price at tick 0โis 1, โ๐ (1) is โ 1.0001 โ 1.00005, and โ๐ (โ1) is 1โ 1.0001 โ 0.99995. When liquidity is added to a range, if one or both of the ticks is not already used as a bound in an existing position, that tick is initialized. Not every tick can be initialized. The pool is instantiated with a parameter, tickSpacing (๐ก๐ ); only ticks with indexes that are divisi- ble by tickSpacing can be initialized. For example, iftickSpacing is 2, then only even ticks (...-4, -2, 0, 2, 4...) can be initialized. Small choices for tickSpacing allow tighter and more precise ranges, but may cause swaps to be more gas-intensive (since each initialized tick that a swap crosses imposes a gas cost on the swapper). Whenever the price crosses an initialized tick, virtual liquidity is kicked in or out. The gas cost of an initialized tick crossing is constant, and is not dependent on the number of positions being kicked in or out at that tick. Ensuring that the right amount of liquidity is kicked in and out of the pool when ticks are crossed, and ensuring that each position earns its proportional share of the fees that were accrued while it was within range, requires some accounting within the pool. The pool contract uses storage variables to track state at a global (per-pool) level, at a per-tick level, and at a per-position level. 6.2 Global State The global state of the contract includes seven storage variables relevant to swaps and liquidity provision. (It has other storage variables that are used for the oracle, as described in section 5.) Type Variable Name Notation uint128 liquidity ๐ฟ uint160 sqrtPriceX96 โ ๐ int24 tick ๐๐ uint256 feeGrowthGlobal0X128 ๐๐,0 uint256 feeGrowthGlobal1X128 ๐๐,1 uint128 protocolFees.token0 ๐๐,0 uint128 protocolFees.token1 ๐๐,1 Table 1: Global State 6.2.1 Price and Liquidity. In Uniswap v2, each pool contract tracks the poolโs current reserves,๐ฅ and ๐ฆ. In Uniswap v3, the contract could be thought of as having virtual reservesโvalues for ๐ฅ and ๐ฆ that allow you to describe the contractโs behavior (between two adjacent ticks) as if it followed the constant product formula. Instead of tracking those virtual reserves, however, the pool contract tracks two different values:liquidity (๐ฟ) and sqrtPrice ( โ ๐). These could be computed from the virtual reserves with the following formulas: ๐ฟ = โ๐ฅ๐ฆ (6.3) โ ๐ = r๐ฆ ๐ฅ (6.4) Conversely, these values could be used to compute the virtual reserves: ๐ฅ = ๐ฟโ ๐ (6.5) ๐ฆ = ๐ฟ ยท โ ๐ (6.6) Using ๐ฟ and โ ๐ is convenient because only one of them changes at a time. Price (and thus โ ๐) changes when swapping within a tick; liquidity changes when crossing a tick, or when minting or burning liquidity. This avoids some rounding errors that could be encountered if tracking virtual reserves. You may notice that the formula for liquidity (based on virtual reserves) is similar to the formula used to initialize the quantity of liquidity tokens (based on actual reserves) in Uniswap v2. before 5
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson any fees have been earned. In some ways, liquidity can be thought of as virtual liquidity tokens. Alternatively, liquidity can be thought of as the amount that token1 reserves (either actual or virtual) changes for a given change in โ ๐: ๐ฟ = ฮ๐ ฮ โ ๐ (6.7) We track โ ๐ instead of ๐ to take advantage of this relationship, and to avoid having to take any square roots when computing swaps, as described in section 6.2.3. The global state also tracks the current tick index as tick (๐๐), a signed integer representing the current tick (more specifically, the nearest tick below the current price). This is an optimization (and a way of avoiding precision issues with logarithms), since at any time, you should be able to compute the current tick based on the current sqrtPrice. Specifically, at any given time, the following equation should be true: ๐๐ = j logโ 1.0001 โ ๐ k (6.8) 6.2.2 Fees. Each pool is initialized with an immutable value, fee (๐พ), representing the fee paid by swappers in units of hundredths of a basis point (0.0001%). It also tracks the current protocol fee, ๐ (which is initialized to zero, but can changed by UNI governance).6 This number gives you the fraction of the fees paid by swappers that currently goes to the protocol rather than to liquidity providers. ๐ only has a limited set of permitted values: 0, 1/4, 1/5, 1/6, 1/7, 1/8, 1/9, or 1/10. The global state also tracks two numbers: feeGrowthGlobal0 (๐๐,0) andfeeGrowthGlobal1 (๐๐,1). These represent the total amount of fees that have been earned per unit of virtual liquidity (๐ฟ), over the entire history of the contract. You can think of them as the total amount of fees that would have been earned by 1 unit of unbounded liquidity that was deposited when the contract was first initialized. They are stored as fixed-point unsigned 128x128 numbers. Note that in Uniswap v3, fees are collected in the tokens themselves rather than in liquidity, for reasons explained in section 3.2.1. Finally, the global state tracks the total accumulated uncollected protocol fee in each token,protocolFees0 (๐๐,0) andprotocolFees1 (๐๐,1). This is an unsigned uint128. The accumulated protocol fees can be collected by UNI governance, by calling thecollectProtocol function. 6.2.3 Swapping Within a Single Tick. For small enough swaps, that do not move the price past a tick, the contracts act like an ๐ฅ ยท๐ฆ = ๐ pool. Suppose ๐พ is the fee, i.e., 0.003, and ๐ฆ๐๐ as the amount of token1 sent in. First, feeGrowthGlobal1 and protocolFees1 are incremented: ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท (1 โ ๐) (6.9) ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท๐ (6.10) ฮ๐ฆ is the increase in ๐ฆ (after the fee is taken out). 6Technically, the storage variable called โprotocolFee" is the denominator of this fraction (or is zero, if ๐ is zero). ฮ๐ฆ = ๐ฆ๐๐ ยท (1 โ ๐พ) (6.11) If you used the computed virtual reserves (๐ฅ and ๐ฆ) for thetoken0 and token1 balances, then this formula could be used to find the amount of token0 sent out: ๐ฅ๐๐๐ = ๐ฅ ยท๐ฆ ๐ฆ + ฮ๐ฆ (6.12) But remember that inv3, the contract actually tracks liquidity (๐ฟ) and square root of price ( โ ๐) instead of ๐ฅ and ๐ฆ. We could compute ๐ฅ and ๐ฆ from those values, and then use those to calculate the execution price of the trade. But it turns out that there are simple formulas that describe the relationship between ฮ โ ๐ and ฮ๐ฆ, for a given ๐ฟ (which can be derived from formula 6.7): ฮ โ ๐ = ฮ๐ฆ ๐ฟ (6.13) ฮ๐ฆ = ฮ โ ๐ ยท๐ฟ (6.14) There are also simple formulas that describe the relationship between ฮ 1โ ๐ and ฮ๐ฅ: ฮ 1โ ๐ = ฮ๐ฅ ๐ฟ (6.15) ฮ๐ฅ = ฮ 1โ ๐ ยท๐ฟ (6.16) When swapping one token for the other, the pool contract can first compute the new โ ๐ using formula 6.13 or 6.15, and then can compute the amount of token0 or token1 to send out using formula 6.14 or 6.16. These formulas will work for any swap that does not push โ ๐ past the price of the next initialized tick. If the computed ฮ โ ๐ would cause โ ๐ to move past that next initialized tick, the contract must only cross up to that tickโusing up only part of the swapโand then cross the tick, as described in section 6.3.1, before continuing with the rest of the swap. 6.2.4 Initialized Tick Bitmap. If a tick is not used as the endpoint of a range with any liquidity in itโthat is, if the tick is uninitial- izedโthen that tick can be skipped during swaps. As an optimization to make finding the next initialized tick more efficient, the pool tracks a bitmap tickBitmap of initialized ticks. The position in the bitmap that corresponds to the tick index is set to 1 if the tick is initialized, and 0 if it is not initialized. When a tick is used as an endpoint for a new position, and that tick is not currently used by any other liquidity, the tick is initialized, and the corresponding bit in the bitmap is set to 1. An initialized tick can become uninitialized again if all of the liquidity for which it is an endpoint is removed, in which case that tickโs position on the bitmap is zeroed out. 6.3 Tick-Indexed State The contract needs to store information about each tick in order to track the amount of net liquidity that should be added or removed when the tick is crossed, as well as to track the fees earned above and below that tick. 6
Uniswap v3 Core Start S0. Check input S1. Swap within current interval S2. Is there remaining input or output? S4. Cross next tick Stop S5. Execute computed swap Pass Fail Yes No Figure 4: Swap Control Flow The contract stores a mapping from tick indexes (int24) to the following seven values: Type Variable Name Notation int128 liquidityNet ฮ๐ฟ uint128 liquidityGross ๐ฟ๐ uint256 feeGrowthOutside0X128 ๐๐,0 uint256 feeGrowthOutside1X128 ๐๐,1 uint256 secondsOutside ๐ ๐ uint256 tickCumulativeOutside ๐๐ uint256 secondsPerLiquidityOutsideX128 ๐ ๐๐ Table 2: Tick-Indexed State Each tick tracks ฮ๐ฟ, the total amount of liquidity that should be kicked in or out when the tick is crossed. The tick only needs to track one signed integer: the amount of liquidity added (or, if negative, removed) when the tick is crossed going left to right. This value does not need to be updated when the tick is crossed (but only when a position with a bound at that tick is updated). We want to be able to uninitialize a tick when there is no longer any liquidity referencing that tick. Since ฮ๐ฟ is a net value, itโs necessary to track a gross tally of liquidity referencing the tick, liquidityGross. This value ensures that even if net liquidity at a tick is 0, we can still know if a tick is referenced by at least one underlying position or not, which tells us whether to update the tick bitmap. feeGrowthOutside{0,1} are used to track how many fees were accumulated within a given range. Since the formulas are the same for the fees collected in token0 and token1, we will omit that sub- script for the rest of this section. You can compute the fees earned per unit of liquidity in token 0 above (๐๐) and below (๐๐) a tick ๐ with a formula that depends on whether the price is currently within or outside that rangeโthat is, whether the current tick index ๐๐ is greater than or equal to ๐: ๐๐ (๐) = ( ๐๐ โ ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ (๐) ๐๐ < ๐ (6.17) ๐๐ (๐) = ( ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ โ ๐๐ (๐) ๐๐ < ๐ (6.18) We can use these functions to compute the total amount of cumulative fees per share ๐๐ in the range between two ticksโa lower tick ๐๐ and an upper tick ๐๐ข: ๐๐ = ๐๐ โ ๐๐ (๐๐ ) โ ๐๐ (๐๐ข ) (6.19) ๐๐ needs to be updated each time the tick is crossed. Specifically, as a tick๐ is crossed in either direction, its๐๐ (for each token) should be updated as follows: ๐๐ (๐) := ๐๐ โ ๐๐ (๐) (6.20) ๐๐ is only needed for ticks that are used as either the lower or upper bound for at least one position. As a result, for efficiency,๐๐ is not initialized (and thus does not need to be updated when crossed) until a position is created that has that tick as one of its bounds. When ๐๐ is initialized for a tick ๐, the valueโby conventionโis chosen as if all of the fees earned to date had occurred below that tick: ๐๐ := ( ๐๐ ๐๐ โฅ ๐ 0 ๐๐ < ๐ (6.21) Note that since ๐๐ values for different ticks could be initialized at different times, comparisons of the ๐๐ values for different ticks are not meaningful, and there is no guarantee that values for ๐๐ will be consistent. This does not cause a problem for per-position accounting, since, as described below, all the position needs to know is the growth in ๐ within a given range since that position was last touched. 7
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson Finally, the contract also stores secondsOutside (๐ ๐), secondsPerLiquidityOutside, andtickCumulativeOutside for each tick. These values are not used within the contract, but are tracked for the benefit of external contracts that need more fine- grained information about the poolโs behavior (for purposes like liquidity mining). All three of these indexes work similarly to the fee growth in- dexes described above. But where the feeGrowthOutside{0,1} indexes track feeGrowthGlobal{0,1}, the secondsOutside index tracks seconds (that is, the current timestamp), secondsPerLiquidityOutside tracks the 1/๐ฟ accumulator (secondsPerLiquidityCumulative) described in section 5.3, and tickCumulativeOutside tracks the log1.0001 ๐ accumulator de- scribed in section 5.2. For example, the seconds spent above (๐ ๐) and below (๐ ๐) a given tick is computed differently based on whether the current price is within that range, and the seconds spent within a range (๐ ๐ ) can be computed using the values of ๐ ๐ and ๐ ๐: ๐ก๐ (๐) = ( ๐ก โ ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก๐ (๐) ๐๐ < ๐ (6.22) ๐ก๐ (๐) = ( ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก โ ๐ก๐ (๐) ๐๐ < ๐ (6.23) ๐ก๐ (๐๐ , ๐๐ข ) = ๐ก โ ๐ก๐ (๐๐ ) โ ๐ก๐ (๐๐ข ) (6.24) The number of seconds spent within a range between two times ๐ก1 and ๐ก2 can be computed by recording the value of ๐ ๐ (๐๐ , ๐๐ข ) at ๐ก1 and at ๐ก2, and subtracting the former from the latter. Like ๐๐, ๐ ๐ does not need to be tracked for ticks that are not on the edge of any position. Therefore, it is not initialized until a position is created that is bounded by that tick. By convention, it is initialized as if every second since the Unix timestamp 0 had been spent below that tick: ๐ก๐ (๐) := ( ๐ก ๐ ๐ โฅ ๐ 0 ๐๐ < ๐ (6.25) As with ๐๐ values, ๐ก๐ values are not meaningfully comparable across different ticks. ๐ก๐ is only meaningful in computing the num- ber of seconds that liquidity was within some particular range between some defined start time (which must be after ๐ก๐ was ini- tialized for both ticks) and some end time. 6.3.1 Crossing a Tick. As described in section 6.2.3, Uniswap v3 acts like it obeys the constant product formula when swapping between initialized ticks. When a swap crosses an initialized tick, however, the contract needs to add or remove liquidity, to ensure that no liquidity provider is insolvent. This means theฮ๐ฟ is fetched from the tick, and applied to the global ๐ฟ. The contract also needs to update the tickโs own state, in order to track the fees earned (and seconds spent) within ranges bounded by this tick. The feeGrowthOutside{0,1} and secondsOutside values are updated to both reflect current values, as well as the proper orientation relative to the current tick: ๐๐ := ๐๐ โ ๐๐ (6.26) ๐ก๐ := ๐ก โ ๐ก๐ (6.27) Once a tick is crossed, the swap can continue as described in section 6.2.3 until it reaches the next initialized tick. 6.4 Position-Indexed State The contract has a mapping from user (an address), lower bound (a tick index, int24), and upper bound (a tick index, int24) to a specific Position struct. Each Position tracks three values: Type Variable Name Notation uint128 liquidity ๐ uint256 feeGrowthInside0LastX128 ๐๐,0 (๐ก0) uint256 feeGrowthInside1LastX128 ๐๐,1 (๐ก0) Table 3: Position-Indexed State liquidity (๐) means the amount of virtual liquidity that the position represented the last time this position was touched. Specif- ically, liquidity could be thought of as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are the respective amounts of virtual token0 and virtual token1 that this liquidity contributes to the pool at any time that it is within range. Unlike pool shares in Uniswap v2 (where the value of each share grows over time), the units for liquidity do not change as fees are accumulated; it is always measured as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are quantities of token0 and token1, respectively. This liquidity number does not reflect the fees that have been accumulated since the contract was last touched, which we will call uncollected fees . Computing these uncollected fees requires additional stored values on the position, feeGrowthInside0Last (๐๐,0 (๐ก0)) and feeGrowthInside1Last (๐๐,1 (๐ก0)), as described be- low. 6.4.1 setPosition. The setPosition function allows a liquidity provider to update their position. Two of the arguments tosetPosition โ lowerTick and upperTickโ when combined with the msg.sender, together specify a position. The function takes one additional parameter, liquidityDelta, to specify how much virtual liquidity the user wants to add or (if negative) remove. First, the function computes the uncollected fees ( ๐๐ข) that the position is entitled to, in each token.7 The amount collected in fees is credited to the user and netted against the amount that they would send in or out for their virtual liquidity deposit. To compute uncollected fees of a token, you need to know how much ๐๐ for the positionโs range (calculated from the rangeโs ๐๐ and ๐๐ as described in section 6.3) has grown since the last time fees were collected for that position. The growth in fees in a given range per unit of liquidity over between times ๐ก0 and ๐ก1 is simply ๐๐ (๐ก1) โ ๐๐ (๐ก0) (where ๐๐ (๐ก0) is stored in the position as feeGrowthInside{0,1}Last, and ๐๐ (๐ก1) can be computed from the current state of the ticks). Multiplying this by the positionโs liquidity gives us the total uncollected fees in token 0 for this position: 7Since the formulas for computing uncollected fees in each token are the same, we will omit that subscript for the rest of this section. 8
Uniswap v3 Core ๐๐ข = ๐ ยท (๐๐ (๐ก1) โ ๐๐ (๐ก0)) (6.28) Then, the contract updates the positionโsliquidity by adding liquidityDelta. It also addsliquidityDelta to theliquidityNet value for the tick at the bottom end of the range, and subtracts it from the liquidityNet at the upper tick (to reflect that this new liquidity would be added when the price crosses the lower tick going up, and subtracted when the price crosses the upper tick going up). If the poolโs current price is within the range of this position, the contract also adds liquidityDelta to the contractโs global liquidity value. Finally, the pool transfers tokens from (or, if liquidityDelta is negative, to) the user, corresponding to the amount of liquidity burned or minted. The amount of token0 (ฮ๐ ) or token1 (ฮ๐ ) that needs to be deposited can be thought of as the amount that would be sold from the position if the price were to move from the current price (๐) to the upper tick or lower tick (for token0 or token1, respectively). These formulas can be derived from formulas 6.14 and 6.16, and depend on whether the current price is below, within, or above the range of the position: ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ 0 ๐๐ < ๐๐ ฮ๐ฟ ยท ( โ ๐ โ p ๐ (๐๐ )) ๐๐ โค ๐๐ < ๐๐ข ฮ๐ฟ ยท ( p ๐ (๐๐ข ) โ p ๐ (๐๐ )) ๐๐ โฅ ๐๐ข (6.29) ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ ฮ๐ฟ ยท ( 1โ ๐ (๐๐ ) โ 1โ ๐ (๐๐ข ) ) ๐๐ < ๐๐ ฮ๐ฟ ยท ( 1โ ๐ โ 1โ ๐ (๐๐ข ) ) ๐๐ โค ๐๐ < ๐๐ข 0 ๐๐ โฅ ๐๐ข (6.30) REFERENCES [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, United States, 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. DISCLAIMER This paper is for general information purposes only. It does not constitute investment advice or a recommendation or solicitation to buy or sell any investment and should not be used in the evaluation of the merits of making any investment decision. It should not be relied upon for accounting, legal or tax advice or investment rec- ommendations. This paper reflects current opinions of the authors and is not made on behalf of Uniswap Labs, Paradigm, or their affiliates and does not necessarily reflect the opinions of Uniswap Labs, Paradigm, their affiliates or individuals associated with them. The opinions reflected herein are subject to change without being updated. 9
Uniswap v3 Core March 2021 Hayden Adams [email protected] Noah Zinsmeister [email protected] Moody Salem [email protected] River Keefer [email protected] Dan Robinson [email protected] ABSTRACT Uniswap v3 is a noncustodial automated market maker imple- mented for the Ethereum Virtual Machine. In comparison to earlier versions of the protocol, Uniswap v3 provides increased capital efficiency and fine-tuned control to liquidity providers, improves the accuracy and convenience of the price oracle, and has a more flexible fee structure. 1 INTRODUCTION Automated market makers (AMMs) are agents that pool liquidity and make it available to traders according to an algorithm [5]. Con- stant function market makers (CFMMs), a broad class of AMMs of which Uniswap is a member, have seen widespread use in the con- text of decentralized finance, where they are typically implemented as smart contracts that trade tokens on a permissionless blockchain [2]. CFMMs as they are implemented today are often capital inef- ficient. In the constant product market maker formula used by Uniswap v1 and v2, only a fraction of the assets in the pool are available at a given price. This is inefficient, particularly when assets are expected to trade close to a particular price at all times. Prior attempts to address this capital efficiency issue, such as Curve [3] and YieldSpace [4], have involved building pools that use different functions to describe the relation between reserves. This requires all liquidity providers in a given pool to adhere to a single formula, and could result in liquidity fragmentation if liquidity providers want to provide liquidity within different price ranges. In this paper, we present Uniswap v3, a novel AMM that gives liquidity providers more control over the price ranges in which their capital is used, with limited effect on liquidity fragmentation and gas inefficiency. This design does not depend on any shared assumption about the price behavior of the tokens. Uniswap v3 is based on the same constant product reserves curve as earlier versions [1], but offers several significant new features: โข Concentrated Liquidity: Liquidity providers (LPs) are given the ability to concentrate their liquidity by โbounding" it within an arbitrary price range. This improves the poolโs capital efficiency and allows LPs to approximate their pre- ferred reserves curve, while still being efficiently aggregated with the rest of the pool. We describe this feature in section 2 and its implementation in Section 6. โข Flexible Fees : The swap fee is no longer locked at 0.30%. Rather, the fee tier for each pool (of which there can be multiple per asset pair) is set on initialization (Section 3.1). The initially supported fee tiers are 0.05%, 0.30%, and 1%. UNI governance is able to add additional values to this set. โข Protocol Fee Governance: UNI governance has more flexibility in setting the fraction of swap fees collected by the protocol (Section 6.2.2). โข Improved Price Oracle: Uniswap v3 provides a way for users to query recent price accumulator values, thus avoiding the need to checkpoint the accumulator value at the exact be- ginning and end of the period for which a TWAP is being measured. (Section 5.1). 1
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson โข Liquidity Oracle: The contracts expose a time-weighted av- erage liquidity oracle (Section 5.3). The Uniswap v2 core contracts are non-upgradeable by de- sign, so Uniswap v3 is implemented as an entirely new set of contracts, available here. The Uniswap v3 core contracts are also non-upgradeable, with some parameters controlled by governance as described in Section 4. 2 CONCENTRATED LIQUIDITY The defining idea of Uniswap v3 is that of concentrated liquidity: liquidity bounded within some price range. In earlier versions, liquidity was distributed uniformly along the ๐ฅ ยท๐ฆ = ๐ (2.1) reserves curve, where ๐ฅ and ๐ฆ are the respective reserves of two assets X and Y, and ๐ is a constant [1]. In other words, earlier ver- sions were designed to provide liquidity across the entire price range (0, โ). This is simple to implement and allows liquidity to be efficiently aggregated, but means that much of the assets held in a pool are never touched. Having considered this, it seems reasonable to allow LPs to concentrate their liquidity to smaller price ranges than (0, โ). We call liquidity concentrated to a finite range a position. A position only needs to maintain enough reserves to support trading within its range, and therefore can act like a constant product pool with larger reserves (we call these the virtual reserves) within that range. ๐ ๐ ๐ ๐ฆreal ๐ฅreal X Reserves Y Reserves virtual reserves Figure 1: Simulation of Virtual Liquidity Specifically, a position only needs to hold enough of asset X to cover price movement to its upper bound, because upwards price movement1 corresponds to depletion of the X reserves. Similarly, it only needs to hold enough of asset Y to cover price movement to its lower bound. Fig. 1 depicts this relationship for a position on a range [๐๐, ๐๐ ] and a current price ๐๐ โ [ ๐๐, ๐๐ ]. ๐ฅreal and ๐ฆreal denote the positionโs real reserves. When the price exits a positionโs range, the positionโs liquidity is no longer active, and no longer earns fees. At that point, its 1We take asset Y to be the unit of account, which corresponds to token1 in our implementation. liquidity is composed entirely of a single asset, because the reserves of the other asset must have been entirely depleted. If the price ever reenters the range, the liquidity becomes active again. The amount of liquidity provided can be measured by the value ๐ฟ, which is equal to โ ๐. The real reserves of a position are described by the curve: (๐ฅ + ๐ฟโ๐๐ ) (๐ฆ + ๐ฟโ๐๐) = ๐ฟ2 (2.2) This curve is a translation of formula 2.1 such that the position is solvent exactly within its range (Fig. 2). ๐ ๐ X Reserves Y Reserves virtual reserves (2.1) real reserves (2.2) Figure 2: Real Reserves Liquidity providers are free to create as many positions as they see fit, each on its own price range. In this way, LPs can approximate any desired distribution of liquidity on the price space (see Fig. 3 for a few examples). Moreover, this serves as a mechanism to let the market decide where liquidity should be allocated. Rational LPs can reduce their capital costs by concentrating their liquidity in a narrow band around the current price, and adding or removing tokens as the price moves to keep their liquidity active. 2.1 Range Orders Positions on very small ranges act similarly to limit ordersโif the range is crossed, the position flips from being composed entirely of one asset, to being composed entirely of the other asset (plus accrued fees). There are two differences between this range order and a traditional limit order: โข There is a limit to how narrow a positionโs range can be. While the price is within that range, the limit order might be partially executed. โข When the position has been crossed, it needs to be with- drawn. If it is not, and the price crosses back across that range, the position will be traded back, effectively reversing the trade. 2
Uniswap v3 Core 0 โ Price Liquidity (I) Uniswap v2 ๐๐ ๐๐ Price Liquidity (II) A single position on [๐๐, ๐๐ ] Price Liquidity (III) A collection of custom positions Figure 3: Example Liquidity Distributions 3 ARCHITECTURAL CHANGES Uniswap v3 makes a number of architectural changes, some of which are necessitated by the inclusion of concentrated liquidity, and some of which are independent improvements. 3.1 Multiple Pools Per Pair In Uniswap v1 and v2, every pair of tokens corresponds to a single liquidity pool, which applies a uniform fee of 0.30% to all swaps. While this default fee tier historically worked well enough for many tokens, it is likely too high for some pools (such as pools between two stablecoins), and too low for others (such as pools that include highly volatile or rarely traded tokens). Uniswap v3 introduces multiple pools for each pair of tokens, each with a different swap fee. All pools are created by the same factory contract. The factory contract initially allows pools to be created at three fee tiers: 0.05%, 0.30%, and 1%. Additional fee tiers can be enabled by UNI governance. 3.2 Non-Fungible Liquidity 3.2.1 Non-Compounding Fees. Fees earned in earlier versions were continuously deposited in the pool as liquidity. This meant that liquidity in the pool would grow over time, even without explicit deposits, and that fee earnings compounded. In Uniswap v3, due to the non-fungible nature of positions, this is no longer possible. Instead, fee earnings are stored separately and held as the tokens in which the fees are paid (see Section 6.2.2). 3.2.2 Removal of Native Liquidity Tokens. In Uniswap v1 and v2, the pool contract is also an ERC-20 token contract, whose tokens represent liquidity held in the pool. While this is convenient, it actually sits uneasily with the Uniswap v2 philosophy that any- thing that does not need to be in the core contracts should be in the periphery, and blessing one โcanonical" ERC-20 implementation discourages the creation of improved ERC-20 token wrappers. Ar- guably, the ERC-20 token implementation should have been in the periphery, as a wrapper on a single liquidity position in the core contract. The changes made in Uniswap v3 force this issue by making completely fungible liquidity tokens impossible. Due to the custom liquidity provision feature, fees are now collected and held by the pool as individual tokens, rather than automatically reinvested as liquidity in the pool. As a result, in v3, the pool contract does not implement the ERC-20 standard. Anyone can create an ERC-20 token contract in the periphery that makes a liquidity position more fungible, but it will have to have additional logic to handle distribution of, or reinvestment of, collected fees. Alternatively, anyone could create a periphery contract that wraps an individual liquidity position (including collected fees) in an ERC-721 non-fungible token. 4 GOVERNANCE The factory has an owner, which is initially controlled by UNI tokenholders.2 The owner does not have the ability to halt the operation of any of the core contracts. As in Uniswap v2, Uniswap v3 has a protocol fee that can be turned on by UNI governance. In Uniswap v3, UNI governance has more flexibility in choosing the fraction of swap fees that go to the protocol, and is able to choose any fraction 1 ๐ where 4 โค ๐ โค 10, or 0. This parameter can be set on a per-pool basis. UNI governance also has the ability to add additional fee tiers. When it adds a new fee tier, it can also define the tickSpacing (see Section 6.1) corresponding to that fee tier. Once a fee tier is added to the factory, it cannot be removed (and the tickSpacing cannot be changed). The initial fee tiers and tick spacings supported are 0.05% (with a tick spacing of 10, approximately 0.10% between initializable ticks), 0.30% (with a tick spacing of 60, approximately 0.60% between initializable ticks), and 1% (with a tick spacing of 200, approximately 2.02% between ticks. Finally, UNI governance has the power to transfer ownership to another address. 5 ORACLE UPGRADES Uniswap v3includes three significant changes to the time-weighted average price (TWAP) oracle that was introduced by Uniswap v2. Most significantly, Uniswap v3 removes the need for users of the oracle to track previous values of the accumulator externally. Uniswap v2 requires users to checkpoint the accumulator value at both the beginning and end of the time period for which they 2Specifically, the owner will be initialized to the Timelock contract from UNI gover- nance, 0x1a9c8182c09f50c8318d769245bea52c32be35bc. 3
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson wanted to compute a TWAP. Uniswap v3 brings the accumulator checkpoints into core, allowing external contracts to compute on- chain TWAPs over recent periods without storing checkpoints of the accumulator value. Another change is that instead of accumulating the sum of prices, allowing users to compute the arithmetic mean TWAP,Uniswap v3 tracks the sum of log prices, allowing users to compute the geometric mean TWAP. Finally, Uniswap v3 adds a liquidity accumulator that is tracked alongside the price accumulator, which accumulates 1 ๐ฟ for each second. This liquidity accumulator is useful for external contracts that want to implement liquidity mining on top of Uniswap v3. It can also be used by other contracts to inform a decision on which of the pools corresponding to a pair (see section 3.1) will have the most reliable TWAP. 5.1 Oracle Observations As in Uniswap v2, Uniswap v3 tracks a running accumulator of the price at the beginning of each block, multiplied by the number of seconds since the last block. A pool in Uniswap v2 stores only the most recent value of this price accumulatorโthat is, the value as of the last block in which a swap occurred. When computing average prices in Uniswap v2, it is the responsibility of the external caller to provide the previous value of the price accumulator. With many users, each will have to provide their own methodology for checkpointing previous values of the accumulator, or coordinate on a shared method to reduce costs. And there is no way to guarantee that every block in which the pool is touched will be reflected in the accumulator. In Uniswap v3, the pool stores a list of previous values for the price accumulator (as well as the liquidity accumulator described in section 5.3). It does this by automatically checkpointing the accumulator value every time the pool is touched for the first time in a block, cycling through an array where the oldest checkpoint is eventually overwritten by a new one, similar to a circular buffer. While this array initially only has room for a single checkpoint, anyone can initialize additional storage slots to lengthen the array, extending to as many as 65,536 checkpoints. 3 This imposes the one-time gas cost of initializing additional storage slots for this array on whoever wants this pair to checkpoint more slots. The pool exposes the array of past observations to users, as well as a convenience function for finding the (interpolated) accumulator value at any historical timestamp within the checkpointed period. 5.2 Geometric Mean Price Oracle Uniswap v2 maintains two price accumulatorsโone for the price of token0 in terms of token1, and one for the price oftoken1 in terms of token0. Users can compute the time-weighted arithmetic mean of the prices over any period, by subtracting the accumulator value at the beginning of the period from the accumulator at the end of the period, then dividing the difference by the number of seconds in the period. Note that accumulators for token0 and token1 are tracked separately, since the time-weighted arithmetic mean price 3The maximum of 65,536 checkpoints allows fetching checkpoints for at least 9 days after they are written, assuming 13 seconds pass between each block and a checkpoint is written every block. of token0 is not equivalent to the reciprocal of the time-weighted arithmetic mean price of token1. Using the time-weighted geometric mean price, as Uniswap v3 does, avoids the need to track separate accumulators for these ratios. The geometric mean of a set of ratios is the reciprocal of the geometric mean of their reciprocals. It is also easy to implement in Uniswap v3 because of its implementation of custom liquidity provision, as described in section 6. In addition, the accumulator can be stored in a smaller number of bits, since it trackslog ๐ rather than ๐, and log ๐ can represent a wide range of prices with consistent precision.4 Finally, there is a theoretical argument that the time- weighted geometric mean price should be a truer representation of the average price.5 Instead of tracking the cumulative sum of the price ๐, Uniswap v3 accumulates the cumulative sum of the current tick index (๐๐๐1.0001๐, the logarithm of price for base 1.0001, which is precise up to 1 basis point). The accumulator at any given time is equal to the sum of ๐๐๐1.0001 (๐) for every second in the history of the contract: ๐๐ก = ๐กร ๐=1 log1.0001 (๐๐ ) (5.1) We want to estimate the geometric mean time-weighted average price (๐๐ก1,๐ก2) over any period ๐ก1 to ๐ก2. ๐๐ก1,๐ก2 = ยฉยญ ยซ ๐ก2ร ๐=๐ก1 ๐๐ ยชยฎ ยฌ 1 ๐ก2โ๐ก1 (5.2) To compute this, you can look at the accumulatorโs value at๐ก1 and at ๐ก2, subtract the first value from the second, divide by the number of seconds elapsed, and compute 1.0001๐ฅ to compute the time weighted geometric mean price. log1.0001 ๐๐ก1,๐ก2 = ร๐ก2 ๐=๐ก1 log1.0001 (๐๐ ) ๐ก2 โ ๐ก1 (5.3) log1.0001 ๐๐ก1,๐ก2 = ๐๐ก2 โ ๐๐ก1 ๐ก2 โ ๐ก1 (5.4) ๐๐ก1,๐ก2 = 1.0001 ๐๐ก2 โ๐๐ก1 ๐ก2โ๐ก1 (5.5) 5.3 Liquidity Oracle In addition to the seconds-weighted accumulator of log1.0001 ๐๐๐๐๐ , Uniswap v3 also tracks a seconds-weighted accumulator of 1 ๐ฟ (the reciprocal of the virtual liquidity currently in range) at the begin- ning of each block: secondsPerLiquidityCumulative (๐ ๐๐ ). This can be used by external liquidity mining contracts to fairly allocate rewards. If an external contract wants to distribute rewards at an even rate of ๐ tokens per second to all active liquidity in the 4In order to support tolerable precision across all possible prices, Uniswap v2 repre- sents each price as a 224-bit fixed-point number. Uniswap v3 only needs to represent ๐๐๐ 1.0001๐ as a signed 24-bit number, and still can detect price movements of one tick, or 1 basis point. 5While arithmetic mean TWAPs are much more widely used, they should theoretically be less accurate in measuring a geometric Brownian motion process (which is how price movements are usually modeled). The arithmetic mean of a geometric Brownian motion process will tend to overweight higher prices (where small percentage movements correspond to large absolute movements) relative to lower ones. 4
Economics and Governance
Uniswap v3 Core contract, and a position with ๐ฟ liquidity was active from ๐ก0 to ๐ก1, then its rewards for that period would be ๐ ยทLยท(๐ ๐๐ (๐ก1) โ ๐ ๐๐ (๐ก0)). In order to extend this so that concentrated liquidity is rewarded only when it is in range,Uniswap v3 stores a computed checkpoint based on this value every time a tick is crossed, as described in section 6.3. This accumulator can also be used by on-chain contracts to make their oracles stronger (such as by evaluating which fee-tier pool to use the oracle from). 6 IMPLEMENTING CONCENTRATED LIQUIDITY The rest of this paper describes how concentrated liquidity provi- sion works, and gives a high-level description of how it is imple- mented in the contracts. 6.1 Ticks and Ranges To implement custom liquidity provision, the space of possible prices is demarcated by discrete ticks. Liquidity providers can pro- vide liquidity in a range between any two ticks (which need not be adjacent). Each range can be specified as a pair of signed integertick indices: a lower tick (๐๐ ) and an upper tick ( ๐๐ข). Ticks represent prices at which the virtual liquidity of the contract can change. We will assume that prices are always expressed as the price of one of the tokensโcalled token0โin terms of the other tokenโ token1. The assignment of the two tokens to token0 and token1 is arbitrary and does not affect the logic of the contract (other than through possible rounding errors). Conceptually, there is a tick at every price ๐ that is an integer power of 1.0001. Identifying ticks by an integer index ๐, the price at each is given by: ๐ (๐) = 1.0001๐ (6.1) This has the desirable property of each tick being a .01% (1 basis point) price movement away from each of its neighboring ticks. For technical reasons explained in 6.2.1, however, pools actually track ticks at every square root price that is an integer power ofโ 1.0001. Consider the above equation, transformed into square root price space: โ๐ (๐) = โ 1.0001 ๐ = 1.0001 ๐ 2 (6.2) As an example,โ๐ (0)โthe square root price at tick 0โis 1, โ๐ (1) is โ 1.0001 โ 1.00005, and โ๐ (โ1) is 1โ 1.0001 โ 0.99995. When liquidity is added to a range, if one or both of the ticks is not already used as a bound in an existing position, that tick is initialized. Not every tick can be initialized. The pool is instantiated with a parameter, tickSpacing (๐ก๐ ); only ticks with indexes that are divisi- ble by tickSpacing can be initialized. For example, iftickSpacing is 2, then only even ticks (...-4, -2, 0, 2, 4...) can be initialized. Small choices for tickSpacing allow tighter and more precise ranges, but may cause swaps to be more gas-intensive (since each initialized tick that a swap crosses imposes a gas cost on the swapper). Whenever the price crosses an initialized tick, virtual liquidity is kicked in or out. The gas cost of an initialized tick crossing is constant, and is not dependent on the number of positions being kicked in or out at that tick. Ensuring that the right amount of liquidity is kicked in and out of the pool when ticks are crossed, and ensuring that each position earns its proportional share of the fees that were accrued while it was within range, requires some accounting within the pool. The pool contract uses storage variables to track state at a global (per-pool) level, at a per-tick level, and at a per-position level. 6.2 Global State The global state of the contract includes seven storage variables relevant to swaps and liquidity provision. (It has other storage variables that are used for the oracle, as described in section 5.) Type Variable Name Notation uint128 liquidity ๐ฟ uint160 sqrtPriceX96 โ ๐ int24 tick ๐๐ uint256 feeGrowthGlobal0X128 ๐๐,0 uint256 feeGrowthGlobal1X128 ๐๐,1 uint128 protocolFees.token0 ๐๐,0 uint128 protocolFees.token1 ๐๐,1 Table 1: Global State 6.2.1 Price and Liquidity. In Uniswap v2, each pool contract tracks the poolโs current reserves,๐ฅ and ๐ฆ. In Uniswap v3, the contract could be thought of as having virtual reservesโvalues for ๐ฅ and ๐ฆ that allow you to describe the contractโs behavior (between two adjacent ticks) as if it followed the constant product formula. Instead of tracking those virtual reserves, however, the pool contract tracks two different values:liquidity (๐ฟ) and sqrtPrice ( โ ๐). These could be computed from the virtual reserves with the following formulas: ๐ฟ = โ๐ฅ๐ฆ (6.3) โ ๐ = r๐ฆ ๐ฅ (6.4) Conversely, these values could be used to compute the virtual reserves: ๐ฅ = ๐ฟโ ๐ (6.5) ๐ฆ = ๐ฟ ยท โ ๐ (6.6) Using ๐ฟ and โ ๐ is convenient because only one of them changes at a time. Price (and thus โ ๐) changes when swapping within a tick; liquidity changes when crossing a tick, or when minting or burning liquidity. This avoids some rounding errors that could be encountered if tracking virtual reserves. You may notice that the formula for liquidity (based on virtual reserves) is similar to the formula used to initialize the quantity of liquidity tokens (based on actual reserves) in Uniswap v2. before 5
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson any fees have been earned. In some ways, liquidity can be thought of as virtual liquidity tokens. Alternatively, liquidity can be thought of as the amount that token1 reserves (either actual or virtual) changes for a given change in โ ๐: ๐ฟ = ฮ๐ ฮ โ ๐ (6.7) We track โ ๐ instead of ๐ to take advantage of this relationship, and to avoid having to take any square roots when computing swaps, as described in section 6.2.3. The global state also tracks the current tick index as tick (๐๐), a signed integer representing the current tick (more specifically, the nearest tick below the current price). This is an optimization (and a way of avoiding precision issues with logarithms), since at any time, you should be able to compute the current tick based on the current sqrtPrice. Specifically, at any given time, the following equation should be true: ๐๐ = j logโ 1.0001 โ ๐ k (6.8) 6.2.2 Fees. Each pool is initialized with an immutable value, fee (๐พ), representing the fee paid by swappers in units of hundredths of a basis point (0.0001%). It also tracks the current protocol fee, ๐ (which is initialized to zero, but can changed by UNI governance).6 This number gives you the fraction of the fees paid by swappers that currently goes to the protocol rather than to liquidity providers. ๐ only has a limited set of permitted values: 0, 1/4, 1/5, 1/6, 1/7, 1/8, 1/9, or 1/10. The global state also tracks two numbers: feeGrowthGlobal0 (๐๐,0) andfeeGrowthGlobal1 (๐๐,1). These represent the total amount of fees that have been earned per unit of virtual liquidity (๐ฟ), over the entire history of the contract. You can think of them as the total amount of fees that would have been earned by 1 unit of unbounded liquidity that was deposited when the contract was first initialized. They are stored as fixed-point unsigned 128x128 numbers. Note that in Uniswap v3, fees are collected in the tokens themselves rather than in liquidity, for reasons explained in section 3.2.1. Finally, the global state tracks the total accumulated uncollected protocol fee in each token,protocolFees0 (๐๐,0) andprotocolFees1 (๐๐,1). This is an unsigned uint128. The accumulated protocol fees can be collected by UNI governance, by calling thecollectProtocol function. 6.2.3 Swapping Within a Single Tick. For small enough swaps, that do not move the price past a tick, the contracts act like an ๐ฅ ยท๐ฆ = ๐ pool. Suppose ๐พ is the fee, i.e., 0.003, and ๐ฆ๐๐ as the amount of token1 sent in. First, feeGrowthGlobal1 and protocolFees1 are incremented: ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท (1 โ ๐) (6.9) ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท๐ (6.10) ฮ๐ฆ is the increase in ๐ฆ (after the fee is taken out). 6Technically, the storage variable called โprotocolFee" is the denominator of this fraction (or is zero, if ๐ is zero). ฮ๐ฆ = ๐ฆ๐๐ ยท (1 โ ๐พ) (6.11) If you used the computed virtual reserves (๐ฅ and ๐ฆ) for thetoken0 and token1 balances, then this formula could be used to find the amount of token0 sent out: ๐ฅ๐๐๐ = ๐ฅ ยท๐ฆ ๐ฆ + ฮ๐ฆ (6.12) But remember that inv3, the contract actually tracks liquidity (๐ฟ) and square root of price ( โ ๐) instead of ๐ฅ and ๐ฆ. We could compute ๐ฅ and ๐ฆ from those values, and then use those to calculate the execution price of the trade. But it turns out that there are simple formulas that describe the relationship between ฮ โ ๐ and ฮ๐ฆ, for a given ๐ฟ (which can be derived from formula 6.7): ฮ โ ๐ = ฮ๐ฆ ๐ฟ (6.13) ฮ๐ฆ = ฮ โ ๐ ยท๐ฟ (6.14) There are also simple formulas that describe the relationship between ฮ 1โ ๐ and ฮ๐ฅ: ฮ 1โ ๐ = ฮ๐ฅ ๐ฟ (6.15) ฮ๐ฅ = ฮ 1โ ๐ ยท๐ฟ (6.16) When swapping one token for the other, the pool contract can first compute the new โ ๐ using formula 6.13 or 6.15, and then can compute the amount of token0 or token1 to send out using formula 6.14 or 6.16. These formulas will work for any swap that does not push โ ๐ past the price of the next initialized tick. If the computed ฮ โ ๐ would cause โ ๐ to move past that next initialized tick, the contract must only cross up to that tickโusing up only part of the swapโand then cross the tick, as described in section 6.3.1, before continuing with the rest of the swap. 6.2.4 Initialized Tick Bitmap. If a tick is not used as the endpoint of a range with any liquidity in itโthat is, if the tick is uninitial- izedโthen that tick can be skipped during swaps. As an optimization to make finding the next initialized tick more efficient, the pool tracks a bitmap tickBitmap of initialized ticks. The position in the bitmap that corresponds to the tick index is set to 1 if the tick is initialized, and 0 if it is not initialized. When a tick is used as an endpoint for a new position, and that tick is not currently used by any other liquidity, the tick is initialized, and the corresponding bit in the bitmap is set to 1. An initialized tick can become uninitialized again if all of the liquidity for which it is an endpoint is removed, in which case that tickโs position on the bitmap is zeroed out. 6.3 Tick-Indexed State The contract needs to store information about each tick in order to track the amount of net liquidity that should be added or removed when the tick is crossed, as well as to track the fees earned above and below that tick. 6
Uniswap v3 Core Start S0. Check input S1. Swap within current interval S2. Is there remaining input or output? S4. Cross next tick Stop S5. Execute computed swap Pass Fail Yes No Figure 4: Swap Control Flow The contract stores a mapping from tick indexes (int24) to the following seven values: Type Variable Name Notation int128 liquidityNet ฮ๐ฟ uint128 liquidityGross ๐ฟ๐ uint256 feeGrowthOutside0X128 ๐๐,0 uint256 feeGrowthOutside1X128 ๐๐,1 uint256 secondsOutside ๐ ๐ uint256 tickCumulativeOutside ๐๐ uint256 secondsPerLiquidityOutsideX128 ๐ ๐๐ Table 2: Tick-Indexed State Each tick tracks ฮ๐ฟ, the total amount of liquidity that should be kicked in or out when the tick is crossed. The tick only needs to track one signed integer: the amount of liquidity added (or, if negative, removed) when the tick is crossed going left to right. This value does not need to be updated when the tick is crossed (but only when a position with a bound at that tick is updated). We want to be able to uninitialize a tick when there is no longer any liquidity referencing that tick. Since ฮ๐ฟ is a net value, itโs necessary to track a gross tally of liquidity referencing the tick, liquidityGross. This value ensures that even if net liquidity at a tick is 0, we can still know if a tick is referenced by at least one underlying position or not, which tells us whether to update the tick bitmap. feeGrowthOutside{0,1} are used to track how many fees were accumulated within a given range. Since the formulas are the same for the fees collected in token0 and token1, we will omit that sub- script for the rest of this section. You can compute the fees earned per unit of liquidity in token 0 above (๐๐) and below (๐๐) a tick ๐ with a formula that depends on whether the price is currently within or outside that rangeโthat is, whether the current tick index ๐๐ is greater than or equal to ๐: ๐๐ (๐) = ( ๐๐ โ ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ (๐) ๐๐ < ๐ (6.17) ๐๐ (๐) = ( ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ โ ๐๐ (๐) ๐๐ < ๐ (6.18) We can use these functions to compute the total amount of cumulative fees per share ๐๐ in the range between two ticksโa lower tick ๐๐ and an upper tick ๐๐ข: ๐๐ = ๐๐ โ ๐๐ (๐๐ ) โ ๐๐ (๐๐ข ) (6.19) ๐๐ needs to be updated each time the tick is crossed. Specifically, as a tick๐ is crossed in either direction, its๐๐ (for each token) should be updated as follows: ๐๐ (๐) := ๐๐ โ ๐๐ (๐) (6.20) ๐๐ is only needed for ticks that are used as either the lower or upper bound for at least one position. As a result, for efficiency,๐๐ is not initialized (and thus does not need to be updated when crossed) until a position is created that has that tick as one of its bounds. When ๐๐ is initialized for a tick ๐, the valueโby conventionโis chosen as if all of the fees earned to date had occurred below that tick: ๐๐ := ( ๐๐ ๐๐ โฅ ๐ 0 ๐๐ < ๐ (6.21) Note that since ๐๐ values for different ticks could be initialized at different times, comparisons of the ๐๐ values for different ticks are not meaningful, and there is no guarantee that values for ๐๐ will be consistent. This does not cause a problem for per-position accounting, since, as described below, all the position needs to know is the growth in ๐ within a given range since that position was last touched. 7
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson Finally, the contract also stores secondsOutside (๐ ๐), secondsPerLiquidityOutside, andtickCumulativeOutside for each tick. These values are not used within the contract, but are tracked for the benefit of external contracts that need more fine- grained information about the poolโs behavior (for purposes like liquidity mining). All three of these indexes work similarly to the fee growth in- dexes described above. But where the feeGrowthOutside{0,1} indexes track feeGrowthGlobal{0,1}, the secondsOutside index tracks seconds (that is, the current timestamp), secondsPerLiquidityOutside tracks the 1/๐ฟ accumulator (secondsPerLiquidityCumulative) described in section 5.3, and tickCumulativeOutside tracks the log1.0001 ๐ accumulator de- scribed in section 5.2. For example, the seconds spent above (๐ ๐) and below (๐ ๐) a given tick is computed differently based on whether the current price is within that range, and the seconds spent within a range (๐ ๐ ) can be computed using the values of ๐ ๐ and ๐ ๐: ๐ก๐ (๐) = ( ๐ก โ ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก๐ (๐) ๐๐ < ๐ (6.22) ๐ก๐ (๐) = ( ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก โ ๐ก๐ (๐) ๐๐ < ๐ (6.23) ๐ก๐ (๐๐ , ๐๐ข ) = ๐ก โ ๐ก๐ (๐๐ ) โ ๐ก๐ (๐๐ข ) (6.24) The number of seconds spent within a range between two times ๐ก1 and ๐ก2 can be computed by recording the value of ๐ ๐ (๐๐ , ๐๐ข ) at ๐ก1 and at ๐ก2, and subtracting the former from the latter. Like ๐๐, ๐ ๐ does not need to be tracked for ticks that are not on the edge of any position. Therefore, it is not initialized until a position is created that is bounded by that tick. By convention, it is initialized as if every second since the Unix timestamp 0 had been spent below that tick: ๐ก๐ (๐) := ( ๐ก ๐ ๐ โฅ ๐ 0 ๐๐ < ๐ (6.25) As with ๐๐ values, ๐ก๐ values are not meaningfully comparable across different ticks. ๐ก๐ is only meaningful in computing the num- ber of seconds that liquidity was within some particular range between some defined start time (which must be after ๐ก๐ was ini- tialized for both ticks) and some end time. 6.3.1 Crossing a Tick. As described in section 6.2.3, Uniswap v3 acts like it obeys the constant product formula when swapping between initialized ticks. When a swap crosses an initialized tick, however, the contract needs to add or remove liquidity, to ensure that no liquidity provider is insolvent. This means theฮ๐ฟ is fetched from the tick, and applied to the global ๐ฟ. The contract also needs to update the tickโs own state, in order to track the fees earned (and seconds spent) within ranges bounded by this tick. The feeGrowthOutside{0,1} and secondsOutside values are updated to both reflect current values, as well as the proper orientation relative to the current tick: ๐๐ := ๐๐ โ ๐๐ (6.26) ๐ก๐ := ๐ก โ ๐ก๐ (6.27) Once a tick is crossed, the swap can continue as described in section 6.2.3 until it reaches the next initialized tick. 6.4 Position-Indexed State The contract has a mapping from user (an address), lower bound (a tick index, int24), and upper bound (a tick index, int24) to a specific Position struct. Each Position tracks three values: Type Variable Name Notation uint128 liquidity ๐ uint256 feeGrowthInside0LastX128 ๐๐,0 (๐ก0) uint256 feeGrowthInside1LastX128 ๐๐,1 (๐ก0) Table 3: Position-Indexed State liquidity (๐) means the amount of virtual liquidity that the position represented the last time this position was touched. Specif- ically, liquidity could be thought of as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are the respective amounts of virtual token0 and virtual token1 that this liquidity contributes to the pool at any time that it is within range. Unlike pool shares in Uniswap v2 (where the value of each share grows over time), the units for liquidity do not change as fees are accumulated; it is always measured as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are quantities of token0 and token1, respectively. This liquidity number does not reflect the fees that have been accumulated since the contract was last touched, which we will call uncollected fees . Computing these uncollected fees requires additional stored values on the position, feeGrowthInside0Last (๐๐,0 (๐ก0)) and feeGrowthInside1Last (๐๐,1 (๐ก0)), as described be- low. 6.4.1 setPosition. The setPosition function allows a liquidity provider to update their position. Two of the arguments tosetPosition โ lowerTick and upperTickโ when combined with the msg.sender, together specify a position. The function takes one additional parameter, liquidityDelta, to specify how much virtual liquidity the user wants to add or (if negative) remove. First, the function computes the uncollected fees ( ๐๐ข) that the position is entitled to, in each token.7 The amount collected in fees is credited to the user and netted against the amount that they would send in or out for their virtual liquidity deposit. To compute uncollected fees of a token, you need to know how much ๐๐ for the positionโs range (calculated from the rangeโs ๐๐ and ๐๐ as described in section 6.3) has grown since the last time fees were collected for that position. The growth in fees in a given range per unit of liquidity over between times ๐ก0 and ๐ก1 is simply ๐๐ (๐ก1) โ ๐๐ (๐ก0) (where ๐๐ (๐ก0) is stored in the position as feeGrowthInside{0,1}Last, and ๐๐ (๐ก1) can be computed from the current state of the ticks). Multiplying this by the positionโs liquidity gives us the total uncollected fees in token 0 for this position: 7Since the formulas for computing uncollected fees in each token are the same, we will omit that subscript for the rest of this section. 8
Uniswap v3 Core ๐๐ข = ๐ ยท (๐๐ (๐ก1) โ ๐๐ (๐ก0)) (6.28) Then, the contract updates the positionโsliquidity by adding liquidityDelta. It also addsliquidityDelta to theliquidityNet value for the tick at the bottom end of the range, and subtracts it from the liquidityNet at the upper tick (to reflect that this new liquidity would be added when the price crosses the lower tick going up, and subtracted when the price crosses the upper tick going up). If the poolโs current price is within the range of this position, the contract also adds liquidityDelta to the contractโs global liquidity value. Finally, the pool transfers tokens from (or, if liquidityDelta is negative, to) the user, corresponding to the amount of liquidity burned or minted. The amount of token0 (ฮ๐ ) or token1 (ฮ๐ ) that needs to be deposited can be thought of as the amount that would be sold from the position if the price were to move from the current price (๐) to the upper tick or lower tick (for token0 or token1, respectively). These formulas can be derived from formulas 6.14 and 6.16, and depend on whether the current price is below, within, or above the range of the position: ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ 0 ๐๐ < ๐๐ ฮ๐ฟ ยท ( โ ๐ โ p ๐ (๐๐ )) ๐๐ โค ๐๐ < ๐๐ข ฮ๐ฟ ยท ( p ๐ (๐๐ข ) โ p ๐ (๐๐ )) ๐๐ โฅ ๐๐ข (6.29) ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ ฮ๐ฟ ยท ( 1โ ๐ (๐๐ ) โ 1โ ๐ (๐๐ข ) ) ๐๐ < ๐๐ ฮ๐ฟ ยท ( 1โ ๐ โ 1โ ๐ (๐๐ข ) ) ๐๐ โค ๐๐ < ๐๐ข 0 ๐๐ โฅ ๐๐ข (6.30) REFERENCES [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, United States, 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. DISCLAIMER This paper is for general information purposes only. It does not constitute investment advice or a recommendation or solicitation to buy or sell any investment and should not be used in the evaluation of the merits of making any investment decision. It should not be relied upon for accounting, legal or tax advice or investment rec- ommendations. This paper reflects current opinions of the authors and is not made on behalf of Uniswap Labs, Paradigm, or their affiliates and does not necessarily reflect the opinions of Uniswap Labs, Paradigm, their affiliates or individuals associated with them. The opinions reflected herein are subject to change without being updated. 9
Uniswap v3 Core March 2021 Hayden Adams [email protected] Noah Zinsmeister [email protected] Moody Salem [email protected] River Keefer [email protected] Dan Robinson [email protected] ABSTRACT Uniswap v3 is a noncustodial automated market maker imple- mented for the Ethereum Virtual Machine. In comparison to earlier versions of the protocol, Uniswap v3 provides increased capital efficiency and fine-tuned control to liquidity providers, improves the accuracy and convenience of the price oracle, and has a more flexible fee structure. 1 INTRODUCTION Automated market makers (AMMs) are agents that pool liquidity and make it available to traders according to an algorithm [5]. Con- stant function market makers (CFMMs), a broad class of AMMs of which Uniswap is a member, have seen widespread use in the con- text of decentralized finance, where they are typically implemented as smart contracts that trade tokens on a permissionless blockchain [2]. CFMMs as they are implemented today are often capital inef- ficient. In the constant product market maker formula used by Uniswap v1 and v2, only a fraction of the assets in the pool are available at a given price. This is inefficient, particularly when assets are expected to trade close to a particular price at all times. Prior attempts to address this capital efficiency issue, such as Curve [3] and YieldSpace [4], have involved building pools that use different functions to describe the relation between reserves. This requires all liquidity providers in a given pool to adhere to a single formula, and could result in liquidity fragmentation if liquidity providers want to provide liquidity within different price ranges. In this paper, we present Uniswap v3, a novel AMM that gives liquidity providers more control over the price ranges in which their capital is used, with limited effect on liquidity fragmentation and gas inefficiency. This design does not depend on any shared assumption about the price behavior of the tokens. Uniswap v3 is based on the same constant product reserves curve as earlier versions [1], but offers several significant new features: โข Concentrated Liquidity: Liquidity providers (LPs) are given the ability to concentrate their liquidity by โbounding" it within an arbitrary price range. This improves the poolโs capital efficiency and allows LPs to approximate their pre- ferred reserves curve, while still being efficiently aggregated with the rest of the pool. We describe this feature in section 2 and its implementation in Section 6. โข Flexible Fees : The swap fee is no longer locked at 0.30%. Rather, the fee tier for each pool (of which there can be multiple per asset pair) is set on initialization (Section 3.1). The initially supported fee tiers are 0.05%, 0.30%, and 1%. UNI governance is able to add additional values to this set. โข Protocol Fee Governance: UNI governance has more flexibility in setting the fraction of swap fees collected by the protocol (Section 6.2.2). โข Improved Price Oracle: Uniswap v3 provides a way for users to query recent price accumulator values, thus avoiding the need to checkpoint the accumulator value at the exact be- ginning and end of the period for which a TWAP is being measured. (Section 5.1). 1
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson โข Liquidity Oracle: The contracts expose a time-weighted av- erage liquidity oracle (Section 5.3). The Uniswap v2 core contracts are non-upgradeable by de- sign, so Uniswap v3 is implemented as an entirely new set of contracts, available here. The Uniswap v3 core contracts are also non-upgradeable, with some parameters controlled by governance as described in Section 4. 2 CONCENTRATED LIQUIDITY The defining idea of Uniswap v3 is that of concentrated liquidity: liquidity bounded within some price range. In earlier versions, liquidity was distributed uniformly along the ๐ฅ ยท๐ฆ = ๐ (2.1) reserves curve, where ๐ฅ and ๐ฆ are the respective reserves of two assets X and Y, and ๐ is a constant [1]. In other words, earlier ver- sions were designed to provide liquidity across the entire price range (0, โ). This is simple to implement and allows liquidity to be efficiently aggregated, but means that much of the assets held in a pool are never touched. Having considered this, it seems reasonable to allow LPs to concentrate their liquidity to smaller price ranges than (0, โ). We call liquidity concentrated to a finite range a position. A position only needs to maintain enough reserves to support trading within its range, and therefore can act like a constant product pool with larger reserves (we call these the virtual reserves) within that range. ๐ ๐ ๐ ๐ฆreal ๐ฅreal X Reserves Y Reserves virtual reserves Figure 1: Simulation of Virtual Liquidity Specifically, a position only needs to hold enough of asset X to cover price movement to its upper bound, because upwards price movement1 corresponds to depletion of the X reserves. Similarly, it only needs to hold enough of asset Y to cover price movement to its lower bound. Fig. 1 depicts this relationship for a position on a range [๐๐, ๐๐ ] and a current price ๐๐ โ [ ๐๐, ๐๐ ]. ๐ฅreal and ๐ฆreal denote the positionโs real reserves. When the price exits a positionโs range, the positionโs liquidity is no longer active, and no longer earns fees. At that point, its 1We take asset Y to be the unit of account, which corresponds to token1 in our implementation. liquidity is composed entirely of a single asset, because the reserves of the other asset must have been entirely depleted. If the price ever reenters the range, the liquidity becomes active again. The amount of liquidity provided can be measured by the value ๐ฟ, which is equal to โ ๐. The real reserves of a position are described by the curve: (๐ฅ + ๐ฟโ๐๐ ) (๐ฆ + ๐ฟโ๐๐) = ๐ฟ2 (2.2) This curve is a translation of formula 2.1 such that the position is solvent exactly within its range (Fig. 2). ๐ ๐ X Reserves Y Reserves virtual reserves (2.1) real reserves (2.2) Figure 2: Real Reserves Liquidity providers are free to create as many positions as they see fit, each on its own price range. In this way, LPs can approximate any desired distribution of liquidity on the price space (see Fig. 3 for a few examples). Moreover, this serves as a mechanism to let the market decide where liquidity should be allocated. Rational LPs can reduce their capital costs by concentrating their liquidity in a narrow band around the current price, and adding or removing tokens as the price moves to keep their liquidity active. 2.1 Range Orders Positions on very small ranges act similarly to limit ordersโif the range is crossed, the position flips from being composed entirely of one asset, to being composed entirely of the other asset (plus accrued fees). There are two differences between this range order and a traditional limit order: โข There is a limit to how narrow a positionโs range can be. While the price is within that range, the limit order might be partially executed. โข When the position has been crossed, it needs to be with- drawn. If it is not, and the price crosses back across that range, the position will be traded back, effectively reversing the trade. 2
Uniswap v3 Core 0 โ Price Liquidity (I) Uniswap v2 ๐๐ ๐๐ Price Liquidity (II) A single position on [๐๐, ๐๐ ] Price Liquidity (III) A collection of custom positions Figure 3: Example Liquidity Distributions 3 ARCHITECTURAL CHANGES Uniswap v3 makes a number of architectural changes, some of which are necessitated by the inclusion of concentrated liquidity, and some of which are independent improvements. 3.1 Multiple Pools Per Pair In Uniswap v1 and v2, every pair of tokens corresponds to a single liquidity pool, which applies a uniform fee of 0.30% to all swaps. While this default fee tier historically worked well enough for many tokens, it is likely too high for some pools (such as pools between two stablecoins), and too low for others (such as pools that include highly volatile or rarely traded tokens). Uniswap v3 introduces multiple pools for each pair of tokens, each with a different swap fee. All pools are created by the same factory contract. The factory contract initially allows pools to be created at three fee tiers: 0.05%, 0.30%, and 1%. Additional fee tiers can be enabled by UNI governance. 3.2 Non-Fungible Liquidity 3.2.1 Non-Compounding Fees. Fees earned in earlier versions were continuously deposited in the pool as liquidity. This meant that liquidity in the pool would grow over time, even without explicit deposits, and that fee earnings compounded. In Uniswap v3, due to the non-fungible nature of positions, this is no longer possible. Instead, fee earnings are stored separately and held as the tokens in which the fees are paid (see Section 6.2.2). 3.2.2 Removal of Native Liquidity Tokens. In Uniswap v1 and v2, the pool contract is also an ERC-20 token contract, whose tokens represent liquidity held in the pool. While this is convenient, it actually sits uneasily with the Uniswap v2 philosophy that any- thing that does not need to be in the core contracts should be in the periphery, and blessing one โcanonical" ERC-20 implementation discourages the creation of improved ERC-20 token wrappers. Ar- guably, the ERC-20 token implementation should have been in the periphery, as a wrapper on a single liquidity position in the core contract. The changes made in Uniswap v3 force this issue by making completely fungible liquidity tokens impossible. Due to the custom liquidity provision feature, fees are now collected and held by the pool as individual tokens, rather than automatically reinvested as liquidity in the pool. As a result, in v3, the pool contract does not implement the ERC-20 standard. Anyone can create an ERC-20 token contract in the periphery that makes a liquidity position more fungible, but it will have to have additional logic to handle distribution of, or reinvestment of, collected fees. Alternatively, anyone could create a periphery contract that wraps an individual liquidity position (including collected fees) in an ERC-721 non-fungible token. 4 GOVERNANCE The factory has an owner, which is initially controlled by UNI tokenholders.2 The owner does not have the ability to halt the operation of any of the core contracts. As in Uniswap v2, Uniswap v3 has a protocol fee that can be turned on by UNI governance. In Uniswap v3, UNI governance has more flexibility in choosing the fraction of swap fees that go to the protocol, and is able to choose any fraction 1 ๐ where 4 โค ๐ โค 10, or 0. This parameter can be set on a per-pool basis. UNI governance also has the ability to add additional fee tiers. When it adds a new fee tier, it can also define the tickSpacing (see Section 6.1) corresponding to that fee tier. Once a fee tier is added to the factory, it cannot be removed (and the tickSpacing cannot be changed). The initial fee tiers and tick spacings supported are 0.05% (with a tick spacing of 10, approximately 0.10% between initializable ticks), 0.30% (with a tick spacing of 60, approximately 0.60% between initializable ticks), and 1% (with a tick spacing of 200, approximately 2.02% between ticks. Finally, UNI governance has the power to transfer ownership to another address. 5 ORACLE UPGRADES Uniswap v3includes three significant changes to the time-weighted average price (TWAP) oracle that was introduced by Uniswap v2. Most significantly, Uniswap v3 removes the need for users of the oracle to track previous values of the accumulator externally. Uniswap v2 requires users to checkpoint the accumulator value at both the beginning and end of the time period for which they 2Specifically, the owner will be initialized to the Timelock contract from UNI gover- nance, 0x1a9c8182c09f50c8318d769245bea52c32be35bc. 3
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson wanted to compute a TWAP. Uniswap v3 brings the accumulator checkpoints into core, allowing external contracts to compute on- chain TWAPs over recent periods without storing checkpoints of the accumulator value. Another change is that instead of accumulating the sum of prices, allowing users to compute the arithmetic mean TWAP,Uniswap v3 tracks the sum of log prices, allowing users to compute the geometric mean TWAP. Finally, Uniswap v3 adds a liquidity accumulator that is tracked alongside the price accumulator, which accumulates 1 ๐ฟ for each second. This liquidity accumulator is useful for external contracts that want to implement liquidity mining on top of Uniswap v3. It can also be used by other contracts to inform a decision on which of the pools corresponding to a pair (see section 3.1) will have the most reliable TWAP. 5.1 Oracle Observations As in Uniswap v2, Uniswap v3 tracks a running accumulator of the price at the beginning of each block, multiplied by the number of seconds since the last block. A pool in Uniswap v2 stores only the most recent value of this price accumulatorโthat is, the value as of the last block in which a swap occurred. When computing average prices in Uniswap v2, it is the responsibility of the external caller to provide the previous value of the price accumulator. With many users, each will have to provide their own methodology for checkpointing previous values of the accumulator, or coordinate on a shared method to reduce costs. And there is no way to guarantee that every block in which the pool is touched will be reflected in the accumulator. In Uniswap v3, the pool stores a list of previous values for the price accumulator (as well as the liquidity accumulator described in section 5.3). It does this by automatically checkpointing the accumulator value every time the pool is touched for the first time in a block, cycling through an array where the oldest checkpoint is eventually overwritten by a new one, similar to a circular buffer. While this array initially only has room for a single checkpoint, anyone can initialize additional storage slots to lengthen the array, extending to as many as 65,536 checkpoints. 3 This imposes the one-time gas cost of initializing additional storage slots for this array on whoever wants this pair to checkpoint more slots. The pool exposes the array of past observations to users, as well as a convenience function for finding the (interpolated) accumulator value at any historical timestamp within the checkpointed period. 5.2 Geometric Mean Price Oracle Uniswap v2 maintains two price accumulatorsโone for the price of token0 in terms of token1, and one for the price oftoken1 in terms of token0. Users can compute the time-weighted arithmetic mean of the prices over any period, by subtracting the accumulator value at the beginning of the period from the accumulator at the end of the period, then dividing the difference by the number of seconds in the period. Note that accumulators for token0 and token1 are tracked separately, since the time-weighted arithmetic mean price 3The maximum of 65,536 checkpoints allows fetching checkpoints for at least 9 days after they are written, assuming 13 seconds pass between each block and a checkpoint is written every block. of token0 is not equivalent to the reciprocal of the time-weighted arithmetic mean price of token1. Using the time-weighted geometric mean price, as Uniswap v3 does, avoids the need to track separate accumulators for these ratios. The geometric mean of a set of ratios is the reciprocal of the geometric mean of their reciprocals. It is also easy to implement in Uniswap v3 because of its implementation of custom liquidity provision, as described in section 6. In addition, the accumulator can be stored in a smaller number of bits, since it trackslog ๐ rather than ๐, and log ๐ can represent a wide range of prices with consistent precision.4 Finally, there is a theoretical argument that the time- weighted geometric mean price should be a truer representation of the average price.5 Instead of tracking the cumulative sum of the price ๐, Uniswap v3 accumulates the cumulative sum of the current tick index (๐๐๐1.0001๐, the logarithm of price for base 1.0001, which is precise up to 1 basis point). The accumulator at any given time is equal to the sum of ๐๐๐1.0001 (๐) for every second in the history of the contract: ๐๐ก = ๐กร ๐=1 log1.0001 (๐๐ ) (5.1) We want to estimate the geometric mean time-weighted average price (๐๐ก1,๐ก2) over any period ๐ก1 to ๐ก2. ๐๐ก1,๐ก2 = ยฉยญ ยซ ๐ก2ร ๐=๐ก1 ๐๐ ยชยฎ ยฌ 1 ๐ก2โ๐ก1 (5.2) To compute this, you can look at the accumulatorโs value at๐ก1 and at ๐ก2, subtract the first value from the second, divide by the number of seconds elapsed, and compute 1.0001๐ฅ to compute the time weighted geometric mean price. log1.0001 ๐๐ก1,๐ก2 = ร๐ก2 ๐=๐ก1 log1.0001 (๐๐ ) ๐ก2 โ ๐ก1 (5.3) log1.0001 ๐๐ก1,๐ก2 = ๐๐ก2 โ ๐๐ก1 ๐ก2 โ ๐ก1 (5.4) ๐๐ก1,๐ก2 = 1.0001 ๐๐ก2 โ๐๐ก1 ๐ก2โ๐ก1 (5.5) 5.3 Liquidity Oracle In addition to the seconds-weighted accumulator of log1.0001 ๐๐๐๐๐ , Uniswap v3 also tracks a seconds-weighted accumulator of 1 ๐ฟ (the reciprocal of the virtual liquidity currently in range) at the begin- ning of each block: secondsPerLiquidityCumulative (๐ ๐๐ ). This can be used by external liquidity mining contracts to fairly allocate rewards. If an external contract wants to distribute rewards at an even rate of ๐ tokens per second to all active liquidity in the 4In order to support tolerable precision across all possible prices, Uniswap v2 repre- sents each price as a 224-bit fixed-point number. Uniswap v3 only needs to represent ๐๐๐ 1.0001๐ as a signed 24-bit number, and still can detect price movements of one tick, or 1 basis point. 5While arithmetic mean TWAPs are much more widely used, they should theoretically be less accurate in measuring a geometric Brownian motion process (which is how price movements are usually modeled). The arithmetic mean of a geometric Brownian motion process will tend to overweight higher prices (where small percentage movements correspond to large absolute movements) relative to lower ones. 4
Uniswap v3 Core contract, and a position with ๐ฟ liquidity was active from ๐ก0 to ๐ก1, then its rewards for that period would be ๐ ยทLยท(๐ ๐๐ (๐ก1) โ ๐ ๐๐ (๐ก0)). In order to extend this so that concentrated liquidity is rewarded only when it is in range,Uniswap v3 stores a computed checkpoint based on this value every time a tick is crossed, as described in section 6.3. This accumulator can also be used by on-chain contracts to make their oracles stronger (such as by evaluating which fee-tier pool to use the oracle from). 6 IMPLEMENTING CONCENTRATED LIQUIDITY The rest of this paper describes how concentrated liquidity provi- sion works, and gives a high-level description of how it is imple- mented in the contracts. 6.1 Ticks and Ranges To implement custom liquidity provision, the space of possible prices is demarcated by discrete ticks. Liquidity providers can pro- vide liquidity in a range between any two ticks (which need not be adjacent). Each range can be specified as a pair of signed integertick indices: a lower tick (๐๐ ) and an upper tick ( ๐๐ข). Ticks represent prices at which the virtual liquidity of the contract can change. We will assume that prices are always expressed as the price of one of the tokensโcalled token0โin terms of the other tokenโ token1. The assignment of the two tokens to token0 and token1 is arbitrary and does not affect the logic of the contract (other than through possible rounding errors). Conceptually, there is a tick at every price ๐ that is an integer power of 1.0001. Identifying ticks by an integer index ๐, the price at each is given by: ๐ (๐) = 1.0001๐ (6.1) This has the desirable property of each tick being a .01% (1 basis point) price movement away from each of its neighboring ticks. For technical reasons explained in 6.2.1, however, pools actually track ticks at every square root price that is an integer power ofโ 1.0001. Consider the above equation, transformed into square root price space: โ๐ (๐) = โ 1.0001 ๐ = 1.0001 ๐ 2 (6.2) As an example,โ๐ (0)โthe square root price at tick 0โis 1, โ๐ (1) is โ 1.0001 โ 1.00005, and โ๐ (โ1) is 1โ 1.0001 โ 0.99995. When liquidity is added to a range, if one or both of the ticks is not already used as a bound in an existing position, that tick is initialized. Not every tick can be initialized. The pool is instantiated with a parameter, tickSpacing (๐ก๐ ); only ticks with indexes that are divisi- ble by tickSpacing can be initialized. For example, iftickSpacing is 2, then only even ticks (...-4, -2, 0, 2, 4...) can be initialized. Small choices for tickSpacing allow tighter and more precise ranges, but may cause swaps to be more gas-intensive (since each initialized tick that a swap crosses imposes a gas cost on the swapper). Whenever the price crosses an initialized tick, virtual liquidity is kicked in or out. The gas cost of an initialized tick crossing is constant, and is not dependent on the number of positions being kicked in or out at that tick. Ensuring that the right amount of liquidity is kicked in and out of the pool when ticks are crossed, and ensuring that each position earns its proportional share of the fees that were accrued while it was within range, requires some accounting within the pool. The pool contract uses storage variables to track state at a global (per-pool) level, at a per-tick level, and at a per-position level. 6.2 Global State The global state of the contract includes seven storage variables relevant to swaps and liquidity provision. (It has other storage variables that are used for the oracle, as described in section 5.) Type Variable Name Notation uint128 liquidity ๐ฟ uint160 sqrtPriceX96 โ ๐ int24 tick ๐๐ uint256 feeGrowthGlobal0X128 ๐๐,0 uint256 feeGrowthGlobal1X128 ๐๐,1 uint128 protocolFees.token0 ๐๐,0 uint128 protocolFees.token1 ๐๐,1 Table 1: Global State 6.2.1 Price and Liquidity. In Uniswap v2, each pool contract tracks the poolโs current reserves,๐ฅ and ๐ฆ. In Uniswap v3, the contract could be thought of as having virtual reservesโvalues for ๐ฅ and ๐ฆ that allow you to describe the contractโs behavior (between two adjacent ticks) as if it followed the constant product formula. Instead of tracking those virtual reserves, however, the pool contract tracks two different values:liquidity (๐ฟ) and sqrtPrice ( โ ๐). These could be computed from the virtual reserves with the following formulas: ๐ฟ = โ๐ฅ๐ฆ (6.3) โ ๐ = r๐ฆ ๐ฅ (6.4) Conversely, these values could be used to compute the virtual reserves: ๐ฅ = ๐ฟโ ๐ (6.5) ๐ฆ = ๐ฟ ยท โ ๐ (6.6) Using ๐ฟ and โ ๐ is convenient because only one of them changes at a time. Price (and thus โ ๐) changes when swapping within a tick; liquidity changes when crossing a tick, or when minting or burning liquidity. This avoids some rounding errors that could be encountered if tracking virtual reserves. You may notice that the formula for liquidity (based on virtual reserves) is similar to the formula used to initialize the quantity of liquidity tokens (based on actual reserves) in Uniswap v2. before 5
Implementation Notes
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson any fees have been earned. In some ways, liquidity can be thought of as virtual liquidity tokens. Alternatively, liquidity can be thought of as the amount that token1 reserves (either actual or virtual) changes for a given change in โ ๐: ๐ฟ = ฮ๐ ฮ โ ๐ (6.7) We track โ ๐ instead of ๐ to take advantage of this relationship, and to avoid having to take any square roots when computing swaps, as described in section 6.2.3. The global state also tracks the current tick index as tick (๐๐), a signed integer representing the current tick (more specifically, the nearest tick below the current price). This is an optimization (and a way of avoiding precision issues with logarithms), since at any time, you should be able to compute the current tick based on the current sqrtPrice. Specifically, at any given time, the following equation should be true: ๐๐ = j logโ 1.0001 โ ๐ k (6.8) 6.2.2 Fees. Each pool is initialized with an immutable value, fee (๐พ), representing the fee paid by swappers in units of hundredths of a basis point (0.0001%). It also tracks the current protocol fee, ๐ (which is initialized to zero, but can changed by UNI governance).6 This number gives you the fraction of the fees paid by swappers that currently goes to the protocol rather than to liquidity providers. ๐ only has a limited set of permitted values: 0, 1/4, 1/5, 1/6, 1/7, 1/8, 1/9, or 1/10. The global state also tracks two numbers: feeGrowthGlobal0 (๐๐,0) andfeeGrowthGlobal1 (๐๐,1). These represent the total amount of fees that have been earned per unit of virtual liquidity (๐ฟ), over the entire history of the contract. You can think of them as the total amount of fees that would have been earned by 1 unit of unbounded liquidity that was deposited when the contract was first initialized. They are stored as fixed-point unsigned 128x128 numbers. Note that in Uniswap v3, fees are collected in the tokens themselves rather than in liquidity, for reasons explained in section 3.2.1. Finally, the global state tracks the total accumulated uncollected protocol fee in each token,protocolFees0 (๐๐,0) andprotocolFees1 (๐๐,1). This is an unsigned uint128. The accumulated protocol fees can be collected by UNI governance, by calling thecollectProtocol function. 6.2.3 Swapping Within a Single Tick. For small enough swaps, that do not move the price past a tick, the contracts act like an ๐ฅ ยท๐ฆ = ๐ pool. Suppose ๐พ is the fee, i.e., 0.003, and ๐ฆ๐๐ as the amount of token1 sent in. First, feeGrowthGlobal1 and protocolFees1 are incremented: ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท (1 โ ๐) (6.9) ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท๐ (6.10) ฮ๐ฆ is the increase in ๐ฆ (after the fee is taken out). 6Technically, the storage variable called โprotocolFee" is the denominator of this fraction (or is zero, if ๐ is zero). ฮ๐ฆ = ๐ฆ๐๐ ยท (1 โ ๐พ) (6.11) If you used the computed virtual reserves (๐ฅ and ๐ฆ) for thetoken0 and token1 balances, then this formula could be used to find the amount of token0 sent out: ๐ฅ๐๐๐ = ๐ฅ ยท๐ฆ ๐ฆ + ฮ๐ฆ (6.12) But remember that inv3, the contract actually tracks liquidity (๐ฟ) and square root of price ( โ ๐) instead of ๐ฅ and ๐ฆ. We could compute ๐ฅ and ๐ฆ from those values, and then use those to calculate the execution price of the trade. But it turns out that there are simple formulas that describe the relationship between ฮ โ ๐ and ฮ๐ฆ, for a given ๐ฟ (which can be derived from formula 6.7): ฮ โ ๐ = ฮ๐ฆ ๐ฟ (6.13) ฮ๐ฆ = ฮ โ ๐ ยท๐ฟ (6.14) There are also simple formulas that describe the relationship between ฮ 1โ ๐ and ฮ๐ฅ: ฮ 1โ ๐ = ฮ๐ฅ ๐ฟ (6.15) ฮ๐ฅ = ฮ 1โ ๐ ยท๐ฟ (6.16) When swapping one token for the other, the pool contract can first compute the new โ ๐ using formula 6.13 or 6.15, and then can compute the amount of token0 or token1 to send out using formula 6.14 or 6.16. These formulas will work for any swap that does not push โ ๐ past the price of the next initialized tick. If the computed ฮ โ ๐ would cause โ ๐ to move past that next initialized tick, the contract must only cross up to that tickโusing up only part of the swapโand then cross the tick, as described in section 6.3.1, before continuing with the rest of the swap. 6.2.4 Initialized Tick Bitmap. If a tick is not used as the endpoint of a range with any liquidity in itโthat is, if the tick is uninitial- izedโthen that tick can be skipped during swaps. As an optimization to make finding the next initialized tick more efficient, the pool tracks a bitmap tickBitmap of initialized ticks. The position in the bitmap that corresponds to the tick index is set to 1 if the tick is initialized, and 0 if it is not initialized. When a tick is used as an endpoint for a new position, and that tick is not currently used by any other liquidity, the tick is initialized, and the corresponding bit in the bitmap is set to 1. An initialized tick can become uninitialized again if all of the liquidity for which it is an endpoint is removed, in which case that tickโs position on the bitmap is zeroed out. 6.3 Tick-Indexed State The contract needs to store information about each tick in order to track the amount of net liquidity that should be added or removed when the tick is crossed, as well as to track the fees earned above and below that tick. 6
Uniswap v3 Core Start S0. Check input S1. Swap within current interval S2. Is there remaining input or output? S4. Cross next tick Stop S5. Execute computed swap Pass Fail Yes No Figure 4: Swap Control Flow The contract stores a mapping from tick indexes (int24) to the following seven values: Type Variable Name Notation int128 liquidityNet ฮ๐ฟ uint128 liquidityGross ๐ฟ๐ uint256 feeGrowthOutside0X128 ๐๐,0 uint256 feeGrowthOutside1X128 ๐๐,1 uint256 secondsOutside ๐ ๐ uint256 tickCumulativeOutside ๐๐ uint256 secondsPerLiquidityOutsideX128 ๐ ๐๐ Table 2: Tick-Indexed State Each tick tracks ฮ๐ฟ, the total amount of liquidity that should be kicked in or out when the tick is crossed. The tick only needs to track one signed integer: the amount of liquidity added (or, if negative, removed) when the tick is crossed going left to right. This value does not need to be updated when the tick is crossed (but only when a position with a bound at that tick is updated). We want to be able to uninitialize a tick when there is no longer any liquidity referencing that tick. Since ฮ๐ฟ is a net value, itโs necessary to track a gross tally of liquidity referencing the tick, liquidityGross. This value ensures that even if net liquidity at a tick is 0, we can still know if a tick is referenced by at least one underlying position or not, which tells us whether to update the tick bitmap. feeGrowthOutside{0,1} are used to track how many fees were accumulated within a given range. Since the formulas are the same for the fees collected in token0 and token1, we will omit that sub- script for the rest of this section. You can compute the fees earned per unit of liquidity in token 0 above (๐๐) and below (๐๐) a tick ๐ with a formula that depends on whether the price is currently within or outside that rangeโthat is, whether the current tick index ๐๐ is greater than or equal to ๐: ๐๐ (๐) = ( ๐๐ โ ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ (๐) ๐๐ < ๐ (6.17) ๐๐ (๐) = ( ๐๐ (๐) ๐๐ โฅ ๐ ๐๐ โ ๐๐ (๐) ๐๐ < ๐ (6.18) We can use these functions to compute the total amount of cumulative fees per share ๐๐ in the range between two ticksโa lower tick ๐๐ and an upper tick ๐๐ข: ๐๐ = ๐๐ โ ๐๐ (๐๐ ) โ ๐๐ (๐๐ข ) (6.19) ๐๐ needs to be updated each time the tick is crossed. Specifically, as a tick๐ is crossed in either direction, its๐๐ (for each token) should be updated as follows: ๐๐ (๐) := ๐๐ โ ๐๐ (๐) (6.20) ๐๐ is only needed for ticks that are used as either the lower or upper bound for at least one position. As a result, for efficiency,๐๐ is not initialized (and thus does not need to be updated when crossed) until a position is created that has that tick as one of its bounds. When ๐๐ is initialized for a tick ๐, the valueโby conventionโis chosen as if all of the fees earned to date had occurred below that tick: ๐๐ := ( ๐๐ ๐๐ โฅ ๐ 0 ๐๐ < ๐ (6.21) Note that since ๐๐ values for different ticks could be initialized at different times, comparisons of the ๐๐ values for different ticks are not meaningful, and there is no guarantee that values for ๐๐ will be consistent. This does not cause a problem for per-position accounting, since, as described below, all the position needs to know is the growth in ๐ within a given range since that position was last touched. 7
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson Finally, the contract also stores secondsOutside (๐ ๐), secondsPerLiquidityOutside, andtickCumulativeOutside for each tick. These values are not used within the contract, but are tracked for the benefit of external contracts that need more fine- grained information about the poolโs behavior (for purposes like liquidity mining). All three of these indexes work similarly to the fee growth in- dexes described above. But where the feeGrowthOutside{0,1} indexes track feeGrowthGlobal{0,1}, the secondsOutside index tracks seconds (that is, the current timestamp), secondsPerLiquidityOutside tracks the 1/๐ฟ accumulator (secondsPerLiquidityCumulative) described in section 5.3, and tickCumulativeOutside tracks the log1.0001 ๐ accumulator de- scribed in section 5.2. For example, the seconds spent above (๐ ๐) and below (๐ ๐) a given tick is computed differently based on whether the current price is within that range, and the seconds spent within a range (๐ ๐ ) can be computed using the values of ๐ ๐ and ๐ ๐: ๐ก๐ (๐) = ( ๐ก โ ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก๐ (๐) ๐๐ < ๐ (6.22) ๐ก๐ (๐) = ( ๐ก๐ (๐) ๐๐ โฅ ๐ ๐ก โ ๐ก๐ (๐) ๐๐ < ๐ (6.23) ๐ก๐ (๐๐ , ๐๐ข ) = ๐ก โ ๐ก๐ (๐๐ ) โ ๐ก๐ (๐๐ข ) (6.24) The number of seconds spent within a range between two times ๐ก1 and ๐ก2 can be computed by recording the value of ๐ ๐ (๐๐ , ๐๐ข ) at ๐ก1 and at ๐ก2, and subtracting the former from the latter. Like ๐๐, ๐ ๐ does not need to be tracked for ticks that are not on the edge of any position. Therefore, it is not initialized until a position is created that is bounded by that tick. By convention, it is initialized as if every second since the Unix timestamp 0 had been spent below that tick: ๐ก๐ (๐) := ( ๐ก ๐ ๐ โฅ ๐ 0 ๐๐ < ๐ (6.25) As with ๐๐ values, ๐ก๐ values are not meaningfully comparable across different ticks. ๐ก๐ is only meaningful in computing the num- ber of seconds that liquidity was within some particular range between some defined start time (which must be after ๐ก๐ was ini- tialized for both ticks) and some end time. 6.3.1 Crossing a Tick. As described in section 6.2.3, Uniswap v3 acts like it obeys the constant product formula when swapping between initialized ticks. When a swap crosses an initialized tick, however, the contract needs to add or remove liquidity, to ensure that no liquidity provider is insolvent. This means theฮ๐ฟ is fetched from the tick, and applied to the global ๐ฟ. The contract also needs to update the tickโs own state, in order to track the fees earned (and seconds spent) within ranges bounded by this tick. The feeGrowthOutside{0,1} and secondsOutside values are updated to both reflect current values, as well as the proper orientation relative to the current tick: ๐๐ := ๐๐ โ ๐๐ (6.26) ๐ก๐ := ๐ก โ ๐ก๐ (6.27) Once a tick is crossed, the swap can continue as described in section 6.2.3 until it reaches the next initialized tick. 6.4 Position-Indexed State The contract has a mapping from user (an address), lower bound (a tick index, int24), and upper bound (a tick index, int24) to a specific Position struct. Each Position tracks three values: Type Variable Name Notation uint128 liquidity ๐ uint256 feeGrowthInside0LastX128 ๐๐,0 (๐ก0) uint256 feeGrowthInside1LastX128 ๐๐,1 (๐ก0) Table 3: Position-Indexed State liquidity (๐) means the amount of virtual liquidity that the position represented the last time this position was touched. Specif- ically, liquidity could be thought of as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are the respective amounts of virtual token0 and virtual token1 that this liquidity contributes to the pool at any time that it is within range. Unlike pool shares in Uniswap v2 (where the value of each share grows over time), the units for liquidity do not change as fees are accumulated; it is always measured as โ๐ฅ ยท๐ฆ, where ๐ฅ and ๐ฆ are quantities of token0 and token1, respectively. This liquidity number does not reflect the fees that have been accumulated since the contract was last touched, which we will call uncollected fees . Computing these uncollected fees requires additional stored values on the position, feeGrowthInside0Last (๐๐,0 (๐ก0)) and feeGrowthInside1Last (๐๐,1 (๐ก0)), as described be- low. 6.4.1 setPosition. The setPosition function allows a liquidity provider to update their position. Two of the arguments tosetPosition โ lowerTick and upperTickโ when combined with the msg.sender, together specify a position. The function takes one additional parameter, liquidityDelta, to specify how much virtual liquidity the user wants to add or (if negative) remove. First, the function computes the uncollected fees ( ๐๐ข) that the position is entitled to, in each token.7 The amount collected in fees is credited to the user and netted against the amount that they would send in or out for their virtual liquidity deposit. To compute uncollected fees of a token, you need to know how much ๐๐ for the positionโs range (calculated from the rangeโs ๐๐ and ๐๐ as described in section 6.3) has grown since the last time fees were collected for that position. The growth in fees in a given range per unit of liquidity over between times ๐ก0 and ๐ก1 is simply ๐๐ (๐ก1) โ ๐๐ (๐ก0) (where ๐๐ (๐ก0) is stored in the position as feeGrowthInside{0,1}Last, and ๐๐ (๐ก1) can be computed from the current state of the ticks). Multiplying this by the positionโs liquidity gives us the total uncollected fees in token 0 for this position: 7Since the formulas for computing uncollected fees in each token are the same, we will omit that subscript for the rest of this section. 8
Uniswap v3 Core ๐๐ข = ๐ ยท (๐๐ (๐ก1) โ ๐๐ (๐ก0)) (6.28) Then, the contract updates the positionโsliquidity by adding liquidityDelta. It also addsliquidityDelta to theliquidityNet value for the tick at the bottom end of the range, and subtracts it from the liquidityNet at the upper tick (to reflect that this new liquidity would be added when the price crosses the lower tick going up, and subtracted when the price crosses the upper tick going up). If the poolโs current price is within the range of this position, the contract also adds liquidityDelta to the contractโs global liquidity value. Finally, the pool transfers tokens from (or, if liquidityDelta is negative, to) the user, corresponding to the amount of liquidity burned or minted. The amount of token0 (ฮ๐ ) or token1 (ฮ๐ ) that needs to be deposited can be thought of as the amount that would be sold from the position if the price were to move from the current price (๐) to the upper tick or lower tick (for token0 or token1, respectively). These formulas can be derived from formulas 6.14 and 6.16, and depend on whether the current price is below, within, or above the range of the position: ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ 0 ๐๐ < ๐๐ ฮ๐ฟ ยท ( โ ๐ โ p ๐ (๐๐ )) ๐๐ โค ๐๐ < ๐๐ข ฮ๐ฟ ยท ( p ๐ (๐๐ข ) โ p ๐ (๐๐ )) ๐๐ โฅ ๐๐ข (6.29) ฮ๐ = ๏ฃฑ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด ๏ฃฒ ๏ฃด๏ฃด๏ฃด๏ฃด๏ฃด๏ฃณ ฮ๐ฟ ยท ( 1โ ๐ (๐๐ ) โ 1โ ๐ (๐๐ข ) ) ๐๐ < ๐๐ ฮ๐ฟ ยท ( 1โ ๐ โ 1โ ๐ (๐๐ข ) ) ๐๐ โค ๐๐ < ๐๐ข 0 ๐๐ โฅ ๐๐ข (6.30) REFERENCES [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, United States, 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. DISCLAIMER This paper is for general information purposes only. It does not constitute investment advice or a recommendation or solicitation to buy or sell any investment and should not be used in the evaluation of the merits of making any investment decision. It should not be relied upon for accounting, legal or tax advice or investment rec- ommendations. This paper reflects current opinions of the authors and is not made on behalf of Uniswap Labs, Paradigm, or their affiliates and does not necessarily reflect the opinions of Uniswap Labs, Paradigm, their affiliates or individuals associated with them. The opinions reflected herein are subject to change without being updated. 9
Uniswap v3 Core March 2021 Hayden Adams [email protected] Noah Zinsmeister [email protected] Moody Salem [email protected] River Keefer [email protected] Dan Robinson [email protected] ABSTRACT Uniswap v3 is a noncustodial automated market maker imple- mented for the Ethereum Virtual Machine. In comparison to earlier versions of the protocol, Uniswap v3 provides increased capital efficiency and fine-tuned control to liquidity providers, improves the accuracy and convenience of the price oracle, and has a more flexible fee structure. 1 INTRODUCTION Automated market makers (AMMs) are agents that pool liquidity and make it available to traders according to an algorithm [5]. Con- stant function market makers (CFMMs), a broad class of AMMs of which Uniswap is a member, have seen widespread use in the con- text of decentralized finance, where they are typically implemented as smart contracts that trade tokens on a permissionless blockchain [2]. CFMMs as they are implemented today are often capital inef- ficient. In the constant product market maker formula used by Uniswap v1 and v2, only a fraction of the assets in the pool are available at a given price. This is inefficient, particularly when assets are expected to trade close to a particular price at all times. Prior attempts to address this capital efficiency issue, such as Curve [3] and YieldSpace [4], have involved building pools that use different functions to describe the relation between reserves. This requires all liquidity providers in a given pool to adhere to a single formula, and could result in liquidity fragmentation if liquidity providers want to provide liquidity within different price ranges. In this paper, we present Uniswap v3, a novel AMM that gives liquidity providers more control over the price ranges in which their capital is used, with limited effect on liquidity fragmentation and gas inefficiency. This design does not depend on any shared assumption about the price behavior of the tokens. Uniswap v3 is based on the same constant product reserves curve as earlier versions [1], but offers several significant new features: โข Concentrated Liquidity: Liquidity providers (LPs) are given the ability to concentrate their liquidity by โbounding" it within an arbitrary price range. This improves the poolโs capital efficiency and allows LPs to approximate their pre- ferred reserves curve, while still being efficiently aggregated with the rest of the pool. We describe this feature in section 2 and its implementation in Section 6. โข Flexible Fees : The swap fee is no longer locked at 0.30%. Rather, the fee tier for each pool (of which there can be multiple per asset pair) is set on initialization (Section 3.1). The initially supported fee tiers are 0.05%, 0.30%, and 1%. UNI governance is able to add additional values to this set. โข Protocol Fee Governance: UNI governance has more flexibility in setting the fraction of swap fees collected by the protocol (Section 6.2.2). โข Improved Price Oracle: Uniswap v3 provides a way for users to query recent price accumulator values, thus avoiding the need to checkpoint the accumulator value at the exact be- ginning and end of the period for which a TWAP is being measured. (Section 5.1). 1
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson โข Liquidity Oracle: The contracts expose a time-weighted av- erage liquidity oracle (Section 5.3). The Uniswap v2 core contracts are non-upgradeable by de- sign, so Uniswap v3 is implemented as an entirely new set of contracts, available here. The Uniswap v3 core contracts are also non-upgradeable, with some parameters controlled by governance as described in Section 4. 2 CONCENTRATED LIQUIDITY The defining idea of Uniswap v3 is that of concentrated liquidity: liquidity bounded within some price range. In earlier versions, liquidity was distributed uniformly along the ๐ฅ ยท๐ฆ = ๐ (2.1) reserves curve, where ๐ฅ and ๐ฆ are the respective reserves of two assets X and Y, and ๐ is a constant [1]. In other words, earlier ver- sions were designed to provide liquidity across the entire price range (0, โ). This is simple to implement and allows liquidity to be efficiently aggregated, but means that much of the assets held in a pool are never touched. Having considered this, it seems reasonable to allow LPs to concentrate their liquidity to smaller price ranges than (0, โ). We call liquidity concentrated to a finite range a position. A position only needs to maintain enough reserves to support trading within its range, and therefore can act like a constant product pool with larger reserves (we call these the virtual reserves) within that range. ๐ ๐ ๐ ๐ฆreal ๐ฅreal X Reserves Y Reserves virtual reserves Figure 1: Simulation of Virtual Liquidity Specifically, a position only needs to hold enough of asset X to cover price movement to its upper bound, because upwards price movement1 corresponds to depletion of the X reserves. Similarly, it only needs to hold enough of asset Y to cover price movement to its lower bound. Fig. 1 depicts this relationship for a position on a range [๐๐, ๐๐ ] and a current price ๐๐ โ [ ๐๐, ๐๐ ]. ๐ฅreal and ๐ฆreal denote the positionโs real reserves. When the price exits a positionโs range, the positionโs liquidity is no longer active, and no longer earns fees. At that point, its 1We take asset Y to be the unit of account, which corresponds to token1 in our implementation. liquidity is composed entirely of a single asset, because the reserves of the other asset must have been entirely depleted. If the price ever reenters the range, the liquidity becomes active again. The amount of liquidity provided can be measured by the value ๐ฟ, which is equal to โ ๐. The real reserves of a position are described by the curve: (๐ฅ + ๐ฟโ๐๐ ) (๐ฆ + ๐ฟโ๐๐) = ๐ฟ2 (2.2) This curve is a translation of formula 2.1 such that the position is solvent exactly within its range (Fig. 2). ๐ ๐ X Reserves Y Reserves virtual reserves (2.1) real reserves (2.2) Figure 2: Real Reserves Liquidity providers are free to create as many positions as they see fit, each on its own price range. In this way, LPs can approximate any desired distribution of liquidity on the price space (see Fig. 3 for a few examples). Moreover, this serves as a mechanism to let the market decide where liquidity should be allocated. Rational LPs can reduce their capital costs by concentrating their liquidity in a narrow band around the current price, and adding or removing tokens as the price moves to keep their liquidity active. 2.1 Range Orders Positions on very small ranges act similarly to limit ordersโif the range is crossed, the position flips from being composed entirely of one asset, to being composed entirely of the other asset (plus accrued fees). There are two differences between this range order and a traditional limit order: โข There is a limit to how narrow a positionโs range can be. While the price is within that range, the limit order might be partially executed. โข When the position has been crossed, it needs to be with- drawn. If it is not, and the price crosses back across that range, the position will be traded back, effectively reversing the trade. 2
Uniswap v3 Core 0 โ Price Liquidity (I) Uniswap v2 ๐๐ ๐๐ Price Liquidity (II) A single position on [๐๐, ๐๐ ] Price Liquidity (III) A collection of custom positions Figure 3: Example Liquidity Distributions 3 ARCHITECTURAL CHANGES Uniswap v3 makes a number of architectural changes, some of which are necessitated by the inclusion of concentrated liquidity, and some of which are independent improvements. 3.1 Multiple Pools Per Pair In Uniswap v1 and v2, every pair of tokens corresponds to a single liquidity pool, which applies a uniform fee of 0.30% to all swaps. While this default fee tier historically worked well enough for many tokens, it is likely too high for some pools (such as pools between two stablecoins), and too low for others (such as pools that include highly volatile or rarely traded tokens). Uniswap v3 introduces multiple pools for each pair of tokens, each with a different swap fee. All pools are created by the same factory contract. The factory contract initially allows pools to be created at three fee tiers: 0.05%, 0.30%, and 1%. Additional fee tiers can be enabled by UNI governance. 3.2 Non-Fungible Liquidity 3.2.1 Non-Compounding Fees. Fees earned in earlier versions were continuously deposited in the pool as liquidity. This meant that liquidity in the pool would grow over time, even without explicit deposits, and that fee earnings compounded. In Uniswap v3, due to the non-fungible nature of positions, this is no longer possible. Instead, fee earnings are stored separately and held as the tokens in which the fees are paid (see Section 6.2.2). 3.2.2 Removal of Native Liquidity Tokens. In Uniswap v1 and v2, the pool contract is also an ERC-20 token contract, whose tokens represent liquidity held in the pool. While this is convenient, it actually sits uneasily with the Uniswap v2 philosophy that any- thing that does not need to be in the core contracts should be in the periphery, and blessing one โcanonical" ERC-20 implementation discourages the creation of improved ERC-20 token wrappers. Ar- guably, the ERC-20 token implementation should have been in the periphery, as a wrapper on a single liquidity position in the core contract. The changes made in Uniswap v3 force this issue by making completely fungible liquidity tokens impossible. Due to the custom liquidity provision feature, fees are now collected and held by the pool as individual tokens, rather than automatically reinvested as liquidity in the pool. As a result, in v3, the pool contract does not implement the ERC-20 standard. Anyone can create an ERC-20 token contract in the periphery that makes a liquidity position more fungible, but it will have to have additional logic to handle distribution of, or reinvestment of, collected fees. Alternatively, anyone could create a periphery contract that wraps an individual liquidity position (including collected fees) in an ERC-721 non-fungible token. 4 GOVERNANCE The factory has an owner, which is initially controlled by UNI tokenholders.2 The owner does not have the ability to halt the operation of any of the core contracts. As in Uniswap v2, Uniswap v3 has a protocol fee that can be turned on by UNI governance. In Uniswap v3, UNI governance has more flexibility in choosing the fraction of swap fees that go to the protocol, and is able to choose any fraction 1 ๐ where 4 โค ๐ โค 10, or 0. This parameter can be set on a per-pool basis. UNI governance also has the ability to add additional fee tiers. When it adds a new fee tier, it can also define the tickSpacing (see Section 6.1) corresponding to that fee tier. Once a fee tier is added to the factory, it cannot be removed (and the tickSpacing cannot be changed). The initial fee tiers and tick spacings supported are 0.05% (with a tick spacing of 10, approximately 0.10% between initializable ticks), 0.30% (with a tick spacing of 60, approximately 0.60% between initializable ticks), and 1% (with a tick spacing of 200, approximately 2.02% between ticks. Finally, UNI governance has the power to transfer ownership to another address. 5 ORACLE UPGRADES Uniswap v3includes three significant changes to the time-weighted average price (TWAP) oracle that was introduced by Uniswap v2. Most significantly, Uniswap v3 removes the need for users of the oracle to track previous values of the accumulator externally. Uniswap v2 requires users to checkpoint the accumulator value at both the beginning and end of the time period for which they 2Specifically, the owner will be initialized to the Timelock contract from UNI gover- nance, 0x1a9c8182c09f50c8318d769245bea52c32be35bc. 3
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson wanted to compute a TWAP. Uniswap v3 brings the accumulator checkpoints into core, allowing external contracts to compute on- chain TWAPs over recent periods without storing checkpoints of the accumulator value. Another change is that instead of accumulating the sum of prices, allowing users to compute the arithmetic mean TWAP,Uniswap v3 tracks the sum of log prices, allowing users to compute the geometric mean TWAP. Finally, Uniswap v3 adds a liquidity accumulator that is tracked alongside the price accumulator, which accumulates 1 ๐ฟ for each second. This liquidity accumulator is useful for external contracts that want to implement liquidity mining on top of Uniswap v3. It can also be used by other contracts to inform a decision on which of the pools corresponding to a pair (see section 3.1) will have the most reliable TWAP. 5.1 Oracle Observations As in Uniswap v2, Uniswap v3 tracks a running accumulator of the price at the beginning of each block, multiplied by the number of seconds since the last block. A pool in Uniswap v2 stores only the most recent value of this price accumulatorโthat is, the value as of the last block in which a swap occurred. When computing average prices in Uniswap v2, it is the responsibility of the external caller to provide the previous value of the price accumulator. With many users, each will have to provide their own methodology for checkpointing previous values of the accumulator, or coordinate on a shared method to reduce costs. And there is no way to guarantee that every block in which the pool is touched will be reflected in the accumulator. In Uniswap v3, the pool stores a list of previous values for the price accumulator (as well as the liquidity accumulator described in section 5.3). It does this by automatically checkpointing the accumulator value every time the pool is touched for the first time in a block, cycling through an array where the oldest checkpoint is eventually overwritten by a new one, similar to a circular buffer. While this array initially only has room for a single checkpoint, anyone can initialize additional storage slots to lengthen the array, extending to as many as 65,536 checkpoints. 3 This imposes the one-time gas cost of initializing additional storage slots for this array on whoever wants this pair to checkpoint more slots. The pool exposes the array of past observations to users, as well as a convenience function for finding the (interpolated) accumulator value at any historical timestamp within the checkpointed period. 5.2 Geometric Mean Price Oracle Uniswap v2 maintains two price accumulatorsโone for the price of token0 in terms of token1, and one for the price oftoken1 in terms of token0. Users can compute the time-weighted arithmetic mean of the prices over any period, by subtracting the accumulator value at the beginning of the period from the accumulator at the end of the period, then dividing the difference by the number of seconds in the period. Note that accumulators for token0 and token1 are tracked separately, since the time-weighted arithmetic mean price 3The maximum of 65,536 checkpoints allows fetching checkpoints for at least 9 days after they are written, assuming 13 seconds pass between each block and a checkpoint is written every block. of token0 is not equivalent to the reciprocal of the time-weighted arithmetic mean price of token1. Using the time-weighted geometric mean price, as Uniswap v3 does, avoids the need to track separate accumulators for these ratios. The geometric mean of a set of ratios is the reciprocal of the geometric mean of their reciprocals. It is also easy to implement in Uniswap v3 because of its implementation of custom liquidity provision, as described in section 6. In addition, the accumulator can be stored in a smaller number of bits, since it trackslog ๐ rather than ๐, and log ๐ can represent a wide range of prices with consistent precision.4 Finally, there is a theoretical argument that the time- weighted geometric mean price should be a truer representation of the average price.5 Instead of tracking the cumulative sum of the price ๐, Uniswap v3 accumulates the cumulative sum of the current tick index (๐๐๐1.0001๐, the logarithm of price for base 1.0001, which is precise up to 1 basis point). The accumulator at any given time is equal to the sum of ๐๐๐1.0001 (๐) for every second in the history of the contract: ๐๐ก = ๐กร ๐=1 log1.0001 (๐๐ ) (5.1) We want to estimate the geometric mean time-weighted average price (๐๐ก1,๐ก2) over any period ๐ก1 to ๐ก2. ๐๐ก1,๐ก2 = ยฉยญ ยซ ๐ก2ร ๐=๐ก1 ๐๐ ยชยฎ ยฌ 1 ๐ก2โ๐ก1 (5.2) To compute this, you can look at the accumulatorโs value at๐ก1 and at ๐ก2, subtract the first value from the second, divide by the number of seconds elapsed, and compute 1.0001๐ฅ to compute the time weighted geometric mean price. log1.0001 ๐๐ก1,๐ก2 = ร๐ก2 ๐=๐ก1 log1.0001 (๐๐ ) ๐ก2 โ ๐ก1 (5.3) log1.0001 ๐๐ก1,๐ก2 = ๐๐ก2 โ ๐๐ก1 ๐ก2 โ ๐ก1 (5.4) ๐๐ก1,๐ก2 = 1.0001 ๐๐ก2 โ๐๐ก1 ๐ก2โ๐ก1 (5.5) 5.3 Liquidity Oracle In addition to the seconds-weighted accumulator of log1.0001 ๐๐๐๐๐ , Uniswap v3 also tracks a seconds-weighted accumulator of 1 ๐ฟ (the reciprocal of the virtual liquidity currently in range) at the begin- ning of each block: secondsPerLiquidityCumulative (๐ ๐๐ ). This can be used by external liquidity mining contracts to fairly allocate rewards. If an external contract wants to distribute rewards at an even rate of ๐ tokens per second to all active liquidity in the 4In order to support tolerable precision across all possible prices, Uniswap v2 repre- sents each price as a 224-bit fixed-point number. Uniswap v3 only needs to represent ๐๐๐ 1.0001๐ as a signed 24-bit number, and still can detect price movements of one tick, or 1 basis point. 5While arithmetic mean TWAPs are much more widely used, they should theoretically be less accurate in measuring a geometric Brownian motion process (which is how price movements are usually modeled). The arithmetic mean of a geometric Brownian motion process will tend to overweight higher prices (where small percentage movements correspond to large absolute movements) relative to lower ones. 4
Uniswap v3 Core contract, and a position with ๐ฟ liquidity was active from ๐ก0 to ๐ก1, then its rewards for that period would be ๐ ยทLยท(๐ ๐๐ (๐ก1) โ ๐ ๐๐ (๐ก0)). In order to extend this so that concentrated liquidity is rewarded only when it is in range,Uniswap v3 stores a computed checkpoint based on this value every time a tick is crossed, as described in section 6.3. This accumulator can also be used by on-chain contracts to make their oracles stronger (such as by evaluating which fee-tier pool to use the oracle from). 6 IMPLEMENTING CONCENTRATED LIQUIDITY The rest of this paper describes how concentrated liquidity provi- sion works, and gives a high-level description of how it is imple- mented in the contracts. 6.1 Ticks and Ranges To implement custom liquidity provision, the space of possible prices is demarcated by discrete ticks. Liquidity providers can pro- vide liquidity in a range between any two ticks (which need not be adjacent). Each range can be specified as a pair of signed integertick indices: a lower tick (๐๐ ) and an upper tick ( ๐๐ข). Ticks represent prices at which the virtual liquidity of the contract can change. We will assume that prices are always expressed as the price of one of the tokensโcalled token0โin terms of the other tokenโ token1. The assignment of the two tokens to token0 and token1 is arbitrary and does not affect the logic of the contract (other than through possible rounding errors). Conceptually, there is a tick at every price ๐ that is an integer power of 1.0001. Identifying ticks by an integer index ๐, the price at each is given by: ๐ (๐) = 1.0001๐ (6.1) This has the desirable property of each tick being a .01% (1 basis point) price movement away from each of its neighboring ticks. For technical reasons explained in 6.2.1, however, pools actually track ticks at every square root price that is an integer power ofโ 1.0001. Consider the above equation, transformed into square root price space: โ๐ (๐) = โ 1.0001 ๐ = 1.0001 ๐ 2 (6.2) As an example,โ๐ (0)โthe square root price at tick 0โis 1, โ๐ (1) is โ 1.0001 โ 1.00005, and โ๐ (โ1) is 1โ 1.0001 โ 0.99995. When liquidity is added to a range, if one or both of the ticks is not already used as a bound in an existing position, that tick is initialized. Not every tick can be initialized. The pool is instantiated with a parameter, tickSpacing (๐ก๐ ); only ticks with indexes that are divisi- ble by tickSpacing can be initialized. For example, iftickSpacing is 2, then only even ticks (...-4, -2, 0, 2, 4...) can be initialized. Small choices for tickSpacing allow tighter and more precise ranges, but may cause swaps to be more gas-intensive (since each initialized tick that a swap crosses imposes a gas cost on the swapper). Whenever the price crosses an initialized tick, virtual liquidity is kicked in or out. The gas cost of an initialized tick crossing is constant, and is not dependent on the number of positions being kicked in or out at that tick. Ensuring that the right amount of liquidity is kicked in and out of the pool when ticks are crossed, and ensuring that each position earns its proportional share of the fees that were accrued while it was within range, requires some accounting within the pool. The pool contract uses storage variables to track state at a global (per-pool) level, at a per-tick level, and at a per-position level. 6.2 Global State The global state of the contract includes seven storage variables relevant to swaps and liquidity provision. (It has other storage variables that are used for the oracle, as described in section 5.) Type Variable Name Notation uint128 liquidity ๐ฟ uint160 sqrtPriceX96 โ ๐ int24 tick ๐๐ uint256 feeGrowthGlobal0X128 ๐๐,0 uint256 feeGrowthGlobal1X128 ๐๐,1 uint128 protocolFees.token0 ๐๐,0 uint128 protocolFees.token1 ๐๐,1 Table 1: Global State 6.2.1 Price and Liquidity. In Uniswap v2, each pool contract tracks the poolโs current reserves,๐ฅ and ๐ฆ. In Uniswap v3, the contract could be thought of as having virtual reservesโvalues for ๐ฅ and ๐ฆ that allow you to describe the contractโs behavior (between two adjacent ticks) as if it followed the constant product formula. Instead of tracking those virtual reserves, however, the pool contract tracks two different values:liquidity (๐ฟ) and sqrtPrice ( โ ๐). These could be computed from the virtual reserves with the following formulas: ๐ฟ = โ๐ฅ๐ฆ (6.3) โ ๐ = r๐ฆ ๐ฅ (6.4) Conversely, these values could be used to compute the virtual reserves: ๐ฅ = ๐ฟโ ๐ (6.5) ๐ฆ = ๐ฟ ยท โ ๐ (6.6) Using ๐ฟ and โ ๐ is convenient because only one of them changes at a time. Price (and thus โ ๐) changes when swapping within a tick; liquidity changes when crossing a tick, or when minting or burning liquidity. This avoids some rounding errors that could be encountered if tracking virtual reserves. You may notice that the formula for liquidity (based on virtual reserves) is similar to the formula used to initialize the quantity of liquidity tokens (based on actual reserves) in Uniswap v2. before 5
Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson any fees have been earned. In some ways, liquidity can be thought of as virtual liquidity tokens. Alternatively, liquidity can be thought of as the amount that token1 reserves (either actual or virtual) changes for a given change in โ ๐: ๐ฟ = ฮ๐ ฮ โ ๐ (6.7) We track โ ๐ instead of ๐ to take advantage of this relationship, and to avoid having to take any square roots when computing swaps, as described in section 6.2.3. The global state also tracks the current tick index as tick (๐๐), a signed integer representing the current tick (more specifically, the nearest tick below the current price). This is an optimization (and a way of avoiding precision issues with logarithms), since at any time, you should be able to compute the current tick based on the current sqrtPrice. Specifically, at any given time, the following equation should be true: ๐๐ = j logโ 1.0001 โ ๐ k (6.8) 6.2.2 Fees. Each pool is initialized with an immutable value, fee (๐พ), representing the fee paid by swappers in units of hundredths of a basis point (0.0001%). It also tracks the current protocol fee, ๐ (which is initialized to zero, but can changed by UNI governance).6 This number gives you the fraction of the fees paid by swappers that currently goes to the protocol rather than to liquidity providers. ๐ only has a limited set of permitted values: 0, 1/4, 1/5, 1/6, 1/7, 1/8, 1/9, or 1/10. The global state also tracks two numbers: feeGrowthGlobal0 (๐๐,0) andfeeGrowthGlobal1 (๐๐,1). These represent the total amount of fees that have been earned per unit of virtual liquidity (๐ฟ), over the entire history of the contract. You can think of them as the total amount of fees that would have been earned by 1 unit of unbounded liquidity that was deposited when the contract was first initialized. They are stored as fixed-point unsigned 128x128 numbers. Note that in Uniswap v3, fees are collected in the tokens themselves rather than in liquidity, for reasons explained in section 3.2.1. Finally, the global state tracks the total accumulated uncollected protocol fee in each token,protocolFees0 (๐๐,0) andprotocolFees1 (๐๐,1). This is an unsigned uint128. The accumulated protocol fees can be collected by UNI governance, by calling thecollectProtocol function. 6.2.3 Swapping Within a Single Tick. For small enough swaps, that do not move the price past a tick, the contracts act like an ๐ฅ ยท๐ฆ = ๐ pool. Suppose ๐พ is the fee, i.e., 0.003, and ๐ฆ๐๐ as the amount of token1 sent in. First, feeGrowthGlobal1 and protocolFees1 are incremented: ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท (1 โ ๐) (6.9) ฮ๐๐,1 = ๐ฆ๐๐ ยท๐พ ยท๐ (6.10) ฮ๐ฆ is the increase in ๐ฆ (after the fee is taken out). 6Technically, the storage variable called โprotocolFee" is the denominator of this fraction (or is zero, if ๐ is zero). ฮ๐ฆ = ๐ฆ๐๐ ยท (1 โ ๐พ) (6.11) If you used the computed virtual reserves (๐ฅ and ๐ฆ) for thetoken0 and token1 balances, then this formula could be used to find the amount of token0 sent out: ๐ฅ๐๐๐ = ๐ฅ ยท๐ฆ ๐ฆ + ฮ๐ฆ (6.12) But remember that inv3, the contract actually tracks liquidity (๐ฟ) and square root of price ( โ ๐) instead of ๐ฅ and ๐ฆ. We could compute ๐ฅ and ๐ฆ from those values, and then use those to calculate the execution price of the trade. But it turns out that there are simple formulas that describe the relationship between ฮ โ ๐ and ฮ๐ฆ, for a given ๐ฟ (which can be derived from formula 6.7): ฮ โ ๐ = ฮ๐ฆ ๐ฟ (6.13) ฮ๐ฆ = ฮ โ ๐ ยท๐ฟ (6.14) There are also simple formulas that describe the relationship between ฮ 1โ ๐ and ฮ๐ฅ: ฮ 1โ ๐ = ฮ๐ฅ ๐ฟ (6.15) ฮ๐ฅ = ฮ 1โ ๐ ยท๐ฟ (6.16) When swapping one token for the other, the pool contract can first compute the new โ ๐ using formula 6.13 or 6.15, and then can compute the amount of token0 or token1 to send out using formula 6.14 or 6.16. These formulas will work for any swap that does not push โ ๐ past the price of the next initialized tick. If the computed ฮ โ ๐ would cause โ ๐ to move past that next initialized tick, the contract must only cross up to that tickโusing up only part of the swapโand then cross the tick, as described in section 6.3.1, before continuing with the rest of the swap. 6.2.4 Initialized Tick Bitmap. If a tick is not used as the endpoint of a range with any liquidity in itโthat is, if the tick is uninitial- izedโthen that tick can be skipped during swaps. As an optimization to make finding the next initialized tick more efficient, the pool tracks a bitmap tickBitmap of initialized ticks. The position in the bitmap that corresponds to the tick index is set to 1 if the tick is initialized, and 0 if it is not initialized. When a tick is used as an endpoint for a new position, and that tick is not currently used by any other liquidity, the tick is initialized, and the corresponding bit in the bitmap is set to 1. An initialized tick can become uninitialized again if all of the liquidity for which it is an endpoint is removed, in which case that tickโs position on the bitmap is zeroed out. 6.3 Tick-Indexed State The contract needs to store information about each tick in order to track the amount of net liquidity that should be added or removed when the tick is crossed, as well as to track the fees earned above and below that tick. 6
Related Stories
Uniswap V2: How Constant Product Markets Changed DeFi Forever
A technical walkthrough of Uniswap's x*y=k formula, price oracles, flash swaps, and the elegant simplicity behind decenโฆ
Technical ExplainerAutomated Market Makers: How DEXs Trade Without Order Books
Understanding constant product formulas, liquidity pools, impermanent loss, and why AMMs revolutionized token trading.
Impact & LegacyDeFi Summer 2020: How Yield Farming Changed Crypto Finance Forever
The explosive growth of decentralized lending, AMMs, and yield farming that proved blockchain could replicate โ and impโฆ
Foire aux questions
- Qu'est-ce que le livre blanc d'Uniswap ?
- Le livre blanc d'Uniswap v3 dรฉcrit un protocole de teneur de marchรฉ automatisรฉ (AMM) ร liquiditรฉ concentrรฉe. Il permet aux fournisseurs de liquiditรฉ d'allouer des capitaux dans des fourchettes de prix personnalisรฉes, amรฉliorant considรฉrablement l'efficacitรฉ du capital par rapport aux AMM ร produit constant.
- Qui a rรฉdigรฉ le livre blanc d'Uniswap et quand ?
- Le livre blanc d'Uniswap v3 a รฉtรฉ rรฉdigรฉ par Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer et Dan Robinson. Uniswap v1 a รฉtรฉ lancรฉ en 2018 par Hayden Adams, et la v3 a รฉtรฉ publiรฉe en mars 2021.
- Quelle est l'innovation technique centrale d'Uniswap ?
- Uniswap v3 a introduit la liquiditรฉ concentrรฉe โ permettant aux fournisseurs de liquiditรฉ (LP) de fournir de la liquiditรฉ dans des fourchettes de prix spรฉcifiques plutรดt que sur l'ensemble de la courbe de prix. Cela permet d'atteindre jusqu'ร 4 000 fois l'efficacitรฉ du capital par rapport ร la formule du produit constant de la v2 (x * y = k).
- Comment fonctionne le mรฉcanisme AMM d'Uniswap ?
- Uniswap remplace les carnets d'ordres traditionnels par des pools de liquiditรฉ. Les traders รฉchangent contre des rรฉserves mutualisรฉes, les prix รฉtant dรฉterminรฉs de maniรจre algorithmique. Dans la v3, la liquiditรฉ est concentrรฉe dans des paliers de prix, et les frais ne s'accumulent que sur les positions actives (dans la fourchette).
- En quoi Uniswap diffรจre-t-il des exchanges centralisรฉs ?
- Uniswap est non-custodial โ les utilisateurs tradent directement depuis leurs portefeuilles sans intermรฉdiaires. Il fonctionne 24h/24 et 7j/7 sans exigences de cotation : n'importe qui peut crรฉer un pool pour n'importe quelle paire de tokens ERC-20. La contrepartie est un slippage potentiellement plus รฉlevรฉ pour les ordres importants.
- Quel est le modรจle d'approvisionnement de UNI ?
- UNI dispose d'une offre totale d'un milliard de tokens distribuรฉs sur 4 ans : 60 % ร la communautรฉ, 21,27 % ร l'รฉquipe, 18,04 % aux investisseurs et 0,69 % aux conseillers. Aprรจs la distribution initiale, une inflation annuelle perpรฉtuelle de 2 % est prรฉvue pour la participation ร la gouvernance.
- Quels sont les principaux cas d'usage d'Uniswap ?
- Uniswap est utilisรฉ pour l'รฉchange de tokens, la fourniture de liquiditรฉ (gรฉnรฉration de rendement), la dรฉcouverte de prix pour les nouveaux tokens et comme infrastructure sous-jacente pour les agrรฉgateurs DeFi. Il est dรฉployรฉ sur Ethereum, Arbitrum, Optimism, Polygon, Base et d'autres chaรฎnes.
- Quel problรจme Uniswap rรฉsout-il ?
- Uniswap rรฉsout le problรจme d'amorรงage de liquiditรฉ pour les actifs crypto de longue traรฎne. Les carnets d'ordres traditionnels nรฉcessitent des teneurs de marchรฉ actifs ; l'AMM d'Uniswap permet ร n'importe qui de fournir de la liquiditรฉ et de faciliter les รฉchanges pour des tokens qui n'auraient autrement aucun marchรฉ.
- Comment fonctionne le modรจle de sรฉcuritรฉ d'Uniswap ?
- Les contrats intelligents d'Uniswap sont immuables une fois dรฉployรฉs et ont subi plusieurs audits. La sรฉcuritรฉ repose sur la couche d'exรฉcution Ethereum. Le protocole ne dispose d'aucune clรฉ administrateur pour les contrats principaux โ il est entiรจrement sans autorisation et ne peut pas รชtre mis en pause ni mis ร niveau.
- Quel est l'รฉtat actuel de l'รฉcosystรจme Uniswap ?
- Uniswap est le plus grand exchange dรฉcentralisรฉ en termes de volume et est dรฉployรฉ sur plus de 10 chaรฎnes. Uniswap v4 introduit les hooks โ des plugins personnalisables pour les pools โ et Unichain, une appchain dรฉdiรฉe ร Uniswap, รฉlargissant les capacitรฉs du protocole.