Creating a Season-Based Reward System

Learn how to implement a season-based reward system using the Tap Ants Voucher System.

Introduction

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.

System Design

Season-Based Reward System Architecture

Season Management

  • Each season has its own TapAntsVoucher contract with a unique season ID
  • Seasons have defined start and end dates
  • Season-specific redemption rules and rewards

Voucher Distribution

  • Users earn vouchers through activities during the season
  • Vouchers are minted to user wallets
  • Distribution can be automatic or manual

Redemption Process

  • Users can redeem vouchers for rewards during or after the season
  • Redemption may have time limits or other constraints
  • Different seasons can offer different rewards

Season Transition

  • Smooth transition between seasons
  • Handling of unredeemed vouchers from previous seasons
  • Introduction of new rewards and rules for the new season

Implementation Steps

1. Setting Up Seasonal Vouchers

Deploy a Voucher Contract for Each Season

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);
    }
}

2. Configuring Redemption Rules

Set Up Redemption Rules for Each Season

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;
    }
}

3. Managing Voucher Distribution

Distribute Vouchers to Users

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]);
        }
    }
}

4. Implementing Season Transitions

Handle Season Transitions

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

Frontend Integration

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:

Season Selector Component

React/JSX
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>
  );
}

Best Practices

Conclusion

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.

Previous: Game IntegrationNext: Building a Redemption Platform