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:
parent
c9c0ce5e95
commit
5d369cfab6
3 changed files with 46 additions and 9 deletions
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue