Bazaar

Introduction

Bazaar is a fully permissionless perpetual-futures protocol for Arbitrum. Any asset with a Pyth price feed — crypto, equities, FX, indices — can be listed through an UMA Optimistic Oracle assertion and traded on a fully on-chain central limit order book, margined in USDC.

The protocol is immutable: no owner, no pauser, no upgradeable proxies, no governance token. Every privileged action is either fully permissionless, economically bonded, or adjudicated by UMA's optimistic oracle.

⚠️ Status: pre-audit, work in progress — not yet deployed. There is no official deployment on any network; do not use with real funds.

Vision

The aim is in the name: a bazaar — an open marketplace where anything with a price can be traded by anyone, against anyone.

Every role a traditional exchange reserves for itself is, here, an open job — anyone can take it, whether an individual trader or a professional independent operator. Anyone can list a market. Anyone can run a sequencer and match orders. Anyone can keep positions honest and collect the bounties for it. Anyone can capitalize the insurance backstop and earn its fees. Anyone can vote a dying market into settlement. There is no company behind the counter — the marketplace is owned and operated by its own participants.

The protocol itself is immutable and unowned, not as ideology but as product: a marketplace for everything can only stay neutral if no one — no team, no company, no committee — is in a position to say no.

Why Bazaar is built the way it is

Three design commitments drive everything else:

1. The book is on-chain; only the sorting is off-chain. Orders rest in contract storage. A permissionless, bonded sequencer periodically submits the resting order IDs in sorted lists, and the contract re-verifies the sort and matches deterministically in three passes. This keeps the matching semantics trustless while avoiding the gas cost of on-chain insertion into a sorted book. Sequencer honesty is not assumed — it is enforced by fraud proofs: censoring an order or mislabeling a batch as price-stale is provable on-chain and slashes the sequencer's bond.

2. Anything with a feed can be a market — including assets that stop existing. Perps on equities and real-world assets need answers that crypto-only protocols never face: What happens off trading hours? What happens when the company is acquired, the feed is decommissioned, or the asset redenominates? Bazaar has a stale-oracle trading regime for market closures and five independent termination paths that guarantee every market can always be wound down to cash settlement — without anyone's permission.

3. Every failure mode ends in a defined state. Losses cascade through a fixed waterfall: liquidation → the vault unwinds inherited inventory on the book → auto-deleveraging Dutch auction → per-pair insurance fund → pro-rata profit haircuts with principal always reserved → termination. There is no path that strands funds behind a revert, and no path that pays out first-come-first-served.

Reading map

Using Bazaar

Bazaar is designed to be used as a deployed protocol, not software you need to run. Everything in this guide is written against the official deployment — once it is live, canonical addresses will be listed in Deployments, and only those addresses (plus pairs discoverable from the listed factory) will be the real Bazaar.

The protocol is not yet deployed. Addresses will appear in Deployments at launch; until then, everything can be exercised on a testnet or local stack.

Pick your path

You want to…Start hereYou need
Trade perps with leverageTradingUSDC on Arbitrum
Earn yield backstopping a marketEarning → Insurance LPUSDC, patience (20-day exit cooldown)
Run bots that collect bountiesEarning → Keeper bountiesa bot, gas money
Operate matching infrastructureEarning → Sequencing≥ $1,000 bond + off-chain matcher
Build a frontend/bot and earn referral feesIntegratorsjust an address
List a new marketMarkets & Listing≥ $4,000 (bond + insurance seed)
Understand the machineProtocol chapterscuriosity

Every role above is permissionless — no signups, no whitelists, no API keys. If you can send a transaction, you can participate.

The five-minute mental model

  • Each market ("pair") is its own contract with its own order book, insurance fund, and USDC collateral pool. One net position per wallet per market.
  • Orders rest on-chain; bonded sequencers batch-match them a few times per second and are slashed if they censor anyone.
  • Margin is dynamic: calmer markets allow up to 25× leverage, wilder ones less. Fall below maintenance margin and anyone may liquidate you.
  • Losses that liquidations can't cover cascade through a defined waterfall (vault → auto-deleveraging → insurance fund → haircuts) — never first-come-first-served, never socialized by surprise rules.
  • Markets on stocks and FX keep working when their venues close, under tightened rules; and any market can be wound down to cash settlement when its underlying dies.

Trading

A practical guide to trading on Bazaar. Plain language first; every section links to the technical chapter that governs it.

Getting in

  1. Fund: hold USDC on Arbitrum. That's the only collateral — no ETH needed if you use a gasless relayer, and even the USDC approval can ride a permit signature.
  2. Deposit: depositCollateral into the specific market you want to trade (min $5). Collateral is per-market; a blowup in one market cannot touch your balance in another. While you hold a position you must keep max(0.5% of notional, $5) in collateral — $5 on a $100 position, $500 on a $100k one. Fees come out of your balance, so this guarantees you can always pay the one that closes your position; without it, a position deep enough in profit could withdraw its whole deposit and then find its closing orders silently canceled.
  3. Trade: place orders. You hold one net position per market — buying 2 ETH long then selling 3 flips you to 1 short.

Order types, practically

You want to…UseBehavior
Fill now at marketMarketfills against resting limits at up to your slippage cap (max 5%); lives ~3 seconds; one at a time
Rest at your priceLimitsits on the book until filled, canceled, or expired (up to ~1 year); postOnly guarantees you're the maker
Enter on a breakoutStopLimitarms when price crosses your trigger, then acts as a limit
Lock in profitTakeProfitreduce-only limit against your position; one per position
Cap your lossStopLossreduce-only market-style close when price crosses your trigger; one per position

Minimum order: $5 notional (closing your whole position is always allowed). Stops trigger on the oracle price of the batch, so a sequencer can't fire your stop early.

Leverage & staying solvent

  • Your maximum leverage is dynamic — up to 25× in calm, well-capitalized markets, less in volatile ones, at most 5× in a market's first days, and a third less on stocks/FX (non-24/7 markets carry a 1.5× margin multiplier). The current requirement is on-chain (BazaarPairLens.checkBucketSolvency).
  • Initial margin gates new orders and withdrawals, counted against your worst case (position + everything resting). Maintenance margin is half of initial — below it, anyone may liquidate you.
  • Rising margin requirements give existing positions a 24-hour grace; falling ones help you immediately. See Margin & Leverage.
  • Funding accrues continuously, capped at ±0.5%/hour: longs pay shorts when the market trades above index, and vice versa. It settles when you close. See Funding.

What can happen to you

Honest list, in increasing order of severity:

  • Your order auto-cancels. If your equity drifts below what a fill would require, the match cancels your order instead of filling it. Resubmit after topping up.
  • Liquidation. Below maintenance margin, your position is closed entirely (no partials) and your remaining collateral is seized. Set stops, or watch checkBucketSolvency. See Liquidations.
  • Auto-deleveraging (ADL). In a crisis, the most profitable, most leveraged traders on the winning side can be force-closed at a worse-than-market (but never loss-making) price to keep the market solvent. If you're up big with high leverage during chaos, you're first in line. See Auto-Deleveraging.
  • Market termination. If the underlying dies (delisting, feed decommission), the market cash-settles at a final price and you withdraw. Your deposit minus your own losses is always reserved for you and never expires; if there isn't enough left to pay every winner's profit, profits are haircut at one uniform percentage — never race-to-exit. You don't need to act during the wind-down: bots are paid to settle positions on your behalf, and getTerminalEntitlement on the lens shows exactly what a withdrawal will pay you. See Termination.

Fees you'll pay

Roughly: fractions of a basis point plus $0.03 per side. Makers pay ~1 bp all-in; takers pay a utilization-priced sequencer fee (0.75–3.75 bp) plus an insurance fee that rises when the market's backstop is thin — and closing risk is always cheaper than opening it. Exact table: Fees.

Stocks & FX after hours

Non-24/7 markets keep trading on the last price when their venue closes: margin requirement doubles for new fills, prices are boxed to ±10% of the last print, and market orders are disabled until the venue reopens. See Price Oracle.

Getting out

withdrawCollateral any time your equity covers initial margin on what remains — valued conservatively at the pessimistic edge of the oracle's confidence band. Flat (no position, no orders)? Withdraw everything, any time. Two freezes to know about: if you hold a position, withdrawals are blocked for as long as ADL remains pending (until the crisis clears or the 24-hour ADL timeout winds the market down — the 10-minute clock is only the auction's score decay); and all withdrawals pause during the 48-hour settlement window just before a dying market settles (that pause is what guarantees everyone is paid from the same, fully-tallied pot).

Earning on Bazaar

Three permissionless ways to earn from the protocol's operation, ordered by how much infrastructure they demand. (Building a frontend? That's its own page: Integrators.)

Insurance LP

What it is: deposit USDC into a specific market's insurance fund and hold shares of it. You are selling crash insurance on that one market — steady fee income against tail risk.

You earn a pro-rata slice of everything the fund collects: insurance fees on every fill, all collateral seized from liquidated traders, the vault's liquidation-unwind profits, slashed sequencer bonds, and forfeited governance bonds.

You risk the fund's payouts: liquidation-unwind losses, ADL winner credits, keeper rewards — and in a market collapse, the fund is the last cushion before trader haircuts, so it can be drained. Share price = fund ÷ shares; it floats both ways.

The terms (full detail: Insurance Fund):

RuleValue
Minimum deposit$5 — per market, choose which books you back
Exitrequest → 20-day cooldown → 3-day execution window
Exit rate limitsslower when the fund is below target — you cannot stampede out of a thinning backstop
Crisis lockwithdrawals blocked while ADL is pending, a termination is scheduled, or the 48 h settlement window is open (bad debt is charged to the fund at finalize — LPs cannot exit ahead of it)
Governancematured shares (7 days) vote on shutting the market down (60% threshold)

The honest framing: you get paid because your capital is locked in exactly the moments you'd want it out. Size accordingly.

Keeper bounties

What it is: run a bot that calls permissionless maintenance functions and collects the bounty attached to each. No bond, no stake — just gas and uptime. All bounties pay in USDC, immediately, from the pair's insurance fund or the offender's bond.

OpportunityCallBountyWhat your bot watches
Liquidationsliquidate(users[], priceUpdate)max($0.10, 2 bp of notional) per positionposition health vs. lagged MMR (checkBucketSolvency on the lens)
ADL executionexecuteAdl(winners[], priceUpdate)0.1% of averted bad debtisAdlPending; rank winners by getAdlScore on the lens, descending — it reproduces the on-chain ranking exactly, and a mis-sorted batch reverts
Censorship proofschallengeOmission(...)1% of the censored notional (min ~$3 of the $20 floor)BatchRecorded events vs. your own view of the resting book
Stale-flag proofschallengeStaleBatch(...)0.5% of batch notionalbatches flagged isStale vs. Pyth's actual publish times
Dead-market cleanupterminateStalePair, terminateScheduledPair, UMA settlement pokes0.1% (10 bp) of the fund, capped $100 (proposals)oracle silence > 21 days; passed cessation timestamps
Terminal settlementliquidate (settlement mode) during the 48 h window, then finalizeTerminationmax($0.10, 2 bp of notional) per position, from that position's own remaining collateral (getTerminalSettlementBounty on the lens quotes it per position)a pair whose settlement price is fixed; needs no priceUpdate and no ETH

