Building and Deploying a P2P Escrow Smart Contract on Electroneum Blockchain: A Comprehensive Guide

Introduction

The Electroneum Smart Chain (ETN-SC) offers a robust environment for developing decentralized applications with fast transaction speeds, low fees, and EVM compatibility. In this comprehensive guide, we'll walk through the complete process of building and deploying a P2P Escrow smart contract on the Electroneum blockchain.

Our P2P Escrow contract provides a secure way for buyers and sellers to conduct transactions without requiring trust between parties. The blockchain acts as a neutral third party that holds funds until predefined conditions are met, ensuring that both buyers and sellers are protected throughout the transaction process.

Prerequisites

For this tutorial, you'll need:

  • Basic knowledge of blockchain concepts and Solidity

  • Node.js (v14 or higher)

  • npm or yarn

  • Git

  • MetaMask wallet

  • ETN tokens for deployment (for testnet deployment, you can use the ETN Testnet Faucet). We will be deploying on both testnet and mainet

Project Setup

Let's start by setting up our project environment:

# Create a new directory for your project
mkdir p2pEscrow
cd p2pEscrow

# Install required dependencies
npm install --save-dev hardhat
npx hardhat init
npm install --save @openzeppelin/contracts

Configuring Hardhat for Electroneum

Next, we need to configure Hardhat to work with the Electroneum blockchain. Create a hardhat.config.ts file with the following content:

import { HardhatUserConfig, vars } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox-viem";

const ANKR_API_KEY = vars.get("ANKR_API_KEY");
const config: HardhatUserConfig = {
  solidity: "0.8.26",
  networks: {
    electroneum: {
      url: `https://rpc.ankr.com/electroneum/${ANKR_API_KEY}`,
      accounts: vars.has("PRIVATE_KEY") ? [vars.get("PRIVATE_KEY")] : [],
    },
    'electroneum-testnet': {
      url: 'https://rpc.ankr.com/electroneum_testnet',
      accounts: vars.has("PRIVATE_KEY") ? [vars.get("PRIVATE_KEY")] : [],
    },
  },
  etherscan: {
    apiKey: {
      electroneum: "empty",
    },
    customChains: [
      {
        network: "electroneum",
        chainId: 52014,
        urls: {
          apiURL: "https://blockexplorer.electroneum.com/api",
          browserURL: "https://blockexplorer.electroneum.com",
        },
      },
      {
        network: "electroneum-testnet",
        chainId: 5201420,
        urls: {
          apiURL: "https://testnet-blockexplorer.electroneum.com/api",
          browserURL: "https://testnet-blockexplorer.electroneum.com"
        }
      }
    ],
  },
};

export default config;

The next thing is to set and store the variables values in hardhat’s vars.json file. Note the we are storing ANKR_API_KEY and PRIVATE_KEY;

You can check the list of the stored variables again using this command;

yarn hardhat vars list

Creating the P2P Escrow Smart Contract

Now, we'll create our P2P Escrow smart contract. This contract will handle the escrow process between buyers and sellers. In the contracts folder let’s create a new file and call it P2PEscrow.sol with our smart contract code;

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

    enum Trade {
        NONE,
        CANCELLED,
        ACTIVE,
        DISPUTED,
        COMPLETED
    }

    enum Delivery {
        UNSHIPPED,
        SHIPPED,
        DELIVERED
    }

