How it works
The idea
Two bulls. One ETH price. The market decides which conviction is louder. No oracle picks a winner; no settlement date forces an exit. Pricing is the answer.
A market starts as a binary question — VITALIK vs HAYDEN, BULL vs BEAR, BTC vs ETH — and ships as two ERC-20 bull tokens, each with its own Uniswap V4 pool against WETH. The two pools launch at parity. From there, where they drift is the only score that matters.
You participate by picking a bull and depositing ETH. The protocol mints synthetic supply against your collateral and provides it as single-sided liquidity into the pool. You walk away with an LP NFT and a debt entry. Repay whenever — the contract burns the synthetic, unwinds the LP, and returns your share of the pool. If your bull ran, you collect more ETH than you put in. If it stalled, less.
This is a conviction market, not a prediction market: there is no payout calendar, no truth oracle, no “winner” ever declared. The price of each bull token is the aggregate weight of capital behind it, and the fee on each pool dynamically rewards the winning side and punishes the losing one. Everything is on-chain, non-custodial, permissionless.
Mechanism
Every market is three contracts working in lockstep: one Stabilizer coordinates a pair of HaloSide ERC-20s, each with its own V4 pool against WETH, and a shared OracleHook reads the TWAP on every swap to retune fees in real time.
Launching at parity
Both pools open with identical liquidity. The creator picks a starting size (in ETH); each pool seeds with that much WETH against initialSupply bull tokens. The two pools begin indistinguishable. Drift only emerges from real activity.
How drift gets corrected
If bull A pulls ahead, the OracleHook sees the TWAP divergence on its next swap and adjusts: A's fee falls, B's fee rises. Selling A is suddenly cheap and minting B is suddenly expensive — a restoring force pulls capital back across. The market never locks itself in; conviction has to keep paying for the lead it already won.
Positions
A position is an on-chain debt record. One transaction opens it, one closes it. While it's open you hold an LP NFT minted in your name; it just lives in the HaloSide contract until repay.
Calling HaloSide.deposit() with msg.value = X ETH kicks off the open flow. The contract wraps your ETH to WETH, provides it as single-sided liquidity into the pool, mints synthetic supply against your collateral, and stores a DebtRecord with (lpTokenId, collateralAmount, mintedAmount, timestamp). Your address indexes into one or more debtIds.
Closing
HaloSide.repay(debtId)burns the synthetic, pulls the LP out of the pool, and ships you the net WETH (unwrapped to ETH on the way out). What you get back is the current pool value of your position — bigger than your deposit if your bull ran, smaller if it didn't.
The LP NFT is custodied by HaloSide for the lifetime of the position so the Stabilizer can read pool state and enforce fee logic without making you sign a message on every rebalance.
Dynamic fees
The OracleHook is a Uniswap V4 hook with the BEFORE_SWAPflag set. Before each swap it reads the TWAP of both pools and computes the current fee for the pool being swapped. The fee is returned to the PoolManager as part of the hook's response — no external oracle, no off-chain keeper.
TWAP period
The Stabilizer stores a twapPeriod (default ~1800 seconds). The hook reads the cumulative tick accumulator at now - twapPeriod and now, divides by the interval, and converts the average tick to a price ratio.
Drift and fee direction
getPriceDeltaBps(sideAddr) returns the signed divergence of the two pools in basis points. Positive = the queried side is ahead. getFeePercentage(sideAddr) maps that divergence to a fee in bps — higher fee for the trailing bull, lower for the winning bull.
The fee schedule creates a self-correcting market: as one side dominates, minting new tokens on the losing side becomes cheaper (lower fee), attracting contrarians who believe in a reversion. Meanwhile, the winning side charges higher fees, harvesting revenue from latecomers.
The math
Uniswap V4 price encoding
V4 stores pool price as sqrtPriceX96 — a Q64.96 fixed-point number equal to √(token₁/token₀) × 2⁹⁶. To recover a human-readable price:
Currency ordering in V4 is deterministic: currency0 < currency1 by address. Whether WETH or the side token is token0 depends on which address sorts lower — the frontend derives this at runtime before building PoolKey.
Initial sqrtPriceX96
At market creation the creator picks a “starting size” S (ETH) and both sides launch withinitialSupply N tokens. The reference price is p₀ = S / N ETH per token:
Pool ID derivation
Uniswap V4 identifies pools by the keccak256 hash of the ABI-encoded PoolKey struct:
Swap direction and price limits
To buy bull tokens with ETH (ETH → Side): set zeroForOne = truewhen WETH is currency0, false otherwise. The sqrtPriceLimitX96 is clamped to just inside the pool's tick bounds to prevent a full single-transaction drain:
TWAP calculation
The hook stores cumulative tick accumulators at each block boundary (via the afterInitializeand beforeSwap callbacks). A TWAP over window T is:
Collateral accounting
Not all deposited ETH becomes collateral. The hook's current fee applies at mint time:
Secondary market
You don't have to open a debt position to participate. Side tokens trade freely in their V4 pools. Any address can buy or sell using PoolSwapTest.swap() (on Mainnet) or the Uniswap Universal Router on production deployments.
The swap flow for ETH → Side A:
amountSpecified < 0 = exact input (you specify how much WETH to spend). The output amount (bull tokens received) depends on the current pool price. Side → ETH swaps return WETH, not native ETH.
Launching a market
Anyone holding enough halo can deploy a new market. The cost is a one-time creation fee in halo (10,000 halo). The fee is burned / sent to the treasury; gas costs ~1.95M units for the full deployment.
Two-transaction flow
halo.approvefactory.createTupleTupleCreated event
The frontend decodes this event from the createTuple receipt to extract the new Stabilizer address and redirect to /markets/{stabilizer}.
Once deployed: no pause, no redeploy, no settlement. The market lives until both pools are drained by repayments. The creator can set the feeReceiver address at deploy time; this address receives the per-mint fee skim for that market.
Contracts
| L1 Markets · halo ERC-20 governance token · 18 decimals | 0x02dBe30f5c4781766e48E6e123E11C025Ee32e20 |
| Factory Deploys and indexes Stabilizer + HaloSide clones | 0xAa7ef737f7eA096DdD59a5896Fb27f0933005d61 |
| Oracle Hook V4 hook singleton — TWAP oracle + dynamic fee controller | 0x25520fE01F23EEBc4612f1ff05E3a6787D231Ac0 |
| Side Impl Implementation contract for all HaloSide ERC-20 proxies | 0x1f89539a9dEEc1b07bE89035bFfB33f82006ad0a |
| Stabilizer Impl Implementation contract for all Stabilizer proxies | 0xb7fdC9717415852Ea01b7f37bF8b95C91f553261 |
| Pool Manager Core V4 singleton — holds all pool state | 0x000000000004444c5dc75cB358380D2e3dE08A90 |
| Position Manager ERC-721 LP NFTs · Permit2 integrated | 0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e |
| State View Read-only: getSlot0(poolId), getLiquidity(poolId) | 0x7fFE42C4a5DEeA5b0feC41C94C136Cf115597227 |
| Pool Swap Test Test router for direct V4 swaps | 0x0000000000000000000000000000000000000000 |
| V4 Quoter Off-chain swap simulation — quoteExactInputSingle | 0x52F0E24D1c21C8A0cB1e5a5dD6198556BD9E1203 |
| Permit2 Canonical Uniswap Permit2 (same address all chains) | 0x000000000022D473030F116dDEE9F6B43aC78BA3 |
| WETH9 Wrapped Ether — deposit ETH → WETH, withdraw WETH → ETH | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 |