Uniswap v3 NFT Management

Overview

โ€ŒEvery Uniswap v3 farming setup has its own specific price curve information, delineated by the following parameters:

  • pool address.

  • token0 and token1 .

  • fee percentage.

  • tickLower and tickUpper.

These are established by the host during the creation of the setup (see here) within its parent contract. Every farmer, upon opening a position, mints an NFT in accordance with these parameters. All NFTs are stored within the contract. A farmer interacts with his or her specific NFT in the farming contract when conducting operations such as adding or removing liquidity or claiming training fees. Advantages

Advantages

โ€ŒHaving an individual NFT per position has its own advantages over sharing NFTs. For example:

  • There are no Impermanent Loss on trading fees amount.

  • Claiming rewards + fees is cheaper for farmers.

NFT Minting & Burning

The following NFT parameters are established when a setup is created:

  • LP address -> from which the two token addresses and the fee of the pool are retrieved

  • tickLower ->parameter representing the value of tickLower used to create the Uniswap v3 NFT for the setup.

  • tickUpper ->parameter representing the value of tickUpper used to create the Uniswap v3 NFT for the setup.

These parameters are saved within the FarmingSetupInof. Once the setup is activatedโ€”i.e, when a farmer creates a new position within itโ€”the NFT for his position is minted, by taking them as the mintParams to create the NFT position via the Uniswap v3 INonfungiblePositionManager:

     data[0] = abi.encodeWithSelector(nonfungiblePositionManager.mint.selector, abi.encode(INonfungiblePositionManager.MintParams({
         token0: token0, 
         token1: IUniswapV3Pool(setupInfo.liquidityPoolTokenAddress).token1(),
         fee: IUniswapV3Pool(setupInfo.liquidityPoolTokenAddress).fee(),
         tickLower: setupInfo.tickLower, 
         tickUpper: setupInfo.tickUpper,
         amount0Desired: request.amount0, 
         amount1Desired: request.amount1,
         amount0Min: 1,
         amount1Min: 1,
         recipient: address(this),
         deadline: block.timestamp + 10000
    })));
    (tokenId, liquidityAmount, amount0, amount1) = abi.decode(IMulticall(address(nonfungiblePositionManager)).multicall{ value: ethValue }(data)[0], (uint256, uint128, uint256, uint256));

When a farmer removes all of his liquidity and thus closes his farming position, his NFT is automatically burned via the INonfungiblePositionManager burn method.

if (liquidityPoolTokenAmount == 0) { 
   nonfungiblePositionManager.burn(farmingPosition.tokenId);
   delete _positions[positionId];
}

โ€ŒIf a setup has been customized to renew after it ends (via the _toggleSetup function), then after it renews, NFTs will be minted (when farmers open positions) with the same parameters as before. This ensures that each farming position always only corresponds to a specific NFT.

Trading Fee Management

โ€ŒTrading fees in Uniswap v3 are managed differently than in the other AMMs supported by the Covenants AMM Aggregator (like Uniswap v2), as they can be withdrawn separately from liquidity. Accordingly, regular v1.5 Covenant farming contracts for Uniswap v3 are designed so that farmers of the same setup withdraw only fees they have earned individually, as these are stored in the NFT of their individual position.

So, when a farmer withdraws his reward, or removes all or part of his liquidity, he also automatically removes from the NFT all of the trading fees he has earned up until that point.

Liquidity

The architecture of Covenant Farming v1.0 contracts was relatively easy to adapt to Uniswap v3. Although Uniswap v3 doesn't use fungible LP tokens, the v3 NFTs do use the "liquidity" parameter which represents the liquidity within an NFT's curve. This liquidity parameter is used in regular v1.5 contracts (instead of LP tokens, like in v1.0 contracts) for farming features such as position reward calculation. Since there is a unique NFT for every farmerโ€™s position, every farmerโ€™s liquidity amount in the farming contract is equivalent to the total liquidity amount in his NFT.

So, for example, in OpenPosition operations, the liquidity parameter is used as follows:

(uint256 tokenId, uint128 liquidityAmount) = _addLiquidity(request.setupIndex, request, 0);
_positions[positionId] = FarmingPosition({
    uniqueOwner: uniqueOwner,
    setupIndex : request.setupIndex,
    tokenId: tokenId,
    reward: 0,
    creationBlock: block.number
});

The liquidityAmount parameter represents the liquidity amount in a Uniswap v3 farming setup.

The same thing applies to the addLiquidity operation:

(, uint128 liquidityAmount) = _addLiquidity(farmingPosition.setupIndex, request, farmingPosition.tokenId);

For WithdrawLiquidity operations, the liquidity parameter is used as follows:

_setups[farmingPosition.setupIndex].totalSupply -= removedLiquidity; 
LiquidityPoolTokenAmount -= removedLiquidity;

The totalSupply of a setup is the sum of all the โ€œliquidityโ€ parameters of every NFT present in the setup.

What was managed through the LP token amount in the v1.0 contracts is managed through the liquidity parameter of the NFT in Regular v1.5 contracts.

Last updated