fix: evaluator: add market simulation and recenter helpers (#455)
- Export waitForReceipt from swap.ts so market.ts and recenter.ts can reuse it - Add market.ts with roundTripSwap: direct-RPC buy+sell round-trip using ethers Wallet - Add recenter.ts with triggerRecenter (calls LiquidityManager.recenter()) and mineBlocks (anvil_mine) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4066ea6db4
commit
1973ccf25b
3 changed files with 204 additions and 1 deletions
140
scripts/harb-evaluator/helpers/market.ts
Normal file
140
scripts/harb-evaluator/helpers/market.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* Direct-RPC market simulation helper.
|
||||
*
|
||||
* Executes swaps via ethers Wallet (not browser/Playwright) so tests can
|
||||
* simulate market activity from non-user accounts without a UI.
|
||||
*/
|
||||
import { Interface, JsonRpcProvider, Wallet } from 'ethers';
|
||||
import { waitForReceipt } from './swap.js';
|
||||
import { rpcCall } from './rpc.js';
|
||||
|
||||
// Infrastructure addresses stable across Anvil forks of Base Sepolia
|
||||
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 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)',
|
||||
];
|
||||
|
||||
export interface MarketSwapConfig {
|
||||
rpcUrl: string;
|
||||
/** Anvil account private key for signing */
|
||||
privateKey: string;
|
||||
/** Account address */
|
||||
accountAddress: string;
|
||||
/** KRK token address */
|
||||
krkAddress: 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,
|
||||
* but generates trading volume through the pool.
|
||||
*
|
||||
* Steps:
|
||||
* 1. Wrap ETH to WETH: call WETH.deposit{value: ethAmount}()
|
||||
* 2. Approve WETH to SwapRouter02
|
||||
* 3. exactInputSingle: WETH → KRK (buy)
|
||||
* 4. Approve KRK to SwapRouter02
|
||||
* 5. exactInputSingle: KRK → WETH (sell all)
|
||||
*
|
||||
* @returns Object with { krkBought: bigint, wethRecovered: bigint }
|
||||
*/
|
||||
export async function roundTripSwap(
|
||||
config: MarketSwapConfig,
|
||||
ethAmount: bigint,
|
||||
): Promise<{ krkBought: bigint; wethRecovered: bigint }> {
|
||||
const provider = new JsonRpcProvider(config.rpcUrl);
|
||||
const wallet = new Wallet(config.privateKey, provider);
|
||||
|
||||
const wethIface = new Interface(WETH_ABI);
|
||||
const erc20Iface = new Interface(ERC20_ABI);
|
||||
const routerIface = new Interface(ROUTER_ABI);
|
||||
|
||||
const maxApproval = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
|
||||
|
||||
// Step 1: Wrap ETH → WETH
|
||||
console.log(`[market] Wrapping ${ethAmount} wei ETH to WETH...`);
|
||||
const depositData = wethIface.encodeFunctionData('deposit', []);
|
||||
const depositTx = await wallet.sendTransaction({ to: WETH, data: depositData, value: ethAmount });
|
||||
await waitForReceipt(config.rpcUrl, depositTx.hash);
|
||||
console.log('[market] ETH wrapped to WETH');
|
||||
|
||||
// Step 2: Approve WETH to router
|
||||
console.log('[market] Approving WETH to router...');
|
||||
const approveWethData = erc20Iface.encodeFunctionData('approve', [SWAP_ROUTER, maxApproval]);
|
||||
const approveWethTx = await wallet.sendTransaction({ to: WETH, data: approveWethData });
|
||||
await waitForReceipt(config.rpcUrl, approveWethTx.hash);
|
||||
console.log('[market] WETH approved');
|
||||
|
||||
// Step 3: Buy KRK (WETH → KRK)
|
||||
console.log('[market] Buying KRK (WETH → KRK)...');
|
||||
const buyData = routerIface.encodeFunctionData('exactInputSingle', [
|
||||
{
|
||||
tokenIn: WETH,
|
||||
tokenOut: config.krkAddress,
|
||||
fee: POOL_FEE,
|
||||
recipient: config.accountAddress,
|
||||
amountIn: ethAmount,
|
||||
amountOutMinimum: 0n,
|
||||
sqrtPriceLimitX96: 0n,
|
||||
},
|
||||
]);
|
||||
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,
|
||||
);
|
||||
console.log(`[market] Bought ${krkBought} KRK`);
|
||||
|
||||
if (krkBought === 0n) {
|
||||
throw new Error('[market] roundTripSwap: bought 0 KRK — pool may be empty or price limit hit');
|
||||
}
|
||||
|
||||
// Step 4: Approve KRK to router
|
||||
console.log('[market] Approving KRK to router...');
|
||||
const approveKrkData = erc20Iface.encodeFunctionData('approve', [SWAP_ROUTER, maxApproval]);
|
||||
const approveKrkTx = await wallet.sendTransaction({ to: config.krkAddress, data: approveKrkData });
|
||||
await waitForReceipt(config.rpcUrl, approveKrkTx.hash);
|
||||
console.log('[market] KRK approved');
|
||||
|
||||
// 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 sellData = routerIface.encodeFunctionData('exactInputSingle', [
|
||||
{
|
||||
tokenIn: config.krkAddress,
|
||||
tokenOut: WETH,
|
||||
fee: POOL_FEE,
|
||||
recipient: config.accountAddress,
|
||||
amountIn: krkBought,
|
||||
amountOutMinimum: 0n,
|
||||
sqrtPriceLimitX96: 0n,
|
||||
},
|
||||
]);
|
||||
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;
|
||||
console.log(`[market] Recovered ${wethRecovered} WETH`);
|
||||
|
||||
return { krkBought, wethRecovered };
|
||||
}
|
||||
63
scripts/harb-evaluator/helpers/recenter.ts
Normal file
63
scripts/harb-evaluator/helpers/recenter.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* LiquidityManager recenter helper and Anvil block-mining utility.
|
||||
*
|
||||
* Pure Node.js — no browser/Playwright dependency.
|
||||
*/
|
||||
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)'];
|
||||
|
||||
export interface RecenterConfig {
|
||||
rpcUrl: string;
|
||||
/** Address of the LiquidityManager contract */
|
||||
lmAddress: string;
|
||||
/** Private key of an account with recenter access (deployer or txnBot) */
|
||||
privateKey: string;
|
||||
/** Address corresponding to privateKey */
|
||||
accountAddress: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @returns isUp - true if price moved up
|
||||
*/
|
||||
export async function triggerRecenter(config: RecenterConfig): Promise<boolean> {
|
||||
const provider = new JsonRpcProvider(config.rpcUrl);
|
||||
const wallet = new Wallet(config.privateKey, provider);
|
||||
|
||||
const iface = new Interface(RECENTER_ABI);
|
||||
const data = iface.encodeFunctionData('recenter', []);
|
||||
|
||||
console.log('[recenter] Calling LiquidityManager.recenter()...');
|
||||
const tx = await wallet.sendTransaction({ to: config.lmAddress, data });
|
||||
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);
|
||||
console.log(`[recenter] isUp = ${isUp}`);
|
||||
|
||||
return Boolean(isUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mine `blocks` empty blocks on Anvil to advance time.
|
||||
* Useful to get past MIN_RECENTER_INTERVAL (if set).
|
||||
*
|
||||
* Uses the anvil_mine RPC method.
|
||||
*/
|
||||
export async function mineBlocks(rpcUrl: string, blocks: number): Promise<void> {
|
||||
await rpcCall(rpcUrl, 'anvil_mine', ['0x' + blocks.toString(16)]);
|
||||
console.log(`[recenter] Mined ${blocks} blocks`);
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ async function erc20BalanceOf(rpcUrl: string, tokenAddress: string, account: str
|
|||
* Throws if the transaction was mined but reverted (status 0x0) so callers
|
||||
* get a clear failure rather than a confusing downstream balance-assertion error.
|
||||
*/
|
||||
async function waitForReceipt(rpcUrl: string, txHash: string, maxAttempts = 20): Promise<void> {
|
||||
export async function waitForReceipt(rpcUrl: string, txHash: string, maxAttempts = 20): Promise<void> {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const receipt = (await rpcCall(rpcUrl, 'eth_getTransactionReceipt', [txHash])) as Record<
|
||||
string,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue