Skip to main content

On-chain fill modes

Options settlement goes through OpshunsMothership (EIP-2535 diamond). Integrators call the mothership address; facets are an implementation detail.

There are three ways to settle an options RFQ / package fill. All of them ultimately mint inventory on the pair OpshunsClob clone via bilateral fillMatched (or take resting book liquidity first). See also CLOB.

ModeWho signs the requestWho fills (msg.sender)PremiumEntrypoint
FillMatched (leg-by-leg RFQ)MakerTakerPer-leg bps (+ optional CLOB offers)executeIntent
FixedPackage (FillMatched package)MakerTakerFixed absolute ERC-20 package premiumexecuteFixedPackageIntent
Dutch auctionTakerMakerTime-varying absolute package premiumexecuteDutchIntent
AuthoritySame as Dutch / FixedPackage + authority approvalSame fillerSame*WithAuthority

Leg-by-leg RFQ is what today’s trading quote box and options RFQ API mostly use. FixedPackage, Dutch, and Authority are on-chain paths for bots and advanced desks.

┌─ Leg-by-leg RFQ ──► optional CLOB takes ──► fillMatched (bps)
Maker-signed ──────┤
└─ FixedPackage ────► package premium ─────► fillMatched (0 bps)

Taker-signed ─────── DutchFill ─────────► dutch premium ───────► fillMatched (0 bps)

Either package ───── + requiredAuthority ► AuthorityApproval ──► *WithAuthority

1. FillMatched — maker-signed leg-by-leg RFQ

Facet: RfqFacet
EIP-712 domain: OpshunsRFQ / "1"
Typed data: OpshunIntentRequestLegByLeg

This is the standard RFQ path: the maker signs directionless Buy/Sell premiums (bps) per leg. The taker opens the API quote window, picks a signed offer, and calls:

executeIntent(
request, // OpshunIntentRequestLegByLeg
makerSignature,
maker,
directions, // Buy/Sell per leg — must match takerAddressHash
takerSalt,
takerPermits, // empty ⇒ ERC-20 allowance pulls
referrer // address(0) = no referral
)

Settlement steps

  1. Verify maker EIP-712 signature, makerExpiry, and takerAddressHash.
  2. Consume usedNonces[maker][makerNonce].
  3. For each leg:
    • If the quote includes ClobOffer[], the mothership takes that resting liquidity as the maker (orderBuyFrom / orderSellFrom). Each CLOB slice must fill exactly or the whole intent reverts (IncompleteFill).
    • Any remainder settles with bilateral fillMatched at the quoted premiumRateBps.
  4. Emit MatchedFilled per fill and RfqExecuted(..., isPackage=false, ...).

Directions (NatSpec)

  • Directions.Buy → taker sells (provides underlying, receives premium).
  • Directions.Sell → taker buys (pays premium).

takerAddressHash = keccak256(abi.encode(taker, salt, directions)) is what makers copy from the RFQ — they never see the raw taker address on the public feed.

Notes

  • No requiredAuthority on this path — authority gating is for Dutch / FixedPackage only.
  • Referral is taker-chosen and is not in the maker’s EIP-712 payload.
  • Cancel unused maker nonces with cancelNonce / cancelNonces (still works while the RFQ facet is frozen).

API + signing walkthrough: Fill options RFQ quotes, EIP-712 signing.


2. FixedPackage — maker-signed package FillMatched

Facet: FixedPackageFacet
EIP-712 domain: OpshunsFixedPackage / "1"
Typed data: FixedPackageRequest

Use this when you want a single absolute package premium (token units) across multiple legs instead of per-leg bps. Legs settle with fillMatched at premiumRateBps = 0 after the package premium is pulled.

executeFixedPackageIntent(
request,
makerSignature,
maker,
directions,
takerSalt,
takerPermits,
referrer
)

Hide-until-execution

The maker signs directionless dual A/B premium quotes so the EIP-712 payload does not leak Buy vs Sell. Real sides are revealed only when the taker submits directions + takerSalt that match takerAddressHash.

Polarity rule: all A legs share one Buy/Sell; all B legs take the opposite side.

Emits RfqExecuted(..., isPackage=true, ...).

Fixed-premium packages live only on FixedPackageFacet — not on RfqFacet.


3. Dutch auction — taker-signed package

Facet: DutchFillFacet
EIP-712 domain: OpshunsDutchFill / "1"
Typed data: DutchFillRequest