Practical notes: every price-touching call takes a priceUpdate — your bot pushes the Pyth update and acts on it atomically (Pyth's fee is paid in ETH, refunded when unused). Bounty payments use non-reverting transfers: use a clean address that can receive USDC. Liquidation and challenge markets are competitive — latency matters; ADL and termination cleanup are sleepier niches.

Sequencing

What it is: run the off-chain matcher for one or more markets. You watch resting orders, sort them, and submit matchBatch a few times per second. This is the protocol's most demanding — and most rewarded — role.

You earn, per fill you match: the dynamic taker fee (0.75 bp → 3.75 bp, priced by how utilized global sequencer capacity is), 0.25 bp from the maker, and $0.03 flat from each side. On thin-order flow the flats dominate; at scale the basis points do.

You stake:$1,000 bond in BazaarSequencer, which caps your throughput at 14× bond per rolling 30 minutes — more volume requires more skin. Withdrawal respects the same window.

You're slashed for exactly two provable offenses (see Sequencers & Fraud Proofs):

  • Censoring an order that should have matched: 7% of the notional. The operational discipline this imposes: include every live in-range order you saw at your observation block — the engine auto-cancels the ones that can't pay, and witnesses the ones it skips, so honest inclusion is always safe.
  • Mislabeling a batch as price-stale when a fresh Pyth tick existed: 1%.

There is no slashing for downtime, for matching slowly, or for someone else matching first — competition is by fee capture, not liveness obligations.

Minimum viable setup: an Arbitrum node or reliable RPC, a Pyth Hermes price stream, an indexer of the pair's OrderUpdated/BatchRecorded events to maintain the resting book, sort logic per the matching invariants, and a funded relayer key. Start on one quiet market with the minimum bond; capacity and fee revenue scale with the bond as you grow.

Integrators: Earn Referral Fees

If you put an order-creation UI in front of users — a web frontend, a Telegram bot, a trading terminal, an aggregator, a vault strategy — Bazaar pays you protocol-level referral fees on every fill your users generate. No registration, no agreement, no API key, no permission: the fee is a field on the order.

How to become one

Put your address in the integrator parameter of every createOrder your product submits:

pair.createOrder(
    orderType, triggerPrice, limitPrice, maxSlippageBp, size,
    isLong, isPostOnly, expirationBlock,
    integrator,          // ← your address. That's the entire integration.
    priceUpdate, nonce, deadline, relayerFee, signature
);

That's it. You are now an integrator.

What you earn

0.25 bp (0.0025%) of the notional of every fill of every order that carries your address — maker and taker orders alike, on every partial fill, for the entire life of the order (a resting limit that fills 40 times pays you 40 times).

Monthly volume through your productYour referral revenue
$1M$25
$10M$250
$100M$2,500
$1B$25,000

Payment is automatic and per-batch: the matching engine accumulates your fees across each batch and transfers USDC to your address when the batch settles. No claiming, no escrow, no minimum.

Stack it with relayer fees

Integrators are naturally positioned to run the meta-transaction relayer for their users: your users sign EIP-712 payloads (no ETH needed, USDC approval via permit), your relayer submits them and collects up to $1 per action in USDC on top of the referral fee. Gasless UX for them, second revenue stream for you. BazaarPairLens.getEip712Constants() and getTypehashes() expose everything needed to build the signatures.

Fine print

  • The 1% protocol-wide bug-bounty tax applies to integrator fees like every other stream — you receive 99% of the 0.25 bp.
  • Payouts use a non-reverting transfer; if your address can't receive USDC (e.g. blacklisted), that batch's fee is forfeited to the pair's insurance fund. Use a boring, clean address.
  • The integrator field is per-order and immutable once set — users of your UI can't be siphoned retroactively, and you can't claim orders you didn't originate.
  • Setting integrator = address(0) charges no integrator fee at all — direct-to-contract traders skip the 0.25 bp entirely. Your referral fee is a real surcharge on your users' fills, so the convenience your product adds has to be worth a quarter of a basis point.

Checklist for a production integration

  1. Set integrator on every order (and test that fills emit your address's transfers). OrderFilled is the only per-fill event — it carries fillSize, executionPrice, the total fee charged that side, and an isMaker flag, and a pair match emits one per side — so index that to attribute your revenue.
  2. Decide gasless vs. direct: relayed calls need the 2-second price-staleness tier — your relayer should attach a fresh Pyth update to each call.
  3. Read Orders for the per-user caps your UI must respect (100 resting limits, 1 market order, 1 TP + 1 SL per position) and surface auto-cancel events (OrderUpdated with Canceled) to users.
  4. Use BazaarPairLens for all read paths — solvency, margin, share prices — rather than re-deriving them client-side.

Local Development

This page sets up a local development stack — building, testing, and running the protocol on Anvil with mocked Pyth/UMA/USDC. If you just want to use the live protocol, start at Using Bazaar instead.

Prerequisites

  • Foundry (forge, cast, anvil)
  • Node.js + npm (for the Pyth Solidity SDK)
  • make, jq

Install & test

git clone --recursive <repo-url> && cd bazaar
npm install          # @pythnetwork/pyth-sdk-solidity
forge build
forge test

The suite is 844 tests across 74 suites — unit suites per library/mechanism plus end-to-end integration suites, including a zero-sum accounting invariant (test/integration/ZeroSumInvariantTest.t.sol). Note that forge build is the slow step, not the tests: via_ir plus the 150-runs compilation restriction compiles the BazaarPair closure twice, so a change to MatchingEngineLib costs minutes while the whole suite runs in about nine seconds.

Local stack on Anvil

Create a .env in the repo root (it is gitignored):

ANVIL_RPC_URL=http://127.0.0.1:8545
ANVIL_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80  # anvil default #0
ANVIL_WALLET=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266

Then:

make anvil           # terminal 1 — local node with tracing
make deploy-anvil    # terminal 2 — full stack + MockPyth/MockUSDC/MockOOv3, mints 10,000 USDC

Deployed addresses land in .anvil-addresses. From there:

# List a market (proposes via mock UMA, warps past liveness, settles)
make deploy-pair-anvil FEED_ID=0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace DESC="ETH/USD"

# Push a mock Pyth price
make set-price-anvil FEED_ID=0xff61... PRICE=2000

# Rest a limit order
make create-order-anvil PAIR=0x... LIMIT_PRICE=2000 SIZE=0.01 IS_LONG=true

# Move time (funding, cooldowns, UMA liveness)
make warp-anvil SECONDS=43200

make help lists everything.

Building this book

The documentation is an mdBook:

mdbook serve docs    # live-reload at http://localhost:3000
mdbook build docs    # static site in docs/book/

Self-Deployment

You do not need this page to use Bazaar. The canonical deployment's addresses are in Deployments, and Using Bazaar covers trading, LPing, sequencing, and integrating against it. This page is for protocol developers and for anyone standing up an independent deployment — in which case note the AGPL-3.0 network clause: a fork operated as a service must publish its modified source.

Deployment is two-step because the heavy logic lives in externally deployed libraries that are linked by address (see Architecture).

Step 1 — deploy the external libraries

make deploy-libs-arb-sepolia    # or: deploy-libs-arb-mainnet (asks for confirmation)

BazaarPair carries a link reference for ten DELEGATECALL libraries — InsuranceVaultLib, LiquidationLib, OrderManagementLib, AdlLib, MatchingEngineLib, CollateralLib, RiskParamsLib, FundingLib, TerminationLib, MetaTxLib — and every one of them must be deployed and linked before the pair implementation can go out.

The script deploys all ten and records their --libraries flags in deployments/<chainid>/libraries.args. Nothing is pasted by hand, and nothing is written to foundry.toml: libraries there is a single global key with no per-network scoping, so one shared list would let a rehearsal network's addresses be linked into a production build — where they hold no code, and every DELEGATECALL from the pair would land on an empty account. Keying the record by chain id makes that unrepresentable.

The library list lives in one place, _libNames() in the script; the artifact id, the link path and the emitted flag are all derived from it, so a library cannot be deployed but left out of the link flags. Real networks' records are committed as deployment provenance; the local Anvil chain's (deployments/31337/) is gitignored as throwaway.

Step 2 — deploy the core

make deploy-arb-sepolia    # or: deploy-arb-mainnet (asks for confirmation)

The target passes that chain's recorded flags to forge and refuses to run if the file for its chain is missing, so step 2 cannot silently produce an unlinked or cross-linked deployment.

The per-chain file only helps if the RPC alias actually serves the chain the target names — the args path is chosen by the make target, not read off the network. Both scripts therefore assert block.chainid against the EXPECTED_CHAIN_ID the target passes and abort before broadcasting on a mismatch. The failure this closes is silent: DELEGATECALL into an address with no code succeeds with empty returndata, so a wrongly-linked deployment would pass its own wiring checks and then do nothing on every library call. (The guard is skipped on Anvil and in dry runs, where the variable is unset.)

script/DeployBazaar.s.sol deploys, in order: BazaarOracleBazaarPairLens → the BazaarPair implementation → BazaarSequencer and BazaarPairTerminator (constructed against the factory address predicted via vm.computeCreateAddress) → BazaarFactory, then asserts the prediction held. The factory constructor independently re-checks the wiring (Factory__WiringMismatch), so a bad prediction cannot deploy a half-wired system.

The sequencer and terminator are pre-deployed against the predicted address because inlining their deployment in the factory constructor would push its initcode past the EIP-3860 limit.

The genesis UMA identifier

DeployBazaar.s.sol passes the factory a UMA_IDENTIFIER constant, currently ASSERT_TRUTH2, and the constructor validates it against UMA's live IdentifierWhitelist before storing it. A value that is not whitelisted, or is not named ASSERT_TRUTH…, reverts the deployment — which is the only acceptable place to discover it, since every assertTruth the protocol ever makes would otherwise revert. Note that OOv3's own defaultIdentifier constant is ASSERT_TRUTH, which UMA has de-whitelisted in favour of the successor on the same oracle, so deriving the value from the oracle is exactly wrong. After deployment the identifier moves only through the governance track. Mock networks accept any identifier, because the mock whitelist reports everything as supported.

Environment

VariableUsed by
ARBITRUM_MAINNET_RPC_URL, ARBITRUM_SEPOLIA_RPC_URL, BASE_SEPOLIA_RPC_URLfoundry.toml RPC endpoints
ACCOUNT, SENDERkeystore account for forge script
ARBISCAN_API_KEY, BASESCAN_API_KEYcontract verification
ANVIL_RPC_URL, ANVIL_PRIVATE_KEY, ANVIL_WALLETlocal Makefile targets

Network support

See Networks for chain IDs and external-contract addresses. Note that BazaarPair reads Arbitrum's ArbSys precompile (0x64) for L2 block numbers — the protocol runs only on Arbitrum chains. The Base Sepolia configuration exists solely to exercise the real UMA OOv3 dispute flow, which has no official Arbitrum Sepolia deployment.

Architecture

                    ┌────────────────────┐
                    │   BazaarFactory    │
                    │ UMA-gated listings │
                    └─────────┬──────────┘
                              │ deploys EIP-1167 clones
                              ▼
 ┌──────────────────┐   ┌────────────────────┐   ┌──────────────────────┐
 │ BazaarSequencer  │◄─►│     BazaarPair     │◄─►│ BazaarPairTerminator │
 │ bonds · volume   │   │ one per market:    │   │ scheduled · cessation│
 │ caps · fraud     │   │ orders · matching  │   │ stale-oracle · vote  │
 │ proofs           │   │ margin · funding   │   │ wind-down paths      │
 └──────────────────┘   │ ADL · insurance    │   └──────────────────────┘
                        └───┬────────────┬───┘
                            │            │
                            ▼            ▼
                   ┌──────────────┐  ┌────────────────┐
                   │ BazaarOracle │  │ BazaarPairLens │
                   │ Pyth adapter │  │ read-only views│
                   └──────────────┘  └────────────────┘

One BazaarPair clone = one market. Each clone holds its own order book, positions, collateral ledger, insurance fund, funding state, and lifecycle flags. The factory, sequencer registry, terminator, oracle adapter, and lens are shared protocol-wide singletons. See Contracts for the per-contract API surface.

Code-size strategy

BazaarPair would blow past the EIP-170 24 KB limit many times over, so the logic is split three ways:

  • External DELEGATECALL libraries — deployed once, linked by address, and executed in the pair's storage context: MatchingEngineLib, OrderManagementLib, CollateralLib, LiquidationLib, AdlLib, InsuranceVaultLib, RiskParamsLib, FundingLib, TerminationLib, MetaTxLib. Their bytecode does not count toward the pair's limit. All ten carry link references in BazaarPair and must be deployed on the target chain and passed as --libraries flags before it can be deployed — see Self-Deployment.
  • Inlined internal libraries — small, hot helpers compiled into callers: BazaarTypes (structs/constants), BucketLib (solvency math), VaultHealthLib (ADL/termination triggers), MmrSampleLib (lagged-MMR ring buffer), BazaarMathLib (fixed-point + effective prices).
  • BazaarPairLens — computed views (solvency checks, share prices, ADL thresholds, EIP-712 constants) that never needed to be in the pair.

Even split three ways the budget is tight: BazaarPair and MatchingEngineLib both exceed EIP-170 at the repo's default optimizer_runs = 220, so foundry.toml compiles that import chain at 150 runs instead (solc treats it as one unit, so restricting the library pulls the pair and factory down with it). The gas cost is modest — matchBatch is storage-dominated, where optimizer runs matter least. The limits are enforced as a test (test/unit/ContractSizeTest.t.sol, production contracts only) rather than via forge build --sizes, whose gate would also fail on test-only harnesses.

Units & conventions

ConventionValue
Internal precision1e18 (BAZAAR_SCALE) for all prices, sizes, notionals, collateral
Token edgeUSDC, 6 decimals — converted only at transfer boundaries; deposits floored to 1e12 granularity so no unbacked dust is credited
Basis pointsBP_SCALE = 10_000 (1 bp = 0.01%)
Extended basis pointsEBP_SCALE = 1_000_000 — fee rates; 100 EBP = 1 bp
Block numbersArbitrum L2 blocks via the ArbSys precompile (0x64), ~250 ms cadence — not block.number, which tracks L1 on Arbitrum
Notionalsize × price / 1e18

Accounting invariant

The pair tracks two ledgers — totalCollateralDeposited (D, sum of every position bucket's collateral) and insuranceFundBalance (I). All internal value movements (vault PnL, ADL credits, fee flows) are booked as I↔D transfers, never bare credits, so I + D always equals the USDC the contract should hold. Every batch, liquidation, ADL execution, and withdrawal re-checks balanceOf(pair) ≥ I + D within a 0.1% tolerance; a shortfall triggers autonomous emergency termination. This is the protocol's tamper-evident seal: if value ever leaks, the market freezes into pro-rata withdrawal mode rather than letting a first-mover drain it.

Markets & Listing

Anyone can list a market. The factory gates listings through an UMA Optimistic Oracle assertion instead of a governance vote or admin whitelist.

Flow

proposePairDeployment(baseFeedId, isContinuouslyTraded, totalAmount, description)
        │  pulls totalAmount USDC (≥ $4,000)
        │  ├─ $1,000 → UMA assertion bond (refunded to proposer on success)
        │  └─ ≥ $3,000 → escrowed insurance-fund seed
        ▼
   48-hour UMA liveness window (anyone can dispute)
        ▼
assertionResolvedCallback / settleDeploymentProposal
        ├─ truthful → EIP-1167 clone deployed, seeded, registered with
        │             sequencer + terminator; pairId = baseFeedId
        └─ disputed-and-false → bond lost to disputer, seed refunded

What UMA verifiers check

The assertion text embeds seven objective criteria:

  1. Feed validitybaseFeedId exists in Pyth's registry and matches the description.
  2. Trading-schedule flagisContinuouslyTraded = true only for genuinely 24/7 assets (crypto). Stocks, FX, and anything with market hours must be false (they get a 1.5× margin multiplier and the stale-oracle regime). Composites must be false.
  3. Eligible asset — an actual tradeable asset or broad market index, USD-denominated.
  4. Composite quote leg — non-USD quotes only through approved fiat legs (EUR, GBP, JPY, CHF, CAD, AUD, NZD, SEK, NOK, DKK, SGD, HKD, CNH).
  5. Linear, non-expiring spot exposure — the test is: could someone hold this asset indefinitely and experience exactly its price change? Futures, options, bonds, leveraged/inverse products, volatility indices, rate products, pegged/rebased assets, and prediction markets are excluded.
  6. No 1:1 duplicates — no wrapped BTC next to BTC, no S&P 500 ETF next to the index (propose the index itself), unless the underlying has no Pyth feed.
  7. Non-USD assets only as composites.

A market whose pair was previously terminated can be re-proposed.

description is the one proposer-controlled string in the claim, so it is bounded to 1–200 bytes of a conservative ASCII subset — letters, digits, space, and . , & / -. Everything outside that set, including every non-ASCII byte, is rejected at submission. The claim is read by humans deciding whether to dispute, and the charset is what stops crafted text from escaping a delimiter, imitating the claim's Field: value syntax, opening a fake numbered criterion, or smuggling homoglyphs and invisible characters past them. Termination claims apply the same discipline to their free-text fields.

Non-USD assets: composite feeds

Assets quoted in another currency (e.g. DAX in EUR) trade against a composite feed: price = base × quote or base ÷ quote depending on the FX feed's direction. Composites are registered permissionlessly and idempotently in BazaarOracle (registerComposite(baseId, quoteId, invertQuote)); the composite ID — keccak256(baseId ‖ quoteId ‖ invertQuote) — is then used anywhere a feed ID is expected. A composite is only as fresh as its stalest leg, and both legs are confidence-checked.

The only governance: swapping the UMA identifier

The oracle address is immutable. OOv3 generations at new addresses have always been ABI-breaking (v1→v2→v3), so no on-chain upgrade path could adopt one anyway — and a mutable oracle pointer's worst case is a malicious oracle activating through an unwatched governance slot, which is protocol-wide adjudication capture. What UMA actually changes over time is the identifier whitelist (entries added and removed by governance vote, on the same oracle address), so that is the axis Bazaar's governance track upgrades. An oracle-contract-level death — deregistration, the DVM abandoning the generation, neither of which has ever happened to a UMA oracle — means a factory redeploy; existing pairs keep their non-UMA termination paths.

proposeUmaIdentifierUpgrade(newIdentifier) — a $5,000 bond (five times listings') and a 2-day liveness window. The window matches listings' — the same watchers review both tracks — and it is the only point at which a wrong-but-whitelisted identifier can be stopped, since the identifier decides how UMA voters adjudicate every future listing and termination; the extra friction lives in the bond and the activation timelock. The candidate is validated against UMA's live IdentifierWhitelist before the bond moves — resolved the same way OOv3 resolves it (finder → IdentifierWhitelist → isIdentifierSupported) — so a poisoned identifier can never enter the pipeline. The genesis identifier is validated the same way in the constructor: a wrong value fails the deploy, not the deployed protocol. One pending upgrade at a time. In-flight assertions carry the identifier they were made under (OOv3 stores it per assertion), so an upgrade cannot strand them. The bond, like every other in the protocol, is a floor rather than a fixed figure — quote requiredIdentifierUpgradeBond(), which tracks UMA's own owner-settable minimum upward.

The candidate must also be named ASSERT_TRUTH…. UMA's whitelist is a "some UMIP defines this" list, not a "safe for adjudicating truth assertions" list, so the whitelist check alone cannot reject a price feed — and OOv3 resolves an assertion TRUE on a DVM price of exactly 1e18. Some whitelisted identifiers can return it: UMIP-29's EURUSD and CHFUSD are 18-decimal FX feeds read off a live source whose voters never look at ancillary data, so at parity they resolve to exactly 1e18 and decide every dispute in advance — auto-approving everything at parity, and auto-rejecting everything anywhere else, which makes disputing a profitable unconditional veto. Others (dead-source feeds like FEIUSD) are unvotable at all, so the DVM request rolls until it is deleted and settleAssertion reverts forever. Requiring the first 12 bytes to be ASCII ASSERT_TRUTH makes that whole class unrepresentable while staying forward-compatible: UMA versions these identifiers by suffix, so a conventionally-named successor is adoptable with no code change. The accepted tradeoff is that a successor named off-convention would need a factory redeploy — a loud, monitored failure, rather than the silent and unrecoverable one a wrong adjudicator produces.

The protocol currently deploys under ASSERT_TRUTH2. This is exactly why the identifier is a governed parameter and not derived from the oracle: OOv3's own defaultIdentifier constant is ASSERT_TRUTH, which UMA has since de-whitelisted in favour of the successor on the same oracle deployment — deriving from it would point the protocol at a dead identifier from block one.

Approval does not swap the identifier — it queues the upgrade behind a further 14-day activation timelock (IDENTIFIER_UPGRADE_TIMELOCK). Until anyone calls activateIdentifierUpgrade() after the timelock, the incoming identifier governs nothing, so traders who distrust it have a guaranteed exit window (insurance-LP withdrawals remain subject to their 20-day cooldown, which outlasts the timelock). The whitelist check runs again at activation; an identifier de-whitelisted during the 16 days cancels the upgrade and keeps the incumbent, so a dud can never brick governance. (The factory also calls syncUmaParams at construction and activation to warm OOv3's identifier cache — a first-assertion gas nicety only. The cache is not a liveness guarantee: syncUmaParams is public and overwrites with the live value, so anyone can flip a de-whitelisted identifier's cache to false. Nothing in Bazaar relies on it — submissions under a dead identifier are prevented upstream by the gates and the routing below.)

De-whitelisting creates a degraded mode: an assertion under the dead identifier would be undisputable — the DVM re-checks the live whitelist on every dispute — and could also hard-revert outright once anyone re-syncs OOv3's public cache. The protocol fails closed instead of failing open: while umaIdentifierIsLive() is false, new pair listings and both UMA termination proposals revert at submission, so nothing enters an undisputable pipeline (non-UMA termination — insurer vote, stale price, insolvency — is unaffected, and umaIdentifierIsLive() doubles as the one-call monitoring endpoint). The identifier-upgrade path is the deliberate exception — it is the repair path — and in degraded mode it routes its assertion under the proposed (validated-live) identifier rather than the dead incumbent, so the recovery proposal itself remains disputable instead of becoming first-proposer-wins.

There is no rollback lever, deliberately. Reverting to a predecessor identifier would require that predecessor to still be whitelisted, but a full upgrade cycle (2-day liveness + 14-day timelock = 16 days) outlasts the ~7.7-day coexistence window UMA left between whitelisting a successor and retiring its predecessor in its only observed migration — so a rollback target is already gone by the time it could be used. Nor would a rollback address the case that would most want one: an identifier that is semantically wrong but still whitelisted, where the incumbent is exactly what you would be rolling back to. A permissionless writer of umaIdentifier guarding a state that cannot be reached is a liability, not insurance. The identifier-upgrade track is the sole recovery route, and the fail-closed gates hold listings and terminations meanwhile — so a slow recovery costs liveness, never funds.

Only one upgrade proposal exists at a time, and UMA returns the bond to the proposer before firing its resolve callback — so a proposer who becomes unable to receive USDC after proposing makes settlement revert, leaving the slot occupied with nothing able to clear it. expireStuckIdentifierUpgradeProposal() bounds that: anyone may discard a proposal that genuinely cannot settle. It attempts settlement first, so a still-resolvable proposal is never thrown away, and the assertion is left live on the oracle so the winning party can still collect their bond later.

For an undisputed proposal no waiting is needed beyond liveness, because a failure there is proof rather than inference: past expirationTime, OOv3's undisputed branch has only the bond payout and the factory's own (non-reverting) callback left, so a failed settlement can only be a blocked payout. A disputed proposal is the one case that needs a timer — being unsettleable is expected while the DVM votes, and that state is not readable from the factory (hasPrice/getPrice are onlyRegisteredContract). It waits out a further DVM_DISPUTE_GRACE of 14 days, which stops a dispute from becoming a cheap veto: without it, any disputer could discard the proposal the moment liveness ended regardless of how the vote later went. The 14 days are derived from mainnet VotingV2's own limits: a commit+reveal round is 48 h (phaseLength = 24 h) and a request is deleted once it rolls past maxRolls = 4, giving it five rounds — 10 days — plus up to one round of enqueue lead, so 12 days is the longest a dispute can legitimately take. That bound is a property of the DVM rather than of what is being proposed, which is why the deployment track shares the same constant.

Deployment proposals get the identical escape hatch for the identical reason: expireStuckDeploymentProposal(pairId) releases a pairId whose assertion can no longer settle — otherwise a blacklisted deployer or an undeliverable disputer payout would occupy that asset's listing slot forever — and credits the escrowed seed to seedRefundOwed, which its owner collects with claimSeedRefund(). The credit is pulled rather than pushed because the crediting happens inside UMA's settlement callback, where a failing transfer would take settlement down with it.

Orders

Orders rest on-chain in the pair's orders mapping and are matched in sequencer batches. Every user has one net position per market (a position bucket); orders modify it.

Order types

TypePriced byTriggerNotes
Marketoracle ± maxSlippageBpslippage cap 5%; 12-block (~3 s) lifetime; 1 active per user; rejected while the oracle is stale
LimitlimitPricethe only post-only-capable type
StopLimitlimitPricearms at triggerPricelimit must be on the fillable side of the trigger (buy: limit ≥ trigger)
TakeProfitlimitPriceposition-reducing only: opposite direction, size ≤ position, 1 per position, never expires
StopLossoracle ± maxSlippageBparms at triggerPricesame restrictions as TakeProfit

Stops are trigger-gated against the batch's settlement oracle price, not a sequencer-chosen price — a sequencer cannot arm a stop early. Buy stops fire at price ≥ trigger, sell stops at ≤ (inclusive).

Creation rules

  • Minimum order notional $5, with one exemption: an exact full-close of your position is always allowed.
  • Initial-margin check at creation covers worst-case exposure: your position plus all resting orders in whichever direction is worse — including the flip case. Under a stale oracle the requirement doubles.
  • Anti-DoS caps: ≤ 100 active Limit+StopLimit orders per user, 1 active Market order, 1 TakeProfit + 1 StopLoss per position. These bound both the sequencer's calldata and the exposure-tally iteration cost.
  • Lifetimes are stamped in Arbitrum L2 blocks: minimum ~3 s (12 blocks), maximum ~1 year; market orders always ~3 s; TP/SL never expire.
  • Every order can carry an integrator address that earns a referral fee on fills.

Cancellation & cleanup

  • cancelOrders(orderIds[], …) — creator-only, up to 200 per call.
  • Expired limit orders are swept lazily during creation, withdrawal, and the getUserActiveLimitOrders query (the matching walk merely skips them); sweeps emit the same OrderUpdated(Canceled) event.
  • The matching engine auto-cancels orders in four situations: a fill that would fail the owner's margin check, a self-match (newer order cancels), a post-only order that would take instead of make, and a resting TP/SL left oversized after a fill shrinks or flips the position. Auto-cancellation is what makes omission challenges sound — every walked order ends in a provable terminal state.

Order lifecycle

createOrder ──► resting ──┬─► filled          (matchBatch, full or partial fills)
                          ├─► canceled        (user, auto-cancel, lazy sweep)
                          └─► expired         (block-stamped lifetime passes)

filledBlock / canceledBlock stamps plus creationBlock ≤ observationBlock liveness rules are exactly what omission challenges replay — see Sequencers & Fraud Proofs.

Batch Matching

Matching runs in batches: a bonded sequencer snapshots the resting book off-chain, sorts it, and calls

matchBatch(
    OrderLists calldata lists,      // {longLimits, shortLimits, longMarkets, shortMarkets}
    uint256 maxMatches,             // sequencer's gas circuit-breaker (must be > 0; no protocol ceiling)
    bytes[] calldata priceUpdate,   // Pyth update for the settlement price
    uint64 observationBlock         // L2 block at which the book was snapshotted
)

The contract does not trust the sorting — it re-verifies order, type, and tiebreaks inline while walking, and reverts on any violation.

Sort invariants

ListPrimary sortTiebreak
longLimitslimitPrice DESCorderId ASC
shortLimitslimitPrice ASCorderId ASC
longMarkets / shortMarketsmaxSlippageBp DESCorderId ASC

Price-time priority is therefore enforced with order IDs (per-pair monotonic counter) as the FIFO axis.

The three passes

PassWho crossesMaker ruleNotes
Avault liquidation inventory × limitsvault has no order; limit's price fillsonly inside a band of min(LIQ_MAX_SLIPPAGE_BP = 5%, current MMR) around oracle; runs even when the oracle is stale — liquidation flow is forced
Bmarkets × limitslimit is always maker; fill at the limit's effective pricetwo deterministic sub-walks (long-markets first); skipped entirely when the oracle is stale
Climits × limitsolder orderId is maker; fill at maker's pricepost-only violations and self-matches auto-cancel the newer order

Market and StopLoss orders have an effective price of oracle ± their slippage cap; limit-typed orders use their limit price. The same effectivePrice function drives matching and omission challenges, so the two can never disagree.

Per-fill checks

Each prospective fill re-checks the owner's margin at the fresh oracle price:

  • Fails normal IMR → the order is auto-canceled (stamped canceledBlock, event emitted). A user who drifted insolvent since creation cannot haunt the book.
  • Passes normal but fails the 2× stale-IMR check (only relevant in stale-oracle batches) → the order is skipped and its ID recorded in the batch witness staleSkippedIds[], keeping it alive for the next fresh batch — and unchallengeable for this one.
  • Stale price band (also stale-only) — a fill priced more than ±10% (MAX_STALE_DEVIATION_BP) from the last oracle price is voided. The out-of-band price is always the maker's by construction (Pass B fills at the limit's price, Pass C at the older order's), so only the maker leg is retired, and only its ID goes into staleSkippedIds[] — enough for the void to read as a legitimate skip rather than an omission. The counterparty stays live and retries against the next order on the maker's side; it needs no witness entry of its own, because the walk never advances past it and so no worse-priced same-side order can match ahead of it.

Fills realize proportional PnL and funding on closing portions, classify open/add/close/flip by size, and update both sides' buckets plus open-interest aggregates.

Observation-block discipline

observationBlock must be in the past and at most 12 L2 blocks (~3 s) old. Orders created after it revert the batch (OrderCreatedAfterObservation); orders already filled at it revert (StaleFilledOrder); orders canceled, filled, or expired in the race window are skipped without penalty. This pins every batch to a verifiable snapshot of the book — the anchor for censorship proofs.

The batch witness

_finalize assembles a BatchInfo struct — total matched notional, oracle price, execution/observation blocks, timestamp, sequencer, stale flag, the worst fill per side per type (price + orderId FIFO watermarks), two Pass-C cross-side witnesses used to catch market-order censorship, and staleSkippedIds[] — hashes it into batchHashes[batchId], and emits the full preimage in BatchRecorded. Anyone can later re-derive the hash and prove misconduct against it: see Sequencers & Fraud Proofs.

Zero-match batches store no hash and are unchallengeable — with no fills there is no matched range to have been censored from.

Volume capacity

Before walking, the pair asks the sequencer registry for the caller's remaining capacity (bond × 14 over a rolling 30 minutes) and reverts if none; fills that would cross the boundary partial-fill up to it. Matched volume is recorded back to the registry after the walk.

Sequencers & Fraud Proofs

Sequencing is permissionless: anyone who bonds USDC in BazaarSequencer can submit batches to any pair. There is no whitelist, no rotation, no leader election — sequencers compete, and the protocol makes misbehavior provable and unprofitable rather than impossible.

Bonds & capacity

ParameterValue
Minimum bond$1,000 (MIN_BOND)
Matching capacity14× bond per rolling 30-minute window (VOLUME_CAP_MULTIPLIER; NUM_BUCKETS × BUCKET_DURATION) — its inverse (≈7.14% of volume) is also the withdrawal floor, sized to cover the 7% max omission slash
Withdrawal ruleremaining bond ≥ $1,000 (or exactly 0) and ≥ what your last-30-minute volume requires

Volume is tracked in 30 one-minute circular buckets per sequencer plus a global aggregate. The capacity rule means a sequencer's worst-case slashable misconduct is always a bounded multiple of what they have at stake.

Sequencer revenue

  • Dynamic taker fee: 0.75 bp while global bond utilization ≤ 50%, rising linearly to 3.75 bp at ≥ 90% — high utilization simultaneously pays sequencers more and invites more bonding.
  • 0.25 bp maker fee and $0.03 flat per side (see Fees).

Challenge 1 — omission (censorship)

challengeOmission(pair, batchId, batchInfo, omittedOrderId) — prove the sequencer left out an order that should have matched. The caller supplies the batch preimage (verified against the stored hash) and the censored order ID; the contract replays the decision:

  1. Preimage hash matches batchHashes[batchId].
  2. The order exists, was created ≤ observationBlock, and was still alive at executionBlock (not canceled, filled, or expired).
  3. Market/StopLoss orders are unchallengeable in stale batches (they're excluded from stale matching by design).
  4. The order is not in staleSkippedIds[] — if it is, the sequencer demonstrably included it and the walk skipped it legitimately.
  5. Stop orders must actually have been triggered at the batch's oracle price.
  6. In-range check: the order's effective price beats the worst same-side fill recorded in the witness (FIFO orderId tiebreak at equal price). Market orders get a second, always-on cross-side check against the Pass-C witnesses — this catches both total market-order censorship and "include only the most aggressive market to game the cutoff" selective censorship.

Slash: 7% of min(batch notional, censored order's notional), minimum $20 — 1% to the challenger, 6% to the pair's insurance fund. The penalty deliberately exceeds the 5% maximum slippage a sequencer could ever extract by censoring, and the victim is deliberately not the payee — so self-censoring your own order is never profitable. Per-batch slashes are capped at 7% of the batch's notional, and each (pair, batch, order) can be won once.

Challenge 2 — false stale flag

challengeStaleBatch(pair, batchId, batchInfo, priceData) — prove a batch was labeled "oracle stale" when a fresh Pyth tick existed. The challenger supplies a Pyth price proof with publish time in [matchTimestamp − 2 s, matchTimestamp − 1 s] (the 1-second grace concedes that a sequencer can't see a tick mined the same second it built the batch). Verified through BazaarOracle.fetchHistoricalPrice.

Slash: 1% of batch notional, minimum $20 — split 50/50 challenger / insurance fund.

Why it matters: stale batches run under a laxer regime (no market orders, wider margin) — a sequencer who pretends the oracle is stale could selectively suppress market flow. This challenge makes the stale flag itself accountable.

What the design intentionally does not punish

  • Not sequencing at all. Bazaar has no liveness slashing; the incentive to sequence is fee revenue, and multiple sequencers can serve the same pair.
  • Omitting an entire side of the limit book. With no same-side fills there is no witness to measure against; the deterrent is forgone fees plus competition.
  • Deferring below-cutoff orders. A sequencer with a bounded gas budget may legitimately include only the most aggressive orders; the FIFO witness only condemns skipping an order better than one that matched.

Both challenges run within 29 minutes of the batch's matchTimestamp (SEQUENCER_WINDOW) — kept strictly below the 30-minute volume-retention window so a batch's bond stays in the withdrawal floor for its entire challenge life; slashes are capped at the sequencer's remaining bond and push USDC into the pair's insurance fund.

Price Oracle

BazaarOracle is a stateless Pyth adapter shared by all pairs. It never stores prices — each pair caches its own last price — and it is entirely permissionless: anyone can push Pyth updates (paying Pyth's fee in ETH) or register composite feeds.

Freshness tiers

PathStaleness bound
Bots & meta-transactions (sequencer batches, liquidations, relayed user actions)2 s (MAX_PRICE_STALENESS)
Direct user transactions10 s (MAX_PRICE_STALENESS_USER)

Every price-touching entry point accepts a priceUpdate byte array so callers can atomically refresh Pyth and act on the fresh price in one transaction.

Confidence brackets

Pyth publishes a confidence interval alongside every price. Bazaar consumes it two ways:

  • Sanity gate — any price with confidence > 2% of spot is rejected outright (MAX_CONFIDENCE_BP). A band that wide means the publishers disagree enough that neither edge is a usable margin input, so the read fails rather than picking one.
  • Directional brackets — reads return (spot, low = spot − conf, high = spot + conf). Withdrawal checks value long exposure at low and short exposure at high — you cannot withdraw against the optimistic edge of the confidence band. Liquidations and ADL deliberately use plain spot: the 2% gate is the conservatism there, and bracket-pricing forced closures would systematically favor one side.

Non-positive prices revert unconditionally on every path — operating on a zero price would mass-liquidate an entire side.

The stale-price regime (market hours)

Equities and FX stop ticking when their venues close, and a perp on them cannot simply halt — funding, margin, and liquidations still need a price. Pairs listed as not continuously traded keep operating on the last price under tightened rules:

  • New fills require 2× initial margin (STALE_MARGIN_MULTIPLIER).
  • Fill prices are confined to ±10% of the last stored price (MAX_STALE_DEVIATION_BP).
  • Market orders are rejected at creation, and Pass B (markets × limits) is skipped in batches.
  • Orders that pass normal margin but fail the stale 2× check are skipped and witnessed (staleSkippedIds), not canceled.

When no fresh price is available, reads fall down a confidence ladder: any-age spot if its confidence still qualifies → Pyth's EMA price if its confidence qualifies → the pair's last stored price. Each rung is gated, so garbage never silently becomes the settlement price.

Composite feeds

Non-USD-quoted assets multiply or divide through an FX leg (registerComposite(baseId, quoteId, invertQuote), permissionless, idempotent; legs must be raw Pyth feeds — composites cannot nest). Bracket math pairs each leg's bound with the direction that moves the composite the same way, publish time is the older of the two legs, and both legs are confidence-checked — plus the composed bracket must itself sit within the 2% cap, because relative widths add under multiplication (1.8% + 0.15% passes; 1.2% + 1.2% fails). USDC itself is hardcoded to $1 — collateral is not oracle-priced.

Historical proofs

fetchHistoricalPrice(feedId, priceUpdate, minPublishTime, maxPublishTime) parses a Pyth update bounded to a publish-time window. It exists for adjudication: stale-flag challenges prove a fresh tick existed, and scheduled terminations settle at a tick from the 2 s window ending at the cessation timestamp.

Margin & Leverage

Margin requirements in Bazaar are dynamic: they respond to realized volatility, realized liquidation quality, and insurance-fund health, rather than being set per-market by governance.

Initial margin (IMR)

Recomputed at most once per minute:

IMR = 3% base
      × volatility multiplier      (1× → 3× as annualized vol goes ~14% → ~100%)
      × liquidation-gap multiplier (1× → 3× as realized gap EMA goes 0 → 3%)
      × insurance multiplier       (1× → 3× as the fund goes target → empty)
      × 1.5 if not continuously traded (stocks, FX)
clamped to [4%, 80%]               (25× down to 1.25× max leverage)
  • Volatility is an EMA of annualized squared returns (τ = 5 days).
  • Liquidation gap is a size-weighted, time-decayed EMA (τ = 3 days) of how far liquidation fills landed from bankruptcy prices — a market that liquidates badly gets more conservative automatically.
  • Insurance health compares the fund to its target ratio.

Warmup: a new market's IMR is floored at 20% (5× leverage; 30% for non-continuously traded pairs) until it is 5 days old and has seen 50,000 price updates. Thin, young books don't get 25× leverage.

IMR is what's checked at order creation and collateral withdrawal — always against worst-case exposure: current position plus resting orders in the worse direction, including the flip case. Under a stale oracle, requirements double.

Maintenance margin (MMR) and the 24-hour grace

MMR = IMR / 2, always. A position is liquidatable when equity < MMR × notional (see Liquidations).

Because IMR is dynamic, a volatility spike could raise MMR and instantly liquidate positions that were healthy a minute ago. Bazaar prevents this with a lagged MMR: the pair samples MMR hourly into a 25-slot ring buffer, and existing positions are judged against

effectiveMMR = min(reference, current)
reference    = the newest sample ≥ 24 h old   (if the position is ≥ 24 h old)
             = the position's entry MMR        (otherwise)

A rising requirement cannot liquidate you for 24 hours; a falling one helps you immediately. The grace re-anchors every time you increase risk (open, add, flip), and a closed position forfeits it entirely.

Solvency math

All solvency questions go through one function — BucketLib.calculateState:

unrealizedPnl = ±(currentNotional − entryValue)
fundingPnl    = ∓(currentFundingIndex − entryFundingIndex) × size / 1e18
equity        = collateral + unrealizedPnl + fundingPnl
isSolvent     = max(0, equity) ≥ effectiveMMR × currentNotional  AND  equity > 0

Matching, withdrawals, liquidations, and ADL all call the same math with the same lagged-MMR inputs, so a position cannot be simultaneously "healthy" to one subsystem and "liquidatable" to another.

Funding

Funding tethers the perp's traded price to its index. Bazaar's implementation has no keeper and no discrete funding events — it accrues continuously, per second, updated lazily whenever any price-touching function runs.

Mark price

The mark is an EMA of batch execution VWAPs:

  • Each batch's weight scales with its share of rolling volume (which itself decays linearly to zero over 60 minutes), capped at 10% per batch. There is no floor — a dust fill against deep volume gets ~0 weight, so a tiny wash trade can't nudge the mark. The 10% cap is the manipulation bound — no single batch, however large, can move the mark more than 10% of the way to its print.
  • Before entering the EMA, the batch print itself is clamped to ±5% of the index — an implausible fill price can't be injected into the mark, and the steady-state mark stays within that band of index.
  • With no trades, the mark decays linearly back to the index over 1 hour — a stale mark cannot pin funding away from index indefinitely.

Funding rate

premium = (mark − index) / index
rate    = clamp(premium / 8, ±0.5% per hour)

The ÷8 dampening turns a persistent 4% premium into 0.5%/h — strong enough to arbitrage real skew, weak enough not to whipsaw. Longs pay shorts when mark > index and vice versa.

The rate accrues into a cumulative funding index denominated in price units (rate × index price, 1e18), so a position's funding PnL is simply Δindex × size — no per-position bookkeeping between touches.

Settlement

Funding settles to cash exactly once — when position shares close (including liquidation and ADL paths, where the estate's accrued funding rides through the vault's aggregate and settles at unwind). Between touches it exists only as the index delta inside solvency math.

Oracle-gap guards

  • Oracle silent > 12 hours (market closed over a weekend): the accrual window restarts at the new tick — no funding accrues over the dark period.
  • Latest oracle tick more than 30 minutes after the last funding accrual: pre-tick accrual is limited to the 30 minutes before the tick.

Both guards prevent a reopening market from instantly charging a weekend's worth of funding computed against a stale mark.

Liquidations

liquidate(usersToLiquidate[], priceUpdate) is permissionless — any keeper can batch-liquidate positions whose equity has fallen below maintenance margin (judged by the same lagged-MMR solvency math the rest of the protocol uses, at plain oracle spot).

What happens to a liquidated position

Liquidation is full — there are no partials:

  1. The entire remaining collateral is seized into the pair's insurance fund.

  2. The position itself transfers to the pair's vault at its bankruptcy price — the price at which its equity would have been exactly zero:

    long:  bankruptcy = (entryValue − collateral − fundingPnl) / size
    short: bankruptcy = (entryValue + collateral + fundingPnl) / size
    
  3. The keeper is paid max($0.10, 2 bp of notional) per position from the insurance fund, unconditionally — the reward does not depend on how the vault later exits the inventory. (Payouts use a non-reverting transfer; a failed payout restores the fund rather than blocking the batch, and the payment is skipped entirely — never partially — when the fund can't cover it.)

  4. The user's TP/SL/market orders are canceled and the bucket fully reset.

The vault: one aggregate, not a queue

Inherited inventory is aggregated into a single net, single-direction holding — sums of size, entry notional, bankruptcy notional, and a size-weighted entry funding index. There is no per-estate FIFO: unwinding is O(1) regardless of how many accounts were liquidated.

A liquidation on the opposite side nets against the aggregate immediately: the overlapping quantity's price legs cancel at oracle and funding legs cancel at the current index, realizing PnL on the spot; the remainder can flip the aggregate's direction.

Exiting the inventory

The vault is not a market maker — it exits as fast as the book allows:

  • Pass A of every batch: the aggregate fills against resting limits on its own side, but only within min(5%, current MMR) of oracle — the vault will not dump into a hole, and with the band capped at MMR a band-edge fill lands near bankruptcy price in calm regimes. Pass A runs even under a stale oracle; liquidation flow is forced flow.
  • Opposite-side netting (above).
  • Auto-deleveraging when the book can't absorb it and the fund is threatened.

Vault PnL from these exits is booked as an insurance↔deposits transfer (profits refill the fund; losses drain it, overrun becoming deficit — realized bad debt that winds the pair down). Note that once a pair's settlement price is fixed, this live liquidation path is off: positions are marked to the frozen price by permissionless settlement instead.

Every liquidation fill also feeds the liquidation-gap EMA — the realized distance between exit price and bankruptcy price — which raises IMR in markets that liquidate badly and sizes the insurance-fund target. Bad liquidations make the market automatically more conservative.

Auto-Deleveraging

ADL is the backstop for the backstop: when the vault's inherited liquidation inventory threatens losses the insurance fund cannot absorb, the protocol force-closes the most profitable, most leveraged counterparties against that inventory — the standard perp-exchange socialization mechanism, implemented as a permissionless Dutch auction.

Trigger & freeze

After every batch, liquidation, withdrawal, and ADL step, the pair computes the vault's expected loss (bankruptcy notional vs. current notional on the aggregate). When it exceeds 80% of the insurance fund, ADL goes pending:

  • All trading and order creation freeze; position-holders' withdrawals freeze (flat users may still exit — their cash can't affect scores or margin).
  • The trigger price and funding index are snapshotted — the whole auction ranks against this frozen book, so a keeper's off-chain sort and on-chain execution agree; the target side is the opposite of the vault's inventory (the winners).
  • Hysteresis: pending state only clears below 60%, so the boundary can't flap.
  • A hard deadline: pending for 24 hours without resolution → the pair winds down at the live price (the settlement price is fixed on the spot and the 48-hour settlement window opens; anyone finalizes after it). ADL cannot become a limbo.
  • If opposing liquidations flip the vault's inventory to the other side mid-auction, the target side is re-pointed and the price/funding snapshot re-taken — but the 24-hour clock is deliberately not reset, so repeated flips can never stall termination.

The auction

Anyone may call executeAdl(winners[], priceUpdate) with up to 25 candidates, sorted by descending ADL score:

adlScore = positionPnL(at snapshot price) / collateral

— profit-to-collateral, so a 10× levered winner outranks a 2× one at equal profit. Collateral for scoring excludes deposits made during the current ADL window (epoch-tagged at deposit time): topping up mid-auction protects your margin but cannot re-rank you out of the queue. A withdrawn-to-zero winner scores effectively infinite — pure-profit claims stand first in line.

Eligibility decays like a Dutch auction: the score threshold starts at 25× and decays quadratically to ~0 over 10 minutes. The worst offenders are executable immediately; by minute ten, any profitable counterparty is.

Settlement

Winners close at the average bankruptcy price of the dead estates — the price that makes the transfer exactly absorb the vault's book loss:

  • Winners whose PnL at that price would be negative are skipped (never worse than break-even).
  • Oversized winners are partially closed pro-rata.
  • The winner's PnL credit is paid from the insurance fund, capped at what the fund holds — in the deep-insolvency tail, later (lower-scored) winners are haircut. Positions still close; the pair stays solvent by construction.
  • Funding settles implicitly — the estates' accrued funding is embedded in the bankruptcy-derived price, and the winner's own funding tab rides inside their PnL. Charging a funding leg on top would bill both sides twice for the same accrual.

The executor earns 0.1% of averted bad debt. Every few closes the trigger condition is re-checked and the auction ends early once the fund is safe.

Insurance Fund

Each pair has its own insurance fund, held inside the pair contract and owned by share-based LPs — anyone can capitalize any market and earn its fee flow. There is no protocol-wide fund: a meme-coin market's blowup cannot touch the ETH market's backstop.

Flows

In: maker + taker insurance fees · seized liquidatee collateral · vault unwind profits · slashed sequencer bonds · forfeited termination bonds · the listing seed (≥ $3,000)

Out: vault unwind losses · ADL winner credits · liquidation keeper rewards · ADL executor rewards · UMA termination-proposer rewards

Losses beyond the fund become deficit — realized bad debt — which winds the pair down (the health check fixes the settlement price and opens the settlement window). The fund is the last cushion before winners get haircut: at finalize, terminal bad debt is charged against it first, and only the shortfall beyond it reduces the winners' payout ratio.

Target sizing

The fund's target is 2%–10% of open interest, interpolated on the same volatility EMA that drives margin, and floored at 3× the realized liquidation-gap EMA. The taker insurance fee then steers the fund toward target: sharply higher when underfunded, discounted to zero when at 2× target. Risk-reducing fills pay at most the base closing fee (keeping any surplus discount) — deleveraging is never punished.

LP mechanics

RuleValue
Minimum deposit$5
Share pricingfund / totalShares; first deposit 1:1. A fund wiped below $1 by bad debt starts a new share epoch on the next deposit: pre-drain balances lazily read as zero (that stake truly went to zero) and the rescuer is minted 1:1 against only their own contribution. Fund value left with no live LPs is priced into permanently locked shares at address(0), so a new depositor can't buy the pre-existing buffer for the price of their deposit
Withdrawaltwo-step: request → 20-day cooldown3-day execution window. Requests are stamped with the share epoch: a fund recap voids pending requests, so a post-recap re-depositor starts a fresh cooldown
Rate limit (below target)0.5% of OI notional per 6-hour period — one cumulative budget shared by all LPs, so Sybil-splitting doesn't help
Rate limit (above target)max(1% of OI, 10% of fund) against the same 6-hour budget
Voting maturityshares must be 7 days old to vote on insurer terminations; deposit lots are tracked 21 days as snipe-vote defense
Deposits≤ 100 per user per rolling 7 days

Withdrawals are blocked while ADL is pending or a termination is scheduled — LPs cannot front-run the exact events they are paid to backstop. They stay blocked through the 48-hour settlement window, because that is when bad debt is tallied and charged against the fund; letting LPs exit first would push those losses onto traders. Shares committed to an active insurer-vote proposal are likewise locked until it resolves. Once a pair is finalized, every gate opens: LPs always have an exit after settlement, for whatever remains of the fund after bad debt.

Insurance LPs also hold a governance right: proposing and voting on insurer-vote termination of their pair — the people with capital at risk can shut the market down.

Fees

All trading fees are charged per fill, on notional, deducted from each side's collateral. Rates are in extended basis points (EBP, 1/100 bp).

StreamMakerTaker
Sequencer0.25 bp + $0.03 flat/side0.75 → 3.75 bp by global bond utilization + $0.03 flat/side
Integrator0.25 bp0.25 bp
Insurance0.5 bpdynamic — steers the fund to target; see below
  • Dynamic taker sequencer fee: 0.75 bp while global sequencer-bond utilization ≤ 50%, linear to 3.75 bp at ≥ 90% — congestion pays sequencers more and attracts more bonded capacity.
  • Dynamic taker insurance fee: the base rate scales with the fund's target ratio0.5 bp at the 2% minimum target, linear to 2 bp at the 10% maximum. Below target it multiplies up sharply — base × (1 + 49 × shortfall²), hard-capped at 45 bp; above target it discounts linearly to zero at 2× target. Risk-reducing size pays the base closing fee (or the dynamic rate, when that is even lower). Exact curve: RiskParamsLib.getTakerInsuranceFeeEbp.
  • Integrator fee: each order carries an optional integrator address (frontend/aggregator) that earns its cut on every fill of that order. Referral revenue is protocol-native — see the integrator guide.
  • Bug-bounty tax: 1% of every fee stream (sequencer, integrator, insurance) is skimmed to a bug-bounty address fixed at factory deployment — a standing white-hat budget that accrues automatically.

Non-trading rewards

ActionRewardPaid from
Liquidation (per position)max($0.10, 2 bp of notional)insurance fund
Terminal settlement (per position)max($0.10, 2 bp of notional)the settled position's own remaining collateral; insurance only when its own loss wiped it out
ADL execution0.1% of averted bad debtinsurance fund
Omission challenge1% of min(batch, order) notionalsequencer bond
Stale-flag challenge½ × 1% of batch notionalsequencer bond
UMA termination proposer0.1% of the fund, capped $100, once per pairinsurance fund

Fee payouts to sequencers/integrators/bounty that fail to transfer (e.g. USDC blacklist) are re-credited to the insurance fund rather than blocking settlement.

Termination & Settlement

Perpetuals on real-world assets need an exit ramp: feeds get decommissioned, companies get acquired, assets redenominate. Every Bazaar market can be wound down without anyone's permission — five independent paths, each ending with positions cash-settled at a defined price and withdrawals open forever after.

The five paths

PathTriggerGate
Scheduled (UMA)A known future cessation: delisting, oracle decommission, structural change (splits, spinoffs, special dividends > 5%, redenominations), or Pyth contract migration$1,000 bond, 12 h UMA liveness; lastTradingTs must be ≥ 12 h (the liveness) in the future, so the cutoff can't land before the dispute window ends
Post-cessation (UMA)The event already happened — proposer supplies only the cessation timestamp; UMA verifiers check the event and timestamp, and the pair settles at the on-chain-verified Pyth tick from that moment$1,000 bond, 72 h liveness (acceptance halts the market instantly, so scrutiny precedes approval); the timestamp must be in the past and no more than 7 days old (MAX_CESSATION_LOOKBACK)
Stale oracleNo Pyth update for 21 days — objectively provable on-chain, no UMA needednone; anyone
Insurer voteMatured insurance shares voting yes reach 60% of the total-share snapshot taken at proposal, within 7 days$500 bond (forfeited to the fund if the vote fails); 7-day share maturity to vote; 7-day execution window; 14-day proposal cooldown
AutonomousRealized bad debt (deficit > 0), ADL timeout (24 h), or books-vs-balance shortfallnone — self-executing inside the health check

The autonomous path fixes the settlement price at the live price mid-transaction and opens the same settlement window as every other path, rather than terminating on the spot. Freezing withdrawals instantly is precisely what the insolvency check needs, and routing through the window means those positions get the same per-user settlement as a planned wind-down. The one exception is a books-vs-balance shortfall with no usable price, which goes straight to emergency termination (raw collateral, pro-rata, no window).

Market-driven price moves are explicitly not valid termination grounds — anticipated volatility is tradeable risk, not a structural event.

Settlement price discipline

Both UMA paths converge on the same settlement machinery: acceptance freezes the pair's price feed at lastTradingTs (for post-cessation a timestamp already in the past, so trading halts immediately) and the pair then settles at the Pyth tick closest to cessation — a signed historical update in [ts − 2 s, ts], verified on-chain, never a number typed into the proposal. For the first 3 hours — measured from the later of lastTradingTs and UMA acceptance — only that genuine tick is accepted (bad or empty data cannot force a fallback while a real tick may exist); after the grace, it falls back to the frozen last stored price so a market can never strand un-terminated. Trading and matching halt past the scheduled timestamp; once the price is fixed, the live liquidation engine is off entirely and positions change only through settlement at that frozen price.

Binding the price to a signed Pyth tick stops a proposer inventing a number, but on the post-cessation path they still choose which historical tick is read. That is why the cessation timestamp is capped at MAX_CESSATION_LOOKBACK (7 days): without it, a proposer holding a position could scan the asset's entire price history for the print that settles most favourably, and UMA voters would have to judge "is this the right moment?" against an unbounded search space. Inside the window, voters need only check placement against a cessation event they can still verify from public sources — and a timestamp that sits far from that event, or looks selected for price rather than accuracy, is grounds to dispute. A genuinely older cessation terminates through the stale-price path instead.

The non-UMA paths pin their price at execution instead: an insurer-vote termination settles at a fresh oracle tick (≤ 2 s old) supplied with the execute call, and a stale-oracle termination at the newest confidence-cleared price the oracle still serves — falling back to the pair's last stored price when even that read fails, with the 21-day staleness gate enforced either way.

The proposer of a successful UMA termination earns 0.1% of the pair's insurance fund (capped $100) — a bounty for noticing dead markets. Both UMA paths quote their bond from requiredTerminationBond() rather than the $1,000 constant: UMA derives its own minimum from an owner-settable final fee, so the constant is a floor that tracks upward.

Proposer-supplied text

Both UMA proposals take two free-text arguments — proposeTermination(pair, pairDescription, lastTradingTs, reason) and proposePostCessationTermination(pair, pairDescription, priceTimestamp, reason) — that end up inside the claim UMA voters read. Because the claim is the entire basis on which a human decides to dispute, that text is treated as hostile input:

  • Length: reason ≤ 1,000 bytes, pairDescription ≤ 100 bytes; neither may be empty.
  • Charset: pairDescription allows letters, digits, space, and . , & / -. reason allows the same set plus : ? = # % _ ~ + so evidence URLs survive. Every other byte — including all non-ASCII, and square brackets in both fields — is rejected at submission.
  • Placement: the contract writes every field and instruction block itself, then splices reason last, between square-bracket fences that label it untrusted proposer text and mark where it ends. The charset is what makes those fences unforgeable, so the colon the URL set re-admits is defused structurally: no proposer string can close the fence, resume the contract's voice, or add a criterion.

The listing claim applies the same discipline to its description.

The 48-hour settlement window

Every non-emergency path is two-stage. The terminator first fixes the settlement price (fixSettlementPrice), which opens a 48-hour settlement window; only after it elapses can anyone call finalizeTermination. Trading, orders, ADL, deposits, and all withdrawals (collateral and insurance alike) are frozen for the duration, so nobody exits ahead of the accounting.

The window exists to do one job: mark every open position to the final price before anyone is paid. During it, liquidate() becomes the settlement entry point — anyone may pass a list of addresses, with no price update and no ETH, because the settlement price is already fixed (this is what makes it work on dead feeds). Settling is pure accounting and harmless to the target: it needs no solvency threshold at all, because marking a position at a price that will never change cannot alter what its owner is owed.

Each settled position does one of two things:

  • A loser's realized loss is deducted from their collateral and released from the principal ledger, which is what funds winners' profits. Any loss exceeding their collateral is registered as bad debt.
  • A winner's profit is registered as a claim — the per-user figure that makes a correct pro-rata payout computable at all.

Settling pays the caller a bounty of max($0.10, 2 bp of notional), charged to the settled position itself — debited from whatever collateral it has left once its own PnL is booked, exactly like every other fee in the protocol. Winners and losers are treated identically, and each pays only for its own settlement. Because the debit hits the bucket and the principal ledger in the same step, the cash leaving is always backed by that ledger reduction: a bounty can never reach another user's reserved principal. A winner whose remaining collateral falls short of the bounty pays the difference out of its registered claim — transferred to the settler as a claim, not cash, so the pot and the total claims are both unchanged and the frozen ratio every other winner receives is untouched; the settler collects it at the same pro-rata terms after finalize. Only a position with neither collateral nor claim left falls to the insurance fund — so the incentive to settle survives exactly the scenario where it matters most, a drained fund in a deep insolvency.

Why any of this is necessary: liquidation only fires on observed live ticks, but termination events are precisely discontinuities — delistings, mergers, feed deaths. A position that gaps from healthy to deeply insolvent on the final print was never liquidatable at that price. Without a settlement pass, its bad debt is invisible, and winners get promised 100% of a pot that cannot pay them — a first-come-first-served drain.

The settlement waterfall

At finalizeTermination the pair computes, once and for all:

  1. Bad debt is charged to the insurance fund, shrinking the insurers' remainder.

  2. The profit ratio is frozen from the cash actually held:

    profitRatio = min(100%, surplus / total registered claims), where surplus = USDC balance − principal ledger − insurers' remainder.

  3. Black-swan backstop: in the impossible case that cash cannot even cover the principal ledger, principal itself is haircut pro-rata against the pot.

Withdrawals then pay principal + claim × frozen ratio. Two properties follow:

  • Principal is reserved at every moment, with no deadline. Your deposit, minus your own trading losses, is never at risk from someone else's bankruptcy — and it doesn't expire if you never show up.
  • Profit shortfalls are shared at one uniform percentage across every registered winner, regardless of who withdraws first.

Deriving the ratio from the real USDC balance rather than from netted per-side aggregates is deliberate: a net figure can read near-zero on an internally hedged side, where a trivial shortfall would wipe out every winner's profit. Anchoring on cash also folds in any drift automatically.

A user who was never settled during the window is not stranded: their first withdrawal settles them inline, paying principal plus a junior profit credit clipped to whatever surplus remains. Settlement also stays open forever after finalize, so a late-discovered loss keeps releasing reserved principal into the surplus.

Two terminal modes exist: normal (as above) and emergency (books-vs-balance failure: PnL is skipped entirely and raw collateral returns pro-rata, with no window). A pair terminates exactly once, into exactly one mode; insurance LPs' withdrawal gates open at finalize.

Note on inherited inventory. When the vault holds liquidation estates at termination, they settle for their full entry→settlement value including the funding leg — matching how the same estates settle on every other path. Dropping that leg would let the estates' funding obligation vanish while their counterparties still collected theirs.

Gasless Transactions

Every user-facing action on a pair — deposits, withdrawals, order creation and cancellation, insurance deposits/withdrawals — can be executed by a relayer carrying the user's EIP-712 signature. Combined with ERC-2612 permit for USDC approvals, a trader needs no ETH at all.

How it works

Each function takes a trailing (nonce, deadline, relayerFee, signature) tuple:

  • Empty signature → normal transaction, msg.sender is the actor, fee ignored.
  • Non-empty → the contract recovers the signer over the typed payload and acts as them; whoever submitted the transaction collects relayerFee in USDC.
RuleValue
Domainname "BazaarPair", version "1", chainId, verifying contract (re-derived if chainId changes — fork protection)
Noncesstrictly sequential per user
Deadlinemust not be passed, and at most 30 s in the future — a signature cannot be warehoused
Relayer fee$1 per action — deducted from the payout on withdrawals, pulled from the user's wallet alongside deposits (and for insurance-withdrawal requests), or charged to bucket collateral for order create/cancel
Price stalenessrelayed calls use the strict 2 s tier, not the 10 s user tier — relayers are bots

Typed structs exist for depositCollateral, withdrawCollateral, createOrder, cancelOrders, depositToInsurance, requestInsuranceWithdrawal, executeInsuranceWithdrawal. BazaarPairLens.getEip712Constants() and getTypehashes() expose everything an integrator needs to build signatures.

Permit deposits

Deposit functions accept an optional ERC-2612 permitData, executed best-effort before the pull — approval and deposit in one signature. A failed permit emits PermitExecutionFailed and falls through to the normal allowance path (so a griefed permit can't block a deposit that would succeed anyway).

Protocol Parameters

All constants live in src/libraries/BazaarTypes.sol, src/libraries/RiskParamsLib.sol, and the contracts that use them. Values below are verified against the code.

Orders & matching

ParameterValue
MIN_ORDER_AMOUNT$5 notional (full closes exempt)
MAX_SLIPPAGE_BP500 (5%) — market/stop-loss slippage cap
MARKET_ORDER_LIFETIME_BLOCKS12 L2 blocks (~3 s)
MIN_ORDER_LIFETIME_BLOCKS / max12 blocks (~3 s) / ~1 year
MAX_ACTIVE_LIMIT_ORDERS_PER_USER100 (Limit + StopLimit)
Active market orders per user1 · TP/SL: 1 each per position
MAX_CANCELS_PER_CALL200
MAX_OBSERVATION_BLOCK_AGE12 L2 blocks
LIQ_MAX_SLIPPAGE_BP500 — cap on the Pass-A band; effective band is ±min(5%, current MMR)
Integrator fee0.25 bp maker + 0.25 bp taker — only on orders naming an integrator

Sequencers

ParameterValue
MIN_BOND$1,000
VOLUME_CAP_MULTIPLIER14× bond per rolling 30 min (NUM_BUCKETS × BUCKET_DURATION)
Challenge window29 min (SEQUENCER_WINDOW), strictly inside the 30-min volume window
OMISSION_PENALTY_BP700 (7%), min $20 — 1% challenger / 6% insurance
STALE_PENALTY_BP100 (1%), min $20 — 50/50 challenger / insurance
Maker sequencer fee0.25 bp
Taker sequencer fee0.75 bp (≤ 50% util) → 3.75 bp (≥ 90%)
SEQUENCER_FLAT_FEE_PER_SIDE$0.03 per side, on top of bps fees

Margin

ParameterValue
BASE_IMR_BP300 (3%)
Multipliersvolatility, liquidation-gap, insurance — each 1×→3×
Non-continuous multiplier1.5×
IMR clamp4% … 80% (25× … 1.25× leverage)
Warmup floor20% IMR (30% for non-continuously traded pairs) for 5 days and 50,000 price updates
MMRIMR / 2
MMR grace24 h lag, 25 hourly samples
STALE_MARGIN_MULTIPLIER2× on new fills under stale oracle
MAX_STALE_DEVIATION_BP1000 (±10% stale-fill band)
MIN_COLLATERAL_AMOUNT$5 minimum collateral deposit
Retained-collateral floora position-holding account must keep max(0.5% of notional, $5) in collateral (MIN_RETAINED_COLLATERAL_BP = 50). Proportional because fees are: it guarantees the fee that closes the position is always payable

Funding

ParameterValue
FUNDING_INTERVAL / cap1 h / ±0.5% per hour
Premium dampening÷ 8
Mark EMA alpha0 … 10% per batch, volume-scaled (no floor)
MAX_MARK_DEVIATION_BP500 — fills clamped to ±5% of index before entering the mark EMA
MARK_DECAY_PERIOD1 h to index with no trades
Oracle-gap guards≤ 30 min accrual on old ticks; > 12 h gap skips the dark period

Liquidation, ADL & insurance

ParameterValue
Liquidation keeper rewardmax($0.10, 2 bp of notional)
ADL trigger / cancelexpected loss > 80% / < 60% of fund
ADL auction25× score → ~0, quadratic, 10 min; ≤ 25 winners per call
ADL timeout24 h → settlement window opens at the live price
ADL executor reward0.1% of averted bad debt
Insurance target2% … 10% of OI (vol-scaled, ≥ 3× gap EMA)
Insurance LPmin $5 deposit; 20-day cooldown + 3-day window; rate limits 0.5% OI / 6 h (below target), max(1% OI, 10% fund) (above)
Maker insurance fee0.5 bp flat
Taker insurance feebase 0.5 bp … 2 bp (scales with target ratio); below target × (1 + 49·deficit²), 45 bp hard cap; discounted toward 0 above target
BUG_BOUNTY_TAX_BP100 (1% of all fee streams)

Oracle

ParameterValue
MAX_PRICE_STALENESS / user tier2 s / 10 s
MAX_CONFIDENCE_BP200 (2% confidence cap)

Meta-transactions

ParameterValue
MAX_RELAYER_FEE$1 per meta-transaction
MAX_DEADLINE_WINDOW30 s max signature deadline

Listing & termination

ParameterValue
Listing≥ $4,000 = $1,000 UMA bond + ≥ $3,000 seed (MIN_INSURANCE_SEED); 48 h liveness
UMA identifier upgrade$5,000 bond, 2-day liveness, then a 14-day activation timelock (IDENTIFIER_UPGRADE_TIMELOCK) after approval
DVM_DISPUTE_GRACE14 days on top of liveness before a disputed proposal may be discarded as unsettleable; shared by the deployment and identifier tracks (it bounds the DVM, not the claim)
Bond constantsfloors, not fixed amounts — requiredDeploymentBond() / requiredIdentifierUpgradeBond() / requiredTerminationBond() each return max(constant, UMA's live minimum)
Proposer free textlisting description ≤ 200 bytes; termination reason ≤ 1,000 and pairDescription ≤ 100 (MAX_REASON_LENGTH, MAX_PAIR_DESCRIPTION_LENGTH); restricted ASCII, no square brackets
Scheduled termination$1,000 bond, 12 h liveness, lastTradingTs ≥ 12 h out; 3 h precise-tick grace
Post-cessation termination$1,000 bond, 72 h liveness; cessation timestamp ≤ 7 days old (MAX_CESSATION_LOOKBACK)
Stale-oracle termination21 days
Terminal settlement window48 h between fixSettlementPrice and finalizeTermination (deposits + all withdrawals frozen)
Terminal settlement bountymax($0.10, 2 bp of notional) per position, debited from that position's own remaining collateral (insurance only when a position is wiped out by its own loss)
Insurer vote$500 bond; 60% of shares; 7-day vote + 7-day execution window; 14-day cooldown; 7-day share maturity
Balance-check tolerance0.1% (USDC_BALANCE_TOLERANCE_BP)
UMA proposer reward0.1% of fund, cap $100, once per pair

Contracts

Access-control legend: anyone = permissionless · bonded = permissionless with economic stake · wired = callable only by a registered protocol contract.

BazaarPair — src/BazaarPair.sol

One EIP-1167 clone per market. Initialized once by the factory.

FunctionAccessPurpose
depositCollateral / withdrawCollateralanyone / meta-txcollateral in/out; withdrawal enforces IMR at bracket prices and, while a position is open, the max(0.5% of notional, $5) retained floor
createOrder / cancelOrdersanyone / meta-txsee Orders
matchBatchbonded sequencerthree-pass batch matching
liquidate(users[], …)anyonefull liquidation of insolvent buckets — and, once a settlement price is fixed, the terminal-settlement entry point (no price update, no ETH, no solvency threshold)
executeAdl(winners[], …)anyoneDutch-auction deleveraging
depositToInsurance / requestInsuranceWithdrawal / executeInsuranceWithdrawalanyone / meta-txinsurance LP flows
refreshPriceanyonepush a Pyth update, roll funding/margin state
creditInsuranceFromSequencerwired (sequencer)books slash proceeds
setScheduledTermination / fixSettlementPrice / creditInsuranceFromTerminatorwired (terminator)lifecycle transitions (finalizeTermination is public after the 48 h settlement window; settling positions at the fixed price runs through liquidate)

Key views: batchHashes, positionBuckets, orders, lastPairPrice, getSharesAsOf, plus everything on the lens.

BazaarFactory — src/BazaarFactory.sol

FunctionAccess
proposePairDeployment / settleDeploymentProposalanyone (bonded)
proposeUmaIdentifierUpgrade / settleIdentifierUpgradeProposalanyone (bonded)
activateIdentifierUpgradeanyone, once the 14-day post-approval timelock elapses
expireStuckDeploymentProposal / expireStuckIdentifierUpgradeProposalanyone, once the proposal is provably unsettleable
claimSeedRefundanyone (pays only the caller's own credit)
assertionResolvedCallback / assertionDisputedCallbackwired (the UMA OO recorded per assertion)
getPairAddress / isPair / getAllPairs / pairsCount / proposal gettersview
umaIdentifierIsLiveview — the one-call monitoring endpoint; false means listings and UMA terminations are failing closed
requiredDeploymentBond / requiredIdentifierUpgradeBondview — quote before approving; the constants are floors under UMA's live minimum

oo is immutable; umaIdentifier is the single governed parameter. See Markets & Listing.

BazaarSequencer — src/BazaarSequencer.sol

FunctionAccess
deposit / withdrawanyone (own bond)
challengeOmission / challengeStaleBatchanyone
recordVolumewired (registered pairs)
registerPairwired (factory)
checkVolumeCapacity / getRollingVolume / getDynamicTakerSequencerFeeview

BazaarOracle — src/BazaarOracle.sol

FunctionAccess
registerCompositeanyone (idempotent)
updateAndFetchPrice / fetchHistoricalPriceanyone, payable (Pyth fee)
tryReadFreshPrice / tryReadStalePrice / getUpdateFee / getCompositeIdview

BazaarPairTerminator — src/BazaarPairTerminator.sol

FunctionAccess
proposeTermination / proposePostCessationTermination / settleTerminationProposalanyone (bonded) — both proposals take a pairDescription and a reason, charset- and length-bounded
terminateScheduledPair / terminateStalePairanyone
proposeInsurerTermination / voteForInsurerTermination / executeInsurerTerminationinsurance shareholders (bonded) / anyone to execute
registerPairwired (factory)
UMA callbackswired (recorded OO)
requiredTerminationBond / getLockedShares / isPairview

BazaarPairLens — src/BazaarPairLens.sol

Stateless views: getPositionBucket, checkBucketSolvency, getInsuranceSharePrice, getInsuranceDepositValue, getAdlScoreThreshold, getAdlScore (a candidate's auction score and eligibility, exactly as executeAdl ranks them — sort batches by it descending), getTerminalEntitlement (post-termination payout components: collateral, registered claim, frozen ratio), getTerminalSettlementBounty (a sweep keeper's reward for settling a position at the fixed price), getMaxWithdrawable (the largest withdrawal the margin/retention gates would accept — pass the conservative bracket price: spot − conf for a long, spot + conf for a short), getPendingLiquidationExposure, getAuxState, plus constant getters for EIP-712 domains/typehashes, order lifetimes, min collateral, insurance-withdrawal limits, vault-health thresholds, and the flat sequencer fee. Note: the pair's getUserActiveLimitOrders mutates (lazy cleanup) — eth_call it, don't index it as pure.

External dependencies

DependencyUsed for
Pyth (IPyth)all pricing
UMA Optimistic Oracle V3listings, identifier upgrades, terminations (address immutable)
USDC (ERC-20 + ERC-2612)sole collateral
Arbitrum ArbSys (0x64)L2 block numbers
OpenZeppelinClones, SafeERC20, Math, Initializable, ReentrancyGuard, EnumerableSet, ECDSA, Strings

Deployments

This page is the canonical registry of official Bazaar deployments. If an address is not listed here (or reachable from BazaarFactory.getAllPairs() on a factory listed here), it is not part of the official deployment — anyone can deploy an AGPL fork, so always verify addresses against this page.

Arbitrum One (42161) — production target

Official Bazaar deployment: not yet live. Addresses will be published here at launch:

ContractAddress
BazaarFactoryTBD
BazaarSequencerTBD
BazaarPairTerminatorTBD
BazaarOracleTBD
BazaarPairLensTBD
BazaarPair implementationTBD

Individual markets are discoverable on-chain from the factory (getAllPairs() / getPairAddress(pairId)) — the factory is the root of trust for what counts as a real Bazaar pair.

External dependencies on Arbitrum One:

ContractAddress
Pyth0xff1a0f4744e8582DF1aE09D5611b887B6a12925C
USDC (native)0xaf88d065e77c8cC2239327C5EDb3A432268e5831
UMA Optimistic Oracle V30xa6147867264374F324524E30C02C331cF28aa879

Arbitrum Sepolia (421614) — testnet

Official Bazaar testnet deployment: not yet live. Addresses will be published here when it goes up:

ContractAddress
BazaarFactoryTBD
BazaarSequencerTBD
BazaarPairTerminatorTBD
BazaarOracleTBD
BazaarPairLensTBD
BazaarPair implementationTBD

External dependencies on Arbitrum Sepolia:

ContractAddress
Pyth0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF
USDC (testnet)0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d
UMA OOv3mock (deployed with 2 h liveness) — UMA has no official Arbitrum Sepolia deployment

Base Sepolia (84532) — UMA dispute testing only

ContractAddress
Pyth0xA2aa501b19aff244D90cc15a4Cf739D2725B5729
USDC0x036CbD53842c5426634e7929541eC2318f3dCF7e
UMA OOv3 (real)0x0F7fC5E6482f096380db6158f978167b57388deE

BazaarPair requires Arbitrum's ArbSys precompile for L2 block numbers, so the pair cannot run on Base. This configuration exists to exercise the full real-UMA dispute flow end to end.

Anvil (31337)

make deploy-anvil deploys MockPyth, MockUSDC (mints 10,000 to your wallet), and MockOptimisticOracleV3 (2 h liveness). Bazaar protocol addresses are written to .anvil-addresses.

External-dependency configuration for every network lives in script/HelperConfig.s.sol.

Security & Status

⚠️ Pre-audit, work in progress. The protocol is not yet deployed — there is no official deployment on any network. Do not use with real funds.

Posture

  • No admin keys. There is nothing to compromise — but also no one who can pause a bad deployment. The termination paths are the only brakes, and anyone can pull them when their objective conditions hold.
  • One governed parameter, and it is the loudest one: the UMA identifier assertions are adjudicated under. It moves only through a $5,000-bonded assertion, a 2-day dispute window, a whitelist re-check, and a 14-day activation timelock users can exit during — and while it is off UMA's live whitelist, listings and UMA terminations fail closed rather than entering an undisputable pipeline. BazaarFactory.umaIdentifierIsLive() is the one-call monitoring endpoint; alarm when it turns false.
  • Sequencer misconduct is bounded, not prevented: bonds, volume caps, and fraud proofs make censorship and stale-flag abuse provable and unprofitable.
  • Accounting is tamper-evident: the insurance + deposits ledgers are reconciled against the actual USDC balance (0.1% tolerance) on every major state-changing flow — matching, liquidations, ADL, and exposure-bearing withdrawals; a mismatch freezes the market into pro-rata emergency withdrawal rather than letting anyone race the exit.
  • A standing white-hat budget: 1% of every fee stream accrues to a bug-bounty address fixed at factory deployment.

Testing

844 tests across 74 suites (test/unit, test/integration), including zero-sum accounting invariants, crisis-backstop scenarios (insurance drain, deficit, deep-insolvency haircuts), negative-path suites, and targeted pins on the accounting edges that are easiest to get wrong — funding accrual through the ADL path, the Pass-A walk direction, and silently-failing ERC-20s. CI pins Foundry to v1.4.3 and runs forge fmt --check, build, and the full suite on every push; EIP-170 limits are enforced as a test (test/unit/ContractSizeTest.t.sol) over production contracts only.

Reporting

Until a formal bug-bounty program exists, report vulnerabilities by opening an issue in the GitHub repository.

License

Bazaar is licensed under the GNU Affero General Public License v3.0 only (AGPL-3.0-only). Every Solidity source file carries the SPDX-License-Identifier: AGPL-3.0-only header, and the full text ships in the repository's LICENSE file.

What this means in practice

  • You may use, study, modify, and redistribute the code freely.
  • If you distribute a modified version, you must release your modifications under AGPL-3.0.
  • §13 — the network clause: if you run a modified version as a network service (which is what deploying a fork of a DeFi protocol is), you must offer its complete corresponding source to the users of that service. There is no SaaS loophole.
  • You may not relicense forks under more restrictive terms, add usage restrictions that conflict with the license's freedoms, or take the code proprietary.

Forking and competing is legal and expected — the license only guarantees that every fork stays as open as the original.