Jida's Blog

UniswapV3: Launch Liquidity Pool in Solidity

20th October 2024
DeFi
Web3
DEX
UniswapV3
Last updated:25th January 2025
6 Minutes
1109 Words

In this article, we would implement a smart contract for launch of liquidity pool between GAS (the native token of NeoX, if you are using chains like Ethereum the native token would be ETH), and ERC20 tokens.

1. Introduction

1.1 Structures

From the code perspective, v3 has a similar structure from v2

1) core

  • UniswapV3Factory: provide the interface for pool creation and tracking.
  • UniswapV3Pool: provide the core functionalities, like swap, mint, burn.

2) periphery

  • SwapRouter: provide the interface for token trade.
  • NonfungiblePositionManager: provide functionalities of adding/ removing/ modifying pool’s liquidity, and tokenize liquidity through NFT

1.2 V3 design

1) Virtual reserves

Instead of providing liquidity across the entire price range in v2, v3 allows LPs to concentrate their liquidity to smaller price ranges. A position only needs to maintain enough reserves to support trading within its range. From the perspective of a single liquidity pool, v3 acts like a liquidity aggregator.

default

2) Tick

  • more about tick:

A tick is a measure of the minimum upward or downward movement in the price of a security.

tick is a finite set of integer in range of . It represents prices at which the virtual liquidity of the contract can change. In v3, the space of possible prices is demarcated by discrete ticks to allow the implementation of custom liquidity provision.

In the implementation, you would need to provide _tickLower and _tickUpper to setup the price range of the liquidity.

The price formula is:

3) Transaction Fees

In v1 and v2, each liquidity pool has a fixed transaction fee of 0.3%. However, the fee can be too high for pools of stable coins, and too low for pools of meme coins. Thus, v3 supports fee tiers at 0.01%, 0.05%, 0.5%, and 1%.

default

2. Implementations

In the following example, we would create a smart contract for creating liquidity pools, adding liquidity, and collecting fees from the pools.

2.1 Get ready

Before working on our smart contract, we would need to find the addresses for UniswapV3Factory and NonfungiblePositionManager. Since our contract would rely on these contracts. You can find the addresses for the blockchain you would be working with from this link: https://docs.uniswap.org/contracts/v3/reference/deployments/

2.2 Get started

  • WGAS: since I would be deployed on NeoX, the native token would be GAS. If you would be deploying on Ethereum, you may change the WGAS address to the WETH address.
  • _dexPoolFee: the transaction fee. In our example, we set it at 10_000( 1%). You can also set it at other supported values.
  • _poolTick: tick for the initial price (would cover how to calculate it later)
  • _tickLower and _tickUpper: bound the price range of the liquidity
1
contract DexLauncher{
2
address public immutable WGAS;
3
uint24 private constant _dexPoolFee = 10_000; // 1%
4
5
int24 private _poolTick;
6
int24 private _tickLower;
7
int24 private _tickUpper;
8
9
IUniswapV3Factory public uniswapV3Factory;
10
INonfungiblePositionManager public uniswapPositionManager;
11
}

2.3 Initialize

1
constructor(
2
address uniswapV3Factory_,
3
address uniswapPositionManager_,
4
address wgas_
5
) {
6
if (uniswapV3Factory_ == address(0) || uniswapPositionManager_ == address(0) || wgas_ == address(0)) {
7
revert InvalidParameters();
8
}
9
10
uniswapV3Factory = IUniswapV3Factory(uniswapV3Factory_);
11
uniswapPositionManager = INonfungiblePositionManager(uniswapPositionManager_);
12
WGAS = wgas_;
13
14
IWETH(WGAS).approve(uniswapV3Factory_, type(uint256).max);
15
}

2.4 Set tick

1) calculate poolTick_

In order to calculate poolTick_, we would be using this formula:

For example, you want to create a liquidity pool of GAS and USDT. And you want to make the initial price to be 3 USDT/GAS. If you have 1,000 GAS for liquidity, you would need to provide 3,000 USDT according. Thus, poolTick_ can be calculated by (assume decimals of both tokens are the same):

2) calculate tickLower_ & tickHigher_

Ticks used for positions in upper and lower ranges must be evenly divisible by the tick spacing. We can calculate tickSpacing by feeAmountTickSpacing(uint24 fee) of UniswapV3Factory.

1
function setTick(int24 poolTick_, int24 tickLower_, int24 tickHigher_) external onlyOperator {
2
int24 tickSpacing = uniswapV3Factory.feeAmountTickSpacing(_dexPoolFee);
3
4
_tickLower = (tickLower_ / tickSpacing) * tickSpacing;
5
_tickUpper = (tickHigher_ / tickSpacing) * tickSpacing;
6
_poolTick = poolTick_;
7
emit tickUpdated(_poolTick, _tickLower, _tickUpper);
8
}

3) Create pool

1
/// @notice Creates and initializes liquidty pool
2
/// @param tk: The token address
3
/// @return pool_ The address of the liquidity pool created
4
function _createPool(address tk) internal returns (address pool_) {
5
(address token0_, address token1_) = tk < WGAS ? (tk, WGAS) : (WGAS, tk);
6
7
// creates a pool for the given two tokens and fee
8
pool_ = uniswapV3Factory.createPool(token0_, token1_, _dexPoolFee);
9
if (pool_ == address(0)) {
10
revert InvalidAddress();
11
}
12
13
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(_poolTick);
14
15
// set the initial price for the pool
6 collapsed lines
16
IUniswapV3Pool(pool_).initialize(sqrtPriceX96);
17
18
emit PoolCreated(tk, pool_, sqrtPriceX96);
19
20
_positionInfoForToken[tk].poolAddress = pool_;
21
}

