Skip to content

fix: scale remaining order amounts exactly, not through basis points - #991

Open
midasbal wants to merge 1 commit into
ProjectOpenSea:mainfrom
midasbal:fix-remaining-amount-basis-points-rounding
Open

fix: scale remaining order amounts exactly, not through basis points#991
midasbal wants to merge 1 commit into
ProjectOpenSea:mainfrom
midasbal:fix-remaining-amount-basis-points-rounding

Conversation

@midasbal

Copy link
Copy Markdown

Problem

mapOrderAmountsFromFilledStatus and mapTipAmountsFromFilledStatus (src/utils/order.ts) compute the remaining-fill fraction by rounding it to basis points first, then multiplying:

const basisPoints = ((totalSize - totalFilled) * ONE_HUNDRED_PERCENT_BP) / totalSize
...
startAmount: multiplyBasisPoints(item.startAmount, basisPoints).toString(),

This is a double-rounded approximation. Basis points only have 1/10000 granularity, so whenever totalSize does not divide evenly into 10000 the result is wrong, not just imprecise.

Concretely: a 9 ETH ERC1155 order split into 3 units, with 1 unit already filled by another fulfiller, leaves a remaining fraction of 2/3. 2/3 as basis points floors to 6666 (not 6666.67), and multiplying back gives 5.9994 ETH instead of the true 6 ETH. This function runs on the default fulfillOrder path, i.e. every call that does not pass an explicit unitsToFill, so it feeds directly into the built transaction's value for native-priced orders. The result is a transaction that reverts with InsufficientNativeTokensSupplied for an ordinary partial fill, on a pre-flight-checked call that should have succeeded.

I confirmed against seaport-core that the fill fraction itself is exact or revert on chain: AmountDeriver._getFraction reverts with InexactFraction() on any remainder, so there is no legitimate case where the true remaining amount is anything other than an exact integer. The bug is not that the SDK rounds in the wrong direction, it is that it computes the wrong exact integer via an unnecessary lossy detour.

Fix

Replace the basis-points detour with the same single exact division the sibling functions already use. multiplyDivision already exists in this file, added by #197:

const multiplyDivision = (amount, numerator, denominator) =>
  (BigInt(amount) * BigInt(numerator)) / BigInt(denominator)

That fix (#197, closing #141 and #185, the exact same InsufficientEtherSupplied-on-partial-fill symptom this bug produces) applied multiplyDivision to mapOrderAmountsFromUnitsToFill and mapTipAmountsFromUnitsToFill. It never reached mapOrderAmountsFromFilledStatus or mapTipAmountsFromFilledStatus, their neighbors in the same file, which is why the same bug class is still here on the default fill path. This PR finishes applying that fix to the two functions it missed. multiplyBasisPoints is unchanged and still used correctly by feeToConsiderationItem and deductFees.

Validation

  • npm run build
  • npm run lint
  • npm run format:check
  • npm run test (245 passing, up from 243)
  • npm run coverage

New regression test in test/fulfill-remaining-default-amount.spec.ts: a 9 ETH / 3 unit ERC1155 order, 1 unit taken by a first fulfiller, fulfilled for the remainder by a second fulfiller via the default path (no unitsToFill). Asserts the built transaction's value equals the exact remaining amount (6 ETH), and, for the ERC20-priced equivalent with exactApproval, that the resulting approve() sets the exact allowance rather than the basis-points approximation. Both assertions fail against unmodified main with the exact 5999400000000000000 vs 6000000000000000000 mismatch described above, confirming they catch the bug.

mapOrderAmountsFromFilledStatus and mapTipAmountsFromFilledStatus computed
the remaining-fill fraction by first rounding it to basis points and then
multiplying, which loses precision whenever totalSize does not divide
evenly into 10000. On a 9 ETH order split into 3 units with 1 already
filled, the remaining 2/3 rounds to 6666bp instead of 6666.67bp, so the
default fulfillOrder path (no unitsToFill passed) built a transaction
with 5.9994 ETH instead of the 6 ETH the contract actually requires,
reverting with InsufficientNativeTokensSupplied.

PR ProjectOpenSea#197 fixed this same bug in the sibling functions
mapOrderAmountsFromUnitsToFill and mapTipAmountsFromUnitsToFill (issues
ProjectOpenSea#141 and ProjectOpenSea#185), replacing the basis points detour with a single exact
division via multiplyDivision. That fix never reached
mapOrderAmountsFromFilledStatus or mapTipAmountsFromFilledStatus, which
still use the old approximation on every default fill.

This reuses the existing multiplyDivision helper in both functions
instead of multiplyBasisPoints, matching the already-accepted pattern.
Verified against seaport-core that the fill fraction is exact or revert
on chain (_getFraction reverts InexactFraction on any remainder), so a
single floored division is correct for both offer and consideration
items with no rounding direction split needed.
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.07%. Comparing base (f618d19) to head (6cb19dd).
⚠️ Report is 376 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #991      +/-   ##
==========================================
+ Coverage   98.27%   99.07%   +0.79%     
==========================================
  Files          35       57      +22     
  Lines       14526    20235    +5709     
  Branches      660     1120     +460     
==========================================
+ Hits        14276    20047    +5771     
+ Misses        245      180      -65     
- Partials        5        8       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants