Files
lmsr-amm/research/uni4_quotes/get_quotes.js
2025-10-29 18:22:23 -04:00

506 lines
18 KiB
JavaScript

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);