contract P2PEscrow is ReentrancyGuard {
    using SafeERC20 for IERC20;

    address private owner;
    address private escrowAcc;
    uint256 public escrowBal;

    uint256 public tradeCount;

    uint128 constant ESCROWFEE_PERCENTAGE = 250; // 2.5% of the product price
    uint256 constant SCALING_FACTOR = 10000;

    struct Product {
        string name;
        uint price;
    }

    struct TradeInfo {
        uint256 tradeId;
        address seller;
        address buyer;
        Product[] products;
        uint256 escrowFee;
        uint256 logisticFee;
        uint256 totalTradingCost;
        Trade tradeStatus;
        Delivery deliveryStatus;
    }

    mapping(uint256 => TradeInfo) private trade;

    constructor() payable {
        owner = msg.sender;
        escrowAcc = address(0);
    }

    modifier preventAddressZero() {
        require(msg.sender != address(0), "Address zero not allowed");
        _;
    }

    modifier onlyOwner() {
        require(msg.sender != address(0), "Address zero not allowed");
        require(msg.sender == owner, "No access");
        _;
    }

    event TradeActive(
        address indexed buyer,
        address indexed seller,
        uint256 escrowFee,
        uint256 totalTradingCost
    );
    event TradeCompleted(address indexed buyer, address indexed seller);
    event Transfer(
        address indexed buyer,
        address indexed spender,
        uint256 amount
    );
    event Action(string actionType, address indexed executor);

    function openTrade(
        IERC20 _token,
        address _seller,
        string[] memory _products,
        uint256[] memory _productPrices,
        uint256 _logisticFee
    ) external preventAddressZero nonReentrant returns (bool success_) {
        require(msg.sender == _seller, "Seller can not buy their product");
        require(_products.length > 0, "At least one product required");
        require(_products.length <= 5, "You can not trade more than 5 products in a trade");

        uint productTotalPrice = 0;
        for (uint i = 0; i < _productPrices.length; i++) {
            productTotalPrice += _productPrices[i];
        }

        require(
            productTotalPrice > 0,
            "Product toatal price cannot be zero ethers"
        );

        uint256 tradeId = tradeCount++;

        TradeInfo memory tradeInfo = trade[tradeId];
        tradeInfo.tradeId = tradeId;
        tradeInfo.seller = _seller;
        tradeInfo.buyer = msg.sender;
        tradeInfo.tradeStatus = Trade.ACTIVE;

        for (uint i = 0; i < _products.length; i++) {
            trade[tradeId].products.push(
                Product(_products[i], _productPrices[i])
            );
        }

        uint256 escrowFee = calcEscrowFee(productTotalPrice);

        tradeInfo.escrowFee = escrowFee;
        tradeInfo.logisticFee = _logisticFee;

        uint256 totalTradingCost = productTotalPrice + _logisticFee + escrowFee;

        tradeInfo.totalTradingCost = totalTradingCost;

        require(
            _token.balanceOf(msg.sender) >= totalTradingCost,
            "Insufficient balance"
        );

        require(
            _token.allowance(msg.sender, escrowAcc) >= totalTradingCost,
            "Amount is not allowed"
        );

        // Transfer _token to the seller
        _token.safeTransferFrom(msg.sender, escrowAcc, totalTradingCost);

        // update escrow balance
        escrowBal += totalTradingCost;

        emit Transfer(msg.sender, escrowAcc, totalTradingCost);
        emit TradeActive(msg.sender, _seller, escrowFee, totalTradingCost);

        return success_;
    }

    function shipProducts(
        uint256 _tradeId
    ) external preventAddressZero returns (bool success_) {
        TradeInfo memory tradeInfo = trade[_tradeId];

        address seller = tradeInfo.seller;

        require(msg.sender == seller, "Unauthorized!");

        require(
            tradeInfo.tradeStatus == Trade.ACTIVE,
            "Trade is not active or does not exist"
        );

        tradeInfo.deliveryStatus = Delivery.SHIPPED;

        emit Action("Product Shipped", seller);

        return success_;
    }

    function completeTrade(
        IERC20 _token,
        uint256 _tradeId
    ) external preventAddressZero nonReentrant returns (bool success_) {
        TradeInfo memory tradeInfo = trade[_tradeId];

        address buyer = tradeInfo.buyer;
        address seller = tradeInfo.seller;
        tradeInfo.tradeStatus = Trade.COMPLETED;

        require(msg.sender == buyer, "Unauthorized!");

        tradeInfo.deliveryStatus = Delivery.DELIVERED;

        uint256 productTotalPrice = tradeInfo.totalTradingCost -
            tradeInfo.escrowFee -
            tradeInfo.logisticFee;

        require(
            _token.balanceOf(escrowAcc) >= tradeInfo.totalTradingCost,
            "Insufficient balance"
        );

        require(
            tradeInfo.deliveryStatus == Delivery.SHIPPED,
            "This product has not being sent for delivery"
        );

        _token.safeTransfer(seller, productTotalPrice);
        _token.safeTransfer(owner, tradeInfo.escrowFee + tradeInfo.logisticFee); // transfer escrow fee and logistic fee to the owner

        emit TradeCompleted(buyer, seller);

        return success_;
    }

    //HELPER
    function calcEscrowFee(
        uint256 _productPrice
    ) private pure returns (uint256) {
        return (_productPrice * ESCROWFEE_PERCENTAGE) / SCALING_FACTOR;
    }
}

