Files
lmsr-amm/research/get_quotes.js
2025-10-23 17:29:21 -04:00

460 lines
18 KiB
JavaScript

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