Merge pull request 'fix: Red-team: replace ethPerToken with exact total-LM-ETH metric (#539)' (#540) from fix/issue-539 into master
This commit is contained in:
commit
8c0683cbba
7 changed files with 813 additions and 269 deletions
|
|
@ -52,6 +52,9 @@ when the protocol changes — not the marketing copy.
|
|||
- Staker governance for optimizer upgrades (vote with stake weight)
|
||||
- On-chain training data → new optimizer contracts via Push3 transpiler
|
||||
- Remove admin key in favor of staker voting
|
||||
- Adversarial backtesting: replay red-team attack sequences against optimizer candidates (#536)
|
||||
- Push3 optimizer evolution: mutate, score against attacks, select survivors (#537)
|
||||
- Unified Push3 → deploy pipeline: transpile, compile, UUPS upgrade in one command (#538)
|
||||
|
||||
## Fee Destination
|
||||
|
||||
|
|
|
|||
59
onchain/script/LmTotalEth.s.sol
Normal file
59
onchain/script/LmTotalEth.s.sol
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pragma solidity ^0.8.19;
|
||||
|
||||
import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol";
|
||||
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
||||
import "forge-std/Script.sol";
|
||||
|
||||
import "@aperture/uni-v3-lib/LiquidityAmounts.sol";
|
||||
import "@aperture/uni-v3-lib/TickMath.sol";
|
||||
|
||||
interface ILM {
|
||||
function positions(uint8 stage) external view returns (uint128 liquidity, int24 tickLower, int24 tickUpper);
|
||||
}
|
||||
|
||||
interface IWETH {
|
||||
function balanceOf(address) external view returns (uint256);
|
||||
}
|
||||
|
||||
/// @title LmTotalEth
|
||||
/// @notice Read-only script: prints total ETH controlled by LiquidityManager
|
||||
/// (free ETH + free WETH + ETH locked in all 3 Uni V3 positions).
|
||||
/// @dev forge script script/LmTotalEth.s.sol --rpc-url $RPC_URL
|
||||
/// Env: LM, WETH, POOL
|
||||
contract LmTotalEth is Script {
|
||||
function run() external view {
|
||||
address lm = vm.envAddress("LM");
|
||||
address weth = vm.envAddress("WETH");
|
||||
address pool = vm.envAddress("POOL");
|
||||
|
||||
// Free balances
|
||||
uint256 freeEth = lm.balance;
|
||||
uint256 freeWeth = IWETH(weth).balanceOf(lm);
|
||||
|
||||
// Current sqrtPrice from pool
|
||||
(uint160 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0();
|
||||
|
||||
// Determine which token is WETH (token0 or token1)
|
||||
bool wethIsToken0 = IUniswapV3Pool(pool).token0() == weth;
|
||||
|
||||
// Sum ETH in all 3 positions: FLOOR=0, ANCHOR=1, DISCOVERY=2
|
||||
uint256 positionEth = 0;
|
||||
for (uint8 stage = 0; stage < 3; stage++) {
|
||||
(uint128 liquidity, int24 tickLower, int24 tickUpper) = ILM(lm).positions(stage);
|
||||
if (liquidity == 0) continue;
|
||||
|
||||
uint160 sqrtA = TickMath.getSqrtRatioAtTick(tickLower);
|
||||
uint160 sqrtB = TickMath.getSqrtRatioAtTick(tickUpper);
|
||||
|
||||
(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, sqrtA, sqrtB, liquidity);
|
||||
|
||||
positionEth += wethIsToken0 ? amount0 : amount1;
|
||||
}
|
||||
|
||||
uint256 total = freeEth + freeWeth + positionEth;
|
||||
|
||||
// Output as plain number for easy bash consumption
|
||||
console2.log(total);
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,10 +50,37 @@ command -v claude &>/dev/null || die "claude CLI not found (install: npm i -g
|
|||
command -v python3 &>/dev/null || die "python3 not found"
|
||||
command -v jq &>/dev/null || die "jq not found"
|
||||
|
||||
# ── 1. Verify stack is running ─────────────────────────────────────────────────
|
||||
log "Verifying Anvil is accessible at $RPC_URL ..."
|
||||
# ── 1. Fresh stack — tear down, rebuild, wait for bootstrap ────────────────────
|
||||
log "Rebuilding fresh stack ..."
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Free RAM: drop caches
|
||||
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches' 2>/dev/null || true
|
||||
|
||||
# Tear down completely (volumes too — clean anvil state)
|
||||
sudo docker compose down -v >/dev/null 2>&1 || true
|
||||
sleep 3
|
||||
|
||||
# Bring up
|
||||
sudo docker compose up -d >/dev/null 2>&1 \
|
||||
|| die "docker compose up -d failed"
|
||||
|
||||
# Wait for bootstrap to complete (max 120s)
|
||||
log "Waiting for bootstrap ..."
|
||||
for i in $(seq 1 40); do
|
||||
if sudo docker logs harb-bootstrap-1 2>&1 | grep -q "Bootstrap complete"; then
|
||||
log " Bootstrap complete (${i}x3s)"
|
||||
break
|
||||
fi
|
||||
if [[ $i -eq 40 ]]; then
|
||||
die "Bootstrap did not complete within 120s"
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
|
||||
# Verify Anvil responds
|
||||
"$CAST" chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|
||||
|| die "Anvil not accessible at $RPC_URL — run: ./scripts/dev.sh start"
|
||||
|| die "Anvil not accessible at $RPC_URL after stack start"
|
||||
|
||||
# ── 2. Read contract addresses ─────────────────────────────────────────────────
|
||||
[[ -f "$DEPLOYMENTS" ]] || die "deployments-local.json not found at $DEPLOYMENTS (bootstrap not complete)"
|
||||
|
|
@ -85,23 +112,67 @@ POOL=$("$CAST" call "$V3_FACTORY" "getPool(address,address,uint24)(address)" \
|
|||
"$WETH" "$KRK" "$POOL_FEE" --rpc-url "$RPC_URL")
|
||||
log " Pool: $POOL"
|
||||
|
||||
# ── 3. Grant recenterAccess to account 2 ──────────────────────────────────────
|
||||
# Done BEFORE the snapshot so every revert restores account 2's access.
|
||||
# LM.recenterAccess is a single address slot — replace it with account 2.
|
||||
# Only the feeDestination is authorised to call setRecenterAccess().
|
||||
log "Granting recenterAccess to account 2 ($RECENTER_ADDR) ..."
|
||||
# ── 3a. Grant recenterAccess FIRST (while original feeDestination is still set) ──
|
||||
FEE_DEST=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL") \
|
||||
|| die "Failed to read feeDestination() from LM"
|
||||
FEE_DEST=$(echo "$FEE_DEST" | tr -d '[:space:]')
|
||||
log "Granting recenterAccess to account 2 ($RECENTER_ADDR) via feeDestination ($FEE_DEST) ..."
|
||||
"$CAST" rpc --rpc-url "$RPC_URL" anvil_impersonateAccount "$FEE_DEST" \
|
||||
|| die "anvil_impersonateAccount $FEE_DEST failed"
|
||||
"$CAST" send --rpc-url "$RPC_URL" --from "$FEE_DEST" --unlocked \
|
||||
"$LM" "setRecenterAccess(address)" "$RECENTER_ADDR" >/dev/null 2>&1 \
|
||||
|| die "setRecenterAccess($RECENTER_ADDR) failed — check that feeDestination is correct"
|
||||
|| die "setRecenterAccess($RECENTER_ADDR) failed"
|
||||
"$CAST" rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST" \
|
||||
|| die "anvil_stopImpersonatingAccount $FEE_DEST failed"
|
||||
log " recenterAccess granted"
|
||||
|
||||
# ── 3b. Override feeDestination to LM itself (fees accrue as liquidity) ────────
|
||||
# feeDestination is a one-shot setter, so we override storage directly.
|
||||
# Slot 7 contains feeDestination (address, lower 20 bytes).
|
||||
# Upper bytes of slot 7 contain other packed state variables.
|
||||
# NOTE: This is fragile — storage layout changes break this. Consider making
|
||||
# feeDestination mutable or using Foundry's vm.store with slot lookup.
|
||||
log "Setting feeDestination to LM ($LM) ..."
|
||||
SLOT7=$("$CAST" storage "$LM" 7 --rpc-url "$RPC_URL" | tr -d '[:space:]')
|
||||
UPPER=${SLOT7:0:26}
|
||||
LM_LOWER=$(echo "$LM" | tr '[:upper:]' '[:lower:]' | sed 's/0x//')
|
||||
NEW_SLOT7="${UPPER}${LM_LOWER}"
|
||||
"$CAST" rpc --rpc-url "$RPC_URL" anvil_setStorageAt "$LM" "0x7" "$NEW_SLOT7" \
|
||||
|| die "anvil_setStorageAt for feeDestination failed"
|
||||
VERIFY=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL" | tr -d '[:space:]')
|
||||
log " feeDestination set to: $VERIFY"
|
||||
[[ "${VERIFY,,}" == "${LM,,}" ]] || die "feeDestination verification failed: expected $LM, got $VERIFY"
|
||||
|
||||
# ── 3c. Fund LM with 1000 ETH and deploy into positions via recenter ───────────
|
||||
# Send ETH as WETH (LM uses WETH internally), then recenter to deploy into positions.
|
||||
# Without recenter, the ETH sits idle and the first recenter mints massive KRK.
|
||||
log "Funding LM with 1000 ETH ..."
|
||||
# Wrap to WETH and transfer to LM
|
||||
"$CAST" send "$WETH" "deposit()" --value 1000ether \
|
||||
--private-key "$ADV_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|
||||
|| die "Failed to wrap ETH"
|
||||
"$CAST" send "$WETH" "transfer(address,uint256)" "$LM" 1000000000000000000000 \
|
||||
--private-key "$ADV_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|
||||
|| die "Failed to transfer WETH to LM"
|
||||
|
||||
# Recenter to deploy the new WETH into positions (establishes realistic baseline)
|
||||
log "Recentering to deploy funded WETH into positions ..."
|
||||
"$CAST" send "$LM" "recenter()" \
|
||||
--private-key "$RECENTER_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 \
|
||||
|| log " WARNING: initial recenter failed (may need amplitude — mining blocks)"
|
||||
# Mine blocks and retry if needed
|
||||
for _i in $(seq 1 3); do
|
||||
for _b in $(seq 1 50); do
|
||||
"$CAST" rpc evm_mine --rpc-url "$RPC_URL" >/dev/null 2>&1
|
||||
done
|
||||
"$CAST" send "$LM" "recenter()" \
|
||||
--private-key "$RECENTER_PK" --rpc-url "$RPC_URL" >/dev/null 2>&1 && break
|
||||
done
|
||||
|
||||
LM_ETH=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
LM_WETH=$("$CAST" call "$WETH" "balanceOf(address)(uint256)" "$LM" --rpc-url "$RPC_URL" | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
log " LM after recenter: ETH=$LM_ETH WETH=$LM_WETH"
|
||||
|
||||
# ── 4. Take Anvil snapshot (clean baseline, includes recenterAccess grant) ─────
|
||||
log "Taking Anvil snapshot..."
|
||||
SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"')
|
||||
|
|
@ -117,43 +188,20 @@ cleanup() {
|
|||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# ── Helper: compute ethPerToken (mirrors floor.ts getEthPerToken) ──────────────
|
||||
# ethPerToken = (lm_native_eth + lm_weth) * 1e18 / adjusted_outstanding_supply
|
||||
# adjusted_supply = outstandingSupply() - KRK_at_feeDestination - KRK_at_stakingPool
|
||||
compute_eth_per_token() {
|
||||
local lm_eth lm_weth supply fee_bal stake_bal adj_supply
|
||||
|
||||
lm_eth=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]')
|
||||
lm_weth=$("$CAST" call "$WETH" "balanceOf(address)(uint256)" "$LM" \
|
||||
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]')
|
||||
supply=$("$CAST" call "$KRK" "outstandingSupply()(uint256)" \
|
||||
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]')
|
||||
|
||||
# Fee destination: read from contract (set at deploy time, may differ per fork)
|
||||
local fee_dest
|
||||
fee_dest=$("$CAST" call "$LM" "feeDestination()(address)" \
|
||||
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]')
|
||||
|
||||
fee_bal=0
|
||||
local zero="0x0000000000000000000000000000000000000000"
|
||||
if [[ "${fee_dest,,}" != "${zero,,}" ]]; then
|
||||
fee_bal=$("$CAST" call "$KRK" "balanceOf(address)(uint256)" "$fee_dest" \
|
||||
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]' || echo 0)
|
||||
fi
|
||||
|
||||
# Staking pool: use the deployed Stake address (mirrors peripheryContracts()[1])
|
||||
stake_bal=$("$CAST" call "$KRK" "balanceOf(address)(uint256)" "$STAKE" \
|
||||
--rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]' || echo 0)
|
||||
|
||||
python3 - <<PYEOF
|
||||
e = int('${lm_eth:-0}' or 0)
|
||||
w = int('${lm_weth:-0}' or 0)
|
||||
s = int('${supply:-0}' or 0)
|
||||
f = int('${fee_bal:-0}' or 0)
|
||||
k = int('${stake_bal:-0}' or 0)
|
||||
adj = s - f - k
|
||||
print(0 if adj <= 0 else (e + w) * 10**18 // adj)
|
||||
PYEOF
|
||||
# ── Helper: compute total ETH controlled by LM ────────────────────────────────
|
||||
# Total = free ETH + free WETH + ETH locked in all 3 Uni V3 positions
|
||||
# This is the real metric: "can the adversary extract ETH from the protocol?"
|
||||
# Uses a forge script with exact Uni V3 integer math (LiquidityAmounts + TickMath)
|
||||
# instead of multiple cast calls + Python float approximation.
|
||||
compute_lm_total_eth() {
|
||||
local output result
|
||||
output=$(LM="$LM" WETH="$WETH" POOL="$POOL" \
|
||||
/home/debian/.foundry/bin/forge script script/LmTotalEth.s.sol \
|
||||
--rpc-url "$RPC_URL" --root "$REPO_ROOT/onchain" --no-color 2>&1)
|
||||
# forge script prints "== Logs ==" then " <value>" — extract the number
|
||||
result=$(echo "$output" | awk '/^== Logs ==/{getline; gsub(/^[[:space:]]+/,""); print; exit}')
|
||||
[[ -n "$result" && "$result" =~ ^[0-9]+$ ]] || die "Failed to read LM total ETH (forge output: $output)"
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# ── Helper: extract strategy findings from stream-json and append to memory ────
|
||||
|
|
@ -169,7 +217,7 @@ extract_memory() {
|
|||
run_num=1
|
||||
fi
|
||||
|
||||
python3 - "$stream_file" "$memory_file" "$run_num" "$FLOOR_BEFORE" <<'PYEOF'
|
||||
python3 - "$stream_file" "$memory_file" "$run_num" "$LM_ETH_BEFORE" <<'PYEOF'
|
||||
import json, sys, re
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
|
@ -177,9 +225,9 @@ stream_file = sys.argv[1]
|
|||
memory_file = sys.argv[2]
|
||||
run_num = int(sys.argv[3])
|
||||
try:
|
||||
floor_before = int(sys.argv[4])
|
||||
lm_eth_before = int(sys.argv[4])
|
||||
except (ValueError, IndexError):
|
||||
print(" extract_memory: invalid floor_before value, skipping", file=sys.stderr)
|
||||
print(" extract_memory: invalid lm_eth_before value, skipping", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
texts = []
|
||||
|
|
@ -209,15 +257,15 @@ for text in texts:
|
|||
current = {
|
||||
"strategy": strat_match.group(1).strip(),
|
||||
"steps": "",
|
||||
"floor_after": None,
|
||||
"lm_eth_after": None,
|
||||
"insight": ""
|
||||
}
|
||||
|
||||
if current:
|
||||
# Capture floor readings — take the last match in the block (most recent value)
|
||||
floor_matches = list(re.finditer(r"(?:floor|ethPerToken)[^\d]*?(\d{4,})\s*(?:wei)?", text, re.IGNORECASE))
|
||||
floor_matches = list(re.finditer(r"(?:floor|ethPerToken|lm.?eth)[^\d]*?(\d{4,})\s*(?:wei)?", text, re.IGNORECASE))
|
||||
if floor_matches:
|
||||
current["floor_after"] = int(floor_matches[-1].group(1))
|
||||
current["lm_eth_after"] = int(floor_matches[-1].group(1))
|
||||
|
||||
# Capture insights
|
||||
for pattern in [r"[Kk]ey [Ii]nsight:\s*(.+)", r"[Ii]nsight:\s*(.+)", r"(?:discovered|learned|realized)\s+(?:that\s+)?(.+)"]:
|
||||
|
|
@ -237,11 +285,11 @@ if current:
|
|||
ts = datetime.now(timezone.utc).isoformat()
|
||||
with open(memory_file, "a") as f:
|
||||
for s in strategies:
|
||||
fa = s["floor_after"] if s.get("floor_after") is not None else floor_before
|
||||
delta_bps = round((fa - floor_before) * 10000 / floor_before) if floor_before else 0
|
||||
if fa < floor_before:
|
||||
fa = s["lm_eth_after"] if s.get("lm_eth_after") is not None else lm_eth_before
|
||||
delta_bps = round((fa - lm_eth_before) * 10000 / lm_eth_before) if lm_eth_before else 0
|
||||
if fa < lm_eth_before:
|
||||
result = "DECREASED"
|
||||
elif fa > floor_before:
|
||||
elif fa > lm_eth_before:
|
||||
result = "INCREASED"
|
||||
else:
|
||||
result = "HELD"
|
||||
|
|
@ -251,8 +299,8 @@ with open(memory_file, "a") as f:
|
|||
"ts": ts,
|
||||
"strategy": s["strategy"][:100],
|
||||
"steps": s["steps"][:300].rstrip("; "),
|
||||
"floor_before": floor_before,
|
||||
"floor_after": fa,
|
||||
"lm_eth_before": lm_eth_before,
|
||||
"lm_eth_after": fa,
|
||||
"delta_bps": delta_bps,
|
||||
"result": result,
|
||||
"insight": s["insight"][:300]
|
||||
|
|
@ -284,10 +332,10 @@ if len(all_entries) > 50:
|
|||
PYEOF
|
||||
}
|
||||
|
||||
# ── 5. Read floor_before ───────────────────────────────────────────────────────
|
||||
# ── 5. Read lm_eth_before ───────────────────────────────────────────────────────
|
||||
log "Reading floor before agent run..."
|
||||
FLOOR_BEFORE=$(compute_eth_per_token)
|
||||
log " floor_before = $FLOOR_BEFORE wei/token"
|
||||
LM_ETH_BEFORE=$(compute_lm_total_eth)
|
||||
log " lm_eth_before = $LM_ETH_BEFORE wei"
|
||||
|
||||
# ── 6. Build agent prompt ──────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -323,15 +371,18 @@ PYEOF
|
|||
fi
|
||||
|
||||
PROMPT=$(cat <<PROMPT_EOF
|
||||
# Red-team mission: break the KRAIKEN protocol floor
|
||||
# Red-team mission: extract ETH from the KRAIKEN LiquidityManager
|
||||
|
||||
## Goal
|
||||
Make ethPerToken **decrease** compared to its current value.
|
||||
**Extract ETH from the LiquidityManager.** Make the LM end up with less total ETH than it started with.
|
||||
|
||||
Current ethPerToken: ${FLOOR_BEFORE} wei/token
|
||||
Total LM ETH before: ${LM_ETH_BEFORE} wei
|
||||
(free ETH + free WETH + ETH locked in all 3 Uni V3 positions)
|
||||
|
||||
ethPerToken measures: (LiquidityManager native ETH + WETH) × 1e18 ÷ adjusted_outstanding_supply
|
||||
It is the floor price — the minimum ETH per KRK token the protocol will pay.
|
||||
The protocol's ETH is like gold in a cold-backed currency — it's "put to work" in Uniswap positions,
|
||||
but it should NEVER be extractable by an adversary. Your job is to prove otherwise.
|
||||
|
||||
The metric is simple: if LM total ETH goes down, you win.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -358,7 +409,7 @@ CAST binary: /home/debian/.foundry/bin/cast
|
|||
### Adversary — Anvil account 8 (your main account)
|
||||
- Address: ${ADV_ADDR}
|
||||
- Private key: ${ADV_PK}
|
||||
- Balance: 10 000 ETH, 0 KRK
|
||||
- Balance: ~9000 ETH (10k minus 1000 ETH used to fund LM), 0 KRK
|
||||
|
||||
### Recenter caller — Anvil account 2
|
||||
- Address: ${RECENTER_ADDR}
|
||||
|
|
@ -407,15 +458,38 @@ The floor formula then additionally subtracts KRK at Stake and feeDestination to
|
|||
|
||||
## Cast command patterns
|
||||
|
||||
### Check the floor (run after each strategy)
|
||||
### Check total LM ETH (run after each strategy)
|
||||
Measures free ETH + free WETH + ETH locked in all 3 Uni V3 positions.
|
||||
\`\`\`bash
|
||||
LM_ETH=\$(/home/debian/.foundry/bin/cast balance ${LM} --rpc-url http://localhost:8545)
|
||||
LM_WETH=\$(/home/debian/.foundry/bin/cast call ${WETH} "balanceOf(address)(uint256)" ${LM} --rpc-url http://localhost:8545)
|
||||
SUPPLY=\$(/home/debian/.foundry/bin/cast call ${KRK} "outstandingSupply()(uint256)" --rpc-url http://localhost:8545)
|
||||
FEE_DEST=\$(/home/debian/.foundry/bin/cast call ${LM} "feeDestination()(address)" --rpc-url http://localhost:8545)
|
||||
FEE_BAL=\$(/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" \$FEE_DEST --rpc-url http://localhost:8545)
|
||||
STAKE_BAL=\$(/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" ${STAKE} --rpc-url http://localhost:8545)
|
||||
python3 -c "e=\$LM_ETH; w=\$LM_WETH; s=\$SUPPLY; f=\$FEE_BAL; k=\$STAKE_BAL; adj=s-f-k; print('ethPerToken:', (e+w)*10**18//adj if adj>0 else 0, 'wei/token')"
|
||||
CAST=/home/debian/.foundry/bin/cast
|
||||
LM_ETH=\$(\$CAST balance ${LM} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
LM_WETH=\$(\$CAST call ${WETH} "balanceOf(address)(uint256)" ${LM} --rpc-url http://localhost:8545 | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
SLOT0=\$(\$CAST call ${POOL} "slot0()(uint160,int24,uint16,uint16,uint16,uint8,bool)" --rpc-url http://localhost:8545)
|
||||
CUR_TICK=\$(echo "\$SLOT0" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
TOKEN0_IS_WETH=\$(python3 -c "print(1 if '${WETH}'.lower() < '${KRK}'.lower() else 0)")
|
||||
POS_ETH=0
|
||||
for STAGE in 0 1 2; do
|
||||
POS=\$(\$CAST call ${LM} "positions(uint8)(uint128,int24,int24)" \$STAGE --rpc-url http://localhost:8545)
|
||||
LIQ=\$(echo "\$POS" | sed -n '1p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
TL=\$(echo "\$POS" | sed -n '2p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
TU=\$(echo "\$POS" | sed -n '3p' | sed 's/\[.*//;s/[[:space:]]//g')
|
||||
POS_ETH=\$(python3 -c "
|
||||
import math
|
||||
L,tl,tu,tc,t0w=int('\$LIQ'),int('\$TL'),int('\$TU'),int('\$CUR_TICK'),bool(\$TOKEN0_IS_WETH)
|
||||
prev=int('\$POS_ETH')
|
||||
if L==0: print(prev); exit()
|
||||
sa=math.sqrt(1.0001**tl); sb=math.sqrt(1.0001**tu); sc=math.sqrt(1.0001**tc)
|
||||
if t0w:
|
||||
e=L*(1/sa-1/sb) if tc<tl else (0 if tc>=tu else L*(1/sc-1/sb))
|
||||
else:
|
||||
e=L*(sb-sa) if tc>=tu else (0 if tc<tl else L*(sc-sa))
|
||||
print(prev+int(e))
|
||||
")
|
||||
done
|
||||
TOTAL=\$(python3 -c "print(int('\$LM_ETH')+int('\$LM_WETH')+int('\$POS_ETH'))")
|
||||
echo "Total LM ETH: \$TOTAL wei (free: \$LM_ETH + \$LM_WETH, positions: \$POS_ETH)"
|
||||
echo "Started with: ${LM_ETH_BEFORE} wei"
|
||||
python3 -c "b=${LM_ETH_BEFORE}; a=int('\$TOTAL'); d=b-a; print(f'Delta: {d} wei ({d*100//b if b else 0}% extracted)' if d>0 else f'Delta: {d} wei (LM gained ETH)')"
|
||||
\`\`\`
|
||||
|
||||
### Wrap ETH to WETH
|
||||
|
|
@ -519,19 +593,30 @@ SNAP=\$(/home/debian/.foundry/bin/cast rpc anvil_snapshot --rpc-url http://local
|
|||
|
||||
---
|
||||
|
||||
## Constraints
|
||||
|
||||
- **feeDestination = LM itself** — fees are NOT extracted, they accrue as LM liquidity.
|
||||
When computing ethPerToken, do NOT subtract KRK at feeDestination (it's the same as LM,
|
||||
and outstandingSupply() already excludes LM-held KRK).
|
||||
- **LM has ~1000 ETH reserve** — proportional to your 10,000 ETH (10:1 ratio). This is a
|
||||
realistic attack scenario, not an empty vault.
|
||||
- **You MUST NOT call anvil_reset, anvil_setCode, or anvil_setStorageAt.** These are infra
|
||||
cheats that invalidate the test. Use only swap/stake/LP/recenter protocol operations.
|
||||
|
||||
## Rules
|
||||
|
||||
1. You have 10 000 ETH. Start by wrapping some if you need WETH for swaps.
|
||||
2. Your goal is to make ethPerToken DECREASE vs the starting value (${FLOOR_BEFORE} wei/token).
|
||||
1. You have ~9000 ETH (after funding LM with 1000 ETH). Start by wrapping some if you need WETH for swaps.
|
||||
2. Your goal is to make the LM's total ETH DECREASE vs the starting value (${LM_ETH_BEFORE} wei).
|
||||
3. Try at least 3 distinct strategies. After each attempt:
|
||||
a. Run the floor-check command above to measure ethPerToken.
|
||||
b. If the floor DECREASED — report this as a SUCCESS and describe the exact steps.
|
||||
c. If the floor held or INCREASED — revert to the snapshot and try a new strategy.
|
||||
a. Run the total LM ETH check command above.
|
||||
b. If total LM ETH DECREASED — report this as a SUCCESS and describe the exact steps.
|
||||
c. If LM ETH held or INCREASED — revert to the snapshot and try a new strategy.
|
||||
Remember: \`anvil_revert\` is one-shot. Take a new snapshot immediately after reverting.
|
||||
4. You may chain multiple actions in one strategy (e.g. large buy → recenter → large sell).
|
||||
5. Be methodical. Report every strategy tried even if it failed.
|
||||
6. If Previous Findings are provided, DO NOT repeat those strategies. Use their insights to design new approaches.
|
||||
7. Prioritize untried COMBINATIONS: staking + LP, staking + recenter timing, LP + multi-step swaps, etc.
|
||||
8. Start executing immediately. No lengthy planning — act, measure, iterate.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -544,21 +629,21 @@ After trying all strategies, output a clearly structured report:
|
|||
\`\`\`
|
||||
=== RED-TEAM REPORT ===
|
||||
|
||||
floor_before: <value> wei/token
|
||||
lm_eth_before: <value> wei (total: free + positions)
|
||||
|
||||
STRATEGY 1: <name>
|
||||
Steps: <what you did>
|
||||
floor_after: <value>
|
||||
Result: INCREASED / HELD / DECREASED
|
||||
lm_eth_after: <value> wei
|
||||
Result: ETH_EXTRACTED / ETH_SAFE / ETH_GAINED
|
||||
|
||||
STRATEGY 2: ...
|
||||
...
|
||||
|
||||
=== CONCLUSION ===
|
||||
Floor broken: YES / NO
|
||||
ETH extracted: YES / NO
|
||||
Winning strategy: <describe if YES, else "None">
|
||||
floor_before: ${FLOOR_BEFORE} wei/token
|
||||
floor_after: <final value>
|
||||
lm_eth_before: ${LM_ETH_BEFORE} wei
|
||||
lm_eth_after: <final value> wei
|
||||
\`\`\`
|
||||
PROMPT_EOF
|
||||
)
|
||||
|
|
@ -601,37 +686,37 @@ with open(sys.argv[1]) as f:
|
|||
PYEOF
|
||||
|
||||
# If the agent crashed and produced no readable output, treat as an infra error
|
||||
# rather than silently reporting FLOOR HELD (a false pass).
|
||||
# rather than silently reporting ETH SAFE (a false pass).
|
||||
if [[ $AGENT_EXIT -ne 0 && ! -s "$REPORT" ]]; then
|
||||
die "claude agent failed (exit $AGENT_EXIT) with no readable output — see $STREAM_LOG"
|
||||
fi
|
||||
|
||||
# ── 8. Read floor_after ────────────────────────────────────────────────────────
|
||||
# ── 8. Read lm_eth_after ────────────────────────────────────────────────────────
|
||||
log "Reading floor after agent run..."
|
||||
FLOOR_AFTER=$(compute_eth_per_token)
|
||||
LM_ETH_AFTER=$(compute_lm_total_eth)
|
||||
|
||||
# ── 8a. Extract and persist strategy findings ──────────────────────────────────
|
||||
log "Extracting strategy findings from agent output..."
|
||||
extract_memory "$STREAM_LOG"
|
||||
log " floor_after = $FLOOR_AFTER wei/token"
|
||||
log " lm_eth_after = $LM_ETH_AFTER wei"
|
||||
|
||||
# ── 9. Summarise results ───────────────────────────────────────────────────────
|
||||
log ""
|
||||
log "=== RED-TEAM SUMMARY ==="
|
||||
log ""
|
||||
log " floor_before : $FLOOR_BEFORE wei/token"
|
||||
log " floor_after : $FLOOR_AFTER wei/token"
|
||||
log " lm_eth_before : $LM_ETH_BEFORE wei"
|
||||
log " lm_eth_after : $LM_ETH_AFTER wei"
|
||||
log ""
|
||||
|
||||
BROKE=false
|
||||
if python3 -c "import sys; sys.exit(0 if int('$FLOOR_AFTER') < int('$FLOOR_BEFORE') else 1)"; then
|
||||
if python3 -c "import sys; sys.exit(0 if int('$LM_ETH_AFTER') < int('$LM_ETH_BEFORE') else 1)"; then
|
||||
BROKE=true
|
||||
fi
|
||||
|
||||
if [[ "$BROKE" == "true" ]]; then
|
||||
DELTA=$(python3 -c "print($FLOOR_BEFORE - $FLOOR_AFTER)")
|
||||
log " RESULT: FLOOR BROKEN ❌"
|
||||
log " Decrease: $DELTA wei/token"
|
||||
DELTA=$(python3 -c "print($LM_ETH_BEFORE - $LM_ETH_AFTER)")
|
||||
log " RESULT: ETH EXTRACTED ❌"
|
||||
log " Decrease: $DELTA wei"
|
||||
log ""
|
||||
log " See $REPORT for the winning strategy."
|
||||
log ""
|
||||
|
|
@ -639,24 +724,24 @@ if [[ "$BROKE" == "true" ]]; then
|
|||
cat >>"$REPORT" <<SUMMARY_EOF
|
||||
|
||||
=== RUNNER SUMMARY ===
|
||||
floor_before : $FLOOR_BEFORE
|
||||
floor_after : $FLOOR_AFTER
|
||||
lm_eth_before : $LM_ETH_BEFORE
|
||||
lm_eth_after : $LM_ETH_AFTER
|
||||
delta : -$DELTA
|
||||
verdict : FLOOR_BROKEN
|
||||
verdict : ETH_EXTRACTED
|
||||
SUMMARY_EOF
|
||||
exit 1
|
||||
else
|
||||
log " RESULT: FLOOR HELD ✅"
|
||||
log " RESULT: ETH SAFE ✅"
|
||||
log ""
|
||||
log " See $REPORT for strategies attempted."
|
||||
log ""
|
||||
cat >>"$REPORT" <<SUMMARY_EOF
|
||||
|
||||
=== RUNNER SUMMARY ===
|
||||
floor_before : $FLOOR_BEFORE
|
||||
floor_after : $FLOOR_AFTER
|
||||
lm_eth_before : $LM_ETH_BEFORE
|
||||
lm_eth_after : $LM_ETH_AFTER
|
||||
delta : 0 (or increase)
|
||||
verdict : FLOOR_HELD
|
||||
verdict : ETH_SAFE
|
||||
SUMMARY_EOF
|
||||
exit 0
|
||||
fi
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue