HookForge

Hooks / Time

ExpirySettle

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.

ExpirySettle implements 3 of the fourteen Uniswap v4 callbacks: afterInitialize, beforeAddLiquidity, beforeSwap.

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.

Configuring a pool

Uniswap v4 removed hookData from initialize, so a hook that needs per-pool parameters has to receive them out of band. ExpirySettle 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,
    ExpirySettleHook.Config({
        maturity: /* uint64 */ 0,
        windowSeconds: /* uint32 */ 0,
        baseFee: /* uint24 */ 0,
        settlementFee: /* uint24 */ 0
    })
);

// 2. Initialize the pool. The hook rejects a pool it was never configured for.
poolManager.initialize(key, startingSqrtPriceX96);
ParameterTypeUnits
maturityuint64
windowSecondsuint32seconds
baseFeeuint24hundredths of a bip (3000 = 0.30%)
settlementFeeuint24hundredths of a bip (3000 = 0.30%)

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("expiry-settle");
const address = hookAddress("expiry-settle", 1);
const key = poolKeyFor({hook: address, currencyA: USDC, currencyB: WETH, tickSpacing: 60, dynamicFee: true});
console.log(poolId(key));

What it reverts with

ErrorMeaning
FeeTooLarge(uint24)A fee was configured above the protocol maximum of 100%.
InvalidWindow()`windowSeconds` was zero, which would turn the ramp into a cliff at maturity.
Matured(uint64)The pool has matured. Its price is final and it no longer trades.
MaturityTooSoon()The maturity must be far enough ahead to contain the whole settlement window.
NotDynamicFee()The hook was attempted to be initialized with a non-dynamic fee.
NotYetMatured(uint64)The pool has not matured, so it has no settlement price yet.
PoolAlreadyInitialized()The pool already exists, so its configuration is final.
PoolNotConfigured()The pool was initialized without a configuration for this hook.
SettlementFeeBelowBase()`settlementFee` must be at least `baseFee`; a ramp that gets cheaper into settlement inverts the point.

Addresses

No addresses published yet. The deploy script mines a deterministic address per chain, so the address a hook will occupy is known before it is deployed; this hook has not had that step run.

Source and verification

The contract is contracts/src/hooks/ExpirySettleHook.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)"    # ExpirySettle
cast call $HOOK "specURI()(string)"     # https://hookforge.pages.dev/schema/hooks/expiry-settle.json
cast call $HOOK "hookTags()(string[])"  # expiry, settlement, dynamic-fee, derivatives, oracle-free