Skip to content

fix: Twap.isValid() never validates the derived per-part amounts - #979

Open
gomesalexandre wants to merge 1 commit into
cowprotocol:mainfrom
gomesalexandre:r6-cow-sdk-twap-isvalid
Open

fix: Twap.isValid() never validates the derived per-part amounts#979
gomesalexandre wants to merge 1 commit into
cowprotocol:mainfrom
gomesalexandre:r6-cow-sdk-twap-isvalid

Conversation

@gomesalexandre

@gomesalexandre gomesalexandre commented Sep 1, 2026

Copy link
Copy Markdown

Twap.isValid() checks the aggregate sellAmount/buyAmount but never the derived per-part partSellAmount/minPartLimit (floor division by numberOfParts, computed once in the base constructor via transformDataToStruct and stored on this.staticInput). The on-chain TWAPOrder handler validates the derived quantities directly and reverts when either is zero:

// composable-cow, TWAPOrder.sol
if (!(self.partSellAmount > 0)) revert IConditionalOrder.OrderNotValid(INVALID_PART_SELL_AMOUNT);
if (!(self.minPartLimit  > 0)) revert IConditionalOrder.OrderNotValid(INVALID_MIN_PART_LIMIT);

Concrete failing case: { sellAmount: 90n, numberOfParts: 100n }partSellAmount = 90n / 100n = 0n (BigInt floor division). The SDK's isValid() reports the order valid, but the on-chain handler reverts on every attempt.

ConditionalOrder.poll() gates on isValid() for its DONT_TRY_AGAIN short-circuit (packages/composable/src/ConditionalOrder.ts:264-269), so an order built this way never gets that clean signal — it just keeps failing against the contract instead of being recognized as invalid up front.

Fix

Add the two missing checks in isValid(), mirroring the Solidity, reusing the value already computed in the constructor (this.staticInput.partSellAmount / .minPartLimit) rather than re-deriving it.

Receipts

New unit tests fail on the pre-fix code and pass after:

$ git stash push -- packages/composable/src/orderTypes/Twap.ts   # revert only the fix
$ npx jest -t "floors to zero"
✕ should invalidate a per-part sell amount that floors to zero across all adapters
  - "isValid": false, "reason": "InvalidPartSellAmount"
  + "isValid": true
✕ should invalidate a per-part min limit that floors to zero across all adapters
  - "isValid": false, "reason": "InvalidMinPartLimit"
  + "isValid": true
Tests: 8 failed, 275 skipped, 283 total

$ git stash pop   # restore the fix
$ npx jest
Test Suites: 9 passed, 9 total
Tests:       283 passed, 283 total

Lint (npx eslint src/orderTypes/Twap.ts) and tsc --noEmit both clean.

Adversarially reviewed with Codex (GPT-5.6) against the diff directly: verdict SHIP, no blocking findings — confirmed staticInput reflects the correct floor-divided values for every construction path (fromData, raw constructor, deserialize), confirmed no existing valid fixture regresses, and confirmed check ordering can't produce a false result. It also flagged a separate, pre-existing, out-of-scope concern (case-sensitive same-token address comparison in the InvalidSameToken check) — noting here for visibility, not fixing it in this PR to keep the diff focused.

Summary by CodeRabbit

  • Bug Fixes

    • Improved TWAP order validation to reject orders whose per-part sell or minimum buy amounts round down to zero.
    • Prevents invalid orders from being submitted when their aggregate amounts appear valid but individual parts cannot execute.
  • Tests

    • Added coverage for zero-value per-part sell and buy amounts caused by rounding.

sellAmount/numberOfParts and buyAmount/numberOfParts are floor-divided in
transformDataToStruct to derive partSellAmount/minPartLimit. isValid() only
checked the aggregate sellAmount/buyAmount, never the derived per-part
values, so an order like {sellAmount: 90n, numberOfParts: 100n} passes SDK
validation while the on-chain TWAPOrder handler reverts on a zero
partSellAmount/minPartLimit on every poll attempt. Since poll() gates on
isValid() for its DONT_TRY_AGAIN short-circuit, such an order never fills
and never cleanly retires.

Mirrors the on-chain check (composable-cow TWAPOrder.sol) by validating
this.staticInput.partSellAmount/minPartLimit are non-zero, using the value
already computed in the constructor rather than re-deriving it.
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: b7a975f2-40c9-4f45-9ec5-075f18f96a34

📥 Commits

Reviewing files that changed from the base of the PR and between 455e44b and 44cab8f.

📒 Files selected for processing (2)
  • packages/composable/src/orderTypes/Twap.ts
  • packages/composable/tests/Twap.spec.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

TWAP validation now rejects orders when floor division produces zero per-part sell or buy amounts. Cross-adapter tests cover both validation errors.

Changes

TWAP validation

Layer / File(s) Summary
Per-part amount checks
packages/composable/src/orderTypes/Twap.ts, packages/composable/tests/Twap.spec.ts
Twap.isValid() returns InvalidPartSellAmount or InvalidMinPartLimit when derived per-part amounts are zero. Tests verify both errors across adapters.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 44cab

The change rejects TWAP orders whose derived per-part amounts are zero, matching the existing contract behavior and preventing repeated failed attempts; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding validation for derived per-part amounts in Twap.isValid().
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 2 files.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/composable/src/orderTypes/Twap.ts

Parsing error: Unable to parse the specified 'tsconfig' file. Ensure it's correct and has valid syntax.

packages/app-data/tsconfig.json(2,14): error TS6053: File '@cow-sdk/typescript-config/base.json' not found.

packages/composable/tests/Twap.spec.ts

Parsing error: Unable to parse the specified 'tsconfig' file. Ensure it's correct and has valid syntax.

packages/app-data/tsconfig.json(2,14): error TS6053: File '@cow-sdk/typescript-config/base.json' not found.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gomesalexandre
gomesalexandre marked this pull request as ready for review September 1, 2026 12:04
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.

1 participant