Skip to content

LimitOrderHook: guard _fillOrder against filling non-converted positions (#132)#133

Open
impetus82 wants to merge 1 commit into
OpenZeppelin:masterfrom
impetus82:fix/limitorderhook-fill-guard-132
Open

LimitOrderHook: guard _fillOrder against filling non-converted positions (#132)#133
impetus82 wants to merge 1 commit into
OpenZeppelin:masterfrom
impetus82:fix/limitorderhook-fill-guard-132

Conversation

@impetus82

Copy link
Copy Markdown

Summary

Addresses #132 (irrecoverable wrong-token fill in LimitOrderHook) by adding a defense-in-depth guard to _fillOrder, plus an internal _setTickLowerLast so subclasses that perform internal swaps can keep _tickLowerLasts fresh.

Root cause

_afterSwap maintains _tickLowerLasts, and _getCrossedTicks derives the fill window from it. Uniswap V4's self-call protection in Hooks.sol skips afterSwap whenever msg.sender == address(hook), so any subclass that calls poolManager.swap() from inside its own unlockCallback (e.g. a swapAndPlaceOrder extension) never runs _afterSwap and leaves _tickLowerLasts stale. A stale _tickLowerLasts lets _getCrossedTicks return a window that includes an order tick whose position the live price has not converted. _fillOrder then removes that position, mints the deposited currency (not the target), credits it, sets filled = true and resets the order id — permanently consuming the order and returning the wrong asset.

The underlying gap: _fillOrder trusts the crossed-ticks window unconditionally.

Fix

  1. Guard in _fillOrder (correct regardless of caller / staleness): read live slot0 and skip the fill unless the position is fully converted —
    • zeroForOne (sell currency0 → currency1): require sqrtPriceX96 >= getSqrtPriceAtTick(tickLower + tickSpacing);
    • oneForZero (sell currency1 → currency0): require sqrtPriceX96 <= getSqrtPriceAtTick(tickLower).
      On a skip the order stays active and cancellable, so the worst case degrades from “wrong token, irrecoverable” to “fill deferred”.
  2. internal _setTickLowerLast(PoolId, int24): lets subclasses performing internal swaps keep _tickLowerLasts consistent with live price (addresses the root cause for that pattern) without the brittle storage-layout sstore workaround.

Test

test_fillOrder_skipsWhenPositionNotConverted_issue132 sets a stale _tickLowerLasts (exactly the state a self-swap leaves) and drives _afterSwap to attempt a fill of a non-converted oneForZero order. Without the guard the test fails (order filled, currency1 credited to an order that expected currency0); with the guard it passes (fill skipped, no miscredit, order cancellable). Full suite stays green (305/305), forge fmt clean.

Notes

  • Complementary to LimitOrderHook: H-01, H-02 fix #119 (H-01/H-02), which also edits LimitOrderHook.sol; happy to rebase onto it.
  • The guard prevents the catastrophic miscredit; _setTickLowerLast lets subclasses prevent the staleness itself.

🤖 Generated with Claude Code

Addresses OpenZeppelin#132. A stale `_tickLowerLasts` -- left whenever a subclass performs an
internal swap, which Uniswap V4 excludes from the `afterSwap` callback via self-call
protection -- makes `_getCrossedTicks` propose a fill window whose order positions the
live price has not converted. `_fillOrder` then mints/credits the deposited currency
instead of the target, marks the order filled and resets it: an irrecoverable
wrong-token fill.

Add a defense-in-depth guard in `_fillOrder` that skips the fill unless live slot0
confirms full conversion (zeroForOne: sqrtPrice >= sqrtPriceAtTick(tickUpper);
oneForZero: sqrtPrice <= sqrtPriceAtTick(tickLower)); on a skip the order stays active
and cancellable. Also add an internal `_setTickLowerLast` so subclasses doing internal
swaps can keep `_tickLowerLasts` fresh without storage-layout assembly.

Includes a reproducing test; full suite green (305/305).

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

netlify Bot commented Jun 20, 2026

Copy link
Copy Markdown

Deploy Preview for uniswap-hooks ready!

Name Link
🔨 Latest commit 5948a73
🔍 Latest deploy log https://app.netlify.com/projects/uniswap-hooks/deploys/6a362b7353a63a0008e68b18
😎 Deploy Preview https://deploy-preview-133--uniswap-hooks.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Jun 20, 2026

Copy link
Copy Markdown

Deploy Preview for uniswap-hooks-docs ready!

Name Link
🔨 Latest commit 5948a73
🔍 Latest deploy log https://app.netlify.com/projects/uniswap-hooks-docs/deploys/6a362b730120020008afcf54
😎 Deploy Preview https://deploy-preview-133--uniswap-hooks-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

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