Writing a smart contract to handle decentralized agricultural supply chain tracking using solidity on the celo blockchain

Writing a smart contract to handle decentralized agricultural supply chain tracking using solidity on the celo blockchain https://celo.academy/uploads/default/optimized/2X/f/f82edbae68ede5a841d41f90ab802a4c73609f76_2_1024x576.png
none 0.0 0

Introduction

In today’s digital world, the agriculture industry is one of the sectors experiencing significant transformation. The conventional methods of tracking products in supply chains are now being enhanced by blockchain technology. This tutorial is designed to introduce you to the concept of using blockchain for agricultural supply chain management. By the end of this tutorial, you will have a solid understanding of how to create a decentralized tracking system using Solidity, a contract-oriented programming language for Celo blockchain.

This tutorial focuses on the implementation of a smart contract for agricultural supply chain management on the Celo blockchain. We will be utilizing Solidity language to write the contract and OpenZeppelin’s contracts for role-based permissions and ERC20 token transactions. By using Solidity, we can create a system that is not only transparent and decentralized but also secure and efficient, thanks to the inherent characteristics of the Celo network.

Contract code Repo

Prerequisites

To follow this tutorial, you will need the following:

  • Basic knowledge of Solidity programming language.

  • A Development Environment Like Remix.

  • The celo Extension Wallet.

SmartContract

Let’s begin writing our smart contract in Remix IDE

