Skip to content
Hookshade

Docs

How it works, in full.

Hookshade is three things: rules that live in your browser, a hook that settles a pool in batches, and a pool of notes the trades can come out of. This page is the whole mechanism, and a row-by-row statement of what on this site is real.


The shape of it

A watch hook is a rule in the plainest sense: a thing to read, a line, and a side. It is kept in this browser, sealed, and evaluated here. The chain never learns it exists; the only thing it ever sees is a read.

The Shade hook is a Uniswap v4 hook. A pool under it cannot be swapped directly: orders are cast into a batch, and a batch is settled as one swap of the residual at one price. The ledger records the net, not the parts.

The shaded pool is a pool of notes — commitments in a Poseidon tree, spent by proof. A cast can be paid from a note; the output goes back to a one-time key. The batch is then settled against the pool, and the pool is nobody.

Watch hook
a sealed rule, in your browser
Shade hook
batches a pool; one net swap per batch
Shaded pool
notes, nullifiers, Groth16
Cast
an order into the open batch
Settle
close a batch; swap the residual
Surface
spend an output note out

Watch hooks: where a rule lives

The browser generates an AES-GCM key and marks it non-extractable; it lives in IndexedDB and can be used but never read. The set of rules and the last two hundred matches are one ciphertext in localStorage. A copied profile folder is ciphertext. A script that reads localStorage gets ciphertext.

Every thirty seconds, while the tab is visible, all enabled rules become one multicall: balances, slot0 words, liquidity words. The RPC sees addresses and slots. It does not see a name, a line, a side, or that anything fired.

A rule fires on a crossing, not a level — the tick on which the value steps over the line. The first read after a rule is set only records where things stand, so a rule drawn below today’s balance does not fire the moment it is made.

To move rules to another browser, export them under a passphrase: PBKDF2-SHA-256 at 310,000 rounds derives the key, and the file is ciphertext without it. There is no account to recover from. That is the point and it is also the cost.

// one tick
reads  = rules.map(toCall)        // address + slot, nothing else
values = multicall3(reads)        // one eth_call
for rule in rules:
  if crossed(rule.last, values[rule], rule.line, rule.side):
    history.unshift(match)        // kept local, sealed
  rule.last = values[rule]
seal(rules, history)              // AES-GCM, key never leaves IndexedDB

The Shade hook

Three calls and three permissions. The permissions are in the address, and the hook has no owner.

cast

cast(PoolKey key, bool zeroForOne, uint128 amountIn, uint128 minOut, address recipient)
  returns (uint256 castId)

Pulls the input — an ERC-20, or native ETH as currency0 — and records the order in the batch that is open right now. A batch is two blocks of the clock the chain gives contracts — on Robinhood Chain that is the Ethereum block number, so a batch is about 24 seconds — and the batch id is that number divided by two. Up to 64 casts per pool per batch.

Nothing touches the pool here. The order sits with the hook until the window closes.

minOut is a hard floor. Before the residual is swapped, settle quotes it with a self-reverting swap, drops every cast that would land under its minOut, re-quotes the smaller set until nothing more drops, and refunds the dropped casts in full. A tight minOut cannot block the batch for anyone else.

settle

settle(PoolKey key, uint256 batchId)

Anyone may call it once the batch’s window has ended, once. Buys and sells are summed, the smaller side is matched in full at the pool’s spot price — no fee, no impact — and only the larger side’s surplus is swapped: one swap, exact input, through the singleton.

The surplus side shares the matched part at spot plus the residual’s output pro rata: one blended price, the same for every cast on that side. If the two sides match to the wei there is no residual, nothing is swapped, and the pool is not touched at all.

Outputs and refunds are credited to the recipient; a contract recipient that implements onSettled is told, under a gas cap, and if that call fails the credit simply waits in owed. That is how the shaded pool catches its own output. The swap’s output must equal the final quote or settle reverts.

collect

collect(Currency c)

Pulls whatever the hook owes you in that currency. Nothing is pushed to an address that did not ask.

beforeSwap · beforeAddLiquidity · beforeRemoveLiquidity

the three permissions the hook’s address declares

beforeSwap reverts for every caller but the hook itself, and the PoolManager does not consult hooks for the hook’s own swaps — so every call that reaches it is a direct swap, and it refuses. A pool under this hook is swapped only by a settlement.

The liquidity hooks revert unless the caller is the gate fixed at deployment — ShadeGate, an owner-only contract that holds the positions and pays removals back to its owner. The pool therefore has one liquidity provider. The hook itself has no owner: PoolManager and gate are immutable, and there is no pause, no upgrade and no fee switch.

The price rule

Call the sellers of currency0 side A, bringing A in total, and the sellers of currency1 side B, bringing B. P₀ is the pool’s spot price at settlement, read from the singleton’s storage before anything is swapped. The smaller side is matched in full at P₀ — no fee, no impact — and every cast on it receives exactly P₀, pro rata.

The larger side’s surplus, A − B/P₀ of currency0 or B − A·P₀ of currency1, is the residual. The pool executes it at whatever it gives, worse than P₀ by its fee and its impact, and the surplus side shares the matched part at P₀ plus the residual’s output pro rata: one blended price, the same for every cast on that side, never better than P₀.

Why not one price for both sides? Because it would not conserve: the matched side would be owed more of the other token than the hook holds, and the gap is exactly the residual’s slippage. Somebody carries it, and the side that caused it does. With no surplus there is no swap at all, and the pool’s liquidity and sqrtPrice are read back unchanged — from the singleton’s storage, not from the hook’s own bookkeeping.

The address is the permission

The PoolManager reads bits 0–13 of a hook address and calls only what they declare. A hook is deployed by CREATE2 to an address that carries the right bits, and the singleton rejects any other at initialize. The Shade hook declares three.

  • 13 beforeInitialize
  • 12 afterInitialize
  • 11 beforeAddLiquidity
  • 10 afterAddLiquidity
  • 9 beforeRemoveLiquidity
  • 8 afterRemoveLiquidity
  • 7 beforeSwap
  • 6 afterSwap
  • 5 beforeDonate
  • 4 afterDonate
  • 3 beforeSwapReturnsDelta
  • 2 afterSwapReturnsDelta
  • 1 afterAddLiquidityReturnsDelta
  • 0 afterRemoveLiquidityReturnsDelta

The shaded pool

RAILGUN’s shape: Poseidon commitments, nullifiers, a 2-in 2-out circuit, notes encrypted to an x25519 viewing key. Two calls are the standard ones; two are new.

shield

shield(ShieldRequest req) returns (uint256 commitment, uint32 leafIndex)

Public in. The pool pulls the token, keeps 0.25%, and inserts Poseidon(npk, token, amount) into a 20-level Poseidon Merkle tree. The ciphertext you pass is the note, encrypted to your viewing key, stored next to the leaf so a wallet can find it with eth_call alone.

transact

transact(Proof proof, uint256 root, uint256[2] nullifiers, uint256[2] commitments,
  uint256 publicAmount, address token, ExtData ext)

A Groth16 proof spends up to two notes and creates two. With publicAmount zero it is a private transfer: sender, receiver and amount are off the ledger. With publicAmount p − x it unshields x to ext.recipient.

The proof binds ext — recipient, relayer, fee, the two encrypted outputs — so a relayer that changes any of them has a proof that no longer verifies.

castFromShade

castFromShade(…transact arguments…, CastParams p)

An unshield whose recipient must be the pool itself, followed by a cast of the amount less the 0.25% and the relayer’s fee. The pool is the address the batch sees; the person is a nullifier. The same proof cannot be replayed through transact, which refuses an unshield to the pool.

p carries the pool key, the direction, minOut, and a fresh one-time note key for the output plus its memo. p is bound into the proof — the public input is keccak(ext, p) — so a relayer cannot change the direction, the floor or the output key. The memo is encrypted before the output amount is known, which is why the token and the value are written beside it in the clear when it arrives.

onSettled

onSettled(uint256 castId, address currencyOut, uint256 amountOut)

Only the hook may call it. The pool collects what it is owed and inserts Poseidon(npk, currencyOut, amountOut) as a new leaf under the one-time key the caster chose, with no fee on the way back in. A refund re-shields the same way, in the input token. If the call did not run, reshield(castId, key) rebuilds the note from the hook’s own record, permissionlessly.

The amount is public; it was always going to be, since a batch’s totals are on the ledger. The key it is shielded to is used once and belongs to nobody the chain can name.

What the pool does not hide: amounts in and out of it, and the set of addresses that have ever shielded. What it hides: who holds what inside it, who sent what to whom, and which note paid for which cast. The trust model is the verifier and the tree; the owner can change the treasury and the screen and nothing else.


What is real

Row by row. A site whose argument is that it says what is real cannot carry a stale claim, so this table is checked after every change on chain.

ClaimStatusDetail
The Uniswap v4 singleton read by this siteRealDeployed at 0x8366a39cc670b4001a1121b8f6a443a643e40951 on chain 4663, 24,009 bytes. The site reads its storage directly.
The pool list, prices, fees and liquidityRealThe list of 160 pools (79 hooked) is a snapshot at block 96,027,693; every number beside it is read by your browser from a public RPC when you open the page. No indexer, no key, no server of ours.
Watch hooksRunningSealed in your browser under a non-extractable AES-GCM key, evaluated every thirty seconds while the tab is visible as one multicall. Nothing is sent anywhere but the read. A hidden tab reads nothing, and the page says so.
Hook permissions on the pools pageRealDecoded from bits 0–13 of each hook address, which is exactly what the PoolManager itself reads.
The liquidity gateDeployedAt 0x39e7D3EBFf16D1c13E3422E4AeAca53526246f01: the one address the hook lets add or remove liquidity. Owner-only, holds the positions, pays removals back to the owner, and initialises pools under the hook. The owner is the project wallet.
The Shade hookDeployedAt 0x862e24Dc6FBbD69e5904d4f9936d6bfb0b7A8a80 — an address whose low bits are 0x0a80, mined and deployed through the Create2Deployer. One market under it so far: WETH/USDG, 0.30%, full-range liquidity through the gate. The same code passes 80 checks against the singleton’s real bytecode on every build; it has not been audited.
The shaded poolDeployedAt 0x3fBf0b82365250A701d36E3A46Be05FC7Ae47209. The shield → cast → settle → surface round trip has run on this chain with real proofs made in a browser-grade prover. The Groth16 setup is a development one; a public ceremony replaces the proving key and verifier before anyone should hold real value in it.
A full shielded round tripDone on chainTwo USDG shielded, cast from the shade with a real proof, settled as the only order in its batch, re-shielded as a WETH note under a one-time key, and surfaced to a wallet: 0.000530806 WETH, which is the batch output less the 0.25%. The transaction hashes are in the contracts README.
The batch settlement on the Shade pageSimulatedThe hook’s arithmetic run in your browser over a pool’s live sqrtPrice, liquidity and fee, as a single in-range position. Exact until the residual crosses a tick.
The nacre in the heroComputedA transfer matrix through an aragonite–conchiolin stack, integrated against the CIE 1931 observer. The hook over it is a photograph-grade render, keyed onto the panel.
The token $HOOKSHADENot yetLaunches on pons; the address will be written here and on the landing in full. It is a separate contract from the hook and the pool, and nothing on this site is an offer to sell it.
An auditNoneNothing here has been audited by anyone. The tests are ours and so is the reasoning.

Addresses

$HOOKSHADE
published at launch