Building a Confidential OTC Settlement Protocol with Oasis Sapphire & Base
Understanding the Problem
Large cryptocurrency trades rarely happen on decentralized exchanges. When someone wants to swap a few hundred dollars worth of ETH for USDC, an automated market maker like Uniswap works exceptionally well. Liquidity is abundant, settlement is immediate, and the entire process can be completed in a single transaction.
The picture changes completely once the trade size reaches institutional territory. Imagine a DAO treasury planning to exchange 500,000 USDC for ETH. Executing that transaction through a public liquidity pool immediately reveals valuable information to the entire market. Arbitrage bots begin simulating profitable routes before the transaction is finalized. Market makers widen spreads. Searchers compete to extract value from the pending order, while other traders infer the treasury's strategy simply by observing the transaction.
The protocol itself is functioning exactly as designed. The problem is that transparency and negotiation don't always belong together. This is precisely why Over-the-Counter (OTC) trading exists. Rather than exposing an order to an entire market, two counterparties negotiate privately before agreeing on a final settlement price. Only after both parties reach an agreement is the trade executed.
Traditional financial institutions have relied on this model for decades because confidentiality reduces information leakage and allows large transactions to occur without unnecessarily moving markets. Public blockchains introduce an interesting challenge. How do you preserve confidential negotiations while still benefiting from decentralized settlement?
This is where Oasis Sapphire becomes particularly interesting. Instead of treating confidentiality as an application-layer concern, Sapphire provides confidential smart contract execution directly within an EVM-compatible environment. Sensitive contract state remains protected while developers continue writing familiar Solidity code.
In this tutorial we'll build a simplified OTC trading protocol that combines confidential negotiation on Oasis Sapphire with transparent settlement on Base. Rather than building another decentralized exchange, we'll focus on the negotiation process itself.
By the end of this guide you'll have built a protocol capable of:
- Creating confidential OTC trade offers
- Receiving private counterparty quotes
- Accepting offers without revealing negotiations publicly
- Escrowing assets before settlement
- Executing the final settlement on Base
- Cancelling expired orders safely
- Preventing replay attacks through signed messages
Although the protocol we'll build is intentionally simplified for educational purposes, its architecture mirrors patterns commonly found in production financial systems.
Understanding the Problem (Market Impact)
Before writing a single line of Solidity, it's worth asking an important question. Why don't institutions simply use AMMs for everything? After all, decentralized exchanges already provide liquidity, transparent pricing, and permissionless access. The answer comes down to market impact.
Suppose Alice wants to purchase 400 ETH. If that trade is submitted directly to a public liquidity pool, several things happen almost immediately. First, the pending transaction becomes visible to sophisticated searchers monitoring the network. Next, those searchers estimate the price impact by simulating the trade against current liquidity. If profitable opportunities exist, competing transactions are constructed before Alice's order has even been finalized.
Even if nobody explicitly frontruns the transaction, the market itself now possesses information that previously existed only inside Alice's trading strategy. That information has value. The larger the order becomes, the more valuable it becomes.
The sequence below illustrates what happens inside a typical public execution environment.
sequenceDiagram
participant Alice
participant Ethereum
participant Searcher
participant LiquidityPool
Alice->>Ethereum: Submit Swap
Ethereum-->>Searcher: Pending Transaction
Searcher->>Searcher: Simulate Trade
Searcher->>LiquidityPool: Competing Transactions
LiquidityPool-->>Alice: Final Execution
Nothing malicious has occurred. Every participant simply acted upon publicly available information. For large trades, however, that transparency introduces unnecessary information leakage. OTC trading avoids this problem entirely by moving negotiation away from the public market. Only the final agreement becomes visible.
Why Oasis Sapphire?
One obvious question is why we need another blockchain at all. Couldn't we simply negotiate off-chain using encrypted messages? In some situations, yes. However, purely off-chain negotiation introduces several new trust assumptions:
- Who stores the offers?
- Who guarantees neither party modified the terms?
- How do we prove a particular offer existed if negotiations break down?
- How do multiple market makers compete fairly?
A confidential smart contract solves these coordination problems. Instead of trusting a centralized OTC desk, participants submit encrypted offers into a shared protocol. The blockchain enforces protocol rules while keeping sensitive trading information confidential.
From the outside, observers know an offer exists. They do not learn:
- which asset is being traded,
- the quoted price,
- the requested amount,
- acceptable settlement conditions,
- or which counterparty accepted the offer.
Only the final settlement transaction becomes public. That distinction dramatically reduces information leakage while preserving decentralized execution.
System Overview
Our protocol consists of five independent components. Each has a single responsibility. Separating these concerns keeps the contracts significantly easier to audit and makes future upgrades less risky.
flowchart LR
User["Trader"]
Frontend["Next.js Frontend"]
SDK["TypeScript SDK"]
Registry["OTC Registry"]
Escrow["Escrow Contract"]
Coordinator["Settlement Coordinator"]
Base["Base Network"]
User --> Frontend
Frontend --> SDK
SDK --> Registry
Registry --> Coordinator
Coordinator --> Escrow
Escrow --> Base
Let's briefly examine the role of each component.
OTC Registry
The registry acts as the confidential marketplace. Every new offer enters the protocol through this contract. Rather than immediately transferring assets, users first describe the trade they would like to perform. Those details remain confidential until the offer is accepted.
Escrow Contract
Once two counterparties agree on a trade, both assets must be secured before settlement begins. Instead of relying on trust between participants, the escrow contract temporarily locks both sides of the trade. Neither participant can withdraw funds unilaterally after acceptance. Only successful settlement or cancellation releases escrowed assets.
Settlement Coordinator
The final contract coordinates settlement on Base. Once escrow conditions are satisfied, settlement becomes deterministic. This separation keeps negotiation logic isolated from execution logic, significantly simplifying auditing.
Project Architecture
We'll build the project as a monorepo. Although our example remains relatively small, organizing the repository this way closely resembles production Solidity projects.
confidential-otc-protocol/
βββ contracts/
β βββ OTCRegistry.sol
β βββ Escrow.sol
β βββ SettlementCoordinator.sol
β βββ libraries/
β β βββSignatureVerifier.sol
β β βββOfferHasher.sol
βββ sdk/
β βββ otcClient.ts
β βββ signer.ts
β βββ quotes.ts
βββ backend/
β βββ api.ts
β βββ relayer.ts
βββ frontend/
β βββ app/
βββ script/
βββ test/
βββ docs/
Throughout this tutorial we'll implement each directory individually before integrating everything into a complete protocol. By the end, you'll have a repository structured much more like a production codebase than a simple Solidity example.
Protocol Lifecycle
Before diving into the implementation, it's useful to visualize the complete lifecycle of an OTC trade.
stateDiagram-v2
[*] --> Draft
Draft --> Submitted
Submitted --> Quoted
Quoted --> Accepted
Accepted --> Escrowed
Escrowed --> Settled
Submitted --> Cancelled
Quoted --> Cancelled
Accepted --> Expired
Notice that settlement is only one stage of the protocol. Most of the protocol's complexity actually happens before assets move. Negotiation, verification, escrow, and acceptance each introduce their own state transitions. Designing these transitions carefully is one of the easiest ways to eliminate entire classes of smart contract bugs before they ever appear.
In the next section we'll begin implementing the protocol's core contract: OTCRegistry.sol. Rather than jumping directly into business logic, we'll first design the underlying data model and explain why each field exists before writing any functions.
Setting Up the Project
Before writing any Solidity, let's create the project structure. We'll use Foundry because it has become the de facto standard for professional Solidity development. It provides fast compilation, excellent testing utilities, built-in fuzzing, and straightforward deployment scripts.
If you haven't already installed Foundry, you can do so with:
curl -L https://foundry.paradigm.xyz | bash
foundryup
Now create a new project.
forge init confidential-otc-protocol
cd confidential-otc-protocol
We'll also install OpenZeppelin contracts.
forge install OpenZeppelin/openzeppelin-contracts
Our project now looks like this.
confidential-otc-protocol/
βββ lib/
βββ script/
βββ src/
βββ test/
βββ foundry.toml
βββ README.md
Throughout the tutorial we'll gradually replace the default template with our protocol implementation.
Why Foundry?
Hardhat remains an excellent framework, but Foundry offers several advantages for protocol development.
- Tests execute considerably faster because they're written directly in Solidity.
- Fuzz testing is built into the framework instead of requiring external plugins.
- Cheatcodes make it straightforward to simulate different users, timestamps, balances, and blockchain conditions.
Since we're building financial infrastructure, confidence in our testing environment matters just as much as confidence in the contracts themselves.
Designing the Registry
Most blockchain tutorials begin with functions. Production protocols begin with data. The quality of a smart contract is largely determined by how well its state is modeled. Changing storage layouts after deployment is difficult, especially once external integrations exist.
Before writing a single function, we should understand exactly what information our registry needs to remember. An OTC offer answers a simple question: "What is one party willing to trade?" That sounds simple, but the protocol needs considerably more information than just two token addresses.
Our registry must remember:
- who created the offer,
- which asset they're selling,
- which asset they expect,
- the amount offered,
- the minimum amount they'll accept,
- when the offer expires,
- whether it's still active,
- and whether somebody has already accepted it.
Representing these pieces of information explicitly makes later contract logic much easier to understand.
Creating the Offer Model
Inside src/, create a file named OTCRegistry.sol. We'll start with the data model before implementing any business logic.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract OTCRegistry {
enum OfferStatus { Open, Accepted, Cancelled, Settled, Expired }
struct Offer {
address maker;
address tokenOffered;
address tokenRequested;
uint256 amountOffered;
uint256 minimumAmountRequested;
uint256 expiry;
uint256 nonce;
OfferStatus status;
}
}
Although this contract contains only a few lines of Solidity, there's already quite a bit happening. Instead of storing offers as loosely connected mappings, we've grouped related information into a single structure. This makes the protocol easier to reason about because every offer becomes a self-contained object.
Let's examine each field.
Maker address maker;
The maker is the wallet that created the offer. This address becomes important throughout the protocol. Only the maker can: cancel an open offer, update certain parameters (before acceptance), or withdraw escrow if settlement fails. Rather than checking ownership through complex access-control mechanisms, we'll simply compare msg.sender against this stored address.
Offered Token address tokenOffered;
This represents the asset currently owned by the maker. For example, USDC or WETH. The registry itself won't hold these tokens. Instead, it records what the maker intends to escrow later in the protocol. Keeping negotiation separate from asset custody makes the protocol significantly easier to audit.
Requested Token address tokenRequested;
This is the asset the maker expects in return. Together, tokenOffered and tokenRequested define the trading pair. Unlike AMMs, we're not computing prices algorithmically. The maker decides exactly what exchange they're willing to accept.
Offered Amount uint256 amountOffered;
This field specifies how many units of the offered token the maker wishes to trade. For example, 500,000 USDC. The protocol intentionally avoids floating-point arithmetic. Every amount is stored using the token's smallest denomination. For ERC-20 tokens, that typically means accounting for decimals within the frontend or SDK.
Minimum Requested Amount uint256 minimumAmountRequested;
This field acts as protection against unfavorable execution. Suppose Alice wants to exchange 500,000 USDC for at least 165 ETH. If a counterparty proposes 160 ETH, the protocol should reject the trade automatically. Encoding these constraints directly into the offer ensures every accepted trade satisfies the maker's original conditions.
Expiry uint256 expiry;
No financial offer should remain valid forever. Market conditions change. Token prices fluctuate. Liquidity disappears. Rather than leaving offers open indefinitely, we encode an explicit expiration time. After expiry, an offer can no longer be accepted and can be safely cancelled.
Comments
No comments yet. Start the discussion.