Skip to content

taking Price Impact into account for partial trade size#433

Open
rouzwelt wants to merge 1 commit intomasterfrom
2026-02-12-price-impact
Open

taking Price Impact into account for partial trade size#433
rouzwelt wants to merge 1 commit intomasterfrom
2026-02-12-price-impact

Conversation

@rouzwelt
Copy link
Collaborator

@rouzwelt rouzwelt commented Feb 12, 2026

Motivation

This PR enables accounting for sushi's swap Price Impact for finding the partial trade size.

Solution

Checks

By submitting this for review, I'm confirming I've done the following:

  • made this PR as small as possible
  • unit-tested any new functionality
  • linked any relevant issues or PRs
  • included screenshots (if this involves a front-end change)

Summary by CodeRabbit

  • New Features

    • Market price quotes now include routing information.
  • Bug Fixes

    • Routes are now validated based on price impact tolerance (2.5% threshold) before acceptance.
  • Tests

    • Added comprehensive test coverage for price calculation scenarios.

@rouzwelt rouzwelt requested a review from hardyjosh February 12, 2026 21:16
@rouzwelt rouzwelt self-assigned this Feb 12, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Walkthrough

The PR extends the router infrastructure to include optional route information in getMarketPrice responses and introduces price impact tolerance validation. A new calculateEffectivePrice helper function is added along with a DEFAULT_PRICE_IMPACT_TOLERANCE constant, with logic updated to validate price impact before accepting routes as valid.

Changes

Cohort / File(s) Summary
Router Type Updates
src/router/router.ts, src/router/types.ts
Extended getMarketPrice return type to include optional route?: MultiRoute field. Added DEFAULT_PRICE_IMPACT_TOLERANCE constant (2.5) for price impact validation.
Sushi Router Implementation
src/router/sushi/index.ts
Updated getMarketPrice to return optional route field. Introduced calculateEffectivePrice helper function to compute effective price from route data. Modified findLargestTradeSize to validate route price impact against tolerance threshold.
Sushi Router Tests
src/router/sushi/index.test.ts
Added comprehensive test suite for calculateEffectivePrice covering scenarios with varying price impacts, token decimals, edge cases, and undefined price impact scenarios.
State Management Integration
src/state/index.ts
Added price impact validation check in getMarketPrice short-circuit logic to reject routes with unacceptable price impact, altering the early return condition.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • hardyjosh
  • Siddharth2207
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main change: incorporating price impact consideration into the partial trade size calculation logic.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 2026-02-12-price-impact

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/state/index.ts (1)

337-345: 🧹 Nitpick | 🔵 Trivial

The synthetic orderDetails is a code smell.

{ takeOrder: { quote: { ratio: 0n } } } as any works because absolute mode ignores ratio, but this couples the callsite to an implementation detail of findLargestTradeSize. A dedicated overload or a distinct method signature for the absolute-mode case would be cleaner.

🤖 Fix all issues with AI agents
In `@src/router/sushi/index.test.ts`:
- Around line 1026-1167: Add an edge-case test that verifies
calculateEffectivePrice handles priceImpact >= 1 without throwing (e.g.,
priceImpact: 1.5) by creating a route with amountOutBI and maximumInput and
asserting the call does not throw and returns parseUnits("0", 18) (or the
defined minimum/clamped value) — target the calculateEffectivePrice function and
use parseUnits for inputs and expected output so the test ensures the guard in
index.ts prevents negative/invalid strings passed to parseUnits.

In `@src/router/sushi/index.ts`:
- Around line 556-571: In calculateEffectivePrice, guard against
route.priceImpact outside [0,1] before computing the multiplier: validate or
clamp route.priceImpact to the 0..1 range (e.g. let impact = Math.min(1,
Math.max(0, route.priceImpact))) and use that clamped value in the parseUnits
call instead of route.priceImpact; keep references to calculatePrice18,
parseUnits, ONE18 and MultiRoute.priceImpact so you locate the code, and ensure
the function returns a non-negative, logically valid bigint price.

