Learn how to build a secure escrow marketplace smart contract using Solidity and Foundry, enabling trustless transactions between buyers and sellers.
React to this article
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. Nick Szabo proposed the idea in 1994. In practice, it behaves like a digital agreement that executes automatically when predetermined conditions are met, without relying on intermediaries.
Once deployed, smart contracts operate independently. No human has to push them forward. If conditions A, B, and C are met, action X executes automatically because the code is the law.
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
Blockchain-based escrow offers reduced fees by eliminating traditional agent commissions and faster settlements through automated execution upon condition fulfillment. It provides global accessibility — anyone with an internet connection can participate — along with transparency that lets all parties verify the contract logic and state. And because the rules are immutable, terms cannot be changed unilaterally.
Project Setup with Foundry
Foundry is a modern, fast Rust-based toolkit for Ethereum development. It gives us the full stack for building, testing, and deploying smart contracts. That keeps the workflow tight.
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.
function purchaseItem(uint256 _itemId) external payable nonReentrant 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); nextPurchaseId++; emit PurchaseCreated(purchaseId, _itemId, msg.sender); // Refund excess payment (do this last to prevent reentrancy) if (msg.value > totalRequired) { (bool success, ) = payable(msg.sender).call{value: msg.value - totalRequired}(""); require(success, "Refund transfer failed"); } return purchaseId;}
Delivery Confirmation
function confirmDelivery(uint256 _purchaseId) external nonReentrant onlyBuyer(_purchaseId) purchaseExists(_purchaseId){ Purchase storage purchase = purchases[_purchaseId]; require(purchase.status == PurchaseStatus.Pending, "Purchase not pending"); // Update state before external calls (checks-effects-interactions pattern) purchase.status = PurchaseStatus.Delivered; emit DeliveryConfirmed(_purchaseId); // Release funds to seller (interactions last) (bool sellerSuccess, ) = payable(purchase.seller).call{value: purchase.amount}(""); require(sellerSuccess, "Seller payment failed"); // Send fee to marketplace owner if (purchase.fee > 0) { (bool ownerSuccess, ) = payable(owner).call{value: purchase.fee}(""); require(ownerSuccess, "Fee payment failed"); }}
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.
# Run all testsforge test# Run specific test with verbose outputforge test --match-test testPurchaseItem -vvv# Test with gas reportingforge test --gas-report# Generate coverage reportforge coverage
Advanced Features
Price Discovery and Bidding
// Add to main contractmapping(uint256 => Bid[]) public itemBids;struct Bid { address bidder; uint256 amount; uint256 timestamp;}function placeBid(uint256 _itemId) external payable nonReentrant { 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"); } // Add new bid first (state change before external call) bids.push(Bid({ bidder: msg.sender, amount: msg.value, timestamp: block.timestamp })); // Refund previous bidder (external call last) if (bids.length > 1) { Bid storage previousBid = bids[bids.length - 2]; (bool success, ) = payable(previousBid.bidder).call{value: previousBid.amount}(""); require(success, "Previous bidder refund failed"); }}
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 // Note: Solidity 0.8.19+ has built-in overflow protection 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... }}
// Pack structs to use fewer storage slotsstruct 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 shows how smart contracts can remove traditional intermediaries while still providing security, transparency, and automation. It is only a foundation. From here, the code can be extended with NFT integration for digital goods, multi-token support beyond ETH, governance mechanisms for fee adjustments, advanced reputation systems, and integration with decentralized storage like 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.
Luu, L. et al. (2016). "Making Smart Contracts Smarter" — CCS. Foundational paper on smart contract security vulnerabilities
Foundry Book — Documentation for the Foundry development toolkit used to build and test the escrow contract
OpenZeppelin Contracts — Security-audited contract library; reference for common patterns like ReentrancyGuard
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.