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

@ -1,14 +1,14 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import {Kraiken} from "./Kraiken.sol";
import {Stake} from "./Stake.sol";
import {IOptimizer, OptimizerInput} from "./IOptimizer.sol";
import { IOptimizer, OptimizerInput } from "./IOptimizer.sol";
import { Kraiken } from "./Kraiken.sol";
import { Stake } from "./Stake.sol";
import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
import {Math} from "@openzeppelin/utils/math/Math.sol";
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol";
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
import { Math } from "@openzeppelin/utils/math/Math.sol";
// ---------------------------------------------------------------------------
// Dyadic rational interface Push3's native number format.
@ -91,12 +91,12 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
// ---- Extended data sources for input slots 2-6 ----
// These are optional; unset addresses leave the corresponding slots as 0.
address public vwapTracker; // slots 2-4 source (VWAPTracker)
address public pool; // slots 2-4, 6 source (Uniswap V3 pool)
address public vwapTracker; // slots 2-4 source (VWAPTracker)
address public pool; // slots 2-4, 6 source (Uniswap V3 pool)
uint256 public lastRecenterTimestamp; // slot 5 source (updated via recordRecenter)
address public recenterRecorder; // authorized to call recordRecenter
address public liquidityManager; // slot 6 source (LiquidityManager positions)
bool public token0isWeth; // true when WETH is token0 in the pool (flips tick direction)
address public recenterRecorder; // authorized to call recordRecenter
address public liquidityManager; // slot 6 source (LiquidityManager positions)
bool public token0isWeth; // true when WETH is token0 in the pool (flips tick direction)
// ---- Normalization constants ----
@ -107,11 +107,11 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
/// @notice Maximum tick divergence (shortTwap vs longTwap) that maps to full volatility (1e18).
/// 1 000 ticks 10% price swing.
uint256 internal constant MAX_VOLATILITY_TICKS = 1_000;
uint256 internal constant MAX_VOLATILITY_TICKS = 1000;
/// @notice Maximum tick trend signal (shortTwap - longTwap) for momentum saturation.
/// 1 000 ticks 10% price trend.
int256 internal constant MAX_MOMENTUM_TICKS = 1_000;
int256 internal constant MAX_MOMENTUM_TICKS = 1000;
/// @notice Time (seconds) beyond which timeSinceRecenter saturates at 1e18. 86 400 = 1 day.
uint256 internal constant MAX_STALE_SECONDS = 86_400;
@ -120,7 +120,7 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
uint32 internal constant SHORT_TWAP_WINDOW = 300;
/// @notice Long TWAP window for volatility / momentum baseline (30 minutes).
uint32 internal constant LONG_TWAP_WINDOW = 1_800;
uint32 internal constant LONG_TWAP_WINDOW = 1800;
/// @dev Reverts if the caller is not the admin.
error UnauthorizedAccount(address account);
@ -161,7 +161,7 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
}
}
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {}
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { }
// ---- Data-source configuration (admin only) ----
@ -173,10 +173,7 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
* @param _token0isWeth True when WETH is token0 in the pool. Needed to correctly
* orient tick-based indicators (pricePosition, volatility, momentum).
*/
function setDataSources(address _vwapTracker, address _pool, address _liquidityManager, bool _token0isWeth)
external
onlyAdmin
{
function setDataSources(address _vwapTracker, address _pool, address _liquidityManager, bool _token0isWeth) external onlyAdmin {
vwapTracker = _vwapTracker;
pool = _pool;
liquidityManager = _liquidityManager;
@ -209,7 +206,7 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
* value = mantissa × 2^(-0) = mantissa.
*/
function _toDyadic(int256 value) internal pure returns (OptimizerInput memory) {
return OptimizerInput({mantissa: value, shift: 0});
return OptimizerInput({ mantissa: value, shift: 0 });
}
/**
@ -219,11 +216,7 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
* ({capitalInefficiency:0, anchorShare:3e17, anchorWidth:100, discoveryDepth:3e17}).
* Update both locations together if the safe defaults ever change.
*/
function _bearDefaults()
internal
pure
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
function _bearDefaults() internal pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
return (0, 3e17, 100, 3e17);
}
@ -265,11 +258,7 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
* @param percentageStaked The percentage (in 1e18 precision) of the authorized stake that is currently staked.
* @return sentimentValue A value in the range 0 to 1e18 where 1e18 represents the worst sentiment.
*/
function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked)
public
pure
returns (uint256 sentimentValue)
{
function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked) public pure returns (uint256 sentimentValue) {
// Ensure percentageStaked doesn't exceed 100%
require(percentageStaked <= 1e18, "Invalid percentage staked");
@ -411,14 +400,12 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
* @return anchorWidth Anchor position width in tick units (uint24)
* @return discoveryDepth Discovery liquidity density (0..1e18)
*/
function getLiquidityParams()
external
view
override
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
OptimizerInput[8] memory inputs;
/**
* @notice Build the 8-slot normalized input array from on-chain data sources.
* @dev Extracted so test harnesses can observe the computed inputs without
* duplicating normalization logic. All slots are in [0, 1e18].
*/
function _buildInputs() internal view returns (OptimizerInput[8] memory inputs) {
// Slot 0: percentageStaked
inputs[0] = _toDyadic(int256(stake.getPercentageStaked()));
@ -460,16 +447,12 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
// Fails gracefully if the pool lacks sufficient observation history.
{
uint32[] memory secondsAgo = new uint32[](3);
secondsAgo[0] = LONG_TWAP_WINDOW; // 1800 s long baseline
secondsAgo[0] = LONG_TWAP_WINDOW; // 1800 s long baseline
secondsAgo[1] = SHORT_TWAP_WINDOW; // 300 s recent
secondsAgo[2] = 0; // now
try IUniswapV3PoolObserve(pool).observe(secondsAgo) returns (
int56[] memory tickCumulatives, uint160[] memory
) {
int24 longTwap =
int24((tickCumulatives[2] - tickCumulatives[0]) / int56(int32(LONG_TWAP_WINDOW)));
int24 shortTwap =
int24((tickCumulatives[2] - tickCumulatives[1]) / int56(int32(SHORT_TWAP_WINDOW)));
secondsAgo[2] = 0; // now
try IUniswapV3PoolObserve(pool).observe(secondsAgo) returns (int56[] memory tickCumulatives, uint160[] memory) {
int24 longTwap = int24((tickCumulatives[2] - tickCumulatives[0]) / int56(int32(LONG_TWAP_WINDOW)));
int24 shortTwap = int24((tickCumulatives[2] - tickCumulatives[1]) / int56(int32(SHORT_TWAP_WINDOW)));
// Adjust both TWAP ticks to KRK-price space (same sign convention)
int24 longAdj = token0isWeth ? longTwap : -longTwap;
@ -478,11 +461,8 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
// Slot 3: volatility = |shortTwap longTwap| / MAX_VOLATILITY_TICKS
{
uint256 absDelta =
twapDelta >= 0 ? uint256(twapDelta) : uint256(-twapDelta);
uint256 vol = absDelta >= MAX_VOLATILITY_TICKS
? 1e18
: absDelta * 1e18 / MAX_VOLATILITY_TICKS;
uint256 absDelta = twapDelta >= 0 ? uint256(twapDelta) : uint256(-twapDelta);
uint256 vol = absDelta >= MAX_VOLATILITY_TICKS ? 1e18 : absDelta * 1e18 / MAX_VOLATILITY_TICKS;
inputs[3] = _toDyadic(int256(vol));
}
@ -522,21 +502,27 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
}
// Slot 7: reserved (0)
}
function getLiquidityParams()
external
view
override
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
OptimizerInput[8] memory inputs = _buildInputs();
// Call calculateParams with a fixed gas budget. Evolved programs that grow
// too large hit the cap and fall back to bear defaults preventing any
// buggy or bloated optimizer from blocking recenter() with an OOG revert.
(bool ok, bytes memory ret) = address(this).staticcall{gas: CALCULATE_PARAMS_GAS_LIMIT}(
abi.encodeCall(this.calculateParams, (inputs))
);
(bool ok, bytes memory ret) = address(this).staticcall{ gas: CALCULATE_PARAMS_GAS_LIMIT }(abi.encodeCall(this.calculateParams, (inputs)));
if (!ok) return _bearDefaults();
// ABI encoding of (uint256, uint256, uint24, uint256) is exactly 128 bytes
// (each value padded to 32 bytes). A truncated return e.g. from a
// malformed evolved program would cause abi.decode to revert; guard here
// so all failure modes fall back via _bearDefaults().
if (ret.length < 128) return _bearDefaults();
(capitalInefficiency, anchorShare, anchorWidth, discoveryDepth) =
abi.decode(ret, (uint256, uint256, uint24, uint256));
(capitalInefficiency, anchorShare, anchorWidth, discoveryDepth) = abi.decode(ret, (uint256, uint256, uint24, uint256));
// Clamp fraction outputs to [0, 1e18] so a buggy evolved program cannot
// produce out-of-range values that confuse the LiquidityManager.
// anchorWidth is already bounded by uint24 at the ABI level.