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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions