DEV Community

Typelex
Typelex

Posted on

Building Gas-Efficient OTC Orders With EIP-712: A Typelex Case Study

Building Gas-Efficient OTC Orders With EIP-712: A Typelex Case Study

Creating every OTC order directly onchain is easy to understand.

The maker submits a transaction, the contract stores the order, and another participant fills it later.

But this model has an obvious weakness:

Every quote costs gas, even if nobody accepts it.

For an OTC or RFQ protocol, this can become inefficient very quickly.

A market maker may prepare dozens of short-lived quotes. A DAO may request offers from several counterparties before choosing one. A fund may receive a price that remains valid for only a few minutes.

Writing every quote into blockchain storage is often unnecessary.

A more efficient approach is to:

  1. Create the order offchain.

  2. Sign its structured data.

  3. Send the signed order to the intended counterparty.

  4. Submit it onchain only when the trade is ready to settle.

This article explores that architecture using Typelex as a practical case study.

We will build a simplified fixed-price OTC settlement system using:

  • EIP-712 typed data

  • Offchain maker signatures

  • Onchain signature verification

  • Nonce-based replay protection

  • Restricted taker wallets

  • Atomic ERC-20 settlement

  • Support for EOAs and smart-contract wallets

Important: The code below is educational. It is not production-ready and should not be deployed without extensive testing, independent review, and a professional security audit.


Why storing every quote onchain is inefficient

Consider a traditional order workflow.

The maker calls a function such as:

createOrder(
    sellToken,
    buyToken,
    sellAmount,
    buyAmount,
    taker,
    deadline
);
Enter fullscreen mode Exit fullscreen mode

The contract records the order in storage.

This requires an onchain transaction before anyone can accept the quote.

That may be reasonable for long-lived public orders. It is less efficient for short-lived OTC quotes.

Imagine the following situation:

  • A treasury requests quotes from five market makers.

  • Each market maker prepares a different offer.

  • The treasury accepts only one.

  • The other four quotes expire.

With a fully onchain system, all five quotes may consume gas even though only one becomes a real transaction.

The blockchain does not need to store every unsuccessful negotiation.

It only needs enough information to verify the order that is eventually executed.


Moving order creation offchain

Instead of publishing the order through a transaction, the maker signs a structured message.

A simplified order may look like this:

struct Order {
    address maker;
    address taker;
    address sellToken;
    address buyToken;
    uint256 sellAmount;
    uint256 buyAmount;
    uint256 nonce;
    uint256 deadline;
}
Enter fullscreen mode Exit fullscreen mode

The signature represents the maker’s authorization:

I agree to sell this exact amount of one token in exchange for this exact amount of another token under these conditions.

The signed order may be delivered through:

  • an RFQ interface;

  • a private API;

  • direct communication;

  • an encrypted message;

  • a backend quotation service.

The order reaches the blockchain only when the taker is ready to settle.

The workflow becomes:

Maker creates a quote offchain
              ↓
Maker signs the structured order
              ↓
Taker receives the order and signature
              ↓
Taker submits them to the contract
              ↓
The contract verifies and settles the trade
Enter fullscreen mode Exit fullscreen mode

Unused quotes never create blockchain storage.


Why EIP-712 is useful

A wallet can sign an arbitrary hash, but raw hexadecimal messages are difficult for users to understand.

EIP-712 provides a standard for signing typed structured data.

Instead of showing an unreadable string, the wallet can display meaningful fields such as:

  • sellToken

  • buyToken

  • sellAmount

  • buyAmount

  • taker

  • deadline

The signature domain can also include:

  • the protocol name;

  • the protocol version;

  • the chain ID;

  • the verifying contract.

For Typelex, the frontend domain may look like this:

const domain = {
  name: "Typelex",
  version: "1",
  chainId: 42161,
  verifyingContract: "0xSettlementContract"
};
Enter fullscreen mode Exit fullscreen mode

Each field serves a purpose.

name identifies the signing application.

version separates different versions of the protocol.

chainId helps prevent a signature created on one network from being reused on another.

verifyingContract binds the signature to one specific settlement contract.

However, EIP-712 does not automatically make an order single-use.

For that, the protocol still needs nonces or another replay-protection mechanism.


Defining the Typelex order

We start with the Solidity structure:

