Index / LiquidityPenaltyHook
pattern · MIT · last updated 2026-08-27
LiquidityPenaltyHook
Withhold JIT LP fees in afterAddLiquidity and donate a linear penalty on early remove.
OpenZeppelin’s LiquidityPenaltyHook (MIT) deters just-in-time liquidity. afterAddLiquidity records lastAddedLiquidityBlock and, if the position was topped up inside blockNumberOffset, takes feeDelta into the hook via afterAddLiquidityReturnDelta. afterRemoveLiquidity settles withheld fees, then if the offset has not elapsed, donates a linear remaining-blocks penalty to in-range LPs and returns the rest through afterRemoveLiquidityReturnDelta. Long-term LPs who add continuously must wait the offset before collecting fees. Experimental library code; low-liquidity pools can still be gamed. Copy the full file. Listing is not an audit.
Permission bits
These bits must match the deployed address. Confirm on-chain before you route.
afterAddLiquidityafterRemoveLiquidityafterAddLiquidityReturnDeltaafterRemoveLiquidityReturnDelta
Solidity
Excerpt from src/general/LiquidityPenaltyHook.sol. Copy the full file to implement this. Not an audit.
// SPDX-License-Identifier: MIT
// OpenZeppelin Uniswap Hooks — excerpt. Full file: source.url
function getHookPermissions() public pure virtual override returns (Hooks.Permissions memory permissions) {
return Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: false,
afterAddLiquidity: true,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: true,
beforeSwap: false,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: false,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: true,
afterRemoveLiquidityReturnDelta: true
});
}
function _afterAddLiquidity(
address sender,
PoolKey calldata key,
ModifyLiquidityParams calldata params,
BalanceDelta, /* delta */
BalanceDelta feeDelta,
bytes calldata
) internal virtual override returns (bytes4, BalanceDelta) {
PoolId poolId = key.toId();
bytes32 positionKey = Position.calculatePositionKey(sender, params.tickLower, params.tickUpper, params.salt);
if (_getBlockNumber() - getLastAddedLiquidityBlock(poolId, positionKey) < blockNumberOffset) {
_updateLastAddedLiquidityBlock(poolId, positionKey);
_takeFeesToHook(key, positionKey, feeDelta);
return (this.afterAddLiquidity.selector, feeDelta);
}
_updateLastAddedLiquidityBlock(poolId, positionKey);
return (this.afterAddLiquidity.selector, BalanceDeltaLibrary.ZERO_DELTA);
}Spec
| Kind | pattern |
|---|---|
| Status | experimental |
| License | MIT |
| Source | https://github.com/OpenZeppelin/uniswap-hooks |
| Categories | MEV protectionLP management |
| Properties | Vanilla swap |
| Chains | Ethereum |
| Website | https://v4hooks.com/learn/openzeppelin-hooks |
| Docs | https://github.com/OpenZeppelin/uniswap-hooks |
FAQ
Can I paste this into production? The snippet is an excerpt. Use the full file at the source URL, match flags to the address, and treat this page as a map, not a guarantee.
How do I build this safely? Start with secure v4 hooks and the OpenZeppelin hooks guide.