Files
web/scripts/adjust_pool_prices_and_stake.js

912 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Adjust Pool Prices Script
* Rebalances an existing LiqP pool's prices to match market prices.
* Uses IPartyInfo.swapToLimitAmounts() to compute the required swap size,
* then executes via pool.swap() (swapToLimit has a known implementation bug).
* Runs two passes to compensate for LMSR price coupling across assets.
* Optionally basket-stakes a configurable USD amount via mint().
*/
import { ethers } from 'ethers';
import { readFile } from 'fs/promises';
import { config } from 'dotenv';
import { execSync } from 'child_process';
config({ path: new URL('../.env', import.meta.url).pathname });
// ============================================================================
// CONFIGURATION
// ============================================================================
const NETWORK_CONFIG = {
mockchain: {
rpcUrl: 'http://localhost:8545',
chainId: '31337',
tokens: {
USDT: { address: '0xbdEd0D2bf404bdcBa897a74E6657f1f12e5C6fb6', coingeckoId: 'tether', decimals: 6 },
USDC: { address: '0x2910E325cf29dd912E3476B61ef12F49cb931096', coingeckoId: 'usd-coin', decimals: 6 }
}
},
mainnet: {
rpcUrl: process.env.MAINNET_RPC_URL,
chainId: '1',
tokens: {
USDT: { address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', coingeckoId: 'tether', decimals: 6 },
USDC: { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', coingeckoId: 'usd-coin', decimals: 6 },
WBTC: { address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', coingeckoId: 'wrapped-bitcoin', decimals: 8 },
WETH: { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', coingeckoId: 'weth', decimals: 18 },
UNI: { address: '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984', coingeckoId: 'uniswap', decimals: 18 },
WSOL: { address: '0xD31a59c85aE9D8edEFeC411D448f90841571b89c', coingeckoId: 'solana', decimals: 9 },
TRX: { address: '0x50327c6c5a14DCaDE707ABad2E27eB517df87AB5', coingeckoId: 'tron', decimals: 6 },
AAVE: { address: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', coingeckoId: 'aave', decimals: 18 },
PEPE: { address: '0x6982508145454Ce325dDbE47a25d4ec3d2311933', coingeckoId: 'pepe', decimals: 18 },
SHIB: { address: '0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE', coingeckoId: 'shiba-inu', decimals: 18 }
},
uniswap: {
swapRouter02: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
factory: '0x1F98431c8aD98523631AE4a59f267346ea31F984',
// Only 0.05% (500) and 0.3% (3000) tiers are used — 1% pools are avoided.
defaultFeeTier: 3000,
feeTierOverrides: { USDT: 500, USDC: 500, WBTC: 3000, WETH: 500 }
}
}
};
const NETWORK = process.env.NETWORK || 'mainnet';
const currentConfig = NETWORK_CONFIG[NETWORK];
if (!currentConfig) {
console.error(`[!] Invalid NETWORK: ${NETWORK}. Must be 'mockchain' or 'mainnet'`);
process.exit(1);
}
const TOKEN_CONFIG = currentConfig.tokens;
// Price deviation threshold: only swap if price is off by more than this
const PRICE_DEVIATION_THRESHOLD = 0.001; // 0.1%
// ============================================================================
// ABIs (minimal subset needed)
// ============================================================================
const ERC20ABI = [
{ type: 'function', name: 'balanceOf', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] },
{ type: 'function', name: 'decimals', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint8' }] },
{ type: 'function', name: 'symbol', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'string' }] },
{ type: 'function', name: 'approve', stateMutability: 'nonpayable', inputs: [{ name: 'spender', type: 'address' }, { name: 'amount', type: 'uint256' }], outputs: [{ name: '', type: 'bool' }] },
{ type: 'function', name: 'allowance', stateMutability: 'view', inputs: [{ name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] }
];
const IPartyPoolABI = [
{ type: 'function', name: 'allTokens', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address[]' }] },
{ type: 'function', name: 'totalSupply', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256' }] },
{ type: 'function', name: 'balances', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256[]' }] },
{ type: 'function', name: 'balanceOf', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }], outputs: [{ name: '', type: 'uint256' }] },
{ type: 'function', name: 'denominators', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint256[]' }] }
];
const IPartyInfoABI = [
{
type: 'function', name: 'price', stateMutability: 'view',
inputs: [{ name: 'pool', type: 'address' }, { name: 'baseTokenIndex', type: 'uint256' }, { name: 'quoteTokenIndex', type: 'uint256' }],
outputs: [{ name: '', type: 'uint256' }]
},
{
type: 'function', name: 'swapToLimitAmounts', stateMutability: 'view',
inputs: [{ name: 'pool', type: 'address' }, { name: 'inputTokenIndex', type: 'uint256' }, { name: 'outputTokenIndex', type: 'uint256' }, { name: 'limitPrice', type: 'int128' }],
outputs: [{ name: 'amountIn', type: 'uint256' }, { name: 'amountOut', type: 'uint256' }, { name: 'inFee', type: 'uint256' }]
},
{
type: 'function', name: 'mintAmounts', stateMutability: 'view',
inputs: [{ name: 'pool', type: 'address' }, { name: 'lpTokenAmount', type: 'uint256' }],
outputs: [{ name: 'depositAmounts', type: 'uint256[]' }]
}
];
const UniswapV3FactoryABI = [
{ type: 'function', name: 'getPool', stateMutability: 'view',
inputs: [{ name: 'tokenA', type: 'address' }, { name: 'tokenB', type: 'address' }, { name: 'fee', type: 'uint24' }],
outputs: [{ name: 'pool', type: 'address' }] }
];
const UniswapV3PoolABI = [
{ type: 'function', name: 'liquidity', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint128' }] }
];
const UniswapSwapRouter02ABI = [
{
type: 'function', name: 'exactOutputSingle', stateMutability: 'payable',
inputs: [{
name: 'params', type: 'tuple',
components: [
{ name: 'tokenIn', type: 'address' },
{ name: 'tokenOut', type: 'address' },
{ name: 'fee', type: 'uint24' },
{ name: 'recipient', type: 'address' },
{ name: 'amountOut', type: 'uint256' },
{ name: 'amountInMaximum', type: 'uint256' },
{ name: 'sqrtPriceLimitX96', type: 'uint160' }
]
}],
outputs: [{ name: 'amountIn', type: 'uint256' }]
}
];
// ============================================================================
// SIGNER ABSTRACTION
// ============================================================================
/**
* Build a signer from parsed CLI options.
*
* mode 'privateKey': all writes go through ethers.Wallet (no cast required).
* mode 'trezor': all writes go through `cast send --trezor` (no private key stored).
*/
function buildSigner(opts, provider) {
if (opts.privateKey) {
const wallet = new ethers.Wallet(opts.privateKey, provider);
return { mode: 'privateKey', address: wallet.address, wallet, rpcUrl: currentConfig.rpcUrl };
} else {
return { mode: 'trezor', address: opts.trezorAddress, wallet: null, rpcUrl: currentConfig.rpcUrl };
}
}
/** Execute a cast send command (Trezor mode). Throws on failure. */
function runCast(signer, to, sigStr, argStr) {
const nonce = signer.nextNonce();
const cmd = [
`cast send ${to}`,
` "${sigStr}"`,
` ${argStr}`,
` --rpc-url '${signer.rpcUrl}'`,
` --from ${signer.address}`,
` --nonce ${nonce}`,
` --trezor --mnemonic-index 0`
].join(' \\\n');
console.log(` [cast] ${cmd.replace(/\s+/g, ' ')}`);
const output = execSync(cmd, { encoding: 'utf-8' });
if (output) console.log(output.trim());
return output;
}
// ============================================================================
// HELPERS
// ============================================================================
const Q64 = 2n ** 64n;
/**
* Convert a floating-point value to Q64.64 BigInt.
* Uses 1e9-scaled integer arithmetic to avoid float precision loss.
*/
function floatToQ64(x) {
return BigInt(Math.round(x * 1e9)) * Q64 / 1_000_000_000n;
}
/**
* Compute the limitPrice argument for swapToLimitAmounts.
*
* Derived from swapAmountsForPriceLimit: setting r0_new = r0_target in
* r0_new = r0² / (L×(1+r0) - r0²) and solving for L gives:
*
* L = r0i² × (1 + r0t) / (r0t × (1 + r0i))
*
* Requires r0tQ64 < r0iQ64 (target price lower than current in swap direction).
* Both arguments are Q64.64 BigInts (raw internal LMSR price ratios, no denom scaling).
*/
function computeLimitPrice(r0iQ64, r0tQ64) {
return r0iQ64 * r0iQ64 * (r0tQ64 + Q64) / (r0tQ64 * (r0iQ64 + Q64));
}
async function fetchCoinGeckoPrices(symbols) {
const relevantTokens = Object.entries(TOKEN_CONFIG).filter(([sym]) => symbols.includes(sym));
const ids = relevantTokens.map(([, t]) => t.coingeckoId).join(',');
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${ids}&vs_currencies=usd`;
console.log(`[~] Fetching prices from CoinGecko...`);
const response = await fetch(url);
if (!response.ok) throw new Error(`CoinGecko API error: ${response.statusText}`);
const data = await response.json();
const prices = {};
for (const [symbol, tokenInfo] of relevantTokens) {
prices[symbol] = data[tokenInfo.coingeckoId]?.usd;
if (!prices[symbol]) throw new Error(`No price returned for ${symbol} (${tokenInfo.coingeckoId})`);
}
return prices;
}
async function diagnoseGasError(err, signer, provider) {
if (err.code !== 'UNPREDICTABLE_GAS_LIMIT') return;
const [balance, feeData] = await Promise.all([
provider.getBalance(signer.address),
provider.getFeeData(),
]);
const maxFee = feeData.maxFeePerGas || feeData.gasPrice || ethers.BigNumber.from(0);
if (maxFee.isZero()) return;
const affordableGas = balance.div(maxFee).toNumber();
if (affordableGas < 100_000) {
const needed = maxFee.mul(100_000).sub(balance);
throw new Error(
`Out of ETH for gas: wallet has ${ethers.utils.formatEther(balance)} ETH ` +
`(~${affordableGas.toLocaleString()} gas units at ${ethers.utils.formatUnits(maxFee, 'gwei')} gwei maxFee). ` +
`Top up at least ${ethers.utils.formatEther(needed)} ETH.`
);
}
}
async function approveToken(signer, provider, tokenAddress, tokenSymbol, spenderAddress, amount) {
const decimals = TOKEN_CONFIG[tokenSymbol]?.decimals ?? 18;
const tokenReadOnly = new ethers.Contract(tokenAddress, ERC20ABI, provider);
const currentAllowance = await tokenReadOnly.allowance(signer.address, spenderAddress);
if (currentAllowance.gte(amount)) {
console.log(` [=] ${tokenSymbol} allowance already sufficient (${ethers.utils.formatUnits(currentAllowance, decimals)})`);
return;
}
if (signer.mode === 'privateKey') {
try {
const token = new ethers.Contract(tokenAddress, ERC20ABI, signer.wallet);
if (tokenSymbol === 'USDT' && currentAllowance.gt(0)) {
const tx = await token.approve(spenderAddress, 0, { nonce: signer.nextNonce() });
await tx.wait();
console.log(` [+] ${tokenSymbol} allowance reset to 0`);
}
const tx = await token.approve(spenderAddress, amount, { nonce: signer.nextNonce() });
await tx.wait();
console.log(` [+] ${tokenSymbol} approved (tx: ${tx.hash})`);
} catch (err) {
await diagnoseGasError(err, signer, provider);
throw err;
}
} else {
if (tokenSymbol === 'USDT' && currentAllowance.gt(0)) {
runCast(signer, tokenAddress, 'approve(address,uint256)', `${spenderAddress} 0`);
console.log(` [+] ${tokenSymbol} allowance reset to 0`);
}
runCast(signer, tokenAddress, 'approve(address,uint256)', `${spenderAddress} ${amount.toString()}`);
console.log(` [+] ${tokenSymbol} approved`);
}
}
async function checkPoolLiquidity(provider, tokenA, tokenB, feeTier) {
const uniswapConfig = currentConfig.uniswap;
const factory = new ethers.Contract(uniswapConfig.factory, UniswapV3FactoryABI, provider);
const poolAddress = await factory.getPool(tokenA, tokenB, feeTier);
if (poolAddress === ethers.constants.AddressZero) {
throw new Error(
`No Uniswap V3 pool exists for this pair at fee tier ${feeTier / 10000}% — ` +
`try a different fee tier or route through an intermediate token`
);
}
const pool = new ethers.Contract(poolAddress, UniswapV3PoolABI, provider);
const liquidity = await pool.liquidity();
if (liquidity.isZero()) {
throw new Error(
`Uniswap V3 pool ${poolAddress} has no active in-range liquidity at fee tier ${feeTier / 10000}% — ` +
`the pool exists but all liquidity is out of range`
);
}
}
async function buyFromUniswap(signer, provider, tokenOut, tokenOutSymbol, amountOut, tokenIn, tokenInSymbol, maxAmountIn, dryRun) {
const uniswapConfig = currentConfig.uniswap;
if (!uniswapConfig) {
throw new Error(`Uniswap not configured for network '${NETWORK}' — cannot buy ${tokenOutSymbol}`);
}
const feeTier = uniswapConfig.feeTierOverrides[tokenOutSymbol] ?? uniswapConfig.defaultFeeTier;
const outDecimals = TOKEN_CONFIG[tokenOutSymbol]?.decimals ?? 18;
const inDecimals = TOKEN_CONFIG[tokenInSymbol]?.decimals ?? 6;
console.log(` [~] Buying ${ethers.utils.formatUnits(amountOut, outDecimals)} ${tokenOutSymbol} from Uniswap V3 (fee tier: ${feeTier / 10000}%)`);
console.log(` [~] Max spending: ${ethers.utils.formatUnits(maxAmountIn, inDecimals)} ${tokenInSymbol}`);
await checkPoolLiquidity(provider, tokenIn, tokenOut, feeTier);
if (dryRun) {
console.log(` [DRY-RUN] Would buy ${tokenOutSymbol} via Uniswap V3`);
return;
}
await approveToken(signer, provider, tokenIn, tokenInSymbol, uniswapConfig.swapRouter02, maxAmountIn);
if (signer.mode === 'privateKey') {
const router = new ethers.Contract(uniswapConfig.swapRouter02, UniswapSwapRouter02ABI, signer.wallet);
let tx;
try {
tx = await router.exactOutputSingle({
tokenIn,
tokenOut,
fee: feeTier,
recipient: signer.address,
amountOut,
amountInMaximum: maxAmountIn,
sqrtPriceLimitX96: 0
}, { nonce: signer.nextNonce() });
} catch (err) {
await diagnoseGasError(err, signer, provider);
throw err;
}
await tx.wait();
console.log(` [+] Bought ${tokenOutSymbol} via Uniswap (tx: ${tx.hash})`);
} else {
// cast send with tuple arg: "(tokenIn,tokenOut,fee,recipient,amountOut,amountInMaximum,sqrtLimit)"
const tupleArg = `"(${tokenIn},${tokenOut},${feeTier},${signer.address},${amountOut.toString()},${maxAmountIn.toString()},0)"`;
runCast(
signer,
uniswapConfig.swapRouter02,
'exactOutputSingle((address,address,uint24,address,uint256,uint256,uint160))',
tupleArg
);
console.log(` [+] Bought ${tokenOutSymbol} via Uniswap`);
}
}
// ============================================================================
// REBALANCING LOGIC
// ============================================================================
/** Query the current pool price deviation for a single token vs CoinGecko. */
async function getTokenDeviation(partyInfo, poolAddr, token, quoteToken, usdPrices) {
const currentQ128BN = await partyInfo.price(poolAddr, token.idx, quoteToken.idx);
const currentQ128 = BigInt(currentQ128BN.toString());
// partyInfo.price returns r0 * 10^(quoteDec-baseDec) in Q128 format.
const currentQ64n = currentQ128 / Q64;
const poolPriceRaw = Number(currentQ64n) / Number(Q64);
const poolPriceHuman = poolPriceRaw * Math.pow(10, token.decimals - quoteToken.decimals);
const poolUSD = poolPriceHuman * usdPrices[quoteToken.symbol];
const targetPriceHuman = usdPrices[token.symbol] / usdPrices[quoteToken.symbol];
const deviation = targetPriceHuman === 0 ? 0
: (poolPriceHuman - targetPriceHuman) / targetPriceHuman * 100;
return { token, deviation, deviationAbs: Math.abs(deviation), poolUSD };
}
/**
* Execute a rebalancing swap for a single token via pool.swap().
* Returns true if a swap was executed, false if price is already within threshold.
*/
async function rebalanceTokenIfNeeded(
{ signer, provider, partyInfo, poolAddr, quoteToken, usdPrices, dryRun, denominators },
token
) {
const currentQ128BN = await partyInfo.price(poolAddr, token.idx, quoteToken.idx);
const currentQ128 = BigInt(currentQ128BN.toString());
const currentQ64n = currentQ128 / Q64;
const poolPriceRaw = Number(currentQ64n) / Number(Q64);
const poolPriceHuman = poolPriceRaw * Math.pow(10, token.decimals - quoteToken.decimals);
const targetPriceHuman = usdPrices[token.symbol] / usdPrices[quoteToken.symbol];
const deviation = targetPriceHuman === 0 ? 0
: (poolPriceHuman - targetPriceHuman) / targetPriceHuman * 100;
if (Math.abs(deviation) < PRICE_DEVIATION_THRESHOLD * 100) {
return false;
}
// LMSR behaves like a regular AMM: injecting a token makes it cheaper.
// Overpriced base: inject TOKEN, extract QUOTE → lowers TOKEN price.
// Underpriced base: inject QUOTE, extract TOKEN → raises TOKEN price.
const overpriced = poolPriceHuman > targetPriceHuman;
const inputToken = overpriced ? token : quoteToken;
const outputToken = overpriced ? quoteToken : token;
const inputIdx = overpriced ? token.idx : quoteToken.idx;
const outputIdx = overpriced ? quoteToken.idx : token.idx;
const direction = overpriced ? 'lower-price' : 'raise-price';
console.log(`${direction}: inject ${inputToken.symbol}, extract ${outputToken.symbol}`);
// Compute raw internal Q64.64 r0 for the swap direction using actual pool denominators.
// partyInfo.price(i,j) = r0_internal(i,j) × D[j] / D[i] in Q128.128
// → r0_internal Q64.64 = price_Q128 × D[i] / D[j] / Q64
const r0PriceQ128 = BigInt((await partyInfo.price(poolAddr, inputIdx, outputIdx)).toString());
const r0InitialQ64 = r0PriceQ128 * denominators[inputIdx] / denominators[outputIdx] / Q64;
// Scale r0_initial to get r0_target using the human-price ratio in the swap direction.
// overpriced (TOKEN→QUOTE): target price is lower → ratio = target/current < 1
// underpriced (QUOTE→TOKEN): we want lower r0(QUOTE,TOKEN) → ratio = current/target < 1
const numPrice = overpriced ? targetPriceHuman : poolPriceHuman;
const denPrice = overpriced ? poolPriceHuman : targetPriceHuman;
const r0TargetQ64 = r0InitialQ64 * BigInt(Math.round(numPrice * 1e9)) / BigInt(Math.round(denPrice * 1e9));
const limitPriceQ64 = computeLimitPrice(r0InitialQ64, r0TargetQ64);
const r0iFloat = Number(r0InitialQ64) / Number(Q64);
const r0tFloat = Number(r0TargetQ64) / Number(Q64);
console.log(` r0Initial=${r0iFloat.toExponential(4)} r0Target=${r0tFloat.toExponential(4)} limitPrice=${limitPriceQ64.toString()}`);
const REDUCE_SIZE_RETRIES = 5;
let amountIn, amountOut, inFee, effectiveLimitPrice;
let r0TargetRetry = r0TargetQ64;
for (let attempt = 0; ; attempt++) {
const retryLimitPrice = attempt === 0 ? limitPriceQ64 : computeLimitPrice(r0InitialQ64, r0TargetRetry);
try {
const result = await partyInfo.swapToLimitAmounts(poolAddr, inputIdx, outputIdx, retryLimitPrice.toString());
amountIn = result.amountIn;
amountOut = result.amountOut;
inFee = result.inFee;
effectiveLimitPrice = retryLimitPrice;
break;
} catch (err) {
if (
attempt < REDUCE_SIZE_RETRIES &&
err.message.includes('arithmetic underflow or overflow')
) {
r0TargetRetry = r0InitialQ64 - (r0InitialQ64 - r0TargetRetry) / 10n;
console.log(` [!] swapToLimitAmounts overflow — retrying with reduced target (attempt ${attempt + 2}/${REDUCE_SIZE_RETRIES + 1})`);
continue;
}
console.log(` [!] swapToLimitAmounts reverted: ${err.message} — skipping`);
return false;
}
}
if (amountIn.isZero()) {
console.log(` [+] amountIn=0, nothing to do`);
return false;
}
console.log(` amountIn: ${ethers.utils.formatUnits(amountIn, inputToken.decimals)} ${inputToken.symbol}`);
console.log(` amountOut: ${ethers.utils.formatUnits(amountOut, outputToken.decimals)} ${outputToken.symbol}`);
console.log(` fee: ${ethers.utils.formatUnits(inFee, inputToken.decimals)} ${inputToken.symbol}`);
// Ensure the signer holds enough of the input token; buy shortfall from Uniswap if needed
const inputERC20 = new ethers.Contract(inputToken.address, ERC20ABI, provider);
const inputBalance = await inputERC20.balanceOf(signer.address);
if (inputBalance.lt(amountIn)) {
const deficit = amountIn.sub(inputBalance);
console.log(` [!] Insufficient ${inputToken.symbol}: ` +
`have ${ethers.utils.formatUnits(inputBalance, inputToken.decimals)}, ` +
`need ${ethers.utils.formatUnits(amountIn, inputToken.decimals)}, ` +
`deficit ${ethers.utils.formatUnits(deficit, inputToken.decimals)}`);
if (inputToken.symbol === quoteToken.symbol) {
console.log(` [!] Input is the quote token — no way to acquire more, skipping`);
return false;
}
const deficitUSD = Number(ethers.utils.formatUnits(deficit, inputToken.decimals)) * usdPrices[inputToken.symbol];
const maxQuoteIn = ethers.utils.parseUnits(
(deficitUSD * 1.02 / usdPrices[quoteToken.symbol]).toFixed(quoteToken.decimals),
quoteToken.decimals
);
await buyFromUniswap(
signer, provider,
inputToken.address, inputToken.symbol, deficit,
quoteToken.address, quoteToken.symbol, maxQuoteIn,
dryRun
);
}
console.log(` [~] Approving ${inputToken.symbol} for pool...`);
if (!dryRun) {
await approveToken(signer, provider, inputToken.address, inputToken.symbol, poolAddr, amountIn.mul(101).div(100));
} else {
console.log(` [DRY-RUN] Would approve ${inputToken.symbol}`);
}
// Execute via pool.swap() with the computed amountIn and the effective limit price.
const deadline = Math.floor(Date.now() / 1000) + 300;
if (!dryRun) {
if (signer.mode === 'privateKey') {
const poolContract = new ethers.Contract(poolAddr, [
{ type: 'function', name: 'swap', stateMutability: 'payable',
inputs: [
{ name: 'payer', type: 'address' }, { name: 'fundingSelector', type: 'bytes4' },
{ name: 'receiver', type: 'address' }, { name: 'inputTokenIndex', type: 'uint256' },
{ name: 'outputTokenIndex', type: 'uint256' }, { name: 'maxAmountIn', type: 'uint256' },
{ name: 'limitPrice', type: 'int128' }, { name: 'deadline', type: 'uint256' },
{ name: 'unwrap', type: 'bool' }, { name: 'cbData', type: 'bytes' }
],
outputs: [{ name: 'amountIn', type: 'uint256' }, { name: 'amountOut', type: 'uint256' }, { name: 'inFee', type: 'uint256' }]
}
], signer.wallet);
let tx;
let swapAmountIn = amountIn;
for (let attempt = 0; ; attempt++) {
const nonce = signer.nextNonce();
try {
tx = await poolContract.swap(
signer.address, '0x00000000', signer.address,
inputIdx, outputIdx, swapAmountIn, effectiveLimitPrice.toString(), deadline, false, '0x',
{ nonce }
);
break;
} catch (err) {
if (
attempt < REDUCE_SIZE_RETRIES &&
err.code === 'UNPREDICTABLE_GAS_LIMIT' &&
err.message.includes('arithmetic underflow or overflow')
) {
signer._nonce--; // tx never submitted; reuse nonce
swapAmountIn = swapAmountIn.div(10);
console.log(` [!] Arithmetic overflow — retrying with ${ethers.utils.formatUnits(swapAmountIn, inputToken.decimals)} ${inputToken.symbol} (attempt ${attempt + 2}/${REDUCE_SIZE_RETRIES})`);
continue;
}
await diagnoseGasError(err, signer, provider);
throw err;
}
}
await tx.wait();
console.log(` [+] Swap executed (tx: ${tx.hash})`);
} else {
runCast(
signer, poolAddr,
'swap(address,bytes4,address,uint256,uint256,uint256,int128,uint256,bool,bytes)',
`${signer.address} "0x00000000" ${signer.address} ${inputIdx} ${outputIdx} ${amountIn.toString()} ${effectiveLimitPrice.toString()} ${deadline} false "0x"`
);
console.log(` [+] Swap executed`);
}
const newQ128BN = await partyInfo.price(poolAddr, token.idx, quoteToken.idx);
const newQ64n = BigInt(newQ128BN.toString()) / Q64;
const newPoolUSD = Number(newQ64n) / Number(Q64)
* Math.pow(10, token.decimals - quoteToken.decimals)
* usdPrices[quoteToken.symbol];
const residual = (newPoolUSD - usdPrices[token.symbol]) / usdPrices[token.symbol] * 100;
console.log(` [+] Updated pool price: $${newPoolUSD.toPrecision(6)} (market: $${usdPrices[token.symbol].toPrecision(6)} residual: ${residual.toFixed(3)}%)`);
} else {
console.log(` [DRY-RUN] Would swap ${inputToken.symbol}${outputToken.symbol} via pool.swap()`);
}
return true;
}
// ============================================================================
// CLI ARGUMENT PARSING
// ============================================================================
function parseArgs() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`
Usage: node adjust_pool_prices_and_stake.js --pool <address> (--private-key <key> | --trezor <address>) [OPTIONS]
Signer (exactly one required):
--private-key <key> Sign all transactions with this private key
--trezor <address> Sign all transactions via Trezor hardware wallet at given address
Options:
--pool <address> Target LiqP pool address (required)
--quote <symbol> Quote token symbol (default: auto-detect USDT/USDC)
--iterations <n> Max rebalancing iterations, best-first (default: 10)
--stake <usd> USD amount to basket-stake via mint() after rebalancing
--skip-rebalance Skip price adjustment; only run staking (requires --stake)
--dry-run Preview only, no transactions executed
--help, -h Show this help message
Environment:
NETWORK 'mainnet' (default) or 'mockchain'
MAINNET_RPC_URL RPC endpoint (required for mainnet)
Examples:
node adjust_pool_prices_and_stake.js --pool 0x1234... --private-key 0xabc... --dry-run
node adjust_pool_prices_and_stake.js --pool 0x1234... --trezor 0xdef... --stake 500
node adjust_pool_prices_and_stake.js --pool 0x1234... --trezor 0xdef... --stake 500 --skip-rebalance
`);
process.exit(0);
}
const opts = {
pool: null, quote: null, stake: null, iterations: 10,
privateKey: null, trezorAddress: null,
skipRebalance: false, dryRun: false
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--pool' && i + 1 < args.length) { opts.pool = args[++i]; }
else if (args[i] === '--quote' && i + 1 < args.length) { opts.quote = args[++i].toUpperCase(); }
else if (args[i] === '--iterations' && i + 1 < args.length) { opts.iterations = parseInt(args[++i], 10); }
else if (args[i] === '--stake' && i + 1 < args.length) { opts.stake = parseFloat(args[++i]); }
else if (args[i] === '--private-key' && i + 1 < args.length) { opts.privateKey = args[++i]; }
else if (args[i] === '--trezor' && i + 1 < args.length) { opts.trezorAddress = args[++i]; }
else if (args[i] === '--skip-rebalance') { opts.skipRebalance = true; }
else if (args[i] === '--dry-run') { opts.dryRun = true; }
}
if (!opts.pool) {
console.error('[!] --pool <address> is required.');
process.exit(1);
}
if (!opts.privateKey && !opts.trezorAddress) {
console.error('[!] Signer required: provide --private-key <key> or --trezor <address>.');
process.exit(1);
}
if (opts.privateKey && opts.trezorAddress) {
console.error('[!] Provide --private-key OR --trezor, not both.');
process.exit(1);
}
if (opts.skipRebalance && !opts.stake) {
console.error('[!] --skip-rebalance requires --stake <usd>.');
process.exit(1);
}
return opts;
}
// ============================================================================
// MAIN
// ============================================================================
async function main() {
const opts = parseArgs();
const { pool: poolAddr, dryRun } = opts;
const signerLabel = opts.trezorAddress ? `trezor:${opts.trezorAddress}` : 'private-key';
console.log(`${'='.repeat(70)}`);
console.log(`Adjust Pool Prices`);
console.log(`Network: ${NETWORK} Pool: ${poolAddr} Signer: ${signerLabel}${opts.skipRebalance ? ' [STAKE-ONLY]' : ''}${dryRun ? ' [DRY-RUN]' : ''}`);
console.log(`${'='.repeat(70)}\n`);
if (!currentConfig.rpcUrl) {
console.error(`[!] Missing RPC URL for network '${NETWORK}' (set MAINNET_RPC_URL)`);
process.exit(1);
}
// ── Phase 0: Setup ─────────────────────────────────────────────────────────
const provider = new ethers.providers.JsonRpcProvider(currentConfig.rpcUrl);
const signer = buildSigner(opts, provider);
console.log(`[+] Signer: ${signer.address} (${signer.mode})`);
if (!dryRun) {
signer._nonce = await provider.getTransactionCount(signer.address, "pending");
console.log(`[+] Nonce: ${signer._nonce} (pending)`);
} else {
signer._nonce = 0;
}
signer.nextNonce = () => signer._nonce++;
const chainInfoData = JSON.parse(
await readFile(new URL('../src/contracts/liqp-deployments.json', import.meta.url), 'utf-8')
);
const chainContracts = chainInfoData[currentConfig.chainId]?.v1;
if (!chainContracts) {
console.error(`[!] No contract addresses for chainId ${currentConfig.chainId}`);
process.exit(1);
}
const PARTY_INFO_ADDRESS = chainContracts.PartyInfo;
console.log(`[+] PartyInfo: ${PARTY_INFO_ADDRESS}`);
const pool = new ethers.Contract(poolAddr, IPartyPoolABI, provider);
const partyInfo = new ethers.Contract(PARTY_INFO_ADDRESS, IPartyInfoABI, provider);
const denomsBN = await pool.denominators();
const denominators = denomsBN.map(d => BigInt(d.toString()));
console.log(`[+] Denominators: [${denominators.join(', ')}]`);
const tokenAddresses = await pool.allTokens();
console.log(`[+] Pool tokens: ${tokenAddresses.length}`);
const tokenConfigByAddr = {};
for (const [sym, info] of Object.entries(TOKEN_CONFIG)) {
tokenConfigByAddr[info.address.toLowerCase()] = { symbol: sym, ...info };
}
const poolTokens = tokenAddresses.map((addr, idx) => {
const meta = tokenConfigByAddr[addr.toLowerCase()];
if (!meta) throw new Error(`Unknown token at pool index ${idx}: ${addr}`);
return { idx, address: addr, symbol: meta.symbol, decimals: meta.decimals, coingeckoId: meta.coingeckoId };
});
let quoteToken;
if (opts.quote) {
quoteToken = poolTokens.find(t => t.symbol === opts.quote);
if (!quoteToken) throw new Error(`Quote token ${opts.quote} not found in pool`);
} else {
quoteToken = poolTokens.find(t => t.symbol === 'USDT') ?? poolTokens.find(t => t.symbol === 'USDC');
if (!quoteToken) throw new Error('No USDT or USDC in pool; specify --quote <symbol>');
}
console.log(`[+] Quote: ${quoteToken.symbol} (index ${quoteToken.idx})\n`);
const nonQuoteTokens = poolTokens.filter(t => t.idx !== quoteToken.idx);
// ── Phase 1: Fetch Prices ───────────────────────────────────────────────────
const symbols = poolTokens.map(t => t.symbol);
const usdPrices = await fetchCoinGeckoPrices(symbols);
console.log(`\n[+] Market prices:`);
for (const sym of symbols) {
console.log(` ${sym.padEnd(6)}: $${usdPrices[sym].toLocaleString()}`);
}
const ctx = { signer, provider, partyInfo, poolAddr, poolTokens, quoteToken, usdPrices, dryRun, denominators };
// ── Phase 2: Rebalancing — best-first iterative (skipped with --skip-rebalance) ─
let swapCount = 0;
if (opts.skipRebalance) {
console.log(`\n[~] Skipping price adjustment (--skip-rebalance).`);
} else {
console.log(`\n${'─'.repeat(70)}`);
console.log(`Rebalancing — best-first, up to ${opts.iterations} iterations (threshold: ${PRICE_DEVIATION_THRESHOLD * 100}%)`);
console.log(`${'─'.repeat(70)}`);
let converged = false;
for (let iter = 1; iter <= opts.iterations; iter++) {
// Query all pool prices in parallel, find the most deviant token
const deviations = await Promise.all(
nonQuoteTokens.map(t => getTokenDeviation(partyInfo, poolAddr, t, quoteToken, usdPrices))
);
console.log(`\n[iter ${iter}/${opts.iterations}]`);
for (const d of deviations) {
const marker = d.deviationAbs >= PRICE_DEVIATION_THRESHOLD * 100 ? '!' : '=';
console.log(` [${marker}] ${d.token.symbol.padEnd(6)} pool=$${d.poolUSD.toPrecision(6)} market=$${usdPrices[d.token.symbol].toPrecision(6)} deviation=${d.deviation.toFixed(3)}%`);
}
const worst = deviations.reduce((a, b) => a.deviationAbs > b.deviationAbs ? a : b);
if (worst.deviationAbs < PRICE_DEVIATION_THRESHOLD * 100) {
console.log(`\n[+] All prices within threshold — converged after ${swapCount} swap(s).`);
converged = true;
break;
}
console.log(`\n Swapping ${worst.token.symbol} (${worst.deviation.toFixed(3)}% off)`);
const swapped = await rebalanceTokenIfNeeded(ctx, worst.token);
if (swapped) swapCount++;
}
if (!converged) {
console.log(`\n[~] Reached iteration limit (${opts.iterations}) with ${swapCount} swap(s) executed.`);
}
}
// ── Phase 4: Optional Basket Stake via mint() ──────────────────────────────
if (opts.stake) {
console.log(`\n${'='.repeat(70)}`);
console.log(`Phase 4: Basket Stake ($${opts.stake} USD)`);
console.log(`${'='.repeat(70)}`);
const totalSupplyBN = await pool.totalSupply();
const balancesBN = await pool.balances();
let tvlUSD = 0;
for (let i = 0; i < poolTokens.length; i++) {
tvlUSD += Number(ethers.utils.formatUnits(balancesBN[i], poolTokens[i].decimals)) * usdPrices[poolTokens[i].symbol];
}
const totalSupplyFloat = Number(ethers.utils.formatUnits(totalSupplyBN, 18));
const lpPriceUSD = tvlUSD / totalSupplyFloat;
console.log(`\n[~] Pool TVL: $${tvlUSD.toFixed(2)}, LP price: $${lpPriceUSD.toFixed(6)}`);
const lpAmountFloat = opts.stake / lpPriceUSD;
const lpAmount = ethers.utils.parseUnits(lpAmountFloat.toFixed(18), 18);
console.log(`[~] Target LP amount: ${lpAmountFloat.toFixed(6)} LP tokens (~$${opts.stake})`);
const depositAmounts = await partyInfo.mintAmounts(poolAddr, lpAmount);
console.log(`\n[~] Required deposits for mint:`);
const deficits = [];
let totalQuoteNeeded = ethers.BigNumber.from(0);
for (let i = 0; i < poolTokens.length; i++) {
const t = poolTokens[i];
const required = depositAmounts[i];
const tokenERC20 = new ethers.Contract(t.address, ERC20ABI, provider);
const balance = await tokenERC20.balanceOf(signer.address);
const sufficient = balance.gte(required);
console.log(
` ${t.symbol.padEnd(6)}: ` +
`${ethers.utils.formatUnits(required, t.decimals).padStart(22)} required ` +
`${ethers.utils.formatUnits(balance, t.decimals).padStart(22)} in wallet ` +
`${sufficient ? '✓' : '✗'}`
);
if (!sufficient) {
const deficit = required.sub(balance);
const deficitUSD = Number(ethers.utils.formatUnits(deficit, t.decimals)) * usdPrices[t.symbol];
const quoteNeeded = ethers.utils.parseUnits(
(deficitUSD * 1.02 / usdPrices[quoteToken.symbol]).toFixed(quoteToken.decimals),
quoteToken.decimals
);
totalQuoteNeeded = totalQuoteNeeded.add(quoteNeeded);
deficits.push({ token: t, deficit, quoteNeeded });
}
}
// Pre-flight: fail before any transaction if quote balance is insufficient
if (deficits.length > 0) {
const quoteERC20 = new ethers.Contract(quoteToken.address, ERC20ABI, provider);
const quoteBalance = await quoteERC20.balanceOf(signer.address);
if (quoteBalance.lt(totalQuoteNeeded)) {
console.error(`\n[!] Pre-flight check FAILED — insufficient ${quoteToken.symbol} to cover token deficits:`);
for (const d of deficits) {
console.error(` ${d.token.symbol}: deficit ${ethers.utils.formatUnits(d.deficit, d.token.decimals)}` +
` → costs ~${ethers.utils.formatUnits(d.quoteNeeded, quoteToken.decimals)} ${quoteToken.symbol}`);
}
console.error(` Total needed: ${ethers.utils.formatUnits(totalQuoteNeeded, quoteToken.decimals)} ${quoteToken.symbol}`);
console.error(` Wallet has: ${ethers.utils.formatUnits(quoteBalance, quoteToken.decimals)} ${quoteToken.symbol}`);
process.exit(1);
}
console.log(`\n[+] Pre-flight check passed`);
}
if (dryRun) {
console.log(`\n[DRY-RUN] Would buy ${deficits.length} deficit token(s) and mint ${lpAmountFloat.toFixed(6)} LP`);
} else {
for (const d of deficits) {
console.log(`\n[~] Buying ${d.token.symbol} deficit from Uniswap...`);
await buyFromUniswap(
signer, provider,
d.token.address, d.token.symbol, d.deficit,
quoteToken.address, quoteToken.symbol, d.quoteNeeded,
false
);
}
console.log(`\n[~] Approving tokens for pool mint...`);
for (let i = 0; i < poolTokens.length; i++) {
const t = poolTokens[i];
const required = depositAmounts[i];
if (required.gt(0)) {
await approveToken(signer, provider, t.address, t.symbol, poolAddr, required.mul(101).div(100));
}
}
const deadline = Math.floor(Date.now() / 1000) + 300;
if (signer.mode === 'privateKey') {
const poolContract = new ethers.Contract(poolAddr, [
{ type: 'function', name: 'mint', stateMutability: 'payable',
inputs: [
{ name: 'payer', type: 'address' }, { name: 'receiver', type: 'address' },
{ name: 'lpTokenAmount', type: 'uint256' }, { name: 'deadline', type: 'uint256' }
],
outputs: [{ name: 'lpMinted', type: 'uint256' }]
}
], signer.wallet);
let tx;
try {
tx = await poolContract.mint(signer.address, signer.address, lpAmount, deadline, { nonce: signer.nextNonce() });
} catch (err) {
await diagnoseGasError(err, signer, provider);
throw err;
}
await tx.wait();
console.log(`[+] Mint executed (tx: ${tx.hash})`);
} else {
runCast(
signer, poolAddr,
'mint(address,address,uint256,uint256)',
`${signer.address} ${signer.address} ${lpAmount.toString()} ${deadline}`
);
console.log(`[+] Mint executed`);
}
const newLpBalance = await pool.balanceOf(signer.address);
console.log(`[+] Wallet LP balance: ${ethers.utils.formatUnits(newLpBalance, 18)}`);
}
}
// ── Summary ────────────────────────────────────────────────────────────────
console.log(`\n${'='.repeat(70)}`);
console.log(`Summary`);
console.log(`${'='.repeat(70)}`);
if (opts.skipRebalance) {
console.log(`Rebalance: skipped (--skip-rebalance)`);
} else {
console.log(`Rebalance: ${swapCount} swap(s), up to ${opts.iterations} iterations${dryRun ? ' (dry-run)' : ''}`);
}
if (opts.stake) {
console.log(`Stake: $${opts.stake} USD${dryRun ? ' (dry-run)' : ''}`);
}
console.log(`Done.`);
}
main().catch(err => {
console.error('[!] Fatal error:', err.message || err);
process.exit(1);
});