DEV Community

Cover image for Building a Smart Contract for Logistics Companies
Drayfocus
Drayfocus

Posted on • Originally published at drayfocus.hashnode.dev

Building a Smart Contract for Logistics Companies

Blockchain technology is a revolutionary tool that can be applied to many industries. You have already seen how blockchain can be used as a secure ledger for digital currency, but it also has many other applications. One such application is in the logistics industry.

The logistics industry has been around for decades and the current system works well for most companies. However, there are still some issues within the industry that need to be addressed. For example, there are times when customer orders outweigh the capacity of the logistic company. They usually resolve this by delaying the time of deliveries which doesn't always turn out to give a good customer experience.

Smart contracts are one way to address these issues. In this article, you will look at how smart contracts can be used in the logistics industry and how they can help improve efficiency and help such a logistic company shed some weight by allowing other logistics companies to plug into their system to deliver the order. You will learn how to build a smart contract by building a system that enables a group of logistics companies within a city to work together as a single unit. Each logistic company would be able to offer delivery assistance to other logistics companies that can't process a delivery quickly. Here, the smart contract acts as the trusted enabler, making the companies exchange delivery data and fees without worry.

In this article, you would do the following:

  • Get familiar with Remix IDE

  • Define how the smart contract would work

  • Build our Smart Contract

Remix IDE

In this article, you will use the Remix IDE. The remix is the official Integrated Development Environment for writing Solidity. The remix is similar to most common IDEs like Vscode, Sublime text, and so on. It allows you to quickly build smart contracts, deploy, and test on Ethereum networks without going through extra setups. You can use it directly on your browser at remix.ethereum.org.

Overview

overview.jpg

The picture above is what you see the first time you visit Remix. Similar to Vscode, the file explorer is on the left side of your screen, the code area on the right, then the terminal/log below the code area.

File Explorer

File Explorer.jpg

This is where you create and manage your files, mostly solidity files (.sol)

Solidity Compiler

Compiler.jpg

Once you are done writing your smart contract, you will need to compile it. With Remix, you can easily configure your compiler and compile your solidity script, so it can run on the Ethereum Virtual Machine.

Deploy and Run Transactions

deploy and run.jpg

After successfully compiling your smart contract, you can deploy it on a test Ethereum network and run it directly on Remix IDE.

How will this smart contract work?

  • The smart contract would be launched by an account into an Ethereum network through a transaction. This account is the contract initiator. The transaction would include some ether (Ethereum cryptocurrency) which the initiator will use to register itself as a member of the smart contract.

  • Each of the logistic companies would create an account on the Ethereum network. These accounts would be used to register as a member on the smart contract.

  • During registration, a minimum amount of ether (e.g 10) is set as the registration fee. This fee serves as a requirement for becoming a member of the Logistics smart contract (PS - requirements can vary in real-life applications).

  • After registration into the smart contract, members are given a reputation number (e.g 10). A member can request delivery assistance from the smart contract. This member is the delivery owner. During this request, an ether amount, equivalent to the delivery fee is sent alongside a hashed (encrypted) data of the delivery.

  • Any member of the smart contract who is interested in assisting in the delivery would respond to the delivery request, decrypt the delivery details, then use the details to complete the delivery process. This member is referred to as the assisting member of the delivery.

  • After the delivery is completed, the assisting member marks the delivery as completed on the smart contract. The delivery owner will confirm the completeness of the delivery (This can happen after the customer has indicated that the delivery has been completed).

  • After the delivery has been confirmed, the delivery fee is sent from the delivery owner's ** balance to the **assisting member balance on the smart contract. The reputation of the assisting member would be increased by some values (e.g 3).

  • If the delivery failed or was canceled by the assisting member, its reputation would be reduced by some value (e.g 5).

  • The reputation of every member on the smart contract is important as it serves as a factor to validate the authenticity of each member ( a logistics company).

Build your Smart Contract

A smart contract is made up of data (states), functions to access or change the data, and rules (modifiers) that act as gatekeepers for the data and the functions.

It is a best practice to analyze a smart contract before writing any line of code. Analyzing a smart contract gives clarity on what we want to build.

The diagram below shows an overview of our smart contract.

smart contract.jpg

Your smart contract would be made up of four important components.

  • Name
  • State data
  • Rules or modifiers
  • Functions

Name

This is simply the name of your contract. It is the first step you need to take in building your smart contract. Go ahead to your Remix IDE, and create a new file named Logistics.sol, then define your smart contract as shown below

