Skip to content

Commit 33121a3

Browse files
committed
LimitOrderHook: L-05 record the baseline for a self-initialized pool
Uniswap V4 does not call a hook's own callbacks, so `_afterInitialize` does not run for a pool the hook initializes itself and `_tickLowerLasts` keeps its zero default. The first swap then measures its crossing from tick zero and fills every order between there and the price. Adds `_recordTickLowerLast` for a subclass to call after initializing a pool itself.
1 parent 2ae32be commit 33121a3

3 files changed

Lines changed: 72 additions & 1 deletion

File tree

src/general/LimitOrderHook.sol

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ library OrderIdLibrary {
6161
* IMPORTANT: Uniswap V4 does not call a hook's own callbacks when that hook is the caller, so {_afterSwap}
6262
* does not run for a swap this hook makes itself. A subclass that swaps internally MUST call
6363
* {_fillCrossedOrders} afterwards, or the tick recorded for the pool falls behind the price and the next
64-
* crossing is measured from it.
64+
* crossing is measured from it. For the same reason {_afterInitialize} does not run for a pool this hook
65+
* initializes itself, so such a subclass MUST call {_recordTickLowerLast} afterwards, or the pool keeps a
66+
* tick-zero baseline and the first swap fills every order between tick zero and the price.
6567
*
6668
* WARNING: This is experimental software and is provided on an "as is" and "as available" basis. We do
6769
* not give any warranties and will not be liable for any losses incurred through any use of this code
@@ -232,6 +234,18 @@ abstract contract LimitOrderHook is BaseHook, IUnlockCallback {
232234
return this.afterInitialize.selector;
233235
}
234236

237+
/**
238+
* @dev Records the tick `key` currently sits at as the baseline the next crossing is measured from,
239+
* without filling anything.
240+
*
241+
* IMPORTANT: A subclass that initializes a pool itself must call this, since the pool does not report
242+
* such an initialization back to the hook.
243+
*/
244+
function _recordTickLowerLast(PoolKey memory key) internal virtual {
245+
PoolId poolId = key.toId();
246+
_tickLowerLasts[poolId] = _getTickLower(_getCurrentTick(poolId), key.tickSpacing);
247+
}
248+
235249
/// @dev Hooks into the `afterSwap` hook to fill the orders the swap crossed.
236250
function _afterSwap(address, PoolKey calldata key, SwapParams calldata params, BalanceDelta, bytes calldata)
237251
internal

src/mocks/general/LimitOrderHookMock.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ contract LimitOrderHookMock is LimitOrderHook {
2020

2121
constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
2222

23+
/**
24+
* @dev Initializes `key` from inside this hook, which the pool does not report back to it. Records the
25+
* baseline when `recordTick`, and models the subclass that forgets to otherwise.
26+
*/
27+
function selfInitialize(PoolKey calldata key, uint160 sqrtPriceX96, bool recordTick) external {
28+
poolManager.initialize(key, sqrtPriceX96);
29+
30+
if (recordTick) _recordTickLowerLast(key);
31+
}
32+
2333
/**
2434
* @dev Swaps `amount` toward `targetTick` from inside this hook's own unlock callback, which the pool
2535
* does not report. Fills the orders it crossed when `fillCrossed`, and models the subclass that forgets

test/general/LimitOrderHook.t.sol

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ contract LimitOrderHookTest is HookTest {
195195
vm.stopPrank();
196196
}
197197

198+
/// @dev A key this hook can initialize itself, distinct from `key` through its tick spacing.
199+
function selfInitKey() internal view returns (PoolKey memory) {
200+
return
201+
PoolKey({
202+
currency0: currency0, currency1: currency1, fee: 3000, tickSpacing: 10, hooks: IHooks(address(hook))
203+
});
204+
}
205+
198206
function initRejectZeroTransferPool()
199207
internal
200208
returns (PoolKey memory poolKey, ERC20RejectZeroTransferMock token, bool tokenIsCurrency0)
@@ -813,6 +821,45 @@ contract LimitOrderHookTest is HookTest {
813821
assertEq(getLiquidityInPosition(key, orderTick, true), liquidity, "liquidity should stay in the pool");
814822
}
815823

824+
function test_fill_selfInitializeDoesNotRecordTheTick() public {
825+
PoolKey memory selfKey = selfInitKey();
826+
827+
hook.selfInitialize(selfKey, TickMath.getSqrtPriceAtTick(6015), false);
828+
829+
assertEq(getCurrentTick(selfKey), 6015, "the pool should be initialized away from tick zero");
830+
assertEq(hook.getTickLowerLast(selfKey.toId()), 0, "the hook should record nothing for it");
831+
}
832+
833+
function test_fill_selfInitializeWithoutRecordFillsUncrossedOrders() public {
834+
PoolKey memory selfKey = selfInitKey();
835+
hook.selfInitialize(selfKey, TickMath.getSqrtPriceAtTick(6015), false);
836+
837+
// an order far below the price, which no swap in this test reaches
838+
hook.placeOrder(selfKey, 3000, false, 1e12);
839+
uint232 orderId = rawOrderIdOf(selfKey, 3000, false);
840+
841+
vm.prank(swapper);
842+
swapToLimit(selfKey, true, -1e6, 6014);
843+
844+
assertGt(getCurrentTick(selfKey), 3000, "the price should stay above the order");
845+
assertTrue(getOrderInfoView(orderId).filled, "the tick-zero baseline fills it anyway");
846+
}
847+
848+
function test_fill_selfInitializeWithRecordLeavesUncrossedOrders() public {
849+
PoolKey memory selfKey = selfInitKey();
850+
hook.selfInitialize(selfKey, TickMath.getSqrtPriceAtTick(6015), true);
851+
852+
assertEq(hook.getTickLowerLast(selfKey.toId()), 6010, "the baseline should be the initialization tick");
853+
854+
hook.placeOrder(selfKey, 3000, false, 1e12);
855+
uint232 orderId = rawOrderIdOf(selfKey, 3000, false);
856+
857+
vm.prank(swapper);
858+
swapToLimit(selfKey, true, -1e6, 6014);
859+
860+
assertFalse(getOrderInfoView(orderId).filled, "an order the price never crossed should stay unfilled");
861+
}
862+
816863
// ------------------------------------- Withdraw ------------------------------------- //
817864

818865
function test_withdraw_notFilled_reverts() public {

0 commit comments

Comments
 (0)