Introduction
In this tutorial, you will learn how to design and implement a smart contract-based system for a tokenized loyalty program using Solidity. The tutorial will cover the benefits of tokenized loyalty programs over traditional loyalty programs, and show how smart contracts can be used to create a loyalty program that rewards customers for their repeat business and loyalty.
The tutorial will guide you through designing a smart contract that handles the issuance and redemption of loyalty tokens, and writing the smart contract logic that facilitates the issuance and redemption of tokens and handles token transfers. The tutorial will also cover implementing a redemption mechanism for tokens and deploying the smart contract on 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.8.3;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
contract LoyaltyProgram is ERC20Capped, Ownable, Pausable {
mapping (address => bool) private _merchants;
mapping (address => uint256) private _discounts;
event DiscountSet(address indexed merchant, uint256 discount);
constructor(string memory name, string memory symbol, uint256 cap) ERC20(name, symbol) ERC20Capped(cap) {}
function mint(address recipient, uint256 amount) public onlyOwner {
require(ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded");
_mint(recipient, amount);
}
function burn(address account, uint256 amount) public onlyOwner {
_burn(account, amount);
}
function addMerchant(address merchant) public onlyOwner {
_merchants[merchant] = true;
}
function removeMerchant(address merchant) public onlyOwner {
_merchants[merchant] = false;
}
function isMerchant(address merchant) public view returns (bool) {
return _merchants[merchant];
}
function setDiscount(address merchant, uint256 discount) public onlyOwner {
require(_merchants[merchant], "Address is not a merchant");
_discounts[merchant] = discount;
emit DiscountSet(merchant, discount);
}
function getDiscount(address merchant) public view returns (uint256) {
require(_merchants[merchant], "Address is not a merchant");
return _discounts[merchant];
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function transfer(address recipient, uint256 amount) public override whenNotPaused returns (bool) {
return super.transfer(recipient, amount);
}
function transferFrom(address sender, address recipient, uint256 amount) public override whenNotPaused returns (bool) {
return super.transferFrom(sender, recipient, amount);
}
}
Contract Breakdown
pragma solidity ^0.8.3;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
contract LoyaltyProgram is ERC20Capped, Ownable, Pausable {
The contract imports and extends several classes from the OpenZeppelin contracts library:
ERC20
: This is a standard interface for Ethereum tokens, allowing them to interact with other tokens and contracts on the network.ERC20Capped
: This is an extension to ERC20 that limits the total supply of tokens that can ever be minted.Ownable
: This provides basic access control mechanism, with an account (the owner) that can be granted exclusive access to certain functions.Pausable
: This provides an emergency stop mechanism that can be triggered by the owner to pause certain functions.
mapping (address => bool) private _merchants;
mapping (address => uint256) private _discounts;
event DiscountSet(address indexed merchant, uint256 discount);
The contract also includes two mappings and one event:
_merchants
: This keeps track of addresses recognized as merchants._discounts
: This keeps track of discounts associated with each merchant.DiscountSet
: This event will be emitted when a discount is set for a merchant.
constructor(string memory name, string memory symbol, uint256 cap) ERC20(name, symbol) ERC20Capped(cap) {}
The constructor
function initializes the contract by setting the name, symbol, and cap for the tokens. It’s called when the contract is first deployed.
function mint(address recipient, uint256 amount) public onlyOwner {
require(ERC20.totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded");
_mint(recipient, amount);
}
The mint
function allows the owner to create new tokens and assign them to a recipient. It checks that creating more tokens would not exceed the cap.
function burn(address account, uint256 amount) public onlyOwner {
_burn(account, amount);
}
The burn
function allows the owner to destroy tokens from a specified account.
function addMerchant(address merchant) public onlyOwner {
_merchants[merchant] = true;
}
function removeMerchant(address merchant) public onlyOwner {
_merchants[merchant] = false;
}
The addMerchant
and removeMerchant
functions allow the owner to manage the list of recognized merchants.
function isMerchant(address merchant) public view returns (bool) {
return _merchants[merchant];
}
The isMerchant
function allows anyone to check whether a given address is recognized as a merchant.
function setDiscount(address merchant, uint256 discount) public onlyOwner {
require(_merchants[merchant], "Address is not a merchant");
_discounts[merchant] = discount;
emit DiscountSet(merchant, discount);
}
The setDiscount
function lets the owner set the discount rate for a given merchant. It emits a DiscountSet
event which can be tracked by external observers (like a frontend application).
function getDiscount(address merchant) public view returns (uint256) {
require(_merchants[merchant], "Address is not a merchant");
return _discounts[merchant];
}
The getDiscount
function lets anyone fetch the discount rate associated with a given merchant.
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
The pause
and unpause
functions allow the contract owner to pause or unpause the contract. While paused, certain functions like transfer
and transferFrom
cannot be executed. This can be a useful tool to mitigate damage in the event of a discovered vulnerability or during maintenance.
function transfer(address recipient, uint256 amount) public override whenNotPaused returns (bool) {
return super.transfer(recipient, amount);
}
function transferFrom(address sender, address recipient, uint256 amount) public override whenNotPaused returns (bool) {
return super.transferFrom(sender, recipient, amount);
}
The transfer
and transferFrom
functions are overridden from the ERC20
standard. They check whether the contract is paused and if not, call the original function.
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 LoyaltyProgram.sol
file in Remix and click on the Solidity Compiler
tab in the sidebar. Click the LoyaltyProgram.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 LoyaltyProgram
.
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 LoyaltyProgram
smart contract to the Celo Testnet using Remix and the Celo Plugin.
Conclusion
In this tutorial, we discussed how to create a customizable, token-based loyalty program on the Celo blockchain using the Solidity language and the OpenZeppelin contracts library.
Our LoyaltyProgram contract uses the ERC20 standard and incorporates features such as token capping, ownership privileges, and an emergency stop mechanism. We introduced custom functionalities such as the ability to add or remove recognized merchants, set discounts for each merchant, and enable these values to be checked.
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.