diff --git a/common/common_structs/src/alias_types.rs b/common/common_structs/src/alias_types.rs index b619eded1..4e785e28e 100644 --- a/common/common_structs/src/alias_types.rs +++ b/common/common_structs/src/alias_types.rs @@ -4,6 +4,7 @@ use crate::{LockedAssetTokenAttributesEx, UnlockSchedule}; pub type Nonce = u64; pub type Epoch = u64; +pub type Week = usize; pub type PaymentsVec = ManagedVec>; pub type UnlockPeriod = UnlockSchedule; pub type OldLockedTokenAttributes = LockedAssetTokenAttributesEx; diff --git a/common/common_structs/src/wrapper_types.rs b/common/common_structs/src/wrapper_types.rs index c1795a796..1090f0942 100644 --- a/common/common_structs/src/wrapper_types.rs +++ b/common/common_structs/src/wrapper_types.rs @@ -15,6 +15,19 @@ impl TokenPair { } } +#[derive(TopEncode, TopDecode, NestedEncode, NestedDecode, TypeAbi)] +pub struct NonceAmountPair { + pub nonce: u64, + pub amount: BigUint, +} + +impl NonceAmountPair { + #[inline] + pub fn new(nonce: u64, amount: BigUint) -> Self { + NonceAmountPair { nonce, amount } + } +} + #[derive( TypeAbi, TopEncode, TopDecode, NestedEncode, NestedDecode, ManagedVecItem, Clone, Debug, )] diff --git a/energy-integration/common-modules/weekly-rewards-splitting/src/base_impl.rs b/energy-integration/common-modules/weekly-rewards-splitting/src/base_impl.rs index 61c8c0227..74a952968 100644 --- a/energy-integration/common-modules/weekly-rewards-splitting/src/base_impl.rs +++ b/energy-integration/common-modules/weekly-rewards-splitting/src/base_impl.rs @@ -1,6 +1,6 @@ elrond_wasm::imports!(); -use common_types::TokenAmountPairsVec; +use common_types::PaymentsVec; use week_timekeeping::Week; use crate::{events, ClaimProgress}; @@ -18,7 +18,7 @@ pub trait WeeklyRewardsSplittingTraitsModule { &self, module: &Self::WeeklyRewardsSplittingMod, week: Week, - ) -> TokenAmountPairsVec<::Api> { + ) -> PaymentsVec<::Api> { let total_rewards_mapper = module.total_rewards_for_week(week); if total_rewards_mapper.is_empty() { let total_rewards = self.collect_rewards_for_week(module, week); @@ -34,7 +34,7 @@ pub trait WeeklyRewardsSplittingTraitsModule { &self, module: &Self::WeeklyRewardsSplittingMod, week: Week, - ) -> TokenAmountPairsVec<::Api>; + ) -> PaymentsVec<::Api>; fn get_claim_progress_mapper( &self, diff --git a/energy-integration/common-modules/weekly-rewards-splitting/src/global_info.rs b/energy-integration/common-modules/weekly-rewards-splitting/src/global_info.rs index 7ec15d45a..483abfc2c 100644 --- a/energy-integration/common-modules/weekly-rewards-splitting/src/global_info.rs +++ b/energy-integration/common-modules/weekly-rewards-splitting/src/global_info.rs @@ -1,6 +1,6 @@ elrond_wasm::imports!(); -use common_types::{TokenAmountPair, Week}; +use common_types::Week; use energy_query::Energy; use week_timekeeping::EPOCHS_IN_WEEK; @@ -73,7 +73,7 @@ pub trait WeeklyRewardsGlobalInfo: crate::events::WeeklyRewardsSplittingEventsMo fn total_rewards_for_week( &self, week: Week, - ) -> SingleValueMapper>>; + ) -> SingleValueMapper>>; #[view(getTotalEnergyForWeek)] #[storage_mapper("totalEnergyForWeek")] diff --git a/energy-integration/common-modules/weekly-rewards-splitting/src/lib.rs b/energy-integration/common-modules/weekly-rewards-splitting/src/lib.rs index dfd8f521e..fde012447 100644 --- a/energy-integration/common-modules/weekly-rewards-splitting/src/lib.rs +++ b/energy-integration/common-modules/weekly-rewards-splitting/src/lib.rs @@ -11,7 +11,7 @@ pub mod events; pub mod global_info; use base_impl::WeeklyRewardsSplittingTraitsModule; -use common_types::{PaymentsVec, TokenAmountPairsVec}; +use common_types::PaymentsVec; use energy_query::Energy; use week_timekeeping::{Week, EPOCHS_IN_WEEK}; @@ -138,7 +138,7 @@ pub trait WeeklyRewardsSplittingModule: &self, week: Week, energy_amount: &BigUint, - total_rewards: &TokenAmountPairsVec, + total_rewards: &PaymentsVec, ) -> PaymentsVec { let mut user_rewards = ManagedVec::new(); if energy_amount == &0 { @@ -149,7 +149,11 @@ pub trait WeeklyRewardsSplittingModule: for weekly_reward in total_rewards { let reward_amount = weekly_reward.amount * energy_amount / &total_energy; if reward_amount > 0 { - user_rewards.push(EsdtTokenPayment::new(weekly_reward.token, 0, reward_amount)); + user_rewards.push(EsdtTokenPayment::new( + weekly_reward.token_identifier, + 0, + reward_amount, + )); } } diff --git a/energy-integration/farm-boosted-yields/src/lib.rs b/energy-integration/farm-boosted-yields/src/lib.rs index eb460a74e..19fd68480 100644 --- a/energy-integration/farm-boosted-yields/src/lib.rs +++ b/energy-integration/farm-boosted-yields/src/lib.rs @@ -5,7 +5,7 @@ elrond_wasm::derive_imports!(); use core::marker::PhantomData; -use common_types::{Nonce, TokenAmountPair, TokenAmountPairsVec}; +use common_types::{Nonce, PaymentsVec}; use week_timekeeping::Week; use weekly_rewards_splitting::{base_impl::WeeklyRewardsSplittingTraitsModule, ClaimProgress}; @@ -121,13 +121,13 @@ where &self, module: &Self::WeeklyRewardsSplittingMod, week: Week, - ) -> TokenAmountPairsVec<::Api> { + ) -> PaymentsVec<::Api> { let reward_token_id = module.reward_token_id().get(); let rewards_mapper = module.accumulated_rewards_for_week(week); let total_rewards = rewards_mapper.get(); rewards_mapper.clear(); - ManagedVec::from_single_item(TokenAmountPair::new(reward_token_id, total_rewards)) + ManagedVec::from_single_item(EsdtTokenPayment::new(reward_token_id, 0, total_rewards)) } fn get_claim_progress_mapper( diff --git a/energy-integration/fees-collector/src/fees_accumulation.rs b/energy-integration/fees-collector/src/fees_accumulation.rs index 48794e829..8032adba4 100644 --- a/energy-integration/fees-collector/src/fees_accumulation.rs +++ b/energy-integration/fees-collector/src/fees_accumulation.rs @@ -1,7 +1,6 @@ elrond_wasm::imports!(); elrond_wasm::derive_imports!(); -use common_types::TokenAmountPair; use week_timekeeping::Week; #[elrond_wasm::module] @@ -21,32 +20,47 @@ pub trait FeesAccumulationModule: "Only known contracts can deposit" ); - let (payment_token, payment_amount) = self.call_value().single_fungible_esdt(); + let payment = self.call_value().single_esdt(); require!( - self.known_tokens().contains(&payment_token), + self.known_tokens().contains(&payment.token_identifier), "Invalid payment token" ); let current_week = self.get_current_week(); - self.accumulated_fees(current_week, &payment_token) - .update(|amt| *amt += &payment_amount); + if payment.token_nonce == 0 { + self.accumulated_fees(current_week, &payment.token_identifier) + .update(|amt| *amt += &payment.amount); + } else { + self.accumulated_locked_fees(current_week, &payment.token_identifier) + .update(|locked_fees| locked_fees.push(payment.clone())); + } - self.emit_deposit_swap_fees_event(caller, current_week, payment_token, payment_amount); + self.emit_deposit_swap_fees_event( + caller, + current_week, + payment.token_identifier, + payment.amount, + ); } fn collect_accumulated_fees_for_week( &self, week: Week, - ) -> ManagedVec> { + ) -> ManagedVec> { let mut results = ManagedVec::new(); let all_tokens = self.all_tokens().get(); for token in &all_tokens { let opt_accumulated_fees = self.get_and_clear_acccumulated_fees(week, &token); if let Some(accumulated_fees) = opt_accumulated_fees { - results.push(TokenAmountPair::new(token, accumulated_fees)); + results.push(EsdtTokenPayment::new(token.clone(), 0, accumulated_fees)); } - } + let opt_accumulated_locked_fees = + self.get_and_clear_acccumulated_locked_fees(week, &token); + if let Some(accumulated_locked_fees) = opt_accumulated_locked_fees { + results.append_vec(accumulated_locked_fees); + } + } results } @@ -59,7 +73,21 @@ pub trait FeesAccumulationModule: let value = mapper.get(); if value > 0 { mapper.clear(); + Some(value) + } else { + None + } + } + fn get_and_clear_acccumulated_locked_fees( + &self, + week: Week, + token: &TokenIdentifier, + ) -> Option>> { + let mapper = self.accumulated_locked_fees(week, token); + let value = mapper.get(); + if !value.is_empty() { + mapper.clear(); Some(value) } else { None @@ -69,4 +97,12 @@ pub trait FeesAccumulationModule: #[view(getAccumulatedFees)] #[storage_mapper("accumulatedFees")] fn accumulated_fees(&self, week: Week, token: &TokenIdentifier) -> SingleValueMapper; + + #[view(getAccumulatedLockedFees)] + #[storage_mapper("accumulatedLockedFees")] + fn accumulated_locked_fees( + &self, + week: Week, + token: &TokenIdentifier, + ) -> SingleValueMapper>>; } diff --git a/energy-integration/fees-collector/src/lib.rs b/energy-integration/fees-collector/src/lib.rs index 0faf1fee6..a8f7140c1 100644 --- a/energy-integration/fees-collector/src/lib.rs +++ b/energy-integration/fees-collector/src/lib.rs @@ -2,7 +2,7 @@ elrond_wasm::imports!(); -use common_types::{PaymentsVec, TokenAmountPair, TokenAmountPairsVec, Week}; +use common_types::{PaymentsVec, Week}; use core::marker::PhantomData; use energy_query::Energy; use weekly_rewards_splitting::base_impl::WeeklyRewardsSplittingTraitsModule; @@ -124,13 +124,13 @@ where &self, module: &Self::WeeklyRewardsSplittingMod, week: Week, - ) -> TokenAmountPairsVec<::Api> { + ) -> PaymentsVec<::Api> { let mut results = ManagedVec::new(); let all_tokens = module.all_tokens().get(); for token in &all_tokens { let opt_accumulated_fees = module.get_and_clear_acccumulated_fees(week, &token); if let Some(accumulated_fees) = opt_accumulated_fees { - results.push(TokenAmountPair::new(token, accumulated_fees)); + results.push(EsdtTokenPayment::new(token, 0, accumulated_fees)); } } diff --git a/energy-integration/fees-collector/tests/fees_collector_rust_test.rs b/energy-integration/fees-collector/tests/fees_collector_rust_test.rs index 37255b506..64a02c94c 100644 --- a/energy-integration/fees-collector/tests/fees_collector_rust_test.rs +++ b/energy-integration/fees-collector/tests/fees_collector_rust_test.rs @@ -1,9 +1,8 @@ mod fees_collector_test_setup; -use common_types::TokenAmountPair; use elrond_wasm::{ elrond_codec::multi_types::OptionalValue, - types::{BigInt, ManagedVec, MultiValueEncoded, OperationCompletionStatus}, + types::{BigInt, EsdtTokenPayment, ManagedVec, MultiValueEncoded, OperationCompletionStatus}, }; use elrond_wasm_debug::{managed_address, managed_biguint, managed_token_id, rust_biguint}; use elrond_wasm_modules::pause::PauseModule; @@ -321,15 +320,16 @@ fn claim_second_week_test() { .b_mock .execute_query(&fc_setup.fc_wrapper, |sc| { let mut expected_total_rewards = ManagedVec::new(); - expected_total_rewards.push(TokenAmountPair { - token: managed_token_id!(FIRST_TOKEN_ID), - amount: managed_biguint!(USER_BALANCE), - }); - expected_total_rewards.push(TokenAmountPair { - token: managed_token_id!(SECOND_TOKEN_ID), - amount: managed_biguint!(USER_BALANCE / 2), - }); - + expected_total_rewards.push(EsdtTokenPayment::new( + managed_token_id!(FIRST_TOKEN_ID), + 0, + managed_biguint!(USER_BALANCE), + )); + expected_total_rewards.push(EsdtTokenPayment::new( + managed_token_id!(SECOND_TOKEN_ID), + 0, + managed_biguint!(USER_BALANCE / 2), + )); assert_eq!(expected_total_rewards, sc.total_rewards_for_week(1).get()); }) .assert_ok(); @@ -366,14 +366,16 @@ fn claim_second_week_test() { ); let mut expected_total_rewards = ManagedVec::new(); - expected_total_rewards.push(TokenAmountPair { - token: managed_token_id!(FIRST_TOKEN_ID), - amount: managed_biguint!(USER_BALANCE), - }); - expected_total_rewards.push(TokenAmountPair { - token: managed_token_id!(SECOND_TOKEN_ID), - amount: managed_biguint!(USER_BALANCE / 2), - }); + expected_total_rewards.push(EsdtTokenPayment::new( + managed_token_id!(FIRST_TOKEN_ID), + 0, + managed_biguint!(USER_BALANCE), + )); + expected_total_rewards.push(EsdtTokenPayment::new( + managed_token_id!(SECOND_TOKEN_ID), + 0, + managed_biguint!(USER_BALANCE / 2), + )); assert_eq!(sc.total_rewards_for_week(1).get(), expected_total_rewards); // first user's new energy is added to week 2 @@ -426,14 +428,16 @@ fn claim_second_week_test() { .b_mock .execute_query(&fc_setup.fc_wrapper, |sc| { let mut expected_total_rewards = ManagedVec::new(); - expected_total_rewards.push(TokenAmountPair { - token: managed_token_id!(FIRST_TOKEN_ID), - amount: managed_biguint!(USER_BALANCE), - }); - expected_total_rewards.push(TokenAmountPair { - token: managed_token_id!(SECOND_TOKEN_ID), - amount: managed_biguint!(USER_BALANCE / 2), - }); + expected_total_rewards.push(EsdtTokenPayment::new( + managed_token_id!(FIRST_TOKEN_ID), + 0, + managed_biguint!(USER_BALANCE), + )); + expected_total_rewards.push(EsdtTokenPayment::new( + managed_token_id!(SECOND_TOKEN_ID), + 0, + managed_biguint!(USER_BALANCE / 2), + )); assert_eq!(sc.total_rewards_for_week(1).get(), expected_total_rewards); // first user's new energy is added to week 2 diff --git a/locked-asset/simple-lock-energy/src/token_merging.rs b/locked-asset/simple-lock-energy/src/token_merging.rs index 693ecac63..193f07643 100644 --- a/locked-asset/simple-lock-energy/src/token_merging.rs +++ b/locked-asset/simple-lock-energy/src/token_merging.rs @@ -1,5 +1,6 @@ elrond_wasm::imports!(); +use common_structs::PaymentsVec; use mergeable::{weighted_average, Mergeable}; use simple_lock::locked_token::LockedTokenAttributes; @@ -49,79 +50,90 @@ pub trait TokenMergingModule: // TODO: Only allow original caller arg for whitelisted addresses #[payable("*")] #[endpoint(mergeTokens)] - fn merge_tokens(&self, opt_original_caller: OptionalValue) -> EsdtTokenPayment { + fn merge_tokens_endpoint( + &self, + opt_original_caller: OptionalValue, + ) -> EsdtTokenPayment { self.require_not_paused(); - let current_epoch = self.blockchain().get_block_epoch(); let actual_caller = self.blockchain().get_caller(); - let original_caller = self.dest_from_optional(opt_original_caller); + + let payments = self.get_non_empty_payments(); + + let output_amount_attributes = self.merge_tokens(payments, opt_original_caller); + + let simulated_lock_payment = EgldOrEsdtTokenPayment::new( + output_amount_attributes.attributes.original_token_id, + output_amount_attributes.attributes.original_token_nonce, + output_amount_attributes.token_amount, + ); + let output_tokens = self.lock_and_send( + &actual_caller, + simulated_lock_payment, + output_amount_attributes.attributes.unlock_epoch, + ); + + self.to_esdt_payment(output_tokens) + } + + fn merge_tokens( + self, + mut payments: PaymentsVec, + opt_original_caller: OptionalValue, + ) -> LockedAmountAttributesPair { let locked_token_mapper = self.locked_token(); + let original_caller = self.dest_from_optional(opt_original_caller); + let current_epoch = self.blockchain().get_block_epoch(); - let mut payments = self.get_non_empty_payments(); locked_token_mapper.require_all_same_token(&payments); let first_payment = payments.get(0); payments.remove(0); - let output_amount_attributes = - self.update_energy(&original_caller, |energy: &mut Energy| { - let first_token_attributes: LockedTokenAttributes = - locked_token_mapper.get_token_attributes(first_payment.token_nonce); + self.update_energy(&original_caller, |energy: &mut Energy| { + let first_token_attributes: LockedTokenAttributes = + locked_token_mapper.get_token_attributes(first_payment.token_nonce); + energy.update_after_unlock_any( + &first_payment.amount, + first_token_attributes.unlock_epoch, + current_epoch, + ); + + locked_token_mapper.nft_burn(first_payment.token_nonce, &first_payment.amount); + + let mut output_pair = LockedAmountAttributesPair { + token_amount: first_payment.amount, + attributes: first_token_attributes, + }; + for payment in &payments { + let attributes: LockedTokenAttributes = + locked_token_mapper.get_token_attributes(payment.token_nonce); energy.update_after_unlock_any( - &first_payment.amount, - first_token_attributes.unlock_epoch, + &payment.amount, + attributes.unlock_epoch, current_epoch, ); - locked_token_mapper.nft_burn(first_payment.token_nonce, &first_payment.amount); + locked_token_mapper.nft_burn(payment.token_nonce, &payment.amount); - let mut output_pair = LockedAmountAttributesPair { - token_amount: first_payment.amount, - attributes: first_token_attributes, + let amount_attr_pair = LockedAmountAttributesPair { + token_amount: payment.amount, + attributes, }; - for payment in &payments { - let attributes: LockedTokenAttributes = - locked_token_mapper.get_token_attributes(payment.token_nonce); - energy.update_after_unlock_any( - &payment.amount, - attributes.unlock_epoch, - current_epoch, - ); - - locked_token_mapper.nft_burn(payment.token_nonce, &payment.amount); - - let amount_attr_pair = LockedAmountAttributesPair { - token_amount: payment.amount, - attributes, - }; - output_pair.merge_with(amount_attr_pair); - } - - let normalized_unlock_epoch = self.unlock_epoch_to_start_of_month_upper_estimate( - output_pair.attributes.unlock_epoch, - ); - output_pair.attributes.unlock_epoch = normalized_unlock_epoch; + output_pair.merge_with(amount_attr_pair); + } - energy.add_after_token_lock( - &output_pair.token_amount, - output_pair.attributes.unlock_epoch, - current_epoch, - ); - - output_pair - }); + let normalized_unlock_epoch = self + .unlock_epoch_to_start_of_month_upper_estimate(output_pair.attributes.unlock_epoch); + output_pair.attributes.unlock_epoch = normalized_unlock_epoch; - let simulated_lock_payment = EgldOrEsdtTokenPayment::new( - output_amount_attributes.attributes.original_token_id, - output_amount_attributes.attributes.original_token_nonce, - output_amount_attributes.token_amount, - ); - let output_tokens = self.lock_and_send( - &actual_caller, - simulated_lock_payment, - output_amount_attributes.attributes.unlock_epoch, - ); + energy.add_after_token_lock( + &output_pair.token_amount, + output_pair.attributes.unlock_epoch, + current_epoch, + ); - self.to_esdt_payment(output_tokens) + output_pair + }) } } diff --git a/locked-asset/simple-lock-energy/src/unlock_with_penalty.rs b/locked-asset/simple-lock-energy/src/unlock_with_penalty.rs index f40d419e4..592ecf85c 100644 --- a/locked-asset/simple-lock-energy/src/unlock_with_penalty.rs +++ b/locked-asset/simple-lock-energy/src/unlock_with_penalty.rs @@ -1,13 +1,15 @@ elrond_wasm::imports!(); elrond_wasm::derive_imports!(); -use common_structs::Epoch; +use common_structs::{Epoch, Nonce, NonceAmountPair, PaymentsVec}; + use simple_lock::locked_token::LockedTokenAttributes; -use crate::lock_options::{EPOCHS_PER_YEAR}; +use crate::{lock_options::EPOCHS_PER_YEAR, token_merging}; const MAX_PERCENTAGE: u16 = 10_000; // 100% const MIN_EPOCHS_TO_REDUCE: Epoch = 1; +const EPOCHS_PER_WEEK: Epoch = 7; static INVALID_PERCENTAGE_ERR_MSG: &[u8] = b"Invalid percentage value"; #[derive(TypeAbi, TopEncode, TopDecode)] @@ -37,6 +39,7 @@ pub trait UnlockWithPenaltyModule: + crate::lock_options::LockOptionsModule + crate::events::EventsModule + elrond_wasm_modules::pause::PauseModule + + token_merging::TokenMergingModule + utils::UtilsModule { /// - min_penalty_percentage / max_penalty_percentage: The penalty for early unlock @@ -111,29 +114,29 @@ pub trait UnlockWithPenaltyModule: let attributes: LockedTokenAttributes = locked_token_mapper.get_token_attributes(payment.token_nonce); - locked_token_mapper.nft_burn(payment.token_nonce, &payment.amount); - let epochs_to_reduce = self.resolve_opt_epochs_to_reduce(opt_epochs_to_reduce, attributes.unlock_epoch); let penalty_amount = self.calculate_penalty_amount(&payment.amount, epochs_to_reduce); + locked_token_mapper.nft_burn(payment.token_nonce, &(&payment.amount - &penalty_amount)); + let current_epoch = self.blockchain().get_block_epoch(); let caller = self.blockchain().get_caller(); let mut energy = self.get_updated_energy_entry_for_user(&caller); energy.deplete_after_early_unlock(&payment.amount, attributes.unlock_epoch, current_epoch); - let mut unlocked_tokens = self.unlock_tokens_unchecked(payment, &attributes); + let mut unlocked_tokens = self.unlock_tokens_unchecked(payment.clone(), &attributes); let unlocked_token_id = unlocked_tokens.token_identifier.clone().unwrap_esdt(); let new_unlock_epoch = attributes.unlock_epoch - epochs_to_reduce; - let amount_to_mint = if new_unlock_epoch == current_epoch { - &unlocked_tokens.amount - } else { - &penalty_amount - }; - self.send() - .esdt_local_mint(&unlocked_token_id, 0, amount_to_mint); + if new_unlock_epoch == current_epoch { + self.send().esdt_local_mint( + &unlocked_token_id, + 0, + &(&unlocked_tokens.amount - &penalty_amount), + ); + } if penalty_amount > 0 { unlocked_tokens.amount -= &penalty_amount; @@ -142,7 +145,11 @@ pub trait UnlockWithPenaltyModule: "No tokens remaining after penalty is applied" ); - self.burn_penalty(unlocked_token_id, &penalty_amount); + self.burn_penalty( + locked_token_mapper.get_token_id(), + payment.token_nonce, + &penalty_amount, + ); } let output_payment = self.lock_and_send(&caller, unlocked_tokens, new_unlock_epoch); @@ -197,25 +204,92 @@ pub trait UnlockWithPenaltyModule: token_amount * penalty_percentage / MAX_PERCENTAGE as u64 } - fn burn_penalty(&self, token_id: TokenIdentifier, fees_amount: &BigUint) { + fn burn_penalty(&self, token_id: TokenIdentifier, token_nonce: Nonce, fees_amount: &BigUint) { let fees_burn_percentage = self.fees_burn_percentage().get(); let burn_amount = fees_amount * fees_burn_percentage as u64 / MAX_PERCENTAGE as u64; let remaining_amount = fees_amount - &burn_amount; if burn_amount > 0 { - self.send().esdt_local_burn(&token_id, 0, &burn_amount); + self.send() + .esdt_local_burn(&token_id, token_nonce, &burn_amount); } if remaining_amount > 0 { - self.send_fees_to_collector(token_id, remaining_amount); + if self.fees_from_penalty_unlocking().is_empty() { + // First fee deposit of the week + self.fees_from_penalty_unlocking() + .set(NonceAmountPair::new(token_nonce, remaining_amount)); + } else { + self.merge_fees_from_penalty(token_nonce, remaining_amount) + } } + + // Only once per week + self.send_fees_to_collector(); } - fn send_fees_to_collector(&self, token_id: TokenIdentifier, amount: BigUint) { + /// Merges new fees with existing fees and saves in storage + fn merge_fees_from_penalty(&self, token_nonce: Nonce, new_fee_amount: BigUint) { + let locked_token_mapper = self.locked_token(); + let existing_nonce_amount_pair = self.fees_from_penalty_unlocking().get(); + let mut payments = PaymentsVec::new(); + payments.push(EsdtTokenPayment::new( + locked_token_mapper.get_token_id(), + token_nonce, + new_fee_amount, + )); + payments.push(EsdtTokenPayment::new( + locked_token_mapper.get_token_id(), + existing_nonce_amount_pair.nonce, + existing_nonce_amount_pair.amount, + )); + + let new_locked_amount_attributes = self.merge_tokens(payments, OptionalValue::None); + + let sft_nonce = self.get_or_create_nonce_for_attributes( + &locked_token_mapper, + &new_locked_amount_attributes + .attributes + .original_token_id + .clone() + .into_name(), + &new_locked_amount_attributes.attributes, + ); + + let new_locked_tokens = locked_token_mapper + .nft_add_quantity(sft_nonce, new_locked_amount_attributes.token_amount); + + self.fees_from_penalty_unlocking().set(NonceAmountPair::new( + new_locked_tokens.token_nonce, + new_locked_tokens.amount, + )); + } + + #[endpoint(sendFeesToCollector)] + fn send_fees_to_collector(&self) { + // Send fees to FeeCollector SC + let current_epoch = self.blockchain().get_block_epoch(); + let last_epoch_fee_sent_to_collector = self.last_epoch_fee_sent_to_collector().get(); + let next_send_epoch = last_epoch_fee_sent_to_collector + EPOCHS_PER_WEEK; + + if current_epoch < next_send_epoch { + return; + } + let sc_address = self.fees_collector_address().get(); + let locked_token_id = self.locked_token().get_token_id(); + let nonce_amount_pair = self.fees_from_penalty_unlocking().get(); + + self.fees_from_penalty_unlocking().clear(); self.fees_collector_proxy_builder(sc_address) .deposit_swap_fees() - .add_esdt_token_transfer(token_id, 0, amount) + .add_esdt_token_transfer( + locked_token_id, + nonce_amount_pair.nonce, + nonce_amount_pair.amount, + ) .execute_on_dest_context_ignore_result(); + + self.last_epoch_fee_sent_to_collector().set(current_epoch); } #[proxy] @@ -235,4 +309,12 @@ pub trait UnlockWithPenaltyModule: #[view(getFeesCollectorAddress)] #[storage_mapper("feesCollectorAddress")] fn fees_collector_address(&self) -> SingleValueMapper; + + #[view(getFeesFromPenaltyUnlocking)] + #[storage_mapper("feesFromPenaltyUnlocking")] + fn fees_from_penalty_unlocking(&self) -> SingleValueMapper>; + + #[view(getLastEpochFeeSentToCollector)] + #[storage_mapper("lastEpochFeeSentToCollector")] + fn last_epoch_fee_sent_to_collector(&self) -> SingleValueMapper; } diff --git a/locked-asset/simple-lock-energy/tests/simple_lock_energy_setup/mod.rs b/locked-asset/simple-lock-energy/tests/simple_lock_energy_setup/mod.rs index 3b3fb4b3b..5873f2c62 100644 --- a/locked-asset/simple-lock-energy/tests/simple_lock_energy_setup/mod.rs +++ b/locked-asset/simple-lock-energy/tests/simple_lock_energy_setup/mod.rs @@ -20,6 +20,7 @@ mod fees_collector_mock; use fees_collector_mock::*; pub const EPOCHS_IN_YEAR: u64 = 360; +pub const EPOCHS_IN_WEEK: u64 = 7; pub const USER_BALANCE: u64 = 1_000_000_000_000_000_000; pub static BASE_ASSET_TOKEN_ID: &[u8] = b"MEX-123456"; @@ -222,6 +223,24 @@ where ) } + pub fn send_fees_to_collector( + &mut self, + caller: &Address, + token_nonce: u64, + amount: u64, + ) -> TxResult { + self.b_mock.execute_esdt_transfer( + caller, + &self.sc_wrapper, + LOCKED_TOKEN_ID, + token_nonce, + &rust_biguint!(amount), + |sc| { + sc.send_fees_to_collector(); + }, + ) + } + pub fn get_penalty_amount( &mut self, token_amount: u64, diff --git a/locked-asset/simple-lock-energy/tests/simple_lock_energy_test.rs b/locked-asset/simple-lock-energy/tests/simple_lock_energy_test.rs index 5c2c6f4b0..6afd8bc21 100644 --- a/locked-asset/simple-lock-energy/tests/simple_lock_energy_test.rs +++ b/locked-asset/simple-lock-energy/tests/simple_lock_energy_test.rs @@ -171,6 +171,209 @@ fn unlock_early_test() { assert_eq!(actual_energy, expected_energy); } +#[test] +fn multiple_early_unlocks_same_week_test() { + let mut setup = SimpleLockEnergySetup::new(simple_lock_energy::contract_obj); + let first_user = setup.first_user.clone(); + let half_balance = USER_BALANCE / 2; + let sixth_balance = half_balance / 3; + + let mut current_epoch = 1; + setup.b_mock.set_block_epoch(current_epoch); + + setup + .lock( + &first_user, + BASE_ASSET_TOKEN_ID, + half_balance, + LOCK_OPTIONS[0], + ) + .assert_ok(); + + // unlock early after half a year - with half a year remaining + // unlock epoch = 360, so epochs remaining after half year (1 + 365 / 2 = 183) + // = 360 - 183 = 177 + let half_year_epochs = EPOCHS_IN_YEAR / 2; + current_epoch += half_year_epochs; + setup.b_mock.set_block_epoch(current_epoch); + + let mut penalty_percentage = 498u64; // 1 + 9_999 * 177 / (10 * 365) ~= 1 + 484 = 485 + let mut expected_penalty_amount = rust_biguint!(sixth_balance) * penalty_percentage / 10_000u64; + let mut penalty_amount = setup.get_penalty_amount(sixth_balance, 179); + assert_eq!(penalty_amount, expected_penalty_amount); + + // Unlock early 1/3 of the LockedTokens + setup + .unlock_early(&first_user, 1, sixth_balance) + .assert_ok(); + + let received_token_amount = rust_biguint!(sixth_balance) - penalty_amount; + let expected_balance = &received_token_amount + half_balance; + setup + .b_mock + .check_esdt_balance(&first_user, BASE_ASSET_TOKEN_ID, &expected_balance); + + // After first early unlock of the week, fees are sent to Fee Collector SC + setup.b_mock.check_nft_balance( + &setup.fees_collector_mock, + LOCKED_TOKEN_ID, + 1, + &(&expected_penalty_amount / 2u64 + 1u64), + Some(&LockedTokenAttributes:: { + original_token_id: managed_token_id_wrapped!(BASE_ASSET_TOKEN_ID), + original_token_nonce: 0, + unlock_epoch: 360, + }), + ); + + // Unlock early the another 1/3 of the LockedTokens, same week -> First Locked Tokens + setup + .unlock_early(&first_user, 1, sixth_balance) + .assert_ok(); + + penalty_percentage = 498u64; // 1 + 9_999 * 177 / (10 * 365) ~= 1 + 484 = 485 + expected_penalty_amount = rust_biguint!(sixth_balance) * penalty_percentage / 10_000u64; + penalty_amount = setup.get_penalty_amount(sixth_balance, 179); + assert_eq!(penalty_amount, expected_penalty_amount); + + let received_token_amount_2 = rust_biguint!(sixth_balance) - penalty_amount; + let expected_balance = &received_token_amount_2 + &received_token_amount + half_balance; + setup + .b_mock + .check_esdt_balance(&first_user, BASE_ASSET_TOKEN_ID, &expected_balance); + + // Energy SC stores the fee until the end of the week + // Doesn't send it to FeeCollector yet + setup.b_mock.check_nft_balance( + &setup.sc_wrapper.address_ref(), + LOCKED_TOKEN_ID, + 1, + &(expected_penalty_amount / 2u64 + 2u64), + Some(&LockedTokenAttributes:: { + original_token_id: managed_token_id_wrapped!(BASE_ASSET_TOKEN_ID), + original_token_nonce: 0, + unlock_epoch: 360, + }), + ); + + // Unlock early the last 1/3 of the LockedTokens, same week -> Locked Token Merging + setup + .unlock_early(&first_user, 1, sixth_balance) + .assert_ok(); + + penalty_percentage = 498u64; // 1 + 9_999 * 177 / (10 * 365) ~= 1 + 484 = 485 + expected_penalty_amount = rust_biguint!(sixth_balance) * penalty_percentage / 10_000u64; + penalty_amount = setup.get_penalty_amount(sixth_balance, 179); + assert_eq!(penalty_amount, expected_penalty_amount); + + let received_token_amount_3 = rust_biguint!(sixth_balance) - penalty_amount; + let expected_balance = + &received_token_amount_3 + &received_token_amount_2 + &received_token_amount + half_balance; + setup + .b_mock + .check_esdt_balance(&first_user, BASE_ASSET_TOKEN_ID, &expected_balance); + + // Energy SC stores the fee until the end of the week + // Doesn't send it to FeeCollector yet + setup.b_mock.check_nft_balance( + &setup.sc_wrapper.address_ref(), + LOCKED_TOKEN_ID, + 2, + &(expected_penalty_amount + 2u64), + Some(&LockedTokenAttributes:: { + original_token_id: managed_token_id_wrapped!(BASE_ASSET_TOKEN_ID), + original_token_nonce: 0, + unlock_epoch: 390, + }), + ); +} + +#[test] +fn multiple_early_unlocks_multiple_weeks_fee_collector_check_test() { + let mut setup = SimpleLockEnergySetup::new(simple_lock_energy::contract_obj); + let first_user = setup.first_user.clone(); + let half_balance = USER_BALANCE / 2; + let quarter_balance = half_balance / 2; + + let mut current_epoch = 1; + setup.b_mock.set_block_epoch(current_epoch); + + setup + .lock( + &first_user, + BASE_ASSET_TOKEN_ID, + half_balance, + LOCK_OPTIONS[0], + ) + .assert_ok(); + + // unlock early after half a year - with half a year remaining + // unlock epoch = 360, so epochs remaining after half year (1 + 365 / 2 = 183) + // = 360 - 183 = 177 + let half_year_epochs = EPOCHS_IN_YEAR / 2; + current_epoch += half_year_epochs; + setup.b_mock.set_block_epoch(current_epoch); + + let mut penalty_percentage = 498u64; // 1 + 9_999 * 177 / (10 * 365) ~= 1 + 484 = 485 + let expected_penalty_amount = rust_biguint!(quarter_balance) * penalty_percentage / 10_000u64; + let mut penalty_amount = setup.get_penalty_amount(quarter_balance, 179); + assert_eq!(penalty_amount, expected_penalty_amount); + + // Unlock early half of the LockedTokens + setup + .unlock_early(&first_user, 1, quarter_balance) + .assert_ok(); + + let received_token_amount = rust_biguint!(quarter_balance) - penalty_amount; + let expected_balance = &received_token_amount + half_balance; + setup + .b_mock + .check_esdt_balance(&first_user, BASE_ASSET_TOKEN_ID, &expected_balance); + + setup.b_mock.check_nft_balance( + &setup.fees_collector_mock, + LOCKED_TOKEN_ID, + 1, + &(&expected_penalty_amount / 2u64), + Some(&LockedTokenAttributes:: { + original_token_id: managed_token_id_wrapped!(BASE_ASSET_TOKEN_ID), + original_token_nonce: 0, + unlock_epoch: 360, + }), + ); + + current_epoch += EPOCHS_IN_WEEK; + setup.b_mock.set_block_epoch(current_epoch); + + // Unlock early the other half of the LockedTokens + setup + .unlock_early(&first_user, 1, quarter_balance) + .assert_ok(); + + penalty_percentage = 478u64; // 1 + 9_999 * 172 / (10 * 360) ~= 1 + 465 = 466 + let expected_penalty_amount_2 = rust_biguint!(quarter_balance) * penalty_percentage / 10_000u64; + penalty_amount = setup.get_penalty_amount(quarter_balance, 172); + assert_eq!(penalty_amount, expected_penalty_amount_2); + + let received_token_amount_2 = rust_biguint!(quarter_balance) - penalty_amount; + let expected_balance = &received_token_amount_2 + &received_token_amount + half_balance; + setup + .b_mock + .check_esdt_balance(&first_user, BASE_ASSET_TOKEN_ID, &expected_balance); + + setup.b_mock.check_nft_balance( + &setup.fees_collector_mock, + LOCKED_TOKEN_ID, + 1, + &((&expected_penalty_amount + &expected_penalty_amount_2) / 2u64), + Some(&LockedTokenAttributes:: { + original_token_id: managed_token_id_wrapped!(BASE_ASSET_TOKEN_ID), + original_token_nonce: 0, + unlock_epoch: 360, + }), + ); +} + #[test] fn reduce_lock_period_test() { let mut setup = SimpleLockEnergySetup::new(simple_lock_energy::contract_obj); @@ -220,16 +423,24 @@ fn reduce_lock_period_test() { }), ); - // check the tokens were half burned, half set to fees collector - setup.b_mock.check_esdt_balance( + // Energy SC stores the fee until the end of the week + setup.b_mock.check_nft_balance( &setup.sc_wrapper.address_ref(), - BASE_ASSET_TOKEN_ID, - &rust_biguint!(0), + LOCKED_TOKEN_ID, + 1, + &(penalty_amount / 2u64 + 1u64), + Some(&LockedTokenAttributes:: { + original_token_id: managed_token_id_wrapped!(BASE_ASSET_TOKEN_ID), + original_token_nonce: 0, + unlock_epoch: 1800, + }), ); + + //at this point, the fee collector should not receive any tokens setup.b_mock.check_esdt_balance( &setup.fees_collector_mock, BASE_ASSET_TOKEN_ID, - &(penalty_amount / 2u64), + &rust_biguint!(0), ); // check new energy amount diff --git a/locked-asset/simple-lock-energy/tests/token_merging_test.rs b/locked-asset/simple-lock-energy/tests/token_merging_test.rs index 194ab19cb..676af01a3 100644 --- a/locked-asset/simple-lock-energy/tests/token_merging_test.rs +++ b/locked-asset/simple-lock-energy/tests/token_merging_test.rs @@ -51,7 +51,7 @@ fn token_merging_test() { setup .b_mock .execute_esdt_multi_transfer(&first_user, &setup.sc_wrapper, &payments[..], |sc| { - let _ = sc.merge_tokens(OptionalValue::None); + let _ = sc.merge_tokens_endpoint(OptionalValue::None); }) .assert_ok();