Skip to content

fix(slippage): fold taker fees into displayed fill price#62

Merged
lukemacauley merged 2 commits into
mainfrom
fix/slippage-fee-inclusive
Jul 9, 2026
Merged

fix(slippage): fold taker fees into displayed fill price#62
lukemacauley merged 2 commits into
mainfrom
fix/slippage-fee-inclusive

Conversation

@lukemacauley

@lukemacauley lukemacauley commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Problem

simulateTakerSwap walks the AMM curve to produce fillPrice but never applies pool fees, so the "Est %" price impact shown in the trade UI understates the true cost — real fills are worse by the fee bps on every trade (a long pays more USD, a short receives less). Users report execution consistently slips more than the estimate.

Fix

simulateTakerSwap gains an optional feeRate (total taker fee as a fraction) and its result now carries, alongside the unchanged usdDelta/fillPrice/exceedsLiquidity:

  • feeRate — the fee applied
  • effectiveUsdDelta — fee-inclusive signed USD (true cash flow)
  • effectiveFillPrice — fee-inclusive per-unit cost (the price the UI should display)
  • liquidityLimited — heuristic flag that the single-region approximation likely understates impact (trade would cross an initialized tick)

Direction is side-signed on the USD leg: long cost raw*(1+feeRate), short proceeds raw*(1-feeRate). Total taker fee = creatorFee + insuranceFee + lpFee (new totalTakerFeeRate helper; protocolFee is a redistribution cut, not additional trader cost). When feeRate is omitted, effective fields equal raw — every existing caller is unaffected, and calculateTakerSlippageLimit / the amt1Limit money path is untouched.

Single-region liquidity caveat

The sim only has current active liquidity, not the full tick map, so it overstates depth once a trade crosses an initialized tick. Now documented precisely and surfaced via liquidityLimited (set past APPROX_TICK_CROSS_WARN_FRACTION, default ~4% price move; forced true in the exceedsLiquidity branch).

Tests / checks

pnpm run ci green: build (tsup), tsc --noEmit, biome lint, 279 unit tests (8 new — proving effective fill is worse than raw by exactly the fee bps for long+short, and raw impact unchanged).

Follow-up (separate PRs)

  • Publish 0.15.0 (minor, additive).
  • perpcity-client: bump SDK pin, pass feeRate: totalTakerFeeRate(perpData.fees) and read effectiveFillPrice in useTakerOrder.ts / TakerClose.ts; optionally wire liquidityLimited to a warning in OrderSummary.tsx.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Taker quote results now include total fee rates plus fee-inclusive effective USD deltas and fill prices.
    • Raw (fee-exclusive) pricing remains available separately.
    • Quote results now expose a liquidityLimited indicator, alongside existing liquidity/exceeds-liquidity behavior.
    • Fee handling is applied consistently across normal execution and zero-liquidity/fallback paths, and invalid (negative/non-finite) fee rates are rejected.
  • Tests
    • Expanded swap/adjust unit coverage for fee effects and liquidity-limiting scenarios.
  • Chores
    • Bumped package version to 0.15.0.

simulateTakerSwap walked the AMM curve but never applied the pool fees
(creator + insurance + LP), so the "Est %" price impact the UI shows
understated the true cost — real fills are worse by the fee bps on every
trade (a long pays more USD, a short receives less).

- simulateTakerSwap now takes an optional feeRate and returns fee-inclusive
  effectiveFillPrice / effectiveUsdDelta alongside the raw curve fillPrice /
  usdDelta, so callers can still separate price impact from fees. Direction is
  side-signed: long *(1+feeRate), short *(1-feeRate).
- Add totalTakerFeeRate(fees) = creatorFee + insuranceFee + lpFee; wire it into
  estimateTakerPosition / estimateTakerAdjust.
- Add a liquidityLimited flag: the single-region (constant-liquidity) sim
  understates impact once a trade crosses an initialized tick; flag when the
  swap moves the pool price past APPROX_TICK_CROSS_WARN_FRACTION so the UI can
  warn. Precise caveat documented in swap.ts.
- Fields are additive; raw fillPrice/usdDelta/exceedsLiquidity and
  calculateTakerSlippageLimit behavior are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Taker swap simulation now aggregates and applies normal taker fees, exposes fee-inclusive quote fields, and reports potential liquidity limits. Position and adjustment estimators propagate these values, with tests covering fee effects, invalid rates, fallback behavior, and liquidity scenarios.

Changes

Taker quote fee modeling

Layer / File(s) Summary
Fee aggregation and result contracts
src/utils/fees.ts, src/utils/swap.ts, src/types/entity-data.ts
Normal taker fees are summed from creator, insurance, and LP fees; quote result types now include fee rates, effective pricing, and liquidity indicators.
Fee-aware swap simulation
src/utils/swap.ts, src/__tests__/unit/swap.test.ts
simulateTakerSwap validates and applies fees across normal, fallback, and exhausted-liquidity paths, and calculates liquidityLimited from price movement.
Estimate integration and validation
src/functions/perp-actions.ts, src/__tests__/unit/adjust-primitives.test.ts, package.json
Position and adjustment estimates pass aggregated fees into swap simulation and return raw and fee-inclusive quote values; buy and sell fee effects are tested, and the package version is updated to 0.15.0.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EstimateTakerAdjust
  participant TotalTakerFeeRate
  participant SimulateTakerSwap
  EstimateTakerAdjust->>TotalTakerFeeRate: Aggregate perpData.fees
  TotalTakerFeeRate-->>EstimateTakerAdjust: Return feeRate
  EstimateTakerAdjust->>SimulateTakerSwap: Simulate swap with feeRate
  SimulateTakerSwap-->>EstimateTakerAdjust: Return raw and effective quote fields
