diff --git a/backstop/src/backstop/deposit.rs b/backstop/src/backstop/deposit.rs index 2095ddbd..8ac17930 100644 --- a/backstop/src/backstop/deposit.rs +++ b/backstop/src/backstop/deposit.rs @@ -14,7 +14,7 @@ pub fn execute_deposit(e: &Env, from: &Address, pool_address: &Address, amount: require_is_from_pool_factory(e, pool_address, pool_balance.shares); let mut user_balance = storage::get_user_balance(e, pool_address, from); - emissions::update_emissions(e, pool_address, &pool_balance, from, &user_balance, false); + emissions::update_emissions(e, pool_address, &pool_balance, from, &user_balance); let backstop_token_client = TokenClient::new(e, &storage::get_backstop_token(e)); backstop_token_client.transfer(from, &e.current_contract_address(), &amount); diff --git a/backstop/src/backstop/withdrawal.rs b/backstop/src/backstop/withdrawal.rs index fd68dcb9..032f4c5b 100644 --- a/backstop/src/backstop/withdrawal.rs +++ b/backstop/src/backstop/withdrawal.rs @@ -17,7 +17,7 @@ pub fn execute_queue_withdrawal( let mut user_balance = storage::get_user_balance(e, pool_address, from); // update emissions - emissions::update_emissions(e, pool_address, &pool_balance, from, &user_balance, false); + emissions::update_emissions(e, pool_address, &pool_balance, from, &user_balance); user_balance.queue_shares_for_withdrawal(e, amount); pool_balance.queue_for_withdraw(amount); @@ -36,7 +36,7 @@ pub fn execute_dequeue_withdrawal(e: &Env, from: &Address, pool_address: &Addres let mut user_balance = storage::get_user_balance(e, pool_address, from); // update emissions - emissions::update_emissions(e, pool_address, &pool_balance, from, &user_balance, false); + emissions::update_emissions(e, pool_address, &pool_balance, from, &user_balance); user_balance.dequeue_shares_for_withdrawal(e, amount, false); user_balance.add_shares(amount); diff --git a/backstop/src/emissions/claim.rs b/backstop/src/emissions/claim.rs index e9019a7b..b98482c7 100644 --- a/backstop/src/emissions/claim.rs +++ b/backstop/src/emissions/claim.rs @@ -5,7 +5,7 @@ use soroban_sdk::{ panic_with_error, vec, Address, Env, IntoVal, Map, Symbol, Val, Vec, }; -use super::update_emissions; +use super::distributor::claim_emissions; /// Perform a claim for backstop deposit emissions by a user from the backstop module pub fn execute_claim(e: &Env, from: &Address, pool_addresses: &Vec
, to: &Address) -> i128 { @@ -18,7 +18,7 @@ pub fn execute_claim(e: &Env, from: &Address, pool_addresses: &Vec, to: for pool_id in pool_addresses.iter() { let pool_balance = storage::get_pool_balance(e, &pool_id); let user_balance = storage::get_user_balance(e, &pool_id, from); - let claim_amt = update_emissions(e, &pool_id, &pool_balance, from, &user_balance, true); + let claim_amt = claim_emissions(e, &pool_id, &pool_balance, from, &user_balance); claimed += claim_amt; claims.set(pool_id, claim_amt); diff --git a/backstop/src/emissions/distributor.rs b/backstop/src/emissions/distributor.rs index 1af1fb6b..1f0af966 100644 --- a/backstop/src/emissions/distributor.rs +++ b/backstop/src/emissions/distributor.rs @@ -7,31 +7,47 @@ use soroban_sdk::{unwrap::UnwrapOptimized, Address, Env}; use crate::{ backstop::{PoolBalance, UserBalance}, constants::SCALAR_7, + require_nonnegative, storage::{self, BackstopEmissionsData, UserEmissionData}, BackstopEmissionConfig, }; /// Update the backstop emissions index for the user and pool -/// -/// Returns the number of tokens that need to be transferred to `user` when `to_claim` -/// is true, or returns zero. pub fn update_emissions( e: &Env, pool_id: &Address, pool_balance: &PoolBalance, user_id: &Address, user_balance: &UserBalance, - to_claim: bool, +) { + if let Some(emis_data) = update_emission_data(e, pool_id, pool_balance) { + update_user_emissions(e, pool_id, user_id, &emis_data, user_balance, false); + } +} + +/// Update for claiming emissions for a user and pool +/// +/// DOES NOT SEND CLAIMED TOKENS TO THE USER. The caller +/// is expected to handle sending the tokens once all claimed pools +/// have been processed. +/// +/// Returns the number of tokens that need to be transferred to `user` +pub(super) fn claim_emissions( + e: &Env, + pool_id: &Address, + pool_balance: &PoolBalance, + user_id: &Address, + user_balance: &UserBalance, ) -> i128 { if let Some(emis_data) = update_emission_data(e, pool_id, pool_balance) { - return update_user_emissions(e, pool_id, user_id, &emis_data, user_balance, to_claim); + update_user_emissions(e, pool_id, user_id, &emis_data, user_balance, true) + } else { + 0 } - // no emissions data for the reserve exists - nothing to update - 0 } /// Update the backstop emissions index for deposits -pub fn update_emission_data( +fn update_emission_data( e: &Env, pool_id: &Address, pool_balance: &PoolBalance, @@ -47,6 +63,12 @@ pub fn update_emission_data( } } +/// Update the backstop emissions index for deposits with the config already read +/// +/// Stores the new backstop emissions data to the ledger +/// +/// ### Returns +/// The new backstop emissions data pub fn update_emission_data_with_config( e: &Env, pool_id: &Address, @@ -70,8 +92,10 @@ pub fn update_emission_data_with_config( e.ledger().timestamp() }; + let unqueued_shares = pool_balance.shares - pool_balance.q4w; + require_nonnegative(e, unqueued_shares); let additional_idx = (i128(max_timestamp - emis_data.last_time) * i128(emis_config.eps)) - .fixed_div_floor(pool_balance.shares - pool_balance.q4w, SCALAR_7) + .fixed_div_floor(unqueued_shares, SCALAR_7) .unwrap_optimized(); let new_data = BackstopEmissionsData { index: additional_idx + emis_data.index, @@ -81,6 +105,11 @@ pub fn update_emission_data_with_config( new_data } +/// Update the user's emissions. If `to_claim` is true, the user's accrued emissions will be returned and +/// a value of zero will be stored to the ledger. +/// +/// ### Returns +/// The number of emitted tokens the caller needs to send to the user fn update_user_emissions( e: &Env, pool: &Address, @@ -93,14 +122,17 @@ fn update_user_emissions( if user_data.index != emis_data.index || to_claim { let mut accrual = user_data.accrued; if user_balance.shares != 0 { + let delta_index = emis_data.index - user_data.index; + require_nonnegative(e, delta_index); let to_accrue = (user_balance.shares) - .fixed_mul_floor(emis_data.index - user_data.index, SCALAR_7) + .fixed_mul_floor(delta_index, SCALAR_7) .unwrap_optimized(); accrual += to_accrue; } return set_user_emissions(e, pool, user, emis_data.index, accrual, to_claim); } - 0 + // no accrual occured and no claim requested + return 0; } else if user_balance.shares == 0 { // first time the user registered an action with the asset since emissions were added return set_user_emissions(e, pool, user, emis_data.index, 0, to_claim); @@ -192,13 +224,11 @@ mod tests { q4w: vec![&e], }; - let result = - update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance, false); + update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); let new_backstop_data = storage::get_backstop_emis_data(&e, &pool_1).unwrap_optimized(); let new_user_data = storage::get_user_emis_data(&e, &pool_1, &samwise).unwrap_optimized(); - assert_eq!(result, 0); assert_eq!(new_backstop_data.last_time, block_timestamp); assert_eq!(new_backstop_data.index, 8248888); assert_eq!(new_user_data.accrued, 7_4139996); @@ -238,21 +268,19 @@ mod tests { q4w: vec![&e], }; - let result = - update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance, false); + update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); let new_backstop_data = storage::get_backstop_emis_data(&e, &pool_1); let new_user_data = storage::get_user_emis_data(&e, &pool_1, &samwise); - assert_eq!(result, 0); assert!(new_backstop_data.is_none()); assert!(new_user_data.is_none()); }); } #[test] - fn test_update_emissions_to_claim() { + fn test_update_emissions_first_action() { let e = Env::default(); - let block_timestamp = BACKSTOP_EPOCH + 1234; + let block_timestamp = BACKSTOP_EPOCH + 12345; e.ledger().set(LedgerInfo { timestamp: block_timestamp, protocol_version: 20, @@ -270,21 +298,16 @@ mod tests { let backstop_emissions_config = BackstopEmissionConfig { expiration: BACKSTOP_EPOCH + 7 * 24 * 60 * 60, - eps: 0_1000000, + eps: 0_0420000, }; let backstop_emissions_data = BackstopEmissionsData { index: 22222, last_time: BACKSTOP_EPOCH, }; - let user_emissions_data = UserEmissionData { - index: 11111, - accrued: 3, - }; e.as_contract(&backstop_id, || { storage::set_last_distribution_time(&e, &BACKSTOP_EPOCH); storage::set_backstop_emis_config(&e, &pool_1, &backstop_emissions_config); storage::set_backstop_emis_data(&e, &pool_1, &backstop_emissions_data); - storage::set_user_emis_data(&e, &pool_1, &samwise, &user_emissions_data); let pool_balance = PoolBalance { shares: 150_0000000, @@ -292,26 +315,24 @@ mod tests { q4w: 0, }; let user_balance = UserBalance { - shares: 9_0000000, + shares: 0, q4w: vec![&e], }; - let result = - update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance, true); + update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); let new_backstop_data = storage::get_backstop_emis_data(&e, &pool_1).unwrap_optimized(); let new_user_data = storage::get_user_emis_data(&e, &pool_1, &samwise).unwrap_optimized(); - assert_eq!(result, 7_4139996); assert_eq!(new_backstop_data.last_time, block_timestamp); - assert_eq!(new_backstop_data.index, 8248888); + assert_eq!(new_backstop_data.index, 34588222); assert_eq!(new_user_data.accrued, 0); - assert_eq!(new_user_data.index, 8248888); + assert_eq!(new_user_data.index, 34588222); }); } #[test] - fn test_update_emissions_first_action() { + fn test_update_emissions_config_set_after_user() { let e = Env::default(); let block_timestamp = BACKSTOP_EPOCH + 12345; e.ledger().set(LedgerInfo { @@ -334,7 +355,7 @@ mod tests { eps: 0_0420000, }; let backstop_emissions_data = BackstopEmissionsData { - index: 22222, + index: 0, last_time: BACKSTOP_EPOCH, }; e.as_contract(&backstop_id, || { @@ -348,28 +369,26 @@ mod tests { q4w: 0, }; let user_balance = UserBalance { - shares: 0, + shares: 9_0000000, q4w: vec![&e], }; - let result = - update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance, false); + update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); let new_backstop_data = storage::get_backstop_emis_data(&e, &pool_1).unwrap_optimized(); let new_user_data = storage::get_user_emis_data(&e, &pool_1, &samwise).unwrap_optimized(); - assert_eq!(result, 0); assert_eq!(new_backstop_data.last_time, block_timestamp); - assert_eq!(new_backstop_data.index, 34588222); - assert_eq!(new_user_data.accrued, 0); - assert_eq!(new_user_data.index, 34588222); + assert_eq!(new_backstop_data.index, 34566000); + assert_eq!(new_user_data.accrued, 31_1094000); + assert_eq!(new_user_data.index, 34566000); }); } #[test] - fn test_update_emissions_config_set_after_user() { + fn test_update_emissions_q4w_not_counted() { let e = Env::default(); - let block_timestamp = BACKSTOP_EPOCH + 12345; + let block_timestamp = BACKSTOP_EPOCH + 1234; e.ledger().set(LedgerInfo { timestamp: block_timestamp, protocol_version: 20, @@ -387,16 +406,84 @@ mod tests { let backstop_emissions_config = BackstopEmissionConfig { expiration: BACKSTOP_EPOCH + 7 * 24 * 60 * 60, - eps: 0_0420000, + eps: 0_1000000, }; let backstop_emissions_data = BackstopEmissionsData { - index: 0, + index: 22222, + last_time: BACKSTOP_EPOCH, + }; + let user_emissions_data = UserEmissionData { + index: 11111, + accrued: 3, + }; + e.as_contract(&backstop_id, || { + storage::set_last_distribution_time(&e, &BACKSTOP_EPOCH); + storage::set_backstop_emis_config(&e, &pool_1, &backstop_emissions_config); + storage::set_backstop_emis_data(&e, &pool_1, &backstop_emissions_data); + storage::set_user_emis_data(&e, &pool_1, &samwise, &user_emissions_data); + + let pool_balance = PoolBalance { + shares: 150_0000000, + tokens: 200_0000000, + q4w: 4_5000000, + }; + let q4w: Q4W = Q4W { + amount: (4_5000000), + exp: (5000), + }; + let user_balance = UserBalance { + shares: 4_5000000, + q4w: vec![&e, q4w], + }; + + update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); + + let new_backstop_data = storage::get_backstop_emis_data(&e, &pool_1).unwrap_optimized(); + let new_user_data = + storage::get_user_emis_data(&e, &pool_1, &samwise).unwrap_optimized(); + assert_eq!(new_backstop_data.last_time, block_timestamp); + assert_eq!(new_backstop_data.index, 8503321); + assert_eq!(new_user_data.accrued, 38214948); + assert_eq!(new_user_data.index, 8503321); + }); + } + + #[test] + fn test_claim_emissions() { + let e = Env::default(); + let block_timestamp = BACKSTOP_EPOCH + 1234; + e.ledger().set(LedgerInfo { + timestamp: block_timestamp, + protocol_version: 20, + sequence_number: 0, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let backstop_id = create_backstop(&e); + let pool_1 = Address::generate(&e); + let samwise = Address::generate(&e); + + let backstop_emissions_config = BackstopEmissionConfig { + expiration: BACKSTOP_EPOCH + 7 * 24 * 60 * 60, + eps: 0_1000000, + }; + let backstop_emissions_data = BackstopEmissionsData { + index: 22222, last_time: BACKSTOP_EPOCH, }; + let user_emissions_data = UserEmissionData { + index: 11111, + accrued: 3, + }; e.as_contract(&backstop_id, || { storage::set_last_distribution_time(&e, &BACKSTOP_EPOCH); storage::set_backstop_emis_config(&e, &pool_1, &backstop_emissions_config); storage::set_backstop_emis_data(&e, &pool_1, &backstop_emissions_data); + storage::set_user_emis_data(&e, &pool_1, &samwise, &user_emissions_data); let pool_balance = PoolBalance { shares: 150_0000000, @@ -408,21 +495,67 @@ mod tests { q4w: vec![&e], }; - let result = - update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance, false); + let result = claim_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); let new_backstop_data = storage::get_backstop_emis_data(&e, &pool_1).unwrap_optimized(); let new_user_data = storage::get_user_emis_data(&e, &pool_1, &samwise).unwrap_optimized(); - assert_eq!(result, 0); + assert_eq!(result, 7_4139996); assert_eq!(new_backstop_data.last_time, block_timestamp); - assert_eq!(new_backstop_data.index, 34566000); - assert_eq!(new_user_data.accrued, 31_1094000); - assert_eq!(new_user_data.index, 34566000); + assert_eq!(new_backstop_data.index, 8248888); + assert_eq!(new_user_data.accrued, 0); + assert_eq!(new_user_data.index, 8248888); }); } + #[test] - fn test_update_emissions_q4w_not_counted() { + fn test_claim_emissions_no_config() { + let e = Env::default(); + let block_timestamp = BACKSTOP_EPOCH + 1234; + e.ledger().set(LedgerInfo { + timestamp: block_timestamp, + protocol_version: 20, + sequence_number: 0, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let backstop_id = create_backstop(&e); + let pool_1 = Address::generate(&e); + let samwise = Address::generate(&e); + + e.as_contract(&backstop_id, || { + storage::set_last_distribution_time(&e, &BACKSTOP_EPOCH); + + let pool_balance = PoolBalance { + shares: 150_0000000, + tokens: 200_0000000, + q4w: 0, + }; + let user_balance = UserBalance { + shares: 9_0000000, + q4w: vec![&e], + }; + + let result = claim_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); + + assert_eq!(result, 0); + let new_backstop_data = storage::get_backstop_emis_data(&e, &pool_1); + let new_user_data = storage::get_user_emis_data(&e, &pool_1, &samwise); + assert!(new_backstop_data.is_none()); + assert!(new_user_data.is_none()); + }); + } + + // @dev: The below tests should be impossible states to reach, but are left + // in to ensure any bad state does not result in incorrect emissions. + + #[test] + #[should_panic(expected = "Error(Contract, #11)")] + fn test_update_emissions_more_q4w_than_shares_panics() { let e = Env::default(); let block_timestamp = BACKSTOP_EPOCH + 1234; e.ledger().set(LedgerInfo { @@ -461,7 +594,7 @@ mod tests { let pool_balance = PoolBalance { shares: 150_0000000, tokens: 200_0000000, - q4w: 4_5000000, + q4w: 150_0000001, }; let q4w: Q4W = Q4W { amount: (4_5000000), @@ -472,17 +605,111 @@ mod tests { q4w: vec![&e, q4w], }; - let result = - update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance, false); + update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); + }); + } - let new_backstop_data = storage::get_backstop_emis_data(&e, &pool_1).unwrap_optimized(); - let new_user_data = - storage::get_user_emis_data(&e, &pool_1, &samwise).unwrap_optimized(); - assert_eq!(result, 0); - assert_eq!(new_backstop_data.last_time, block_timestamp); - assert_eq!(new_backstop_data.index, 8503321); - assert_eq!(new_user_data.accrued, 38214948); - assert_eq!(new_user_data.index, 8503321); + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn test_update_emissions_negative_time_dif() { + let e = Env::default(); + let block_timestamp = BACKSTOP_EPOCH + 1234; + e.ledger().set(LedgerInfo { + timestamp: block_timestamp, + protocol_version: 20, + sequence_number: 0, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let backstop_id = create_backstop(&e); + let pool_1 = Address::generate(&e); + let samwise = Address::generate(&e); + + let backstop_emissions_config = BackstopEmissionConfig { + expiration: BACKSTOP_EPOCH + 7 * 24 * 60 * 60, + eps: 0_1000000, + }; + let backstop_emissions_data = BackstopEmissionsData { + index: 22222, + last_time: block_timestamp + 1, + }; + let user_emissions_data = UserEmissionData { + index: 11111, + accrued: 3, + }; + e.as_contract(&backstop_id, || { + storage::set_last_distribution_time(&e, &BACKSTOP_EPOCH); + storage::set_backstop_emis_config(&e, &pool_1, &backstop_emissions_config); + storage::set_backstop_emis_data(&e, &pool_1, &backstop_emissions_data); + storage::set_user_emis_data(&e, &pool_1, &samwise, &user_emissions_data); + + let pool_balance = PoolBalance { + shares: 150_0000000, + tokens: 200_0000000, + q4w: 0, + }; + let user_balance = UserBalance { + shares: 4_5000000, + q4w: vec![&e], + }; + + update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); + }); + } + + #[test] + #[should_panic(expected = "Error(Contract, #11)")] + fn test_update_emissions_negative_user_index() { + let e = Env::default(); + let block_timestamp = BACKSTOP_EPOCH + 1234; + e.ledger().set(LedgerInfo { + timestamp: block_timestamp, + protocol_version: 20, + sequence_number: 0, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let backstop_id = create_backstop(&e); + let pool_1 = Address::generate(&e); + let samwise = Address::generate(&e); + + let backstop_emissions_config = BackstopEmissionConfig { + expiration: BACKSTOP_EPOCH + 7 * 24 * 60 * 60, + eps: 0_1000000, + }; + let backstop_emissions_data = BackstopEmissionsData { + index: 22222, + last_time: BACKSTOP_EPOCH, + }; + let user_emissions_data = UserEmissionData { + index: 34566000 + 1, + accrued: 3, + }; + e.as_contract(&backstop_id, || { + storage::set_last_distribution_time(&e, &BACKSTOP_EPOCH); + storage::set_backstop_emis_config(&e, &pool_1, &backstop_emissions_config); + storage::set_backstop_emis_data(&e, &pool_1, &backstop_emissions_data); + storage::set_user_emis_data(&e, &pool_1, &samwise, &user_emissions_data); + + let pool_balance = PoolBalance { + shares: 150_0000000, + tokens: 200_0000000, + q4w: 0, + }; + let user_balance = UserBalance { + shares: 4_5000000, + q4w: vec![&e], + }; + + update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); }); } } diff --git a/backstop/src/emissions/mod.rs b/backstop/src/emissions/mod.rs index fa6c629a..57e9d321 100644 --- a/backstop/src/emissions/mod.rs +++ b/backstop/src/emissions/mod.rs @@ -2,7 +2,7 @@ mod claim; pub use claim::execute_claim; mod distributor; -pub use distributor::{update_emission_data, update_emissions}; +pub use distributor::update_emissions; mod manager; pub use manager::{add_to_reward_zone, gulp_emissions, gulp_pool_emissions}; diff --git a/pool/src/emissions/distributor.rs b/pool/src/emissions/distributor.rs index b7478e7d..d33064ea 100644 --- a/pool/src/emissions/distributor.rs +++ b/pool/src/emissions/distributor.rs @@ -7,6 +7,7 @@ use crate::{ errors::PoolError, pool::User, storage::{self, ReserveEmissionsData, UserEmissionData}, + validator::require_nonnegative, ReserveEmissionsConfig, }; @@ -33,14 +34,13 @@ pub fn execute_claim(e: &Env, from: &Address, reserve_token_ids: &Vec