Skip to content

[AUD-029] [INFO] Signed conversion has no slow-reference differential fuzz (unsigned has LibParseDecimalSlow) #99

Description

@thedavidmeister

Whole-repo audit 2026-08-24 at commit 3e76bab (audit skill 0.35.0). Pass 2 status: ledgered (audit/mutation-test-scans.json, scan of 2026-08-24T08:55:39Z at 35496d8).

test/src/lib/parse/LibParseDecimal.unsafeDecimalStringToSignedInt.t.sol:17-255

The unsigned conversion has a spec-derived slow reference (LibParseDecimalSlow) fuzzed over arbitrary and digit-dense bytes, but the signed conversion has none — its coverage is a targeted branch matrix (all branches are individually discriminated: sign consumption, both overflow boundaries including int256 min, error propagation for all three selectors, lone '-', '+', '--'). The asymmetry means a future edit to the signed wrapper is protected only by the enumerated cases, not by an oracle over arbitrary input.

Proposed fix

Extend test/lib/parse/LibParseDecimalSlow.sol with a signed reference and fuzz it from the existing signed test contract:

function decimalStringToSignedIntSlow(bytes memory data) internal pure returns (bytes4, int256) {
    if (data.length == 0) {
        return (ParseEmptyDecimalString.selector, 0);
    }
    bool isNeg = data[0] == "-";
    bytes memory digits = data;
    if (isNeg) {
        digits = new bytes(data.length - 1);
        for (uint256 i = 0; i < digits.length; i++) {
            digits[i] = data[i + 1];
        }
    }
    (bytes4 errorSelector, uint256 value) = decimalStringToIntSlow(digits);
    if (errorSelector != bytes4(0)) {
        return (errorSelector, 0);
    }
    if (isNeg) {
        if (value > uint256(type(int256).max) + 1) {
            return (ParseDecimalOverflow.selector, 0);
        }
        if (value == uint256(type(int256).max) + 1) {
            return (bytes4(0), type(int256).min);
        }
        // forge-lint: disable-next-line(unsafe-typecast)
        return (bytes4(0), -int256(value));
    }
    if (value > uint256(type(int256).max)) {
        return (ParseDecimalOverflow.selector, 0);
    }
    // forge-lint: disable-next-line(unsafe-typecast)
    return (bytes4(0), int256(value));
}

// In TestLibParseDecimalUnsafeDecimalStringToSignedInt:
function testUnsafeStrToSignedIntReference(bytes memory data) external pure {
    (bytes4 slowSelector, int256 slowValue) = LibParseDecimalSlow.decimalStringToSignedIntSlow(data);
    (bytes4 errorSelector, int256 result) = LibParseDecimal.unsafeDecimalStringToSignedInt(
        Pointer.unwrap(data.dataPointer()), Pointer.unwrap(data.endDataPointer())
    );
    assertEq(errorSelector, slowSelector);
    assertEq(result, slowValue);
}

Metadata

Metadata

Labels

auditAudit findinginfoAudit findingpass2Audit finding

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions