diff --git a/onchain/src/Optimizer.sol b/onchain/src/Optimizer.sol index e849323..f564a9f 100644 --- a/onchain/src/Optimizer.sol +++ b/onchain/src/Optimizer.sol @@ -75,6 +75,19 @@ contract Optimizer is Initializable, UUPSUpgradeable { /// @dev Reverts if the caller is not the admin. error UnauthorizedAccount(address account); + /// @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 + /// preventing unbounded growth from blocking recenter(). + /// + /// Note (EIP-150 / 63-64 rule): the outer getLiquidityParams() call must + /// arrive with at least ⌈200_000 × 64/63⌉ ≈ 203_175 gas for the inner + /// staticcall to actually receive 200 000. Callers with exactly 200–203 k + /// gas will see a spurious bear-defaults fallback. This is not a practical + /// concern from recenter(), which always has abundant gas. + uint256 internal constant CALCULATE_PARAMS_GAS_LIMIT = 200_000; + /** * @notice Initialize the Optimizer. * @param _kraiken The address of the Kraiken token. @@ -131,13 +144,6 @@ 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 ---- /** @@ -150,7 +156,10 @@ contract Optimizer is Initializable, UUPSUpgradeable { /** * @notice Safe bear-mode defaults returned when calculateParams exceeds its - * gas budget or reverts. Mirrors the catch block in LiquidityManager. + * gas budget or reverts. + * @dev Values must stay in sync with the catch block in LiquidityManager.recenter() + * ({capitalInefficiency:0, anchorShare:3e17, anchorWidth:100, discoveryDepth:3e17}). + * Update both locations together if the safe defaults ever change. */ function _bearDefaults() internal @@ -347,6 +356,11 @@ contract Optimizer is Initializable, UUPSUpgradeable { abi.encodeCall(this.calculateParams, (inputs)) ); if (!ok) return _bearDefaults(); + // ABI encoding of (uint256, uint256, uint24, uint256) is exactly 128 bytes + // (each value padded to 32 bytes). A truncated return — e.g. from a + // malformed evolved program — would cause abi.decode to revert; guard here + // so all failure modes fall back via _bearDefaults(). + if (ret.length < 128) return _bearDefaults(); return abi.decode(ret, (uint256, uint256, uint24, uint256)); } } diff --git a/onchain/test/FitnessEvaluator.t.sol b/onchain/test/FitnessEvaluator.t.sol index e726872..20cdf40 100644 --- a/onchain/test/FitnessEvaluator.t.sol +++ b/onchain/test/FitnessEvaluator.t.sol @@ -148,6 +148,11 @@ 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 Must match Optimizer.CALCULATE_PARAMS_GAS_LIMIT. Candidates that exceed + /// this limit would unconditionally produce bear defaults in production and + /// are disqualified (fitness = 0) rather than scored against their theoretical output. + uint256 internal constant CALCULATE_PARAMS_GAS_LIMIT = 200_000; + /// @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). @@ -260,6 +265,22 @@ contract FitnessEvaluator is Test { try Optimizer(optProxy).calculateParams(sampleInputs) returns (uint256, uint256, uint24, uint256) { } catch { } uint256 gasForCalcParams = gasBefore - gasleft(); + // Hard disqualification: candidates that exceed the production gas cap would + // unconditionally produce bear defaults from getLiquidityParams() in every + // recenter call — equivalent to deploying the bear-defaults optimizer. + // Score them as 0 so the evolution pipeline never selects a program that is + // functionally dead on-chain. + if (gasForCalcParams > CALCULATE_PARAMS_GAS_LIMIT) { + console.log( + string.concat( + '{"candidate_id":"', candidateId, + '","fitness":0,"error":"gas_over_limit","gas_used":', _uint2str(gasForCalcParams), + "}" + ) + ); + continue; + } + // Score: sum lm_eth_total across all attack sequences. uint256 totalFitness = 0; Vm.DirEntry[] memory entries = vm.readDir(attacksDir); diff --git a/tools/push3-evolution/revm-evaluator/batch-eval.sh b/tools/push3-evolution/revm-evaluator/batch-eval.sh index 45b6357..030eb18 100755 --- a/tools/push3-evolution/revm-evaluator/batch-eval.sh +++ b/tools/push3-evolution/revm-evaluator/batch-eval.sh @@ -16,7 +16,9 @@ # # Output (stdout): # One JSON object per candidate: -# {"candidate_id":"gen0_c000","fitness":123456789} +# {"candidate_id":"gen0_c000","fitness":123456789,"gas_used":15432} +# Over-gas-limit candidates emit fitness:0 with "error":"gas_over_limit". +# Downstream parsers use the "fitness" key; extra fields are ignored. # # Exit codes: # 0 Success.