Skip to content

refactor(router): cache precomputed sqrtPriceLimit values in init#1265

Open
aronpark1007 wants to merge 2 commits intomainfrom
refactor/cache-sqrt-price-limit-in-init
Open

refactor(router): cache precomputed sqrtPriceLimit values in init#1265
aronpark1007 wants to merge 2 commits intomainfrom
refactor/cache-sqrt-price-limit-in-init

Conversation

@aronpark1007
Copy link
Copy Markdown

@aronpark1007 aronpark1007 commented Apr 21, 2026

Description

Precomputes TickMathGetSqrtRatioAtTick results for all fee tier and swap direction combinations in init() and caches them in a map, replacing per-swap tick math computation with a
simple map lookup.

Why This Change Is Needed

TickMathGetSqrtRatioAtTick involves heavy u256 bit arithmetic and was called on every swap to compute the sqrt price limit. Since the inputs are determined by a fixed set of 4 fee
tiers and 2 swap directions, all 8 combinations can be precomputed once at initialization. This was confirmed to be the largest gas reduction among the optimizations explored in
this series.

Test Plan

No new tests required. Existing tests cover all changes:

cache precomputed sqrtPriceLimit values in init

  • TestCalculateSqrtPriceLimitForSwap (swap_inner_test.gno) — validates basic behavior of calculateSqrtPriceLimitForSwap
  • TestCalculateSqrtPriceLimitForSwap_ZeroForOne (swap_test.gno) — covers limit value accuracy per zeroForOne direction
  • TestCalculateSqrtPriceLimitForSwap_AllFees (swap_test.gno) — validates all cached values across 4 fee tiers × 2 directions, covers unknown fee panic case

Summary by CodeRabbit

  • Chores
    • Optimized swap calculation performance by implementing precomputed cached values for swap configurations based on fee tiers and transaction directions. This reduces computational overhead during swap execution, improves overall system efficiency and routing performance, while maintaining complete backward compatibility with all existing swap operations.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Warning

Rate limit exceeded

@aronpark1007 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 43 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 18 minutes and 43 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5f08d746-e4f5-4f2d-9a28-3c3ae39c04d4

📥 Commits

Reviewing files that changed from the base of the PR and between 0f130ac and ee7fb98.

📒 Files selected for processing (2)
  • contract/r/gnoswap/router/v1/swap_inner.gno
  • contract/r/gnoswap/router/v1/swap_inner_test.gno

Walkthrough

This change optimizes sqrtPriceLimitX96 computation by introducing caching. Instead of calculating these values per invocation, they are precomputed once during initialization for each fee tier and swap direction combination, stored in package-level maps, and retrieved via lookups in the swap function.

Changes

Cohort / File(s) Summary
Caching Optimization
contract/r/gnoswap/router/v1/swap_inner.gno
Added init() function to precompute and cache sqrtPriceLimitX96 values in two package-level maps (sqrtPriceLimitForward, sqrtPriceLimitBackward) keyed by fee tier. Modified calculateSqrtPriceLimitForSwap() to perform direct map lookups instead of per-call TickMath computation. Added import of gno.land/r/gnoswap/pool/v1 for fee tier constants.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: caching precomputed sqrtPriceLimit values during initialization instead of computing them per-call.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/cache-sqrt-price-limit-in-init

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
Copy Markdown

@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: 1

🧹 Nitpick comments (1)
contract/r/gnoswap/router/v1/swap_inner.gno (1)

17-17: Import is necessary but creates inconsistency — use plv1.FeeTier* constants in getMinTick/getMaxTick to match init().

Line 17: The plv1 import is already used in init() at line 35 to iterate over plv1.FeeTier100/500/3000/10000, but the same fee values are hardcoded as literal case arms in getMinTick/getMaxTick (lines 193–200, 213–220). Replace the literal case values with plv1.FeeTier* constants to maintain consistency and reduce duplication:

