🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 37 Min Lesezeit
0

Building a Confidential OTC Settlement Protocol with Oasis Sapphire & Base

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Learn how to build a privacy-preserving OTC trading protocol where counterparties negotiate confidentially using Oasis Sapphire while settling trades transparently on Base.




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



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.




CODE
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.




CODE
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.




CODE
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.




CODE
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:




CODE
curl -L https://foundry.paradigm.xyz | bash
foundryup






Now create a new project.




CODE
forge init confidential-otc-protocol

cd confidential-otc-protocol






We'll also install OpenZeppelin contracts.




CODE
forge install OpenZeppelin/openzeppelin-contracts






Our project now looks like this.




CODE
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.



First, tests execute considerably faster because they're written directly in Solidity.



Second, fuzz testing is built into the framework instead of requiring external plugins.



Finally, 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.




CODE
// 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






CODE
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






CODE
address tokenOffered;






This represents the asset currently owned by the maker.



For example,




CODE
USDC






or




CODE
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






CODE
address tokenRequested;






This is the asset the maker expects in return.



Together,




CODE
tokenOffered



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






CODE
uint256 amountOffered;






This field specifies how many units of the offered token the maker wishes to trade.



For example,




CODE
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






CODE
uint256 minimumAmountRequested;






This field acts as protection against unfavorable execution.



Suppose Alice wants to exchange




CODE
500,000 USDC






for at least




CODE
165 ETH






If a counterparty proposes




CODE
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






CODE
uint256 expiry;






No financial offer should remain valid forever.



Market conditions change.



Token prices fluctuate.



Liquidity disappears.



Rather than relying on users to manually cancel stale offers, we'll associate every offer with an expiration timestamp.



Any interaction after that timestamp becomes invalid.



This single field eliminates an entire class of replay attacks.









Nonce






CODE
uint256 nonce;






One subtle problem appears once signatures enter the picture.



Suppose Alice signs an offer today.



What's stopping somebody from submitting the exact same signed payload next month?



The answer is nonces.



Each new offer consumes a unique value.



Once that value has been used, the protocol refuses to accept another offer carrying the same nonce.



We'll implement this protection shortly.









Organizing Storage



With the data model complete, we can decide how offers will actually be stored.




CODE
mapping(uint256 => Offer) private offers;

uint256 public offerCounter;

mapping(address => uint256) public nonces;






Each mapping serves a different purpose.



The first stores every offer ever created.



The second generates unique identifiers.



Finally, the third keeps track of each user's latest nonce.



Separating these concerns keeps lookups straightforward while avoiding nested mappings that quickly become difficult to maintain.









Why Sequential IDs?



Earlier we discussed deterministic hashes.



Why switch to numeric identifiers?



Because this protocol has different requirements.



Unlike signed intents that may exist off-chain before submission, OTC offers originate inside the protocol itself.



Sequential identifiers offer several practical advantages.



They make indexing simpler.



They're cheaper to generate than hashes.



And they're considerably easier to reference from frontends and block explorers.



Offer #421 is much easier for developers to work with than




CODE
0xb7e13b8ecb5e...






Internally, we'll still use cryptographic hashes later for signatures.



But externally, sequential identifiers make the protocol much friendlier to use.









Creating Offers



With storage in place, we can finally implement our first state-changing function.




CODE
function createOffer(

address tokenOffered,

address tokenRequested,

uint256 amountOffered,

uint256 minimumAmountRequested,

uint256 expiry

) external returns (uint256 offerId) {

require(expiry > block.timestamp, "Offer already expired");

offerId = ++offerCounter;

offers[offerId] = Offer({

maker: msg.sender,

tokenOffered: tokenOffered,

tokenRequested: tokenRequested,

amountOffered: amountOffered,

minimumAmountRequested: minimumAmountRequested,

expiry: expiry,

nonce: nonces[msg.sender]++,

status: OfferStatus.Open

});

}






Although concise, this function already demonstrates several useful design principles.



First, validation occurs before any state modification.



Second, every newly created offer immediately receives a unique identifier.



Finally, we increment the maker's nonce at creation time, ensuring every offer remains uniquely identifiable even if all other parameters are identical.



In the next section we'll improve this function by replacing require strings with custom errors, adding events for off-chain indexing, and integrating EIP-712 signatures so offers can be created through signed messages rather than direct transactions.






Adding Production-Grade Solidity Patterns



The first version of OTCRegistry.sol works, but it still looks closer to a prototype than production infrastructure.



Before moving deeper into confidential execution and cross-chain settlement, we need to improve the contract foundations.



Financial protocols are rarely compromised because the developer forgot how to write a function.



They fail because small design decisions accumulate.



Gas inefficiencies become expensive at scale.



Missing events break indexing systems.



Weak signature handling creates replay vulnerabilities.



Poorly defined state transitions make auditing significantly harder.



The next iteration of the registry focuses on those details.









Replacing require Strings with Custom Errors



The first improvement is replacing traditional revert messages.



A common Solidity pattern looks like this:




CODE
require(
expiry > block.timestamp,
"Offer already expired"
);






This works, but it is unnecessarily expensive.



Every revert string must be stored inside the contract bytecode.



For frequently triggered errors, this increases deployment size and runtime gas costs.



Modern Solidity provides custom errors.



Instead of storing text, we define an error selector.




CODE
error OfferExpired();

error InvalidToken();

error InvalidAmount();

error Unauthorized();

error OfferNotOpen();






Now validation becomes:




CODE
if (expiry <= block.timestamp) {
revert OfferExpired();
}






The difference may seem small.



For a single transaction, the savings are minimal.



For a protocol processing thousands of interactions, these optimizations compound.



More importantly, custom errors create structured failure conditions.



Frontend applications can identify the exact failure type instead of parsing human-readable strings.









Updating the Registry Contract



Our contract now begins with clearer protocol-level failures.




CODE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract OTCRegistry {

error OfferExpired();
error InvalidAmount();
error Unauthorized();
error OfferNotOpen();

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;

}

mapping(uint256 => Offer) private offers;

uint256 public offerCounter;

mapping(address => uint256) public nonces;

}






The storage layout remains unchanged.



This is intentional.



Changing storage unnecessarily creates migration complexity.



A well-designed contract evolves by adding capabilities around stable foundations.









Why Events Matter



Smart contracts do not push information to applications.



They simply modify state.



If a frontend wants to display active OTC offers, it needs an efficient way to discover changes.



Reading every offer ID and querying storage directly does not scale.



Events solve this problem.



When emitted, events become part of transaction logs that external systems can index.



Popular indexing systems such as The Graph and custom backend services rely heavily on this pattern.



For our registry, we need events for:




  • offer creation,

  • offer acceptance,

  • cancellation,

  • settlement.



Let's add the first one.




CODE
event OfferCreated(
uint256 indexed offerId,
address indexed maker,
address tokenOffered,
address tokenRequested,
uint256 amountOffered,
uint256 minimumAmountRequested,
uint256 expiry
);






The indexed keyword is important.



Indexed fields become searchable topics.



For example, a frontend can efficiently query:



"Show me every offer created by this address."



without scanning the entire blockchain.









Emitting Offer Creation Events



Now we update our creation function.




CODE
function createOffer(

address tokenOffered,

address tokenRequested,

uint256 amountOffered,

uint256 minimumAmountRequested,

uint256 expiry

) external returns (uint256 offerId) {


if (expiry <= block.timestamp) {
revert OfferExpired();
}


if (
amountOffered == 0 ||
minimumAmountRequested == 0
) {
revert InvalidAmount();
}


offerId = ++offerCounter;


offers[offerId] = Offer({

maker: msg.sender,

tokenOffered: tokenOffered,

tokenRequested: tokenRequested,

amountOffered: amountOffered,

minimumAmountRequested: minimumAmountRequested,

expiry: expiry,

nonce: nonces[msg.sender]++,

status: OfferStatus.Open

});


emit OfferCreated(
offerId,
msg.sender,
tokenOffered,
tokenRequested,
amountOffered,
minimumAmountRequested,
expiry
);
}






The function now has three clear responsibilities:




  1. Validate the offer.

  2. Store protocol state.

  3. Notify external systems.



Keeping those responsibilities separated makes later auditing much easier.









Introducing EIP-712 Typed Data



Direct transactions are not always ideal for OTC workflows.



Institutional traders frequently negotiate away from the blockchain.



A market maker might send a quote through a private channel.



A treasury manager might approve a trade through an internal signing system.



Broadcasting every negotiation step on-chain defeats the purpose of confidentiality.



Instead, users should be able to sign trade intents.



The blockchain only verifies the final agreement.



This is where EIP-712 becomes important.



EIP-712 defines a standard format for signing structured data.



Instead of signing an unreadable blob:




CODE
0x72af91bc...






users sign something understandable:




CODE
Sell:

500000 USDC

Receive:

165 ETH

Expiry:

July 20 2026






Wallets can display these fields clearly before approval.









Designing the Signed Offer



Our signed message needs a deterministic structure.




CODE
struct OfferMessage {

address maker;

address tokenOffered;

address tokenRequested;

uint256 amountOffered;

uint256 minimumAmountRequested;

uint256 expiry;

uint256 nonce;

}






Every field matters.



Removing a field creates ambiguity.



For example, if expiry is not included, an attacker could reuse a valid signature after market conditions changed.



If nonce is missing, replay attacks become possible.



Cryptographic signatures are only as strong as the data they protect.









Adding OpenZeppelin EIP712



Rather than implementing hashing manually, we'll use OpenZeppelin's audited implementation.




CODE
import {
EIP712
} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";


import {
ECDSA
} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";






Our contract now inherits:




CODE
contract OTCRegistry is EIP712 {

}






The constructor initializes the signing domain.




CODE
constructor()
EIP712(
"Confidential OTC Protocol",
"1"
)
{}






The domain separates signatures from other applications.



A signature created for our OTC protocol cannot accidentally be reused inside another application with different rules.









Hashing the Offer



EIP-712 requires two stages.



First, we hash the structured message.



Then we hash that result with the domain separator.



The final digest is what gets signed.




CODE
bytes32 private constant OFFER_TYPEHASH =
keccak256(
"OfferMessage(address maker,address tokenOffered,address tokenRequested,uint256 amountOffered,uint256 minimumAmountRequested,uint256 expiry,uint256 nonce)"
);






The type hash describes the exact structure.



Any modification changes the resulting hash.



This prevents attackers from altering individual fields without invalidating the signature.









