Sepolia

Integration

The rules that hold between repositories. Each one exists because breaking it fails in a way that is quiet rather than loud, which is exactly the kind of thing no single repository's README will catch.

Deploy order

The order is forced by the fact that later contracts are checked against earlier addresses at deploy time, and by the gate: a collar router must be a known intermediary before it can hold a lending long on a gated market.

It inverts the source dependency, and that is not a mistake. bivium-option's code depends on bivium-lending's (it has it as a submodule), so it compiles second — but LendingController's constructor needs the CollarRouter address, so it deploys second. Compile-time and deploy-time run opposite ways here, and reading the submodule order as a deploy order is how a gated market ends up without a working collar.

  1. bivium-protocol — script/Deploy.s.sol. Produces Core, Exchange, ratifiers and periphery.
  2. bivium-option — script/DeployOption.s.sol. CollarRouter and HedgeRouter, each checked to be wired to that protocol deployment's core and Exchange.
  3. bivium-lending — script/DeployLending.s.sol with EXTRA_INTERMEDIARIES=<CollarRouter>, so collars work on gated markets.
  4. bivium-lending — script/RegisterMarket.s.sol, once per market.
  5. bivium-lending — script/DeployInvest.s.sol for an Earn pool: the module, its HedgeDesk, and a CollateralDutchBook unless an existing one is given. The broadcaster deploys the module as its own curator, binds the desk, lists the initial quoters, attestors and rungs, and hands the curator role to CURATOR in the same run, so a half-configured module is never curated by the wrong key. Rungs on gated markets additionally need gate.setLender(module, true) from the gate owner; the script logs any that are missing.
  6. bivium-protocol-tbv — script/DeployTbv.s.sol, for TBV markets: vaultBTC, the escrow and the app, which deploys its TbvController and TbvCallBook. The script reads both back against the app and logs TBV_CALL_BOOK for the frontend's relayer.
  7. bivium-protocol-tbv — script/DeployTbvInvest.s.sol for an Earn pool on TBV markets: step 5's module, desk and book, given only TBV_APP. The core, Exchange, controller, vaultBTC and TBVBTC are read from the app, so a pool cannot be wired to another deployment's tokens, and the app is the module's redeemer. Rungs are checked against TbvController — registered, in the loan token, not matured, named by the even series id. There is no gate step: TBV markets have no gate.

An Earn pool is unreachable until three other repositories name it: the frontend's deployment file (earnPools, and selfRatifiers, without which the relayer refuses its bids), a bivium-curator wrangler.*.toml, and a bivium-protocol-mm quote config — with controllerKind: "tbv" for a pool on TBV markets.

Deployments are passed forward as JSON (PROTOCOL_DEPLOYMENT, or explicit BIVIUM_PROTOCOL_CORE / BIVIUM_PROTOCOL_EXCHANGE). A set but malformed value reverts; it never falls back to a default. That is deliberate — a silent fallback here deploys a contract wired to the wrong core, and nothing downstream would notice until a fill failed.

Redeploying the core redeploys everything downstream

Core and Exchange addresses are immutable in the contracts bound to them. An Exchange redeploy orphans every module, router and open offer bound to the old one. This has already happened once: an Earn pool deployed 2026-09-29 was bound to a pre-Permit2 Exchange that neither the SDK nor the mm could drive, and was abandoned untraded rather than migrated. Plan a core change as a stack migration, not as a contract upgrade.

Submodule bumps

Solidity dependencies are git submodules under lib/:

bivium-lending       → lib/bivium-protocol
bivium-option        → lib/bivium-protocol, lib/bivium-lending
bivium-protocol-tbv  → lib/bivium-protocol, lib/bivium-lending

A pin is a commit, so a protocol change reaches products only when each product bumps its submodule and its tests are re-run against the new commit. Bump deliberately and in dependency order: protocol first, then lending, then option and tbv. Do not bump a product to a protocol commit that the deployed Exchange does not match — the ABI check will pass locally and the fill will fail on chain.

The SDK release chain

