diff --git a/onchain/src/IOptimizer.sol b/onchain/src/IOptimizer.sol index 047bcdd..4531a8a 100644 --- a/onchain/src/IOptimizer.sol +++ b/onchain/src/IOptimizer.sol @@ -3,7 +3,9 @@ pragma solidity ^0.8.19; /** * @notice Dyadic rational input: mantissa × 2^(-shift). - * For shift == 0 (current usage via _toDyadic), value == mantissa. + * shift is reserved for future use and MUST be 0. All production + * Optimizer implementations require shift == 0 and revert otherwise. + * When shift == 0 (as produced by _toDyadic), value == mantissa. */ struct OptimizerInput { int256 mantissa; diff --git a/onchain/src/Optimizer.sol b/onchain/src/Optimizer.sol index 7c596e7..e08d835 100644 --- a/onchain/src/Optimizer.sol +++ b/onchain/src/Optimizer.sol @@ -356,11 +356,13 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer { virtual returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { - // Guard against negative mantissa — uint256() cast silently wraps negatives. + // Guard against non-zero shift and negative mantissa. + // shift is reserved for future use; uint256() cast silently wraps negatives. for (uint256 k; k < 8; k++) { + require(inputs[k].shift == 0, "shift not yet supported"); require(inputs[k].mantissa >= 0, "negative mantissa"); } - // Extract slots 0 and 1 (shift=0 assumed — mantissa IS the value) + // Extract slots 0 and 1 (shift=0 enforced above — mantissa IS the value) uint256 percentageStaked = uint256(inputs[0].mantissa); uint256 averageTaxRate = uint256(inputs[1].mantissa); diff --git a/onchain/src/OptimizerV3.sol b/onchain/src/OptimizerV3.sol index 8fa995e..e7375f9 100644 --- a/onchain/src/OptimizerV3.sol +++ b/onchain/src/OptimizerV3.sol @@ -22,6 +22,13 @@ contract OptimizerV3 is Optimizer { override returns (uint256 ci, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { + // Guard against non-zero shift and negative mantissa. + // shift is reserved for future use; uint256() cast silently wraps negatives. + for (uint256 k; k < 8; k++) { + require(inputs[k].shift == 0, "shift not yet supported"); + require(inputs[k].mantissa >= 0, "negative mantissa"); + } + // ── BEGIN TRANSPILER OUTPUT (optimizer_v3.push3) ── // Do NOT edit by hand — regenerate via: npx tsx tools/push3-transpiler/transpile-cli.ts diff --git a/onchain/test/Optimizer.t.sol b/onchain/test/Optimizer.t.sol index 18e0728..57daa62 100644 --- a/onchain/test/Optimizer.t.sol +++ b/onchain/test/Optimizer.t.sol @@ -359,6 +359,18 @@ contract OptimizerTest is Test { } } + /** + * @notice calculateParams reverts when any slot has shift != 0 + */ + function testCalculateParamsRevertsOnNonZeroShift() public { + for (uint256 k = 0; k < 8; k++) { + OptimizerInput[8] memory inputs; + inputs[k] = OptimizerInput({ mantissa: 0, shift: 1 }); + vm.expectRevert("shift not yet supported"); + optimizer.calculateParams(inputs); + } + } + /** * @notice Non-admin calling upgradeTo should revert with UnauthorizedAccount */ diff --git a/onchain/test/OptimizerV3.t.sol b/onchain/test/OptimizerV3.t.sol new file mode 100644 index 0000000..fc3b662 --- /dev/null +++ b/onchain/test/OptimizerV3.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import {OptimizerV3} from "../src/OptimizerV3.sol"; +import {OptimizerInput} from "../src/IOptimizer.sol"; +import "forge-std/Test.sol"; + +/** + * @title OptimizerV3Test + * @notice Unit tests for OptimizerV3.calculateParams input validation guards + * (shift and negative-mantissa) and basic bear/bull output correctness. + */ +contract OptimizerV3Test is Test { + OptimizerV3 v3; + + function setUp() public { + v3 = new OptimizerV3(); + } + + // ---- Helpers ---- + + function _inputs(uint256 percentageStaked, uint256 averageTaxRate) + internal + pure + returns (OptimizerInput[8] memory inp) + { + inp[0] = OptimizerInput({mantissa: int256(percentageStaked), shift: 0}); + inp[1] = OptimizerInput({mantissa: int256(averageTaxRate), shift: 0}); + } + + // ---- Shift guard ---- + + function testNonZeroShiftReverts() public { + for (uint256 k = 0; k < 8; k++) { + OptimizerInput[8] memory inp; + inp[k] = OptimizerInput({mantissa: 0, shift: 1}); + vm.expectRevert("shift not yet supported"); + v3.calculateParams(inp); + } + } + + // ---- Negative mantissa guard ---- + + function testNegativeMantissaSlot0Reverts() public { + OptimizerInput[8] memory inp; + inp[0] = OptimizerInput({mantissa: -1, shift: 0}); + vm.expectRevert("negative mantissa"); + v3.calculateParams(inp); + } + + function testNegativeMantissaSlot1Reverts() public { + OptimizerInput[8] memory inp; + inp[0] = OptimizerInput({mantissa: int256(95e16), shift: 0}); + inp[1] = OptimizerInput({mantissa: -1, shift: 0}); + vm.expectRevert("negative mantissa"); + v3.calculateParams(inp); + } + + function testNegativeMantissaHighSlotReverts() public { + OptimizerInput[8] memory inp; + inp[5] = OptimizerInput({mantissa: -1, shift: 0}); + vm.expectRevert("negative mantissa"); + v3.calculateParams(inp); + } + + // ---- Basic output correctness ---- + + function testBearAtLowStaking() public view { + (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = + v3.calculateParams(_inputs(50e16, 0)); + assertEq(ci, 0); + assertEq(as_, 3e17); + assertEq(aw, 100); + assertEq(dd, 3e17); + } + + function testBullAtHighStaking() public view { + (uint256 ci, uint256 as_, uint24 aw, uint256 dd) = + v3.calculateParams(_inputs(96e16, 0)); + assertEq(ci, 0); + assertEq(as_, 1e18); + assertEq(aw, 20); + assertEq(dd, 1e18); + } +} diff --git a/onchain/test/OptimizerV3Push3.t.sol b/onchain/test/OptimizerV3Push3.t.sol index bae7169..f606c41 100644 --- a/onchain/test/OptimizerV3Push3.t.sol +++ b/onchain/test/OptimizerV3Push3.t.sol @@ -224,6 +224,17 @@ contract OptimizerV3Push3Test is Test { push3.calculateParams(inp); } + // ---- Shift guard ---- + + function testNonZeroShiftReverts() public { + for (uint256 k = 0; k < 8; k++) { + OptimizerInput[8] memory inp; + inp[k] = OptimizerInput({mantissa: 0, shift: 1}); + vm.expectRevert("shift not yet supported"); + push3.calculateParams(inp); + } + } + // ---- Fuzz ---- function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view {