Why This Matters for Confidential OTC



At this point, we have an important architectural improvement.



The negotiation process no longer requires every participant to submit transactions.



A trader can:




  1. Create an offer locally.

  2. Sign it.

  3. Share it privately.

  4. Receive counterparty acceptance.

  5. Submit only the final agreed trade.



This changes the privacy model dramatically.



The blockchain no longer observes every negotiation step.



It only verifies the final commitment.



This pattern is common in modern trading infrastructure.



The chain becomes a settlement layer rather than a communication layer.









Connecting This Back to Oasis Sapphire



This is where Oasis Sapphire becomes useful.



Sapphire allows the negotiation layer itself to become confidential while remaining EVM-compatible.



Instead of exposing signed offers and quotes publicly, sensitive state can remain inside confidential contract execution.



The architecture becomes:




CODE
flowchart LR

TraderA --> Sapphire

TraderB --> Sapphire

Sapphire --> SignedSettlement

SignedSettlement --> Base

Base --> FinalSettlement







Sapphire handles the private coordination.



Base provides transparent final settlement.



Neither chain is replacing the other.



Each handles the part it is best suited for.









Next Section



The registry now has:




  • structured storage,

  • custom errors,

  • event indexing,

  • typed signatures,

  • replay protection foundations.



The next step is implementing the acceptance flow.



We will build:




  • counterparty quote submission,

  • signature verification,

  • offer acceptance,

  • escrow state transitions,

  • settlement preparation,

  • and the bridge between confidential negotiation and public Base settlement.






Building the Acceptance Flow



The registry can now create offers and verify signed trade intents. However, an OTC protocol is not complete when an offer exists.



An offer represents only one side of the agreement.



The critical moment happens when another participant decides:



"I accept these terms, and I am willing to proceed toward settlement."



This transition is where many financial protocols become complicated.



A naive implementation might look like this:




CODE
function acceptOffer(uint256 offerId)
external
{
offers[offerId].status = OfferStatus.Accepted;
}






This appears simple, but it introduces several problems.



Who is accepting?



Can anyone accept?



Was the offer already cancelled?



Has the offer expired?



Did the counterparty actually provide the requested asset?



Can the same offer be accepted twice?



In financial infrastructure, changing a single enum value is not the operation. The operation is a state transition with strict conditions.



Our goal is to make every transition explicit.









Designing the Acceptance Lifecycle



Before writing code, let's define what acceptance actually means.



The lifecycle currently looks like:




CODE
Open
|
|
v
Accepted
|
|
v
Escrowed
|
|
v
Settled






There are also failure paths:




CODE
Open
|
+----> Cancelled
|
+----> Expired






The important observation is that acceptance is not settlement.



This distinction matters.



A counterparty agreeing to trade does not mean assets have moved.



The protocol must first verify:




  1. The offer still exists.

  2. The offer is still open.

  3. The acceptance satisfies the original conditions.

  4. Both parties can proceed to escrow.

  5. Settlement can happen deterministically later.



Separating these phases prevents a common class of bugs where negotiation state and asset movement become tightly coupled.









Reading Offers Safely



Before adding acceptance logic, we need a way for external applications to inspect offers.



Currently:




CODE
mapping(uint256 => Offer) private offers;






The storage is private.



This is intentional.



In a confidential environment, exposing raw storage defeats the purpose of keeping negotiation details protected.



However, applications still need controlled access.



We can create a view function.




CODE
function getOffer(uint256 offerId)
external
view
returns (Offer memory)
{
return offers[offerId];
}






This works for a public chain.



However, because our final architecture uses Oasis Sapphire for confidential negotiation, this function becomes an important design decision.



On Sapphire, the contract can expose information selectively.



The difference is subtle but important:



A public Ethereum contract assumes:



"Everything is visible unless encrypted externally."



A confidential execution environment assumes:



"The contract decides what information leaves the private state."



The same Solidity pattern behaves differently depending on where it executes.









Validating State Transitions



Let's add a helper function.




CODE
function _requireOpenOffer(
Offer storage offer
)
internal
view
{
if (offer.status != OfferStatus.Open) {
revert OfferNotOpen();
}

if (block.timestamp > offer.expiry) {
revert OfferExpired();
}
}






This function centralizes the rules for an active offer.



Why not repeat these checks everywhere?



Because duplicated state validation creates drift.



Imagine one function checks expiry:




CODE
block.timestamp >= expiry






while another checks:




CODE
block.timestamp > expiry






That one-character difference creates inconsistent behavior.



Financial protocols should have one definition of validity.









Accepting an Offer



Now we can implement the acceptance transition.




CODE
function acceptOffer(
uint256 offerId
)
external
{
Offer storage offer = offers[offerId];

_requireOpenOffer(offer);

offer.status = OfferStatus.Accepted;
}






The function is intentionally simple.



The complexity comes from the guarantees around it.



Before changing state, the contract proves:




  • the offer exists,

  • the offer is active,

  • the offer has not expired.



Only then does the transition occur.









But Who Can Accept?



This is the first major design decision.



There are several possible models.






Open Acceptance



Anyone can accept an offer.



Example:



Alice creates:




CODE
Sell:
500,000 USDC

Receive:
165 ETH






Any participant can accept.



