import { expect, test } from '@playwright/test'; import { getStackConfig, validateStackHealthy } from '../setup/stack'; const STACK_CONFIG = getStackConfig(); const STACK_RPC_URL = STACK_CONFIG.rpcUrl; // Solidity function selectors const POSITIONS_SELECTOR = '0xf86aafc0'; // positions(uint8) const RECENTER_SELECTOR = '0xf46e1346'; // recenter() const RECENTER_ACCESS_SELECTOR = '0xdef51130'; // recenterAccess() // Position stages (matches ThreePositionStrategy.Stage enum) const STAGE_FLOOR = 0; const STAGE_ANCHOR = 1; const STAGE_DISCOVERY = 2; const STAGE_NAMES = ['FLOOR', 'ANCHOR', 'DISCOVERY'] as const; interface Position { liquidity: bigint; tickLower: number; tickUpper: number; } async function rpcCall(method: string, params: unknown[]): Promise { const response = await fetch(STACK_RPC_URL, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), }); const data = await response.json(); if (data.error) throw new Error(`RPC error: ${data.error.message}`); return data.result; } async function rpcCallRaw(method: string, params: unknown[]): Promise<{ result?: unknown; error?: { message: string; data?: string } }> { const response = await fetch(STACK_RPC_URL, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), }); return response.json(); } function readPosition(result: string): Position { const liquidity = BigInt('0x' + result.slice(2, 66)); const tickLowerRaw = BigInt('0x' + result.slice(66, 130)); const tickUpperRaw = BigInt('0x' + result.slice(130, 194)); const toInt24 = (val: bigint): number => { const n = Number(val & 0xffffffn); return n >= 0x800000 ? n - 0x1000000 : n; }; return { liquidity, tickLower: toInt24(tickLowerRaw), tickUpper: toInt24(tickUpperRaw), }; } async function getPosition(lmAddress: string, stage: number): Promise { const stageHex = stage.toString(16).padStart(64, '0'); const result = (await rpcCall('eth_call', [ { to: lmAddress, data: `${POSITIONS_SELECTOR}${stageHex}` }, 'latest', ])) as string; return readPosition(result); } test.describe('Recenter Positions', () => { test.beforeAll(async () => { await validateStackHealthy(STACK_CONFIG); }); test('bootstrap recenter created valid positions with liquidity', async () => { const lmAddress = STACK_CONFIG.contracts.LiquidityManager; console.log(`[TEST] LiquidityManager: ${lmAddress}`); for (const stage of [STAGE_FLOOR, STAGE_ANCHOR, STAGE_DISCOVERY]) { const position = await getPosition(lmAddress, stage); const name = STAGE_NAMES[stage]; console.log( `[TEST] ${name}: liquidity=${position.liquidity}, ticks=[${position.tickLower}, ${position.tickUpper}]`, ); expect(position.liquidity).toBeGreaterThan(0n); expect(position.tickUpper).toBeGreaterThan(position.tickLower); console.log(`[TEST] ${name} position verified`); } // Verify position ordering: floor above anchor above discovery (token0isWeth) const floor = await getPosition(lmAddress, STAGE_FLOOR); const anchor = await getPosition(lmAddress, STAGE_ANCHOR); const discovery = await getPosition(lmAddress, STAGE_DISCOVERY); // Floor should be above anchor (higher tick = cheaper KRK side when token0isWeth) expect(floor.tickLower).toBeGreaterThanOrEqual(anchor.tickUpper); // Discovery should be below anchor expect(discovery.tickUpper).toBeLessThanOrEqual(anchor.tickLower); console.log('[TEST] Position ordering verified: discovery < anchor < floor'); console.log('[TEST] All three positions have non-zero liquidity'); }); test('recenter() enforces access control', async () => { const lmAddress = STACK_CONFIG.contracts.LiquidityManager; // Read the recenterAccess address const recenterAccessResult = (await rpcCall('eth_call', [ { to: lmAddress, data: RECENTER_ACCESS_SELECTOR }, 'latest', ])) as string; const recenterAddr = '0x' + recenterAccessResult.slice(26); console.log(`[TEST] recenterAccess: ${recenterAddr}`); expect(recenterAddr).not.toBe('0x' + '0'.repeat(40)); console.log('[TEST] recenterAccess is set (not zero address)'); // Try calling recenter from an unauthorized address — should revert with "access denied" const unauthorizedAddr = '0x1111111111111111111111111111111111111111'; const callResult = await rpcCallRaw('eth_call', [ { from: unauthorizedAddr, to: lmAddress, data: RECENTER_SELECTOR }, 'latest', ]); expect(callResult.error).toBeDefined(); expect(callResult.error!.message).toContain('access denied'); console.log('[TEST] Unauthorized recenter correctly rejected with "access denied"'); }); test('recenter() enforces amplitude check', async () => { const lmAddress = STACK_CONFIG.contracts.LiquidityManager; // Read the recenterAccess address const recenterAccessResult = (await rpcCall('eth_call', [ { to: lmAddress, data: RECENTER_ACCESS_SELECTOR }, 'latest', ])) as string; const recenterAddr = '0x' + recenterAccessResult.slice(26); // Call recenter from the authorized address without moving the price // Should revert with "amplitude not reached" since price hasn't moved enough const callResult = await rpcCallRaw('eth_call', [ { from: recenterAddr, to: lmAddress, data: RECENTER_SELECTOR }, 'latest', ]); // The call should fail — either "amplitude not reached" or just revert // (Pool state may vary, but it should not succeed without price movement) expect(callResult.error).toBeDefined(); console.log(`[TEST] Recenter guard active: ${callResult.error!.message}`); console.log('[TEST] Recenter correctly prevents no-op recentering'); }); });