diff --git a/README.md b/README.md index c46aa61..8080a39 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ # ORE -**Mine blockspace. Trade hashpower. Earn rewards.** +**Mine blocks. Trade hashpower. Earn rewards.** + +## Summary + +ORE is a cryptocurrency mining and trading game. Miners compete to earn block rewards by buying and selling hashpower in an open market. The more hashpower a miner has, the greater their chances of finding the best hash and winning the block reward. + +Rewards and + ## API - [`Consts`](api/src/consts.rs) – Program constants. @@ -10,20 +17,33 @@ ## Instructions -- [`Open`](program/src/open.rs) - Open a new block. -- [`Close`](program/src/close.rs) - Close a block and pay out the reward. -- [`Mine`](program/src/mine.rs) - Mine the current block. -- [`Swap`](program/src/swap.rs) - Trade in a hashpower market. +#### User +- [`Claim`](program/src/claim.rs) - Claims miner rewards. +- [`Close`](program/src/close.rs) - Closes a block account. +- [`Initialize`](program/src/initialize.rs) - Initializes the program. +- [`Log`](program/src/log.rs) – Logs events as non-truncatable data. +- [`Mine`](program/src/mine.rs) - Submits hashes for scoring. +- [`Open`](program/src/open.rs) - Opens a new block. +- [`Reset`](program/src/reset.rs) – Resets the hashpower market for the next block. +- [`Swap`](program/src/swap.rs) - Executes a buy or sell in the hashpower market. + +#### Admin +- [`SetAdmin`](program/src/set_admin.rs) - Re-assigns the admin authority. +- [`SetFeeCollector`](program/src/set_admin.rs) - Updates the fee collection address. +- [`SetFeeRate`](program/src/set_admin.rs) - Updates the fee charged per swap. ## State -- [`Block`](api/src/state/block.rs) - A period of time for mining. +- [`Block`](api/src/state/block.rs) - A round in the game. - [`Config`](api/src/state/config.rs) - Global program configuration. -- [`Market`](api/src/state/market.rs) - Hashpower market for a given block. +- [`Market`](api/src/state/market.rs) - Hashpower market. - [`Miner`](api/src/state/miner.rs) - Tracks a miner state and history. -- [`Permit`](api/src/state/permit.rs) - Tracks a miner's commitment to mine a block. -- [`Stake`](api/src/state/stake.rs) - Tracks a miner's collateral for trading in a market. - [`Treasury`](api/src/state/treasury.rs) - The mint authority of the ORE token. +## Block lifecycle + +- Open a block with an upcoming ID. +- Reset the market to move it forward to the next ID. + ## Tests diff --git a/api/src/consts.rs b/api/src/consts.rs index 8ae0352..b054fc4 100644 --- a/api/src/consts.rs +++ b/api/src/consts.rs @@ -58,12 +58,15 @@ pub const TREASURY_ADDRESS: Pubkey = /// The address of the treasury account. pub const TREASURY_BUMP: u8 = ed25519::derive_program_address(&[TREASURY], &PROGRAM_ID).1; -/// Denominator for protocol fee calculations. -pub const FEE_RATE_BPS: u64 = 100; +/// Swap fee in lamports. +pub const FEE_LAMPORTS: u64 = 1_000_000; // 0.001 SOL /// Denominator for fee calculations. pub const DENOMINATOR_BPS: u64 = 10_000; +/// Window to submit hashes, in slots. +pub const INITIAL_BLOCK_DURATION: u64 = 1500; + /// Window to submit hashes, in slots. pub const MINING_WINDOW: u64 = 1500; @@ -74,7 +77,7 @@ pub const SLOT_WINDOW: u64 = 4; pub const HASHPOWER_LIQUIDITY: u64 = 1_000_000; /// The ORE liquidity to seed the markets with. -pub const ORE_LIQUIDITY: u64 = ONE_ORE * 5; +pub const ORE_LIQUIDITY: u64 = ONE_ORE * 100; /// The minimum difficulty required for payout. pub const NUGGET_DIFFICULTY: u64 = 10; diff --git a/api/src/instruction.rs b/api/src/instruction.rs index 25086d8..a0d1c89 100644 --- a/api/src/instruction.rs +++ b/api/src/instruction.rs @@ -4,6 +4,7 @@ use steel::*; #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] pub enum OreInstruction { // User + Claim = 0, Log = 1, Mine = 2, Swap = 3, @@ -18,6 +19,12 @@ pub enum OreInstruction { SetFeeRate = 10, } +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Claim { + pub amount: [u8; 8], +} + #[repr(C)] #[derive(Clone, Copy, Debug, Pod, Zeroable)] pub struct Open { @@ -89,6 +96,7 @@ pub struct SetFeeRate { pub fee_rate: [u8; 8], } +instruction!(OreInstruction, Claim); instruction!(OreInstruction, Open); instruction!(OreInstruction, Close); instruction!(OreInstruction, Reset); diff --git a/api/src/state/market/market.rs b/api/src/state/market/market.rs index 1d97d6f..5d543c2 100644 --- a/api/src/state/market/market.rs +++ b/api/src/state/market/market.rs @@ -134,7 +134,6 @@ account!(OreAccount, Market); #[cfg(test)] mod tests { - use crate::consts::FEE_RATE_BPS; use super::*; @@ -441,7 +440,7 @@ mod tests { fee: FeeParams { cumulative: 0, uncollected: 0, - rate: FEE_RATE_BPS, + rate: 100, // 100 bps }, snapshot: Snapshot { enabled: 1, diff --git a/program/src/claim.rs b/program/src/claim.rs new file mode 100644 index 0000000..380b087 --- /dev/null +++ b/program/src/claim.rs @@ -0,0 +1,56 @@ +use ore_api::prelude::*; +use steel::*; + +/// Claims a block reward. +pub fn process_claim(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // Parse data. + let args = Claim::try_from_bytes(data)?; + let amount = u64::from_le_bytes(args.amount); + + // Load accounts. + let [signer_info, miner_info, miner_rewards_info, recipient_info, mint_info, system_program, token_program, associated_token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + signer_info.is_signer()?; + let miner = miner_info + .as_account::(&ore_api::ID)? + .assert(|m| m.authority == *signer_info.key)?; + let miner_rewards = + miner_rewards_info.as_associated_token_account(&miner_info.key, &mint_info.key)?; + mint_info.has_address(&MINT_ADDRESS)?.as_mint()?; + system_program.is_program(&system_program::ID)?; + token_program.is_program(&spl_token::ID)?; + associated_token_program.is_program(&spl_associated_token_account::ID)?; + + // Load recipient. + if recipient_info.data_is_empty() { + create_associated_token_account( + signer_info, + signer_info, + recipient_info, + mint_info, + system_program, + token_program, + associated_token_program, + )?; + } else { + recipient_info.as_associated_token_account(signer_info.key, mint_info.key)?; + } + + // Load amount. + let amount = miner_rewards.amount().min(amount); + + // Transfer reward to recipient. + transfer_signed( + miner_info, + miner_rewards_info, + mint_info, + token_program, + amount, + &[MINER, miner.authority.as_ref()], + )?; + + Ok(()) +} diff --git a/program/src/close.rs b/program/src/close.rs index b2082d0..0c61a26 100644 --- a/program/src/close.rs +++ b/program/src/close.rs @@ -5,7 +5,7 @@ use steel::*; pub fn process_close(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. let clock = Clock::get()?; - let [signer_info, block_info, miner_info, mint_info, opener_info, recipient_info, treasury_info, system_program, token_program, ore_program] = + let [signer_info, block_info, miner_info, miner_rewards_info, mint_info, opener_info, recipient_info, treasury_info, treasury_tokens_info, system_program, token_program, associated_token_program, ore_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -17,10 +17,29 @@ pub fn process_close(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul mint_info.has_address(&MINT_ADDRESS)?.as_mint()?; opener_info.is_writable()?.has_address(&block.opener)?; treasury_info.as_account::(&ore_api::ID)?; + treasury_tokens_info + .is_writable()? + .as_associated_token_account(treasury_info.key, mint_info.key)?; system_program.is_program(&system_program::ID)?; token_program.is_program(&spl_token::ID)?; + associated_token_program.is_program(&spl_associated_token_account::ID)?; ore_program.is_program(&ore_api::ID)?; + // Load miner rewards. + if miner_rewards_info.data_is_empty() { + create_associated_token_account( + signer_info, + miner_info, + miner_rewards_info, + mint_info, + system_program, + token_program, + associated_token_program, + )?; + } else { + miner_rewards_info.as_associated_token_account(&miner_info.key, &mint_info.key)?; + } + // Payout block reward. if block.best_hash_miner != Pubkey::default() { // Load recipient. @@ -29,21 +48,16 @@ pub fn process_close(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul .as_account_mut::(&ore_api::ID)? .assert_mut(|m| m.authority == block.best_hash_miner)?; - // Limit payout to supply cap. - let ore_mint = mint_info.as_mint()?; - let max_reward = MAX_SUPPLY.saturating_sub(ore_mint.supply()); - let reward_amount = block.reward.min(max_reward); - // Update stats. - miner.total_rewards += reward_amount; + miner.total_rewards += block.reward; - // Mint reward to recipient. - mint_to_signed( - mint_info, - recipient_info, + // Transfer reward to miner. + transfer_signed( treasury_info, + treasury_tokens_info, + miner_rewards_info, token_program, - reward_amount, + block.reward, &[TREASURY], )?; } diff --git a/program/src/initialize.rs b/program/src/initialize.rs index 870dfea..4fc285d 100644 --- a/program/src/initialize.rs +++ b/program/src/initialize.rs @@ -5,7 +5,7 @@ use steel::*; /// Initializes the program. pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. - let [signer_info, config_info, market_info, mint_info, treasury_info, vault_info, system_program, token_program] = + let [signer_info, config_info, market_info, mint_info, treasury_info, treasury_tokens_info, vault_info, system_program, token_program, associated_token_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -24,12 +24,14 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program .is_writable()? .is_empty()? .has_seeds(&[TREASURY], &ore_api::ID)?; + treasury_tokens_info.is_writable()?.is_empty()?; vault_info .is_writable()? .is_empty()? .has_address(&vault_pda().0)?; system_program.is_program(&system_program::ID)?; token_program.is_program(&spl_token::ID)?; + associated_token_program.is_program(&spl_associated_token_account::ID)?; // Create config account. create_program_account::( @@ -41,9 +43,9 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program )?; let config = config_info.as_account_mut::(&ore_api::ID)?; config.admin = *signer_info.key; - config.block_duration = 1500; + config.block_duration = INITIAL_BLOCK_DURATION; config.fee_collector = *signer_info.key; - config.fee_rate = 0; + config.fee_rate = FEE_LAMPORTS; // Initialize market. let initial_id: u64 = 0; @@ -66,7 +68,7 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program balance_virtual: 0, }; market.fee = FeeParams { - rate: FEE_RATE_BPS, + rate: 0, uncollected: 0, cumulative: 0, }; @@ -87,6 +89,21 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program &[TREASURY], )?; + // Load treasury tokens. + if treasury_tokens_info.data_is_empty() { + create_associated_token_account( + signer_info, + treasury_info, + treasury_tokens_info, + mint_info, + system_program, + token_program, + associated_token_program, + )?; + } else { + treasury_tokens_info.as_associated_token_account(treasury_info.key, mint_info.key)?; + } + // Initialize vault token account. if vault_info.data_is_empty() { let vault_pda = vault_pda(); diff --git a/program/src/lib.rs b/program/src/lib.rs index 706f53d..2eeeff8 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,3 +1,4 @@ +mod claim; mod close; mod initialize; mod log; @@ -9,6 +10,7 @@ mod set_fee_collector; mod set_fee_rate; mod swap; +use claim::*; use close::*; use initialize::*; use log::*; @@ -32,6 +34,7 @@ pub fn process_instruction( match ix { // User + OreInstruction::Claim => process_claim(accounts, data)?, OreInstruction::Open => process_open(accounts, data)?, OreInstruction::Close => process_close(accounts, data)?, OreInstruction::Log => process_log(accounts, data)?, diff --git a/program/src/open.rs b/program/src/open.rs index 73239b4..2d7fe91 100644 --- a/program/src/open.rs +++ b/program/src/open.rs @@ -1,5 +1,4 @@ use ore_api::prelude::*; -use solana_nostd_keccak::hash; use steel::*; /// Opens a new block. @@ -34,7 +33,7 @@ pub fn process_open(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult let block = block_info.as_account_mut::(&ore_api::ID)?; block.id = id; block.opener = *signer_info.key; - block.reward = calculate_reward(block.id); + block.reward = 0; block.best_hash = [0; 32]; block.best_hash_miner = Pubkey::default(); block.start_slot = u64::MAX; // Set by reset @@ -61,34 +60,3 @@ pub fn process_open(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult Ok(()) } - -fn calculate_reward(block_id: u64) -> u64 { - // Generate noise. - let noise_seed = block_id.to_le_bytes(); - let noise = hash(&noise_seed); - - // Extract the first byte (0 to 255). - let byte_value = noise[0]; - - // Map to 1-10 using integer division - let n = (byte_value / 25) + 1; - - // Ensure the value doesn't exceed 10 - let n = n.min(10); - - n as u64 * ONE_ORE -} - -#[test] -fn test_lode_rewards() { - for i in 0u64..1000 { - let lode_reward = ONE_ORE * calculate_reward(i) as u64; - let target_block_reward = ONE_ORE * 10; - let expected_hashes_per_block = HASHPOWER_LIQUIDITY / 2; - let expected_qualifying_hashes = - expected_hashes_per_block / 2u64.pow(NUGGET_DIFFICULTY as u32); - let difficulty_reward = (target_block_reward - lode_reward) / expected_qualifying_hashes; - println!("{}: {} {}", i, lode_reward, difficulty_reward); - } - // assert!(false); -} diff --git a/program/src/reset.rs b/program/src/reset.rs index 9410400..787b06c 100644 --- a/program/src/reset.rs +++ b/program/src/reset.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use ore_api::prelude::*; use solana_program::slot_hashes::SlotHashes; use steel::*; @@ -6,7 +8,7 @@ use steel::*; pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. let clock = Clock::get()?; - let [signer_info, block_prev_info, block_next_info, config_info, fee_collector_info, market_info, mint_info, vault_info, system_program, token_program, ore_program, slot_hashes_sysvar] = + let [signer_info, block_prev_info, block_next_info, config_info, fee_collector_info, market_info, mint_info, treasury_info, treasury_tokens_info, vault_info, system_program, token_program, ore_program, slot_hashes_sysvar] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -21,6 +23,10 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul .as_account_mut::(&ore_api::ID)? .assert_mut(|m| m.block_id == block_next.id - 1)?; mint_info.has_address(&MINT_ADDRESS)?.as_mint()?; + treasury_info.as_account::(&ore_api::ID)?; + treasury_tokens_info + .is_writable()? + .as_associated_token_account(treasury_info.key, mint_info.key)?; let vault = vault_info.as_associated_token_account(&mint_info.key, &mint_info.key)?; system_program.is_program(&system_program::ID)?; token_program.is_program(&spl_token::ID)?; @@ -34,28 +40,33 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul .assert_mut(|b| b.id == market.block_id)? .assert_mut(|b| b.end_slot <= clock.slot)?; - // Set the slot hash of the previous block. - let slot_hashes = - bincode::deserialize::(slot_hashes_sysvar.data.borrow().as_ref()).unwrap(); - let Some(slot_hash) = slot_hashes.get(&block_prev.end_slot) else { - // If mine is not called within ~2.5 minutes of the block starting, - // then the slot hash will be unavailable and secure hashes cannot be generated. - return Ok(()); - }; - block_prev.slot_hash = slot_hash.to_bytes(); - } + // Get the slot hash, given the end slot of the previous block. + if let Ok(slot_hash) = get_slot_hash(block_prev.end_slot, slot_hashes_sysvar) { + // Set the block slot hash. + block_prev.slot_hash = slot_hash; - // Payout fee. - if market.fee.uncollected > 0 { - transfer_signed( - market_info, - vault_info, - fee_collector_info, - token_program, - market.fee.uncollected, - &[MARKET], - )?; - market.fee.uncollected = 0; + // Calculate the block reward. + let block_reward = calculate_block_reward(&block_prev.slot_hash); + + // Limit the block reward to supply cap. + let ore_mint = mint_info.as_mint()?; + let max_reward = MAX_SUPPLY.saturating_sub(ore_mint.supply()); + let block_reward = block_reward.min(max_reward); + + // Set the block reward. + block_prev.reward = block_reward; + + // Mint the block reward to the treasury. + // This will get transferred to the miner account for claiming when the block is closed. + mint_to_signed( + mint_info, + treasury_tokens_info, + treasury_info, + token_program, + block_reward, + &[TREASURY], + )?; + } } // Burn vault liquidity. @@ -79,7 +90,7 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul market.snapshot.base_balance = 0; market.snapshot.quote_balance = 0; market.snapshot.slot = 0; - market.fee.rate = config.fee_rate; + market.fee.rate = 0; market.fee.uncollected = 0; market.fee.cumulative = 0; @@ -89,3 +100,56 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul Ok(()) } + +fn get_slot_hash( + slot: u64, + slot_hashes_sysvar: &AccountInfo<'_>, +) -> Result<[u8; 32], ProgramError> { + let slot_hashes = + bincode::deserialize::(slot_hashes_sysvar.data.borrow().as_ref()).unwrap(); + let Some(slot_hash) = slot_hashes.get(&slot) else { + // If reset is not called within ~2.5 minutes of the block ending, + // then the slot hash will be unavailable and secure hashes cannot be generated. + return Err(ProgramError::InvalidAccountData); + }; + let slot_hash = slot_hash.to_bytes(); + Ok(slot_hash) +} + +fn calculate_block_reward(slot_hash: &[u8]) -> u64 { + let block_distribution = HashMap::from([ + (1u64, 737869762948382064), // 4% + (2u64, 1641760222560150093), // 4.9% + (3u64, 2564097426245627674), // 5% + (4u64, 3486434629931105255), // 5% + (5u64, 4408771833616582835), // 5% + (6u64, 5331109037302060416), // 5% + (7u64, 6253446240987537997), // 5% + (8u64, 7175783444673015578), // 5% + (9u64, 8098120648358493158), // 5% + (10u64, 9020457852043970739), // 5% + (11u64, 9942795055729448320), // 5% + (12u64, 10865132259414925901), // 5% + (13u64, 11787469463100403481), // 5% + (14u64, 12709806666785881062), // 5% + (15u64, 13632143870471358643), // 5% + (16u64, 14554481074156836224), // 5% + (17u64, 15476818277842313804), // 5% + (18u64, 16399155481527791385), // 5% + (19u64, 17321492685213268966), // 5% + (20u64, 18243829888898746547), // 5% + (100u64, 18428297329635842063), // 1% + (1000u64, u64::MAX), // 0.1% + ]); + let r1 = u64::from_le_bytes(slot_hash[0..8].try_into().unwrap()); + let r2 = u64::from_le_bytes(slot_hash[8..16].try_into().unwrap()); + let r3 = u64::from_le_bytes(slot_hash[16..24].try_into().unwrap()); + let r4 = u64::from_le_bytes(slot_hash[24..32].try_into().unwrap()); + let r = r1 ^ r2 ^ r3 ^ r4; + for (k, v) in block_distribution.iter() { + if r <= *v { + return *k; + } + } + 0 +} diff --git a/program/src/set_fee_rate.rs b/program/src/set_fee_rate.rs index d2c9bef..03c6259 100644 --- a/program/src/set_fee_rate.rs +++ b/program/src/set_fee_rate.rs @@ -17,12 +17,6 @@ pub fn process_set_fee_rate(accounts: &[AccountInfo<'_>], data: &[u8]) -> Progra .assert_mut(|c| c.admin == *signer_info.key)?; system_program.is_program(&system_program::ID)?; - // Limit fee rate. - assert!( - new_fee_rate <= FEE_RATE_BPS, - "Fee rate must be less than or equal to 100 bps" - ); - // Set fee rate. config.fee_rate = new_fee_rate; diff --git a/program/src/swap.rs b/program/src/swap.rs index e1a86f7..3d1816b 100644 --- a/program/src/swap.rs +++ b/program/src/swap.rs @@ -11,7 +11,7 @@ pub fn process_swap(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult // Load accounts. let clock = Clock::get()?; - let [signer_info, block_info, market_info, miner_info, mint_info, tokens_info, vault_info, system_program, token_program, associated_token_program, ore_program] = + let [signer_info, block_info, config_info, fee_collector_info, market_info, miner_info, mint_info, tokens_info, vault_info, system_program, token_program, associated_token_program, ore_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); @@ -21,6 +21,10 @@ pub fn process_swap(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult .as_account_mut::(&ore_api::ID)? .assert_mut(|b| b.start_slot <= clock.slot)? .assert_mut(|b| b.end_slot > clock.slot)?; + let config = config_info.as_account_mut::(&ore_api::ID)?; + fee_collector_info + .is_writable()? + .has_address(&config.fee_collector)?; let market = market_info .as_account_mut::(&ore_api::ID)? .assert_mut(|m| m.block_id == block.id)? @@ -41,6 +45,11 @@ pub fn process_swap(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult associated_token_program.is_program(&spl_associated_token_account::ID)?; ore_program.is_program(&ore_api::ID)?; + // Pay swap fee. + if config.fee_rate > 0 { + signer_info.send(config.fee_rate, fee_collector_info); + } + // Load token acccounts. if tokens_info.data_is_empty() { create_associated_token_account( @@ -58,7 +67,11 @@ pub fn process_swap(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult .as_associated_token_account(signer_info.key, mint_info.key)?; } - // Update market state. + // Set the sniper fee based on time since the market began. + let fee_rate = calculate_sniper_fee(&clock, block); + market.fee.rate = fee_rate; + + // Execute the swap let mut swap_event = market.swap(amount, direction, precision, clock)?; swap_event.authority = *signer_info.key; swap_event.block_id = block.id; @@ -109,3 +122,53 @@ pub fn process_swap(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult Ok(()) } + +fn calculate_sniper_fee(clock: &Clock, block: &Block) -> u64 { + let elapsed_slots = clock.slot.saturating_sub(block.start_slot); + + if elapsed_slots >= 100 { + return 0; + } + + // Linear decay from 5000 bps (50%) to 0 bps over 100 slots + // Using formula: y = mx + b + // Where: + // - x is elapsed_slots (0 to 100) + // - y is fee_bps (5000 to 0) + // - m = -50 (slope) + // - b = 5000 (y-intercept) + + let remaining_fee = 5000 - (elapsed_slots * 50); + remaining_fee +} + +#[test] +fn test_sniper_fees() { + let mut clock = Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + + let block = Block { + id: 0, + opener: Pubkey::default(), + reward: 0, + best_hash: [0; 32], + best_hash_miner: Pubkey::default(), + start_slot: 0, + end_slot: u64::MAX, + slot_hash: [0; 32], + total_hashpower: 0, + }; + + for i in 0..200 { + clock.slot = i; + let fee = calculate_sniper_fee(&clock, &block); + println!("Slot {}: {} bps fee", i, fee); + } + + // assert!(false); +}