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.
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.
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.
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.
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.
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 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.
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.
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(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.
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
| Contract | Role |
|---|---|
| Latent.sol | ERC-20. 36,000,000 fixed, no owner, no mint, no pause. Burned to expose. |
| Darkroom.sol | The v4 hook. Flags 0x1044 — afterInitialize, afterSwap, afterSwapReturnsDelta. |
| Plate.sol | ERC-721. On-chain SVG. Mintable only by the darkroom, pinned at construction. |
Constants
| Name | Value | Meaning |
|---|---|---|
| ROLL | 36,000,000 | Total supply. One roll of thirty-six. |
| SILVER | 100e18 | Burned per exposure. |
| LATITUDE | 256 | Blocks before the grain is gone. The EVM's limit, not ours. |
| FULL_EXPOSURE | 60,000 | Light at which a plate is fully exposed. |
| TOLL_BPS | 50 | 0.50% of the unspecified amount, taken as the fixer's stock. |
| FLAGS | 0x1044 | The hook's permissions, encoded in its address. |
Functions
| Function | Who | Effect |
|---|---|---|
| expose() | anyone | Burn silver, record an exposure, return its id. |
| preview(id) | view | Seed, density and fog state as they stand right now. |
| develop(id) | anyone | Fix the image and mint the plate to its owner. |
| fix() | anyone | Turn collected toll into liquidity nobody can remove. |
| room() | view | Light, plates exposed, liquidity locked. |
| burn(amount) | anyone | Destroy 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.
| Error | When |
|---|---|
| NotOpen | The pool has not been initialized against this hook yet. |
| NoSuchExposure | That plate was never exposed. |
| TooSoon | The grain block is not strictly in the past yet. |
| AlreadyDeveloped | That plate has already been fixed. |
| NothingToFix | No 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.