Skip to content

refactor(pool): optimize swap hook checks and cache block timestamp#1267

Open
aronpark1007 wants to merge 5 commits intomainfrom
refactor/pool-swap-optimization
Open

refactor(pool): optimize swap hook checks and cache block timestamp#1267
aronpark1007 wants to merge 5 commits intomainfrom
refactor/pool-swap-optimization

Conversation

@aronpark1007
Copy link
Copy Markdown

@aronpark1007 aronpark1007 commented Apr 22, 2026

Summary

  • Remove redundant Has check before Get for swap hooks — Get already handles the missing case by returning nil
  • Cache block timestamp once at swap entry instead of calling time.Now() on every tick cross during the swap loop

Test Plan

No new tests required. Existing tests cover all changes:

remove redundant Has check before Get for swap hooks

  • TestSwap_ExecutionAndSecurity — covers Swap() execution with nil hook, naturally validates nil return behavior

cache block timestamp during swap

  • TestOracle_SwapScenarios — covers oracle write timestamp path inside swap
  • TestOracle_TWAPConsistencyCases — covers TWAP timestamp consistency including tick crossing
  • TestSwap_PriceLimitEdgeCase_ZeroAmount — reflects newSwapCache signature change (already updated to pass 0)

Summary by CodeRabbit

  • Bug Fixes
    • Improved timestamp consistency across swap flows to ensure deterministic behavior during execution and simulations
  • Refactor
    • Reduced redundant system time calls in swap processing for cleaner, more consistent runtime behavior
  • Tests
    • Adjusted swap tests to align with the timestamp handling changes
  • Chores
    • Updated integration deployment expectations for increased gas/fee values

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Walkthrough

Capture a single swap-start blockTimestamp and thread it through swap execution: into the swap cache, oracle observation writes, and tick-cross hooks; update constructors and call sites to accept/use this cached timestamp instead of calling time.Now() in multiple places.

Changes

Cohort / File(s) Summary
Swap Execution
contract/r/gnoswap/pool/v1/swap.gno
Capture blockTimestamp once at swap entry and pass it into newSwapCache, swap-start hook, oracle writes, and tick-cross hook invocations; replace ad-hoc time.Now() calls with the cached timestamp.
Swap Cache Factory / Types
contract/r/gnoswap/pool/v1/type.gno
newSwapCache signature gains blockTimestamp int64; SwapCache.blockTimestamp initialized from parameter; remove time import and inline time calls.
Tests
contract/r/gnoswap/pool/v1/swap_test.gno
Update calls to newSwapCache in tests to pass the additional timestamp argument (e.g., 0 for deterministic tests).
Integration Test Data
tests/integration/testdata/deploy.txtar
Adjust expected gas fee/wanted values and update GAS USED stdout pattern to match increased gas usage.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SwapEngine as Swap
    participant Cache
    participant Oracle
    participant TickHook

    Client->>Swap: initiate Swap request
    Swap->>Swap: capture blockTimestamp
    Swap->>Cache: newSwapCache(..., blockTimestamp)
    Swap->>Oracle: write observation(using cache.blockTimestamp)
    Swap->>TickHook: invoke tickCross(tick, cache.blockTimestamp)
    TickHook-->>Swap: tick hook result
    Oracle-->>Swap: confirmation
    Swap-->>Client: swap result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(pool): optimize swap hook checks and cache block timestamp' directly matches the main changes: removing redundant hook checks and caching block timestamp to avoid repeated time.Now() calls in swap loops.
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.

✏️ 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/pool-swap-optimization

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.

Caution

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

⚠️ Outside diff range comments (1)
contract/r/gnoswap/pool/store.gno (1)

177-217: ⚠️ Potential issue | 🔴 Critical

Update test expectations in pool/store_test.gno — GetSwapStartHook and GetSwapEndHook now return nil instead of panicking on uninitialized access.

The test cases at lines 465-471 ("panic when getting uninitialized swap start hook") and lines 522-528 ("panic when getting uninitialized swap end hook") declare shouldPanic: true, but the new implementation returns nil on KV-retrieval errors rather than panicking. These tests will fail and must be rewritten to assert nil returns instead.

Note: The actual usage in swap.gno (lines 175–176, 185–186) already guards calls with if swapStartHook != nil and if swapEndHook != nil checks, so the nil-return behavior is compatible with the Swap flow.

🧹 Nitpick comments (2)
contract/r/gnoswap/pool/store.gno (1)

229-242: Inconsistency: GetTickCrossHook still panics on KV error.

This PR relaxes GetSwapStartHook / GetSwapEndHook to return nil on KV-retrieval errors, but GetTickCrossHook at lines 229-242 is left panicking. The only current caller in contract/r/gnoswap/pool/v1/swap.gno (lines 721-722) still guards with HasTickCrossHook() first, so it is safe today, but the asymmetry is easy to misuse if a future caller drops the Has check here the same way swap-start/end just did. Consider either mirroring the nil-on-error behavior (and documenting it) or keeping all three symmetric on panic — whichever the PR intended.

contract/r/gnoswap/pool/v1/swap.gno (1)

313-313: Minor: DrySwap still calls time.Now().Unix() inline — consider for consistency.

Not a defect (DrySwap is read-only and short-lived, and the cache's timestamp is unused for any observable DrySwap output in current tests), but if the intent of the PR is to standardize a single timestamp per swap invocation, you could hoist this to a local variable the same way Swap does at line 172 for stylistic consistency.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e20c315a-271d-467e-b0b5-de01df31bb65

📥 Commits

Reviewing files that changed from the base of the PR and between 7931e1a and 3b7ba04.

📒 Files selected for processing (4)
  • contract/r/gnoswap/pool/store.gno
  • contract/r/gnoswap/pool/v1/swap.gno
  • contract/r/gnoswap/pool/v1/swap_test.gno
  • contract/r/gnoswap/pool/v1/type.gno

GetSwapStartHook / GetSwapEndHook now return nil instead of panicking when uninitialized. Previous shouldPanic tests were false positives due to uassert.NotEqual not handling nil function type comparison correctly.
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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e2bdc7b4-af80-48ab-9298-67933dafe74e

📥 Commits

Reviewing files that changed from the base of the PR and between 3b7ba04 and bfeac62.

📒 Files selected for processing (1)
  • contract/r/gnoswap/pool/store_test.gno

Comment thread contract/r/gnoswap/pool/store_test.gno Outdated
Copy link
Copy Markdown
Member

@jinoosss jinoosss left a comment

Choose a reason for hiding this comment

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

The changes to swapCache look fine.
However, the naming convention for functions that query hooks is changing, which may differ from the convention used in existing functions.

Comment on lines 180 to 209
result, err := s.kvStore.Get(StoreKeySwapStartHook.String())
if err != nil {
panic(err)
return nil
}

swapStartHook, ok := result.(func(cur realm, poolPath string, timestamp int64))
if !ok {
panic(ufmt.Sprintf("failed to cast result to func(poolPath string, timestamp int64): %T", result))
}

return swapStartHook
}

// SetSwapStartHook stores the swap start hook function.
func (s *poolStore) SetSwapStartHook(swapStartHook func(cur realm, poolPath string, timestamp int64)) error {
return s.kvStore.Set(StoreKeySwapStartHook.String(), swapStartHook)
}

// HasSwapEndHook checks if the swap end hook is set.
func (s *poolStore) HasSwapEndHook() bool {
return s.kvStore.Has(StoreKeySwapEndHook.String())
}

