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.
| Mode | Who signs the request | Who fills (msg.sender) | Premium | Entrypoint |
|---|---|---|---|---|
| FillMatched (leg-by-leg RFQ) | Maker | Taker | Per-leg bps (+ optional CLOB offers) | executeIntent |
| FixedPackage (FillMatched package) | Maker | Taker | Fixed absolute ERC-20 package premium | executeFixedPackageIntent |
| Dutch auction | Taker | Maker | Time-varying absolute package premium | executeDutchIntent |
| Authority | Same as Dutch / FixedPackage + authority approval | Same filler | Same | *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
- Verify maker EIP-712 signature,
makerExpiry, andtakerAddressHash. - Consume
usedNonces[maker][makerNonce]. - 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
fillMatchedat the quotedpremiumRateBps.
- If the quote includes
- Emit
MatchedFilledper fill andRfqExecuted(..., 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
requiredAuthorityon 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
| Field | Role |
|---|---|
intents / tokens | Legs + CLOB token bindings |
dutchPremium | Schedule (DutchPackagePremium) — no auctionEnd inside the premium struct |
auctionEnd | Sole hard deadline (UniswapX-style; no separate takerExpiry) |
makerAddressHash | Maker binding or polarity-only directionsHash on the auctioneer path |
takerNonce | Replay protection |
requiredAuthority | KYC fill-authority gate (0 = none; mothership = any registered) |
requiredDutchAuthority | Auctioneer gate (0 = none; mothership = any registered dutch auctioneer) |
referrer | Taker-bound referral |
Premium schedule (DutchPackagePremium)
| Field | Role |
|---|---|
token | ERC-20 used for the package premium |
makerPaysTaker | Premium direction |
startPremium / endPremium | Absolute amounts at auction start / end |
decayRate | Step per second of elapsed time |
auctionStart | Unix start (end is top-level auctionEnd on the request) |
Pricing (quoteDutchPremium / LibFixedPackageSettle.quoteDutchAmount):
- Reverts if
block.timestamp < auctionStartorauctionEnd < auctionStart. - Elapsed time is capped at
auctionEnd— the quote freezes after the end. - Steps =
min(decayRate * elapsed, |start − end|). - If
startPremium >= endPremiumthe 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
| Path | requiredDutchAuthority | makerAddressHash |
|---|---|---|
| Open / bound maker | address(0) | hash(maker, makerSalt, directions) |
| Auctioneer / API competition | mothership (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:
executeDutchIntentWithAuthorityexecuteFixedPackageIntentWithAuthority
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
| Value | Meaning |
|---|---|
address(0) | Open fill only (executeDutchIntent / executeFixedPackageIntent) |
Mothership (address(this)) | Any registered authority may approve |
| Concrete address | That 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 + authority | FixedPackage + authority | |
|---|---|---|
| Request signer | Taker | Maker |
| Filler | Maker | Taker |
| Counterparty in request | makerAddressHash must be 0 | takerAddressHash binds taker + polarity |
| Who picks the filler | Authority via required non-zero exclusiveFill | Maker already signed; exclusiveFill optional (may pin the taker) |
referrer | In 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:
-
Dedicated KYC authority address
Deploy or operate an EIP-1271 “KYC desk” contract (or EOA behind a custody stack) and register it withsetFillAuthority. Intents setrequiredAuthorityto that address so only that desk’s approvals clear. Multiple desks can coexist (different registered addresses for different venues / tiers). -
Any registered KYC provider
SetrequiredAuthorityto 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. -
Authority picks the counterparty (Dutch)
Taker opens a Dutch auction withmakerAddressHash = 0and a non-zerorequiredAuthority. The authority only signsAuthorityApprovalwithexclusiveFill = 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. -
Authority pins the filling taker (FixedPackage)
Maker posts a FixedPackage withrequiredAuthorityset. Optional non-zeroexclusiveFillpins the KYC’d taker who may submit. Or leaveexclusiveFill = 0and rely ontakerAddressHashplus off-chain issuance of approvals only to verified wallets. -
metadataas 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 readAuthorityApprovalUsed(includesmetadata) for audit trails. On-chain does not interpret the string; your authority (and product UI) does. -
Product surface later
Future trading / API “KYC modes” can mean: only create Dutch / FixedPackage quotes with a givenrequiredAuthority, 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 RFQ | FixedPackage | Dutch | |
|---|---|---|---|
| Signer | Maker | Maker | Taker |
| Filler | Taker | Taker | Maker |
| Package premium | None (bps in PairKey) | Fixed absolute dual A/B | Time-varying absolute |
fillMatched bps | Quoted premiumRateBps | 0 | 0 |
| Resting CLOB legs | Optional ClobOffer[] | No | No |
| Authority | No | Yes | Yes |
| EIP-712 domain | OpshunsRFQ | OpshunsFixedPackage | OpshunsDutchFill |
Integrator checklist
- Use the mothership digests from
RfqHashFacet/FillHashFacet(digestOpshunIntentRequestLegByLeg, FixedPackage / Dutch / Authority digests) — do not hand-roll EIP-712. - Unique nonces per signer; cancel stale ones on-chain.
- Approvals: ERC-20 allowance to the mothership and/or Permit2 witnesses that cover premium plus protocol fee where applicable.
- Standard ERC-20 only (no fee-on-transfer / rebasing).
- Dutch: set
referrerin the signed request (address(0)if none). FixedPackage / leg-by-leg: passreferrerat fill unless the entrypoint binds it in the request. - Authority KYC flows: register the desk, set
requiredAuthority, signmetadataconsistently, and use exclusive fill when the authority must pick the executor. - Respect facet freeze: fills revert while frozen; nonce cancel and ownership still work.
Source of truth in core: OpshunsMothership/README.md, FILL_AUTHORITY.md.