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 (inventory RFQ) | Maker | Taker | None (premiumTick = 0) | executeIntent |
| FixedPackage | 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 |
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
- Verify maker EIP-712 signature,
makerExpiry, andtakerAddressHash. - Consume
usedNonces[maker][makerNonce]. - Each leg settles with bilateral
fillMatchedatpremiumTick = 0. - Emit
MatchedFilledper fill andRfqExecuted(..., 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
requiredAuthorityon this path: authority gating is for Dutch / FixedPackage only. referreris not its own EIP-712 field. It is insidetakerAddressHash.- Gas-sponsored quotes set
takerAddressHashtobytes32(0). The taker signsTakerGasSponsoredFill(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
| 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 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
| 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) | 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:
-
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
| Inventory RFQ | FixedPackage | Dutch | |
|---|---|---|---|
| Signer | Maker | Maker | Taker |
| Filler | Taker | Taker | Maker |
| Cash premium | None | Fixed absolute dual A/B | Time-varying absolute |
fillMatched premium | zero | zero | zero |
| Resting CLOB slices | No | No | No |
| Authority | No | Yes | Yes |
referrer | Inside takerAddressHash | Inside takerAddressHash | Field on DutchFillRequest |
| EIP-712 domain | OpshunsRFQ | OpshunsFixedPackage | OpshunsDutchFill |
Integrator checklist
- Use the mothership digests from
RfqHashFacet/FillHashFacet. Do not hand-roll EIP-712. In TS:signRfqPackageOffer/signRfqLegByLegOffer/signDutchFillRequestfrom@opshuns/sdk. - Unique nonces per signer; cancel stale ones on-chain.
- Approvals cover the cash premium and/or underlying plus the live protocol fee. Maker-funded legs pay half. Schedule: Fees.
- Standard ERC-20 only (no fee-on-transfer / rebasing).
- Pass the same
referrerthat was hashed. Dutch: it is a field on the taker-signed request. FixedPackage and inventory RFQ: it is insidetakerAddressHash. - 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.