// GetSwapEndHook retrieves the swap end hook function.
// Returns nil if no hook is set.
func (s *poolStore) GetSwapEndHook() func(cur realm, poolPath string) error {
result, err := s.kvStore.Get(StoreKeySwapEndHook.String())
if err != nil {
panic(err)
return nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Errors occurring in the store are ignored.
This differs from how other query functions in the store are handled.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Instead of changing the store's error handling, we can keep GetSwapStartHook / GetSwapEndHook panicking on error and simplify the call site by trusting Has* as the guard:

if i.store.HasSwapStartHook() {
    i.store.GetSwapStartHook()(cross, pool.PoolPath(), blockTimestamp)
}

Since Has* already confirms the key exists, the nil check after Get* is unnecessary. This removes the redundant double-check without touching the store's error handling convention.

How about reverting the store.gno change and updating the call site this way instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

🧹 Nitpick comments (1)
contract/r/gnoswap/pool/v1/swap.gno (1)

312-312: Minor: DrySwap left out of the caching pattern.

DrySwap still calls time.Now().Unix() inline when constructing the cache while Swap now hoists it to a named blockTimestamp local. It is only used once here so there's no correctness/performance concern, but introducing the same local would keep the two call sites stylistically consistent and make future reuse (e.g., if DrySwap ever grows oracle/hook calls) trivial.

♻️ Optional tiny consistency tweak
 	feeProtocol := getFeeProtocol(slot0Start, zeroForOne)
-	cache := newSwapCache(feeProtocol, poolSnapshot.Liquidity().Clone(), time.Now().Unix())
+	blockTimestamp := time.Now().Unix()
+	cache := newSwapCache(feeProtocol, poolSnapshot.Liquidity().Clone(), blockTimestamp)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b74525a4-a194-4401-972c-a547f879ff53

📥 Commits

Reviewing files that changed from the base of the PR and between bfeac62 and 14d95ad.

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

Comment on lines 175 to -180
if i.store.HasSwapStartHook() {
swapStartHook := i.store.GetSwapStartHook()

if swapStartHook != nil {
currentTime := time.Now().Unix()
swapStartHook(cross, pool.PoolPath(), currentTime)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The function type of hook can be nil.
When actually using the function, it would be best to add a nil check for defensive programming.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

As discussed, rolled back all store-related changes, keeping only the block timestamp caching.
See revert commit: 294d158

@sonarqubecloud
Copy link
Copy Markdown

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/pool/v1/swap.gno (1)

321-321: Optional: mirror the blockTimestamp caching in DrySwap for consistency.

Swap now captures blockTimestamp := time.Now().Unix() once and passes it to newSwapCache, while DrySwap still inlines time.Now().Unix() in the constructor call. Functionally equivalent in Gno (block time is constant within a transaction), but it makes DrySwap diverge stylistically from Swap and obscures the fact that both paths should observe the same moment in simulated execution. Consider hoisting the call for readability/symmetry.

♻️ Proposed refactor
 	feeProtocol := getFeeProtocol(slot0Start, zeroForOne)
-	cache := newSwapCache(feeProtocol, poolSnapshot.Liquidity().Clone(), time.Now().Unix())
+	blockTimestamp := time.Now().Unix()
+	cache := newSwapCache(feeProtocol, poolSnapshot.Liquidity().Clone(), blockTimestamp)

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0cfdfe2a-56e7-4bad-9229-063e74be814a

📥 Commits

Reviewing files that changed from the base of the PR and between 14d95ad and 294d158.

📒 Files selected for processing (2)
  • contract/r/gnoswap/pool/v1/swap.gno
  • tests/integration/testdata/deploy.txtar
✅ Files skipped from review due to trivial changes (1)
  • tests/integration/testdata/deploy.txtar

Comment on lines 729 to 733
if i.store.HasTickCrossHook() {
tickCrossHook := i.store.GetTickCrossHook()

currentTime := time.Now().Unix()
tickCrossHook(cross, pool.PoolPath(), step.tickNext, zeroForOne, currentTime)
tickCrossHook(cross, pool.PoolPath(), step.tickNext, zeroForOne, cache.blockTimestamp)
}
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 | 🟡 Minor

Add a nil guard on tickCrossHook for consistency with swap start/end hooks.

SwapStartHook (lines 178) and SwapEndHook (line 192) both defensively check != nil after Get...(), but tickCrossHook is invoked directly after GetTickCrossHook() without the same guard. Per the PR objectives ("A revert commit restores the nil guard for swap hooks"), this path looks like it was missed in the revert. If Has...() and Get...() ever go out of sync (e.g., hook was set to nil without clearing the "has" flag), this will panic mid-swap after state has already been partially mutated and a reentrancy lock acquired — hard to recover from.

🛡️ Proposed fix
 		if i.store.HasTickCrossHook() {
 			tickCrossHook := i.store.GetTickCrossHook()
-
-			tickCrossHook(cross, pool.PoolPath(), step.tickNext, zeroForOne, cache.blockTimestamp)
+			if tickCrossHook != nil {
+				tickCrossHook(cross, pool.PoolPath(), step.tickNext, zeroForOne, cache.blockTimestamp)
+			}
 		}
📝 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
if i.store.HasTickCrossHook() {
tickCrossHook := i.store.GetTickCrossHook()
currentTime := time.Now().Unix()
tickCrossHook(cross, pool.PoolPath(), step.tickNext, zeroForOne, currentTime)
tickCrossHook(cross, pool.PoolPath(), step.tickNext, zeroForOne, cache.blockTimestamp)
}
if i.store.HasTickCrossHook() {
tickCrossHook := i.store.GetTickCrossHook()
if tickCrossHook != nil {
tickCrossHook(cross, pool.PoolPath(), step.tickNext, zeroForOne, cache.blockTimestamp)
}
}

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.

3 participants