Detailed Explanation of the P2P Escrow Smart Contract Code

The P2PEscrow contract is a decentralized escrow system that facilitates secure transactions between buyers and sellers without requiring trust between parties. Here's a comprehensive breakdown of how it works:

Core Components

Enums

Trade Enum: Tracks the status of a trade

  • NONE: Default state

  • CANCELLED: Trade has been cancelled

  • ACTIVE: Trade is currently active

  • DISPUTED: Trade is in dispute

  • COMPLETED: Trade has been completed successfully

    Delivery Enum: Tracks the delivery status of products

  • UNSHIPPED: Product has not been shipped yet

  • SHIPPED: Product has been shipped but not delivered

  • DELIVERED: Product has been delivered to the buyer

Data Structures

Product Struct: Represents a single product in a trade

  • name: Name of the product

  • price: Price of the product in ETN tokens

    TradeInfo Struct: Contains all details about a trade

  • tradeId: Unique identifier for the trade

  • seller: Address of the seller

  • buyer: Address of the buyer

  • products: Array of Product structs

  • escrowFee: Fee charged by the platform (2.5%)

  • logisticFee: Fee for shipping/logistics

  • totalTradingCost: Total cost including product price, escrow fee, and logistics

  • tradeStatus: Current status from the Trade enum

  • deliveryStatus: Current delivery status from the Delivery enum

State Variables

  • owner: Address of the contract owner/platform admin

  • escrowAcc: Address of the escrow account (set to address(0) in constructor)

  • escrowBal: Total balance held in escrow

  • tradeCount: Running counter of total trades

  • ESCROWFEE_PERCENTAGE: Constant fee percentage (250 = 2.5%)

  • SCALING_FACTOR: Divisor for calculating percentage (10000)

  • trade: Mapping from tradeId to TradeInfo

Events

  • TradeActive: Emitted when a new trade is initiated

  • TradeCompleted: Emitted when a trade is completed

  • Transfer: Emitted when funds are transferred

  • Action: Emitted for various actions (e.g., shipping)

Modifiers

  • preventAddressZero: Prevents interactions from the zero address

  • onlyOwner: Restricts certain functions to only be callable by the owner

Key Functions

1. openTrade

This function initiates a new trade between a buyer and seller:

  • Parameters:

  • _token: ERC20 token used for payment (ETN token)

  • _seller: Address of the seller

  • _products: Array of product names

  • _productPrices: Array of product prices

  • _logisticFee: Fee for shipping/logistics

  • Workflow:

  1. Verifies the buyer is not the seller

  2. Ensures at least one product is included (maximum 5)

  3. Calculates total product price

  4. Creates a new trade with unique ID

  5. Records seller, buyer, and sets trade status to ACTIVE

  6. Adds products to the trade

  7. Calculates escrow fee (2.5% of product price)

  8. Calculates total cost (products + logistics + escrow fee)

  9. Checks buyer has sufficient token balance

  10. Checks buyer has approved contract to transfer tokens

  11. Transfers tokens from buyer to the contract

  12. Updates escrow balance

  13. Emits events for transfer and trade activation

2. shipProducts

Called by the seller to indicate products have been shipped:

  • Parameters:

  • _tradeId: ID of the trade

  • Workflow:

  1. Verifies caller is the seller

  2. Ensures trade is active

  3. Updates delivery status to SHIPPED

  4. Emits Action event with "Product Shipped" message

