fix: address review findings for gas-limit fitness pressure (#637)

- Optimizer.sol: move CALCULATE_PARAMS_GAS_LIMIT constant to top of
  contract (after error declaration) to avoid mid-contract placement.
  Expand natspec with EIP-150 63/64 note: callers need ~203 175 gas to
  deliver the full 200 000 budget to the inner staticcall.

- Optimizer.sol: add ret.length < 128 guard before abi.decode in
  getLiquidityParams(). Malformed return data (truncated / wrong ABI)
  from an evolved program now falls back to _bearDefaults() instead of
  propagating an unhandled revert. The 128-byte minimum is the ABI
  encoding of (uint256, uint256, uint24, uint256) — four 32-byte slots.

- Optimizer.sol: add cross-reference comment to _bearDefaults() noting
  that its values must stay in sync with LiquidityManager.recenter()'s
  catch block to prevent silent divergence.

- FitnessEvaluator.t.sol: add CALCULATE_PARAMS_GAS_LIMIT mirror constant
  (must match Optimizer.sol). Disqualify candidates whose measured gas
  exceeds the production cap with fitness=0 and error="gas_over_limit"
  — prevents the pipeline from selecting programs that are functionally
  dead on-chain (would always produce bear defaults in production).

- batch-eval.sh: update output format comment to document the gas_used
  field and over-gas-limit error object added by this feature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-13 01:05:37 +00:00
parent c9c0ce5e95
commit 5d369cfab6
3 changed files with 46 additions and 9 deletions

View file

@ -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 200203 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));
}
}

View file

@ -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);

View file

@ -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.