fix: address review findings in market and recenter helpers
- recenter.ts: parse isUp from Recentered event logs instead of a follow-up eth_call that would decode wrong post-recenter state - recenter.ts: remove hardcoded private key from comment; add blocks>0 guard in mineBlocks; call provider.destroy() to prevent leaked intervals - market.ts: snapshot KRK balance before buy to compute krkBought as delta instead of cumulative total; call provider.destroy() on exit; remove unused withdraw entry from WETH_ABI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1973ccf25b
commit
9cda5beb4a
2 changed files with 49 additions and 32 deletions
|
|
@ -13,7 +13,7 @@ const SWAP_ROUTER = '0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4';
|
|||
const WETH = '0x4200000000000000000000000000000000000006';
|
||||
const POOL_FEE = 10_000; // 1% tier used by the KRAIKEN pool
|
||||
|
||||
const WETH_ABI = ['function deposit() payable', 'function withdraw(uint256 wad)'];
|
||||
const WETH_ABI = ['function deposit() payable'];
|
||||
const ERC20_ABI = ['function approve(address spender, uint256 amount) returns (bool)'];
|
||||
const ROUTER_ABI = [
|
||||
'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)',
|
||||
|
|
@ -29,6 +29,11 @@ export interface MarketSwapConfig {
|
|||
krkAddress: string;
|
||||
}
|
||||
|
||||
async function erc20Balance(rpcUrl: string, tokenAddress: string, account: string): Promise<bigint> {
|
||||
const data = '0x70a08231' + account.slice(2).padStart(64, '0');
|
||||
return BigInt((await rpcCall(rpcUrl, 'eth_call', [{ to: tokenAddress, data }, 'latest'])) as string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a round-trip swap: buy KRK with `ethAmount` ETH, then sell ALL KRK back.
|
||||
* Uses direct RPC transactions (not browser/UI). Net effect on price ≈ 0,
|
||||
|
|
@ -71,6 +76,8 @@ export async function roundTripSwap(
|
|||
console.log('[market] WETH approved');
|
||||
|
||||
// Step 3: Buy KRK (WETH → KRK)
|
||||
// Snapshot balance before so krkBought is the delta, not the cumulative total.
|
||||
const krkBefore = await erc20Balance(config.rpcUrl, config.krkAddress, config.accountAddress);
|
||||
console.log('[market] Buying KRK (WETH → KRK)...');
|
||||
const buyData = routerIface.encodeFunctionData('exactInputSingle', [
|
||||
{
|
||||
|
|
@ -86,15 +93,11 @@ export async function roundTripSwap(
|
|||
const buyTx = await wallet.sendTransaction({ to: SWAP_ROUTER, data: buyData });
|
||||
await waitForReceipt(config.rpcUrl, buyTx.hash);
|
||||
|
||||
// Read KRK balance to know how much was bought
|
||||
const balanceOfSelector = '0x70a08231';
|
||||
const balanceData = balanceOfSelector + config.accountAddress.slice(2).padStart(64, '0');
|
||||
const krkBought = BigInt(
|
||||
(await rpcCall(config.rpcUrl, 'eth_call', [{ to: config.krkAddress, data: balanceData }, 'latest'])) as string,
|
||||
);
|
||||
const krkBought = (await erc20Balance(config.rpcUrl, config.krkAddress, config.accountAddress)) - krkBefore;
|
||||
console.log(`[market] Bought ${krkBought} KRK`);
|
||||
|
||||
if (krkBought === 0n) {
|
||||
provider.destroy();
|
||||
throw new Error('[market] roundTripSwap: bought 0 KRK — pool may be empty or price limit hit');
|
||||
}
|
||||
|
||||
|
|
@ -107,12 +110,7 @@ export async function roundTripSwap(
|
|||
|
||||
// Step 5: Sell all KRK (KRK → WETH)
|
||||
console.log(`[market] Selling ${krkBought} KRK back to WETH...`);
|
||||
const wethBefore = BigInt(
|
||||
(await rpcCall(config.rpcUrl, 'eth_call', [
|
||||
{ to: WETH, data: balanceOfSelector + config.accountAddress.slice(2).padStart(64, '0') },
|
||||
'latest',
|
||||
])) as string,
|
||||
);
|
||||
const wethBefore = await erc20Balance(config.rpcUrl, WETH, config.accountAddress);
|
||||
const sellData = routerIface.encodeFunctionData('exactInputSingle', [
|
||||
{
|
||||
tokenIn: config.krkAddress,
|
||||
|
|
@ -127,13 +125,9 @@ export async function roundTripSwap(
|
|||
const sellTx = await wallet.sendTransaction({ to: SWAP_ROUTER, data: sellData });
|
||||
await waitForReceipt(config.rpcUrl, sellTx.hash);
|
||||
|
||||
const wethAfter = BigInt(
|
||||
(await rpcCall(config.rpcUrl, 'eth_call', [
|
||||
{ to: WETH, data: balanceOfSelector + config.accountAddress.slice(2).padStart(64, '0') },
|
||||
'latest',
|
||||
])) as string,
|
||||
);
|
||||
const wethRecovered = wethAfter - wethBefore;
|
||||
provider.destroy();
|
||||
|
||||
const wethRecovered = (await erc20Balance(config.rpcUrl, WETH, config.accountAddress)) - wethBefore;
|
||||
console.log(`[market] Recovered ${wethRecovered} WETH`);
|
||||
|
||||
return { krkBought, wethRecovered };
|
||||
|
|
|
|||
|
|
@ -7,13 +7,21 @@ import { Interface, JsonRpcProvider, Wallet } from 'ethers';
|
|||
import { rpcCall } from './rpc.js';
|
||||
import { waitForReceipt } from './swap.js';
|
||||
|
||||
const RECENTER_ABI = ['function recenter() external returns (bool isUp)'];
|
||||
const RECENTER_ABI = [
|
||||
'function recenter() external returns (bool isUp)',
|
||||
'event Recentered(int24 indexed currentTick, bool indexed isUp)',
|
||||
];
|
||||
|
||||
export interface RecenterConfig {
|
||||
rpcUrl: string;
|
||||
/** Address of the LiquidityManager contract */
|
||||
lmAddress: string;
|
||||
/** Private key of an account with recenter access (deployer or txnBot) */
|
||||
/**
|
||||
* Private key of an account with recenter access.
|
||||
* In the local Anvil stack, recenterAccess is granted to the deployer
|
||||
* (Anvil account index 0) and the txnBot (Anvil account index 2).
|
||||
* See scripts/bootstrap-common.sh for how access is granted at deploy time.
|
||||
*/
|
||||
privateKey: string;
|
||||
/** Address corresponding to privateKey */
|
||||
accountAddress: string;
|
||||
|
|
@ -22,9 +30,9 @@ export interface RecenterConfig {
|
|||
/**
|
||||
* Call LiquidityManager.recenter() from an authorized account.
|
||||
*
|
||||
* In the local Anvil stack, recenterAccess is granted to:
|
||||
* - Deployer (account 0): PK 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
||||
* - TxnBot (account 2): derived from mnemonic index 2
|
||||
* Reads isUp from the Recentered(int24 currentTick, bool isUp) event emitted
|
||||
* by the mined transaction — not from a follow-up eth_call, which would
|
||||
* simulate a second recenter on already-updated state and return the wrong value.
|
||||
*
|
||||
* @returns isUp - true if price moved up
|
||||
*/
|
||||
|
|
@ -40,15 +48,29 @@ export async function triggerRecenter(config: RecenterConfig): Promise<boolean>
|
|||
await waitForReceipt(config.rpcUrl, tx.hash);
|
||||
console.log(`[recenter] recenter() mined: ${tx.hash}`);
|
||||
|
||||
// Decode return value via eth_call (receipt doesn't expose return data)
|
||||
const returnData = (await rpcCall(config.rpcUrl, 'eth_call', [
|
||||
{ from: config.accountAddress, to: config.lmAddress, data },
|
||||
'latest',
|
||||
])) as string;
|
||||
const [isUp] = iface.decodeFunctionResult('recenter', returnData);
|
||||
provider.destroy();
|
||||
|
||||
// Parse isUp from the Recentered event in the receipt logs.
|
||||
// A follow-up eth_call would simulate on post-recenter state and give the wrong result.
|
||||
const receipt = (await rpcCall(config.rpcUrl, 'eth_getTransactionReceipt', [tx.hash])) as {
|
||||
logs: Array<{ address: string; topics: string[]; data: string }>;
|
||||
};
|
||||
|
||||
const recenterEventTopic = iface.getEvent('Recentered')!.topicHash;
|
||||
const log = receipt.logs.find(
|
||||
l =>
|
||||
l.address.toLowerCase() === config.lmAddress.toLowerCase() &&
|
||||
l.topics[0] === recenterEventTopic,
|
||||
);
|
||||
if (!log) {
|
||||
throw new Error('[recenter] Recentered event not found in receipt — did the tx revert silently?');
|
||||
}
|
||||
|
||||
const parsed = iface.parseLog({ topics: log.topics, data: log.data });
|
||||
const isUp = Boolean(parsed!.args.isUp);
|
||||
console.log(`[recenter] isUp = ${isUp}`);
|
||||
|
||||
return Boolean(isUp);
|
||||
return isUp;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,6 +80,7 @@ export async function triggerRecenter(config: RecenterConfig): Promise<boolean>
|
|||
* Uses the anvil_mine RPC method.
|
||||
*/
|
||||
export async function mineBlocks(rpcUrl: string, blocks: number): Promise<void> {
|
||||
if (blocks <= 0) throw new Error(`mineBlocks: blocks must be > 0, got ${blocks}`);
|
||||
await rpcCall(rpcUrl, 'anvil_mine', ['0x' + blocks.toString(16)]);
|
||||
console.log(`[recenter] Mined ${blocks} blocks`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue