Documentation · v0 · unaudited
Every coin struck at
the price you paid
OBOL is an Obol-inspired token economy for Robinhood Chain: a fixed-supply ERC-20 governance token, a Uniswap v4 hook that ferries a toll to a retroactive-funding treasury, and ERC-402 — a draft standard where the token itself remembers each holder's cost basis.
0x0B01…D5F7 on Ethereum; this is a
different contract on a different chain.Quickstart
The repo is a standard Foundry project. Dependencies (Uniswap v4-core / v4-periphery,
OpenZeppelin, forge-std) are vendored under lib/.
# build all contracts forge build # run the full suite (ERC-402 accounting, token, v4 integration) forge test -vv
| Path | What it is |
|---|---|
src/ObolToken.sol | The OBOL token — ERC-20 + Permit + Votes + ERC-402. |
src/hooks/CharonTollHook.sol | Uniswap v4 hook: launch-fee decay, toll, ERC-402 striker. |
src/erc402/ | IERC402.sol + ERC402.sol reference implementation. |
spec/ERC-402.md | The draft standard, written up EIP-style. |
src/launch/ | SingleSidedSeed — computes the single-sided launch range and liquidity. |
script/Deploy.s.sol | One-shot deploy + hook-address mining + pool init + single-sided seed. |
Architecture
A single buy on the OBOL pool touches every piece in one transaction: the hook taxes the ETH side, enforces the max buy, and strikes the buyer's coins at the execution price.
ERC-402 — Price-Struck Fungible Tokens
ERC-20 tokens forget their history the moment coins move. ERC-402 tokens don't. Each holder carries an on-chain weighted-average strike price — a native cost basis, denominated in a declared quote asset. This makes PnL, tax lots, and execution reporting consensus facts rather than an indexer's opinion — which is exactly what a brokerage-grade chain needs.
Coins acquire a strike price two ways:
- Transfers carry their strike. A recipient's average blends in the sender's average, weighted by amount. The sender's average never changes.
- Strikers declare execution prices. Authorized venues (like the AMM
hook that watched your swap execute) call
strike()with the exact price, which takes precedence over blending for that delivery.
402
Payment Required is the web's placeholder for a price; ERC-402 records the price
that was actually paid.Interface IERC402 · requires ERC-20, ERC-165
interface IERC402 { event Struck(address indexed striker, address indexed account, uint256 amount, uint256 priceWad); event StrikerSet(address indexed striker, bool allowed); function quoteAsset() external view returns (address); function priceStruckOf(address account) external view returns (uint256 avgPriceWad); function costBasisOf(address account) external view returns (uint256 quoteValue); function strikeCreditOf(address account) external view returns (uint256); function isStriker(address account) external view returns (bool); function strike(address account, uint256 amount, uint256 priceWad) external; }
Units. Prices are wads — quote-asset smallest units per
1e18 token units. quoteAsset() may be
address(0) for the native currency. By definition
costBasisOf(a) == balanceOf(a) * priceStruckOf(a) / 1e18.
Accounting rules average-cost method
| Event | Effect on the average strike |
|---|---|
| Transfer A→B of n | B's average becomes the amount-weighted blend of B's old average and A's average. A is unchanged. |
| strike(B, n, p) | Blends n coins at price p into B immediately, and records a strike credit of n. Reverts unless the caller is a striker. |
| Delivery to a credited account | Up to credit coins skip blending (already priced) and reduce the credit. Remainder blends normally. |
| Mint | Blends at price 0 ("struck at genesis") unless a striker credited it first. |
| Burn | Never changes the burner's average. |
strike() must be called before the matching delivery,
in the same transaction.Contracts
Three contracts, wired together at deploy time. The token trusts the hook as an ERC-402 striker; the hook holds the treasury address and toll parameters.
ObolToken is ERC402, ERC20Permit, ERC20Votes, Ownable
Fixed supply of 600,000,000 OBOL, minted once at construction to the deployer — and every last coin is seeded into the launch pool. No team allocation, no vesting cliffs; the treasury is funded by the launch tax and toll instead. Governance ships in the token — it's ERC-20 Votes, delegable from block one — and permit enables gasless approvals.
constructor( address owner_, // receives the full supply, for seeding the pool address quoteAsset_ // ERC-402 strike denomination (0 = native ETH) )
| Member | Notes |
|---|---|
setStriker(account, allowed) | onlyOwner. Authorize/revoke an ERC-402 striker — grant this to the CharonTollHook after deploy. |
TOTAL_SUPPLY | Constant, 600_000_000e18. |
delegate / getVotes | Standard ERC-20 Votes governance surface. |
CharonTollHook is BaseHook, Ownable
A Uniswap v4 hook with four duties, all firing inside the swap.
- The launch tax — buys and sells both open under a 50% tax
that decays linearly to the resting 0.10% toll over the first hour, collected in
the quote currency (ETH) for the treasury. Buy-side tax accrues as ERC-6909 claims
(the buyer's ETH hasn't settled when the hook runs) redeemed via
collectTax(); sell-side tax is taken from the ETH output directly. LP fee stays a flat 0.30%. - The max buy — any single swap buying more than 0.77% of supply reverts. Owner-tunable, 0 disables.
- Charon's toll — after the decay, the resting 0.10% tax remains on every crossing (capped at 1%, owner-tunable).
- The striker — when the pool trades OBOL, each buy calls
strike()at the exact execution price, tax included. If the hook isn't (yet) an authorized striker, the call is caught and the pool never bricks.
| Member | Notes |
|---|---|
currentTaxBpsOf(poolId) | The decaying buy/sell tax the pool charges right now, in bps. |
setTollBps(bps) | onlyOwner. The resting tax. Reverts with TollTooHigh above MAX_TOLL_BPS (100 = 1%). |
setMaxBuyBps(bps) | onlyOwner. Per-swap buy cap in bps of supply (default 77 = 0.77%); 0 disables. |
setTreasury(addr) | onlyOwner. Redirect where the tax lands. |
collectTax(currency, to) / claimableTax(currency) | onlyOwner. Redeem accrued buy-tax claims for real currency. |
seedSingleSided(key, amount, payer) | onlyOwner. Seed OBOL-only launch liquidity; the hook holds the position. |
withdrawSeed(key, recipient) | onlyOwner. Withdraw the seeded position plus accrued LP fees. |
| hookData | Optional ABI-encoded address naming the strike beneficiary; defaults to tx.origin. |
afterInitialize, beforeSwap,
afterSwap, and afterSwapReturnDelta.
v4 encodes permissions in the address bits, so the deploy script mines a CREATE2 salt to
land on a matching address.Deploy — Robinhood Chain
Robinhood Chain is an Arbitrum Orbit L2 that settles to Ethereum, with ETH as the gas
token — a fitting home for a cost-basis-aware token standard. Both RPCs are preconfigured in
foundry.toml.
| Testnet | Mainnet | |
|---|---|---|
| Chain ID | 46630 | 4663 |
| RPC | rpc.testnet.chain.robinhood.com | rpc.mainnet.chain.robinhood.com |
| Explorer | explorer.testnet.chain.robinhood.com | robinhoodchain.blockscout.com |
| Faucet | faucet.testnet.chain.robinhood.com | — |
Get testnet ETH from the faucet, then deploy. The script deploys the token, mines the
hook address, grants it striker rights, initializes the dynamic-fee launch pool, and
seeds it single-sided — OBOL only, in a range just above the launch
price. Buyers bring the quote asset; the launch price is the floor, and there is nothing
to dump into at t=0. The hook itself holds the LP position (protocol-owned
liquidity), so launch fees accrue to the protocol, and the owner can withdraw the position
plus accrued fees via withdrawSeed().
# minimal testnet deploy (fresh PoolManager, native-ETH quote)
PRIVATE_KEY=0x… forge script script/Deploy.s.sol \
--rpc-url robinhood_testnet --broadcast
| Env var | Default | Meaning |
|---|---|---|
PRIVATE_KEY | required | Deployer key. |
POOL_MANAGER | fresh | Reuse an existing v4 PoolManager instead of deploying one. |
QUOTE_ASSET | address(0) | Pool + ERC-402 quote currency (native ETH by default). |
RAF_TREASURY | deployer | Retroactive-funding treasury. |
START_MCAP_USD | 5000 | Launch market cap in USD; sets the pool's start price. |
ETH_PRICE_USD | 1880 | ETH/USD used to convert the market cap. |
SQRT_PRICE_X96 | 1:1 | Initial pool price (the launch floor). |
SEED_OBOL | full supply | OBOL seeded single-sided at launch; 0 skips. |
Verify on Blockscout:
forge verify-contract <ADDRESS> src/ObolToken.sol:ObolToken \ --verifier blockscout \ --verifier-url https://explorer.testnet.chain.robinhood.com/api \ --chain-id 46630
Security notes
- Strikers are trusted price oracles. A malicious striker can falsify cost bases or leave dangling credits. Grant it only to venues whose price reports are tied to real executions (like the AMM hook reading swap deltas).
- priceStruckOf is provenance, not a price oracle. Transfers at an old average can nudge a counterparty's basis; protocol logic consuming it should treat it as informational unless every strike path is trusted.
- Fresh PoolManager is testnet-only. Uniswap v4-core is BUSL-licensed; production chains use the governed canonical deployment.
- Unaudited. Do not launch real-value tokens from this code without an audit.