create_pool refactor; test pool deployed; etc

This commit is contained in:
2026-05-10 17:57:45 -04:00
parent d31f99d330
commit e1d2acea0d
13 changed files with 2359 additions and 444 deletions

View File

@@ -0,0 +1,911 @@
#!/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);
});