diff --git a/contracts/UniswapV2Router01.sol b/contracts/UniswapV2Router01.sol index 05937e3..3b1bf69 100644 --- a/contracts/UniswapV2Router01.sol +++ b/contracts/UniswapV2Router01.sol @@ -11,10 +11,6 @@ import "./interfaces/IUniswapV2Pair.sol"; contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { address public immutable factory; - modifier ensure(uint deadline) { - require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED'); - _; - } constructor(address _factory) public { // Revert if the factory address is the zero address or an empty string @@ -24,59 +20,37 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { } function addLiquidity( - address tokenA, - address tokenB, - address to, - uint deadline, - uint salt - ) external override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) { - if (IUniswapV2Factory(factory).getTokenPair(tokenA, tokenB) == address(0)) { - IUniswapV2Factory(factory).createPair(tokenA, tokenB, 1, salt); - } - address pair = IUniswapV2Factory(factory).getTokenPair(tokenA, tokenB); - uint tokenAId = NilCurrencyBase(tokenA).getCurrencyId(); - uint tokenBId = NilCurrencyBase(tokenB).getCurrencyId(); - + address pair, + address to + ) public override { Nil.Token[] memory tokens = Nil.msgTokens(); if (tokens.length != 2) { revert("Send only 2 tokens to add liquidity"); } - assert(tokenAId == tokens[0].id); - assert(tokenBId == tokens[1].id); - - if (tokens.length != 2) { - revert("UniswapV2Router: Expect 2 tokens to add liquidity"); - } - sendCurrencyInternal(pair, tokenAId, tokens[0].amount); - sendCurrencyInternal(pair, tokenBId, tokens[1].amount); - liquidity = IUniswapV2Pair(pair).mint(to); - amountA = tokens[0].amount; - amountB = tokens[1].amount; + smartCall(pair, tokens, abi.encodeWithSignature("mint(address)", to)); } // **** REMOVE LIQUIDITY **** function removeLiquidity( - address tokenA, - address tokenB, - uint liquidity, - uint amountAMin, - uint amountBMin, - address to, - uint deadline - ) public override ensure(deadline) returns (uint amountA, uint amountB) { - address pair = IUniswapV2Factory(factory).getTokenPair(tokenA, tokenB); - (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB); - (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to); - (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0); - require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); - require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT'); + address pair, + address to + ) public override { + Nil.Token[] memory tokens = Nil.msgTokens(); + if (tokens.length != 1) { + revert("UniswapV2Router: should contains only pair token"); + } + smartCall(pair, tokens, abi.encodeWithSignature("burn(address)", to)); + } + + function swap(address to, address pair, uint amount0Out, uint amount1Out) public override { Nil.Token[] memory tokens = Nil.msgTokens(); if (tokens.length != 1) { revert("UniswapV2Router: should contains only pair token"); } - sendCurrencyInternal(pair, tokens[0].id, tokens[0].amount); // send liquidity to pair + 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 { @@ -91,13 +65,14 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { } } + // 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 ensure(deadline) returns (uint[] memory amounts) { + ) 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]); @@ -106,13 +81,14 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { _swap(amounts, path, 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 ensure(deadline) returns (uint[] memory amounts) { + ) 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]); @@ -133,14 +109,16 @@ contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { return UniswapV2Library.getAmountOut(amountOut, reserveIn, reserveOut); } - function getAmountsOut(uint amountIn, address[] memory path) public view override returns (uint[] memory amounts) { - return UniswapV2Library.getAmountsOut(factory, amountIn, path); - } - - function getAmountsIn(uint amountOut, address[] memory path) public view override returns (uint[] memory amounts) { - return UniswapV2Library.getAmountsIn(factory, amountOut, path); + receive() external payable { } - receive() external payable { + function smartCall(address dst, Nil.Token[] memory tokens, bytes memory callData) private returns (bool) { + if (Nil.getShardId(dst) == Nil.getShardId(address(this))) { + (bool success,) = Nil.syncCall(dst, gasleft(), 0, tokens, callData); + return success; + } else { + Nil.asyncCall(dst, address(0), address(0), 0, Nil.FORWARD_REMAINING, false, 0, tokens, callData); + return true; + } } } \ No newline at end of file diff --git a/contracts/interfaces/IUniswapV2Router01.sol b/contracts/interfaces/IUniswapV2Router01.sol index 9c2d640..87e8def 100644 --- a/contracts/interfaces/IUniswapV2Router01.sol +++ b/contracts/interfaces/IUniswapV2Router01.sol @@ -5,21 +5,20 @@ pragma solidity ^0.8.0; interface IUniswapV2Router01 { function addLiquidity( - address tokenA, - address tokenB, - address to, - uint deadline, - uint salt - ) external returns (uint amountA, uint amountB, uint liquidity); + address pair, + address to + ) external; function removeLiquidity( - address tokenA, - address tokenB, - uint liquidity, - uint amountAMin, - uint amountBMin, + address pair, + address to + ) external; + function swap( address to, - uint deadline - ) external returns (uint amountA, uint amountB); + address pair, + uint amount0Out, + uint amount1Out + ) external; + function swapExactTokensForTokens( uint amountIn, uint amountOutMin, @@ -38,6 +37,4 @@ interface IUniswapV2Router01 { 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); function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); - function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); - function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); } \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index adb32e9..335f310 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -21,6 +21,7 @@ import "./tasks/core/factory/create-pair"; // Demo Tasks import "./tasks/core/demo"; +import "./tasks/core/demo-router"; dotenv.config(); diff --git a/tasks/core/demo-router.ts b/tasks/core/demo-router.ts new file mode 100644 index 0000000..c607a8a --- /dev/null +++ b/tasks/core/demo-router.ts @@ -0,0 +1,398 @@ +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", "Run demo with Uniswap Router").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: "addLiquidity", + args: [pairAddress, walletAddress], + }), + 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: "swap", + args: [walletAddress, pairAddress, 0, expectedOutputAmount], + }), + 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: "removeLiquidity", + args: [pairAddress, walletAddress], + }), + 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(), + ); + }, +);