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 (inventory RFQ)MakerTakerNone (premiumTick = 0)executeIntent
FixedPackageMakerTakerFixed absolute ERC-20 package premiumexecuteFixedPackageIntent
Dutch auctionTakerMakerTime-varying absolute package premiumexecuteDutchIntent
AuthoritySame as Dutch / FixedPackage + authority approvalSame fillerSame*WithAuthority

The trading quote box and options RFQ API use FixedPackage. Inventory executeIntent is zero-premium only. Dutch and Authority are for bots and advanced desks. If you broker matching yourself, start with Settle matched intents with your own RFQ; for book-only liquidity use Call the CLOB directly. TypeScript helpers: TypeScript SDK.

┌─ Inventory RFQ ───► fillMatched (premiumTick = 0, no cash premium)
Maker-signed ──────┤
└─ FixedPackage ────► package premium ─────► fillMatched (zero premium)

Taker-signed ─────── DutchFill ─────────► dutch premium ───────► fillMatched (zero premium)

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

1. FillMatched: maker-signed inventory RFQ​

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

OpshunIntentRequestLegByLeg(OpshunIntentShared[] intents,uint32 makerExpiry,bytes32 takerAddressHash,uint256 makerNonce)

There is no per-leg premium and no ClobOffer. fillMatched always uses premiumTick = 0. The API auction does not use this path; priced quotes are FixedPackage.

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

Settlement steps​

  1. Verify maker EIP-712 signature, makerExpiry, and takerAddressHash.
  2. Consume usedNonces[maker][makerNonce].
  3. Each leg settles with bilateral fillMatched at premiumTick = 0.
  4. Emit MatchedFilled per fill and RfqExecuted(..., isPackage=false, ...).

Directions (NatSpec)​

  • Directions.Buy → taker sells (provides underlying).
  • Directions.Sell → taker buys.
takerAddressHash = keccak256(abi.encode(taker, salt, directionsHash, referrer))

Makers copy that hash from the RFQ. They never see the raw taker address on the public feed. Changing referrer at fill reverts.

When the quote maker is the seller (sellFrom == maker), the underlying protocol fee is half the taker rate. The premium protocol power is unused because the fill is zero-premium. Schedule: Fees.

Notes​

  • No requiredAuthority on this path: authority gating is for Dutch / FixedPackage only.
  • referrer is not its own EIP-712 field. It is inside takerAddressHash.
  • Gas-sponsored quotes set takerAddressHash to bytes32(0). The taker signs TakerGasSponsoredFill (maker, quote, salt, directions, referrer).
  • Cancel unused maker nonces with cancelNonce / cancelNonces (still works while the RFQ facet is frozen).
  • Signatures that still include per-leg premiums do not verify. Re-sign.

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 one or more legs. This is what POST /api/quote asks makers to sign. Legs settle with fillMatched at premiumTick = 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 + referrer that match takerAddressHash (hash(taker, salt, directionsHash, referrer)). referrer is not a FixedPackageRequest field.

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

When makerPays is set for the filled polarity, the package-premium protocol fee is half the taker rate. Package premium has no referral. Underlying on each leg still can. Schedule: Fees. 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 with zero premium (premiumTick = 0).

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)Inside maker-signed takerAddressHash

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​

Inventory RFQFixedPackageDutch
SignerMakerMakerTaker
FillerTakerTakerMaker
Cash premiumNoneFixed absolute dual A/BTime-varying absolute
fillMatched premiumzerozerozero
Resting CLOB slicesNoNoNo
AuthorityNoYesYes
referrerInside takerAddressHashInside takerAddressHashField on DutchFillRequest
EIP-712 domainOpshunsRFQOpshunsFixedPackageOpshunsDutchFill

Integrator checklist​

  1. Use the mothership digests from RfqHashFacet / FillHashFacet. Do not hand-roll EIP-712. In TS: signRfqPackageOffer / signRfqLegByLegOffer / signDutchFillRequest from @opshuns/sdk.
  2. Unique nonces per signer; cancel stale ones on-chain.
  3. Approvals cover the cash premium and/or underlying plus the live protocol fee. Maker-funded legs pay half. Schedule: Fees.
  4. Standard ERC-20 only (no fee-on-transfer / rebasing).
  5. Pass the same referrer that was hashed. Dutch: it is a field on the taker-signed request. FixedPackage and inventory RFQ: it is inside takerAddressHash.
  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.