UNISWAP V3 (Periphery) NonfungiblePositionManager CODE REVIEW DEEP DIVE
This is not an audit of v3. Interested in the Audit report?
PS: This will probably be outdated by the time you read this.
Uniswap V3 periphery is made up of the following functions.
NonFungiblePositionManager
NonFungiblePositionDescriptor
SwapRouter
v3Migrator
We will focus on the functionalities of The NonFungiblePositionManager.
Why USE NPM (not npm of NodeJS) - NonFungible Position Manager?
Imagine you have a collection of toys, and each toy is unique and special in its own way. You have a teddy bear, a robot, a car, and many other toys. Now, let's say you want to trade or exchange these toys with your friends. The problem is, these toys are not all the same, so you can't just trade them like you would with identical toys.
In the world of cryptocurrencies and tokens, something similar happens. People have unique digital assets or tokens, just like your special toys. These digital assets can represent things like digital art, virtual land, or even shares in a project. Each of these assets can be different and special in its own way, just like your toys.
Uniswap is like a big playground where people come to trade their digital assets. It's like a toy exchange, but for digital items. In the past, Uniswap worked in a way that made it a bit tricky to trade these unique assets. It was like trying to swap your toys for others, but the playground rules made it hard to do so if your toys were different sizes or shapes.
To solve this problem, the creators of Uniswap built something called the NonfungiblePositionManager, which is like a new set of rules for the playground. This special manager helps people trade their unique digital assets more easily. It allows people to create positions for their unique assets, just like setting up a specific trading booth for each type of toy. Now, when someone wants to trade, they can go to the right booth and exchange their unique assets for others without any trouble.
So, to put it simply, the NonfungiblePositionManager in Uniswap V3 was created to make it easier for people to trade their special and unique digital assets, just like you'd have specific trading booths for different types of toys in your playground. This way, everyone can have a fair and fun time trading their digital toys on the Uniswap playground.
ANALYSIS OF “NonfungiblePositionManager” CONTRACT AND FUNCTIONS
pragma solidity =0.7.6;
This statement specifies the use of Solidity compiler version 0.7.6 for compiling the smart contract.
pragma abicoder v2;
This statement indicates that the function's arguments and return values should be encoded and decoded using the"ABI coder v2" rules, which provide more flexibility and safety in handling data during contract interactions.
Interfaces:
INonFungiblePositionManager:
This interface defines a set of functions for managing and interacting with non-fungible tokens representing positions in Uniswap V3 pools.
It extends multiple other interfaces like IPoolInitializer, IPeripheryPayments, IPeripheryImmutableState, IERC721Metadata, IERC721Enumerable, and IERC721Permit. (We shall discuss these later).
This implies that the following functions along with other functions must be implemented.
Skip to NonFungiblePositionManager functionalities to get more details about the functions?
function mint(MintParams calldata params) external payable returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
: This method allows you to create a new position wrapped in a non-fungible token (NFT) in a Uniswap V3 pool.
function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable returns (uint128 liquidity, uint256 amount0, amount1)
: This method Increases the amount of liquidity in a position, with tokens paid by the sender.
function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable returns (uint256 amount0, uint256 amount1)
: This method increases the amount of liquidity in a position and accounts for it.
function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1)
: This method collects up to a maximum amount of fees owed to a specific position to a recipient.
function burn(uint256 tokenId) external payable
: This method burns a token ID, deleting it from the NFT contract when it has no liquidity and all tokens have been collected.
INonFungiblePositionDescriptor
This interface describes a set of functions for generating URIs that provide descriptions for non-fungible token (NFT) positions managed by a position manager.
The following function(s) must be implemented.
function tokenURI(INonfungiblePositionManager positionManager, uint256 tokenId) external view returns (string memory)
: This function takes a positionManager and a tokenId as parameters and returns a URI that describes the metadata of a specific NFT token. The URI could be a data URI with JSON contents directly inlined, and it complies with ERC721 standards for metadata.
IUniswapV3Pool
This comes from the core which is detailed in the Core section of this code review. In general, this interface specifies that the following functions must be implemented (The names are descriptive).
IUniswapV3PoolActions: The functions defined in this interface allow anyone to perform actions like initialize liquidity, add liquidity (mint), collect liquidity, burn liquidity, and swap tokens.
IUniswapV3PoolDerivedState: The functions defined in this interface keep track of variables that are not stored in the blockchain but rather computed.
IUniswapV3PoolState: These functions keep track of states that are stored and modified per transaction. This makes a lot of sense because gas can be saved and storage space conserved easily by grouping states together based on certain decided qualities.
IUniswapV3PoolImmutables: Functions that return values that should and would never change.
IUniswapV3Factory
This also comes from V3 Core. It provides a way to interact with the Uniswap V3 Factory, allowing for the creation of Uniswap V3 pools and control over the protocol fees. It facilitates the management of pools and fees within the Uniswap V3 ecosystem. The following functions must be implemented:
function owner() external view returns (address);
: Retrieve the current owner of the factory.
feeAmountTickSpacing(uint24 fee)`: Get the tick spacing for a given fee amount if it's enabled.
function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool);
: Get the pool address for a given pair of tokens and a fee.
function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool);`: Create a new pool for a pair of tokens with a specified fee.
function setOwner(address _owner) external;
: Update the owner of the factory.
function enableFeeAmount(uint24 fee, int24 tickSpacing) external;
: Enable a fee amount with a specified tick spacing.
IUniswapV3MintCallback
This also comes from Core. It is designed to be implemented by any contract that calls the IUniswapV3PoolActions#mint function. It defines a callback function that must be implemented to handle post-minting actions.
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata data) external;
: This function is called to msg.sender after minting liquidity to a position from IUniswapV3Pool#mint. It is responsible for handling the payment of pool tokens owed for the minted liquidity. The implementation must ensure that the caller is a UniswapV3Pool deployed by the canonical UniswapV3Factory.
Libraries:
PositionKey
This contains a function which returns a hash of the owner's address and time interval boundaries. This in turn serves as a unique key.
PoolAddress
This library provides functions for deriving a Uniswap V3 pool address based on the factory, the two tokens involved in the pool, and the fee level. This ensures that the tokens and fee level are correctly sorted and hashed, which is important for interacting with these specific pools on the Ethereum blockchain. Here's what each part of the library does:
POOL_INIT_CODE_HASH:
This is a constant value representing a specific hash, used internally for pool address computation.
PoolKey:
This is a struct data structure representing the key identifying a Uniswap V3 pool. It includes the addresses of the two tokens involved in the pool and the fee level for that pool.
getPoolKey:
This function takes two token addresses (tokenA and tokenB) and the fee level (fee) as parameters. It returns a PoolKey that ensures the tokens are sorted in a specific order (the one with the lower address comes first).
computeAddress:
This function computes the address of a Uniswap V3 pool contract based on the Uniswap V3 factory address and a PoolKey. It ensures that the tokens are sorted in the correct order and uses a deterministic hashing process to derive the pool contract address. This address calculation is essential for interacting with specific Uniswap V3 pools created by the factory.
FixedPoint128
This is from V3 Core. The library is used to handle decimal calculations with integer operations by defining the fixed-point representation that can be used throughout the contract for precision in financial or mathematical calculations. The constant Q128 is defined as the fixed-point representation of 1 in 128bits which is 0x100000000000000000000000000000000.
Why is this Variable allocated 256bits instead of 128??
This is to ensure that the fixed-point representation can handle a wide range of values and maintain precision during calculations without risking overflow or loss of precision. While the name "FixedPoint128" might suggest 128 bits, it's referring to the 128-bit fractional part of the fixed-point representation, which is stored in a uint256 for practical reasons.
FullMath
This is also from V3 Core. It provides mathematical functions for performing multiplication and division with a focus on preventing overflow and ensuring precision.
The functions accessible include:
mulDiv:
This function calculates floor(a×b÷denominator) with full precision. It prevents result overflow and ensures that the denominator is not zero. It uses assembly to perform 512-bit multiplication and then handles the division, including dividing by powers of two and finding the modular inverse.
mulDivRoundingUp:
This function calculates ceil(a×b÷denominator) with full precision. It achieves this by calling the mulDiv function and then, if there's a remainder (resulting from a modulus operation), it increments the result. This ensures that the result is rounded up.
Shall we go a bit deeper?
Certainly General!!!
library FullMath {
uint256 internal constant Q128 = 0x100000000000000000000000000000000;
}
This part simply sets up the FullMath
library and defines a constant Q128
as a 128-bit fixed-point representation equivalent to 1.
mulDiv Function:
function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
The mulDiv
function is declared, which takes three uint256
parameters: a
, b
, and denominator
. It returns a uint256
result.
uint256 prod0;
uint256 prod1;
assembly {
let mm := mulmod(a, b, not(0))
prod0 := mul(a, b)
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
}
Inside the assembly block, the product of a
and b
is calculated considering that this product can exceed 256 bits, which could lead to an overflow issue. The not(0) is the bitwise not operation applied to the number zero. This effectively returns the max possible value of the integer. Consider the example below.
Let's calculate the result of 20 * 60770 % not(0)
with real values:
20
and60770
are the operands for multiplication.%
represents the modulo operation, which calculates the remainder when the left operand is divided by the right operand.not(0)
represents the bitwise NOT operation applied to0
, effectively making it the maximum possible value, which is dependent on the data type being used.
Assuming we are working with a 32-bit data type, the maximum possible value would be 2^32 - 1
, which is 4294967295.
Now, let's calculate the result:
20 * 60770 % 4294967295
Calculating 20 * 60770
results in 1215400
. Now, we need to find the remainder when 1215400
is divided by 4294967295
:
1215400 % 4294967295 = 1215400
In this case, the result of 20 * 60770 % not(0)
is 1215400
, which is less than 4294967295
. If you were to perform this operation using a 32-bit unsigned integer in a programming language, you would obtain the same result. If for any reason we get a value larger than the maximum possible value of the integer data type, we will get the modulo of that and thus always end up with a value lower than the maximum. Thus effectively avoiding an overflow.
Deeper still?
Yes? Or skip?
Yes, Chief!!
mm variable is calculated as mulmod(a, b, not(0)). This calculates the product of a and b while ensuring that it's limited to 256 bits, preventing an overflow.
prod0 is calculated as a straightforward multiplication of a and b.
The line lt(mm, prod0) checks if mm is less than prod0, effectively checking for overflow by comparing the two products.
The sub function subtracts lt(mm, prod0) from mm, and then subtracts the result from prod0. This effectively calculates the most significant 256 bits of the product and handles overflow by subtracting any overflowed bits.
So, prod1 ends up containing the most significant 256 bits of the product of a and b, and it's calculated in a way that ensures there's no overflow or loss of precision. This is a crucial step in handling 512-bit multiplication within the 256-bit constraints of Ethereum's Solidity language.
Should we dive deeper with Illustrations this time?
Recall that we are evaluating this line => prod1 := sub(sub(mm, prod0), lt(mm, prod0))
Test Case 1: Failing (Overflow)
Suppose we have the following input data:
a = 2^255
(max 255-bit number)b = 2^255
(max 255-bit number)mm = a * b
, which results in a 510-bit number (above 256 bits)
In this case, the product mm
is above 256 bits and an overflow occurs. The line in question is used to handle this situation.
mm
overflows, solt(mm, prod0)
is true becausemm
is less thanprod0
.sub(mm, prod0)
would result in a negative number (underflow).To prevent this underflow, we use
lt(mm, prod0)
to create a value of1
ortrue
, which is then subtracted frommm
. So,prod1
will capture the most significant 256 bits of the product minus any overflowed bits.
Test Case 2: Successful
Now, let's consider a case where there's no overflow:
a = 100
(random small number)b = 200
(random small number)mm = a * b
, which results in a 32-bit number (below 256 bits)
In this case, the product mm
doesn't overflow, and lt(mm, prod0)
is false because mm
is greater than or equal to prod0
.
sub(mm, prod0)
is calculated as zero because there's no overflow.lt(mm, prod0)
is also zero because it's false.
So, in this case, prod1
remains zero, and prod0
holds the full 32-bit product.
Test Case 3: With real data Assuming:
mm = 4000
(result ofmulmod(2000, 3000, 5000)
),prod0 = 6000000
(result ofmul(2000, 3000)
),max_value = 5000
(the assumed maximum value for an unsigned integer),
We'll calculate the values step by step:
sub(mm, prod0)
: Calculates the difference betweenmm
andprod0
. This step checks if there's an overflow by examining if the result is greater than the maximum value (max_value
). In this case, there's no overflow because4000 - 6000000
is less than the maximum value:sub(mm, prod0) = 4000 - 6000000 = -5996000
lt(mm, prod0)
: Comparesmm
withprod0
to check ifmm
is less thanprod0
. If there's an overflow, this comparison will be1
(true), indicating thatmm
is indeed less thanprod0
. In this case,mm
is less thanprod0
because4000
is less than6000000
.lt(mm, prod0) = 1
Finally, the result is subtracted by
1
ifmm
is less thanprod0
. This adjustment is made to account for the overflow, ensuring thatprod1
contains the most significant bits within the limit ofmax_value
:Since
lt(mm, prod0)
is1
, we subtract1
from the previous result:prod1 = -5996000 - 1 = -5996001
So, in this specific scenario, sub(mm, prod0)
results in a negative value, lt(mm, prod0)
is 1
, and prod1
is adjusted to -5996001
to account for the overflow and stay within the limit of 5000
as the maximum value for an unsigned integer.
if (prod1 == 0) {
require(denominator > 0);
assembly {
result := div(prod0, denominator)
}
return result;
}
If prod1
(the most significant 256 bits of the product) is zero, which means that there was no overflow during multiplication. In this case, it checks that the denominator
is not zero and performs a standard division to obtain the result.
require(denominator > prod1);
- The above code ensures that the
denominator
is greater thanprod1
.
uint256 remainder;
assembly {
remainder := mulmod(a, b, denominator)
}
- The above code calculates the remainder by performing a modulo operation on
a * b
withdenominator
.
assembly {
prod1 := sub(prod1, gt(remainder, prod0))
prod0 := sub(prod0, remainder)
}
- The above code subtracts the remainder from
prod0
and checks whether the remainder is greater thanprod0
. This is part of handling the remainder of the division process.
uint256 twos = -denominator & denominator;
assembly {
denominator := div(denominator, twos)
}
- The above code calculates the largest power of two divisors of
denominator
and dividesdenominator
by this power of two.
assembly {
prod0 := div(prod0, twos)
}
- The above code divides
prod0
by the same power of two.
assembly {
twos := add(div(sub(0, twos), twos), 1)
}
- The above code computes the inverse of
twos
modulo 2^256.
prod0 |= prod1 * twos;
- The above code combines
prod0
andprod1
by shiftingprod1
intoprod0
usingtwos
as a bitmask.
uint256 inv = (3 * denominator) ^ 2;
- The above code computes an initial estimate of the modular inverse, starting with a seed that is correct for four bits.
inv *= 2 - denominator * inv; // inverse mod 2^8
inv *= 2 - denominator * inv; // inverse mod 2^16
inv *= 2 - denominator * inv; // inverse mod 2^32
inv *= 2 - denominator * inv; // inverse mod 2^64
inv *= 2 - denominator * inv; // inverse mod 2^128
inv *= 2 - denominator * inv; // inverse mod 2^256
- The above code uses Newton-Raphson iteration to improve the precision of the inverse, doubling the correct bits in each step.
result = prod0 * inv;
return result;
- The above code multiplies
prod0
by the final inverse, giving the correct result modulo 2^256.
mulDivRoundingUp Function:
function mulDivRoundingUp(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
result = mulDiv(a, b, denominator);
if (mulmod(a, b, denominator) > 0) {
require(result < type(uint256).max);
result++;
}
}
- The
mulDivRoundingUp
function calculates the ceiling (rounded up) result of thea×b÷denominator
. It calls themulDiv
function to get the floor result and then checks if there is a remainder by performing a modulo operation. If there's a remainder, it increments the result to ensure it is rounded up. It also checks for overflow by verifying thatresult
is less than the maximum value ofuint256
.
TickMath
This is also from V3 Core. It provides essential mathematical functions to handle Uniswap V3's tick-based price management, making it possible to convert between price ticks and square root prices accurately. It includes two main functions:
getSqrtRatioAtTick(int24 tick)
: This function calculates the square root price for a given tick. The tick represents the position of a price on the price curve. The tick value is converted into a square root price as a fixed-point number (Q64.96) that represents the square root of the ratio of two assets (token1/token0). It supports prices between 2^-128 and 2^128.
getTickAtSqrtRatio(uint160 sqrtPriceX96)
: This function does the reverse operation. It calculates the greatest tick value such that the square root of the ratio of two assets is less than or equal to the input square root price. This is useful for determining the tick value for a given price.
Here we go again. Let’s dig deeper!
function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24 tick) {
This is a special function called getTickAtSqrtRatio
. It takes one input (sqrtPriceX96
) and returns a single output (tick
). You can think of this function as a magical box that takes a number and gives you another number.
require(sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO, 'R');
Here, we are making sure that the sqrtPriceX96
is within a certain range. If it's not, the function will stop and show an error message. Think of it as a bouncer at a club, checking if you're allowed in.
uint256 ratio = uint256(sqrtPriceX96) << 32;
We're taking the sqrtPriceX96
, turning it into a bigger number (uint256
), and then moving it to the left by 32 positions (shifting). It's like taking a small toy car and making it bigger and faster.
uint256 r = ratio;
uint256 msb = 0;
We're creating two containers, r
and msb
, to hold some values. Think of r
as a bucket to hold water, and msb
as an empty cup.
assembly {
let f := shl(7, gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF))
msb := or(msb, f)
r := shr(f, r)
}
This part is like a magic spell (assembly) that does some tricky stuff. If the number in r
is big, it sets a special flag (f
). Then, it combines this flag with msb
and changes r
a bit. Imagine you have a cookie jar, and if it's too full, you put a note on it saying it's full. Then, you put that note in your cup, and you eat some cookies to make the jar not so full.
if (msb >= 128) r = ratio >> (msb - 127);
else r = ratio << (127 - msb);
This part checks if the cup (msb
) has a lot of notes (more than or equal to 128). If it does, it changes the bucket (ratio
) in a certain way. If there aren't many notes, it changes the bucket differently. It's like deciding how to pour water into a smaller container based on how full your cup is.
int256 log_2 = (int256(msb) - 128) << 64;
We're creating another number (log_2
) by taking the cup (msb
) and making it a little smaller. Then, we shift it to the left by 64.
int24 tickLow = int24((log_2 - 3402992956809132418596140100660247210) >> 128);
int24 tickHi = int24((log_2 + 291339464771989622907027621153398088495) >> 128);
Here, we're making two more numbers (tickLow
and tickHi
) by doing some math with log_2
.
tick = tickLow == tickHi ? tickLow : getSqrtRatioAtTick(tickHi) <= sqrtPriceX96 ? tickHi : tickLow;
Finally, we decide on a special number (tick
). If tickLow
is the same as tickHi
, we choose tickLow
. If not, we check some more stuff (using the getSqrtRatioAtTick
function) and decide which number is the best. It's like picking your favorite toy to play with.
CallbackValidation
This library helps ensure that the call is coming from the correct Uniswap V3 Pool and that it's not a fraudulent or unauthorized call.
verifyCallback
function takes four inputs: the Uniswap V3 factory's contract address (factory
), the contract address of one of the tokens involved in the pool (tokenA
), the contract address of the other token (tokenB
), and the fee collected upon every swap in the pool (fee
). It returns the contract address of the valid Uniswap V3 Pool (pool
).The
verifyCallback
function usesPoolAddress.getPoolKey
to calculate a unique identifier for the specific Uniswap V3 Pool, based on the token addresses and the fee.After obtaining the
poolKey
, the function computes the contract address of the Uniswap V3 Pool by usingPoolAddress.computeAddress
, which combines the factory's address and thepoolKey
.Finally, it checks if the caller of the function (
msg.sender
) is the same as the computed Uniswap V3 Pool contract address (pool
). If they are not the same, it will throw an error, ensuring that only valid Uniswap V3 Pools can call this function.
LiquidityAmounts
This library provides functions for performing various calculations related to liquidity amounts in Uniswap V3 Pools. These calculations help determine the amount of liquidity to be added or removed based on token amounts and price information within the pool.
toUint128(uint256 x)
: This private function takes a uint256
input, downcasts it to a uint128
, and returns the downcasted value. It ensures that the downcasted value does not exceed uint128
limits.
getLiquidityForAmount0(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint256 amount0)
: This function calculates the amount of liquidity that will be received for a given amount of token0
and a price range within the pool. It uses the formula amount0 * (sqrt(upper) * sqrt(lower)) / (sqrt(upper) - sqrt(lower))
to compute the liquidity. This is used when adding token0
liquidity to the pool.
getLiquidityForAmount1(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint256 amount1)
: Similar to the previous function, this one calculates the liquidity received for a given amount of token1
within a specified price range. It uses the formula amount1 / (sqrt(upper) - sqrt(lower))
. This is used when adding token1
liquidity to the pool.
getLiquidityForAmounts(uint160 sqrtRatioX96, uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint256 amount0, uint256 amount1)
: This function calculates the maximum liquidity received when adding both token0
and token1
to the pool. It considers different cases based on the current price within the pool and the specified price range.
getAmount0ForLiquidity(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity)
: This function computes the amount of token0
tokens that correspond to a given amount of liquidity within a specified price range. It's used to determine how many token0
tokens you can withdraw from the pool based on the liquidity you hold.
getAmount1ForLiquidity(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity)
: Similar to the previous function, this one calculates the amount of token1
tokens corresponding to a given amount of liquidity. It helps determine how many token1
tokens you can withdraw from the pool based on the liquidity you have.
getAmountsForLiquidity(uint160 sqrtRatioX96, uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity)
: This function determines both the token0
and token1
values associated with a given amount of liquidity. It considers various cases based on the current price within the pool and the specified price range, providing the corresponding token values.
Base Contracts:
LiquidityManagement
This is an abstract contract declaration with some interfaces that this contract implements. It includes IUniswapV3MintCallback, PeripheryImmutableState, and PeripheryPayments. LiquidityManagement contract provides a foundation for managing liquidity within Uniswap V3 pools. It defines the addLiquidity function to facilitate the addition of liquidity to these pools and includes a callback function for handling events when liquidity is minted. It relies on other libraries and interfaces to provide the necessary functionality. Other contracts can inherit from this one and implement specific use cases involving liquidity management in Uniswap V3.
Structs in LiquidityManagement:
MintCallbackData: A struct used to hold data related to minting, including poolKey and payer. It's used when calling the uniswapV3MintCallback function.
AddLiquidityParams: A struct that defines parameters required to add liquidity to a Uniswap V3 pool. These parameters include token addresses (token0 and token1), the fee, the recipient of the liquidity tokens, tick boundaries (tickLower and tickUpper), and the desired and minimum amounts of both tokens (amount0Desired, amount1Desired, amount0Min, and amount1Min).
Functions in LiquidityManagement:
uniswapV3MintCallback Function: This function is part of the IUniswapV3MintCallback interface. It's called by the Uniswap V3 pool when liquidity is minted. This function receives the owed amounts of token0 and token1 and the callback data (decoded from bytes). It verifies the callback using CallbackValidation, and if owed amounts are greater than zero, it invokes the pay function.
addLiquidity Function: This function is used to add liquidity to an initialized pool. It calculates the liquidity amount based on the provided parameters using the LiquidityAmounts library. It then calls the mint function on the Uniswap V3 pool to mint the liquidity, providing the recipient, tick boundaries, liquidity amount, and callback data.
If the received amounts of token0 and token1 do not meet the minimum specified amounts (amount0Min and amount1Min), a price slippage check is performed, and it will revert if the check fails.
PeripheryImmutableState: This contract provides a way to store and access immutable state data, specifically the addresses of the factory contract and the WETH9 contract. These addresses are set during deployment and remain unchanged throughout the contract's lifetime. Other contracts in the periphery can inherit from this one and utilize the provided state data.
Immutable State: The contract has two immutable state variables, meaning they cannot be changed after deployment:
factory: This is an address representing the Uniswap V3 factory contract. WETH9: This is an address representing the WETH9 contract. WETH9 is a popular implementation of Wrapped Ether, and it is often used in decentralized finance (DeFi) protocols.
Constructor: The constructor is responsible for initializing the factory and WETH9 state variables with the provided addresses. These addresses are passed as arguments when deploying a contract that inherits from PeripheryImmutableState. This constructor is executed only once during the deployment of such contracts.
Multicall: The Multicall contract provides a way to batch multiple function calls into a single transaction, potentially saving on gas costs and improving efficiency. However, it's important to note that the success of the entire transaction depends on the success of each individual function call within it. If any of the calls fails, the entire transaction is reverted.
The multicall function allows multiple external method calls in a single transaction. It takes an array of bytes called data, which represents the encoded function calls that you want to execute in this single transaction. The function is declared as public payable to allow the sending of Ether, though it's not clear from this code how Ether is used.
results: This is an array of bytes that will store the results of the function calls.
The function iterates over the data array, which contains the encoded function call data for each method to be called.
For each method call, it uses a delegate call to execute the function in the current context.
If a function call fails (the success variable is false), it reverts with an error message. The error message is decoded from the result data and includes a custom error message indicating the reason for the revert.
If the function call succeeds, the result is stored in the results array.
ERC721Permit: This is an extension of the ERC721 standard for non-fungible tokens (NFTs). It adds the ability to approve token transfers via a signature, commonly referred to as a "permit" function.
Features
It extends
ERC721
(the ERC721 NFT contract) and implements theIERC721Permit
interface.Internal Function
_getAndIncrementNonce
: This is a virtual internal function used to retrieve and increment the nonce for a given token ID. The nonce is used in the permit signature to ensure uniqueness.nameHash
andversionHash
: These are private immutable state variables representing the keccak256 hashes of the contract's name and version.Constructor: The constructor sets the
nameHash
andversionHash
based on the provided name, symbol, and version of the NFT contract.DOMAIN_SEPARATOR
Function: This function returns the EIP-712 domain separator for permit signatures. It's used to separate the permit signature from other signatures in the contract.PERMIT_TYPEHASH
Constant: This is a constant representing the type of the permit message. It's a unique identifier for the permit function.permit
Function: This is the core function that allows a user to approve a transfer of an NFT via a permit. It takes the following parameters:spender
: The address that is being given permission to transfer the token.tokenId
: The ID of the token.deadline
: The timestamp by which the permit must be used.v
,r
, ands
: These are parts of the permit signature.
The function performs the following steps:
Checks if the permit has expired based on the given
deadline
.Computes a
digest
that represents the hashed message to be signed.Retrieves the current owner of the token.
Validates the permit signature in the following ways:
Checks if the
spender
is not the current owner.If the owner is a contract, it validates the signature using the
isValidSignature
function from theIERC1271
interface.If the owner is not a contract, it validates the signature using the
ecrecover
function.
If the signature is valid, the
spender
is approved to transfer the token.
PeripheryValidation: This contract provides a reusable modifier checkDeadline that can be used in other contracts to ensure that certain time-bound conditions are met before executing specific functions. This is often used to protect against actions that are only valid within a certain time frame.
SelfPermit: This contract provides functionality for self-approving transactions on EIP-2612-compliant tokens.
It allows users to approve a contract and call a function that requires approval in a single transaction effectively simplifying the approval process.
Function selfPermit
: This function allows the caller to execute a permit on a specified token, granting an allowance. It takes the following parameters:
token
: The address of the ERC-20 token for which the permit is requested.value
: The amount for which the permit is granted.deadline
: The timestamp until which the permit is valid.v
,r
,s
: Components of the signature to verify the permit.
This function calls the permit
function of the IERC20Permit
interface to approve the transaction.
Function selfPermitIfNecessary
: This function is intended for executing a permit only if the existing allowance is insufficient. It takes parameters similar to selfPermit
. If the existing allowance is less than the specified value, it calls the selfPermit
function to grant the permit.
Function selfPermitAllowed
**: This function is used to execute a permit on a token that implements the IERC20PermitAllowed
interface. It takes the following parameters:
token
: The address of the token that allowspermit
andpermitAllowed
.nonce
: A unique nonce for the permit.expiry
: The timestamp until which the permit is valid.v
,r
,s
: Components of the signature to verify the permit.
This function calls the permit
function of the IERC20PermitAllowed
interface to approve the transaction.
Function selfPermitAllowedIfNecessary
**: Similar to selfPermitIfNecessary
, this function is used to execute a permit only if the existing allowance is insufficient for tokens that implement the IERC20PermitAllowed
interface. If the existing allowance is less than the maximum value (represented by type(uint256).max
), it calls the selfPermitAllowed
function to grant the permit.
PoolInitializer: In summary, this contract helps create and initialize Uniswap V3 pools for specific token pairs and fee tiers. If a pool doesn't exist or hasn't been initialized, this contract will handle the creation and initialization process.
The contract is declared as PoolInitializer and inherits from the IPoolInitializer interface and PeripheryImmutableState.
Function createAndInitializePoolIfNecessary: This function allows users to create and initialize a Uniswap V3 pool if it doesn't already exist or if it exists but hasn't been initialized.
Parameters
token0 and token1 are the addresses of the two tokens that will form the trading pair in the pool.
fee represents the fee tier for the pool.
sqrtPriceX96 is the initial square root price for the pool.
Steps:
It checks whether token0 has a lower address than token1. This is important because Uniswap V3 pools are created with the smaller address as token0.
It attempts to retrieve the pool address for the given token pair and fee tier using the IUniswapV3Factory interface's getPool function.
If the pool doesn't exist (pool address is 0), it creates a new pool using - - IUniswapV3Factory's createPool function with the specified token0, token1, and fee.
After creating the pool, it initializes the pool with the provided sqrtPriceX96 using IUniswapV3Pool's initialize function.
If the pool already exists but hasn't been initialized (the sqrtPriceX96Existing is 0), it initializes the existing pool with the provided sqrtPriceX96.
Functions function positions(uint256 tokenId) external view override returns (...)
This function allows you to retrieve details about a Uniswap V3 position associated with a specific token ID.
It returns information such as nonce, operator, token addresses, tick range, liquidity, fee growth, and tokens owed.
function cachePoolKey(address pool, PoolAddress.PoolKey memory poolKey) private returns (uint80 poolId)`
This internal function is used to cache a pool key by its address and store it in the
_poolIds
and_poolIdToPoolKey
mappings.If the pool doesn't exist in the mappings, it assigns a unique
poolId
and maps it to thepoolKey
.
function mint(MintParams calldata params) external payable override checkDeadline(params.deadline) returns (...)
This function allows users to mint a new Uniswap V3 position represented as an ERC721 token.
It adds liquidity to a pool, mints a new token, and stores the associated position details.
function tokenURI(uint256 tokenId) public view override(ERC721, IERC721Metadata) returns (string memory)
This function returns the token URI for a specific ERC721 token.
It's an overridden function from the ERC721 and IERC721Metadata interfaces.
function increaseLiquidity(IncreaseLiquidityParams calldata params) external payable override checkDeadline(params.deadline) returns (...)`
This function allows users to increase the liquidity of an existing Uniswap V3 position.
It updates the position details, including tokens owed, fee growth, and liquidity.
function decreaseLiquidity(DecreaseLiquidityParams calldata params) external payable override isAuthorizedForToken(params.tokenId) checkDeadline(params.deadline) returns (...)
This function allows users to decrease the liquidity of an existing Uniswap V3 position.
It burns the specified liquidity, updates position details, and checks for price slippage.
function collect(CollectParams calldata params) external payable override isAuthorizedForToken(params.tokenId) returns (...)`
This function allows users to collect tokens owed from a Uniswap V3 position.
It updates fee growth, calculates the amount of tokens collected, and records the updated tokens owed.
function burn(uint256 tokenId) external payable override isAuthorizedForToken(tokenId)
This function allows users to burn (delete) an ERC721 token representing a Uniswap V3 position.
It ensures that the position is cleared (no liquidity or tokens owed) before burning the token.
function _getAndIncrementNonce(uint256 tokenId) internal override returns (uint256)
- This internal function retrieves the nonce for a specific token and increments it.
function getApproved(uint256 tokenId) public view override(ERC721, IERC721) returns (address)
- This function returns the address that is approved to interact with a specific ERC721 token. - It is an overridden function from both the ERC721 and IERC721 interfaces.
function _approve(address to, uint256 tokenId) internal override(ERC721)
- This internal function sets the operator (address allowed to interact with the token) for a given ERC721 token. - It is an overridden function from the ERC721 interface.
Congratulations on going through this review of NonFungiblePositionManager of uniswapV3 periphery. Hope you found it comprehensive enough. Feel free to provide clarifications to grey areas.
Subscribe to my newsletter
Read articles from Samson Ajulor directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Samson Ajulor
Samson Ajulor
On hashnode, I am a researcher. I reside somewhere on planet earth and I have a life. Thank you.