912 lines
40 KiB
JavaScript
912 lines
40 KiB
JavaScript
#!/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);
|
||
});
|