diff --git a/onchain/src/Optimizer.sol b/onchain/src/Optimizer.sol index f564a9f..d2c8063 100644 --- a/onchain/src/Optimizer.sol +++ b/onchain/src/Optimizer.sol @@ -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; } } diff --git a/onchain/src/OptimizerV3Push3.sol b/onchain/src/OptimizerV3Push3.sol index dd0060e..520bdc1 100644 --- a/onchain/src/OptimizerV3Push3.sol +++ b/onchain/src/OptimizerV3Push3.sol @@ -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); + } } } diff --git a/tools/push3-transpiler/src/index.ts b/tools/push3-transpiler/src/index.ts index 1a38061..f8d9423 100644 --- a/tools/push3-transpiler/src/index.ts +++ b/tools/push3-transpiler/src/index.ts @@ -65,11 +65,23 @@ function main(): void { ' 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 {', ...functionBody, ` ci = uint256(${ciVar});`, ` anchorShare = uint256(${anchorShareVar});`, ` anchorWidth = uint24(${anchorWidthVar});`, ` discoveryDepth = uint256(${discoveryDepthVar});`, + ' }', ' }', '}', '', diff --git a/tools/push3-transpiler/src/transpiler.ts b/tools/push3-transpiler/src/transpiler.ts index fe4a5bb..b86e022 100644 --- a/tools/push3-transpiler/src/transpiler.ts +++ b/tools/push3-transpiler/src/transpiler.ts @@ -31,13 +31,15 @@ function emit(state: TranspilerState, line: string): void { function dpop(state: TranspilerState, ctx: string): string { const v = state.dStack.pop(); - if (v === undefined) throw new Error(`DYADIC stack underflow at ${ctx}`); + // Stack underflow → Push3 no-op semantics: treat missing value as 0 + if (v === undefined) return '0'; return v; } function bpop(state: TranspilerState, ctx: string): string { const v = state.bStack.pop(); - if (v === undefined) throw new Error(`BOOLEAN stack underflow at ${ctx}`); + // Stack underflow → Push3 no-op semantics: treat missing bool as false + if (v === undefined) return 'false'; return v; } @@ -102,7 +104,8 @@ function processInstruction(name: string, state: TranspilerState): void { case 'DYADIC./': { const b = dpop(state, 'DYADIC./'); const a = dpop(state, 'DYADIC./'); - state.dStack.push(`(${a} / ${b})`); + // Safe division: div-by-zero → 0, matching Push3 no-op semantics. + state.dStack.push(`(${b} == 0 ? 0 : ${a} / ${b})`); break; } case 'DYADIC.+': { @@ -391,15 +394,13 @@ export function transpile(program: Node): TranspileResult { processItems(program.items, state); // Pop 4 outputs: top → ci, then anchorShare, anchorWidth, discoveryDepth. - if (state.dStack.length !== 4) { - throw new Error( - `Program must leave exactly 4 values on the DYADIC stack; found ${state.dStack.length}`, - ); - } - const ciVar = state.dStack.pop()!; - const anchorShareVar = state.dStack.pop()!; - const anchorWidthVar = state.dStack.pop()!; - const discoveryDepthVar = state.dStack.pop()!; + // If the stack has fewer than 4 values, fall back to bear defaults for the + // missing positions — matches Push3 no-op semantics (empty stack → 0/default). + // If the stack has more than 4 values, take the top 4 and discard the rest. + const ciVar = state.dStack.pop() ?? '0'; + const anchorShareVar = state.dStack.pop() ?? '300000000000000000'; + const anchorWidthVar = state.dStack.pop() ?? '100'; + const discoveryDepthVar = state.dStack.pop() ?? '300000000000000000'; return { functionBody: state.lines, ciVar, anchorShareVar, anchorWidthVar, discoveryDepthVar }; }