ETH-USDC pool_design; uni4 quotes refactor
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,7 +5,7 @@
|
|||||||
/.env
|
/.env
|
||||||
/.env-*
|
/.env-*
|
||||||
/.idea/
|
/.idea/
|
||||||
|
package-lock.json
|
||||||
/broadcast
|
/broadcast
|
||||||
|
|
||||||
*secret*
|
*secret*
|
||||||
|
|||||||
@@ -1,459 +0,0 @@
|
|||||||
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);
|
|
||||||
4031
research/package-lock.json
generated
4031
research/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,14 +7,15 @@ import numpy as np
|
|||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
LMSR_FEE = 0.0025
|
||||||
|
# UNISWAP_GAS=0
|
||||||
|
# LMSR_GAS=0
|
||||||
UNISWAP_GAS=115_000
|
UNISWAP_GAS=115_000
|
||||||
LMSR_GAS=119_000
|
LMSR_GAS=150_000
|
||||||
ETH_PRICE=4500
|
ETH_PRICE=4000
|
||||||
UNISWAP_GAS_COST=UNISWAP_GAS*ETH_PRICE/1e9
|
UNISWAP_GAS_COST=UNISWAP_GAS*ETH_PRICE/1e9
|
||||||
LMSR_GAS_COST=LMSR_GAS*ETH_PRICE/1e9
|
LMSR_GAS_COST=LMSR_GAS*ETH_PRICE/1e9
|
||||||
|
|
||||||
LMSR_FEE = 0.000010
|
|
||||||
|
|
||||||
print(f' LMSR gas: ${LMSR_GAS_COST:.2}')
|
print(f' LMSR gas: ${LMSR_GAS_COST:.2}')
|
||||||
print(f'Uniswap gas: ${UNISWAP_GAS_COST:.2}')
|
print(f'Uniswap gas: ${UNISWAP_GAS_COST:.2}')
|
||||||
@@ -96,9 +97,9 @@ def lmsr_swap_amount_out(
|
|||||||
# No available output to withdraw
|
# No available output to withdraw
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
# Compute r0 = exp((q_i - q_j) / b)
|
# Compute r0 = exp((q_j - q_i) / b) so small-trade out/in ≈ marginal price p_j/p_i
|
||||||
try:
|
try:
|
||||||
r0 = math.exp((qi - qj) / b)
|
r0 = math.exp((qj - qi) / b)
|
||||||
except OverflowError:
|
except OverflowError:
|
||||||
raise ArithmeticError("exponential overflow in r0 computation")
|
raise ArithmeticError("exponential overflow in r0 computation")
|
||||||
|
|
||||||
@@ -138,24 +139,81 @@ def lmsr_swap_amount_out(
|
|||||||
|
|
||||||
return float(amount_out)
|
return float(amount_out)
|
||||||
|
|
||||||
|
def lmsr_marginal_price(balances, base_index, quote_index, kappa):
|
||||||
|
"""
|
||||||
|
Compute the LMSR marginal price ratio p_quote / p_base for the given balances state.
|
||||||
|
|
||||||
def compare():
|
Formula:
|
||||||
kappa = 10
|
b = kappa * S, where S = sum(balances)
|
||||||
balance0 = 10_000_000 # estimated from the production pool
|
price = exp((q_quote - q_base) / b)
|
||||||
balances = [balance0, balance0]
|
|
||||||
X = np.geomspace(1, 10_000_000, 100)
|
|
||||||
Y = [max(0, 1 -
|
|
||||||
(lmsr_swap_amount_out(balances, float(amount_in), 0, 1, LMSR_FEE, kappa) - LMSR_GAS_COST)
|
|
||||||
/ amount_in)
|
|
||||||
for amount_in in X]
|
|
||||||
plt.plot(X, Y, label=f'LMSR {kappa:.2f}')
|
|
||||||
|
|
||||||
d = pd.read_csv('swap_results_block_23640998.csv')
|
Parameters:
|
||||||
d.columns = ['block', 'price0', 'price1', 'in0', 'out0', 'in1', 'out1']
|
- balances: iterable of per-token balances (q_i)
|
||||||
uniswap_slippage0 = 1 - (d.out0 - UNISWAP_GAS_COST) / d.in0 / d.iloc[0].price0
|
- base_index: index of the base token
|
||||||
plt.plot(d.in0, uniswap_slippage0, label='CP0')
|
- quote_index: index of the quote token
|
||||||
# uniswap_slippage1 = 1 - (d.out1 - UNISWAP_GAS_COST) / d.in1 / d.iloc[0].price1
|
- kappa: liquidity parameter κ (must be positive)
|
||||||
# plt.plot(d.in1, uniswap_slippage1, label='CP1')
|
|
||||||
|
Returns:
|
||||||
|
- float: marginal price p_quote / p_base
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
q = [float(x) for x in balances]
|
||||||
|
k = float(kappa)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
raise ValueError("Invalid numeric input") from e
|
||||||
|
|
||||||
|
n = len(q)
|
||||||
|
if not (0 <= base_index < n and 0 <= quote_index < n):
|
||||||
|
raise IndexError("token indices out of range")
|
||||||
|
if k <= 0.0:
|
||||||
|
raise ValueError("kappa must be positive")
|
||||||
|
|
||||||
|
S = sum(q)
|
||||||
|
if S <= 0.0:
|
||||||
|
raise ValueError("size metric (sum balances) must be positive")
|
||||||
|
|
||||||
|
b = k * S
|
||||||
|
if b <= 0.0:
|
||||||
|
raise ValueError("computed b must be positive")
|
||||||
|
|
||||||
|
return float(math.exp((q[quote_index] - q[base_index]) / b))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def compare(file, tvl, kappa):
|
||||||
|
d = pd.read_csv(file)
|
||||||
|
d.columns = ['block', 'price0', 'price1', 'in0', 'out0', 'rate']
|
||||||
|
|
||||||
|
# Calibrate LMSR balances so that exp((q1 - q0)/b) equals the initial price
|
||||||
|
p0 = float(d.iloc[0].price0)
|
||||||
|
S = float(tvl) # choose the LMSR size metric
|
||||||
|
b = kappa * S
|
||||||
|
delta = b * math.log(p0) # q1 - q0
|
||||||
|
q0 = 0.5 * (S - delta)
|
||||||
|
q1 = 0.5 * (S + delta)
|
||||||
|
if q0 <= 0.0 or q1 <= 0.0:
|
||||||
|
raise ValueError("Invalid LMSR calibration: choose kappa such that kappa * ln(price0) < 1.")
|
||||||
|
balances = [q0, q1]
|
||||||
|
print(balances)
|
||||||
|
X = np.geomspace(1, 1_000_000, 100)
|
||||||
|
orig_price = lmsr_marginal_price(balances, 0, 1, kappa)
|
||||||
|
in_out = [(float(amount_in), lmsr_swap_amount_out(balances, float(amount_in), 0, 1, LMSR_FEE, kappa)) for amount_in in X]
|
||||||
|
print(in_out)
|
||||||
|
# Relative execution price deviation from the initial marginal price:
|
||||||
|
# slippage = |(amount_out/amount_in)/orig_price - 1|
|
||||||
|
eps = 1e-12
|
||||||
|
Y = [max(eps, abs((amount_out / amount_in) / orig_price - 1.0))
|
||||||
|
for amount_in, amount_out in in_out]
|
||||||
|
plt.plot(X, Y, label=f'LMSR {LMSR_FEE:.2%} κ={kappa:.2f}', color='cornflowerblue')
|
||||||
|
|
||||||
|
# Uniswap execution price deviation from its initial quoted price:
|
||||||
|
# slippage = |(out/in)/initial_price - 1|
|
||||||
|
uniswap_exec_price0 = d.out0 / d.in0
|
||||||
|
uniswap_slippage0 = (uniswap_exec_price0 / d.iloc[0].price0 - 1.0).abs().clip(lower=1e-12)
|
||||||
|
uniswap_fee = round(uniswap_slippage0.iloc[0], 6)
|
||||||
|
plt.plot(d.in0, uniswap_slippage0, label=f'Uniswap {uniswap_fee:.2%}', color='hotpink')
|
||||||
|
# uniswap_slippage1 = |(out1/in1)/price1 - 1|
|
||||||
|
# plt.plot(d.in1, (d.out1 / d.in1 / d.iloc[0].price1 - 1.0).abs().clip(lower=1e-12), label='CP1')
|
||||||
|
|
||||||
# Interpolate Uniswap slippage to match LMSR x-coordinates
|
# Interpolate Uniswap slippage to match LMSR x-coordinates
|
||||||
interp_uniswap = np.interp(X, d.in0, uniswap_slippage0)
|
interp_uniswap = np.interp(X, d.in0, uniswap_slippage0)
|
||||||
@@ -166,6 +224,7 @@ def compare():
|
|||||||
plt.yscale('log')
|
plt.yscale('log')
|
||||||
plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: '{:g}'.format(x)))
|
plt.gca().xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: '{:g}'.format(x)))
|
||||||
plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.2%}'.format(y)))
|
plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda y, _: '{:.2%}'.format(y)))
|
||||||
|
plt.gca().set_ylim(top=.1)
|
||||||
plt.xlabel('Input Amount')
|
plt.xlabel('Input Amount')
|
||||||
plt.ylabel('Slippage')
|
plt.ylabel('Slippage')
|
||||||
plt.title('Pool Slippages')
|
plt.title('Pool Slippages')
|
||||||
@@ -197,5 +256,7 @@ def plot_kappa():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
compare()
|
# compare('uni4_quotes/swap_results_block_23640998.csv')
|
||||||
|
# compare('uni4_quotes/ETH-USDC-30.csv', 53_000_000, 0.1)
|
||||||
|
compare('uni4_quotes/ETH-USDC-30.csv', 1_00_000, .1)
|
||||||
# plot_kappa()
|
# plot_kappa()
|
||||||
|
|||||||
496
research/uni4_quotes/get_quotes.js
Normal file
496
research/uni4_quotes/get_quotes.js
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
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'
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokenA = USDC_TOKEN
|
||||||
|
const tokenB = WETH_TOKEN
|
||||||
|
// Pool ID to fetch pool key from
|
||||||
|
// const POOL_ID = '0x8aa4e11cbdf30eedc92100f4c8a31ff748e201d44712cc8c90d189edaa8e4e47';
|
||||||
|
const POOL_ID = '0xdce6394339af00981949f5f3baf27e3610c76326a700af57e4b3e3ae4977f78d';
|
||||||
|
|
||||||
|
// 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);
|
||||||
18
research/uni4_quotes/package.json
Normal file
18
research/uni4_quotes/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "uni4_quotes",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"author": "",
|
||||||
|
"type": "module",
|
||||||
|
"main": "get_quotes.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node get_quotes.js",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@uniswap/sdk-core": "^7.8.0",
|
||||||
|
"@uniswap/v4-sdk": "^1.22.0",
|
||||||
|
"ethers": "^5.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user