Documentation

The protocol,
end to end.

Anvil is an options venue with no liquidation price: every contract is backed by collateral before it exists, so there is nothing to liquidate. This page is the whole design in one place: the mechanics, the arithmetic, the deliberate omissions and the contract surface, written for someone who wants to understand it before touching it.

Everything here about the protocol is drawn from docs/ARCHITECTURE.md, the ten module READMEs, and the Solidity itself. Where a README and the Solidity disagree, the Solidity wins and the disagreement is listed rather than resolved quietly.

Section 01

What Anvil is
a venue with nothing to liquidate.

Someone holding an asset escrows it, picks a price and a date, names their own premium, and lists it. A buyer pays that premium for the right (not the obligation) to buy the asset at that price before the date. If they exercise, they pay the strike and take the asset. If they do not, it expires and the writer keeps both the asset and the premium.

Anvil itself never takes a position. It is the venue, not the counterparty. Its only revenue is a cut of each premium, capped in code at 2% and shipping at 0.5%, which means it earns whether the market goes up, down or nowhere, and it cannot lose money on a trade. The fee never rests in a protocol account: it moves from buyer to treasury inside the same call that moves the premium from buyer to writer.

Three properties that fall out of the design

  • Nothing can default. A writer must escrow the collateral before an offer appears on the shelf: OfferBook.list calls escrow.commit before it writes the offer struct. Every option in circulation is backed by collateral already sitting in WriterEscrow. There is no margin, no leverage on the writer’s side, and no liquidation engine, because there is nothing to liquidate.
  • There is no oracle. Anvil settles physically: the holder pays the strike and receives the underlying. Nobody rational does that unless the option is in the money, so the holder’s own decision carries exactly the information a price feed would have supplied.
  • The protocol never prices anything. Writers name their own premiums. A writer who prices badly is the one who finds out. Anvil has no volatility surface to maintain and no model risk, and it cannot be arbitraged through mispricing because it never quotes.

A worked example

Gold at $4,500 an ounce. A writer escrows 100 oz, lists calls at a $4,700 strike expiring in thirty days, and asks $35 per contract of one ounce.

Outcomes at expiry
Gold at $4,400  not exercised   buyer  -$3,500 premium
                                writer keeps 100 oz + $3,500

Gold at $4,650  not exercised   buyer  -$3,500 premium
                                writer keeps 100 oz + $3,500

Gold at $5,200  exercised       buyer  pays $470,000 for gold worth $520,000
                                       net +$46,500 after premium
                                writer gets $470,000 + $3,500,
                                       gave up gold worth $520,000

The writer’s downside is never losing money. It is underperforming holding. They keep the first 4.4% of any rally, from spot up to the strike, plus the premium, and give up everything above $4,700. That is the trade, and it should be stated plainly to anyone writing.

One thing the prose gets wrong

Every document in this repository describes a single instrument: the writer escrows the underlying, the holder pays quote to take it. The contracts are more general than that. AnvilTypes.sol declares OptionKind { CALL, PUT }, createSeries takes a kind, and AnvilMath.collateralLeg and strikeLeg swap the two tokens for a put:

contracts/shared/libraries/AnvilMath.sol
         collateral leg          strike leg
CALL     underlying, size        quote, strike
PUT      quote, strike           underlying, size

A put is the same machine with the tokens swapped, and it has an integration test of its own. Where this page says “underlying” and “quote”, the contracts say “collateral leg” and “strike leg”, and for a put those are the other way round.

Section 02

Core concepts

Four ideas carry the whole design: the series, the tick grid, the availability identity, and the ladder that routes around a sold-out strike. Everything else is plumbing around them.

Vocabulary
TermMeaning
UnderlyingThe asset being sold. For a call, what the writer escrows.
QuoteThe token the strike is paid in. For a call, what the buyer pays to exercise.
StrikeQuote paid per contract to exercise. In the quote token's own units.
Contract sizeUnderlying delivered per contract. In the underlying's own units.
SeriesOne line: (underlying, quote, strike, expiry) plus a kind. The atomic unit.
OfferOne writer's listing on a series, at their own premium. Filled by id.
AvailableContracts written and still unsold at a strike.
AssignmentA writer's pro-rata share of the exercises in their series.

The series is the atomic unit

A series is one tradeable line, fully described by its terms. Every offer, option token and escrow balance is scoped to one. The id is a hash of the terms, so the same terms always name the same series and two people opening “$4,700 gold calls expiring Friday” independently land on the same line rather than on two thin ones.

contracts/shared/types/AnvilTypes.sol
struct Series {
    address     underlying;    // the asset being priced
    address     quote;         // the token the strike is denominated in
    uint128     contractSize;  // underlying per contract, in its own units
    uint128     strike;        // quote per contract, in its own units
    uint64      expiry;        // unix seconds; exercise is impossible after
    OptionKind  kind;          // CALL or PUT - decides which token is collateral
    SeriesStatus status;       // NONE | ACTIVE | EXPIRED
}

There are no fixed-point prices anywhere in Anvil. A strike is quote-per-contract in the quote token’s own units and a contract size is underlying units, so every number in the system is a raw token amount. That removes an entire class of decimal-scaling bugs, at the cost of a frontend having to know both tokens’ decimals.

Listing an asset is admin-gated; opening a strike is not. SeriesFactory.listAsset fixes the quote token, contract size and tick for an underlying, and after that createSeries is permissionless: an empty series holds no collateral and obliges nobody, so gating it would only slow the market down. Expiry must be in the future and within maxHorizon, which initialises to 180 days.

The tick grid

Strikes must be an exact multiple of the asset’s tick. createSeries reverts with StrikeOffGrid otherwise. Without it you get a long tail of near-identical strikes ($150.00, $150.01, $150.02), each holding thin inventory that nobody can compare and nobody can fill.

Snapping concentrates writers onto rungs a buyer can actually compare. It is the only place in the protocol where a design decision exists purely to make a market legible rather than to make it safe.

Availability is an identity, not a cap

A strike is not an infinitely deep order book. It holds exactly as many contracts as writers have collateralized:

contracts/InventoryManager/InventoryManager.sol
available = listed − sold

listed rises when a writer escrows and offers, and falls when they pull an unsold offer. sold only ever rises. So “132 available” means 132 contracts are genuinely backed by underlying sitting in escrow right now: not a limit somebody chose, and not a quote that evaporates when you click it. recordSold reverts with InsufficientAvailability rather than overselling.

The ladder, and sold-out routing

InventoryManager keeps the strikes for an underlying and expiry in a sorted list, inserted in position at series creation. When a strike sells out it exposes the nearest live rung above and below, so a buyer can be routed on-chain rather than trusting the frontend to know what is left.

Routing a buyer past a sold-out rung
ladder(underlying, expiry)            -> Rung[]  every strike, ascending
nextRungUp(underlying, expiry, K)    -> Rung    nearest at or above K
nextRungDown(underlying, expiry, K)  -> Rung    nearest at or below K

struct Rung { bytes32 seriesId; uint128 strike; uint128 available; }

