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
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue