Uniswap v3 Core March 2021 Hayden Adams Noah Zinsmeister Moody Salem [email protected] [email protected] [email protected] River Keefer Dan Robinson [email protected] [email protected] ABSTRACT In this paper, we present Uniswap v3, a novel AMM that gives Uniswap v3 is a noncustodial automated market maker imple- liquidity providers more control over the price ranges in which mented for the Ethereum Virtual Machine. In comparison to earlier their capital is used, with limited effect on liquidity fragmentation versions of the protocol, Uniswap v3 provides increased capital and gas inefficiency. This design does not depend on any shared efficiency and fine-tuned control to liquidity providers, improves assumption about the price behavior of the tokens. Uniswap v3 the accuracy and convenience of the price oracle, and has a more is based on the same constant product reserves curve as earlier flexible fee structure. versions [1], but offers several significant new features: • Concentrated Liquidity: Liquidity providers (LPs) are given 1 INTRODUCTION the ability to concentrate their liquidity by “bounding" it Automated market makers (AMMs) are agents that pool liquidity within an arbitrary price range. This improves the pool’s and make it available to traders according to an algorithm [5]. Con- capital efficiency and allows LPs to approximate their pre- stant function market makers (CFMMs), a broad class of AMMs of ferred reserves curve, while still being efficiently aggregated which Uniswap is a member, have seen widespread use in the con- with the rest of the pool. We describe this feature in section text of decentralized finance, where they are typically implemented 2 and its implementation in Section 6. as smart contracts that trade tokens on a permissionless blockchain • Flexible Fees: The swap fee is no longer locked at 0.30%. [2]. Rather, the fee tier for each pool (of which there can be CFMMs as they are implemented today are often capital inef- multiple per asset pair) is set on initialization (Section 3.1). ficient. In the constant product market maker formula used by The initially supported fee tiers are 0.05%, 0.30%, and 1%. Uniswap v1 and v2, only a fraction of the assets in the pool are UNI governance is able to add additional values to this set. available at a given price. This is inefficient, particularly when • Protocol Fee Governance: UNI governance has more flexibility assets are expected to trade close to a particular price at all times. in setting the fraction of swap fees collected by the protocol Prior attempts to address this capital efficiency issue, such as (Section 6.2.2). Curve [3] and YieldSpace [4], have involved building pools that use • Improved Price Oracle: Uniswap v3 provides a way for users different functions to describe the relation between reserves. This to query recent price accumulator values, thus avoiding the requires all liquidity providers in a given pool to adhere to a single need to checkpoint the accumulator value at the exact be- formula, and could result in liquidity fragmentation if liquidity ginning and end of the period for which a TWAP is being providers want to provide liquidity within different price ranges. 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- liquidity is composed entirely of a single asset, because the reserves erage liquidity oracle (Section 5.3). of the other asset must have been entirely depleted. If the price ever The Uniswap v2 core contracts are non-upgradeable by de- reenters the range, the liquidity becomes active again. sign, so Uniswap v3 is implemented as an entirely new set of The amount of liquidity √ provided can be measured by the value contracts, available here. The Uniswap v3 core contracts are also 𝐿, which is equal to 𝑘. The real reserves of a position are described non-upgradeable, with some parameters controlled by governance by the curve: as described in Section 4. 𝐿 √ (𝑥 + √ )(𝑦 + 𝐿 𝑝𝑎 ) = 𝐿 2 (2.2) 2 CONCENTRATED LIQUIDITY 𝑝𝑏 The defining idea of Uniswap v3 is that of concentrated liquidity: This curve is a translation of formula 2.1 such that the position is liquidity bounded within some price range. solvent exactly within its range (Fig. 2). In earlier versions, liquidity was distributed uniformly along the 𝑥 ·𝑦 = 𝑘 (2.1) reserves curve, where 𝑥 and 𝑦 are the respective reserves of two virtual reserves (2.1) assets X and Y, and 𝑘 is a constant [1]. In other words, earlier ver- real reserves (2.2) 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. Y Reserves 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. 𝑎 virtual reserves X Reserves 𝑥 real Figure 2: Real Reserves 𝑏 Y 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 𝑦real 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. X Reserves 2.1 Range Orders Figure 1: Simulation of Virtual Liquidity Positions on very small ranges act similarly to limit orders—if the Specifically, a position only needs to hold enough of asset X to range is crossed, the position flips from being composed entirely cover price movement to its upper bound, because upwards price of one asset, to being composed entirely of the other asset (plus movement1 corresponds to depletion of the X reserves. Similarly, accrued fees). There are two differences between this range order it only needs to hold enough of asset Y to cover price movement and a traditional limit order: to its lower bound. Fig. 1 depicts this relationship for a position on • There is a limit to how narrow a position’s range can be. a range [𝑝𝑎 , 𝑝𝑏 ] and a current price 𝑝𝑐 ∈ [𝑝𝑎 , 𝑝𝑏 ]. 𝑥 real and 𝑦real While the price is within that range, the limit order might denote the position’s real reserves. be partially executed. When the price exits a position’s range, the position’s liquidity • When the position has been crossed, it needs to be with- is no longer active, and no longer earns fees. At that point, its drawn. If it is not, and the price crosses back across that 1 We take asset Y to be the unit of account, which corresponds to token1 in our range, the position will be traded back, effectively reversing implementation. the trade. 2

Uniswap v3 Core Liquidity Liquidity Liquidity 0 ∞ 𝑝𝑎 𝑝𝑏 Price Price Price (I) Uniswap v2 (II) A single position on [𝑝𝑎 , 𝑝𝑏 ] (III) A collection of custom positions Figure 3: Example Liquidity Distributions 3 ARCHITECTURAL CHANGES pool as individual tokens, rather than automatically reinvested as Uniswap v3 makes a number of architectural changes, some of liquidity in the pool. which are necessitated by the inclusion of concentrated liquidity, As a result, in v3, the pool contract does not implement the and some of which are independent improvements. ERC-20 standard. Anyone can create an ERC-20 token contract in the periphery that makes a liquidity position more fungible, but 3.1 Multiple Pools Per Pair it will have to have additional logic to handle distribution of, or reinvestment of, collected fees. Alternatively, anyone could create In Uniswap v1 and v2, every pair of tokens corresponds to a single a periphery contract that wraps an individual liquidity position liquidity pool, which applies a uniform fee of 0.30% to all swaps. (including collected fees) in an ERC-721 non-fungible token. While this default fee tier historically worked well enough for many tokens, it is likely too high for some pools (such as pools between 4 GOVERNANCE two stablecoins), and too low for others (such as pools that include highly volatile or rarely traded tokens). The factory has an owner, which is initially controlled by UNI Uniswap v3 introduces multiple pools for each pair of tokens, tokenholders.2 The owner does not have the ability to halt the each with a different swap fee. All pools are created by the same operation of any of the core contracts. factory contract. The factory contract initially allows pools to be As in Uniswap v2, Uniswap v3 has a protocol fee that can be created at three fee tiers: 0.05%, 0.30%, and 1%. Additional fee tiers turned on by UNI governance. In Uniswap v3, UNI governance has can be enabled by UNI governance. 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. 3.2 Non-Fungible Liquidity UNI governance also has the ability to add additional fee tiers. 3.2.1 Non-Compounding Fees. Fees earned in earlier versions were When it adds a new fee tier, it can also define the tickSpacing continuously deposited in the pool as liquidity. This meant that (see Section 6.1) corresponding to that fee tier. Once a fee tier is liquidity in the pool would grow over time, even without explicit added to the factory, it cannot be removed (and the tickSpacing deposits, and that fee earnings compounded. cannot be changed). The initial fee tiers and tick spacings supported In Uniswap v3, due to the non-fungible nature of positions, this are 0.05% (with a tick spacing of 10, approximately 0.10% between is no longer possible. Instead, fee earnings are stored separately initializable ticks), 0.30% (with a tick spacing of 60, approximately and held as the tokens in which the fees are paid (see Section 6.2.2). 0.60% between initializable ticks), and 1% (with a tick spacing of 3.2.2 Removal of Native Liquidity Tokens. In Uniswap v1 and v2, 200, approximately 2.02% between ticks. the pool contract is also an ERC-20 token contract, whose tokens Finally, UNI governance has the power to transfer ownership to represent liquidity held in the pool. While this is convenient, it another address. 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 5 ORACLE UPGRADES periphery, and blessing one “canonical" ERC-20 implementation Uniswap v3 includes three significant changes to the time-weighted discourages the creation of improved ERC-20 token wrappers. Ar- average price (TWAP) oracle that was introduced by Uniswap v2. guably, the ERC-20 token implementation should have been in the Most significantly, Uniswap v3 removes the need for users of periphery, as a wrapper on a single liquidity position in the core the oracle to track previous values of the accumulator externally. contract. Uniswap v2 requires users to checkpoint the accumulator value The changes made in Uniswap v3 force this issue by making at both the beginning and end of the time period for which they completely fungible liquidity tokens impossible. Due to the custom 2 Specifically, the owner will be initialized to the Timelock contract from UNI gover- liquidity provision feature, fees are now collected and held by the nance, 0x1a9c8182c09f50c8318d769245bea52c32be35bc. 3

Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson wanted to compute a TWAP. Uniswap v3 brings the accumulator Using the time-weighted geometric mean price, as Uniswap v3 checkpoints into core, allowing external contracts to compute on- does, avoids the need to track separate accumulators for these chain TWAPs over recent periods without storing checkpoints of ratios. The geometric mean of a set of ratios is the reciprocal of the the accumulator value. geometric mean of their reciprocals. It is also easy to implement Another change is that instead of accumulating the sum of prices, in Uniswap v3 because of its implementation of custom liquidity allowing users to compute the arithmetic mean TWAP, Uniswap provision, as described in section 6. In addition, the accumulator can v3 tracks the sum of log prices, allowing users to compute the be stored in a smaller number of bits, since it tracks log 𝑃 rather than geometric mean TWAP. 𝑃, and log 𝑃 can represent a wide range of prices with consistent Finally, Uniswap v3 adds a liquidity accumulator that is tracked precision.4 Finally, there is a theoretical argument that the time- alongside the price accumulator. This liquidity accumulator can be weighted geometric mean price should be a truer representation of used by other contracts to inform a decision on which of the pools the average price.5 corresponding to a pair (see section 3.1) will have the most reliable Instead of tracking the cumulative sum of the price 𝑃, Uniswap TWAP. 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 5.1 Oracle Observations point). The accumulator at any given time is equal to the sum of 𝑙𝑜𝑔1.0001 (𝑃) for every second in the history of the contract: 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. Õ 𝑎𝑡 = log1.0001 (𝑃𝑖 ) (5.1) A pool in Uniswap v2 stores only the most recent value of this 𝑖=1 price accumulator—that is, the value as of the last block in which a swap occurred. When computing average prices in Uniswap v2, it We want to estimate the geometric mean time-weighted average is the responsibility of the external caller to provide the previous price (𝑝𝑡1 ,𝑡2 ) over any period 𝑡 1 to 𝑡 2 . value of the price accumulator. With many users, each will have to 1 provide their own methodology for checkpointing previous values 𝑡2 𝑡 2 −𝑡 1 ©Ö ª of the accumulator, or coordinate on a shared method to reduce 𝑃𝑡1 ,𝑡2 = 𝑃𝑖 ® (5.2) costs. And there is no way to guarantee that every block in which «𝑖=𝑡1 ¬ the pool is touched will be reflected in the accumulator. To compute this, you can look at the accumulator’s value at 𝑡 1 In Uniswap v3, the pool stores a list of previous values for the and at 𝑡 2 , subtract the first value from the second, divide by the accumulator. It does this by automatically checkpointing the accu- number of seconds elapsed, and compute 1.0001𝑥 to compute the mulator value every time the pool is touched for the first time in time weighted geometric mean price. a block, cycling through an array where the oldest checkpoint is eventually overwritten by a new one, similar to a circular buffer. Í𝑡2 log1.0001 (𝑃𝑖 ) While this array initially only has room for a single checkpoint, log1.0001 𝑃𝑡1 ,𝑡2 = 𝑖=𝑡 1 (5.3) anyone can initialize additional storage slots to lengthen the array, 𝑡2 − 𝑡1 extending to as many as 65,536 checkpoints.3 This imposes the 𝑎𝑡 − 𝑎𝑡 1 one-time gas cost of initializing additional storage slots for this log1.0001 𝑃𝑡1 ,𝑡2 = 2 (5.4) array on whoever wants this pair to checkpoint more slots. 𝑡2 − 𝑡1 The pool exposes the array of past observations to users, as well 𝑎𝑡 2 −𝑎𝑡 1 as a convenience function for finding the (interpolated) accumulator 𝑃𝑡1 ,𝑡2 = 1.0001 𝑡 2 −𝑡 1 (5.5) value at any historical timestamp within the checkpointed period. 5.3 Liquidity Oracle 5.2 Geometric Mean Price Oracle In addition to the time weighted average price, Uniswap v3 also Uniswap v2 maintains two price accumulators—one for the price of tracks an accumulator of the current value of 𝐿 (the virtual liquidity token0 in terms of token1, and one for the price of token1 in terms currently in range) at the beginning of each block. This can be of token0. Users can compute the time-weighted arithmetic mean used by on-chain contracts to make their oracles stronger (such of the prices over any period, by subtracting the accumulator value as by evaluating which fee-tier pool to use the oracle from). This at the beginning of the period from the accumulator at the end of liquidity accumulator’s values are checkpointed along with the the period, then dividing the difference by the number of seconds price accumulator. in the period. Note that accumulators for token0 and token1 are tracked separately, since the time-weighted arithmetic mean price 4 Inorder to support tolerable precision across all possible prices, Uniswap v2 repre- of token0 is not equivalent to the reciprocal of the time-weighted sents each price as a 224-bit fixed-point number. Uniswap v3 only needs to represent arithmetic mean price of token1. 𝑙𝑜𝑔1.0001 𝑃 as a signed 24-bit number, and still can detect price movements of one tick, or 1 basis point. 5 While 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 3 The maximum of 65,536 checkpoints allows fetching checkpoints for at least 9 days movements are usually modeled). The arithmetic mean of a geometric Brownian motion after they are written, assuming 13 seconds pass between each block and a checkpoint process will tend to overweight higher prices (where small percentage movements is written every block. correspond to large absolute movements) relative to lower ones. 4

Uniswap v3 Core 6 IMPLEMENTING CONCENTRATED 6.2 Global State LIQUIDITY The global state of the contract includes seven storage variables The rest of this paper describes how concentrated liquidity provi- relevant to swaps and liquidity provision. (It has other storage sion works, and gives a high-level description of how it is imple- variables that are used for the oracle, as described in section 5.) mented in the contracts. Type Variable Name Notation 6.1 Ticks and Ranges uint128 liquidity 𝐿 √ uint160 sqrtPriceX96 𝑃 To implement custom liquidity provision, the space of possible int24 tick 𝑖𝑐 prices is demarcated by discrete ticks. Liquidity providers can pro- uint256 feeGrowthGlobal0X128 𝑓𝑔,0 vide liquidity in a range between any two ticks (which need not be uint256 feeGrowthGlobal1X128 𝑓𝑔,1 adjacent). uint128 protocolFees.token0 𝑓𝑝,0 Each range can be specified as a pair of signed integer tick indices: uint128 protocolFees.token1 𝑓𝑝,1 a lower tick (𝑖𝑙 ) and an upper tick (𝑖𝑢 ). Ticks represent prices at Table 1: Global State 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 6.2.1 Price and Liquidity. In Uniswap v2, each pool contract tracks and does not affect the logic of the contract (other than through the pool’s current reserves, 𝑥 and 𝑦. In Uniswap v3, the contract possible rounding errors). could be thought of as having virtual reserves—values for 𝑥 and 𝑦 Conceptually, there is a tick at every price 𝑝 that is an integer that allow you to describe the contract’s behavior (between two power of 1.0001. Identifying ticks by an integer index 𝑖, the price at adjacent ticks) as if it followed the constant product formula. each is given by: Instead of tracking those virtual reserves, however, the pool contract √ tracks two different values: liquidity (𝐿) and sqrtPrice 𝑝 (𝑖) = 1.0001𝑖 (6.1) ( 𝑃). These could be computed from the virtual reserves with the following formulas: This has the desirable property of each tick being a .01% (1 basis point) price movement away from each of its neighboring ticks. √ 𝐿 = 𝑥𝑦 (6.3) For technical reasons explained in 6.2.1, however, pools actually track √ ticks at every square root price that is an integer power of √ r 𝑦 1.0001. Consider the above equation, transformed into square root 𝑃= (6.4) 𝑥 price space: Conversely, these values could be used to compute the virtual √ √ 𝑖 𝑖 reserves: 𝑝 (𝑖) = 1.0001 = 1.0001 2 (6.2) √ √ 𝐿 As an example, 𝑝 (0)—the square root price at tick 0—is 1, 𝑝 (1) 𝑥= √ (6.5) √ √ 𝑃 is 1.0001 ≈ 1.00005, and 𝑝 (−1) is √ 1 ≈ 0.99995. √ 1.0001 When liquidity is added to a range, if one or both of the ticks 𝑦=𝐿· 𝑃 (6.6) √ is not already used as a bound in an existing position, that tick is Using 𝐿 and 𝑃 is convenient √ because only one of them changes initialized. at a time. Price (and thus 𝑃) changes when swapping within a Not every tick can be initialized. The pool is instantiated with a tick; liquidity changes when crossing a tick, or when minting or parameter, tickSpacing (𝑡𝑠 ); only ticks with indexes that are divisi- burning liquidity. This avoids some rounding errors that could be ble by tickSpacing can be initialized. For example, if tickSpacing encountered if tracking virtual reserves. is 2, then only even ticks (...-4, -2, 0, 2, 4...) can be initialized. Small You may notice that the formula for liquidity (based on virtual choices for tickSpacing allow tighter and more precise ranges, but reserves) is similar to the formula used to initialize the quantity of may cause swaps to be more gas-intensive (since each initialized liquidity tokens (based on actual reserves) in Uniswap v2. before tick that a swap crosses imposes a gas cost on the swapper). any fees have been earned. In some ways, liquidity can be thought Whenever the price crosses an initialized tick, virtual liquidity of as virtual liquidity tokens. is kicked in or out. The gas cost of an initialized tick crossing is Alternatively, liquidity can be thought of as the amount that constant, and is not dependent on the number of positions being √ reserves (either actual or virtual) changes for a given change token1 kicked in or out at that tick. in 𝑃: Ensuring that the right amount of liquidity is kicked in and out of the pool when ticks are crossed, and ensuring that each position Δ𝑌 𝐿= √ (6.7) earns its proportional share of the fees that were accrued while Δ 𝑃 √ it was within range, requires some accounting within the pool. We track 𝑃 instead of 𝑃 to take advantage of this relationship, The pool contract uses storage variables to track state at a global and to avoid having to take any square roots when computing (per-pool) level, at a per-tick level, and at a per-position level. swaps, as described in section 6.2.3. 5

Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson The global state also tracks the current tick index as tick (𝑖𝑐 ), a execution price of the trade. But it turns out that √ there are simple signed integer representing the current tick (more specifically, the formulas that describe the relationship between Δ 𝑃 and Δ𝑦, for a nearest tick below the current price). This is an optimization (and given 𝐿 (which can be derived from formula 6.7): a way of avoiding precision issues with logarithms), since at any time, you should be able to compute the current tick based on the √ Δ𝑦 Δ 𝑃= (6.13) current sqrtPrice. Specifically, at any given time, the following 𝐿 equation should be true: √ Δ𝑦 = Δ 𝑃 · 𝐿 (6.14) j √ k 𝑖𝑐 = log√1.0001 𝑃 (6.8) There are also simple formulas that describe the relationship between Δ √1 and Δ𝑥: 6.2.2 Fees. Each pool is initialized with an immutable value, fee 𝑃 (𝛾), representing the fee paid by swappers in units of hundredths 1 Δ𝑥 of a basis point (0.0001%). Δ√ = (6.15) 𝑃 𝐿 It also tracks the current protocol fee, 𝜙 (which is initialized to zero, but can changed by UNI governance).6 This number gives you 1 Δ𝑥 = Δ √ · 𝐿 (6.16) the fraction of the fees paid by swappers that currently goes to the 𝑃 protocol rather than to liquidity providers. 𝜙 only has a limited set When swapping one √ token for the other, the pool contract can of permitted values: 0, 1/4, 1/5, 1/6, 1/7, 1/8, 1/9, or 1/10. first compute the new 𝑃 using formula 6.13 or 6.15, and then The global state also tracks two numbers: feeGrowthGlobal0 can compute the amount of token0 or token1 to send out using (𝑓𝑔,0 ) and feeGrowthGlobal1 (𝑓𝑔,1 ). These represent the total amount formula 6.14 or 6.16. of fees that have been earned per unit of virtual liquidity (𝐿), over √ These formulas will work for any swap that does not push √𝑃 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 √ of the next initialized tick. If the computed Δ 𝑃 past the price would cause 𝑃 to move past that next initialized tick, the contract liquidity that was deposited when the contract was first initialized. must only cross up to that tick—using up only part of the swap—and They are stored as fixed-point unsigned 128x128 numbers. Note then cross the tick, as described in section 6.3.1, before continuing that in Uniswap v3, fees are collected in the tokens themselves with the rest of the swap. rather than in liquidity, for reasons explained in section 3.2.1. Finally, the global state tracks the total accumulated uncollected 6.2.4 Initialized Tick Bitmap. If a tick is not used as the endpoint protocol fee in each token, protocolFees0 (𝑓𝑝,0 ) and protocolFees1 of a range with any liquidity in it—that is, if the tick is uninitial- (𝑓𝑝,1 ). This is an unsigned uint128. The accumulated protocol fees ized—then that tick can be skipped during swaps. can be collected by UNI governance, by calling the collectProtocol As an optimization to make finding the next initialized tick more function. efficient, the pool tracks a bitmap tickBitmap of initialized ticks. The position in the bitmap that corresponds to the tick index is set 6.2.3 Swapping Within a Single Tick. For small enough swaps, that to 1 if the tick is initialized, and 0 if it is not initialized. do not move the price past a tick, the contracts act like an 𝑥 · 𝑦 = 𝑘 When a tick is used as an endpoint for a new position, and that pool. tick is not currently used by any other liquidity, the tick is initialized, Suppose 𝛾 is the fee, i.e., 0.003, and 𝑦𝑖𝑛 as the amount of token1 and the corresponding bit in the bitmap is set to 1. An initialized sent in. tick can become uninitialized again if all of the liquidity for which First, feeGrowthGlobal1 and protocolFees1 are incremented: it is an endpoint is removed, in which case that tick’s position on the bitmap is zeroed out. Δ𝑓𝑔,1 = 𝑦𝑖𝑛 · 𝛾 · (1 − 𝜙) (6.9) 6.3 Tick-Indexed State Δ𝑓𝑝,1 = 𝑦𝑖𝑛 · 𝛾 · 𝜙 (6.10) The contract needs to store information about each tick in order to Δ𝑦 is the increase in 𝑦 (after the fee is taken out). 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 Δ𝑦 = 𝑦𝑖𝑛 · (1 − 𝛾) (6.11) and below that tick. If you used the computed virtual reserves (𝑥 and 𝑦) for the token0 The contract stores a mapping from tick indexes (int24) to the and token1 balances, then this formula could be used to find the following four values: amount of token0 sent out: 𝑥 ·𝑦 Type Variable Name Notation 𝑥𝑒𝑛𝑑 = (6.12) Δ𝐿 𝑦 + Δ𝑦 int128 liquidityNet But remember that in v3, uint128 liquidityGross 𝐿𝑔 √ the contract actually tracks liquidity (𝐿) uint256 feeGrowthOutside0X128 𝑓𝑜,0 and square root of price ( 𝑃) instead of 𝑥 and 𝑦. We could compute 𝑥 and 𝑦 from those values, and then use those to calculate the uint256 feeGrowthOutside1X128 𝑓𝑜,1 6 Technically, Table 2: Tick-Indexed State the storage variable called “protocolFee" is the denominator of this fraction (or is zero, if 𝜙 is zero). 6

Uniswap v3 Core Start Fail S0. Check input Stop Pass S1. Swap within current interval No S2. Is there remaining input or output? S5. Execute computed swap Yes S4. Cross next tick Figure 4: Swap Control Flow Each tick tracks Δ𝐿, the total amount of liquidity that should be kicked in or out when the tick is crossed. The tick only needs 𝑓𝑜 (𝑖) := 𝑓𝑔 − 𝑓𝑜 (𝑖) (6.20) to track one signed integer: the amount of liquidity added (or, if 𝑓𝑜 is only needed for ticks that are used as either the lower or negative, removed) when the tick is crossed going left to right. This upper bound for at least one position. As a result, for efficiency, 𝑓𝑜 is value does not need to be updated when the tick is crossed (but not initialized (and thus does not need to be updated when crossed) only when a position with a bound at that tick is updated). until a position is created that has that tick as one of its bounds. We want to be able to uninitialize a tick when there is no longer When 𝑓𝑜 is initialized for a tick 𝑖, the value—by convention—is any liquidity referencing that tick. Since Δ𝐿 is a net value, it’s chosen as if all of the fees earned to date had occurred below that necessary to track a gross tally of liquidity referencing the tick, 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 𝑓𝑔 𝑖𝑐 ≥ 𝑖 𝑓𝑜 := (6.21) tick bitmap. 0 𝑖𝑐 < 𝑖 feeGrowthOutside{0,1} are used to track how many fees were Note that since 𝑓𝑜 values for different ticks could be initialized accumulated within a given range. Since the formulas are the same at different times, comparisons of the 𝑓𝑜 values for different ticks for the fees collected in token0 and token1, we will omit that sub- are not meaningful, and there is no guarantee that values for 𝑓𝑜 script for the rest of this section. will be consistent. This does not cause a problem for per-position You can compute the fees earned per unit of liquidity in token 0 accounting, since, as described below, all the position needs to know above (𝑓𝑎 ) and below (𝑓𝑏 ) a tick 𝑖 with a formula that depends on is the growth in 𝑔 within a given range since that position was last whether the price is currently within or outside that range—that is, touched. whether the current tick index 𝑖𝑐 is greater than or equal to 𝑖: Finally, the contract also stores secondsOutside (𝑡𝑜 ) for each ( tick. This can be thought of as the amount of time spent on the 𝑓𝑔 − 𝑓𝑜 (𝑖) 𝑖𝑐 ≥ 𝑖 other side of this tick (relative to the current price), and can be used 𝑓𝑎 (𝑖) = (6.17) 𝑓𝑜 (𝑖) 𝑖𝑐 < 𝑖 to compute the number of seconds that have been spend above or ( below a tick for a particular range. This value is not used within the 𝑓𝑜 (𝑖) 𝑖𝑐 ≥ 𝑖 contract, but is tracked for the convenience of external contracts 𝑓𝑏 (𝑖) = (6.18) 𝑓𝑔 − 𝑓𝑜 (𝑖) 𝑖𝑐 < 𝑖 that want to know how many seconds a given position has been We can use these functions to compute the total amount of active. cumulative fees per share 𝑓𝑖𝑙 ,𝑖𝑢 in the range between two ticks—a As with 𝑓𝑎 and 𝑓𝑏 , the time spent above (𝑡𝑎 ) and below (𝑡𝑏 ) a lower tick 𝑖𝑙 and an upper tick 𝑖𝑢 : given tick is computed differently based on whether the current price is within that range, and the time spent within a range (𝑡𝑟 ) 𝑓𝑖𝑙 ,𝑖𝑢 (0) = 𝑓𝑔 − 𝑓𝑏 (𝑖𝑙 ) − 𝑓𝑎 (𝑖𝑢 ) (6.19) can be computed using the values of 𝑡𝑎 and 𝑡𝑏 𝑓𝑜,1 needs to be updated each time the tick is crossed. Specifically, ( as a tick 𝑖 is crossed in either direction, its 𝑓𝑜 (for each token) should 𝑡 − 𝑡𝑜 (𝑖) 𝑖𝑐 ≥ 𝑖 𝑡𝑎 (𝑖) = (6.22) be updated as follows: 𝑡𝑜 (𝑖) 𝑖𝑐 < 𝑖 7

Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson ( 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 𝑡𝑏 (𝑖) = (6.23) 𝑡 − 𝑡𝑜 (𝑖) 𝑖𝑐 < 𝑖 share grows over time), the units for liquidity do not change as fees √ are accumulated; it is always measured as 𝑥 · 𝑦, where 𝑥 and 𝑦 𝑡𝑟 (𝑖𝑙 , 𝑖𝑢 ) = 𝑡 − 𝑡𝑏 (𝑖𝑙 ) − 𝑡𝑎 (𝑖𝑢 ) (6.24) are quantities of token0 and token1, respectively. The number of seconds spent within a range between two times This liquidity number does not reflect the fees that have been 𝑡 1 and 𝑡 2 can be computed by recording the value of 𝑡𝑟 (𝑖𝑙 , 𝑖𝑢 ) at 𝑡 1 accumulated since the contract was last touched, which we will and at 𝑡 2 , and subtracting the former from the latter. call uncollected fees. Computing these uncollected fees requires Like 𝑓𝑜 , 𝑡𝑜 does not need to be tracked for ticks that are not additional stored values on the position, feeGrowthInside0Last on the edge of any position. Therefore, it is not initialized until a (𝑓𝑟,0 (𝑡 0 )) and feeGrowthInside1Last (𝑓𝑟,1 (𝑡 0 )), as described be- position is created that is bounded by that tick. By convention, it is low. initialized as if every second since the Unix timestamp 0 had been spent below that tick: 6.4.1 setPosition. The setPosition function allows a liquidity provider to update their position. Two of the arguments to setPosition —lowerTick and upperTick— ( 𝑡 𝑖𝑐 ≥ 𝑖 𝑡𝑜 (𝑖) := (6.25) when combined with the msg.sender, together specify a position. 0 𝑖𝑐 < 𝑖 The function takes one additional parameter, liquidityDelta, As with 𝑓𝑜 values, 𝑡𝑜 values are not meaningfully comparable to specify how much virtual liquidity the user wants to add or (if across different ticks. 𝑡𝑜 is only meaningful in computing the num- negative) remove. ber of seconds that liquidity was within some particular range First, the function computes the uncollected fees (𝑓𝑢 ) that the between some defined start time (which must be after 𝑡𝑜 was ini- position is entitled to, in each token.7 The amount collected in fees tialized for both ticks) and some end time. is credited to the user and netted against the amount that they 6.3.1 Crossing a Tick. As described in section 6.2.3, Uniswap v3 would send in or out for their virtual liquidity deposit. acts like it obeys the constant product formula when swapping To compute uncollected fees of a token, you need to know between initialized ticks. When a swap crosses an initialized tick, how much 𝑓𝑟 for the position’s range (calculated from the range’s however, the contract needs to add or remove liquidity, to ensure 𝑖𝑙 and 𝑖𝑟 as described in section 6.3) has grown since the last that no liquidity provider is insolvent. This means the Δ𝐿 is fetched time fees were collected for that position. The growth in fees in from the tick, and applied to the global 𝐿. a given range per unit of liquidity over between times 𝑡 0 and 𝑡 1 The contract also needs to update the tick’s own state, in order is simply 𝑓𝑟 (𝑡 1 ) − 𝑓𝑟 (𝑡 0 ) (where 𝑓𝑟 (𝑡 0 ) is stored in the position as to track the fees earned (and seconds spent) within ranges bounded feeGrowthInside{0,1}Last, and 𝑓𝑟 (𝑡 1 ) can be computed from by this tick. The feeGrowthOutside{0,1} and secondsOutside the current state of the ticks). Multiplying this by the position’s values are updated to both reflect current values, as well as the liquidity gives us the total uncollected fees in token 0 for this proper orientation relative to the current tick: position: 𝑓𝑜 := 𝑓𝑔 − 𝑓𝑜 (6.26) 𝑓𝑢 = 𝑙 · (𝑓𝑟 (𝑡 1 ) − 𝑓𝑟 (𝑡 0 )) (6.28) Then, the contract updates the position’s liquidity by adding 𝑡𝑜 := 𝑡 − 𝑡𝑜 (6.27) liquidityDelta. It also adds liquidityDelta to the liquidityNet Once a tick is crossed, the swap can continue as described in value for the tick at the bottom end of the range, and subtracts it section 6.2.3 until it reaches the next initialized tick. from the liquidityNet at the upper tick (to reflect that this new liquidity would be added when the price crosses the lower tick 6.4 Position-Indexed State going up, and subtracted when the price crosses the upper tick The contract has a mapping from user (an address), lower bound going up). If the pool’s current price is within the range of this (a tick index, int24), and upper bound (a tick index, int24) to a position, the contract also adds liquidityDelta to the contract’s specific Position struct. Each Position tracks three values: global liquidity value. Finally, the pool transfers tokens from (or, if liquidityDelta Type Variable Name Notation is negative, to) the user, corresponding to the amount of liquidity uint128 liquidity 𝑙 burned or minted. uint256 feeGrowthInside0LastX128 𝑓𝑟,0 (𝑡 0 ) The amount of token0 (Δ𝑋 ) or token1 (Δ𝑌 ) that needs to be uint256 feeGrowthInside1LastX128 𝑓𝑟,1 (𝑡 0 ) 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 Table 3: Position-Indexed State 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 liquidity (𝑙) means the amount of virtual liquidity that the range of the position: position represented the last time this position was touched. Specif- √ ically, liquidity could be thought of as 𝑥 · 𝑦, where 𝑥 and 𝑦 are 7 Since the formulas for computing uncollected fees in each token are the same, we the respective amounts of virtual token0 and virtual token1 that will omit that subscript for the rest of this section. 8

Uniswap v3 Core [4] Allan Niemerg, Dan Robinson, and Lev Livnev. 2020. YieldSpace: An Automated Liquidity Provider for Fixed Yield Tokens. Retrieved Feb 24, 2021 from https: 0 𝑖𝑐 < 𝑖𝑙 //yield.is/YieldSpace.pdf √ Δ𝑌 = Δ𝐿 · ( 𝑃 − 𝑝 (𝑖𝑙 )) p 𝑖𝑙 ≤ 𝑖𝑐 < 𝑖𝑢 (6.29) [5] Abraham Othman. 2012. Automated Market Making: Theory and Practice. Ph.D. Dissertation. Carnegie Mellon University. Δ𝐿 · ( 𝑝 (𝑖𝑢 ) − 𝑝 (𝑖𝑙 )) 𝑖𝑐 ≥ 𝑖𝑢 p p Δ𝐿 · ( √ 1 − √ 1 ) 𝑖𝑐 < 𝑖𝑙 DISCLAIMER 𝑝 (𝑖𝑙 ) 𝑝 (𝑖𝑢 ) This paper is for general information purposes only. It does not Δ𝑋 = Δ𝐿 · ( √1 − √ 1 ) 𝑖𝑙 ≤ 𝑖𝑐 < 𝑖𝑢 (6.30) constitute investment advice or a recommendation or solicitation to 𝑃 𝑝 (𝑖𝑢 ) 0 𝑖𝑐 ≥ 𝑖𝑢 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 REFERENCES relied upon for accounting, legal or tax advice or investment rec- [1] Hayden Adams, Noah Zinsmeister, and Dan Robinson. 2020. Uniswap v2 Core. ommendations. This paper reflects current opinions of the authors Retrieved Feb 24, 2021 from https://uniswap.org/whitepaper.pdf and is not made on behalf of Uniswap Labs, Paradigm, or their [2] Guillermo Angeris and Tarun Chitra. 2020. Improved Price Oracles: Constant Function Market Makers. In Proceedings of the 2nd ACM Conference on Advances affiliates and does not necessarily reflect the opinions of Uniswap in Financial Technologies (AFT ’20). Association for Computing Machinery, New Labs, Paradigm, their affiliates or individuals associated with them. York, NY, United States, 80–91. https://doi.org/10.1145/3419614.3423251 The opinions reflected herein are subject to change without being [3] Michael Egorov. 2019. StableSwap - Efficient Mechanism for Stablecoin Liquidity. Retrieved Feb 24, 2021 from https://www.curve.fi/stableswap-paper.pdf updated. 9