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
| Chain | Robinhood Chain |
|---|---|
| Chain id | 4663 (0x1237) |
| Testnet id | 46630 (0xb626) |
| RPC | https://rpc.mainnet.chain.robinhood.com |
| Explorer | robinhoodchain.blockscout.com |
| Gas token | ETH |
| Stack | Arbitrum Nitro, settling to Ethereum |
| EVM | Cancun. 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
| Contract | Address |
|---|---|
| Obol token | Not deployed |
| ObolHook | Not deployed — will satisfy addr & 0x3FFF == 0x30C4 |
| Hoard | Not deployed |
| v4 PoolManager | 0x8366a39CC670B4001A1121B8F6A443A643e40951 |
| v4 PositionManager | 0x58daec3116aae6D93017bAAea7749052E8a04fA7 |
| v4 StateView | 0xF3334192D15450CdD385c8B70e03f9A6bD9E673b |
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.
| Category | Values |
|---|---|
| Background | void, pitch green, slate, ash |
| Bands | steel, iron, brass, silver |
| Seal | none, lead, wax, gold |
| Light | green, none, white, amber |
| Particles | none, green, white |
| Border | none, 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.
| Callback | Bit | Behaviour |
|---|---|---|
| beforeInitialize | 0x2000 | Reverts unless the key matches the canonical OBOL pool. Prevents anyone binding the hook to a pool of their own choosing. |
| afterInitialize | 0x1000 | Calls updateDynamicLPFee to set the 1.00% base, since dynamic pools open at zero. |
| beforeSwap | 0x0080 | Returns fee | OVERRIDE_FEE_FLAG derived from the trader's best grade. |
| afterSwap | 0x0040 | Reads sqrtPriceX96, writes the strike price to transient storage, and takes the hoard's share. |
| afterSwapReturnDelta | 0x0004 | Makes 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. |