Both searches skip rungs with nothing behind them. A registered but unwritten strike is not somewhere a buyer can go, so it is not offered as an alternative. If no rung qualifies, the call reverts with NoLadderRung rather than returning an empty struct that a caller might mistake for a real one.

Section 03

The lifecycle
six steps, and what moves.

Every trade on Anvil walks the same path. The order of the first step is the load-bearing decision in the whole protocol: collateral moves before the offer exists, which is why nothing on the shelf can be unbacked and why there is no liquidation engine anywhere in the repository.

The path
LIST ──▶ BUY ──▶ HOLD ──▶ EXERCISE ──▶ (expiry) ──▶ SETTLE
                          │                          ▲
                          └── or let it lapse ───────┘
  1. List

    OfferBook → WriterEscrow → InventoryManager

    A writer escrows the collateral leg, names their own premium, and lists an offer. Collateral moves before the offer exists, so nothing on the shelf is ever unbacked.

    underlying: writer → WriterEscrow. listed += quantity.

  2. Buy

    OfferBook → FeeController → OptionToken

    A buyer picks an offer by id and pays the premium. It goes straight from buyer to writer, minus the protocol fee, which goes straight from buyer to treasury. Anvil never holds premium. maxPremium and deadline bound what the buyer can be charged and how stale the transaction may be.

    quote: buyer → writer (total − fee), buyer → treasury (fee). options minted to buyer. sold += quantity.

  3. Hold

    OptionToken

    Options are ERC-1155 and freely transferable while the series is live. Transferring one moves the right to exercise and no obligation, because the holder never had one. The worst a recipient can do is let it expire.

    options: holder → anyone. Nothing else moves.

  4. Exercise

    ExerciseEngine → OptionToken → WriterEscrow

    Before expiry, the holder pays strike times quantity and receives the underlying. Their option tokens burn first, so a holder without the balance fails before any token moves. The strike leg is pulled in before the collateral leg goes out. No oracle is involved.

    options burned. quote: holder → WriterEscrow. underlying: WriterEscrow → holder. exercised += quantity.

  5. Expire

    SeriesFactory

    After expiry the series stops trading and can no longer be exercised. Anyone may flip its status, and the first writer to settle flips it as a side effect, so writers never need a keeper to get paid.

    status: ACTIVE → EXPIRED. No tokens move.

  6. Settle

    ExerciseEngine → WriterEscrow

    Each writer collects their pro-rata assignment: quote for the contracts assigned to them, underlying back for everything else, including anything they wrote but never sold. Once per writer per series.

    quote: WriterEscrow → writer (assigned). underlying: WriterEscrow → writer (committed − assigned).

What cannot happen

A writer cannot cancel an option someone is holding. cancel returns only the unsold remainder; contracts already bought stay collateralized until expiry. A buyer cannot buy their own offer; fill reverts with BuyingOwnOffer. A holder cannot exercise at or after expiry; exercise reverts with Expired. A writer cannot settle before expiry, or twice.

The one thing that can go wrong is a holder forgetting. An in-the-money option left alone until expiry pays nothing, the same as on any physically settled venue, and the reason a frontend should nag.

Section 04

Assignment
pro-rata, and deferred.

When a holder exercises, Anvil does not decide whose collateral was taken. The swap happens at the series level: underlying leaves the pool, quote enters it, and nobody is assigned at that moment. Only after expiry does each writer compute their share.

contracts/shared/libraries/AnvilMath.sol
assigned = theirSold × seriesExercised / seriesSold

They then collect assigned contracts’ worth of quote, plus everything else back as underlying, including anything they wrote but never sold. previewSettle returns exactly what settleWriter would pay, because settlement calls it.

Why it is deferred

Gas. Exercise costs O(1) regardless of how many writers back a strike. The obvious alternative (walking a writer list on every exercise and marking each one assigned) gets more expensive the more writers a series attracts, which is exactly backwards: the most popular series would be the most expensive to trade.

The cost of deferring is that a writer does not know their assignment until expiry. They know the ratio can only move against them as more exercises land, and they know their maximum: they can be assigned on every contract they sold, and never on one they did not.

Why the ratio has to stay fixed

settleWriter zeroes the writer’s own position but deliberately does not decrement the series totals. If it did, the denominator would shrink as writers settled and a later settler would compute a different assignment than an earlier one for the same series. The pool totals are frozen at expiry so every writer gets the same answer whenever they show up.

contracts/WriterEscrow/WriterEscrow.sol
// Zero the writer's position before paying out. `_pools` totals are
// deliberately NOT decremented: the assignment ratio must stay fixed
// across every writer's settlement, or later settlers get a different
// answer than earlier ones.
_committed[seriesId][writer] = 0;
_sold[seriesId][writer] = 0;

Rounding favours the pool

Assignment rounds down. The sum of every writer’s assignment can therefore fall a few units short of the quote the pool actually took in: at most one unit per writer. That dust stays in escrow and is recoverable by admin through skim, which can only ever move the difference between the held balance and the tracked obligations for that token.

The direction of the rounding is the point. It always errs toward the pool holding too much rather than too little, so it can never leave a writer unpaid. skim cannot reach collateral, because collateral is inside obligations.

The same accounting is what makes solvency checkable from outside: obligations(token) is what the escrow owes in that token, and the escrow’s balance should always cover it.

Section 05

What Anvil
deliberately does not do.

Most of this design is a list of refusals. Each one deletes a subsystem, and every deleted subsystem takes a class of failure with it. These are not features waiting to be added later, and each has a price that is stated here rather than hidden.

There is no PricingEngine.sol, no PriceFeed.sol, no Liquidator.sol, and no protocol-owned position anywhere in the venue. Those are not files that have yet to be written. They are files the design is arranged to make unnecessary.

No pricing engine

Writers name their own premiums, and the ladder shows what is on offer. Nothing in the protocol has an opinion about what an option is worth. A writer who prices badly is the one who finds out: the offer sits unfilled, or it fills instantly and they wish it had not.

Removes model risk. There is no volatility surface to maintain, nothing to recalibrate when a regime changes, and no way to arbitrage the protocol through mispricing, because it never quotes. The failure class deleted is the one where the venue's own model is wrong and the venue pays for it.

The costA thin book looks empty rather than looking liquid at a bad price. That is a real cost, and it is what the liquidity vault exists to address: outside the venue, at the depositors' risk.

No oracle

Settlement is physical: the holder pays the strike and receives the underlying. Nobody rational does that unless the option is in the money, so the holder's own decision carries exactly the information a price feed would have supplied. The protocol never has to know what anything is worth.

Removes the largest attack surface an options protocol normally has. No settlement price to manipulate, no staleness window, no dispute period, no guardian veto, and no way for one block of price action at expiry to decide who gets paid.

The costHolders must remember to exercise. An in-the-money option left alone until expiry pays nothing. It also means moneyness cannot be enforced on-chain, which is the one place this decision genuinely hurts; see the risk limits section.

No protocol counterparty in the venue

Anvil does not write options, does not take the other side, and does not run a vault inside the venue. Its only revenue is a capped cut of each premium, so it earns whether the market goes up, down or nowhere.

Removes every path by which the protocol itself can lose money on a trade. A protocol-owned writer vault would make the protocol short volatility: it would win small and often and lose large and rarely, and a bad month would be paid for out of the same balance sheet that guarantees everyone else's settlement.

The costThere is a LiquidityVault in the repository, and it is deliberately a client rather than a module: it writes through AnvilCore exactly as a person would, and the protocol cannot tell it apart from one. Nothing in the venue changed to accommodate it.

No liquidations

Every position is fully collateralized at listing time. The writer's collateral is in escrow before the offer exists; the buyer's only outlay is the premium they already paid. There is no margin and no leverage on either side.

Removes the liquidation engine entirely, and with it the keeper network, the liquidation bonus, the bad-debt socialisation mechanism, and the cascade where forced selling into a falling market triggers the next liquidation. There is nothing to liquidate because nothing can default.

The costCapital efficiency. Writing a hundred contracts means locking a hundred contracts' worth of collateral for the full tenor, with no way to do more with less.

What is left is a venue. Writers bring collateral and a price, buyers bring premium, and Anvil takes a capped cut of that premium for making the exchange safe. It earns whether the market goes up, down or nowhere, and it never takes the other side of anything.

Section 06

Markets
fourteen, and why these.

Fourteen markets are listed in web/lib/markets.ts: three commodities, eight equities, three crypto. The ordering is deliberate and follows the risk tiers in contracts/RiskLimits. None of these real assets exist on any chain this protocol runs on; the testnet deployment lists mock stand-ins for two of them (tXAU and tAAPL), and everything else on this list trades only in the simulator.

web/lib/markets.ts: the listed set
SymbolNameClassQuoteContractTickVolTier
XAUGoldCommoditiesUSDC1 oz$5015%1
XAGSilverCommoditiesUSDC10 oz$126%1
WTICrude oilCommoditiesUSDC10 bbl$135%2
AAPLAppleEquitiesUSDC1 share$526%1
MSFTMicrosoftEquitiesUSDC1 share$1024%1
NVDANvidiaEquitiesUSDC1 share$2.545%2
METAMetaEquitiesUSDC1 share$1035%2
AMDAMDEquitiesUSDC1 share$1050%2
MUMicronEquitiesUSDC1 share$2045%2
SNDKSanDiskEquitiesUSDC1 share$2550%2
TSLATeslaEquitiesUSDC1 share$1055%2
BTCBitcoinCryptoUSDC0.1 BTC$250055%3
ETHEthereumCryptoUSDC1 ETH$10070%3
SOLSolanaCryptoUSDC1 SOL$5100%3

“Contract” is what one contract delivers on exercise, and it is the contractSize passed to SeriesFactory.listAsset, in the underlying’s own units. “Tick” is the strike grid in the quote’s units. Silver and oil are the only assets here where one contract is more than one unit, which is the usual convention for both: a one-ounce silver contract would carry a premium too small to be worth a transaction. Bitcoin runs the other way (one contract is 0.1 BTC), because a whole-coin contract would make the smallest possible position cost the full price of a coin.

The reference spots in that file are round illustrative numbers used to generate a plausibly shaped chain. They are not quotes, there is no feed behind them, and every surface that renders them says so.

Why commodities and equities lead

The instinct to open with the safest asset is right about risk and wrong about revenue, if “safest” means a tokenized T-bill. An at-the-money option is worth roughly

The approximation the whole argument rests on
premium ≈ 0.4 × S × σ × √T

which means premium scales with volatility. Over thirty days, as a fraction of notional:

30-day at-the-money premium by volatility
UnderlyingAnnualized vol30-day ATM premium
Tokenized T-bill~0.5%0.06%
Gold~15%1.7%
Large-cap equity~25%2.9%
Crude oil~35%4.0%
BTC~45%5.2%
SOL~100%11.5%

A T-bill is safe to write because there is nothing to insure. At 0.06% the writer earns about $60 per $100,000 committed for a month, and locks the capital up to do it. That is not a conservative first product; it is no product. Tokenized T-bills are absent from the listed set for exactly this reason.

Gold and equities are the sweet spot: real premium, and a tail that behaves nothing like crypto. They lead the ladder. Real-world assets still belong on the collateral side (idle base asset parked in a yield-bearing T-bill token), just not on the underlying side. One caveat from WriterEscrow: rebasing tokens break its fixed-amount accounting, so use a non-rebasing wrapper.

How the sample chains are generated

None of the numbers on the trade desk are market data. What is real is the shape: each chain is generated from one two-term model rather than typed by hand, so the relationships an options chain cannot violate hold everywhere by construction.

web/lib/markets.ts
call(K) = max(S − K, 0) + TV(K)
put(K)  = max(K − S, 0) + TV(K)

// Both sides take the SAME time value at a given strike, so
// put-call parity holds exactly:
call(K) − put(K) === S − K

Strikes are centred on spot, snapped to the market’s tick, five rungs either side. One rung per side is sold out on purpose, so routing to the nearest live rung stays demonstrable on the desk. That is a display decision, not a protocol one.

Section 07

Risk limits
three tiers, one principle.

RiskLimits decides which assets the liquidity vault may write, how much of its capital may point at any one of them, and what has to be true before a riskier tier unlocks. It constrains the vault only. Nothing in it applies to a person writing an option directly through AnvilCore, who is limited by their own collateral and nothing else.

Every cap is a percentage of base-asset capital, and every input is a base-asset amount. That is deliberate: it is what lets the whole risk engine work without a price feed. It holds because the vault’s primary product is the cash-secured put, whose collateral is the base asset, so the size of the risk is known exactly at write time with no valuation involved. Covered calls are collateralized in underlying the vault only holds because assignment delivered it, which makes them self-limiting: you cannot write more calls than you were assigned.

The tiers, as applied by script/ConfigureRisk.s.sol
LimitTier 1Tier 2Tier 3
AssetsGold, silver, large-cap equityOil, high-beta equity, BTC, ETHCrypto majors
Typical vol15–30%35–60%80–120%
Unlocks at$100,000$500,000$2,000,000
Max per asset45%25%12%
Max per series15%8%4%
Max tenor90 days45 days21 days
Max utilization85%65%45%

None of these numbers live in RiskLimits.sol. The contract ships with an empty tier mapping and every asset at UNTIERED, which means unwritable. The table above is applied afterwards by script/ConfigureRisk.s.sol, which has never been run. An unconfigured RiskLimits blocks every write, which is the safe default and also the current state.

Where the per-series numbers come from

They are set so the worst realistic single-series loss lands at roughly the same fraction of capital in every tier, about 1.5%. A more volatile asset is not banned; it is sized down until it hurts the same amount when it goes wrong.

The derivation, from contracts/RiskLimits/README.md
TierPlausible drawdownLoss if ~15% OTM× series capDamage to capital
120%~10% of collateral15%1.5%
235%~20% of collateral8%1.6%
355%~35% of collateral4%1.4%

That equalisation is the principle worth keeping if the numbers are ever retuned. The specific percentages are governance’s opinion; the property that a tier-3 blowup and a tier-1 blowup cost the vault the same is the design.

Why utilization is attached to the tier

Utilization is a global measure (total capital committed across everything), but the cap checked is the one belonging to the tier being written. The effect is that the riskiest asset carries the tightest gate on the whole book: a tier-3 write only clears if total commitment is under 45%, while tier 1 can run the book to 85%.

Why tenor tightens as volatility rises

Variance grows with the square root of time, so a 90-day position on a 100%-vol token carries far more of it than a 90-day position on gold. Tenor is also lockup: deposits and redemptions are shut until every position settles, so a long-dated write on a volatile name holds every depositor hostage to it.

The limit that is not enforced

Moneyness. “The strike must be within 20% of spot” is the single most useful limit an options book can have, and it cannot be checked without a price. Enforcing it would put an oracle back in the critical path, the dependency the whole protocol was designed to avoid.

The tradeoff is not symmetric, and it is worth being precise about. An oracle used only for pre-trade limits is far weaker than one used for settlement: if it breaks, the vault stops writing, it does not pay the wrong person. That makes it a defensible thing to add later. It is left out for now so the assumption stays visible.

A manager choosing sane strikes is an off-chain assumption, not an on-chain guarantee. A manager who writes deep in-the-money options passes every check in this contract and loses the vault its money on the first expiry. Volatility is unverified too: tiers encode governance’s view of how volatile an asset is, and an asset can change regime faster than governance moves.

script/ConfigureRisk.s.sol, last two lines of output
console2.log("Reminder: moneyness is NOT enforced on-chain.");
console2.log("A manager can write deep-ITM strikes and pass every check here.");

Section 08

The liquidity vault
depositors are underwriters.

Read this before depositing

Depositors here are underwriters, not lenders. The word “lending” does not describe what happens. A lender is owed principal back. A depositor here owns a share of whatever the vault is holding when they leave, and that can be worth less than they put in. The share balance can fall.

The vault writes options. Writing options earns a premium and takes on an obligation: it wins small and often, and loses larger and rarely. At a fair premium the expected value is zero: the profit is entirely whatever margin the manager prices above fair value, and the risk is entirely that the manager prices below it.

Why it exists

Anvil’s order book cannot fill an order nobody wrote. Early on there are no writers, so a buyer clicks buy and nothing happens. The vault is the counterparty of last resort: capital that is always willing to write, so the shelf is never bare. That convenience is paid for by the people whose capital it is.

It is a client, not a core module

The vault writes through AnvilCore exactly as a person would, and the protocol cannot tell it apart from one. Nothing in the venue changed to accommodate it. That separation is the point: the venue keeps its property of being unable to lose money, and the risk lives behind an explicit deposit instead of in the protocol everyone else uses. Someone who only wants to trade never touches it.

Share pricing without an oracle

An ordinary vault marks its open positions to compute a share price, and marking an option needs a price feed, the exact dependency Anvil removed. Two decisions avoid reintroducing it.

  • Rounds. Deposits and redemptions are only open while the vault is flat, with no unsettled positions. roundActive() is the gate, and both deposit and redeem revert with RoundIsActive while it is true. There is never a moment where someone can enter or exit at a stale mark, because when the gate is open there is nothing to mark.
  • In-kind redemption. Leaving hands you a pro-rata slice of every token the vault holds, not a valuation of it. If assignment left the vault holding the underlying, redeemers receive underlying. Nobody has to agree on what anything is worth.

The cost is a lockup: capital cannot leave mid-round. That is disclosed rather than engineered around, because the alternative is an oracle.

One asymmetry worth understanding

Redemption works on a mixed vault; it pays out in kind across everything held. Deposits do not. If a previous round ended in assignment, the vault is sitting on underlying, and someone depositing only the base asset would buy a share of that underlying for nothing, diluting everyone already there. Pricing that deposit fairly would need to value the underlying, which needs a feed.

contracts/LiquidityVault/LiquidityVault.sol
if (roundActive())     revert Errors.RoundIsActive(round);
if (!_holdsOnlyBase()) revert Errors.VaultNotInBaseAsset();

// First depositor sets the peg 1:1. Afterwards shares price off the
// base-asset balance, which is exact because it is all the vault has.
shares = totalShares == 0 ? assets : (assets * totalShares) / held;

LPs can always leave; they cannot always enter. That asymmetry is the price of not having a price feed, and it falls on the person trying to get in rather than the person trying to get out, which is the right way round.

Trust assumptions

Worth being explicit, because this module has more of them than the rest of Anvil put together.

  • The manager sets every premium. Underpricing bleeds depositors slowly and invisibly. This is the single largest risk and it is not enforceable on-chain.
  • The manager chooses what to write, and can therefore concentrate the vault into one strike or expiry, up to whatever RiskLimits allows, and RiskLimits is optional: writeOffer skips the check entirely when the address is zero.
  • Admin can replace the manager, but cannot touch deposits or force a redemption.
  • Settlement is permissionless. Depositors never need the manager’s cooperation to get the vault back to a redeemable state; anyone may call settlePosition.

This module has no tests. Not incomplete tests: no test file at all. It is also absent from script/Deploy.s.sol on purpose; script/DeployVault.s.sol exists for it, and has never been broadcast to any chain.

Section 09

Contract reference
ten modules.

Each module lives in its own folder under contracts/, holding its implementation, its interface, a README explaining the design decisions behind it, and co-located tests where any exist. Signatures below are transcribed from the Solidity rather than the READMEs, and returns clauses are given in the interface’s own names.

Eight of the ten are deployed and wired by script/Deploy.s.sol, and are live on the testnet. The remaining two, LiquidityVault and RiskLimits, have their own scripts (DeployVault.s.sol, ConfigureRisk.s.sol) which compile but have never been broadcast anywhere. shared/ holds types, errors, math, the auth base and safe ERC-20 transfers, and is not deployed at all.

All of it is fully open source, each module in its own public repository under github.com/anvilcash. That is a position, not a disclosure ritual: a venue that asks people to escrow collateral has no business being a black box, and the deployed bytecode is verified against this source on the chain explorer so anyone can check that what is published is what runs. One periphery contract sits alongside the ten: a PaymentRouter that routes every desk purchase through the protocol token, verified on the explorer, and a client of the venue like any other caller.

Dependency direction: modules never call up into Core
                          AnvilCore
                              │
        ┌───────────┬─────────┼──────────┬────────────┐
        ▼           ▼         ▼          ▼            ▼
  SeriesFactory  OfferBook  Exercise   (pause)    Inventory
        │           │       Engine                 Manager
        │           │          │
        │           ▼          ▼
        │      WriterEscrow ◄──┘
        │           │
        └───────────┴──────────► OptionToken   FeeController

The graph stays acyclic and every contract is independently testable. shared/ never imports a module. Access control is uniform: every module inherits AnvilAuth, which gives it an admin, a single core address, and a set of trusted module addresses, and exposes the three modifiers named in the tables below.

AnvilCore

live on testnetsource ↗

The orchestrator, and the only address anyone needs to call.

Holds no balances and owns no market state. It exists to be a stable address, to enforce the pause, and to establish caller identity; every module is guarded by onlyCore, so routing through Core is what makes the module graph safe to expose. Every entry point passes msg.sender through and never lets a caller name a different beneficiary: there is deliberately no writeFor(address) or exerciseFor(address). While paused, reads stay live so positions remain inspectable, but writers cannot settle.

