Fan Token Staking and Rewards System Development
Fan tokens are a distinct niche: Chiliz/Socios model, tokens of sports clubs, musicians, streamers. The mechanics differ from DeFi staking: here, value is not purely financial. Token holders expect exclusive content access, voting rights in polls ("what color should the away jersey be?"), NFT drops, meet-and-greet lotteries. Staking should reflect this.
Technically it's still ERC-20 + reward distribution, but with non-financial reward mechanics on top. Let's break down how this works.
Reward Mechanics and Implementation
Multi-tiered rewards
The classical approach for fan tokens: three to four tiers with different sets of benefits. Logic: the more staked and the longer, the higher the tier.
| Tier | Minimum Tokens | Lock Period | Benefits |
|---|---|---|---|
| Bronze | 100 FAN | None | Voting, basic content |
| Silver | 500 FAN | 30 days | + early access to NFT drops |
| Gold | 2000 FAN | 90 days | + merchandise/ticket lottery |
| Platinum | 10000 FAN | 180 days | + meet-and-greet, exclusive calls |
The tier is determined on-chain through the staking contract. Off-chain services (content platform, lottery system) read the tier through a view function and unlock corresponding features.
Reward distribution: two approaches
Approach 1 — Pull model (MasterChef-style). Each staker accumulates pending rewards, which they claim manually. Uses accRewardPerShare — an accumulator updated with each deposit/withdraw. Scales well: users pay gas for reward distribution, not the protocol.
struct UserInfo {
uint256 amount; // staked FAN
uint256 rewardDebt; // debt for correct calculation
uint256 lockedUntil; // unlock timestamp
uint8 tier; // current tier
}
function pendingReward(address user) public view returns (uint256) {
UserInfo storage info = userInfo[user];
uint256 accPerShare = accRewardPerShare; // updated value
if (block.timestamp > lastRewardTime && totalStaked > 0) {
uint256 elapsed = block.timestamp - lastRewardTime;
uint256 newReward = elapsed * rewardPerSecond;
accPerShare += newReward * 1e12 / totalStaked;
}
return info.amount * accPerShare / 1e12 - info.rewardDebt;
}
Approach 2 — Push model (snapshots). The team periodically snapshots staked balances and distributes rewards (e.g., after each match — distribute reward pool to all Gold+ holders). Implemented as MerkleDistributor rounds.
Push advantages: can distribute non-fungible rewards (NFTs) and off-chain values (access codes). Disadvantages: requires regular protocol team transactions.
For fan tokens, optimal: pull model for financial rewards (tokens), push/snapshot for special events (NFT drops, lotteries).
NFT integration as reward
Popular pattern: staking unlocks access to mint exclusive NFTs. Not free (otherwise everyone mints and sells) — minting is available only when reaching the required tier and maintaining the stake.
Implementation: NFT contract checks the staking contract in the mint function:
function mintExclusive(uint256 tokenId) external {
IFanStaking staking = IFanStaking(stakingContract);
require(staking.getUserTier(msg.sender) >= GOLD_TIER, "Insufficient tier");
require(!hasMinted[msg.sender][tokenId], "Already minted");
hasMinted[msg.sender][tokenId] = true;
_mint(msg.sender, tokenId);
}
Bonuses for duration and loyalty multiplier
The longer an address holds its stake without withdrawal, the higher its effective stake for reward calculation. Mechanics: loyaltyMultiplier grows over time (e.g., +10% every 30 days, capped at 200%). Any withdraw resets it to 100%.
Store multiplier as uint256 percentage and multiply amount before calculating rewardDebt. This creates incentive not to touch the stake — reducing circulating supply and price pressure.
Voting for Holders
Fan polls are simple on-chain votes, accessible only to staked addresses of the required tier.
No need for a full-blown DAO framework. A lightweight contract suffices:
function vote(uint256 pollId, uint8 optionIndex) external {
require(stakingContract.getUserTier(msg.sender) >= polls[pollId].minTier, "Tier too low");
require(block.timestamp < polls[pollId].endTime, "Poll ended");
require(!hasVoted[pollId][msg.sender], "Already voted");
hasVoted[pollId][msg.sender] = true;
uint256 weight = stakingContract.getStakedAmount(msg.sender); // vote weight = stake
polls[pollId].votes[optionIndex] += weight;
emit VoteCast(pollId, msg.sender, optionIndex, weight);
}
Results are publicly verifiable on-chain, important for fan trust.
Technical Specifics of Fan Environment
ERC-20 with transfer restrictions. Some fan tokens prohibit transfers in official marketplaces (only through approved DEX or Chiliz Chain). When developing staking, consider: if the token has transferFrom restrictions, deposits into the staking contract might be blocked. Solution: whitelist the staking contract in the token.
Multi-chain separation. Chiliz Chain is a separate EVM-compatible L1. If a fan token lives there, the entire staking contract deploys on Chiliz Chain. Gas is cheap, but the ecosystem is limited. Bridge to Ethereum/Polygon possible for DeFi integrations.
Regulatory caution. Fan tokens in some jurisdictions qualify as securities if they grant financial rights. Staking with token rewards increases this risk. Legal consultation before launch is mandatory.
Stack and Timeline
Solidity 0.8.x + Hardhat or Foundry + OpenZeppelin. Frontend: wagmi + React for Ethereum/Polygon. For Chiliz Chain — custom JSON-RPC, wagmi with custom chain config.
Developing a staking contract with tiers and pull model — 2–3 weeks. With NFT integration, lottery mechanics, and voting — 4–6 weeks. Audit is mandatory before launching with real funds — another 2–3 weeks.







