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:
parent
411c567cd6
commit
bb150671ea
2 changed files with 86 additions and 91 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue