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
- Assemble the exact request object you will POST (normalized addresses, integers).
- Call
digestOpshunIntentRequestLegByLeg(request)on the mothership or use the SDK helper of the same name /signRfqLegByLegOffer. - Sign the 32-byte digest with the maker key (recoverable secp256k1,
vas27/28). - 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
| Field | Notes |
|---|---|
intents | Must match the RFQ byte-for-byte after API normalization |
taker_address_hash | keccak256(abi.encode(taker, salt, directionsHash, referrer)). Copy from the request |
maker_expiry | uint32 unix seconds; must be in the future at fill time |
maker_nonce | Unique per maker; consumed on-chain |
referrer | Not 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, optionalrequiredAuthority)
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(DutchPackagePremiumschedule withauctionStartonly, top-levelauctionEnd, 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.