2.5 mint liquidity

1
/// @notice Calls the mint function in periphery of uniswap v3, and refunds the exceeding parts.
2
/// @param tk: The token address
3
/// @param pool: The address of the liquidity pool to mint
4
/// @param tkAmountToMint: The amount of token to mint
5
/// @param amountTkMin: The minimum amount of tokens to mint in liqudity pool
6
/// @param amountGASMin: The minimum amount of GAS to mint in liqudity pool
7
/// @return tokenId The id of the newly minted ERC721
8
/// @return liquidity The amount of liquidity for the position
9
/// @return token0 The Address of token0
10
/// @return amount0 The amount of token0
11
/// @return token1 The Address of token1
12
/// @return amount1 The amount of token1
13
function _mintLiquidity(
14
address tk,
15
address pool,
52 collapsed lines
16
uint256 tkAmountToMint,
17
uint256 amountTkMin,
18
uint256 amountGASMin
19
)
20
internal
21
returns (uint256 tokenId, uint128 liquidity, address token0, uint256 amount0, address token1, uint256 amount1)
22
{
23
uint256 gasAmountToMint = msg.value;
24
25
{
26
TransferHelper.safeTransferFrom(tk, msg.sender, address(this), tkAmountToMint);
27
IWETH(WGAS).deposit{value: gasAmountToMint}();
28
29
// Approve the position manager
30
TransferHelper.safeApprove(tk, address(uniswapPositionManager), tkAmountToMint);
31
TransferHelper.safeApprove(WGAS, address(uniswapPositionManager), gasAmountToMint);
32
}
33
34
(token0, token1) = tk < WGAS ? (tk, WGAS) : (WGAS, tk);
35
(uint256 tk0AmountToMint, uint256 tk1AmountToMint) =
36
tk < WGAS ? (tkAmountToMint, gasAmountToMint) : (gasAmountToMint, tkAmountToMint);
37
38
{
39
INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({
40
token0: token0,
41
token1: token1,
42
fee: _dexPoolFee,
43
tickLower: _tickLower,
44
tickUpper: _tickUpper,
45
amount0Desired: tk0AmountToMint,
46
amount1Desired: tk1AmountToMint,
47
amount0Min: amountTkMin,
48
amount1Min: amountGASMin,
49
recipient: msg.sender,
50
deadline: block.timestamp
51
});
52
53
(tokenId, liquidity, amount0, amount1) = uniswapPositionManager.mint(params);
54
emit PoolLiquidityMinted(tk, tokenId, pool, amount0, amount1, liquidity);
55
}
56
57
_positionInfoForToken[tk] = PositionInfo({lpTokenId: tokenId, poolAddress: pool});
58
59
// Create a deposit
60
_createDeposit(msg.sender, tokenId);
61
62
// Remove allowance and refund in both assets.
63
uint256 tk0Refund = _removeAllowanceAndRefundToken(token0, amount0, tk0AmountToMint);
64
uint256 tk1Refund = _removeAllowanceAndRefundToken(token1, amount1, tk1AmountToMint);
65
66
emit PoolLiquidityRefunded(pool, msg.sender, token0, tk0Refund, token1, tk1Refund);
67
}

uniswapV3SwapCallback

1
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes memory) external {
2
IWETH(WGAS).transfer(msg.sender, amount0Delta > amount1Delta ? uint256(amount0Delta) : uint256(amount1Delta));
3
}

helper functions

1
function _removeAllowanceAndRefundToken(
2
address tk,
3
uint256 amount,
4
uint256 amountToMint
5
)
6
internal
7
returns (uint256 refundAmount)
8
{
9
if (amount < amountToMint) {
10
TransferHelper.safeApprove(tk, address(uniswapPositionManager), 0);
11
refundAmount = amountToMint - amount;
12
if (refundAmount > 1 ether) {
13
TransferHelper.safeTransfer(tk, msg.sender, refundAmount);
14
}
15
}
7 collapsed lines
16
}
17
18
function _createDeposit(address owner, uint256 tokenId) internal {
19
(,, address token0, address token1,,,, uint128 liquidity,,,,) = uniswapPositionManager.positions(tokenId);
20
// set the owner and data for position
21
deposits[tokenId] = Deposit({owner: owner, liquidity: liquidity, token0: token0, token1: token1});
22
}

3. Full implementation

You can view the full implementation from this link: https://github.com/jidalii/uniswap-v3-playground

4. References

https://docs.uniswap.org/contracts/v3/guides/providing-liquidity/the-full-contract

https://trapdoortech.medium.com/uniswap-deep-dive-into-v3-technical-white-paper-2fe2b5c90d2

https://support.uniswap.org/hc/en-us/articles/21069524840589-What-is-a-tick-when-providing-liquidity

Article title:UniswapV3: Launch Liquidity Pool in Solidity
Article author:Jida-Li
Release time:20th October 2024