From 2f04b465c771587ee085c492d28a5df9abd545b0 Mon Sep 17 00:00:00 2001 From: rolaman Date: Wed, 2 Oct 2024 17:48:37 +0300 Subject: [PATCH 1/7] demo-router: contract + task --- contracts/UniswapV2Pair.sol | 7 + contracts/UniswapV2Router01.sol | 149 ++++++++++++++------ contracts/interfaces/IUniswapV2Pair.sol | 2 + contracts/interfaces/IUniswapV2Router01.sol | 31 ++-- contracts/libraries/UniswapV2Library.sol | 38 +---- 5 files changed, 137 insertions(+), 90 deletions(-) diff --git a/contracts/UniswapV2Pair.sol b/contracts/UniswapV2Pair.sol index 0c4f15c..9c7e6fe 100644 --- a/contracts/UniswapV2Pair.sol +++ b/contracts/UniswapV2Pair.sol @@ -235,5 +235,12 @@ contract UniswapV2Pair is NilCurrencyBase, IUniswapV2Pair { ); } + function token0Id() external view returns (uint256) { + return tokenId0; + } + function token1Id() external view returns (uint256) { + return tokenId1; + } + receive() external payable {} } diff --git a/contracts/UniswapV2Router01.sol b/contracts/UniswapV2Router01.sol index 3b1bf69..6b6dff6 100644 --- a/contracts/UniswapV2Router01.sol +++ b/contracts/UniswapV2Router01.sol @@ -19,6 +19,11 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { factory = _factory; } + modifier sameShard(address _addr) { + require(Nil.getShardId(_addr) == Nil.getShardId(address(this)), "Sync calls require same shard for all contracts"); + _; + } + function addLiquidity( address pair, address to @@ -30,6 +35,67 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { smartCall(pair, tokens, abi.encodeWithSignature("mint(address)", to)); } + function addLiquiditySync( + address pair, + address to, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin + ) public override sameShard(pair) returns (uint amountA, uint amountB) { + Nil.Token[] memory tokens = Nil.msgTokens(); + if (tokens.length != 2) { + revert("Send only 2 tokens to add liquidity"); + } + (amountA, amountB) = _addLiquiditySync(pair, tokens[0].id, tokens[1].id, amountADesired, amountBDesired, amountAMin, amountBMin); + + if (amountA < tokens[0].amount) { + Nil.Token[] memory tokenAReturns = new Nil.Token[](1); + tokenAReturns[0].id = tokens[0].id; + tokenAReturns[0].amount = tokens[0].amount - amountA; + smartCall(to, tokenAReturns, ""); + } + if (amountB < tokens[1].amount) { + Nil.Token[] memory tokenBReturns = new Nil.Token[](1); + tokenBReturns[0].id = tokens[1].id; + tokenBReturns[0].amount = tokens[1].amount - amountB; + smartCall(to, tokenBReturns, ""); + } + + Nil.Token[] memory tokensToSend = new Nil.Token[](2); + tokensToSend[0].id = tokens[0].id; + tokensToSend[0].amount = amountA; + tokensToSend[1].id = tokens[1].id; + tokensToSend[1].amount = amountA; + smartCall(pair, tokensToSend, abi.encodeWithSignature("mint(address)", to)); + } + + function _addLiquiditySync( + address pair, + uint256 tokenA, + uint256 tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin + ) private returns (uint amountA, uint amountB) { + (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(pair, tokenA, tokenB); + if (reserveA == 0 && reserveB == 0) { + (amountA, amountB) = (amountADesired, amountBDesired); + } else { + uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); + if (amountBOptimal <= amountBDesired) { + require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + (amountA, amountB) = (amountADesired, amountBOptimal); + } else { + uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); + assert(amountAOptimal <= amountADesired); + require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); + (amountA, amountB) = (amountAOptimal, amountBDesired); + } + } + } + // **** REMOVE LIQUIDITY **** function removeLiquidity( address pair, @@ -42,59 +108,50 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { smartCall(pair, tokens, abi.encodeWithSignature("burn(address)", to)); } - function swap(address to, address pair, uint amount0Out, uint amount1Out) public override { + function removeLiquiditySync( + address pair, + address to, + uint amountAMin, + uint amountBMin + ) public override sameShard(pair) returns (uint amountA, uint amountB) { Nil.Token[] memory tokens = Nil.msgTokens(); if (tokens.length != 1) { revert("UniswapV2Router: should contains only pair token"); } - smartCall(pair, tokens, abi.encodeWithSignature("swap(uint256,uint256,address)", amount0Out, amount1Out, to)); - } - - // TODO: This method are used for swapping via multiple pairs. Not supported in nil for now - // **** SWAP **** - // requires the initial amount to have already been sent to the first pair - function _swap(uint[] memory amounts, address[] memory path, address _to) private { - for (uint i; i < path.length - 1; i++) { - (address input, address output) = (path[i], path[i + 1]); - (address token0,) = UniswapV2Library.sortTokens(input, output); - uint amountOut = amounts[i + 1]; - (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0)); - address to = i < path.length - 2 ? IUniswapV2Factory(factory).getTokenPair(output, path[i + 2]) : _to; - address pair = IUniswapV2Factory(factory).getTokenPair(input, output); - IUniswapV2Pair(pair).swap(amount0Out, amount1Out, to); + (bool success, bytes memory result) = smartCall(pair, tokens, abi.encodeWithSignature("burn(address)", to)); + if (success) { + (amountA, amountB) = abi.decode(result, (uint256, uint256)); + } else { + revert("Burn call is not successful"); } } - // TODO: This method are used for swapping via multiple pairs. Not supported in nil for now - function swapExactTokensForTokens( - uint amountIn, - uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) external override returns (uint[] memory amounts) { - amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path); - require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); - address pair = IUniswapV2Factory(factory).getTokenPair(path[0], path[1]); + function swap(address to, address pair, uint amount0Out, uint amount1Out) public override { Nil.Token[] memory tokens = Nil.msgTokens(); - sendCurrencyInternal(pair, tokens[0].id, amounts[0]); - _swap(amounts, path, to); + if (tokens.length != 1) { + revert("UniswapV2Router: should contains only pair token"); + } + smartCall(pair, tokens, abi.encodeWithSignature("swap(uint256,uint256,address)", amount0Out, amount1Out, to)); } - // TODO: This method are used for swapping via multiple pairs. Not supported in nil for now - function swapTokensForExactTokens( - uint amountOut, - uint amountInMax, - address[] calldata path, - address to, - uint deadline - ) external override returns (uint[] memory amounts) { - amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path); - require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT'); - address pair = IUniswapV2Factory(factory).getTokenPair(path[0], path[1]); + function swapExactTokenForTokenSync( + address pair, + uint amountOutMin, + address to + ) external override sameShard(pair) returns (uint amount) { Nil.Token[] memory tokens = Nil.msgTokens(); - sendCurrencyInternal(pair, tokens[0].id, amounts[0]); - _swap(amounts, path, to); + if (tokens.length != 1) { + revert("UniswapV2Router: should contains only pair token"); + } + uint256 token0Id = IUniswapV2Pair(pair).token0Id(); + uint256 token1Id = IUniswapV2Pair(pair).token1Id(); + uint256 tokenBId = tokens[0].id != token0Id ? token0Id : token1Id; + (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(pair, tokens[0].id, tokenBId); + amount = UniswapV2Library.getAmountOut(tokens[0].amount, reserveA, reserveB); + require(amount >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); + uint amount0Out = tokens[0].id == token0Id ? tokens[0].amount : amount; + uint amount1Out = tokens[0].id != token0Id ? tokens[0].amount : amount; + smartCall(pair, tokens, abi.encodeWithSignature("swap(uint256,uint256,address)", amount0Out, amount1Out, to)); } function quote(uint amountA, uint reserveA, uint reserveB) public pure override returns (uint amountB) { @@ -112,13 +169,13 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { receive() external payable { } - function smartCall(address dst, Nil.Token[] memory tokens, bytes memory callData) private returns (bool) { + function smartCall(address dst, Nil.Token[] memory tokens, bytes memory callData) private returns (bool, bytes memory) { if (Nil.getShardId(dst) == Nil.getShardId(address(this))) { - (bool success,) = Nil.syncCall(dst, gasleft(), 0, tokens, callData); - return success; + (bool success, bytes memory result) = Nil.syncCall(dst, gasleft(), 0, tokens, callData); + return (success, result); } else { Nil.asyncCall(dst, address(0), address(0), 0, Nil.FORWARD_REMAINING, false, 0, tokens, callData); - return true; + return (true, ""); } } } \ No newline at end of file diff --git a/contracts/interfaces/IUniswapV2Pair.sol b/contracts/interfaces/IUniswapV2Pair.sol index c761bcb..db2e628 100644 --- a/contracts/interfaces/IUniswapV2Pair.sol +++ b/contracts/interfaces/IUniswapV2Pair.sol @@ -21,6 +21,8 @@ interface IUniswapV2Pair { function factory() external view returns (address); function token0() external view returns (address); function token1() external view returns (address); + function token0Id() external view returns (uint256); + function token1Id() external view returns (uint256); function getReserves() external view diff --git a/contracts/interfaces/IUniswapV2Router01.sol b/contracts/interfaces/IUniswapV2Router01.sol index 87e8def..d5179b9 100644 --- a/contracts/interfaces/IUniswapV2Router01.sol +++ b/contracts/interfaces/IUniswapV2Router01.sol @@ -8,10 +8,24 @@ interface IUniswapV2Router01 { address pair, address to ) external; + function addLiquiditySync( + address pair, + address to, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin + ) external returns (uint amountA, uint amountB); function removeLiquidity( address pair, address to ) external; + function removeLiquiditySync( + address pair, + address to, + uint amountAMin, + uint amountBMin + ) external returns (uint amountA, uint amountB); function swap( address to, address pair, @@ -19,20 +33,11 @@ interface IUniswapV2Router01 { uint amount1Out ) external; - function swapExactTokensForTokens( - uint amountIn, + function swapExactTokenForTokenSync( + address pair, uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) external returns (uint[] memory amounts); - function swapTokensForExactTokens( - uint amountOut, - uint amountInMax, - address[] calldata path, - address to, - uint deadline - ) external returns (uint[] memory amounts); + address to + ) external returns (uint amount); function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); diff --git a/contracts/libraries/UniswapV2Library.sol b/contracts/libraries/UniswapV2Library.sol index 4fcc1cf..be7a745 100644 --- a/contracts/libraries/UniswapV2Library.sol +++ b/contracts/libraries/UniswapV2Library.sol @@ -3,24 +3,22 @@ pragma solidity ^0.8.0; import "./SafeMath.sol"; import "../interfaces/IUniswapV2Pair.sol"; -import "../interfaces/IUniswapV2Factory.sol"; library UniswapV2Library { using SafeMath for uint; // returns sorted token addresses, used to handle return values from pairs sorted in this order - function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { - require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES'); - (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); - require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS'); + function sortTokens(uint256 tokenAId, uint256 tokenBId) internal pure returns (uint256 token0, uint256 token1) { + require(tokenAId != tokenBId, 'UniswapV2Library: IDENTICAL_ADDRESSES'); + (token0, token1) = tokenAId < tokenBId ? (tokenAId, tokenBId) : (tokenBId, tokenAId); + require(token0 != uint256(0), 'UniswapV2Library: ZERO_ADDRESS'); } // fetches and sorts the reserves for a pair - function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) { - (address token0,) = sortTokens(tokenA, tokenB); - address pair = IUniswapV2Factory(factory).getTokenPair(tokenA, tokenB); + function getReserves(address pair, uint256 tokenAId, uint256 tokenBId) internal view returns (uint reserveA, uint reserveB) { + (uint token0,) = sortTokens(tokenAId, tokenBId); (uint reserve0, uint reserve1) = IUniswapV2Pair(pair).getReserves(); - (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0); + (reserveA, reserveB) = tokenAId == token0 ? (reserve0, reserve1) : (reserve1, reserve0); } // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset @@ -48,26 +46,4 @@ library UniswapV2Library { uint denominator = reserveOut.sub(amountOut).mul(997); amountIn = (numerator / denominator).add(1); } - - // performs chained getAmountOut calculations on any number of pairs - function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) { - require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); - amounts = new uint[](path.length); - amounts[0] = amountIn; - for (uint i; i < path.length - 1; i++) { - (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]); - amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut); - } - } - - // performs chained getAmountIn calculations on any number of pairs - function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) { - require(path.length >= 2, 'UniswapV2Library: INVALID_PATH'); - amounts = new uint[](path.length); - amounts[amounts.length - 1] = amountOut; - for (uint i = path.length - 1; i > 0; i--) { - (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]); - amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut); - } - } } \ No newline at end of file From 47dd844e1a957c44ae0291904a74eba29b1b3e2a Mon Sep 17 00:00:00 2001 From: rolaman Date: Wed, 2 Oct 2024 18:51:01 +0300 Subject: [PATCH 2/7] demo-router: contract + task --- hardhat.config.ts | 1 + tasks/core/demo-router-sync.ts | 402 +++++++++++++++++++++++++++++++++ 2 files changed, 403 insertions(+) create mode 100644 tasks/core/demo-router-sync.ts diff --git a/hardhat.config.ts b/hardhat.config.ts index 335f310..3eb524b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -22,6 +22,7 @@ import "./tasks/core/factory/create-pair"; // Demo Tasks import "./tasks/core/demo"; import "./tasks/core/demo-router"; +import "./tasks/core/demo-router-sync"; dotenv.config(); diff --git a/tasks/core/demo-router-sync.ts b/tasks/core/demo-router-sync.ts new file mode 100644 index 0000000..dd8f4e5 --- /dev/null +++ b/tasks/core/demo-router-sync.ts @@ -0,0 +1,402 @@ +import {shardNumber} from "@nilfoundation/hardhat-plugin/dist/utils/conversion"; +import {waitTillCompleted} from "@nilfoundation/niljs"; +import {task} from "hardhat/config"; +import {encodeFunctionData} from "viem"; +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-router-sync", "Run demo with Uniswap Router with Sync methods").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 {deployedContract: RouterContract, contractAddress: routerAddress} = + await deployNilContract(hre, "UniswapV2Router01", [ + factoryAddress.toLowerCase(), + ]); + + console.log("Router deployed " + routerAddress); + + 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. ROUTER: ADD LIQUIDITY + const pairArtifact = await hre.artifacts.readArtifact("UniswapV2Pair"); + const routerArtifact = + await hre.artifacts.readArtifact("UniswapV2Router01"); + + // Mint liquidity + console.log("Adding liquidity..."); + + const hash = await wallet.sendMessage({ + to: routerAddress, + feeCredit: BigInt(10_000_000), + value: BigInt(0), + refundTo: wallet.address, + data: encodeFunctionData({ + abi: routerArtifact.abi, + functionName: "addLiquiditySync", + args: [pairAddress, walletAddress, mintCurrency0Amount, mintCurrency1Amount, 1, 1], + }), + 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()); + + console.log("Liquidity added..."); + + // Retrieve and log reserves from the pair + const [reserve0, reserve1] = await pair.getReserves(); + console.log( + `ADDLIQUIDITY RESULT: Reserves - Currency0: ${reserve0.toString()}, Currency1: ${reserve1.toString()}`, + ); + + // Check and log liquidity provider balance + const lpBalance = await pair.getCurrencyBalanceOf(walletAddress); + console.log( + "ADDLIQUIDITY RESULT: Liquidity provider balance in wallet:", + lpBalance.toString(), + ); + + // Retrieve and log total supply for the pair + const totalSupply = await pair.getCurrencyTotalSupply(); + console.log( + "ADDLIQUIDITY RESULT: Total supply of pair tokens:", + totalSupply.toString(), + ); + + // 4. ROUTER: 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(), + ); + + // Execute the swap + console.log("Executing swap..."); + + // Send currency0 to the pair contract + const hash2 = await wallet.sendMessage({ + to: routerAddress, + feeCredit: BigInt(10_000_000), + value: BigInt(0), + data: encodeFunctionData({ + abi: routerArtifact.abi, + functionName: "swapExactTokenForTokenSync", + args: [pairAddress, 1, walletAddress], + }), + refundTo: wallet.address, + tokens: [ + { + id: await firstCurrency.getCurrencyId(), + amount: BigInt(swapAmount), + }, + ], + }); + + await waitTillCompleted(publicClient, shardNumber(walletAddress), hash2); + + const hash2Receipt = (await publicClient.getMessageReceiptByHash(hash2))!; + console.log(`hash2: ${hash2}`); + console.log(`hash2Receipt: ${JSON.stringify(hash2Receipt, (key, value) => typeof value === 'bigint' ? value.toString() : value)}`); + + console.log( + `Sent ${swapAmount.toString()} of currency0 to the pair contract. Tx - ${hash2}`, + ); + + 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. ROUTER: REMOVE LIQUIDITY + 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()); + // Execute burn + console.log("Executing burn..."); + // Send LP tokens to the user wallet + const hash3 = await wallet.sendMessage({ + // @ts-ignore + to: routerAddress, + feeCredit: BigInt(10_000_000), + value: BigInt(0), + data: encodeFunctionData({ + abi: routerArtifact.abi, + functionName: "removeLiquiditySync", + args: [pairAddress, walletAddress, 100, 100], + }), + refundTo: walletAddress, + tokens: [ + { + id: lpAddress, + amount: BigInt(userLpBalance), + }, + ], + }); + + await waitTillCompleted(publicClient, shardNumber(walletAddress), hash3); + + console.log("Burn executed."); + + // Log balances after burn + const balanceToken0 = await firstCurrency.getCurrencyBalanceOf( + pairAddress.toLowerCase(), + ); + const balanceToken1 = await secondCurrency.getCurrencyBalanceOf( + pairAddress.toLowerCase(), + ); + console.log( + "REMOVELIQUIDITY RESULT: Pair Balance token0 after burn:", + balanceToken0.toString(), + ); + console.log( + "REMOVELIQUIDITY RESULT: Pair Balance token1 after burn:", + balanceToken1.toString(), + ); + + userBalanceToken0 = await firstCurrency.getCurrencyBalanceOf(walletAddress); + userBalanceToken1 = + await secondCurrency.getCurrencyBalanceOf(walletAddress); + console.log( + "REMOVELIQUIDITY RESULT: User Balance token0 after burn:", + userBalanceToken0.toString(), + ); + console.log( + "REMOVELIQUIDITY RESULT: User Balance token1 after burn:", + userBalanceToken1.toString(), + ); + + // Fetch and log reserves after burn + const reserves = await pair.getReserves(); + console.log( + "REMOVELIQUIDITY RESULT: Reserves from pair after burn:", + reserves[0].toString(), + reserves[1].toString(), + ); + }, +); From 28a724c34bf457be6149f74b4365871164f08f41 Mon Sep 17 00:00:00 2001 From: rolaman Date: Thu, 3 Oct 2024 10:29:13 +0300 Subject: [PATCH 3/7] demo-router: contract + task --- contracts/UniswapV2Router01.sol | 9 ++++++--- hardhat.config.ts | 12 +++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/contracts/UniswapV2Router01.sol b/contracts/UniswapV2Router01.sol index 6b6dff6..00a618b 100644 --- a/contracts/UniswapV2Router01.sol +++ b/contracts/UniswapV2Router01.sol @@ -149,9 +149,12 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(pair, tokens[0].id, tokenBId); amount = UniswapV2Library.getAmountOut(tokens[0].amount, reserveA, reserveB); require(amount >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'); - uint amount0Out = tokens[0].id == token0Id ? tokens[0].amount : amount; - uint amount1Out = tokens[0].id != token0Id ? tokens[0].amount : amount; - smartCall(pair, tokens, abi.encodeWithSignature("swap(uint256,uint256,address)", amount0Out, amount1Out, to)); + uint amount0Out = tokens[0].id == token0Id ? 0 : amount; + uint amount1Out = tokens[0].id != token0Id ? 0 : amount; + (bool success, bytes memory result) = smartCall(pair, tokens, abi.encodeWithSignature("swap(uint256,uint256,address)", amount0Out, amount1Out, to)); + if (!success) { + revert("UniswapV2Router: should get success swap result"); + } } function quote(uint amountA, uint reserveA, uint reserveB) public pure override returns (uint amountB) { diff --git a/hardhat.config.ts b/hardhat.config.ts index 3eb524b..9a7f85e 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -30,7 +30,16 @@ const config: NilHardhatUserConfig = { ignition: { requiredConfirmations: 1, }, - solidity: "0.8.24", + solidity: { + version: "0.8.24", // or your desired version + settings: { + viaIR: true, // needed to compile router + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, networks: { nil: { url: process.env.NIL_RPC_ENDPOINT, @@ -38,6 +47,7 @@ const config: NilHardhatUserConfig = { }, }, walletAddress: process.env.WALLET_ADDR, + }; export default config; From 90e23d48ab0b27a222fc232395a0f5f4271cf666 Mon Sep 17 00:00:00 2001 From: rolaman Date: Thu, 3 Oct 2024 10:33:01 +0300 Subject: [PATCH 4/7] demo-router: contract + task --- hardhat.config.ts | 1 - tasks/core/demo-router-sync.ts | 762 ++++++++++++++++----------------- 2 files changed, 373 insertions(+), 390 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 9a7f85e..e48859b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -47,7 +47,6 @@ const config: NilHardhatUserConfig = { }, }, walletAddress: process.env.WALLET_ADDR, - }; export default config; diff --git a/tasks/core/demo-router-sync.ts b/tasks/core/demo-router-sync.ts index dd8f4e5..be37bc8 100644 --- a/tasks/core/demo-router-sync.ts +++ b/tasks/core/demo-router-sync.ts @@ -1,402 +1,386 @@ -import {shardNumber} from "@nilfoundation/hardhat-plugin/dist/utils/conversion"; -import {waitTillCompleted} from "@nilfoundation/niljs"; -import {task} from "hardhat/config"; -import {encodeFunctionData} from "viem"; +import { shardNumber } from "@nilfoundation/hardhat-plugin/dist/utils/conversion"; +import { waitTillCompleted } from "@nilfoundation/niljs"; +import { task } from "hardhat/config"; +import { encodeFunctionData } from "viem"; import type { Currency, UniswapV2Factory, UniswapV2Pair, } from "../../typechain-types"; -import {createClient} from "../util/client"; +import { createClient } from "../util/client"; import { faucetWithdrawal, mintAndSendCurrency, sleep, } from "../util/currencyUtils"; -import {deployNilContract} from "../util/deploy"; -import {calculateOutputAmount} from "../util/math"; - -task("demo-router-sync", "Run demo with Uniswap Router with Sync methods").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(), +import { deployNilContract } from "../util/deploy"; +import { calculateOutputAmount } from "../util/math"; + +task( + "demo-router-sync", + "Run demo with Uniswap Router with Sync methods", +).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 { deployedContract: RouterContract, contractAddress: routerAddress } = + await deployNilContract(hre, "UniswapV2Router01", [ + factoryAddress.toLowerCase(), ]); - console.log("Factory deployed " + factoryAddress); - console.log("Currency0 deployed " + currency0Address); - console.log("Currency1 deployed " + currency1Address); - - const {deployedContract: RouterContract, contractAddress: routerAddress} = - await deployNilContract(hre, "UniswapV2Router01", [ - factoryAddress.toLowerCase(), - ]); - - console.log("Router deployed " + routerAddress); - - 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. ROUTER: ADD LIQUIDITY - const pairArtifact = await hre.artifacts.readArtifact("UniswapV2Pair"); - const routerArtifact = - await hre.artifacts.readArtifact("UniswapV2Router01"); - - // Mint liquidity - console.log("Adding liquidity..."); - - const hash = await wallet.sendMessage({ - to: routerAddress, - feeCredit: BigInt(10_000_000), - value: BigInt(0), - refundTo: wallet.address, - data: encodeFunctionData({ - abi: routerArtifact.abi, - functionName: "addLiquiditySync", - args: [pairAddress, walletAddress, mintCurrency0Amount, mintCurrency1Amount, 1, 1], - }), - 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()); - - console.log("Liquidity added..."); - - // Retrieve and log reserves from the pair - const [reserve0, reserve1] = await pair.getReserves(); - console.log( - `ADDLIQUIDITY RESULT: Reserves - Currency0: ${reserve0.toString()}, Currency1: ${reserve1.toString()}`, - ); - - // Check and log liquidity provider balance - const lpBalance = await pair.getCurrencyBalanceOf(walletAddress); - console.log( - "ADDLIQUIDITY RESULT: Liquidity provider balance in wallet:", - lpBalance.toString(), - ); - - // Retrieve and log total supply for the pair - const totalSupply = await pair.getCurrencyTotalSupply(); - console.log( - "ADDLIQUIDITY RESULT: Total supply of pair tokens:", - totalSupply.toString(), - ); - - // 4. ROUTER: 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(), - ); - - // Execute the swap - console.log("Executing swap..."); - - // Send currency0 to the pair contract - const hash2 = await wallet.sendMessage({ - to: routerAddress, - feeCredit: BigInt(10_000_000), - value: BigInt(0), - data: encodeFunctionData({ - abi: routerArtifact.abi, - functionName: "swapExactTokenForTokenSync", - args: [pairAddress, 1, walletAddress], - }), - refundTo: wallet.address, - tokens: [ - { - id: await firstCurrency.getCurrencyId(), - amount: BigInt(swapAmount), - }, - ], - }); - - await waitTillCompleted(publicClient, shardNumber(walletAddress), hash2); - - const hash2Receipt = (await publicClient.getMessageReceiptByHash(hash2))!; - console.log(`hash2: ${hash2}`); - console.log(`hash2Receipt: ${JSON.stringify(hash2Receipt, (key, value) => typeof value === 'bigint' ? value.toString() : value)}`); - - console.log( - `Sent ${swapAmount.toString()} of currency0 to the pair contract. Tx - ${hash2}`, - ); - - 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. ROUTER: REMOVE LIQUIDITY - 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()); - // Execute burn - console.log("Executing burn..."); - // Send LP tokens to the user wallet - const hash3 = await wallet.sendMessage({ - // @ts-ignore - to: routerAddress, - feeCredit: BigInt(10_000_000), - value: BigInt(0), - data: encodeFunctionData({ - abi: routerArtifact.abi, - functionName: "removeLiquiditySync", - args: [pairAddress, walletAddress, 100, 100], - }), - refundTo: walletAddress, - tokens: [ - { - id: lpAddress, - amount: BigInt(userLpBalance), - }, + console.log("Router deployed " + routerAddress); + + 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. ROUTER: ADD LIQUIDITY + const pairArtifact = await hre.artifacts.readArtifact("UniswapV2Pair"); + const routerArtifact = await hre.artifacts.readArtifact("UniswapV2Router01"); + + // Mint liquidity + console.log("Adding liquidity..."); + + const hash = await wallet.sendMessage({ + to: routerAddress, + feeCredit: BigInt(10_000_000), + value: BigInt(0), + refundTo: wallet.address, + data: encodeFunctionData({ + abi: routerArtifact.abi, + functionName: "addLiquiditySync", + args: [ + pairAddress, + walletAddress, + mintCurrency0Amount, + mintCurrency1Amount, + 1, + 1, ], - }); - - await waitTillCompleted(publicClient, shardNumber(walletAddress), hash3); - - console.log("Burn executed."); - - // Log balances after burn - const balanceToken0 = await firstCurrency.getCurrencyBalanceOf( - pairAddress.toLowerCase(), - ); - const balanceToken1 = await secondCurrency.getCurrencyBalanceOf( - pairAddress.toLowerCase(), - ); - console.log( - "REMOVELIQUIDITY RESULT: Pair Balance token0 after burn:", - balanceToken0.toString(), - ); - console.log( - "REMOVELIQUIDITY RESULT: Pair Balance token1 after burn:", - balanceToken1.toString(), - ); - - userBalanceToken0 = await firstCurrency.getCurrencyBalanceOf(walletAddress); - userBalanceToken1 = - await secondCurrency.getCurrencyBalanceOf(walletAddress); - console.log( - "REMOVELIQUIDITY RESULT: User Balance token0 after burn:", - userBalanceToken0.toString(), - ); - console.log( - "REMOVELIQUIDITY RESULT: User Balance token1 after burn:", - userBalanceToken1.toString(), - ); - - // Fetch and log reserves after burn - const reserves = await pair.getReserves(); - console.log( - "REMOVELIQUIDITY RESULT: Reserves from pair after burn:", - reserves[0].toString(), - reserves[1].toString(), - ); - }, -); + }), + 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()); + + console.log("Liquidity added..."); + + // Retrieve and log reserves from the pair + const [reserve0, reserve1] = await pair.getReserves(); + console.log( + `ADDLIQUIDITY RESULT: Reserves - Currency0: ${reserve0.toString()}, Currency1: ${reserve1.toString()}`, + ); + + // Check and log liquidity provider balance + const lpBalance = await pair.getCurrencyBalanceOf(walletAddress); + console.log( + "ADDLIQUIDITY RESULT: Liquidity provider balance in wallet:", + lpBalance.toString(), + ); + + // Retrieve and log total supply for the pair + const totalSupply = await pair.getCurrencyTotalSupply(); + console.log( + "ADDLIQUIDITY RESULT: Total supply of pair tokens:", + totalSupply.toString(), + ); + + // 4. ROUTER: 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(), + ); + + // Execute the swap + console.log("Executing swap..."); + + // Send currency0 to the pair contract + const hash2 = await wallet.sendMessage({ + to: routerAddress, + feeCredit: BigInt(10_000_000), + value: BigInt(0), + data: encodeFunctionData({ + abi: routerArtifact.abi, + functionName: "swapExactTokenForTokenSync", + args: [pairAddress, 1, walletAddress], + }), + 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. Tx - ${hash2}`, + ); + + 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. ROUTER: REMOVE LIQUIDITY + 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()); + // Execute burn + console.log("Executing burn..."); + // Send LP tokens to the user wallet + const hash3 = await wallet.sendMessage({ + // @ts-ignore + to: routerAddress, + feeCredit: BigInt(10_000_000), + value: BigInt(0), + data: encodeFunctionData({ + abi: routerArtifact.abi, + functionName: "removeLiquiditySync", + args: [pairAddress, walletAddress, 100, 100], + }), + refundTo: walletAddress, + tokens: [ + { + id: lpAddress, + amount: BigInt(userLpBalance), + }, + ], + }); + + await waitTillCompleted(publicClient, shardNumber(walletAddress), hash3); + + console.log("Burn executed."); + + // Log balances after burn + const balanceToken0 = await firstCurrency.getCurrencyBalanceOf( + pairAddress.toLowerCase(), + ); + const balanceToken1 = await secondCurrency.getCurrencyBalanceOf( + pairAddress.toLowerCase(), + ); + console.log( + "REMOVELIQUIDITY RESULT: Pair Balance token0 after burn:", + balanceToken0.toString(), + ); + console.log( + "REMOVELIQUIDITY RESULT: Pair Balance token1 after burn:", + balanceToken1.toString(), + ); + + userBalanceToken0 = await firstCurrency.getCurrencyBalanceOf(walletAddress); + userBalanceToken1 = await secondCurrency.getCurrencyBalanceOf(walletAddress); + console.log( + "REMOVELIQUIDITY RESULT: User Balance token0 after burn:", + userBalanceToken0.toString(), + ); + console.log( + "REMOVELIQUIDITY RESULT: User Balance token1 after burn:", + userBalanceToken1.toString(), + ); + + // Fetch and log reserves after burn + const reserves = await pair.getReserves(); + console.log( + "REMOVELIQUIDITY RESULT: Reserves from pair after burn:", + reserves[0].toString(), + reserves[1].toString(), + ); +}); From 5b75687392478bbf4465935b7f5c7cba1b56b0e5 Mon Sep 17 00:00:00 2001 From: rolaman Date: Sun, 6 Oct 2024 16:24:49 +0300 Subject: [PATCH 5/7] demo-router: contract + task --- README.md | 69 ++++++++++++++++++++++----------- contracts/UniswapV2Router01.sol | 1 + 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 9193bdc..2db5c57 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ ## Overview -This repository is an example repo to showcase how to migrate dApps from Ethereum-like networks to =nil;. Uniswap V2 serves as a great base to demonstrate the following: +This repository is an example repo to showcase how to migrate dApps from Ethereum-like networks to =nil;. +Uniswap V2 serves as a great base to demonstrate the following: 1. How to work with =nil; multi-currencies. 2. How to utilize async calls. @@ -15,17 +16,15 @@ This repository is an example repo to showcase how to migrate dApps from Ethereu ```shell git clone https://github.com/NilFoundation/nil -cd nil -git checkout a3a99e1 ``` 2) Configure the node and create a new wallet: ```shell -./build/bin/nil_cli config init -./build/bin/nil_cli config set rpc_endpoint NIL_ENDPOINT -./build/bin/nil_cli keygen new -./build/bin/nil_cli wallet new +./build/bin/nil config init +./build/bin/nil config set rpc_endpoint NIL_ENDPOINT +./build/bin/nil keygen new +./build/bin/nil wallet new ``` 3) Create a `.env` file in the root of the project with the following configuration: @@ -41,34 +40,58 @@ Ensure to replace `` with the actual private key without 4) Enable debug logs. In `hardhat.config.ts` set `debug: true`. For now you can't see deployed contract addresses directly from hardhat results. +## How to Use -## How to use - -1) First you need to deploy token contracts and factory(or use existing ones). -In case of deployment new contracts please copy them from the debug logs. +### 1. Run Demo with Direct Messages to the Pair Contract +This demo task demonstrates how to create a `UniswapV2Pair` for two currencies, +add liquidity to the LP, perform swaps, and remove liquidity by directly calling `UniswapV2Pair`. ```shell -npx hardhat flow_1 --network nil +npx hardhat demo --network nil ``` -Deployment log example -``` -Response deployment {"hash":"0x0b788324e101a972c383d0a8ecd58084921d3ac84869b761c643317728eaf66d","address":"0x0001fd2e170eec3b3b538183c4d749adca5065b1"} +#### Important: +- The `UniswapV2Pair` is deployed on the same shard as the `UniswapV2Factory`. +- Calculations are processed on the user's side. +- Both the currency address and its ID are stored. + +### 2. Run Demo with Async Router Calls +This demo task shows how to deploy the `UniswapV2Router01` contract +and use it as a proxy for adding/removing liquidity and performing swaps. +Async calls are used, meaning `UniswapV2Router01` can be deployed on a different shard than the core contracts. + +```shell +npx hardhat demo-router --network nil ``` -2) Run flow to check DEX flows. This task will init a pair contract(or fetch the existed). -Then it mint pair tokens and run swap +#### Important: +- The `UniswapV2Factory` is used for creating new pairs. `UniswapV2Router01` calls already deployed pair contracts. +- `UniswapV2Router01` can be deployed on a different shard. +- Vulnerability: no checks are performed during adding/removing liquidity and swaps. +Rates and output amounts are entirely calculated on the user side. + +### 3. Run Demo with Sync Router Calls (1 Shard) +This demo task shows how to deploy the `UniswapV2Router01` contract +and use it as a proxy for adding/removing liquidity and swaps via sync calls. +It allows checks on amounts before pair calls and maintains currency rates. + ```shell -npx hardhat flow_2 --network nil --token0 --token1 --toburn --factory +npx hardhat demo-router-sync --network nil ``` +#### Important: +- `UniswapV2Router01` should be deployed on the same shard as the pair contract. +- It maintains the currency exchange rate when adding/removing liquidity. +- It supports limit checks for currency amounts. + ## Current Issues -1. Deployed contract address can be fetched only from debug logs -2. No chained swaps support -3. Security: no router contract to send tokens and call pair message in single transaction. +1. Use Nil.js wallet to attach tokens to the transaction message. +2. No support for chained swaps on the Nil network. +3. Fee collection is not implemented. ## License -This project is licensed under the GPL-3.0 License. See the [LICENSE](./LICENSE) file for more details. Portions of this project are derived from [Uniswap V2](https://github.com/Uniswap/v2-core) and are also subject to the GPL-3.0 License. - +This project is licensed under the GPL-3.0 License. +See the [LICENSE](./LICENSE) file for more details. Portions of this project +are derived from [Uniswap V2](https://github.com/Uniswap/v2-core) and are also subject to the GPL-3.0 License. diff --git a/contracts/UniswapV2Router01.sol b/contracts/UniswapV2Router01.sol index 00a618b..e7d3083 100644 --- a/contracts/UniswapV2Router01.sol +++ b/contracts/UniswapV2Router01.sol @@ -9,6 +9,7 @@ import "@nilfoundation/smart-contracts/contracts/Nil.sol"; import "./interfaces/IUniswapV2Pair.sol"; contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { + // not used now address public immutable factory; From aaaef18f97e3b98de0a38caeff4fe6d02a6d8223 Mon Sep 17 00:00:00 2001 From: rolaman Date: Mon, 7 Oct 2024 09:41:38 +0300 Subject: [PATCH 6/7] demo-router: contract + task --- README.md | 84 +++++++++++++++++++------------------------------------ 1 file changed, 29 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 2419d1c..e6bb625 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,36 @@ as well as performing operations like minting, swapping, and burning 1. **Using Factory and Pair Contracts Only** This demo handles deploying the Factory and Pair contracts and executing a complete flow of operations [View the demo task](https://github.com/NilFoundation/uniswap-v2-nil/blob/main/tasks/core/demo.ts) + + **Important:** + - The `UniswapV2Pair` is deployed on the same shard as the `UniswapV2Factory`. + - Calculations are processed on the user's side. + - Both the currency address and its ID are stored. + 2. **Using Factory, Pair, and Router Contracts** This demo includes an additional layer by utilizing the Router contract along with Factory and Pair [View the demo-router task](https://github.com/NilFoundation/uniswap-v2-nil/blob/main/tasks/core/demo-router.ts) + **Important:** + - The `UniswapV2Factory` is used for creating new pairs. `UniswapV2Router01` calls already deployed pair contracts. + - `UniswapV2Router01` can be deployed on a different shard. + - Vulnerability: no checks are performed during adding/removing liquidity and swaps. + Rates and output amounts are entirely calculated on the user side. + + +3. **Using Router with Sync Calls (1 shard)** + This demo task shows how to deploy the `UniswapV2Router01` contract + and use it as a proxy for adding/removing liquidity and swaps via sync calls. + It allows checks on amounts before pair calls and maintains currency rates. + [View the demo-router task](https://github.com/NilFoundation/uniswap-v2-nil/blob/main/tasks/core/demo-router-sync.ts) + + **Important:** + - `UniswapV2Router01` should be deployed on the same shard as the pair contract. + - It maintains the currency exchange rate when adding/removing liquidity. + - It supports limit checks for currency amounts. + + ### Running the Demo Tasks 1. **Compile the Project**: ```bash @@ -60,6 +85,10 @@ as well as performing operations like minting, swapping, and burning ```bash npx hardhat demo-router --network nil ``` + - For the demo with Router (Sync calls): + ```bash + npx hardhat demo-router-sync --network nil + ``` ### Manual Setup If you prefer to run everything manually, we provide Ignition modules for each contract: @@ -80,58 +109,3 @@ Your input and contributions are greatly appreciated! ## License This project is licensed under the GPL-3.0 License. See the [LICENSE](./LICENSE) file for more details. Portions of this project are derived from [Uniswap V2](https://github.com/Uniswap/v2-core) and are also subject to the GPL-3.0 License. - - - - - - -## How to Use - -### 1. Run Demo with Direct Messages to the Pair Contract -This demo task demonstrates how to create a `UniswapV2Pair` for two currencies, -add liquidity to the LP, perform swaps, and remove liquidity by directly calling `UniswapV2Pair`. - -```shell -npx hardhat demo --network nil -``` - -#### Important: -- The `UniswapV2Pair` is deployed on the same shard as the `UniswapV2Factory`. -- Calculations are processed on the user's side. -- Both the currency address and its ID are stored. - -### 2. Run Demo with Async Router Calls -This demo task shows how to deploy the `UniswapV2Router01` contract -and use it as a proxy for adding/removing liquidity and performing swaps. -Async calls are used, meaning `UniswapV2Router01` can be deployed on a different shard than the core contracts. - -```shell -npx hardhat demo-router --network nil -``` - -#### Important: -- The `UniswapV2Factory` is used for creating new pairs. `UniswapV2Router01` calls already deployed pair contracts. -- `UniswapV2Router01` can be deployed on a different shard. -- Vulnerability: no checks are performed during adding/removing liquidity and swaps. - Rates and output amounts are entirely calculated on the user side. - -### 3. Run Demo with Sync Router Calls (1 Shard) -This demo task shows how to deploy the `UniswapV2Router01` contract -and use it as a proxy for adding/removing liquidity and swaps via sync calls. -It allows checks on amounts before pair calls and maintains currency rates. - -```shell -npx hardhat demo-router-sync --network nil -``` - -#### Important: -- `UniswapV2Router01` should be deployed on the same shard as the pair contract. -- It maintains the currency exchange rate when adding/removing liquidity. -- It supports limit checks for currency amounts. - -## Current Issues - -1. Use Nil.js wallet to attach tokens to the transaction message. -2. No support for chained swaps on the Nil network. -3. Fee collection is not implemented. \ No newline at end of file From d4975078d7dcaeaad45e9d80091cdfe281124f5b Mon Sep 17 00:00:00 2001 From: rolaman Date: Mon, 7 Oct 2024 14:09:02 +0300 Subject: [PATCH 7/7] demo-router: contract + task --- README.md | 3 +-- contracts/UniswapV2Router01.sol | 10 ---------- ignition/modules/Router.ts | 7 +------ tasks/core/demo-router-sync.ts | 4 +--- tasks/core/demo-router.ts | 4 +--- 5 files changed, 4 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index e6bb625..014d8a3 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,8 @@ as well as performing operations like minting, swapping, and burning [View the demo task](https://github.com/NilFoundation/uniswap-v2-nil/blob/main/tasks/core/demo.ts) **Important:** - - The `UniswapV2Pair` is deployed on the same shard as the `UniswapV2Factory`. - Calculations are processed on the user's side. - - Both the currency address and its ID are stored. + - Both the currency address and its ID are stored in the pair contract. 2. **Using Factory, Pair, and Router Contracts** diff --git a/contracts/UniswapV2Router01.sol b/contracts/UniswapV2Router01.sol index e7d3083..8a08823 100644 --- a/contracts/UniswapV2Router01.sol +++ b/contracts/UniswapV2Router01.sol @@ -9,16 +9,6 @@ import "@nilfoundation/smart-contracts/contracts/Nil.sol"; import "./interfaces/IUniswapV2Pair.sol"; contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { - // not used now - address public immutable factory; - - - constructor(address _factory) public { - // Revert if the factory address is the zero address or an empty string - require(_factory != address(0), "Factory address cannot be the zero address"); - - factory = _factory; - } modifier sameShard(address _addr) { require(Nil.getShardId(_addr) == Nil.getShardId(address(this)), "Sync calls require same shard for all contracts"); diff --git a/ignition/modules/Router.ts b/ignition/modules/Router.ts index b744b66..413f071 100644 --- a/ignition/modules/Router.ts +++ b/ignition/modules/Router.ts @@ -1,12 +1,7 @@ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; module.exports = buildModule("DeployUniswapV2Router01", (m) => { - const factory = m.getParameter( - "factory", - "0x0000000000000000000000000000000000000000", - ); - - const router = m.contract("UniswapV2Router01", [factory]); + const router = m.contract("UniswapV2Router01"); return { router }; }); diff --git a/tasks/core/demo-router-sync.ts b/tasks/core/demo-router-sync.ts index be37bc8..dffed45 100644 --- a/tasks/core/demo-router-sync.ts +++ b/tasks/core/demo-router-sync.ts @@ -57,9 +57,7 @@ task( console.log("Currency1 deployed " + currency1Address); const { deployedContract: RouterContract, contractAddress: routerAddress } = - await deployNilContract(hre, "UniswapV2Router01", [ - factoryAddress.toLowerCase(), - ]); + await deployNilContract(hre, "UniswapV2Router01"); console.log("Router deployed " + routerAddress); diff --git a/tasks/core/demo-router.ts b/tasks/core/demo-router.ts index c607a8a..148d08a 100644 --- a/tasks/core/demo-router.ts +++ b/tasks/core/demo-router.ts @@ -57,9 +57,7 @@ task("demo-router", "Run demo with Uniswap Router").setAction( console.log("Currency1 deployed " + currency1Address); const { deployedContract: RouterContract, contractAddress: routerAddress } = - await deployNilContract(hre, "UniswapV2Router01", [ - factoryAddress.toLowerCase(), - ]); + await deployNilContract(hre, "UniswapV2Router01"); console.log("Router deployed " + routerAddress);