3. completeTrade

Called by the buyer to confirm receipt and complete the trade:

  • Parameters:

  • _token: ERC20 token reference

  • _tradeId: ID of the trade

  • Workflow:

  1. Verifies caller is the buyer

  2. Ensures trade is active

  3. Verifies product has been shipped

  4. Updates trade status to COMPLETED

  5. Updates delivery status to DELIVERED

  6. Calculates product price (total minus fees)

  7. Verifies contract has sufficient balance

  8. Transfers product price to seller

  9. Transfers fees to contract owner

  10. Updates escrow balance

  11. Emits TradeCompleted event

4. calcEscrowFee

Helper function to calculate the escrow fee (2.5% of product price):

  • Calculation: (_productPrice * ESCROWFEE_PERCENTAGE) / SCALING_FACTOR

Security Features

  1. ReentrancyGuard: Prevents reentrancy attacks during fund transfers (nonReentrant)

  2. SafeERC20: Ensures safe token transfers and handling

  3. Access Control: Functions restricted to appropriate parties (buyer/seller)

  4. State Validation: Ensures proper trade and delivery status before actions

  5. Balance Checks: Verifies sufficient funds before transfers

Business Logic

The contract implements a complete escrow flow:

  1. Buyer initiates trade: Specifies seller, products, and prices

  2. Funds locked in contract: Buyer's funds are held in escrow

  3. Seller ships product: Updates status to indicate shipping

  4. Buyer confirms receipt: Upon delivery, buyer confirms receipt

  5. Automatic settlement: Funds distributed to seller and platform

Notable Points

  1. The contract supports multiple products in a single trade (up to 5)

  2. The platform charges a 2.5% fee on the product price

  3. Additional logistics fees can be specified per trade

  4. The contract uses ERC20 tokens (likely ETN tokens) rather than native ETH

  5. While there's a DISPUTED enum state, there's no explicit dispute resolution function

  6. The contract requires buyers to approve token transfers before initiating trades

This P2P Escrow contract effectively creates a trustless marketplace where neither buyer nor seller needs to trust each other, as the smart contract acts as the neutral intermediary that enforces the rules of the transaction.

Hardhat Ignition for Deploying the P2PEscrow contract

Hardhat Ignition is a powerful deployment system that manages the complexity of deploying smart contracts to the blockchain. The P2PEscrow contract will be deployed using an Ignition module, which provides several advantages. In the ignition folder, create P2PEscrow.ts file with this code;

import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";


const P2PEscrowModule = buildModule("P2PEscrowModule", (m) => {

  const p2PEscrow = m.contract("P2PEscrow");

  return { p2PEscrow };
});

export default P2PEscrowModule;

Deploying to Electroneum Smart Chain

Let's deploy and verify our contract to the Electroneum testnet and mainnet using hardhat commands. Take note that when you deploy to testnet, the contract wont verify, until deployed to mainnet:

## testnet
npx hardhat ignition deploy ignition/modules/P2PEscrow.ts --network electroneum-testnet --verify
## mainnet
npx hardhat ignition deploy ignition/modules/P2PEscrow.ts --network electroneum --verify

Here is the results https://blockexplorer.electroneum.com/address/0xCcB8a254Eb41292b45bD48B61723cE8e4E9141D7#code;

Conclusion

Congratulations! You've successfully built and deployed a P2P Escrow smart contract on the Electroneum blockchain. This contract provides a secure way for buyers and sellers to conduct transactions without requiring trust between parties. Feel free to play around with the contract and add more features such as, cancel trade logic, dispute logic, resolve dispute logic, etc.

The Electroneum Smart Chain offers an excellent platform for developing decentralized applications with its fast transaction speeds, low fees, and EVM compatibility.

Refrences

https://github.com/DevBigEazi/p2pEscrow

https://hardhat.org/hardhat-runner/docs/getting-started#installationEdit this text

https://electroneum.com/blockchain/

2
Subscribe to my newsletter

Read articles from Isiaq A. Tajudeen directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Isiaq A. Tajudeen
Isiaq A. Tajudeen