General
Smart contract general utilities and implementations
import "@openzeppelin/src/general/AntiSandwichHook.sol";
This hook implements the sandwich-resistant AMM design introduced here. Specifically, this hook guarantees that no swaps get filled at a price better than the price at the beginning of the slot window (i.e. one block).
Within a slot window, swaps impact the pool asymmetrically for buys and sells. When a buy order is executed, the offer on the pool increases in accordance with the xy=k curve. However, the bid price remains constant, instead increasing the amount of liquidity on the bid. Subsequent sells eat into this liquidity, while decreasing the offer price according to xy=k.
In order to use this hook, the inheriting contract must implement the _handleCollectedFees
function
to determine how to handle the collected fees from the anti-sandwich mechanism.
NOTE: The Anti-sandwich mechanism only protects swaps in the zeroForOne swap direction. Swaps in the !zeroForOne direction are not protected by this hook design.
Since this hook makes MEV not profitable, there's not as much arbitrage in
the pool, making prices at beginning of the block not necessarily close to market price.
In _beforeSwap
, the hook iterates over all ticks between last tick and current tick.
Developers must be aware that for large price changes in pools with small tick spacing, the for
loop will iterate over a large number of ticks, which could lead to MemoryOOG
error.
This is experimental software and is provided on an "as is" and "as available" basis. We do
not give any warranties and will not be liable for any losses incurred through any use of this code base.
Available since v1.1.0
Functions
- constructor(_poolManager)
- _beforeSwap(sender, key, params, hookData)
- _getBlockNumber()
- _getTargetUnspecified(, key, params, )
- getHookPermissions()
BaseDynamicAfterFee
- _transientTargetUnspecifiedAmount()
- _transientApplyTarget()
- _setTransientTargetUnspecifiedAmount(value)
- _setTransientApplyTarget(value)
- _afterSwap(sender, key, params, delta, )
- _afterSwapHandler(key, params, delta, targetUnspecifiedAmount, feeAmount)
IHookEvents
BaseHook
- _validateHookAddress(hook)
- beforeInitialize(sender, key, sqrtPriceX96)
- _beforeInitialize(, , )
- afterInitialize(sender, key, sqrtPriceX96, tick)
- _afterInitialize(, , , )
- beforeAddLiquidity(sender, key, params, hookData)
- _beforeAddLiquidity(, , , )
- beforeRemoveLiquidity(sender, key, params, hookData)
- _beforeRemoveLiquidity(, , , )
- afterAddLiquidity(sender, key, params, delta0, delta1, hookData)
- _afterAddLiquidity(, , , , , )
- afterRemoveLiquidity(sender, key, params, delta0, delta1, hookData)
- _afterRemoveLiquidity(, , , , , )
- beforeSwap(sender, key, params, hookData)
- afterSwap(sender, key, params, delta, hookData)
- beforeDonate(sender, key, amount0, amount1, hookData)
- _beforeDonate(, , , , )
- afterDonate(sender, key, amount0, amount1, hookData)
- _afterDonate(, , , , )
- poolManager()
IHooks
Events
Errors
constructor(contract IPoolManager _poolManager)
internal
#_beforeSwap(address sender, struct PoolKey key, struct SwapParams params, bytes hookData) → bytes4, BeforeSwapDelta, uint24
internal
#Handles the before swap hook.
For the first swap in a block, it saves the current pool state as a checkpoint.
For subsequent swaps in the same block, it calculates a target output based on the beginning-of-block state,
and sets the inherited _targetOutput
and _applyTargetOutput
variables to enforce price limits in BaseHook._afterSwap
.
_getBlockNumber() → uint48
internal
#Returns the current block number.
_getTargetUnspecified(address, struct PoolKey key, struct SwapParams params, bytes) → uint256 targetUnspecifiedAmount, bool applyTarget
internal
#Calculates the unspecified amount based on the pool state at the beginning of the block. This prevents sandwich attacks by ensuring trades can't get better prices than what was available at the start of the block. Note that the calculated unspecified amount could either be input or output, depending if it's an exactInput or outputOutput swap. In cases of zeroForOne == true, the target unspecified amount is not applicable, and the max uint256 value is returned as a flag only.
The anti-sandwich mechanism works such as:
- For currency0 to currency1 swaps (zeroForOne = true): The pool behaves normally with xy=k curve.
- For currency1 to currency0 swaps (zeroForOne = false): The price is fixed at the beginning-of-block price, which prevents attackers from manipulating the price within a block.
getHookPermissions() → struct Hooks.Permissions permissions
public
#Set the hook permissions, specifically beforeSwap
, afterSwap
, and afterSwapReturnDelta
.
import "@openzeppelin/src/general/LimitOrderHook.sol";
The order id library.
Functions
equals(OrderIdLibrary.OrderId a, OrderIdLibrary.OrderId b) → bool
internal
#Compare two order ids for equality. Takes two OrderId
values a
and b
and
returns whether their underlying values are equal.
unsafeIncrement(OrderIdLibrary.OrderId a) → OrderIdLibrary.OrderId
internal
#Increment the order id a
. Might overflow.
import "@openzeppelin/src/general/LimitOrderHook.sol";
Limit Order Mechanism hook.
Allows users to place limit orders at specific ticks outside of the current price range, which will be filled if the pool's price crosses the order's tick.
Note that given the way UniswapV4 pools works, when liquidity is added out of the current range, a single currency will be provided, instead of both currencies as in in-range liquidity additions.
Orders can be cancelled at any time until they are filled and their liquidity is removed from the pool. Once completely filled, the resulting liquidity can be withdrawn from the pool.
When cancelling or adding more liquidity into an existing order, it's possible that fees
have been accrued. In those cases, the accrued fees are added to the order info, benefitting the remaining limit order placers.
This is experimental software and is provided on an "as is" and "as available" basis. We do
not give any warranties and will not be liable for any losses incurred through any use of this code base.
Available since v1.1.0
Functions
- constructor(_poolManager)
- _afterInitialize(, key, , tick)
- _afterSwap(, key, params, , )
- placeOrder(key, tick, zeroForOne, liquidity)
- cancelOrder(key, tickLower, zeroForOne, to)
- withdraw(orderId, to)
- unlockCallback(rawData)
- _handlePlaceCallback(placeData)
- _handleCancelCallback(cancelData)
- _handleWithdrawCallback(withdrawData)
- _fillOrder(key, tickLower, zeroForOne)
- _getCrossedTicks(poolId, tickSpacing)
- getTickLowerLast(poolId)
- getOrderId(key, tickLower, zeroForOne)
- _getTickLower(tick, tickSpacing)
- getOrderLiquidity(orderId, owner)
- _getTick(poolId)
- getOrderInfo(orderId)
- getHookPermissions()
IUnlockCallback
BaseHook
- _validateHookAddress(hook)
- beforeInitialize(sender, key, sqrtPriceX96)
- _beforeInitialize(, , )
- afterInitialize(sender, key, sqrtPriceX96, tick)
- beforeAddLiquidity(sender, key, params, hookData)
- _beforeAddLiquidity(, , , )
- beforeRemoveLiquidity(sender, key, params, hookData)
- _beforeRemoveLiquidity(, , , )
- afterAddLiquidity(sender, key, params, delta0, delta1, hookData)
- _afterAddLiquidity(, , , , , )
- afterRemoveLiquidity(sender, key, params, delta0, delta1, hookData)
- _afterRemoveLiquidity(, , , , , )
- beforeSwap(sender, key, params, hookData)
- _beforeSwap(, , , )
- afterSwap(sender, key, params, delta, hookData)
- beforeDonate(sender, key, amount0, amount1, hookData)
- _beforeDonate(, , , , )
- afterDonate(sender, key, amount0, amount1, hookData)
- _afterDonate(, , , , )
- poolManager()
IHooks
Events
Errors
constructor(contract IPoolManager _poolManager)
public
#Set the PoolManager
address.
_afterInitialize(address, struct PoolKey key, uint160, int24 tick) → bytes4
internal
#Hooks into the afterInitialize
hook to set the last tick lower for the pool.
_afterSwap(address, struct PoolKey key, struct SwapParams params, BalanceDelta, bytes) → bytes4, int128
internal
#Hooks into the afterSwap
hook to get the ticks crossed by the swap and fill the orders that are crossed, filling them.
placeOrder(struct PoolKey key, int24 tick, bool zeroForOne, uint128 liquidity)
public
#Places a limit order by adding liquidity out of range at a specific tick. The order will be filled when the
pool price crosses the specified tick
. Takes a PoolKey
key
, target tick
, direction zeroForOne
indicating
whether to buy currency0 or currency1, and amount of liquidity
to place. The interaction with the poolManager
is done
via the unlock
function, which will trigger the [
BaseCustomAccounting.unlockCallback](base#BaseCustomAccounting-unlockCallback-bytes-)
function.
cancelOrder(struct PoolKey key, int24 tickLower, bool zeroForOne, address to)
public
#Cancels a limit order by removing liquidity from the pool. Takes a PoolKey
key
, tickLower
of the order,
direction zeroForOne
indicating whether it was buying currency0 or currency1, and recipient address to
for the
removed liquidity. Note that partial cancellation is not supported - the entire liquidity added by the msg.sender will be removed.
Note also that cancelling an order will cancel the order placed by the msg.sender, not orders placed by other users in the same tick range.
The interaction with the poolManager
is done via the unlock
function, which will trigger the [
BaseCustomAccounting.unlockCallback](base#BaseCustomAccounting-unlockCallback-bytes-)
function.
withdraw(OrderIdLibrary.OrderId orderId, address to) → uint256 amount0, uint256 amount1
public
#Withdraws liquidity from a filled order, sending it to address to
. Takes an OrderId
orderId
of the filled
order to withdraw from. Returns the withdrawn amounts as (amount0, amount1)
. Can only be called after the order is
filled - use cancelOrder
to remove liquidity from unfilled orders. The interaction with the poolManager
is done via the
unlock
function, which will trigger the [
BaseCustomAccounting.unlockCallback](base#BaseCustomAccounting-unlockCallback-bytes-)
function.
unlockCallback(bytes rawData) → bytes returnData
public
#Handles callbacks from the PoolManager
for order operations. Takes encoded rawData
containing the callback type
and operation-specific data. Returns encoded data containing fees accrued for cancel operations, or empty bytes
otherwise. Only callable by the PoolManager.
_handlePlaceCallback(struct LimitOrderHook.PlaceCallbackData placeData) → uint256 amount0Fee, uint256 amount1Fee
internal
#Internal handler for place order callbacks. Takes placeData
containing the order details and adds the
specified liquidity to the pool out of range. Reverts if the order would be placed in range or on the wrong
side of the range.
_handleCancelCallback(struct LimitOrderHook.CancelCallbackData cancelData) → uint256 amount0Fee, uint256 amount1Fee
internal
#Internal handler for cancel order callbacks. Takes cancelData
containing the cancellation details and
removes liquidity from the pool. Returns accrued fees (amount0Fee, amount1Fee)
which are allocated to remaining
limit order placers, or to the cancelling user if they're removing all liquidity.
_handleWithdrawCallback(struct LimitOrderHook.WithdrawCallbackData withdrawData)
internal
#Internal handler for withdraw callbacks. Takes withdrawData
containing withdrawal amounts and recipient,
burns the specified currency amounts from the hook, and transfers them to the recipient address.
_fillOrder(struct PoolKey key, int24 tickLower, bool zeroForOne)
internal
#Internal handler for filling limit orders when price crosses a tick. Takes a PoolKey
key
, target tickLower
,
and direction zeroForOne
. Removes liquidity from filled orders, mints the received currencies to the hook, and
updates order state to track filled amounts.
_getCrossedTicks(PoolId poolId, int24 tickSpacing) → int24 tickLower, int24 lower, int24 upper
internal
#Internal helper that calculates the range of ticks crossed during a price change. Takes a PoolId
poolId
and tickSpacing
, returns the current tickLower
and the range of ticks crossed (lower
, upper
) that need
to be checked for limit orders.
getTickLowerLast(PoolId poolId) → int24
public
#Returns the last recorded lower tick for a given pool. Takes a PoolId
poolId
and returns the
stored tickLowerLast
value.
getOrderId(struct PoolKey key, int24 tickLower, bool zeroForOne) → OrderIdLibrary.OrderId
public
#Retrieves the order id for a given pool position. Takes a PoolKey
key
, target tickLower
, and direction
zeroForOne
indicating whether it's buying currency0 or currency1. Returns the OrderIdLibrary.OrderId
associated with this
position, or the default order id if no order exists.
_getTickLower(int24 tick, int24 tickSpacing) → int24
internal
#Get the tick lower. Takes a tick
and tickSpacing
and returns the nearest valid tick boundary
at or below the input tick, accounting for negative tick handling.
getOrderLiquidity(OrderIdLibrary.OrderId orderId, address owner) → uint256
external
#Get the liquidity of an order for a given order id and owner. Takes an OrderIdLibrary.OrderId
orderId
and owner
address
and returns the amount of liquidity the owner has contributed to the order.
_getTick(PoolId poolId) → int24 tick
internal
#Get the current tick for a given pool. Takes a PoolId
poolId
and returns the tick calculated
from the pool's current sqrt price.
getOrderInfo(OrderIdLibrary.OrderId orderId) → bool filled, Currency currency0, Currency currency1, uint256 currency0Total, uint256 currency1Total, uint128 liquidityTotal
external
#Get the order info for a given order id. Takes an OrderIdLibrary.OrderId
orderId
and returns the order info.
getHookPermissions() → struct Hooks.Permissions permissions
public
#Get the hook permissions for this contract. Returns a Hooks.Permissions
struct configured to enable
afterInitialize
and afterSwap
hooks while disabling all other hooks.
Place(address indexed owner, OrderIdLibrary.OrderId indexed orderId, struct PoolKey key, int24 tickLower, bool zeroForOne, uint128 liquidity)
event
#Emitted when an owner
places a limit order with the given orderId
, in the pool identified by key
,
at the given tickLower
, zeroForOne
indicating the direction of the order, and liquidity
the amount of liquidity
added.
Fill(OrderIdLibrary.OrderId indexed orderId, struct PoolKey key, int24 tickLower, bool zeroForOne)
event
#Emitted when a limit order with the given orderId
is filled in the pool identified by key
,
at the given tickLower
, zeroForOne
indicating the direction of the order.
Cancel(address indexed owner, OrderIdLibrary.OrderId indexed orderId, struct PoolKey key, int24 tickLower, bool zeroForOne, uint128 liquidity)
event
#Emitted when an owner
cancels a limit order with the given orderId
, in the pool identified by key
,
at the given tickLower
, zeroForOne
indicating the direction of the order, and liquidity
the amount of liquidity
removed.
Withdraw(address indexed owner, OrderIdLibrary.OrderId indexed orderId, uint128 liquidity)
event
#Emitted when an owner
withdraws their liquidity
from a limit order with the given orderId
, in the pool identified by key
,
at the given tickLower
, zeroForOne
indicating the direction of the order.
ZeroLiquidity()
error
#Zero liquidity was attempted to be added or removed.
InRange()
error
#Limit order was placed in range.
CrossedRange()
error
#Limit order placed on the wrong side of the range.
Filled()
error
#Limit order was already filled.
NotFilled()
error
#Limit order is not filled.
import "@openzeppelin/src/general/LiquidityPenaltyHook.sol";
Just-in-Time (JIT) liquidity provisioning resistant hook.
This hook disincentivizes JIT attacks by penalizing LP fee collection during BaseHook._afterRemoveLiquidity
,
and disabling it during BaseHook._afterAddLiquidity
if liquidity was recently added to the position.
The penalty is donated to the pool's liquidity providers in range at the time of removal.
See LiquidityPenaltyHook._calculateLiquidityPenalty
for penalty calculation.
NOTE: If a long term liquidity provider adds liquidity continuously, a pause of blockNumberOffset
before removing will be needed if feesAccrued
collection is intended, in order to avoid getting
penalized by the JIT protection mechanism.
Altrough this hook achieves it's objective of protecting long term LP's in most scenarios,
low liquidity pools and long-tail assets may still be vulnerable depending on the configured blockNumberOffset
.
Larger values of such are recommended in those cases in order to decrease the profitability of the attack.
In low liquidity pools, this hook may be vulnerable to multi-account strategies: attackers may bypass JIT protection
by using a secondary account to add minimal liquidity at a target tick with no other liquidity, then moving the price there after a JIT attack. This allows penalty fees to be redirected to the attacker's secondary account. While technically feasible, this attack is rarely profitable in practice, due to the cost associated with moving the price to the target tick.
This is experimental software and is provided on an "as is" and "as available" basis. We do
not give any warranties and will not be liable for any losses incurred through any use of this code base.
Available since v0.1.1
Functions
- constructor(_poolManager, _blockNumberOffset)
- _afterAddLiquidity(sender, key, params, , feeDelta, )
- _afterRemoveLiquidity(sender, key, params, , feeDelta, )
- _getBlockNumber()
- _updateLastAddedLiquidityBlock(poolId, positionKey)
- _takeFeesToHook(key, positionKey, feeDelta)
- _settleFeesFromHook(key, positionKey)
- _calculateLiquidityPenalty(feeDelta, lastAddedLiquidityBlock)
- getLastAddedLiquidityBlock(poolId, positionKey)
- getWithheldFees(poolId, positionKey)
- getHookPermissions()
- MIN_BLOCK_NUMBER_OFFSET()
- blockNumberOffset()
BaseHook
- _validateHookAddress(hook)
- beforeInitialize(sender, key, sqrtPriceX96)
- _beforeInitialize(, , )
- afterInitialize(sender, key, sqrtPriceX96, tick)
- _afterInitialize(, , , )
- beforeAddLiquidity(sender, key, params, hookData)
- _beforeAddLiquidity(, , , )
- beforeRemoveLiquidity(sender, key, params, hookData)
- _beforeRemoveLiquidity(, , , )
- afterAddLiquidity(sender, key, params, delta0, delta1, hookData)
- afterRemoveLiquidity(sender, key, params, delta0, delta1, hookData)
- beforeSwap(sender, key, params, hookData)
- _beforeSwap(, , , )
- afterSwap(sender, key, params, delta, hookData)
- _afterSwap(, , , , )
- beforeDonate(sender, key, amount0, amount1, hookData)
- _beforeDonate(, , , , )
- afterDonate(sender, key, amount0, amount1, hookData)
- _afterDonate(, , , , )
- poolManager()
IHooks
Errors
constructor(contract IPoolManager _poolManager, uint48 _blockNumberOffset)
public
#Sets the PoolManager
address and the getBlockNumberOffset
.
_afterAddLiquidity(address sender, struct PoolKey key, struct ModifyLiquidityParams params, BalanceDelta, BalanceDelta feeDelta, bytes) → bytes4, BalanceDelta
internal
#Tracks lastAddedLiquidityBlock
and withholds feeDelta
if liquidity was recently added within
the blockNumberOffset
period.
See BaseHook._afterRemoveLiquidity
for claiming the withheld fees back.
_afterRemoveLiquidity(address sender, struct PoolKey key, struct ModifyLiquidityParams params, BalanceDelta, BalanceDelta feeDelta, bytes) → bytes4, BalanceDelta
internal
#Penalizes the collection of any existing LP feesDelta
and withheldFees
after liquidity removal if
liquidity was recently added to the position.
NOTE: The penalty is applied on both withheldFees
and feeDelta
equally.
Therefore, regardless of how many times liquidity was added to the position within the blockNumberOffset
period,
all accrued fees are penalized as if the liquidity was added only once during that period. This ensures that
splitting liquidity additions within the blockNumberOffset
period does not reduce or increase the penalty.
The penalty is donated to the pool's liquidity providers in range at the time of liquidity removal,
which may be different from the liquidity providers in range at the time of liquidity addition.
_getBlockNumber() → uint48
internal
#Returns the current block number.
_updateLastAddedLiquidityBlock(PoolId poolId, bytes32 positionKey)
internal
#Updates the lastAddedLiquidityBlock
for a liquidity position.
_takeFeesToHook(struct PoolKey key, bytes32 positionKey, BalanceDelta feeDelta)
internal
#Takes feeDelta
from a liquidity position as withheldFees
into this hook.
_settleFeesFromHook(struct PoolKey key, bytes32 positionKey) → BalanceDelta withheldFees
internal
#Returns withheldFees
from this hook to the liquidity provider.
_calculateLiquidityPenalty(BalanceDelta feeDelta, uint48 lastAddedLiquidityBlock) → BalanceDelta liquidityPenalty
internal
#Calculates the penalty to be applied to JIT liquidity provisioning.
The penalty is calculated as a linear function of the block number difference between the lastAddedLiquidityBlock
and the currentBlockNumber
.
The used formula is:
liquidityPenalty = feeDelta * ( 1 - (currentBlockNumber - lastAddedLiquidityBlock) / blockNumberOffset)
As a result, the penalty is 100% at the same block where liquidity was last added and zero after the blockNumberOffset
block time window.
NOTE: Won't overflow if currentBlockNumber - lastAddedLiquidityBlock < blockNumberOffset
is verified prior to calling this function.
getLastAddedLiquidityBlock(PoolId poolId, bytes32 positionKey) → uint48
public
#Tracks the lastAddedLiquidityBlock
for a liquidity position.
lastAddedLiquidityBlock
is the block number when liquidity was last added to the position.
getWithheldFees(PoolId poolId, bytes32 positionKey) → BalanceDelta
public
#Returns the withheldFees
for a liquidity position.
withheldFees
are UniswapV4's feesAccrued
retained by this hook during liquidity addition if liquidity
has been recently added within the blockNumberOffset
block time window, with the purpose of disabling fee
collection during JIT liquidity provisioning attacks. See BaseHook._afterRemoveLiquidity
for claiming the fees back.
getHookPermissions() → struct Hooks.Permissions permissions
public
#Set the hooks permissions, specifically afterAddLiquidity
, afterAddLiquidityReturnDelta
, afterRemoveLiquidity
and afterRemoveLiquidityReturnDelta
.
MIN_BLOCK_NUMBER_OFFSET() → uint48
public
#The minimum value for the LiquidityPenaltyHook.blockNumberOffset
.
blockNumberOffset() → uint48
public
#The minimum time window (in blocks) that must pass after adding liquidity before it can be removed without any penalty. During this period, JIT attacks are deterred through fee withholding and penalties. Higher values provide stronger JIT protection but may discourage legitimate LPs.
BlockNumberOffsetTooLow()
error
#The hook was attempted to be constructed with a blockNumberOffset
lower than MIN_BLOCK_NUMBER_OFFSET
.
NoLiquidityToReceiveDonation()
error
#A penalty was attempted to be applied and donated to LP's in range, but there aren't any.