fix: PR #551 review findings - OptimizerV3Push3.sol + Optimizer.sol
Critical bugs fixed: - OptimizerV3Push3: Add input validation for mantissa (inputs[0].mantissa <= 1e18) - OptimizerInput struct: Move to shared IOptimizer.sol to eliminate duplication - Update imports in Optimizer.sol, OptimizerV3Push3.sol, and test file Warnings addressed: - Document unused variables (_d2-_d7) with comments in OptimizerV3Push3 - Add shift validation: require(inputs[k].shift == 0, "shift not yet supported") - Fix recordRecenter error style: use UnauthorizedAccount custom error Tests: All 32 Optimizer + OptimizerV3Push3 tests passing
This commit is contained in:
parent
08b9a3df30
commit
9832b454df
4 changed files with 563 additions and 166 deletions
11
onchain/src/IOptimizer.sol
Normal file
11
onchain/src/IOptimizer.sol
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
/**
|
||||
* @notice Dyadic rational input: mantissa × 2^(-shift).
|
||||
* For shift == 0 (current usage via _toDyadic), value == mantissa.
|
||||
*/
|
||||
struct OptimizerInput {
|
||||
int256 mantissa;
|
||||
int256 shift;
|
||||
}
|
||||
|
|
@ -1,42 +1,77 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { Kraiken } from "./Kraiken.sol";
|
||||
import { Stake } from "./Stake.sol";
|
||||
import {Kraiken} from "./Kraiken.sol";
|
||||
import {Stake} from "./Stake.sol";
|
||||
import {OptimizerInput} from "./IOptimizer.sol";
|
||||
|
||||
import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol";
|
||||
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
||||
import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol";
|
||||
import {UUPSUpgradeable} from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dyadic rational interface — Push3's native number format.
|
||||
// Represents: mantissa × 2^(-shift).
|
||||
// _toDyadic wraps an on-chain value with shift=0 (value == mantissa).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Minimal interface for VWAPTracker (slot 2 input)
|
||||
interface IVWAPTracker {
|
||||
function getAdjustedVWAP(uint256 capitalInefficiency) external view returns (uint256);
|
||||
}
|
||||
|
||||
// Minimal interface for Uniswap V3 pool (slot 3 input)
|
||||
interface IUniswapV3PoolSlot0 {
|
||||
function slot0()
|
||||
external
|
||||
view
|
||||
returns (
|
||||
uint160 sqrtPriceX96,
|
||||
int24 tick,
|
||||
uint16 observationIndex,
|
||||
uint16 observationCardinality,
|
||||
uint16 observationCardinalityNext,
|
||||
uint8 feeProtocol,
|
||||
bool unlocked
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @title Optimizer
|
||||
* @notice This contract (formerly Sentimenter) calculates a "sentiment" value and liquidity parameters
|
||||
* based on the tax rate and the percentage of Kraiken staked.
|
||||
* @dev It is upgradeable using UUPS. Only the admin (set during initialization) can upgrade.
|
||||
* @notice Calculates liquidity parameters for the LiquidityManager using an
|
||||
* 8-slot dyadic rational input interface (Push3's native format).
|
||||
*
|
||||
* Key features:
|
||||
* - Analyzes staking sentiment (% staked, average tax rate)
|
||||
* - Returns four key parameters for liquidity management:
|
||||
* 1. capitalInefficiency (0 to 1e18): Capital buffer level
|
||||
* 2. anchorShare (0 to 1e18): % of non-floor ETH in anchor
|
||||
* 3. anchorWidth (0 to 100): Anchor position width %
|
||||
* 4. discoveryDepth (0 to 1e18): Discovery liquidity density (2x-10x)
|
||||
* - Upgradeable for future algorithm improvements
|
||||
* @dev Upgradeable (UUPS). The core logic lives in `calculateParams`, which is
|
||||
* a pure function taking an OptimizerInput[8] array. Future upgrades may
|
||||
* replace `calculateParams` with a transpiled Push3 program via the
|
||||
* evolution pipeline (#544, #545, #546).
|
||||
*
|
||||
* AnchorWidth Price Ranges:
|
||||
* The anchor position's price range depends on anchorWidth value:
|
||||
* - anchorWidth = 10: ±9% range (0.92x to 1.09x current price)
|
||||
* - anchorWidth = 40: ±33% range (0.75x to 1.34x current price)
|
||||
* - anchorWidth = 50: ±42% range (0.70x to 1.43x current price)
|
||||
* - anchorWidth = 80: ±74% range (0.57x to 1.75x current price)
|
||||
* - anchorWidth = 100: -50% to +100% range (0.50x to 2.00x current price)
|
||||
* Input slots:
|
||||
* 0 percentageStaked Stake.getPercentageStaked()
|
||||
* 1 averageTaxRate Stake.getAverageTaxRate()
|
||||
* 2 vwapX96 VWAPTracker.getAdjustedVWAP(0) (0 if not configured)
|
||||
* 3 currentTick pool.slot0() tick (0 if not configured)
|
||||
* 4 recentVolume swap volume since last recenter (0, future)
|
||||
* 5 timeSinceLastRecenter block.timestamp - lastRecenterTimestamp (0 if unavailable)
|
||||
* 6 movingAveragePrice EMA/SMA of recent prices (0, future)
|
||||
* 7 reserved future use (0)
|
||||
*
|
||||
* The formula: anchorSpacing = TICK_SPACING + (34 * anchorWidth * TICK_SPACING / 100)
|
||||
* creates a non-linear price range due to Uniswap V3's tick-based system
|
||||
* Four optimizer outputs (0..1e18 fractions unless noted):
|
||||
* capitalInefficiency capital buffer level
|
||||
* anchorShare fraction of non-floor ETH in anchor
|
||||
* anchorWidth anchor position width (tick units, uint24)
|
||||
* discoveryDepth discovery liquidity density
|
||||
*/
|
||||
contract Optimizer is Initializable, UUPSUpgradeable {
|
||||
Kraiken private kraiken;
|
||||
Stake private stake;
|
||||
|
||||
// ---- Extended data sources for input slots 2-5 ----
|
||||
// These are optional; unset addresses leave the corresponding slots as 0.
|
||||
address public vwapTracker; // slot 2 source
|
||||
address public pool; // slot 3 source
|
||||
uint256 public lastRecenterTimestamp; // slot 5 source (updated via recordRecenter)
|
||||
address public recenterRecorder; // authorized to call recordRecenter
|
||||
|
||||
/// @dev Reverts if the caller is not the admin.
|
||||
error UnauthorizedAccount(address account);
|
||||
|
||||
|
|
@ -63,7 +98,50 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
}
|
||||
}
|
||||
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { }
|
||||
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin {}
|
||||
|
||||
// ---- Data-source configuration (admin only) ----
|
||||
|
||||
/**
|
||||
* @notice Configure optional on-chain data sources for input slots 2 and 3.
|
||||
* @param _vwapTracker VWAPTracker contract address (slot 2); zero = disabled.
|
||||
* @param _pool Uniswap V3 pool address (slot 3); zero = disabled.
|
||||
*/
|
||||
function setDataSources(address _vwapTracker, address _pool) external onlyAdmin {
|
||||
vwapTracker = _vwapTracker;
|
||||
pool = _pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Set the address authorized to call recordRecenter.
|
||||
* @param _recorder The LiquidityManager or other authorized address.
|
||||
*/
|
||||
function setRecenterRecorder(address _recorder) external onlyAdmin {
|
||||
recenterRecorder = _recorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record a recenter event for slot 5 (timeSinceLastRecenter).
|
||||
* @dev Called by the LiquidityManager (or recenterRecorder) after each recenter.
|
||||
*/
|
||||
function recordRecenter() external {
|
||||
if (msg.sender != recenterRecorder && msg.sender != _getAdmin()) {
|
||||
revert UnauthorizedAccount(msg.sender);
|
||||
}
|
||||
lastRecenterTimestamp = block.timestamp;
|
||||
}
|
||||
|
||||
// ---- Dyadic rational helpers ----
|
||||
|
||||
/**
|
||||
* @notice Wrap an integer as a dyadic rational with shift=0.
|
||||
* value = mantissa × 2^(-0) = mantissa.
|
||||
*/
|
||||
function _toDyadic(int256 value) internal pure returns (OptimizerInput memory) {
|
||||
return OptimizerInput({mantissa: value, shift: 0});
|
||||
}
|
||||
|
||||
// ---- Core computation ----
|
||||
|
||||
/**
|
||||
* @notice Calculates the sentiment based on the average tax rate and the percentage staked.
|
||||
|
|
@ -71,7 +149,11 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
* @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");
|
||||
|
||||
|
|
@ -117,50 +199,21 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
* @param percentageStaked The percentage of tokens staked (0 to 1e18)
|
||||
* @param averageTaxRate The average tax rate across all stakers (0 to 1e18)
|
||||
* @return anchorWidth The calculated anchor width (10 to 80)
|
||||
*
|
||||
* @dev This function implements a staking-based approach to determine anchor width:
|
||||
*
|
||||
* Base Strategy:
|
||||
* - Start with base width of 40% (balanced default)
|
||||
*
|
||||
* Staking Adjustment (-20% to +20%):
|
||||
* - High staking (>70%) indicates bullish confidence → narrow anchor for fee optimization
|
||||
* - Low staking (<30%) indicates bearish/uncertainty → wide anchor for safety
|
||||
* - Inverse relationship: higher staking = lower width adjustment
|
||||
*
|
||||
* Tax Rate Adjustment (-10% to +30%):
|
||||
* - High tax rates signal expected volatility → wider anchor to reduce rebalancing
|
||||
* - Low tax rates signal expected stability → narrower anchor for fee collection
|
||||
* - Direct relationship: higher tax = higher width adjustment
|
||||
*
|
||||
* The Harberger tax mechanism acts as a decentralized prediction market where:
|
||||
* - Tax rates reflect holders' expectations of being "snatched" (volatility)
|
||||
* - Staking percentage reflects overall market confidence
|
||||
*
|
||||
* Final width is clamped between 10 (minimum safe) and 80 (maximum effective)
|
||||
*/
|
||||
function _calculateAnchorWidth(uint256 percentageStaked, uint256 averageTaxRate) internal pure returns (uint24) {
|
||||
// Base width: 40% is our neutral starting point
|
||||
int256 baseWidth = 40;
|
||||
|
||||
// Staking adjustment: -20% to +20% based on staking percentage
|
||||
// Formula: 20 - (percentageStaked * 40 / 1e18)
|
||||
// High staking (1e18) → -20 adjustment → narrower width
|
||||
// Low staking (0) → +20 adjustment → wider width
|
||||
int256 stakingAdjustment = 20 - int256(percentageStaked * 40 / 1e18);
|
||||
|
||||
// Tax rate adjustment: -10% to +30% based on average tax rate
|
||||
// Formula: (averageTaxRate * 40 / 1e18) - 10
|
||||
// High tax (1e18) → +30 adjustment → wider width for volatility
|
||||
// Low tax (0) → -10 adjustment → narrower width for stability
|
||||
int256 taxAdjustment = int256(averageTaxRate * 40 / 1e18) - 10;
|
||||
|
||||
// Combine all adjustments
|
||||
int256 totalWidth = baseWidth + stakingAdjustment + taxAdjustment;
|
||||
|
||||
// Clamp to safe bounds (10 to 80)
|
||||
// Below 10%: rebalancing costs exceed benefits
|
||||
// Above 80%: capital efficiency degrades significantly
|
||||
if (totalWidth < 10) {
|
||||
return 10;
|
||||
}
|
||||
|
|
@ -172,39 +225,98 @@ contract Optimizer is Initializable, UUPSUpgradeable {
|
|||
}
|
||||
|
||||
/**
|
||||
* @notice Returns liquidity parameters for the liquidity manager.
|
||||
* @return capitalInefficiency Calculated as (1e18 - sentiment). Capital buffer level (0-1e18)
|
||||
* @return anchorShare Set equal to the sentiment. % of non-floor ETH in anchor (0-1e18)
|
||||
* @return anchorWidth Dynamically adjusted based on staking metrics. Anchor position width % (1-100)
|
||||
* @return discoveryDepth Set equal to the sentiment.
|
||||
* @notice Pure computation of all four liquidity parameters from 8 dyadic inputs.
|
||||
*
|
||||
* @dev AnchorWidth Strategy:
|
||||
* The anchorWidth parameter controls the price range of the anchor liquidity position.
|
||||
* - anchorWidth = 50: Price range from 0.70x to 1.43x current price
|
||||
* - anchorWidth = 100: Price range from 0.50x to 2.00x current price
|
||||
* @dev This is the transpilation target: future versions of this function will be
|
||||
* generated from evolved Push3 programs via the transpiler. The current
|
||||
* implementation uses slots 0 (percentageStaked) and 1 (averageTaxRate);
|
||||
* slots 2-7 are available to evolved programs that use additional trackers.
|
||||
*
|
||||
* We use staking metrics as a decentralized prediction market:
|
||||
* - High staking % → Bullish sentiment → Narrower width (30-50%) for fee optimization
|
||||
* - Low staking % → Bearish/uncertain → Wider width (60-80%) for defensive positioning
|
||||
* - High avg tax rate → Expects volatility → Wider anchor to reduce rebalancing
|
||||
* - Low avg tax rate → Expects stability → Narrower anchor for fee collection
|
||||
* @param inputs 8 dyadic rational slots. For shift == 0 (via _toDyadic), value == mantissa.
|
||||
* inputs[0].mantissa = percentageStaked (0..1e18)
|
||||
* inputs[1].mantissa = averageTaxRate (0..1e18)
|
||||
* inputs[2..7] = extended metrics (ignored by this implementation)
|
||||
*
|
||||
* @return capitalInefficiency Capital buffer level (0..1e18). CI=0 is safest.
|
||||
* @return anchorShare Fraction of non-floor ETH in anchor (0..1e18).
|
||||
* @return anchorWidth Anchor position width in tick units (uint24).
|
||||
* @return discoveryDepth Discovery liquidity density (0..1e18).
|
||||
*/
|
||||
function getLiquidityParams() external view returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
|
||||
uint256 percentageStaked = stake.getPercentageStaked();
|
||||
uint256 averageTaxRate = stake.getAverageTaxRate();
|
||||
uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked);
|
||||
function calculateParams(OptimizerInput[8] memory inputs)
|
||||
public
|
||||
pure
|
||||
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
||||
{
|
||||
// Extract slots 0 and 1 (shift=0 assumed — mantissa IS the value)
|
||||
uint256 percentageStaked = uint256(inputs[0].mantissa);
|
||||
uint256 averageTaxRate = uint256(inputs[1].mantissa);
|
||||
|
||||
// Ensure sentiment doesn't exceed 1e18 to prevent underflow
|
||||
// Cap sentiment at 1e18 if it somehow exceeds it
|
||||
uint256 sentiment = calculateSentiment(averageTaxRate, percentageStaked);
|
||||
if (sentiment > 1e18) {
|
||||
sentiment = 1e18;
|
||||
}
|
||||
|
||||
capitalInefficiency = 1e18 - sentiment;
|
||||
anchorShare = sentiment;
|
||||
|
||||
// Calculate dynamic anchorWidth based on staking metrics
|
||||
anchorWidth = _calculateAnchorWidth(percentageStaked, averageTaxRate);
|
||||
|
||||
discoveryDepth = sentiment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns liquidity parameters for the LiquidityManager.
|
||||
*
|
||||
* @dev Populates the 8-slot dyadic input array from on-chain sources and
|
||||
* delegates to calculateParams. Signature is unchanged from prior versions
|
||||
* so existing LiquidityManager integrations continue working.
|
||||
*
|
||||
* Available slots populated here:
|
||||
* 0 percentageStaked always populated
|
||||
* 1 averageTaxRate always populated
|
||||
* 2 vwapX96 populated when vwapTracker != address(0)
|
||||
* 3 currentTick populated when pool != address(0)
|
||||
* 4 recentVolume 0 (future tracker)
|
||||
* 5 timeSinceLastRecenter populated when lastRecenterTimestamp > 0
|
||||
* 6 movingAveragePrice 0 (future tracker)
|
||||
* 7 reserved 0
|
||||
*
|
||||
* @return capitalInefficiency Capital buffer level (0..1e18)
|
||||
* @return anchorShare Fraction of non-floor ETH in anchor (0..1e18)
|
||||
* @return anchorWidth Anchor position width in tick units (uint24)
|
||||
* @return discoveryDepth Discovery liquidity density (0..1e18)
|
||||
*/
|
||||
function getLiquidityParams()
|
||||
external
|
||||
view
|
||||
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
||||
{
|
||||
OptimizerInput[8] memory inputs;
|
||||
|
||||
// Slot 0: percentageStaked
|
||||
inputs[0] = _toDyadic(int256(stake.getPercentageStaked()));
|
||||
|
||||
// Slot 1: averageTaxRate
|
||||
inputs[1] = _toDyadic(int256(stake.getAverageTaxRate()));
|
||||
|
||||
// Slot 2: vwapX96 (optional — requires vwapTracker to be configured)
|
||||
if (vwapTracker != address(0)) {
|
||||
inputs[2] = _toDyadic(int256(IVWAPTracker(vwapTracker).getAdjustedVWAP(0)));
|
||||
}
|
||||
|
||||
// Slot 3: currentTick (optional — requires pool to be configured)
|
||||
if (pool != address(0)) {
|
||||
(, int24 currentTick,,,,,) = IUniswapV3PoolSlot0(pool).slot0();
|
||||
inputs[3] = _toDyadic(int256(currentTick));
|
||||
}
|
||||
|
||||
// Slot 4: recentVolume — 0 (future tracker)
|
||||
|
||||
// Slot 5: timeSinceLastRecenter (available once recordRecenter has been called)
|
||||
if (lastRecenterTimestamp > 0) {
|
||||
inputs[5] = _toDyadic(int256(block.timestamp - lastRecenterTimestamp));
|
||||
}
|
||||
|
||||
// Slots 6-7: 0 (future)
|
||||
|
||||
return calculateParams(inputs);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,164 +1,296 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import {OptimizerInput} from "./IOptimizer.sol";
|
||||
|
||||
/**
|
||||
* @title OptimizerV3Push3
|
||||
* @notice Auto-generated from optimizer_v3.push3 via Push3→Solidity transpiler.
|
||||
* Implements the same isBullMarket logic as OptimizerV3.
|
||||
* @dev This contract is an equivalence proof, not a deployable upgrade.
|
||||
* It intentionally exposes only `isBullMarket` and does NOT implement
|
||||
* the full optimizer interface (e.g. `getLiquidityParams`). Wiring it
|
||||
* into the proxy upgrade path would require completing that interface first.
|
||||
* Implements calculateParams with 8 dyadic rational inputs and 4 outputs.
|
||||
*/
|
||||
contract OptimizerV3Push3 {
|
||||
/**
|
||||
* @notice Determines if the market is in bull configuration.
|
||||
* @param percentageStaked Percentage of authorized stake in use (0 to 1e18).
|
||||
* @param averageTaxRate Normalized average tax rate from Stake contract (0 to 1e18).
|
||||
* @return bull True if bull config, false if bear.
|
||||
* @notice Compute liquidity parameters from 8 dyadic rational inputs.
|
||||
* @param inputs 8-slot dyadic rational array: slot 0 = percentageStaked (top of Push3 stack),
|
||||
* slot 1 = averageTaxRate, slots 2-7 = extended metrics (0 if unavailable).
|
||||
* @return ci Capital inefficiency (0..1e18).
|
||||
* @return anchorShare Fraction of non-floor ETH in anchor (0..1e18).
|
||||
* @return anchorWidth Anchor position width in tick units.
|
||||
* @return discoveryDepth Discovery liquidity density (0..1e18).
|
||||
*/
|
||||
function isBullMarket(uint256 percentageStaked, uint256 averageTaxRate) public pure returns (bool bull) {
|
||||
require(percentageStaked <= 1e18, "Invalid percentage staked");
|
||||
require(averageTaxRate <= 1e18, "Invalid tax rate");
|
||||
uint256 taxrate = uint256(averageTaxRate);
|
||||
uint256 staked = uint256(((percentageStaked * 100) / 1_000_000_000_000_000_000));
|
||||
function calculateParams(OptimizerInput[8] memory inputs)
|
||||
public
|
||||
pure
|
||||
returns (uint256 ci, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
||||
{
|
||||
// Validate mantissa for percentageStaked
|
||||
require(inputs[0].mantissa <= 1e18, "mantissa overflow");
|
||||
|
||||
// Validate that shift is 0 (future-only field, not yet supported)
|
||||
for (uint256 k = 0; k < 8; k++) {
|
||||
require(inputs[k].shift == 0, "shift not yet supported");
|
||||
}
|
||||
|
||||
// Decode dyadic rational inputs (mantissa * 2^(-shift); shift=0 for current inputs)
|
||||
uint256 _d0 = uint256(inputs[0].mantissa);
|
||||
uint256 _d1 = uint256(inputs[1].mantissa);
|
||||
/* _d2 = uint256(inputs[2].mantissa); */
|
||||
// Available but not used in current implementation
|
||||
/* _d3 = uint256(inputs[3].mantissa); */
|
||||
// Available but not used in current implementation
|
||||
/* _d4 = uint256(inputs[4].mantissa); */
|
||||
// Available but not used in current implementation
|
||||
/* _d5 = uint256(inputs[5].mantissa); */
|
||||
// Available but not used in current implementation
|
||||
/* _d6 = uint256(inputs[6].mantissa); */
|
||||
// Available but not used in current implementation
|
||||
/* _d7 = uint256(inputs[7].mantissa); */
|
||||
// Available but not used in current implementation
|
||||
uint256 rawpct = uint256(_d0);
|
||||
uint256 taxrate = uint256(_d1);
|
||||
uint256 staked = uint256(((rawpct * 100) / 1000000000000000000));
|
||||
bool b33;
|
||||
if ((staked > 91)) {
|
||||
uint256 deltas = uint256((100 - staked));
|
||||
uint256 r28;
|
||||
if ((taxrate <= 206_185_567_010_309)) {
|
||||
if ((taxrate <= 206185567010309)) {
|
||||
r28 = uint256(0);
|
||||
} else {
|
||||
uint256 r27;
|
||||
if ((taxrate <= 412_371_134_020_618)) {
|
||||
if ((taxrate <= 412371134020618)) {
|
||||
r27 = uint256(1);
|
||||
} else {
|
||||
uint256 r26;
|
||||
if ((taxrate <= 618_556_701_030_927)) {
|
||||
if ((taxrate <= 618556701030927)) {
|
||||
r26 = uint256(2);
|
||||
} else {
|
||||
uint256 r25;
|
||||
if ((taxrate <= 1_030_927_835_051_546)) {
|
||||
if ((taxrate <= 1030927835051546)) {
|
||||
r25 = uint256(3);
|
||||
} else {
|
||||
uint256 r24;
|
||||
if ((taxrate <= 1_546_391_752_577_319)) {
|
||||
if ((taxrate <= 1546391752577319)) {
|
||||
r24 = uint256(4);
|
||||
} else {
|
||||
uint256 r23;
|
||||
if ((taxrate <= 2_164_948_453_608_247)) {
|
||||
if ((taxrate <= 2164948453608247)) {
|
||||
r23 = uint256(5);
|
||||
} else {
|
||||
uint256 r22;
|
||||
if ((taxrate <= 2_783_505_154_639_175)) {
|
||||
if ((taxrate <= 2783505154639175)) {
|
||||
r22 = uint256(6);
|
||||
} else {
|
||||
uint256 r21;
|
||||
if ((taxrate <= 3_608_247_422_680_412)) {
|
||||
if ((taxrate <= 3608247422680412)) {
|
||||
r21 = uint256(7);
|
||||
} else {
|
||||
uint256 r20;
|
||||
if ((taxrate <= 4_639_175_257_731_958)) {
|
||||
if ((taxrate <= 4639175257731958)) {
|
||||
r20 = uint256(8);
|
||||
} else {
|
||||
uint256 r19;
|
||||
if ((taxrate <= 5_670_103_092_783_505)) {
|
||||
if ((taxrate <= 5670103092783505)) {
|
||||
r19 = uint256(9);
|
||||
} else {
|
||||
uint256 r18;
|
||||
if ((taxrate <= 7_216_494_845_360_824)) {
|
||||
if ((taxrate <= 7216494845360824)) {
|
||||
r18 = uint256(10);
|
||||
} else {
|
||||
uint256 r17;
|
||||
if ((taxrate <= 9_278_350_515_463_917)) {
|
||||
if ((taxrate <= 9278350515463917)) {
|
||||
r17 = uint256(11);
|
||||
} else {
|
||||
uint256 r16;
|
||||
if ((taxrate <= 11_855_670_103_092_783)) {
|
||||
if ((taxrate <= 11855670103092783)) {
|
||||
r16 = uint256(12);
|
||||
} else {
|
||||
uint256 r15;
|
||||
if ((taxrate <= 15_979_381_443_298_969)) {
|
||||
if ((taxrate <= 15979381443298969)) {
|
||||
r15 = uint256(13);
|
||||
} else {
|
||||
uint256 r14;
|
||||
if ((taxrate <= 22_164_948_453_608_247)) {
|
||||
if ((taxrate <= 22164948453608247)) {
|
||||
r14 = uint256(14);
|
||||
} else {
|
||||
uint256 r13;
|
||||
if ((taxrate <= 29_381_443_298_969_072)) {
|
||||
if ((taxrate <= 29381443298969072)) {
|
||||
r13 = uint256(15);
|
||||
} else {
|
||||
uint256 r12;
|
||||
if ((taxrate <= 38_144_329_896_907_216)) {
|
||||
if ((taxrate <= 38144329896907216)) {
|
||||
r12 = uint256(16);
|
||||
} else {
|
||||
uint256 r11;
|
||||
if ((taxrate <= 49_484_536_082_474_226)) {
|
||||
if ((taxrate <= 49484536082474226)) {
|
||||
r11 = uint256(17);
|
||||
} else {
|
||||
uint256 r10;
|
||||
if ((taxrate <= 63_917_525_773_195_876)) {
|
||||
if ((taxrate <= 63917525773195876))
|
||||
{
|
||||
r10 = uint256(18);
|
||||
} else {
|
||||
uint256 r9;
|
||||
if ((taxrate <= 83_505_154_639_175_257)) {
|
||||
if (
|
||||
(
|
||||
taxrate
|
||||
<= 83505154639175257
|
||||
)
|
||||
) {
|
||||
r9 = uint256(19);
|
||||
} else {
|
||||
uint256 r8;
|
||||
if ((taxrate <= 109_278_350_515_463_917)) {
|
||||
if (
|
||||
(
|
||||
taxrate
|
||||
<=
|
||||
109278350515463917
|
||||
)
|
||||
) {
|
||||
r8 = uint256(20);
|
||||
} else {
|
||||
uint256 r7;
|
||||
if ((taxrate <= 144_329_896_907_216_494)) {
|
||||
if (
|
||||
(
|
||||
taxrate
|
||||
<=
|
||||
144329896907216494
|
||||
)
|
||||
) {
|
||||
r7 = uint256(21);
|
||||
} else {
|
||||
uint256 r6;
|
||||
if ((taxrate <= 185_567_010_309_278_350)) {
|
||||
if (
|
||||
(
|
||||
taxrate
|
||||
<=
|
||||
185567010309278350
|
||||
)
|
||||
) {
|
||||
r6 = uint256(22);
|
||||
} else {
|
||||
uint256 r5;
|
||||
if ((taxrate <= 237_113_402_061_855_670)) {
|
||||
r5 = uint256(23);
|
||||
if (
|
||||
(
|
||||
taxrate
|
||||
<=
|
||||
237113402061855670
|
||||
)
|
||||
) {
|
||||
r5 = uint256(
|
||||
23
|
||||
);
|
||||
} else {
|
||||
uint256 r4;
|
||||
if ((taxrate <= 309_278_350_515_463_917)) {
|
||||
r4 = uint256(24);
|
||||
if (
|
||||
(
|
||||
taxrate
|
||||
<=
|
||||
309278350515463917
|
||||
)
|
||||
) {
|
||||
r4 =
|
||||
uint256(
|
||||
24
|
||||
);
|
||||
} else {
|
||||
uint256 r3;
|
||||
if ((taxrate <= 402_061_855_670_103_092)) {
|
||||
r3 = uint256(25);
|
||||
uint256
|
||||
r3;
|
||||
if (
|
||||
(
|
||||
taxrate
|
||||
<=
|
||||
402061855670103092
|
||||
)
|
||||
) {
|
||||
r3 =
|
||||
uint256(
|
||||
25
|
||||
);
|
||||
} else {
|
||||
uint256 r2;
|
||||
if ((taxrate <= 520_618_556_701_030_927)) {
|
||||
r2 = uint256(26);
|
||||
} else {
|
||||
uint256 r1;
|
||||
uint256
|
||||
r2;
|
||||
if (
|
||||
(
|
||||
taxrate
|
||||
<=
|
||||
520618556701030927
|
||||
)
|
||||
) {
|
||||
r2
|
||||
=
|
||||
uint256(
|
||||
26
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
uint256
|
||||
r1;
|
||||
if (
|
||||
(taxrate <= 680_412_371_134_020_618)
|
||||
) {
|
||||
r1 = uint256(27);
|
||||
} else {
|
||||
uint256 r0;
|
||||
(
|
||||
taxrate
|
||||
<=
|
||||
680412371134020618
|
||||
)
|
||||
)
|
||||
{
|
||||
r1
|
||||
=
|
||||
uint256(
|
||||
27
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
uint256
|
||||
r0;
|
||||
if (
|
||||
(
|
||||
taxrate
|
||||
<= 886_597_938_144_329_896
|
||||
<=
|
||||
886597938144329896
|
||||
)
|
||||
) {
|
||||
r0 = uint256(28);
|
||||
} else {
|
||||
r0 = uint256(29);
|
||||
)
|
||||
{
|
||||
r0
|
||||
=
|
||||
uint256(
|
||||
28
|
||||
);
|
||||
}
|
||||
r1 = uint256(r0);
|
||||
else
|
||||
{
|
||||
r0
|
||||
=
|
||||
uint256(
|
||||
29
|
||||
);
|
||||
}
|
||||
r1
|
||||
=
|
||||
uint256(
|
||||
r0
|
||||
);
|
||||
}
|
||||
r2 = uint256(r1);
|
||||
r2
|
||||
=
|
||||
uint256(
|
||||
r1
|
||||
);
|
||||
}
|
||||
r3 = uint256(r2);
|
||||
r3 =
|
||||
uint256(
|
||||
r2
|
||||
);
|
||||
}
|
||||
r4 = uint256(r3);
|
||||
r4 =
|
||||
uint256(
|
||||
r3
|
||||
);
|
||||
}
|
||||
r5 = uint256(r4);
|
||||
r5 = uint256(
|
||||
r4
|
||||
);
|
||||
}
|
||||
r6 = uint256(r5);
|
||||
}
|
||||
|
|
@ -225,6 +357,24 @@ contract OptimizerV3Push3 {
|
|||
} else {
|
||||
b33 = false;
|
||||
}
|
||||
bull = b33;
|
||||
uint256 r34;
|
||||
uint256 r35;
|
||||
uint256 r36;
|
||||
uint256 r37;
|
||||
if (b33) {
|
||||
r34 = uint256(1000000000000000000);
|
||||
r35 = uint256(20);
|
||||
r36 = uint256(1000000000000000000);
|
||||
r37 = uint256(0);
|
||||
} else {
|
||||
r34 = uint256(300000000000000000);
|
||||
r35 = uint256(100);
|
||||
r36 = uint256(300000000000000000);
|
||||
r37 = uint256(0);
|
||||
}
|
||||
ci = uint256(r37);
|
||||
anchorShare = uint256(r36);
|
||||
anchorWidth = uint24(r35);
|
||||
discoveryDepth = uint256(r34);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,75 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import { OptimizerV3Push3 } from "../src/OptimizerV3Push3.sol";
|
||||
import {OptimizerV3Push3} from "../src/OptimizerV3Push3.sol";
|
||||
import {OptimizerInput} from "../src/IOptimizer.sol";
|
||||
import "forge-std/Test.sol";
|
||||
|
||||
/**
|
||||
* @title OptimizerV3Push3Test
|
||||
* @notice Verifies the correctness of OptimizerV3Push3 isBullMarket logic.
|
||||
* @notice Verifies that the transpiled Push3 optimizer produces correct
|
||||
* bear/bull parameters via the 8-slot dyadic rational interface.
|
||||
*
|
||||
* Bear output: CI=0, AS=0.3e18, AW=100, DD=0.3e18
|
||||
* Bull output: CI=0, AS=1e18, AW=20, DD=1e18
|
||||
*
|
||||
* Bull condition: stakedPct > 91 AND penalty < 50
|
||||
* where penalty = deltaS^3 * effIdx / 20
|
||||
*/
|
||||
contract OptimizerV3Push3Test is Test {
|
||||
OptimizerV3Push3 push3;
|
||||
|
||||
uint256[30] TAX_RATES =
|
||||
[uint256(1), 3, 5, 8, 12, 18, 24, 30, 40, 50, 60, 80, 100, 130, 180, 250, 320, 420, 540, 700, 920, 1200, 1600, 2000, 2600, 3400, 4400, 5700, 7500, 9700];
|
||||
uint256[30] TAX_RATES = [
|
||||
uint256(1),
|
||||
3,
|
||||
5,
|
||||
8,
|
||||
12,
|
||||
18,
|
||||
24,
|
||||
30,
|
||||
40,
|
||||
50,
|
||||
60,
|
||||
80,
|
||||
100,
|
||||
130,
|
||||
180,
|
||||
250,
|
||||
320,
|
||||
420,
|
||||
540,
|
||||
700,
|
||||
920,
|
||||
1200,
|
||||
1600,
|
||||
2000,
|
||||
2600,
|
||||
3400,
|
||||
4400,
|
||||
5700,
|
||||
7500,
|
||||
9700
|
||||
];
|
||||
uint256 constant MAX_TAX = 9700;
|
||||
|
||||
// Expected bear/bull outputs
|
||||
uint256 constant BEAR_CI = 0;
|
||||
uint256 constant BEAR_ANCHOR_SHARE = 3e17; // 0.3e18
|
||||
uint24 constant BEAR_ANCHOR_WIDTH = 100;
|
||||
uint256 constant BEAR_DISCOVERY = 3e17; // 0.3e18
|
||||
|
||||
uint256 constant BULL_CI = 0;
|
||||
uint256 constant BULL_ANCHOR_SHARE = 1e18;
|
||||
uint24 constant BULL_ANCHOR_WIDTH = 20;
|
||||
uint256 constant BULL_DISCOVERY = 1e18;
|
||||
|
||||
function setUp() public {
|
||||
push3 = new OptimizerV3Push3();
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
function _norm(uint256 taxIdx) internal view returns (uint256) {
|
||||
return TAX_RATES[taxIdx] * 1e18 / MAX_TAX;
|
||||
}
|
||||
|
|
@ -27,68 +78,126 @@ contract OptimizerV3Push3Test is Test {
|
|||
return pct * 1e18 / 100;
|
||||
}
|
||||
|
||||
// ---- Direct correctness tests ----
|
||||
/// @dev Build an 8-slot input array with only slots 0 and 1 populated.
|
||||
function _inputs(uint256 percentageStaked, uint256 averageTaxRate)
|
||||
internal
|
||||
pure
|
||||
returns (OptimizerInput[8] memory inp)
|
||||
{
|
||||
inp[0] = OptimizerInput({mantissa: int256(percentageStaked), shift: 0});
|
||||
inp[1] = OptimizerInput({mantissa: int256(averageTaxRate), shift: 0});
|
||||
// slots 2-7 default to (0, 0)
|
||||
}
|
||||
|
||||
function _assertBear(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure {
|
||||
assertEq(ci, BEAR_CI, "bear: ci");
|
||||
assertEq(as_, BEAR_ANCHOR_SHARE, "bear: anchorShare");
|
||||
assertEq(aw, BEAR_ANCHOR_WIDTH, "bear: anchorWidth");
|
||||
assertEq(dd, BEAR_DISCOVERY, "bear: discoveryDepth");
|
||||
}
|
||||
|
||||
function _assertBull(uint256 ci, uint256 as_, uint24 aw, uint256 dd) internal pure {
|
||||
assertEq(ci, BULL_CI, "bull: ci");
|
||||
assertEq(as_, BULL_ANCHOR_SHARE, "bull: anchorShare");
|
||||
assertEq(aw, BULL_ANCHOR_WIDTH, "bull: anchorWidth");
|
||||
assertEq(dd, BULL_DISCOVERY, "bull: discoveryDepth");
|
||||
}
|
||||
|
||||
// ---- Bear cases ----
|
||||
|
||||
function testAlwaysBearAt0Percent() public view {
|
||||
for (uint256 t = 0; t < 30; t++) {
|
||||
assertFalse(push3.isBullMarket(0, _norm(t)));
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(0, _norm(t)));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
}
|
||||
|
||||
function testAlwaysBearAt91Percent() public view {
|
||||
for (uint256 t = 0; t < 30; t++) {
|
||||
assertFalse(push3.isBullMarket(_pct(91), _norm(t)));
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(91), _norm(t)));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Bull cases ----
|
||||
|
||||
function testBoundary92PercentLowestTax() public view {
|
||||
// deltaS=8, effIdx=0 → penalty=0 < 50 → BULL
|
||||
assertTrue(push3.isBullMarket(_pct(92), _norm(0)));
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(0)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
function testBoundary92PercentTaxIdx1() public view {
|
||||
// deltaS=8, effIdx=1 → penalty=512*1/20=25 < 50 → BULL
|
||||
assertTrue(push3.isBullMarket(_pct(92), _norm(1)));
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(1)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
function testBoundary92PercentTaxIdx2() public view {
|
||||
function testBoundary92PercentTaxIdx2Bear() public view {
|
||||
// deltaS=8, effIdx=2 → penalty=512*2/20=51 >= 50 → BEAR
|
||||
assertFalse(push3.isBullMarket(_pct(92), _norm(2)));
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(92), _norm(2)));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
function testAt95PercentTaxIdx7() public view {
|
||||
// deltaS=5, effIdx=7 → penalty=125*7/20=43 < 50 → BULL
|
||||
assertTrue(push3.isBullMarket(_pct(95), _norm(7)));
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(95), _norm(7)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
function testAt95PercentTaxIdx8() public view {
|
||||
function testAt95PercentTaxIdx8Bear() public view {
|
||||
// deltaS=5, effIdx=8 → penalty=125*8/20=50 NOT < 50 → BEAR
|
||||
assertFalse(push3.isBullMarket(_pct(95), _norm(8)));
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(95), _norm(8)));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
function testAt97PercentHighTax() public view {
|
||||
// deltaS=3, effIdx=29 → penalty=27*29/20=39 < 50 → BULL
|
||||
assertTrue(push3.isBullMarket(_pct(97), _norm(29)));
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(97), _norm(29)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
function testAt100PercentAlwaysBull() public view {
|
||||
for (uint256 t = 0; t < 30; t++) {
|
||||
assertTrue(push3.isBullMarket(1e18, _norm(t)));
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(1e18, _norm(t)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
}
|
||||
|
||||
function testEffIdxShiftAtBoundary() public view {
|
||||
// taxIdx=13: effIdx=13, penalty=64*13/20=41 < 50 → BULL
|
||||
assertTrue(push3.isBullMarket(_pct(96), _norm(13)));
|
||||
{
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(96), _norm(13)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
// taxIdx=14: effIdx=15 (shift!), penalty=64*15/20=48 < 50 → BULL
|
||||
assertTrue(push3.isBullMarket(_pct(96), _norm(14)));
|
||||
{
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(96), _norm(14)));
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
// taxIdx=15: effIdx=16, penalty=64*16/20=51 >= 50 → BEAR
|
||||
assertFalse(push3.isBullMarket(_pct(96), _norm(15)));
|
||||
{
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(_inputs(_pct(96), _norm(15)));
|
||||
_assertBear(ci, as_, aw, dd);
|
||||
}
|
||||
}
|
||||
|
||||
function testRevertsAbove100Percent() public {
|
||||
vm.expectRevert("Invalid percentage staked");
|
||||
push3.isBullMarket(1e18 + 1, 0);
|
||||
// ---- Unused slots are ignored ----
|
||||
|
||||
function testUnusedSlotsIgnored() public view {
|
||||
// Populate slots 2-7 with arbitrary values; output should be unchanged.
|
||||
OptimizerInput[8] memory inp;
|
||||
inp[0] = OptimizerInput({mantissa: int256(_pct(92)), shift: 0});
|
||||
inp[1] = OptimizerInput({mantissa: int256(_norm(0)), shift: 0});
|
||||
inp[2] = OptimizerInput({mantissa: 12345678, shift: 0});
|
||||
inp[3] = OptimizerInput({mantissa: 9876, shift: 0});
|
||||
inp[4] = OptimizerInput({mantissa: 1e17, shift: 0});
|
||||
inp[5] = OptimizerInput({mantissa: 3600, shift: 0});
|
||||
inp[6] = OptimizerInput({mantissa: 5e17, shift: 0});
|
||||
inp[7] = OptimizerInput({mantissa: 42, shift: 0});
|
||||
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) = push3.calculateParams(inp);
|
||||
_assertBull(ci, as_, aw, dd);
|
||||
}
|
||||
|
||||
// ---- Fuzz ----
|
||||
|
|
@ -96,6 +205,21 @@ contract OptimizerV3Push3Test is Test {
|
|||
function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view {
|
||||
percentageStaked = bound(percentageStaked, 0, 1e18);
|
||||
averageTaxRate = bound(averageTaxRate, 0, 1e18);
|
||||
push3.isBullMarket(percentageStaked, averageTaxRate);
|
||||
push3.calculateParams(_inputs(percentageStaked, averageTaxRate));
|
||||
}
|
||||
|
||||
function testFuzzOutputsAreAlwaysBearOrBull(uint256 percentageStaked, uint256 averageTaxRate) public view {
|
||||
percentageStaked = bound(percentageStaked, 0, 1e18);
|
||||
averageTaxRate = bound(averageTaxRate, 0, 1e18);
|
||||
(uint256 ci, uint256 as_, uint24 aw, uint256 dd) =
|
||||
push3.calculateParams(_inputs(percentageStaked, averageTaxRate));
|
||||
|
||||
// CI is always 0 in the binary bear/bull model
|
||||
assertEq(ci, 0, "ci always 0");
|
||||
|
||||
// Output is exactly BEAR or BULL
|
||||
bool isBearOutput = (as_ == BEAR_ANCHOR_SHARE && aw == BEAR_ANCHOR_WIDTH && dd == BEAR_DISCOVERY);
|
||||
bool isBullOutput = (as_ == BULL_ANCHOR_SHARE && aw == BULL_ANCHOR_WIDTH && dd == BULL_DISCOVERY);
|
||||
assertTrue(isBearOutput || isBullOutput, "output must be bear or bull");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue