Uniswap v3 Core
March 2021
Hayden Adams

Noah Zinsmeister

Moody Salem

hayden@uniswap.org

noah@uniswap.org

moody@uniswap.org

River Keefer

Dan Robinson

river@uniswap.org

dan@paradigm.xyz

ABSTRACTIn 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 fragmentationand gas inefficiency. This design does not depend on any sharedassumption 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:

Uniswap v3 is a noncustodial automated market maker implemented for the Ethereum Virtual Machine. In comparison to earlier
versions of the protocol, Uniswap v3 provides increased capitalefficiency and fine-tuned control to liquidity providers, improves
the accuracy and convenience of the price oracle, and has a more
flexible fee structure.

1• 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 preferred reserves curve, while still being efficiently aggregatedwith 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 theneed to checkpoint the accumulator value at the exact beginning and end of the period for which a TWAP is being
measured. (Section 5.1).

INTRODUCTION

Automated market makers (AMMs) are agents that pool liquidity
and make it available to traders according to an algorithm [5]. Constant function market makers (CFMMs), a broad class of AMMs ofwhich Uniswap is a member, have seen widespread use in the context 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 inefficient. In the constant product market maker formula used by
Uniswap v1 and v2, only a fraction of the assets in the pool areavailable at a given price. This is inefficient, particularly whenassets 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. Thisrequires 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.
1Hayden Adams, Noah Zinsmeister, Moody Salem, River Keefer, and Dan Robinson• Liquidity Oracle: The contracts expose a time-weighted average liquidity oracle (Section 5.3).
The Uniswap v2 core contracts are non-upgradeable by design, so Uniswap v3 is implemented as an entirely new set ofcontracts, available here. The Uniswap v3 core contracts are also
non-upgradeable, with some parameters controlled by governanceas described in Section 4.

2

liquidity is composed entirely of a single asset, because the reservesof the other asset must have been entirely depleted. If the price everreenters 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 describedby the curve:√𝐿(𝑥 + √ )(𝑦 + 𝐿 𝑝𝑎 ) = 𝐿 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.2)

This curve is a translation of formula 2.1 such that the position issolvent exactly within its range (Fig. 2).

(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 versions were designed to provide liquidity across the entire pricerange (0, ∞). This is simple to implement and allows liquidity tobe efficiently aggregated, but means that much of the assets held ina pool are never touched.
Having considered this, it seems reasonable to allow LPs toconcentrate 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 withlarger reserves (we call these the virtual reserves) within that range.

virtual reserves (2.1)
real reserves (2.2)

Y Reserves𝑏𝑎virtual reserves
X Reserves
𝑥 realFigure 2: Real Reserves

Y Reserves𝑏Liquidity providers are free to create as many positions as theysee 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 ina narrow band around the current price, and adding or removing
tokens as the price moves to keep their liquidity active.

𝑐
𝑦real
𝑎
X Reserves

2.1

Figure 1: Simulation of Virtual Liquidity

Range Orders

Positions on very small ranges act similarly to limit orders—if therange is crossed, the position flips from being composed entirelyof one asset, to being composed entirely of the other asset (plusaccrued fees). There are two differences between this range orderand a traditional limit order:

Specifically, a position only needs to hold enough of asset X tocover price movement to its upper bound, because upwards pricemovement1 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 ona 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• 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 withdrawn. If it is not, and the price crosses back across that
range, the position will be traded back, effectively reversing
the trade.

1 We take asset Y to be the unit of account, which corresponds to token1 in our

implementation.
2

∞

0

Liquidity

Liquidity

Liquidity

Uniswap v3 Core

𝑝𝑎

𝑝𝑏

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 asliquidity 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 createa periphery contract that wraps an individual liquidity position(including collected fees) in an ERC-721 non-fungible token.

Uniswap v3 makes a number of architectural changes, some ofwhich 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 becreated at three fee tiers: 0.05%, 0.30%, and 1%. Additional fee tiers
can be enabled by UNI governance.

3.2

4

GOVERNANCE

The factory has an owner, which is initially controlled by UNI
tokenholders.2 The owner does not have the ability to halt theoperation of any of the core contracts.
As in Uniswap v2, Uniswap v3 has a protocol fee that can beturned on by UNI governance. In Uniswap v3, UNI governance has
more flexibility in choosing the fraction of swap fees that go to theprotocol, 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 isadded 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 of200, approximately 2.02% between ticks.
Finally, UNI governance has the power to transfer ownership toanother address.

Non-Fungible Liquidity

3.2.1 Non-Compounding Fees. Fees earned in earlier versions were
continuously deposited in the pool as liquidity. This meant thatliquidity 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, thisis 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 anything 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. Arguably, 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 the5

ORACLE UPGRADES

Uniswap v3 includes 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 they2 Specifically, 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 onchain 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 thegeometric mean TWAP.
Finally, Uniswap v3 adds a liquidity accumulator that is tracked
alongside the price accumulator, which accumulates 𝐿1 for eachsecond. 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 whichof the pools corresponding to a pair (see section 3.1) will have themost reliable TWAP.

5.1

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 theseratios. The geometric mean of a set of ratios is the reciprocal of thegeometric mean of their reciprocals. It is also easy to implementin 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 tracks log 𝑃 rather than𝑃, and log 𝑃 can represent a wide range of prices with consistent
precision.4 Finally, there is a theoretical argument that the timeweighted 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:

Oracle Observations

As in Uniswap v2, Uniswap v3 tracks a running accumulator of
the price at the beginning of each block, multiplied by the numberof seconds since the last block.
A pool in Uniswap v2 stores only the most recent value of thisprice accumulator—that is, the value as of the last block in which aswap occurred. When computing average prices in Uniswap v2, itis the responsibility of the external caller to provide the previous
value of the price accumulator. With many users, each will have toprovide their own methodology for checkpointing previous valuesof 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 describedin section 5.3). It does this by automatically checkpointing theaccumulator value every time the pool is touched for the first timein 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 thisarray 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