The completed code Should look like this.

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SupplyChain is AccessControl {
    
    IERC20 private currencyToken; 

    bytes32 public constant PRODUCER_ROLE = keccak256("PRODUCER_ROLE");
    bytes32 public constant SHIPPER_ROLE = keccak256("SHIPPER_ROLE");
    bytes32 public constant BUYER_ROLE = keccak256("BUYER_ROLE");

    struct Product {
        string name;
        address producer;
        address shipper;
        address buyer;
        string origin;
        string location;
        uint256 price;
        bool isReceived;
        bool isPaid;
        bool isDisputed;
    }

    uint public productCount = 0;
    mapping(uint => Product) public products;
    mapping(address => bool) public verifiedUsers;

    event ProductCreated(
        uint productId,
        string name,
        address producer,
        string origin,
        uint256 price
    );

    event ProductShipped(
        uint productId,
        address shipper,
        string location
    );

    event ProductReceived(
        uint productId,
        address buyer
    );

    event PaymentTransferred(
        uint productId,
        address buyer,
        address producer,
        uint256 amount
    );

    event DisputeRaised(
        uint productId,
        address disputer
    );

    event DisputeResolved(
        uint productId
    );

    constructor(IERC20 _currencyToken) {
        currencyToken = _currencyToken;
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function verifyUser(address user) public {
        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Caller is not an admin");
        verifiedUsers[user] = true;
    }

    function createProduct(string memory _name, string memory _origin, uint256 _price) public {
        require(hasRole(PRODUCER_ROLE, msg.sender), "Caller is not a producer");
        require(verifiedUsers[msg.sender], "Caller is not verified");

        productCount++;
        products[productCount] = Product(_name, msg.sender, address(0), address(0), _origin, _origin, _price, false, false, false);

        emit ProductCreated(productCount, _name, msg.sender, _origin, _price);
    }
    
    function shipProduct(uint _productId, string memory _location) public {
        require(hasRole(SHIPPER_ROLE, msg.sender), "Caller is not a shipper");
        require(verifiedUsers[msg.sender], "Caller is not verified");

        Product memory _product = products[_productId];
        require(_product.producer != address(0), "This product does not exist");
        require(_product.isReceived == false, "This product has already been received");
        require(_product.isPaid == true, "This product has not been paid for");
        require(_product.isDisputed == false, "This product is under dispute");

        _product.shipper = msg.sender;
        _product.location = _location;
        products[_productId] = _product;

        emit ProductShipped(_productId, msg.sender, _location);
    }
    
    function receiveProduct(uint _productId) public {
        require(hasRole(BUYER_ROLE,
        msg.sender), "Caller is not a buyer");
        require(verifiedUsers[msg.sender], "Caller is not verified");

        Product memory _product = products[_productId];
        require(_product.producer != address(0), "This product does not exist");
        require(_product.isReceived == false, "This product has already been received");
        require(_product.isPaid == true, "This product has not been paid for");
        require(_product.isDisputed == false, "This product is under dispute");

        _product.buyer = msg.sender;
        _product.isReceived = true;
        products[_productId] = _product;

        emit ProductReceived(_productId, msg.sender);
    }

    function payForProduct(uint _productId) public {
        Product memory _product = products[_productId];
        require(_product.producer != address(0), "This product does not exist");
        require(_product.isReceived == false, "This product has already been received");
        require(_product.isPaid == false, "This product has already been paid for");

        uint256 price = _product.price;
        require(currencyToken.balanceOf(msg.sender) >= price, "Insufficient balance to pay for this product");

        currencyToken.transferFrom(msg.sender, _product.producer, price);
        _product.isPaid = true;
        products[_productId] = _product;

        emit PaymentTransferred(_productId, msg.sender, _product.producer, price);
    }

    function raiseDispute(uint _productId) public {
        Product memory _product = products[_productId];
        require(_product.producer != address(0), "This product does not exist");
        require((_product.buyer == msg.sender || _product.producer == msg.sender), "Only buyer or producer can raise a dispute");
        require(_product.isDisputed == false, "This product is already under dispute");

        _product.isDisputed = true;
        products[_productId] = _product;

        emit DisputeRaised(_productId, msg.sender);
    }

    function resolveDispute(uint _productId) public {
        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Caller is not an admin");

        Product memory _product = products[_productId];
        require(_product.producer != address(0), "This product does not exist");
        require(_product.isDisputed == true, "This product is not under dispute");

        _product.isDisputed = false;
        products[_productId] = _product;

        emit DisputeResolved(_productId);
    }
}

Contract Breakdown

Imports and Contract Declaration

// SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SupplyChain is AccessControl {

At the top of the contract, the necessary OpenZeppelin contracts are imported. Then, the contract SupplyChain is declared, inheriting from OpenZeppelin’s AccessControl .

Role Definitions and Structs

  
    IERC20 private currencyToken; 

    bytes32 public constant PRODUCER_ROLE = keccak256("PRODUCER_ROLE");
    bytes32 public constant SHIPPER_ROLE = keccak256("SHIPPER_ROLE");
    bytes32 public constant BUYER_ROLE = keccak256("BUYER_ROLE");

    struct Product {
        string name;
        address producer;
        address shipper;
        address buyer;
        string origin;
        string location;
        uint256 price;
        bool isReceived;
        bool isPaid;
        bool isDisputed;
    }

The contract defines roles for various parties involved in the supply chain: producers, shippers, and buyers. It also declares a Product struct, which contains all the information related to a product in the supply chain.

State Variables

    uint public productCount = 0;
    mapping(uint => Product) public products;
    mapping(address => bool) public verifiedUsers;

The contract declares several state variables to keep track of products and verified users. The productCount variable keeps track of the total number of products. The products mapping is used to store all the products, where each product is associated with a unique id. The verifiedUsers mapping stores a boolean value for each user address indicating whether the user is verified or not.

Events

 event ProductCreated(
        uint productId,
        string name,
        address producer,
        string origin,
        uint256 price
    );

    event ProductShipped(
        uint productId,
        address shipper,
        string location
    );

    event ProductReceived(
        uint productId,
        address buyer
    );

    event PaymentTransferred(
        uint productId,
        address buyer,
        address producer,
        uint256 amount
    );

    event DisputeRaised(
        uint productId,
        address disputer
    );

    event DisputeResolved(
        uint productId
    );

Several events are declared to provide notifications about significant actions that have taken place within the contract, such as the creation of a product, a product being shipped, a product being received, payment being transferred, and disputes being raised or resolved.

Constructor

 constructor(IERC20 _currencyToken) {
        currencyToken = _currencyToken;
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

The constructor sets the ERC20 token that will be used for payments in the supply chain. It also assigns the contract deployer the default admin role, which allows the deployer to assign roles to other users.

Function for Verifying Users

 function verifyUser(address user) public {
        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Caller is not an admin");
        verifiedUsers[user] = true;
    }

The verifyUser function allows an admin to verify a user. This is a simple way to ensure that only legitimate parties can participate in the supply chain.

Function for Creating Products

 function createProduct(string memory _name, string memory _origin, uint256 _price) public {
        require(hasRole(PRODUCER_ROLE, msg.sender), "Caller is not a producer");
        require(verifiedUsers[msg.sender], "Caller is not verified");

        productCount++;
        products[productCount] = Product(_name, msg.sender, address(0), address(0), _origin, _origin, _price, false, false, false);

        emit ProductCreated(productCount, _name, msg.sender, _origin, _price);
    }

The createProduct function allows a verified producer to create a new product. It requires the name, origin, and price of the product as arguments.

Function for Shipping Products

 function shipProduct(uint _productId, string memory _location) public {
        require(hasRole(SHIPPER_ROLE, msg.sender), "Caller is not a shipper");
        require(verifiedUsers[msg.sender], "Caller is not verified");

        Product memory _product = products[_productId];
        require(_product.producer != address(0), "This product does not exist");
        require(_product.isReceived == false, "This product has already been received");
        require(_product.isPaid == true, "This product has not been paid for");
        require(_product.isDisputed == false, "This product is under dispute");

        _product.shipper = msg.sender;
        _product.location = _location;
        products[_productId] = _product;

        emit ProductShipped(_productId, msg.sender, _location);
    }

The shipProduct function allows a verified shipper to mark a product as shipped. It requires the product id and the new location of the product as arguments.

Function for Receiving Products

function receiveProduct(uint _productId) public {
        require(hasRole(BUYER_ROLE,
        msg.sender), "Caller is not a buyer");
        require(verifiedUsers[msg.sender], "Caller is not verified");

        Product memory _product = products[_productId];
        require(_product.producer != address(0), "This product does not exist");
        require(_product.isReceived == false, "This product has already been received");
        require(_product.isPaid == true, "This product has not been paid for");
        require(_product.isDisputed == false, "This product is under dispute");

        _product.buyer = msg.sender;
        _product.isReceived = true;
        products[_productId] = _product;

        emit ProductReceived(_productId, msg.sender);
    }

The receiveProduct function allows a verified buyer to mark a product as received. It requires the product id as an argument.

Function for Paying for Products

 function payForProduct(uint _productId) public {
        Product memory _product = products[_productId];
        require(_product.producer != address(0), "This product does not exist");
        require(_product.isReceived == false, "This product has already been received");
        require(_product.isPaid == false, "This product has already been paid for");

        uint256 price = _product.price;
        require(currencyToken.balanceOf(msg.sender) >= price, "Insufficient balance to pay for this product");

        currencyToken.transferFrom(msg.sender, _product.producer, price);
        _product.isPaid = true;
        products[_productId] = _product;

        emit PaymentTransferred(_productId, msg.sender, _product.producer, price);
    }

The payForProduct function allows a user to pay for a product using the specified ERC20 token. The function checks that the user has enough tokens to cover the cost, then transfers the tokens from the user to the producer.

Functions for Handling Disputes

function raiseDispute(uint _productId) public {
        Product memory _product = products[_productId];
        require(_product.producer != address(0), "This product does not exist");
        require((_product.buyer == msg.sender || _product.producer == msg.sender), "Only buyer or producer can raise a dispute");
        require(_product.isDisputed == false, "This product is already under dispute");

        _product.isDisputed = true;
        products[_productId] = _product;

        emit DisputeRaised(_productId, msg.sender);
    }

 function resolveDispute(uint _productId) public {
        require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Caller is not an admin");

        Product memory _product = products[_productId];
        require(_product.producer != address(0), "This product does not exist");
        require(_product.isDisputed == true, "This product is not under dispute");

        _product.isDisputed = false;
        products[_productId] = _product;

        emit DisputeResolved(_productId);
    }

The raiseDispute and resolveDispute functions allow disputes to be raised and resolved. Only the buyer or producer of a product can raise a dispute, and only an admin can resolve a dispute.

Deployment

Install the Celo Plugin

First, you’ll need to install the Celo Plugin for Remix. To do this, open Remix and click on the Plugin Manager icon on the left-hand side. Search for Celo and click the Install button next to the Celo Plugin. Once the installation is complete, you’ll see a new Celo tab appear in the sidebar.

Connect to the Celo Alfajores Testnet

To deploy our smart contract successfully, we need the celo extention wallet which can be downloaded from here

Next, we need to fund our newly created wallet which can done using the celo alfojares faucet Here

Next, you’ll need to connect Remix to the Celo Testnet. Click on the Celo tab in the sidebar and then click on the Connect to Network button.

Compile Contract

Open the SupplyChain.sol file in Remix and click on the Solidity Compiler tab in the sidebar. Click the SupplyChain.sol button to compile the contract.

Deploy the Contract

Click on the Deploy & Run Transactions tab in the sidebar. In the Contract dropdown menu, select SupplyChain.sol.

Interact with the Contract

Once the contract is deployed, you can interact with it using the functions in the Deployed Contracts section of the Deploy & Run Transactions tab. You’ll need to connect to the contract using the At Address button and entering the contract address. From there, you can call the various functions of the contract.

That’s it! With these steps, you should be able to deploy the SupplyChain smart contract to the Celo Testnet using Remix and the Celo Plugin.

Conclusion

In this tutorial, we have delved into the creation of a smart contract for managing an agricultural supply chain on the Celo blockchain. We have explored the different aspects of the contract, including the use of OpenZeppelin’s AccessControl for role-based access control and IERC20 for handling token transactions.

We’ve walked through the process of creating products, shipping them, verifying users, receiving products, transferring payments, and handling disputes. All these functionalities were implemented using Solidity on the Celo blockchain, providing a reliable, transparent, and decentralized system for agricultural supply chain management.

Learn More

I hope you learned a lot from this tutorial. Here are some relevant links that would aid your learning further.

About the author

Hello, I’am Richard Michael, a fullstack web3 developer.

4 Likes

Approved for you to get started. You can manage the tutorial here by changing the category to Proposals > In Progress then Proposals > Review as you complete the tutorial. Thanks!

i will review this @richiemikke

The function for receiving product headline is not in the right format. Fix that and you’ll be ready for publish.

@4undRaiser i have fixed it.

@richiemikke looks good you can go ahead and move to publish

1 Like

Your tutorial has provide a breakdown of the contract’s code, explaining the purpose and functionality of each section, which aids in understanding the implementation. The deployment process is also briefly outlined, mentioning the Celo Plugin for Remix as a helpful tool. Welldone!