Skip to content

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

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