use cosmwasm_std::{
entry_point, to_json_binary, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo,
Response, StdResult, Uint128, Uint256, WasmMsg,
};
use cw2::{get_contract_version, set_contract_version};
use crate::emission::emission_rate_per_second;
use crate::error::ContractError;
use crate::msg::{
ConfigResponse, EmissionInfoResponse, ExecuteMsg, InstantiateMsg, MinerStatsResponse, QueryMsg,
RewardCalculationResponse, StatsResponse, TestingOverrides, WindowStatusResponse,
};
use crate::state::{
FeeBucket, FeeHistory, MineConfig, MinerStats, PidState, SlidingWindow, Stats, WindowEntry,
CONFIG, FEE_HISTORY, MINER_STATS, PID_STATE, PROOF_PRUNE_CURSOR, SLIDING_WINDOW, STATS,
USED_PROOF_HASHES,
};
const CONTRACT_NAME: &str = "crates.io:litium-mine";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_WINDOW_SIZE: u32 = 5000;
const DEFAULT_PID_INTERVAL: u64 = 100;
const DEFAULT_GAS_COST_UBOOT: u128 = 250_000;
/// Absolute floor for proof difficulty โ cheap early reject (spec ยง8.4).
const BASE_DIFFICULTY_TARGET: u32 = 8;
/// Cap for dynamic min_profitable_difficulty to prevent nonsensical values.
const MAX_MIN_PROFITABLE_DIFFICULTY: u32 = 32;
const DEFAULT_FEE_BUCKET_DURATION: u64 = 600;
const DEFAULT_FEE_NUM_BUCKETS: u32 = 36;
/// Referral share from each accepted proof reward.
const REFERRAL_SHARE_NUM: u128 = 10;
const REFERRAL_SHARE_DEN: u128 = 100;
/// PID alpha/beta are stored as micros (per-million).
/// alpha range: [300_000, 700_000] โ [0.3, 0.7]
/// beta range: [0, 900_000] โ [0.0, 0.9]
const ALPHA_MIN: u64 = 300_000;
const ALPHA_MAX: u64 = 700_000;
const BETA_MIN: u64 = 0;
const BETA_MAX: u64 = 900_000;
const MICROS: u64 = 1_000_000;
/// PID gains (moderate PD mode from spec) โ scaled by 1e6 for integer math.
const KP_A: i64 = 4_000; // 0.004 * 1e6
const KD_A: i64 = 8_000; // 0.008 * 1e6
const KP_B: i64 = 15_000; // 0.015 * 1e6
const KD_B: i64 = 30_000; // 0.03 * 1e6
/// Warmup P-only gains (spec ยง6 conservative mode) โ D gains zeroed.
const KP_A_WARMUP: i64 = 5_000; // 0.005 * 1e6
const KP_B_WARMUP: i64 = 20_000; // 0.02 * 1e6
/// EMA smoothing factor lambda=0.3, stored as 300_000 (per-million).
const EMA_LAMBDA: i64 = 300_000;
/// Compute dynamic minimum profitable difficulty (spec ยง8.4).
///
/// d_breakeven = gas_cost / base_rate
/// min_profitable = 2 * d_breakeven
/// result = max(BASE_DIFFICULTY_TARGET, min(min_profitable, MAX_CAP))
fn compute_min_profitable_difficulty(gas_cost: Uint128, base_rate: Uint128) -> u32 {
if gas_cost.is_zero() || base_rate.is_zero() {
return BASE_DIFFICULTY_TARGET;
}
let min_profitable = (gas_cost
.checked_mul(Uint128::from(2u128))
.unwrap_or(Uint128::MAX))
.checked_div(base_rate)
.unwrap_or(Uint128::zero());
let capped = min_profitable
.u128()
.min(MAX_MIN_PROFITABLE_DIFFICULTY as u128) as u32;
BASE_DIFFICULTY_TARGET.max(capped)
}
/// Scale factor for PID error signals (1e9).
const ERR_SCALE: i64 = 1_000_000_000;
// ============================================================
// Cross-contract message types
// ============================================================
#[derive(serde::Serialize)]
#[serde(rename_all = "snake_case")]
enum LitiumCoreExecuteMsg {
Mint { to: String, amount: Uint128 },
}
#[derive(serde::Serialize)]
#[serde(rename_all = "snake_case")]
enum LitiumReferExecuteMsg {
BindReferrer { miner: String, referrer: String },
AccrueReward { miner: String, amount: Uint128 },
}
#[derive(serde::Serialize)]
#[serde(rename_all = "snake_case")]
enum LitiumStakeExecuteMsg {
AccrueReward { amount: Uint128 },
}
#[derive(serde::Serialize)]
#[serde(rename_all = "snake_case")]
enum LitiumStakeQueryMsg {
TotalStaked {},
TotalPendingRewards {},
}
#[derive(serde::Serialize)]
#[serde(rename_all = "snake_case")]
enum LitiumCoreQueryMsg {
TotalMinted {},
BurnStats {},
}
#[derive(serde::Serialize, serde::Deserialize)]
struct TotalStakedResponse {
total_staked: Uint128,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct TotalPendingRewardsResponse {
total_pending_rewards: Uint128,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct TotalMintedResponse {
total_minted: Uint128,
#[allow(dead_code)]
supply_cap: Uint128,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct BurnStatsResponse {
total_burned: Uint128,
}
// ============================================================
// Instantiate
// ============================================================
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let admin = msg
.admin
.map(|a| deps.api.addr_validate(&a))
.transpose()?
.unwrap_or(info.sender.clone());
let window_size = msg.window_size.unwrap_or(DEFAULT_WINDOW_SIZE);
let pid_interval = msg.pid_interval.unwrap_or(DEFAULT_PID_INTERVAL);
let gas_cost = msg
.estimated_gas_cost_uboot
.unwrap_or(Uint128::from(DEFAULT_GAS_COST_UBOOT));
let fee_bucket_duration = msg
.fee_bucket_duration
.unwrap_or(DEFAULT_FEE_BUCKET_DURATION);
let fee_num_buckets = msg.fee_num_buckets.unwrap_or(DEFAULT_FEE_NUM_BUCKETS);
let config = MineConfig {
max_proof_age: msg.max_proof_age,
estimated_gas_cost_uboot: gas_cost,
core_contract: deps.api.addr_validate(&msg.core_contract)?,
stake_contract: deps.api.addr_validate(&msg.stake_contract)?,
refer_contract: deps.api.addr_validate(&msg.refer_contract)?,
token_contract: msg.token_contract,
admin,
paused: false,
window_size,
pid_interval,
genesis_time: msg.genesis_time.unwrap_or(1_772_619_159),
warmup_base_rate: msg.warmup_base_rate,
fee_bucket_duration,
fee_num_buckets,
};
CONFIG.save(deps.storage, &config)?;
// Initialize empty fee history
let fee_history = FeeHistory {
buckets: (0..fee_num_buckets)
.map(|_| FeeBucket {
epoch: 0,
amount: Uint128::zero(),
})
.collect(),
bucket_duration: fee_bucket_duration,
};
FEE_HISTORY.save(deps.storage, &fee_history)?;
// Initialize empty sliding window
let window = SlidingWindow {
entries: Vec::new(),
head: 0,
count: 0,
total_d: 0,
t_first: 0,
t_last: 0,
};
SLIDING_WINDOW.save(deps.storage, &window)?;
// Initialize PID state: alpha=0.5 (500_000 micros), beta=0.0
let pid = PidState {
alpha: 500_000,
beta: 0,
e_eff_prev: 0,
e_cov_prev: 0,
de_eff: 0,
de_cov: 0,
cached_staking_share: 0,
};
PID_STATE.save(deps.storage, &pid)?;
let stats = Stats {
total_proofs: 0,
total_rewards: Uint128::zero(),
unique_miners: 0,
total_difficulty_bits: 0,
};
STATS.save(deps.storage, &stats)?;
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("window_size", window_size.to_string())
.add_attribute("admin", config.admin.to_string()))
}
// ============================================================
// Execute
// ============================================================
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::SubmitProof {
hash,
nonce,
miner_address,
challenge,
difficulty,
timestamp,
referrer,
} => execute_submit_proof(
deps,
env,
info,
hash,
nonce,
miner_address,
challenge,
difficulty,
timestamp,
referrer,
),
ExecuteMsg::AccrueFees { amount } => execute_accrue_fees(deps, env, info, amount),
ExecuteMsg::UpdateConfig {
max_proof_age,
admin,
estimated_gas_cost_uboot,
core_contract,
stake_contract,
refer_contract,
warmup_base_rate,
pid_interval,
genesis_time,
} => execute_update_config(
deps,
info,
max_proof_age,
admin,
estimated_gas_cost_uboot,
core_contract,
stake_contract,
refer_contract,
warmup_base_rate,
pid_interval,
genesis_time,
),
ExecuteMsg::ApplyTestingOverrides { overrides } => {
execute_apply_testing_overrides(deps, info, overrides)
}
ExecuteMsg::ResetState { genesis_time } => {
execute_reset_state(deps, env, info, genesis_time)
}
ExecuteMsg::Pause {} => execute_pause(deps, info),
ExecuteMsg::Unpause {} => execute_unpause(deps, info),
}
}
// ============================================================
// Submit Proof (core on_proof flow)
// ============================================================
#[allow(clippy::too_many_arguments)]
fn execute_submit_proof(
deps: DepsMut,
env: Env,
info: MessageInfo,
hash_hex: String,
nonce: u64,
miner_address: String,
challenge_hex: String,
claimed_difficulty: u32,
timestamp: u64,
referrer: Option<String>,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if config.paused {
return Err(ContractError::Paused {});
}
// 1. Cheap early reject: absolute difficulty floor (spec ยง8.4)
if claimed_difficulty < BASE_DIFFICULTY_TARGET {
return Err(ContractError::BelowProfitableThreshold {
got: claimed_difficulty,
min: BASE_DIFFICULTY_TARGET,
});
}
let miner = deps.api.addr_validate(&miner_address)?;
// Sender must be the miner (prevents referrer hijacking via front-running)
if info.sender != miner {
return Err(ContractError::Unauthorized {});
}
// 2. Verify proof (hash, timestamp, difficulty)
let (valid, _actual_difficulty_bits) = verify_proof(
nonce,
&challenge_hex,
&hash_hex,
claimed_difficulty,
config.max_proof_age,
timestamp,
env.block.time.seconds(),
)?;
if !valid {
return Err(ContractError::HashMismatch {});
}
// 3. Anti-replay: check proof hash not already used
let hash_bytes = hex::decode(&hash_hex).map_err(|_| ContractError::InvalidHash {})?;
if USED_PROOF_HASHES.has(deps.storage, &hash_bytes) {
return Err(ContractError::DuplicateProofHash {});
}
USED_PROOF_HASHES.save(deps.storage, &hash_bytes, ×tamp)?;
// Amortized pruning: remove up to 10 stale proof hashes per submission
prune_old_proof_hashes(
deps.storage,
env.block.time.seconds(),
config.max_proof_age,
10,
);
// 4. Check referrer constraints
let mut miner_stats = MINER_STATS
.may_load(deps.storage, &miner)?
.unwrap_or(MinerStats {
proofs_submitted: 0,
total_rewards: Uint128::zero(),
last_proof_time: 0,
});
let is_first_proof = miner_stats.proofs_submitted == 0;
if !is_first_proof && referrer.is_some() {
return Err(ContractError::ReferrerLockedAfterFirstProof {});
}
// 5. Compute reward using sliding window
// Use claimed_difficulty (not actual_difficulty_bits) per spec:
// reward(d) = base_rate * d, where d is client-chosen difficulty.
// actual_difficulty_bits may be higher due to hash luck, but reward is
// based on what was committed to, ensuring fair pricing.
let pid = PID_STATE.load(deps.storage)?;
let window = SLIDING_WINDOW.load(deps.storage)?;
let fee_history = FEE_HISTORY.load(deps.storage)?;
let now = env.block.time.seconds();
let d = claimed_difficulty;
let (total_reward, base_rate_used) =
compute_proof_reward(&config, &window, &pid, &fee_history, now, d)?;
// 5b. Economic reject: dynamic min profitable difficulty (spec ยง8.4)
let min_profitable_d =
compute_min_profitable_difficulty(config.estimated_gas_cost_uboot, base_rate_used);
if claimed_difficulty < min_profitable_d {
return Err(ContractError::BelowProfitableThreshold {
got: claimed_difficulty,
min: min_profitable_d,
});
}
// 6. Deduct gas cost from gross reward (gas is not minted โ equivalent to fee payment)
let gas_deduction = std::cmp::min(config.estimated_gas_cost_uboot, total_reward);
let net_reward = total_reward.saturating_sub(gas_deduction);
// 7. Split net reward per spec ยง5: 10% referral, 90% split between stakers/miners
let referral_reward = net_reward.multiply_ratio(REFERRAL_SHARE_NUM, REFERRAL_SHARE_DEN);
let post_referral = net_reward.saturating_sub(referral_reward);
let staking_share = compute_staking_share(&deps.as_ref(), &config, &pid);
let staking_reward = post_referral.multiply_ratio(staking_share as u128, MICROS as u128);
let miner_reward = post_referral.saturating_sub(staking_reward);
// 8. Update sliding window (ring buffer push) โ use claimed difficulty
let mut window = window;
push_to_window(&mut window, config.window_size, claimed_difficulty, now);
SLIDING_WINDOW.save(deps.storage, &window)?;
// 9. Update stats
let mut stats = STATS.load(deps.storage)?;
stats.total_proofs += 1;
stats.total_rewards += total_reward;
stats.total_difficulty_bits += claimed_difficulty as u64;
if is_first_proof {
stats.unique_miners += 1;
}
STATS.save(deps.storage, &stats)?;
miner_stats.proofs_submitted += 1;
miner_stats.total_rewards += miner_reward;
miner_stats.last_proof_time = now;
MINER_STATS.save(deps.storage, &miner, &miner_stats)?;
// 10. PID update every K proofs
if config.pid_interval > 0 && window.count % config.pid_interval == 0 && window.count > 0 {
let mut pid = PID_STATE.load(deps.storage)?;
let is_warmup = window.entries.len() < config.window_size as usize;
pid_update(
&deps.as_ref(),
&config,
&window,
&fee_history,
now,
&mut pid,
is_warmup,
)?;
PID_STATE.save(deps.storage, &pid)?;
}
// 11. Build response with cross-contract messages
let mut resp = Response::new()
.add_attribute("action", "submit_proof")
.add_attribute("miner", miner.to_string())
.add_attribute("difficulty", d.to_string())
.add_attribute("gross_reward", total_reward.to_string())
.add_attribute("gas_deduction", gas_deduction.to_string())
.add_attribute("net_reward", net_reward.to_string())
.add_attribute("miner_reward", miner_reward.to_string())
.add_attribute("staking_reward", staking_reward.to_string())
.add_attribute("referral_reward", referral_reward.to_string())
.add_attribute("base_rate", base_rate_used.to_string())
.add_attribute("min_profitable_difficulty", min_profitable_d.to_string());
// Mint mining reward via litium-core
if !miner_reward.is_zero() {
resp = resp.add_message(build_core_mint_msg(
&config,
miner.to_string(),
miner_reward,
)?);
}
// Bind referrer + accrue referral reward via litium-refer
if let Some(ref referrer_addr) = referrer {
let bind_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.refer_contract.to_string(),
msg: cosmwasm_std::to_json_binary(&LitiumReferExecuteMsg::BindReferrer {
miner: miner.to_string(),
referrer: referrer_addr.clone(),
})?,
funds: vec![],
});
resp = resp.add_message(bind_msg);
}
if !referral_reward.is_zero() {
let accrue_ref_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.refer_contract.to_string(),
msg: cosmwasm_std::to_json_binary(&LitiumReferExecuteMsg::AccrueReward {
miner: miner.to_string(),
amount: referral_reward,
})?,
funds: vec![],
});
resp = resp.add_message(accrue_ref_msg);
}
// Accrue staking reward via litium-stake
if !staking_reward.is_zero() {
let accrue_stake_msg = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.stake_contract.to_string(),
msg: cosmwasm_std::to_json_binary(&LitiumStakeExecuteMsg::AccrueReward {
amount: staking_reward,
})?,
funds: vec![],
});
resp = resp.add_message(accrue_stake_msg);
}
Ok(resp)
}
// ============================================================
// Reward Computation
// ============================================================
/// Compute reward for a proof with `d` difficulty bits.
/// Returns (total_reward, base_rate_used).
pub fn compute_proof_reward(
config: &MineConfig,
window: &SlidingWindow,
pid: &PidState,
fee_history: &FeeHistory,
now: u64,
d: u32,
) -> Result<(Uint128, Uint128), ContractError> {
let window_entries = window.entries.len() as u32;
// During warmup (window not full), use fixed base_rate
if window_entries < config.window_size {
let base_rate = config.warmup_base_rate;
let reward = base_rate.checked_mul(Uint128::from(d as u128))?;
return Ok((reward.max(Uint128::one()), base_rate));
}
// Normal mode: compute from sliding window
let time_span = window.t_last.saturating_sub(window.t_first);
if time_span == 0 {
// All proofs at same timestamp โ use warmup rate
let base_rate = config.warmup_base_rate;
let reward = base_rate.checked_mul(Uint128::from(d as u128))?;
return Ok((reward.max(Uint128::one()), base_rate));
}
let d_rate_num = window.total_d; // sum of difficulty bits in window
let d_rate_den = time_span; // seconds
// E_target = emission_rate_per_second(genesis_time, now)
let e_target = emission_rate_per_second(config.genesis_time, now);
// Windowed fee rate: F = recent_fees(window.time_span) / time_span
let windowed_fees = fee_history_windowed_sum(fee_history, window.t_first, window.t_last);
let fee_rate_den = Uint128::from(time_span);
// G = E_target + F/time_span * (1 - beta)
let beta_complement = MICROS - pid.beta.min(MICROS); // (1 - beta) in micros
let fee_contribution = if fee_rate_den.is_zero() {
Uint128::zero()
} else {
windowed_fees
.multiply_ratio(beta_complement, MICROS)
.checked_div(fee_rate_den)
.unwrap_or(Uint128::zero())
};
let gross_rate = e_target.checked_add(fee_contribution)?; // G per second
// base_rate = G / D_rate (staking/referral split happens in execute_submit_proof)
// = gross_rate / (d_rate_num / d_rate_den)
// = (gross_rate * d_rate_den) / d_rate_num
let base_rate = if d_rate_num == 0 {
config.warmup_base_rate
} else {
let numerator = Uint256::from(gross_rate).checked_mul(Uint256::from(d_rate_den))?;
let denominator = Uint256::from(d_rate_num);
if denominator.is_zero() {
Uint256::from(config.warmup_base_rate)
} else {
numerator.checked_div(denominator)?
}
.try_into()
.unwrap_or(config.warmup_base_rate)
};
// reward = base_rate * d
let reward = base_rate.checked_mul(Uint128::from(d as u128))?;
Ok((reward.max(Uint128::one()), base_rate))
}
/// Compute staking share S^alpha as micros [0, MICROS].
/// Queries litium-stake and litium-core for live staked/supply values.
fn compute_staking_share(deps: &Deps, config: &MineConfig, pid: &PidState) -> u64 {
let s_ratio = query_staking_ratio(deps, config).unwrap_or(0.0);
let alpha = pid.alpha as f64 / MICROS as f64;
let share = s_ratio.powf(alpha);
(share * MICROS as f64).min(MICROS as f64) as u64
}
/// Query staking ratio S = staked / effective_supply.
///
/// Effective supply includes accrued-but-unminted staking rewards to prevent
/// the "sawtooth" effect where claiming staking rewards mass-mints tokens and
/// suddenly changes the S-ratio.
fn query_staking_ratio(deps: &Deps, config: &MineConfig) -> StdResult<f64> {
let staked: TotalStakedResponse = deps.querier.query_wasm_smart(
config.stake_contract.to_string(),
&LitiumStakeQueryMsg::TotalStaked {},
)?;
let minted: TotalMintedResponse = deps.querier.query_wasm_smart(
config.core_contract.to_string(),
&LitiumCoreQueryMsg::TotalMinted {},
)?;
let burned: BurnStatsResponse = deps.querier.query_wasm_smart(
config.core_contract.to_string(),
&LitiumCoreQueryMsg::BurnStats {},
)?;
let pending: TotalPendingRewardsResponse = deps.querier.query_wasm_smart(
config.stake_contract.to_string(),
&LitiumStakeQueryMsg::TotalPendingRewards {},
)?;
// effective supply = minted - burned + pending staking rewards (unminted but accrued)
let circulating =
minted.total_minted.saturating_sub(burned.total_burned) + pending.total_pending_rewards;
if circulating.is_zero() {
return Ok(0.0);
}
let ratio = staked.total_staked.u128() as f64 / circulating.u128() as f64;
Ok(ratio.min(1.0))
}
// ============================================================
// Proof Hash Pruning
// ============================================================
/// Amortized pruning of stale proof hashes. Scans up to `limit` entries from
/// a stored cursor, deleting those with timestamps older than `now - max_age`.
fn prune_old_proof_hashes(
storage: &mut dyn cosmwasm_std::Storage,
now: u64,
max_age: u64,
limit: usize,
) {
use cosmwasm_std::Order;
let cutoff = now.saturating_sub(max_age);
let cursor = PROOF_PRUNE_CURSOR.may_load(storage).ok().flatten();
let start = cursor.as_deref().map(cw_storage_plus::Bound::exclusive);
let entries: Vec<(Vec<u8>, u64)> = USED_PROOF_HASHES
.range(storage, start, None, Order::Ascending)
.take(limit)
.filter_map(|r| r.ok())
.collect();
let mut last_key = None;
for (key, ts) in &entries {
if *ts < cutoff {
USED_PROOF_HASHES.remove(storage, key);
}
last_key = Some(key.clone());
}
if entries.len() < limit {
// Wrapped around โ reset cursor so next pass starts from beginning
PROOF_PRUNE_CURSOR.remove(storage);
} else if let Some(key) = last_key {
let _ = PROOF_PRUNE_CURSOR.save(storage, &key);
}
}
// ============================================================
// Sliding Window Operations
// ============================================================
/// Push a new entry into the ring buffer.
pub fn push_to_window(window: &mut SlidingWindow, max_size: u32, difficulty: u32, timestamp: u64) {
let max_size = max_size as usize;
if window.entries.len() < max_size {
// Window not full yet โ just append
window.entries.push(WindowEntry {
difficulty,
timestamp,
});
} else {
// Ring buffer: overwrite oldest entry
let head = window.head as usize;
let old = &window.entries[head];
window.total_d = window.total_d.saturating_sub(old.difficulty as u64);
window.entries[head] = WindowEntry {
difficulty,
timestamp,
};
window.head = ((head + 1) % max_size) as u32;
// Recompute t_first from the entry at the new head position
let new_oldest = window.head as usize;
window.t_first = window.entries[new_oldest].timestamp;
}
window.total_d += difficulty as u64;
window.t_last = timestamp;
window.count += 1;
// Set t_first on first entry
if window.count == 1 || window.entries.len() == 1 {
window.t_first = timestamp;
}
}
// ============================================================
// PID Controller
// ============================================================
fn pid_update(
deps: &Deps,
config: &MineConfig,
window: &SlidingWindow,
fee_history: &FeeHistory,
now: u64,
pid: &mut PidState,
is_warmup: bool,
) -> StdResult<()> {
let s_ratio = query_staking_ratio(deps, config).unwrap_or(0.0);
let alpha_f = pid.alpha as f64 / MICROS as f64;
let time_span = window.t_last.saturating_sub(window.t_first).max(1) as f64;
let d_rate = window.total_d as f64 / time_span;
let e = emission_rate_per_second(config.genesis_time, now).u128() as f64;
let windowed_fees = fee_history_windowed_sum(fee_history, window.t_first, window.t_last);
let fee_rate = windowed_fees.u128() as f64 / time_span;
let beta_f = pid.beta as f64 / MICROS as f64;
let g = e + fee_rate * (1.0 - beta_f);
let s_alpha = s_ratio.powf(alpha_f);
let r_pow = g * (1.0 - s_alpha);
let r_pos = g * s_alpha;
// Efficiency errors
let eta_pow = if r_pow > 0.0 { d_rate / r_pow } else { 0.0 };
let eta_pos = if r_pos > 0.0 {
let minted: TotalMintedResponse = deps
.querier
.query_wasm_smart(
config.core_contract.to_string(),
&LitiumCoreQueryMsg::TotalMinted {},
)
.unwrap_or(TotalMintedResponse {
total_minted: Uint128::zero(),
supply_cap: Uint128::zero(),
});
let burned: BurnStatsResponse = deps
.querier
.query_wasm_smart(
config.core_contract.to_string(),
&LitiumCoreQueryMsg::BurnStats {},
)
.unwrap_or(BurnStatsResponse {
total_burned: Uint128::zero(),
});
let pending: TotalPendingRewardsResponse = deps
.querier
.query_wasm_smart(
config.stake_contract.to_string(),
&LitiumStakeQueryMsg::TotalPendingRewards {},
)
.unwrap_or(TotalPendingRewardsResponse {
total_pending_rewards: Uint128::zero(),
});
let circulating = (minted.total_minted.saturating_sub(burned.total_burned)
+ pending.total_pending_rewards)
.u128() as f64;
(s_ratio * circulating) / r_pos
} else {
0.0
};
let e_eff = ((eta_pow - eta_pos) * ERR_SCALE as f64) as i64;
let e_cov = if e > 0.0 {
((fee_rate / e - 1.0) * ERR_SCALE as f64) as i64
} else {
0
};
// EMA derivatives
let de_eff_raw = e_eff.saturating_sub(pid.e_eff_prev);
let de_eff =
(EMA_LAMBDA * de_eff_raw / 1_000_000) + ((1_000_000 - EMA_LAMBDA) * pid.de_eff / 1_000_000);
let de_cov_raw = e_cov.saturating_sub(pid.e_cov_prev);
let de_cov =
(EMA_LAMBDA * de_cov_raw / 1_000_000) + ((1_000_000 - EMA_LAMBDA) * pid.de_cov / 1_000_000);
// Select gains: P-only during warmup (spec ยง6), moderate PD otherwise
let (kp_a, kd_a, kp_b, kd_b) = if is_warmup {
(KP_A_WARMUP, 0i64, KP_B_WARMUP, 0i64)
} else {
(KP_A, KD_A, KP_B, KD_B)
};
// Update alpha: alpha += Kp_a * e_eff + Kd_a * de_eff (all in micros)
let alpha_delta = (kp_a * e_eff / ERR_SCALE) + (kd_a * de_eff / ERR_SCALE);
let new_alpha = (pid.alpha as i64 + alpha_delta).clamp(ALPHA_MIN as i64, ALPHA_MAX as i64);
pid.alpha = new_alpha as u64;
// Update beta: beta += Kp_b * e_cov + Kd_b * de_cov
let beta_delta = (kp_b * e_cov / ERR_SCALE) + (kd_b * de_cov / ERR_SCALE);
let new_beta = (pid.beta as i64 + beta_delta).clamp(BETA_MIN as i64, BETA_MAX as i64);
pid.beta = new_beta as u64;
pid.e_eff_prev = e_eff;
pid.e_cov_prev = e_cov;
pid.de_eff = de_eff;
pid.de_cov = de_cov;
// Cache S^alpha for use in base_rate computation between PID updates
let new_alpha_f = pid.alpha as f64 / MICROS as f64;
let new_s_alpha = s_ratio.powf(new_alpha_f);
pid.cached_staking_share = (new_s_alpha * MICROS as f64).min(MICROS as f64) as u64;
Ok(())
}
// ============================================================
// AccrueFees (from litium-core)
// ============================================================
fn execute_accrue_fees(
deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Uint128,
) -> Result<Response, ContractError> {
let config = CONFIG.load(deps.storage)?;
if info.sender != config.core_contract {
return Err(ContractError::UnauthorizedFeeAccrual {});
}
// Store RAW fee amount into time-bucketed history for PID controller
let now = env.block.time.seconds();
let mut history = FEE_HISTORY.load(deps.storage)?;
fee_history_accrue(&mut history, now, amount);
FEE_HISTORY.save(deps.storage, &history)?;
// Fee is already burned in litium-core (spec ยง4). We only track it here.
Ok(Response::new()
.add_attribute("action", "accrue_fees")
.add_attribute("amount", amount.to_string()))
}
// ============================================================
// Fee History Helpers
// ============================================================
/// Accrue a raw fee amount into the appropriate time bucket.
fn fee_history_accrue(history: &mut FeeHistory, now: u64, raw_amount: Uint128) {
if history.buckets.is_empty() || history.bucket_duration == 0 {
return;
}
let current_epoch = now / history.bucket_duration;
let num_buckets = history.buckets.len();
let slot = (current_epoch as usize) % num_buckets;
if history.buckets[slot].epoch != current_epoch {
// Stale bucket โ reset
history.buckets[slot] = FeeBucket {
epoch: current_epoch,
amount: raw_amount,
};
} else {
history.buckets[slot].amount += raw_amount;
}
}
/// Sum raw fees whose bucket epoch falls within [t_min/duration, t_max/duration].
pub fn fee_history_windowed_sum(history: &FeeHistory, t_min: u64, t_max: u64) -> Uint128 {
if history.buckets.is_empty() || history.bucket_duration == 0 {
return Uint128::zero();
}
let epoch_min = t_min / history.bucket_duration;
let epoch_max = t_max / history.bucket_duration;
let mut sum = Uint128::zero();
for bucket in &history.buckets {
if bucket.epoch >= epoch_min && bucket.epoch <= epoch_max {
sum += bucket.amount;
}
}
sum
}
// ============================================================
// Cross-contract helpers
// ============================================================
fn build_core_mint_msg(
config: &MineConfig,
to: String,
amount: Uint128,
) -> Result<CosmosMsg, ContractError> {
Ok(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: config.core_contract.to_string(),
msg: cosmwasm_std::to_json_binary(&LitiumCoreExecuteMsg::Mint { to, amount })?,
funds: vec![],
}))
}
// ============================================================
// Proof Verification
// ============================================================
fn decode_hex_32(input: &str) -> Result<[u8; 32], ContractError> {
let raw = hex::decode(input).map_err(|_| ContractError::InvalidHash {})?;
if raw.len() != 32 {
return Err(ContractError::InvalidHash {});
}
let mut out = [0u8; 32];
out.copy_from_slice(&raw);
Ok(out)
}
fn compute_pow_hash(challenge: &[u8; 32], nonce: u64) -> [u8; 32] {
let input = uhash_core::build_input(challenge, nonce);
uhash_core::hash(&input)
}
#[allow(clippy::too_many_arguments)]
fn verify_proof(
nonce: u64,
challenge_hex: &str,
hash_hex: &str,
required_difficulty: u32,
max_proof_age: u64,
proof_timestamp: u64,
now_ts: u64,
) -> Result<(bool, u32), ContractError> {
if proof_timestamp > now_ts {
return Err(ContractError::TimestampInFuture {});
}
if now_ts.saturating_sub(proof_timestamp) > max_proof_age {
return Err(ContractError::TimestampTooOld {
max_age: max_proof_age,
});
}
let challenge = decode_hex_32(challenge_hex)?;
let computed = compute_pow_hash(&challenge, nonce);
let provided = hex::decode(hash_hex).map_err(|_| ContractError::InvalidHash {})?;
if provided.len() != 32 {
return Err(ContractError::InvalidHash {});
}
if computed.as_slice() != provided.as_slice() {
return Err(ContractError::HashMismatch {});
}
let difficulty_bits = count_leading_zero_bits(&computed);
if difficulty_bits < required_difficulty {
return Err(ContractError::InsufficientDifficulty {});
}
Ok((true, difficulty_bits))
}
fn count_leading_zero_bits(hash: &[u8; 32]) -> u32 {
let mut zero_bits = 0u32;
for byte in hash.iter() {
if *byte == 0 {
zero_bits += 8;
} else {
zero_bits += byte.leading_zeros();
break;
}
}
zero_bits
}
// ============================================================
// Admin Operations
// ============================================================
#[allow(clippy::too_many_arguments)]
fn execute_update_config(
deps: DepsMut,
info: MessageInfo,
max_proof_age: Option<u64>,
admin: Option<String>,
estimated_gas_cost_uboot: Option<Uint128>,
core_contract: Option<String>,
stake_contract: Option<String>,
refer_contract: Option<String>,
warmup_base_rate: Option<Uint128>,
pid_interval: Option<u64>,
genesis_time: Option<u64>,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(ContractError::Unauthorized {});
}
if let Some(a) = max_proof_age {
config.max_proof_age = a;
}
if let Some(a) = admin {
config.admin = deps.api.addr_validate(&a)?;
}
if let Some(g) = estimated_gas_cost_uboot {
config.estimated_gas_cost_uboot = g;
}
if let Some(c) = core_contract {
config.core_contract = deps.api.addr_validate(&c)?;
}
if let Some(s) = stake_contract {
config.stake_contract = deps.api.addr_validate(&s)?;
}
if let Some(r) = refer_contract {
config.refer_contract = deps.api.addr_validate(&r)?;
}
if let Some(w) = warmup_base_rate {
config.warmup_base_rate = w;
}
if let Some(p) = pid_interval {
config.pid_interval = p;
}
if let Some(g) = genesis_time {
config.genesis_time = g;
}
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("action", "update_config"))
}
fn execute_apply_testing_overrides(
deps: DepsMut,
info: MessageInfo,
overrides: TestingOverrides,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(ContractError::Unauthorized {});
}
if let Some(a) = overrides.max_proof_age {
config.max_proof_age = a;
}
CONFIG.save(deps.storage, &config)?;
// Override windowed fees: spread the given amount across ALL buckets
// so it appears in any windowed sum regardless of epoch range.
if let Some(f) = overrides.override_windowed_fees {
let window = SLIDING_WINDOW.load(deps.storage)?;
let history = FEE_HISTORY.load(deps.storage)?;
let duration = history.bucket_duration.max(1);
// Place entire amount in one bucket whose epoch matches the window's t_last
let target_epoch = window.t_last / duration;
let num_buckets = history.buckets.len();
let slot = (target_epoch as usize) % num_buckets.max(1);
FEE_HISTORY.update(deps.storage, |mut h| -> StdResult<_> {
for bucket in h.buckets.iter_mut() {
*bucket = FeeBucket {
epoch: 0,
amount: Uint128::zero(),
};
}
if !h.buckets.is_empty() {
h.buckets[slot] = FeeBucket {
epoch: target_epoch,
amount: f,
};
}
Ok(h)
})?;
}
STATS.update(deps.storage, |mut stats| -> StdResult<_> {
if let Some(total_proofs) = overrides.stats_total_proofs {
stats.total_proofs = total_proofs;
}
if let Some(total_rewards) = overrides.stats_total_rewards {
stats.total_rewards = total_rewards;
}
Ok(stats)
})?;
if overrides.window_count.is_some() || overrides.window_total_d.is_some() {
SLIDING_WINDOW.update(deps.storage, |mut w| -> StdResult<_> {
if let Some(c) = overrides.window_count {
w.count = c;
}
if let Some(d) = overrides.window_total_d {
w.total_d = d;
}
Ok(w)
})?;
}
if overrides.pid_alpha.is_some() || overrides.pid_beta.is_some() {
PID_STATE.update(deps.storage, |mut p| -> StdResult<_> {
if let Some(a) = overrides.pid_alpha {
p.alpha = a;
}
if let Some(b) = overrides.pid_beta {
p.beta = b;
}
Ok(p)
})?;
}
Ok(Response::new().add_attribute("action", "apply_testing_overrides"))
}
/// Full state reset for daily testing. Zeroes all runtime state back to
/// fresh-instantiation values while preserving config (except genesis_time).
fn execute_reset_state(
deps: DepsMut,
env: Env,
info: MessageInfo,
genesis_time: Option<u64>,
) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(ContractError::Unauthorized {});
}
// Update genesis_time (default: current block time)
config.genesis_time = genesis_time.unwrap_or(env.block.time.seconds());
CONFIG.save(deps.storage, &config)?;
// Reset stats
STATS.save(
deps.storage,
&Stats {
total_proofs: 0,
total_rewards: Uint128::zero(),
unique_miners: 0,
total_difficulty_bits: 0,
},
)?;
// Reset sliding window
SLIDING_WINDOW.save(
deps.storage,
&SlidingWindow {
entries: Vec::new(),
head: 0,
count: 0,
total_d: 0,
t_first: 0,
t_last: 0,
},
)?;
// Reset PID state
PID_STATE.save(
deps.storage,
&PidState {
alpha: 500_000,
beta: 0,
e_eff_prev: 0,
e_cov_prev: 0,
de_eff: 0,
de_cov: 0,
cached_staking_share: 0,
},
)?;
// Reset fee history
FEE_HISTORY.save(
deps.storage,
&FeeHistory {
buckets: (0..config.fee_num_buckets)
.map(|_| FeeBucket {
epoch: 0,
amount: Uint128::zero(),
})
.collect(),
bucket_duration: config.fee_bucket_duration,
},
)?;
// Clear proof prune cursor
PROOF_PRUNE_CURSOR.remove(deps.storage);
// Clear all miner stats (range remove)
let miners: Vec<_> = MINER_STATS
.keys(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.collect::<Result<Vec<_>, _>>()?;
for addr in &miners {
MINER_STATS.remove(deps.storage, addr);
}
// Clear all used proof hashes
let hashes: Vec<_> = USED_PROOF_HASHES
.keys(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.collect::<Result<Vec<_>, _>>()?;
for hash in &hashes {
USED_PROOF_HASHES.remove(deps.storage, hash);
}
Ok(Response::new()
.add_attribute("action", "reset_state")
.add_attribute("genesis_time", config.genesis_time.to_string())
.add_attribute("miners_cleared", miners.len().to_string())
.add_attribute("hashes_cleared", hashes.len().to_string()))
}
fn execute_pause(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(ContractError::Unauthorized {});
}
config.paused = true;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("action", "pause"))
}
fn execute_unpause(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
let mut config = CONFIG.load(deps.storage)?;
if info.sender != config.admin {
return Err(ContractError::Unauthorized {});
}
config.paused = false;
CONFIG.save(deps.storage, &config)?;
Ok(Response::new().add_attribute("action", "unpause"))
}
// ============================================================
// Migrate
// ============================================================
/// Legacy config for v0.7.0 migration (has min_difficulty field).
#[derive(serde::Serialize, serde::Deserialize)]
struct LegacyMineConfig {
pub max_proof_age: u64,
pub estimated_gas_cost_uboot: Uint128,
pub core_contract: cosmwasm_std::Addr,
pub stake_contract: cosmwasm_std::Addr,
pub refer_contract: cosmwasm_std::Addr,
pub token_contract: String,
pub admin: cosmwasm_std::Addr,
pub paused: bool,
pub window_size: u32,
pub pid_interval: u64,
pub genesis_time: u64,
pub warmup_base_rate: Uint128,
pub min_difficulty: u32,
pub fee_bucket_duration: u64,
pub fee_num_buckets: u32,
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(deps: DepsMut, _env: Env, _msg: Empty) -> Result<Response, ContractError> {
let stored = get_contract_version(deps.storage)?;
let stored_ver = stored
.version
.parse::<semver::Version>()
.map_err(|_| ContractError::MigrationError {})?;
let current_ver = CONTRACT_VERSION
.parse::<semver::Version>()
.map_err(|_| ContractError::MigrationError {})?;
if stored_ver >= current_ver {
return Err(ContractError::MigrationError {});
}
// Migrate config: remove deprecated min_difficulty field
let raw = deps.storage.get(b"config");
if let Some(data) = raw {
let legacy: LegacyMineConfig =
cosmwasm_std::from_json(&data).map_err(|_| ContractError::MigrationError {})?;
let new_config = MineConfig {
max_proof_age: legacy.max_proof_age,
estimated_gas_cost_uboot: legacy.estimated_gas_cost_uboot,
core_contract: legacy.core_contract,
stake_contract: legacy.stake_contract,
refer_contract: legacy.refer_contract,
token_contract: legacy.token_contract,
admin: legacy.admin,
paused: legacy.paused,
window_size: legacy.window_size,
pid_interval: legacy.pid_interval,
genesis_time: legacy.genesis_time,
warmup_base_rate: legacy.warmup_base_rate,
fee_bucket_duration: legacy.fee_bucket_duration,
fee_num_buckets: legacy.fee_num_buckets,
};
CONFIG.save(deps.storage, &new_config)?;
}
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Response::default().add_attribute("action", "migrate"))
}
// ============================================================
// Queries
// ============================================================
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Config {} => to_json_binary(&query_config(deps, &env)?),
QueryMsg::WindowStatus {} => to_json_binary(&query_window_status(deps, &env)?),
QueryMsg::Stats {} => to_json_binary(&query_stats(deps)?),
QueryMsg::MinerStats { address } => to_json_binary(&query_miner_stats(deps, address)?),
QueryMsg::CalculateReward { difficulty_bits } => {
to_json_binary(&query_calculate_reward(deps, env, difficulty_bits)?)
}
QueryMsg::EmissionInfo {} => to_json_binary(&query_emission_info(deps, &env)?),
}
}
fn query_config(deps: Deps, env: &Env) -> StdResult<ConfigResponse> {
let config = CONFIG.load(deps.storage)?;
let pid = PID_STATE.load(deps.storage)?;
let window = SLIDING_WINDOW.load(deps.storage)?;
let fee_history = FEE_HISTORY.load(deps.storage)?;
let now = env.block.time.seconds();
let (_reward, base_rate) = compute_proof_reward(&config, &window, &pid, &fee_history, now, 1)
.unwrap_or((Uint128::one(), config.warmup_base_rate));
let min_profitable_d =
compute_min_profitable_difficulty(config.estimated_gas_cost_uboot, base_rate);
Ok(ConfigResponse {
max_proof_age: config.max_proof_age,
estimated_gas_cost_uboot: config.estimated_gas_cost_uboot,
core_contract: config.core_contract.to_string(),
stake_contract: config.stake_contract.to_string(),
refer_contract: config.refer_contract.to_string(),
token_contract: config.token_contract,
admin: config.admin.to_string(),
paused: config.paused,
window_size: config.window_size,
pid_interval: config.pid_interval,
genesis_time: config.genesis_time,
min_profitable_difficulty: min_profitable_d,
alpha: pid.alpha,
beta: pid.beta,
fee_bucket_duration: config.fee_bucket_duration,
fee_num_buckets: config.fee_num_buckets,
warmup_base_rate: config.warmup_base_rate,
})
}
fn query_window_status(deps: Deps, env: &Env) -> StdResult<WindowStatusResponse> {
let config = CONFIG.load(deps.storage)?;
let window = SLIDING_WINDOW.load(deps.storage)?;
let pid = PID_STATE.load(deps.storage)?;
let fee_history = FEE_HISTORY.load(deps.storage)?;
let now = env.block.time.seconds();
let window_entries = window.entries.len() as u32;
let time_span = window.t_last.saturating_sub(window.t_first).max(1);
let d_rate = window.total_d as f64 / time_span as f64;
// Compute current base_rate (d=1 gives base_rate directly)
let (_reward, base_rate) = compute_proof_reward(&config, &window, &pid, &fee_history, now, 1)
.unwrap_or((Uint128::one(), config.warmup_base_rate));
let min_profitable_d =
compute_min_profitable_difficulty(config.estimated_gas_cost_uboot, base_rate);
Ok(WindowStatusResponse {
proof_count: window.count,
window_d_rate: format!("{:.6}", d_rate),
window_size: config.window_size,
window_entries,
base_rate,
min_profitable_difficulty: min_profitable_d,
alpha: format!("{:.6}", pid.alpha as f64 / MICROS as f64),
beta: format!("{:.6}", pid.beta as f64 / MICROS as f64),
})
}
fn query_stats(deps: Deps) -> StdResult<StatsResponse> {
let stats = STATS.load(deps.storage)?;
let avg_difficulty = if stats.total_proofs > 0 {
(stats.total_difficulty_bits / stats.total_proofs) as u32
} else {
0
};
Ok(StatsResponse {
total_proofs: stats.total_proofs,
total_rewards: stats.total_rewards,
unique_miners: stats.unique_miners,
avg_difficulty,
})
}
fn query_miner_stats(deps: Deps, address: String) -> StdResult<MinerStatsResponse> {
let addr = deps.api.addr_validate(&address)?;
let stats = MINER_STATS
.may_load(deps.storage, &addr)?
.unwrap_or(MinerStats {
proofs_submitted: 0,
total_rewards: Uint128::zero(),
last_proof_time: 0,
});
Ok(MinerStatsResponse {
address,
proofs_submitted: stats.proofs_submitted,
total_rewards: stats.total_rewards,
last_proof_time: stats.last_proof_time,
})
}
fn query_calculate_reward(
deps: Deps,
env: Env,
difficulty_bits: u32,
) -> StdResult<RewardCalculationResponse> {
let config = CONFIG.load(deps.storage)?;
let window = SLIDING_WINDOW.load(deps.storage)?;
let pid = PID_STATE.load(deps.storage)?;
let fee_history = FEE_HISTORY.load(deps.storage)?;
let now = env.block.time.seconds();
let (gross_reward, _base_rate) =
compute_proof_reward(&config, &window, &pid, &fee_history, now, difficulty_bits)
.unwrap_or((Uint128::zero(), Uint128::zero()));
Ok(RewardCalculationResponse {
gross_reward,
estimated_gas_cost_uboot: config.estimated_gas_cost_uboot,
earns_reward: gross_reward >= Uint128::one(),
})
}
fn query_emission_info(deps: Deps, env: &Env) -> StdResult<EmissionInfoResponse> {
let config = CONFIG.load(deps.storage)?;
let pid = PID_STATE.load(deps.storage)?;
let window = SLIDING_WINDOW.load(deps.storage)?;
let fee_history = FEE_HISTORY.load(deps.storage)?;
let now = env.block.time.seconds();
let e_rate = emission_rate_per_second(config.genesis_time, now);
let time_span = window.t_last.saturating_sub(window.t_first).max(1);
let windowed_fees = fee_history_windowed_sum(&fee_history, window.t_first, window.t_last);
let fee_rate = if time_span > 0 {
windowed_fees
.checked_div(Uint128::from(time_span))
.unwrap_or(Uint128::zero())
} else {
Uint128::zero()
};
let beta_complement = MICROS - pid.beta.min(MICROS);
let fee_contribution = fee_rate.multiply_ratio(beta_complement, MICROS);
let gross_rate = e_rate.checked_add(fee_contribution).unwrap_or(e_rate);
let s_ratio = query_staking_ratio(&deps, &config).unwrap_or(0.0);
let alpha_f = pid.alpha as f64 / MICROS as f64;
let s_alpha = s_ratio.powf(alpha_f);
let staking_share = (s_alpha * MICROS as f64).min(MICROS as f64) as u128;
// Post-referral: 90% of gross split between stakers and miners (spec ยง5)
let post_referral_rate =
gross_rate.multiply_ratio(REFERRAL_SHARE_DEN - REFERRAL_SHARE_NUM, REFERRAL_SHARE_DEN);
let staking_rate = post_referral_rate.multiply_ratio(staking_share, MICROS as u128);
let mining_rate = post_referral_rate.saturating_sub(staking_rate);
Ok(EmissionInfoResponse {
alpha: pid.alpha,
beta: pid.beta,
emission_rate: e_rate,
gross_rate,
mining_rate,
staking_rate,
windowed_fees,
})
}
cw-cyber/contracts/litium-mine/src/contract.rs
ฯ 0.0%
use ;
use ;
use crateemission_rate_per_second;
use crateContractError;
use crate;
use crate;
const CONTRACT_NAME: &str = "crates.io:litium-mine";
const CONTRACT_VERSION: &str = env!;
const DEFAULT_WINDOW_SIZE: u32 = 5000;
const DEFAULT_PID_INTERVAL: u64 = 100;
const DEFAULT_GAS_COST_UBOOT: u128 = 250_000;
/// Absolute floor for proof difficulty โ cheap early reject (spec ยง8.4).
const BASE_DIFFICULTY_TARGET: u32 = 8;
/// Cap for dynamic min_profitable_difficulty to prevent nonsensical values.
const MAX_MIN_PROFITABLE_DIFFICULTY: u32 = 32;
const DEFAULT_FEE_BUCKET_DURATION: u64 = 600;
const DEFAULT_FEE_NUM_BUCKETS: u32 = 36;
/// Referral share from each accepted proof reward.
const REFERRAL_SHARE_NUM: u128 = 10;
const REFERRAL_SHARE_DEN: u128 = 100;
/// PID alpha/beta are stored as micros (per-million).
/// alpha range: [300_000, 700_000] โ [0.3, 0.7]
/// beta range: [0, 900_000] โ [0.0, 0.9]
const ALPHA_MIN: u64 = 300_000;
const ALPHA_MAX: u64 = 700_000;
const BETA_MIN: u64 = 0;
const BETA_MAX: u64 = 900_000;
const MICROS: u64 = 1_000_000;
/// PID gains (moderate PD mode from spec) โ scaled by 1e6 for integer math.
const KP_A: i64 = 4_000; // 0.004 * 1e6
const KD_A: i64 = 8_000; // 0.008 * 1e6
const KP_B: i64 = 15_000; // 0.015 * 1e6
const KD_B: i64 = 30_000; // 0.03 * 1e6
/// Warmup P-only gains (spec ยง6 conservative mode) โ D gains zeroed.
const KP_A_WARMUP: i64 = 5_000; // 0.005 * 1e6
const KP_B_WARMUP: i64 = 20_000; // 0.02 * 1e6
/// EMA smoothing factor lambda=0.3, stored as 300_000 (per-million).
const EMA_LAMBDA: i64 = 300_000;
/// Compute dynamic minimum profitable difficulty (spec ยง8.4).
///
/// d_breakeven = gas_cost / base_rate
/// min_profitable = 2 * d_breakeven
/// result = max(BASE_DIFFICULTY_TARGET, min(min_profitable, MAX_CAP))
/// Scale factor for PID error signals (1e9).
const ERR_SCALE: i64 = 1_000_000_000;
// ============================================================
// Cross-contract message types
// ============================================================
// ============================================================
// Instantiate
// ============================================================
// ============================================================
// Execute
// ============================================================
// ============================================================
// Submit Proof (core on_proof flow)
// ============================================================
// ============================================================
// Reward Computation
// ============================================================
/// Compute reward for a proof with `d` difficulty bits.
/// Returns (total_reward, base_rate_used).
/// Compute staking share S^alpha as micros [0, MICROS].
/// Queries litium-stake and litium-core for live staked/supply values.
/// Query staking ratio S = staked / effective_supply.
///
/// Effective supply includes accrued-but-unminted staking rewards to prevent
/// the "sawtooth" effect where claiming staking rewards mass-mints tokens and
/// suddenly changes the S-ratio.
// ============================================================
// Proof Hash Pruning
// ============================================================
/// Amortized pruning of stale proof hashes. Scans up to `limit` entries from
/// a stored cursor, deleting those with timestamps older than `now - max_age`.
// ============================================================
// Sliding Window Operations
// ============================================================
/// Push a new entry into the ring buffer.
// ============================================================
// PID Controller
// ============================================================
// ============================================================
// AccrueFees (from litium-core)
// ============================================================
// ============================================================
// Fee History Helpers
// ============================================================
/// Accrue a raw fee amount into the appropriate time bucket.
/// Sum raw fees whose bucket epoch falls within [t_min/duration, t_max/duration].
// ============================================================
// Cross-contract helpers
// ============================================================
// ============================================================
// Proof Verification
// ============================================================
// ============================================================
// Admin Operations
// ============================================================
/// Full state reset for daily testing. Zeroes all runtime state back to
/// fresh-instantiation values while preserving config (except genesis_time).
// ============================================================
// Migrate
// ============================================================
/// Legacy config for v0.7.0 migration (has min_difficulty field).
// ============================================================
// Queries
// ============================================================