fix(slippage): fold taker fees into displayed fill price#62
Conversation
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>
📝 WalkthroughWalkthroughTaker 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. ChangesTaker quote fee modeling
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
src/__tests__/unit/adjust-primitives.test.tssrc/__tests__/unit/swap.test.tssrc/functions/perp-actions.tssrc/types/entity-data.tssrc/utils/fees.tssrc/utils/swap.ts
| 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; |
There was a problem hiding this comment.
🎯 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.
| 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.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
package.json (1)
5-5: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a CHANGELOG entry for 0.15.0.
The changelog's most recent section is
0.14.1, with no entry for0.15.0covering 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.
Problem
simulateTakerSwapwalks the AMM curve to producefillPricebut 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
simulateTakerSwapgains an optionalfeeRate(total taker fee as a fraction) and its result now carries, alongside the unchangedusdDelta/fillPrice/exceedsLiquidity:feeRate— the fee appliedeffectiveUsdDelta— 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 proceedsraw*(1-feeRate). Total taker fee =creatorFee + insuranceFee + lpFee(newtotalTakerFeeRatehelper; protocolFee is a redistribution cut, not additional trader cost). WhenfeeRateis omitted, effective fields equal raw — every existing caller is unaffected, andcalculateTakerSlippageLimit/ theamt1Limitmoney 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 pastAPPROX_TICK_CROSS_WARN_FRACTION, default ~4% price move; forced true in theexceedsLiquiditybranch).Tests / checks
pnpm run cigreen: 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)
0.15.0(minor, additive).feeRate: totalTakerFeeRate(perpData.fees)and readeffectiveFillPriceinuseTakerOrder.ts/TakerClose.ts; optionally wireliquidityLimitedto a warning inOrderSummary.tsx.🤖 Generated with Claude Code
Summary by CodeRabbit
liquidityLimitedindicator, alongside existing liquidity/exceeds-liquidity behavior.