External functions of AnvilCore
SignatureNotes
write(bytes32 seriesId, uint128 quantity, uint128 premium) returns (uint256 offerId)Escrow the collateral leg and list contracts at your own premium.
buy(uint256 offerId, uint128 quantity, uint256 maxPremium, uint64 deadline)Take contracts from one offer. maxPremium and deadline are the buyer's slippage and staleness guards.
cancel(uint256 offerId)Withdraw the unsold remainder of your own offer. Contracts already bought stay collateralized.
exercise(bytes32 seriesId, uint128 quantity)Before expiry: pay the strike leg, take the collateral leg, burn the options.
settle(bytes32 seriesId)After expiry: collect your pro-rata assignment and everything unassigned.
createSeries(address underlying, uint128 strike, uint64 expiry, OptionKind kind) returns (bytes32)Open a strike. Permissionless. Not declared in IAnvilCore.
ladderFor(address underlying, uint64 expiry) returns (Rung[])Every strike for an underlying and expiry, ascending, with availability.
reroute(address underlying, uint64 expiry, uint128 strike, bool searchUp) returns (Rung)Nearest rung with contracts left, when the one you wanted is gone.
availabilityOf(bytes32 seriesId) returns (uint128)Contracts still buyable at a strike. Not declared in IAnvilCore.
writerEscrow() returns (address)The address writers approve.
setModules(address seriesFactory, address inventoryManager, address offerBook, address writerEscrow, address exerciseEngine, address optionToken, address feeController)[onlyAdmin]Wiring. Reverts on any zero address.
setPaused(bool paused)[onlyAdmin]A pause traps collateral, so it is an emergency lever rather than a routine one.

SeriesFactory

live on testnetsource ↗

Defines the terms every other module refers to: (underlying, quote, strike, expiry), plus a per-asset contract size.

Listing an asset is admin-gated; opening a strike is not. Admin fixes the quote token, contract size and strike tick per underlying; after that anyone may create a series, because an empty series holds no collateral and obliges nobody. Strikes must be an exact multiple of the tick or createSeries reverts with StrikeOffGrid. Expiry must be in the future and within maxHorizon, which initialises to 180 days. Delisting stops new series but never strands collateral already escrowed; existing series keep trading and settling.

External functions of SeriesFactory
SignatureNotes
listAsset(address underlying, address quote, uint128 contractSize, uint128 tick)[onlyAdmin]Fixes the quote token, contract size and strike grid. Nothing trades until this is called.
delistAsset(address underlying)[onlyAdmin]Stops new series. Existing series keep trading and settling.
setMaxHorizon(uint64 seconds_)[onlyAdmin]Default is 180 days.
createSeries(address underlying, uint128 strike, uint64 expiry, OptionKind kind) returns (bytes32 seriesId)Permissionless. Registers the strike on InventoryManager's ladder as a side effect.
seriesIdFor(address underlying, address quote, uint128 strike, uint64 expiry, OptionKind kind) returns (bytes32)Pure. The id is a hash of the terms, so the same terms always name the same series.
getSeries(bytes32 seriesId) returns (Series)Reverts with SeriesUnknown if it does not exist.
isTradeable(bytes32 seriesId) returns (bool)ACTIVE and not yet past expiry.
markExpired(bytes32 seriesId)Permissionless and idempotent. Writers should not need a keeper to get paid.
configOf(address underlying) returns (address quote, uint128 contractSize, uint128 tick, bool listed)The per-asset terms admin fixed.

InventoryManager

live on testnetsource ↗

The shelf: how many contracts exist at each strike, and how many are left.

Availability is not a cap somebody chose. It is the sum of collateral writers have actually escrowed, minus what buyers have taken. This module also owns the ladder: the strikes for an underlying and expiry, kept sorted on insert, so a buyer arriving at a sold-out strike is routed to the nearest live rung on-chain rather than trusting the frontend to know. Rungs with no collateral behind them are skipped: a registered-but-unwritten strike is not somewhere a buyer can go.

External functions of InventoryManager
SignatureNotes
availableOf(bytes32 seriesId) returns (uint128)listed minus sold. The number the UI shows.
listedOf(bytes32 seriesId) returns (uint128)Contracts writers have ever escrowed for.
soldOf(bytes32 seriesId) returns (uint128)Contracts buyers have taken. Only ever rises.
registerSeries(bytes32 seriesId, address underlying, uint64 expiry, uint128 strike)[onlyCoreOrModule]Called once by SeriesFactory at series creation. Inserts the rung in sorted position.
addListed(bytes32 seriesId, uint128 quantity)[onlyCoreOrModule]A writer escrowed and listed.
removeListed(bytes32 seriesId, uint128 quantity)[onlyCoreOrModule]A writer pulled an unsold offer.
recordSold(bytes32 seriesId, uint128 quantity)[onlyCoreOrModule]Reverts with InsufficientAvailability past what is on the shelf.
ladder(address underlying, uint64 expiry) returns (Rung[])Every strike, ascending.
nextRungUp(address underlying, uint64 expiry, uint128 strike) returns (Rung)Nearest strike at or above, with contracts available.
nextRungDown(address underlying, uint64 expiry, uint128 strike) returns (Rung)Nearest strike at or below, with contracts available.

OfferBook

live on testnetsource ↗

Where writers list and buyers take.

Ordering inside list matters: collateral is escrowed before the offer exists, so nothing on the shelf is ever unbacked. Premium moves buyer to writer directly inside fill, minus the protocol fee, which moves buyer to treasury in the same call; Anvil never holds premium. Offers are filled individually by id rather than swept by a matching engine; a frontend sorts by premium and points the buyer at the cheapest, which costs nothing on-chain. fill also refuses BuyingOwnOffer.

External functions of OfferBook
SignatureNotes
list(bytes32 seriesId, address writer, uint128 quantity, uint128 premium) returns (uint256 offerId)[onlyCore]Commits collateral, then adds to inventory, then creates the offer. In that order.
fill(uint256 offerId, address buyer, uint128 quantity, uint256 maxPremium, uint64 deadline)[onlyCore]Records the sale, pays writer and treasury, mints the buyer's options.
cancel(uint256 offerId, address writer)[onlyCore]Only the unsold remainder comes back. A writer cannot renege on an option someone holds.
quoteFill(uint256 offerId, uint128 quantity) returns (uint256 total, uint256 fee)What the buyer pays, and the protocol's cut inside it.
getOffer(uint256 offerId) returns (Offer)writer, seriesId, premium, quantity, filled, open.
remainingOf(uint256 offerId) returns (uint128)quantity minus filled, or zero once closed.
offerCount() returns (uint256)Ids are sequential from 1.

WriterEscrow

live on testnetsource ↗

Custody for every writer's collateral, and the pro-rata settlement after expiry.

This is what makes Anvil unable to default: a writer cannot offer a contract without first moving collateral in here, so every option in circulation is backed by collateral already present. Per-token obligations are tracked so solvency is checkable: held balance should always cover what is owed, and skim can only ever remove the excess. On exercise the strike leg is pulled in before the collateral leg goes out, so a failed payment reverts before anything leaves. At settlement the pool totals are deliberately not decremented, because the assignment ratio must give every writer the same answer no matter who settles first.

External functions of WriterEscrow
SignatureNotes
commit(bytes32 seriesId, address writer, uint128 quantity, uint256 collateralAmount)[onlyCoreOrModule]Pulls the collateral leg from the writer and commits it to the series.
release(bytes32 seriesId, address writer, uint128 quantity, uint256 collateralAmount)[onlyCoreOrModule]Returns collateral for contracts never sold. Cannot dip below what the writer has sold.
recordSold(bytes32 seriesId, address writer, uint128 quantity)[onlyCoreOrModule]Notes that a writer's contracts were bought.
fulfilExercise(bytes32 seriesId, address holder, uint128 quantity, uint256 collateralOut, uint256 strikeIn)[onlyCoreOrModule]The swap at the heart of physical settlement. Strike leg in first, then collateral leg out.
settleWriter(bytes32 seriesId, address writer) returns (uint256 collateralOut, uint256 strikeOut)[onlyCoreOrModule]Once per writer per series, after expiry. Reverts with AlreadySettled or NothingToClaim.
previewSettle(bytes32 seriesId, address writer) returns (uint128 assigned, uint256 collateralOut, uint256 strikeOut)What settleWriter would pay right now. The same function settlement itself calls.
committedOf(bytes32 seriesId, address writer) returns (uint128)Contracts this writer has collateral escrowed for.
soldOf(bytes32 seriesId, address writer) returns (uint128)Contracts this writer has actually sold.
seriesTotals(bytes32 seriesId) returns (uint128 committed, uint128 sold, uint128 exercised)The three numbers the pro-rata formula is built from.
obligations(address token) returns (uint256)What the escrow owes in that token. Solvency is held >= owed.
hasSettled(bytes32 seriesId, address writer) returns (bool)Settlement is once-only per writer.
skim(address token, address to)[onlyAdmin]Recovers dust only: held balance minus obligations. It cannot reach collateral.

ExerciseEngine

live on testnetsource ↗

Turns an option into the collateral leg before expiry, and pays writers after it.

This is why Anvil has no oracle. A physically settled option needs no price feed because the holder decides: exercising means paying the strike and taking the collateral, and nobody rational does that unless the option is in the money, so the holder's own choice carries exactly the information a feed would have supplied. Options burn before any token moves, so a holder without the balance fails first. settle calls markExpired, so the first writer to settle flips the series status for everyone. The cost of this design is real: an in-the-money option left alone until expiry pays nothing, which is why a frontend should nag.

External functions of ExerciseEngine
SignatureNotes
exercise(bytes32 seriesId, address holder, uint128 quantity)[onlyCore]Reverts with Expired at or after expiry. Burns first, then swaps.
settle(bytes32 seriesId, address writer)[onlyCore]Reverts with NotExpired before expiry. Marks the series expired, then settles the writer.
quoteExercise(bytes32 seriesId, uint128 quantity) returns (uint256 strikeIn, uint256 collateralOut)What exercising costs and delivers. Pure multiplication: no price is consulted.
isExercisable(bytes32 seriesId) returns (bool)Convenience for a frontend deciding whether to prompt a holder.

OptionToken

live on testnetsource ↗

The options themselves, as ERC-1155. One fungible id per series.

Only one side is tokenized. The writer's obligation lives in WriterEscrow as collateral, not as a token, so there is no short token and the id is simply the series id cast to uint256: no bit-packing, no encoding to get wrong. Options are freely transferable while a series is live; transferring one moves the right to exercise and no obligation, because the holder never had one. Known deviation: safeTransferFrom does not call onERC1155Received and there is no safeBatchTransferFrom. The hook is omitted deliberately, because it hands control to an arbitrary callee mid-transfer. Treat this as a transfer-only ERC-1155 and track balances by reading balanceOf.

External functions of OptionToken
SignatureNotes
idFor(bytes32 seriesId) returns (uint256)Pure. The series id, reinterpreted.
balanceOf(address account, uint256 id) returns (uint256)The only reliable way to observe a holding.
balanceOfBatch(address[] accounts, uint256[] ids) returns (uint256[])Lengths must match.
totalSupply(uint256 id) returns (uint256)Options outstanding on a series.
safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)No receiver hook is called. data is accepted and ignored.
setApprovalForAll(address operator, bool approved)Operator approval is all-or-nothing, per ERC-1155.
isApprovedForAll(address account, address operator) returns (bool)
supportsInterface(bytes4 interfaceId) returns (bool)Reports ERC-165 and ERC-1155 despite the missing hook.
mint(bytes32 seriesId, address to, uint256 quantity)[onlyCoreOrModule]Issued on fill.
burn(bytes32 seriesId, address from, uint256 quantity)[onlyCoreOrModule]Destroyed on exercise.

FeeController

live on testnetsource ↗

Anvil's entire business model: a cut of each premium.

One fee, charged once, on the premium a buyer pays a writer. Nothing is charged on exercise or settlement: a holder exercising is already paying the strike, and a writer collecting assignment proceeds has been paid nothing new. The rate is capped by MAX_TRADE_FEE_BPS, an immutable 200 bps, so governance can lower the fee but a compromised admin key cannot turn a venue fee into confiscation. The contract ships with tradeFeeBps = 50, which is 0.5%. It never takes custody: fees move straight from buyer to treasury inside the same transfer as the premium, and this contract only sets the rate and counts.

External functions of FeeController
SignatureNotes
quoteTradeFee(uint256 premiumTotal) returns (uint256)Rounds down, like every bps calculation in Anvil.
accrue(bytes32 seriesId, uint256 amount)[onlyCoreOrModule]Bookkeeping only. The tokens went straight to the treasury.
setTradeFee(uint16 bps_)[onlyAdmin]Reverts with FeeTooHigh above 200 bps.
setTreasury(address treasury_)[onlyAdmin]
MAX_TRADE_FEE_BPS() returns (uint16)200. A constant, not a setting.
tradeFeeBps() returns (uint16)Initialises to 50.
treasury() returns (address)
totalAccrued() returns (uint256)

LiquidityVault

never deployedsource ↗

A standing writer funded by depositors, so a buyer's order fills instead of waiting for a counterparty.

It is a client, not a core module: it writes through AnvilCore exactly as a person would, and the protocol cannot tell it apart from one. Nothing in the venue changed to accommodate it, which is the point: the venue keeps its property of being unable to lose money, and the risk lives here, behind an explicit deposit. Depositors are underwriters, not lenders: the share balance can fall. Deposits and redemptions are open only while the vault is flat, and redemption pays out in kind, which is what lets the vault price shares without a feed.

