# HookForge > A catalogue of Uniswap v4 hooks nobody has built: novel AMM mechanisms in audited-shape Solidity, each with its prior art and its limitations stated, a TypeScript SDK, an MCP server for agents, and deployments on Base, Arbitrum, Unichain and Robinhood Chain. 15 hooks are shipped: contract, tests, deploy script, manifest and documentation, MIT licensed, no admin keys. Source: https://github.com/nirholas/hookforge Every hook also describes itself on-chain through IHookMetadata (hookName, hookVersion, specURI, hookTags), so a hook address can be identified in one eth_call with no registry in the loop. ## Hooks - [AgentBudget](https://hookforge.pages.dev/hooks/agent-budget/): A spending limit the venue itself enforces: an autonomous agent may swap on this pool only within a budget its principal signed, and the pool refuses the swap that would exceed it. - [X402Gate](https://hookforge.pages.dev/hooks/x402-gate/): Makes the pool an x402 resource server: a swapper that presents a signed x402 payment gets a cheaper fee on that swap, and the payment settles atomically with the trade. - [RatchetFloor](https://hookforge.pages.dev/hooks/ratchet-floor/): A price floor that only ever moves up. - [AntiSnipeRamp](https://hookforge.pages.dev/hooks/anti-snipe-ramp/): Opens a pool at a punitive fee that decays to normal over a fixed window, and pays every cent of the difference to liquidity providers rather than to the deployer. - [LiquidityFloor](https://hookforge.pages.dev/hooks/liquidity-floor/): A liquidity commitment enforced per position: a fraction of what you add cannot leave until the pool's unlock date, and every provider is held to that fraction of their own stake rather than to a shared pool total. - [ArbTaxDecay](https://hookforge.pages.dev/hooks/arb-tax-decay/): Prices the staleness of a pool: the longer a pool goes untraded, the more the next swap pays. - [FlowClassifier](https://hookforge.pages.dev/hooks/flow-classifier/): Publishes, on-chain, how much of a pool's order flow arrives first in the block and how far it moves the price when it does. It changes nothing about the pool it measures. - [MarkoutFee](https://hookforge.pages.dev/hooks/markout-fee/): A fee that learns. The pool measures whether its own past trades turned out to be informed, and charges the next one accordingly. - [PriorityFeeTax](https://hookforge.pages.dev/hooks/priority-fee-tax/): Charges a swap in proportion to what it paid the block producer to get where it is in the block. - [CircuitBreaker](https://hookforge.pages.dev/hooks/circuit-breaker/): Halts swapping for a cooldown after the price moves further than a pool is willing to move in one window, and lets liquidity leave the whole time. - [DepegShield](https://hookforge.pages.dev/hooks/depeg-shield/): Makes leaving a peg expensive and returning to it cheap, in proportion to how far the pool has already strayed. - [DrawdownCap](https://hookforge.pages.dev/hooks/drawdown-cap/): A limit down. The pool may not fall more than a fixed distance below where the current epoch opened, and the limit resets on a schedule rather than on anyone's say-so. - [OracleBand](https://hookforge.pages.dev/hooks/oracle-band/): Refuses to let a pool settle at a price the wider market does not recognise. - [ExpirySettle](https://hookforge.pages.dev/hooks/expiry-settle/): Gives a pool a maturity, and makes moving its price monotonically more expensive as that maturity approaches, so the settlement price is dearest to manipulate exactly when manipulating it would pay most. - [TradingCalendar](https://hookforge.pages.dev/hooks/trading-calendar/): Gives a pool a trading session, and closes it by raising the price of immediacy rather than by reverting. ## Documentation - [Start here](https://hookforge.pages.dev/docs/): what a v4 hook is, how to run and use these. - [TypeScript SDK](https://hookforge.pages.dev/docs/sdk/): catalogue, address book, pool keys, permission decoding. - [MCP server](https://hookforge.pages.dev/docs/mcp/): six tools for agents; npx -y @hookforge/mcp - [On-chain metadata](https://hookforge.pages.dev/docs/metadata/): the IHookMetadata interface. - [Deploy a hook](https://hookforge.pages.dev/docs/deploy/): mining a CREATE2 salt so the address encodes the permission flags. - [For agents](https://hookforge.pages.dev/docs/agents/): every machine-readable endpoint. ## Machine-readable - https://hookforge.pages.dev/api/hooks.json - https://hookforge.pages.dev/api/chains.json - https://hookforge.pages.dev/schema/hooks/.json - https://hookforge.pages.dev/llms-full.txt ## Important caveats - These contracts are unaudited. - A deployment whose status is "deterministic" is a mined CREATE2 address with no code at it yet. Never present one as live. - Fee-overriding hooks require a pool initialized with the dynamic-fee flag and revert otherwise. --- # AgentBudget Slug: agent-budget Family: Agent-native Contract: AgentBudgetHook Tags: agent, delegation, spending-limit, eip712, no-admin Page: https://hookforge.pages.dev/hooks/agent-budget/ Manifest: https://hookforge.pages.dev/schema/hooks/agent-budget.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/AgentBudgetHook.sol Callbacks: afterSwap Parameters: none ## Summary A spending limit the venue itself enforces: an autonomous agent may swap on this pool only within a budget its principal signed, and the pool refuses the swap that would exceed it. ## How it works Handing a key to an autonomous agent means choosing between two bad options. Approve a router for a small amount and the agent stalls the moment it needs more, at which point somebody has to be awake to raise it. Approve it for the usual unbounded amount and the only thing standing between a bug and the whole balance is the agent's own code, which is the thing you were trying not to trust. Smart accounts answer this with a policy engine, which works and costs you a smart account: a migration, a new address, a new set of integrations, and a policy layer that every venue has to be taught about. This hook puts the limit somewhere neither of those touch, in the pool, where it applies to any wallet, any router and any account type, because it is enforced by the venue rather than by the spender. A principal signs one `Delegation`, off-chain and once: an agent address, a per-epoch cap in each of the pool's two currencies, an epoch length, an expiry. The agent then signs each swap it makes under that delegation. On `afterSwap` the hook checks both signatures, measures what the swap actually spent from the balance delta, and reverts if that would take the agent past its cap for the current epoch. A revert in `afterSwap` unwinds the swap with it, so an over-budget trade cannot land. Measuring in `afterSwap` rather than `beforeSwap` is deliberate and is what makes the cap honest. Before the swap the only figure available is `amountSpecified`, which on an exact-output swap says nothing about how much the swapper will actually pay; a budget checked against it would be trivially evaded by asking for an exact output and letting the input land wherever the curve puts it. After the swap the true spend is in the delta. What this does not do, stated plainly, because the distinction matters: it never custodies funds, never moves a token, and grants no allowance. The agent still needs its own ERC-20 approval to trade at all. The hook only refuses to let this pool be the venue for a swap outside the budget. An agent with an unbounded approval can still spend elsewhere, so this is a limit on a venue, not on a key, and it is worth exactly as much as the set of venues that enforce it. Both signatures are checked with ERC-1271 as well as ECDSA, so a principal or an agent may itself be a contract. ## Prior art Per-agent policy engines exist in smart-account land (session keys, ERC-7710 delegations, module-based spending limits), and hooks that gate swaps on an allowlist or a credential are common. Enforcing a signed, per-epoch, per-currency spending cap inside the AMM, on behalf of an EOA principal with no smart account anywhere in the path, is the contribution here. ## Where it does not help The cap binds this pool only. An agent holding an unbounded ERC-20 approval can spend the same funds on any venue that does not enforce the delegation, so this raises the cost of a compromised agent rather than bounding it absolutely. It also requires the caller to pass hookData, so an aggregator that strips it will simply be unable to trade the pool. --- # X402Gate Slug: x402-gate Family: Agent-native Contract: X402GateHook Tags: agent-native, x402, dynamic-fee, metering Page: https://hookforge.pages.dev/hooks/x402-gate/ Manifest: https://hookforge.pages.dev/schema/hooks/x402-gate.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/X402GateHook.sol Callbacks: beforeSwap, afterInitialize Parameters: asset (address), payTo (address), price (uint256), baseFee (uint24), discountedFee (uint24) ## Summary Makes the pool an x402 resource server: a swapper that presents a signed x402 payment gets a cheaper fee on that swap, and the payment settles atomically with the trade. ## How it works x402 is how autonomous agents already pay for things. An agent asks for a resource, gets back HTTP 402 with a machine-readable price, signs an EIP-3009 authorization for that amount, retries with the signature in a header, and the resource server settles it. Thousands of endpoints speak it and every agent framework has a client for it. What nothing speaks it for is liquidity, which is odd, because a fee tier is exactly the kind of thing an agent would want to buy: it is metered, it is worth different amounts to different callers, and the caller knows its own value better than the venue does. The usual way to give one class of swapper a better price is to gate on what they *are*: hold this NFT, be on this allowlist, pass this KYC check. Every one of those is a proxy for willingness to pay, maintained by hand, and wrong at the edges. This hook gates on the thing itself. Pay the pool's posted price and the swap is cheaper. Do not, and it is not. Nobody curates anything. Three properties fall out of doing this in the hook rather than over HTTP: The 402 challenge is on-chain. {quote} returns the same fields an x402 client reads out of a 402 response body: scheme, network, amount, asset, recipient, resource, timeout. An agent discovers the price with one `eth_call` against the pool it was already going to trade, with no endpoint to find, no server to be up, and no TLS. Payment and delivery are atomic, which over HTTP they are not. An x402 client that pays for an API call and then receives a 500 has paid for nothing and must argue about a refund. Here the payment settles inside `beforeSwap`, so if the swap reverts for any reason afterwards, slippage, liquidity, another hook, the payment reverts with it. The failure mode that makes x402 awkward to build on does not exist in this direction. There is no facilitator. The x402 deployment model puts a trusted service between payer and resource server to verify and broadcast the authorization. The pool can do both itself, because it is already a contract and the payment is already an on-chain object, so the trusted third party is simply absent rather than decentralized. How the discount is applied matters. The fee this hook returns is an *LP* fee, so the discount is paid for by liquidity providers taking less on that swap, and the payment is what compensates them. Providers are therefore selling cheap execution for a fixed fee up front instead of a variable one on the back end, which is a trade they can price: if the posted price is set below the fee revenue given up, the pool leaks, and setting it is the one judgement the pool creator has to get right. Payments are pulled with `receiveWithAuthorization` rather than `transferWithAuthorization`. Both are EIP-3009 and x402's own reference facilitator uses the latter, but the latter can be broadcast by anyone: a bystander could submit the payer's authorization on its own, the payment would land, the nonce would burn, and the swap that the payment was for would then revert with the payer out of pocket. Requiring `msg.sender == to` removes that. Unrecognised `hookData` is ignored rather than rejected. The payload is prefixed with {X402_PAYMENT_MAGIC}, and anything that does not start with it is treated as "no payment offered" and charged the base fee. A pool that reverted on hookData it did not understand would be untradeable through any router that puts its own data there, which is most of them. ## Prior art Fee discounts gated on NFT or token ownership, allowlists and KYC attestations are among the most common hook patterns, and x402 itself is a widely deployed HTTP payment protocol with an EIP-3009 settlement scheme. Neither has met the other: making an AMM pool an x402 resource server, so that the 402 challenge is an `eth_call` and settlement is atomic with the swap it paid for, is the contribution here. ## Where it does not help A payment costs a full EIP-3009 transfer, so the discount only repays its own gas above a swap size that depends on the chain and the spread between the two fee tiers. On a mainnet-priced chain that floor is high enough that this is a hook for agents moving real size, not for retail swaps. It also inherits the hookData problem: an aggregator that does not forward hookData cannot present a payment, and its users silently pay the base fee rather than getting an error telling them why. --- # RatchetFloor Slug: ratchet-floor Family: Curves Contract: RatchetFloorHook Tags: price-floor, launch, no-admin, oracle-free Page: https://hookforge.pages.dev/hooks/ratchet-floor/ Manifest: https://hookforge.pages.dev/schema/hooks/ratchet-floor.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/RatchetFloorHook.sol Callbacks: afterSwap, afterInitialize Parameters: offsetTicks (uint24) ## Summary A price floor that only ever moves up. ## How it works A token's floor price is normally a promise: a treasury that says it will bid, a team that says it will buy back. Promises are only as good as the balance behind them and the people holding the keys. This hook makes the floor a property of the pool instead. It records the highest tick the pool has ever reached and refuses to let the price settle more than `offsetTicks` below it: floor = max(floor, highWaterTick - offsetTicks) The floor is monotone by construction. It has no setter, no owner and no emergency path, so it cannot be lowered by anyone, including whoever deployed the pool. A rally raises it permanently; a decline never moves it. How a swap meets the floor matters, so it is worth being precise. Uniswap v4 swaps already take a `sqrtPriceLimitX96`, and {sqrtPriceFloorX96} returns exactly the value to pass: a swap carrying it fills as much as the floor allows and stops there, which is the behaviour a seller wants. The `afterSwap` check is the backstop for callers that pass no limit, and for those the swap reverts rather than partially filling. Routers should read the floor; the revert exists so that a router which does not cannot break the invariant. One tick is one basis point to within rounding, so `offsetTicks = 2000` is a floor twenty percent below the high. ## Prior art Floor prices are usually a treasury commitment (protocol-owned liquidity, OHM-style backing) or a buyback hook that spends fees defending a level. Both depend on a balance and on whoever can move it. Enforcing a monotone floor as an invariant of the pool, with no treasury and no key, is a different construction: nothing is spent defending it and nothing can lower it. ## Where it does not help This guarantees the pool will not print below the floor. It does not guarantee anyone can sell at the floor, because it holds no capital: once the price reaches the floor there is simply no more selling into the pool, and a holder who wants out has to wait for the price to recover or trade elsewhere. It converts a liquidity risk into a liquidity halt, honestly and predictably, but it does not make the risk disappear. A pool that needs a real bid at the floor needs a treasury behind it, and this is not that. --- # AntiSnipeRamp Slug: anti-snipe-ramp Family: Launch Contract: AntiSnipeRampHook Tags: launch, anti-snipe, dynamic-fee, no-admin Page: https://hookforge.pages.dev/hooks/anti-snipe-ramp/ Manifest: https://hookforge.pages.dev/schema/hooks/anti-snipe-ramp.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/AntiSnipeRampHook.sol Callbacks: beforeSwap, afterInitialize Parameters: startFee (uint24), endFee (uint24), rampSeconds (uint32), maxSwapDuringRamp (uint128) ## Summary Opens a pool at a punitive fee that decays to normal over a fixed window, and pays every cent of the difference to liquidity providers rather than to the deployer. ## How it works The first block of a new pool is the most valuable block it will ever have. A bot that buys in it and sells an hour later takes the entire launch premium, and everyone who arrives through the front door pays for it. The usual answers are a whitelist, which is a promise rather than a mechanism, or a bonding curve that hands the premium to the deployer, which moves the extraction rather than removing it. This hook makes the first block expensive to trade in and lets that expense decay: fee(t) = startFee - (startFee - endFee) * min(t, rampSeconds) / rampSeconds A sniper in the first second pays `startFee`, which can be set high enough that the trade is not worth making. A buyer twenty minutes later pays close to `endFee`. Because the fee is an LP fee, the premium the early trader surrenders is paid to the people who put the liquidity up, not to whoever deployed the token. There is no address in this contract that can receive anything. A second lever handles the case where the fee alone is not enough. While the ramp is running, a single swap may not exceed `maxSwapDuringRamp` units of the specified currency. This is a size cap, not an identity check, and it is deliberately not per-address: a hook sees the router that called the `PoolManager`, not the person behind it, so any per-address limit is a limit on routers and is defeated by a fresh key. Capping size is enforceable against everyone equally, including the deployer. Set it to zero to disable it. Prior art: liquidity bootstrapping pools ramp the *price* down and were built for price discovery; several launchpad hooks charge a launch fee and route it to a creator or a protocol treasury. Ramping the *fee* down while directing the proceeds to liquidity is a different mechanism with a different beneficiary, and it composes with any curve rather than replacing it. ## Prior art Liquidity bootstrapping pools ramp the price down and were built for price discovery; several launchpad hooks charge a launch fee and route it to a creator or a treasury. Ramping the fee down while directing the proceeds to liquidity is a different mechanism with a different beneficiary, and it composes with any curve rather than replacing it. ## Where it does not help The size cap is per swap, not per address: a hook sees the router that called the PoolManager, not the person behind it, so a determined buyer can split across transactions. The cap raises the cost of sniping rather than preventing it, and the fee ramp is what does the real work. --- # LiquidityFloor Slug: liquidity-floor Family: Liquidity provider economics Contract: LiquidityFloorHook Tags: lp-economics, commitment, launch, rug-resistance Page: https://hookforge.pages.dev/hooks/liquidity-floor/ Manifest: https://hookforge.pages.dev/schema/hooks/liquidity-floor.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/LiquidityFloorHook.sol Callbacks: beforeRemoveLiquidity, afterAddLiquidity, afterInitialize Parameters: floorBps (uint16), unlockTimestamp (uint64) ## Summary A liquidity commitment enforced per position: a fraction of what you add cannot leave until the pool's unlock date, and every provider is held to that fraction of their own stake rather than to a shared pool total. ## How it works A new pool has a bootstrapping problem that is really a credibility problem. Swappers cannot tell the difference between liquidity that intends to stay and liquidity that will leave the moment the pool is quoted by an aggregator, and the second kind is indistinguishable from the first right up until it is gone. The published answers lock whole positions for a term. That works and it is also badly mispriced: a provider who would happily commit a third of their capital for six months has to choose between committing all of it and committing none. Term locks therefore select for the providers least sensitive to the lock, which is not the same set as the providers most useful to the pool. This hook takes the commitment as a *fraction*. Configure `floorBps` and `unlockTimestamp`, and thereafter each position may freely withdraw down to `floorBps` of everything it has ever added, with the remainder released at the unlock. Adding more liquidity raises your own floor proportionally, so topping up is never a trap: you keep the same ratio of free to committed capital that you signed up for. The design is deliberately race-free, which is the property that distinguishes it from a pool-wide minimum. A floor expressed as "total pool liquidity must stay above X" is a bank run waiting to happen: it is satisfiable by whoever withdraws first and binding only on whoever is last, so rational providers race for the exit precisely when the pool most needs them. Holding each position to its own commitment removes the race entirely. Nothing another provider does can change what you are allowed to withdraw. The hook takes no fee, holds no funds and has no privileged role. It cannot stop a provider from ceasing to quote, only from removing committed liquidity, and it does not restrict swaps at all. Position identity is the v4 position key: the address that called `modifyLiquidity` on the `PoolManager` (in practice a position manager or a router), the tick range, and the caller's salt. Two providers sharing one position manager therefore share a commitment only if they also share a salt, which position managers do not do. Prior art: `LiquidityLock`, `Timelock Addition` and `LockingLiquidity` all lock positions wholesale for a term. Fractional, per-position, top-up-safe commitments are the contribution here. ## Prior art LiquidityLock, Timelock Addition and LockingLiquidity all lock positions wholesale for a term. Fractional, per-position, top-up-safe commitments are the contribution here, along with the observation that a pool-wide minimum is a bank run rather than a floor. ## Where it does not help The commitment binds the v4 position key, which is the address that called modifyLiquidity. A provider who routes through a position manager that pools many users under one key would share a commitment with them; every mainstream position manager gives each position its own key, but a custom router need not. --- # ArbTaxDecay Slug: arb-tax-decay Family: Order flow and MEV Contract: ArbTaxDecayHook Tags: mev, lvr, dynamic-fee, oracle-free Page: https://hookforge.pages.dev/hooks/arb-tax-decay/ Manifest: https://hookforge.pages.dev/schema/hooks/arb-tax-decay.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/ArbTaxDecayHook.sol Callbacks: beforeSwap, afterInitialize Parameters: baseFee (uint24), maxSurcharge (uint24), halfLife (uint32) ## Summary Prices the staleness of a pool: the longer a pool goes untraded, the more the next swap pays. ## How it works Loss-versus-rebalancing is the dominant cost of providing liquidity to a constant-function AMM. It is paid when an arbitrageur brings a stale pool price back to the market price, and the size of that arbitrage grows with how long the pool sat unpriced. Ordinary flow does not have this property: a swap that lands one second after another swap is almost certainly not an arbitrage, because there was no time for the reference price to drift. This hook turns that observation into a fee. It measures the time since the pool last traded and adds a surcharge that grows with it along a saturating curve, capped at `maxSurcharge`: surcharge(elapsed) = maxSurcharge * elapsed / (elapsed + halfLife) At `elapsed == halfLife` the arbitrageur pays half the cap; a swap in the same second as the previous one pays only `baseFee`. The surcharge is an LP fee, so the value it captures is paid to in-range liquidity providers. The hook never custodies funds and holds no privileged role. Two properties make this cheap to reason about. It needs no oracle, so there is nothing to manipulate and no liveness dependency. And it is monotone in a quantity the arbitrageur cannot control: waiting longer to arbitrage a pool only raises the toll, so the strategy that minimizes the tax is to trade the pool more often, which is exactly the behaviour that keeps the price fresh for everyone else. Prior art: dynamic-fee hooks keyed on realized volatility or on price movement are common, and the LVR literature (Milionis, Moallemi, Roughgarden, Zhang) motivates charging arbitrageurs more. Keying the fee on time-since-last- trade rather than on a price signal is the part that is new here, and it is what removes the oracle. Limitation, stated plainly: on a pool that trades continuously the surcharge is near zero, so this hook does nothing for a busy major pair. It is aimed at the long tail, where pools are quiet for minutes or hours at a time and the arbitrage on the first trade back is the whole of the LP's loss. ## Prior art Dynamic-fee hooks keyed on realized volatility or on price movement are common, and the loss-versus-rebalancing literature (Milionis, Moallemi, Roughgarden, Zhang) motivates charging arbitrageurs more. Keying the fee on time since the last trade rather than on a price signal is what is new here, and it is what removes the oracle. ## Where it does not help On a pool that trades continuously the surcharge is near zero, so this does nothing for a busy major pair. It is aimed at the long tail, where pools sit quiet for minutes or hours and the arbitrage on the first trade back is the whole of the provider loss. --- # FlowClassifier Slug: flow-classifier Family: Order flow and MEV Contract: FlowClassifierHook Tags: mev, public-good, analytics, order-flow, oracle-free Page: https://hookforge.pages.dev/hooks/flow-classifier/ Manifest: https://hookforge.pages.dev/schema/hooks/flow-classifier.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/FlowClassifierHook.sol Callbacks: afterSwap, beforeSwap Parameters: none ## Summary Publishes, on-chain, how much of a pool's order flow arrives first in the block and how far it moves the price when it does. It changes nothing about the pool it measures. ## How it works Almost every fee mechanism in this catalogue is a guess about who is trading. The staleness tax guesses from elapsed time. The priority-fee tax guesses from what a trade bid for its position. Each one re-derives the same hidden quantity privately, none of them publishes it, and no two of them agree. The quantity they are all reaching for has a clean on-chain signature. Arbitrage arrives first in its block, because being second is worthless when the whole trade is closing a gap somebody else can close instead. Ordinary flow does not care where in a block it lands. So the split between swaps that led their block and swaps that followed one is a measurable proxy for the split between informed and uninformed flow, and the ratio of how far each population moves the price is a measure of how expensive the informed half is. This hook counts both and publishes them through {IFlowStats}: - `leadShareBps`: the fraction of swaps that were first in their block. - `leadImpactRatioBps`: how much further the average leading swap moves the price than the average following one. `10_000` means they move it equally. A pool being arbitraged reads well above that. The point is that it is a public good rather than a mechanism. It sets no fee, returns no delta, takes no payment and rejects nothing; a test asserts that a swap through a measured pool receives exactly what the same swap through an identical unhooked pool receives. Other hooks can price from it, routers can prefer pools whose flow is cheap to fill, providers can decide whether a pool is worth quoting, and indexers can rank pools by something more meaningful than volume. Counters saturate rather than wrap. A wrapped counter reports a small number where a huge one belongs and every ratio derived from it becomes quietly wrong; a saturated one stops moving and keeps the last true value, which is a failure a reader can notice. ## Prior art Off-chain, order-flow toxicity is standard: VPIN, markout, and the lead-lag analysis every market maker runs on its own fills. On-chain, hooks consume such signals privately to set a fee. Publishing the measurement itself, from a hook that deliberately does nothing else, so that every other contract can read one pool's flow quality instead of each guessing at it, is the contribution here. ## Where it does not help First-in-block is a proxy, not a fact. A private-mempool arbitrage that lands second still leads economically, and an ordinary swap that happens to be first is counted as leading. The measure is meaningful in aggregate over many blocks and says nothing reliable about any single swap. On a chain with sub-second blocks where most blocks hold one swap, nearly everything leads and the ratio degenerates. --- # MarkoutFee Slug: markout-fee Family: Order flow and MEV Contract: MarkoutFeeHook Tags: mev, dynamic-fee, markout, adverse-selection, oracle-free Page: https://hookforge.pages.dev/hooks/markout-fee/ Manifest: https://hookforge.pages.dev/schema/hooks/markout-fee.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/MarkoutFeeHook.sol Callbacks: afterSwap, beforeSwap, afterInitialize Parameters: baseFee (uint24), maxSurcharge (uint24), alphaWad (uint64), minHorizon (uint32), saturationTicks (uint24) ## Summary A fee that learns. The pool measures whether its own past trades turned out to be informed, and charges the next one accordingly. ## How it works Every dynamic-fee hook published so far prices a swap from something observable at the moment it arrives: the size, the recent volatility, the gas it bid, the distance from an oracle. All of those are proxies for the question that actually matters, which is whether the person on the other side knew something. None of them measure it, because at the moment a swap arrives that fact has not happened yet. It has happened a few seconds later. If a swap pushed the price down and the price kept falling, the seller was right and the liquidity that filled them lost; that is an informed trade, and it is the cost the LP literature calls adverse selection. If the price came back, the swap was noise and the fee it paid was pure revenue. This is the markout that every market maker computes on their own flow, and a pool has everything needed to compute it: the tick it was left at, and the tick it is at now. So this hook grades each swap after the fact. On the next swap it compares the current tick to the tick the previous swap left behind, decides whether that swap was informed, and folds the verdict into an exponentially weighted average. The fee it quotes is: fee = baseFee + maxSurcharge * toxicity A pool whose flow is mostly retail converges toward `baseFee` and becomes cheap. A pool being picked off converges toward the cap and becomes expensive, without anybody deciding that, and without an oracle to manipulate or a governance process to capture. The score is public, so anything else on-chain can read a pool's measured toxicity rather than guessing at it. Two details keep the measurement honest. A verdict is only recorded once `minHorizon` seconds have passed, because a comparison inside the same block is measuring the swap's own price impact rather than what happened next. And the verdict is graded by magnitude rather than treated as a coin flip: a move that continues by a tenth of a tick is weak evidence, a move that continues by a hundred ticks is strong, so the observation is scaled and clamped instead of being rounded to a yes or a no. {FlowClassifierHook} measures a related quantity and deliberately does nothing with it; this hook is the other half of that pair, the one that acts on what it measures. The hook takes no fee for itself, custodies nothing, and has no privileged role. `baseFee`, the cap, the smoothing and the horizon are fixed before the pool exists and can never be changed. ## Prior art Dynamic fees keyed on realized volatility, swap size, price movement or an oracle gap are all well covered, and markout is the standard way a market maker grades its own flow off-chain. Computing markout on-chain, from the pool's own tick history, and feeding it back as the pool's fee, is the contribution here. It is the difference between reacting to a proxy for adverse selection and measuring the thing itself. ## Where it does not help The verdict on a swap arrives with the next swap, so a pool that trades once a day prices today's flow on yesterday's evidence, and a pool with no second swap never grades the first. It is a lagging signal by construction: a regime change is paid for at the old rate until the average catches up. Note also that a sustained one-directional run grades as informed, because every swap in it is followed by one pushing the same way. That is the intended reading rather than a flaw, but it does mean a pool tracking a strong trend will quote its trend-following flow expensively even when that flow is not picking anyone off. --- # PriorityFeeTax Slug: priority-fee-tax Family: Order flow and MEV Contract: PriorityFeeTaxHook Tags: mev, dynamic-fee, order-flow, oracle-free Page: https://hookforge.pages.dev/hooks/priority-fee-tax/ Manifest: https://hookforge.pages.dev/schema/hooks/priority-fee-tax.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/PriorityFeeTaxHook.sol Callbacks: beforeSwap, afterInitialize Parameters: baseFee (uint24), maxSurcharge (uint24), halfPriorityWei (uint128) ## Summary Charges a swap in proportion to what it paid the block producer to get where it is in the block. ## How it works Position in a block is worth something only to flow that is racing: an arbitrageur closing a gap against a centralized venue, a liquidator, a sandwicher. A person swapping a hundred dollars of one token for another does not care whether they land at index 3 or index 30, and does not bid for it. So the priority fee a transaction attaches is a revealed measure of how much the trade is worth to the trader beyond the trade itself, and that surplus is value the pool's liquidity providers are the counterparty to. The hook reads `tx.gasprice - block.basefee`, the priority fee per unit of gas actually paid, and adds a surcharge along a saturating curve: surcharge(priority) = maxSurcharge * priority / (priority + halfPriority) At `priority == halfPriority` the swap pays half the cap. The surcharge is an LP fee, so it goes to in-range liquidity; the hook takes nothing and holds nothing. The trader cannot dodge it by bidding low, because bidding low is exactly the concession the hook is asking for: a searcher who drops their priority fee to avoid the surcharge loses the race that made the trade profitable. That is the point. The hook prices the option to be early rather than trying to detect who is early. Prior art: fee mechanisms keyed on realized volatility, on price movement and on swap size are all well covered. The idea that priority fees reveal flow toxicity is discussed in the ordering-fee literature and in Uniswap's own research on priority-ordering auctions, but the surcharge itself has been implemented at the sequencer or the router, never inside the pool where the liquidity providers who bear the cost can be paid directly. Chain support, stated plainly. This works where there is a real priority-fee market: Ethereum, Base, Unichain, Optimism, Blast and other OP-stack chains. On Arbitrum One transactions are ordered first-come-first-served and the priority fee is normally zero, so on that chain the hook charges `baseFee` and nothing more. It is safe there, it is simply inert, and a pool on Arbitrum should use {ArbTaxDecayHook} instead. ## Prior art Fee mechanisms keyed on realized volatility, on price movement and on swap size are all well covered. That priority fees reveal flow toxicity is discussed in the ordering-fee literature and in Uniswap research on priority-ordering auctions, but the surcharge has only ever been implemented at the sequencer or the router, never inside the pool where the liquidity providers who bear the cost can be paid directly. ## Where it does not help Needs a real priority-fee market. On Arbitrum One, where ordering is first-come-first-served and the priority fee is normally zero, the hook is safe but inert and a pool there should use ArbTaxDecay instead. --- # CircuitBreaker Slug: circuit-breaker Family: Risk Contract: CircuitBreakerHook Tags: risk, circuit-breaker, oracle-free, no-admin Page: https://hookforge.pages.dev/hooks/circuit-breaker/ Manifest: https://hookforge.pages.dev/schema/hooks/circuit-breaker.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/CircuitBreakerHook.sol Callbacks: afterSwap, beforeSwap, afterInitialize Parameters: maxTickMove (uint24), windowSeconds (uint32), cooldownSeconds (uint32) ## Summary Halts swapping for a cooldown after the price moves further than a pool is willing to move in one window, and lets liquidity leave the whole time. ## How it works Every venue outside crypto stops trading after a limit move, for a reason that has nothing to do with paternalism: a violent move is usually either an error or an attack, and the cheapest defence against both is to stop, let information arrive, and start again. On-chain the same event is normally handled by a governance multisig that pauses a contract minutes after it mattered. This hook makes the rule mechanical and local to one pool. It keeps a reference tick, refreshed at most once per `windowSeconds`. After every swap it compares the new tick to that reference. If the pool moved further than `maxTickMove`, swapping halts for `cooldownSeconds` and then resumes on its own. There is no admin, no pause key and no way for anyone, including the deployer, to halt a pool that has not moved or to extend a halt that has expired. The design decision worth stating: the swap that breaches the limit is allowed to complete. Reverting it instead would turn the hook into a price cap, and a price cap on an AMM is a strictly worse instrument than a halt. It cannot be enforced (the same move arrives as several smaller swaps), it strands the pool at a price the market has left, and it guarantees that the arbitrage against the pool stays open and profitable for as long as the cap holds. Halting after the fact gives up the last swap and buys the thing that actually matters, which is time. Liquidity operations are never blocked. A provider can withdraw during a halt, which is the property that makes this safe to use: the worst case for someone caught in a halted pool is that they exit rather than trade. One tick is one basis point to within rounding (`1.0001^1`), so `maxTickMove = 500` is a five percent move. Prior art: pause-guardian patterns are everywhere and oracle-deviation checks exist as hooks. An autonomous, self-clearing, per-pool halt with no privileged role and no oracle does not. ## Prior art Pause-guardian patterns are everywhere and oracle-deviation checks exist as hooks. An autonomous, self-clearing, per-pool halt with no privileged role and no oracle does not. ## Where it does not help A halt is a blunt instrument: it stops honest trading as well as the attack, and it leaves the pool arbitrageable the moment it lifts. It is the right trade only where the alternative is a pool drained at a price nobody would have quoted. --- # DepegShield Slug: depeg-shield Family: Risk Contract: DepegShieldHook Tags: risk, stablecoin, dynamic-fee, oracle-free Page: https://hookforge.pages.dev/hooks/depeg-shield/ Manifest: https://hookforge.pages.dev/schema/hooks/depeg-shield.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/DepegShieldHook.sol Callbacks: beforeSwap, afterInitialize Parameters: pegTick (int24), baseFee (uint24), minFee (uint24), maxSurcharge (uint24), halfDeviationTicks (uint24) ## Summary Makes leaving a peg expensive and returning to it cheap, in proportion to how far the pool has already strayed. ## How it works A pegged pool fails in a specific way. Something spooks the market, the first sellers cross, the price slips, and the slip is itself the signal that brings the next sellers. Liquidity providers are filled the whole way down at a fee that was set for a pool sitting at par. By the time anyone reacts, the pool is one-sided and the providers own the asset that broke. This hook makes the fee a function of two things: how far the pool is from the peg, and which way the swap pushes it. A swap that widens the gap pays `baseFee` plus a surcharge that grows with the existing deviation. A swap that closes the gap pays less than `baseFee`, down to a floor, with the discount growing the same way. The result is a spread that opens as the pool strays and pays anyone willing to push it back. widening: fee = baseFee + maxSurcharge * deviation / (deviation + halfDeviation) restoring: fee = baseFee - (baseFee - minFee) * deviation / (deviation + halfDeviation) Both are LP fees, so the surcharge is paid to liquidity and the discount is given up by liquidity. That is the right trade for a provider in a pegged pool: paying for the flow that repairs the pool is cheaper than being filled on the way out. The peg is a tick, not an oracle. `pegTick = 0` is a one-to-one pool; a pair whose par is not one-to-one sets the tick that corresponds to par. Since it is fixed at initialization, there is nothing to manipulate and no feed to go stale, and a pool whose peg genuinely re-bases has to be re-created, which for a pegged pair is the honest outcome. Deviation is measured in ticks. One tick is one basis point to within rounding, so `halfDeviationTicks = 50` means half the surcharge applies once the pool is fifty basis points off par. Prior art: stable-swap curves flatten the price impact near par, and dynamic-fee hooks keyed on volatility exist. Neither is directional. A curve treats a swap toward the peg and a swap away from it identically, and a volatility fee charges the repairing flow exactly as much as the flow that broke the pool. Charging asymmetrically by direction of travel is what is new here. ## Prior art Stable-swap curves flatten price impact near par, and dynamic-fee hooks keyed on volatility exist. Neither is directional: a curve prices a swap toward the peg and one away from it identically, and a volatility fee charges the repairing flow exactly as much as the flow that broke the pool. Charging asymmetrically by direction of travel is what is new. ## Where it does not help The peg is fixed at initialization, so a pair whose par genuinely re-bases has to be re-created. For a pegged pair that is the honest outcome, but it does mean this is the wrong hook for a drifting reference such as a yield-bearing wrapper. --- # DrawdownCap Slug: drawdown-cap Family: Risk Contract: DrawdownCapHook Tags: risk, circuit-breaker, oracle-free, no-admin Page: https://hookforge.pages.dev/hooks/drawdown-cap/ Manifest: https://hookforge.pages.dev/schema/hooks/drawdown-cap.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/DrawdownCapHook.sol Callbacks: afterSwap, beforeSwap, afterInitialize Parameters: maxFallTicks (uint24), epochSeconds (uint32) ## Summary A limit down. The pool may not fall more than a fixed distance below where the current epoch opened, and the limit resets on a schedule rather than on anyone's say-so. ## How it works {CircuitBreakerHook} is symmetric and reactive: a violent move in either direction halts the pool, then the halt clears. This is the other shape, and it is the one commodity and equity venues actually use. It is asymmetric, because a collapse and a rally are not the same event for the people holding the asset. It is a hard cap rather than a trigger, so the fall never happens rather than being noticed after it did. And it resets on a clock, so everyone can see in advance when selling reopens and at what level. allowed while openTick - tick <= maxFallTicks, where openTick is the tick at the start of the epoch Buying is never restricted. A pool at its limit can still be bid up, and doing so does not raise the limit for that epoch, because the reference is the epoch's opening price and not a running high. When the epoch rolls, the pool takes its current price as the new opening and gets a fresh allowance. As with {RatchetFloorHook}, the cap is expressed as a price a router can trade into: {sqrtPriceLimitDownX96} returns the value to pass as a swap's `sqrtPriceLimitX96`, so a seller fills as far as the cap allows and stops there. The `afterSwap` revert is the backstop for callers that pass no limit. Liquidity operations are never blocked, so nobody is trapped by a limit-down epoch. One tick is one basis point to within rounding, so `maxFallTicks = 1000` is a ten percent daily limit. ## Prior art Trading halts and price bands are standard on regulated venues and absent on-chain, where the closest equivalents are governance pause switches and oracle-deviation guards. Hook implementations of trading hours exist. A scheduled, asymmetric, self-resetting limit down with no privileged role does not. ## Where it does not help A limit down does not stop a decline, it defers one. If the market has genuinely repriced, the pool reopens each epoch and falls again, one limit at a time, and in the meantime the gap between the pool and the real price is an arbitrage that grows. It buys holders time to react, which is worth something, and it costs liquidity providers the trades they would rather have made, which is not free. --- # OracleBand Slug: oracle-band Family: Risk Contract: OracleBandHook Tags: risk, oracle, manipulation-resistance, no-admin Page: https://hookforge.pages.dev/hooks/oracle-band/ Manifest: https://hookforge.pages.dev/schema/hooks/oracle-band.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/OracleBandHook.sol Callbacks: afterSwap, beforeSwap, afterInitialize Parameters: oracle (address), maxDeviationBps (uint32), maxStaleness (uint32) ## Summary Refuses to let a pool settle at a price the wider market does not recognise. ## How it works A thin pool can be walked anywhere. Buy through the last tick of liquidity and the pool will quote you a price no other venue would, and whatever reads that pool afterwards, a lending market, a vault, another pool's oracle, inherits the number. The pool is not wrong: it faithfully reports what it was paid. It is simply alone. This hook gives the pool a second opinion. Before and after every swap it compares the pool price against a reference from an {IPriceOracle} and rejects the swap if the result lands outside a band around it: deviation = |poolPrice / referencePrice - 1|, rejected when deviation > maxDeviationBps The check runs on both sides of the swap, and the pair is what makes it hard to defeat. The `beforeSwap` check refuses to trade from a price that is already outside the band, so a pool pushed out of line in one transaction cannot be traded against in the next. The `afterSwap` check refuses to leave the pool outside the band, which is what stops the walk in the first place. Neither check can be satisfied by splitting a large swap into small ones, because the constraint is on the resulting price rather than on the size of the trade. Prices are compared in `sqrtPriceX96`, squared back through `FullMath` so the comparison is on the actual price ratio and not on an approximation of it that drifts as the deviation grows. Liquidity operations are untouched. A provider can always withdraw, including while the band is refusing swaps, which is the property that makes it safe to sit behind one. The obvious objection: this makes the pool depend on an oracle, and oracles fail. So the hook treats failure as refusal rather than as permission. A feed older than `maxStaleness` halts swapping instead of waving it through, and a feed that reverts propagates rather than being caught. A pool that would rather trade blind than not trade should not use this hook, and the fee-based hooks in this catalogue are the oracle-free alternative. ## Prior art Oracle-deviation checks exist inside individual protocols, and Detox uses Pyth to detect MEV and redirect it. Both act after the fact, on a pool that has already printed the price. Enforcing the band as a precondition on both sides of the swap, so the out-of-band price is never written at all, is what is new here. ## Where it does not help The pool inherits the oracle's liveness. If the feed stops, swapping stops, and on a chain where the feed updates on a deviation threshold rather than a heartbeat, a quiet market can look stale. Set maxStaleness against the feed's actual publication cadence, not against how fresh you would like it to be. --- # ExpirySettle Slug: expiry-settle Family: Time Contract: ExpirySettleHook Tags: expiry, settlement, dynamic-fee, derivatives, oracle-free Page: https://hookforge.pages.dev/hooks/expiry-settle/ Manifest: https://hookforge.pages.dev/schema/hooks/expiry-settle.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/ExpirySettleHook.sol Callbacks: beforeSwap, beforeAddLiquidity, afterInitialize Parameters: maturity (uint64), windowSeconds (uint32), baseFee (uint24), settlementFee (uint24) ## Summary Gives a pool a maturity, and makes moving its price monotonically more expensive as that maturity approaches, so the settlement price is dearest to manipulate exactly when manipulating it would pay most. ## How it works Any dated instrument that settles against a market price has the same problem at the end of its life. The payoff of pushing the price around is largest in the final minutes, because there is no time left for anybody to push it back, and the cost of pushing it is unchanged from any other moment. Traditional markets answer this with a settlement window: the official price is an average over the closing period rather than a single print, which makes a manipulator pay for the whole window instead of one instant. A pool cannot average its own price without an oracle, but it can do something a traditional venue cannot: change what moving the price costs. As maturity approaches, this hook ramps the fee from `baseFee` up to `settlementFee` across the last `windowSeconds`, on a curve that is quadratic rather than linear so the final moments are much more expensive than the early part of the window: fee(t) = baseFee + (settlementFee - baseFee) * elapsed^2 / windowSeconds^2 A manipulator who wants to move the settlement print has to choose between acting early, where the fee is low but there is time for somebody to trade against them, and acting late, where nobody can respond but every basis point of the move costs several times more. The fee is paid to the liquidity that has to absorb the move, which is the party bearing the cost. At maturity the pool stops trading. Swaps revert, so the price cannot move again, and `settlementPrice` is simply the pool's final tick. Liquidity may always be removed, including after maturity, because a matured pool that cannot be exited is a trap rather than an instrument. Adding liquidity after maturity reverts: there is nothing left to provide liquidity for, and permitting it would only let somebody strand funds. The hook holds nothing, takes nothing for itself, and has no privileged role. The maturity is fixed before the pool exists and cannot be moved by anyone, which is the property that makes the instrument datable at all. ## Prior art Dated AMMs exist (YieldSpace and Pendle-style curves converge to par at maturity), and hooks that halt trading on a schedule exist. Settlement-window design is standard in traditional derivatives. Making the *cost* of moving an AMM's price rise on a convex curve into its own settlement, as the on-chain substitute for a time-averaged settlement price, is the contribution here. ## Where it does not help It raises the cost of manipulation, it does not prevent it. A manipulator whose payoff exceeds the ramped fee will still pay it, and the right response is to size `settlementFee` against the notional settling on the price rather than against ordinary trading. The hook also cannot know what the pool settles for, so if nothing actually references `settlementPrice`, the ramp is pure cost with no benefit. --- # TradingCalendar Slug: trading-calendar Family: Time Contract: TradingCalendarHook Tags: dynamic-fee, schedule, rwa, oracle-free Page: https://hookforge.pages.dev/hooks/trading-calendar/ Manifest: https://hookforge.pages.dev/schema/hooks/trading-calendar.json Source: https://github.com/nirholas/hookforge/blob/main/contracts/src/hooks/TradingCalendarHook.sol Callbacks: beforeSwap, afterInitialize Parameters: openSecond (uint32), closeSecond (uint32), rampSeconds (uint32), sessionFee (uint24), closedFee (uint24), daysMask (uint8) ## Summary Gives a pool a trading session, and closes it by raising the price of immediacy rather than by reverting. ## How it works Some assets should not be quoted around the clock at the same spread. A pool whose reference market is open for part of the day is being priced blind the rest of the time, and the liquidity sitting in it overnight is providing a free option to anyone with better information about where the asset will open. The obvious hook for this reverts outside session hours, and several published ones do exactly that. Reverting is the wrong instrument. A pool that reverts is a pool that every router, aggregator and quoting service must special case; it fails after the user has signed; it strands liquidity providers who wanted to exit; and it converts a pricing problem into a liveness problem. Worse, it does not stop informed flow at all, it just moves it to the first second after the open, where it hits the same liquidity at the same stale price. This hook keeps the pool open and quotable at every instant and expresses the session in the fee instead: - Inside the session, swaps pay `sessionFee`. - Outside it, swaps pay `closedFee`, which is meant to be punitive rather than prohibitive. - Across `rampSeconds` on either side of each boundary the fee moves linearly between the two, so the open and the close are gradients rather than cliffs and there is no single block worth racing to. The ramp is the part that matters. A cliff at the open creates a race: the first swap after the boundary captures the whole overnight gap at the session spread. A ramp means the trader who wants that gap must choose between paying for it early and waiting for a lower fee while the price moves against them, which is precisely the tradeoff that makes the gap get closed gradually and by more than one participant. Sessions are expressed in UTC seconds-of-day and may wrap midnight (`open > close` describes an overnight session). `daysMask` selects the days of the week the session runs, bit 0 being Monday. A pool with `daysMask` covering all seven days and a 24-hour session is always in session, which is a valid way to disable the calendar. Prior art: "New York Trading Hours" and "Trading Hours" hooks revert outside a window. Continuous fee ramps around scheduled events appear in the `UniCast` design for known catalysts. Expressing a *recurring weekly calendar* as a continuous fee surface, with no revert path and no oracle, is the contribution here. ## Prior art The published calendar hooks ("New York Trading Hours", "Trading Hours") revert outside a window. Continuous fee ramps around a single scheduled event appear in the UniCast design. Expressing a recurring weekly calendar as a continuous fee surface, with no revert path and no oracle, is the contribution here. ## Where it does not help The calendar is a fixed weekly pattern in UTC. It does not know about holidays, half days, or daylight-saving shifts in the reference market, so a pool tracking an asset with an irregular schedule has to pick a session that is correct most weeks and accept that it is wrong on the exceptions.