𝑎𝑡 =

𝑡
Õ

log1.0001 (𝑃𝑖 )

(5.1)

𝑖=1

We want to estimate the geometric mean time-weighted average
price (𝑝𝑡1 ,𝑡2 ) over any period 𝑡 1 to 𝑡 2 .
1
𝑡 2 −𝑡 1
𝑡2©Ö ª
𝑃𝑡1 ,𝑡2 = ­
𝑃𝑖 ®
(5.2)
«𝑖=𝑡1 ¬
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 thetime weighted geometric mean price.

Í𝑡2

log1.0001 𝑃𝑡1 ,𝑡2 =𝑖=𝑡 1 log1.0001 (𝑃𝑖 )𝑡2 − 𝑡1

 𝑎𝑡 − 𝑎𝑡 1
log1.0001 𝑃𝑡1 ,𝑡2 = 2
𝑡2 − 𝑡1
𝑎𝑡 2 −𝑎𝑡 1

𝑃𝑡1 ,𝑡2 = 1.0001 𝑡2 −𝑡1

5.3

(5.3)
(5.4)
(5.5)

Liquidity Oracle

In addition to the seconds-weighted accumulator of log1.0001 𝑝𝑟𝑖𝑐𝑒,
Uniswap v3 also tracks a seconds-weighted accumulator of 𝐿1 (thereciprocal of the virtual liquidity currently in range) at the beginning 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 theGeometric Mean Price Oracle

Uniswap v2 maintains two price accumulators—one for the price of
token0 in terms of token1, and one for the price of token1 in termsof token0. Users can compute the time-weighted arithmetic meanof 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 aretracked separately, since the time-weighted arithmetic mean price4 In 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.
5 While arithmetic mean TWAPs are much more widely used, they should theoreticallybe less accurate in measuring a geometric Brownian motion process (which is how pricemovements 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.

3 The 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.
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 maketheir oracles stronger (such as by evaluating which fee-tier pool to
use the oracle from).

6

Whenever the price crosses an initialized tick, virtual liquidity
is kicked in or out. The gas cost of an initialized tick crossing isconstant, 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 outof 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.

IMPLEMENTING CONCENTRATEDLIQUIDITY

6.2

The rest of this paper describes how concentrated liquidity provision works, and gives a high-level description of how it is implemented in the contracts.

6.1

Type
uint128
uint160
int24
uint256
uint256
uint128
uint128

Ticks and Ranges

To implement custom liquidity provision, the space of possible
prices is demarcated by discrete ticks. Liquidity providers can provide liquidity in a range between any two ticks (which need not be
adjacent).
Each range can be specified as a pair of signed integer tick indices:a lower tick (𝑖𝑙 ) and an upper tick (𝑖𝑢 ). Ticks represent prices atwhich the virtual liquidity of the contract can change. We willassume that prices are always expressed as the price of one of thetokens—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 throughpossible 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 ateach is given by:
𝑝 (𝑖) = 1.0001𝑖

Global StateThe 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.)
Variable Nameliquidity
sqrtPriceX96
tick
feeGrowthGlobal0X128
feeGrowthGlobal1X128
protocolFees.token0
protocolFees.token1
Table 1: Global State

Notation
𝐿
√
𝑃
𝑖𝑐
𝑓𝑔,0
𝑓𝑔,1
𝑓𝑝,0
𝑓𝑝,1

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 poolcontract
tracks two different values: liquidity (𝐿) and sqrtPrice√( 𝑃). These could be computed from the virtual reserves with thefollowing formulas:(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:√
𝐿 = 𝑥𝑦
√
𝑃=

(6.3)

r

𝑦
(6.4)
𝑥
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)√√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
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 aparameter, tickSpacing (𝑡𝑠 ); only ticks with indexes that are divisible by tickSpacing can be initialized. For example, if tickSpacingis 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).

𝐿
𝑥= √
𝑃
√
𝑦=𝐿· 𝑃

(6.5)

(6.6)
√
Using 𝐿 and 𝑃 is convenient
because
only
one
of
them
changes√
at a time. Price (and thus 𝑃) changes when swapping within atick; 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 ofliquidity 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 thoughtof as virtual liquidity tokens.
Alternatively, liquidity can be thought of as the amount thattoken1√ reserves (either actual or virtual) changes for a given change
in 𝑃:

Δ𝑦 = 𝑦𝑖𝑛 · (1 − 𝛾)

If you used the computed virtual reserves (𝑥 and 𝑦) for the token0
and token1 balances, then this formula could be used to find theamount of token0 sent out:

Δ𝑌
(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 (𝑖𝑐 ), asigned integer representing the current tick (more specifically, thenearest tick below the current price). This is an optimization (anda way of avoiding precision issues with logarithms), since at any
time, you should be able to compute the current tick based on thecurrent sqrtPrice. Specifically, at any given time, the following
equation should be true:j√ k
𝑖𝑐 = log√1.0001 𝑃
(6.8)

𝑥 ·𝑦
(6.12)
𝑦 + Δ𝑦But remember that in v3,√ 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 theexecution price of the trade. But it turns out that √there are simple
formulas that describe the relationship between Δ 𝑃 and Δ𝑦, for agiven 𝐿 (which can be derived from formula 6.7):

𝐿=𝑥𝑒𝑛𝑑 =

√
Δ𝑦
Δ 𝑃=
𝐿
√
Δ𝑦 = Δ 𝑃 · 𝐿(6.14)

𝑃

Δ𝑥
1
Δ√ =
𝐿
𝑃

(6.15)

1(6.16)
Δ𝑥 = Δ √ · 𝐿
𝑃When swapping one √token for the other, the pool contract canfirst compute the new 𝑃 using formula 6.13 or 6.15, and thencan 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 continuingwith the rest of the swap.
6.2.4 Initialized Tick Bitmap. If a tick is not used as the endpointof a range with any liquidity in it—that is, if the tick is uninitialized—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 thattick 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.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 = 𝑦𝑖𝑛 · 𝛾 · 𝜙
Δ𝑦 is the increase in 𝑦 (after the fee is taken out).

(6.13)

There are also simple formulas that describe the relationshipbetween Δ √1 and Δ𝑥:

6.2.2 Fees. Each pool is initialized with an immutable value, fee(𝛾), representing the fee paid by swappers in units of hundredthsof a basis point (0.0001%).
It also tracks the current protocol fee, 𝜙 (which is initialized tozero, but can changed by UNI governance).6 This number gives you
the fraction of the fees paid by swappers that currently goes to theprotocol rather than to liquidity providers. 𝜙 only has a limited setof 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 ) and feeGrowthGlobal1 (𝑓𝑔,1 ). These represent the total amountof fees that have been earned per unit of virtual liquidity (𝐿), overthe 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 unboundedliquidity that was deposited when the contract was first initialized.
They are stored as fixed-point unsigned 128x128 numbers. Notethat in Uniswap v3, fees are collected in the tokens themselvesrather 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 ) and protocolFees1(𝑓𝑝,1 ). This is an unsigned uint128. The accumulated protocol fees
can be collected by UNI governance, by calling the collectProtocol
function.

