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