This document explains how OnChain Labs onboards private market makers (PMMs) to its RFQ routing stack. It covers the end-to-end workflow—from API expectations through smart-contract semantics—so teams can plug into the aggregator without reverse-engineering the contracts or scripts in this repository.
| Term | Description |
|---|---|
| PMM | Private Market Maker that streams bespoke RFQ liquidity through OnChain Labs routing |
| RFQ ID | uint64 identifier embedded in OrderRFQ.rfqId; used for replay protection |
| flagsAndAmount | uint256 passed into fill methods; high bits encode execution flags, low 160 bits encode the desired maker or taker amount |
| Settlement limit | 60% minimum fill ratio enforced against makerAmount and takerAmount to avoid dust fills |
| Permit2 witness | Extra typed data blob signed by makers when using permitWitnessTransferFrom |
| Permit2 witness type | Canonical string describing the custom witness struct (e.g., ExampleWitness witness)ExampleWitness(address user)TokenPermissions(address token,uint256 amount)) |
| WETH unwrap flag | Bit 252 in flagsAndAmount; when set, the maker leg unwraps WETH and sends native ETH |
| Order invalidator | Bitmask stored per maker address to flag consumed or cancelled RFQ IDs |
The OnChain Labs DEX aggregator traditionally relied on AMM liquidity. Large trades experienced slippage and MEV exposure, so RFQ connectivity on EVM chains was prioritized to deliver tighter spreads and deterministic settlement. The Solidity contracts in this repository implement the on-chain leg of that RFQ flow, while off-chain makers expose pricing and signing services.
- v2.0 - Added Permit2-based maker transfers.
- v3.0 (Nov 2025) - Matches the contracts in
src/today: explicitOrderRFQstruct, inline Permit2 signatures + witness data, cancellable RFQ IDs, settlement guardrails, and updated EIP-712 domain"OnChain Labs PMM Protocol". This is the baseline for the current public release.
Private market makers plug into the OnChain Labs DEX aggregator by returning signed RFQ orders that can be filled on-chain via PMMProtocol. The router compares PMM quotes against AMM and CEX-style order books and selects the path with the best expected outcome.
- API activation - Makers expose REST/WebSocket feeds for
/levelsand/order. - Data sync - Once the feeds pass integration tests, the router polls them per chain for live depth.
- Price aggregation - RFQ quotes are merged with AMM books to build an aggregated execution graph.
- Path selection - During quote requests, the router simulates all paths and picks PMM legs when they win on price + gas.
- Quote delivery - Users review the combined quote and request calldata for the selected PMM order.
- Order signature - The maker service signs the
OrderRFQstruct and, if needed, embeds a Permit2 witness signature. - Execution - The taker broadcasts
fillOrderRFQ*along with the maker signature (and optionally a Permit2 signature and/or ERC20 permit for the taker asset). - Settlement flow - Maker funds move directly to the taker (or a specified target). Taker funds move to the maker, optionally wrapping/unwrapping WETH as dictated by the flags.
- Replay protection - RFQ IDs are tracked per maker via bitmask invalidators and can also be cancelled on-chain.
- Deterministic signatures - All fills rely on the
OrderRFQLib.hashhelper and the domain"OnChain Labs PMM Protocol" / v1.0. - Partial fills - Supported through
flagsAndAmount, including maker- or taker-denominated inputs, while enforcing a 60% minimum settlement ratio. - Permit2-native maker leg - Makers can set
usePermit2=trueand optionally ship inline Permit2 signatures + witness metadata; otherwise standardtransferFromis used. - WETH unwrap option - Bit 252 unwraps WETH before forwarding funds, enabling native ETH settlement.
- Taker ERC20 permits -
fillOrderRFQToWithPermitcan execute an ERC20 permit (EIP-2612 or Dai-like) before consuming the order. - Security hardening - Contract inherits
ReentrancyGuard, validates signatures for EOAs or smart-contract signers, and rejects unexpectedmsg.value.
struct OrderRFQ {
uint256 rfqId; // 64-bit ID used for invalidation tracking
uint256 expiry; // Unix timestamp; block.timestamp must be <= expiry
address makerAsset; // Token sent by the maker
address takerAsset; // Token sent by the taker
address makerAddress; // Signer and fund owner
uint256 makerAmount; // Quoted maker size
uint256 takerAmount; // Quoted taker size
bool usePermit2; // Toggles Permit2 transfers on the maker leg
bytes permit2Signature; // Optional inline Permit2 signature (65 bytes if present)
bytes32 permit2Witness; // Packed witness hash when using Permit2 witnesses
string permit2WitnessType;// Canonical witness type string for Permit2
}Field notes:
rfqIdshould fit within 64 bits; higher bits are truncated when the invalidator slot is computed.makerAmount/takerAmountare capped by the settlement limit check (>= 60% when partially filling) and by the Permit2uint160ceiling whenusePermit2is true.permit2Signaturecan be empty if the maker relies on pre-approved allowances with Permit2; if populated, it must encode either a standardpermitTransferFrom(no witness data) or apermitWitnessTransferFrompayload whose witness fields match the values supplied alongside the order.
flagsAndAmount is a compact instruction word used across the fill functions.
| Bit | Constant | Description |
|---|---|---|
| 255 | _MAKER_AMOUNT_FLAG |
Interpret the low bits as a maker-denominated amount; otherwise as taker amount. Zero means "fill the entire order". |
| 254 | _SIGNER_SMART_CONTRACT_HINT |
Set when the maker signature originates from a contract implementing ERC-1271. |
| 253 | _IS_VALID_SIGNATURE_65_BYTES |
When combined with the hint bit, enforces that the calldata signature is exactly 65 bytes. |
| 252 | _UNWRAP_WETH_FLAG |
Unwraps WETH into native ETH before forwarding to the taker/target. |
The lower 160 bits (_AMOUNT_MASK) carry the requested fill amount. If that amount exceeds the quoted maker or taker totals, the fill reverts with RFQ_MakerAmountExceeded or RFQ_TakerAmountExceeded. Amount calculations rely on AmountCalculator.getMakerAmount and AmountCalculator.getTakerAmount:
makerAmount = (takerAmount * orderMakerAmount) / orderTakerAmount
takerAmount = (makerAmount * orderTakerAmount + orderMakerAmount - 1) / orderMakerAmount
// Emitted on every successful fill
event OrderFilledRFQ(
uint256 indexed rfqId,
uint256 expiry,
address indexed makerAsset,
address indexed takerAsset,
address makerAddress,
uint256 expectedMakerAmount,
uint256 expectedTakerAmount,
uint256 filledMakerAmount,
uint256 filledTakerAmount,
bool usePermit2,
bytes permit2Signature,
bytes32 permit2Witness,
string permit2WitnessType
);
// Emitted when makers cancel IDs manually
event OrderCancelledRFQ(uint256 indexed rfqId, address indexed maker);DOMAIN_SEPARATOR()- Returns the current EIP-712 domain separator.invalidatorForOrderRFQ(address maker, uint256 slot)- Exposes the raw 256-bit bitmap for a given maker/slot.isRfqIdUsed(address maker, uint64 rfqId)- Convenience helper to check whether an RFQ ID is consumed.fillOrderRFQ(...)- Fills tomsg.senderusing a standard signature.fillOrderRFQCompact(...)- Accepts a compact signature(r, vs)and optimizes calldata for EOAs.fillOrderRFQTo(...)- Same asfillOrderRFQbut forwards maker funds totarget.fillOrderRFQToWithPermit(...)- Executes an ERC20 permit for the taker asset before calling_fillOrderRFQTo.cancelOrderRFQ(uint64 rfqId)- Allows makers to invalidate quotes on-chain.
All fill variants are nonReentrant, validate signature length hints, and revert with descriptive errors from libraries/Errors.sol when preconditions fail.
- Target check - Zero addresses are rejected via
RFQ_ZeroTargetIsForbidden. - Expiry -
block.timestampmust be <=order.expiry. - Invalidation -
_invalidateOrdersets the relevant bit in the maker's bitmap and fails if already set. - Amount derivation - If the masked amount is zero, the full quote is used. Otherwise the contract derives the complementary side via
AmountCalculatorand enforces upper bounds. - Settlement limit - Both maker and taker sides must be at least 60% of their quoted values (
_SETTLE_LIMIT / _SETTLE_LIMIT_BASE). This blocks dust fills. - Maker leg transfer - If
order.usePermit2is true, the contract either executes a Permit2 transfer with the inline signature (witness-aware) or callstransferFromon Permit2 using prior allowances. Otherwise,SafeERC20.safeTransferFrompulls funds directly from the maker. - WETH unwrap (optional) - When
_UNWRAP_WETH_FLAGis set andmakerAssetequals the configured WETH, funds are sent to the protocol, unwrapped, and forwarded as native ETH with a 5k gas stipend. - Taker leg transfer - If the taker asset is WETH and
msg.value == takerAmount, the protocol wraps the ETH and pushes WETH to the maker. Otherwise, it requiresmsg.value == 0and executessafeTransferFromfrom the taker. - Event emission -
OrderFilledRFQis emitted with expected and actual amounts along with the Permit2 usage flag.
order.usePermit2 switches the maker leg to Uniswap's Permit2 contract (0x000000000022D473030F116dDEE9F6B43aC78BA3). Two options exist:
- Allowance-based - Makers pre-approve Permit2 and leave
permit2Signatureempty. During settlement the contract invokessafeTransferFromPermit2, which enforces theuint160amount ceiling. - Signature-based - Makers embed a signature in
permit2Signature. The protocol buildsIPermit2.PermitTransferFromusingorder.rfqIdas the nonce andorder.expiryas the deadline. Ifpermit2WitnessTypeis non-empty, it callspermitWitnessTransferFromwith the supplied witness hash; otherwise it callspermitTransferFrom.
Witness workflow:
permit2Witnessmust equal the keccak256 hash of the witness data (seescript/signOrderRFQ.js::calculateWitness).permit2WitnessTypemust include the entire custom type string concatenated with the Permit2 base types, exactly as required by Permit2's typed data format.
fillOrderRFQToWithPermit accepts an arbitrary permit blob before calling fillOrderRFQTo. SafeERC20.safePermit auto-detects EIP-2612 (7-word payload) vs Dai-style (8-word payload) permits, enabling gas-efficient taker approvals.
RFQ IDs are tracked via a two-level bitmap:
- Slot index =
rfqId >> 8. - Bit position =
rfqId & 0xff.
cancelOrderRFQ sets the corresponding bit and emits OrderCancelledRFQ. The same helper is used internally when orders are filled. Any attempt to reuse an RFQ ID triggers RFQ_InvalidatedOrder or RFQ_OrderAlreadyCancelledOrUsed.
refer to: https://web3.okx.com/zh-hant/build/dev-docs/wallet-api/dex-api-market-maker
When returning permit2Signature, makers should also provide the witness metadata used to construct the signature. Example witness JSON handed to the signing stack:
{
"witness": {
"user": "0xexampletaker"
},
"witnessTypeString": "ExampleWitness witness)ExampleWitness(address user)TokenPermissions(address token,uint256 amount)"
}The contract hashes permit2Signature and permit2WitnessType internally to remain consistent with OrderRFQLib.hash.
The repository ships script/signOrderRFQ.js, which mirrors the Solidity hashing logic.
import { signOrderRFQ, calculateWitness, WITNESS_TYPE_STRING } from "./script/signOrderRFQ.js";
const order = {
rfqId: 123456789n,
expiry: BigInt(Math.floor(Date.now() / 1000) + 90),
makerAsset: addresses.USDT,
takerAsset: addresses.WETH,
makerAddress: makerWallet.address,
makerAmount: 22_723_800n,
takerAmount: 6_000_000_000_000_000n,
usePermit2: true,
permit2Signature: permitSigHex, // 0x prefixed or "0x"
permit2Witness: calculateWitness({ user: takerAddress }),
permit2WitnessType: WITNESS_TYPE_STRING
};
const signature = await signOrderRFQ({
privateKey: makerPrivateKey,
verifyingContract: pmmProtocolAddress,
chainId: 42161,
order
});The helper also exports signPermit2WithWitness, which produces the Permit2 signature expected in order.permit2Signature.
forge test- Runs the Foundry test-suite (test/), including fork tests for Permit2 witness flows and WETH unwrap scenarios.broadcast/Deploy*.s.sol- Contains deployment artifacts generated via Foundry Scripts; review them before mainnet pushes.
- Document version: v3.1
- Last updated: 2025
- Language: English