Loading

Possibly related PRs

Suggested reviewers: koko1123

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the core change: taker fees are folded into displayed fill prices for slippage calculations.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/slippage-fee-inclusive

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

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/swap.ts`:
- Around line 137-152: Bound feeRate to the valid range in simulateTakerSwap:
update its validation to reject values greater than or equal to 1, alongside
negative and non-finite values. Keep the existing feeScaled and feeMultiplier
calculations unchanged, and ensure the thrown validation error clearly states
that feeRate must be finite, non-negative, and less than 1.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c37b5a89-b628-401d-a0b0-70c20923f0fa

📥 Commits

Reviewing files that changed from the base of the PR and between b112164 and 268de04.

📒 Files selected for processing (6)
  • src/__tests__/unit/adjust-primitives.test.ts
  • src/__tests__/unit/swap.test.ts
  • src/functions/perp-actions.ts
  • src/types/entity-data.ts
  • src/utils/fees.ts
  • src/utils/swap.ts

Comment thread src/utils/swap.ts
Comment on lines 137 to +152
export function simulateTakerSwap(opts: {
sqrtPriceX96: bigint;
liquidity: bigint;
perpDelta: bigint;
markPrice: number;
feeRate?: number;
}): SimulatedTakerSwap {
const { sqrtPriceX96, liquidity, perpDelta, markPrice } = opts;
const feeRate = opts.feeRate ?? 0;
if (!Number.isFinite(feeRate) || feeRate < 0) {
throw new Error("feeRate must be a non-negative, finite number");
}
const feeScaled = BigInt(Math.round(feeRate * Number(FEE_SCALE)));
const isLong = perpDelta > 0n;
const absPerpDelta = perpDelta < 0n ? -perpDelta : perpDelta;
const feeMultiplier = isLong ? 1 + feeRate : 1 - feeRate;

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Consider bounding feeRate below 1.

Validation only rejects negative/non-finite feeRate. If feeRate >= 1 ever reached this function (e.g. misconfigured on-chain fees), applyFeeToUsd for a short computes (FEE_SCALE - feeScaled) as negative, and BigInt division truncates toward zero, silently flipping the sign of effectiveUsdDelta relative to usdDelta instead of throwing. Given current callers derive feeRate from real fee configs, this is unlikely, but a cheap upper-bound check would prevent a badly-signed effective price from ever being displayed.

🛡️ Proposed fix
-  if (!Number.isFinite(feeRate) || feeRate < 0) {
+  if (!Number.isFinite(feeRate) || feeRate < 0 || feeRate >= 1) {
     throw new Error("feeRate must be a non-negative, finite number");
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function simulateTakerSwap(opts: {
sqrtPriceX96: bigint;
liquidity: bigint;
perpDelta: bigint;
markPrice: number;
feeRate?: number;
}): SimulatedTakerSwap {
const { sqrtPriceX96, liquidity, perpDelta, markPrice } = opts;
const feeRate = opts.feeRate ?? 0;
if (!Number.isFinite(feeRate) || feeRate < 0) {
throw new Error("feeRate must be a non-negative, finite number");
}
const feeScaled = BigInt(Math.round(feeRate * Number(FEE_SCALE)));
const isLong = perpDelta > 0n;
const absPerpDelta = perpDelta < 0n ? -perpDelta : perpDelta;
const feeMultiplier = isLong ? 1 + feeRate : 1 - feeRate;
export function simulateTakerSwap(opts: {
sqrtPriceX96: bigint;
liquidity: bigint;
perpDelta: bigint;
markPrice: number;
feeRate?: number;
}): SimulatedTakerSwap {
const { sqrtPriceX96, liquidity, perpDelta, markPrice } = opts;
const feeRate = opts.feeRate ?? 0;
if (!Number.isFinite(feeRate) || feeRate < 0 || feeRate >= 1) {
throw new Error("feeRate must be a non-negative, finite number");
}
const feeScaled = BigInt(Math.round(feeRate * Number(FEE_SCALE)));
const isLong = perpDelta > 0n;
const absPerpDelta = perpDelta < 0n ? -perpDelta : perpDelta;
const feeMultiplier = isLong ? 1 + feeRate : 1 - feeRate;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/swap.ts` around lines 137 - 152, Bound feeRate to the valid range
in simulateTakerSwap: update its validation to reject values greater than or
equal to 1, alongside negative and non-finite values. Keep the existing
feeScaled and feeMultiplier calculations unchanged, and ensure the thrown
validation error clearly states that feeRate must be finite, non-negative, and
less than 1.

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
package.json (1)

5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a CHANGELOG entry for 0.15.0.

The changelog's most recent section is 0.14.1, with no entry for 0.15.0 covering the fee-inclusive slippage changes described in the PR objectives.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 5, Add a new 0.15.0 section at the top of the
changelog, before the existing 0.14.1 section, documenting the fee-inclusive
slippage changes introduced by this PR.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@package.json`:
- Line 5: Add a new 0.15.0 section at the top of the changelog, before the
existing 0.14.1 section, documenting the fee-inclusive slippage changes
introduced by this PR.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ba9af6d-5c22-4e9d-8d2b-28b548a50c01

📥 Commits

Reviewing files that changed from the base of the PR and between 268de04 and 6615a19.

📒 Files selected for processing (1)
  • package.json

@lukemacauley
lukemacauley merged commit ea4c4ff into main Jul 9, 2026
7 checks passed
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