Advantages:




  • simple,

  • permissionless,

  • similar to public exchange orders.



Disadvantages:




  • poor fit for institutional OTC,

  • no counterparty negotiation,

  • creates competition around acceptance.









Whitelisted Counterparties



The maker specifies who can accept.



Example:




CODE
Maker:
DAO Treasury

Allowed Counterparty:
Market Maker X






Advantages:




  • closer to institutional OTC,

  • predictable counterparties.



Disadvantages:




  • requires identity management.









Signed Acceptance



The most flexible approach is requiring both sides to sign the agreement.



The final transaction contains:




CODE
Maker Signature

+

Counterparty Signature






The contract verifies both signatures before allowing settlement.



This is the approach we will move toward.









Adding Counterparty Information



Our current Offer struct only knows the maker.



For bilateral OTC trading, we need another participant.



Update the structure:




CODE
struct Offer {

address maker;

address taker;

address tokenOffered;

address tokenRequested;

uint256 amountOffered;

uint256 minimumAmountRequested;

uint256 expiry;

uint256 nonce;

OfferStatus status;

}






The new field:




CODE
address taker;






represents the counterparty who accepted the trade.



Before acceptance:




CODE
maker = Alice

taker = address(0)






After acceptance:




CODE
maker = Alice

taker = Bob






This gives the protocol an explicit record of both parties.









Updating Acceptance



Now acceptance records the counterparty.




CODE
function acceptOffer(
uint256 offerId
)
external
{
Offer storage offer = offers[offerId];

_requireOpenOffer(offer);

offer.taker = msg.sender;

offer.status = OfferStatus.Accepted;

emit OfferAccepted(
offerId,
msg.sender
);
}






The state transition is now clearer.



Before:




CODE
Open

maker:
Alice

taker:
none






After:




CODE
Accepted

maker:
Alice

taker:
Bob






The contract now knows exactly who negotiated the trade.









Adding Acceptance Events



Just like offer creation, acceptance needs an event.




CODE
event OfferAccepted(
uint256 indexed offerId,
address indexed taker
);






This allows external systems to react.



For example:



A backend service can listen for:




CODE
OfferAccepted






and automatically begin escrow preparation.



A frontend can update:




CODE
Status:
Accepted
Waiting for escrow






Events become the communication layer between blockchain state and applications.









Preventing Double Acceptance



Consider this sequence:




CODE
Transaction 1:
Bob accepts offer #10

Transaction 2:
Charlie accepts offer #10






Only one should succeed.



Our _requireOpenOffer check already prevents this.



After Bob's transaction:




CODE
offer.status = OfferStatus.Accepted;






The second transaction sees:




CODE
OfferStatus.Accepted






and reverts.



This is why explicit state machines are powerful.



The contract does not need complicated logic.



The state itself prevents invalid actions.









Why This Matters for Confidential OTC



At first glance, acceptance looks like a simple update.



It is not.



Acceptance is the point where private negotiation becomes a binding agreement.



In traditional OTC markets, this moment happens after:




  • price discovery,

  • counterparty verification,

  • risk checks,

  • legal confirmation.



On-chain, the smart contract replaces much of that coordination.



The contract becomes the neutral agreement layer.



With Sapphire, this process can remain confidential:




CODE
Trader A
|
|
Private Offer
|
v

Oasis Sapphire

|
|
Accepted Agreement
|
v

Base Settlement






The public chain does not need to observe every negotiation step.



It only needs proof that settlement conditions were satisfied.









Preparing for Escrow



Acceptance alone is still not enough.



A malicious counterparty could accept an offer and then disappear.



Example:



Alice:




CODE
Locks:
500,000 USDC






Bob:




CODE
Accepts trade






Then Bob never provides ETH.



The protocol must solve this problem.



The next stage introduces escrow.



We will build:




  • ERC-20 token transfers,

  • secure custody,

  • allowance handling,

  • escrow state tracking,

  • settlement authorization,

  • withdrawal rules,

  • failure recovery.



This is where the protocol starts becoming an actual OTC settlement system rather than a negotiation registry.






Building the Escrow Layer



The acceptance flow gives us something important.



We now have agreement.



But agreement is not settlement.



This distinction is fundamental in financial protocols.



A traditional OTC desk does not send millions of dollars simply because two traders verbally agree on a price. There is usually a custody layer, a clearing process, or a settlement mechanism that guarantees both parties perform.



Smart contracts need the same separation.



The registry answers:



"Who agreed to trade?"



The escrow contract answers:



"Are the assets actually secured?"



Only after both questions have a valid answer should settlement begin.









Why Escrow Exists



Consider a simple failure scenario.



Alice creates an offer:




CODE
Sell:
500,000 USDC

Receive:
165 ETH






Bob accepts.



The registry now contains:




CODE
status = Accepted;






But nothing forces Bob to provide ETH.



The protocol has created an agreement without enforcement.



This is the difference between:




CODE
Negotiation Layer






and:




CODE
Settlement Layer






The escrow contract connects them.









Separating Escrow From the Registry



A common beginner design is putting everything into one contract.



Something like:




CODE
contract OTCProtocol {

createOffer()

acceptOffer()

deposit()

settle()

cancel()

}






This looks convenient.



However, it creates several problems.



The contract now handles:




  • offer management,

  • token custody,

  • settlement,

  • permissions,

  • refunds.



