OHMVAULT: a token floored on OHM
OHMVAULT is an ERC-20 on Ethereum that trades in a single Uniswap v4 pool against OHM, the Olympus reserve currency. The whole genesis supply and an OHM seed go into that pool as full-range liquidity that no contract can remove. A hook takes 5% of every swap and adds part of it back to the locked position, so the OHM under the curve grows with volume. Supply is elastic but gated: new tokens are minted by a Ratchet only while OHM backing per token is above an up-only mark, in both OHM and dollar terms, at most 2% of supply per six-hour slot; and by a Bond that pays for every token it mints with OHM added to the locked position, never below live backing.
The protocol is SEMI v1 (semivault.xyz, Robinhood Chain, August 2026) moved to Ethereum and to a new collateral. Most of the code is unchanged. This paper describes every contract and constant, and spends most of its length on the parts that changed and why, using SEMI's on-chain history as the test set. The working ticker is OHMV.
1System overview
Nine contracts. None has an owner. Every privileged setter is a one-shot pointer that the launch script calls once, after which it reverts for every address, the deployer's included.
| Contract | Role | Lines | Mutable configuration |
|---|---|---|---|
| VaultToken | ERC-20, 18 decimals; one gated mint path | 89 | minter, set once |
| VaultMinter | The only address the token accepts mints from; forwards for two callers | 54 | ratchet, bond, each set once |
| VaultLauncher | Opens the pool and owns the seed position | 175 | none; launched = true |
| VaultHook | v4 hook: 5% fee, launch minute, harvest, floor donations, backing oracle, TWAP | 454 | launcher, ratchet, each set once |
| VaultRatchet | Backing-gated emissions on a fixed 6 h grid, OHM and USD marks | 205 | none |
| VaultStaker | Receives emissions; pending, active and cooling stake | 197 | ratchet, set once |
| VaultBond | OHM in, locked LP up, discounted OHMV out over 24 h | 189 | none |
| OhmUsdOracle | Chainlink OHM/ETH × ETH/USD with staleness limits | 64 | none |
| VaultLens | One-call proof-of-reserves view | 79 | none |
The test suite has 32 unit and fuzz tests against a local PoolManager with a 9-decimal mock OHM and mock feeds, and one fork test that runs the full launch against the real mainnet PoolManager, OHM and Chainlink feeds. Fuzzing covers the two properties everything else rests on: a bond never lowers backing per token, and a mint never leaves live backing under the mark.
2The token
VaultToken is a minimal ERC-20 with no transfer hooks, no tax, no pause, no blacklist and no owner. Name, symbol and links are constructor arguments, so the final brand ships in the same bytecode the tests ran against and can't change afterwards. The constructor mints 10,000,000 tokens to the deployer. The only other mint path is mint(to, amount), callable only by the minter, which is set once to VaultMinter. VaultMinter accepts calls from exactly two contracts: VaultRatchet and VaultBond.
There is no burn function. Tokens sent to 0x…dEaD leave circulation but stay in totalSupply, and backing is always computed against totalSupply, so a burn never flatters the numbers.
3Launch and locked liquidity
The launch transaction
VaultLauncher.launch(ohmSeed) runs once, from the deployer, and does five things in one transaction:
- pulls the entire 10,000,000 tokens (
POOL_SUPPLY) and the OHM seed from the deployer; - initializes the OHMV/OHM pool with
lpFee 0,tickSpacing 60and VaultHook attached, priced exactly at the seed ratio; - adds everything as one full-range position owned by the launcher, after shaving
liq / 1,000,000 + 1so v4's round-up on adds can never ask for more than it holds; - calls
VaultRatchet.poke(), which starts the 6-hour clock and seals both marks at the seed backing; - emits
FloorPoured.
The planned seed is $2,000 of OHM, about 100 OHM at $20. Launch price is then 10,000,000 / 100 = 100,000 OHMV per OHM, and the fully diluted value at launch equals the seed. There is no dev bag: the deployer holds 0 OHMV when launch returns, and the launch script checks it.
The launcher has no function that removes liquidity, and the hook's own compounded position has the same property. OHM leaves the pool only when someone sells OHMV into it.
Deployment order
scripts/launch-ohmvault.mjs sends 18 transactions from one wallet, in this order, writing each address to deployment-ohmvault.json as it goes so an interrupted run can be read back:
| # | Transaction | Note |
|---|---|---|
| 1–4 | VaultToken, VaultMinter, VaultStaker, OhmUsdOracle | plain deploys; the token deploy is genesis |
| 5 | VaultHook via the CREATE2 deployer 0x4e59…956C | salt mined so the address ends in flag bits 0x30C8 |
| 6–9 | VaultRatchet, VaultBond, VaultLauncher, VaultLens | immutable wiring in constructors |
| 10–15 | minter.setRatchet, minter.setBond, token.setMinter, staker.setRatchet, hook.setRatchet, hook.setLauncher | every one-shot setter used up |
| 16–17 | approve 10M OHMV and the OHM seed to the launcher | |
| 18 | launch(seed) | pool opens; the launch minute starts |
How SEMI launched, from the chain
SEMI's launch on Robinhood Chain is the reference run. From its Transfer and Ratchet logs:
| Event | Block | Time (UTC, 13 Aug 2026) |
|---|---|---|
| Genesis: 10M SEMI minted to the deployer | 35,772,946 | 23:07:15 |
| Launch: 10M SEMI and 2 MU into the pool, first poke, mark sealed at 0.2 µMU | 35,773,265 | 23:07:47, 32 s after genesis |
| First buy, 459,615 SEMI, inside the launch minute | 35,773,270 | 5 blocks after the launch transaction |
| Next buys | 23:08:14 (+27 s), three in one block at 23:08:39 (+52 s) | |
| Steady buying begins as the fee reaches 5% | from about 23:08:47 (+60 s) | |
| First sells back into the pool | 23:08:52 and 23:09:04 | |
| First Ratchet mint | 36,216,079 | 14 Aug 11:27, the second regular poke |
Robinhood Chain makes a block roughly every tenth of a second; Ethereum makes one every 12 seconds. The launch minute is therefore five blocks long on Ethereum, and the fee each block pays is fixed in advance:
| Seconds after launch | 0 | 12 | 24 | 36 | 48 | 60+ |
|---|---|---|---|---|---|---|
| Hook fee | 50% | 33.8% | 21.2% | 12.2% | 6.8% | 5% |
A bot that lands in the launch block itself pays 50%, and while the fee is above 5% no single swap may take more than 10% of the pool's reserve. On Ethereum the 18 setup transactions take a few minutes, and the token contract is public from transaction 1. That gives no head start: there is no pool to trade in until transaction 18, and the hook refuses to initialize a pool for anyone but the launcher.
4VaultHook: the fee
The hook's address carries permission flags 0x30C8: beforeInitialize, afterInitialize, beforeSwap, afterSwap and beforeSwapReturnsDelta. The constructor checks its own address against these flags and reverts on a mismatch. beforeInitialize reverts unless the initializer is the launcher, so no other pool can be opened on this hook. afterInitialize stores startTime.
The skim
On every swap not made by the hook itself, beforeSwap computes the fee on the absolute specified amount and takes it in the specified currency:
Currency feeCur = (params.amountSpecified < 0) == params.zeroForOne ? key.currency0 : key.currency1; uint256 fee = (absAmt * feeBps) / 10_000; poolManager.take(feeCur, address(this), fee); return (beforeSwap.selector, toBeforeSwapDelta(int128(int256(fee)), 0), 0);
Returning the fee as the specified delta makes the swapper owe it. For exact-input swaps the fee is on the input: OHMV on sells, OHM on buys. The hook accrues fees in both tokens until a harvest.
The launch minute
For 60 seconds after startTime the rate falls quadratically from 50% to 5%, front-loaded:
The hook's own swaps pay 5% too
Uniswap v4 does not run a hook's callbacks for swaps the hook makes itself. In SEMI v1 that made the hook's own swaps free. Here _swap sends 95% of the intended amount into the pool and keeps 5% back as fees, so harvest compounds and the sold half of every bond pay the same skim as a trader. That fee stays in the hook and is split at the next harvest like any other.
Each of the hook's own swaps is also bounded to a price move of about 8% (√price × 0.9591 or × 1.0392). On a pool where every leg pays 5%, a sandwich around a move smaller than 8% costs the attacker more in skims than the move is worth. An oversized harvest fills partially and the rest waits for the next one; an oversized donation is refunded.
Harvest
harvest() is permissionless. Inside one unlock it sells the hook's whole OHMV balance for OHM, then splits the hook's OHM 40/60:
_compound(ohmAmt) sells 1/1.95 of the OHM for OHMV, so that after the 5% skim on the sold part the two halves match, and adds both as full-range liquidity owned by the hook. There is no burn slice: SEMI v1 sent 20% of each harvest to a GIWA buy-and-burn pointer, which was in practice the operations wallet. On Ethereum that slice is simply part of the 60%.
donateFloor
donateFloor(ohmAmt) is permissionless. It pulls OHM from the caller and runs only the compounding step: no treasury cut. It returns what it placed and refunds what the 8% bound left unplaced. Because the sold half pays 5%, the share of a donation that reaches the pool is 1 − 0.05 / 1.95 ≈ 97.44% (donationNetBps()). VaultBond delivers all bonded OHM through this function.
5VaultHook: the backing oracle
Reserves from protocol liquidity only
reserves() reads the liquidity of exactly two positions with getPositionInfo (launcher and hook, full range, salt 0) and converts it to token amounts at the current price. OHM sent to the hook or pool directly and any third-party liquidity are invisible to it. Nobody can park liquidity to force a mint and pull it afterwards.
Price never sits under live backing
For a full-range constant-product position, price in OHM per OHMV is R_ohm / R_tok and live backing is R_ohm / S. Their ratio is S / R_tok: total supply over the tokens held by the protocol position. Since that position holds part of the supply, the ratio is never below 1. If every holder sold everything, the price would end exactly at backing. Backing itself moves with trading, though: buys add OHM to the pool and sells take it out.
The minimum over the window
The hook records epochMin = min(epochMin, ohmReserve()) before every external swap, after every external swap, and after every harvest or donation. consumeMin(), callable only by the Ratchet, returns that minimum and resets it. SEMI v1 sampled only before swaps, so a window's last trade was never seen until the next window; sampling after each swap as well closes that.
TWAP
Each observation also updates a time-weighted price accumulator and writes at most one entry per block into a 32-slot ring. twapTokenPerOhm() returns the average OHMV-per-OHM over the last 10 minutes, falling back to the oldest entry if the ring is younger. The bond uses it (section 9).
6VaultRatchet: emissions
Backing ratio and marks, scaled by 10³⁶ because OHM has 9 decimals (at 10¹⁸, one token's backing would read about 10⁴ and move in 0.01% steps):
A fixed clock
The launch poke sets genesis. Slots are 6 hours long and counted from it. poke() is permissionless and pays no reward; it reverts until a slot boundary has passed since the last settlement, then settles once, however many slots were missed. In SEMI v1 the six hours ran from the previous poke, so whoever poked chose which observations fell into the window. Here the boundaries are fixed and a late poke changes nothing but gas.
poke(). Each path that reads the oracle also calls staker.notify, which advances the staker's epoch.Two marks: OHM and dollars
The OHM mark alone has one blind spot. If OHM crashes against the dollar, arbitrageurs sell cheap OHM into every OHM pool, including this one; OHM backing per token rises while the dollar value of the floor falls. The Ratchet therefore keeps a second mark in USD per token and needs a new reading above both. An OHM crash with arbitrage inflow raises the OHM reading but not the dollar one: no mint. An OHM pump with no trading raises the dollar reading but not the OHM one: no mint. A stale or broken feed means no mint and both marks untouched; the slot still ticks so stakers keep activating.
The trailing mark
In SEMI v1 the mark was set to the post-mint backing after every mint. That is harmless for small gains. For large ones it threw most of the gain away: the 2% cap paid a sliver, and the mark still jumped to the top. OHMVAULT moves the mark by the part of the gain the mint actually paid for, and lets it trail the post-mint backing by at most 20%:
When the cap doesn't bind, carried equals the post-mint backing exactly and the rule is v1's. When it binds, the unpaid part of the gain stays in front of the mark and is paid at up to 2% a slot for as long as the backing holds. The three invariants of v1 still hold: nothing mints unless backing is above the mark; the mark never goes down; after a mint, live backing is at or above the mark.
When there are no stakers, the whole gain moves both marks and nothing is minted, so the first staker can't collect a backlog. This is unchanged from v1.
7The trailing mark on SEMI's history
SEMI's Ratchet was poked 146 times after initialization between 14 August and 6 October 2026. It minted 27 times, 12,143,846 SEMI in total, and never after 3 September. Bonds minted 59.1M in the same period: 73% of all SEMI ever created came from bonding, 15% from emissions.
The question for OHMVAULT was how to get more paying slots without changing what the mint is backed by. The test: take SEMI's real window-minimum backing at every poke and its real bond mints, keep both fixed, and replay only the mark rule. Supply in the replay grows by the extra emissions, which dilutes the backing reading the way it would have.
| Mark rule | Slots that minted | Emitted | Share of final supply |
|---|---|---|---|
| SEMI v1, on chain | 27 | 12.1M | 14.9% |
| SEMI v1 rule, replayed (check) | 23 | 12.2M | 13.0% |
| 10% trail | 34 | 20.8M | 20.3% |
| 20% trail (OHMVAULT) | 44 | 29.5M | 26.6% |
| 30% trail | 56 | 44.1M | 35.2% |
The replayed v1 rule lands within 0.2% of the real emissions, with four fewer paying slots than the chain, which is close enough to trust the comparison. At 20%, emissions are 2.4× v1's and the number of paying slots roughly doubles. 20% was chosen over 30% because the 30% run kept printing into the first day of SEMI's September drawdown.
What no mark rule fixes: from 3 September SEMI's backing fell about 65% and never came back. Any rule that keeps the promise "never mint below an up-only mark" stops paying in that situation, and the trailing mark does too. It extends the payout of every rise that holds; it does not pay through a bear market. Getting emissions there would mean letting the mark fall, which is the one thing this design will not do.
8VaultStaker
An accumulator without rebasing. The Ratchet mints ΔS to the staker and calls notify(ΔS):
Entry: one slot
New stake is pending and earns nothing until the next tick. Staking one block before a poke and leaving after it earns nothing.
In SEMI v1 a pending stake became active only when its owner next touched the contract. A holder who staked and walked away was invisible to the Ratchet, which then saw "no stakers" and banked the gain instead of minting it. Here the tick itself activates everything staked in the closed slot (pendingTotal[epoch]), and each account settles lazily from the accumulator value recorded at that tick (accAtEpoch). No keeper and no loop over users.
Exit: one slot
unstake(amount) draws from active stake only, and not within 6 hours of the account's last stake. It does not pay out. The amount stops earning immediately and moves into cooling with coolingEnds = now + 6 h. withdraw() pays it out once that time has passed. Unstaking again while a cooldown runs adds to it and restarts the 6 hours on the total. Rewards are accounted separately: claim() works at any time, cooldown or not, and can never touch principal.
The exit wait does two jobs. It makes staking a commitment of at least two slots, so rewards go to people who stay, and it slows a run on the exit: a staker who wants out in a panic is out six hours later, not in the same block as everyone else.
9VaultBond
bond(ohmIn, minTokenOut) pulls OHM, routes it through donateFloor, and mints OHMV through VaultMinter, vesting linearly over 24 hours:
The gate defends live backing
In SEMI v1 the gate was ohmIn / mark. After SEMI's September drawdown the mark sat 2.8× above live backing, so every bond was capped far below the market and bonding was dead until a 137% recovery. A bond only needs to leave backing per token where it was to be non-dilutive, and ohmIn / live backing is exactly that. The marks now gate emissions only. The 1 ppm shave keeps v4's liquidity rounding from ever tipping a capped bond a fraction under par; the fuzz test checks it.
Quote, partial fills, skim
The price is the lower of the 10-minute TWAP and spot, so dumping right before a bond does not improve it. The quote and the gate are fixed before the donation runs. The hook places what its 8% bound allows and refunds the rest in the same transaction; OHMV is minted only against OHM that reached the floor. The sold half pays the pool's 5%, about 2.56% of the bond, which stays in the hook as fees. quote() shows the result net of that skim.
A new bond pays out whatever has vested, then puts the unvested remainder and the new amount on a fresh 24-hour schedule. There is no per-slot bond cap.
10OHM and its price
OHM v2 (0x64aa…f1D5, 9 decimals) is the token of Olympus, on Ethereum since 2021. Olympus backs it with a treasury the protocol owns and runs Range Bound Stability: standing treasury orders that buy OHM below a lower band and sell above an upper band. Holders can also borrow against staked OHM through Cooler Loans instead of selling. The point for OHMVAULT is that OHM has a defended price; a floor made of it moves less with the market than a floor made of ETH would. Bands, backing and treasury figures change and are published by Olympus; check them there.
The OHM in OHMVAULT's pool is plain OHM, not staked gOHM, and earns no Olympus yield. It grows from fees and bonds.
OhmUsdOracle
OHM/ETH has a 24-hour heartbeat and a 2% deviation trigger; ETH/USD has a 1-hour heartbeat and 0.5%. The limits sit two hours past the OHM heartbeat and one hour past the ETH one. A reading that fails any check returns ok = false and the Ratchet skips the mint; it never reverts the poke.
11What changed from SEMI v1
| Change | Why |
|---|---|
| Pair asset is OHM (9 dp) on Ethereum instead of MU on Robinhood Chain | A defended collateral on the main chain, with deep OHM/ETH liquidity for routing in and out. |
| Harvest 40% floor / 60% operations; no burn slice | v1's 20% GIWA burn went to the operations wallet anyway, and there is no GIWA on Ethereum. |
| The hook's own swaps pay 5% | v4 skips hook callbacks for the hook's swaps, so v1's compounds and bonds traded free. Now every leg pays. |
| Hook's own swaps bounded to ~8% (v1: 2× / ½×) | No sub-8% move can be sandwiched profitably through two 5% skims. |
Reserves sampled after every swap as well as before (flags 0x30C8) | v1 never saw a window's closing reserve. |
| Fixed 6 h slots from launch | v1 let the poker choose the window. |
| USD mark next to the OHM mark | Blocks the "collateral crashes, arbitrage OHM flows in, mint" path. |
| Trailing mark (20%) | v1 paid one slot per pump. Replayed on v1's tape: 44 slots and 29.5M instead of 27 and 12.1M. |
| Backing scaled 10³⁶ | OHM's 9 decimals made 10¹⁸ coarse. |
| Passive stakers activate at the tick | v1 banked gains when holders had staked and walked away. |
| 6 h unstake cooldown | One slot to enter, one to leave. |
| Bond gate on live backing | v1's mark gate shut bonding for weeks after a drawdown. |
| Bond quote = min(TWAP, spot); partial fills with refund | A dump before bonding doesn't help; large bonds are safe under the 8% bound. |
| Removed: FloorWall, router fee tiers, launchpad, The Fab, SemiScape | Not part of this launch. |
Everything else is v1's logic: the 5% skim and launch minute, the protocol-only reserve reading, the minimum over the window, α = ¾, the 2% cap, banking when nobody is staked, the 20% discount and 24-hour vest, 10M supply all in the pool, one-shot setters and no owner.
12Read paths
VaultLens.snapshot() returns in one call: supply, OHM and OHMV in the protocol positions, both marks, live backing, spot and TWAP, Chainlink's USD price and whether it's fresh, lifetime emissions, the current and last settled slot, whether a poke is due, the current fee, and what 1 OHM would bond for right now with the gate flag. Anyone can re-derive every number the site shows from that one read. VaultStaker.earned(u), stakeOf(u), cooling(u) and coolingEnds(u) cover a single account; VaultBond.claimable(u) and locked(u) cover its bond.
13Trust model and risks
What nobody can do
Remove the locked liquidity. Mint outside the two gates. Change the fee, the split, α, the cap, the trail, the discount, the vest or the cooldown. Pause anything. Point any contract somewhere else. Each of these needs a function that doesn't exist or a one-shot setter that has already been used.
What the team controls
The operations wallet, which receives 60% of harvested fees (3% of volume). It is the team's income and pays for development, infrastructure and promotion. Nothing in the mechanics depends on it.
Risks
- OHM. Olympus is governed by its DAO, which can mint OHM, move the bands and change the treasury. OHMVAULT has no say. If OHM fails, the floor under OHMV fails with it.
- Oracle. Chainlink can go stale or be wrong. Stale halts minting; wrong-but-fresh could let one slot mint against a bad dollar reading, still bounded by the OHM mark and the 2% cap.
- More emissions. The trailing mark pays more and for longer. Stakers who sell their rewards add selling pressure during the run. The run is bounded by the 20% trail, never by the size of a pump.
- Bond supply. On SEMI, bonds created 73% of supply. With the gate on live backing bonding stays open in drawdowns, so supply can grow faster than in v1. Each bond is non-dilutive to backing per token by construction; it can still dilute price.
- Liquidity and routing. The pool starts at $2,000 of OHM. The Uniswap app routes hooked pools only once Uniswap allowlists the hook. Until then buyers come through the site, bots that support v4 hooks, or direct pool calls.
- Code. 32 tests, fuzzing and a fork rehearsal are not an audit. v1's logic ran for two months on Robinhood Chain with real money; the changed parts have not.
- Keeper.
poke()andharvest()need someone to send them. Both are permissionless, so a dead keeper delays but never locks anything.
14Address registry
| Name | Address |
|---|---|
| OHM v2 (Olympus) | 0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5 |
| Uniswap v4 PoolManager | 0x000000000004444c5dc75cB358380D2e3dE08A90 |
| Chainlink OHM/ETH | 0x9a72298ae3886221820B1c878d12D872087D3a23 |
| Chainlink ETH/USD | 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 |
| CREATE2 deployer | 0x4e59b44847b379578588920cA78FbF26c0B4956C |
| Operations wallet (TREASURY) | 0x63888d25934504CfcFb83aCc3f9af90B3e30dDce |
| VaultToken · Minter · Hook · Launcher · Ratchet · Staker · Bond · Oracle · Lens | published here, on the site and on X at launch |
All nine protocol contracts will be verified on Etherscan with exact-match source.