Skip to main content

EIP-712 signing

Maker, taker (Dutch), and fill-authority offers must recover to the expected address (or pass EIP-1271). The API can verify options RFQ maker signatures against on-chain digests when verify_offer_signatures is true (default outside tests).

Always take digests from the mothership (RfqHashFacet / FillHashFacet) — do not hand-roll typed-data hashing.

Options RFQ — leg-by-leg FillMatched

Domain (Solady EIP-712 on the mothership):

  • Name: OpshunsRFQ
  • Version: 1
  • verifyingContract: mothership address for that chain
  • chainId: target chain

Typed struct: OpshunIntentRequestLegByLeg — intents + directionless premiums (+ optional clob_offers) + makerExpiry + takerAddressHash + makerNonce.

Practical approach

  1. Assemble the exact request object you will POST (normalized addresses, integers).
  2. Call digestOpshunIntentRequestLegByLeg(request).
  3. Sign the 32-byte digest with the maker key (recoverable secp256k1, v as 27/28).
  4. POST { offer_type: "leg_by_leg", request, maker_signature }.

Do not ship placeholder signatures. On-chain executeIntent reverts with InvalidIntentSignature().

Field gotchas

FieldNotes
intentsMust match the RFQ byte-for-byte after API normalization
taker_address_hashCopy from the request event / quote
maker_expiryuint32 unix seconds; must be in the future at fill time
maker_nonceUnique per maker; consumed on-chain
Premium Buy / SellDirectionless; taker directions pick which side applies
clob_offersOptional resting-book slices; each must fill exactly on-chain

Settlement: On-chain fill modes · CLOB.

FixedPackage (maker-signed package)

  • Name: OpshunsFixedPackage
  • Version: 1
  • Typed: FixedPackageRequest (dual A/B absolute package premium, legs, takerAddressHash, makerNonce / makerExpiry, optional requiredAuthority)

Digest via FillHashFacet helpers on the mothership. Fill with executeFixedPackageIntent (or *WithAuthority when gated). Buy/Sell polarity is not in the signed payload — revealed at fill via directions + salt matching takerAddressHash.

Dutch auction (taker-signed package)

  • Name: OpshunsDutchFill
  • Version: 1
  • Typed: DutchFillRequest (DutchPackagePremium schedule with auctionStart only, top-level auctionEnd, legs, makerAddressHash, takerNonce, requiredAuthority, requiredDutchAuthority, referrer)

Taker signs; maker fills via executeDutchIntent / executeDutchIntentWithAuctioneer / executeDutchIntentWithAuthority as appropriate. Premium decays or ramps between startPremium and endPremium from auctionStart until auctionEnd (sole hard deadline — no separate takerExpiry).

Auctioneer competition path (API dutch_only): makerAddressHash = directionsHash(directions), requiredDutchAuthority = mothership.

Fill authority

  • Name: OpshunsFillAuthority
  • Version: 1
  • Typed: AuthorityApproval(fillDigest, exclusiveFill, authorityNonce, metadata)

Required when the Dutch / FixedPackage request sets requiredAuthority != address(0). Authority must be registered (setFillAuthority). Dutch + authority requires non-zero exclusiveFill (authority picks the maker); FixedPackage may use optional exclusive fill to pin the taker.

metadata is opaque bytes hashed into the approval (must match at fill). Use it for off-chain policy tags (e.g. KYC tier). Dutch fillDigest includes the taker-signed referrer; FixedPackage digests do not include referrer (taker passes it at fill).

For KYC-oriented product shapes (dedicated desk, exclusive maker pick, attestation tags), see On-chain fill modes — Future KYC-specific modes.

Details: On-chain fill modes (Authority section).

Borrow RFQ (coming soon)

Borrow is not live. When it ships, lenders will sign SignedLoan under the MarginRFQCore EIP-712 domain (see margin ABIs / MarginRFQTypes), including the RFQ's borrower_hash and the same positions array.

Borrowers would later sign a LoanRequest wrapping the selected loan for borrow. Do not treat this as a production signing path yet.

Local verification

Against Anvil after mix dev.local:

# Example: eth_call digest on mothership, then sign with cast / ethers
cast call $MOTHERSHIP \
"digestOpshunIntentRequestLegByLeg(((address,uint8,uint8,uint32,uint32,uint160,uint256)[],((uint16,(uint16,uint256)[]),(uint16,(uint16,uint256)[]))[],uint32,bytes32,uint256))" \
...

The API fake market maker uses the same digest → ExSecp256k1.sign path for local fills.