Merge pull request 'fix: Shift field silently ignored — dyadic rational inputs effectively unsupported (#606)' (#1053) from fix/issue-606 into master

This commit is contained in:
johba 2026-03-20 13:35:32 +01:00
commit 86e258f6e1
6 changed files with 122 additions and 3 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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

View file

@ -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
*/

View file

@ -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);
}
}

View file

@ -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 {