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)
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)
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
| Callback | Behaviour |
|---|---|
beforeRemoveLiquidity | Reverts 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. |
afterRemoveLiquidity | If 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 others | Not implemented. Hook address flags: beforeRemoveLiquidity | afterRemoveLiquidity | afterRemoveLiquidityReturnDelta (0x301). |
Contract API
| Function | Who | What |
|---|---|---|
writeCall(tokenId, callOnCurrency0, strikeSqrtPriceX96, expiry, premium) → optionId | position owner | Lists 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) | writer | Withdraws an unsold listing. |
buyCall(optionId) | anyone | Pulls the premium (ERC-20 approval needed), checks the position is still intact and unlocked, locks it. |
transferCall(optionId, to) | holder | Transfers the right to the payoff. |
settleExpired(optionId) | anyone | After 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) | holder | After expiry + SETTLEMENT_GRACE (3 days) if still unsettled. |
withdraw(currency) | anyone | Pulls the caller's claimable balance (payoffs, premiums). |
getOption(id), previewPayoff(id, sqrtPriceX96), maxPayoff(id), payoffCurrency(id), lockOf(positionKey), claimable(account, currency) | views | State and quoting helpers for UIs. |
Events & errors
| Event | When |
|---|---|
CallWritten | listing created (optionId, tokenId, writer, side, strike, expiry, liquidity, premium) |
CallBought | premium escrowed, position locked |
CallSettled | each settling removal: liquidity settled, settlement √price, payoff, complete flag |
PremiumReclaimed, CallCancelled, CallTransferred, Withdrawn | as 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).
| Contract | Address |
|---|---|
| PoolManager | 0x8366a39CC670B4001A1121B8F6A443A643e40951 |
| PositionManager | 0x58daec3116aae6D93017bAAea7749052E8a04fA7 |
| StateView | 0xf3334192d15450cdd385c8b70e03f9a6bd9e673b |
| Permit2 | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
| CREATE2 deployer | 0x4e59b44847b379578588920cA78FbF26c0B4956C |
| 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.