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
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
pragma solidity ^0.8.19;
|
pragma solidity ^0.8.19;
|
||||||
|
|
||||||
import {Kraiken} from "./Kraiken.sol";
|
import { IOptimizer, OptimizerInput } from "./IOptimizer.sol";
|
||||||
import {Stake} from "./Stake.sol";
|
import { Kraiken } from "./Kraiken.sol";
|
||||||
import {IOptimizer, OptimizerInput} from "./IOptimizer.sol";
|
import { Stake } from "./Stake.sol";
|
||||||
|
|
||||||
import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol";
|
import { TickMath } from "@aperture/uni-v3-lib/TickMath.sol";
|
||||||
import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol";
|
||||||
import {Math} from "@openzeppelin/utils/math/Math.sol";
|
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
||||||
import {TickMath} from "@aperture/uni-v3-lib/TickMath.sol";
|
import { Math } from "@openzeppelin/utils/math/Math.sol";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Dyadic rational interface — Push3's native number format.
|
// 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 ----
|
// ---- Extended data sources for input slots 2-6 ----
|
||||||
// These are optional; unset addresses leave the corresponding slots as 0.
|
// These are optional; unset addresses leave the corresponding slots as 0.
|
||||||
address public vwapTracker; // slots 2-4 source (VWAPTracker)
|
address public vwapTracker; // slots 2-4 source (VWAPTracker)
|
||||||
address public pool; // slots 2-4, 6 source (Uniswap V3 pool)
|
address public pool; // slots 2-4, 6 source (Uniswap V3 pool)
|
||||||
uint256 public lastRecenterTimestamp; // slot 5 source (updated via recordRecenter)
|
uint256 public lastRecenterTimestamp; // slot 5 source (updated via recordRecenter)
|
||||||
address public recenterRecorder; // authorized to call recordRecenter
|
address public recenterRecorder; // authorized to call recordRecenter
|
||||||
address public liquidityManager; // slot 6 source (LiquidityManager positions)
|
address public liquidityManager; // slot 6 source (LiquidityManager positions)
|
||||||
bool public token0isWeth; // true when WETH is token0 in the pool (flips tick direction)
|
bool public token0isWeth; // true when WETH is token0 in the pool (flips tick direction)
|
||||||
|
|
||||||
// ---- Normalization constants ----
|
// ---- 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).
|
/// @notice Maximum tick divergence (shortTwap vs longTwap) that maps to full volatility (1e18).
|
||||||
/// 1 000 ticks ≈ 10% price swing.
|
/// 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.
|
/// @notice Maximum tick trend signal (shortTwap - longTwap) for momentum saturation.
|
||||||
/// 1 000 ticks ≈ 10% price trend.
|
/// 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.
|
/// @notice Time (seconds) beyond which timeSinceRecenter saturates at 1e18. 86 400 = 1 day.
|
||||||
uint256 internal constant MAX_STALE_SECONDS = 86_400;
|
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;
|
uint32 internal constant SHORT_TWAP_WINDOW = 300;
|
||||||
|
|
||||||
/// @notice Long TWAP window for volatility / momentum baseline (30 minutes).
|
/// @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.
|
/// @dev Reverts if the caller is not the admin.
|
||||||
error UnauthorizedAccount(address account);
|
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) ----
|
// ---- 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
|
* @param _token0isWeth True when WETH is token0 in the pool. Needed to correctly
|
||||||
* orient tick-based indicators (pricePosition, volatility, momentum).
|
* orient tick-based indicators (pricePosition, volatility, momentum).
|
||||||
*/
|
*/
|
||||||
function setDataSources(address _vwapTracker, address _pool, address _liquidityManager, bool _token0isWeth)
|
function setDataSources(address _vwapTracker, address _pool, address _liquidityManager, bool _token0isWeth) external onlyAdmin {
|
||||||
external
|
|
||||||
onlyAdmin
|
|
||||||
{
|
|
||||||
vwapTracker = _vwapTracker;
|
vwapTracker = _vwapTracker;
|
||||||
pool = _pool;
|
pool = _pool;
|
||||||
liquidityManager = _liquidityManager;
|
liquidityManager = _liquidityManager;
|
||||||
|
|
@ -209,7 +206,7 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
|
||||||
* value = mantissa × 2^(-0) = mantissa.
|
* value = mantissa × 2^(-0) = mantissa.
|
||||||
*/
|
*/
|
||||||
function _toDyadic(int256 value) internal pure returns (OptimizerInput memory) {
|
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}).
|
* ({capitalInefficiency:0, anchorShare:3e17, anchorWidth:100, discoveryDepth:3e17}).
|
||||||
* Update both locations together if the safe defaults ever change.
|
* Update both locations together if the safe defaults ever change.
|
||||||
*/
|
*/
|
||||||
function _bearDefaults()
|
function _bearDefaults() internal pure returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
|
||||||
internal
|
|
||||||
pure
|
|
||||||
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
|
||||||
{
|
|
||||||
return (0, 3e17, 100, 3e17);
|
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.
|
* @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.
|
* @return sentimentValue A value in the range 0 to 1e18 where 1e18 represents the worst sentiment.
|
||||||
*/
|
*/
|
||||||
function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked)
|
function calculateSentiment(uint256 averageTaxRate, uint256 percentageStaked) public pure returns (uint256 sentimentValue) {
|
||||||
public
|
|
||||||
pure
|
|
||||||
returns (uint256 sentimentValue)
|
|
||||||
{
|
|
||||||
// Ensure percentageStaked doesn't exceed 100%
|
// Ensure percentageStaked doesn't exceed 100%
|
||||||
require(percentageStaked <= 1e18, "Invalid percentage staked");
|
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 anchorWidth Anchor position width in tick units (uint24)
|
||||||
* @return discoveryDepth Discovery liquidity density (0..1e18)
|
* @return discoveryDepth Discovery liquidity density (0..1e18)
|
||||||
*/
|
*/
|
||||||
function getLiquidityParams()
|
/**
|
||||||
external
|
* @notice Build the 8-slot normalized input array from on-chain data sources.
|
||||||
view
|
* @dev Extracted so test harnesses can observe the computed inputs without
|
||||||
override
|
* duplicating normalization logic. All slots are in [0, 1e18].
|
||||||
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
*/
|
||||||
{
|
function _buildInputs() internal view returns (OptimizerInput[8] memory inputs) {
|
||||||
OptimizerInput[8] memory inputs;
|
|
||||||
|
|
||||||
// Slot 0: percentageStaked
|
// Slot 0: percentageStaked
|
||||||
inputs[0] = _toDyadic(int256(stake.getPercentageStaked()));
|
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.
|
// Fails gracefully if the pool lacks sufficient observation history.
|
||||||
{
|
{
|
||||||
uint32[] memory secondsAgo = new uint32[](3);
|
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[1] = SHORT_TWAP_WINDOW; // 300 s — recent
|
||||||
secondsAgo[2] = 0; // now
|
secondsAgo[2] = 0; // now
|
||||||
try IUniswapV3PoolObserve(pool).observe(secondsAgo) returns (
|
try IUniswapV3PoolObserve(pool).observe(secondsAgo) returns (int56[] memory tickCumulatives, uint160[] memory) {
|
||||||
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)));
|
||||||
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)
|
// Adjust both TWAP ticks to KRK-price space (same sign convention)
|
||||||
int24 longAdj = token0isWeth ? longTwap : -longTwap;
|
int24 longAdj = token0isWeth ? longTwap : -longTwap;
|
||||||
|
|
@ -478,11 +461,8 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
|
||||||
|
|
||||||
// Slot 3: volatility = |shortTwap − longTwap| / MAX_VOLATILITY_TICKS
|
// Slot 3: volatility = |shortTwap − longTwap| / MAX_VOLATILITY_TICKS
|
||||||
{
|
{
|
||||||
uint256 absDelta =
|
uint256 absDelta = twapDelta >= 0 ? uint256(twapDelta) : uint256(-twapDelta);
|
||||||
twapDelta >= 0 ? uint256(twapDelta) : uint256(-twapDelta);
|
uint256 vol = absDelta >= MAX_VOLATILITY_TICKS ? 1e18 : absDelta * 1e18 / MAX_VOLATILITY_TICKS;
|
||||||
uint256 vol = absDelta >= MAX_VOLATILITY_TICKS
|
|
||||||
? 1e18
|
|
||||||
: absDelta * 1e18 / MAX_VOLATILITY_TICKS;
|
|
||||||
inputs[3] = _toDyadic(int256(vol));
|
inputs[3] = _toDyadic(int256(vol));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -522,21 +502,27 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slot 7: reserved (0)
|
// 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
|
// Call calculateParams with a fixed gas budget. Evolved programs that grow
|
||||||
// too large hit the cap and fall back to bear defaults — preventing any
|
// too large hit the cap and fall back to bear defaults — preventing any
|
||||||
// buggy or bloated optimizer from blocking recenter() with an OOG revert.
|
// buggy or bloated optimizer from blocking recenter() with an OOG revert.
|
||||||
(bool ok, bytes memory ret) = address(this).staticcall{gas: CALCULATE_PARAMS_GAS_LIMIT}(
|
(bool ok, bytes memory ret) = address(this).staticcall{ gas: CALCULATE_PARAMS_GAS_LIMIT }(abi.encodeCall(this.calculateParams, (inputs)));
|
||||||
abi.encodeCall(this.calculateParams, (inputs))
|
|
||||||
);
|
|
||||||
if (!ok) return _bearDefaults();
|
if (!ok) return _bearDefaults();
|
||||||
// ABI encoding of (uint256, uint256, uint24, uint256) is exactly 128 bytes
|
// ABI encoding of (uint256, uint256, uint24, uint256) is exactly 128 bytes
|
||||||
// (each value padded to 32 bytes). A truncated return — e.g. from a
|
// (each value padded to 32 bytes). A truncated return — e.g. from a
|
||||||
// malformed evolved program — would cause abi.decode to revert; guard here
|
// malformed evolved program — would cause abi.decode to revert; guard here
|
||||||
// so all failure modes fall back via _bearDefaults().
|
// so all failure modes fall back via _bearDefaults().
|
||||||
if (ret.length < 128) return _bearDefaults();
|
if (ret.length < 128) return _bearDefaults();
|
||||||
(capitalInefficiency, anchorShare, anchorWidth, discoveryDepth) =
|
(capitalInefficiency, anchorShare, anchorWidth, discoveryDepth) = abi.decode(ret, (uint256, uint256, uint24, uint256));
|
||||||
abi.decode(ret, (uint256, uint256, uint24, uint256));
|
|
||||||
// Clamp fraction outputs to [0, 1e18] so a buggy evolved program cannot
|
// Clamp fraction outputs to [0, 1e18] so a buggy evolved program cannot
|
||||||
// produce out-of-range values that confuse the LiquidityManager.
|
// produce out-of-range values that confuse the LiquidityManager.
|
||||||
// anchorWidth is already bounded by uint24 at the ABI level.
|
// anchorWidth is already bounded by uint24 at the ABI level.
|
||||||
|
|
|
||||||
|
|
@ -332,7 +332,7 @@ contract OptimizerTest is Test {
|
||||||
*/
|
*/
|
||||||
function testCalculateParamsRevertsOnNegativeMantissa0() public {
|
function testCalculateParamsRevertsOnNegativeMantissa0() public {
|
||||||
OptimizerInput[8] memory inputs;
|
OptimizerInput[8] memory inputs;
|
||||||
inputs[0] = OptimizerInput({mantissa: -1, shift: 0});
|
inputs[0] = OptimizerInput({ mantissa: -1, shift: 0 });
|
||||||
vm.expectRevert("negative mantissa");
|
vm.expectRevert("negative mantissa");
|
||||||
optimizer.calculateParams(inputs);
|
optimizer.calculateParams(inputs);
|
||||||
}
|
}
|
||||||
|
|
@ -342,7 +342,7 @@ contract OptimizerTest is Test {
|
||||||
*/
|
*/
|
||||||
function testCalculateParamsRevertsOnNegativeMantissa1() public {
|
function testCalculateParamsRevertsOnNegativeMantissa1() public {
|
||||||
OptimizerInput[8] memory inputs;
|
OptimizerInput[8] memory inputs;
|
||||||
inputs[1] = OptimizerInput({mantissa: -1, shift: 0});
|
inputs[1] = OptimizerInput({ mantissa: -1, shift: 0 });
|
||||||
vm.expectRevert("negative mantissa");
|
vm.expectRevert("negative mantissa");
|
||||||
optimizer.calculateParams(inputs);
|
optimizer.calculateParams(inputs);
|
||||||
}
|
}
|
||||||
|
|
@ -353,7 +353,7 @@ contract OptimizerTest is Test {
|
||||||
function testCalculateParamsRevertsOnNegativeMantissaSlots2to7() public {
|
function testCalculateParamsRevertsOnNegativeMantissaSlots2to7() public {
|
||||||
for (uint256 k = 2; k < 8; k++) {
|
for (uint256 k = 2; k < 8; k++) {
|
||||||
OptimizerInput[8] memory inputs;
|
OptimizerInput[8] memory inputs;
|
||||||
inputs[k] = OptimizerInput({mantissa: -1, shift: 0});
|
inputs[k] = OptimizerInput({ mantissa: -1, shift: 0 });
|
||||||
vm.expectRevert("negative mantissa");
|
vm.expectRevert("negative mantissa");
|
||||||
optimizer.calculateParams(inputs);
|
optimizer.calculateParams(inputs);
|
||||||
}
|
}
|
||||||
|
|
@ -389,22 +389,15 @@ contract OptimizerTest is Test {
|
||||||
* 2-6 without wiring a full protocol stack.
|
* 2-6 without wiring a full protocol stack.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/// @dev Harness: overrides calculateParams to write its inputs into public storage
|
/// @dev Harness: exposes the internal _buildInputs() so tests can assert the
|
||||||
/// so tests can assert the slot values directly.
|
/// normalized slot values that getLiquidityParams feeds into calculateParams.
|
||||||
contract OptimizerInputCapture is Optimizer {
|
contract OptimizerInputCapture is Optimizer {
|
||||||
int256[8] public capturedMantissa;
|
/// @notice Returns the mantissa of each normalized input slot.
|
||||||
|
function getComputedInputs() external view returns (int256[8] memory mantissas) {
|
||||||
function calculateParams(OptimizerInput[8] memory inputs)
|
OptimizerInput[8] memory inputs = _buildInputs();
|
||||||
public
|
for (uint256 i; i < 8; i++) {
|
||||||
pure
|
mantissas[i] = inputs[i].mantissa;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -416,7 +409,8 @@ contract OptimizerVwapHarness is Optimizer {
|
||||||
}
|
}
|
||||||
|
|
||||||
contract OptimizerNormalizedInputsTest is Test {
|
contract OptimizerNormalizedInputsTest is Test {
|
||||||
Optimizer optimizer;
|
OptimizerInputCapture capture;
|
||||||
|
Optimizer optimizer; // alias — points to the same proxy as `capture`
|
||||||
MockStake mockStake;
|
MockStake mockStake;
|
||||||
MockKraiken mockKraiken;
|
MockKraiken mockKraiken;
|
||||||
MockVWAPTracker mockVwap;
|
MockVWAPTracker mockVwap;
|
||||||
|
|
@ -433,10 +427,10 @@ contract OptimizerNormalizedInputsTest is Test {
|
||||||
mockPool = new MockPool();
|
mockPool = new MockPool();
|
||||||
mockLm = new MockLiquidityManagerPositions();
|
mockLm = new MockLiquidityManagerPositions();
|
||||||
|
|
||||||
Optimizer impl = new Optimizer();
|
OptimizerInputCapture impl = new OptimizerInputCapture();
|
||||||
bytes memory initData =
|
bytes memory initData = abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
|
||||||
abi.encodeWithSelector(Optimizer.initialize.selector, address(mockKraiken), address(mockStake));
|
|
||||||
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData);
|
ERC1967Proxy proxy = new ERC1967Proxy(address(impl), initData);
|
||||||
|
capture = OptimizerInputCapture(address(proxy));
|
||||||
optimizer = Optimizer(address(proxy));
|
optimizer = Optimizer(address(proxy));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -470,8 +464,8 @@ contract OptimizerNormalizedInputsTest is Test {
|
||||||
ticks[0] = 0;
|
ticks[0] = 0;
|
||||||
ticks[1] = 1000;
|
ticks[1] = 1000;
|
||||||
ticks[2] = -1000;
|
ticks[2] = -1000;
|
||||||
ticks[3] = 100000;
|
ticks[3] = 100_000;
|
||||||
ticks[4] = -100000;
|
ticks[4] = -100_000;
|
||||||
|
|
||||||
for (uint256 i = 0; i < ticks.length; i++) {
|
for (uint256 i = 0; i < ticks.length; i++) {
|
||||||
int24 origTick = ticks[i];
|
int24 origTick = ticks[i];
|
||||||
|
|
@ -479,10 +473,7 @@ contract OptimizerNormalizedInputsTest is Test {
|
||||||
uint256 priceX96 = Math.mulDiv(sqrtRatio, sqrtRatio, 1 << 96);
|
uint256 priceX96 = Math.mulDiv(sqrtRatio, sqrtRatio, 1 << 96);
|
||||||
int24 recovered = harness.exposed_vwapToTick(priceX96);
|
int24 recovered = harness.exposed_vwapToTick(priceX96);
|
||||||
// Allow ±1 tick error from integer sqrt truncation
|
// Allow ±1 tick error from integer sqrt truncation
|
||||||
assertTrue(
|
assertTrue(recovered == origTick || recovered == origTick - 1 || recovered == origTick + 1, "round-trip tick error > 1");
|
||||||
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");
|
assertEq(elapsed, 43_200, "elapsed should be exactly half of MAX_STALE_SECONDS");
|
||||||
// 43200 * 1e18 / 86400 = 0.5e18
|
// 43200 * 1e18 / 86400 = 0.5e18
|
||||||
assertEq(elapsed * 1e18 / 86_400, 5e17, "half-stale should normalize to 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 {
|
function testTimeSinceRecenterSaturatesAt1e18() public {
|
||||||
|
|
@ -535,11 +530,14 @@ contract OptimizerNormalizedInputsTest is Test {
|
||||||
int24 targetTick = 500;
|
int24 targetTick = 500;
|
||||||
_seedVwapAtTick(targetTick);
|
_seedVwapAtTick(targetTick);
|
||||||
mockPool.setCurrentTick(targetTick); // current == vwap → 0.5e18
|
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();
|
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 {
|
function testPricePositionBelowLowerBound() public {
|
||||||
|
|
@ -601,9 +599,16 @@ contract OptimizerNormalizedInputsTest is Test {
|
||||||
_seedVwapAtTick(0);
|
_seedVwapAtTick(0);
|
||||||
mockPool.setCurrentTick(0);
|
mockPool.setCurrentTick(0);
|
||||||
// shortTwap - longTwap = 1000 ticks = MAX_MOMENTUM_TICKS → momentum = 1e18
|
// 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
|
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 {
|
function testMomentumFullBearAtNegMaxDelta() public {
|
||||||
|
|
@ -611,7 +616,7 @@ contract OptimizerNormalizedInputsTest is Test {
|
||||||
_seedVwapAtTick(0);
|
_seedVwapAtTick(0);
|
||||||
mockPool.setCurrentTick(0);
|
mockPool.setCurrentTick(0);
|
||||||
// shortTwap - longTwap = -1000 = -MAX_MOMENTUM_TICKS → momentum = 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
|
optimizer.getLiquidityParams(); // must not revert
|
||||||
}
|
}
|
||||||
|
|
@ -638,6 +643,10 @@ contract OptimizerNormalizedInputsTest is Test {
|
||||||
mockPool.setRevertOnObserve(true);
|
mockPool.setRevertOnObserve(true);
|
||||||
|
|
||||||
optimizer.getLiquidityParams(); // must not revert
|
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 {
|
function testUtilizationRateOutOfRange() public {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue