diff --git a/README.md b/README.md index fa4ba8c..014d8a3 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,35 @@ 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:** + - Calculations are processed on the user's side. + - Both the currency address and its ID are stored in the pair contract. + 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 +84,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,4 +108,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. - 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..8a08823 100644 --- a/contracts/UniswapV2Router01.sol +++ b/contracts/UniswapV2Router01.sol @@ -9,14 +9,10 @@ import "@nilfoundation/smart-contracts/contracts/Nil.sol"; import "./interfaces/IUniswapV2Pair.sol"; contract UniswapV2Router01 is IUniswapV2Router01, NilCurrencyBase { - 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"); + _; } function addLiquidity( @@ -30,6 +26,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 +99,53 @@ 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 ? 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) { @@ -112,13 +163,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 diff --git a/hardhat.config.ts b/hardhat.config.ts index 335f310..e48859b 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(); @@ -29,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, 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 new file mode 100644 index 0000000..dffed45 --- /dev/null +++ b/tasks/core/demo-router-sync.ts @@ -0,0 +1,384 @@ +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"); + + 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); + + 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(), + ); +}); 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);