Learn how to implement a season-based reward system using the Tap Ants Voucher System.
Season-based reward systems are popular in games, loyalty programs, and subscription services. They allow businesses to create time-limited promotions, refresh their offerings periodically, and maintain user engagement over time. The Tap Ants Voucher System is designed to support season-based rewards through its immutable season ID parameter.
In this tutorial, we'll walk through the process of creating a complete season-based reward system using the Tap Ants Voucher System. We'll cover setting up seasonal vouchers, configuring redemption rules, and managing transitions between seasons.
Each season needs its own TapAntsVoucher contract with a unique season ID.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./TapAntsVoucher.sol";
contract SeasonManager {
address public owner;
mapping(uint256 => address) public seasonVouchers;
uint256 public currentSeasonId;
event SeasonCreated(uint256 seasonId, address voucherContract);
event SeasonChanged(uint256 oldSeasonId, uint256 newSeasonId);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
function createNewSeason(string memory name, string memory symbol) external onlyOwner {
uint256 newSeasonId = currentSeasonId + 1;
// Deploy a new voucher contract for the season
TapAntsVoucher newVoucher = new TapAntsVoucher(
string(abi.encodePacked(name, " S", toString(newSeasonId))),
string(abi.encodePacked(symbol, toString(newSeasonId))),
newSeasonId,
owner
);
// Store the voucher contract address
seasonVouchers[newSeasonId] = address(newVoucher);
// Update the current season
currentSeasonId = newSeasonId;
emit SeasonCreated(newSeasonId, address(newVoucher));
}
function changeCurrentSeason(uint256 seasonId) external onlyOwner {
require(seasonVouchers[seasonId] != address(0), "Season does not exist");
uint256 oldSeasonId = currentSeasonId;
currentSeasonId = seasonId;
emit SeasonChanged(oldSeasonId, seasonId);
}
function getSeasonVoucher(uint256 seasonId) external view returns (address) {
return seasonVouchers[seasonId];
}
function getCurrentSeasonVoucher() external view returns (address) {
return seasonVouchers[currentSeasonId];
}
// Helper function to convert uint to string
function toString(uint256 value) internal pure returns (string memory) {
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
}
Configure the VoucherRedeemContract to support vouchers from different seasons with different redemption rules.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./VoucherRedeemContract.sol";
import "./SeasonManager.sol";
contract SeasonRedemptionManager {
address public owner;
SeasonManager public seasonManager;
VoucherRedeemContract public redeemContract;
// Mapping from season ID to redemption token address
mapping(uint256 => address) public seasonRedemptionTokens;
// Mapping from season ID to conversion rate
mapping(uint256 => uint256) public seasonConversionRates;
// Mapping from season ID to redemption deadline (0 means no deadline)
mapping(uint256 => uint256) public seasonRedemptionDeadlines;
event RedemptionRuleSet(uint256 seasonId, address redemptionToken, uint256 conversionRate, uint256 deadline);
constructor(address _seasonManager, address _redeemContract) {
owner = msg.sender;
seasonManager = SeasonManager(_seasonManager);
redeemContract = VoucherRedeemContract(_redeemContract);
}
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
function setSeasonRedemptionRule(
uint256 seasonId,
address redemptionToken,
uint256 conversionRate,
uint256 deadline
) external onlyOwner {
address voucherAddress = seasonManager.getSeasonVoucher(seasonId);
require(voucherAddress != address(0), "Season does not exist");
// Store the redemption rules
seasonRedemptionTokens[seasonId] = redemptionToken;
seasonConversionRates[seasonId] = conversionRate;
seasonRedemptionDeadlines[seasonId] = deadline;
// Configure the redemption contract
redeemContract.addSupportedVoucher(voucherAddress, redemptionToken, conversionRate);
emit RedemptionRuleSet(seasonId, redemptionToken, conversionRate, deadline);
}
function updateSeasonConversionRate(uint256 seasonId, uint256 newConversionRate) external onlyOwner {
address voucherAddress = seasonManager.getSeasonVoucher(seasonId);
require(voucherAddress != address(0), "Season does not exist");
// Update the conversion rate
seasonConversionRates[seasonId] = newConversionRate;
// Update the redemption contract
redeemContract.updateConversionRate(voucherAddress, newConversionRate);
emit RedemptionRuleSet(
seasonId,
seasonRedemptionTokens[seasonId],
newConversionRate,
seasonRedemptionDeadlines[seasonId]
);
}
function updateSeasonRedemptionDeadline(uint256 seasonId, uint256 newDeadline) external onlyOwner {
address voucherAddress = seasonManager.getSeasonVoucher(seasonId);
require(voucherAddress != address(0), "Season does not exist");
// Update the deadline
seasonRedemptionDeadlines[seasonId] = newDeadline;
emit RedemptionRuleSet(
seasonId,
seasonRedemptionTokens[seasonId],
seasonConversionRates[seasonId],
newDeadline
);
}
function canRedeem(uint256 seasonId) public view returns (bool) {
uint256 deadline = seasonRedemptionDeadlines[seasonId];
if (deadline == 0) {
return true; // No deadline means redemption is always allowed
}
return block.timestamp <= deadline;
}
}
Create a system to distribute vouchers to users based on their activities.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./TapAntsVoucher.sol";
import "./SeasonManager.sol";
contract VoucherDistributor {
address public owner;
SeasonManager public seasonManager;
// Mapping to track user activities
mapping(address => mapping(string => uint256)) public userActivities;
// Mapping to track vouchers already claimed for activities
mapping(address => mapping(string => bool)) public activityClaimed;
event ActivityRecorded(address user, string activityId, uint256 value);
event VouchersDistributed(address user, uint256 seasonId, uint256 amount);
constructor(address _seasonManager) {
owner = msg.sender;
seasonManager = SeasonManager(_seasonManager);
}
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
// Record a user activity (called by authorized backend)
function recordActivity(address user, string calldata activityId, uint256 value) external onlyOwner {
userActivities[user][activityId] = value;
emit ActivityRecorded(user, activityId, value);
}
// Distribute vouchers based on a specific activity
function distributeVouchersForActivity(
address user,
string calldata activityId,
uint256 amount
) external onlyOwner {
require(!activityClaimed[user][activityId], "Activity already claimed");
require(userActivities[user][activityId] > 0, "No activity recorded");
// Mark activity as claimed
activityClaimed[user][activityId] = true;
// Get the current season voucher
uint256 currentSeasonId = seasonManager.currentSeasonId();
address voucherAddress = seasonManager.getCurrentSeasonVoucher();
// Mint vouchers to the user
TapAntsVoucher voucher = TapAntsVoucher(voucherAddress);
voucher.mint(user, amount);
emit VouchersDistributed(user, currentSeasonId, amount);
}
// Batch distribute vouchers to multiple users
function batchDistributeVouchers(
address[] calldata users,
uint256[] calldata amounts
) external onlyOwner {
require(users.length == amounts.length, "Arrays length mismatch");
uint256 currentSeasonId = seasonManager.currentSeasonId();
address voucherAddress = seasonManager.getCurrentSeasonVoucher();
TapAntsVoucher voucher = TapAntsVoucher(voucherAddress);
for (uint256 i = 0; i < users.length; i++) {
voucher.mint(users[i], amounts[i]);
emit VouchersDistributed(users[i], currentSeasonId, amounts[i]);
}
}
}
Create a system to manage transitions between seasons, including handling unredeemed vouchers.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./SeasonManager.sol";
import "./SeasonRedemptionManager.sol";
import "./TapAntsVoucher.sol";
contract SeasonTransitionManager {
address public owner;
SeasonManager public seasonManager;
SeasonRedemptionManager public redemptionManager;
// Mapping to track if a user has migrated vouchers from one season to another
mapping(address => mapping(uint256 => mapping(uint256 => bool))) public seasonMigrated;
event SeasonTransitioned(uint256 oldSeasonId, uint256 newSeasonId);
event VouchersMigrated(address user, uint256 fromSeasonId, uint256 toSeasonId, uint256 amount);
constructor(address _seasonManager, address _redemptionManager) {
owner = msg.sender;
seasonManager = SeasonManager(_seasonManager);
redemptionManager = SeasonRedemptionManager(_redemptionManager);
}
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_;
}
// Transition to a new season
function transitionToNewSeason(string calldata name, string calldata symbol) external onlyOwner {
uint256 oldSeasonId = seasonManager.currentSeasonId();
// Create a new season
seasonManager.createNewSeason(name, symbol);
uint256 newSeasonId = seasonManager.currentSeasonId();
emit SeasonTransitioned(oldSeasonId, newSeasonId);
}
// Allow users to migrate unredeemed vouchers from a previous season to the current season
function migrateVouchers(uint256 fromSeasonId, uint256 amount) external {
address user = msg.sender;
uint256 currentSeasonId = seasonManager.currentSeasonId();
// Check if the user has already migrated from this season
require(!seasonMigrated[user][fromSeasonId][currentSeasonId], "Already migrated");
// Check if the season can still be redeemed
require(!redemptionManager.canRedeem(fromSeasonId), "Season can still be redeemed");
// Get the voucher contracts
address fromVoucherAddress = seasonManager.getSeasonVoucher(fromSeasonId);
address toVoucherAddress = seasonManager.getCurrentSeasonVoucher();
require(fromVoucherAddress != address(0), "From season does not exist");
require(toVoucherAddress != address(0), "To season does not exist");
TapAntsVoucher fromVoucher = TapAntsVoucher(fromVoucherAddress);
TapAntsVoucher toVoucher = TapAntsVoucher(toVoucherAddress);
// Check if the user has enough vouchers
uint256 balance = fromVoucher.balanceOf(user);
require(balance >= amount, "Insufficient vouchers");
// Burn vouchers from the old season
fromVoucher.burn(user, amount, "");
// Apply migration rate (e.g., 80% of original value)
uint256 migrationRate = 80; // 80%
uint256 migratedAmount = (amount * migrationRate) / 100;
// Mint vouchers for the new season
toVoucher.mint(user, migratedAmount);
// Mark as migrated
seasonMigrated[user][fromSeasonId][currentSeasonId] = true;
emit VouchersMigrated(user, fromSeasonId, currentSeasonId, migratedAmount);
}
// Allow admin to set migration rates between seasons
function setMigrationRate(uint256 fromSeasonId, uint256 toSeasonId, uint256 rate) external onlyOwner {
// Implementation for setting custom migration rates between specific seasons
}
}
To complete the season-based reward system, you'll need to create a frontend that allows users to interact with the system. Here's a simplified example of a React component for displaying seasons and vouchers:
import React, { useState, useEffect } from "react";
import { ethers } from "ethers";
import { seasonManagerAbi } from "./abis/seasonManagerAbi";
import { tapAntsVoucherAbi } from "./abis/tapAntsVoucherAbi";
const SEASON_MANAGER_ADDRESS = "0x..."; // Your SeasonManager address
export function SeasonSelector() {
const [seasons, setSeasons] = useState([]);
const [selectedSeason, setSelectedSeason] = useState(null);
const [voucherBalance, setVoucherBalance] = useState("0");
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
async function loadSeasons() {
try {
const provider = new ethers.providers.Web3Provider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = provider.getSigner();
const userAddress = await signer.getAddress();
const seasonManager = new ethers.Contract(
SEASON_MANAGER_ADDRESS,
seasonManagerAbi,
provider
);
// Get current season
const currentSeasonId = await seasonManager.currentSeasonId();
// Load all seasons (simplified - in a real app, you'd need to track all created seasons)
const loadedSeasons = [];
for (let i = 1; i <= currentSeasonId.toNumber(); i++) {
const voucherAddress = await seasonManager.getSeasonVoucher(i);
if (voucherAddress !== ethers.constants.AddressZero) {
const voucher = new ethers.Contract(
voucherAddress,
tapAntsVoucherAbi,
provider
);
const name = await voucher.name();
const symbol = await voucher.symbol();
const balance = await voucher.balanceOf(userAddress);
loadedSeasons.push({
id: i,
name,
symbol,
voucherAddress,
balance: ethers.utils.formatUnits(balance, 18),
isCurrent: i === currentSeasonId.toNumber()
});
}
}
setSeasons(loadedSeasons);
// Set selected season to current season
const current = loadedSeasons.find(s => s.isCurrent);
if (current) {
setSelectedSeason(current);
setVoucherBalance(current.balance);
}
setLoading(false);
} catch (err) {
console.error("Error loading seasons:", err);
setError("Failed to load seasons");
setLoading(false);
}
}
loadSeasons();
}, []);
const handleSeasonChange = (seasonId) => {
const season = seasons.find(s => s.id === seasonId);
if (season) {
setSelectedSeason(season);
setVoucherBalance(season.balance);
}
};
if (loading) {
return <div>Loading seasons...</div>;
}
if (error) {
return <div className="text-red-500">{error}</div>;
}
return (
<div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-4">Season Vouchers</h2>
<div className="mb-4">
<label htmlFor="season-select" className="block text-sm font-medium text-gray-700 mb-1">
Select Season
</label>
<select
id="season-select"
value={selectedSeason?.id || ""}
onChange={(e) => handleSeasonChange(Number(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
>
{seasons.map((season) => (
<option key={season.id} value={season.id}>
{season.name} {season.isCurrent ? "(Current)" : ""}
</option>
))}
</select>
</div>
{selectedSeason && (
<div className="mb-4 p-3 bg-gray-100 rounded">
<p className="text-sm text-gray-600">Your Voucher Balance</p>
<p className="text-xl font-semibold">{voucherBalance} {selectedSeason.symbol}</p>
<div className="mt-4">
<h3 className="text-md font-medium mb-2">Season Details</h3>
<p className="text-sm"><strong>Season ID:</strong> {selectedSeason.id}</p>
<p className="text-sm"><strong>Voucher Address:</strong> {selectedSeason.voucherAddress}</p>
<p className="text-sm"><strong>Status:</strong> {selectedSeason.isCurrent ? "Active" : "Inactive"}</p>
</div>
<div className="mt-4 flex space-x-2">
<button
className="bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Redeem Vouchers
</button>
{!selectedSeason.isCurrent && (
<button
className="bg-gray-600 text-white py-2 px-4 rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
>
Migrate to Current Season
</button>
)}
</div>
</div>
)}
</div>
);
}
In this tutorial, we've covered the implementation of a complete season-based reward system using the Tap Ants Voucher System. We've created contracts for managing seasons, configuring redemption rules, distributing vouchers, and handling season transitions. We've also shown how to integrate this system with a frontend application.
This system provides a flexible foundation that can be customized for various use cases, such as game rewards, loyalty programs, or subscription services. By leveraging the non-transferable nature of the TapAntsVoucher contract and the season-based design, you can create engaging reward systems that maintain user interest over time.