UNISWAP V3 (Periphery) NonfungiblePositionManager CODE REVIEW DEEP DIVE

Samson AjulorSamson Ajulor
29 min read

This is not an audit of v3. Interested in the Audit report?

Click here!!

PS: This will probably be outdated by the time you read this.

Uniswap V3 periphery is made up of the following functions.

  1. NonFungiblePositionManager

  2. NonFungiblePositionDescriptor

  3. SwapRouter

  4. 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:

  1. 20 and 60770 are the operands for multiplication.

  2. % represents the modulo operation, which calculates the remainder when the left operand is divided by the right operand.

  3. not(0) represents the bitwise NOT operation applied to 0, 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, so lt(mm, prod0) is true because mm is less than prod0.

  • sub(mm, prod0) would result in a negative number (underflow).

  • To prevent this underflow, we use lt(mm, prod0) to create a value of 1 or true, which is then subtracted from mm. 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 of mulmod(2000, 3000, 5000)),

  • prod0 = 6000000 (result of mul(2000, 3000)),

  • max_value = 5000 (the assumed maximum value for an unsigned integer),

We'll calculate the values step by step:

  1. sub(mm, prod0): Calculates the difference between mm and prod0. 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 because 4000 - 6000000 is less than the maximum value:

     sub(mm, prod0) = 4000 - 6000000 = -5996000
    
  2. lt(mm, prod0): Compares mm with prod0 to check if mm is less than prod0. If there's an overflow, this comparison will be 1 (true), indicating that mm is indeed less than prod0. In this case, mm is less than prod0 because 4000 is less than 6000000.

     lt(mm, prod0) = 1
    
  3. Finally, the result is subtracted by 1 if mm is less than prod0. This adjustment is made to account for the overflow, ensuring that prod1 contains the most significant bits within the limit of max_value:

    Since lt(mm, prod0) is 1, we subtract 1 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 than prod1.
uint256 remainder;
assembly {
    remainder := mulmod(a, b, denominator)
}
  • The above code calculates the remainder by performing a modulo operation on a * b with denominator.
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 than prod0. 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 divides denominator 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 and prod1 by shifting prod1 into prod0 using twos 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 the a×b÷denominator. It calls the mulDiv 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 that result is less than the maximum value of uint256.

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 uses PoolAddress.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 using PoolAddress.computeAddress, which combines the factory's address and the poolKey.

  • 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 the IERC721Permit 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 and versionHash: These are private immutable state variables representing the keccak256 hashes of the contract's name and version.

  • Constructor: The constructor sets the nameHash and versionHash 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, and s: 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 the IERC1271 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 allows permit and permitAllowed.

  • 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 the poolKey.

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.

0
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.