From b60ace8d84cf8c6a38113a3639afb37bb57a3c61 Mon Sep 17 00:00:00 2001 From: mootz12 Date: Thu, 4 Jan 2024 10:30:12 -0500 Subject: [PATCH 1/2] chore: add negative number checks during emissions calculations --- backstop/src/emissions/distributor.rs | 173 +++++++++++++++++++++++++- pool/src/emissions/distributor.rs | 106 ++++++++++++++++ 2 files changed, 277 insertions(+), 2 deletions(-) diff --git a/backstop/src/emissions/distributor.rs b/backstop/src/emissions/distributor.rs index 1af1fb6b..d71ad9fc 100644 --- a/backstop/src/emissions/distributor.rs +++ b/backstop/src/emissions/distributor.rs @@ -7,6 +7,7 @@ use soroban_sdk::{unwrap::UnwrapOptimized, Address, Env}; use crate::{ backstop::{PoolBalance, UserBalance}, constants::SCALAR_7, + require_nonnegative, storage::{self, BackstopEmissionsData, UserEmissionData}, BackstopEmissionConfig, }; @@ -70,8 +71,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, @@ -93,8 +96,10 @@ 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; } @@ -421,6 +426,7 @@ mod tests { assert_eq!(new_user_data.index, 34566000); }); } + #[test] fn test_update_emissions_q4w_not_counted() { let e = Env::default(); @@ -485,4 +491,167 @@ mod tests { assert_eq!(new_user_data.index, 8503321); }); } + + // @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 { + 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, + tokens: 200_0000000, + q4w: 150_0000001, + }; + 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, false); + }); + } + + #[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, false); + }); + } + + #[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, false); + }); + } } diff --git a/pool/src/emissions/distributor.rs b/pool/src/emissions/distributor.rs index b7478e7d..019022e2 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, }; @@ -178,6 +179,8 @@ fn update_user_emissions( if user_data.index != res_emis_data.index || claim { let mut accrual = user_data.accrued; if balance != 0 { + let delta_index = res_emis_data.index - user_data.index; + require_nonnegative(e, &delta_index); let to_accrue = balance .fixed_mul_floor(res_emis_data.index - user_data.index, supply_scalar) .unwrap_optimized(); @@ -340,6 +343,60 @@ mod tests { }); } + #[test] + #[should_panic(expected = "attempt to subtract with overflow")] + fn test_update_emissions_negative_time_diff() { + let e = Env::default(); + e.mock_all_auths(); + + let pool = testutils::create_pool(&e); + let samwise = Address::generate(&e); + + e.ledger().set(LedgerInfo { + timestamp: 1501000000, + protocol_version: 20, + sequence_number: 123, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let supply: i128 = 50_0000000; + let user_position: i128 = 2_0000000; + e.as_contract(&pool, || { + let reserve_emission_config = ReserveEmissionsConfig { + expiration: 1600000000, + eps: 0_0100000, + }; + let reserve_emission_data = ReserveEmissionsData { + index: 2345678, + last_time: 1501000000 + 1, + }; + let user_emission_data = UserEmissionData { + index: 1234567, + accrued: 0_1000000, + }; + let res_token_type = 0; + let res_token_index = 1 * 2 + res_token_type; + + storage::set_res_emis_config(&e, &res_token_index, &reserve_emission_config); + storage::set_res_emis_data(&e, &res_token_index, &reserve_emission_data); + storage::set_user_emissions(&e, &samwise, &res_token_index, &user_emission_data); + + update_emissions( + &e, + res_token_index, + supply, + 1_0000000, + &samwise, + user_position, + false, + ); + }); + } + /********** update emission data **********/ #[test] @@ -1034,6 +1091,55 @@ mod tests { }); } + #[test] + #[should_panic(expected = "Error(Contract, #4)")] + fn test_update_user_emissions_negative_index() { + let e = Env::default(); + e.mock_all_auths(); + + let pool = testutils::create_pool(&e); + + let samwise = Address::generate(&e); + + e.ledger().set(LedgerInfo { + timestamp: 1500000000, + protocol_version: 20, + sequence_number: 123, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let supply_scalar = 1_0000000; + let user_balance = 0_5000000; + e.as_contract(&pool, || { + let reserve_emission_data = ReserveEmissionsData { + index: 123456789, + last_time: 1500000000, + }; + let user_emission_data = UserEmissionData { + index: 123456789 + 1, + accrued: 0_1000000, + }; + + let res_token_type = 1; + let res_token_index = 1 * 2 + res_token_type; + storage::set_user_emissions(&e, &samwise, &res_token_index, &user_emission_data); + + update_user_emissions( + &e, + &reserve_emission_data, + res_token_index, + supply_scalar, + &samwise, + user_balance, + true, + ); + }); + } + //********** execute claim **********/ #[test] fn test_execute_claim() { From 88f66f1fa690be2cc80f7be847ae3931e46b2f57 Mon Sep 17 00:00:00 2001 From: mootz12 Date: Thu, 4 Jan 2024 15:07:47 -0500 Subject: [PATCH 2/2] chore: fix emission module interface to seperate claiming and updating usage --- backstop/src/backstop/deposit.rs | 2 +- backstop/src/backstop/withdrawal.rs | 4 +- backstop/src/emissions/claim.rs | 4 +- backstop/src/emissions/distributor.rs | 194 ++++++++++------ backstop/src/emissions/mod.rs | 2 +- pool/src/emissions/distributor.rs | 305 ++++++++++++++++++++++++-- pool/src/pool/user.rs | 3 +- 7 files changed, 414 insertions(+), 100 deletions(-) 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 d71ad9fc..1f0af966 100644 --- a/backstop/src/emissions/distributor.rs +++ b/backstop/src/emissions/distributor.rs @@ -13,26 +13,41 @@ use crate::{ }; /// 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, @@ -48,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, @@ -84,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, @@ -105,7 +131,8 @@ fn update_user_emissions( } 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); @@ -197,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); @@ -243,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, @@ -275,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, @@ -297,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 { @@ -339,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, || { @@ -353,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, @@ -392,43 +406,50 @@ 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: 0, + q4w: 4_5000000, + }; + let q4w: Q4W = Q4W { + amount: (4_5000000), + exp: (5000), }; let user_balance = UserBalance { - shares: 9_0000000, - q4w: vec![&e], + shares: 4_5000000, + 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, 34566000); - assert_eq!(new_user_data.accrued, 31_1094000); - assert_eq!(new_user_data.index, 34566000); + assert_eq!(new_backstop_data.index, 8503321); + assert_eq!(new_user_data.accrued, 38214948); + assert_eq!(new_user_data.index, 8503321); }); } #[test] - fn test_update_emissions_q4w_not_counted() { + fn test_claim_emissions() { let e = Env::default(); let block_timestamp = BACKSTOP_EPOCH + 1234; e.ledger().set(LedgerInfo { @@ -467,28 +488,65 @@ mod tests { let pool_balance = PoolBalance { shares: 150_0000000, tokens: 200_0000000, - q4w: 4_5000000, - }; - let q4w: Q4W = Q4W { - amount: (4_5000000), - exp: (5000), + q4w: 0, }; let user_balance = UserBalance { - shares: 4_5000000, - q4w: vec![&e, q4w], + shares: 9_0000000, + 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, 8503321); - assert_eq!(new_user_data.accrued, 38214948); - assert_eq!(new_user_data.index, 8503321); + assert_eq!(new_backstop_data.index, 8248888); + assert_eq!(new_user_data.accrued, 0); + assert_eq!(new_user_data.index, 8248888); + }); + } + + #[test] + 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()); }); } @@ -547,7 +605,7 @@ mod tests { q4w: vec![&e, q4w], }; - update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance, false); + update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); }); } @@ -599,7 +657,7 @@ mod tests { q4w: vec![&e], }; - update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance, false); + update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance); }); } @@ -651,7 +709,7 @@ mod tests { q4w: vec![&e], }; - update_emissions(&e, &pool_1, &pool_balance, &samwise, &user_balance, false); + 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 019022e2..d33064ea 100644 --- a/pool/src/emissions/distributor.rs +++ b/pool/src/emissions/distributor.rs @@ -34,14 +34,13 @@ pub fn execute_claim(e: &Env, from: &Address, reserve_token_ids: &Vec, to: ), _ => panic_with_error!(e, PoolError::BadRequest), }; - to_claim += update_emissions( + to_claim += claim_emissions( e, reserve_token_id, supply, 10i128.pow(reserve_config.decimals), from, user_balance, - true, ); } None => { @@ -74,7 +73,6 @@ pub fn execute_claim(e: &Env, from: &Address, reserve_token_ids: &Vec, to: /// * `supply_scalar` - The scalar of the reserve token /// * `user` - The user performing an action against the reserve /// * `balance` - The current balance of the user -/// * `claim` - Whether or not to claim the user's accrued emissions /// /// ### Panics /// If the reserve update failed @@ -85,35 +83,57 @@ pub fn update_emissions( supply_scalar: i128, user: &Address, balance: i128, - claim: bool, -) -> i128 { +) { if let Some(res_emis_data) = update_emission_data(e, res_token_id, supply, supply_scalar) { - return update_user_emissions( + update_user_emissions( e, &res_emis_data, res_token_id, supply_scalar, user, balance, - claim, + false, ); } - // no emissions data for the reserve exists - nothing to update - 0 } -/// Update the reserve token emission data +/// Update and claim the emissions for a reserve token. /// -/// Returns the new ReserveEmissionData, if None if no data exists +/// Returns the amount of tokens to claim. /// /// ### Arguments /// * `res_token_id` - The reserve token being acted against => (reserve index * 2 + (0 for debtToken or 1 for blendToken)) /// * `supply` - The current supply of the reserve token /// * `supply_scalar` - The scalar of the reserve token +/// * `user` - The user claiming for the reserve +/// * `balance` - The current balance of the user /// /// ### Panics /// If the reserve update failed -pub fn update_emission_data( +fn claim_emissions( + e: &Env, + res_token_id: u32, + supply: i128, + supply_scalar: i128, + user: &Address, + balance: i128, +) -> i128 { + if let Some(res_emis_data) = update_emission_data(e, res_token_id, supply, supply_scalar) { + update_user_emissions( + e, + &res_emis_data, + res_token_id, + supply_scalar, + user, + balance, + true, + ) + } else { + 0 + } +} + +fn update_emission_data( e: &Env, res_token_id: u32, supply: i128, @@ -131,6 +151,18 @@ pub fn update_emission_data( } } +/// Update the reserve token emission data +/// +/// Returns the new ReserveEmissionData, if None if no data exists +/// +/// ### Arguments +/// * `res_token_id` - The reserve token being acted against => (reserve index * 2 + (0 for debtToken or 1 for blendToken)) +/// * `supply` - The current supply of the reserve token +/// * `supply_scalar` - The scalar of the reserve token +/// * `emis_config` - The reserve token emission configuration +/// +/// ### Panics +/// If the reserve update failed pub(super) fn update_emission_data_with_config( e: &Env, res_token_id: u32, @@ -277,14 +309,13 @@ mod tests { storage::set_res_emis_data(&e, &res_token_index, &reserve_emission_data); storage::set_user_emissions(&e, &samwise, &res_token_index, &user_emission_data); - let _result = update_emissions( + update_emissions( &e, res_token_index, supply, 1_0000000, &samwise, user_position, - false, ); let new_reserve_emission_data = @@ -325,21 +356,17 @@ mod tests { let res_token_type = 1; let res_token_index = 1 * 2 + res_token_type; - let result = update_emissions( + update_emissions( &e, res_token_index, supply, 1_0000000, &samwise, user_position, - false, ); - if result == 0 { - assert!(storage::get_res_emis_data(&e, &res_token_index).is_none()); - assert!(storage::get_user_emissions(&e, &samwise, &res_token_index).is_none()); - } else { - assert!(false); - } + + assert!(storage::get_res_emis_data(&e, &res_token_index).is_none()); + assert!(storage::get_user_emissions(&e, &samwise, &res_token_index).is_none()); }); } @@ -392,11 +419,115 @@ mod tests { 1_0000000, &samwise, user_position, - false, ); }); } + /********** claim_emissions **********/ + + #[test] + fn test_claim_emissions() { + let e = Env::default(); + e.mock_all_auths(); + + let pool = testutils::create_pool(&e); + let samwise = Address::generate(&e); + + e.ledger().set(LedgerInfo { + timestamp: 1501000000, // 10^6 seconds have passed + protocol_version: 20, + sequence_number: 123, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let supply: i128 = 50_0000000; + let user_position: i128 = 2_0000000; + e.as_contract(&pool, || { + let reserve_emission_config = ReserveEmissionsConfig { + expiration: 1600000000, + eps: 0_0100000, + }; + let reserve_emission_data = ReserveEmissionsData { + index: 2345678, + last_time: 1500000000, + }; + let user_emission_data = UserEmissionData { + index: 1234567, + accrued: 0_1000000, + }; + let res_token_type = 0; + let res_token_index = 1 * 2 + res_token_type; + + storage::set_res_emis_config(&e, &res_token_index, &reserve_emission_config); + storage::set_res_emis_data(&e, &res_token_index, &reserve_emission_data); + storage::set_user_emissions(&e, &samwise, &res_token_index, &user_emission_data); + + let result = claim_emissions( + &e, + res_token_index, + supply, + 1_0000000, + &samwise, + user_position, + ); + + assert_eq!(result, 400_3222222); + let new_reserve_emission_data = + storage::get_res_emis_data(&e, &res_token_index).unwrap_optimized(); + let new_user_emission_data = + storage::get_user_emissions(&e, &samwise, &res_token_index).unwrap_optimized(); + assert_eq!(new_reserve_emission_data.last_time, 1501000000); + assert_eq!( + new_user_emission_data.index, + new_reserve_emission_data.index + ); + assert_eq!(new_user_emission_data.accrued, 0); + }); + } + + #[test] + fn test_claim_emissions_no_config_ignores() { + let e = Env::default(); + e.mock_all_auths(); + + let pool = testutils::create_pool(&e); + let samwise = Address::generate(&e); + + e.ledger().set(LedgerInfo { + timestamp: 1501000000, // 10^6 seconds have passed + protocol_version: 20, + sequence_number: 123, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let supply: i128 = 100_0000000; + let user_position: i128 = 2_0000000; + e.as_contract(&pool, || { + let res_token_type = 1; + let res_token_index = 1 * 2 + res_token_type; + + claim_emissions( + &e, + res_token_index, + supply, + 1_0000000, + &samwise, + user_position, + ); + + assert!(storage::get_res_emis_data(&e, &res_token_index).is_none()); + assert!(storage::get_user_emissions(&e, &samwise, &res_token_index).is_none()); + }); + } + /********** update emission data **********/ #[test] @@ -1140,7 +1271,8 @@ mod tests { }); } - //********** execute claim **********/ + //********** execute claim **********// + #[test] fn test_execute_claim() { let e = Env::default(); @@ -1266,6 +1398,131 @@ mod tests { }); } + #[test] + fn test_execute_claim_with_already_claimed_reserve() { + let e = Env::default(); + e.mock_all_auths_allowing_non_root_auth(); + e.budget().reset_unlimited(); + + let pool = testutils::create_pool(&e); + let bombadil = Address::generate(&e); + let samwise = Address::generate(&e); + let merry = Address::generate(&e); + + let (_, blnd_token_client) = testutils::create_blnd_token(&e, &pool, &bombadil); + let (backstop, _) = testutils::create_backstop(&e); + // mock backstop having emissions for pool + e.as_contract(&backstop, || { + blnd_token_client.approve(&backstop, &pool, &100_000_0000000_i128, &1000000); + }); + blnd_token_client.mint(&backstop, &100_000_0000000); + + e.ledger().set(LedgerInfo { + timestamp: 1501000000, // 10^6 seconds have passed + protocol_version: 20, + sequence_number: 123, + network_id: Default::default(), + base_reserve: 10, + min_temp_entry_ttl: 10, + min_persistent_entry_ttl: 10, + max_entry_ttl: 2000000, + }); + + let (underlying_0, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config, mut reserve_data) = testutils::default_reserve_meta(); + reserve_config.decimals = 5; + reserve_data.b_supply = 100_00000; + reserve_data.d_supply = 50_00000; + testutils::create_reserve(&e, &pool, &underlying_0, &reserve_config, &reserve_data); + + let (underlying_1, _) = testutils::create_token_contract(&e, &bombadil); + let (mut reserve_config, mut reserve_data) = testutils::default_reserve_meta(); + reserve_config.decimals = 9; + reserve_config.index = 1; + reserve_data.b_supply = 100_000_000_000; + reserve_data.d_supply = 50_000_000_000; + testutils::create_reserve(&e, &pool, &underlying_1, &reserve_config, &reserve_data); + + let user_positions = Positions { + liabilities: map![&e, (0, 2_00000)], + collateral: map![&e, (1, 1_000_000_000)], + supply: map![&e, (1, 1_000_000_000)], + }; + e.as_contract(&pool, || { + storage::set_backstop(&e, &backstop); + storage::set_user_positions(&e, &samwise, &user_positions); + + let reserve_emission_config_0 = ReserveEmissionsConfig { + expiration: 1600000000, + eps: 0_0100000, + }; + let reserve_emission_data_0 = ReserveEmissionsData { + index: 2345678, + last_time: 1500000000, + }; + let user_emission_data_0 = UserEmissionData { + index: 1234567, + accrued: 0_1000000, + }; + let res_token_index_0 = 0 * 2 + 0; // d_token for reserve 0 + + let reserve_emission_config_1 = ReserveEmissionsConfig { + expiration: 1600000000, + eps: 0_0150000, + }; + let reserve_emission_data_1 = ReserveEmissionsData { + index: 1345678, + last_time: 1501000000, + }; + let user_emission_data_1 = UserEmissionData { + index: 1345678, + accrued: 0, + }; + let res_token_index_1 = 1 * 2 + 1; // b_token for reserve 1 + + storage::set_res_emis_config(&e, &res_token_index_0, &reserve_emission_config_0); + storage::set_res_emis_data(&e, &res_token_index_0, &reserve_emission_data_0); + storage::set_user_emissions(&e, &samwise, &res_token_index_0, &user_emission_data_0); + + storage::set_res_emis_config(&e, &res_token_index_1, &reserve_emission_config_1); + storage::set_res_emis_data(&e, &res_token_index_1, &reserve_emission_data_1); + storage::set_user_emissions(&e, &samwise, &res_token_index_1, &user_emission_data_1); + + let reserve_token_ids: Vec = vec![&e, res_token_index_0, res_token_index_1]; + let result = execute_claim(&e, &samwise, &reserve_token_ids, &merry); + + let new_reserve_emission_data = + storage::get_res_emis_data(&e, &res_token_index_0).unwrap_optimized(); + let new_user_emission_data = + storage::get_user_emissions(&e, &samwise, &res_token_index_0).unwrap_optimized(); + assert_eq!(new_reserve_emission_data.last_time, 1501000000); + assert_eq!( + new_user_emission_data.index, + new_reserve_emission_data.index + ); + assert_eq!(new_user_emission_data.accrued, 0); + + let new_reserve_emission_data_1 = + storage::get_res_emis_data(&e, &res_token_index_1).unwrap_optimized(); + let new_user_emission_data_1 = + storage::get_user_emissions(&e, &samwise, &res_token_index_1).unwrap_optimized(); + assert_eq!(new_reserve_emission_data_1.last_time, 1501000000); + assert_eq!( + new_user_emission_data_1.index, + new_reserve_emission_data_1.index + ); + assert_eq!(new_user_emission_data.accrued, 0); + assert_eq!(result, 400_3222222); + + // verify tokens are sent + assert_eq!(blnd_token_client.balance(&merry), 400_3222222); + assert_eq!( + blnd_token_client.balance(&backstop), + 100_000_0000000 - 400_3222222 + ) + }); + } + #[test] #[should_panic(expected = "Error(Contract, #2)")] fn test_calc_claim_with_invalid_reserve_panics() { diff --git a/pool/src/pool/user.rs b/pool/src/pool/user.rs index e81f818b..b07502e7 100644 --- a/pool/src/pool/user.rs +++ b/pool/src/pool/user.rs @@ -197,9 +197,9 @@ impl User { reserve.scalar, &self.address, amount, - false, ); } + fn update_b_emissions(&self, e: &Env, reserve: &Reserve, amount: i128) { emissions::update_emissions( e, @@ -208,7 +208,6 @@ impl User { reserve.scalar, &self.address, amount, - false, ); } }