Introduction
Welcome to this tutorial! Today, we will be diving into the world of Solidity and blockchain by building a tokenized rewards program for a fitness and wellness center.
Our task is to create a system where fitness center members earn tokens by attending classes or purchasing services. These tokens can later be redeemed for rewards. This encourages regular engagement with the fitness center and provides a fun and interactive experience for members.
To accomplish this, we’ll create two Celo smart contracts: one for the token itself (FitnessToken
), which will follow the ERC20 standard, and another (FitnessRewards
) to manage the fitness center’s rewards program.
Here’s the GitHub repo of our code. source code
Prerequisites
To follow this tutorial, you will need the following:
- Solidity programming language.
- Remix IDE.
- The celo Extension Wallet.
SmartContract
Complete contracts.
pragma solidity ^0.8.6;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract FitnessToken is ERC20, Ownable {
constructor() ERC20("FitnessToken", "FT") {}
function mint(address recipient, uint256 amount) external onlyOwner {
_mint(recipient, amount);
}
function burnFrom(address account, uint256 amount) external onlyOwner {
_burn(account, amount);
}
}
contract FitnessRewards is Ownable {
FitnessToken private fitnessToken;
struct Member {
uint256 totalAttendedClasses;
uint256 totalPurchasedServices;
}
mapping(address => Member) private members;
event ClassAttended(address indexed member, uint256 totalAttendedClasses, uint256 totalTokens);
event ServicePurchased(address indexed member, uint256 totalPurchasedServices, uint256 totalTokens);
event RewardRedeemed(address indexed member, uint256 tokensBurned);
constructor(FitnessToken _fitnessToken) {
fitnessToken = _fitnessToken;
}
function attendClass(address memberAddress) external onlyOwner {
members[memberAddress].totalAttendedClasses += 1;
uint256 tokensToMint = 10; // Award 10 FTs for each class
fitnessToken.mint(memberAddress, tokensToMint);
emit ClassAttended(memberAddress, members[memberAddress].totalAttendedClasses, tokensToMint);
}
function purchaseService(address memberAddress) external payable onlyOwner {
members[memberAddress].totalPurchasedServices += 1;
uint256 tokensToMint = 50; // Award 50 FTs for each service purchased
fitnessToken.mint(memberAddress, tokensToMint);
emit ServicePurchased(memberAddress, members[memberAddress].totalPurchasedServices, tokensToMint);
}
function redeemRewards(address memberAddress, uint256 tokenAmount) external {
require(tokenAmount <= fitnessToken.balanceOf(memberAddress), "Not enough FTs to redeem");
fitnessToken.burnFrom(memberAddress, tokenAmount);
emit RewardRedeemed(memberAddress, tokenAmount);
}
function getTotalAttendedClasses(address memberAddress) external view returns (uint256) {
return members[memberAddress].totalAttendedClasses;
}
function getTotalPurchasedServices(address memberAddress) external view returns (uint256) {
return members[memberAddress].totalPurchasedServices;
}
}
Understanding the FitnessToken Contract
Pragma and Imports
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.6;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
The pragma solidity ^0.8.6;
line specifies the compiler version to use. Here we’re using 0.8.6 or any newer version that doesn’t break functionality.
The import
statements import code from the OpenZeppelin library, a library of secure and tested contract standards.
FitnessToken Contract Declaration
contract FitnessToken is ERC20, Ownable {
Here we’re declaring a new contract FitnessToken
which inherits from the ERC20
and Ownable
contracts.
Constructor
constructor() ERC20("FitnessToken", "FT") {}
The constructor
is a special function that is called when the contract is first deployed. Here we’re calling the ERC20
constructor with two parameters: the name and the symbol of the token.
Mint Function
function mint(address recipient, uint256 amount) external onlyOwner {
_mint(recipient, amount);
}
This function allows the contract owner to “mint” or create new tokens. The onlyOwner
modifier ensures that only the owner of the contract can call this function.
Burn Function
function burnFrom(address account, uint256 amount) external onlyOwner {
_burn(account, amount);
}
This function allows the contract owner to “burn” or destroy tokens from a specific account. Again, the onlyOwner
modifier ensures that only the contract owner can call this function.
Understanding the FitnessRewards Contract
The FitnessRewards
contract handles the reward logic for the fitness center.
FitnessToken and Member Struct
FitnessToken private fitnessToken;
struct Member {
uint256 totalAttendedClasses;
uint256 totalPurchasedServices;
}
Here we’re declaring a private instance of the FitnessToken
contract, and a Member
struct that keeps track of how many classes a member has attended and how many services they’ve purchased.
Members Mapping and Events
mapping(address => Member) private members;
event ClassAttended(address indexed member, uint256 totalAttendedClasses, uint256 totalTokens);
event ServicePurchased(address indexed member, uint256 totalPurchasedServices, uint256 totalTokens);
event RewardRedeemed(address indexed member, uint256 tokensBurned);
The mapping
assigns a Member
struct to each address (each member). The events
are emitted when a member attends a class, purchases a service, or redeems a reward. Events are a way for smart contracts to communicate that something has happened on the blockchain to external listeners.
Constructor
constructor(FitnessToken _fitnessToken) {
fitnessToken = _fitnessToken;
}
The constructor
has one parameter: _fitnessToken
, which is an instance of the FitnessToken
contract. The passed in FitnessToken
instance is then assigned to the fitnessToken
state variable of the FitnessRewards
contract.
With this setup, the FitnessRewards
contract can interact with the FitnessToken
contract (for example, minting or burning tokens) because it now holds a reference to the FitnessToken
contract.
attendClass Function
function attendClass(address memberAddress) external onlyOwner {
members[memberAddress].totalAttendedClasses += 1;
uint256 tokensToMint = 10; // Award 10 FTs for each class
fitnessToken.mint(memberAddress, tokensToMint);
emit ClassAttended(memberAddress, members[memberAddress].totalAttendedClasses, tokensToMint);
}
This function is called when a member attends a class. It increases the total count of attended classes by the member, mints new tokens for the member, and emits an event. The onlyOwner
modifier ensures that only the contract owner can call this function.
purchaseService Function
function purchaseService(address memberAddress) external payable onlyOwner {
members[memberAddress].totalPurchasedServices += 1;
uint256 tokensToMint = 50; // Award 50 FTs for each service purchased
fitnessToken.mint(memberAddress, tokensToMint);
emit ServicePurchased(memberAddress, members[memberAddress].totalPurchasedServices, tokensToMint);
}
This function is similar to the attendClass
function. It increases the total count of purchased services by the member, mints new tokens for the member, and emits an event. The onlyOwner
modifier again ensures that only the contract owner can call this function.
redeemRewards Function
function redeemRewards(address memberAddress, uint256 tokenAmount) external {
require(tokenAmount <= fitnessToken.balanceOf(memberAddress), "Not enough FTs to redeem");
fitnessToken.burnFrom(memberAddress, tokenAmount);
emit RewardRedeemed(memberAddress, tokenAmount);
}
This function is called when a member wants to redeem their rewards. It first checks if the member has enough tokens to redeem, then burns the tokens from the member’s balance, and emits an event.
getTotalAttendedClasses and getTotalPurchasedServices Functions
function getTotalAttendedClasses(address memberAddress) external view returns (uint256) {
return members[memberAddress].totalAttendedClasses;
}
function getTotalPurchasedServices(address memberAddress) external view returns (uint256) {
return members[memberAddress].totalPurchasedServices;
}
These are “view” functions, which means they don’t change the state of the contract but allow anyone to view the total number of classes attended and services purchased by a member.
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 contract file in Remix and click on the Solidity Compiler
tab in the sidebar. Click the `Compile button to compile the contract.
Deploy the Contract
Click on the Deploy & Run Transactions
tab in the sidebar. In the Contract
dropdown menu, select the contract.
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.
Conclusion
Congratulations on completing this tutorial! You have now successfully developed a tokenized rewards program using Solidity. You’ve learned how to create an ERC20 token, implement a system to reward user behavior with tokens, and provide a mechanism for redeeming those tokens.
Extra Resources
Learn more by following these links.
About the author
I’ am a passionate web3 developer who loves to write and learn new skills