diff --git a/onchain/src/IOptimizer.sol b/onchain/src/IOptimizer.sol index 0a4d537..4531a8a 100644 --- a/onchain/src/IOptimizer.sol +++ b/onchain/src/IOptimizer.sol @@ -3,10 +3,9 @@ pragma solidity ^0.8.19; /** * @notice Dyadic rational input: mantissa × 2^(-shift). - * shift is reserved for future use and MUST be 0. All current - * Optimizer implementations (Optimizer, OptimizerV3, OptimizerV3Push3) - * require shift == 0 and revert otherwise. When shift == 0 (as - * produced by _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 e520456..e08d835 100644 --- a/onchain/src/Optimizer.sol +++ b/onchain/src/Optimizer.sol @@ -362,7 +362,7 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer { 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 ecfc30e..e7375f9 100644 --- a/onchain/src/OptimizerV3.sol +++ b/onchain/src/OptimizerV3.sol @@ -22,9 +22,11 @@ contract OptimizerV3 is Optimizer { override returns (uint256 ci, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) { - // Guard against non-zero shift (reserved for future use). + // 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) ── 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); + } +}