diff --git a/hardhat.config.ts b/hardhat.config.ts index 51ecf5e..adb32e9 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -19,6 +19,9 @@ import "./tasks/core/pair/swap"; import "./tasks/core/factory/get-pair"; import "./tasks/core/factory/create-pair"; +// Demo Tasks +import "./tasks/core/demo"; + dotenv.config(); const config: NilHardhatUserConfig = { diff --git a/tasks/core/demo.ts b/tasks/core/demo.ts new file mode 100644 index 0000000..6ce738a --- /dev/null +++ b/tasks/core/demo.ts @@ -0,0 +1,383 @@ +import { shardNumber } from "@nilfoundation/hardhat-plugin/dist/utils/conversion"; +import { waitTillCompleted } from "@nilfoundation/niljs"; +import { task } from "hardhat/config"; +import type { + Currency, + UniswapV2Factory, + UniswapV2Pair, +} from "../../typechain-types"; +import { createClient } from "../util/client"; +import { + faucetWithdrawal, + mintAndSendCurrency, + sleep, +} from "../util/currencyUtils"; +import { deployNilContract } from "../util/deploy"; +import { calculateOutputAmount } from "../util/math"; + +task("demo", "Run demo for Uniswap Pairs and Factory").setAction( + async (taskArgs, hre) => { + const walletAddress = process.env.WALLET_ADDR; + if (!walletAddress) { + throw new Error("WALLET_ADDR is not set in environment variables"); + } + + const faucetAddress = process.env.FAUCET_ADDR; + + const shardId = 1; + const mintAmount = 100000; + const mintCurrency0Amount = 10000; + const mintCurrency1Amount = 10000; + const swapAmount = 1000; + + const { wallet, publicClient, signer } = await createClient(); + + const { + deployedContract: factoryContract, + contractAddress: factoryAddress, + } = await deployNilContract(hre, "UniswapV2Factory", [walletAddress]); + const { + deployedContract: Currency0Contract, + contractAddress: currency0Address, + } = await deployNilContract(hre, "Currency", [ + "currency0", + await signer.getPublicKey(), + ]); + const { + deployedContract: Currency1Contract, + contractAddress: currency1Address, + } = await deployNilContract(hre, "Currency", [ + "currency1", + await signer.getPublicKey(), + ]); + + console.log("Factory deployed " + factoryAddress); + console.log("Currency0 deployed " + currency0Address); + console.log("Currency1 deployed " + currency1Address); + + const factory = factoryContract as UniswapV2Factory; + + // 1. CREATE PAIR + await factory.createPair( + currency0Address.toLowerCase(), + currency1Address.toLowerCase(), + Math.floor(Math.random() * 10000000), + shardId, + ); + + const pairAddress = await factory.getTokenPair( + currency0Address.toLowerCase(), + currency1Address.toLowerCase(), + ); + + // Log the pair address + console.log(`Pair created successfully at address: ${pairAddress}`); + + // Attach to the Currency contract for both currencies + + const firstCurrency = Currency0Contract as Currency; + const firstCurrencyId = await firstCurrency.getCurrencyId(); + console.log(`First currency ID: ${firstCurrencyId}`); + + const secondCurrency = Currency1Contract as Currency; + const secondCurrencyId = await secondCurrency.getCurrencyId(); + console.log(`Second currency ID: ${secondCurrencyId}`); + + // Attach to the newly created Uniswap V2 Pair contract + const pairContract = await hre.ethers.getContractFactory("UniswapV2Pair"); + const pair = pairContract.attach(pairAddress) as UniswapV2Pair; + + // Initialize the pair with currency addresses and IDs + await pair.initialize( + currency0Address.toLowerCase(), + currency1Address.toLowerCase(), + firstCurrencyId, + secondCurrencyId, + ); + + console.log(`Pair initialized successfully at address: ${pairAddress}`); + + // Prepare currencies + await faucetWithdrawal( + currency0Address.toLowerCase(), + 100000000000n, + faucetAddress, + hre, + publicClient, + ); + + await sleep(2000); + + await faucetWithdrawal( + currency1Address.toLowerCase(), + 100000000000n, + faucetAddress, + hre, + publicClient, + ); + + await sleep(2000); + + // 2. MINT CURRENCIES + console.log( + `Minting ${mintAmount} Currency0 to wallet ${walletAddress}...`, + ); + await mintAndSendCurrency({ + publicClient, + signer, + currencyContract: firstCurrency, + contractAddress: currency0Address.toLowerCase(), + walletAddress, + mintAmount, + hre, + }); + + // Mint and send Currency1 + console.log( + `Minting ${mintAmount} Currency1 to wallet ${walletAddress}...`, + ); + await mintAndSendCurrency({ + publicClient, + signer, + currencyContract: secondCurrency, + contractAddress: currency1Address.toLowerCase(), + walletAddress, + mintAmount, + hre, + }); + + // Verify the balance of the recipient wallet for both currencies + const recipientBalanceCurrency0 = + await firstCurrency.getCurrencyBalanceOf(walletAddress); + const recipientBalanceCurrency1 = + await secondCurrency.getCurrencyBalanceOf(walletAddress); + + console.log( + `Recipient balance after transfer - Currency0: ${recipientBalanceCurrency0}, Currency1: ${recipientBalanceCurrency1}`, + ); + + // 3. PAIR: MINT + + // Send currency amounts to the pair contract + console.log( + `Sending ${mintCurrency0Amount} currency0 and ${mintCurrency1Amount} currency1 to ${pairAddress}...`, + ); + const hash = await wallet.sendMessage({ + // @ts-ignore + to: pairAddress, + feeCredit: BigInt(10_000_000), + value: BigInt(0), + refundTo: wallet.address, + tokens: [ + { + id: await firstCurrency.getCurrencyId(), + amount: BigInt(mintCurrency0Amount), + }, + { + id: await secondCurrency.getCurrencyId(), + amount: BigInt(mintCurrency1Amount), + }, + ], + }); + + await waitTillCompleted(publicClient, shardNumber(walletAddress), hash); + + // Log balances in the pair contract + const pairCurrency0Balance = + await firstCurrency.getCurrencyBalanceOf(pairAddress); + console.log("Pair Balance of Currency0:", pairCurrency0Balance.toString()); + + const pairCurrency1Balance = + await secondCurrency.getCurrencyBalanceOf(pairAddress); + console.log("Pair Balance of Currency1:", pairCurrency1Balance.toString()); + + // Mint liquidity + console.log("Minting pair tokens..."); + await pair.mint(walletAddress); + console.log("Liquidity added..."); + + // Retrieve and log reserves from the pair + const [reserve0, reserve1] = await pair.getReserves(); + console.log( + `MINT RESULT: Reserves - Currency0: ${reserve0.toString()}, Currency1: ${reserve1.toString()}`, + ); + + // Check and log liquidity provider balance + const lpBalance = await pair.getCurrencyBalanceOf(walletAddress); + console.log( + "MINT RESULT: Liquidity provider balance in wallet:", + lpBalance.toString(), + ); + + // Retrieve and log total supply for the pair + const totalSupply = await pair.getCurrencyTotalSupply(); + console.log( + "MINT RESULT: Total supply of pair tokens:", + totalSupply.toString(), + ); + + // 4. PAIR: SWAP + const expectedOutputAmount = calculateOutputAmount( + BigInt(swapAmount), + reserve0, + reserve1, + ); + console.log( + "Expected output amount for swap:", + expectedOutputAmount.toString(), + ); + + // Log balances before the swap + const balanceCurrency0Before = + await firstCurrency.getCurrencyBalanceOf(walletAddress); + const balanceCurrency1Before = + await secondCurrency.getCurrencyBalanceOf(walletAddress); + console.log( + "Balance of currency0 before swap:", + balanceCurrency0Before.toString(), + ); + console.log( + "Balance of currency1 before swap:", + balanceCurrency1Before.toString(), + ); + + // Attach to the UserWallet contract + + // Send currency0 to the pair contract + const hash2 = await wallet.sendMessage({ + // @ts-ignore + to: pairAddress, + feeCredit: BigInt(10_000_000), + value: BigInt(0), + refundTo: wallet.address, + tokens: [ + { + id: await firstCurrency.getCurrencyId(), + amount: BigInt(swapAmount), + }, + ], + }); + + await waitTillCompleted(publicClient, shardNumber(walletAddress), hash2); + + console.log( + `Sent ${swapAmount.toString()} of currency0 to the pair contract.`, + ); + + // Execute the swap + console.log("Executing swap..."); + await pair.swap(0, expectedOutputAmount, walletAddress); + console.log("Swap executed successfully."); + + // Log balances after the swap + const balanceCurrency0After = + await firstCurrency.getCurrencyBalanceOf(walletAddress); + const balanceCurrency1After = + await secondCurrency.getCurrencyBalanceOf(walletAddress); + console.log( + "SWAP RESULT: Balance of currency0 after swap:", + balanceCurrency0After.toString(), + ); + console.log( + "SWAP RESULT: Balance of currency1 after swap:", + balanceCurrency1After.toString(), + ); + + // 5. PAIR: BURN + const total = await pair.getCurrencyTotalSupply(); + console.log("Total supply:", total.toString()); + + // Fetch and log pair balances before burn + const pairBalanceToken0 = await firstCurrency.getCurrencyBalanceOf( + pairAddress.toLowerCase(), + ); + const pairBalanceToken1 = await secondCurrency.getCurrencyBalanceOf( + pairAddress.toLowerCase(), + ); + console.log( + "Pair Balance token0 before burn:", + pairBalanceToken0.toString(), + ); + console.log( + "Pair Balance token1 before burn:", + pairBalanceToken1.toString(), + ); + + // Fetch and log user balances before burn + let userBalanceToken0 = + await firstCurrency.getCurrencyBalanceOf(walletAddress); + let userBalanceToken1 = + await secondCurrency.getCurrencyBalanceOf(walletAddress); + console.log( + "User Balance token0 before burn:", + userBalanceToken0.toString(), + ); + console.log( + "User Balance token1 before burn:", + userBalanceToken1.toString(), + ); + + const lpAddress = await pair.getCurrencyId(); + const userLpBalance = await pair.getCurrencyBalanceOf(walletAddress); + console.log("Total LP balance for user wallet:", userLpBalance.toString()); + + // Send LP tokens to the user wallet + const hash3 = await wallet.sendMessage({ + // @ts-ignore + to: pairAddress, + feeCredit: BigInt(10_000_000), + value: BigInt(0), + refundTo: walletAddress, + tokens: [ + { + id: lpAddress, + amount: BigInt(userLpBalance), + }, + ], + }); + + await waitTillCompleted(publicClient, shardNumber(walletAddress), hash3); + + // Execute burn + console.log("Executing burn..."); + await pair.burn(walletAddress); + console.log("Burn executed."); + console.log("Built tokens"); + + // Log balances after burn + const balanceToken0 = await firstCurrency.getCurrencyBalanceOf( + pairAddress.toLowerCase(), + ); + const balanceToken1 = await secondCurrency.getCurrencyBalanceOf( + pairAddress.toLowerCase(), + ); + console.log( + "BURN RESULT: Pair Balance token0 after burn:", + balanceToken0.toString(), + ); + console.log( + "BURN RESULT: Pair Balance token1 after burn:", + balanceToken1.toString(), + ); + + userBalanceToken0 = await firstCurrency.getCurrencyBalanceOf(walletAddress); + userBalanceToken1 = + await secondCurrency.getCurrencyBalanceOf(walletAddress); + console.log( + "BURN RESULT: User Balance token0 after burn:", + userBalanceToken0.toString(), + ); + console.log( + "BURN RESULT: User Balance token1 after burn:", + userBalanceToken1.toString(), + ); + + // Fetch and log reserves after burn + const reserves = await pair.getReserves(); + console.log( + "BURN RESULT: Reserves from pair after burn:", + reserves[0].toString(), + reserves[1].toString(), + ); + }, +); diff --git a/tasks/router/router_1.ts b/tasks/router/router_1.ts deleted file mode 100644 index 9a7eb65..0000000 --- a/tasks/router/router_1.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - Faucet, - HttpTransport, - LocalECDSAKeySigner, - PublicClient, - WalletV1, - generateRandomPrivateKey, -} from "@nilfoundation/niljs"; -import { task } from "hardhat/config"; -import { encodeFunctionData } from "viem"; -import { - Token, - UniswapV2Factory, - UniswapV2Pair, - UniswapV2Router01, -} from "../../typechain-types"; - -task("router_1", "Router: init and add liquidity") - .addParam("token0") - .addParam("token1") - .addParam("router") - .addParam("factory") - .setAction(async (taskArgs, hre) => { - const walletAddress = process.env.WALLET_ADDR; - - if (!walletAddress) { - throw new Error("WALLET_ADDR is not set in environment variables"); - } - - const client = new PublicClient({ - transport: new HttpTransport({ - endpoint: "http://127.0.0.1:8529", - }), - shardId: 1, - }); - - const faucet = new Faucet(client); - - const signer = new LocalECDSAKeySigner({ - privateKey: `0x${process.env.PRIVATE_KEY}`, - }); - - const pubkey = await signer.getPublicKey(); - - const wallet = new WalletV1({ - pubkey: pubkey, - salt: BigInt(Math.round(Math.random() * 10000)), - shardId: 1, - client, - signer, - }); - - const token0Address = taskArgs.token0.toLowerCase(); - const token1Address = taskArgs.token1.toLowerCase(); - const factoryAddress = taskArgs.factory.toLowerCase(); - const routerAddress = taskArgs.router.toLowerCase(); - - const UniswapV2Router = - await hre.ethers.getContractFactory("UniswapV2Router01"); - const router = UniswapV2Router.attach(factoryAddress) as UniswapV2Router01; - - wallet.sendMessage({ - to: walletAddress, - feeCredit: 1_000_000n * 10n, - value: 0n, - data: encodeFunctionData({ - abi: UniswapV2Router01.abi, - functionName: "setCurrencyName", - args: ["MY_TOKEN"], - }), - }); - - // TODO - }); diff --git a/tasks/util/deploy.ts b/tasks/util/deploy.ts new file mode 100644 index 0000000..7e78e15 --- /dev/null +++ b/tasks/util/deploy.ts @@ -0,0 +1,23 @@ +import assert from "node:assert"; +import type { HardhatRuntimeEnvironment } from "hardhat/types"; + +export async function deployNilContract( + hre: HardhatRuntimeEnvironment, + name: string, + args: unknown[] = [], +) { + const factory = await hre.ethers.getContractFactory(name); + assert.ok(factory.runner); + assert.ok(factory.runner.sendTransaction); + + const deployTx = await factory.getDeployTransaction(...args); + const sentTx = await factory.runner.sendTransaction(deployTx); + const txReceipt = await sentTx.wait(); + + if (!txReceipt || !txReceipt.contractAddress) { + throw new Error("Contract deployment failed"); + } + + const deployedContract = factory.attach(txReceipt.contractAddress); + return { deployedContract, contractAddress: txReceipt.contractAddress }; +} diff --git a/tasks/util/math.ts b/tasks/util/math.ts new file mode 100644 index 0000000..055d7b9 --- /dev/null +++ b/tasks/util/math.ts @@ -0,0 +1,10 @@ +export function calculateOutputAmount( + amountIn: bigint, + reserveIn: bigint, + reserveOut: bigint, +): bigint { + const amountInWithFee = amountIn * BigInt(997); + const numerator = amountInWithFee * reserveOut; + const denominator = reserveIn * BigInt(1000) + amountInWithFee; + return numerator / denominator; +}