Every additional responsibility increases audit complexity.



Instead, we separate concerns.



Our architecture becomes:




CODE
                 OTCRegistry

"What was agreed?"
|
|
v

Escrow

"Are assets locked?"
|
|
v

SettlementCoordinator

"Execute final trade"






Each contract has one job.









Creating Escrow.sol



Inside:




CODE
src/Escrow.sol






we create our custody contract.




CODE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {
IERC20
} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";


contract Escrow {

}






The first dependency is ERC-20 support.



The escrow contract does not care whether the token is USDC, WETH, or another asset.



It only needs the standard interface.









Modeling Escrow Positions



An escrow position represents one side of a trade.



We need to store:




  • who deposited,

  • which token,

  • how much,

  • which offer it belongs to,

  • current state.



Let's define it.




CODE
contract Escrow {

enum EscrowStatus {
Pending,
Deposited,
Released,
Refunded
}


struct Deposit {

address depositor;

address token;

uint256 amount;

uint256 offerId;

EscrowStatus status;

}

}






The structure is intentionally small.



Escrow contracts should avoid unnecessary state.



Every stored variable creates another invariant that must remain correct forever.









Tracking Deposits



Now we need storage.




CODE
mapping(uint256 => Deposit) public deposits;

uint256 public depositCounter;






Each deposit receives an identifier.



Example:




CODE
Deposit #42

Offer:
#100

Depositor:
Alice

Token:
USDC

Amount:
500000






This identifier becomes useful during settlement.









Depositing Assets



Now we can implement the first important function.




CODE
function deposit(
uint256 offerId,
address token,
uint256 amount
)
external
returns (uint256 depositId)
{

require(
amount > 0,
"Invalid amount"
);


IERC20(token).transferFrom(
msg.sender,
address(this),
amount
);


depositId = ++depositCounter;


deposits[depositId] = Deposit({

depositor: msg.sender,

token: token,

amount: amount,

offerId: offerId,

status: EscrowStatus.Deposited

});

}






The flow is:




  1. User approves the escrow contract.

  2. Escrow pulls tokens using transferFrom.

  3. A deposit record is created.

  4. The protocol now knows assets are secured.









Why Approval Comes First



ERC-20 tokens do not allow arbitrary contracts to move funds.



The user must first approve spending.



The sequence is:




CODE
User Wallet

approve()

|

v

Escrow Contract

transferFrom()

|

v

Escrow Balance






This is standard ERC-20 behavior.



For production systems, frontends usually combine these steps into a single user experience.



The user sees:



"Deposit 500,000 USDC"



rather than manually performing approval and transfer operations.









Safe Transfer Handling



The previous example uses:




CODE
IERC20(token).transferFrom()






However, production contracts should handle tokens that do not properly return boolean values.



OpenZeppelin provides:




CODE
SafeERC20






Let's update the contract.