TypeScript consumers do not depend on a sibling checkout or a registry. @bivium/protocol-sdk is built in bivium-protocol-cli, frozen as a release, and vendored into each consumer as a tarball whose integrity the lockfile pins:

bivium-protocol-cli  releases/sdk/<version>/   ← the frozen artifact + manifest.json
        │
        ├─ tarball → bivium-protocol-mm/vendor/
        ├─ tarball → bivium-curator/vendor/         (plus a tarball of mm itself)
        └─ tarball → bivium-protocol-frontend/vendor/

Three consumers, not two: the web app signs and checks the same offers, so it carries the same frozen release rather than its own encoding.

Each release carries a manifest.json recording the source commit, the upstream lock hash, the tarball SHA-256 and the npm integrity string, and node scripts/freeze-sdk-release.mjs --check in that repo verifies a release has not drifted. Consumers' vendor/README.md records which release they carry and where it came from.

Why this shape: it keeps npm ci --ignore-scripts working with no private token and no sibling checkout, and it makes "which encoding is this runner actually using" a question with a hash for an answer. The cost is that an SDK change does not propagate — each consumer re-vendors deliberately.

Four rules:

  • Never overwrite a released version with different contents. The version is the identity of an artifact, not a branch name.
  • Bump consumers in chain order: SDK, then mm, then curator — curator vendors both, and a curator carrying mm built against a different SDK than the one beside it is the failure this ordering avoids. The frontend bumps independently, since nothing depends on it.
  • Copy a release byte for byte. A consumer takes the tarball out of releases/sdk/<version>/ unchanged and points package.json at it; the archive SHA-256 in that release's manifest.json is what makes "same encoding" checkable rather than asserted.
  • The Offer shape is the SDK's. The canonical Offer includes permit2; the pre-Permit2 Offer is intentionally unsupported. A deployment whose Exchange does not match the SDK's ABI, selectors, signature domain or embedded Offer signature fails the runner's preflight — which is the loud failure you want, so do not route around it.

The Direct SDK

bivium-tbv-direct's @bivium/direct-sdk follows the same shape on a smaller chain: built in that monorepo, packed (npm pack in packages/sdk) and vendored into bivium-v3-frontend as vendor/bivium-direct-sdk-<version>.tgz, whose integrity the lockfile pins; the frontend README records the source commit and the archive SHA-256. There is no releases/ directory or manifest yet, so that README line is the whole provenance record. The same rule holds: a changed SDK is a new version, never the old version re-packed.

Checkouts go stale, fast

This stack moves quickly across ten repositories, and a local checkout is routinely dozens of commits behind its default branch. Before reading code to answer a question — and certainly before documenting it — git fetch and read the default branch, not the working tree. The same applies to a repository's own README: a Status section can contradict the Scope section above it after a feature lands.

Where this document and a repository disagree about that repository, the repository wins.

Verification pins are not current

bivium-protocol-lean4 pins the exact source revisions it modelled. That is the right discipline, and it means a pin dates the review rather than tracking the code. Treat a pinned theorem as evidence about that revision only.

Two things follow for anyone changing the contracts. A change to Core.sol accounting does not inherit the existing proofs — the model must be re-extracted and re-pinned, and the conformance corpus re-run. And a change that moves a function invalidates its proofs more quietly than a change that edits one, because nothing fails: the theorem still compiles against the old pin while the function it describes no longer exists where it was. That has already happened once — the hedge files the Lean repo pins are on a bivium-option revision that is not on main, and the hedge role now sits in bivium-lending's HedgeDesk.

One pin defends itself and one does not, which is why only one of them drifted. The conformance harness vendors the Core source byte for byte and checks its hashes before replaying anything, so that pin can only fall behind visibly. The hedge pin has no harness behind it, and it is the one now pointing at a revision that is on no main.

Verification has the current pin table and what each artifact does and does not claim.

The price boundary

The contracts read no price. Everything that does is off-chain — and there are more such inputs than "the price feed" suggests. They are not interchangeable, and every one of them fails closed:

