fix: feat: Push3 default outputs — crash/no-output falls back to bear strategy (#634)

Three defensive layers so every Push3 program runs without reverting:

Layer A (transpiler/index.ts): assign bear defaults (CI=0, AS=0.3e18,
AW=100, DD=0.3e18) to all four outputs at the top of calculateParams.
Any output the evolved program does not overwrite keeps the safe default.

Layer B (transpiler/transpiler.ts): graceful stack underflow — dpop/bpop
return '0'/'false' instead of throwing, and the final output-pop falls
back to bear-default literals when fewer than 4 values remain on the
stack. Wrong output count no longer aborts transpilation.

Layer C (transpiler/transpiler.ts + index.ts): wrap the entire function
body in `unchecked {}` so integer overflow wraps (matching Push3), and
emit `(b == 0 ? 0 : a / b)` for every DYADIC./ (div-by-zero → 0,
matching Push3 no-op semantics).

Layer 2 (Optimizer.sol getLiquidityParams): clamp the three fraction
outputs (capitalInefficiency, anchorShare, discoveryDepth) to [0, 1e18]
after abi.decode so a buggy evolved program cannot produce out-of-range
values even if it runs without reverting.

Regenerated OptimizerV3Push3.sol with the updated transpiler; all 193
tests pass (34 Optimizer/OptimizerV3Push3 tests explicitly).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-13 03:47:49 +00:00
parent 8e4bd905ac
commit c87064dc6c
4 changed files with 48 additions and 25 deletions

View file

@ -361,6 +361,13 @@ contract Optimizer is Initializable, UUPSUpgradeable {
// 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));
(capitalInefficiency, anchorShare, anchorWidth, discoveryDepth) =
abi.decode(ret, (uint256, uint256, uint24, uint256));
// Clamp fraction outputs to [0, 1e18] so a buggy evolved program cannot
// produce out-of-range values that confuse the LiquidityManager.
// anchorWidth is already bounded by uint24 at the ABI level.
if (capitalInefficiency > 1e18) capitalInefficiency = 1e18;
if (anchorShare > 1e18) anchorShare = 1e18;
if (discoveryDepth > 1e18) discoveryDepth = 1e18;
}
}

View file

@ -11,18 +11,9 @@ import {OptimizerInput} from "./IOptimizer.sol";
contract OptimizerV3Push3 {
/**
* @notice Compute liquidity parameters from 8 dyadic rational inputs.
* @dev capitalInefficiency (ci) is intentionally hardcoded to 0 in both the bear
* and bull branches of this implementation. CI is a pure risk lever that
* controls the VWAP bias applied when placing the floor position: CI=0 means
* the floor tracks the raw VWAP with no upward adjustment, which is the
* safest setting and carries zero effect on fee revenue. Any integrating
* proxy (e.g. ThreePositionStrategy) must therefore treat the floor scarcity
* and VWAP adjustment as if no capital-inefficiency premium is active.
* Future optimizer versions that expose non-zero CI values should document
* the resulting floor-placement and eth-scarcity effects explicitly.
* @param inputs 8-slot dyadic rational array: slot 0 = percentageStaked (top of Push3 stack),
* slot 1 = averageTaxRate, slots 2-7 = extended metrics (0 if unavailable).
* @return ci Capital inefficiency (0..1e18). Always 0 in this implementation.
* @return ci Capital inefficiency (0..1e18).
* @return anchorShare Fraction of non-floor ETH in anchor (0..1e18).
* @return anchorWidth Anchor position width in tick units.
* @return discoveryDepth Discovery liquidity density (0..1e18).
@ -40,9 +31,20 @@ contract OptimizerV3Push3 {
require(inputs[k].shift == 0, "shift not yet supported");
}
// Layer A: bear defaults any output not overwritten by the program keeps these.
// Matches Push3 no-op semantics: a program that crashes or produces no output
// returns safe bear-mode parameters rather than reverting.
ci = 0;
anchorShare = 300000000000000000;
anchorWidth = 100;
discoveryDepth = 300000000000000000;
// Layer C: unchecked arithmetic overflow wraps (matches Push3 semantics).
// Division by zero is guarded at the expression level (b == 0 ? 0 : a / b).
unchecked {
uint256 percentagestaked = uint256(uint256(inputs[0].mantissa));
uint256 taxrate = uint256(uint256(inputs[1].mantissa));
uint256 staked = uint256(((percentagestaked * 100) / 1000000000000000000));
uint256 staked = uint256((1000000000000000000 == 0 ? 0 : (percentagestaked * 100) / 1000000000000000000));
uint256 r37;
uint256 r38;
uint256 r39;
@ -242,7 +244,7 @@ contract OptimizerV3Push3 {
uint256 r34;
uint256 r35;
uint256 r36;
if ((((((deltas * deltas) * deltas) * effidx) / 20) < 50)) {
if (((20 == 0 ? 0 : (((deltas * deltas) * deltas) * effidx) / 20) < 50)) {
r33 = uint256(1000000000000000000);
r34 = uint256(20);
r35 = uint256(1000000000000000000);
@ -267,5 +269,6 @@ contract OptimizerV3Push3 {
anchorShare = uint256(r39);
anchorWidth = uint24(r38);
discoveryDepth = uint256(r37);
}
}
}