Token Rewards System
Integrate a blockchain token rewards system that allows users to view token information, check pending rewards, and claim their rewards on Base Mainnet.
Overview
The Token Rewards System enables users to:
- View token information (no wallet required)
- Check pending rewards (without connecting wallet)
- Claim rewards (with wallet connection)
Contract Details
- Network: Base Mainnet (Chain ID: 8453)
- RPC URL: https://mainnet.base.org (opens in a new tab)
- Factory Contract:
0xeF6ce237F69D238Fe3CfAB801f24b4cfd3A54B9D - Example Token:
0x937f6Af292875cC2a3b64bFC31B09d5A50B1cF1a
Block Explorer Links
- Factory Contract (opens in a new tab) (Verified)
- Example Token (opens in a new tab) (Verified)
Contract ABI
[
{
"inputs": [],
"name": "name",
"outputs": [{"type": "string"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [{"type": "string"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lpTokenId",
"outputs": [{"type": "uint256"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "feeReceiver",
"outputs": [{"type": "address"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "factoryCreator",
"outputs": [{"type": "address"}],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "get_rewards",
"outputs": [
{"type": "uint256", "name": "tokensToFeeReceiver"},
{"type": "uint256", "name": "ethToFactoryCreator"}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{"type": "address", "name": "account"}],
"name": "balanceOf",
"outputs": [{"type": "uint256"}],
"stateMutability": "view",
"type": "function"
}
]Implementation Guide
Installation
npm install ethers1. Get Token Information (Read-only)
Fetch basic token information without requiring a wallet connection.
import { ethers } from 'ethers';
const TOKEN_ADDRESS = '0x937f6Af292875cC2a3b64bFC31B09d5A50B1cF1a';
const BASE_RPC = 'https://mainnet.base.org';
const TOKEN_ABI = [
'function name() view returns (string)',
'function symbol() view returns (string)',
'function lpTokenId() view returns (uint256)',
'function feeReceiver() view returns (address)',
'function factoryCreator() view returns (address)',
'function get_rewards() returns (uint256 tokensToFeeReceiver, uint256 ethToFactoryCreator)',
'function balanceOf(address) view returns (uint256)'
];
async function getTokenInfo(tokenAddress) {
const provider = new ethers.JsonRpcProvider(BASE_RPC);
const token = new ethers.Contract(tokenAddress, TOKEN_ABI, provider);
const [name, symbol, lpTokenId, feeReceiver, factoryCreator] = await Promise.all([
token.name(),
token.symbol(),
token.lpTokenId(),
token.feeReceiver(),
token.factoryCreator()
]);
return {
name,
symbol,
lpTokenId: lpTokenId.toString(),
feeReceiver,
factoryCreator
};
}2. Check Pending Rewards (Simulation)
Check pending rewards without executing a transaction using staticCall().
async function checkPendingRewards(tokenAddress) {
const provider = new ethers.JsonRpcProvider(BASE_RPC);
const token = new ethers.Contract(tokenAddress, TOKEN_ABI, provider);
try {
const [tokensToReceiver, ethToCreator] = await token.get_rewards.staticCall();
return {
tokenRewards: ethers.formatEther(tokensToReceiver),
ethRewards: ethers.formatEther(ethToCreator),
tokenRewardsRaw: tokensToReceiver.toString(),
ethRewardsRaw: ethToCreator.toString()
};
} catch (error) {
console.error('Error checking rewards:', error);
return { tokenRewards: '0', ethRewards: '0' };
}
}3. Claim Rewards (Requires Wallet)
Execute the transaction to claim rewards. Requires wallet connection.
async function claimRewards(tokenAddress) {
if (!window.ethereum) {
throw new Error('No wallet found. Please install MetaMask.');
}
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);
const signer = await provider.getSigner();
const token = new ethers.Contract(tokenAddress, TOKEN_ABI, signer);
try {
const tx = await token.get_rewards();
console.log('Transaction sent:', tx.hash);
const receipt = await tx.wait();
console.log('Transaction confirmed:', receipt);
return { success: true, txHash: receipt.hash };
} catch (error) {
console.error('Error claiming rewards:', error);
return { success: false, error: error.message };
}
}React Component Example
Complete React component implementation:
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';
export default function RewardsPanel({ tokenAddress }) {
const [tokenInfo, setTokenInfo] = useState(null);
const [pendingRewards, setPendingRewards] = useState(null);
const [isClaiming, setIsClaiming] = useState(false);
useEffect(() => {
// Fetch token info once
getTokenInfo(tokenAddress).then(setTokenInfo);
// Poll rewards every 30 seconds
const interval = setInterval(() => {
checkPendingRewards(tokenAddress).then(setPendingRewards);
}, 30000);
// Initial fetch
checkPendingRewards(tokenAddress).then(setPendingRewards);
return () => clearInterval(interval);
}, [tokenAddress]);
const handleClaim = async () => {
setIsClaiming(true);
try {
const result = await claimRewards(tokenAddress);
if (result.success) {
alert(\`Rewards claimed! TX: \${result.txHash}\`);
// Refresh rewards after claiming
checkPendingRewards(tokenAddress).then(setPendingRewards);
} else {
alert(\`Error: \${result.error}\`);
}
} finally {
setIsClaiming(false);
}
};
return (
<div className="rewards-panel">
<h2>{tokenInfo?.name} ({tokenInfo?.symbol})</h2>
<div className="pending-rewards">
<h3>Pending Rewards</h3>
<p>Token Fees: {pendingRewards?.tokenRewards || '0'} tokens</p>
<p>ETH Fees: {pendingRewards?.ethRewards || '0'} ETH</p>
</div>
<button
onClick={handleClaim}
disabled={isClaiming}
className="claim-button"
>
{isClaiming ? 'Claiming...' : 'Claim Rewards'}
</button>
<div className="fee-info">
<small>Token fees → {tokenInfo?.feeReceiver}</small><br/>
<small>ETH fees → {tokenInfo?.factoryCreator}</small>
</div>
</div>
);
}UI/UX Requirements
Essential Features
-
Token Information Display
- Show token name and symbol prominently
- Display LP Token ID
- Show fee receiver addresses
-
Pending Rewards Display
- Real-time updates (poll every 30 seconds)
- Show both token fees and ETH fees separately
- Format numbers for readability
-
Claim Functionality
- "Claim Rewards" button (only clickable if rewards > 0)
- Show transaction status: Pending → Confirmed/Failed
- Disable button during transaction
-
Fee Distribution Info
- "Token fees go to Fee Receiver: [address]"
- "ETH fees go to Factory Creator: [address]"
-
Auto-refresh
- Refresh rewards display after successful claim
- Continue polling for new rewards
-
Error Handling
- No wallet installed
- Wrong network
- Transaction rejection
- Contract errors
How It Works
Rewards Distribution
- Anyone can call
get_rewards()- No special permissions required - Automatic distribution - Rewards go to the correct recipients
- Token fees →
feeReceiveraddress - ETH fees →
factoryCreatoraddress - The caller doesn't receive anything - They just trigger the distribution
Important Notes
- Use
staticCall()for preview to avoid actual execution - Always verify the chain is Base (8453) before transactions
- Rewards accumulate from trading activity on Uniswap V4
- No gas optimization needed - the caller pays gas but gets no rewards
Testing
Test Token
Address: 0x937f6Af292875cC2a3b64bFC31B09d5A50B1cF1a
Expected Results:
- Name: "Hi-hat Token"
- Symbol: "HIHAT"
- LP Token ID: 518214
- Token fees pending: ~24.35 tokens
- ETH fees pending: ~0.0000834 ETH
Testing Checklist
- ✓ Token info loads without wallet
- ✓ Pending rewards display correctly
- ✓ Rewards update every 30 seconds
- ✓ Claim button connects wallet
- ✓ Transaction status shows correctly
- ✓ Rewards refresh after claim
- ✓ Error messages are clear
Error Handling
Common errors and solutions:
| Error | Cause | Solution |
|---|---|---|
| "No wallet found" | MetaMask not installed | Prompt user to install wallet |
| "Wrong network" | Not on Base | Prompt to switch to Base |
| "Transaction rejected" | User cancelled | Allow retry |
| "Insufficient funds" | Not enough ETH for gas | Show balance warning |
| "Contract error" | Various | Display error message |
Dependencies
{
"dependencies": {
"ethers": "^6.0.0"
}
}Optional Wallet Libraries
For better wallet connection experience:
# wagmi + RainbowKit (React)
npm install wagmi @rainbow-me/rainbowkit
# web3modal (Multi-framework)
npm install @web3modal/wagmi
# ConnectKit (React)
npm install connectkitBest Practices
-
Performance
- Cache token info (it doesn't change)
- Use efficient polling intervals (30s recommended)
- Debounce claim button clicks
-
User Experience
- Show loading states clearly
- Display transaction links to Basescan
- Format large numbers for readability
- Use tooltips for technical terms
-
Security
- Validate chain ID before transactions
- Handle all error cases
- Never expose private keys
- Use HTTPS only
-
Mobile Responsiveness
- Test on mobile devices
- Ensure buttons are easily clickable
- Display addresses in truncated format
- Support mobile wallet apps
Support
For issues or questions:
- Check the Base documentation (opens in a new tab)
- View contracts on Basescan (opens in a new tab)
- Review transaction logs for debugging