Skip to content

Bug: _deriveReceivedItemsHash() double-floor precision loss breaks Substandard 6 partial fills #294

@kokman092

Description

@kokman092

Summary

_deriveReceivedItemsHash() in both ImmutableSignedZoneV2 and ImmutableSignedZoneV3 uses Math.mulDiv() (floor division) to reconstruct the original consideration amount from a partial fill. Because Seaport also floors when scaling down, two consecutive floor operations produce an irreversible rounding error — causing Substandard6Violation reverts on 90%+ of partial fill ratios.

Affected Files

  • contracts/trading/seaport/zones/immutable-signed-zone/v2/ImmutableSignedZoneV2.sol (L565-585)
  • contracts/trading/seaport16/zones/immutable-signed-zone/v3/ImmutableSignedZoneV3.sol (same function)
  • Deployed at: 0x1004f9615E79462c711Ff05a386BdbA91a7628C3 (Immutable zkEVM)

Root Cause

For a partial fill of 33/100 with original consideration = 10:

  1. Seaport floors DOWN: floor(10 × 33 / 100) = 3
  2. Zone floors DOWN again: Math.mulDiv(3, 100, 33) = 9 (should be 10)
  3. Strict equality: hash(9) ≠ hash(10)Substandard6Violation

Reproduction

forge init poc && cd poc
# Save test file below as test/Sub6.t.sol
forge test -vv --fork-url https://rpc.immutable.com
// test/Sub6.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";

contract Sub6Test is Test {
    function test_doublFloor() public pure {
        uint256 originalOffer = 100;
        uint256 originalCons = 10;
        uint256 failCount = 0;
        for (uint256 f = 1; f < 100; f++) {
            uint256 actual = (originalCons * f) / originalOffer;
            uint256 reconstructed = (actual * originalOffer) / f;
            if (reconstructed != originalCons) failCount++;
        }
        assertEq(failCount, 90); // 90/99 fills produce wrong result
    }
}

Impact

  • 90/99 (90.9%) of partial fill ratios revert for a 100/10 order
  • 999/999 (100%) revert for a 1000/7 order (realistic game asset scenario)
  • Substandard 6 is designed for "best-efforts partial fulfilment scenarios" but fails for nearly all of them

Suggested Fix

Replace floor division with ceiling:

- Math.mulDiv(receivedItems[i].amount, scalingFactorNumerator, scalingFactorDenominator)
+ Math.mulDiv(receivedItems[i].amount, scalingFactorNumerator, scalingFactorDenominator, Math.Rounding.Ceil)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions