Uniswap v4 hook · Robinhood Chain · tokenized stocks

Covered calls on AAPL and NVDA, settled by the pool itself.

A liquidity position already sells the stock as it rallies. This hook lets the LP sell that upside as a real covered call: the buyer pays a premium, the position is locked until expiry, and the payoff is taken out of the tokens that leave the pool. No oracle, no price feed, no custody.

Value at expiry · one unit of liquidity · $235–$265 range

LP position valueWhat the call buyer receives above a $250 strike

As the stock rises through the range the position converts stock into USDG. Everything it earns above the strike belongs to the call buyer; the LP keeps the rest, all swap fees, and the premium.

The lifecycle

Five steps, two hook callbacks, zero oracles.

0101 · LP

Provide liquidity

An LP mints an AAPL/USDG position in a pool that uses the hook, through the normal Uniswap v4 PositionManager. They keep full ownership of the LP position.

0202 · Writer

Write the call

The LP lists a call on that position: a strike inside the range, an expiry, and a premium in USDG. Nothing is locked yet.

0303 · Buyer

Premium, then lock

The premium goes into escrow and the position locks. beforeRemoveLiquidity now reverts any withdrawal or burn until expiry. Fee collection still works.

0404 · Clock

Expiry

After expiry anyone can trigger settlement, because the LP approved the hook as an operator on the LP position when they wrote the call.

0505 · Hook

Settle at unwind

afterRemoveLiquidity reads the tokens leaving the position, derives the price from their mix, and takes the buyer's payoff straight out of the withdrawal. The premium is released to the LP.

Why no oracle is needed

The position's own composition is the price.

A concentrated position with liquidity L on a range [Pa, Pb] holds, at price P:

amount1 = L · (√P − √Pa)
amount0 = L · (1/√P − 1/√Pb)

So when the position is unwound, the quote it hands back pins the price exactly: √P = √Pa + amount1 / L. Nobody reports a price and nobody can lie about one.

The call pays, in USDG, every dollar the position earned by selling the stock above the strike K:

payoff = L · max(√P − √K, 0)
capped at amount1, and at L · (√Pb − √K) once P ≥ Pb

The strike must sit inside the range, so the position always holds enough to pay. It is a covered call by construction.

Why the writer cannot cheat

Locks, escrow, and a grace period.

  • The lock follows the position. It is keyed the way the PoolManager keys positions (pool, owner contract, ticks, token id), so transferring the LP position changes nothing.
  • Premium is escrowed in the hook and released to the writer only when the call is fully settled.
  • Anyone can force settlement after expiry through the writer's operator approval, so the writer cannot wait for a dip.
  • If the writer revokes that approval and stalls past the 3-day grace, the buyer reclaims the premium. The lock still holds, so the payoff is still taken whenever the writer finally unwinds. Stalling costs the premium and gains nothing.
  • Partial unwinds settle pro rata. Liquidity added during the lock is not part of the deal.

Options are the largest retail product on Robinhood's app. Robinhood Chain puts the underlying stocks on-chain as plain ERC-20s and ships Uniswap v4. This hook is the missing piece between the two.

Robinhood Chain testnet · read directly from the public RPC

Live feed

Connecting…
Chain
—
Latest block
—
Stock price in pool
—
Calls written
—
Hook
—

Options newest first · click one to chart it

Loading…

Payoff curve select an option

No option selected.

Pool price from Swap events

No swaps yet.

Event log

Loading…

Pool

Robinhood Chain testnet · your wallet, real transactions

Trade

Provide liquidity, write a covered call on it, buy one, settle, withdraw. Every button sends a real transaction from your wallet to the hook or the Uniswap v4 PositionManager.

loading wallet…
WALLET

Balances

Address
—
ETH
—
Stock
—
Quote
—
Claimable in hook
—

Test tokens are mock ERC-20s with an open mint: 100 stock and 50,000 quote per click. Gas needs testnet ETH from the faucet.

01

Provide liquidity

Mints a Uniswap v4 position in the hooked pool around the current price. The position is the collateral for any call you write on it.

—
02

Write a call

Approves the hook as operator on the LP position first if needed.
03

Open calls

Connect a wallet to load.
04

Active & settled

Anyone can settle an expired call. Payoff goes to the holder's claimable balance, the premium to the writer's; withdraw above.

—
LOG

Transactions

Nothing yet.

Overview

CoveredCallHook is a Uniswap v4 hook plus a very small option market. It uses two liquidity callbacks and nothing else: beforeRemoveLiquidity to lock a position while a bought call is live, and afterRemoveLiquidity (with the return-delta permission) to settle out of the withdrawn tokens. Swaps are untouched. Pools must be created with the hook in their key.

Positions are standard Uniswap v4 LP positions, held through the PositionManager. The LP keeps custody throughout. Premiums and payoffs are always in the pool's quote token (USDG); the hook never holds stock tokens.

The instrument

