Merge pull request 'fix: feat: Push3 evolution — gas limit as fitness pressure (#637)' (#645) from fix/issue-637 into master
This commit is contained in:
commit
f10e70171c
3 changed files with 96 additions and 3 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.
|
||||
|
|
@ -141,6 +154,21 @@ 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.
|
||||
* @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
|
||||
pure
|
||||
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
|
||||
{
|
||||
return (0, 3e17, 100, 3e17);
|
||||
}
|
||||
|
||||
// ---- Core computation ----
|
||||
|
||||
/**
|
||||
|
|
@ -321,6 +349,18 @@ 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();
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,18 @@ 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).
|
||||
/// 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 +255,32 @@ 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();
|
||||
|
||||
// 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);
|
||||
|
|
@ -254,8 +293,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.
|
||||
|
|
|
|||
|
|
@ -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