Uniswap Walking Through

Promising Future

Target

In a nutshell, I am creating an NFT project that is using an ERC404 implementation DN404, so I am kind of worried about whether it is swappable with the Uniswap toolchain.

TL;DR;

  1. Deploy the ERC20 contract on Ethereum Sepolia, cause the Uniswap contracts on it are the most frequently used way to test swapping.
  2. Create a token pair pool with this tool
  3. Add liquidity to the pool.
  4. Swap it, it works.

Deploy contracts to testnet

To verify this, I need to find the officially deployed contracts, which you can find here,
Next, I started asking GPT, the first step will be, deploying my token contract to the aiming testnet, in my case, I tried :
“Sepolia”,
“Base Sepolia”,
“Arbitum Sepolia”,
“Optimism Sepolia”.
Make sure you minted enough tokens to fulfill the following steps.

Using V3 Uniswap

Create the token pair pool, I started doing this with MyToken and the WETH. Base is doing a great job on Docs, you can easily find trustworthy contracts here. On the contrary, Uniswap’s example code is full of typos and even the class name can be misspelled, I wonder if they run their code before putting it in Dev Docs.
Due to Uniswap’s messy sample code, it misguided me a lot, sometimes started to doubt my value of existence. I eventually combined GPT’s guide code and the official example from Uniswap. Here is what it looks like.

Step 1: create-pool.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { ethers } from 'hardhat';
import { abi as UniswapV3FactoryABI } from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json';
import dotenv from 'dotenv';
dotenv.config({ path: 'uniswap.env' });

const {
UNISWAP_FACTORY_ADDRESS,//the uniswap v3 factory address
GBS1, // Deployed GBS Token1 address
GBS2, // Deployed GBS Token2 address/ The WETH address
FEE, // the pool fee
} = process.env;

async function main() {
const [deployer] = await ethers.getSigners();
console.log('Deployer address:', deployer.address);

const factory = new ethers.Contract(
UNISWAP_FACTORY_ADDRESS!,
UniswapV3FactoryABI,
deployer
);

// Check if the pool already exists
const poolAddress = await factory.getPool(
GBS1!,
GBS2!,
FEE
);
if (poolAddress !== ethers.constants.AddressZero) {
console.log('Pool already exists at address:', poolAddress);
return;
}
console.log(poolAddress);
try {
// Create and initialize the pool
const tx = await factory.createPool(
GBS1!,
GBS2!,
FEE,
{
gasLimit: ethers.utils.parseUnits('5000000', 'wei'), // Adjust the gas limit as necessary
}
);

const receipt = await tx.wait();
console.log('Pool created and initialized:', receipt.transactionHash);
} catch (error:any) {
console.error('Error creating and initializing pool:', error.transactionHash);
}
}

Step 2: init-pool.ts.

