feat: OptimizerV3 with direct 2D staking-to-LP parameter mapping

Core protocol changes for launch readiness:

- OptimizerV3: binary bear/bull mapping from (staking%, avgTax) — avoids
  exploitable AW 30-90 kill zone. Bear: AS=30%, AW=100, CI=0, DD=0.3e18.
  Bull: AS=100%, AW=20, CI=0, DD=1e18. UUPS upgradeable with __gap[48].
- Directional VWAP: only records prices on ETH inflow (buys), preventing
  sell-side dilution of price memory
- Floor formula: unified max(scarcity, mirror, clamp) — VWAP mirror uses
  distance from adjusted VWAP as floor distance, no branching
- PriceOracle (M-1 fix): correct fallback TWAP divisor (60000s, not 300s)
- Access control (M-2 fix): deployer-only guard on one-time setters
- Recenter rate limit (M-3 fix): 60-second cooldown for open recenters
- Safe fallback params: recenter() optimizer-failure defaults changed from
  exploitable CI=50%/AW=50 to safe bear-mode CI=0/AW=100
- Recentered event for monitoring and indexing
- VERSION bump to 2, kraiken-lib COMPATIBLE_CONTRACT_VERSIONS updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-02-13 18:21:18 +00:00
parent 21857ae8ca
commit 85350caf52
38 changed files with 3793 additions and 205 deletions

View file

@ -72,6 +72,7 @@ test.describe('Max Stake All Tax Rates', () => {
});
test('fills all tax rates until maxStake is reached', async ({ browser, request }) => {
test.setTimeout(10 * 60 * 1000); // 10 minutes — this test creates 30 staking positions via UI
console.log('[TEST] Creating wallet context...');
const context = await createWalletContext(browser, {
privateKey: ACCOUNT_PRIVATE_KEY,

View file

@ -0,0 +1,153 @@
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<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() 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');
});
});

View file

@ -0,0 +1,177 @@
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)
// OptimizerV3 known bear-market parameters
const BEAR_ANCHOR_SHARE = 3n * 10n ** 17n; // 3e17 = 30%
const BEAR_ANCHOR_WIDTH = 100n;
const BEAR_DISCOVERY_DEPTH = 3n * 10n ** 17n; // 3e17
// 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('OptimizerV3 proxy returns valid bear-market 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}`);
// With no staking activity, OptimizerV3 should return bear-market defaults
expect(params.capitalInefficiency).toBe(0n);
expect(params.anchorShare).toBe(BEAR_ANCHOR_SHARE);
expect(params.anchorWidth).toBe(BEAR_ANCHOR_WIDTH);
expect(params.discoveryDepth).toBe(BEAR_DISCOVERY_DEPTH);
console.log('[TEST] OptimizerV3 returns correct bear-market parameters');
});
test('bootstrap positions reflect optimizer anchorWidth=100 parameter', async () => {
const lmAddress = STACK_CONFIG.contracts.LiquidityManager;
const optimizerAddress = STACK_CONFIG.contracts.OptimizerProxy;
if (!optimizerAddress) {
test.skip();
return;
}
// Read optimizer params
const params = await readLiquidityParams(optimizerAddress);
const anchorWidth = Number(params.anchorWidth);
console.log(`[TEST] Optimizer anchorWidth: ${anchorWidth}`);
// Read anchor position from LM (created by bootstrap's recenter call)
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}`);
// Verify the anchor spread matches the optimizer formula:
// anchorSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100)
// For anchorWidth=100: anchorSpacing = 200 + (34 * 100 * 200 / 100) = 200 + 6800 = 7000
// Full anchor = 2 * anchorSpacing = 14000 ticks
const expectedSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING) / 100;
const expectedSpread = expectedSpacing * 2;
console.log(`[TEST] Expected anchor spread: ${expectedSpread} (anchorSpacing=${expectedSpacing})`);
expect(anchorSpread).toBe(expectedSpread);
console.log('[TEST] Anchor spread matches optimizer anchorWidth=100 formula');
});
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');
});
});

View file

@ -13,6 +13,7 @@ export interface ContractAddresses {
Kraiken: string;
Stake: string;
LiquidityManager: string;
OptimizerProxy?: string;
}
export interface StackConfig {