In `@src/router/types.ts`:
- Line 22: Fix the typo in the comment text in src/router/types.ts: change
"Default price imapct tolerance used for getting unit market price." to "Default
price impact tolerance used for getting unit market price." — locate the comment
containing "Default price imapct tolerance" and update the word "imapct" to
"impact" so the documentation string is correct.
- Line 26: The DEFAULT_PRICE_IMPACT_TOLERANCE constant is set to 2.5 (250%) but
the code/tests use a 0–1 fractional scale; change the exported constant
DEFAULT_PRICE_IMPACT_TOLERANCE from 2.5 to 0.025 (2.5% expressed as a fraction)
and keep the as const typing so callers using priceImpact comparisons (e.g., any
logic referencing DEFAULT_PRICE_IMPACT_TOLERANCE) correctly treat it as a 0–1
tolerance.

Comment on lines +1026 to +1167
describe("calculateEffectivePrice", () => {
const mockFromToken = new Token({
address: "0x1111111111111111111111111111111111111111",
decimals: 18,
symbol: "TOKEN1",
chainId: 1,
name: "Token 1",
});

const mockToToken = new Token({
address: "0x2222222222222222222222222222222222222222",
decimals: 6,
symbol: "TOKEN2",
chainId: 1,
name: "Token 2",
});

it("should return the base price when priceImpact is undefined", () => {
const maximumInput = parseUnits("100", 18);
const route: MultiRoute = {
status: RouteStatus.Success,
amountOutBI: parseUnits("200", 6),
priceImpact: undefined,
} as any as MultiRoute;

const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken);

// Expected price: (200 * 10^6 * 10^18) / (100 * 10^18) = 2 * 10^6 = 2000000
// Scaled to 18 decimals: 2 * 10^18
expect(result).toBe(parseUnits("2", 18));
});

it("should apply price impact when priceImpact is defined", () => {
const maximumInput = parseUnits("100", 18);
const route: MultiRoute = {
status: RouteStatus.Success,
amountOutBI: parseUnits("200", 6),
priceImpact: 0.05, // 5% price impact
} as any as MultiRoute;

const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken);

// Base price: 2 * 10^18
// With 5% impact: 2 * 0.95 = 1.9 * 10^18
expect(result).toBe(parseUnits("1.9", 18));
});

it("should handle zero price impact", () => {
const maximumInput = parseUnits("100", 18);
const route: MultiRoute = {
status: RouteStatus.Success,
amountOutBI: parseUnits("150", 6),
priceImpact: 0,
} as any as MultiRoute;

const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken);

// Price: 1.5 * 10^18, no impact
expect(result).toBe(parseUnits("1.5", 18));
});

it("should handle small price impact correctly", () => {
const maximumInput = parseUnits("1000", 18);
const route: MultiRoute = {
status: RouteStatus.Success,
amountOutBI: parseUnits("1000", 6),
priceImpact: 0.001, // 0.1% price impact
} as any as MultiRoute;

const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken);

// Base price: 1 * 10^18
// With 0.1% impact: 1 * 0.999 = 0.999 * 10^18
expect(result).toBe(parseUnits("0.999", 18));
});

it("should handle very small amounts", () => {
const maximumInput = parseUnits("0.001", 18); // 1e-3
const route: MultiRoute = {
status: RouteStatus.Success,
amountOutBI: parseUnits("0.002", 6), // 2e-3
priceImpact: 0.1, // 10% price impact
} as any as MultiRoute;

const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken);

// Base price: 2 * 10^18
// With 10% impact: 2 * 0.9 = 1.8 * 10^18
expect(result).toBe(parseUnits("1.8", 18));
});

it("should handle different token decimals", () => {
const token8Decimals: Token = {
...mockFromToken,
decimals: 8,
} as Token;

const maximumInput = parseUnits("50", 8);
const route: MultiRoute = {
status: RouteStatus.Success,
amountOutBI: parseUnits("100", 6),
priceImpact: 0.02, // 2% price impact
} as any as MultiRoute;

const result = calculateEffectivePrice(maximumInput, route, token8Decimals, mockToToken);

// Base price: 2 * 10^18
// With 2% impact: 2 * 0.98 = 1.96 * 10^18
expect(result).toBe(parseUnits("1.96", 18));
});

