Building Agro Supply Chain Smart Contract with Solidity on Celo

Introduction

In this tutorial, we will be building a supply chain smart contract that keeps track of farm products from the farmer to the end consumer. The contract is built using Solidity and deployed on the Celo blockchain. By employing this approach, participants in the supply chain can enhance transparency, reduce costs, and bolster security.

To illustrate this further, let’s consider a hypothetical scenario: a farmer produces a product and adds it to the inventory of available items that can be purchased by a distributor. The distributor then processes the product and prepares it for sale. Subsequently, a retailer acquires the product from the distributor and ultimately offers it for sale to the end consumers.

Prerequisites

To follow this tutorial, you will need:

  • Node.js
  • Some knowledge of Solidity

Requirements

Writing the Smart Contract

Navigate to the hardhat folder and create a new file supplyChain.sol

supplyChain.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0

contract SupplyChain {
       enum ProductStatus { ForSale, Processed, Sold, Shipped}

Here, we’ll start by declaring our contract supplyChain Inside the contract, there is an enum declaration named ProductStatus that defines four possible values: ForSale, Processed, Shipped, and Sold. The enum is used to represent the status of a product within the supply chain

Struct

   struct Product {
        uint productId;
        string productName;
        uint quantity;
        uint price;
        address payable farmer;
        address payable distributor;
        address payable retailer;
        ProductStatus status;
    }

uint productId: An unsigned integer representing the unique identifier of the product.

string productName: A string representing the name of the product.

uint quantity: An unsigned integer indicating the quantity of the product.

uint price: An unsigned integer denoting the price of the product.

address payable farmer: This is the payable address of the farmer associated with the product.

address payable distributor: A payable Ethereum address representing the address of the distributor associated with the product.

address payable retailer: A payable Ethereum address representing the address of the retailer associated with the product.

ProductStatus status: A variable of the enum type ProductStatus that indicates the current state of the product.

Constructor

   constructor() {
        owner = msg.sender;  
    }

    address public owner;

    mapping(uint => Product) public products;
    uint public productCount;

The constructor initializes the owner variable with the address of the contract deployer and declares it as public.

The public mapping products stores product information based on their unique IDs while productCount keeps track of the total number of products in the supply chain.

Event

    event ProductAdded(uint productId, string productName, uint quantity, uint price, address farmer);
    event ProductProcessed(uint productId, address distributor);
    event ProductSold(uint productId, address retailer);
    event ProductShipped(uint productId, address retailer);

event ProductAdded is triggered when a product is added. It emits the product’s ID, name, quantity, price, and the address of the associated farmer.

event ProductProcessed is triggered when a product is processed. It emits the product’s ID and the address of the distributor responsible for the processing.

event ProductSold is triggered when a product is sold. It emits the product’s ID and the address of the retailer who purchased the product.

even ProductShipped is triggered when a product is shipped. It emits the product’s ID and the address of the retailer shipping the product to the consumer.

Modifier

  modifier onlyFarmer(uint _productId) {
       require(msg.sender == products[_productId].farmer, "Only the farmer can perform this action.");
       _;
   }

   modifier onlyDistributor(uint _productId) {
       require(msg.sender == products[_productId].distributor, "Only the distributor can perform this action.");
       _;
   }

   modifier onlyRetailer(uint _productId) {
       require(msg.sender == products[_productId].retailer, "Only the retailer can perform this action.");
       _;
   }

   modifier productExists(uint _productId) {
       require(_productId <= productCount, "Product does not exist.");
       _;
   }

The modifiers ensure that only the roles associated with a specific product can perform an action. It verifies if the msg.sender matches the address of the respective role associated with the product. If not, it throws an error message.

addProduct

   function addProduct(string memory _productName, uint _quantity, uint _price) public {
        productCount++;
        products[productCount] = Product(productCount, _productName, _quantity, _price, payable(msg.sender), payable(address(0)), payable(address(0)), ProductStatus.ForSale);
        emit ProductAdded(productCount, _productName, _quantity, _price, msg.sender);
    }

The function addProduct adds a product to the mapping, increments the productCount variable, and emits the corresponding event ProductAdded.

processProduct

function processProduct(uint _productId) public productExists(_productId)        
      onlyDistributor(_productId) {
      require(products[_productId].status == ProductStatus.ForSale, "Product not available for distribution.");

        products[_productId].distributor = payable(msg.sender);
        products[_productId].status = ProductStatus.Processed;
        emit ProductProcessed(_productId, msg.sender);
    }

The processProductfunction processes a product for distribution by updating the distributor address, changing the product status, and emitting the corresponding event. It requires that the product exists, and only the associated distributor can execute this function. Additionally, the product must be in the ForSale state to be eligible for distribution.

sellProduct

  function sellProduct(uint _productId) public productExists(_productId) onlyDistributor(_productId) {
        require(products[_productId].status == ProductStatus.Processed, "Product not available for sale.");

        products[_productId].retailer = payable(msg.sender);
        products[_productId].status = ProductStatus.Sold;
        emit ProductSold(_productId, msg.sender);
    }

The sellProduct function enables the product to be sold by performing tasks such as updating its status, assigning the retailer, and emitting an event to notify about the sale.

buyProduct

  function buyProduct(uint _productId) public payable productExists(_productId) {
       require(products[_productId].status == ProductStatus.ForSale, "Product not available for sale.");
        require(msg.value >= products[_productId].price, "Insufficient payment.");

        products[_productId].retailer = payable(msg.sender);
        products[_productId].status = ProductStatus.Sold;
        emit ProductSold(_productId, msg.sender);
    }

The buyProduct function allows a buyer to purchase a product by providing the necessary payment. It verifies that the product is available for sale and that the payment is sufficient. If the conditions are met, it updates the retailer and status of the product.

shipProduct

function shipProduct(uint _productId) public productExists(_productId)  
        onlyRetailer(_productId) {
        require(products[_productId].status == ProductStatus.Sold, "Product not sold yet.");

        products[_productId].status = ProductStatus.Shipped;
        emit ProductShipped(_productId, msg.sender);
}

The shipProduct function ensures that only the retailer associated with a specific product can execute the shipProduct function. It checks if the product has been sold and updates its status to “Shipped” before emitting the ProductShipped event.

Your final code should be looking like this:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SupplyChain {
    enum ProductStatus { ForSale, Processed, Sold, Shipped }

    struct Product {
        uint productId;
        string productName;
        uint quantity;
        uint price;
        address payable farmer;
        address payable distributor;
        address payable retailer;
        ProductStatus status;
    }



    constructor() {
        owner = msg.sender;  
    }

    address public owner;

    mapping(uint => Product) public products;
    uint public productCount;

    event ProductAdded(uint productId, string productName, uint quantity, uint price, address farmer);
    event ProductProcessed(uint productId, address distributor);
    event ProductSold(uint productId, address retailer);
    event ProductShipped(uint productId, address retailer);

    modifier onlyFarmer(uint _productId) {
        require(msg.sender == products[_productId].farmer, "Only the farmer can perform this action.");
        _;
    }

    modifier onlyDistributor(uint _productId) {
        require(msg.sender == products[_productId].distributor, "Only the distributor can perform this action.");
        _;
    }

    modifier onlyRetailer(uint _productId) {
        require(msg.sender == products[_productId].retailer, "Only the retailer can perform this action.");
        _;
    }

    modifier productExists(uint _productId) {
        require(_productId <= productCount, "Product does not exist.");
        _;
    }

    function addProduct(string memory _productName, uint _quantity, uint _price) public {
        productCount++;
        products[productCount] = Product(productCount, _productName, _quantity, _price, payable(msg.sender), payable(address(0)), payable(address(0)), ProductStatus.ForSale);
        emit ProductAdded(productCount, _productName, _quantity, _price, msg.sender);
    }

    function processProduct(uint _productId) public productExists(_productId) onlyDistributor(_productId) {
        require(products[_productId].status == ProductStatus.ForSale, "Product not available for distribution.");

        products[_productId].distributor = payable(msg.sender);
        products[_productId].status = ProductStatus.Processed;
        emit ProductProcessed(_productId, msg.sender);
    }


  function sellProduct(uint _productId) public productExists(_productId) onlyDistributor(_productId) {
        require(products[_productId].status == ProductStatus.Processed, "Product not available for sale.");

        products[_productId].retailer = payable(msg.sender);
        products[_productId].status = ProductStatus.Sold;
        emit ProductSold(_productId, msg.sender);
    }

    function buyProduct(uint _productId) public payable productExists(_productId) {
        require(products[_productId].status == ProductStatus.ForSale, "Product not available for sale.");
        require(msg.value >= products[_productId].price, "Insufficient payment.");

        products[_productId].retailer = payable(msg.sender);
        products[_productId].status = ProductStatus.Sold;
        emit ProductSold(_productId, msg.sender);
    }


    function shipProduct(uint _productId) public productExists(_productId) onlyRetailer(_productId) {
        require(products[_productId].status == ProductStatus.Sold, "Product not sold yet.");

        products[_productId].status = ProductStatus.Shipped;
        emit ProductShipped(_productId, msg.sender);
    }
}

Deployment

To ensure a successful deployment of our smart contract, we need to have the celo extension wallet. You can download it here

To proceed, we also need to fund the wallet we have just created. This can be done using the celo alfojares faucet

Once these initial steps are complete, locate the plugin logo positioned at the bottom left corner and initiate a search for the celo plugin.

Install the plugin and you will notice the celo logo appearing in the side tab once the installation is complete.

After connecting your celo wallet, you will be able to choose the desired contract for deployment.

To deploy, compile the supplyChain.sol contract and then click on the deploy button

Conclusion

We have now come to the end of this tutorial. By following this tutorial, you should have a good understanding of how to build an agro supply chain smart contract with solidity on the celo blockchain.

References

4 Likes

Fantastic news! Your proposal has landed in this week’s top voted list. As you begin your project journey, remember to align with our community and technical guidelines, ensuring a high quality platform for our developers. Congratulations! :mortar_board: :seedling:

3 Likes

i will be reviewing this.

3 Likes

i can’t see any option to put the product to sale

2 Likes

I have included it, please recheck.

2 Likes

the smart contract doesn’t work correct way

1 Like

I will be taking over the review.

1 Like

Alright, Thank you

1 Like