Here the taker posts a multi-leg package with a time-varying absolute premium. The maker races to fulfill when the price is acceptable.

executeDutchIntent(
request, // includes referrer — taker-bound in EIP-712
takerSignature,
taker,
directions,
makerSalt
// no separate referrer arg — settlement uses request.referrer
)

Auctioneer-gated fills use executeDutchIntentWithAuctioneer (+ DutchAuctioneerFillArgs). KYC fill-authority uses executeDutchIntentWithAuthority.

DutchFillRequest fields

FieldRole
intents / tokensLegs + CLOB token bindings
dutchPremiumSchedule (DutchPackagePremium) — no auctionEnd inside the premium struct
auctionEndSole hard deadline (UniswapX-style; no separate takerExpiry)
makerAddressHashMaker binding or polarity-only directionsHash on the auctioneer path
takerNonceReplay protection
requiredAuthorityKYC fill-authority gate (0 = none; mothership = any registered)
requiredDutchAuthorityAuctioneer gate (0 = none; mothership = any registered dutch auctioneer)
referrerTaker-bound referral

Premium schedule (DutchPackagePremium)

FieldRole
tokenERC-20 used for the package premium
makerPaysTakerPremium direction
startPremium / endPremiumAbsolute amounts at auction start / end
decayRateStep per second of elapsed time
auctionStartUnix start (end is top-level auctionEnd on the request)

Pricing (quoteDutchPremium / LibFixedPackageSettle.quoteDutchAmount):

  • Reverts if block.timestamp < auctionStart or auctionEnd < auctionStart.
  • Elapsed time is capped at auctionEnd — the quote freezes after the end.
  • Steps = min(decayRate * elapsed, |start − end|).
  • If startPremium >= endPremium the premium decays down; otherwise it ramps up.

After the absolute premium settles, legs use fillMatched at 0 bps.

Referral on Dutch

referrer is a field on DutchFillRequest and is hashed into the taker EIP-712 digest. Open and authority Dutch entrypoints take no separate referrer argument — the filling maker cannot override it. Changing referrer after sign invalidates both the taker signature and any prior authority approval over that fillDigest.

Maker binding

PathrequiredDutchAuthoritymakerAddressHash
Open / bound makeraddress(0)hash(maker, makerSalt, directions)
Auctioneer / API competitionmothership (or specific auctioneer)directionsHash(directions) — polarity only; auctioneer picks maker via exclusiveFill

Emits DutchExecuted (not RfqExecuted).

API competition lifecycle (POST /api/quote with offer_formats: dutch_only): Dutch auction API. Trading UI choice: Trading app — Dutch auction.


4. Authority fills

Facet: FillAuthorityFacet
EIP-712 domain: OpshunsFillAuthority / "1"
Typed data: AuthorityApproval

When a Dutch or FixedPackage request sets requiredAuthority != address(0), open entrypoints refuse the fill. Settlement must go through:

  • executeDutchIntentWithAuthority
  • executeFixedPackageIntentWithAuthority

On-chain, the authority is a gate: a registered EOA or EIP-1271 contract that must approve a specific fill digest (and optionally pin the executor) before settlement. Policy (who is allowed, KYC tier, jurisdiction) lives off-chain in how that authority decides to sign — the chain only checks registration, signature, nonces, and exclusive-fill binding.

requiredAuthority

ValueMeaning
address(0)Open fill only (executeDutchIntent / executeFixedPackageIntent)
Mothership (address(this))Any registered authority may approve
Concrete addressThat registered authority only

Register with setFillAuthority(authority, true) (EOA or EIP-1271). Facet owner controls registration.

Authority approval

AuthorityApproval(
bytes32 fillDigest, // digest of the Dutch or FixedPackage request
bytes32 exclusiveFill, // 0 or exclusiveFillId(executor, exclusiveNonce)
uint256 authorityNonce, // consumed for the authority
bytes metadata // opaque off-chain policy (hashed into the digest)
)

exclusiveFillId(executor, exclusiveNonce) = keccak256(abi.encode(executor, exclusiveNonce)).

metadata is opaque to the protocol but bound into the approval digest. The bytes submitted at fill must match what the authority signed (e.g. tests use labels like kyc:tier1). Mismatch → InvalidAuthoritySignature.

Dutch + authorityFixedPackage + authority
Request signerTakerMaker
FillerMakerTaker
Counterparty in requestmakerAddressHash must be 0takerAddressHash binds taker + polarity
Who picks the fillerAuthority via required non-zero exclusiveFillMaker already signed; exclusiveFill optional (may pin the taker)
referrerIn taker-signed request (in fillDigest)Taker calldata at fill (not in maker digest)

Nonces burned: authorityNonce always; if exclusiveFill != 0, also the executor’s exclusiveNonce. Shared map: mothership usedNonces.

Wrong entrypoint for open vs authority reverts (AuthorityRequired / AuthorityNotRequired).

Future KYC-specific modes

Authority is the intended hook for permissioned / KYC’d markets without changing FillMatched, Dutch pricing, or CLOB accounting. The protocol stays settlement-agnostic; compliance logic stays with the registered authority (and any off-chain API that brokers approvals).

Typical patterns:

  1. Dedicated KYC authority address
    Deploy or operate an EIP-1271 “KYC desk” contract (or EOA behind a custody stack) and register it with setFillAuthority. Intents set requiredAuthority to that address so only that desk’s approvals clear. Multiple desks can coexist (different registered addresses for different venues / tiers).

  2. Any registered KYC provider
    Set requiredAuthority to the mothership (address(this)). Several KYC vendors can each be registered; any one valid approval unlocks the fill. Useful when the product requires “some accredited attestor” rather than a single brand.

  3. Authority picks the counterparty (Dutch)
    Taker opens a Dutch auction with makerAddressHash = 0 and a non-zero requiredAuthority. The authority only signs AuthorityApproval with exclusiveFill = exclusiveFillId(approvedMaker, exclusiveNonce) after that maker passes KYC (and whatever book-building rules you run off-chain). Unauthorized makers cannot fill even if they see the auction.

  4. Authority pins the filling taker (FixedPackage)
    Maker posts a FixedPackage with requiredAuthority set. Optional non-zero exclusiveFill pins the KYC’d taker who may submit. Or leave exclusiveFill = 0 and rely on takerAddressHash plus off-chain issuance of approvals only to verified wallets.

  5. metadata as a KYC attestation tag
    Bind policy into the signature without new selectors — e.g. metadata = "kyc:tier1", a jurisdiction code, a session id, or a hash of an off-chain attestation. Fill calldata must replay the same bytes. Frontends and indexers can read AuthorityApprovalUsed (includes metadata) for audit trails. On-chain does not interpret the string; your authority (and product UI) does.

  6. Product surface later
    Future trading / API “KYC modes” can mean: only create Dutch / FixedPackage quotes with a given requiredAuthority, only list makers who can obtain exclusive fills from that authority, and only show books where approvals are available. Leg-by-leg open RFQ (executeIntent) remains permissionless; KYC’d flow routes through authority-gated packages.

Nothing in this section is live as a branded KYC product yet — the primitives are on-chain now so those modes can ship as policy + UI/API without a diamond replace of fill logic.


Quick compare

Leg-by-leg RFQFixedPackageDutch
SignerMakerMakerTaker
FillerTakerTakerMaker
Package premiumNone (bps in PairKey)Fixed absolute dual A/BTime-varying absolute
fillMatched bpsQuoted premiumRateBps00
Resting CLOB legsOptional ClobOffer[]NoNo
AuthorityNoYesYes
EIP-712 domainOpshunsRFQOpshunsFixedPackageOpshunsDutchFill

Integrator checklist

  1. Use the mothership digests from RfqHashFacet / FillHashFacet (digestOpshunIntentRequestLegByLeg, FixedPackage / Dutch / Authority digests) — do not hand-roll EIP-712.
  2. Unique nonces per signer; cancel stale ones on-chain.
  3. Approvals: ERC-20 allowance to the mothership and/or Permit2 witnesses that cover premium plus protocol fee where applicable.
  4. Standard ERC-20 only (no fee-on-transfer / rebasing).
  5. Dutch: set referrer in the signed request (address(0) if none). FixedPackage / leg-by-leg: pass referrer at fill unless the entrypoint binds it in the request.
  6. Authority KYC flows: register the desk, set requiredAuthority, sign metadata consistently, and use exclusive fill when the authority must pick the executor.
  7. Respect facet freeze: fills revert while frozen; nonce cancel and ownership still work.

Source of truth in core: OpshunsMothership/README.md, FILL_AUTHORITY.md.