A Uniswap v4 hook · Ethereum

The image is already there.
Developing is only agreeing to look at it.

A pool that behaves like a camera. Trading is light. You open a shutter when you choose, a blockhash you cannot see decides the grain, and you have 256 blocks to develop before the image is gone for good.

developing…
How it works

Four things decide a plate, and you control exactly one of them.

Nothing here is attested by anyone. There is no operator, no signature, no oracle, no allowlist. The randomness is a blockhash and the light is the pool's own trading.

01 / LIGHT

Trading is light

Every swap advances a counter by the logarithm of its volume. A whale and a minnow differ by a few hundred units, not twelve orders of magnitude — so to double the light you must double the number of trades, never the size of one.

02 / SHUTTER

You open it yourself

Exposing burns silver and records who, when, and how much light had fallen so far. You make the call yourself, because a hook can never know who is trading — through the app the caller is always the router, never the buyer.

03 / GRAIN

A block you cannot see

The seed is the hash of your address, the plate's id, and the blockhash of the block after you exposed. At the moment you commit, that block does not exist. And developing is permissionless, so you cannot re-roll by refusing to finish.

04 / DENSITY

The one real choice

Density is the light that fell between exposing and developing. Develop early for a thin, underexposed print. Hold the shutter open and risk blowing it out. Wait past 256 blocks and the grain is gone — the plate fogs.

The deadline is not invented. The EVM can only see 256 blocks into the past. A plate left longer than that cannot ever be developed at its true grain — not by you, not by anyone, not with any amount of gas. The chain's own forgetfulness is the clock the whole piece runs on.
Interactive

The darkroom

A faithful simulation of the contract: the same light accumulator, the same seed shape, the same 256-block latitude, the same renderer. Open a shutter, let the market work, and decide when to stop.

Block
Light
0
Density
Latitude left
Exposure0%
no plate
The edition

Same grain, different light.

One seed, developed at five different moments. The geometry is fixed the instant the grain block lands; only how much silver reaches the paper is still open.

Documentation

Docs

Overview

Latent is three contracts. Latent.sol is the silver: an ERC-20 with a fixed supply of 36,000,000, no owner, no mint and no pause, burned to open a shutter. Darkroom.sol is a Uniswap v4 hook that measures the light a pool throws and keeps the register of exposures. Plate.sol is the developed image, drawn on chain from the seed alone.

There is no fourth contract, no admin, no upgrade path and no off-chain component. Everything the project claims to do happens in a transaction you can read.

Light

On every swap the hook advances a monotonic counter:

light += magnitude(|amount0|)   // magnitude = log2 in 8.8 fixed point

Recording the logarithm rather than the amount is deliberate. It means a single very large trade cannot flood the accumulator and blow out every open plate at once: doubling the light requires twice as many trades, not one twice the size.

Exposing

expose() burns SILVER LATENT from the caller and writes an exposure: owner, the block it happened in, and the light reading at that instant. It is a call you make directly, not something inferred from a swap, because a v4 hook cannot know who is trading — through the Uniswap interface the address it sees is UniversalRouter, never the person.

Developing

The seed is fixed by a block that did not exist when you committed:

seed = keccak256(owner, id, blockhash(exposedAt + 1))

You cannot aim it, and you cannot re-roll it. develop(id) is permissionless — anyone may finish anyone's plate, and it still mints to the original owner — so declining to develop a result you dislike buys you nothing.

Density is the light that fell between exposing and developing, capped at FULL_EXPOSURE. That is the one variable the artist actually chooses.

Preview before you commit. preview(id) returns the exact seed, density and fog state as a pure read. Being able to look changes nothing, which is the point.

Fog

blockhash reaches back exactly 256 blocks and then returns zero. Past that the grain block is unrecoverable, so the plate develops fogged: a different, visibly muted palette and a seed derived from the exposure alone. A fogged plate is still a plate. It is simply, permanently, a frame you left too long.

The fixer

The hook takes a small toll on swaps and holds it as ERC-6909 claims. fix() converts that stock into a full-range liquidity position owned by the hook.

There is no counterpart function. Nothing in the contract ever passes a negative liquidityDelta. The position is permanent because the code that would remove it does not exist — not because somebody is holding a key. The test suite asserts this by calling five plausible exit selectors and requiring all five to fail.

Contracts

ContractRole
Latent.solERC-20. 36,000,000 fixed, no owner, no mint, no pause. Burned to expose.
Darkroom.solThe v4 hook. Flags 0x1044 — afterInitialize, afterSwap, afterSwapReturnsDelta.
Plate.solERC-721. On-chain SVG. Mintable only by the darkroom, pinned at construction.

Constants

NameValueMeaning
ROLL36,000,000Total supply. One roll of thirty-six.
SILVER100e18Burned per exposure.
LATITUDE256Blocks before the grain is gone. The EVM's limit, not ours.
FULL_EXPOSURE60,000Light at which a plate is fully exposed.
TOLL_BPS500.50% of the unspecified amount, taken as the fixer's stock.
FLAGS0x1044The hook's permissions, encoded in its address.

Functions

FunctionWhoEffect
expose()anyoneBurn silver, record an exposure, return its id.
preview(id)viewSeed, density and fog state as they stand right now.
develop(id)anyoneFix the image and mint the plate to its owner.
fix()anyoneTurn collected toll into liquidity nobody can remove.
room()viewLight, plates exposed, liquidity locked.
burn(amount)anyoneDestroy your own silver. Supply only falls.

Errors

Every failure reverts with a named error. Nothing in this system swallows a failure and emits a note about it — a transaction that succeeds did what it said.

ErrorWhen
NotOpenThe pool has not been initialized against this hook yet.
NoSuchExposureThat plate was never exposed.
TooSoonThe grain block is not strictly in the past yet.
AlreadyDevelopedThat plate has already been fixed.
NothingToFixNo toll has accumulated.

Launching

The hook is built to be listable from the Uniswap interface. It uses no dynamic fee, never reads sender or hookData, does not bind on tickSpacing, has no beforeInitialize that could refuse the pool, and never touches the PoolManager's transient sync slot during a swap.

This is verified rather than asserted: the test suite opens and funds the pool through the real PositionManager, buys through the real UniversalRouter, exposes, develops and takes ownership of a plate — on a mainnet fork, against the deployed contracts.