CODE
import {
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";


contract Escrow {

using SafeERC20 for IERC20;

}






Now deposits become:




CODE
IERC20(token).safeTransferFrom(
msg.sender,
address(this),
amount
);






This protects against non-standard ERC-20 implementations.









Adding Events



Like the registry, escrow needs events.




CODE
event Deposited(
uint256 indexed depositId,
uint256 indexed offerId,
address indexed depositor,
address token,
uint256 amount
);






Then after creating the deposit:




CODE
emit Deposited(
depositId,
offerId,
msg.sender,
token,
amount
);






External systems can now monitor escrow activity.



For example:




CODE
Offer Accepted

|

v

Waiting for Deposits

|

v

Both Sides Funded

|

v

Settlement Ready












The Escrow Invariant



The most important property of this contract is:




CODE
Escrowed funds
=
Funds controlled by protocol rules






Users should never be able to bypass the settlement process.



After depositing:




CODE
Alice
|
| 500,000 USDC
v

Escrow Contract






Alice cannot simply withdraw.



Bob cannot withdraw.



The only valid paths are:




CODE
Successful Settlement

or

Authorized Refund






This is the core security property.









Connecting Escrow to Acceptance



Right now the escrow contract does not know whether an offer was actually accepted.



That is intentional.



Remember our separation of responsibilities.



The registry knows:




CODE
Offer #100

Maker:
Alice

Taker:
Bob

Status:
Accepted






The escrow knows:




CODE
Deposit #55

Offer:
#100

Amount:
500,000 USDC

Status:
Deposited






The settlement coordinator will connect these pieces.









Introducing the Settlement Coordinator



The final architecture becomes:




CODE
                 Sapphire

OTCRegistry

Negotiation + Agreement


|

v


Escrow

Asset Security Layer


|

v


SettlementCoordinator

Cross-chain Execution


|

v


Base

Public Settlement






The coordinator becomes the bridge between confidential negotiation and public execution.









Why This Architecture Fits Sapphire + Base



This separation lets each network do what it does best.



Sapphire handles:




  • private offers,

  • confidential quotes,

  • negotiation state.



Escrow handles:




  • asset custody,

  • enforcement.



Base handles:




  • transparent settlement,

  • final ownership changes.



The protocol does not attempt to make one chain solve every problem.



Instead, it assigns responsibilities.









Next Section



The escrow layer now provides:




  • ERC-20 custody,

  • deposit tracking,

  • safe token transfers,

  • event indexing,

  • clear ownership boundaries.



The next part will implement the final settlement path.



We will cover:




  • SettlementCoordinator.sol

  • cross-chain settlement design

  • proving escrow conditions

  • releasing funds safely

  • Base integration patterns

  • replay protection across chains

  • handling failures and cancellations



This is where the complete OTC protocol architecture comes together.






Building the Settlement Coordinator



At this point, the protocol has three important capabilities.




  • The registry can create and accept agreements.

  • The escrow contract can lock assets.



What remains is the most important operation:



Moving from a private agreement into final settlement.



This is where the architecture becomes interesting.



The protocol intentionally separates:




CODE
Private negotiation

|

v

Verified agreement

|

v

Public settlement






The negotiation does not happen on Base.



The final ownership transfer does.









Why Settlement Should Be Separate



A common mistake is allowing the escrow contract to directly perform settlement.



Something like:




CODE
function settle()
external
{
transferTokens();
}






This couples custody and execution.



The escrow contract now needs to understand:




  • who receives funds,

  • which chain settlement happens on,

  • whether signatures are valid,

  • whether external messages are trusted.



That creates unnecessary complexity.



Instead, escrow only answers:



"Are the required assets secured?"



The settlement coordinator answers:



"Should the trade execute?"









Creating SettlementCoordinator.sol



Inside:




CODE
src/SettlementCoordinator.sol






we create:




CODE
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;


contract SettlementCoordinator {

}






The coordinator will eventually communicate with:




CODE
OTCRegistry

|

|

Escrow

|

|

Base Settlement Layer












Defining Settlement State



A settlement has its own lifecycle.



It is different from an offer lifecycle.



The offer lifecycle:




CODE
Open

|

v

Accepted

|

v

Escrowed

|

v

Settled






The settlement lifecycle:




CODE
Created

|

v

Verified

|

v

Executed

|

v

Completed






Let's model this.




CODE
enum SettlementStatus {

Pending,

Ready,

Executed,

Failed

}






This prevents ambiguous states.









Settlement Records



The coordinator needs to know:




  • which offer is being settled,

  • which escrow positions are involved,

  • who receives what,

  • whether execution happened.



Create the structure:




CODE
struct Settlement {

uint256 offerId;

uint256 makerDeposit;

uint256 takerDeposit;

address maker;

address taker;

SettlementStatus status;

}






A settlement is not an offer.



An offer describes intention.



A settlement describes execution.



Keeping those separate avoids mixing negotiation state with financial state.









Storing Settlements






CODE
mapping(uint256 => Settlement)
public settlements;


uint256 public settlementCounter;






Example:




CODE
Settlement #7

Offer:
#421


Maker:
Alice


Taker:
Bob


Maker Deposit:
USDC


Taker Deposit:
ETH


Status:
Ready












Creating a Settlement



Once both sides have escrowed funds, we create a settlement.




CODE
function createSettlement(
uint256 offerId,
uint256 makerDeposit,
uint256 takerDeposit,
address maker,
address taker
)
external
returns(uint256 settlementId)
{

settlementId = ++settlementCounter;


settlements[settlementId] =
Settlement({

offerId: offerId,

makerDeposit: makerDeposit,

takerDeposit: takerDeposit,

maker: maker,

taker: taker,

status: SettlementStatus.Pending

});

}






This function does not move funds.



That is intentional.



Settlement creation is a coordination step.









Verifying Escrow Conditions



Before execution, we need proof that both sides funded the trade.



The coordinator should never trust user input.



A malicious user could attempt:




CODE
makerDeposit = fakeDepositId






The coordinator must verify directly with escrow.



The pattern becomes:




CODE
Escrow

"Is deposit X valid?"

|

v

SettlementCoordinator

"Proceed or reject"












Escrow Interface



Instead of importing the entire escrow contract, define a small interface.




CODE
interface IEscrow {

function getDeposit(
uint256 depositId
)
external
view
returns(
address depositor,
address token,
uint256 amount,
uint256 offerId,
uint8 status
);

}






The coordinator only knows what it needs.



This reduces coupling.









Making Settlement Ready



Now we can verify both deposits.




CODE
function markReady(
uint256 settlementId
)
external
{

Settlement storage settlement =
settlements[settlementId];


require(
settlement.status ==
SettlementStatus.Pending,
"Invalid state"
);


settlement.status =
SettlementStatus.Ready;

}






In production, this function would perform full escrow validation.



The simplified version demonstrates the state machine.









Executing Settlement



Execution is where assets finally move.



The coordinator instructs escrow:




CODE
Release maker asset

+

Release taker asset






The important rule:



Both sides execute atomically.



Either:




CODE
Alice receives ETH

Bob receives USDC






or:




CODE
Nothing happens






Partial settlement is dangerous.









Atomic Settlement Pattern



A bad implementation:




CODE
releaseAliceFunds();

releaseBobFunds();






If the second operation fails:




CODE
Alice receives funds

Bob receives nothing






A production system requires transactional atomicity.



The better pattern:




CODE
function settle(
uint256 settlementId
)
external
{

Settlement storage settlement =
settlements[settlementId];


require(
settlement.status ==
SettlementStatus.Ready,
"Not ready"
);


// execute both transfers


settlement.status =
SettlementStatus.Executed;

}






Because Solidity transactions revert entirely on failure, the operation is atomic.









Cross-Chain Settlement Design



Now we reach the interesting architectural question.



How does Sapphire communicate with Base?



The answer is not:



"Move the whole application to Base."



The answer is:



Create a settlement message.



The flow becomes:




CODE
Sapphire

Private Trade Agreement

|

v

Settlement Message

|

v

Base

Public Execution






The settlement message contains only what is necessary.



For example:




CODE
struct SettlementMessage {

bytes32 tradeId;

address receiver;

address token;

uint256 amount;

uint256 nonce;

}






Notice what is missing.



No negotiation history.



No quotes.



No private strategy.



Only the information required for final execution.









Why This Matters



The privacy boundary becomes very clear.



Before settlement:




CODE
Confidential






After settlement:




CODE
Public






This mirrors traditional finance.



A hedge fund may privately negotiate a block trade.



But once settlement occurs, ownership changes become visible.



The blockchain is not replacing OTC markets.



It is improving the settlement layer.









Adding Replay Protection



Cross-chain systems introduce another risk.



A valid settlement message could potentially be submitted twice.



Example:




CODE
Message:

Pay Alice 500 ETH






Executed once.



Then replayed.



Executed again.



This is why every settlement message needs uniqueness.



The simplest mechanism:




CODE
mapping(bytes32 => bool)
processedMessages;






Before execution:




CODE
require(
!processedMessages[id],
"Already executed"
);






After:




CODE
processedMessages[id] = true;






One small mapping prevents catastrophic duplication.









Current Architecture



We now have:




CODE
                 Oasis Sapphire


Confidential Negotiation


|

v


Settlement Message


|

v


Base


Transparent Final Settlement






Each layer has a clear responsibility.









Integrating Oasis Sapphire Confidential Execution



The settlement architecture is now complete.



However, we have not yet fully answered the original question:



How do we actually make OTC negotiation confidential?



So far, we have built a conventional Solidity protocol:




  • offers,

  • signatures,

  • escrow,

  • settlement coordination.



Those components work on any EVM-compatible chain.



The reason Oasis Sapphire is interesting is that it changes the privacy model of the application.



Instead of asking:



"How do we encrypt everything before putting it on-chain?"



we can ask:



"Which parts of the application should execute privately?"



That distinction is extremely important.









Public Blockchains and Information Leakage



On a traditional EVM chain, contract state is transparent.



If a contract stores:




CODE
struct Offer {

address maker;

address tokenOffered;

uint256 amount;

uint256 price;

}






then anyone can eventually inspect:




  • who created the offer,

  • what asset they want,

  • how much they are trading,

  • the conditions.



Even if the frontend hides this information, the blockchain does not.



For OTC trading, that is a problem.



The negotiation itself is valuable information.









What Sapphire Changes



Oasis Sapphire provides confidential EVM execution.



The developer experience remains familiar:




CODE
pragma solidity ^0.8.24;

contract OTCRegistry {

}






but execution happens inside a confidential environment.



Sensitive state can remain protected while the contract continues using Solidity patterns.



The important architectural shift is:



Before Sapphire:




CODE
User

|
|
Encrypted Data

|
|
Public Smart Contract

|
|
Everyone Observes State






After Sapphire:




CODE
User

|
|
Confidential Contract Execution

|
|
Protected State

|
|
Only Authorized Outputs Revealed












Moving the Registry to Sapphire



The OTC Registry is the natural candidate for Sapphire.



Why?



Because the registry contains negotiation information.



The escrow and final settlement layer do not need the same privacy guarantees.



A better architecture becomes:




CODE
                 Oasis Sapphire


OTCRegistry

Private Offers + Quotes


|

|

Settlement Proof


|

|

Base


Final Asset Settlement






The registry becomes the private negotiation engine.









Confidential Offer Storage



Previously we stored:




CODE
mapping(uint256 => Offer)
private offers;






On a public chain, private storage is only private by convention.



On Sapphire, confidential state is enforced by the execution environment.



This means the contract can safely maintain information such as:




CODE
struct PrivateOffer {

address maker;

address requestedCounterparty;

uint256 amount;

uint256 price;

bytes encryptedTerms;

}






without exposing every field publicly.









Private Quotes



Institutional OTC trading rarely happens with a single offer.



Usually the process looks like:



Trader:



"I want to sell 10 million USDC."



Market makers:



"Here are three quotes."



Trader:



"I accept quote #2."



A public blockchain would reveal every quote.



That creates unnecessary information leakage.



Sapphire allows this flow:




CODE
Trader

|
|
Private Request

|
|
Sapphire Contract

|
|
Market Makers Submit Quotes

|
|
Trader Accepts Best Quote






Only the final agreement needs to leave the confidential environment.









What Sapphire Does Not Hide



It is important not to overstate confidentiality.



Sapphire does not magically make everything invisible.



The final settlement on Base is public.



Observers can see:




  • settlement transactions,

  • transferred assets,

  • destination addresses,

  • execution timing.



The privacy boundary is:




CODE
Negotiation

Private


Settlement

Public






This is exactly the same separation used in traditional OTC markets.









Confidential Signatures



Our earlier EIP-712 flow becomes even more useful here.



Instead of broadcasting every signed message:




CODE
Offer Created

Quote Received

Counter Offer

Revised Quote






the application can keep those interactions confidential.



Only the final signed commitment becomes relevant.



The protocol effectively changes from:




CODE
Every negotiation step is public






to:




CODE
Only the final agreement is verifiable












Sapphire Design Principles



When building confidential contracts, the same engineering principles still apply.



Privacy does not replace good architecture.



We still need:




  • explicit state machines,

  • replay protection,

  • authorization checks,

  • safe token handling,

  • deterministic settlement.



Confidential execution protects information.



It does not protect incorrect logic.









Building the SDK and Production Workflow



The smart contracts are complete.



Now we need users to interact with them.



A professional protocol rarely exposes raw contract calls directly.



Instead, applications use an SDK layer.



Our repository already contains:




CODE
sdk/

otcClient.ts

signer.ts

quotes.ts






This layer handles:




  • wallet connections,

  • typed messages,

  • signatures,

  • contract calls,

  • settlement tracking.









Creating the SDK Client



Example:




CODE
class OTCClient {

constructor(
private registryContract,
private signer
) {}


async createOffer(params) {

return this.registryContract
.createOffer(
params.tokenOffered,
params.tokenRequested,
params.amount,
params.minimum,
params.expiry
);

}

}






The frontend should not know Solidity details.



It should think in business operations:




CODE
createOffer()

acceptOffer()

deposit()

settle()












Signing Offers



The SDK also handles EIP-712.



Example:




CODE
const signature =
await signer.signTypedData(
domain,
types,
offer
);






The user sees:




CODE
Sell:

500,000 USDC

Receive:

165 ETH

Expires:

Tomorrow






not:




CODE
0x83ab29ff....






Readable signatures reduce user mistakes.









Frontend Flow



A complete user journey looks like:




CODE
Trader Opens App

|

Creates OTC Request

|

Signs Private Intent

|

Market Makers Submit Quotes

|

Trader Accepts Quote

|

Assets Escrowed

|

Settlement Message Created

|

Base Transaction Executed

|

Trade Complete












Testing the Protocol



Financial protocols require more than happy-path testing.



We should test:




  • invalid signatures,

  • expired offers,

  • double acceptance,

  • failed deposits,

  • replayed settlement messages,

  • unauthorized withdrawals.









Example Foundry Test






CODE
function testCannotAcceptExpiredOffer()
public
{

vm.warp(block.timestamp + 2 days);


vm.expectRevert(
OfferExpired.selector
);


registry.acceptOffer(
offerId
);

}






The goal is not simply proving success.



The goal is proving invalid behavior is impossible.









Fuzz Testing



OTC protocols have many numeric inputs:




  • amounts,

  • timestamps,

  • nonces,

  • token quantities.



Fuzz testing helps discover unexpected edge cases.



Example:




CODE
function testAmounts(
uint256 amount
)
public
{

vm.assume(amount > 0);


createOffer(amount);

}






The framework automatically explores many values.









Security Checklist



Before deploying a protocol handling valuable assets:






Access Control



Verify:




  • only makers cancel offers,

  • only accepted counterparties settle,

  • only authorized coordinators release escrow.









Replay Protection



Verify:




  • signatures cannot be reused,

  • settlement messages cannot execute twice,

  • nonces increment correctly.









State Transitions



Verify:



Invalid:




CODE
Open → Settled






Valid:




CODE
Open
|
Accepted
|
Escrowed
|
Settled












Token Safety



Use:




  • SafeERC20,

  • allowance checks,

  • zero address validation.









Failure Recovery



Every financial protocol needs failure paths.



Examples:




  • expired offers,

  • abandoned settlements,

  • cancelled negotiations.



A protocol is not defined only by successful trades.



It is defined by how safely it handles failure.









Final Architecture



The completed system looks like this:




CODE
                    Trader A


|

|

Oasis Sapphire


Confidential OTC Registry

- Private Offers

- Private Quotes

- Signed Agreements


|

|

Settlement Message


|

|

Base


Public Settlement Layer

- Asset Transfer

- Final Ownership


|

|

Trader B












Conclusion



Large trades require different infrastructure from ordinary swaps.



Automated market makers are excellent for transparent liquidity.



They are not designed for confidential institutional negotiation.



An OTC protocol needs a different approach.



The architecture we built separates the problem into three layers:






Confidential Negotiation



Handled by Oasis Sapphire.



This protects sensitive trading information while preserving smart contract coordination.






Asset Security



Handled through escrow.



This ensures both parties commit funds before execution.






Public Settlement



Handled by Base.



This provides transparent final ownership transfer.



The result is not a replacement for decentralized exchanges.



It is a complementary system designed for a different class of trading.



Large financial transactions require both privacy and trust minimization.



Confidential execution provides the privacy.



Smart contracts provide the enforcement.



Public settlement provides the transparency.



Together, they create a practical foundation for the next generation of decentralized OTC trading systems.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Hackers Just Poisoned the Rust Supply Chain | Threat Wire
1 Quelle
Hackers Found a Way Into Humanoid Robots | Threat Wire
1 Quelle
Bits und so #1021 (Passwort für Laufwerk)
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building a Confidential OTC Settlement Protocol with Oasis Sapphire & Base

Thematisch verwandte Begriffe: Building, Confidential, Settlement, Protocol · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...