Δ𝑓𝑔,1 = 𝑦𝑖𝑛 · 𝛾 · (1 − 𝜙)(6.11)(6.9)

6.3(6.10)

Tick-Indexed StateThe contract needs to store information about each tick in order totrack the amount of net liquidity that should be added or removed
when the tick is crossed, as well as to track the fees earned aboveand below that tick.

6 Technically, the storage variable called “protocolFee" is the denominator of thisfraction (or is zero, if 𝜙 is zero).
6

Uniswap v3 Core

Start

Fail
S0.

Stop

Check input
Pass

S1.

Swap within current interval

No
S2.

Is there remaining input or output?

S5.

Execute computed swap

Yes
S4.

Cross next tickFigure 4: Swap Control Flow

The contract stores a mapping from tick indexes (int24) to thefollowing seven values:Type
int128uint128
uint256
uint256
uint256
uint256
uint256

Variable NameliquidityNet
liquidityGross
feeGrowthOutside0X128
feeGrowthOutside1X128
secondsOutside
tickCumulativeOutside
secondsPerLiquidityOutsideX128
Table 2: Tick-Indexed State(𝑓𝑎 (𝑖) =Notation
Δ𝐿
𝐿𝑔
𝑓𝑜,0
𝑓𝑜,1
𝑠𝑜
𝑖𝑜
𝑠𝑙𝑜

(
𝑓𝑏 (𝑖) =𝑓𝑔 − 𝑓𝑜 (𝑖)𝑓𝑜 (𝑖)

𝑖𝑐 ≥ 𝑖𝑖𝑐 < 𝑖(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—alower 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:

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. Thisvalue does not need to be updated when the tick is crossed (butonly 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’snecessary to track a gross tally of liquidity referencing the tick,liquidityGross. This value ensures that even if net liquidity ata 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 thetick bitmap.
feeGrowthOutside{0,1} are used to track how many fees wereaccumulated within a given range. Since the formulas are the samefor the fees collected in token0 and token1, we will omit that subscript 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 onwhether the price is currently within or outside that range—that is,whether the current tick index 𝑖𝑐 is greater than or equal to 𝑖:

𝑓𝑜 (𝑖) := 𝑓𝑔 − 𝑓𝑜 (𝑖)(6.20)

𝑓𝑜 is only needed for ticks that are used as either the lower orupper 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—ischosen 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, and tickCumulativeOutside foreach tick. These values are not used within the contract, but are
tracked for the benefit of external contracts that need more finegrained information about the pool’s behavior (for purposes likeliquidity mining).
All three of these indexes work similarly to the fee growth indexes 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, andtickCumulativeOutside tracks the log1.0001 𝑃 accumulator described in section 5.2.
For example, the seconds spent above (𝑠𝑎 ) and below (𝑠𝑏 ) a giventick is computed differently based on whether the current price iswithin that range, and the seconds spent within a range (𝑠𝑟 ) can becomputed using the values of 𝑠𝑎 and 𝑠𝑏 :(𝑡 − 𝑡𝑜 (𝑖)𝑡𝑎 (𝑖) =𝑡𝑜 (𝑖)(
𝑡𝑜 (𝑖)𝑡𝑏 (𝑖) =𝑡 − 𝑡𝑜 (𝑖)𝑖𝑐 ≥ 𝑖𝑖𝑐 < 𝑖(6.22)

𝑖𝑐 ≥ 𝑖𝑖𝑐 < 𝑖(6.23)

𝑡𝑟 (𝑖𝑙 , 𝑖𝑢 ) = 𝑡 − 𝑡𝑏 (𝑖𝑙 ) − 𝑡𝑎 (𝑖𝑢 )𝑡𝑜 := 𝑡 − 𝑡𝑜6.4(
𝑡 𝑖𝑐 ≥ 𝑖
0 𝑖𝑐 < 𝑖Typeuint128
uint256
uint256

Variable Name
Notation
liquidity𝑙feeGrowthInside0LastX128 𝑓𝑟,0 (𝑡 0 )feeGrowthInside1LastX128 𝑓𝑟,1 (𝑡 0 )Table 3: Position-Indexed Stateliquidity (𝑙) means the amount of virtual liquidity that theposition 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 eachshare 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 willcall uncollected fees. Computing these uncollected fees requires
additional stored values on the position, feeGrowthInside0Last(𝑓𝑟,0 (𝑡 0 )) and feeGrowthInside1Last (𝑓𝑟,1 (𝑡 0 )), as described below.

(6.24)

6.4.1 setPosition. The setPosition function allows a liquidity
provider to update their position.
Two of the arguments to setPosition —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 (ifnegative) remove.
First, the function computes the uncollected fees (𝑓𝑢 ) that theposition is entitled to, in each token.7 The amount collected in feesis credited to the user and netted against the amount that theywould 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 lasttime fees were collected for that position. The growth in fees ina 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 fromthe current state of the ticks). Multiplying this by the position’s
liquidity gives us the total uncollected fees in token 0 for thisposition:(6.25)

As with 𝑓𝑜 values, 𝑡𝑜 values are not meaningfully comparable
across different ticks. 𝑡𝑜 is only meaningful in computing the number of seconds that liquidity was within some particular range
between some defined start time (which must be after 𝑡𝑜 was initialized 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 ensurethat no liquidity provider is insolvent. This means the Δ𝐿 is fetchedfrom 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 boundedby this tick. The feeGrowthOutside{0,1} and secondsOutside
values are updated to both reflect current values, as well as theproper orientation relative to the current tick:𝑓𝑜 := 𝑓𝑔 − 𝑓𝑜Position-Indexed StateThe 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:

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 noton the edge of any position. Therefore, it is not initialized until aposition is created that is bounded by that tick. By convention, it isinitialized as if every second since the Unix timestamp 0 had beenspent below that tick:𝑡𝑜 (𝑖) :=(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.

7 Since the formulas for computing uncollected fees in each token are the same, wewill omit that subscript for the rest of this section.

(6.26)
8

Uniswap v3 CoreREFERENCES
𝑓𝑢 = 𝑙 · (𝑓𝑟 (𝑡 1 ) − 𝑓𝑟 (𝑡 0 ))(6.28)
Then, the contract updates the position’s liquidity by adding
liquidityDelta. It also adds liquidityDelta to the liquidityNet
value for the tick at the bottom end of the range, and subtracts itfrom the liquidityNet at the upper tick (to reflect that this new
liquidity would be added when the price crosses the lower tickgoing up, and subtracted when the price crosses the upper tickgoing up). If the pool’s current price is within the range of thisposition, 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 bedeposited can be thought of as the amount that would be sold fromthe position if the price were to move from the current price (𝑃) tothe upper tick or lower tick (for token0 or token1, respectively).
These formulas can be derived from formulas 6.14 and 6.16, anddepend on whether the current price is below, within, or above therange of the position:
0
𝑖𝑐 < 𝑖𝑙p√
Δ𝑌 = Δ𝐿 · ( 𝑃 − 𝑝 (𝑖𝑙 ))𝑖𝑙 ≤ 𝑖𝑐 < 𝑖𝑢

p
p


 Δ𝐿 · ( 𝑝 (𝑖𝑢 ) − 𝑝 (𝑖𝑙 )) 𝑖𝑐 ≥ 𝑖𝑢
Δ𝐿 · ( √ 1 − √ 1 ) 𝑖𝑐 < 𝑖𝑙
𝑝 (𝑖𝑙 )
𝑝 (𝑖𝑢 )

Δ𝑋 = Δ𝐿 · ( √1 − √ 1 )𝑖𝑙 ≤ 𝑖𝑐 < 𝑖𝑢𝑃
𝑝 (𝑖𝑢 )
0𝑖𝑐 ≥ 𝑖𝑢[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 Advancesin 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 AutomatedLiquidity 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 notconstitute investment advice or a recommendation or solicitation tobuy or sell any investment and should not be used in the evaluationof the merits of making any investment decision. It should not be
relied upon for accounting, legal or tax advice or investment recommendations. This paper reflects current opinions of the authors
and is not made on behalf of Uniswap Labs, Paradigm, or theiraffiliates 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.

(6.29)(6.30)

9