Documentation

Uniswap v4 hook · not deployed
Revised as the work changes
1

Overview

KERR is a hook contract for Uniswap v4 attached to a single KERR/ETH pool, using native ETH rather than a wrapped form. It does one thing: on every swap it takes 2% in KERR and burns it in the same transaction.

Not sent to a dead address — burned. totalSupply() falls by exactly the amount taken, at every swap, and there is no path that puts it back.

The goal is not yield, and there is no reserve, no redemption and no floor. What is on offer is a single property you can check rather than believe: total supply is non-increasing, and total burned is non-decreasing. No function reverses either, no role pauses them, and no upgrade path exists that could add one.

Roy Kerr described the geometry of a rotating black hole in 1963. Its interesting feature is not that it pulls, but that it has a surface past which return is undefined.

2

Mechanism

2.1  Which callback fires

A trader calls swap. Because the hook's address is written into the PoolKey, the PoolManager must call it — there is no route to this pool that avoids the hook, since a swap that skips it is by definition a swap on a different pool.

The slice is always denominated in KERR, never in ETH, so which callback does the work depends on the direction. On a sell, KERR is the input and the slice comes off it in beforeSwap. On a buy, KERR is the output and the slice comes off it in afterSwap. Both callbacks return a delta, so both return-delta flags are required alongside them.

Native ETH is Currency.wrap(address(0)) and always sorts as currency0, so KERR is currency1 and zeroForOne alone tells you which side you are on.

function _beforeSwap(
    address, PoolKey calldata key,
    SwapParams calldata params, bytes calldata
) internal override returns (bytes4, BeforeSwapDelta, uint24) {

    // a sell: KERR is the input
    if (params.zeroForOne)
        return (BaseHook.beforeSwap.selector, ZERO_DELTA, 0);

    uint256 amountIn = uint256(-params.amountSpecified);
    uint256 slice   = amountIn * BURN_BPS / 10_000;

    _swallow(slice);

    return (
        BaseHook.beforeSwap.selector,
        toBeforeSwapDelta(int128(int256(slice)), 0),
        0
    );
}

2.2  The burn itself

This is the part worth reading twice, because it is where most burn mechanisms quietly cheat. Moving tokens to 0x…dEaD does not reduce totalSupply(); it relocates a balance and leaves the supply figure untouched. Anyone computing scarcity from totalSupply() would be reading a number that never moved.

The hook therefore pulls the slice out of the PoolManager and calls burn on the token, which decrements supply for real.

function _swallow(uint256 slice) private {
    if (slice == 0) return;

    poolManager.take(KERR, address(this), slice);
    token.burn(slice);              // totalSupply() falls

    totalBurned += slice;           // up, only
    emit Swallowed(slice, totalBurned);
}

Returning a non-zero delta needs beforeSwapReturnDelta and afterSwapReturnDelta on top of the callbacks themselves. Omit either and the PoolManager reverts.

2.3  What the trader pays

2% on top of the pool's own LP fee, so roughly 2.3% all in. It reaches the trader as a worse effective rate, in both directions, with no exemption for any address.

Every other lifecycle callback is switched off, including the liquidity ones. Providers add and withdraw exactly as they would anywhere else; this hook has no opinion about them and no ability to form one.

3

Contracts

Two of them. Neither holds a balance between transactions.

Kerr.sol
The hook. Picks the callback that matches the direction, computes the slice, takes it out of the PoolManager and burns it. Around eighty lines, which is the point — a contract nobody can fix should be a contract everybody can read.
Token.sol
ERC-20, full supply minted at construction, no mint function afterwards. It exposes burn, and the hook address is the only one allowed to call it. Burning is therefore the only thing that ever changes total supply.

An earlier draft added a read-only Lens contract. It was dropped: totalSupply() and totalBurned() are already public, and a third contract is a third thing to read for no gain.

4

Parameters

All immutable, all fixed at construction, none settable afterwards.

  • BURN_BPS200, so 2%. Identical in both directions, applied to every address without exception. This number cannot be changed after deployment, including by us, including if it turns out to be wrong.
  • Token address — set once in the hook constructor. The hook cannot be pointed at a different token later.
  • Ownership — no owner, no role, no guardian, no pause. Nothing to renounce, because there was never anything to hold.
  • Upgradeability — deployed directly. No proxy, no delegatecall, no implementation slot.
  • Treasury share — none. The full 2% is burned; no portion is diverted anywhere. If that changes before deployment it will be written here first.
5

Deployment

v4 stores hook permissions in the low bits of the hook's own address, so a contract cannot merely claim a permission — the address has to carry it.

That means mining a CREATE2 salt until the resulting address has the right bits set, then deploying with it and initialising the pool in the same script, so the first swap is already subject to the hook.

forge script script/Deploy.s.sol \
  --rpc-url $RPC --broadcast --verify

Once deployed, the hook address is part of the PoolKey. Changing it would mean a different pool, not a modified one.

6

Checking it yourself

Everything above reduces to a handful of calls you can make without asking anyone.

  • Read totalSupply(). Compare it against the initial supply minus totalBurned(). They agree at every block, or something is wrong.
  • Check that supply actually moves. If a burn mechanism leaves totalSupply() flat, it is sending tokens to a dead address and calling it a burn. This one does not.
  • Index the Swallowed event across the whole history. Every entry adds. None subtracts.
  • Search the verified source for onlyOwner, Ownable, delegatecall, selfdestruct, upgradeTo, mint. None appear.
  • Read the PoolKey. The hook address is one of its fields, which is why it cannot be routed around.
7

Questions

Can the team mint more tokens?
No. Supply is minted once at construction and the contract has no mint function afterwards. The only function that touches supply is burn, and the hook is the only caller allowed to reach it.
Is the burn real, or just a transfer to a dead wallet?
Real. burn decrements totalSupply(). You can watch the figure fall block by block, which is not true of the dead-address version most tokens use.
Can liquidity providers still remove liquidity?
Yes, at any time. The hook does not implement the liquidity callbacks at all, so add and remove behave as on any other v4 pool.
Can the 2% be lowered later?
No, by anyone, ever. It is immutable and there is no proxy. This is the point of the design and also its largest risk.
What stops a second pool without the hook?
Nothing, and nothing could. This is why the claim is scoped to the pool rather than to the token.
Is there a presale?
No presale and no allocation round. Launch details will be published before deployment, not after.