Skip to content
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9b9d630
feat: uniswap v3 deployment and liquidity commands
fadeev Apr 16, 2025
3d4725e
create pool command
fadeev Apr 17, 2025
5c52248
pools show
fadeev Apr 17, 2025
2209951
pools show
fadeev Apr 17, 2025
ba4ae4b
refactor
fadeev Apr 17, 2025
dc308ae
add liquidity
fadeev Apr 18, 2025
9fd0071
refactor
fadeev Apr 18, 2025
442977d
lint
fadeev Apr 18, 2025
50362ab
refactor
fadeev Apr 18, 2025
fe543e4
lint
fadeev Apr 18, 2025
691b2fb
refactor
fadeev Apr 18, 2025
f6a7c28
refactor
fadeev Apr 18, 2025
2f6a239
check pool exists
fadeev Apr 18, 2025
f49f222
check pool exists
fadeev Apr 18, 2025
28cad95
balances
fadeev Apr 18, 2025
3ea1b78
merge main
fadeev Apr 18, 2025
c6bfb85
zod
fadeev Apr 18, 2025
d16debf
remove duplicate cmd
fadeev Apr 18, 2025
b15c2db
custom price
fadeev Apr 18, 2025
27a0062
refactor
fadeev Apr 18, 2025
0234c46
fix show
fadeev Apr 18, 2025
6f1553b
price
fadeev May 5, 2025
2985c00
updated factory
fadeev May 5, 2025
e09172a
update constants
fadeev May 5, 2025
4904414
merge main
fadeev May 5, 2025
1aa83e7
wip
fadeev May 5, 2025
6907609
Merge branch 'main' into pools-commands
hernan-clich May 28, 2025
4bc23f3
merge main
fadeev Jul 7, 2025
653ad1f
lint
fadeev Jul 7, 2025
cfea79d
node 22
fadeev Jul 7, 2025
cfb9973
fix build
fadeev Jul 7, 2025
8b6e857
fix build
fadeev Jul 7, 2025
4dc7f2c
move pools command inside zetachain namespace
fadeev Jul 7, 2025
1fd27fd
show quotes
fadeev Jul 7, 2025
583a152
wip
fadeev Jul 7, 2025
dff4ad9
wip
fadeev Jul 7, 2025
d26804c
lint
fadeev Jul 7, 2025
1795013
wip
fadeev Jul 8, 2025
228b66d
Merge branch 'main' into pools-commands
fadeev Jul 25, 2025
01acd19
fix pools show
fadeev Jul 25, 2025
a3d96c5
update defaults
fadeev Jul 25, 2025
439a441
update defaults
fadeev Jul 25, 2025
de14ad5
fix
fadeev Jul 25, 2025
cf8946f
seems to be working
fadeev Jul 25, 2025
b8e2bd3
Add fixes for undefined decimals, precision loss and rounding errors
hernan-clich Jul 31, 2025
16bca71
Merge branch 'main' into pools-commands
hernan-clich Jul 31, 2025
01757b2
Merge branch 'main' into pools-commands
hernan-clich Jul 31, 2025
e01d06f
Type fixes
hernan-clich Jul 31, 2025
4cfa442
Lint fixes
hernan-clich Jul 31, 2025
66e1330
Swapped places in array destructuring
hernan-clich Aug 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@
"@openzeppelin/contracts-upgradeable": "^5.0.2",
"@solana/wallet-adapter-react": "^0.15.35",
"@solana/web3.js": "1.95.8",
"@types/inquirer": "^9.0.7",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v3-core": "^1.0.1",
"@uniswap/v3-periphery": "^1.4.4",
"@zetachain/faucet-cli": "^4.1.1",
"@zetachain/networks": "^13.0.0",
"@zetachain/protocol-contracts": "^12.0.0",
Expand All @@ -130,6 +133,7 @@
"form-data": "^4.0.0",
"handlebars": "4.7.7",
"hardhat": "^2.22.8",
"inquirer": "^12.5.2",
"lodash": "^4.17.21",
"ora": "5.4.1",
"spinnies": "^0.5.1",
Expand Down
8 changes: 6 additions & 2 deletions packages/commands/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Command } from "commander";

import { accountsCommand } from "./accounts";
import { poolsCommand } from "./pools/";
import { solanaEncodeCommand } from "./solanaEncode";

export const toolkitCommand = new Command("toolkit")
.description("Local development environment")
.helpCommand(false);

