harb/tests/e2e/04-recenter-positions.spec.ts
openhands 9b53f409b7 fix: update e2e tests for public recenter() — remove recenterAccess references
recenterAccess() was removed from LiquidityManager in this PR.
The old tests called recenterAccess() (selector 0xdef51130) which now
reverts, causing both recenter tests to fail.

Update tests to match the new public recenter() behavior:
- Test 1: verify any address may call recenter() without "access denied"
- Test 2: same caller pattern, guard errors are still acceptable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 01:44:15 +00:00

150 lines
5.9 KiB
TypeScript

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()
// 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<unknown> {
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<Position> {
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() is public — any address may attempt it', async () => {
const lmAddress = STACK_CONFIG.contracts.LiquidityManager;
// recenter() is now public: anyone can call it (recenterAccess was removed).
// After bootstrap the cooldown and amplitude guards will typically fire,
// but the revert reason must NOT be "access denied".
const callerAddr = '0x1111111111111111111111111111111111111111';
const callResult = await rpcCallRaw('eth_call', [
{ from: callerAddr, to: lmAddress, data: RECENTER_SELECTOR },
'latest',
]);
if (callResult.error) {
// Acceptable guard errors: cooldown, amplitude, TWAP — NOT access control
const msg = callResult.error.message ?? '';
expect(msg).not.toContain('access denied');
console.log(`[TEST] Recenter guard active (expected): ${msg}`);
console.log('[TEST] No "access denied" — access control correctly removed');
} else {
console.log('[TEST] Recenter succeeded from arbitrary address — access control is gone');
}
});
test('recenter() enforces amplitude check', async () => {
const lmAddress = STACK_CONFIG.contracts.LiquidityManager;
// Call recenter from any address without moving the price.
// Should revert with a guard error (cooldown, amplitude, or TWAP), not crash.
const callerAddr = '0x1111111111111111111111111111111111111111';
const callResult = await rpcCallRaw('eth_call', [
{ from: callerAddr, to: lmAddress, data: RECENTER_SELECTOR },
'latest',
]);
// After bootstrap's initial swap + recenter, calling recenter again may either:
// - Fail with "amplitude not reached" / "recenter cooldown" / "price deviated from oracle"
// - Succeed if contract's amplitude threshold allows it (e.g., after swap moved price)
// Both outcomes are valid — the key invariant is that recenter doesn't crash unexpectedly
if (callResult.error) {
console.log(`[TEST] Recenter guard active: ${callResult.error.message}`);
console.log('[TEST] Recenter correctly prevents no-op recentering');
} else {
console.log('[TEST] Recenter succeeded (price movement from bootstrap swap was sufficient)');
console.log('[TEST] This is acceptable — amplitude threshold was met');
}
});
});