Write parameters: the LP position ID (tokenId), callOnCurrency0 (true when the stock is the pool's currency0), a strike as sqrtPriceX96, an expiry timestamp, and a premium in the payoff currency.

Call on currency0 (stock is token0)

√P = √Pa + amount1 / L
payoff (currency1) = L · max(√P − √K, 0), capped at amount1
strike constraint: √Pa ≤ √K < √Pb

Call on currency1 (stock is token1, as AAPL and NVDA are on mainnet)

1/√P = amount0 / (L · 2⁹⁶) + 1/√Pb
payoff (currency0) = L · max(1/√P − 1/√K, 0), capped at amount0
strike constraint: √Pa < √K ≤ √Pb

Each derivation uses the side of the principal that pays, rounds in the writer's favour, and is capped by what actually left the position, so the hook can never owe more than it holds. Swap fees collected on removal are excluded from settlement and stay with the LP. Partial removals settle the removed fraction; the lock releases when the covered liquidity is fully settled.

Hook callbacks

CallbackBehaviour
beforeRemoveLiquidityReverts PositionLocked(optionId, expiry) if the position has an active call and block.timestamp < expiry. A zero-liquidity modify (fee collection) passes. Increases are not hooked.
afterRemoveLiquidityIf the position has an active call (necessarily expired), computes the payoff from delta − feesAccrued, credits it to the holder, calls take for it, and returns a hook delta of the same amount so the LP's withdrawal is reduced. Releases the premium when settlement completes.
all othersNot implemented. Hook address flags: beforeRemoveLiquidity | afterRemoveLiquidity | afterRemoveLiquidityReturnDelta (0x301).

Contract API

FunctionWhoWhat
writeCall(tokenId, callOnCurrency0, strikeSqrtPriceX96, expiry, premium) → optionIdposition ownerLists a call. Requires the hook to be approved on the LP position and the position to live in a pool with this hook. Does not lock.
cancelCall(optionId)writerWithdraws an unsold listing.
buyCall(optionId)anyonePulls the premium (ERC-20 approval needed), checks the position is still intact and unlocked, locks it.
transferCall(optionId, to)holderTransfers the right to the payoff.
settleExpired(optionId)anyoneAfter expiry: decreases the covered liquidity through the PositionManager as operator and sends the remainder to the LP position owner. Settlement happens in the callback.
reclaimPremium(optionId)holderAfter expiry + SETTLEMENT_GRACE (3 days) if still unsettled.
withdraw(currency)anyonePulls the caller's claimable balance (payoffs, premiums).
getOption(id), previewPayoff(id, sqrtPriceX96), maxPayoff(id), payoffCurrency(id), lockOf(positionKey), claimable(account, currency)viewsState and quoting helpers for UIs.

Events & errors

EventWhen
CallWrittenlisting created (optionId, tokenId, writer, side, strike, expiry, liquidity, premium)
CallBoughtpremium escrowed, position locked
CallSettledeach settling removal: liquidity settled, settlement √price, payoff, complete flag
PremiumReclaimed, CallCancelled, CallTransferred, Withdrawnas named

Errors: PositionLocked, NotPositionOwner, HookNotApprovedOnPosition, WrongPool, InvalidStrike, InvalidExpiry, ZeroPremium, WrongStatus, AlreadyLocked, ListingExpired, PositionShrunk, NotExpired, GracePeriodNotOver, NotBuyer, NotWriter, NothingToWithdraw. A hook revert surfaces from the PoolManager wrapped as WrappedError(hook, selector, reason, HookCallFailed).

Addresses

Uniswap v4 is deployed at the same addresses on Robinhood Chain mainnet (4663) and testnet (46630).

ContractAddress
PoolManager0x8366a39CC670B4001A1121B8F6A443A643e40951
PositionManager0x58daec3116aae6D93017bAAea7749052E8a04fA7
StateView0xf3334192d15450cdd385c8b70e03f9a6bd9e673b
Permit20x000000000022D473030F116dDEE9F6B43aC78BA3
CREATE2 deployer0x4e59b44847b379578588920cA78FbF26c0B4956C
AAPL (mainnet)0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9
NVDA (mainnet)0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC
USDG (mainnet)0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168
CoveredCallHook (testnet)not deployed yet

RPC: https://rpc.mainnet.chain.robinhood.com and https://rpc.testnet.chain.robinhood.com. Explorers: robinhoodchain.blockscout.com and explorer.testnet.chain.robinhood.com.

Run it yourself

Foundry project with 22 tests against the real PoolManager and PositionManager. The scripts write deployments/<chainId>.json, which this page reads as its config.

# deploy the hook (CREATE2-mined address with the right permission bits)
forge script script/DeployCoveredCallHook.s.sol --rpc-url robinhood_testnet --broadcast

# create a stock/quote pool on the hook (DEPLOY_MOCKS=true deploys tAAPL + tUSDG on testnet)
DEPLOY_MOCKS=true forge script script/CreateStockPool.s.sol --rpc-url robinhood_testnet --broadcast

# mint an LP position, write a 10-minute call, buy it, push the price through the strike
forge script script/TestnetDemo.s.sol --sig "run()" --rpc-url robinhood_testnet --broadcast

# after expiry: force settlement and withdraw
forge script script/TestnetDemo.s.sol --sig "settle()" --rpc-url robinhood_testnet --broadcast

Writing a call by hand: approve the hook on the LP position (PositionManager.approve(hook, tokenId)), then writeCall. Buying: approve the quote token to the hook, then buyCall. Strikes are sqrtPriceX96 values; a UI should convert from a dollar strike with sqrt(price) · 2⁹⁶, minding token order.

Limits & risks

  • New pools only. The hook is part of the pool key, so liquidity must be in hooked pools, not the existing hookless AAPL/USDG pools.
  • Premium at settlement. The writer receives the premium when the call settles, not when it is bought. That is what makes the no-custody enforcement work.
  • Settlement price is the price at unwind, which is why anyone may settle the moment the call expires. Run a keeper or settle from the buyer's side.
  • Writer sets the premium. There is no on-chain pricing. Quote Black-Scholes off-chain; Robinhood publishes a Chainlink feed per stock token.
  • Not audited. Testnet only until it is.