Maker quote. Chain fill.
A normal swap asks a pool.
RFQ asks a maker.
An AMM computes execution from reserves. Input dx moves along a curve and returns dy. RFQ starts before the curve. The trade request goes off chain, a maker signs a short-lived quote, and an on-chain contract checks whether that signed quote can still settle.
AMM: taker -> pool curve -> amountOut
RFQ: taker -> maker quote -> signed order -> settlementRFQ moves price discovery off chain. Settlement stays on chain.
That boundary is the design. The taker does not custody funds with the maker. The maker cannot pull assets just by sending a quote. Token movement happens only through a settlement contract, and only when signatures and token permissions line up.
RFQ has two core parties.
Taker consumes the quote. This is often a wallet user, an aggregator contract, or a proxy acting for the final trader.
Maker produces the quote. This is often a market maker, OTC desk, project-side liquidity account, or protocol pool.
Their work is different.
taker:
ask for quote
check output
sign transaction
approve or permit sell token
pay gas in RFQ-T
maker:
price the order
sign quote
keep inventory or pool liquidity
approve settlement if funds stay in maker walletThe maker signs a price commitment. The taker signs execution, or signs a taker order in gasless flows. Permit signs token authorization. These are separate signatures.
An RFQ request can be plain.
chain: Ethereum
sell: 1000 USDC
buy: WETH
taker: 0xUserThat request is normally not a signature. It is an ask for price.
The maker response becomes the verifiable object. The useful fields are the fields that the contract later binds into a hash.
makerToken
takerToken
makerAmount
takerAmount
maker
taker
expiry
nonce or salt
chainId
settlement contract0x exposes the shape clearly in RfqOrder.
/// @dev An RFQ limit order.
struct RfqOrder {
IERC20Token makerToken;
IERC20Token takerToken;
uint128 makerAmount;
uint128 takerAmount;
address maker;
address taker;
address txOrigin;
bytes32 pool;
uint64 expiry;
uint256 salt;
}These fields define price and boundaries.
makerToken is the asset paid by the maker. takerToken is the asset paid by the taker. makerAmount / takerAmount is the quoted exchange ratio. taker can bind the quote to one receiver. expiry limits the lifetime. salt supports cancellation and replay protection.
If calldata changes 1000 USDC -> 0.32 WETH into 1000 USDC -> 0.30 WETH, the signed hash changes as long as the amount fields are included. The maker signature no longer verifies.
The maker quote signature is the center of RFQ.
0x uses an EIP-712 order hash and also supports ETHSIGN. The contract recovers a signer and requires that signer to be the maker or an authorized signer for the maker.
// Signature must be valid for the order.
{
address signer = LibSignature.getSignerOfHash(orderInfo.orderHash, params.signature);
if (signer != params.order.maker && !isValidOrderSigner(params.order.maker, signer)) {
LibNativeOrdersRichErrors
.OrderNotSignedByMakerError(orderInfo.orderHash, signer, params.order.maker)
.rrevert();
}
}Hashflow RFQ-T uses an EIP-191 style maker quote. It is not typed data, but it is still contract-verifiable. The fields are packed, hashed, and then wrapped with the Ethereum Signed Message prefix.
function _hashQuoteRFQT(RFQTQuote memory quote)
private
view
returns (bytes32)
{
return
keccak256(
abi.encodePacked(
'\x19Ethereum Signed Message:\n32',
keccak256(
abi.encodePacked(
address(this),
quote.trader,
quote.effectiveTrader,
quote.externalAccount,
quote.baseToken,
quote.quoteToken,
quote.baseTokenAmount,
quote.quoteTokenAmount,
quote.nonce,
quote.quoteExpiry,
quote.txid,
block.chainid
)
)
)
);
}The pool then recovers the signer and compares it with the configured maker signer.
bytes32 quoteHash = _hashQuoteRFQT(quote);
SignerConfiguration memory signerConfig = signerConfiguration;
require(signerConfig.enabled, 'HashflowPool::tradeRFQT Disabled.');
require(
quoteHash.recover(quote.signature) == signerConfig.signer,
'HashflowPool::tradeRFQT Invalid signer.'
);EIP-191 does not carry field names. The field meaning comes from the contract that builds the digest. The safety check is not the label EIP-191. The safety check is whether token addresses, amounts, trader, chain id, pool or settlement address, expiry, and nonce are all bound into the signed digest.
In RFQ-T, the taker sends the transaction and pays gas.
0x fillRfqOrder uses msg.sender as the taker and returns the maker asset to msg.sender.
function fillRfqOrder(
LibNativeOrder.RfqOrder memory order,
LibSignature.Signature memory signature,
uint128 takerTokenFillAmount
) public returns (uint128 takerTokenFilledAmount, uint128 makerTokenFilledAmount) {
FillNativeOrderResults memory results = _fillRfqOrderPrivate(
FillRfqOrderPrivateParams({
order: order,
signature: signature,
takerTokenFillAmount: takerTokenFillAmount,
taker: msg.sender,
useSelfBalance: false,
recipient: msg.sender
})
);
(takerTokenFilledAmount, makerTokenFilledAmount) = (
results.takerTokenFilledAmount,
results.makerTokenFilledAmount
);
}The private fill path checks the order before settlement:
Only then does settlement run.
On chain, the transfers are not simultaneous. The EVM executes steps in order.
The transaction is still atomic. If a later step fails, earlier state changes and token transfers revert.
0x first clamps the requested fill amount to the remaining fillable amount, then computes output from the signed ratio.
takerTokenFilledAmount = LibSafeMathV06.min128(
settleInfo.takerTokenFillAmount,
settleInfo.takerAmount.safeSub128(settleInfo.takerTokenFilledAmount)
);
// Compute the maker token amount.
// This should never overflow because the values are all clamped to
// (2^128-1).
makerTokenFilledAmount = uint128(
LibMathV06.getPartialAmountFloor(
uint256(takerTokenFilledAmount),
uint256(settleInfo.takerAmount),
uint256(settleInfo.makerAmount)
)
);The contract then transfers both sides.
if (settleInfo.payer == address(this)) {
// Transfer this -> maker.
_transferERC20Tokens(settleInfo.takerToken, settleInfo.maker, takerTokenFilledAmount);
} else {
// Transfer taker -> maker.
_transferERC20TokensFrom(settleInfo.takerToken, settleInfo.payer, settleInfo.maker, takerTokenFilledAmount);
}
// Transfer maker -> recipient.
_transferERC20TokensFrom(settleInfo.makerToken, settleInfo.maker, settleInfo.recipient, makerTokenFilledAmount);There are two quantities.
takerTokenFilledAmount is the amount paid by the taker.
makerTokenFilledAmount is the amount paid by the maker.
Partial fill scales output by the signed ratio.
If the maker balance is insufficient, or if the maker has not approved enough allowance to the settlement contract, the second transferFrom fails. The transaction reverts. The earlier taker transfer reverts too.
AirSwap shows the same P2P settlement pattern more directly. Sender pays signer. Signer pays recipient.
// Transfer token from sender to signer
SafeTransferLib.safeTransferFrom(
senderToken,
msg.sender,
signerWallet,
senderAmount
);
// Transfer token from signer to recipient
SafeTransferLib.safeTransferFrom(
signerToken,
signerWallet,
recipient,
signerAmount
);The contract cannot pull taker funds from nowhere.
ERC-20 transfer permission lives in the token contract. A settlement contract can transfer taker tokens only when allowance exists.
allowance[taker][settlement] >= amount0x calls ERC-20 transferFrom directly in its token spender helper.
/// @dev Transfers ERC20 tokens from `owner` to `to`.
/// @param token The token to spend.
/// @param owner The owner of the tokens.
/// @param to The recipient of the tokens.
/// @param amount The amount of `token` to transfer.
function _transferERC20TokensFrom(IERC20Token token, address owner, address to, uint256 amount) internal {
require(address(token) != address(this), "FixinTokenSpender/CANNOT_INVOKE_SELF");
assembly {
let ptr := mload(0x40) // free memory pointer
// selector for transferFrom(address,address,uint256)
mstore(ptr, 0x23b872dd00000000000000000000000000000000000000000000000000000000)
mstore(add(ptr, 0x04), and(owner, ADDRESS_MASK))
mstore(add(ptr, 0x24), and(to, ADDRESS_MASK))
mstore(add(ptr, 0x44), amount)
let success := call(gas(), and(token, ADDRESS_MASK), 0, ptr, 0x64, ptr, 32)
let rdsize := returndatasize()
// Check for ERC20 success. ERC20 tokens should return a boolean,
// but some don't. We accept 0-length return data as success, or at
// least 32 bytes that starts with a 32-byte boolean true.
success := and(
success, // call itself succeeded
or(
iszero(rdsize), // no return data, or
and(
iszero(lt(rdsize, 32)), // at least 32 bytes
eq(mload(ptr), 1) // starts with uint256(1)
)
)
)
if iszero(success) {
returndatacopy(ptr, 0, rdsize)
revert(ptr, rdsize)
}
}
}There is no special transfer power here. The helper sends transferFrom(owner, to, amount) to the token contract.
The taker side therefore needs prior approve, or a permit path.
Permit turns a token allowance into a signature.
Permit2 checks deadline, amount, nonce, and owner signature. Only then does it transfer the token.
function _permitTransferFrom(
PermitTransferFrom memory permit,
SignatureTransferDetails calldata transferDetails,
address owner,
bytes32 dataHash,
bytes calldata signature
) private {
uint256 requestedAmount = transferDetails.requestedAmount;
if (block.timestamp > permit.deadline) revert SignatureExpired(permit.deadline);
if (requestedAmount > permit.permitted.amount) revert InvalidAmount(permit.permitted.amount);
_useUnorderedNonce(owner, permit.nonce);
signature.verify(_hashTypedData(dataHash), owner);
ERC20(permit.permitted.token).safeTransferFrom(owner, transferDetails.to, requestedAmount);
}A solver or relayer cannot use permit as a blank check. The contract enforces requestedAmount <= permitted.amount, consumes the nonce, and rejects expired signatures.
Permit signs token authorization. It does not sign the RFQ price.
A full RFQ execution can involve three signatures:
maker signature: maker signs the quote
taker tx sig: taker signs the chain transaction
permit sig: taker signs token allowanceGasless and RFQ-M modes add a taker quote signature. The taker is not the transaction sender, but signs the acceptable trade terms. The relayer only submits the signed intent.
RFQ does not require trust in the off-chain display.
The enforceable object is the signed message verified by the settlement contract.
The maker cannot alter a signed price. Changing token, amount, taker, chain id, or settlement address changes the hash and invalidates the signature.
The aggregator cannot pull tokens by itself. It can submit calldata. Token movement still requires settlement or Permit2, plus allowance or permit from the asset owner.
The maker can quote a bad price. RFQ price quality comes from maker competition and aggregator routing, not from the settlement contract.
The maker can refuse to quote. RFQ has availability and access assumptions. AMM liquidity can be hit whenever the pool exists; maker services can reject requests.
The maker can run out of balance or allowance. The result is a revert. No half-settlement remains, although gas can be lost.
A front end can display one trade and submit another. The contract only sees calldata and signatures. The protection surface is wallet simulation, calldata inspection, minimum received, spender address, permit amount, and permit deadline.
RFQ and intent overlap, but they are not the same mechanism.
RFQ-T is:
taker asks
maker signs quote
taker sends transaction
contract settlesIntent execution is:
taker signs constraints
solver finds execution
solver sends transaction
contract checks resultRFQ is about maker pricing. Intent is about taker-signed result constraints.
A solver can satisfy an intent with RFQ, AMM routing, an order book, internal inventory, batch matching, or cross-chain execution. RFQ is a liquidity source. Intent is an authorization and execution model.
When RFQ-M or gasless swap adds a taker-signed order and lets a relayer submit the transaction, the boundary gets thin. The contract must then verify both the maker price commitment and the taker execution authorization.
The decentralized part of RFQ is settlement.
Price discovery is off chain. Makers can refuse quotes. APIs can filter addresses. Aggregators can choose how quotes are ranked. This is not the same openness as a permissionless AMM curve.
Settlement is on chain. Signature, deadline, nonce, amount, recipient, and allowance are contract-verifiable.
The compact model is:
price off chain
commitment by signature
settlement on chain
failure by revertThis is where RFQ escapes the AMM curve.
Large orders do not have to push through x * y = k. A maker can price from CEX liquidity, OTC flow, on-chain inventory, lending positions, and internal risk controls. The chain only answers one question:
Can this signed commitment still execute exactly by its fields?If yes, settlement is atomic.
If no, the transaction reverts.