Designing an Escrow Contract for a Crypto Marketplace
The problem the contract has to solve
Crypto is a great store of value and a terrible medium of exchange for physical goods. The failure mode is the same every time: a buyer sends funds, the seller either ships late, ships something else, or vanishes — and there is no authority that can reverse the transfer. Centralized marketplaces paper over this with chargebacks and dispute queues, but that requires custody, KYC, and a customer-support org. None of that is cheap, and all of it re-introduces the middleman that crypto users are trying to leave behind.
The brief for CryptoRabbit was narrower: build an escrow that sits between buyer and seller, holds funds until the order is resolved, and gives the platform no ability to take the money. Everything else in the product — listings, search, notifications — is ordinary web software. The interesting part is the contract.
Why an on-chain, non-custodial escrow
We considered three models:
- Platform wallet. The marketplace holds funds and releases them on order completion. Simplest to build, but it makes us a money-services business in most jurisdictions and a single point of failure for a hack.
- Multisig. Funds held in a 2-of-3 with buyer, seller, and platform as signers. Better than custody, but the platform key is still a target and the UX of "please sign this to release" leaks complexity into the product.
- Non-custodial escrow contract. Funds enter an on-chain escrow at checkout and can only exit to the buyer or the seller along rules the contract enforces. The platform never signs a value-moving transaction.
We picked option 3. The principle we wanted to hold onto was simple: the platform should not be able to take user money, even if it wants to. That rules out a custody database entry pretending to be a ledger.
The state machine
Every order maps to one escrow instance with a finite-state machine. The states that matter:
Funded— buyer has deposited; seller has not acknowledged.Accepted— seller has committed to fulfill; the shipment clock starts.Shipped— seller has posted a tracking reference.Delivered— carrier confirmed delivery (or buyer confirmed manually).Released— funds have moved to the seller.Refunded— funds have moved back to the buyer.Disputed— a contested path that branches to eitherReleasedorRefunded.
Transitions are gated by either a signed action from one of the two parties or a timeout. The contract does not know what "fair" means; it knows what the two parties agreed to when the buyer checked out.
Timeouts are the whole design
The single most important lesson: in an escrow, timeouts are not a safety net, they are the design. Every state that can stall needs a default resolution after a known duration. A seller who goes dark after Funded should not freeze the buyer's money; a buyer who forgets to confirm delivery should not freeze the seller's. The durations are parameters, not constants — different categories of goods need different windows — but every state has one, and every timeout has a default party it resolves in favor of.
Dispute resolution, narrowly scoped
The Disputed path is where pragmatism meets decentralization. A fully trustless dispute resolution system (a la Kleros) is a research problem on its own; we did not want to ship one. Instead:
- Either party can raise a dispute before the delivery-confirmation timeout.
- The contract pauses timeouts and emits a
Disputedevent. - Resolution is signed by an arbitration address that is separate from the platform's operating keys and, critically, can be replaced by a DAO or a third-party arbiter without contract migration.
The platform acts as arbiter today. The design does not require that it always will.
Ethereum vs. Solana: why both
We support both chains, and the architectural split is worth naming explicitly:
- Ethereum (Solidity). Better fit where buyers and sellers already live in an EVM wallet, where ERC-20 stablecoin settlement is the norm, and where predictable, cheap L2 fees (Base, Arbitrum, Optimism) make small-ticket goods viable. Tooling — Foundry, Hardhat, the OpenZeppelin primitives — is mature.
- Solana (Anchor). Better fit where the UX demands near-instant finality and sub-cent fees — ticketing, in-game items, high-frequency small transactions. Account-model differences (each escrow is a PDA with explicit rent) change how you think about storage cost, but the happy-path UX is hard to beat.
The contract surface is intentionally the same on both chains: same state machine, same events, same resolver roles. The indexer and the product code do not need to care which chain a particular order settled on.
What we left out on purpose
The first version does not do partial refunds, split shipments, or seller-reputation staking. Each of those is a legitimate feature, and each one adds branches to the state machine. We held the line on scope so the audit surface stayed small and the mental model for both parties stayed obvious: funds go in, funds come out one way or the other, and a clock is always running.
Takeaway for teams building their own
If you are designing something in this shape, the questions worth answering before you write any Solidity or Anchor code are:
- Who can move the money, under what conditions, and can you prove the platform is not in that set?
- What happens if each party does nothing? Every state needs that answer.
- Who resolves disputes, and can that role be swapped out without migrating the contract?
- Which chain actually matches the UX — not which chain sounds best in a pitch?
Everything else is implementation detail. These four decisions are the product.

Written by Roman Khrystynych, founder of Khrystynych Innovations Inc — an AI and Web3 consultancy specializing in multimodal RAG, AI automation, AI training, and smart contract engineering on Ethereum and Solana.
Have a project in mind? Let's talk.