Creating a tokenized loyalty program for a business On the Celo Blockchain

Creating a tokenized loyalty program for a business On the Celo Blockchain https://celo.academy/uploads/default/optimized/2X/9/97405a74090ba1a1f7a9612fe2e2690483def6c8_2_1024x576.png
none 0.0 0

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.

Contract code GitHub 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.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.

8 Likes

Great tutorial, would love to see this get built. What token standard would you be using for this?

4 Likes

i’ll be using erc721

5 Likes

Alright. that’s good too

5 Likes

Congratulations on being among the top voted proposals this week! This is now approved by @Celo_Academy for you to get started whenever you’d like. :mortar_board: :seedling:

5 Likes

@richiemikke i will review your article

5 Likes

@richiemikke tutorial looks good to go. you can move to publish

6 Likes

Thanks for this contribution.

6 Likes

Editing to GitHub

2 Likes

Concise functions such as this is great for beginners getting atarted with Solidity. Keep the good job up.

2 Likes

Great tutorial, good job

the tutorial is well detailed and can be incorporated into real life use cases , welldone brother.

9 Likes

Nice one @richiemikke,

Please endeavor to write unit tests if you’re writing contracts. It proves that your contracts are working as expected. It is mandatory to test your contracts if they’re working

6 Likes