pragma solidity >=0.4.16 <0.9.0;
contract Logistics {
// our smart contract code goes here
}
Enter fullscreen mode Exit fullscreen mode

The first line pragma solidity >=0.4.16 <0.9.0 indicates that the solidity version you are using is between 0.4.16 to 0.8.0. This version specification is needed to correctly compile our solidity code. The "contract Logistics" on line 2 defines our smart contract.

States data

States data are variables (data storage containers) whose values are permanently stored in contract storage. It is always present throughout the existence of the contract. Solidity is a typed language and it has different data types similar to C++. In this smart contract, you would only use four data types.

  • *address *

This is used for storing addresses of contracts or accounts. it is a 160-bit value that does not allow any arithmetic operations.

  • struct

The struct type is used for storing related data as one.

  • uint

uint means unsigned integer. it is used for storing integer values

  • mapping

mapping is quite similar to a javascript array. It is used to store value in a key=>value pair format.

You can check out other data types on Solidity Documentation.

When declaring state data in solidity, the data type is written before the state name. For example,
address payinguser: address is the data type, while payinguser is the name of the state.

Let's define the state data for our smart contract.

  • initiator -> The address of the account that will launch the logistics smart contract
    address initiator;
Enter fullscreen mode Exit fullscreen mode
  • memberState -> Holds data for each member registered on the contract. It contains the logistics company's balance and reputation which are both integers.
    struct memberState {
        uint balance;
        uint reputation;
    }
Enter fullscreen mode Exit fullscreen mode
  • deliveryState -> Holds data for each delivery that will be sent to the smart contract. It contains hashed delivery details, ** delivery fee** in ether, address ** of the **delivery owner, the delivery status which is an integer that describes the present state of the delivery, and lastly, the address of the assisting member.
// when status = 0, it means pending
// when status = 1, it means in progress
// when status = 2, it means completed
// when status = 3, it means confirmed
   struct deliveryState {
        uint deliveryFee;
        uint status;
        uint hashedDeliveryDetails;
        address owner;
        address assistingMember;
    }
Enter fullscreen mode Exit fullscreen mode
  • membersData -> This maps the account address of every registered member on the smart contract to the memberState, in key => value pair format. The public keyword allows the memberData to be accessed outside the smart contract by the public.
  mapping (address => memberState) public membersData;
Enter fullscreen mode Exit fullscreen mode
  • members -> This maps the account address of every registered member to their state of membership in the smart contract. The membership state is an integer of either 0 or 1 as the value. 0 indicates no membership, while 1 indicates active membership.
 mapping (address => uint) members;
Enter fullscreen mode Exit fullscreen mode
  • deliveryData -> This maps the hashed delivery details to a delivery state in the smart contract in a key => value pair format. It is also public and can be accessed outside the smart contract.
mapping (uint => deliveryState) public deliveryData;
Enter fullscreen mode Exit fullscreen mode

Now, add all these data states to your Logistics.sol file. You should have something similar to this:

pragma solidity >=0.4.16 <0.9.0;
contract Logistics {

 // The address of the account that launched the smart contract
    address initiator;

    // state data for each member registered on the contract
    struct memberState {
        uint balance;
        uint reputation;
    }

 // state data for each delivery transactions executed through the smart contract
// when status = 0, it means pending
// when status = 1, it means in progress
// when status = 2, it means completed
// when status = 3, it means confirmed
    struct deliveryState {
        uint deliveryFee;
        uint status;
        uint hashedDeliveryDetails;
        address owner;
        address assistingMember;
    }

    // mapping are live arrays with key and value pairs
    mapping (address => memberState) public membersData;
    mapping (address => uint) members;
    mapping (uint => deliveryState) public deliveryData;
}
Enter fullscreen mode Exit fullscreen mode

Rules or modifiers

Smart contract rules in solidity are defined using the modifier keyword. Modifiers are used for restricting access to data or functions in a smart contract. In this smart contract, you have only one modifier,

  • onlyMember -> This modifier allows only accounts that are registered as a member of the smart contract to access a particular data or function.
 modifier onlyMember{
        require(members[msg.sender] == 1);
        _;
    }
Enter fullscreen mode Exit fullscreen mode

The msg used in the modifier is a global variable in solidity. It holds the information of a transaction. msg.sender gets the address of the account that initiated the transaction, while msg.value gets the ether (Ethereum cryptocurrency) sent with the transaction.

Add the modifier to your smart contract:

