Our world is forever being impacted by the rise of the Web3 Economy. It will be of great advantage to those who are currently capitalizing on this Web3 space.
As shown in a survey conducted by Cointelegraph Research and Keychain Ventures, $14.6 billion was invested in the blockchain business in the first quarter of 2022, continuing a trend in which total capital invested in this space has risen each quarter since the beginning of 2021.
Besides that, the survey also discovered that the average dollar value of each sale has risen in the last three months, now standing at approximately $32.3 million.
To break down the report, here is what you should know; The Web3 space sporadically growing. Money is being pumped into this sector. This money is used for building new Web 3.0 systems that will provide services to people, and revenue to the investors. But who are those to build these systems? They are the developers!
Hey, I could be of help to you as a personal tutor to speed you up with web3 development, kindly book a session with me.
In this tutorial, you will learn how to code an Escrow smart contract step by step, you will also get to understand why it is important for you to learn.
If you are fired up like I am, let’s jump into the lesson…
One of the major areas of investment in the Web 3.0 space is in the development of decentralized financial systems (Defi). This area in the Web3 economy focuses on providing platforms for securely moving money from one destination to the other at a lightning speed. There are a lot of mixes and differences involved in Defi technologies, but one common thing among them is to process digital currencies.
We do live in a world where a lot of people cannot be trusted with monetary engagements and as such rely on Escrow systems.
Escrows are third-party systems that help facilitate business transactions between two parties thereby prohibiting monetary losses.
For example, James who lives in Alabama wants to buy a new MacBook from Dave who sells from San Francisco. But James doesn’t know Dave who lives far away and can’t trust that he will deliver the Laptop.
The above problem is exactly what Escrows services resolve, bringing the James and Dave example to resolution. Dave and James agreed to entrust the sales process to a trustable organization to facilitate the transaction for a percentage fee.
By so doing, James gives the money to the Escrow provider and Dave also gives the MacBook to the Escrow provider as well. The Escrow provider hand over the laptop to James and Dave the money for the laptop while the Escrow provider takes their cut.
The above situation is what we see in most online businesses such as:
This is how the system works: escrow smart contracts protect your funds until conditions are met and all parties involved in the trade are satisfied.
Buyer and seller agree to terms A seller lists an item for sale, and the buyer pays the agreed-upon price.
The seller delivers the product The funds are held in the escrow smart contract until all conditions have been met and the buyer has confirmed receipt.
The buyer approves, and the seller gets paid The funds will be automatically transferred to the seller's digital wallet by the smart contract.
It’s time to see an example solidity code for implementing an Escrow system.
Below is a fully coded Escrow smart contract example, we’ll next discuss how each function culminates in proffering an Escrow solution.
Get in touch with me, if you want me to build a DApp for your business.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Escrow is ReentrancyGuard {
address public escAcc;
uint256 public escBal;
uint256 public escAvailBal;
uint256 public escFee;
uint256 public totalItems = 0;
uint256 public totalConfirmed = 0;
uint256 public totalDisputed = 0;
mapping(uint256 => ItemStruct) private items;
mapping(address => ItemStruct[]) private itemsOf;
mapping(address => mapping(uint256 => bool)) public requested;
mapping(uint256 => address) public ownerOf;
mapping(uint256 => Available) public isAvailable;
enum Status {
OPEN,
PENDING,
DELIVERY,
CONFIRMED,
DISPUTTED,
REFUNDED,
WITHDRAWED
}
enum Available { NO, YES }
struct ItemStruct {
uint256 itemId;
string purpose;
uint256 amount;
uint256 timestamp;
address owner;
address provider;
Status status;
bool provided;
bool confirmed;
}
event Action (
uint256 itemId,
string actionType,
Status status,
address indexed executor
);
constructor(uint256 _escFee) {
escAcc = msg.sender;
escBal = 0;
escAvailBal = 0;
escFee = _escFee;
}
function createItem(
string calldata purpose
) payable external returns (bool) {
require(bytes(purpose).length > 0, "Purpose cannot be empty");
require(msg.value > 0 ether, "Item cannot be zero ethers");
uint256 itemId = totalItems++;
ItemStruct storage item = items[itemId];
item.itemId = itemId;
item.purpose = purpose;
item.amount = msg.value;
item.timestamp = block.timestamp;
item.owner = msg.sender;
item.status = Status.OPEN;
itemsOf[msg.sender].push(item);
ownerOf[itemId] = msg.sender;
isAvailable[itemId] = Available.YES;
escBal += msg.value;
emit Action (
itemId,
"ITEM CREATED",
Status.OPEN,
msg.sender
);
return true;
}
function getItems()
external
view
returns (ItemStruct[] memory props) {
props = new ItemStruct[](totalItems);
for (uint256 i = 0; i < totalItems; i++) {
props[i] = items[i];
}
}
function getItem(uint256 itemId)
external
view
returns (ItemStruct memory) {
return items[itemId];
}
function myItems()
external
view
returns (ItemStruct[] memory) {
return itemsOf[msg.sender];
}
function requestItem(uint256 itemId) external returns (bool) {
require(msg.sender != ownerOf[itemId], "Owner not allowed");
require(isAvailable[itemId] == Available.YES, "Item not available");
requested[msg.sender][itemId] = true;
emit Action (
itemId,
"REQUESTED",
Status.OPEN,
msg.sender
);
return true;
}
function approveRequest(
uint256 itemId,
address provider
) external returns (bool) {
require(msg.sender == ownerOf[itemId], "Only owner allowed");
require(isAvailable[itemId] == Available.YES, "Item not available");
require(requested[provider][itemId], "Provider not on the list");
isAvailable[itemId] == Available.NO;
items[itemId].status = Status.PENDING;
items[itemId].provider = provider;
emit Action (
itemId,
"APPROVED",
Status.PENDING,
msg.sender
);
return true;
}
function performDelievery(uint256 itemId) external returns (bool) {
require(msg.sender == items[itemId].provider, "Service not awarded to you");
require(!items[itemId].provided, "Service already provided");
require(!items[itemId].confirmed, "Service already confirmed");
items[itemId].provided = true;
items[itemId].status = Status.DELIVERY;
emit Action (
itemId,
"DELIVERY INTIATED",
Status.DELIVERY,
msg.sender
);
return true;
}
function confirmDelivery(
uint256 itemId,
bool provided
) external returns (bool) {
require(msg.sender == ownerOf[itemId], "Only owner allowed");
require(items[itemId].provided, "Service not provided");
require(items[itemId].status != Status.REFUNDED, "Already refunded, create a new Item");
if(provided) {
uint256 fee = (items[itemId].amount * escFee) / 100;
payTo(items[itemId].provider, (items[itemId].amount - fee));
escBal -= items[itemId].amount;
escAvailBal += fee;
items[itemId].confirmed = true;
items[itemId].status = Status.CONFIRMED;
totalConfirmed++;
}else {
items[itemId].status = Status.DISPUTTED;
}
emit Action (
itemId,
"DISPUTTED",
Status.DISPUTTED,
msg.sender
);
return true;
}
function refundItem(uint256 itemId) external returns (bool) {
require(msg.sender == escAcc, "Only Escrow allowed");
require(!items[itemId].confirmed, "Service already provided");
payTo(items[itemId].owner, items[itemId].amount);
escBal -= items[itemId].amount;
items[itemId].status = Status.REFUNDED;
totalDisputed++;
emit Action (
itemId,
"REFUNDED",
Status.REFUNDED,
msg.sender
);
return true;
}
function withdrawFund(
address to,
uint256 amount
) external returns (bool) {
require(msg.sender == escAcc, "Only Escrow allowed");
require(amount > 0 ether && amount <= escAvailBal, "Zero withdrawal not allowed");
payTo(to, amount);
escAvailBal -= amount;
emit Action (
block.timestamp,
"WITHDRAWED",
Status.WITHDRAWED,
msg.sender
);
return true;
}
function payTo(
address to,
uint256 amount
) internal returns (bool) {
(bool success,) = payable(to).call{value: amount}("");
require(success, "Payment failed");
return true;
}
}
Now, let’s demystify this smart contract function by function…
Structuring the Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Escrow is ReentrancyGuard {
// code goes here...
}
The above code snippet defines our smart contract, the solidity compiler version to be used and the type of license to be used.
SPDX signifies the type of License this smart contract uses while pragma speaks of the Solidity compiler version used.
We then imported a Reentrancy guard from openzeppelin and inherited its properties to our smart contracts.
Then we defined the smart contract’s name as Escrow, coupling it together with the imported openzeppelin smart contract.
Defining Smart Contract Variables
address public escAcc;
uint256 public escBal;
uint256 public escAvailBal;
uint256 public escFee;
uint256 public totalItems = 0;
uint256 public totalConfirmed = 0;
uint256 public totalDisputed = 0;
We have the Escrow account which will be captured from the deploying address. The escrow balance will keep track of all the money sent into the smart contract.
Escrow available balance will hold all the fees received for every successful transaction.
The escrow fee will have the stipulated percentage fee on each transaction. This will be entered at the moment of the smart contract deployment.
We also have three variables keeping track of the total items created, confirmed, or disputed in this platform.
Defining the mapping structures
mapping(uint256 => ItemStruct) private items;
mapping(address => ItemStruct[]) private itemsOf;
mapping(address => mapping(uint256 => bool)) public requested;
mapping(uint256 => address) public ownerOf;
mapping(uint256 => Available) public isAvailable;
Items() map will keep track of all the items created on this marketplace, it uses a defined item structure which you can see in the next block.
ItemsOf() holds the items of a specific user according to his address. Requested() keep track of every item a user has requested whenever an Item becomes available in the marketplace.
OwnerOf() keeps track of the owner or creator of each item, while isAvailable() keeps track of items that have not been assigned to another individual for delivery.
Defining Structs and Enums
enum Status {
OPEN,
PENDING,
DELIVERY,
CONFIRMED,
DISPUTTED,
REFUNDED,
WITHDRAWED
}
enum Available { NO, YES }
struct ItemStruct {
uint256 itemId;
string purpose;
uint256 amount;
uint256 timestamp;
address owner;
address provider;
Status status;
bool provided;
bool confirmed;
}
The Status enum defines the various status an Item will go through before and after resolving disputes. Enums represent characters as integers starting from zero to infinity. But it is wise to keep enum structures short for the sake of maintainability.
Available is another enum indicating whether an item has been assigned to a person or not. And the ItemStruct() is a structure specifying the details of each item.
Connect with me if you want a trustworthy smart contract developer.
Specifying Event and Constructor
event Action (
uint256 itemId,
string actionType,
Status status,
address indexed executor
);
constructor(uint256 _escFee) {
escAcc = msg.sender;
escBal = 0;
escAvailBal = 0;
escFee = _escFee;
}
The action event was designed to be dynamic to soothe the various purposes we will need it for. Instead of defining several events for each function we execute, it was better to have just one event logging off different information.
The constructor on the other hand initializes the Escrow variables such as the fee per transaction.
CreateItem() function
function createItem(
string calldata purpose
) payable external returns (bool) {
// Validating parameters
require(bytes(purpose).length > 0, "Purpose cannot be empty");
require(msg.value > 0 ether, "Item cannot be zero ethers");
// Creating the item
uint256 itemId = totalItems++;
ItemStruct storage item = items[itemId];
item.itemId = itemId;
item.purpose = purpose;
item.amount = msg.value;
item.timestamp = block.timestamp;
item.owner = msg.sender;
item.status = Status.OPEN;
// Assigning to owner and stating availability
itemsOf[msg.sender].push(item);
ownerOf[itemId] = msg.sender;
isAvailable[itemId] = Available.YES;
escBal += msg.value;
// Emitting or Logging of created Item information
emit Action (
itemId,
"ITEM CREATED",
Status.OPEN,
msg.sender
);
return true;
}
In this function, we created an item specifying the purpose and the amount capped for anyone who will take on this item. It is important to note that this item could be a product or a service.
The above function creates an Item, assigns an item to the creator’s account, opens it up for interested providers, and emits a created event.
This is the only function with the highest number of codes, the rest will be a lot simpler.
By the way, if you want a tutor who will privately coach you on Smart Contract and DApp development, kindly book a session with me.
GetItems() function
function getItems()
external
view
returns (ItemStruct[] memory props) {
props = new ItemStruct[](totalItems);
for (uint256 i = 0; i < totalItems; i++) {
props[i] = items[i];
}
}
The above function simply returns an array of all the items in our platform.
GetItem() function
function getItem(uint256 itemId)
external
view
returns (ItemStruct memory) {
return items[itemId];
}
This function fetches an item based on the supplied item ID.
MyItems() function
function myItems()
external
view
returns (ItemStruct[] memory) {
return itemsOf[msg.sender];
}
This beautifully returns all the items an owner has created without needing to loop through records, isn’t that cool?
RequestItem() function
function requestItem(uint256 itemId)
external returns (bool) {
// Perfoms essential record validation
require(msg.sender != ownerOf[itemId], "Owner not allowed");
require(isAvailable[itemId] == Available.YES, "Item not available");
// Places request on an item
requested\[msg.sender\][itemId] = true;
emit Action (
itemId,
"REQUESTED",
Status.OPEN,
msg.sender
);
return true;
}
The responsibility of this function is to help a user apply for an item. His request will be added to a list of people also requesting to be approved for the task or purpose associated with the item. An item has a purpose, the purpose is what the seller is paying for.
ApproveRequest() function
function approveRequest(
uint256 itemId,
address provider
) external returns (bool) {
// Checks for essential requirement
require(msg.sender == ownerOf[itemId], "Only owner allowed");
require(isAvailable[itemId] == Available.YES, "Item not available");
require(requested\[provider\][itemId], "Provider not on the list");
// Assigns an item to a provider
isAvailable[itemId] == Available.NO;
items[itemId].status = Status.PENDING;
items[itemId].provider = provider;
emit Action (
itemId,
"APPROVED",
Status.PENDING,
msg.sender
);
return true;
}
This function assigns an item to a provider based on the item’s ID. The item’s owner awards the item to one of the providers by approving a person from the requested array.
PerformDelievery() function
function performDelievery(uint256 itemId)
external returns (bool) {
// Checks for essential conditions
require(msg.sender == items[itemId].provider, "Service not awarded to you");
require(!items[itemId].provided, "Service already provided");
require(!items[itemId].confirmed, "Service already confirmed");
// Marks item as provided
items[itemId].provided = true;
items[itemId].status = Status.DELIVERY;
emit Action (
itemId,
"DELIVERY INTIATED",
Status.DELIVERY,
msg.sender
);
return true;
}
This function marks an Item as provided and only executable by the provider to whom the item was awarded.
ConfirmDelievery() function
function confirmDelivery(
uint256 itemId,
bool provided
) external returns (bool) {
// Checks vital condition
require(msg.sender == ownerOf[itemId], "Only owner allowed");
require(items[itemId].provided, "Service not provided");
require(items[itemId].status != Status.REFUNDED, "Already refunded, create a new Item");
// Indicates delievery status
if(provided) {
// Pays the provider
uint256 fee = (items[itemId].amount * escFee) / 100;
payTo(items[itemId].provider, (items[itemId].amount - fee));
// Recalibrates records
escBal -= items[itemId].amount;
escAvailBal += fee;
items[itemId].confirmed = true;
// Marks as confirmed
items[itemId].status = Status.CONFIRMED;
totalConfirmed++;
}else {
// Marks as disputted
items[itemId].status = Status.DISPUTTED;
}
emit Action (
itemId,
"DISPUTTED",
Status.DISPUTTED,
msg.sender
);
return true;
}
This function marks an item as delivered or disputed by specifying in the parameter. This operation can only be performed by the owner of the item.
RefundItem() function
function refundItem(uint256 itemId)
external returns (bool) {
// Checks for essential condition
require(msg.sender == escAcc, "Only Escrow allowed");
require(!items[itemId].confirmed, "Service already provided");
// performs refund and record recalibration
payTo(items[itemId].owner, items[itemId].amount);
escBal -= items[itemId].amount;
items[itemId].status = Status.REFUNDED;
totalDisputed++;
emit Action (
itemId,
"REFUNDED",
Status.REFUNDED,
msg.sender
);
return true;
}
This function refunds an owner his money if the item was disputed, only the Escrow account holder is permitted to operate this function.
WithdrawFund() function
function withdrawFund(
address to,
uint256 amount
) external returns (bool) {
// Checks for essential condition
require(msg.sender == escAcc, "Only Escrow allowed");
require(amount > 0 ether && amount <= escAvailBal, "Zero withdrawal not allowed");
// Sends money to an address
payTo(to, amount);
escAvailBal -= amount;
emit Action (
block.timestamp,
"WITHDRAWED",
Status.WITHDRAWED,
msg.sender
);
return true;
}
This function sends money from the Escrow available balance to a specified account and can only be operated by the Escrow account holder.
The PayTo() function
function payTo(
address to,
uint256 amount
) internal returns (bool) {
(bool success,) = payable(to).call{value: amount}("");
require(success, "Payment failed");
return true;
}
Lastly, this function securely sends a specified amount of money from the smart contract to another account.
And that is how you create an Escrow smart contract…
Hire me if you want me to build some cool smart contracts for your business.
The demand for smart contract developers is ever-increasing and if there is the best time to jump into this space it was yesterday, but the second-best time is now.
A secured approach in your smart contract development is super important to always bear in mind as you write your codes.
If you want to partner with me, kindly write to me directly.
Also, check out my previous tutorials, subscribe, follow me, and give a thumbs up.
I will again see you in the next tutorial…
Gospel Darlington kick-started his journey as a software engineer in 2016. Over the years, he has grown full-blown skills in JavaScript stacks such as React, ReactNative, NextJs, and now blockchain.
He is currently freelancing, building apps for clients, and writing technical tutorials teaching others how to do what he does.
Gospel Darlington is open and available to hear from you. You can reach him on LinkedIn, Facebook, Github, or on his website.