diff --git a/adhoc_script/swap_sim.js b/adhoc_script/swap_sim.js deleted file mode 100644 index e69de29..0000000 diff --git a/adhoc_scripts/get_quotes.js b/adhoc_scripts/get_quotes.js new file mode 100644 index 0000000..f205b0b --- /dev/null +++ b/adhoc_scripts/get_quotes.js @@ -0,0 +1,459 @@ +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 1 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);