External functions of LiquidityVault
SignatureNotes
deposit(uint256 assets) returns (uint256 shares)Reverts with RoundIsActive while positions are open, and VaultNotInBaseAsset if a past round left the vault holding underlying.
redeem(uint256 shares)In kind: a pro-rata slice of every token held. Nothing is priced or sold on your behalf.
previewRedeem(uint256 shares) returns (address[] tokens, uint256[] amounts)The same function redeem itself calls.
writeOffer(bytes32 seriesId, uint128 quantity, uint128 premium) returns (uint256 offerId)[onlyManager]The manager names the premium. That number is the whole business, and nothing on-chain can check it.
cancelOffer(uint256 offerId)[onlyManager]Pulls the unsold remainder of one of the vault's offers.
settlePosition(bytes32 seriesId)Permissionless. Depositors must never need the manager's cooperation to get back to a redeemable state.
roundActive() returns (bool)True while any written position has not settled. The deposit and redemption gate.
openPositions() returns (bytes32[])Series written and not yet settled.
heldTokens() returns (address[])What redemption will pay out in.
capital() returns (uint256)Base-asset capital: idle plus committed.
committedBase(address underlying, bytes32 seriesId) returns (uint256 total, uint256 forAsset, uint256 forSeries)The three commitment figures RiskLimits.checkWrite needs.
setManager(address manager_)[onlyAdmin]Admin can replace the manager but cannot touch deposits or force a redemption.
setRiskLimits(address riskLimits_)[onlyAdmin]Zero disables the checks entirely.
setDepositCap(uint256 cap)[onlyAdmin]Zero means uncapped.

RiskLimits

never deployedsource ↗

Which assets the vault may write, how much of the treasury may point at one of them, and what has to be true before a riskier tier unlocks.

Every cap is a percentage of base-asset capital and every input is a base-asset amount, which is what lets the whole risk engine work without a price feed. It holds because the vault's primary product is the cash-secured put, whose collateral is the base asset, so the size of the risk is known exactly at write time. Covered calls are collateralized in underlying the vault only holds because assignment delivered it, so they are self-limiting. The contract ships with no tiers and no assigned assets; an unassigned asset cannot be written at all, which is the safe default.

External functions of RiskLimits
SignatureNotes
checkWrite(address underlying, uint64 expiry, uint256 capital, uint256 committedTotal, uint256 committedAsset, uint256 committedSeries, uint256 addition)A view that reverts: AssetUntiered, TierLocked, TenorTooLong, SeriesCapExceeded, AssetCapExceeded, UtilizationExceeded.
headroom(address underlying, uint256 capital, uint256 committedTotal, uint256 committedAsset, uint256 committedSeries) returns (uint256)The largest additional commitment allowed right now. The lowest of the three caps.
unlockedTier(uint256 capital) returns (uint8)Highest tier the vault's capital currently unlocks.
tierOf(address underlying) returns (uint8)0 is UNTIERED, which means unwritable.
getTier(uint8 tierId) returns (Tier)minCapital, maxAssetBps, maxSeriesBps, maxTenor, maxUtilBps, enabled.
setTier(uint8 tierId, Tier tier)[onlyAdmin]Every number in the tier table comes from here.
setAssetTier(address underlying, uint8 tierId)[onlyAdmin]Assign only what you are ready to underwrite.
untierAsset(address underlying)[onlyAdmin]Back to unwritable.

Where the READMEs and the Solidity disagree

Found while transcribing the signatures above. In every case this page follows the Solidity, because the Solidity is what would run.

Calls vs puts

Docs sayARCHITECTURE.md and every module README describe one instrument: the writer escrows the underlying and the holder pays quote to take it.

Code saysAnvilTypes.sol declares OptionKind { CALL, PUT }, createSeries takes a kind, and AnvilMath.collateralLeg / strikeLeg swap the two tokens for a put. Puts are a real code path, with an integration test of their own. The prose describes only half of what the contracts do.

OptionToken name and symbol

Docs sayOptionToken/README.md and IOptionToken describe ERC-1155 call options.

Code saysOptionToken.sol hard-codes name = "Anvil Call Option" and symbol = "ANVIL-CALL", but the same contract mints the id for a PUT series. A put position is labelled a call by the token that represents it.

RiskLimits tiers

Docs sayRiskLimits/README.md presents three tiers with capital gates, per-asset, per-series, tenor and utilization caps, as though they were properties of the contract.

Code saysRiskLimits.sol ships with no tiers configured. The constructor takes only an admin, the tier mapping starts empty, and tierOf defaults to UNTIERED. Every number in that table is applied afterwards by script/ConfigureRisk.s.sol. An unconfigured RiskLimits blocks every write.

The fee that actually ships

Docs sayFeeController/README.md states the cap: an immutable constant of 2%.

Code saysTrue: MAX_TRADE_FEE_BPS is 200. The README does not mention that tradeFeeBps initialises to 50, so the fee the contract ships with is 0.5%, not 2%.

AnvilCore's surface

Docs sayAnvilCore/README.md lists createSeries and availabilityOf among the calls Core exposes.

Code saysBoth exist on AnvilCore.sol, but neither is declared in IAnvilCore. An integrator coding against the interface will not see them.

Module count

Docs sayThe root README says the deploy script deploys all eight modules, and ARCHITECTURE.md's contract list has eight entries.

Code saysThere are ten module folders. LiquidityVault and RiskLimits are absent from both documents and from script/Deploy.s.sol.

The expiry horizon

Docs sayNo README mentions a limit on how far out a series may be opened.

Code saysSeriesFactory.maxHorizon initialises to 180 days and createSeries reverts with ExpiryTooFar beyond it. Admin can change it with setMaxHorizon.

Section 10

Testnet
the chain, walked.

The venue is live on the Robinhood testnet (chain id 46630): series open, offers on the shelf, faucet tokens for anyone who wants to paper-trade it. What follows is what is deployed, not a recipe.

  • The venue

    live

    All eight core modules are deployed on the Robinhood testnet (chain id 46630) with the permission graph fully wired, and the wiring was verified the only way that counts: real writes and fills executed against it without a revert. AnvilCore, the only address callers use, is at 0xFe0eaeA76a64F174a16F3baeF1dCAAC23c33c886.

  • The books

    live

    Gold (tXAU) and Apple (tAAPL) are listed against tUSDC, with series opened around thirty days out. The first offers were written on-chain before the desk went up, so the shelf was never empty.

  • The tokens

    live

    tUSDC, tXAU and tAAPL are mock tokens with an open mint: a faucet, on purpose, testnet only. The desk exposes it as a button, so paper-trading needs nothing but a wallet.

  • The payment rail

    live

    tANVIL (0xc70D774D4B879E3A6b5549302A2b9479A4D9fED5) and a PaymentRouter (0x954Baed6caE30166Fe883Fc4AB42837CF46a74F7) route every purchase across the ANVIL book, whether the buyer shows up with ETH, tUSDC or tANVIL; the 30 bps spread on that leg accrues to the treasury, and the route is printed on the ticket, fee included. The venue's books stay quoted in tUSDC: dollar-stable strikes with physical settlement is what keeps Anvil oracle-free, so the token is the payment rail, not the unit of account. The router is periphery, and the venue does not know it exists.

  • The terminal

    live

    Sign-in works with a wallet or an email, the desk trades the live on-chain books, and the terminal's status strip shows all three configuration checks green.

Section 11

Roadmap
paced by the treasury.

