const { ethers } = require('ethers'); const { Token } = require('@uniswap/sdk-core'); const fs = require('fs'); // Token definitions const ChainId = { MAINNET: 1 }; const USDC_TOKEN = new Token( ChainId.MAINNET, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 6, 'USDC', 'USDC' ); const USDT_TOKEN = new Token( ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'USDT' ); // Configuration const QUOTER_ADDRESS = '0x52f0e24d1c21c8a0cb1e5a5dd6198556bd9e1203'; const POSITION_MANAGER_ADDRESS = '0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e'; // Pool ID to fetch pool key from const POOL_ID = '0x8aa4e11cbdf30eedc92100f4c8a31ff748e201d44712cc8c90d189edaa8e4e47'; // 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; } } // Convert sqrtPriceX96 to human-readable price function sqrtPriceX96ToPrice(sqrtPriceX96, decimals0, decimals1) { const Q96 = ethers.BigNumber.from(2).pow(96); const sqrtPrice = ethers.BigNumber.from(sqrtPriceX96); // Calculate price = (sqrtPriceX96 / 2^96)^2 const numerator = sqrtPrice.mul(sqrtPrice); const denominator = Q96.mul(Q96); // Adjust for token decimals const decimalAdjustment = ethers.BigNumber.from(10).pow(decimals0 - decimals1); // Convert to float for readability return parseFloat(numerator.toString()) / parseFloat(denominator.toString()) * parseFloat(decimalAdjustment.toString()); } // 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 human-readable price const price = sqrtPriceX96ToPrice(slot0.sqrtPriceX96, 6, 6); // USDC=6, USDT=6 const inversePrice = 1 / price; console.log('\n=== Pool State ==='); console.log('Block:', blockNumber); console.log('sqrtPriceX96:', slot0.sqrtPriceX96.toString()); console.log('Tick:', slot0.tick.toString()); console.log('Price (USDT per USDC):', price.toFixed(8)); console.log('Price (USDC per USDT):', inversePrice.toFixed(8)); console.log('LP Fee:', slot0.lpFee, `(${(slot0.lpFee / 10000).toFixed(2)}%)`); return { sqrtPriceX96: slot0.sqrtPriceX96.toString(), tick: slot0.tick.toString(), price: price, // Human-readable price inversePrice: inversePrice, 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 let currentAmount = 1; // Start with 1 token const multiplier = 1.3; // 30% increase const maxIterations = 200; // Limit to 5 iterations for testing // Test USDC→USDT (forward direction) console.log('\n=== USDC → USDT (Forward Direction) ==='); for (let i = 0; i < maxIterations; i++) { // Check if currentAmount exceeds 10 million if (currentAmount > 10000000) { console.log(`\nReached maximum amount limit of 1 million USDC. Stopping iterations.`); break; } console.log(`\n--- Iteration ${i + 1}: ${currentAmount.toFixed(6)} USDC ---`); const amountIn = ethers.utils.parseUnits(currentAmount.toFixed(6), USDC_TOKEN.decimals); // Make the call at the specific block using overrides const quotedAmountOut = await quoterContract.callStatic.quoteExactInputSingle({ poolKey: poolKey, zeroForOne: true, exactAmount: amountIn.toString(), hookData: '0x00', }, { blockTag: targetBlock // Query at specific historical block }); const actualAmountOut = ethers.BigNumber.from(quotedAmountOut.amountOut); // Calculate ideal amount out (1:1 ratio for stablecoins) const idealAmountOut = amountIn; // Since both USDC and USDT have 6 decimals // Calculate the difference from ideal const difference = actualAmountOut.sub(idealAmountOut); const isPositive = difference.gte(0); // Calculate slippage in basis points and percentage const slippageBasisPoints = difference.mul(10000).div(idealAmountOut); const slippagePercentage = parseFloat(slippageBasisPoints.toString()) / 100; // Calculate effective exchange rate const effectiveRate = parseFloat(ethers.utils.formatUnits(actualAmountOut, USDT_TOKEN.decimals)) / currentAmount; // Log results for this amount console.log(`Amount In: ${currentAmount.toFixed(6)} USDC`); console.log(`Amount Out: ${ethers.utils.formatUnits(actualAmountOut, USDT_TOKEN.decimals)} USDT`); console.log(`Effective Rate: ${effectiveRate.toFixed(6)} USDT/USDC`); console.log(`Ideal Rate (1:1): ${currentAmount.toFixed(6)} USDT`); console.log(`Difference: ${isPositive ? '+' : ''}${ethers.utils.formatUnits(difference, USDT_TOKEN.decimals)} USDT`); console.log(`Slippage: ${isPositive ? '+' : ''}${slippagePercentage.toFixed(4)}% (${isPositive ? '+' : ''}${slippageBasisPoints.toString()} basis points)`); // Store result resultsForward.push({ iteration: i + 1, amountIn: currentAmount, amountInFormatted: currentAmount.toFixed(6) + ' USDC', amountOut: ethers.utils.formatUnits(actualAmountOut, USDT_TOKEN.decimals) + ' USDT', effectiveRate: effectiveRate, slippagePercentage: slippagePercentage, slippageBasisPoints: parseInt(slippageBasisPoints.toString()), isPositive: isPositive }); // Increase amount by 30% for next iteration currentAmount *= multiplier; } // Reset current amount for reverse direction currentAmount = 1; // Test USDT→USDC (reverse direction) console.log('\n\n=== USDT → USDC (Reverse Direction) ==='); for (let i = 0; i < maxIterations; i++) { // Check if currentAmount exceeds 1 million if (currentAmount > 10000000) { console.log(`\nReached maximum amount limit of 1 million USDT. Stopping iterations.`); break; } console.log(`\n--- Iteration ${i + 1}: ${currentAmount.toFixed(6)} USDT ---`); const amountIn = ethers.utils.parseUnits(currentAmount.toFixed(6), USDT_TOKEN.decimals); // Make the call at the specific block using overrides with zeroForOne: false const quotedAmountOut = await quoterContract.callStatic.quoteExactInputSingle({ poolKey: poolKey, zeroForOne: false, // false for USDT→USDC exactAmount: amountIn.toString(), hookData: '0x00', }, { blockTag: targetBlock // Query at specific historical block }); const actualAmountOut = ethers.BigNumber.from(quotedAmountOut.amountOut); // Calculate ideal amount out (1:1 ratio for stablecoins) const idealAmountOut = amountIn; // Since both USDC and USDT have 6 decimals // Calculate the difference from ideal const difference = actualAmountOut.sub(idealAmountOut); const isPositive = difference.gte(0); // Calculate slippage in basis points and percentage const slippageBasisPoints = difference.mul(10000).div(idealAmountOut); const slippagePercentage = parseFloat(slippageBasisPoints.toString()) / 100; // Calculate effective exchange rate const effectiveRate = parseFloat(ethers.utils.formatUnits(actualAmountOut, USDC_TOKEN.decimals)) / currentAmount; // Log results for this amount console.log(`Amount In: ${currentAmount.toFixed(6)} USDT`); console.log(`Amount Out: ${ethers.utils.formatUnits(actualAmountOut, USDC_TOKEN.decimals)} USDC`); console.log(`Effective Rate: ${effectiveRate.toFixed(6)} USDC/USDT`); console.log(`Ideal Rate (1:1): ${currentAmount.toFixed(6)} USDC`); console.log(`Difference: ${isPositive ? '+' : ''}${ethers.utils.formatUnits(difference, USDC_TOKEN.decimals)} USDC`); console.log(`Slippage: ${isPositive ? '+' : ''}${slippagePercentage.toFixed(4)}% (${isPositive ? '+' : ''}${slippageBasisPoints.toString()} basis points)`); // Store result resultsReverse.push({ iteration: i + 1, amountIn: currentAmount, amountInFormatted: currentAmount.toFixed(6) + ' USDT', amountOut: ethers.utils.formatUnits(actualAmountOut, USDC_TOKEN.decimals) + ' USDC', effectiveRate: effectiveRate, slippagePercentage: slippagePercentage, slippageBasisPoints: parseInt(slippageBasisPoints.toString()), isPositive: isPositive }); // Increase amount by 30% for next iteration currentAmount *= multiplier; } // Summary console.log('\n\n=== Summary of Results ==='); console.log('\n--- Forward Direction (USDC → USDT) ---'); console.log('\nAmount In → Amount Out (Rate) [Slippage]'); resultsForward.forEach(r => { console.log(`${r.amountInFormatted} → ${r.amountOut} (${r.effectiveRate.toFixed(6)}) [${r.isPositive ? '+' : ''}${r.slippagePercentage.toFixed(4)}%]`); }); console.log('\n--- Reverse Direction (USDT → USDC) ---'); console.log('\nAmount In → Amount Out (Rate) [Slippage]'); resultsReverse.forEach(r => { console.log(`${r.amountInFormatted} → ${r.amountOut} (${r.effectiveRate.toFixed(6)}) [${r.isPositive ? '+' : ''}${r.slippagePercentage.toFixed(4)}%]`); }); 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 function writeResultsToCSV(blockNumber, priceData, quoteResults) { const filename = `swap_results_block_${blockNumber}.csv`; // Create CSV header let csvContent = 'Block Number,USDT per USDC,USDC per USDT,Forward Direction Amount In,Forward Direction Amount Out,Reverse Direction Amount In,Reverse Direction Amount Out\n'; // Get max number of iterations const maxIterations = Math.max(quoteResults.forward.length, quoteResults.reverse.length); // Add data rows for (let i = 0; i < maxIterations; i++) { const forwardResult = quoteResults.forward[i] || { amountIn: '', amountOut: '' }; const reverseResult = quoteResults.reverse[i] || { amountIn: '', amountOut: '' }; // Only include block number and prices in first row if (i === 0) { csvContent += `${blockNumber},${priceData.price.toFixed(8)},${priceData.inversePrice.toFixed(8)},`; } else { csvContent += `,,,`; } // Extract just the numbers from the formatted strings const forwardAmountIn = forwardResult.amountIn ? forwardResult.amountIn.toFixed(6) : ''; const forwardAmountOut = forwardResult.amountOut ? forwardResult.amountOut.split(' ')[0] : ''; const reverseAmountIn = reverseResult.amountIn ? reverseResult.amountIn.toFixed(6) : ''; const reverseAmountOut = reverseResult.amountOut ? reverseResult.amountOut.split(' ')[0] : ''; csvContent += `${forwardAmountIn},${forwardAmountOut},${reverseAmountIn},${reverseAmountOut}\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);