import { ethers } from 'ethers'; import { Token } from '@uniswap/sdk-core'; import fs from 'fs'; // // TOKEN DEFINITIONS // const ChainId = { MAINNET: 1 }; const USDC_TOKEN = new Token( ChainId.MAINNET, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 6, 'USDC', 'USDC' ); const WETH_TOKEN = new Token( ChainId.MAINNET, '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 18, 'WETH', 'WETH' ); const USDT_TOKEN= new Token( ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'USDT' ); // // POOL DEFINITION // const tokenA = USDC_TOKEN const tokenB = WETH_TOKEN // Pool ID to fetch pool key from // const POOL_ID = '0x8aa4e11cbdf30eedc92100f4c8a31ff748e201d44712cc8c90d189edaa8e4e47'; // USDT-USDC 0.0010% // const POOL_ID = '0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d'; // USDC-WETH 0.30% const POOL_ID = '0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d'; // USDC-WETH 0.05% // Configuration const QUOTER_ADDRESS = '0x52f0e24d1c21c8a0cb1e5a5dd6198556bd9e1203'; const POSITION_MANAGER_ADDRESS = '0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e'; // Providers const anvilProvider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545'); // Quoter contract ABI const QUOTER_ABI = [ { inputs: [ { components: [ { components: [ { name: 'currency0', type: 'address' }, { name: 'currency1', type: 'address' }, { name: 'fee', type: 'uint24' }, { name: 'tickSpacing', type: 'int24' }, { name: 'hooks', type: 'address' } ], name: 'poolKey', type: 'tuple' }, { name: 'zeroForOne', type: 'bool' }, { name: 'exactAmount', type: 'uint128' }, { name: 'hookData', type: 'bytes' } ], name: 'params', type: 'tuple' } ], name: 'quoteExactInputSingle', outputs: [ { components: [ { name: 'amountOut', type: 'uint256' } ], name: 'result', type: 'tuple' } ], stateMutability: 'view', type: 'function' } ]; // StateView ABI for getSlot0 and getLiquidity const STATE_VIEW_ABI = [ { inputs: [ // { name: 'manager', type: 'address' }, { name: 'poolId', type: 'bytes32' } ], name: 'getSlot0', outputs: [ { name: 'sqrtPriceX96', type: 'uint160' }, { name: 'tick', type: 'int24' }, { name: 'protocolFee', type: 'uint24' }, { name: 'lpFee', type: 'uint24' } ], stateMutability: 'view', type: 'function' }, ]; // StateView contract address for reading pool state const STATE_VIEW_ADDRESS = '0x7ffe42c4a5deea5b0fec41c94c136cf115597227'; // Position Manager ABI const POSITION_MANAGER_ABI = [ { inputs: [{ name: 'poolId', type: 'bytes25' }], name: 'poolKeys', outputs: [ { components: [ { name: 'currency0', type: 'address' }, { name: 'currency1', type: 'address' }, { name: 'fee', type: 'uint24' }, { name: 'tickSpacing', type: 'int24' }, { name: 'hooks', type: 'address' } ], name: 'poolKey', type: 'tuple' } ], stateMutability: 'view', type: 'function' } ]; // Function to get pool key from Position Manager (uses mainnet) async function getPoolKey() { try { const positionManager = new ethers.Contract( POSITION_MANAGER_ADDRESS, POSITION_MANAGER_ABI, anvilProvider ); // Extract first 25 bytes (50 hex chars + 0x prefix = 52 chars) const poolIdBytes25 = POOL_ID.slice(0, 52); const poolKey = await positionManager.poolKeys(poolIdBytes25); return { currency0: poolKey.currency0, currency1: poolKey.currency1, fee: poolKey.fee, tickSpacing: poolKey.tickSpacing, hooks: poolKey.hooks }; } catch (error) { console.error('Error fetching pool key:', error.message); throw error; } } // Helper function to format BigNumber for display (use only for console output) function formatBigNumberForDisplay(bigNumber, decimals) { const formatted = ethers.utils.formatUnits(bigNumber, decimals); return formatted; } // Helper function to convert BigNumber to fixed decimal string without scientific notation function bigNumberToFixed(bigNumber, decimals) { const str = ethers.utils.formatUnits(bigNumber, decimals); // Ensure no scientific notation if (str.includes('e')) { const num = parseFloat(str); return num.toFixed(decimals); } return str; } // Convert sqrtPriceX96 to price as a decimal string (maintains precision) // Returns price representing token1/token0 function sqrtPriceX96ToPrice(sqrtPriceX96, decimals0, decimals1) { const sqrtPrice = ethers.BigNumber.from(sqrtPriceX96); console.log('DEBUG sqrtPriceX96ToPrice:'); console.log(' sqrtPriceX96:', sqrtPriceX96.toString()); console.log(' decimals0:', decimals0, 'decimals1:', decimals1); if (sqrtPrice.isZero()) { throw new Error('sqrtPriceX96 cannot be zero'); } // Calculate price = (sqrtPriceX96 / 2^96)^2 * 10^(decimals0 - decimals1) // Using BigNumber arithmetic throughout to maintain precision const Q96 = ethers.BigNumber.from(2).pow(96); // price = sqrtPrice^2 / 2^192 * 10^(decimals0 - decimals1) // To maintain precision, we need to be careful about the order of operations // First square the sqrtPrice const sqrtPriceSquared = sqrtPrice.mul(sqrtPrice); console.log(' sqrtPriceSquared:', sqrtPriceSquared.toString()); // Calculate 2^192 const Q192 = Q96.mul(Q96); console.log(' Q192:', Q192.toString()); // Adjust for decimals: if decimals0 > decimals1, we multiply, else divide const decimalDiff = decimals0 - decimals1; console.log(' decimalDiff:', decimalDiff); // We need to calculate: sqrtPriceSquared * 10^decimalDiff / Q192 // Use a large precision scale to avoid truncation // We'll use 30 decimals to ensure we have enough precision even for extreme price ratios const PRECISION_DECIMALS = 30; const precisionScale = ethers.BigNumber.from(10).pow(PRECISION_DECIMALS); let price; if (decimalDiff >= 0) { const decimalAdjustment = ethers.BigNumber.from(10).pow(decimalDiff); const numerator = sqrtPriceSquared.mul(decimalAdjustment).mul(precisionScale); console.log(' numerator:', numerator.toString()); price = numerator.div(Q192); } else { // When decimalDiff is negative, multiply denominator instead of dividing numerator // This avoids precision loss const decimalAdjustment = ethers.BigNumber.from(10).pow(-decimalDiff); const numerator = sqrtPriceSquared.mul(precisionScale); console.log(' numerator:', numerator.toString()); const denominator = Q192.mul(decimalAdjustment); console.log(' denominator:', denominator.toString()); price = numerator.div(denominator); } console.log(' price (raw):', price.toString()); const priceFormatted = ethers.utils.formatUnits(price, PRECISION_DECIMALS); console.log(' price (formatted):', priceFormatted); return priceFormatted; } // Helper to calculate inverse price from a decimal string function inversePrice(priceStr) { const priceNum = parseFloat(priceStr); if (priceNum === 0 || !isFinite(priceNum)) { console.warn('Warning: Cannot calculate inverse of zero or invalid price'); return '0'; } // Calculate 1/price using standard division const inverse = 1.0 / priceNum; // If result is very large, format as integer-like // If result is normal decimal, format with precision if (inverse > 1e10) { return inverse.toExponential(18).replace(/e\+?/, 'e'); } // Format with high precision, removing trailing zeros let formatted = inverse.toFixed(18); // Remove trailing zeros but keep at least one decimal place formatted = formatted.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '.0'); return formatted; } // Get pool price using StateView async function getPoolPriceWithSDK(blockNumber) { try { // Create StateView contract with ethers const stateViewContract = new ethers.Contract( STATE_VIEW_ADDRESS, STATE_VIEW_ABI, anvilProvider ); // Get slot0 data const slot0 = await stateViewContract.getSlot0(POOL_ID, { blockTag: blockNumber }); // Convert sqrtPriceX96 to price as BigNumber (maintains precision) console.log('\n=== Pool State (Debug) ==='); console.log('Block:', blockNumber); console.log('sqrtPriceX96:', slot0.sqrtPriceX96.toString()); console.log('Tick:', slot0.tick.toString()); console.log('Token A decimals:', tokenA.decimals); console.log('Token B decimals:', tokenB.decimals); // Calculate price: token0/token1 = USDC/WETH // Using swapped decimals gives us the correct direction const token0PerToken1 = sqrtPriceX96ToPrice(slot0.sqrtPriceX96, tokenB.decimals, tokenA.decimals); console.log('Price USDC per WETH:', token0PerToken1); // Calculate the reciprocal to get WETH per USDC const token0PerToken1Num = parseFloat(token0PerToken1); const token1PerToken0 = (1.0 / token0PerToken1Num).toFixed(18).replace(/\.?0+$/, ''); console.log('Price WETH per USDC (reciprocal):', token1PerToken0); console.log('\n=== Pool State ==='); console.log('Block:', blockNumber); console.log('sqrtPriceX96:', slot0.sqrtPriceX96.toString()); console.log('Tick:', slot0.tick.toString()); console.log(`Price calculated as: token1/token0 where token0=${tokenA.symbol}(${tokenA.decimals}d), token1=${tokenB.symbol}(${tokenB.decimals}d)`); console.log(`${tokenB.symbol} per ${tokenA.symbol}:`, token1PerToken0); console.log(`${tokenA.symbol} per ${tokenB.symbol}:`, token0PerToken1); console.log('LP Fee:', slot0.lpFee, `(${(slot0.lpFee / 10000).toFixed(2)}%)`); // Sanity check with actual swap data console.log('\nSanity check: If swapping 1 USDC should get ~0.000256 WETH based on your data'); return { sqrtPriceX96: slot0.sqrtPriceX96.toString(), tick: slot0.tick.toString(), priceStr: token1PerToken0, // WETH per USDC (tokenB per tokenA) inversePriceStr: token0PerToken1, // USDC per WETH (tokenA per tokenB) protocolFee: slot0.protocolFee, lpFee: slot0.lpFee }; } catch (error) { console.error('Error getting pool price:', error.message); throw error; } } // Get quote from historical block (10 blocks back) with multiple amount testing async function getQuoteFromHistoricalBlock(poolKey, targetBlock) { try { // Get starting pool price using SDK try { await getPoolPriceWithSDK(targetBlock); } catch (error) { console.error('Error getting pool price with SDK:', error.message); } console.log('\n=== Testing Multiple Input Amounts ==='); console.log('Testing amounts from 1 USDC, increasing by 30% each iteration (limited to 5 iterations)'); console.log('Testing both directions: USDC→USDT and USDT→USDC\n'); // Create a new provider instance that queries at specific block const quoterContract = new ethers.Contract(QUOTER_ADDRESS, QUOTER_ABI, anvilProvider); const resultsForward = []; // USDC→USDT const resultsReverse = []; // USDT→USDC // Start with 1 token as BigNumber let currentAmountBN = ethers.utils.parseUnits("1", tokenA.decimals); const multiplierNumerator = 13; // 130% = 13/10 const multiplierDenominator = 10; const maxIterations = 200; const maxAmount = ethers.utils.parseUnits("1000000", tokenA.decimals); // 10 million limit // Test USDC→USDT (forward direction) console.log('\n=== USDC → USDT (Forward Direction) ==='); for (let i = 0; i < maxIterations; i++) { // Check if currentAmountBN exceeds 10 million if (currentAmountBN.gt(maxAmount)) { console.log(`\nReached maximum amount limit of 10 million USDC. Stopping iterations.`); break; } const currentAmountFormatted = formatBigNumberForDisplay(currentAmountBN, tokenA.decimals); console.log(`\n--- Iteration ${i + 1}: ${currentAmountFormatted} USDC ---`); const amountIn = currentAmountBN; // Make the call at the specific block using overrides const quotedAmountOut = await quoterContract.callStatic.quoteExactInputSingle({ poolKey: poolKey, zeroForOne: tokenA.address.toLowerCase() > tokenB.address.toLowerCase(), exactAmount: amountIn.toString(), hookData: '0x00', }, { blockTag: targetBlock // Query at specific historical block }); console.log('in/out', amountIn, quotedAmountOut.amountIn, quotedAmountOut.amountOut); const actualAmountOut = ethers.BigNumber.from(quotedAmountOut.amountOut); // Calculate effective exchange rate as BigNumber (with extra precision) // effectiveRate = actualAmountOut / amountIn // Scale to show tokenB per 1 tokenA (use tokenB decimals for result) const scaleFactor = ethers.BigNumber.from(10).pow(tokenB.decimals); const effectiveRateBN = actualAmountOut.mul(scaleFactor).div(amountIn); // Log results for this amount (format only for display) console.log(`Amount In: ${currentAmountFormatted} ${tokenA.symbol}`); console.log(`Amount Out: ${formatBigNumberForDisplay(actualAmountOut, tokenB.decimals)} ${tokenB.symbol}`); // Store result with full precision (as strings) resultsForward.push({ iteration: i + 1, amountInBN: amountIn.toString(), amountOutBN: actualAmountOut.toString(), effectiveRateBN: effectiveRateBN.toString(), }); // Increase amount by 30% for next iteration using BigNumber math currentAmountBN = currentAmountBN.mul(multiplierNumerator).div(multiplierDenominator); } // Summary console.log('\n\n=== Summary of Results ==='); console.log(`--- Forward Direction (${tokenA.symbol} → ${tokenB.symbol}) ---`); console.log('\nAmount In → Amount Out (Rate) [Slippage]'); resultsForward.forEach(r => { const amountIn = formatBigNumberForDisplay(ethers.BigNumber.from(r.amountInBN), tokenA.decimals); const amountOut = formatBigNumberForDisplay(ethers.BigNumber.from(r.amountOutBN), tokenB.decimals); const rate = formatBigNumberForDisplay(ethers.BigNumber.from(r.effectiveRateBN), tokenB.decimals); console.log(`${amountIn} ${tokenA.symbol} → ${amountOut} ${tokenB.symbol} (${rate})`); }); return { blockNumber: targetBlock, forward: resultsForward, reverse: resultsReverse }; } catch (error) { console.error('Error getting historical quote:', error.message); throw error; } } // Function to write results to CSV with full precision function writeResultsToCSV(blockNumber, priceData, quoteResults) { const filename = `swap_results_block_${blockNumber}.csv`; // Create CSV header let csvContent = `block,${tokenB.symbol} per ${tokenA.symbol},${tokenA.symbol} per ${tokenB.symbol},Amount In,Amount Out,Effective Rate\n`; // Get max number of iterations const maxIterations = quoteResults.forward.length; // Add data rows for (let i = 0; i < maxIterations; i++) { const forwardResult = quoteResults.forward[i]; // Use full precision for prices (no scientific notation) const priceStr = priceData.priceStr; const inversePriceStr = priceData.inversePriceStr; csvContent += `${blockNumber},${priceStr},${inversePriceStr},`; // Forward direction data with full precision const amountInFormatted = bigNumberToFixed(ethers.BigNumber.from(forwardResult.amountInBN), tokenA.decimals); const amountOutFormatted = bigNumberToFixed(ethers.BigNumber.from(forwardResult.amountOutBN), tokenB.decimals); const effectiveRate = bigNumberToFixed(ethers.BigNumber.from(forwardResult.effectiveRateBN), tokenB.decimals); csvContent += `${amountInFormatted},${amountOutFormatted},${effectiveRate}\n`; } // Write to file fs.writeFileSync(filename, csvContent); console.log(`\n✅ Results written to ${filename}`); } // Main function async function main() { console.log('=== Uniswap V4 Quote and Gas Estimation ===\n'); // Get current block const currentBlock = await anvilProvider.getBlockNumber(); const targetBlock = currentBlock - 10; // Fetch pool key from Position Manager let poolKey; try { poolKey = await getPoolKey(); } catch (error) { console.error('Failed to fetch pool key. Exiting.'); return; } // Get pool price data let poolPriceData; try { poolPriceData = await getPoolPriceWithSDK(targetBlock); } catch (error) { console.error('Failed to get pool price data:', error.message); return; } // Get quotes for both directions try { const quoteResults = await getQuoteFromHistoricalBlock(poolKey, targetBlock); console.log('✅ Historical quote successful!'); // Write results to CSV writeResultsToCSV(targetBlock, poolPriceData, quoteResults); } catch (error) { console.error('❌ Failed to get historical quote:', error.message); } } // Run the main function main().catch(console.error);