Input Gives To When it fails
bivium-protocol-spot S, the collateral's price now, from a venue index the curator's mark, the mm's guards 502; the consumer suppresses that market
the SDK's venue surface (Deribit or Derive) σ, an implied-volatility surface the curator's mark; advisory only for the mm that market is suppressed, with no venue fallback
the pair's own Uniswap v4 pool S, for a curator configured MARK_SOURCE = "pool"; σ is then a measurement pasted into the config the curator's mark the mark throws and nothing is valued
the app's /api/reference S and realised σ, from daily candles the lend door, display only 502

The split that matters is not the count but the consequence. The first three feed machines deciding what to quote and what to mark, where being wrong costs a bad fill. The last feeds a person deciding whether a bid is fair, where being wrong costs a misleading number on a screen and nothing else — nothing from it enters an offer.

See Architecture for why the boundary sits where it does, Curator for how the mark combines the first three, and Frontend for the last.

bivium-protocol-spot

A venue index per allowlisted pair, published in the units each consumer reads:

Consumer Configuration Reads
bivium-curator MARK_SPOT_URL loanPerCollateral (whole loan tokens per whole collateral), observedAt
bivium-protocol-mm markets[].spotUrl ratio (loan base per collateral base × 1e36), observedAt

One observation serves both; they differ only in scaling, so the two never disagree about what the price is. GET /pair?collateral=…&loanToken=… answers one pair and GET / lists the configured ones.

Four properties are load-bearing for everything above it:

  • Nothing is inferred. A pair in PAIRS names both token addresses, both decimals, the venue index, and the assertion that makes that index this pair's price — for example that a given open-mint test token tracks BTC and another is USDC. The assertion comes back with every observation, so a consumer always sees what it is trusting rather than inheriting it silently. Pairs are directional, and anything unlisted is a 404, never a guess.
  • Never stale. The index must be at most 30 s old and at most 30 s ahead of our clock — the same convention as the rest of the tooling — or the answer is 502 spot-unavailable, with no upstream message passed through. Responses are no-store.
  • Rounded down, the conservative side for both consumers at once: a lower price raises the curator's LTV and lowers its ceiling, and trips the mm's strike-at-or-above-spot guard sooner. Rounding goes through the venue's own decimal, so binary noise cannot cost a unit.
  • Allowlisted by address. Anyone can mint a token called WBTC on a test chain, so a pair is keyed by address and admitted deliberately.

Why not the pool

A pool's own price is no price when its tokens are open-mint test tokens: anyone can move such a pool for free, and the curator's ceiling is computed from it. This is not hypothetical — on Robinhood testnet the bUSD/mNVDA pool was uninitialised, nothing else on that chain priced mNVDA, and the one pool that did exist implied $1,346 against a $170 manifest.

The result is the design working rather than an outage: the mark read throws and nothing is valued — fills wait unmarked and nothing acts on bad data. A price that fails toward "pay anything" is worse than no price. Do not add a fallback to get past it; see Curator for which direction each failure has to fall.

Operational: Deribit refuses Cloudflare

Deribit answers Cloudflare's shared egress with 429 — 6 of 6 probes on 2026-10-01, while a local machine got 200 — so the Worker cannot be relied on to reach it. The same handler runs as a plain node server (npm run serve, reading the same PAIRS from wrangler.toml) on a host with its own egress, which is what the live curator reads. The Worker stays deployed and answers 502 whenever the venue refuses it. CI parses the deployed PAIRS, because a malformed list would answer 503 to everyone.

Roles are roles, not owners

Market maker and curator describe jobs, and one actor commonly does both — the curator is defined as the market maker plus the Earn pool. The job says nothing about the authority, which comes entirely from how the quoting is funded, and the three funding kinds are not parallel in what they permit. Tooling has the table; the one to carry around is that a funding account's authorized maker can spend the entire account, which is neither a passive-depositor vault nor a limited quoting permission and must not be described as one.

Vendored from bivium-protocol-doc 57ed1bdc6cfe, synced 2026-10-27. Where this and a repository disagree about that repository, the repository wins.