Documentation

Integration

Interfaces, events and the things that will surprise you. For the conceptual model, read the overview first.

Pre-deployment

No contracts are deployed. The signatures below are the interface we are building to and may still change before launch. Addresses will be published on this page with verified bytecode when they exist — if you find an OBOL address anywhere else today, it is not ours.

Network

ChainRobinhood Chain
Chain id4663 (0x1237)
Testnet id46630 (0xb626)
RPChttps://rpc.mainnet.chain.robinhood.com
Explorerrobinhoodchain.blockscout.com
Gas tokenETH
StackArbitrum Nitro, settling to Ethereum
EVMCancun. TSTORE/TLOAD, MCOPY and PUSH0 all available, which is what makes v4 possible here.

One sequencing note that may matter to you: the chain uses first-come-first-served ordering, so transactions cannot bid for priority. If you are building anything that assumes a priority-fee auction, that assumption does not hold here.

Addresses

ContractAddress
Obol tokenNot deployed
ObolHookNot deployed — will satisfy addr & 0x3FFF == 0x30C4
HoardNot deployed
v4 PoolManager0x8366a39CC670B4001A1121B8F6A443A643e40951
v4 PositionManager0x58daec3116aae6D93017bAAea7749052E8a04fA7
v4 StateView0xF3334192D15450CdD385c8B70e03f9A6bD9E673b

The three Uniswap addresses are the canonical v4 deployment on chain 4663 and are not ours. Verify them against Uniswap's own deployments registry before use.

Dual dispatch

One contract serves both interfaces, so approve and transferFrom are overloaded by value. Ids are offset by a high-bit prefix so they cannot collide with plausible token amounts.

uint256 constant ID_PREFIX = 1 << 255;

// value < ID_PREFIX  -> treated as an ERC-20 amount
// value >= ID_PREFIX -> treated as an ERC-721 token id

transferFrom(from, to, 5e17);              // 0.5 OBOL
transferFrom(from, to, ID_PREFIX | 402);      // coin #402

When displaying ids to users, subtract the prefix. Coin #402 is ID_PREFIX | 402 on-chain.

Reading a coin

// --- supply -------------------------------------------------
function totalSupply()          external view returns (uint256);
function obolCirculating()      external view returns (uint256); // struck - banked
function obolStruck()           external view returns (uint256); // highest id issued
function obolBanked()           external view returns (uint256);

// --- the coin itself ----------------------------------------
function strikeOf(uint256 id)  external view returns (uint256 priceWei);
function gradeOf(uint256 id)   external view returns (Grade);
function struckAt(uint256 id)  external view returns (uint64 blockNumber);
function tokenURI(uint256 id)  external view returns (string memory);

// --- holdings -----------------------------------------------
function obolBalanceOf(address) external view returns (uint256);
function obolsOf(address)      external view returns (uint256[] memory);
function bestGradeOf(address)  external view returns (Grade); // sets the fee
function isExempt(address)     external view returns (bool);

// --- backing -------------------------------------------------
function hoardBalance()        external view returns (uint256);
function backingPerObol()      external view returns (uint256);

enum Grade { Good, Fine, VeryFine, ExtremelyFine, MintState, Proof }

strikeOf returns wei per whole unit at the moment of the strike, or zero if the coin was struck outside a swap. Grade is ordered ascending so a numeric comparison is a quality comparison.

Do not confuse grade with the cosmetic traits. Grade is consensus state: it comes from the strike price, it is stored, and it decides the fee. The six traits are a pure function of the id, derived identically on-chain and in the interface, stored nowhere and priced by nothing. If you are indexing, treat traits as display metadata.

Traits

Six cosmetic categories, each a deterministic weighted draw from the token id. They are regenerated rather than stored, so a recycled id keeps its traits even though it is re-struck at a new price and can come back at a different grade.

CategoryValues
Backgroundvoid, pitch green, slate, ash
Bandssteel, iron, brass, silver
Sealnone, lead, wax, gold
Lightgreen, none, white, amber
Particlesnone, green, white
Bordernone, hairline, green, white
function traitsOf(uint256 id) external pure returns (Traits memory);

struct Traits { uint8 background; uint8 bands; uint8 seal; uint8 light; uint8 particles; uint8 border; }

pure, not view — there is no state to read. The same id returns the same traits on any chain, forever.

Events

// Standard, emitted for both legs.
event Transfer(address indexed from, address indexed to, uint256 amount);
event Transfer(address indexed from, address indexed to, uint256 indexed id);

// ERC-402 specific.
event Struck(
    uint256 indexed id,
    address indexed to,
    uint256 strikePrice,
    Grade   grade,
    bool    recycled
);

event Melted(uint256 indexed id, address indexed from);
event HoardFunded(uint256 amount, uint256 newBalance);
event GateOpened(uint256 timestamp);

// EIP-4906, emitted when a banked id is re-struck.
event MetadataUpdate(uint256 _tokenId);

Index on Struck to build the price history. recycled tells you whether the id came from the bank, which matters because a recycled coin's earlier strike and grade are no longer valid and any cached metadata should be dropped.

Hook interface

The hook implements five of v4's fourteen permissions. Those five are encoded in the low bits of its address, which is mined with CREATE2 before deployment and can never change.

CallbackBitBehaviour
beforeInitialize0x2000Reverts unless the key matches the canonical OBOL pool. Prevents anyone binding the hook to a pool of their own choosing.
afterInitialize0x1000Calls updateDynamicLPFee to set the 1.00% base, since dynamic pools open at zero.
beforeSwap0x0080Returns fee | OVERRIDE_FEE_FLAG derived from the trader's best grade.
afterSwap0x0040Reads sqrtPriceX96, writes the strike price to transient storage, and takes the hoard's share.
afterSwapReturnDelta0x0004Makes that share actually chargeable. Without it the delta is silently discarded.

The hook does not identify you

In v4 the sender argument is the router, and hookData comes straight from the caller. Neither is trustworthy, so the hook uses neither. It publishes the price to transient storage and the token — which already knows the real recipient — decides whether a coin is owed. If you are integrating, you do not need to pass anything in hookData; it is ignored.

Fee resolution

The pool's fee field is 0x800000 exactly — the dynamic-fee sentinel. Quoting a trade therefore requires knowing the trader, because two wallets get two different prices for the same swap.

Grade            Fee      Basis points
--------------   ------   ------------
Proof            0.25%    25
Mint State       0.40%    40
Extremely Fine   0.55%    55
Very Fine        0.70%    70
Fine             0.85%    85
Good / none      1.00%    100   <- base

Call bestGradeOf(trader) to resolve the applicable tier before quoting. Standard quoters that ignore the hook will return the base fee and under-quote the output for any holder.

Gotchas

Ids are recycled Coin #402 today may be a different coin, with a different strike and grade, next month. Never cache metadata without listening for MetadataUpdate.
Whole-unit transfers are heavier Moving 3.0 OBOL moves three NFTs. Budget gas accordingly; fractional transfers below a unit boundary are cheap.
Contracts receive coins If your contract will hold whole units and does not want the ERC-721 side, request exemption. Unlike earlier hybrids, self-exemption is not gated on tx.origin, so smart accounts can call it.
Quotes are wallet-specific A cached quote is only valid for the address it was generated for.
Unpriced strikes exist A coin created by a plain transfer rather than a swap has strikeOf == 0 and the lowest grade. Handle the zero case.
The gate Before GateOpened, transfers between non-exempt addresses revert. Do not treat that as a bug in your integration.