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
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
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%.
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 beGAS
. If you would be deploying on Ethereum, you may change theWGAS
address to theWETH
address._dexPoolFee
: the transaction fee. In our example, we set it at10_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
1contract 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
1constructor(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
.
1function 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 pool2/// @param tk: The token address3/// @return pool_ The address of the liquidity pool created4function _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 fee8 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 pool6 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 address3/// @param pool: The address of the liquidity pool to mint4/// @param tkAmountToMint: The amount of token to mint5/// @param amountTkMin: The minimum amount of tokens to mint in liqudity pool6/// @param amountGASMin: The minimum amount of GAS to mint in liqudity pool7/// @return tokenId The id of the newly minted ERC7218/// @return liquidity The amount of liquidity for the position9/// @return token0 The Address of token010/// @return amount0 The amount of token011/// @return token1 The Address of token112/// @return amount1 The amount of token113function _mintLiquidity(14 address tk,15 address pool,52 collapsed lines
16 uint256 tkAmountToMint,17 uint256 amountTkMin,18 uint256 amountGASMin19)20 internal21 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 manager30 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.timestamp51 });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 deposit60 _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
1function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes memory) external {2 IWETH(WGAS).transfer(msg.sender, amount0Delta > amount1Delta ? uint256(amount0Delta) : uint256(amount1Delta));3}
helper functions
1function _removeAllowanceAndRefundToken(2 address tk,3 uint256 amount,4 uint256 amountToMint5)6 internal7 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
18function _createDeposit(address owner, uint256 tokenId) internal {19 (,, address token0, address token1,,,, uint128 liquidity,,,,) = uniswapPositionManager.positions(tokenId);20 // set the owner and data for position21 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