The venue starts where full collateral makes a backstop unnecessary, covered calls and cash-secured puts, and widens toward full options only as fast as the treasury grows into the risk. The milestones are ordered; the pace is set by a balance sheet, not a calendar.

  • Token launch

    ahead

    ANVIL graduates from its testnet rehearsal to a real token. The payment rail is already built and exercised: every purchase on the desk crosses the ANVIL book today, so launch is a change of collateral, not of plumbing. From the first live trade, the venue's take starts accruing in the token it runs on.

  • Treasury accumulation

    ahead

    Every trade pays the treasury twice: the fee on premium, and the 30 bps spread on the payment leg. Nothing is asked of the treasury early; it compounds quietly while the venue trades, because the size of that balance sheet is what every later step is gated on.

  • Mainnet launch

    ahead

    The venue deploys to mainnet with the same eight modules and the same one-directional permission graph that run on the testnet today, hardened by everything the testnet surfaces between now and then, and opens with the instruments that need no backstop at all: covered calls and cash-secured puts.

  • Mobile app

    ahead

    The terminal in your pocket: the same desk, the same books, sized for a phone. Covered calls are a check-twice-a-day position, which is exactly what a phone is for.

The instrument ladder

Every rung keeps the founding rule, collateral before the contract exists, and each one asks a little more of the treasury than the last. The first rung asks nothing, which is why it is the one trading today.

  • Covered calls & cash-secured puts

    live

    The launch set, and the only instruments that need zero treasury: the writer escrows the full underlying (calls) or the full strike in quote (puts) before the offer exists, so the worst case is already funded. This is the venue as it runs today.

  • Spreads

    ahead

    Two fully-backed legs netted into one position: a written call collateralized by a held call instead of by the underlying. The escrow math gets richer but the backing rule survives, and the treasury only underwrites the gap between the strikes, a bounded, knowable number per position.

  • Full options

    ahead

    Undercollateralized writing, margin against a portfolio rather than a position. This is the stage everything else exists to reach, and the stage that cannot be rushed: a venue with no liquidation price can only offer it when the treasury is large enough to be the buyer of last resort on the tail it creates. The treasury's size sets the pace, which is why it accumulates first.

The flywheel

The venue’s take is two small tolls on activity: the fee on premium (FeeController, capped by an immutable constant) and the 30 bps spread on the payment leg every desk purchase crosses. Neither requires the treasury to trade, predict, or hold risk; both scale with nothing but usage. That is the whole engine.

  1. The venue trades

    Writers escrow collateral and list; buyers pay premium. Every fill routes the fee on premium to the treasury, and every purchase crosses the ANVIL book on the payment rail, where the 30 bps spread on that leg accrues to the treasury too.

  2. The treasury grows

    Two streams, both proportional to volume, neither dependent on the treasury taking risk. Part of the take accrues in ANVIL itself, so venue usage is structural demand for the token rather than a story about one.

  3. The balance sheet unlocks capacity

    Position limits in the risk tiers are sized so the worst plausible loss on a single series costs the same share of the treasury whichever asset it is. A larger treasury therefore means larger limits, more listed markets, and eventually the next rung of the instrument ladder.

  4. Capacity attracts flow

    Deeper books, bigger size and richer instruments bring more writers and buyers, whose trades are step one. The loop closes; each turn is funded by the last one.

The order of operations matters. A venue that sold undercollateralized options first would be borrowing against a treasury it had not earned; this one sells the instruments that need no backstop, lets the tolls compound, and buys each new capability with retained earnings. Usage funds capacity, capacity earns usage, and the treasury is the flywheel’s bearing: everything turns around how much stands behind the venue.

Section 12

Precedent
each piece proven somewhere.

Anvil is a new combination of old, proven parts. The strategy is the most-validated options product in finance, the collateral model ran at nine figures on-chain, and the settlement mechanic has a working proof of concept. What has not been proven is the three together; that is the bet, and it should be stated as one.

The strategy: covered calls at mass-market scale

Covered calls are the oldest conservative options strategy there is, and packaged “buy-write” products are enormous in traditional finance: JPMorgan’s JEPI grew into one of the largest actively managed ETFs doing essentially what an Anvil writer does, and Global X’s QYLD built roughly eight billion dollars doing it mechanically. “Own the asset, sell the upside, collect income” is demonstrated mass-market demand, not a thesis.

The collateral model: the DOV wave

The 2021 DeFi Option Vault generation: Ribbon Finance (peaked above $300M TVL), Thetanuts, Friktion, Katana, sold fully collateralized covered calls and marketed exactly the property this protocol is named for: full collateralization eliminates liquidations entirely. The writer side of this design is proven at scale. The epilogue is instructive too: vault yields compressed as everyone sold the same expiries, the 2022 bear punished depositors, and Ribbon pivoted into Aevo, a leveraged derivatives exchange; the model this protocol deliberately rejects.

The settlement mechanic: Opyn v1, and oracle-free precedent

Opyn’s first protocol (2020) issued fully collateralized, physically settled options: the holder paid the strike and took the asset, no oracle involved. Structurally it is the closest ancestor of this design, and it worked mechanically; it never found liquidity, and Opyn v2 chased capital efficiency into margin and cash settlement. Panoptic later showed “oracle-free options” can carry a protocol, via a very different mechanism over concentrated-liquidity positions.

A live neighbor

Meraki offers fully collateralized calls and puts on tokenized equities, deployed on the same chain this protocol targets, with one decisive difference: it settles in cash against live oracles. It validates the market while leaving this design its differentiation: physical settlement, no oracle to manipulate, and writer-set premiums instead of oracle marks.

The lesson every predecessor teaches: the writer side is easy to fill and the buyer side is hard. Ribbon sold its entire flow to a handful of market-making firms in weekly auctions; organic buyers never materialized on-chain. This design’s structural answer is a buyer-facing chain with real availability and writer-named prices rather than auctions dumping flow, but “who buys the calls” is the question the whole graveyard says to obsess over.

  • Proof of concept for the mechanic: Opyn v1 (2020), fully collateralized, physically settled, oracle-free.
  • Proof of the collateral model at scale: Ribbon and the DOV wave, $300M+ TVL, “no liquidations” as the headline.
  • Proof of the strategy at mass-market scale: JEPI, QYLD and the buy-write ETF complex.
  • Unclaimed territory: physically settled, oracle-free, writer-priced, in one venue.

Section 13

Backing
and what it does not buy.

Anvil has raised a $500,000 pre-seed round led by Lightspeed, with participation from Blockwall and a group of angel investors.

Round
Pre-seed
Raised
$500,000
Lead
Lightspeed
Participation
Blockwall · angels

Funding is not an audit. Nothing in this section makes the contracts any safer than they were before the round closed.

That distinction is worth being explicit about, because “backed by” is routinely read as diligence on the code. It is not. The investors above bought equity in the company building Anvil. They did not review the contracts, they do not warrant them, and no amount of capital on the balance sheet makes an unaudited protocol safe to put money into.

The operative statement of risk stays simple: the tests pass and a testnet deployment exists, but there has been no audit and nothing of value has ever touched the contracts. Read this section as who is paying for the time it takes to change that.