fix: Shift field silently ignored — dyadic rational inputs effectively unsupported (#606)

Add require(shift == 0) guards to Optimizer.calculateParams and
OptimizerV3.calculateParams so non-zero shifts revert instead of being
silently discarded.  OptimizerV3Push3 already had this guard.

Update IOptimizer.sol NatSpec to document that shift is reserved for
future use and must be 0 in all current implementations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-20 11:42:50 +00:00
parent 26957dae88
commit 42b4bf4149
5 changed files with 35 additions and 2 deletions

View file

@ -3,7 +3,10 @@ pragma solidity ^0.8.19;
/** /**
* @notice Dyadic rational input: mantissa × 2^(-shift). * @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 current
* Optimizer implementations (Optimizer, OptimizerV3, OptimizerV3Push3)
* require shift == 0 and revert otherwise. When shift == 0 (as
* produced by _toDyadic), value == mantissa.
*/ */
struct OptimizerInput { struct OptimizerInput {
int256 mantissa; int256 mantissa;

View file

@ -356,8 +356,10 @@ contract Optimizer is Initializable, UUPSUpgradeable, IOptimizer {
virtual virtual
returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) 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++) { for (uint256 k; k < 8; k++) {
require(inputs[k].shift == 0, "shift not yet supported");
require(inputs[k].mantissa >= 0, "negative mantissa"); 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 assumed mantissa IS the value)

View file

@ -22,6 +22,11 @@ contract OptimizerV3 is Optimizer {
override override
returns (uint256 ci, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) returns (uint256 ci, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth)
{ {
// Guard against non-zero shift (reserved for future use).
for (uint256 k; k < 8; k++) {
require(inputs[k].shift == 0, "shift not yet supported");
}
// BEGIN TRANSPILER OUTPUT (optimizer_v3.push3) // BEGIN TRANSPILER OUTPUT (optimizer_v3.push3)
// Do NOT edit by hand regenerate via: npx tsx tools/push3-transpiler/transpile-cli.ts // 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 * @notice Non-admin calling upgradeTo should revert with UnauthorizedAccount
*/ */

View file

@ -224,6 +224,17 @@ contract OptimizerV3Push3Test is Test {
push3.calculateParams(inp); 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 ---- // ---- Fuzz ----
function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view { function testFuzzNeverReverts(uint256 percentageStaked, uint256 averageTaxRate) public view {