toolkitCommand.addCommand(solanaEncodeCommand);
toolkitCommand.addCommand(accountsCommand);
toolkitCommand
.addCommand(solanaEncodeCommand)
.addCommand(poolsCommand)
.addCommand(solanaEncodeCommand)
.addCommand(accountsCommand);
7 changes: 7 additions & 0 deletions packages/commands/src/pools/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const DEFAULT_RPC =
"https://zetachain-athens.g.allthatnode.com/archive/evm";
export const DEFAULT_FACTORY = "0x7E032E349853178C233a2560d9Ea434ac82228e0";
export const DEFAULT_WZETA = "0x5F0b1a82749cb4E2278EC87F8BF6B618dC71a8bf";
export const DEFAULT_FEE = 3000; // 0.3%
export const DEFAULT_POSITION_MANAGER =
"0xFc5D90f650cf46Cecf96C66a4993f97D2a49f93B";
106 changes: 106 additions & 0 deletions packages/commands/src/pools/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as UniswapV3Factory from "@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json";
import * as UniswapV3Pool from "@uniswap/v3-core/artifacts/contracts/UniswapV3Pool.sol/UniswapV3Pool.json";
import { Command } from "commander";
import { Contract, ethers, JsonRpcProvider, Wallet } from "ethers";

import {
type CreatePoolOptions,
createPoolOptionsSchema,
PoolCreationError,
} from "../../../../types/pools";
import { DEFAULT_FACTORY, DEFAULT_FEE, DEFAULT_RPC } from "./constants";

