Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ Use `opReferenceCheck` to validate that `run` output matches a pure reference im
## Boundary Tests

Always test both sides: the max valid value (should succeed) and one past it (should revert).

## One Test at a Time

Write one test function per edit-compile-run cycle. Do not batch multiple new tests into a single edit. Writing one test at a time produces higher quality code — each test gets full attention, compilation errors are caught immediately, and failures are unambiguous.
16 changes: 8 additions & 8 deletions audit/2026-02-17-03/triage.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,14 @@ Tracks the disposition of every LOW+ finding from pass2 audit reports (test cove
- [FIXED] A20-1: (LOW) No test verifying `erc20-allowance` handles infinite approvals without revert — added `testOpERC20AllowanceInfiniteApproval` with type(uint256).max allowance
- [FIXED] A20-2: (LOW) No test for `decimals()` revert when token does not implement `IERC20Metadata` — all three ERC20 ops have `testOp*DecimalsReadFailure` tests verifying `TokenDecimalsReadFailure` revert via TOFU `safeDecimalsForTokenReadOnly`
- [FIXED] A20-4: (LOW) No test for input values with upper 96 bits set (address truncation) — added NotAnAddress runtime validation with fuzz tests across all 10 address-taking opcodes
- [PENDING] A21-1: (LOW) No test for `referenceFn` `BadOutputsLength` revert path
- [PENDING] A23-1: (LOW) LibOpGreaterThanOrEqualTo missing negative number and float equality eval tests
- [PENDING] A23-2: (LOW) LibOpLessThanOrEqualTo missing negative number and float equality eval tests
- [PENDING] A23-3: (LOW) LibOpConditions no test for exactly 2 inputs (minimum case)
- [PENDING] A23-4: (LOW) LibOpConditions odd-input revert path with reason string not tested via opReferenceCheck
- [PENDING] A24-1: (LOW) LibOpE missing operand disallowed test
- [PENDING] A24-2: (LOW) LibOpExp and LibOpExp2 fuzz tests restrict inputs to non-negative small values only
- [PENDING] A24-3: (LOW) LibOpGm fuzz test restricts inputs to non-negative small values only
- [FIXED] A21-1: (LOW) No test for `referenceFn` `BadOutputsLength` revert path — added testOpExternReferenceFnBadOutputsLength and testOpExternReferenceFnBadOutputsLengthTooMany
- [FIXED] A23-1: (LOW) LibOpGreaterThanOrEqualTo missing negative number and float equality eval tests — added 4 eval tests: 1.1>=1.2, 1.0>=1, -1.1>=-1.2, -1>=0
- [FIXED] A23-2: (LOW) LibOpLessThanOrEqualTo missing negative number and float equality eval tests — added 4 eval tests: 1.1<=1.2, 1.0<=1, -1.1<=-1.2, -1<=0
- [DISMISSED] A23-3: (LOW) LibOpConditions no test for exactly 2 inputs (minimum case) — false positive; testOpConditionsEval1TrueInputZeroOutput and testOpConditionsEval2MixedInputs both use exactly 2 inputs
- [DISMISSED] A23-4: (LOW) LibOpConditions odd-input revert path with reason string not tested via opReferenceCheck — finding itself acknowledges "adequate coverage"; testOpConditionsRunNoConditionsMet fuzz covers odd inputs with reason strings, testOpConditionsEvalErrorCode covers it at eval level
- [FIXED] A24-1: (LOW) LibOpE missing operand disallowed test — added testOpEEvalOperandDisallowed
- [FIXED] A24-2: (LOW) LibOpExp and LibOpExp2 fuzz tests restrict inputs to non-negative small values only — added testOpExpEvalNegativeInput (exp(-1)=1/e) and testOpExp2EvalNegativeInput (exp2(-1)=0.5)
- [FIXED] A24-3: (LOW) LibOpGm fuzz test restricts inputs to non-negative small values only — fixed implementation to compute signed geometric mean: sign * sqrt(|a| * |b|). Expanded fuzz bounds to include negatives. Added eval tests for mixed signs (unit and non-unit), both negative (equal and unequal), zero with negative, and zero bytes identity. Signed GM function upstreamed to rain.math.float as GitHub issue.
- [PENDING] A24-4: (LOW) LibOpFloor eval tests missing negative value coverage
- [PENDING] A25-1: (LOW) LibOpInv missing test for division by zero (inv(0))
- [PENDING] A25-2: (LOW) LibOpSub missing zero outputs and two outputs tests
Expand Down
4 changes: 2 additions & 2 deletions src/generated/Rainterpreter.pointers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ pragma solidity ^0.8.25;
// file needs the contract to exist so that it can be compiled.

/// @dev Hash of the known bytecode.
bytes32 constant BYTECODE_HASH = bytes32(0x4c3528ad310e82eb5cab9d310acfdf8e6035d78ea54401dc926c9803503de53b);
bytes32 constant BYTECODE_HASH = bytes32(0x66a2e1c7c62ac40c6dc3c281364370e8fd18e2cce1a2819dc38b01880cbce0fa);

/// @dev The function pointers known to the interpreter for dynamic dispatch.
/// By setting these as a constant they can be inlined into the interpreter
/// and loaded at eval time for very low gas (~100) due to the compiler
/// optimising it to a single `codecopy` to build the in memory bytes array.
bytes constant OPCODE_FUNCTION_POINTERS =
hex"08f70929094d0ac50b270b390b4b0b630b860bbc0bce0be00c810ca00db50e900f2c105d11530db511fc12d713911421143214431443145414a9158115d015e815fc1644165c1674169816ad16c516dd1725174c175e17bf180c185918a618f3190019b319d519e21a701aa11ae41b081b151b221b721ba61bb31c001c311c621caf1ce01cf81d861db21dd41e621f46";
hex"08f70929094d0ac50b270b390b4b0b630b860bbc0bce0be00c810ca00db50e900f2c105d11530db511fc12d713911421143214431443145414a9158115d015e815fc1644165c1674169816ad16c516dd1725174c175e17bf180c185918a618f3190019b319d519e21a701aa11ae41b081b151b221bb41be81bf51c421c731ca41cf11d221d3a1dc81df41e161ea41f88";
8 changes: 4 additions & 4 deletions src/lib/deploy/LibInterpreterDeploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ library LibInterpreterDeploy {

/// The address of the `Rainterpreter` contract when deployed with the rain
/// standard zoltu deployer.
address constant INTERPRETER_DEPLOYED_ADDRESS = address(0xa6c66cD36596a365DE26a649BBFCC7EfF94facdF);
address constant INTERPRETER_DEPLOYED_ADDRESS = address(0xe758FACfeb8E02bfed2d8C53B731D687DDD88A68);

/// The code hash of the `Rainterpreter` contract when deployed with the rain
/// standard zoltu deployer. This can be used to verify that the deployed
/// contract has the expected bytecode, which provides stronger guarantees
/// than just checking the address.
bytes32 constant INTERPRETER_DEPLOYED_CODEHASH =
bytes32(0x4c3528ad310e82eb5cab9d310acfdf8e6035d78ea54401dc926c9803503de53b);
bytes32(0x66a2e1c7c62ac40c6dc3c281364370e8fd18e2cce1a2819dc38b01880cbce0fa);

/// The address of the `RainterpreterExpressionDeployer` contract when
/// deployed with the rain standard zoltu deployer.
Expand All @@ -55,12 +55,12 @@ library LibInterpreterDeploy {

/// The address of the `RainterpreterDISPaiRegistry` contract when deployed
/// with the rain standard zoltu deployer.
address constant DISPAIR_REGISTRY_DEPLOYED_ADDRESS = address(0xE1F3845aa22b3Cfbe89cFAB564F6f3D590605777);
address constant DISPAIR_REGISTRY_DEPLOYED_ADDRESS = address(0x10B6e5DF88894d20d27E23Dd62Fb6741AC760676);

/// The code hash of the `RainterpreterDISPaiRegistry` contract when
/// deployed with the rain standard zoltu deployer. This can be used to
/// verify that the deployed contract has the expected bytecode, which
/// provides stronger guarantees than just checking the address.
bytes32 constant DISPAIR_REGISTRY_DEPLOYED_CODEHASH =
bytes32(0xb979c41f1d8a0911b4b4a35f05c5ba7ab4e498f06e5d313d5007fcfb9d019c69);
bytes32(0x44bdd025183eb606fb828fa64322aa81b3da6a525e5dfde49e8dbe1fbce56b4b);
}
31 changes: 22 additions & 9 deletions src/lib/op/math/LibOpGm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {IntegrityCheckState} from "../../integrity/LibIntegrityCheck.sol";
import {LibDecimalFloat, Float} from "rain.math.float/lib/LibDecimalFloat.sol";

/// @title LibOpGm
/// @notice Opcode for the geometric average of two decimal floating point
/// numbers.
/// @notice Opcode for the signed geometric mean of two decimal floating point
/// numbers. Computes `sign * sqrt(|a| * |b|)`, where the sign is negative when
/// an odd number of inputs are negative.
library LibOpGm {
using LibDecimalFloat for Float;

Expand All @@ -22,8 +23,9 @@ library LibOpGm {
return (2, 1);
}

/// @notice gm
/// decimal floating point geometric average of two numbers.
/// @notice Signed geometric mean of two decimal floating point numbers.
/// Computes `sign * sqrt(|a| * |b|)`. The result is negative when exactly
/// one input is negative, positive otherwise.
/// @param stackTop Pointer to the top of the stack.
/// @return The new stack top pointer after execution.
function run(InterpreterState memory, OperandV2, Pointer stackTop) internal view returns (Pointer) {
Expand All @@ -34,10 +36,15 @@ library LibOpGm {
stackTop := add(stackTop, 0x20)
b := mload(stackTop)
}
a = a.mul(b).pow(LibDecimalFloat.FLOAT_HALF, LibDecimalFloat.LOG_TABLES_ADDRESS);
bool aNeg = a.lt(LibDecimalFloat.FLOAT_ZERO);
bool bNeg = b.lt(LibDecimalFloat.FLOAT_ZERO);
Float result = a.abs().mul(b.abs()).pow(LibDecimalFloat.FLOAT_HALF, LibDecimalFloat.LOG_TABLES_ADDRESS);
if (aNeg != bNeg) {
result = result.minus();
}

assembly ("memory-safe") {
mstore(stackTop, a)
mstore(stackTop, result)
}
return stackTop;
}
Expand All @@ -50,12 +57,18 @@ library LibOpGm {
view
returns (StackItem[] memory)
{
// The geometric mean is sqrt(a * b).
// The geometric mean is sign * sqrt(|a| * |b|), where sign is
// negative if an odd number of inputs are negative.
Float a = Float.wrap(StackItem.unwrap(inputs[0]));
Float b = Float.wrap(StackItem.unwrap(inputs[1]));
a = a.mul(b).pow(LibDecimalFloat.FLOAT_HALF, LibDecimalFloat.LOG_TABLES_ADDRESS);
bool aNeg = a.lt(LibDecimalFloat.FLOAT_ZERO);
bool bNeg = b.lt(LibDecimalFloat.FLOAT_ZERO);
Float result = a.abs().mul(b.abs()).pow(LibDecimalFloat.FLOAT_HALF, LibDecimalFloat.LOG_TABLES_ADDRESS);
if (aNeg != bNeg) {
result = result.minus();
}
StackItem[] memory outputs = new StackItem[](1);
outputs[0] = StackItem.wrap(Float.unwrap(a));
outputs[0] = StackItem.wrap(Float.unwrap(result));
return outputs;
}
}
121 changes: 121 additions & 0 deletions test/src/lib/op/00/LibOpExtern.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,127 @@ contract LibOpExternTest is OpTest {
LibOpExtern.run(state, operand, stackTop);
}

/// Exposed externally so mocks and reverts play nice with referenceFn.
function externalReferenceFn(InterpreterState memory state, OperandV2 operand, StackItem[] memory inputs)
external
view
returns (StackItem[] memory)
{
return LibOpExtern.referenceFn(state, operand, inputs);
}

/// Test that `referenceFn` reverts with `BadOutputsLength` when the extern
/// returns fewer outputs than the operand specifies.
function testOpExternReferenceFnBadOutputsLength(
IInterpreterExternV4 extern,
bytes32[] memory constants,
uint16 constantIndex,
StackItem[] memory inputs,
StackItem[] memory outputs
) external {
vm.assume(constants.length > 0);
if (inputs.length > 0x0F) {
uint256[] memory inputsCopy;
assembly ("memory-safe") {
inputsCopy := inputs
}
inputsCopy.truncate(0x0F);
}
if (outputs.length > 0x0F) {
uint256[] memory outputsCopy;
assembly ("memory-safe") {
outputsCopy := outputs
}
outputsCopy.truncate(0x0F);
}
// Need at least 1 output so we can return a mismatched count.
vm.assume(outputs.length > 0);

InterpreterState memory state = opTestDefaultInterpreterState();
state.constants = constants;

assumeEtchable(address(extern));
vm.etch(address(extern), hex"fe");
mockImplementsERC165IInterpreterExternV4(extern);

constantIndex = uint16(bound(constantIndex, 0, state.constants.length - 1));

OperandV2 operand = LibOperand.build(uint8(inputs.length), uint8(outputs.length), constantIndex);
ExternDispatchV2 externDispatch = LibExtern.encodeExternDispatch(0, operand);
EncodedExternDispatchV2 encodedExternDispatch = LibExtern.encodeExternCall(extern, externDispatch);
state.constants[constantIndex] = EncodedExternDispatchV2.unwrap(encodedExternDispatch);

// Mock extern to return one fewer output than expected.
StackItem[] memory badOutputs = new StackItem[](outputs.length - 1);
for (uint256 i = 0; i < badOutputs.length; i++) {
badOutputs[i] = outputs[i];
}
vm.mockCall(
address(extern),
abi.encodeWithSelector(IInterpreterExternV4.extern.selector, externDispatch),
abi.encode(badOutputs)
);

vm.expectRevert(abi.encodeWithSelector(BadOutputsLength.selector, outputs.length, badOutputs.length));
this.externalReferenceFn(state, operand, inputs);
}

/// Test that `referenceFn` reverts with `BadOutputsLength` when the extern
/// returns more outputs than the operand specifies.
function testOpExternReferenceFnBadOutputsLengthTooMany(
IInterpreterExternV4 extern,
bytes32[] memory constants,
uint16 constantIndex,
StackItem[] memory inputs,
StackItem[] memory outputs
) external {
vm.assume(constants.length > 0);
if (inputs.length > 0x0F) {
uint256[] memory inputsCopy;
assembly ("memory-safe") {
inputsCopy := inputs
}
inputsCopy.truncate(0x0F);
}
// Cap outputs to 0x0E so we can add one more.
if (outputs.length > 0x0E) {
uint256[] memory outputsCopy;
assembly ("memory-safe") {
outputsCopy := outputs
}
outputsCopy.truncate(0x0E);
}

InterpreterState memory state = opTestDefaultInterpreterState();
state.constants = constants;

assumeEtchable(address(extern));
vm.etch(address(extern), hex"fe");
mockImplementsERC165IInterpreterExternV4(extern);

constantIndex = uint16(bound(constantIndex, 0, state.constants.length - 1));

OperandV2 operand = LibOperand.build(uint8(inputs.length), uint8(outputs.length), constantIndex);
ExternDispatchV2 externDispatch = LibExtern.encodeExternDispatch(0, operand);
EncodedExternDispatchV2 encodedExternDispatch = LibExtern.encodeExternCall(extern, externDispatch);
state.constants[constantIndex] = EncodedExternDispatchV2.unwrap(encodedExternDispatch);

// Mock extern to return one more output than expected.
StackItem[] memory extraOutputs = new StackItem[](outputs.length + 1);
for (uint256 i = 0; i < outputs.length; i++) {
extraOutputs[i] = outputs[i];
}

vm.mockCall(
address(extern),
abi.encodeWithSelector(IInterpreterExternV4.extern.selector, externDispatch),
abi.encode(extraOutputs)
);

vm.expectRevert(abi.encodeWithSelector(BadOutputsLength.selector, outputs.length, extraOutputs.length));
this.externalReferenceFn(state, operand, inputs);
}

/// Test the eval of extern directly.
function testOpExternRunHappy(
IInterpreterExternV4 extern,
Expand Down
20 changes: 20 additions & 0 deletions test/src/lib/op/logic/LibOpGreaterThanOrEqualTo.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,26 @@ contract LibOpGreaterThanOrEqualToTest is OpTest {
checkHappy("_: greater-than-or-equal-to(1 1);", bytes32(uint256(1)), "");
}

/// Test 1.1 >= 1.2, which should return 0.
function testOpGreaterThanOrEqualToEvalOnePointOneGteOnePointTwo() external view {
checkHappy("_: greater-than-or-equal-to(1.1 1.2);", 0, "");
}

/// Test 1.0 >= 1, which should return 1 (equal).
function testOpGreaterThanOrEqualToEvalOnePointZeroGteOne() external view {
checkHappy("_: greater-than-or-equal-to(1.0 1);", bytes32(uint256(1)), "");
}

/// Test -1.1 >= -1.2, which should return 1.
function testOpGreaterThanOrEqualToEvalNegOnePointOneGteNegOnePointTwo() external view {
checkHappy("_: greater-than-or-equal-to(-1.1 -1.2);", bytes32(uint256(1)), "");
}

/// Test -1 >= 0, which should return 0.
function testOpGreaterThanOrEqualToEvalNegOneGteZero() external view {
checkHappy("_: greater-than-or-equal-to(-1 0);", 0, "");
}

/// Test that a greater than or equal to without inputs fails integrity check.
function testOpGreaterThanOrEqualToEvalFail0Inputs() public {
vm.expectRevert(abi.encodeWithSelector(BadOpInputsLength.selector, 0, 2, 0));
Expand Down
20 changes: 20 additions & 0 deletions test/src/lib/op/logic/LibOpLessThanOrEqualTo.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,26 @@ contract LibOpLessThanOrEqualToTest is OpTest {
assertEq(kvs.length, 0);
}

/// Test 1.1 <= 1.2, which should return 1.
function testOpLessThanOrEqualToEvalOnePointOneLteOnePointTwo() external view {
checkHappy("_: less-than-or-equal-to(1.1 1.2);", bytes32(uint256(1)), "");
}

/// Test 1.0 <= 1, which should return 1 (equal).
function testOpLessThanOrEqualToEvalOnePointZeroLteOne() external view {
checkHappy("_: less-than-or-equal-to(1.0 1);", bytes32(uint256(1)), "");
}

/// Test -1.1 <= -1.2, which should return 0.
function testOpLessThanOrEqualToEvalNegOnePointOneLteNegOnePointTwo() external view {
checkHappy("_: less-than-or-equal-to(-1.1 -1.2);", 0, "");
}

/// Test -1 <= 0, which should return 1.
function testOpLessThanOrEqualToEvalNegOneLteZero() external view {
checkHappy("_: less-than-or-equal-to(-1 0);", bytes32(uint256(1)), "");
}

/// Test that a less than or equal to without inputs fails integrity check.
function testOpLessThanOrEqualToEvalFail0Inputs() public {
vm.expectRevert(abi.encodeWithSelector(BadOpInputsLength.selector, 0, 2, 0));
Expand Down
7 changes: 6 additions & 1 deletion test/src/lib/op/math/LibOpE.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd
pragma solidity =0.8.25;

import {OpTest} from "test/abstract/OpTest.sol";
import {OpTest, UnexpectedOperand} from "test/abstract/OpTest.sol";
import {InterpreterState} from "src/lib/state/LibInterpreterState.sol";
import {LibOpE} from "src/lib/op/math/LibOpE.sol";
import {LibOperand, OperandV2} from "test/lib/operand/LibOperand.sol";
Expand Down Expand Up @@ -74,4 +74,9 @@ contract LibOpETest is OpTest {
function testOpEEvalTwoOutputs() external {
checkBadOutputs("_ _: e();", 0, 1, 2);
}

/// Test that operand is disallowed.
function testOpEEvalOperandDisallowed() external {
checkUnhappyParse("_: e<0>();", abi.encodeWithSelector(UnexpectedOperand.selector));
}
}
7 changes: 7 additions & 0 deletions test/src/lib/op/math/LibOpExp.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ contract LibOpExpTest is OpTest {
);
}

/// Test exp with negative input. exp(-1) = 1/e ≈ 0.3678794...
function testOpExpEvalNegativeInput() external view {
(Float expected,) =
LibDecimalFloat.packLossy(3678794411714423215955237701614608674458111310317678345078368016974, -67);
checkHappy("_: exp(-1);", Float.unwrap(expected), "e^(-1)");
}

/// Test the eval of `exp` for bad inputs.
function testOpExpEvalZeroInputs() external {
checkBadInputs("_: exp();", 0, 1, 0);
Expand Down
7 changes: 7 additions & 0 deletions test/src/lib/op/math/LibOpExp2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ contract LibOpExp2Test is OpTest {
checkHappy("_: exp2(3);", Float.unwrap(LibDecimalFloat.packLossless(8000, -3)), "2^3");
}

/// Test exp2 with negative input. exp2(-1) = 0.5.
function testOpExp2EvalNegativeInput() external view {
(Float expected,) =
LibDecimalFloat.packLossy(5000000000000000000000000000000000000000000000000000000000000000000, -67);
checkHappy("_: exp2(-1);", Float.unwrap(expected), "2^(-1)");
}

/// Test the eval of `exp2` for bad inputs.
function testOpExp2EvalBad() external {
checkBadInputs("_: exp2();", 0, 1, 0);
Expand Down
Loading