paint-brush
How to Code an Escrow Smart Contract: The Secured Approachby@daltonic
6,213 reads
6,213 reads

How to Code an Escrow Smart Contract: The Secured Approach

by Darlington Gospel May 22nd, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The average dollar value of each sale has risen in the last three months, now standing at approximately $32.3 million. $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. Escrows are third-party systems that help facilitate business transactions between two parties thereby prohibiting monetary losses. This money is used for building new Web 3.0 systems that will provide services to people, and revenue to the investors.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - How to Code an Escrow Smart Contract: The Secured Approach
Darlington Gospel  HackerNoon profile picture

Introduction

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…

Why you should Master Escrow Smart Contracts

Free Selective Focus Photography of Man Facing Computer Stock Photo

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:

  • Buying and Selling of Jewelries.
  • Car rental businesses.
  • Automobile, Aircraft, and Boat sales.
  • Sales of Digital Collectibles.
  • Freelancers websites
  • Ecommerce and more.

How an Escrow Smart Contract Works

Free Colleaagues working Together Stock Photo

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.

Escrow Smart Contract Example

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.

Conclusion

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…

About the Author

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.