Smart contracts have revolutionized how we think about digital agreements and transactions. Today, we'll dive deep into building a practical escrow marketplace smart contract—a system that enables secure transactions between buyers and sellers without requiring trust in a central authority.
A smart contract is a program that runs over a blockchain network, originally proposed by cryptographer Nick Szabo in 1994. Think of it as a digital agreement that automatically executes when predetermined conditions are met, without requiring intermediaries.
Once deployed, smart contracts operate independently without human intervention. The code is the law—if conditions A, B, and C are met, action X will automatically execute.
All smart contract code and transactions are visible on the blockchain, providing complete transparency to all participants.
After deployment, smart contracts cannot be changed (unless specifically designed with upgrade mechanisms), ensuring that the rules remain constant.
Smart contracts run on a decentralized network of computers, eliminating single points of failure and reducing the need for trusted intermediaries.
Before diving into code, let's understand what an escrow system does:
Traditional Escrow : A neutral third party holds funds or assets until contractual obligations are met by all parties involved.
Smart Contract Escrow : The blockchain itself acts as the neutral third party, automatically releasing funds when conditions are satisfied.
Reduced fees : No traditional escrow agent commissions
Faster settlements : Automated execution upon condition fulfillment
Global accessibility : Anyone with an internet connection can participate
Transparency : All parties can verify the contract logic and state
Immutable rules : Terms cannot be changed unilaterally
Foundry is a modern, fast Rust-based toolkit for Ethereum application development. It provides everything we need for smart contract development, testing, and deployment.
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
# Verify installation
forge --version
cast --version
anvil --version
# Create new Foundry project
forge init escrow-marketplace
cd escrow-marketplace
# Project structure
# ├── src/ # Smart contracts
# ├── test/ # Test files
# ├── script/ # Deployment scripts
# ├── foundry.toml # Configuration
# └── lib/ # Dependencies
Our escrow marketplace will consist of several key components:
Item Listings : Sellers can list items for sale
Purchase Orders : Buyers can create purchase orders with escrowed funds
Dispute Resolution : Mechanism for handling conflicts
Fee Management : Marketplace fees and distribution
Complete escrow marketplace transaction flow showing interactions between Seller, Buyer, Escrow smart contract, and Contract owner. The diagram illustrates the full lifecycle including item listing, fund placement, delivery confirmation, dispute resolution, and fee distribution.
Escrow purchase state machine: transactions flow from Pending (funds escrowed) through various states. The happy path is Pending → Delivered (funds released). Alternative paths handle disputes and cancellations, ensuring trustless transactions between parties.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19 ;
contract EscrowMarketplace {
// Marketplace owner
address public owner;
// Fee percentage (in basis points, e.g., 250 = 2.5%)
uint256 public feePercentage;
// Item counter for unique IDs
uint256 public nextItemId;
// Purchase counter for unique IDs
uint256 public nextPurchaseId;
// Item states
enum ItemStatus { Active , Sold , Cancelled }
// Purchase states
enum PurchaseStatus {
Pending , // Funds escrowed, awaiting delivery
Delivered , // Buyer confirmed delivery
Disputed , // Dispute raised
Resolved , // Dispute resolved
Cancelled // Purchase cancelled
}
// Item structure
struct Item {
uint256 id;
address seller;
string title;
string description;
uint256 price;
ItemStatus status;
uint256 createdAt;
}
// Purchase structure
struct Purchase {
uint256 id;
uint256 itemId;
address buyer;
address seller;
uint256 amount;
uint256 fee;
PurchaseStatus status;
uint256 createdAt;
uint256 deliveryDeadline;
}
// Storage mappings
mapping ( uint256 => Item) public items;
mapping ( uint256 => Purchase) public purchases;
mapping ( address => uint256 []) public userItems;
mapping ( address => uint256 []) public userPurchases;
}
// Events for frontend integration
event ItemListed ( uint256 indexed itemId , address indexed seller , uint256 price );
event PurchaseCreated ( uint256 indexed purchaseId , uint256 indexed itemId , address indexed buyer );
event DeliveryConfirmed ( uint256 indexed purchaseId );
event DisputeRaised ( uint256 indexed purchaseId , address indexed raiser );
event DisputeResolved ( uint256 indexed purchaseId , address winner );
// Constructor
constructor ( uint256 _feePercentage) {
owner = msg.sender ;
feePercentage = _feePercentage;
nextItemId = 1 ;
nextPurchaseId = 1 ;
}
// Modifiers
modifier onlyOwner () {
require ( msg.sender == owner, "Only owner can call this function" );
_ ;
}
modifier onlyBuyer ( uint256 _purchaseId) {
require ( msg.sender == purchases[_purchaseId].buyer, "Only buyer can call this" );
_ ;
}
modifier onlySeller ( uint256 _purchaseId) {
require ( msg.sender == purchases[_purchaseId].seller, "Only seller can call this" );
_ ;
}
modifier purchaseExists ( uint256 _purchaseId) {
require (_purchaseId < nextPurchaseId && _purchaseId > 0 , "Purchase does not exist" );
_ ;
}
function listItem (
string memory _title,
string memory _description,
uint256 _price
) external returns ( uint256 ) {
require ( bytes (_title).length > 0 , "Title cannot be empty" );
require (_price > 0 , "Price must be greater than 0" );
uint256 itemId = nextItemId;
items[itemId] = Item ({
id : itemId,
seller : msg.sender ,
title : _title,
description : _description,
price : _price,
status : ItemStatus.Active,
createdAt : block .timestamp
});
userItems[ msg.sender ]. push (itemId);
nextItemId ++ ;
emit ItemListed (itemId, msg.sender , _price);
return itemId;
}
function cancelItemListing ( uint256 _itemId) external {
Item storage item = items[_itemId];
require (item.seller == msg.sender , "Only seller can cancel" );
require (item.status == ItemStatus.Active, "Item not active" );
item.status = ItemStatus.Cancelled;
}
function purchaseItem ( uint256 _itemId) external payable returns ( uint256 ) {
Item storage item = items[_itemId];
require (item.status == ItemStatus.Active, "Item not available" );
require (item.seller != msg.sender , "Cannot buy your own item" );
require ( msg .value >= item.price, "Insufficient payment" );
// Calculate fees
uint256 fee = (item.price * feePercentage) / 10000 ;
uint256 totalRequired = item.price + fee;
require ( msg .value >= totalRequired, "Insufficient payment including fees" );
// Create purchase order
uint256 purchaseId = nextPurchaseId;
purchases[purchaseId] = Purchase ({
id : purchaseId,
itemId : _itemId,
buyer : msg.sender ,
seller : item.seller,
amount : item.price,
fee : fee,
status : PurchaseStatus.Pending,
createdAt : block .timestamp,
deliveryDeadline : block .timestamp + 7 days // 7 day delivery window
});
// Update item status
item.status = ItemStatus.Sold;
// Track user purchases
userPurchases[ msg.sender ]. push (purchaseId);
// Refund excess payment
if ( msg .value > totalRequired) {
payable ( msg.sender ). transfer ( msg .value - totalRequired);
}
nextPurchaseId ++ ;
emit PurchaseCreated (purchaseId, _itemId, msg.sender );
return purchaseId;
}
function confirmDelivery ( uint256 _purchaseId)
external
onlyBuyer ( _purchaseId )
purchaseExists ( _purchaseId )
{
Purchase storage purchase = purchases[_purchaseId];
require (purchase.status == PurchaseStatus.Pending, "Purchase not pending" );
purchase.status = PurchaseStatus.Delivered;
// Release funds to seller
payable (purchase.seller). transfer (purchase.amount);
// Send fee to marketplace owner
if (purchase.fee > 0 ) {
payable (owner). transfer (purchase.fee);
}
emit DeliveryConfirmed (_purchaseId);
}
This function demonstrates the core value of escrow: trustless fund distribution based on delivery confirmation.
Fund flow sequence: buyer's funds are held in escrow until delivery is confirmed. Upon confirmation, funds are atomically distributed to the seller and marketplace owner. The dispute resolution path shows how conflicts are handled through owner intervention.
function raiseDispute ( uint256 _purchaseId)
external
purchaseExists ( _purchaseId )
{
Purchase storage purchase = purchases[_purchaseId];
require (
msg.sender == purchase.buyer || msg.sender == purchase.seller,
"Only buyer or seller can raise dispute"
);
require (purchase.status == PurchaseStatus.Pending, "Can only dispute pending purchases" );
require ( block .timestamp <= purchase.deliveryDeadline, "Delivery deadline passed" );
purchase.status = PurchaseStatus.Disputed;
emit DisputeRaised (_purchaseId, msg.sender );
}
function resolveDispute ( uint256 _purchaseId, bool _favorBuyer)
external
onlyOwner
purchaseExists ( _purchaseId )
{
Purchase storage purchase = purchases[_purchaseId];
require (purchase.status == PurchaseStatus.Disputed, "Purchase not disputed" );
purchase.status = PurchaseStatus.Resolved;
if (_favorBuyer) {
// Refund buyer (minus marketplace fee for handling dispute)
payable (purchase.buyer). transfer (purchase.amount);
payable (owner). transfer (purchase.fee);
emit DisputeResolved (_purchaseId, purchase.buyer);
} else {
// Pay seller
payable (purchase.seller). transfer (purchase.amount);
payable (owner). transfer (purchase.fee);
emit DisputeResolved (_purchaseId, purchase.seller);
}
}
function claimRefund ( uint256 _purchaseId)
external
onlyBuyer ( _purchaseId )
purchaseExists ( _purchaseId )
{
Purchase storage purchase = purchases[_purchaseId];
require (purchase.status == PurchaseStatus.Pending, "Purchase not pending" );
require ( block .timestamp > purchase.deliveryDeadline, "Delivery deadline not passed" );
purchase.status = PurchaseStatus.Cancelled;
// Refund buyer (keep marketplace fee)
payable (purchase.buyer). transfer (purchase.amount);
payable (owner). transfer (purchase.fee);
}
Testing is crucial for smart contracts. Here's our testing approach using Foundry:
// test/EscrowMarketplace.t.sol
pragma solidity ^0.8.19 ;
import "forge-std/Test.sol" ;
import "../src/EscrowMarketplace.sol" ;
contract EscrowMarketplaceTest is Test {
EscrowMarketplace marketplace;
address owner = address ( 0x1 );
address seller = address ( 0x2 );
address buyer = address ( 0x3 );
uint256 constant ITEM_PRICE = 1 ether ;
uint256 constant FEE_PERCENTAGE = 250 ; // 2.5%
function setUp () public {
vm. startPrank (owner);
marketplace = new EscrowMarketplace (FEE_PERCENTAGE);
vm. stopPrank ();
// Give test accounts some ETH
vm. deal (seller, 10 ether );
vm. deal (buyer, 10 ether );
}
function testListItem () public {
vm. startPrank (seller);
uint256 itemId = marketplace. listItem ( "Test Item" , "Description" , ITEM_PRICE);
( uint256 id, address itemSeller, string memory title, , uint256 price, , ) =
marketplace. items (itemId);
assertEq (id, 1 );
assertEq (itemSeller, seller);
assertEq (price, ITEM_PRICE);
assertEq (title, "Test Item" );
vm. stopPrank ();
}
function testPurchaseItem () public {
// Seller lists item
vm. startPrank (seller);
uint256 itemId = marketplace. listItem ( "Test Item" , "Description" , ITEM_PRICE);
vm. stopPrank ();
// Calculate total payment including fees
uint256 fee = (ITEM_PRICE * FEE_PERCENTAGE) / 10000 ;
uint256 totalPayment = ITEM_PRICE + fee;
// Buyer purchases item
vm. startPrank (buyer);
uint256 purchaseId = marketplace.purchaseItem{value : totalPayment}(itemId);
( uint256 id, uint256 purchaseItemId, address purchaseBuyer, , , , , , ) =
marketplace. purchases (purchaseId);
assertEq (id, 1 );
assertEq (purchaseItemId, itemId);
assertEq (purchaseBuyer, buyer);
vm. stopPrank ();
}
function testConfirmDelivery () public {
// Setup: List and purchase item
vm. startPrank (seller);
uint256 itemId = marketplace. listItem ( "Test Item" , "Description" , ITEM_PRICE);
vm. stopPrank ();
uint256 fee = (ITEM_PRICE * FEE_PERCENTAGE) / 10000 ;
uint256 totalPayment = ITEM_PRICE + fee;
vm. startPrank (buyer);
uint256 purchaseId = marketplace.purchaseItem{value : totalPayment}(itemId);
vm. stopPrank ();
// Record balances before delivery confirmation
uint256 sellerBalanceBefore = seller.balance;
uint256 ownerBalanceBefore = owner.balance;
// Confirm delivery
vm. startPrank (buyer);
marketplace. confirmDelivery (purchaseId);
vm. stopPrank ();
// Verify payments
assertEq (seller.balance, sellerBalanceBefore + ITEM_PRICE);
assertEq (owner.balance, ownerBalanceBefore + fee);
// Verify purchase status
(, , , , , , EscrowMarketplace.PurchaseStatus status, , ) =
marketplace. purchases (purchaseId);
assertEq ( uint256 (status), uint256 (EscrowMarketplace.PurchaseStatus.Delivered));
}
function testDisputeResolution () public {
// Setup purchase
vm. startPrank (seller);
uint256 itemId = marketplace. listItem ( "Test Item" , "Description" , ITEM_PRICE);
vm. stopPrank ();
uint256 fee = (ITEM_PRICE * FEE_PERCENTAGE) / 10000 ;
uint256 totalPayment = ITEM_PRICE + fee;
vm. startPrank (buyer);
uint256 purchaseId = marketplace.purchaseItem{value : totalPayment}(itemId);
// Raise dispute
marketplace. raiseDispute (purchaseId);
vm. stopPrank ();
// Owner resolves in favor of buyer
vm. startPrank (owner);
marketplace. resolveDispute (purchaseId, true );
vm. stopPrank ();
// Verify refund
(, , , , , , EscrowMarketplace.PurchaseStatus status, , ) =
marketplace. purchases (purchaseId);
assertEq ( uint256 (status), uint256 (EscrowMarketplace.PurchaseStatus.Resolved));
}
function testRefundOnLateDelivery () public {
// Setup purchase
vm. startPrank (seller);
uint256 itemId = marketplace. listItem ( "Test Item" , "Description" , ITEM_PRICE);
vm. stopPrank ();
uint256 fee = (ITEM_PRICE * FEE_PERCENTAGE) / 10000 ;
uint256 totalPayment = ITEM_PRICE + fee;
vm. startPrank (buyer);
uint256 purchaseId = marketplace.purchaseItem{value : totalPayment}(itemId);
vm. stopPrank ();
uint256 buyerBalanceBefore = buyer.balance;
// Fast forward past delivery deadline
vm. warp ( block .timestamp + 8 days );
// Claim refund
vm. startPrank (buyer);
marketplace. claimRefund (purchaseId);
vm. stopPrank ();
// Verify refund (buyer gets item price back, marketplace keeps fee)
assertEq (buyer.balance, buyerBalanceBefore + ITEM_PRICE);
}
}
# Run all tests
forge test
# Run specific test with verbose output
forge test --match-test testPurchaseItem -vvv
# Test with gas reporting
forge test --gas-report
# Generate coverage report
forge coverage
// Add to main contract
mapping ( uint256 => Bid[]) public itemBids;
struct Bid {
address bidder;
uint256 amount;
uint256 timestamp;
}
function placeBid ( uint256 _itemId) external payable {
Item storage item = items[_itemId];
require (item.status == ItemStatus.Active, "Item not active" );
require ( msg.sender != item.seller, "Seller cannot bid" );
require ( msg .value > 0 , "Bid must be greater than 0" );
// Check if bid is higher than current highest
Bid[] storage bids = itemBids[_itemId];
if (bids.length > 0 ) {
require ( msg .value > bids[bids.length - 1 ].amount, "Bid too low" );
}
// Store previous bidder funds for refund
if (bids.length > 0 ) {
Bid storage previousBid = bids[bids.length - 1 ];
payable (previousBid.bidder). transfer (previousBid.amount);
}
bids. push ( Bid ({
bidder : msg.sender ,
amount : msg .value,
timestamp : block .timestamp
}));
}
mapping ( address => UserReputation) public reputations;
struct UserReputation {
uint256 totalSales;
uint256 totalPurchases;
uint256 successfulTransactions;
uint256 disputesLost;
uint256 rating; // Out of 10000 (100.00%)
}
function updateReputation ( address _user, bool _positive) internal {
UserReputation storage rep = reputations[_user];
if (_positive) {
rep.successfulTransactions ++ ;
} else {
rep.disputesLost ++ ;
}
// Calculate new rating
uint256 total = rep.successfulTransactions + rep.disputesLost;
if (total > 0 ) {
rep.rating = (rep.successfulTransactions * 10000 ) / total;
}
}
import "@openzeppelin/contracts/security/ReentrancyGuard.sol" ;
contract EscrowMarketplace is ReentrancyGuard {
// Add nonReentrant modifier to functions that transfer ETH
function confirmDelivery ( uint256 _purchaseId)
external
nonReentrant
onlyBuyer ( _purchaseId )
purchaseExists ( _purchaseId )
{
// Implementation...
}
}
function listItem (
string memory _title,
string memory _description,
uint256 _price
) external returns ( uint256 ) {
require ( bytes (_title).length > 0 && bytes (_title).length <= 100 , "Invalid title length" );
require ( bytes (_description).length <= 1000 , "Description too long" );
require (_price > 0 && _price <= 1000 ether , "Invalid price range" );
// Implementation...
}
import "@openzeppelin/contracts/access/AccessControl.sol" ;
contract EscrowMarketplace is AccessControl {
bytes32 public constant ADMIN_ROLE = keccak256 ( "ADMIN_ROLE" );
bytes32 public constant MODERATOR_ROLE = keccak256 ( "MODERATOR_ROLE" );
constructor ( uint256 _feePercentage) {
_grantRole (DEFAULT_ADMIN_ROLE, msg.sender );
_grantRole (ADMIN_ROLE, msg.sender );
// Implementation...
}
function resolveDispute ( uint256 _purchaseId, bool _favorBuyer)
external
onlyRole ( MODERATOR_ROLE )
{
// Implementation...
}
}
// script/Deploy.s.sol
pragma solidity ^0.8.19 ;
import "forge-std/Script.sol" ;
import "../src/EscrowMarketplace.sol" ;
contract DeployEscrowMarketplace is Script {
function run () external {
uint256 deployerPrivateKey = vm. envUint ( "PRIVATE_KEY" );
vm. startBroadcast (deployerPrivateKey);
EscrowMarketplace marketplace = new EscrowMarketplace ( 250 ); // 2.5% fee
console. log ( "EscrowMarketplace deployed to:" , address (marketplace));
vm. stopBroadcast ();
}
}
// Web3 integration example
import { ethers } from 'ethers' ;
const contractABI = [ ... ]; // Contract ABI
const contractAddress = '0x...' ;
async function purchaseItem ( itemId , itemPrice , fee ) {
const provider = new ethers.providers. Web3Provider (window.ethereum);
const signer = provider. getSigner ();
const contract = new ethers. Contract (contractAddress, contractABI, signer);
try {
const totalPayment = ethers.utils. parseEther ((itemPrice + fee). toString ());
const tx = await contract. purchaseItem (itemId, { value: totalPayment });
console. log ( 'Transaction hash:' , tx.hash);
const receipt = await tx. wait ();
console. log ( 'Purchase confirmed!' , receipt);
// Extract purchase ID from events
const purchaseEvent = receipt.events?. find ( e => e.event === 'PurchaseCreated' );
const purchaseId = purchaseEvent?.args?.purchaseId;
return { success: true , purchaseId };
} catch (error) {
console. error ( 'Purchase failed:' , error);
return { success: false , error: error.message };
}
}
// Pack structs to use fewer storage slots
struct Item {
uint128 price; // Reduced from uint256
uint64 createdAt; // Unix timestamp fits in uint64
uint32 id; // Supports up to 4.2B items
uint8 status; // Enum fits in uint8
address seller; // 20 bytes
string title; // Variable length
string description; // Variable length
}
function listMultipleItems (
string [] memory _titles,
string [] memory _descriptions,
uint256 [] memory _prices
) external returns ( uint256 [] memory ) {
require (_titles.length == _descriptions.length &&
_titles.length == _prices.length, "Array length mismatch" );
uint256 [] memory itemIds = new uint256 [](_titles. length );
for ( uint i = 0 ; i < _titles.length; i ++ ) {
itemIds[i] = listItem (_titles[i], _descriptions[i], _prices[i]);
}
return itemIds;
}
We've built a comprehensive escrow marketplace smart contract that demonstrates the power of blockchain technology for creating autonomous and transparent software . Our implementation includes:
✅ Item listing and management
✅ Secure escrow mechanism
✅ Automatic fund release
✅ Dispute resolution system
✅ Fee management
✅ Comprehensive testing
✅ Reentrancy protection
✅ Input validation
✅ Access control
✅ Time-based refunds
✅ Unit tests for all functions
✅ Integration testing
✅ Edge case handling
✅ Gas optimization verification
This escrow marketplace demonstrates how smart contracts can eliminate the need for traditional intermediaries while providing security, transparency, and automation. The code serves as a foundation that can be extended with additional features like:
NFT integration for digital goods
Multi-token support beyond ETH
Governance mechanisms for fee adjustments
Advanced reputation systems
Integration with decentralized storage (IPFS)
Deploy to testnets (Sepolia, Goerli) for live testing
Build a frontend application using React and Web3 libraries
Implement additional security audits using tools like Slither
Explore Layer 2 deployment for reduced gas costs
Study existing marketplaces like OpenSea for inspiration
The world of smart contracts is vast and rapidly evolving. This escrow marketplace is just the beginning—use it as a stepping stone to build more complex decentralized applications that can truly transform how we transact and interact in the digital economy.
Source Code : The complete implementation with tests is available on GitHub .
Remember: Always conduct thorough testing and security audits before deploying smart contracts to mainnet. The immutable nature of blockchain means bugs can be costly and difficult to fix.