HookForge

Hooks / Risk

DrawdownCap

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.

DrawdownCap implements 3 of the fourteen Uniswap v4 callbacks: afterInitialize, beforeSwap, afterSwap.

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.

Configuring a pool

Uniswap v4 removed hookData from initialize, so a hook that needs per-pool parameters has to receive them out of band. DrawdownCap takes them through configure, which anyone may call for a pool key whose pool does not exist yet, and which nobody may call afterwards. The parameters are part of what the pool is, so they are fixed for its lifetime.

// 1. Fix the terms, before the pool exists.
hook.configure(
    key,
    DrawdownCapHook.Config({
        maxFallTicks: /* uint24 */ 0,
        epochSeconds: /* uint32 */ 0
    })
);

// 2. Initialize the pool. The hook rejects a pool it was never configured for.
poolManager.initialize(key, startingSqrtPriceX96);
ParameterTypeUnits
maxFallTicksuint24tick
epochSecondsuint32seconds

From TypeScript

The SDK ships the catalogue, the address book and the pool-key helpers, so a client never hardcodes an address or recomputes a pool id by hand.

npm i @hookforge/sdk

import {getHook, hookAddress, poolKeyFor, poolId} from "@hookforge/sdk";

const hook = getHook("drawdown-cap");
const address = hookAddress("drawdown-cap", 1);
const key = poolKeyFor({hook: address, currencyA: USDC, currencyB: WETH, tickSpacing: 60});
console.log(poolId(key));

What it reverts with

ErrorMeaning
InvalidConfig()`maxFallTicks` or `epochSeconds` was zero.
LimitDown(int24,int24)The swap would take the pool past this epoch's limit down. Pass `sqrtPriceLimitDownX96` as a price limit.
PoolAlreadyInitialized()The pool already exists, so its configuration is final.
PoolNotConfigured()The pool was initialized without a configuration for this hook.

Addresses

ChainChain IDAddress
unichain1300x96DEB6525EACf0847E7a1E0250ABe65a892dd0c0
robinhood46630xCDbF94558886fB07615550c21CDbAf3b9b68d0c0
base84530xd9bDc663eaDf9f1Cf3dEEa393D45Be6342b510C0
arbitrum421610xf3dffF3782A5789fC987389a647C8061A76e10c0

These addresses are deterministic, not live. They are the CREATE2 addresses the deploy script mines so that the low fourteen bits encode this hook's permissions. Until the deploy runs on a chain, there is no code at them. Check before you send anything anywhere.

Source and verification

The contract is contracts/src/hooks/DrawdownCapHook.sol, and everything on this page is generated from it: the prose is its NatSpec, the parameters are its configure ABI, the callbacks above are the flags it declares, and the tags are the strings its own hookTags() returns. A hook cannot be documented here as something it is not.

Ask the deployed contract what it is and it answers directly, with no registry in the loop:

cast call $HOOK "hookName()(string)"    # DrawdownCap
cast call $HOOK "specURI()(string)"     # https://hookforge.pages.dev/schema/hooks/drawdown-cap.json
cast call $HOOK "hookTags()(string[])"  # risk, circuit-breaker, oracle-free, no-admin