Created
March 9, 2025 12:11
-
-
Save ahmedali8/3145761bda88d8dad4ca1f01e8b9f989 to your computer and use it in GitHub Desktop.
Amounts from Liquidity & Swap Calculations - Uniswap V4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { SqrtPriceMath, TickMath } from "@uniswap/v3-sdk"; | |
| import Decimal from "decimal.js"; | |
| import { ethers } from "ethers"; | |
| import JSBI from "jsbi"; | |
| export const JSBI_ZERO = JSBI.BigInt(0); | |
| export function getSqrtPriceAtTick(tick: string): string { | |
| return new Decimal(1.0001).pow(tick).sqrt().mul(new Decimal(2).pow(96)).toFixed(0); | |
| } | |
| export function getAmount0Delta(sqrtPriceAX96: JSBI, sqrtPriceBX96: JSBI, liquidity: JSBI): JSBI { | |
| if (JSBI.lessThan(liquidity, JSBI_ZERO)) { | |
| return SqrtPriceMath.getAmount0Delta(sqrtPriceAX96, sqrtPriceBX96, JSBI.unaryMinus(liquidity), false); | |
| } else { | |
| return JSBI.unaryMinus(SqrtPriceMath.getAmount0Delta(sqrtPriceAX96, sqrtPriceBX96, liquidity, true)); | |
| } | |
| } | |
| export function getAmount1Delta(sqrtPriceAX96: JSBI, sqrtPriceBX96: JSBI, liquidity: JSBI): JSBI { | |
| if (JSBI.lessThan(liquidity, JSBI_ZERO)) { | |
| return SqrtPriceMath.getAmount1Delta(sqrtPriceAX96, sqrtPriceBX96, JSBI.unaryMinus(liquidity), false); | |
| } else { | |
| return JSBI.unaryMinus(SqrtPriceMath.getAmount1Delta(sqrtPriceAX96, sqrtPriceBX96, liquidity, true)); | |
| } | |
| } | |
| export function modifyLiquidity( | |
| _tickLower: string, | |
| _tickUpper: string, | |
| _liquidity: string, | |
| slot0Tick: string, | |
| slot0Price: string, | |
| ): string[] { | |
| const liquidity = JSBI.BigInt(_liquidity); | |
| if (JSBI.EQ(liquidity, 0)) { | |
| return [JSBI_ZERO.toString(), JSBI_ZERO.toString()]; | |
| } | |
| // State values in slot0. | |
| const tick = JSBI.BigInt(slot0Tick); | |
| const sqrtPriceX96 = JSBI.BigInt(slot0Price); | |
| // Position lower and upper ticks. | |
| const tickLower = JSBI.BigInt(_tickLower); | |
| const tickUpper = JSBI.BigInt(_tickUpper); | |
| const delta: string[] = []; | |
| if (JSBI.LT(tick, tickLower)) { | |
| // The current tick is less than the lowest tick of the position, so the position is entirely in token0. | |
| const priceLower = JSBI.BigInt(getSqrtPriceAtTick(_tickLower)); | |
| const priceUpper = JSBI.BigInt(getSqrtPriceAtTick(_tickUpper)); | |
| const amount0 = getAmount0Delta(priceLower, priceUpper, liquidity); | |
| delta.push(amount0.toString()); | |
| delta.push(JSBI_ZERO.toString()); | |
| } else if (JSBI.LT(tick, tickUpper)) { | |
| // In-range liquidity. As we are just calculating the values of the delta. | |
| // We do not update any global state variable for state.liquidity, but note that the protocol increments state liquidity in this case. | |
| const priceUpper = JSBI.BigInt(getSqrtPriceAtTick(_tickUpper)); | |
| let priceLower = JSBI.BigInt(getSqrtPriceAtTick(_tickLower)); | |
| // When tickLower == the current tick, the price returned from getSqrtPriceATick has a slight error in JS. | |
| // In solidity because the calculations for getSqrtRatioAtTick(tickLower) == currentSqrtPriceX96, the | |
| // numerator becomes 0, and the amount becomes 0. | |
| // So instead of using the price from getSqrtRatioAtTick, we set the priceLower to the current price. | |
| if (JSBI.EQ(tickLower, tick)) { | |
| priceLower = sqrtPriceX96; | |
| } | |
| const amount0 = getAmount0Delta(sqrtPriceX96, priceUpper, liquidity); | |
| const amount1 = getAmount1Delta(priceLower, sqrtPriceX96, liquidity); | |
| delta.push(amount0.toString()); | |
| delta.push(amount1.toString()); | |
| } else { | |
| // The current tick is greater than the highest tick of the position, meaning the position is entirely in token1. | |
| const priceLower = JSBI.BigInt(getSqrtPriceAtTick(_tickLower)); | |
| const priceUpper = JSBI.BigInt(getSqrtPriceAtTick(_tickUpper)); | |
| const amount1 = getAmount1Delta(priceLower, priceUpper, liquidity); | |
| delta.push(JSBI_ZERO.toString()); | |
| delta.push(amount1.toString()); | |
| } | |
| return delta; | |
| } | |
| export function getInt128Array(delta: string[]): string { | |
| return ethers.utils.defaultAbiCoder.encode(["int128[]"], [delta]); | |
| } | |
| // ------------------------------------------------------------------------------------------------------------------- // | |
| const currentTick = 175052; | |
| // First token1 Then token0 | |
| const liquidity = JSBI.BigInt("4899712312116710985145008"); | |
| const sqrtPriceX96 = JSBI.BigInt("501066558273621464884319660560427"); | |
| const priceUpper = JSBI.BigInt(getSqrtPriceAtTick((-193620).toString())); | |
| const priceLower = JSBI.BigInt(getSqrtPriceAtTick((-194040).toString())); | |
| const amount0 = getAmount0Delta(sqrtPriceX96, priceUpper, liquidity); | |
| const amount1 = getAmount1Delta(priceLower, sqrtPriceX96, liquidity); | |
| console.log("amount0: ", amount0.toString()); // -2000130431716761975 wei (2.000130431716761975 eth) | |
| console.log("amount1: ", amount1.toString()); // -79999999999999999989440096 wei tokens (79999999.999999999989440096 tokens) | |
| // const delta = modifyLiquidity( | |
| // TickMath.MIN_TICK.toString(), | |
| // TickMath.MAX_TICK.toString(), | |
| // liquidity.toString(), | |
| // currentTick.toString(), | |
| // sqrtPriceX96.toString(), | |
| // ); | |
| // console.log("delta: ", delta); | |
| // console.log("int128Array: ", getInt128Array(delta)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ... | |
| "dependencies": { | |
| "@uniswap/sdk-core": "^7.5.0", | |
| "@uniswap/v3-sdk": "^3.24.0", | |
| "@uniswap/v4-sdk": "^1.18.1", | |
| "decimal.js": "^10.5.0", | |
| "ethers": "5.7.1", | |
| "hardhat": "2.14.1", | |
| "jsbi": "^3.1.4" | |
| }, | |
| ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { TOKEN_ADDRESS } from "./constants"; | |
| import { stateView } from "./contracts"; | |
| import { ChainId, type Currency, CurrencyAmount, Ether, Token } from "@uniswap/sdk-core"; | |
| import { | |
| ADDRESS_ZERO, | |
| FeeAmount, | |
| TICK_SPACINGS, | |
| TickConstructorArgs, | |
| TickMath, | |
| nearestUsableTick, | |
| } from "@uniswap/v3-sdk"; | |
| import { Pool, Position } from "@uniswap/v4-sdk"; | |
| import { ethers } from "hardhat"; | |
| import JSBI from "jsbi"; | |
| async function main() { | |
| const [owner] = await ethers.getSigners(); | |
| console.log(`Owner ${owner.address}, balance: ${ethers.utils.formatEther(await owner.getBalance())} ETH`); | |
| const chainId = ChainId.SEPOLIA; | |
| // Currencies | |
| const currencyNative = Ether.onChain(chainId); | |
| const currency0: Currency = new Token(chainId, ADDRESS_ZERO, 18, "ETH", "Ether"); | |
| const currency1: Currency = new Token(chainId, TOKEN_ADDRESS, 18, "TKN3", "VToken3"); | |
| const fee = FeeAmount.HIGH; // 1% | |
| const tickSpacing = TICK_SPACINGS[fee]; // 200 for HIGH | |
| const poolId = Pool.getPoolId(currencyNative, currency1, fee, tickSpacing, ADDRESS_ZERO); | |
| // console.log("PoolId: ", poolId); | |
| const poolKey = Pool.getPoolKey(currencyNative, currency1, fee, tickSpacing, ADDRESS_ZERO); | |
| // console.log("PoolKey: ", poolKey); | |
| // Provide full-range liquidity to the pool | |
| const tickLower = nearestUsableTick(TickMath.MIN_TICK, poolKey.tickSpacing); | |
| const tickUpper = nearestUsableTick(TickMath.MAX_TICK, poolKey.tickSpacing); | |
| // Get Slot0 values | |
| const slot0 = await stateView.getSlot0(poolId); | |
| // Get Liquidity | |
| const _liquidity = await stateView.getLiquidity(poolId); | |
| // Slot0 values | |
| const sqrtRatioX96 = JSBI.BigInt(slot0.sqrtPriceX96.toString()); // JSBI.BigInt("501066558273621464884319660560427"); | |
| const tick = slot0.tick; // TickMath.getTickAtSqrtRatio(sqrtRatioX96); | |
| const liquidity = JSBI.BigInt(_liquidity.toString()); // JSBI.BigInt("12649523095253078069592"); | |
| // Tick Bitmap | |
| const tickBitmap: TickConstructorArgs[] = [ | |
| { | |
| index: tickLower, | |
| liquidityNet: liquidity, | |
| liquidityGross: liquidity, | |
| }, | |
| { | |
| index: tickUpper, | |
| liquidityNet: JSBI.multiply(liquidity, JSBI.BigInt(-1)), | |
| liquidityGross: liquidity, | |
| }, | |
| ]; | |
| // Pool Instance | |
| const pool = new Pool( | |
| currencyNative, | |
| currency1, | |
| fee, | |
| tickSpacing, | |
| ADDRESS_ZERO, | |
| sqrtRatioX96, | |
| liquidity, | |
| tick, | |
| tickBitmap, | |
| ); | |
| console.log("Pool", pool); | |
| // Position Instance | |
| const position = new Position({ | |
| pool, | |
| liquidity, | |
| tickLower, | |
| tickUpper, | |
| }); | |
| const { amount0, amount1 } = position.mintAmounts; | |
| console.log("Amount0: ", amount0.toString()); | |
| console.log("Amount1: ", amount1.toString()); | |
| /// SWAP /// | |
| { | |
| console.log("ETH -> TKN3"); | |
| const inputAmount = CurrencyAmount.fromRawAmount(currencyNative, 1e18); // 1 ETH | |
| const [outputAmount] = await pool.getOutputAmount(inputAmount); | |
| console.log("Output Amount", outputAmount.quotient.toString()); | |
| console.log("Output Amount", outputAmount.toExact()); | |
| } | |
| { | |
| console.log("TKN3 -> ETH"); | |
| const inputAmount = CurrencyAmount.fromRawAmount(currency1, 1_000_000e18); // 1,000,000 TKN3 | |
| const [outputAmount] = await pool.getOutputAmount(inputAmount); | |
| console.log("Output Amount", outputAmount.quotient.toString()); | |
| console.log("Output Amount", outputAmount.toExact()); | |
| } | |
| { | |
| console.log("ETH <- TKN3"); | |
| const outputAmount = CurrencyAmount.fromRawAmount(currency1, 1_000_000e18); // 1,000,000 TKN3 | |
| const [inputAmount] = await pool.getInputAmount(outputAmount); | |
| console.log("Input Amount", inputAmount.quotient.toString()); | |
| console.log("Input Amount", inputAmount.toExact()); | |
| } | |
| { | |
| console.log("Three Swaps of ETH <- TKN3"); | |
| console.log("Current SqrtRatioX96", pool.sqrtRatioX96.toString()); | |
| const outputAmount = CurrencyAmount.fromRawAmount(currency1, 1_000_000e18); // 1,000,000 TKN3 | |
| const [inputAmount, updatedPool] = await pool.getInputAmount(outputAmount); | |
| console.log("Input Amount", inputAmount.toExact()); | |
| const [inputAmount1, updatedPool2] = await updatedPool.getInputAmount(outputAmount); | |
| console.log("Input Amount 1", inputAmount1.toExact()); | |
| const [inputAmount2, updatedPool3] = await updatedPool2.getInputAmount(outputAmount); | |
| console.log("Input Amount 2", inputAmount2.toExact()); | |
| console.log("Latest SqrtRatioX96", updatedPool3.sqrtRatioX96.toString()); | |
| } | |
| console.log("a: ", CurrencyAmount.fromFractionalAmount(currency1, 1_000_000, 18)); | |
| console.log("b: ", CurrencyAmount.fromFractionalAmount(currency1, 1_000_000, 18).quotient.toString()); | |
| } | |
| main() | |
| .then(() => process.exit(0)) | |
| .catch((error) => { | |
| console.error(error); | |
| process.exit(1); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Would love to see example for NFT v4 NFT id to current amounts, fees collected, ranges and currencies in ts. Happy to pay.