it("should handle high price impact", () => {
const maximumInput = parseUnits("1000", 18);
const route: MultiRoute = {
status: RouteStatus.Success,
amountOutBI: parseUnits("500", 6),
priceImpact: 0.5, // 50% price impact
} as any as MultiRoute;

const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken);

// Base price: 0.5 * 10^18
// With 50% impact: 0.5 * 0.5 = 0.25 * 10^18
expect(result).toBe(parseUnits("0.25", 18));
});

it("should handle very small price impact (scientific notation)", () => {
const maximumInput = parseUnits("10000", 18);
const route: MultiRoute = {
status: RouteStatus.Success,
amountOutBI: parseUnits("10000", 6),
priceImpact: 1e-20, // extremely small impact
} as any as MultiRoute;

const result = calculateEffectivePrice(maximumInput, route, mockFromToken, mockToToken);

// Base price: 1 * 10^18
// With negligible impact, should be very close to base price
expect(result).toBeGreaterThan(parseUnits("0.999999999999999999", 18));
expect(result).toBeLessThanOrEqual(parseUnits("1", 18));
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Good test coverage for calculateEffectivePrice.

The tests thoroughly cover various scenarios. Consider adding an edge-case test for priceImpact >= 1 (e.g., 1.5) to validate the guard suggested in src/router/sushi/index.ts, since parseUnits would throw on the resulting negative string.

🤖 Prompt for AI Agents
In `@src/router/sushi/index.test.ts` around lines 1026 - 1167, Add an edge-case
test that verifies calculateEffectivePrice handles priceImpact >= 1 without
throwing (e.g., priceImpact: 1.5) by creating a route with amountOutBI and
maximumInput and asserting the call does not throw and returns parseUnits("0",
18) (or the defined minimum/clamped value) — target the calculateEffectivePrice
function and use parseUnits for inputs and expected output so the test ensures
the guard in index.ts prevents negative/invalid strings passed to parseUnits.

Comment on lines +556 to +571
export function calculateEffectivePrice(
maximumInput: bigint,
route: MultiRoute,
fromToken: Token,
toToken: Token,
): bigint {
const price = calculatePrice18(
maximumInput,
route.amountOutBI,
fromToken.decimals,
toToken.decimals,
);
if (typeof route.priceImpact === "undefined") {
return price;
}
return (price * parseUnits((1 - route.priceImpact).toFixed(12), 18)) / ONE18;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

viem parseUnits negative number string behavior documentation

💡 Result:

parseUnits does accept negative decimal strings (e.g. "-1.5"). Internally it detects a leading -, parses/rounds the absolute value the same way as for positives, then re-applies the minus sign and returns a negative bigint. [2]

So, for example (behavior implied by implementation):

  • parseUnits('-1', 18)-1000000000000000000n
  • parseUnits('-1.234', 2)-123n (fraction is rounded/truncated to the given decimals) [2]

The official docs describe parseUnits as multiplying a string number by 10**exponent, but don’t explicitly call out negative input. [1]

Sources:
[1] Viem docs – parseUnits https://viem.sh/docs/utilities/parseUnits
[2] Viem implementation (parseUnits.js) https://app.unpkg.com/viem%402.0.6/files/_cjs/utils/unit/parseUnits.js


🏁 Script executed:

# First, let's examine the actual code around lines 556-571
cat -n src/router/sushi/index.ts | sed -n '550,575p'

Repository: rainlanguage/rain.solver

Length of output: 748


🏁 Script executed:

# Check how priceImpact is validated or bounded in the codebase
rg "priceImpact" -B 2 -A 2 --max-count 20

Repository: rainlanguage/rain.solver

Length of output: 4513


The concern about parseUnits throwing is incorrect, but defensive bounds-checking would still be prudent.

parseUnits from viem does accept negative decimal strings and returns a negative bigint rather than throwing. However, if route.priceImpact exceeds 1, the calculation (1 - route.priceImpact) still produces a negative value, leading to a negative or zero effective price—a logically invalid result for a price calculation.

While this scenario is unlikely (tests only show priceImpact values between 0 and 1), adding explicit bounds-checking would prevent nonsensical outcomes:

-    return (price * parseUnits((1 - route.priceImpact).toFixed(12), 18)) / ONE18;
+    const impactMultiplier = Math.max(0, 1 - route.priceImpact);
+    return (price * parseUnits(impactMultiplier.toFixed(12), 18)) / ONE18;
📝 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 calculateEffectivePrice(
maximumInput: bigint,
route: MultiRoute,
fromToken: Token,
toToken: Token,
): bigint {
const price = calculatePrice18(
maximumInput,
route.amountOutBI,
fromToken.decimals,
toToken.decimals,
);
if (typeof route.priceImpact === "undefined") {
return price;
}
return (price * parseUnits((1 - route.priceImpact).toFixed(12), 18)) / ONE18;
export function calculateEffectivePrice(
maximumInput: bigint,
route: MultiRoute,
fromToken: Token,
toToken: Token,
): bigint {
const price = calculatePrice18(
maximumInput,
route.amountOutBI,
fromToken.decimals,
toToken.decimals,
);
if (typeof route.priceImpact === "undefined") {
return price;
}
const impactMultiplier = Math.max(0, 1 - route.priceImpact);
return (price * parseUnits(impactMultiplier.toFixed(12), 18)) / ONE18;
}
🤖 Prompt for AI Agents
In `@src/router/sushi/index.ts` around lines 556 - 571, In
calculateEffectivePrice, guard against route.priceImpact outside [0,1] before
computing the multiplier: validate or clamp route.priceImpact to the 0..1 range
(e.g. let impact = Math.min(1, Math.max(0, route.priceImpact))) and use that
clamped value in the parseUnits call instead of route.priceImpact; keep
references to calculatePrice18, parseUnits, ONE18 and MultiRoute.priceImpact so
you locate the code, and ensure the function returns a non-negative, logically
valid bigint price.

} from "../order";

/*
* Default price imapct tolerance used for getting unit market price.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: "imapct" → "impact"

- * Default price imapct tolerance used for getting unit market price.
+ * Default price impact tolerance used for getting unit market price.
📝 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
* Default price imapct tolerance used for getting unit market price.
* Default price impact tolerance used for getting unit market price.
🤖 Prompt for AI Agents
In `@src/router/types.ts` at line 22, Fix the typo in the comment text in
src/router/types.ts: change "Default price imapct tolerance used for getting
unit market price." to "Default price impact tolerance used for getting unit
market price." — locate the comment containing "Default price imapct tolerance"
and update the word "imapct" to "impact" so the documentation string is correct.

* this is not used for actual trade size finding in simulations but
* only for getting market price for unit token.
*/
export const DEFAULT_PRICE_IMPACT_TOLERANCE = 2.5 as const;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how priceImpact is used/compared throughout the codebase
rg -n "priceImpact" --type=ts -C3

Repository: rainlanguage/rain.solver

Length of output: 6810


Change DEFAULT_PRICE_IMPACT_TOLERANCE from 2.5 to 0.025.

priceImpact uses a 0–1 fractional scale as confirmed by the tests (e.g., 0.05 = 5%, 0.1 = 10%). A tolerance of 2.5 means 250% and effectively disables tolerance checking. The intended value for a 2.5% tolerance is 0.025.

🤖 Prompt for AI Agents
In `@src/router/types.ts` at line 26, The DEFAULT_PRICE_IMPACT_TOLERANCE constant
is set to 2.5 (250%) but the code/tests use a 0–1 fractional scale; change the
exported constant DEFAULT_PRICE_IMPACT_TOLERANCE from 2.5 to 0.025 (2.5%
expressed as a fraction) and keep the as const typing so callers using
priceImpact comparisons (e.g., any logic referencing
DEFAULT_PRICE_IMPACT_TOLERANCE) correctly treat it as a 0–1 tolerance.

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