Of course, you can initialize it with the “createAndInitializePoolIfNecessary” function on NonfungiblePositionManager, but I got stuck on calculating a valid sqrtPriceX96 value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import { abi as IUniswapV3PoolABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json';
import { abi as UniswapV3FactoryABI } from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json';
import { ethers } from 'hardhat';
import { encodeSqrtRatioX96 } from '@uniswap/v3-sdk';
import dotenv from 'dotenv';
dotenv.config({ path: 'uniswap.env' });
const { UNISWAP_FACTORY_ADDRESS, GBS1, GBS2, FEE } = process.env;
const INITIAL_PRICE = '1';

async function initializePool() {
const [deployer] = await ethers.getSigners();
const factory = new ethers.Contract(
UNISWAP_FACTORY_ADDRESS!,
UniswapV3FactoryABI,
deployer
);

const poolAddress = await factory.getPool(GBS1!, GBS2!, FEE);
const poolContract = new ethers.Contract(
poolAddress,
IUniswapV3PoolABI,
deployer
);
// Example price of Token B in terms of Token A,
// if you want 1 token A equals 1000 token B, initial price of A should be 0.001,and B should still be 1
const priceTokenA = ethers.utils.parseUnits('1', 18);
const priceTokenB = ethers.utils.parseUnits('1', 18);

// Calculate the sqrtPriceX96 using the encodeSqrtRatioX96 function
const sqrtPriceX96 = encodeSqrtRatioX96(
priceTokenA.toString(),
priceTokenB.toString()
);
console.log(sqrtPriceX96.toString());

const tx = await poolContract.initialize(sqrtPriceX96.toString(), {
gasLimit: ethers.utils.parseUnits('5000000', 'wei'),
});

await tx.wait();

console.log('Pool initialized with price:', INITIAL_PRICE);

const token0 = await ethers.getContractAt('IERC20', GBS1!);
await token0.approve(poolAddress, BigInt(1000) * 10n ** 18n);
}

Step 3: add-liquidity.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import { ethers } from 'hardhat';
import { abi as UniswapV3FactoryABI } from '@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json';
import { abi as IUniswapV3PoolABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/IUniswapV3Pool.sol/IUniswapV3Pool.json';
import {
nearestUsableTick,
NonfungiblePositionManager,
Position,
MintOptions,
Pool,
FeeAmount,
} from '@uniswap/v3-sdk';
import { Token, Percent } from '@uniswap/sdk-core';
import dotenv from 'dotenv';
dotenv.config({ path: 'uniswap.env' });
const {
UNISWAP_FACTORY_ADDRESS,
NONFUNGIBLE_POSITION_MANAGER_ADDRESS,
GBS1,
GBS2,
FEE,
} = process.env;
const TOKEN0 = new Token(84532, GBS1!, 18);
const TOKEN1 = new Token(84532, GBS2!, 18);
const SLIPPAGE_TOLERANCE = new Percent(50, 10_000); // 0.5%
async function addLiquidity() {
const [deployer] = await ethers.getSigners();

const factory = new ethers.Contract(
UNISWAP_FACTORY_ADDRESS!,
UniswapV3FactoryABI,
deployer
);
const poolAddress = await factory.getPool(GBS1!, GBS2!, FEE);
const poolContract = new ethers.Contract(
poolAddress,
IUniswapV3PoolABI,
deployer
);

const [liquidity, slot0] = await Promise.all([
poolContract.liquidity(),
poolContract.slot0(),
]);
const pool = new Pool(
TOKEN0,
TOKEN1,
FeeAmount.LOW,
slot0.sqrtPriceX96.toString(),
liquidity.toString(),
slot0.tick
);
const AMOUNT0_DESIRED = ethers.utils.parseUnits('10000', 18); // Amount of MyToken to add
const AMOUNT1_DESIRED = ethers.utils.parseUnits('10000', 18);
const position = new Position({
pool,
liquidity: AMOUNT0_DESIRED.toString(),
tickLower:
nearestUsableTick(slot0.tick, pool.tickSpacing) - pool.tickSpacing * 2,
tickUpper:
nearestUsableTick(slot0.tick, pool.tickSpacing) + pool.tickSpacing * 2,
});

const mintOptions: MintOptions = {
recipient: deployer.address,
slippageTolerance: SLIPPAGE_TOLERANCE,
deadline: Math.floor(Date.now() / 1000) + 60 * 20,
};

const { calldata, value } = NonfungiblePositionManager.addCallParameters(
position,
mintOptions
);

const transaction = {
to: NONFUNGIBLE_POSITION_MANAGER_ADDRESS,
from: deployer.address,
data: calldata,
value: value,
};

const token0 = await ethers.getContractAt('IERC20', TOKEN0.address);
const token1 = await ethers.getContractAt('IERC20', TOKEN1.address);

const balance0 = await token0.balanceOf(deployer.address);
console.log(`balance0 ${ethers.utils.formatUnits(balance0, 18)}`);
const balance1 = await token1.balanceOf(deployer.address);
console.log(`balance1 ${ethers.utils.formatUnits(balance1, 18)}`);

console.log('Approving token0...');
await token0.approve(NONFUNGIBLE_POSITION_MANAGER_ADDRESS!, AMOUNT0_DESIRED);
console.log('Approving token1...');
await token1.approve(NONFUNGIBLE_POSITION_MANAGER_ADDRESS!, AMOUNT1_DESIRED);

const tx = await deployer.sendTransaction(transaction);
await tx.wait();

console.log('Position minted successfully');
}

But at this stage, something weird happened, no matter how much WETH I desired to add, it always transferred only 1 wei to the pool. I double-checked the parameters I input, and nothing was wrong, so I started disputing if the WETH was working correctly on this testnet. That’s why I changed the token pair to my deployed tokens, GBS1:the DN404 token, GBS2:COCO a standard ERC20 token.
Step 4: swap.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { ethers } from 'hardhat';
import { abi as ISwapRouterABI } from '@uniswap/v3-periphery/artifacts/contracts/interfaces/ISwapRouter.sol/ISwapRouter.json';
import { Token, Percent } from '@uniswap/sdk-core';

import dotenv from 'dotenv';
import { encodeSqrtRatioX96 } from '@uniswap/v3-sdk';
dotenv.config({ path: 'uniswap.env' });

const { GBS1, GBS2, SWAP_ROUTER_ADDRESS,FEE } =
process.env;

const TOKEN0 = new Token(84532, GBS1!, 18);
const TOKEN1 = new Token(84532, GBS2!, 18);

async function swap() {
const [deployer] = await ethers.getSigners();

const amountIn = ethers.utils.parseUnits('100', 18); // Amount of MyToken to add
const amountOutMin = ethers.utils.parseUnits('0', 18);
const swapRouter = new ethers.Contract(
SWAP_ROUTER_ADDRESS!,
ISwapRouterABI,
deployer
);
const inputTokenContract = await ethers.getContractAt(
'IERC20',
TOKEN0.address
);
await inputTokenContract.approve(SWAP_ROUTER_ADDRESS!, amountIn);
const priceTokenA = ethers.utils.parseUnits('1', 18);
const priceTokenB = ethers.utils.parseUnits('1', 18);
const sqrtPriceX96 = encodeSqrtRatioX96(
priceTokenA.toString(),
priceTokenB.toString()
);
const params = {
tokenIn: TOKEN0.address,
tokenOut: TOKEN1.address,
fee: 500, // Set your fee tier here
recipient: deployer.address,
amountIn: amountIn,
amountOutMinimum: amountOutMin,
deadline: Math.floor(Date.now() / 1000) + 60 * 20,
sqrtPriceLimitX96: 0//sqrtPriceX96.toString(),
};
console.log(params);
const data = swapRouter.interface.encodeFunctionData(
"exactInputSingle",
[params]);
const txArgs = {
to: SWAP_ROUTER_ADDRESS,
data: data,
gasLimit: ethers.utils.parseUnits('500000', 'wei')
}
const transaction = await deployer.sendTransaction(txArgs);
const res = await transaction.wait();
console.log('OK',res.transactionHash);
}

Due to the weird thing from the add liquidity step, this step fails all the time, and what’s more irritating is, the transaction error info lacking of detail all the time.
Till here, I already put three days into this seemingly simple task. After this, I tried to walk these steps on other testnets, but none of them worked, some of them couldn’t be added liquidity, and the others failed on swapping, and as always, there was no detailed error information. You know it was wrong, just don’t know why.
FailButFailToKnowWhy

Using web3swag Swap App

Twists and turns
So I stopped and contemplated for a moment. Wasn’t that my goal verifying the legitimacy of my DN404 token on uniswap? There is no need to complete the whole process with code by myself.
After googling a bit, I found a tool here, that only supports Ethereum Sepolia, but it doesn’t matter, I deployed the contract there too.
After several straightforward steps, I can finally swap my Token “GBS1.0” to “COCO”. And I found that it’s using the V2 Uniswap contract, not V3.