struct Order {
    address maker;
    address taker;
    address sellToken;
    address buyToken;
    uint256 sellAmount;
    uint256 buyAmount;
    uint256 nonce;
    uint256 deadline;
}
Enter fullscreen mode Exit fullscreen mode

The corresponding EIP-712 type hash must contain exactly the same fields in exactly the same order:

bytes32 private constant ORDER_TYPEHASH =
    keccak256(
        "Order("
        "address maker,"
        "address taker,"
        "address sellToken,"
        "address buyToken,"
        "uint256 sellAmount,"
        "uint256 buyAmount,"
        "uint256 nonce,"
        "uint256 deadline"
        ")"
    );
Enter fullscreen mode Exit fullscreen mode

This part is easy to underestimate.

A small mismatch between the frontend and Solidity definitions will produce a different digest.

For example, the signature will fail if:

  • the field order is different;

  • one field uses another type;

  • capitalization does not match;

  • the frontend omits a field;

  • the Solidity contract adds a field that the frontend does not sign.

The signature may still be cryptographically valid, but it will not be valid for the message the contract reconstructs.


A simplified Typelex settlement contract

Below is a complete educational example.

// 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 TypelexSignedOrders 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 maker => mapping(uint256 nonce => bool used))
        public nonceUsed;

    event OrderFilled(
        bytes32 indexed orderHash,
        address indexed maker,
        address indexed taker,
        address sellToken,
        address buyToken,
        uint256 sellAmount,
        uint256 buyAmount,
        uint256 nonce
    );

    event NonceCancelled(
        address indexed maker,
        uint256 indexed nonce
    );

    error InvalidAddress();
    error InvalidTokenPair();
    error InvalidAmount();
    error OrderExpired();
    error NonceAlreadyUsed();
    error UnauthorizedTaker();
    error InvalidSignature();

    constructor() EIP712("Typelex", "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 {
        _validateOrder(order);

        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.sellToken,
            order.buyToken,
            order.sellAmount,
            order.buyAmount,
            order.nonce
        );
    }

    function cancelNonce(
        uint256 nonce
    ) external {
        if (nonceUsed[msg.sender][nonce]) {
            revert NonceAlreadyUsed();
        }

        nonceUsed[msg.sender][nonce] = true;

        emit NonceCancelled(msg.sender, nonce);
    }

    function _validateOrder(
        Order calldata order
    ) internal view {
        if (
            order.maker == address(0) ||
            order.sellToken == address(0) ||
            order.buyToken == address(0)
        ) {
            revert InvalidAddress();
        }

        if (order.sellToken == order.buyToken) {
            revert InvalidTokenPair();
        }

        if (
            order.sellAmount == 0 ||
            order.buyAmount == 0
        ) {
            revert InvalidAmount();
        }

        if (block.timestamp > order.deadline) {
            revert OrderExpired();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The contract uses several OpenZeppelin components:

  • EIP712 for typed-data hashing;

  • SignatureChecker for EOA and smart-wallet signature validation;

  • SafeERC20 for safer token transfers;

  • ReentrancyGuard for protection against nested settlement calls.

Now let us break down the execution flow.


Step 1: Validate the order

The contract first rejects obviously invalid parameters.

_validateOrder(order);
Enter fullscreen mode Exit fullscreen mode

The validation checks for:

  • zero maker address;

  • zero token address;

  • identical sell and buy tokens;

  • zero token amounts;

  • expired deadlines.

These checks belong inside the smart contract.

A frontend can perform the same validation to improve user experience, but frontend checks are not security controls.

Anyone can bypass the interface and call the contract directly.


Step 2: Check whether the nonce was used

if (nonceUsed[order.maker][order.nonce]) {
    revert NonceAlreadyUsed();
}
Enter fullscreen mode Exit fullscreen mode

The nonce makes the order single-use.

Each maker has an independent nonce space.

That means:

Maker A, nonce 10
Enter fullscreen mode Exit fullscreen mode

and:

Maker B, nonce 10
Enter fullscreen mode Exit fullscreen mode

are two separate orders.

But Maker A cannot successfully settle nonce 10 twice.

Without this check, the same signed order could potentially be replayed until the maker’s allowance or balance was exhausted.


Step 3: Verify the taker

Some OTC orders may be public.

Others may be intended for one specific wallet.

The contract supports both:

if (
    order.taker != address(0) &&
    order.taker != msg.sender
) {
    revert UnauthorizedTaker();
}
Enter fullscreen mode Exit fullscreen mode

When:

taker = address(0);
Enter fullscreen mode Exit fullscreen mode

any wallet may fill the order.

When taker contains a specific address, only that wallet can execute it.

This is especially important for privately negotiated RFQ quotes.

A maker may offer a special rate to one approved counterparty. Without a taker restriction, anyone who receives the signed order could attempt to execute it.

A restricted taker does not make the order private. It only controls who can fill it.

Blockchain observers may still see the final settlement transaction.


Step 4: Reconstruct the signed digest

The contract hashes the complete order:

bytes32 orderHash = hashOrder(order);
Enter fullscreen mode Exit fullscreen mode

The digest includes:

  • all order fields;

  • the Typelex domain name;

  • the protocol version;

  • the current chain ID;

  • the verifying contract.

This means a taker cannot modify one field and reuse the same signature.

Changing:

buyAmount: 500,000 USDC
Enter fullscreen mode Exit fullscreen mode

to:

buyAmount: 490,000 USDC
Enter fullscreen mode Exit fullscreen mode

produces a different digest.

The maker’s original signature will no longer be valid.


Step 5: Verify the maker’s signature

bool validSignature =
    SignatureChecker.isValidSignatureNow(
        order.maker,
        orderHash,
        signature
    );
Enter fullscreen mode Exit fullscreen mode

The contract asks one question:

Did the maker authorize this exact order?

Using SignatureChecker provides support for two wallet types.

Externally owned accounts

Traditional wallets sign with an ECDSA private key.

Smart-contract wallets

Multisigs and smart accounts may validate signatures through ERC-1271.

This distinction matters because a contract wallet does not necessarily have one private key corresponding directly to its address.

Its authorization may depend on:

  • several owners;

  • a signing threshold;

  • installed modules;

  • custom validation logic.

The signature should therefore be validated during execution, not just once when the order is received by the frontend.


Step 6: Mark the nonce as used

nonceUsed[order.maker][order.nonce] = true;
Enter fullscreen mode Exit fullscreen mode

The contract updates its internal state before calling external token contracts.

This follows the checks-effects-interactions pattern:

  1. Perform checks.

  2. Update state.

  3. Call external contracts.

If a malicious token attempts to reenter fillOrder, the nonce has already been marked as used.

The nonReentrant modifier adds another layer of protection.

If a later token transfer fails, the complete transaction reverts, including the nonce update.

The order is not permanently consumed by a failed settlement.


Step 7: Transfer both assets

The taker sends the requested payment token to the maker:

IERC20(order.buyToken).safeTransferFrom(
    msg.sender,
    order.maker,
    order.buyAmount
);
Enter fullscreen mode Exit fullscreen mode

The maker sends the offered token to the taker:

IERC20(order.sellToken).safeTransferFrom(
    order.maker,
    msg.sender,
    order.sellAmount
);
Enter fullscreen mode Exit fullscreen mode

Both operations happen inside the same transaction.

The settlement has only two valid outcomes:

Both transfers succeed
Enter fullscreen mode Exit fullscreen mode

or:

The entire transaction reverts
Enter fullscreen mode Exit fullscreen mode

This is the atomic property of the exchange.

The maker should not lose tokens without receiving payment.

The taker should not send payment without receiving the purchased asset.


Signing the order with Viem

The frontend may use signTypedData to request the maker’s signature.

import type {
  Address,
  Hex,
  WalletClient
} from "viem";

type TypelexOrder = {
  maker: Address;
  taker: Address;
  sellToken: Address;
  buyToken: Address;
  sellAmount: bigint;
  buyAmount: bigint;
  nonce: bigint;
  deadline: bigint;
};

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;

export async function signTypelexOrder(
  walletClient: WalletClient,
  maker: Address,
  settlementContract: Address,
  chainId: number,
  order: TypelexOrder
): Promise<Hex> {
  if (
    order.maker.toLowerCase() !== maker.toLowerCase()
  ) {
    throw new Error(
      "Connected wallet does not match the order maker"
    );
  }

  if (
    order.sellAmount <= 0n ||
    order.buyAmount <= 0n
  ) {
    throw new Error(
      "Order amounts must be greater than zero"
    );
  }

  const currentTimestamp =
    BigInt(Math.floor(Date.now() / 1000));

  if (order.deadline <= currentTimestamp) {
    throw new Error(
      "Order deadline has already passed"
    );
  }

  return walletClient.signTypedData({
    account: maker,
    domain: {
      name: "Typelex",
      version: "1",
      chainId,
      verifyingContract: settlementContract
    },
    primaryType: "Order",
    types: orderTypes,
    message: order
  });
}
Enter fullscreen mode Exit fullscreen mode

The frontend definitions must match Solidity exactly.

Double-check:

  • Field names

  • Field order

  • Solidity types

  • Protocol name

  • Protocol version

  • Chain ID

  • Verifying contract

A single mismatch creates a different signature digest.


Example order

Imagine that the maker wants to sell 100,000 tokens for 500,000 USDC.

The order is restricted to one counterparty and expires after ten minutes.

const order: TypelexOrder = {
  maker: "0xMakerWallet",
  taker: "0xApprovedTaker",
  sellToken: "0xTokenAddress",
  buyToken: "0xUSDCAddress",
  sellAmount: 100_000n * 10n ** 18n,
  buyAmount: 500_000n * 10n ** 6n,
  nonce: 84n,
  deadline:
    BigInt(Math.floor(Date.now() / 1000)) + 600n
};
Enter fullscreen mode Exit fullscreen mode

Notice the difference in decimals.

The example assumes:

  • the offered token uses 18 decimals;

  • USDC uses 6 decimals.

The contract works with integer units.

It does not understand a human-readable value such as 500,000 USDC unless the frontend converts it correctly.


A valid signature does not guarantee settlement

This point is critical.

A valid signature proves that the maker authorized the order.

It does not prove that the maker can still complete it.

Before settlement, the maker may:

  • transfer the tokens elsewhere;

  • revoke the contract allowance;

  • spend part of the balance;

  • become blacklisted by the token issuer;

  • sign several orders backed by the same funds.

A complete settlement check therefore includes more than signature verification.

Is the signature valid?
Is the nonce unused?
Is the order unexpired?
Is the taker authorized?
Does the maker have enough balance?
Is the allowance sufficient?
Are token transfers currently enabled?
Enter fullscreen mode Exit fullscreen mode

A well-designed interface should simulate the transaction before asking the taker to submit it.

However, the smart contract must remain the final source of truth.


Why the maker needs an allowance

The example uses a non-escrowed model.

The maker keeps the offered tokens until the order is filled.

During settlement, the contract calls:

safeTransferFrom(
    order.maker,
    msg.sender,
    order.sellAmount
);
Enter fullscreen mode Exit fullscreen mode

The maker must approve the settlement contract before execution.

This creates a trade-off.

Advantage

The maker does not need to lock assets when signing a quote.

Disadvantage

The quote may become unfillable if the balance or allowance changes.

A signature is therefore an authorization, not proof that liquidity is reserved.


Replay protection

Without replay protection, the same signed order could be executed repeatedly.

The contract prevents this with:

nonceUsed[maker][nonce] = true;
Enter fullscreen mode Exit fullscreen mode

After a successful fill, the nonce cannot be used again.

The maker can also cancel an unused nonce:

function cancelNonce(
    uint256 nonce
) external {
    if (nonceUsed[msg.sender][nonce]) {
        revert NonceAlreadyUsed();
    }

    nonceUsed[msg.sender][nonce] = true;

    emit NonceCancelled(
        msg.sender,
        nonce
    );
}
Enter fullscreen mode Exit fullscreen mode

This cancellation requires an onchain transaction.

That is one of the central trade-offs of offchain orders:

Creating a quote can be free, but guaranteed cancellation still requires an onchain state update.


Better cancellation models

A production protocol may use a more advanced cancellation system.

Minimum valid nonce

The maker stores a minimum acceptable nonce:

mapping(address => uint256)
    public minValidNonce;
Enter fullscreen mode Exit fullscreen mode

If the maker raises the value from 100 to 200, every order below 200 becomes invalid.

Benefit: Many old orders can be cancelled in one transaction.

Trade-off: Nonce ordering becomes part of the protocol logic.

Nonce bitmap

Nonce states can be grouped into bitmaps.

This may reduce storage costs when large numbers of orders are filled or cancelled.

Benefit: Efficient storage.

Trade-off: More complex implementation and testing.

Order-hash cancellation

The contract stores the hash of each cancelled order.

Benefit: Precise cancellation.

Trade-off: A new storage entry is required for every cancelled order.

Short expiration windows

RFQ orders may expire after a few minutes.

Benefit: Stale quotes disappear quickly.

Trade-off: Expiration does not replace explicit cancellation while the order remains valid.


Smart-wallet signatures can change over time

An EOA signature normally remains valid for the same digest unless the private key is compromised.

A smart-contract wallet behaves differently.

Its validation rules may change after:

  • an owner is removed;

  • the signing threshold changes;

  • a module is disabled;

  • the wallet contract is upgraded;

  • recovery logic is triggered.

A signature that was valid earlier may no longer be valid when settlement occurs.

That is why ERC-1271 signatures should be checked at execution time.


Fixed price and zero slippage

The order defines exact quantities:

100,000 TOKEN
for
500,000 USDC
Enter fullscreen mode Exit fullscreen mode

The contract does not query an AMM pool to calculate the output.

Therefore, the rate does not gradually deteriorate as the order consumes liquidity.

This provides:

Zero execution slippage relative to the signed order terms.

It does not guarantee:

  • the best market price;

  • a fair OTC quote;

  • future token value;

  • stablecoin peg stability;

  • economic profitability.

The smart contract guarantees execution consistency.

It does not evaluate the quality of the negotiation.


Fixed pricing reduces some MEV exposure

A traditional sandwich attack depends on changing the state of a public liquidity pool around a victim’s swap.

The attacker modifies the pool price before the victim’s transaction and trades again afterward.

A signed Typelex order does not use the external pool to determine its output.

A bot cannot change:

500,000 USDC for 100,000 TOKEN
Enter fullscreen mode Exit fullscreen mode

into:

500,000 USDC for 95,000 TOKEN
Enter fullscreen mode Exit fullscreen mode

by trading against a public AMM before settlement.

The order contains exact amounts.

However, this does not mean the transaction is fully MEV-proof.

An observer may still:

  • see the pending transaction;

  • compete for block inclusion;

  • attempt to delay the transaction;

  • identify the participating wallets;

  • trade on another venue;

  • react to later hedging activity.

The architecture protects fixed settlement terms from AMM-based repricing.

It does not provide complete privacy or eliminate every form of MEV.


Fee-on-transfer tokens

The contract assumes that transferring 100 units causes the recipient to receive exactly 100 units.

That assumption is not valid for every token.

A fee-on-transfer token may behave like this:

Requested transfer: 100,000
Recipient receives: 98,000
Enter fullscreen mode Exit fullscreen mode

SafeERC20 helps with inconsistent ERC-20 return values, but it does not guarantee that the recipient received the nominal amount.

Possible protocol policies include:

  • Allowlisting reviewed tokens

  • Rejecting fee-on-transfer assets

  • Checking balances before and after transfers

  • Using token-specific adapters

  • Defining settlement by actual received amounts

Balance checks are also imperfect for rebasing tokens.

Token compatibility should therefore be a documented protocol decision.


Approval risk

Many applications ask users for unlimited token approvals.

That improves convenience, but it also increases the consequences of a contract vulnerability.

Safer alternatives include:

  • approving only the amount required for one order;

  • using permit-based approvals when available;

  • displaying active allowances in the interface;

  • separating settlement contracts by version;

  • resetting allowances after use.

There is no universal best option.

The protocol must balance:

  • user experience;

  • gas cost;

  • contract complexity;

  • approval exposure.


Overcommitted orders

A maker may sign several orders using the same balance.

For example:

Maker balance: 1,000,000 TOKEN

Order A: Sell 1,000,000 TOKEN
Order B: Sell 1,000,000 TOKEN
Order C: Sell 1,000,000 TOKEN
Enter fullscreen mode Exit fullscreen mode

All three signatures may be valid.

But after Order A executes, Orders B and C may fail because the maker no longer has enough tokens.

Offchain signatures do not reserve inventory.

Possible solutions include:

  • using escrow;

  • creating very short-lived RFQ quotes;

  • exposing outstanding order liabilities;

  • limiting active nonce ranges;

  • accepting that signed quotes are not guaranteed liquidity.


Escrowed versus non-escrowed orders

Both approaches have advantages.

Non-escrowed signed orders

The maker signs offchain and keeps the assets until settlement.

Advantages:

  • no transaction is required to create an order;

  • unused quotes cost no gas;

  • funds remain available until execution;

  • the model works well for RFQ systems.

Trade-offs:

  • the maker may lose balance or allowance;

  • several orders may compete for the same assets;

  • the taker has less certainty that the order is fillable.

Escrowed orders

The maker deposits tokens into a smart contract before settlement.

Advantages:

  • the assets are reserved;

  • the taker has stronger execution certainty;

  • overcommitting the same balance becomes harder.

Trade-offs:

  • order creation requires gas;

  • funds remain locked;

  • unused orders still create onchain state;

  • escrow-contract risk becomes more important.

A hybrid protocol could support both models.

Short-lived RFQ quotes may use signatures, while large strategic transactions may use isolated escrow.


Security tests worth writing

A production implementation needs more than a few unit tests.

It should include:

  • unit tests;

  • fuzz testing;

  • integration tests;

  • invariant testing;

  • adversarial token mocks.

At minimum, the following properties should be tested.

The same nonce cannot execute twice

First fill: succeeds
Second fill: reverts
Enter fullscreen mode Exit fullscreen mode

A cancelled nonce cannot execute

After cancellation, every signature using that maker and nonce must fail.

An expired order cannot execute

Test:

  • one second before expiration;

  • exactly at expiration;

  • one second after expiration.

The contract should clearly define its boundary behavior.

Changing any field invalidates the signature

Modify each field independently:

  • maker;

  • taker;

  • sell token;

  • buy token;

  • sell amount;

  • buy amount;

  • nonce;

  • deadline.

Every modified order must fail verification.

A signature cannot be replayed on another contract

Deploy two settlement contracts.

A signature created for Contract A should fail on Contract B.

A signature cannot be replayed on another chain

A different chain ID should produce a different digest.

Only the approved taker can fill a private order

Possessing the signed order must not be enough for an unauthorized wallet.

Failed transfers restore the nonce state

If either token transfer fails, the entire transaction should revert and the nonce should remain unused.

Reentrancy cannot settle the order twice

Tests should use malicious token contracts capable of making callback attempts.

Smart-wallet signatures work correctly

Test ERC-1271 validation and changes to wallet configuration.


What Typelex gains from signed orders

This architecture offers several practical benefits.

Lower cost for unused quotes

Quotes that expire without being filled never create onchain storage.

Faster RFQ workflows

Market makers can create quotes without waiting for a transaction to confirm.

Controlled distribution

A signed order can be sent directly to one intended counterparty.

Fixed settlement conditions

The signature commits the maker to:

  • exact token addresses;

  • exact quantities;

  • one nonce;

  • one deadline;

  • an optional taker wallet.

Flexible execution

The taker or an approved relayer can submit the final transaction.

Broader wallet support

The system can support:

  • ordinary wallets;

  • multisigs;

  • smart accounts;

  • contract-based treasuries.

These advantages do not remove the need for:

  • correct pricing;

  • sufficient balances;

  • token allowances;

  • replay protection;

  • token compatibility policies;

  • contract security.


Final thoughts

An onchain OTC protocol does not need to publish every quote to the blockchain.

EIP-712 allows Typelex to represent an order as structured data, collect the maker’s authorization offchain, and verify it only when settlement happens.

The full flow is:

Define the order terms
          ↓
Sign typed data offchain
          ↓
Send the order to the taker
          ↓
Verify the signature onchain
          ↓
Check nonce, deadline, and wallet
          ↓
Transfer both assets atomically
Enter fullscreen mode Exit fullscreen mode

The signature itself is not the hardest part.

The difficult work is designing the complete order lifecycle:

  • Replay protection

  • Cancellation

  • Wallet restrictions

  • Allowance management

  • Smart-account support

  • Stale quote handling

  • Overcommitted balances

  • Nonstandard token behavior

  • Atomic state transitions

Typelex can use this architecture to separate two responsibilities.

Counterparties or RFQ systems determine the economic terms.

The smart contract verifies authorization and enforces settlement.

That separation can make OTC execution more gas-efficient without turning every offchain quote into a trusted manual exchange.


Disclosure: This article was prepared with AI assistance and reviewed before publication.

Top comments (0)