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 processProduct
function 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.