Build #001: an anti-snipe limit for the first 20 blocks

A max buy per wallet that only applies while a launch is young, and lifts itself after block 20.

Build #001 cover
fig. 00 · build #001 · 248 words

The rule arrived as one sentence: nobody buys more than 1% of supply in the first 20 blocks after launch. Here is how it became a hook. This is an example build, written to show the process; it is not deployed.

The spec

  • The limit applies to buys only. Sells are never blocked.
  • The window is counted in blocks from pool initialisation, not in time.
  • The cap is per receiving wallet, summed across every buy inside the window.
  • After block 20 the hook does nothing. It cannot be switched back on.

The hook

Simplified for reading. The real build also handles exact-output swaps and routers that buy on behalf of a user.

function beforeSwap(address, PoolKey calldata key, SwapParams calldata p, bytes calldata hookData)
    external override onlyPoolManager
    returns (bytes4, BeforeSwapDelta, uint24)
{
    PoolId id = key.toId();
    if (block.number < launchBlock[id] + WINDOW && isBuy(key, p)) {
        address buyer = abi.decode(hookData, (address));
        bought[id][buyer] += amountOut(p);
        require(bought[id][buyer] <= maxBuy[id], "anti-snipe: cap");
    }
    return (this.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}

Tests

Unit tests cover the edges: a buy exactly at the cap, one wei over, the last block of the window and the first block after it. Fuzzing throws 10,000 random sequences of buys from up to 50 wallets at a forked pool, and checks one invariant: no wallet ends the window above the cap.

What it will never do

Block a sell, change the cap after launch, or restart the window. All three are written into the spec, and all three have tests.