- 01-acquire-and-stake: replace flat 3 s wait with a 30 s polling loop so
Ponder indexing lag no longer causes a spurious positions.length=0 failure.
- 05-optimizer-integration test 1: replace hard-coded OptimizerV3 bear-market
constants (anchorShare=3e17, anchorWidth=100, discoveryDepth=3e17) with
Optimizer.sol invariant checks:
capitalInefficiency + anchorShare == 1e18
discoveryDepth == anchorShare
anchorWidth ∈ [10, 80]
- 05-optimizer-integration test 2: decouple bootstrap-position assertion from
current optimizer state. Earlier tests change staking state, so the current
optimizer anchorWidth differs from the one used at bootstrap time. Instead,
reverse-calculate the implied anchorWidth from the observed anchor spread and
verify it lies within [10, 80].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
187 lines
7.2 KiB
TypeScript
187 lines
7.2 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 GET_LIQUIDITY_PARAMS_SELECTOR = '0xbd53c0dc'; // getLiquidityParams()
|
|
const POSITIONS_SELECTOR = '0xf86aafc0'; // positions(uint8)
|
|
|
|
// Optimizer.sol invariants (capitalInefficiency + anchorShare = 1e18)
|
|
const ONE_ETHER = 10n ** 18n;
|
|
|
|
// Position stages
|
|
const STAGE_FLOOR = 0;
|
|
const STAGE_ANCHOR = 1;
|
|
const STAGE_DISCOVERY = 2;
|
|
|
|
// TICK_SPACING from ThreePositionStrategy
|
|
const TICK_SPACING = 200;
|
|
|
|
interface LiquidityParams {
|
|
capitalInefficiency: bigint;
|
|
anchorShare: bigint;
|
|
anchorWidth: bigint;
|
|
discoveryDepth: bigint;
|
|
}
|
|
|
|
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 readLiquidityParams(optimizerAddress: string): Promise<LiquidityParams> {
|
|
const result = (await rpcCall('eth_call', [
|
|
{ to: optimizerAddress, data: GET_LIQUIDITY_PARAMS_SELECTOR },
|
|
'latest',
|
|
])) as string;
|
|
|
|
return {
|
|
capitalInefficiency: BigInt('0x' + result.slice(2, 66)),
|
|
anchorShare: BigInt('0x' + result.slice(66, 130)),
|
|
anchorWidth: BigInt('0x' + result.slice(130, 194)),
|
|
discoveryDepth: BigInt('0x' + result.slice(194, 258)),
|
|
};
|
|
}
|
|
|
|
function toInt24(val: bigint): number {
|
|
const n = Number(val & 0xffffffn);
|
|
return n >= 0x800000 ? n - 0x1000000 : n;
|
|
}
|
|
|
|
test.describe('Optimizer Integration', () => {
|
|
test.beforeAll(async () => {
|
|
await validateStackHealthy(STACK_CONFIG);
|
|
});
|
|
|
|
test('Optimizer proxy returns valid parameters', async () => {
|
|
const optimizerAddress = STACK_CONFIG.contracts.OptimizerProxy;
|
|
if (!optimizerAddress) {
|
|
console.log(
|
|
'[TEST] SKIP: OptimizerProxy not in deployments-local.json (older deployment)',
|
|
);
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
console.log(`[TEST] OptimizerProxy: ${optimizerAddress}`);
|
|
|
|
const params = await readLiquidityParams(optimizerAddress);
|
|
console.log(`[TEST] capitalInefficiency: ${params.capitalInefficiency}`);
|
|
console.log(`[TEST] anchorShare: ${params.anchorShare}`);
|
|
console.log(`[TEST] anchorWidth: ${params.anchorWidth}`);
|
|
console.log(`[TEST] discoveryDepth: ${params.discoveryDepth}`);
|
|
|
|
// Optimizer.sol invariants:
|
|
// capitalInefficiency + anchorShare == 1e18
|
|
expect(params.capitalInefficiency + params.anchorShare).toBe(ONE_ETHER);
|
|
// discoveryDepth == anchorShare
|
|
expect(params.discoveryDepth).toBe(params.anchorShare);
|
|
// anchorWidth in [10, 80]
|
|
expect(params.anchorWidth).toBeGreaterThanOrEqual(10n);
|
|
expect(params.anchorWidth).toBeLessThanOrEqual(80n);
|
|
|
|
console.log('[TEST] Optimizer returns valid parameters (invariants satisfied)');
|
|
});
|
|
|
|
test('bootstrap positions reflect valid optimizer anchorWidth', async () => {
|
|
const lmAddress = STACK_CONFIG.contracts.LiquidityManager;
|
|
const optimizerAddress = STACK_CONFIG.contracts.OptimizerProxy;
|
|
if (!optimizerAddress) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Read anchor position from LM (created by bootstrap's recenter call).
|
|
// Note: optimizer state may have changed since bootstrap (e.g. staking activity in
|
|
// earlier tests), so we don't read the *current* optimizer params here. Instead
|
|
// we reverse-calculate the anchorWidth that was in effect when recenter() ran and
|
|
// verify it falls within Optimizer.sol's valid range [10, 80].
|
|
const anchorResult = (await rpcCall('eth_call', [
|
|
{
|
|
to: lmAddress,
|
|
data: `${POSITIONS_SELECTOR}${'1'.padStart(64, '0')}`, // Stage.ANCHOR = 1
|
|
},
|
|
'latest',
|
|
])) as string;
|
|
|
|
const tickLower = toInt24(BigInt('0x' + anchorResult.slice(66, 130)));
|
|
const tickUpper = toInt24(BigInt('0x' + anchorResult.slice(130, 194)));
|
|
const anchorSpread = tickUpper - tickLower;
|
|
|
|
console.log(`[TEST] Anchor position: ticks=[${tickLower}, ${tickUpper}], spread=${anchorSpread}`);
|
|
|
|
// Reverse the formula to recover anchorWidth:
|
|
// anchorSpread = 2 * (TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100))
|
|
// => anchorWidth = (anchorSpread / 2 - TICK_SPACING) * 100 / (34 * TICK_SPACING)
|
|
const halfSpread = anchorSpread / 2;
|
|
expect(halfSpread).toBeGreaterThan(TICK_SPACING);
|
|
|
|
const impliedAnchorWidth = Math.round(((halfSpread - TICK_SPACING) * 100) / (34 * TICK_SPACING));
|
|
console.log(`[TEST] Implied anchorWidth from spread: ${impliedAnchorWidth}`);
|
|
|
|
// Optimizer.sol constrains anchorWidth to [10, 80]
|
|
expect(impliedAnchorWidth).toBeGreaterThanOrEqual(10);
|
|
expect(impliedAnchorWidth).toBeLessThanOrEqual(80);
|
|
|
|
// Confirm the implied anchorWidth reproduces the exact spread (no rounding error)
|
|
const expectedSpacing = TICK_SPACING + (34 * impliedAnchorWidth * TICK_SPACING) / 100;
|
|
const expectedSpread = expectedSpacing * 2;
|
|
expect(anchorSpread).toBe(expectedSpread);
|
|
|
|
console.log(`[TEST] Anchor spread ${anchorSpread} corresponds to valid anchorWidth=${impliedAnchorWidth}`);
|
|
});
|
|
|
|
test('all three positions have valid relative sizing', async () => {
|
|
const lmAddress = STACK_CONFIG.contracts.LiquidityManager;
|
|
const optimizerAddress = STACK_CONFIG.contracts.OptimizerProxy;
|
|
if (!optimizerAddress) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Read all three positions
|
|
const positions = [];
|
|
const stageNames = ['FLOOR', 'ANCHOR', 'DISCOVERY'];
|
|
for (const stage of [STAGE_FLOOR, STAGE_ANCHOR, STAGE_DISCOVERY]) {
|
|
const stageHex = stage.toString(16).padStart(64, '0');
|
|
const result = (await rpcCall('eth_call', [
|
|
{ to: lmAddress, data: `${POSITIONS_SELECTOR}${stageHex}` },
|
|
'latest',
|
|
])) as string;
|
|
|
|
const liquidity = BigInt('0x' + result.slice(2, 66));
|
|
const tickLower = toInt24(BigInt('0x' + result.slice(66, 130)));
|
|
const tickUpper = toInt24(BigInt('0x' + result.slice(130, 194)));
|
|
const spread = tickUpper - tickLower;
|
|
|
|
positions.push({ liquidity, tickLower, tickUpper, spread });
|
|
console.log(`[TEST] ${stageNames[stage]}: spread=${spread}, liquidity=${liquidity}`);
|
|
}
|
|
|
|
// Floor should be narrow (TICK_SPACING width = 200 ticks)
|
|
expect(positions[0].spread).toBe(TICK_SPACING);
|
|
console.log('[TEST] Floor has expected narrow width (200 ticks)');
|
|
|
|
// Anchor should be wider than floor
|
|
expect(positions[1].spread).toBeGreaterThan(positions[0].spread);
|
|
console.log('[TEST] Anchor is wider than floor');
|
|
|
|
// Discovery should have significant spread
|
|
expect(positions[2].spread).toBeGreaterThan(0);
|
|
console.log('[TEST] Discovery has positive spread');
|
|
|
|
// Floor liquidity should be highest (concentrated in narrow range)
|
|
expect(positions[0].liquidity).toBeGreaterThan(positions[1].liquidity);
|
|
console.log('[TEST] Floor liquidity > anchor liquidity (as expected for concentrated position)');
|
|
|
|
console.log('[TEST] All position sizing validated against optimizer parameters');
|
|
});
|
|
});
|