├── tsconfig.json ├── package.json ├── contracts ├── interface │ └── ILiquidityValueCalculator.sol └── LiquidityValueCalculator.sol ├── ignition └── modules │ └── Lock.ts ├── deploy └── deploy.ts ├── LICENSE ├── README.md ├── hardhat.config.ts ├── .gitignore └── test └── Calculator.test.ts /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-project", 3 | "devDependencies": { 4 | "@nomicfoundation/hardhat-toolbox": "^5.0.0", 5 | "@uniswap/v2-core": "^1.0.1", 6 | "@uniswap/v2-periphery": "^1.1.0-beta.0", 7 | "hardhat": "^2.22.15" 8 | }, 9 | "dependencies": { 10 | "dotenv": "^16.4.5" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/interface/ILiquidityValueCalculator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.27; 3 | 4 | interface ILiquidityValueCalculator { 5 | function computeLiquidityShareValue( 6 | uint liquidity, 7 | address tokenA, 8 | address tokenB 9 | ) external returns (uint tokenAAmount, uint tokenBAmount); 10 | } 11 | -------------------------------------------------------------------------------- /ignition/modules/Lock.ts: -------------------------------------------------------------------------------- 1 | // This setup uses Hardhat Ignition to manage smart contract deployments. 2 | // Learn more about it at https://hardhat.org/ignition 3 | 4 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 5 | 6 | const JAN_1ST_2030 = 1893456000; 7 | const ONE_GWEI: bigint = 1_000_000_000n; 8 | 9 | const LockModule = buildModule("LockModule", (m) => { 10 | const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030); 11 | const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI); 12 | 13 | const lock = m.contract("Lock", [unlockTime], { 14 | value: lockedAmount, 15 | }); 16 | 17 | return { lock }; 18 | }); 19 | 20 | export default LockModule; 21 | -------------------------------------------------------------------------------- /deploy/deploy.ts: -------------------------------------------------------------------------------- 1 | // Importing necessary functionalities from the Hardhat package. 2 | import { ethers } from 'hardhat' 3 | 4 | async function main() { 5 | // Retrieve the first signer, typically the default account in Hardhat, to use as the deployer. 6 | const [deployer] = await ethers.getSigners(); 7 | const instanceLiquidityValueCalculator = await ethers.deployContract("LiquidityValueCalculator"); 8 | await instanceLiquidityValueCalculator.waitForDeployment() 9 | const LiquidityValueCalculator_Address = await instanceLiquidityValueCalculator.getAddress(); 10 | console.log(`LiquidityValueCalculator is deployed. ${LiquidityValueCalculator_Address}`); 11 | } 12 | 13 | // This pattern allows the use of async/await throughout and ensures that errors are caught and handled properly. 14 | main() 15 | .then(() => process.exit(0)) 16 | .catch(error => { 17 | console.error(error) 18 | process.exitCode = 1 19 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mitsuru Kudo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Liquidity-Share-Value-Calculator 2 | 3 | A Solidity smart contract for calculating liquidity share values in Uniswap V2 pairs. 4 | 5 | ## Overview 6 | 7 | The LiquidityValueCalculator contract provides functionality to compute the value of liquidity shares in terms of both tokens in a Uniswap V2 pair. This is particularly useful for liquidity providers who want to know the underlying value of their LP tokens. 8 | 9 | ## Features 10 | 11 | - Calculate the exact amount of both tokens represented by LP tokens 12 | - Compatible with any Uniswap V2 pair 13 | - Gas-efficient calculations 14 | - View functions that don't modify state 15 | 16 | ## Contract Details 17 | 18 | ### Constructor 19 | - Takes a Uniswap V2 factory address as parameter 20 | - Stores the factory address for pair calculations 21 | 22 | ### Main Functions 23 | 24 | #### computeLiquidityShareValue 25 | ```solidity 26 | function computeLiquidityShareValue( 27 | uint liquidity, 28 | address tokenA, 29 | address tokenB 30 | ) external view returns (uint tokenAAmount, uint tokenBAmount) 31 | ``` 32 | - Input: Liquidity amount and addresses of both tokens 33 | - Output: Equivalent amounts of tokenA and tokenB 34 | - Uses current reserves and total supply for accurate calculations 35 | 36 | #### pairInfo (Internal) 37 | ```solidity 38 | function pairInfo( 39 | address tokenA, 40 | address tokenB 41 | ) internal view returns (uint reserveA, uint reserveB, uint totalLiquidity) 42 | ``` 43 | - Retrieves current reserves and total supply from the pair 44 | - Handles token ordering based on addresses 45 | - Returns reserveA, reserveB, and total liquidity 46 | 47 | ## Dependencies 48 | - Uniswap V2 Core 49 | - Uniswap V2 Periphery 50 | - Solidity ^0.8.27 51 | 52 | ## License 53 | This project is licensed under the MIT License - see the [MIT](./LICENSE) file for details. 54 | -------------------------------------------------------------------------------- /contracts/LiquidityValueCalculator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.27; 3 | 4 | import "./interface/ILiquidityValueCalculator.sol"; 5 | import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; 6 | import "@uniswap/v2-periphery/contracts/libraries/UniswapV2Library.sol"; 7 | 8 | contract LiquidityValueCalculator is ILiquidityValueCalculator { 9 | address public factory; 10 | 11 | constructor(address factory_) { 12 | factory = factory_; 13 | } 14 | 15 | /** 16 | * @dev Returns the pair info of tokenA and tokenB 17 | * @param tokenA The amount of liquidity to compute the value of 18 | * @param tokenB The amount of liquidity to compute the value of 19 | */ 20 | function pairInfo( 21 | address tokenA, 22 | address tokenB 23 | ) 24 | internal 25 | view 26 | returns (uint reserveA, uint reserveB, uint totalLiquidity) 27 | { 28 | IUniswapV2Pair pair = IUniswapV2Pair( 29 | UniswapV2Library.pairFor(factory, tokenA, tokenB) 30 | ); 31 | (uint reserve0, uint reserve1, ) = pair.getReserves(); 32 | (reserveA, reserveB) = tokenA < tokenB 33 | ? (reserve0, reserve1) 34 | : (reserve1, reserve0); 35 | totalLiquidity = pair.totalSupply(); 36 | } 37 | 38 | /** 39 | * @dev Computes the value of a liquidity share in terms of tokenA and tokenB 40 | */ 41 | function computeLiquidityShareValue( 42 | uint liquidity, 43 | address tokenA, 44 | address tokenB 45 | ) external view returns (uint tokenAAmount, uint tokenBAmount) { 46 | (uint reserveA, uint reserveB, uint totalLiquidity) = pairInfo( 47 | tokenA, 48 | tokenB 49 | ); 50 | tokenAAmount = (liquidity * reserveA) / totalLiquidity; 51 | tokenBAmount = (liquidity * reserveB) / totalLiquidity; 52 | } 53 | } -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox"; 3 | 4 | const infuraKey: string = process.env.INFURA_API_KEY as string; 5 | const privateKey: string = process.env.PRIVATE_KEY ? process.env.PRIVATE_KEY as string : ""; 6 | const etherscanKey: string = process.env.ETHERSCAN_KEY ? process.env.ETHERSCAN_KEY as string : ""; 7 | const basescanKey: string = process.env.BASESCAN_KEY ? process.env.BASESCAN_KEY as string : ""; 8 | const bscscanKey: string = process.env.BSCSCAN_KEY ? process.env.BSCSCAN_KEY as string : ""; 9 | 10 | const config: HardhatUserConfig = { 11 | solidity: { 12 | version: "0.8.27", 13 | settings: { 14 | optimizer: { 15 | enabled: true, 16 | runs: 100, 17 | }, 18 | // viaIR: true, 19 | }, 20 | }, 21 | networks: { 22 | sepolia: { 23 | url: `https://sepolia.infura.io/v3/${infuraKey}`, 24 | accounts: [`0x${privateKey}`], 25 | }, 26 | mainnet: { 27 | url: `https://mainnet.infura.io/v3/${infuraKey}`, 28 | accounts: [`0x${privateKey}`], 29 | }, 30 | base_sepolia: { 31 | url: "https://base-sepolia.blockpi.network/v1/rpc/public", 32 | accounts: [`0x${privateKey}`], 33 | }, 34 | base_mainnet: { 35 | url: "https://mainnet.base.org", 36 | accounts: [`0x${privateKey}`], 37 | }, 38 | holesky_testnet: { 39 | url: "https://ethereum-holesky-rpc.publicnode.com", 40 | accounts: [`0x${privateKey}`], 41 | }, 42 | hardhat: { 43 | chainId: 31337, 44 | }, 45 | }, 46 | etherscan: { 47 | apiKey: { 48 | mainnet: etherscanKey, 49 | sepolia: etherscanKey, 50 | basemainnet: basescanKey, 51 | baseSepolia: basescanKey, // etherscan: ED2NED96C214Y891MR98PZZ1Q45VTFYZRV BSC: 1UME8V5UP4AZHYDF7RWC78GTIXXRPJHTQY Base: 1SZX9N4CQNAX489BHPEW27C2FG5PPP4MB1 52 | holesky: etherscanKey, 53 | bscTestnet: bscscanKey 54 | }, 55 | }, 56 | gasReporter: { 57 | enabled: true, 58 | }, 59 | sourcify: { 60 | enabled: true, 61 | }, 62 | }; 63 | 64 | export default config; 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | node_modules 133 | .env 134 | 135 | # Hardhat files 136 | /cache 137 | /artifacts 138 | 139 | # TypeChain files 140 | /typechain 141 | /typechain-types 142 | 143 | # solidity-coverage files 144 | /coverage 145 | /coverage.json 146 | 147 | # Hardhat Ignition default folder for deployments against a local node 148 | ignition/deployments/chain-31337 149 | -------------------------------------------------------------------------------- /test/Calculator.test.ts: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const { ethers } = require("hardhat"); 3 | 4 | describe("LiquidityValueCalculator", function () { 5 | let liquidityCalculator: any; 6 | let factory: any; 7 | let tokenA: any; 8 | let tokenB: any; 9 | let pair: any; 10 | let owner: any; 11 | 12 | beforeEach(async function () { 13 | // Get signers 14 | [owner] = await ethers.getSigners(); 15 | 16 | // Deploy mock tokens 17 | const MockToken = await ethers.getContractFactory("MockERC20"); 18 | tokenA = await MockToken.deploy("Token A", "TKNA"); 19 | tokenB = await MockToken.deploy("Token B", "TKNB"); 20 | 21 | // Deploy mock factory 22 | const MockFactory = await ethers.getContractFactory("MockUniswapV2Factory"); 23 | factory = await MockFactory.deploy(); 24 | 25 | // Deploy mock pair 26 | const MockPair = await ethers.getContractFactory("MockUniswapV2Pair"); 27 | pair = await MockPair.deploy(); 28 | 29 | // Setup mock factory to return our mock pair 30 | await factory.setPair(tokenA.address, tokenB.address, pair.address); 31 | 32 | // Deploy LiquidityValueCalculator 33 | const LiquidityValueCalculator = await ethers.getContractFactory("LiquidityValueCalculator"); 34 | liquidityCalculator = await LiquidityValueCalculator.deploy(factory.address); 35 | }); 36 | 37 | describe("Constructor", function () { 38 | it("Should set the factory address correctly", async function () { 39 | expect(await liquidityCalculator.factory()).to.equal(factory.address); 40 | }); 41 | }); 42 | 43 | describe("computeLiquidityShareValue", function () { 44 | beforeEach(async function () { 45 | // Setup mock reserves and total supply 46 | await pair.setReserves( 47 | ethers.utils.parseEther("100"), // reserve0 48 | ethers.utils.parseEther("200"), // reserve1 49 | 1234567 // blockTimestampLast (not used in our tests) 50 | ); 51 | await pair.setTotalSupply(ethers.utils.parseEther("1000")); 52 | }); 53 | 54 | it("Should calculate correct liquidity share values when tokenA < tokenB", async function () { 55 | // Ensure tokenA has lower address than tokenB 56 | const [lowerToken, higherToken] = tokenA.address.toLowerCase() < tokenB.address.toLowerCase() 57 | ? [tokenA, tokenB] 58 | : [tokenB, tokenA]; 59 | 60 | const liquidity = ethers.utils.parseEther("100"); // 10% of total supply 61 | const [tokenAAmount, tokenBAmount] = await liquidityCalculator.computeLiquidityShareValue( 62 | liquidity, 63 | lowerToken.address, 64 | higherToken.address 65 | ); 66 | 67 | expect(tokenAAmount).to.equal(ethers.utils.parseEther("10")); // 10% of reserve0 68 | expect(tokenBAmount).to.equal(ethers.utils.parseEther("20")); // 10% of reserve1 69 | }); 70 | 71 | it("Should calculate correct liquidity share values when tokenA > tokenB", async function () { 72 | // Ensure tokenA has higher address than tokenB 73 | const [lowerToken, higherToken] = tokenA.address.toLowerCase() > tokenB.address.toLowerCase() 74 | ? [tokenB, tokenA] 75 | : [tokenA, tokenB]; 76 | 77 | const liquidity = ethers.utils.parseEther("500"); // 50% of total supply 78 | const [tokenAAmount, tokenBAmount] = await liquidityCalculator.computeLiquidityShareValue( 79 | liquidity, 80 | higherToken.address, 81 | lowerToken.address 82 | ); 83 | 84 | expect(tokenAAmount).to.equal(ethers.utils.parseEther("50")); // 50% of reserve0 85 | expect(tokenBAmount).to.equal(ethers.utils.parseEther("100")); // 50% of reserve1 86 | }); 87 | }); 88 | }); 89 | --------------------------------------------------------------------------------