Building an Escrow Marketplace Smart Contract: A Beginner's Guide to Solidity

April 5, 202510 min read2 views

Learn how to build a secure escrow marketplace smart contract using Solidity and Foundry, enabling trustless transactions between buyers and sellers.

Building an Escrow Marketplace Smart Contract: A Beginner's Guide to Solidity

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:

  1. Item Listings: Sellers can list items for sale
  2. Purchase Orders: Buyers can create purchase orders with escrowed funds
  3. Dispute Resolution: Mechanism for handling conflicts
  4. Fee Management: Marketplace fees and distribution

Escrow Marketplace Flow

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.

Loading diagram...

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.

Loading diagram...

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

  1. Deploy to testnets (Sepolia, Goerli) for live testing
  2. Build a frontend application using React and Web3 libraries
  3. Implement additional security audits using tools like Slither
  4. Explore Layer 2 deployment for reduced gas costs
  5. 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.

Blockchain FoundationsPart 2 of 2
Series Progress2 / 2
G

Gabriel Jeronimo

Blockchain Developer & Smart Contract Engineer

Blockchain developer specializing in Ethereum, Solidity, and DeFi protocols. Expert in smart contract development and Web3 technologies.

View all articles →