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.

In TypeScript, use @opshuns/sdk (../sdk): signRfqLegByLegOffer, signRfqPackageOffer, signDutchFillRequest, signMarginLoanOffer (digest-only for margin). Guide: TypeScript SDK.

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, makerExpiry, takerAddressHash, makerNonce. No per-leg premium and no clob_offers. Inventory fillMatched is zero-premium. The API standard auction does not use this type; it uses FixedPackage below.

Practical approach​

  1. Assemble the exact request object you will POST (normalized addresses, integers).
  2. Call digestOpshunIntentRequestLegByLeg(request) on the mothership or use the SDK helper of the same name / signRfqLegByLegOffer.
  3. Sign the 32-byte digest with the maker key (recoverable secp256k1, v as 27/28).
  4. This digest does not verify signatures that still include directionless premiums. Re-sign after that removal.
import { signRfqLegByLegOffer } from "@opshuns/sdk"

const offer = await signRfqLegByLegOffer(wallet, mothership, chainId, request)

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_hashkeccak256(abi.encode(taker, salt, directionsHash, referrer)). Copy from the request
maker_expiryuint32 unix seconds; must be in the future at fill time
maker_nonceUnique per maker; consumed on-chain
referrerNot its own field. Changing it at fill fails the hash check

takerAddressHash for a gas-sponsored open quote is bytes32(0). The taker then signs TakerGasSponsoredFill, which binds maker, quote, salt, directions, and referrer.

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. The API signs the token-bound digest (digestFixedPackageRequestTokenBound): intents carry token0 / token1, not opshuns6909Contract. Fill with executeFixedPackageIntent (or *WithAuthority when gated). Buy/Sell polarity is not a premium field: revealed at fill via directions + salt. referrer is inside takerAddressHash, not a struct field. SDK: signRfqPackageOffer, then client.placeBid.

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 and inventory RFQ bind referrer inside takerAddressHash instead of a top-level field.

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))" \
...

Or use @opshuns/sdk: digestOpshunIntentRequestLegByLeg / signRfqLegByLegOffer: TypeScript SDK. Example scripts: ../sdk/packages/typescript/examples.

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