pragma solidity >=0.4.16 <0.9.0;
contract Logistics {

 // The address of the account that launched the smart contract
    address initiator;

    // state data for each member registered on the contract
    struct memberState {
        uint balance;
        uint reputation;
    }

    // state data for each delivery transactions executed through the smart contract
    struct deliveryState {
        uint deliveryFee;
        uint status;
        uint hashedDeliveryDetails;
        address owner;
        address assistingMember;
    }

    // mapping are live arrays with key and value pairs
    mapping (address => memberState) public membersData;
    mapping (address => uint) members;
    mapping (uint => deliveryState) public deliveryData;

 // modifiers
    modifier onlyMember{
        require(members[msg.sender] == 1);
        _;
    }
}
Enter fullscreen mode Exit fullscreen mode

Functions

These are the actions and use-cases of the smart contract. You need to perform verification and validations before performing any action. Verification can be achieved by attaching the right modifier to the function, while validations can be achieved by using simple conditional statements. To return meaningful responses during verification and validation, let's add some error handlers to our smart contract.

 // errors
    error notAuthorized(string reason);
    error insufficientFee(string reason, uint registrationFee);
Enter fullscreen mode Exit fullscreen mode

For your smart contract, you have 7 functions:

  • constructor -> This function is common to every smart contract and it is called when launching the contract.
 constructor() public payable {
        initiator = msg.sender;
        // here, you will register the initiator of the contract as a member
        members[msg.sender] = 1;
        membersData[msg.sender].balance = msg.value;
        membersData[msg.sender].reputation = 10;
    }
Enter fullscreen mode Exit fullscreen mode

The public keyword indicates that the function can be called outside the smart contract while the payable keyword indicates that the function requires cryptocurrency input.

Here, you set the initiator address and added the account as the first member of the smart contract.

Remember: msg.sender and msg.value are both global variables. msg.sender returns the address to the account that is calling a function while msg.value returns the value of the cryptocurrency attached to the transaction calling the function.

  • register -> This function is used to self-register an account into the smart contract.
 function register() public payable {
        uint registrationFee = msg.value;
        address newMember = msg.sender;
        if (registrationFee < 10)
           revert insufficientFee({
                reason: "Ether value is less than the required registration fee",
                registrationFee: 10
            });
        members[newMember] = 1;
        membersData[newMember].balance = msg.value;
        membersData[newMember].reputation = 10;
    }
Enter fullscreen mode Exit fullscreen mode

Here, you validated a registration fee of 10 ethers and threw in an error for lesser fees. You also set the membership state, balance, and default reputation for the new member.

  • requestForDelivery -> A member calls this function to request for delivery assistance. The member must send a hashed delivery detail and an equivalent cryptocurrency value when calling the function.
function requestForDelivery(uint hashedDeliveryDetails) onlyMember public payable {
        if(members[msg.sender] != 1)
           revert notAuthorized({
                reason: "Only members of this contract can request for delivery"
            });

         uint senderReputation = membersData[msg.sender].reputation;
         if(senderReputation < 6)
            revert notAuthorized({
                reason: "You have a low reputation, complete more deliveries to increase your reputations"
            });

        deliveryData[hashedDeliveryDetails].deliveryFee = msg.value;
        deliveryData[hashedDeliveryDetails].status = 0;
        deliveryData[hashedDeliveryDetails].status = hashedDeliveryDetails;
        deliveryData[hashedDeliveryDetails].owner = msg.sender;

        membersData[msg.sender].balance = membersData[msg.sender].balance + msg.value;
    }
Enter fullscreen mode Exit fullscreen mode

In the code above, you used the onlyMember modifier to verify accounts that can access the function. The payable keyword is an indication that payment in the form of cryptocurrency is required. You checked for the reputation of the member to verify its access to request assistance. You also saved the delivery data and temporarily stored the delivery fee in the wallet of the delivery owner. The delivery fee would be sent to the assisting member after completing the delivery.

  • respondToDeliveryRequest -> Anyone can access the deliveryData since it is public. Interested members can respond to the delivery request by using the hashedDeliveryDetails to respond and handle the delivery.
 function respondToDeliveryRequest(uint hashedDeliveryDetails) onlyMember public {
        if(members[msg.sender] != 1)
           revert notAuthorized({
                reason: "Only members of this contract can respond to delivery request"
            });
        uint memberReputation = membersData[msg.sender].reputation;
         if(memberReputation < 1)
            revert notAuthorized({
                reason: "You have exhausted your reputations"
            });
        uint deliveryStatus = deliveryData[hashedDeliveryDetails].status;
        if(deliveryStatus != 0)
            revert notAuthorized({
                reason: "This delivery is no longer available"
            });

        deliveryData[hashedDeliveryDetails].status = 1;
        deliveryData[hashedDeliveryDetails].assistingMember = msg.sender;
    }