Example fix for getMinTick
func getMinTick(fee uint32) int32 {
	switch fee {
	case plv1.FeeTier100:
		return -887272
	case plv1.FeeTier500:
		return -887270
	case plv1.FeeTier3000:
		return -887220
	case plv1.FeeTier10000:
		return -887200
	default:
		panic(...)
	}
}

No import cycle detected (pool/v1 does not import router/v1).


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8092e1a-a895-46f2-a7f7-46af11842008

📥 Commits

Reviewing files that changed from the base of the PR and between 7931e1a and 0f130ac.

📒 Files selected for processing (1)
  • contract/r/gnoswap/router/v1/swap_inner.gno

Comment on lines +29 to +54
var (
sqrtPriceLimitForward = make(map[uint32]*u256.Uint) // zeroForOne=true: sqrtRatioAtTick(minTick+1) + 1
sqrtPriceLimitBackward = make(map[uint32]*u256.Uint) // zeroForOne=false: sqrtRatioAtTick(maxTick-1) - 1
)

func init() {
for _, fee := range []uint32{plv1.FeeTier100, plv1.FeeTier500, plv1.FeeTier3000, plv1.FeeTier10000} {
// zeroForOne=true: price must stay above minimum
minTick := getMinTick(fee) + 1
fwd := common.TickMathGetSqrtRatioAtTick(minTick) // returns a fresh allocation
if fwd.IsZero() {
fwd = u256.MustFromDecimal(MIN_SQRT_RATIO)
}
fwd.Add(fwd, one) // fwd += 1 in-place
sqrtPriceLimitForward[fee] = fwd

// zeroForOne=false: price must stay below maximum
maxTick := getMaxTick(fee) - 1
bwd := common.TickMathGetSqrtRatioAtTick(maxTick) // returns a fresh allocation
if bwd.IsZero() {
bwd = u256.MustFromDecimal(MAX_SQRT_RATIO)
}
bwd.Sub(bwd, one) // bwd -= 1 in-place
sqrtPriceLimitBackward[fee] = bwd
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cached pointers are mutable and aliased to callers — invariant is comment-only.

sqrtPriceLimitForward[fee] / sqrtPriceLimitBackward[fee] hold *u256.Uint, and calculateSqrtPriceLimitForSwap (Lines 183/185) returns that same pointer directly to callers. Today the only downstream use is .ToString() (Lines 81, 119), so it works — but u256.Uint exposes in-place mutators (Add, Sub, Set, etc.) and the init itself relies on that (fwd.Add(fwd, one)). A future refactor that, say, tweaks the limit before passing it to pl.Swap would silently corrupt the global cache for every subsequent swap at that fee tier, permanently — no panic, just wrong prices.

The "read-only" invariant is enforced solely by a comment. Consider returning a defensive clone on lookup, or storing the decimal string (which is what pl.Swap/pl.DrySwap actually consume) to eliminate the aliasing footgun. Caching the string also sidesteps the repeated ToString() cost on the hot path.

♻️ Option: cache the string form (no aliasing, no repeated ToString)
 var (
-	sqrtPriceLimitForward  = make(map[uint32]*u256.Uint) // zeroForOne=true:  sqrtRatioAtTick(minTick+1) + 1
-	sqrtPriceLimitBackward = make(map[uint32]*u256.Uint) // zeroForOne=false: sqrtRatioAtTick(maxTick-1) - 1
+	sqrtPriceLimitForwardStr  = make(map[uint32]string) // zeroForOne=true
+	sqrtPriceLimitBackwardStr = make(map[uint32]string) // zeroForOne=false
 )

…and have calculateSqrtPriceLimitForSwap return the string (adjusting swapInner/swapDryInner to skip the redundant ToString()). Alternatively, keep the map but return cached.Clone() on each lookup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@aronpark1007 aronpark1007 force-pushed the refactor/cache-sqrt-price-limit-in-init branch from 0f130ac to ac29aa0 Compare April 21, 2026 09:44
@sonarqubecloud
Copy link
Copy Markdown

@aronpark1007
Copy link
Copy Markdown
Author

@jinoosss
Extracted repeated map lookup into mustGetSqrtPriceLimit and added test coverage for the extracted function.

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