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.
What Are Smart Contracts?
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.
Key Characteristics of Smart Contracts:
1. Autonomous
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.
2. Transparent
All smart contract code and transactions are visible on the blockchain, providing complete transparency to all participants.
3. Immutable
After deployment, smart contracts cannot be changed (unless specifically designed with upgrade mechanisms), ensuring that the rules remain constant.
4. Distributed
Smart contracts run on a decentralized network of computers, eliminating single points of failure and reducing the need for trusted intermediaries.
Understanding Escrow Systems
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.
Benefits of Blockchain-Based Escrow:
- 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
Project Setup with Foundry
Foundry is a modern, fast Rust-based toolkit for Ethereum application development. It provides everything we need for smart contract development, testing, and deployment.
Installing Foundry
# Install Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
# Verify installation
forge --version
cast --version
anvil --version
Creating Our Project
# 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
Smart Contract Architecture
Our escrow marketplace will consist of several key components:
Core 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.
State Variables and Structs
// 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;
}
Core Functionality Implementation
Contract Constructor and Modifiers
// 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");
_;
}
Listing Items
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;
}
Creating Purchase Orders with Escrow
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;
}
Delivery Confirmation
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.
Dispute Resolution System
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);
}
}
Automatic Refund for Late Delivery
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);
}
Comprehensive Testing Strategy
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);
}
}
Running Tests
# 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
Advanced Features
Price Discovery and Bidding
// 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
}));
}
Reputation System
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;
}
}
Security Considerations
Reentrancy Protection
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...
}
}
Input Validation
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...
}
Access Control
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...
}
}
Deployment and Interaction
Deployment Script
// 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();
}
}
Frontend Integration
// 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 };
}
}
Gas Optimization Techniques
Efficient Storage Patterns
// 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
}
Batch Operations
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;
}
Conclusion
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:
Key Features Implemented:
- ✅ Item listing and management
- ✅ Secure escrow mechanism
- ✅ Automatic fund release
- ✅ Dispute resolution system
- ✅ Fee management
- ✅ Comprehensive testing
Security Measures:
- ✅ Reentrancy protection
- ✅ Input validation
- ✅ Access control
- ✅ Time-based refunds
Testing Coverage:
- ✅ 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)
Learning Path Forward
- 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.
