fix: feat: Push3 evolution — gas limit as fitness pressure (#637)

- Optimizer.getLiquidityParams() now forwards calculateParams through a
  staticcall capped at 200 000 gas. Programs that exceed the budget or
  revert fall back to bear defaults (CI=0, AS=30%, AW=100, DD=0.3e18),
  so a bloated evolved optimizer can never OOG-revert inside recenter().

- FitnessEvaluator.t.sol measures gas used by calculateParams against
  fixed representative inputs (50% staked, 5% avg tax) after each
  bootstrap. A soft penalty of GAS_PENALTY_FACTOR (1e13 wei/gas) is
  subtracted from total fitness before the JSON score line is emitted.
  Leaner programs win ties; gas_used is included in the output for
  observability. At ~15k gas (current seed) the penalty is ~1.5e17 wei;
  at the 200k hard cap boundary it reaches ~2e18 wei.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-13 00:25:49 +00:00
parent f1ed0e4fdc
commit c9c0ce5e95
2 changed files with 58 additions and 2 deletions

View file

@ -131,6 +131,13 @@ contract Optimizer is Initializable, UUPSUpgradeable {
lastRecenterTimestamp = block.timestamp;
}
/// @dev Gas budget forwarded to calculateParams via staticcall.
/// Evolved programs that exceed this are treated as crashes same outcome
/// as a revert and getLiquidityParams() returns bear defaults instead.
/// 200 000 gives ~13x headroom over the current seed (~15 k gas) while
/// keeping unbounded growth from ever blocking recenter().
uint256 internal constant CALCULATE_PARAMS_GAS_LIMIT = 200_000;
// ---- Dyadic rational helpers ----
/**
@ -141,6 +148,18 @@ contract Optimizer is Initializable, UUPSUpgradeable {
return OptimizerInput({mantissa: value, shift: 0});
}
/**
* @notice Safe bear-mode defaults returned when calculateParams exceeds its
* gas budget or reverts. Mirrors the catch block in LiquidityManager.
*/
function _bearDefaults()
internal
pure
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{
return (0, 3e17, 100, 3e17);
}
// ---- Core computation ----
/**
@ -321,6 +340,13 @@ contract Optimizer is Initializable, UUPSUpgradeable {
// Slots 6-7: 0 (future)
return calculateParams(inputs);
// Call calculateParams with a fixed gas budget. Evolved programs that grow
// too large hit the cap and fall back to bear defaults preventing any
// buggy or bloated optimizer from blocking recenter() with an OOG revert.
(bool ok, bytes memory ret) = address(this).staticcall{gas: CALCULATE_PARAMS_GAS_LIMIT}(
abi.encodeCall(this.calculateParams, (inputs))
);
if (!ok) return _bearDefaults();
return abi.decode(ret, (uint256, uint256, uint24, uint256));
}
}

View file

@ -36,6 +36,7 @@ import "forge-std/Test.sol";
import { Kraiken } from "../src/Kraiken.sol";
import { Stake } from "../src/Stake.sol";
import { Optimizer } from "../src/Optimizer.sol";
import { OptimizerInput } from "../src/IOptimizer.sol";
import { LiquidityManager } from "../src/LiquidityManager.sol";
import { ERC1967Proxy } from "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol";
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
@ -147,6 +148,13 @@ contract FitnessEvaluator is Test {
/// Chosen to be deterministic and not collide with real Base addresses.
address internal constant IMPL_SLOT = address(uint160(uint256(keccak256("fitness.impl.slot"))));
/// @dev Soft gas penalty: wei deducted from fitness per gas unit used by calculateParams.
/// Creates selection pressure toward leaner programs while keeping gas as a
/// secondary criterion (ETH retention still dominates).
/// At 15 k gas (current seed): ~1.5e17 wei penalty.
/// At 200 k gas (hard cap boundary): ~2e18 wei penalty.
uint256 internal constant GAS_PENALTY_FACTOR = 1e13;
// Anvil test accounts (deterministic mnemonic)
/// @dev Account 8 adversary (10 000 ETH in Anvil; funded via vm.deal here)
@ -242,6 +250,16 @@ contract FitnessEvaluator is Test {
continue;
}
// Measure gas used by calculateParams with fixed representative inputs.
// Fixed inputs ensure fair, reproducible comparison across all candidates.
// Uses slot 0 = 50% staked, slot 1 = 5% avg tax rate; remaining slots = 0.
OptimizerInput[8] memory sampleInputs;
sampleInputs[0] = OptimizerInput({ mantissa: 5e17, shift: 0 });
sampleInputs[1] = OptimizerInput({ mantissa: 5e16, shift: 0 });
uint256 gasBefore = gasleft();
try Optimizer(optProxy).calculateParams(sampleInputs) returns (uint256, uint256, uint24, uint256) { } catch { }
uint256 gasForCalcParams = gasBefore - gasleft();
// Score: sum lm_eth_total across all attack sequences.
uint256 totalFitness = 0;
Vm.DirEntry[] memory entries = vm.readDir(attacksDir);
@ -254,8 +272,20 @@ contract FitnessEvaluator is Test {
vm.revertTo(atkSnap);
}
// Apply soft gas penalty: fitness = score - (gasUsed * GAS_PENALTY_FACTOR).
// Leaner programs win ties; programs at the hard-cap boundary incur ~2 ETH penalty.
uint256 gasPenalty = gasForCalcParams * GAS_PENALTY_FACTOR;
uint256 adjustedFitness = totalFitness > gasPenalty ? totalFitness - gasPenalty : 0;
// Emit score as a JSON line (parsed by batch-eval.sh).
console.log(string.concat('{"candidate_id":"', candidateId, '","fitness":', _uint2str(totalFitness), "}"));
console.log(
string.concat(
'{"candidate_id":"', candidateId,
'","fitness":', _uint2str(adjustedFitness),
',"gas_used":', _uint2str(gasForCalcParams),
"}"
)
);
}
// Close manifest files.