Building Gas-Efficient OTC Orders With EIP-712: A Cabledex Case Study
Creating every OTC order directly onchain is simple, but not always efficient.
A maker publishes an order, the contract stores it, and the counterparty fills it later. The problem is that every order consumes gas, including quotes that expire or are never accepted.
For an RFQ-based protocol, many quotes may exist for only a few minutes.
A more efficient model is:
Create the order offchain.
Sign the order using EIP-712.
Send it to the intended counterparty.
Submit it onchain only when settlement is ready.
This article uses Cabledex as a practical example of fixed-price OTC settlement with offchain signatures.
We will cover:
EIP-712 typed orders
Signature verification
Nonce-based replay protection
Restricted taker wallets
Atomic ERC-20 settlement
Note: The following code is educational and should not be used in production without extensive testing and an independent security audit.
Why move quotes offchain?
Imagine that a DAO requests quotes from five market makers.
Only one quote is accepted.
If every quote is created onchain, all five require gas even though four of them are never used.
With signed offchain orders, only the accepted quote reaches the blockchain.
The workflow becomes:
Maker creates a quote
↓
Maker signs the order
↓
Taker receives the signed quote
↓
Taker submits it onchain
↓
The contract verifies and settles
This is particularly useful for short-lived RFQ orders.
Defining the order
A simplified Cabledex order may contain:
struct Order {
address maker;
address taker;
address sellToken;
address buyToken;
uint256 sellAmount;
uint256 buyAmount;
uint256 nonce;
uint256 deadline;
}
The fields specify:
Maker: the wallet offering the asset
Taker: the wallet allowed to fill the order
Sell token: the asset offered by the maker
Buy token: the asset requested in return
Amounts: the exact quantities being exchanged
Nonce: a unique order identifier
Deadline: the expiration timestamp
The order does not need to be stored onchain when it is created.
The maker signs these parameters instead.
Why EIP-712?
EIP-712 allows wallets to sign structured data rather than an unreadable hexadecimal message.
The signing domain can bind the order to:
the Cabledex protocol;
a specific protocol version;
one blockchain network;
one settlement contract.
A frontend domain may look like this:
const domain = {
name: "Cabledex",
version: "1",
chainId: 42161,
verifyingContract: "0xSettlementContract"
};
This reduces the risk of using the same signature in a different context.
However, EIP-712 does not automatically prevent replay attacks. The settlement contract still needs to track used nonces.
Simplified settlement contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {
EIP712
} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {
SignatureChecker
} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol";
import {
IERC20
} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {
ReentrancyGuard
} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract CabledexSignedOrders is EIP712, ReentrancyGuard {
using SafeERC20 for IERC20;
struct Order {
address maker;
address taker;
address sellToken;
address buyToken;
uint256 sellAmount;
uint256 buyAmount;
uint256 nonce;
uint256 deadline;
}
bytes32 private constant ORDER_TYPEHASH =
keccak256(
"Order("
"address maker,"
"address taker,"
"address sellToken,"
"address buyToken,"
"uint256 sellAmount,"
"uint256 buyAmount,"
"uint256 nonce,"
"uint256 deadline"
")"
);
mapping(address => mapping(uint256 => bool))
public nonceUsed;
event OrderFilled(
bytes32 indexed orderHash,
address indexed maker,
address indexed taker,
uint256 nonce
);
error InvalidOrder();
error OrderExpired();
error NonceAlreadyUsed();
error UnauthorizedTaker();
error InvalidSignature();
constructor() EIP712("Cabledex", "1") {}
function hashOrder(
Order calldata order
) public view returns (bytes32) {
bytes32 structHash = keccak256(
abi.encode(
ORDER_TYPEHASH,
order.maker,
order.taker,
order.sellToken,
order.buyToken,
order.sellAmount,
order.buyAmount,
order.nonce,
order.deadline
)
);
return _hashTypedDataV4(structHash);
}
function fillOrder(
Order calldata order,
bytes calldata signature
) external nonReentrant {
if (
order.maker == address(0) ||
order.sellToken == address(0) ||
order.buyToken == address(0) ||
order.sellToken == order.buyToken ||
order.sellAmount == 0 ||
order.buyAmount == 0
) {
revert InvalidOrder();
}
if (block.timestamp > order.deadline) {
revert OrderExpired();
}
if (nonceUsed[order.maker][order.nonce]) {
revert NonceAlreadyUsed();
}
if (
order.taker != address(0) &&
order.taker != msg.sender
) {
revert UnauthorizedTaker();
}
bytes32 orderHash = hashOrder(order);
bool validSignature =
SignatureChecker.isValidSignatureNow(
order.maker,
orderHash,
signature
);
if (!validSignature) {
revert InvalidSignature();
}
nonceUsed[order.maker][order.nonce] = true;
IERC20(order.buyToken).safeTransferFrom(
msg.sender,
order.maker,
order.buyAmount
);
IERC20(order.sellToken).safeTransferFrom(
order.maker,
msg.sender,
order.sellAmount
);
emit OrderFilled(
orderHash,
order.maker,
msg.sender,
order.nonce
);
}
function cancelNonce(
uint256 nonce
) external {
if (nonceUsed[msg.sender][nonce]) {
revert NonceAlreadyUsed();
}
nonceUsed[msg.sender][nonce] = true;
}
}
This example uses:
EIP712to construct the typed-data digestSignatureCheckerto support ordinary and smart-contract walletsSafeERC20for safer token transfersReentrancyGuardto block nested settlement calls
How settlement works
When the taker calls fillOrder, the contract performs several checks.
1. Validate the order
The contract rejects invalid token addresses, identical token pairs, zero amounts, and expired orders.
Frontend validation may improve user experience, but the contract must enforce the actual rules.
2. Check the nonce
if (nonceUsed[order.maker][order.nonce]) {
revert NonceAlreadyUsed();
}
A successful order marks its nonce as used.
This prevents the same signature from being executed multiple times.
3. Verify the taker
if (
order.taker != address(0) &&
order.taker != msg.sender
) {
revert UnauthorizedTaker();
}
When taker is the zero address, the order is public.
When it contains a specific wallet, only that wallet may fill the order.
This is useful for privately negotiated OTC quotes.
4. Verify the signature
The contract reconstructs the order hash and checks whether the maker authorized it.
Changing any signed field invalidates the signature.
A taker cannot secretly change:
500,000 USDC
to:
490,000 USDC
while using the original signature.
5. Transfer both assets
The payment token moves from the taker to the maker.
The offered token moves from the maker to the taker.
Both transfers occur inside one transaction.
The result is:
Both sides settle, or the complete transaction reverts.
Signing the order with Viem
A frontend may create the signature using signTypedData.
const orderTypes = {
Order: [
{ name: "maker", type: "address" },
{ name: "taker", type: "address" },
{ name: "sellToken", type: "address" },
{ name: "buyToken", type: "address" },
{ name: "sellAmount", type: "uint256" },
{ name: "buyAmount", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
} as const;
const signature = await walletClient.signTypedData({
account: maker,
domain: {
name: "Cabledex",
version: "1",
chainId,
verifyingContract: settlementContract
},
primaryType: "Order",
types: orderTypes,
message: order
});
The frontend and Solidity definitions must match exactly.
Check:
Field names
Field order
Field types
Protocol name and version
Chain ID
Settlement contract address
Any mismatch will produce a different digest.
A signature does not reserve liquidity
The maker keeps the offered tokens until the trade executes.
That makes order creation cheaper, but it creates an important limitation.
Before settlement, the maker may:
spend the tokens;
revoke the allowance;
transfer the balance elsewhere;
sign several orders using the same funds.
A valid signature proves authorization.
It does not prove that the maker still has enough balance and allowance.
A Cabledex interface should therefore simulate the transaction before the taker submits it.
Fixed price and execution slippage
The order contains exact quantities.
For example:
Maker sends: 100,000 TOKEN
Taker sends: 500,000 USDC
The contract does not calculate the price through an AMM pool.
This means the trade has:
No execution slippage relative to the signed order.
However, this does not guarantee that the quote is fair or remains attractive until expiration.
The external market may move after the maker signs the order.
Cabledex enforces the agreed terms. It does not evaluate the economic quality of the deal.
What about MEV?
A standard sandwich attack depends on changing an AMM pool before and after a user’s swap.
A fixed-price Cabledex order does not use the pool to determine its output.
A bot cannot change the signed quantities by moving an external AMM price.
Still, this should not be described as complete MEV protection.
An observer may still:
detect the pending settlement;
compete for block inclusion;
delay the transaction;
identify the participating wallets;
trade on another venue.
The model protects the fixed execution terms, not the privacy of the transaction.
Important token edge cases
Not every ERC-20 token behaves normally.
A production protocol must consider:
fee-on-transfer tokens;
rebasing assets;
pausable tokens;
blacklisting;
upgradeable token contracts;
nonstandard return values.
For example, a transfer may request 100,000 units while the recipient receives only 98,000.
Possible solutions include:
supporting only reviewed tokens;
rejecting fee-on-transfer assets;
checking balance changes;
using token-specific adapters.
Token compatibility should be a documented protocol policy.
Signed orders versus escrow
Signed orders
The maker keeps the assets until execution.
Benefits:
No gas cost for unused quotes
No locked assets
Fast RFQ workflow
Trade-offs:
Balance or allowance may disappear
Several quotes may use the same funds
Escrowed orders
The maker deposits the offered asset first.
Benefits:
Liquidity is reserved
Greater certainty for the taker
Trade-offs:
Order creation costs gas
Funds remain locked
Unused orders still create onchain state
Cabledex could use signed orders for short-lived quotes and escrow for larger strategic trades.
Security properties to test
A production version should verify that:
A nonce cannot execute twice
A cancelled order cannot execute
An expired order fails
Changing any field invalidates the signature
Only the approved taker can fill a private order
A signature cannot be replayed on another contract or chain
Failed transfers revert the nonce update
Reentrancy cannot settle an order twice
Smart-contract wallet signatures work correctly
Tests should include malicious token contracts, not only standard ERC-20 mocks.
Final thoughts
EIP-712 allows Cabledex to keep OTC quote creation offchain while preserving onchain verification and settlement.
The complete flow is straightforward:
Create the order
↓
Sign it offchain
↓
Send it to the taker
↓
Verify it onchain
↓
Settle both assets atomically
The main benefit is not simply lower gas usage.
It is the ability to separate two responsibilities:
Counterparties determine the price and trade conditions.
The smart contract verifies authorization and executes settlement.
This model can make OTC and RFQ workflows more efficient, but it still requires careful handling of nonces, allowances, token behavior, signatures, and contract security.
Disclosure: This article was prepared with AI assistance and reviewed before publication.
Top comments (0)