Skip to content

Fix haircut mismatch between reported and on-chain amounts#4109

Merged
squadgazzz merged 8 commits intomainfrom
fix-haircut-logic-2
Feb 2, 2026
Merged

Fix haircut mismatch between reported and on-chain amounts#4109
squadgazzz merged 8 commits intomainfrom
fix-haircut-logic-2

Conversation

@squadgazzz
Copy link
Copy Markdown
Contributor

@squadgazzz squadgazzz commented Jan 30, 2026

Description

Fixes the mismatch between driver-reported amounts and on-chain executed amounts when the haircut is configured. Previously, the driver reported higher buy amounts than users actually received on-chain (for sell orders), resulting in a discrepancy that matched the configured haircut.

Root cause: sell_amount() and buy_amount() did NOT include haircut, but custom_prices() (used for on-chain encoding) DID. This caused reported amounts to differ from on-chain execution.

Changes

Include haircut effects in sell_amount() and buy_amount() so that:

  • Reported amounts include haircut
  • On-chain execution matches reported amounts
  • Autopilot scores based on actual (haircutted) amounts

For sell orders:

  • sell_amount() → unchanged (user sells exactly what they signed)
  • buy_amount() → reduced by haircut (user receives less)

For buy orders:

  • sell_amount() → increased by haircut (user pays more)
  • buy_amount() → unchanged (user receives exactly what they signed for)

How to test

Adjusted existing tests.

@squadgazzz squadgazzz marked this pull request as ready for review January 30, 2026 13:25
@squadgazzz squadgazzz requested a review from a team as a code owner January 30, 2026 13:25
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly addresses the mismatch between reported and on-chain amounts when a haircut is configured by moving the haircut logic into the sell_amount() and buy_amount() functions. This ensures consistency. The tests have been updated appropriately to validate the new behavior. I have one piece of feedback regarding error handling, specifically the use of the correct error variant for arithmetic underflow.

Comment thread crates/driver/src/domain/competition/solution/trade.rs Outdated
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Comment thread crates/e2e/tests/e2e/limit_orders.rs Outdated
Copy link
Copy Markdown
Contributor

@MartinquaXD MartinquaXD left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change looks reasonable but given that the other changes also looked reasonable and then resulted in the circuit breaker deny-listing the solver could you please also manually test this on staging before merging?

Other than that only minor nits.

Comment on lines +252 to +253
// Base buy amount from executed sell
let base = self
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since both computations do the same logic you could simplify like this, no?:

self.executed.0
    .checked_sub(self.haircut_fee)
    .ok_or(Math::Negative)?
    .checked_mul(prices.sell)
    .ok_or(Math::Overflow)?
    .checked_div(prices.buy
    .ok_or(Math::DivisionByZero)?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, the original logic uses different rounding for the base amount vs. the fee:

let base = ceil(executed * sell / buy);
let haircut_in_buy = floor(haircut * sell / buy);
let result = base - haircut_in_buy;

The proposed simplification:

let result = (executed - haircut) * sell / buy;

These are not mathematically equivalent.

Concrete example:

  • executed = 11, haircut = 4, sell = 2, buy = 5

Original:

  • base = ceil(11 × 2 / 5) = ceil(22/5) = ceil(4.4) = 5
  • haircut_in_buy = floor(4 × 2 / 5) = floor(8/5) = floor(1.6) = 1
  • result = 5 - 1 = 4

Simplified (even with ceil):

  • result = ceil((11 - 4) × 2 / 5) = ceil(14/5) = ceil(2.8) = 3

Comment thread crates/driver/src/domain/competition/solution/trade.rs
Comment thread crates/driver/src/tests/cases/haircut.rs Outdated
Comment thread crates/driver/src/tests/cases/haircut.rs Outdated
@squadgazzz
Copy link
Copy Markdown
Contributor Author

Change looks reasonable but given that the other changes also looked reasonable and then resulted in the circuit breaker deny-listing the solver could you please also manually test this on staging before merging?

Yep, it was already tested: https://cowservices.slack.com/archives/C0361CDD1FZ/p1769791530558949

@squadgazzz squadgazzz added the hotfix Labels PRs that should be applied into production right away label Feb 2, 2026
@squadgazzz squadgazzz enabled auto-merge February 2, 2026 15:04
@squadgazzz squadgazzz added this pull request to the merge queue Feb 2, 2026
Merged via the queue into main with commit 77bbbdf Feb 2, 2026
19 checks passed
@squadgazzz squadgazzz deleted the fix-haircut-logic-2 branch February 2, 2026 15:22
@github-actions github-actions bot locked and limited conversation to collaborators Feb 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

hotfix Labels PRs that should be applied into production right away

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants