mystery block reward, sniper fee

This commit is contained in:
Hardhat Chad
2025-07-18 11:09:06 -07:00
parent acbb1be65f
commit 461505cd2c
12 changed files with 303 additions and 94 deletions

56
program/src/claim.rs Normal file
View File

@@ -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::<Miner>(&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(())
}

View File

@@ -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::<Treasury>(&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::<Miner>(&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],
)?;
}

View File

@@ -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::<Config>(
@@ -41,9 +43,9 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program
)?;
let config = config_info.as_account_mut::<Config>(&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();

View File

@@ -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)?,

View File

@@ -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::<Block>(&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);
}

View File

@@ -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::<Market>(&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::<Treasury>(&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::<SlotHashes>(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::<SlotHashes>(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
}

View File

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

View File

@@ -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::<Block>(&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::<Config>(&ore_api::ID)?;
fee_collector_info
.is_writable()?
.has_address(&config.fee_collector)?;
let market = market_info
.as_account_mut::<Market>(&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);
}