diff --git a/src/icp_swap/src/script.rs b/src/icp_swap/src/script.rs index df36e3d6..687d2d19 100644 --- a/src/icp_swap/src/script.rs +++ b/src/icp_swap/src/script.rs @@ -345,32 +345,37 @@ fn setup_timers(distribution_interval_seconds: u64) { } async fn distribute_reward_wrapper() { - // Distribution FIRST - let it complete and settle state - match distribute_reward().await { - Ok(_) => (), - Err(e) => { + // SURPLUS PROCESSING FIRST - add any external deposits to pool + use crate::update::process_surplus; // Updated import + + match process_surplus().await { + Ok(msg) => { register_info_log( - caller(), + Principal::anonymous(), "distribute_reward_wrapper", - &format!("Distribution failed: {}. Skipping surplus sweep to maintain state consistency.", e) + &format!("Surplus processing: {}", msg) + ); + } + Err(e) => { + // Log but don't fail - surplus processing is opportunistic + register_error_log( + Principal::anonymous(), + "distribute_reward_wrapper", + e ); - // If distribution fails, skip sweep to avoid inconsistent state - return; } } - // Only sweep if distribution succeeded - use crate::update::sweep_surplus_to_revshare; - match sweep_surplus_to_revshare().await { - Ok(msg) => { + // DISTRIBUTION SECOND - includes any just-processed surplus + match distribute_reward().await { + Ok(_) => { register_info_log( Principal::anonymous(), "distribute_reward_wrapper", - &format!("Sweep result: {}", msg) + "Distribution completed successfully" ); } Err(e) => { - // Log but don't fail - sweep is opportunistic register_error_log( Principal::anonymous(), "distribute_reward_wrapper", diff --git a/src/icp_swap/src/storage.rs b/src/icp_swap/src/storage.rs index 16ff0bce..8425c2f4 100644 --- a/src/icp_swap/src/storage.rs +++ b/src/icp_swap/src/storage.rs @@ -11,10 +11,13 @@ use std::collections::{ BTreeSet, HashMap }; use crate::utils::DEFAULT_SECONDARY_RATIO; use crate::ExecutionError; -// Surplus sweep configuration -pub const SURPLUS_SWEEP_THRESHOLD_E8S: u64 = 100_000_000; // 1 ICP -pub const OPERATIONAL_BUFFER_E8S: u64 = 10_000_000; // 0.1 ICP -pub const MIN_SWEEP_AMOUNT_E8S: u64 = 1_000_000; // 0.01 ICP +// Surplus processing configuration +// Note: Threshold lowered to 0.01 ICP since internal accounting has no transfer fees +pub const MIN_SWEEP_AMOUNT_E8S: u64 = 1_000_000; // 0.01 ICP (minimum surplus to process) + +// Deprecated constants - kept for historical record reading but no longer used +// pub const SURPLUS_SWEEP_THRESHOLD_E8S: u64 = 100_000_000; // 1 ICP (deprecated - was used for external transfers) +// pub const OPERATIONAL_BUFFER_E8S: u64 = 10_000_000; // 0.1 ICP (deprecated - was used for external transfers) // Reconciliation thresholds (SECURITY-FOCUSED) // Negative discrepancy: ALWAYS flagged (missing funds is critical) @@ -331,7 +334,7 @@ pub struct SweepRecord { pub amount_swept: u64, pub surplus_before: u64, pub operational_buffer_kept: u64, - pub transfer_block_index: u64, + pub transfer_block_index: Option, // None = internal pool update, Some(index) = external transfer pub success: bool, pub error_message: Option, } diff --git a/src/icp_swap/src/update.rs b/src/icp_swap/src/update.rs index 7f900659..e42530b4 100644 --- a/src/icp_swap/src/update.rs +++ b/src/icp_swap/src/update.rs @@ -1839,21 +1839,24 @@ async fn transfer_icp_to_lbry_fun(amount: u64) -> Result { result.map_err(|e| format!("Transfer failed: {:?}", e)) } -/// Sweeps surplus ICP to alex-revshare canister when threshold exceeded +/// Processes surplus ICP by adding it to the reward pool for staker distribution +/// +/// This function detects positive discrepancies between actual and expected ICP balance +/// and adds the surplus to REWARD_POOL. External deposits (e.g., from parent companies) +/// automatically flow to stakers in the next distribution cycle. /// /// Safety guarantees: -/// - CEI pattern enforced -/// - Atomic state updates -/// - Rollback on failure +/// - Checked arithmetic prevents overflow /// - Comprehensive logging -/// - Minimum buffer maintained -pub async fn sweep_surplus_to_revshare() -> Result { +/// - Lower threshold (0.01 ICP) appropriate for internal accounting +/// - No transfer fees since this is internal state update +pub async fn process_surplus() -> Result { // 1. CHECK PHASE - Gather state and validate conditions // Get actual balance from ledger let actual_balance = fetch_canister_icp_balance().await .map_err(|e| ExecutionError::StateError( - format!("Failed to fetch balance for sweep: {:?}", e) + format!("Failed to fetch balance for surplus processing: {:?}", e) ))?; // Calculate expected balance (all tracked obligations) @@ -1864,183 +1867,96 @@ pub async fn sweep_surplus_to_revshare() -> Result { }); let archived_balance = crate::queries::get_total_archived_balance(); - let expected_balance = reward_pool + uncollected_alex + total_staked + archived_balance; + // Use checked arithmetic to prevent overflow + let expected_balance = reward_pool + .checked_add(uncollected_alex) + .and_then(|sum| sum.checked_add(total_staked)) + .and_then(|sum| sum.checked_add(archived_balance)) + .ok_or_else(|| ExecutionError::AdditionOverflow { + operation: "Calculating expected balance".to_string(), + details: format!("reward_pool: {}, uncollected_alex: {}, total_staked: {}, archived_balance: {}", + reward_pool, uncollected_alex, total_staked, archived_balance) + })?; // Calculate surplus (positive discrepancy only) if actual_balance <= expected_balance { - // No surplus or negative discrepancy - nothing to sweep register_info_log( Principal::anonymous(), - "sweep_surplus_to_revshare", - &format!("No surplus to sweep. Actual: {} <= Expected: {}", actual_balance, expected_balance) + "process_surplus", + &format!("No surplus to process. Actual: {} <= Expected: {}", actual_balance, expected_balance) ); - return Ok("No surplus to sweep".to_string()); + return Ok("No surplus to process".to_string()); } let surplus = actual_balance - expected_balance; - // Check if surplus exceeds threshold - if surplus < SURPLUS_SWEEP_THRESHOLD_E8S { + // Check if surplus exceeds threshold (using MIN_SWEEP_AMOUNT_E8S for internal ops) + // This is lowered to 0.01 ICP since internal accounting has no transfer fees + if surplus < MIN_SWEEP_AMOUNT_E8S { register_info_log( Principal::anonymous(), - "sweep_surplus_to_revshare", - &format!("Surplus {} below threshold {}. No sweep needed.", surplus, SURPLUS_SWEEP_THRESHOLD_E8S) + "process_surplus", + &format!("Surplus {} below threshold {}. Will process when threshold met.", surplus, MIN_SWEEP_AMOUNT_E8S) ); return Ok(format!("Surplus {} below threshold", surplus)); } - // Calculate sweep amount (keep operational buffer + account for transfer fee) - const ICP_TRANSFER_FEE: u64 = 10_000; // 0.0001 ICP - - // Use checked arithmetic to prevent underflow - let total_reserve = OPERATIONAL_BUFFER_E8S.saturating_add(ICP_TRANSFER_FEE); - - let sweep_amount = if surplus > total_reserve { - surplus.saturating_sub(total_reserve) - } else { - // This shouldn't happen given threshold check, but safety first - register_info_log( - Principal::anonymous(), - "sweep_surplus_to_revshare", - &format!("Surplus {} not enough above buffer + fee. No sweep.", surplus) - ); - return Ok("Surplus insufficient above buffer + fee".to_string()); - }; - - // Validate minimum sweep amount (avoid tiny transfers) - if sweep_amount < MIN_SWEEP_AMOUNT_E8S { - register_info_log( - Principal::anonymous(), - "sweep_surplus_to_revshare", - &format!("Sweep amount {} below minimum {}. Waiting for more surplus.", sweep_amount, MIN_SWEEP_AMOUNT_E8S) - ); - return Ok(format!("Sweep amount {} below minimum", sweep_amount)); - } - - // Check time since last sweep (prevent rapid repeated sweeps - 1 hour minimum) - let last_sweep = get_last_sweep_timestamp(); + // Check time since last processing (prevent rapid repeated processing - 1 hour minimum) + let last_process = get_last_sweep_timestamp(); // Reuse existing timestamp tracker let now = ic_cdk::api::time(); let one_hour_nanos = 3_600_000_000_000u64; // 1 hour in nanoseconds - if last_sweep > 0 && now >= last_sweep { - // Use saturating arithmetic to prevent underflow - let time_since = now.saturating_sub(last_sweep); + if last_process > 0 && now >= last_process { + let time_since = now.saturating_sub(last_process); if time_since < one_hour_nanos { register_info_log( Principal::anonymous(), - "sweep_surplus_to_revshare", - &format!("Last sweep was {} nanos ago (< 1 hour). Skipping to prevent rapid sweeps.", time_since) + "process_surplus", + &format!("Last processing was {} nanos ago (< 1 hour). Skipping to prevent rapid processing.", time_since) ); - return Ok("Too soon since last sweep".to_string()); + return Ok("Too soon since last processing".to_string()); } } register_info_log( Principal::anonymous(), - "sweep_surplus_to_revshare", - &format!("Sweep conditions met. Surplus: {} E8S, Sweep amount: {} E8S, Buffer kept: {} E8S", - surplus, sweep_amount, OPERATIONAL_BUFFER_E8S) + "process_surplus", + &format!("Processing {} E8S surplus by adding to reward pool", surplus) ); - // 2. EFFECT PHASE - Update state BEFORE external interaction - // (No state to update pre-transfer - surplus isn't tracked in state) - - // 3. INTERACT PHASE - External transfer - let transfer_result = transfer_surplus_to_revshare(sweep_amount).await; - - // 4. RECORD PHASE - Log outcome - let sweep_record = match transfer_result { - Ok(block_index) => { - register_info_log( - Principal::anonymous(), - "sweep_surplus_to_revshare", - &format!("Successfully swept {} E8S to revshare. Block: {}", sweep_amount, block_index) - ); - - SweepRecord { - timestamp: now, - amount_swept: sweep_amount, - surplus_before: surplus, - operational_buffer_kept: OPERATIONAL_BUFFER_E8S, - transfer_block_index: block_index, - success: true, - error_message: None, - } - } - Err(e) => { - register_error_log( - Principal::anonymous(), - "sweep_surplus_to_revshare", - ExecutionError::TransferFailed { - source: "icp_swap".to_string(), - dest: "revshare".to_string(), - token: "ICP".to_string(), - amount: sweep_amount, - details: e.clone(), - reason: "Surplus sweep transfer failed".to_string(), - } - ); - - SweepRecord { - timestamp: now, - amount_swept: sweep_amount, - surplus_before: surplus, - operational_buffer_kept: OPERATIONAL_BUFFER_E8S, - transfer_block_index: 0, - success: false, - error_message: Some(e.clone()), + // 2. EFFECT PHASE - Update REWARD_POOL with checked arithmetic + REWARD_POOL.with(|p| -> Result<(), ExecutionError> { + let current = p.borrow().get(&()).unwrap_or(0); + let new_total = current.checked_add(surplus).ok_or_else(|| + ExecutionError::AdditionOverflow { + operation: "Adding surplus to reward pool".to_string(), + details: format!("Current pool: {}, Surplus: {}", current, surplus) } - } - }; - - // Record sweep in history (always record, success or failure) - record_sweep(sweep_record.clone()); - - // Return result - if sweep_record.success { - Ok(format!("Swept {} E8S to revshare (block: {})", sweep_amount, sweep_record.transfer_block_index)) - } else { - Err(ExecutionError::StateError( - format!("Sweep failed: {}", sweep_record.error_message.unwrap_or_default()) - )) - } -} - -/// Helper function to transfer surplus ICP to lbry_fun (alex-revshare) -async fn transfer_surplus_to_revshare(amount: u64) -> Result { - // Get lbry_fun canister ID (hardcoded - same as ALEX fee destination) - let revshare_canister = Principal::from_text("oni4e-oyaaa-aaaap-qp2pq-cai") - .map_err(|e| format!("Invalid revshare canister ID: {}", e))?; + )?; + p.borrow_mut().insert((), new_total); + Ok(()) + })?; - // Get ICP ledger ID from config - let icp_ledger_id = CONFIGS.with(|configs| { - configs.borrow() - .get(&()) - .map(|c| c.icp_ledger_id) - .unwrap_or(MAINNET_LEDGER_CANISTER_ID) - }); + register_info_log( + Principal::anonymous(), + "process_surplus", + &format!("Successfully added {} E8S to reward pool. New pool balance: {}", + surplus, + REWARD_POOL.with(|p| p.borrow().get(&()).unwrap_or(0))) + ); - // Prepare transfer args - let transfer_args = TransferArg { - from_subaccount: None, - to: revshare_canister.into(), - fee: None, // Let ledger use default (10,000 E8S) - created_at_time: None, - memo: None, - amount: Nat::from(amount), + // 3. RECORD PHASE - Track for historical purposes + let process_record = SweepRecord { + timestamp: now, + amount_swept: surplus, + surplus_before: surplus, + operational_buffer_kept: 0, // No buffer needed for internal accounting + transfer_block_index: None, // None indicates internal state update (no external transfer) + success: true, + error_message: None, }; - // Execute transfer - let (result,) = ic_cdk::call::<(TransferArg,), (Result,)>( - icp_ledger_id, - "icrc1_transfer", - (transfer_args,) - ) - .await - .map_err(|e| format!("Transfer call failed: {:?}", e))?; + record_sweep(process_record); // Reuse existing tracking mechanism - // Return the amount on success (we know what we transferred) - result - .map(|_block| amount) // Return the amount we transferred - .map_err(|e| format!("Transfer failed: {:?}", e)) + Ok(format!("Processed {} E8S surplus to reward pool", surplus)) }