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:
Verifies the buyer is not the seller
Ensures at least one product is included (maximum 5)
Calculates total product price
Creates a new trade with unique ID
Records seller, buyer, and sets trade status to ACTIVE
Adds products to the trade
Calculates escrow fee (2.5% of product price)
Calculates total cost (products + logistics + escrow fee)
Checks buyer has sufficient token balance
Checks buyer has approved contract to transfer tokens
Transfers tokens from buyer to the contract
Updates escrow balance
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:
Verifies caller is the seller
Ensures trade is active
Updates delivery status to SHIPPED
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:
Verifies caller is the buyer
Ensures trade is active
Verifies product has been shipped
Updates trade status to COMPLETED
Updates delivery status to DELIVERED
Calculates product price (total minus fees)
Verifies contract has sufficient balance
Transfers product price to seller
Transfers fees to contract owner
Updates escrow balance
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
ReentrancyGuard: Prevents reentrancy attacks during fund transfers (nonReentrant)
SafeERC20: Ensures safe token transfers and handling
Access Control: Functions restricted to appropriate parties (buyer/seller)
State Validation: Ensures proper trade and delivery status before actions
Balance Checks: Verifies sufficient funds before transfers
Business Logic
The contract implements a complete escrow flow:
Buyer initiates trade: Specifies seller, products, and prices
Funds locked in contract: Buyer's funds are held in escrow
Seller ships product: Updates status to indicate shipping
Buyer confirms receipt: Upon delivery, buyer confirms receipt
Automatic settlement: Funds distributed to seller and platform
Notable Points
The contract supports multiple products in a single trade (up to 5)
The platform charges a 2.5% fee on the product price
Additional logistics fees can be specified per trade
The contract uses ERC20 tokens (likely ETN tokens) rather than native ETH
While there's a DISPUTED enum state, there's no explicit dispute resolution function
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
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
