fix: OptimizerInputCapture test harness is structurally broken (#652)

The pure override in OptimizerInputCapture could not write to storage,
and getLiquidityParams calls calculateParams via staticcall which
prevents both storage writes and event emissions.

Fix: extract the input-building normalization from getLiquidityParams
into _buildInputs() (internal view, behavior-preserving refactor).
The test harness now exposes _buildInputs() via getComputedInputs(),
allowing tests to assert actual normalized slot values.

Updated tests for pricePosition, timeSinceRecenter, volatility,
momentum, and utilizationRate to assert non-zero captured values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-19 13:57:25 +00:00
parent 411c567cd6
commit bb150671ea
2 changed files with 86 additions and 91 deletions

View file

@ -332,7 +332,7 @@ contract OptimizerTest is Test {
*/
function testCalculateParamsRevertsOnNegativeMantissa0() public {
OptimizerInput[8] memory inputs;
inputs[0] = OptimizerInput({mantissa: -1, shift: 0});
inputs[0] = OptimizerInput({ mantissa: -1, shift: 0 });
vm.expectRevert("negative mantissa");
optimizer.calculateParams(inputs);
}
@ -342,7 +342,7 @@ contract OptimizerTest is Test {
*/
function testCalculateParamsRevertsOnNegativeMantissa1() public {
OptimizerInput[8] memory inputs;
inputs[1] = OptimizerInput({mantissa: -1, shift: 0});
inputs[1] = OptimizerInput({ mantissa: -1, shift: 0 });
vm.expectRevert("negative mantissa");
optimizer.calculateParams(inputs);
}
@ -353,7 +353,7 @@ contract OptimizerTest is Test {
function testCalculateParamsRevertsOnNegativeMantissaSlots2to7() public {
for (uint256 k = 2; k < 8; k++) {
OptimizerInput[8] memory inputs;
inputs[k] = OptimizerInput({mantissa: -1, shift: 0});
inputs[k] = OptimizerInput({ mantissa: -1, shift: 0 });
vm.expectRevert("negative mantissa");
optimizer.calculateParams(inputs);
}
@ -389,22 +389,15 @@ contract OptimizerTest is Test {
* 2-6 without wiring a full protocol stack.
*/
/// @dev Harness: overrides calculateParams to write its inputs into public storage
/// so tests can assert the slot values directly.
/// @dev Harness: exposes the internal _buildInputs() so tests can assert the
/// normalized slot values that getLiquidityParams feeds into calculateParams.
contract OptimizerInputCapture is Optimizer {
int256[8] public capturedMantissa;
function calculateParams(OptimizerInput[8] memory inputs)
public
pure
virtual
override
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
// This pure function can't write storage. We rely on getLiquidityParams()
// going through staticcall, so we can't capture state here.
// Instead, call the real implementation for output correctness.
return super.calculateParams(inputs);
/// @notice Returns the mantissa of each normalized input slot.
function getComputedInputs() external view returns (int256[8] memory mantissas) {
OptimizerInput[8] memory inputs = _buildInputs();
for (uint256 i; i < 8; i++) {
mantissas[i] = inputs[i].mantissa;
}
}
}
@ -416,7 +409,8 @@ contract OptimizerVwapHarness is Optimizer {
}
contract OptimizerNormalizedInputsTest is Test {
Optimizer optimizer;
OptimizerInputCapture capture;
Optimizer optimizer; // alias points to the same proxy as `capture`
MockStake mockStake;
MockKraiken mockKraiken;
MockVWAPTracker mockVwap;
@ -433,10 +427,10 @@ contract OptimizerNormalizedInputsTest is Test {
mockPool = new MockPool();
mockLm = new MockLiquidityManagerPositions();
Optimizer impl = new Optimizer();
bytes memory initData =
abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
OptimizerInputCapture impl = new OptimizerInputCapture();
bytes memory initData = abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData);
capture = OptimizerInputCapture(address(proxy));
optimizer = Optimizer(address(proxy));
}
@ -470,8 +464,8 @@ contract OptimizerNormalizedInputsTest is Test {
ticks[0] = 0;
ticks[1] = 1000;
ticks[2] = -1000;
ticks[3] = 100000;
ticks[4] = -100000;
ticks[3] = 100_000;
ticks[4] = -100_000;
for (uint256 i = 0; i < ticks.length; i++) {
int24 origTick = ticks[i];
@ -479,10 +473,7 @@ contract OptimizerNormalizedInputsTest is Test {
uint256 priceX96 = Math.mulDiv(sqrtRatio, sqrtRatio, 1 << 96);
int24 recovered = harness.exposed_vwapToTick(priceX96);
// Allow ±1 tick error from integer sqrt truncation
assertTrue(
recovered == origTick || recovered == origTick - 1 || recovered == origTick + 1,
"round-trip tick error > 1"
);
assertTrue(recovered == origTick || recovered == origTick - 1 || recovered == origTick + 1, "round-trip tick error > 1");
}
}
@ -510,6 +501,10 @@ contract OptimizerNormalizedInputsTest is Test {
assertEq(elapsed, 43_200, "elapsed should be exactly half of MAX_STALE_SECONDS");
// 43200 * 1e18 / 86400 = 0.5e18
assertEq(elapsed * 1e18 / 86_400, 5e17, "half-stale should normalize to 0.5e18");
// Verify slot 5 via capture harness
int256[8] memory m = capture.getComputedInputs();
assertEq(m[5], int256(5e17), "slot 5 should be 0.5e18 at half-stale");
}
function testTimeSinceRecenterSaturatesAt1e18() public {
@ -535,11 +530,14 @@ contract OptimizerNormalizedInputsTest is Test {
int24 targetTick = 500;
_seedVwapAtTick(targetTick);
mockPool.setCurrentTick(targetTick); // current == vwap 0.5e18
mockPool.setRevertOnObserve(true); // disable volatility/momentum for isolation
mockPool.setRevertOnObserve(true); // disable volatility/momentum for isolation
// getLiquidityParams calculateParams uses slots 0,1 only; output unchanged.
// But we verify no revert and the state is consistent.
optimizer.getLiquidityParams();
// Verify slot 2 (pricePosition) is approximately 0.5e18 when current == vwap
int256[8] memory m = capture.getComputedInputs();
// Allow ±1 tick error from _vwapToTick integer sqrt truncation
assertTrue(m[2] > 4.5e17 && m[2] < 5.5e17, "pricePosition should be ~0.5e18 at VWAP");
}
function testPricePositionBelowLowerBound() public {
@ -601,9 +599,16 @@ contract OptimizerNormalizedInputsTest is Test {
_seedVwapAtTick(0);
mockPool.setCurrentTick(0);
// shortTwap - longTwap = 1000 ticks = MAX_MOMENTUM_TICKS momentum = 1e18
mockPool.setTwapTicks(0, 1_000); // longTwap=0, shortTwap=1000
mockPool.setTwapTicks(0, 1000); // longTwap=0, shortTwap=1000
optimizer.getLiquidityParams(); // must not revert
// Verify slots 3 (volatility) and 4 (momentum) via capture harness.
// Note: with token0isWeth=false, pool ticks are negated into KRK-price space.
// Pool shortTwap=1000, longTwap=0 KRK-space twapDelta = -1000 (max bear).
int256[8] memory m = capture.getComputedInputs();
assertEq(m[3], int256(1e18), "volatility should be 1e18 at max delta");
assertEq(m[4], int256(0), "momentum should be 0 (max bear in KRK-price space)");
}
function testMomentumFullBearAtNegMaxDelta() public {
@ -611,7 +616,7 @@ contract OptimizerNormalizedInputsTest is Test {
_seedVwapAtTick(0);
mockPool.setCurrentTick(0);
// shortTwap - longTwap = -1000 = -MAX_MOMENTUM_TICKS momentum = 0
mockPool.setTwapTicks(1_000, 0); // longTwap=1000, shortTwap=0
mockPool.setTwapTicks(1000, 0); // longTwap=1000, shortTwap=0
optimizer.getLiquidityParams(); // must not revert
}
@ -638,6 +643,10 @@ contract OptimizerNormalizedInputsTest is Test {
mockPool.setRevertOnObserve(true);
optimizer.getLiquidityParams(); // must not revert
// Verify slot 6 (utilizationRate) via capture harness
int256[8] memory m = capture.getComputedInputs();
assertEq(m[6], int256(1e18), "utilizationRate should be 1e18 when tick is in anchor range");
}
function testUtilizationRateOutOfRange() public {