Enter fullscreen mode Exit fullscreen mode

Above, you prevented members with no reputations from assisting in any delivery. You also checked the delivery status to prevent multiple delivery assistance, then updated its status and the assisting member’s address.

  • markDeliveryAsCompleted -> The assisting member of delivery calls this function after completing the delivery.
function markDeliveryAsCompleted(uint hashedDeliveryDetails) onlyMember public {
        if(members[msg.sender] != 1)
           revert notAuthorized({
                reason: "Only members of this contract can update this delivery"
            });
        address assistingMember = deliveryData[hashedDeliveryDetails].assistingMember;
        if(assistingMember != msg.sender) 
            revert notAuthorized({
                reason: "You don't have access to update this delivery"
            });
        deliveryData[hashedDeliveryDetails].status = 2;
    }
Enter fullscreen mode Exit fullscreen mode

Here, you ensured the account calling the function has the right access then update the delivery status as completed if it does.

  • markDeliveryAsCancled -> This function is used to cancel delivery in progress. Only the delivery assisting member and the smart contract initiator can access it.
function markDeliveryAsCancled(uint hashedDeliveryDetails) onlyMember public {
        if(members[msg.sender] != 1)
           revert notAuthorized({
                reason: "Only members of this contract can update this delivery"
            });
        address assistingMember = deliveryData[hashedDeliveryDetails].assistingMember;
        if(assistingMember != msg.sender && msg.sender != initiator) 
            revert notAuthorized({
                reason: "You don't have access to update this delivery"
            });

        deliveryData[hashedDeliveryDetails].status = 0;

        if (assistingMember == msg.sender) {
             membersData[msg.sender].reputation = membersData[msg.sender].reputation - 5;
        }

        if (msg.sender == initiator) {
             membersData[msg.sender].reputation = membersData[msg.sender].reputation - 6;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Here, if the delivery was canceled by the assisting member, its reputation drops by 5, but if it was done by the initiator, its reputation will drop by 6.

  • markDeliveryAsConfirmed -> This function is used to confirm delivery. It can only be called by the owner of the delivery and the contract initiator who calls it when the owner is not available.
function markDeliveryAsConfirmed(uint hashedDeliveryDetails) onlyMember public {
        if(members[msg.sender] != 1)
           revert notAuthorized({
                reason: "Only members of this contract can update this delivery"
            });
        address deliveryOwner = deliveryData[hashedDeliveryDetails].owner;
        if(deliveryOwner != msg.sender && msg.sender != initiator) 
            revert notAuthorized({
                reason: "You don't have access to confirm this delivery"
            });

        deliveryData[hashedDeliveryDetails].status = 3;

        address assistingMember = deliveryData[hashedDeliveryDetails].assistingMember;
        uint deliveryFee = deliveryData[hashedDeliveryDetails].deliveryFee;

        membersData[deliveryOwner].balance = membersData[deliveryOwner].balance - deliveryFee;

        membersData[assistingMember].reputation = membersData[assistingMember].reputation + 3;
        membersData[assistingMember].balance =  membersData[assistingMember].balance + deliveryFee;

    }
Enter fullscreen mode Exit fullscreen mode

Here, you removed the delivery fee from the delivery owner's balance and added it to the assisting member's balance.

This is the full code for our logistics smart contract:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;

contract Logistics {
    // The address of the account that launched the smart contract
    address initiator;

    // state data for each member registered on the contract
    struct memberState {
        uint balance;
        uint reputation;
    }

 // state data for each delivery transactions executed through the smart contract
// when status = 0, it means pending
// when status = 1, it means in progress
// when status = 2, it means completed
// when status = 3, it means confirmed
    struct deliveryState {
        uint deliveryFee;
        uint status;
        uint hashedDeliveryDetails;
        address owner;
        address assistingMember;
    }

    // mapping are live arrays with key and value pairs
    mapping (address => memberState) public membersData;
    mapping (address => uint) members;
    mapping (uint => deliveryState) public deliveryData;

    // modifiers
    modifier onlyMember{
        require(members[msg.sender] == 1);
        _;
    }

    // errors
    error notAuthorized(string reason);
    error insufficientFee(string reason, uint registrationFee);

    constructor() public payable {
        initiator = msg.sender;
        // here you will register the initiator of the contract as a member
        members[msg.sender] = 1;
        membersData[msg.sender].balance = msg.value;
        membersData[msg.sender].reputation = 10;
    }

    function register() public payable {
        uint registrationFee = msg.value;
        address newMember = msg.sender;
        if (registrationFee < 10)
           revert insufficientFee({
                reason: "Ether value is less than the required registration fee",
                registrationFee: 10
            });
        members[newMember] = 1;
        membersData[newMember].balance = msg.value;
        membersData[newMember].reputation = 10;
    }

    function requestForDelivery(uint hashedDeliveryDetails) onlyMember public payable {
        if(members[msg.sender] != 1)
           revert notAuthorized({
                reason: "Only members of this contract can request for delivery"
            });

         uint senderReputation = membersData[msg.sender].reputation;
         if(senderReputation < 6)
            revert notAuthorized({
                reason: "You have a low reputation. Complete more deliveries to increase your reputation".
            });

        deliveryData[hashedDeliveryDetails].deliveryFee = msg.value;
        deliveryData[hashedDeliveryDetails].status = 0;
        deliveryData[hashedDeliveryDetails].status = hashedDeliveryDetails;
        deliveryData[hashedDeliveryDetails].owner = msg.sender;

        membersData[msg.sender].balance = membersData[msg.sender].balance + msg.value;
    }

    function respondToDeliveryRequest(uint hashedDeliveryDetails) onlyMember public {
        if(members[msg.sender] != 1)
           revert notAuthorized({
                reason: "Only members of this contract can respond to delivery request"
            });
        uint memberReputation = membersData[msg.sender].reputation;
         if(memberReputation < 1)
            revert notAuthorized({
                reason: "You have exhausted your reputations"
            });
        uint deliveryStatus = deliveryData[hashedDeliveryDetails].status;
        if(deliveryStatus != 0)
            revert notAuthorized({
                reason: "This delivery is no longer available".
            });

        deliveryData[hashedDeliveryDetails].status = 1;
        deliveryData[hashedDeliveryDetails].assistingMember = msg.sender;
    }

    function markDeliveryAsCompleted(uint hashedDeliveryDetails) onlyMember public {
        if(members[msg.sender] != 1)
           revert notAuthorized({
                reason: "Only members of this contract can update this delivery"
            });
        address assistingMember = deliveryData[hashedDeliveryDetails].assistingMember;
        if(assistingMember != msg.sender) 
            revert notAuthorized({
                reason: "You don't have access to update this delivery".
            });
        deliveryData[hashedDeliveryDetails].status = 2;
    }

    function markDeliveryAsCancled(uint hashedDeliveryDetails) onlyMember public {
        if(members[msg.sender] != 1)
           revert notAuthorized({
                reason: "Only members of this contract can update this delivery"
            });
        address assistingMember = deliveryData[hashedDeliveryDetails].assistingMember;
        if(assistingMember != msg.sender && msg.sender != initiator) 
            revert notAuthorized({
                reason: "You don't have access to update this delivery"
            });

        deliveryData[hashedDeliveryDetails].status = 0;

        if (assistingMember == msg.sender) {
             membersData[msg.sender].reputation = membersData[msg.sender].reputation - 5;
        }

        if (msg.sender == initiator) {
             membersData[msg.sender].reputation = membersData[msg.sender].reputation - 6;
        }
    }

    function markDeliveryAsConfirmed(uint hashedDeliveryDetails) onlyMember public {
        if(members[msg.sender] != 1)
           revert notAuthorized({
                reason: "Only members of this contract can update this delivery"
            });
        address deliveryOwner = deliveryData[hashedDeliveryDetails].owner;
        if(deliveryOwner != msg.sender && msg.sender != initiator) 
            revert notAuthorized({
                reason: "You don't have access to confirm this delivery"
            });

        deliveryData[hashedDeliveryDetails].status = 3;

        address assistingMember = deliveryData[hashedDeliveryDetails].assistingMember;
        uint deliveryFee = deliveryData[hashedDeliveryDetails].deliveryFee;

        membersData[deliveryOwner].balance = membersData[deliveryOwner].balance - deliveryFee;

        membersData[assistingMember].reputation = membersData[assistingMember].reputation + 3;
        membersData[assistingMember].balance =  membersData[assistingMember].balance + deliveryFee;

    }

}
Enter fullscreen mode Exit fullscreen mode

Congratulations! You just built a Logistics smart contract.

Conclusion

In this article, you learned how to build a smart contract to help Logistics companies become more efficient in how they collaborate . In the next article, you would learn how to deploy and test the smart contract.

Let's connect.

👉 On Twitter @drayfocus

👉 On LinkedIn Akinola Ayomide

Cheers!

Top comments (0)