The Kings Card NFT Rewards Smart Contract monitors $PEASANT token purchases and distributes gamified NFT rewards to incentivize community participation and token holding. The contract implements a sophisticated lottery system with three distinct card types, each offering different redemption mechanisms and reward structures.
The system consists of two interconnected smart contracts:
#[account]
pub struct Purchase {
pub buyer: Pubkey, // Wallet address of buyer
pub amount_usd: u64, // Purchase amount in USDT cents
pub token_amount: u64, // Amount of $PEASANT tokens purchased
pub timestamp: i64, // Unix timestamp of purchase
pub block_number: u64, // Block number for ordering
pub processed: bool, // Whether purchase was processed for rewards
}
#[account]
pub struct PurchaseBlock {
pub block_id: u64, // Sequential block identifier (1, 2, 3...)
pub start_purchase: u64, // First purchase number in block
pub end_purchase: u64, // Last purchase number in block
pub entries: Vec<BlockEntry>, // All entries in this block
pub processed_auto: bool, // Auto-redeem cards distributed
pub processed_flexible: bool, // Flexible cards distributed
pub total_entries: u32, // Total lottery entries in block
}
#[account]
pub struct BlockEntry {
pub wallet: Pubkey, // Wallet address
pub entry_count: u8, // Number of entries (1-12 max per day)
pub purchase_amounts: Vec<u64>, // Amounts for each entry
pub token_amounts: Vec<u64>, // Token amounts for each entry
}
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
pub enum CardType {
AutoRedeem {
auto_redeem_time: i64, // Unix timestamp for automatic redemption
},
Flexible {
min_hold_period: i64, // Minimum hold time in seconds
full_value_period: i64, // Full value window in seconds
max_hold_deadline: i64, // Deadline before forced redemption
},
Legendary {
expiry_date: i64, // Expiry timestamp
decay_start: i64, // When daily decay begins
},
}
#[account]
pub struct KingsCard {
pub card_id: u64, // Unique card identifier
pub card_type: CardType, // Card type with specific parameters
pub reward_token_amount: u64, // Amount of $PEASANT tokens as reward
pub reward_usd_cap: u64, // USD value cap at time of issuance
pub mint_time: i64, // When card was minted
pub owner: Pubkey, // Current owner wallet
pub original_purchase_amount: u64, // Original purchase that won this card
pub is_redeemed: bool, // Redemption status
pub redemption_time: Option<i64>, // When card was redeemed (if applicable)
}
#[account]
pub struct ContractState {
pub total_purchases: u64, // Total purchases processed
pub current_block: u64, // Current purchase block number
pub legendary_milestone: u64, // Next legendary card milestone
// Card circulation tracking
pub auto_cards_in_circulation: u16, // Current auto-redeem cards (max 400)
pub flexible_cards_in_circulation: u16, // Current flexible cards (max 200)
pub legendary_cards_in_circulation: u8, // Current legendary cards (max 5)
// Recycling queues
pub auto_card_recycle_queue: Vec<RecycleEntry>,
pub flexible_card_recycle_queue: Vec<RecycleEntry>,
pub legendary_card_recycle_queue: Vec<RecycleEntry>,
pub authority: Pubkey, // Contract authority
pub escrow_account: Pubkey, // Token escrow account
pub treasury_account: Pubkey, // Tax collection account
}
#[account]
pub struct RecycleEntry {
pub card_id: u64, // Card that was redeemed
pub recycle_time: i64, // When card becomes available (24h delay)
}
#[account]
pub struct WalletLimits {
pub wallet: Pubkey, // Wallet address
pub daily_entries: u8, // Entries used today (max 12)
pub last_reset: i64, // Last daily reset timestamp
pub hourly_purchases: Vec<HourlyPurchase>, // Purchases in current hour
}
#[account]
pub struct HourlyPurchase {
pub timestamp: i64, // Purchase timestamp
pub usd_amount: u64, // Purchase amount in USDT cents
pub counted: bool, // Whether this purchase counted toward limits
}
pub fn process_purchase(
ctx: Context<ProcessPurchase>,
usd_amount: u64, // Purchase amount in USDT cents
token_amount: u64, // Actual $PEASANT tokens purchased
price_feed_data: PriceFeedData // Oracle price data for verification
) -> Result<()> {
// Step 1: Validate purchase meets minimum threshold ($10 USD)
require!(usd_amount >= 1000, ErrorCode::PurchaseTooSmall); // $10.00 in cents
// Step 2: Apply anti-gaming limits
let wallet_limits = &mut ctx.accounts.wallet_limits;
let current_time = Clock::get()?.unix_timestamp;
// Reset daily counter if 24h passed
if current_time >= wallet_limits.last_reset + 86400 {
wallet_limits.daily_entries = 0;
wallet_limits.hourly_purchases.clear();
wallet_limits.last_reset = current_time;
}
// Remove purchases older than 1 hour
wallet_limits.hourly_purchases.retain(|p| current_time - p.timestamp < 3600);
// Add current purchase
wallet_limits.hourly_purchases.push(HourlyPurchase {
timestamp: current_time,
usd_amount,
counted: false,
});
// Apply hourly limit (max 2 purchases per hour)
let mut counted_this_hour = 0;
let mut eligible_purchases = Vec::new();
// Sort by amount (ascending) to count smallest first
wallet_limits.hourly_purchases.sort_by_key(|p| p.usd_amount);
for purchase in &mut wallet_limits.hourly_purchases {
if counted_this_hour < 2 && !purchase.counted {
purchase.counted = true;
eligible_purchases.push(purchase.usd_amount);
counted_this_hour += 1;
}
}
// Calculate total eligible amount and entries
let total_eligible_amount: u64 = eligible_purchases.iter().sum();
let entries = std::cmp::min(
total_eligible_amount / 100000, // Each $1000 = 1 entry (100000 cents)
12 - wallet_limits.daily_entries as u64 // Respect daily limit
) as u8;
require!(entries > 0, ErrorCode::NoEligibleEntries);
// Update daily entry count
wallet_limits.daily_entries += entries;
// Step 3: Record purchase and add to current block
let state = &mut ctx.accounts.contract_state;
state.total_purchases += 1;
let current_block_num = ((state.total_purchases - 1) / 100) + 1;
if current_block_num > state.current_block {
state.current_block = current_block_num;
// Initialize new block if needed
}
// Step 4: Add entries to current block
add_entries_to_block(ctx, entries, usd_amount, token_amount)?;
// Step 5: Check if block is complete (100 purchases)
if state.total_purchases % 100 == 0 {
process_block_lottery(ctx, current_block_num)?;
}
// Step 6: Check legendary milestone (every 1000 purchases)
if state.total_purchases % 1000 == 0 {
process_legendary_milestone(ctx)?;
}
Ok(())
}
pub fn process_block_lottery(
ctx: Context<ProcessBlockLottery>,
block_number: u64,
) -> Result<()> {
let state = &mut ctx.accounts.contract_state;
let block = &mut ctx.accounts.purchase_block;
// Step 1: Check available card slots
let auto_slots_available = 400 - state.auto_cards_in_circulation;
let flexible_slots_available = 200 - state.flexible_cards_in_circulation;
let auto_cards_to_award = std::cmp::min(4, auto_slots_available);
let flexible_cards_to_award = std::cmp::min(2, flexible_slots_available);
// Step 2: Process recycled cards (24h delay)
process_card_recycling(ctx)?;
// Step 3: Generate random selections
let vrf_data = &ctx.accounts.vrf_account.load()?;
let random_seed = vrf_data.get_result()?;
// Step 4: Award Auto-Redeem cards
if auto_cards_to_award > 0 {
let winners = select_random_entries(
&block.entries,
auto_cards_to_award as usize,
random_seed,
0 // Salt for auto cards
);
for (winner_idx, entry_idx) in winners {
mint_auto_redeem_card(
ctx,
&block.entries[winner_idx],
entry_idx,
generate_auto_redeem_params(random_seed, winner_idx)?,
)?;
}
state.auto_cards_in_circulation += auto_cards_to_award;
block.processed_auto = true;
}
// Step 5: Award Flexible cards
if flexible_cards_to_award > 0 {
let winners = select_random_entries(
&block.entries,
flexible_cards_to_award as usize,
random_seed,
1 // Salt for flexible cards
);
for (winner_idx, entry_idx) in winners {
mint_flexible_card(
ctx,
&block.entries[winner_idx],
entry_idx,
generate_flexible_params(random_seed, winner_idx)?,
)?;
}
state.flexible_cards_in_circulation += flexible_cards_to_award;
block.processed_flexible = true;
}
Ok(())
}
fn select_random_entries(
entries: &[BlockEntry],
count: usize,
random_seed: [u8; 32],
salt: u8,
) -> Result<Vec<(usize, usize)>> {
// Create weighted entry pool
let mut entry_pool = Vec::new();
for (entry_idx, entry) in entries.iter().enumerate() {
for sub_entry_idx in 0..entry.entry_count {
entry_pool.push((entry_idx, sub_entry_idx as usize));
}
}
require!(entry_pool.len() > 0, ErrorCode::NoEntriesAvailable);
// Fisher-Yates shuffle with seeded random
let mut rng = create_seeded_rng(random_seed, salt);
let mut winners = Vec::new();
for _ in 0..std::cmp::min(count, entry_pool.len()) {
let idx = rng.next_u32() as usize % entry_pool.len();
winners.push(entry_pool.swap_remove(idx));
}
Ok(winners)
}
fn create_seeded_rng(seed: [u8; 32], salt: u8) -> ChaCha20Rng {
let mut final_seed = seed;
final_seed[0] ^= salt; // XOR salt into seed
ChaCha20Rng::from_seed(final_seed)
}
fn generate_auto_redeem_params(
random_seed: [u8; 32],
winner_idx: usize,
) -> Result<(i64, u8)> {
let mut rng = create_seeded_rng(random_seed, (winner_idx as u8) ^ 0xAA);
// Random auto-redeem time: 4-10 days
let days = 4 + (rng.next_u32() % 7); // 4-10 days
let auto_redeem_time = Clock::get()?.unix_timestamp + (days as i64 * 86400);
// Random reward percentage: 1-5%
let reward_percent = 1 + (rng.next_u32() % 5) as u8; // 1-5%
Ok((auto_redeem_time, reward_percent))
}
fn generate_flexible_params(
random_seed: [u8; 32],
winner_idx: usize,
) -> Result<(i64, i64, i64, u8)> {
let mut rng = create_seeded_rng(random_seed, (winner_idx as u8) ^ 0xBB);
let current_time = Clock::get()?.unix_timestamp;
// Random hold period: 4 days to 3 weeks
let min_days = 4;
let max_days = 21;
let hold_days = min_days + (rng.next_u32() % (max_days - min_days + 1));
let full_value_period = hold_days as i64 * 86400;
// Forced redemption deadline: 1 week after full value period
let max_hold_deadline = current_time + full_value_period + (7 * 86400);
// Random reward percentage: 1-5%
let reward_percent = 1 + (rng.next_u32() % 5) as u8;
Ok((
4 * 86400, // min_hold_period (4 days)
full_value_period, // full_value_period
max_hold_deadline, // max_hold_deadline
reward_percent, // reward_percent
))
}
fn generate_legendary_params(
random_seed: [u8; 32],
winner_idx: usize,
) -> Result<(i64, u8)> {
let mut rng = create_seeded_rng(random_seed, (winner_idx as u8) ^ 0xCC);
let current_time = Clock::get()?.unix_timestamp;
// Random expiry: 4 weeks to 6 months
let min_weeks = 4;
let max_weeks = 26; // ~6 months
let expiry_weeks = min_weeks + (rng.next_u32() % (max_weeks - min_weeks + 1));
let expiry_date = current_time + (expiry_weeks as i64 * 7 * 86400);
// Random reward percentage: 1-5%
let reward_percent = 1 + (rng.next_u32() % 5) as u8;
Ok((expiry_date, reward_percent))
}
The contract includes specialized minting functions for each card type, calculating rewards based on purchase amounts and applying the $1000 USD cap:
pub fn mint_auto_redeem_card(
ctx: Context<MintCard>,
winning_entry: &BlockEntry,
entry_idx: usize,
params: (i64, u8), // (auto_redeem_time, reward_percent)
) -> Result<()> {
let (auto_redeem_time, reward_percent) = params;
// Calculate reward amount
let purchase_amount = winning_entry.purchase_amounts[entry_idx];
let token_amount = winning_entry.token_amounts[entry_idx];
let base_reward = (token_amount as u128 * reward_percent as u128) / 100;
// Apply $1000 USD cap
let usd_cap = 100000; // $1000.00 in cents
let reward_token_amount = if purchase_amount > usd_cap {
// Cap reward at $1000 worth of tokens
calculate_tokens_for_usd(usd_cap, ctx.accounts.price_feed)?
} else {
base_reward as u64
};
// Create card
let card = &mut ctx.accounts.kings_card;
card.card_id = generate_card_id()?;
card.card_type = CardType::AutoRedeem { auto_redeem_time };
card.reward_token_amount = reward_token_amount;
card.reward_usd_cap = usd_cap;
card.mint_time = Clock::get()?.unix_timestamp;
card.owner = winning_entry.wallet;
card.original_purchase_amount = purchase_amount;
card.is_redeemed = false;
// Mint NFT using Metaplex
mint_nft_metadata(ctx, &format!("Auto-Redeem Kings Card #{}", card.card_id))?;
emit!(CardMinted {
card_id: card.card_id,
card_type: "AutoRedeem".to_string(),
owner: card.owner,
reward_amount: reward_token_amount,
});
Ok(())
}
Each card type has specific redemption rules with timing constraints, pro-rating mechanisms, and automatic tax collection:
pub fn redeem_auto_card(ctx: Context<RedeemCard>) -> Result<()> {
let card = &mut ctx.accounts.kings_card;
let current_time = Clock::get()?.unix_timestamp;
match &card.card_type {
CardType::AutoRedeem { auto_redeem_time } => {
require!(
current_time >= *auto_redeem_time,
ErrorCode::AutoRedeemNotReady
);
},
_ => return Err(ErrorCode::WrongCardType.into()),
}
// Calculate current token value and tax
let current_token_price = get_current_token_price(ctx.accounts.price_feed)?;
let reward_value_usd = (card.reward_token_amount as u128 * current_token_price as u128) / 1e9 as u128;
// Apply 5% tax
let tax_amount = (card.reward_token_amount as u128 * 5) / 100;
let user_reward = card.reward_token_amount - tax_amount as u64;
// Transfer tokens to user
transfer_tokens(
ctx.accounts.escrow_account.to_account_info(),
ctx.accounts.user_token_account.to_account_info(),
user_reward,
)?;
// Transfer tax to treasury
transfer_tokens(
ctx.accounts.escrow_account.to_account_info(),
ctx.accounts.treasury_account.to_account_info(),
tax_amount as u64,
)?;
// Mark as redeemed and schedule for recycling
card.is_redeemed = true;
card.redemption_time = Some(current_time);
schedule_card_recycling(ctx, card.card_id, current_time + 86400)?; // 24h delay
// Update circulation count
let state = &mut ctx.accounts.contract_state;
state.auto_cards_in_circulation -= 1;
emit!(CardRedeemed {
card_id: card.card_id,
owner: card.owner,
token_amount: user_reward,
tax_amount: tax_amount as u64,
});
Ok(())
}
Every 1000 purchases triggers a legendary card lottery, selecting a winner from all entries in the previous 1000 purchases:
pub fn process_legendary_milestone(ctx: Context<ProcessLegendary>) -> Result<()> {
let state = &mut ctx.accounts.contract_state;
// Check if legendary slot available
if state.legendary_cards_in_circulation >= 5 {
// No slots available, skip this milestone
return Ok(());
}
// Select winner from last 1000 purchases
let end_purchase = state.total_purchases;
let start_purchase = if end_purchase >= 1000 {
end_purchase - 999 // Last 1000 purchases
} else {
1 // From beginning if less than 1000 total
};
// Collect all entries from the range
let mut candidate_entries = Vec::new();
for purchase_num in start_purchase..=end_purchase {
let block_num = ((purchase_num - 1) / 100) + 1;
let entry_in_block = ((purchase_num - 1) % 100) as usize;
// Load block and get entry
let block = load_purchase_block(ctx, block_num)?;
if let Some(entry) = get_entry_for_purchase(block, entry_in_block) {
candidate_entries.push((block_num, entry_in_block, entry));
}
}
require!(!candidate_entries.is_empty(), ErrorCode::NoLegendaryCanditates);
// Random selection
let vrf_data = &ctx.accounts.vrf_account.load()?;
let random_seed = vrf_data.get_result()?;
let mut rng = create_seeded_rng(random_seed, 0xCC);
// Flatten to individual entries
let mut all_entries = Vec::new();
for (block_num, entry_idx, entry) in candidate_entries {
for sub_entry_idx in 0..entry.entry_count {
all_entries.push((block_num, entry_idx, sub_entry_idx, &entry));
}
}
let winner_idx = rng.next_u32() as usize % all_entries.len();
let (_, _, sub_entry_idx, winning_entry) = &all_entries[winner_idx];
// Generate legendary card parameters
let (expiry_date, reward_percent) = generate_legendary_params(random_seed, winner_idx)?;
// Mint legendary card
mint_legendary_card(
ctx,
winning_entry,
*sub_entry_idx,
(expiry_date, reward_percent),
)?;
state.legendary_cards_in_circulation += 1;
Ok(())
}
#[error_code]
pub enum ErrorCode {
#[msg("Purchase amount too small (minimum $10 USD)")]
PurchaseTooSmall,
#[msg("No eligible entries after applying limits")]
NoEligibleEntries,
#[msg("Auto-redeem time not reached yet")]
AutoRedeemNotReady,
#[msg("Minimum hold period not met")]
MinHoldNotMet,
#[msg("Wrong card type for this operation")]
WrongCardType,
#[msg("Card has expired and has no value")]
CardExpired,
#[msg("No entries available for lottery")]
NoEntriesAvailable,
#[msg("No candidates available for legendary card")]
NoLegendaryCanditates,
#[msg("Insufficient escrow balance")]
InsufficientEscrow,
#[msg("Invalid price feed data")]
InvalidPriceFeed,
#[msg("Daily entry limit exceeded")]
DailyLimitExceeded,
#[msg("Hourly purchase limit exceeded")]
HourlyLimitExceeded,
}
The contract implements multiple security layers including price feed validation, ownership verification, and anti-reentrancy protection:
The contract emits comprehensive events for all major operations, enabling off-chain monitoring and analytics:
#[event]
pub struct PurchaseProcessed {
pub buyer: Pubkey,
pub usd_amount: u64,
pub token_amount: u64,
pub entries_awarded: u8,
pub block_number: u64,
}
#[event]
pub struct CardMinted {
pub card_id: u64,
pub card_type: String,
pub owner: Pubkey,
pub reward_amount: u64,
}
#[event]
pub struct CardRedeemed {
pub card_id: u64,
pub owner: Pubkey,
pub token_amount: u64,
pub tax_amount: u64,
}
#[event]
pub struct LegendaryMilestone {
pub milestone_number: u64,
pub total_candidates: u32,
pub winner: Option<Pubkey>,
}
// Devnet deployment
pub const DEVNET_CONFIG: ContractConfig = ContractConfig {
min_purchase_usd: 1000, // $10.00 in cents
max_daily_entries: 12,
hourly_purchase_limit: 2,
auto_card_limit: 400,
flexible_card_limit: 200,
legendary_card_limit: 5,
recycle_delay: 86400, // 24 hours
tax_rate: 500, // 5.00% in basis points
};
// Mainnet deployment
pub const MAINNET_CONFIG: ContractConfig = ContractConfig {
min_purchase_usd: 1000, // $10.00 in cents
max_daily_entries: 12,
hourly_purchase_limit: 2,
auto_card_limit: 400,
flexible_card_limit: 200,
legendary_card_limit: 5,
recycle_delay: 86400, // 24 hours
tax_rate: 500, // 5.00% in basis points
};
pub fn initialize_contract(
ctx: Context<Initialize>,
config: ContractConfig,
authority: Pubkey,
escrow_account: Pubkey,
treasury_account: Pubkey,
) -> Result<()> {
let state = &mut ctx.accounts.contract_state;
state.total_purchases = 0;
state.current_block = 0;
state.legendary_milestone = 1000;
state.auto_cards_in_circulation = 0;
state.flexible_cards_in_circulation = 0;
state.legendary_cards_in_circulation = 0;
state.authority = authority;
state.escrow_account = escrow_account;
state.treasury_account = treasury_account;
// Initialize recycling queues
state.auto_card_recycle_queue = Vec::new();
state.flexible_card_recycle_queue = Vec::new();
state.legendary_card_recycle_queue = Vec::new();
Ok(())
}
This technical specification provides comprehensive implementation details for the Kings Card NFT Rewards Smart Contract system. It covers all core mechanics, data structures, algorithms, and security measures required to build a robust, fair, and transparent reward distribution system for the $PEASANT token revival project.