const main = async (options: CreatePoolOptions): Promise<void> => {
try {
// Validate options
const validatedOptions = createPoolOptionsSchema.parse(options);

if (validatedOptions.tokens.length !== 2) {
throw new Error("Exactly 2 token addresses must be provided");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Check token ordering to comply with Uniswap V3 requirements

Uniswap V3 requires tokens to be in ascending order by address. Your validation checks the number of tokens but not their order, which could lead to failed transactions.

if (validatedOptions.tokens.length !== 2) {
  throw new Error("Exactly 2 token addresses must be provided");
}

+ // Ensure tokens are in ascending order
+ const token0 = validatedOptions.tokens[0].toLowerCase();
+ const token1 = validatedOptions.tokens[1].toLowerCase();
+ if (token0 === token1) {
+   throw new Error("Cannot create pool with identical tokens");
+ }
+ 
+ // Sort tokens to comply with Uniswap V3 requirements
+ if (token0 > token1) {
+   // Swap tokens and their corresponding prices
+   validatedOptions.tokens = [token1, token0];
+   if (validatedOptions.prices) {
+     validatedOptions.prices = [validatedOptions.prices[1], validatedOptions.prices[0]];
+   }
+ }

Committable suggestion skipped: line range outside the PR's diff.


// Initialize provider and signer
const provider = new JsonRpcProvider(validatedOptions.rpc);
const signer = new Wallet(validatedOptions.privateKey, provider);

console.log("Creating Uniswap V3 pool...");
console.log("Signer address:", await signer.getAddress());
console.log(
"Balance:",
ethers.formatEther(await provider.getBalance(await signer.getAddress())),
"ZETA"
);

// Initialize factory contract
const uniswapV3FactoryInstance = new Contract(
validatedOptions.factory,
UniswapV3Factory.abi,
signer
);

// Create the pool
console.log("\nCreating pool...");
const fee = validatedOptions.fee;
const createPoolTx = (await uniswapV3FactoryInstance.createPool(
validatedOptions.tokens[0],
validatedOptions.tokens[1],
fee
)) as ethers.TransactionResponse;
console.log("Pool creation transaction hash:", createPoolTx.hash);
await createPoolTx.wait();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Check if pool already exists before attempting creation

The code directly attempts to create a pool without checking if it already exists, which will fail if the pool has been previously created. Adding this check would improve user experience and prevent unnecessary transactions.

// Create the pool
console.log("\nCreating pool...");
const fee = validatedOptions.fee;
+ 
+ // Check if pool already exists
+ const existingPoolAddress = await uniswapV3FactoryInstance.getPool(
+   validatedOptions.tokens[0],
+   validatedOptions.tokens[1],
+   fee
+ );
+ 
+ if (existingPoolAddress !== "0x0000000000000000000000000000000000000000") {
+   console.log("Pool already exists at:", existingPoolAddress);
+   return existingPoolAddress;
+ }
+ 
const createPoolTx = (await uniswapV3FactoryInstance.createPool(
  validatedOptions.tokens[0],
  validatedOptions.tokens[1],
  fee
)) as ethers.TransactionResponse;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create the pool
console.log("\nCreating pool...");
const fee = validatedOptions.fee;
const createPoolTx = (await uniswapV3FactoryInstance.createPool(
validatedOptions.tokens[0],
validatedOptions.tokens[1],
fee
)) as ethers.TransactionResponse;
console.log("Pool creation transaction hash:", createPoolTx.hash);
await createPoolTx.wait();
// Create the pool
console.log("\nCreating pool...");
const fee = validatedOptions.fee;
// Check if pool already exists
const existingPoolAddress = await uniswapV3FactoryInstance.getPool(
validatedOptions.tokens[0],
validatedOptions.tokens[1],
fee
);
if (existingPoolAddress !== "0x0000000000000000000000000000000000000000") {
console.log("Pool already exists at:", existingPoolAddress);
return existingPoolAddress;
}
const createPoolTx = (await uniswapV3FactoryInstance.createPool(
validatedOptions.tokens[0],
validatedOptions.tokens[1],
fee
)) as ethers.TransactionResponse;
console.log("Pool creation transaction hash:", createPoolTx.hash);
await createPoolTx.wait();

// Get the pool address
const poolAddress = (await uniswapV3FactoryInstance.getPool(
validatedOptions.tokens[0],
validatedOptions.tokens[1],
fee
)) as string;
console.log("Pool deployed at:", poolAddress);

// Initialize the pool
const pool = new Contract(poolAddress, UniswapV3Pool.abi, signer);
const sqrtPriceX96 = ethers.toBigInt("79228162514264337593543950336"); // sqrt(1) * 2^96
const initTx = (await pool.initialize(
sqrtPriceX96
)) as ethers.TransactionResponse;
console.log("Pool initialization transaction hash:", initTx.hash);
await initTx.wait();

console.log("\nPool created and initialized successfully!");
console.log("Pool address:", poolAddress);
} catch (error) {
const poolError = error as PoolCreationError;
console.error("\nPool creation failed with error:");
console.error("Error message:", poolError.message);
if (poolError.receipt) {
console.error("Transaction receipt:", poolError.receipt);
}
if (poolError.transaction) {
console.error("Transaction details:", poolError.transaction);
}
process.exit(1);
}
};

export const createCommand = new Command("create")
.description("Create a new Uniswap V3 pool")
.requiredOption(
"--private-key <privateKey>",
"Private key for signing transactions"
)
.option("--rpc <rpc>", "RPC URL for the network", DEFAULT_RPC)
.option(
"--factory <factory>",
"Uniswap V3 Factory contract address",
DEFAULT_FACTORY
)
.requiredOption(
"--tokens <tokens...>",
"Token addresses for the pool (exactly 2 required)"
)
.option(
"--fee <fee>",
"Fee tier for the pool (3000 = 0.3%)",
DEFAULT_FEE.toString()
)
.action(main);
187 changes: 187 additions & 0 deletions packages/commands/src/pools/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import * as UniswapV3Factory from "@uniswap/v3-core/artifacts/contracts/UniswapV3Factory.sol/UniswapV3Factory.json";
import * as NonfungiblePositionManager from "@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json";
import * as SwapRouter from "@uniswap/v3-periphery/artifacts/contracts/SwapRouter.sol/SwapRouter.json";
import { Command } from "commander";
import { ContractFactory, ethers, JsonRpcProvider, Wallet } from "ethers";

import {
DeploymentError,
type DeployOptions,
deployOptionsSchema,
} from "../../../../types/pools";
import { DEFAULT_RPC, DEFAULT_WZETA } from "./constants";

const deployOpts = {
gasLimit: 8000000,
};

const estimateGas = async (
contractFactory: ContractFactory,
args: unknown[] = []
): Promise<bigint | null> => {
try {
const deployment = await contractFactory.getDeployTransaction(...args);
const gasEstimate = await contractFactory.runner?.provider?.estimateGas(
deployment
);
console.log("Estimated gas:", gasEstimate?.toString());
return gasEstimate ?? null;
} catch (error) {
console.error("Gas estimation failed:", error);
return null;
}
};

const main = async (options: DeployOptions): Promise<void> => {
try {
// Validate options
const validatedOptions = deployOptionsSchema.parse(options);

// Initialize provider and signer
const provider = new JsonRpcProvider(validatedOptions.rpc);
const signer = new Wallet(validatedOptions.privateKey, provider);

console.log("Deploying Uniswap V3 contracts...");
console.log("Deployer address:", await signer.getAddress());
console.log("Network:", (await provider.getNetwork()).name);
console.log(
"Balance:",
ethers.formatEther(await provider.getBalance(await signer.getAddress())),
"ZETA"
);

// Deploy Uniswap V3 Factory
console.log("\nDeploying Uniswap V3 Factory...");
const uniswapV3Factory = new ContractFactory(
UniswapV3Factory.abi,
UniswapV3Factory.bytecode,
signer
);

// Estimate gas for factory deployment
const factoryGasEstimate = await estimateGas(uniswapV3Factory);
if (factoryGasEstimate) {
deployOpts.gasLimit = Number(factoryGasEstimate * 2n);
}

console.log("Using gas limit:", deployOpts.gasLimit.toString());

const uniswapV3FactoryInstance = await uniswapV3Factory.deploy(deployOpts);
console.log(
"Factory deployment transaction hash:",
uniswapV3FactoryInstance.deploymentTransaction()?.hash
);

await uniswapV3FactoryInstance.waitForDeployment();
console.log(
"Uniswap V3 Factory deployed at:",
await uniswapV3FactoryInstance.getAddress()
);

// Deploy Swap Router
console.log("\nDeploying Swap Router...");
const swapRouter = new ContractFactory(
SwapRouter.abi,
SwapRouter.bytecode,
signer
);

// Estimate gas for router deployment
const routerGasEstimate = await estimateGas(swapRouter, [
await uniswapV3FactoryInstance.getAddress(),
validatedOptions.wzeta,
]);
if (routerGasEstimate) {
deployOpts.gasLimit = Number(routerGasEstimate * 2n);
}

console.log("Using gas limit:", deployOpts.gasLimit.toString());

const swapRouterInstance = await swapRouter.deploy(
await uniswapV3FactoryInstance.getAddress(),
validatedOptions.wzeta,
deployOpts
);
console.log(
"Router deployment transaction hash:",
swapRouterInstance.deploymentTransaction()?.hash
);

await swapRouterInstance.waitForDeployment();
console.log(
"Swap Router deployed at:",
await swapRouterInstance.getAddress()
);

// Deploy Nonfungible Position Manager
console.log("\nDeploying Nonfungible Position Manager...");
const nonfungiblePositionManager = new ContractFactory(
NonfungiblePositionManager.abi,
NonfungiblePositionManager.bytecode,
signer
);

// Estimate gas for position manager deployment
const positionManagerGasEstimate = await estimateGas(
nonfungiblePositionManager,
[
await uniswapV3FactoryInstance.getAddress(),
validatedOptions.wzeta,
await swapRouterInstance.getAddress(),
]
);
if (positionManagerGasEstimate) {
deployOpts.gasLimit = Number(positionManagerGasEstimate * 2n);
}

console.log("Using gas limit:", deployOpts.gasLimit.toString());

const nonfungiblePositionManagerInstance =
await nonfungiblePositionManager.deploy(
await uniswapV3FactoryInstance.getAddress(),
validatedOptions.wzeta,
await swapRouterInstance.getAddress(),
deployOpts
);
console.log(
"Position Manager deployment transaction hash:",
nonfungiblePositionManagerInstance.deploymentTransaction()?.hash
);

await nonfungiblePositionManagerInstance.waitForDeployment();
console.log(
"Nonfungible Position Manager deployed at:",
await nonfungiblePositionManagerInstance.getAddress()
);

console.log("\nDeployment completed successfully!");
console.log("\nContract addresses:");
console.log(
"Uniswap V3 Factory:",
await uniswapV3FactoryInstance.getAddress()
);
console.log("Swap Router:", await swapRouterInstance.getAddress());
console.log(
"Nonfungible Position Manager:",
await nonfungiblePositionManagerInstance.getAddress()
);
} catch (error) {
const deploymentError = error as DeploymentError;
console.error("\nDeployment failed with error:");
console.error("Error message:", deploymentError.message);
if (deploymentError.receipt) {
console.error("Transaction receipt:", deploymentError.receipt);
}
if (deploymentError.transaction) {
console.error("Transaction details:", deploymentError.transaction);
}
process.exit(1);
}
};

export const deployCommand = new Command("deploy")
.description("Deploy Uniswap V3 contracts")
.requiredOption("--private-key <privateKey>", "Private key for deployment")
.option("--rpc <rpc>", "RPC URL for the network", DEFAULT_RPC)
.option("--wzeta <wzeta>", "WZETA token address", DEFAULT_WZETA)
.action(main);
13 changes: 13 additions & 0 deletions packages/commands/src/pools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Command } from "commander";

import { createCommand } from "./create";
import { deployCommand } from "./deploy";
import { liquidityCommand } from "./liquidity";
import { showCommand } from "./show";

export const poolsCommand = new Command("pools")
.description("Manage Uniswap V3 pools")
.addCommand(deployCommand)
.addCommand(createCommand)
.addCommand(showCommand)
.addCommand(liquidityCommand);
Loading