From 3fc00b6ccd15cb0e93f097ae483e734594deab5c Mon Sep 17 00:00:00 2001 From: Dorin Marian Iancu Date: Wed, 4 Dec 2024 09:30:28 +0200 Subject: [PATCH 1/5] impl farm with top up --- dex/farm-with-top-up/README.md | 105 ------------- dex/farm-with-top-up/src/base_functions.rs | 97 +++++++++--- dex/farm-with-top-up/src/custom_rewards.rs | 144 ++++++++++++++++++ .../src/external_interaction.rs | 7 +- dex/farm-with-top-up/src/lib.rs | 66 +++----- dex/farm-with-top-up/wasm/src/lib.rs | 16 +- dex/proxy-deployer/src/set_contract_active.rs | 10 +- .../farm-staking/src/base_impl_wrapper.rs | 1 + 8 files changed, 263 insertions(+), 183 deletions(-) delete mode 100644 dex/farm-with-top-up/README.md create mode 100644 dex/farm-with-top-up/src/custom_rewards.rs diff --git a/dex/farm-with-top-up/README.md b/dex/farm-with-top-up/README.md deleted file mode 100644 index fcc860299..000000000 --- a/dex/farm-with-top-up/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# Farm Smart Contract - -## Abstract - -Liquidity providers of xExchange are incentivized with MEX rewards in exchange for them locking their LP tokens in Farm contracts. - -## Introduction - -This smart contract has the role of generating and distributing MEX tokens to liquidity providers that choose to lock their LP tokens, thus increasing the ecosystem stability. - -## Endpoints - -### init - -```rust - #[init] - fn init( - &self, - reward_token_id: TokenIdentifier, - farming_token_id: TokenIdentifier, - division_safety_constant: BigUint, - pair_contract_address: ManagedAddress, - ); -``` - -The arguments are: - -- __reward_token_id__ - MEX token ID -- __farming_token_id__ - token used for farming - LP tokens usually -- __division_safety_constant__ - a constant that is used for math safety functions - increasing precision of reward distribution -- __pair_contract_address__ - almost each farm contract has an associated pair contract, exception being the MEX farm. This address needs to be known because in case of penalty burn, the farm will need the Pair contract in order to convert LP tokens to MEX and then burn them - -### enterFarm - -```rust - #[payable("*")] - #[endpoint(enterFarm)] - fn enter_farm(&self); -``` - -This endpoint receives at least one payment: - -- The first payment has to be of type __farming_token_id__. The actual token that is meant to be locked inside the Farm contract. -- The additional payments, if any, will be Farm positions and will be used to be merged with the newly created position, in order to consolidate all previous positions with the current one. - -This endpoint will give back to the caller a Farm position as a result. The Farm position is a META esdt that contains, in its attributes, information about the user input tokens and the current state of the contract when the user did enter. This information will be later used when trying to claim rewards or exit farm. - -### exitFarm - -```rust - #[payable("*")] - #[endpoint(exitFarm)] - fn exit_farm(&self); -``` - -This endpoint receives one payment, and that is the Farm Position. Based on an internal counter that the contract keeps track of, which is the __rps__ - meaning reward per share, the contract can calculate the reward that it needs to return to the caller for those specific tokens that he has sent. The output will consist of two payments: the LP tokens initially added and the accumulated rewards. - -This contract simulates per-block-reward-generation by keeping track of the last block that generated mex and keeps updating on every endpoint execution. Everytime an execution happens, the contract will generate the rewards for previous blocks. This is the case for the first successful TX inside a block, so only once per block this check has to be made and the action to be taken. - -If a user decides to exit too early, they will receive a penalty. This contract will take a part of its input LP tokens and will used them to buyback-and-burn MEX. This is done via a smart contract call to the configured pair contract address, via __removeLiquidityAndBuybackAndBurnToken__ endpoint. - -### claimRewards - -```rust - #[payable("*")] - #[endpoint(claimRewards)] - fn claim_rewards(&self); -``` - -This endpoint receives at least one payment: - -- The first payment is a Farm position that is 'harvested'. So for this position, the contract will calculate the reward and will return it to its caller. The contract will create a new position that has the ```RPS`` (Reward per share) reset. -- The additional payments, if any, will be other Farm positions and will be used to be merged with the newly created one. - -### compoundRewards - -```rust - #[payable("*")] - #[endpoint(compoundRewards)] - fn compound_rewards(&self); -``` - -This endpoint is similar with claimRewards, the differences being that instead of giving back the rewards to the caller, they are compounded into the newly created position (with the reset RPS). For this to be possible, reward token and farming token have to be the same, hence it is applicable only in case of MEX Farm. - -### mergePositions - -```rust - #[payable("*")] - #[endpoint(mergeFarmTokens)] - fn merge_farm_tokens(&self); -``` - -This endpoint merges two or more farm positions together and returns a single consolidated position to the caller. - -## Testing - -Aside from the scenario tests, there are a lot of tests that are available in the rust test suite. - -## Interaction - -The interaction scripts for this contract are located in the dex subdirectory of the root project directory. - -## Deployment - -The deployment of this contract is done using interaction scripts and it is managed by its admin (regular wallet at the moment, yet soon to be governance smart contract). diff --git a/dex/farm-with-top-up/src/base_functions.rs b/dex/farm-with-top-up/src/base_functions.rs index 7402eafb6..c5b66e1b0 100644 --- a/dex/farm-with-top-up/src/base_functions.rs +++ b/dex/farm-with-top-up/src/base_functions.rs @@ -7,7 +7,7 @@ multiversx_sc::derive_imports!(); use core::marker::PhantomData; use common_errors::ERROR_ZERO_AMOUNT; -use common_structs::FarmTokenAttributes; +use common_structs::{FarmTokenAttributes, Nonce}; use contexts::storage_cache::StorageCache; use farm_base_impl::base_traits_impl::{DefaultFarmWrapper, FarmContract}; @@ -16,7 +16,7 @@ pub type DoubleMultiPayment = MultiValue2, EsdtTokenPayme pub type ClaimRewardsResultType = DoubleMultiPayment; pub type ExitFarmResultType = DoubleMultiPayment; -pub const DEFAULT_FARM_POSITION_MIGRATION_NONCE: u64 = 1; +pub const DEFAULT_FARM_POSITION_MIGRATION_NONCE: Nonce = 1; pub struct ClaimRewardsResultWrapper { pub new_farm_token: EsdtTokenPayment, @@ -66,6 +66,7 @@ pub trait BaseFunctionsModule: + weekly_rewards_splitting::locked_token_buckets::WeeklyRewardsLockedTokenBucketsModule + weekly_rewards_splitting::update_claim_progress_energy::UpdateClaimProgressEnergyModule + energy_query::EnergyQueryModule + + crate::custom_rewards::CustomRewardsModule { fn enter_farm>( &self, @@ -190,7 +191,7 @@ pub trait BaseFunctionsModule: } fn claim_only_boosted_payment(&self, caller: &ManagedAddress) -> BigUint { - let reward = Wrapper::::calculate_boosted_rewards(self, caller); + let reward = FarmWithTopUpWrapper::::calculate_boosted_rewards(self, caller); if reward > 0 { self.reward_reserve().update(|reserve| *reserve -= &reward); } @@ -261,13 +262,29 @@ pub trait BaseFunctionsModule: } } -pub struct Wrapper { +pub struct FarmWithTopUpWrapper< + T: crate::custom_rewards::CustomRewardsModule + + rewards::RewardsModule + + config::ConfigModule + + farm_token::FarmTokenModule + + pausable::PausableModule + + permissions_module::PermissionsModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + farm_boosted_yields::FarmBoostedYieldsModule, +> { _phantom: PhantomData, } -impl Wrapper +impl FarmWithTopUpWrapper where - T: BaseFunctionsModule + farm_boosted_yields::FarmBoostedYieldsModule, + T: crate::custom_rewards::CustomRewardsModule + + rewards::RewardsModule + + config::ConfigModule + + farm_token::FarmTokenModule + + pausable::PausableModule + + permissions_module::PermissionsModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + farm_boosted_yields::FarmBoostedYieldsModule, { pub fn calculate_boosted_rewards( sc: &::FarmSc, @@ -279,28 +296,72 @@ where } } -impl FarmContract for Wrapper +impl FarmContract for FarmWithTopUpWrapper where - T: BaseFunctionsModule + farm_boosted_yields::FarmBoostedYieldsModule, + T: crate::custom_rewards::CustomRewardsModule + + rewards::RewardsModule + + config::ConfigModule + + farm_token::FarmTokenModule + + pausable::PausableModule + + permissions_module::PermissionsModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + farm_boosted_yields::FarmBoostedYieldsModule, { type FarmSc = T; type AttributesType = FarmTokenAttributes<::Api>; + #[inline] + fn mint_rewards( + _sc: &Self::FarmSc, + _token_id: &TokenIdentifier<::Api>, + _amount: &BigUint<::Api>, + ) { + } + + fn mint_per_block_rewards( + sc: &Self::FarmSc, + _token_id: &TokenIdentifier<::Api>, + ) -> BigUint<::Api> { + let current_block_nonce = sc.blockchain().get_block_nonce(); + let last_reward_nonce = sc.last_reward_block_nonce().get(); + if current_block_nonce <= last_reward_nonce { + return BigUint::zero(); + } + + let extra_rewards = + Self::calculate_per_block_rewards(sc, current_block_nonce, last_reward_nonce); + sc.last_reward_block_nonce().set(current_block_nonce); + + extra_rewards + } + fn generate_aggregated_rewards( sc: &Self::FarmSc, storage_cache: &mut StorageCache, ) { - let total_reward = Self::mint_per_block_rewards(sc, &storage_cache.reward_token_id); - if total_reward > 0u64 { - storage_cache.reward_reserve += &total_reward; - let split_rewards = sc.take_reward_slice(total_reward); - - if storage_cache.farm_token_supply != 0u64 { - let increase = (&split_rewards.base_farm * &storage_cache.division_safety_constant) - / &storage_cache.farm_token_supply; - storage_cache.reward_per_share += &increase; - } + let accumulated_rewards_mapper = sc.accumulated_rewards(); + let mut accumulated_rewards = accumulated_rewards_mapper.get(); + let reward_capacity = sc.reward_capacity().get(); + let remaining_rewards = &reward_capacity - &accumulated_rewards; + + let mut total_reward = Self::mint_per_block_rewards(sc, &storage_cache.reward_token_id); + total_reward = core::cmp::min(total_reward, remaining_rewards); + if total_reward == 0 { + return; + } + + storage_cache.reward_reserve += &total_reward; + accumulated_rewards += &total_reward; + accumulated_rewards_mapper.set(&accumulated_rewards); + + let split_rewards = sc.take_reward_slice(total_reward); + if storage_cache.farm_token_supply == 0 { + return; } + + let increase = (&split_rewards.base_farm * &storage_cache.division_safety_constant) + / &storage_cache.farm_token_supply; + storage_cache.reward_per_share += &increase; } fn calculate_rewards( diff --git a/dex/farm-with-top-up/src/custom_rewards.rs b/dex/farm-with-top-up/src/custom_rewards.rs new file mode 100644 index 000000000..39e1c688f --- /dev/null +++ b/dex/farm-with-top-up/src/custom_rewards.rs @@ -0,0 +1,144 @@ +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +use common_structs::{Epoch, Percent}; +use contexts::storage_cache::StorageCache; +use farm_base_impl::base_traits_impl::FarmContract; + +use crate::base_functions::FarmWithTopUpWrapper; + +// TODO: Will need to be changed when block duration changes +pub const BLOCKS_IN_YEAR: u64 = 31_536_000 / 6; // seconds_in_year / 6_seconds_per_block + +pub const MAX_PERCENT: Percent = 10_000; +pub const MAX_MIN_UNBOND_EPOCHS: Epoch = 30; +pub static WITHDRAW_AMOUNT_TOO_HIGH: &[u8] = + b"Withdraw amount is higher than the remaining uncollected rewards"; + +#[multiversx_sc::module] +pub trait CustomRewardsModule: + rewards::RewardsModule + + config::ConfigModule + + farm_token::FarmTokenModule + + pausable::PausableModule + + permissions_module::PermissionsModule + + events::EventsModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + farm_base_impl::base_farm_init::BaseFarmInitModule + + farm_base_impl::base_farm_validation::BaseFarmValidationModule + + farm_base_impl::enter_farm::BaseEnterFarmModule + + farm_base_impl::claim_rewards::BaseClaimRewardsModule + + farm_base_impl::compound_rewards::BaseCompoundRewardsModule + + farm_base_impl::exit_farm::BaseExitFarmModule + + utils::UtilsModule + + farm_boosted_yields::FarmBoostedYieldsModule + + farm_boosted_yields::boosted_yields_factors::BoostedYieldsFactorsModule + + farm_boosted_yields::custom_reward_logic::CustomRewardLogicModule + + week_timekeeping::WeekTimekeepingModule + + weekly_rewards_splitting::WeeklyRewardsSplittingModule + + weekly_rewards_splitting::events::WeeklyRewardsSplittingEventsModule + + weekly_rewards_splitting::global_info::WeeklyRewardsGlobalInfo + + weekly_rewards_splitting::locked_token_buckets::WeeklyRewardsLockedTokenBucketsModule + + weekly_rewards_splitting::update_claim_progress_energy::UpdateClaimProgressEnergyModule + + energy_query::EnergyQueryModule // + crate::base_functions::BaseFunctionsModule +{ + #[payable("*")] + #[endpoint(topUpRewards)] + fn top_up_rewards(&self) { + self.require_caller_has_admin_permissions(); + + let (payment_token, payment_amount) = self.call_value().single_fungible_esdt(); + let reward_token_id = self.reward_token_id().get(); + require!(payment_token == reward_token_id, "Invalid token"); + + self.reward_capacity().update(|r| *r += payment_amount); + + self.update_start_of_epoch_timestamp(); + } + + #[endpoint(withdrawRewards)] + fn withdraw_rewards(&self, withdraw_amount: BigUint) { + self.require_caller_has_admin_permissions(); + + let mut storage_cache = StorageCache::new(self); + FarmWithTopUpWrapper::::generate_aggregated_rewards(self, &mut storage_cache); + + let reward_capactiy = self.reward_capacity().get(); + let accumulated_rewards = self.accumulated_rewards().get(); + let remaining_rewards = &reward_capactiy - &accumulated_rewards; + require!( + withdraw_amount <= remaining_rewards, + WITHDRAW_AMOUNT_TOO_HIGH + ); + require!( + reward_capactiy >= withdraw_amount, + "Not enough rewards to withdraw" + ); + + let new_capacity = &reward_capactiy - &withdraw_amount; + self.reward_capacity().set(new_capacity); + + let caller = self.blockchain().get_caller(); + let reward_token_id = self.reward_token_id().get(); + self.send().direct_non_zero( + &caller, + &EgldOrEsdtTokenIdentifier::esdt(reward_token_id), + 0, + &withdraw_amount, + ); + + self.update_start_of_epoch_timestamp(); + } + + #[endpoint(startProduceRewards)] + fn start_produce_rewards_endpoint(&self) { + self.require_caller_has_admin_permissions(); + self.start_produce_rewards(); + + self.update_start_of_epoch_timestamp(); + } + + #[endpoint(endProduceRewards)] + fn end_produce_rewards(&self) { + self.require_caller_has_admin_permissions(); + + let mut storage_cache = StorageCache::new(self); + FarmWithTopUpWrapper::::generate_aggregated_rewards(self, &mut storage_cache); + self.produce_rewards_enabled().set(false); + + self.update_start_of_epoch_timestamp(); + } + + #[endpoint(setPerBlockRewardAmount)] + fn set_per_block_rewards(&self, per_block_amount: BigUint) { + self.require_caller_has_admin_permissions(); + require!(per_block_amount != 0, "Amount cannot be zero"); + + let mut storage_cache = StorageCache::new(self); + FarmWithTopUpWrapper::::generate_aggregated_rewards(self, &mut storage_cache); + self.per_block_reward_amount().set(&per_block_amount); + + self.update_start_of_epoch_timestamp(); + } + + #[endpoint(setBoostedYieldsRewardsPercentage)] + fn set_boosted_yields_rewards_percentage(&self, percentage: Percent) { + self.require_caller_has_admin_permissions(); + require!(percentage <= MAX_PERCENT, "Invalid percentage"); + + let mut storage_cache = StorageCache::new(self); + FarmWithTopUpWrapper::::generate_aggregated_rewards(self, &mut storage_cache); + + self.boosted_yields_rewards_percentage().set(percentage); + + self.update_start_of_epoch_timestamp(); + } + + #[view(getAccumulatedRewards)] + #[storage_mapper("accumulatedRewards")] + fn accumulated_rewards(&self) -> SingleValueMapper; + + #[view(getRewardCapacity)] + #[storage_mapper("reward_capacity")] + fn reward_capacity(&self) -> SingleValueMapper; +} diff --git a/dex/farm-with-top-up/src/external_interaction.rs b/dex/farm-with-top-up/src/external_interaction.rs index db3275e36..5b422b57f 100644 --- a/dex/farm-with-top-up/src/external_interaction.rs +++ b/dex/farm-with-top-up/src/external_interaction.rs @@ -3,7 +3,7 @@ multiversx_sc::imports!(); use common_structs::FarmTokenAttributes; use crate::{ - base_functions::{self, ClaimRewardsResultType, Wrapper}, + base_functions::{self, ClaimRewardsResultType, FarmWithTopUpWrapper}, EnterFarmResultType, }; @@ -37,6 +37,7 @@ pub trait ExternalInteractionsModule: + weekly_rewards_splitting::update_claim_progress_energy::UpdateClaimProgressEnergyModule + energy_query::EnergyQueryModule + utils::UtilsModule + + crate::custom_rewards::CustomRewardsModule { #[payable("*")] #[endpoint(enterFarmOnBehalf)] @@ -57,7 +58,7 @@ pub trait ExternalInteractionsModule: let boosted_rewards_payment = EsdtTokenPayment::new(self.reward_token_id().get(), 0, boosted_rewards); - let new_farm_token = self.enter_farm::>(user.clone()); + let new_farm_token = self.enter_farm::>(user.clone()); self.send() .direct_non_zero_esdt_payment(&caller, &new_farm_token); self.send() @@ -81,7 +82,7 @@ pub trait ExternalInteractionsModule: ); self.require_user_whitelisted(&user, &caller); - let claim_rewards_result = self.claim_rewards::>(user.clone()); + let claim_rewards_result = self.claim_rewards::>(user.clone()); self.send() .direct_non_zero_esdt_payment(&caller, &claim_rewards_result.new_farm_token); diff --git a/dex/farm-with-top-up/src/lib.rs b/dex/farm-with-top-up/src/lib.rs index 10c0179e7..4860053ca 100644 --- a/dex/farm-with-top-up/src/lib.rs +++ b/dex/farm-with-top-up/src/lib.rs @@ -4,10 +4,11 @@ multiversx_sc::imports!(); multiversx_sc::derive_imports!(); pub mod base_functions; +pub mod custom_rewards; pub mod external_interaction; -use base_functions::{ClaimRewardsResultType, DoubleMultiPayment, Wrapper}; -use common_structs::FarmTokenAttributes; +use base_functions::{ClaimRewardsResultType, DoubleMultiPayment, FarmWithTopUpWrapper}; +use common_structs::{FarmTokenAttributes, Percent}; use contexts::storage_cache::StorageCache; use farm_base_impl::base_traits_impl::FarmContract; @@ -16,10 +17,10 @@ use fixed_supply_token::FixedSupplyToken; pub type EnterFarmResultType = DoubleMultiPayment; pub type ExitFarmWithPartialPosResultType = DoubleMultiPayment; -pub const MAX_PERCENT: u64 = 10_000; +pub const MAX_PERCENT: Percent = 10_000; #[multiversx_sc::contract] -pub trait Farm: +pub trait FarmWithTopUp: rewards::RewardsModule + config::ConfigModule + farm_token::FarmTokenModule @@ -49,6 +50,7 @@ pub trait Farm: + weekly_rewards_splitting::update_claim_progress_energy::UpdateClaimProgressEnergyModule + energy_query::EnergyQueryModule + utils::UtilsModule + + custom_rewards::CustomRewardsModule { #[init] fn init( @@ -103,7 +105,7 @@ pub trait Farm: let boosted_rewards_payment = EsdtTokenPayment::new(self.reward_token_id().get(), 0, boosted_rewards); - let new_farm_token = self.enter_farm::>(orig_caller.clone()); + let new_farm_token = self.enter_farm::>(orig_caller.clone()); self.send() .direct_non_zero_esdt_payment(&caller, &new_farm_token); self.send() @@ -126,7 +128,7 @@ pub trait Farm: self.migrate_old_farm_positions(&orig_caller); - let claim_rewards_result = self.claim_rewards::>(orig_caller); + let claim_rewards_result = self.claim_rewards::>(orig_caller); self.send() .direct_non_zero_esdt_payment(&caller, &claim_rewards_result.new_farm_token); self.send() @@ -148,7 +150,8 @@ pub trait Farm: self.migrate_old_farm_positions(&orig_caller); - let output_farm_token_payment = self.compound_rewards::>(orig_caller.clone()); + let output_farm_token_payment = + self.compound_rewards::>(orig_caller.clone()); self.send() .direct_non_zero_esdt_payment(&caller, &output_farm_token_payment); self.update_energy_and_progress(&orig_caller); @@ -168,7 +171,8 @@ pub trait Farm: let orig_caller = self.get_orig_caller_from_opt(&caller, opt_orig_caller); let payment = self.call_value().single_esdt(); let migrated_amount = self.migrate_old_farm_positions(&orig_caller); - let exit_farm_result = self.exit_farm::>(orig_caller.clone(), payment); + let exit_farm_result = + self.exit_farm::>(orig_caller.clone(), payment); self.decrease_old_farm_positions(migrated_amount, &orig_caller); self.send() @@ -209,7 +213,8 @@ pub trait Farm: } fn merge_and_update_farm_tokens(&self, orig_caller: ManagedAddress) -> EsdtTokenPayment { - let mut output_attributes = self.merge_and_return_attributes::>(&orig_caller); + let mut output_attributes = + self.merge_and_return_attributes::>(&orig_caller); output_attributes.original_owner = orig_caller; let new_token_amount = output_attributes.get_total_supply(); @@ -241,7 +246,7 @@ pub trait Farm: let mut storage_cache = StorageCache::new(self); self.validate_contract_state(storage_cache.contract_state, &storage_cache.farm_token_id); - Wrapper::::generate_aggregated_rewards(self, &mut storage_cache); + FarmWithTopUpWrapper::::generate_aggregated_rewards(self, &mut storage_cache); let boosted_rewards = self.claim_only_boosted_payment(user); let boosted_rewards_payment = @@ -257,43 +262,6 @@ pub trait Farm: boosted_rewards_payment } - #[endpoint(startProduceRewards)] - fn start_produce_rewards_endpoint(&self) { - self.require_caller_has_admin_permissions(); - self.start_produce_rewards(); - - self.update_start_of_epoch_timestamp(); - } - - #[endpoint(endProduceRewards)] - fn end_produce_rewards_endpoint(&self) { - self.require_caller_has_admin_permissions(); - self.end_produce_rewards::>(); - - self.update_start_of_epoch_timestamp(); - } - - #[endpoint(setPerBlockRewardAmount)] - fn set_per_block_rewards_endpoint(&self, per_block_amount: BigUint) { - self.require_caller_has_admin_permissions(); - self.set_per_block_rewards::>(per_block_amount); - - self.update_start_of_epoch_timestamp(); - } - - #[endpoint(setBoostedYieldsRewardsPercentage)] - fn set_boosted_yields_rewards_percentage(&self, percentage: u64) { - self.require_caller_has_admin_permissions(); - require!(percentage <= MAX_PERCENT, "Invalid percentage"); - - let mut storage_cache = StorageCache::new(self); - Wrapper::::generate_aggregated_rewards(self, &mut storage_cache); - - self.boosted_yields_rewards_percentage().set(percentage); - - self.update_start_of_epoch_timestamp(); - } - #[view(calculateRewardsForGivenPosition)] fn calculate_rewards_for_given_position( &self, @@ -304,9 +272,9 @@ pub trait Farm: self.require_queried(); let mut storage_cache = StorageCache::new(self); - Wrapper::::generate_aggregated_rewards(self, &mut storage_cache); + FarmWithTopUpWrapper::::generate_aggregated_rewards(self, &mut storage_cache); - Wrapper::::calculate_rewards( + FarmWithTopUpWrapper::::calculate_rewards( self, &user, &farm_token_amount, diff --git a/dex/farm-with-top-up/wasm/src/lib.rs b/dex/farm-with-top-up/wasm/src/lib.rs index 22d108f14..deb1d351a 100644 --- a/dex/farm-with-top-up/wasm/src/lib.rs +++ b/dex/farm-with-top-up/wasm/src/lib.rs @@ -6,9 +6,9 @@ // Init: 1 // Upgrade: 1 -// Endpoints: 60 +// Endpoints: 64 // Async Callback: 1 -// Total number of exported functions: 63 +// Total number of exported functions: 67 #![no_std] @@ -26,10 +26,6 @@ multiversx_sc_wasm_adapter::endpoints! { exitFarm => exit_farm_endpoint mergeFarmTokens => merge_farm_tokens_endpoint claimBoostedRewards => claim_boosted_rewards - startProduceRewards => start_produce_rewards_endpoint - endProduceRewards => end_produce_rewards_endpoint - setPerBlockRewardAmount => set_per_block_rewards_endpoint - setBoostedYieldsRewardsPercentage => set_boosted_yields_rewards_percentage calculateRewardsForGivenPosition => calculate_rewards_for_given_position getRewardPerShare => reward_per_share getRewardReserve => reward_reserve @@ -80,6 +76,14 @@ multiversx_sc_wasm_adapter::endpoints! { getCurrentClaimProgress => current_claim_progress setEnergyFactoryAddress => set_energy_factory_address getEnergyFactoryAddress => energy_factory_address + topUpRewards => top_up_rewards + withdrawRewards => withdraw_rewards + startProduceRewards => start_produce_rewards_endpoint + endProduceRewards => end_produce_rewards + setPerBlockRewardAmount => set_per_block_rewards + setBoostedYieldsRewardsPercentage => set_boosted_yields_rewards_percentage + getAccumulatedRewards => accumulated_rewards + getRewardCapacity => reward_capacity ) } diff --git a/dex/proxy-deployer/src/set_contract_active.rs b/dex/proxy-deployer/src/set_contract_active.rs index 43095107b..9f819a6a3 100644 --- a/dex/proxy-deployer/src/set_contract_active.rs +++ b/dex/proxy-deployer/src/set_contract_active.rs @@ -7,7 +7,9 @@ use pausable::ProxyTrait as _; multiversx_sc::imports!(); #[multiversx_sc::module] -pub trait SetContractActiveModule: crate::storage::StorageModule { +pub trait SetContractActiveModule: + crate::storage::StorageModule + crate::remove_contracts::RemoveContractsModule +{ /// Boosted yields percent must be >= 0 (0%) and <= 10_000 (100%) /// /// Only callable by contract deployer @@ -37,7 +39,11 @@ pub trait SetContractActiveModule: crate::storage::StorageModule { self.set_rewards_per_block(contract.clone(), rewards_per_block); self.set_boosted_yields_factors(contract.clone(), boosted_yields_factors); self.set_boosted_yields_percent(contract.clone(), boosted_yields_percent); - self.unpause_contract(contract); + self.unpause_contract(contract.clone()); + + // Contract was only added as admin so we don't have to change all the permissions around + let own_sc_address = self.blockchain().get_sc_address(); + self.remove_admin(contract, own_sc_address); } fn set_rewards_per_block(&self, contract: ManagedAddress, rewards_per_block: BigUint) { diff --git a/farm-staking/farm-staking/src/base_impl_wrapper.rs b/farm-staking/farm-staking/src/base_impl_wrapper.rs index c2384ab68..41936099e 100644 --- a/farm-staking/farm-staking/src/base_impl_wrapper.rs +++ b/farm-staking/farm-staking/src/base_impl_wrapper.rs @@ -74,6 +74,7 @@ where type FarmSc = T; type AttributesType = StakingFarmTokenAttributes<::Api>; + #[inline] fn mint_rewards( _sc: &Self::FarmSc, _token_id: &TokenIdentifier<::Api>, From 499f81c0e4ac31866159d15ea4a20e18105f8c63 Mon Sep 17 00:00:00 2001 From: Dorin Marian Iancu Date: Wed, 4 Dec 2024 09:43:13 +0200 Subject: [PATCH 2/5] test setup --- .../tests/farm_with_top_up_setup/mod.rs | 160 ++++++++++++++++++ .../tests/farm_with_top_up_test.rs | 12 ++ 2 files changed, 172 insertions(+) create mode 100644 dex/farm-with-top-up/tests/farm_with_top_up_setup/mod.rs create mode 100644 dex/farm-with-top-up/tests/farm_with_top_up_test.rs diff --git a/dex/farm-with-top-up/tests/farm_with_top_up_setup/mod.rs b/dex/farm-with-top-up/tests/farm_with_top_up_setup/mod.rs new file mode 100644 index 000000000..519fbf9a5 --- /dev/null +++ b/dex/farm-with-top-up/tests/farm_with_top_up_setup/mod.rs @@ -0,0 +1,160 @@ +use common_structs::Timestamp; +use config::ConfigModule; +use farm_token::FarmTokenModule; +use farm_with_top_up::FarmWithTopUp; +use multiversx_sc::{ + imports::StorageTokenWrapper, + types::{Address, EsdtLocalRole, MultiValueEncoded}, +}; +use multiversx_sc_scenario::{ + imports::{BlockchainStateWrapper, ContractObjWrapper}, + managed_address, managed_biguint, managed_token_id, rust_biguint, DebugApi, +}; +use pausable::{PausableModule, State}; +use permissions_hub::PermissionsHub; +use permissions_hub_module::PermissionsHubModule; +use timestamp_oracle::{epoch_to_timestamp::EpochToTimestampModule, TimestampOracle}; + +pub static REWARD_TOKEN_ID: &[u8] = b"REW-123456"; +pub static FARMING_TOKEN_ID: &[u8] = b"LPTOK-123456"; +pub static FARM_TOKEN_ID: &[u8] = b"FARM-123456"; +pub const DIV_SAFETY: u64 = 1_000_000_000_000; +pub const PER_BLOCK_REWARD_AMOUNT: u64 = 1_000; +pub const FARMING_TOKEN_BALANCE: u64 = 200_000_000; +pub const TIMESTAMP_PER_EPOCH: Timestamp = 24 * 60 * 60; + +pub struct FarmWithTopUpSetup +where + FarmObjBuilder: 'static + Copy + Fn() -> farm_with_top_up::ContractObj, + TimestampOracleObjBuilder: 'static + Copy + Fn() -> timestamp_oracle::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, +{ + pub b_mock: BlockchainStateWrapper, + pub owner: Address, + pub user: Address, + pub farm_wrapper: ContractObjWrapper, FarmObjBuilder>, + pub timestamp_oracle_wrapper: + ContractObjWrapper, TimestampOracleObjBuilder>, + pub permissions_hub_wrapper: + ContractObjWrapper, PermissionsHubObjBuilder>, +} + +impl + FarmWithTopUpSetup +where + FarmObjBuilder: 'static + Copy + Fn() -> farm_with_top_up::ContractObj, + TimestampOracleObjBuilder: 'static + Copy + Fn() -> timestamp_oracle::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, +{ + pub fn new( + farm_builder: FarmObjBuilder, + timestamp_oracle_builder: TimestampOracleObjBuilder, + permissions_hub_builder: PermissionsHubObjBuilder, + ) -> Self { + let rust_zero = rust_biguint!(0); + let mut b_mock = BlockchainStateWrapper::new(); + let owner = b_mock.create_user_account(&rust_zero); + let user = b_mock.create_user_account(&rust_zero); + let farm_wrapper = + b_mock.create_sc_account(&rust_zero, Some(&owner), farm_builder, "farm.wasm"); + + let timestamp_oracle_wrapper = b_mock.create_sc_account( + &rust_zero, + Some(&owner), + timestamp_oracle_builder, + "timestamp oracle", + ); + b_mock + .execute_tx(&owner, ×tamp_oracle_wrapper, &rust_zero, |sc| { + sc.init(0); + + for i in 0..=100 { + sc.set_start_timestamp_for_epoch(i, i * TIMESTAMP_PER_EPOCH + 1); + } + }) + .assert_ok(); + + let permissions_hub_wrapper = b_mock.create_sc_account( + &rust_zero, + Some(&owner), + permissions_hub_builder, + "permissions_hub.wasm", + ); + + b_mock + .execute_tx(&owner, &permissions_hub_wrapper, &rust_zero, |sc| { + sc.init(); + }) + .assert_ok(); + + b_mock + .execute_tx(&owner, &farm_wrapper, &rust_zero, |sc| { + let reward_token_id = managed_token_id!(REWARD_TOKEN_ID); + let farming_token_id = managed_token_id!(FARMING_TOKEN_ID); + let division_safety_constant = managed_biguint!(DIV_SAFETY); + + sc.init( + reward_token_id, + farming_token_id, + division_safety_constant, + managed_address!(&owner), + managed_address!(timestamp_oracle_wrapper.address_ref()), + MultiValueEncoded::new(), + ); + + let farm_token_id = managed_token_id!(FARM_TOKEN_ID); + sc.farm_token().set_token_id(farm_token_id); + + sc.per_block_reward_amount() + .set(&managed_biguint!(PER_BLOCK_REWARD_AMOUNT)); + + sc.state().set(State::Active); + sc.produce_rewards_enabled().set(true); + + sc.set_permissions_hub_address(managed_address!( + permissions_hub_wrapper.address_ref() + )); + }) + .assert_ok(); + + let farm_token_roles = [ + EsdtLocalRole::NftCreate, + EsdtLocalRole::NftAddQuantity, + EsdtLocalRole::NftBurn, + ]; + b_mock.set_esdt_local_roles( + farm_wrapper.address_ref(), + FARM_TOKEN_ID, + &farm_token_roles[..], + ); + + let farming_token_roles = [EsdtLocalRole::Burn]; + b_mock.set_esdt_local_roles( + farm_wrapper.address_ref(), + FARMING_TOKEN_ID, + &farming_token_roles[..], + ); + + let reward_token_roles = [EsdtLocalRole::Mint]; + b_mock.set_esdt_local_roles( + farm_wrapper.address_ref(), + REWARD_TOKEN_ID, + &reward_token_roles[..], + ); + + b_mock.set_esdt_balance( + &user, + FARMING_TOKEN_ID, + &rust_biguint!(FARMING_TOKEN_BALANCE), + ); + + FarmWithTopUpSetup { + b_mock, + owner, + user, + farm_wrapper, + timestamp_oracle_wrapper, + permissions_hub_wrapper, + } + } +} diff --git a/dex/farm-with-top-up/tests/farm_with_top_up_test.rs b/dex/farm-with-top-up/tests/farm_with_top_up_test.rs new file mode 100644 index 000000000..69783d418 --- /dev/null +++ b/dex/farm-with-top-up/tests/farm_with_top_up_test.rs @@ -0,0 +1,12 @@ +use farm_with_top_up_setup::FarmWithTopUpSetup; + +pub mod farm_with_top_up_setup; + +#[test] +fn setup_farm_with_top_up_test() { + let _ = FarmWithTopUpSetup::new( + farm_with_top_up::contract_obj, + timestamp_oracle::contract_obj, + permissions_hub::contract_obj, + ); +} From 919a1a1e84e73e687e2875428dfb59c00cc4fe50 Mon Sep 17 00:00:00 2001 From: Dorin Marian Iancu Date: Wed, 4 Dec 2024 10:31:24 +0200 Subject: [PATCH 3/5] test --- .../tests/farm_with_top_up_setup/mod.rs | 67 +++++++++++++++++-- .../tests/farm_with_top_up_test.rs | 49 +++++++++++++- 2 files changed, 111 insertions(+), 5 deletions(-) diff --git a/dex/farm-with-top-up/tests/farm_with_top_up_setup/mod.rs b/dex/farm-with-top-up/tests/farm_with_top_up_setup/mod.rs index 519fbf9a5..5ba0a003a 100644 --- a/dex/farm-with-top-up/tests/farm_with_top_up_setup/mod.rs +++ b/dex/farm-with-top-up/tests/farm_with_top_up_setup/mod.rs @@ -1,9 +1,9 @@ -use common_structs::Timestamp; +use common_structs::{Nonce, Timestamp}; use config::ConfigModule; use farm_token::FarmTokenModule; -use farm_with_top_up::FarmWithTopUp; +use farm_with_top_up::{custom_rewards::CustomRewardsModule, FarmWithTopUp}; use multiversx_sc::{ - imports::StorageTokenWrapper, + imports::{OptionalValue, StorageTokenWrapper}, types::{Address, EsdtLocalRole, MultiValueEncoded}, }; use multiversx_sc_scenario::{ @@ -21,6 +21,7 @@ pub static FARM_TOKEN_ID: &[u8] = b"FARM-123456"; pub const DIV_SAFETY: u64 = 1_000_000_000_000; pub const PER_BLOCK_REWARD_AMOUNT: u64 = 1_000; pub const FARMING_TOKEN_BALANCE: u64 = 200_000_000; +pub const REWARD_TOKEN_BALANCE: u64 = 500_000_000; pub const TIMESTAMP_PER_EPOCH: Timestamp = 24 * 60 * 60; pub struct FarmWithTopUpSetup @@ -93,13 +94,15 @@ where let farming_token_id = managed_token_id!(FARMING_TOKEN_ID); let division_safety_constant = managed_biguint!(DIV_SAFETY); + let mut admins = MultiValueEncoded::new(); + admins.push(managed_address!(&owner)); sc.init( reward_token_id, farming_token_id, division_safety_constant, managed_address!(&owner), managed_address!(timestamp_oracle_wrapper.address_ref()), - MultiValueEncoded::new(), + admins, ); let farm_token_id = managed_token_id!(FARM_TOKEN_ID); @@ -148,6 +151,12 @@ where &rust_biguint!(FARMING_TOKEN_BALANCE), ); + b_mock.set_esdt_balance( + &owner, + REWARD_TOKEN_ID, + &rust_biguint!(REWARD_TOKEN_BALANCE), + ); + FarmWithTopUpSetup { b_mock, owner, @@ -157,4 +166,54 @@ where permissions_hub_wrapper, } } + + pub fn admin_deposit_rewards(&mut self, rew_amount: u64) { + self.b_mock + .execute_esdt_transfer( + &self.owner, + &self.farm_wrapper, + REWARD_TOKEN_ID, + 0, + &rust_biguint!(rew_amount), + |sc| { + sc.top_up_rewards(); + }, + ) + .assert_ok(); + } + + pub fn user_enter_farm(&mut self, farming_amount: u64) { + self.b_mock + .execute_esdt_transfer( + &self.user, + &self.farm_wrapper, + FARMING_TOKEN_ID, + 0, + &rust_biguint!(farming_amount), + |sc| { + sc.enter_farm_endpoint(OptionalValue::None); + }, + ) + .assert_ok(); + } + + pub fn user_claim_rewards(&mut self, farm_token_nonce: Nonce, farm_token_amount: u64) -> Nonce { + let mut farm_token_nonce_out = 0; + self.b_mock + .execute_esdt_transfer( + &self.user, + &self.farm_wrapper, + FARM_TOKEN_ID, + farm_token_nonce, + &rust_biguint!(farm_token_amount), + |sc| { + let (new_farm_token, _rewards) = + sc.claim_rewards_endpoint(OptionalValue::None).into_tuple(); + farm_token_nonce_out = new_farm_token.token_nonce; + }, + ) + .assert_ok(); + + farm_token_nonce_out + } } diff --git a/dex/farm-with-top-up/tests/farm_with_top_up_test.rs b/dex/farm-with-top-up/tests/farm_with_top_up_test.rs index 69783d418..18922c118 100644 --- a/dex/farm-with-top-up/tests/farm_with_top_up_test.rs +++ b/dex/farm-with-top-up/tests/farm_with_top_up_test.rs @@ -1,4 +1,5 @@ -use farm_with_top_up_setup::FarmWithTopUpSetup; +use farm_with_top_up_setup::{FarmWithTopUpSetup, REWARD_TOKEN_ID}; +use multiversx_sc_scenario::rust_biguint; pub mod farm_with_top_up_setup; @@ -10,3 +11,49 @@ fn setup_farm_with_top_up_test() { permissions_hub::contract_obj, ); } + +#[test] +fn admin_deposit_user_claim_test() { + let mut setup = FarmWithTopUpSetup::new( + farm_with_top_up::contract_obj, + timestamp_oracle::contract_obj, + permissions_hub::contract_obj, + ); + + let farm_token_amount = 100_000; + let mut farm_token_nonce = 1; + setup.user_enter_farm(farm_token_amount); + + // 5 blocks pass + setup.b_mock.set_block_nonce(5); + + // user received no rewards, as none were deposited yet + farm_token_nonce = setup.user_claim_rewards(farm_token_nonce, farm_token_amount); + setup + .b_mock + .check_esdt_balance(&setup.user, REWARD_TOKEN_ID, &rust_biguint!(0)); + + // admin deposit rewards + setup.admin_deposit_rewards(4_000); + + // another 5 blocks pass + setup.b_mock.set_block_nonce(10); + + // user claim rewards - received only 4_000 out of 5_000, since farm ran out of rewards + farm_token_nonce = setup.user_claim_rewards(farm_token_nonce, farm_token_amount); + setup + .b_mock + .check_esdt_balance(&setup.user, REWARD_TOKEN_ID, &rust_biguint!(4_000)); + + // admin deposit more rewards + setup.admin_deposit_rewards(50_000); + + // another 5 blocks pass + setup.b_mock.set_block_nonce(15); + + // user claim rewards - received the full 5_000 amount + _ = setup.user_claim_rewards(farm_token_nonce, farm_token_amount); + setup + .b_mock + .check_esdt_balance(&setup.user, REWARD_TOKEN_ID, &rust_biguint!(4_000 + 5_000)); +} From 8cadec49bf50cc1605cceaef819f80d9966164a1 Mon Sep 17 00:00:00 2001 From: Dorin Marian Iancu Date: Wed, 4 Dec 2024 10:56:25 +0200 Subject: [PATCH 4/5] tests --- Cargo.lock | 1 + dex/proxy-deployer/Cargo.toml | 3 + dex/proxy-deployer/src/deploy.rs | 111 +++++++++++--- dex/proxy-deployer/src/set_contract_active.rs | 1 + .../mod.rs | 127 ++++++++++++++++ .../proxy_deployer_farm_with_top_up_tests.rs | 143 ++++++++++++++++++ dex/proxy-deployer/wasm/Cargo.lock | 31 ++++ dex/proxy-deployer/wasm/src/lib.rs | 5 +- 8 files changed, 400 insertions(+), 22 deletions(-) create mode 100644 dex/proxy-deployer/tests/proxy_deployer_farm_with_top_up_setup/mod.rs create mode 100644 dex/proxy-deployer/tests/proxy_deployer_farm_with_top_up_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 5b82e337a..ee9eec823 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1624,6 +1624,7 @@ dependencies = [ "common_structs", "farm-boosted-yields", "farm-staking", + "farm-with-top-up", "farm_token", "multiversx-sc", "multiversx-sc-scenario", diff --git a/dex/proxy-deployer/Cargo.toml b/dex/proxy-deployer/Cargo.toml index 0a6fa03f0..d045790f3 100644 --- a/dex/proxy-deployer/Cargo.toml +++ b/dex/proxy-deployer/Cargo.toml @@ -15,6 +15,9 @@ features = ["esdt-token-payment-legacy-decode"] [dependencies.farm-staking] path = "../../farm-staking/farm-staking" +[dependencies.farm-with-top-up] +path = "../farm-with-top-up" + [dependencies.common_structs] path = "../../common/common_structs" diff --git a/dex/proxy-deployer/src/deploy.rs b/dex/proxy-deployer/src/deploy.rs index 9e03ee986..6a5681edf 100644 --- a/dex/proxy-deployer/src/deploy.rs +++ b/dex/proxy-deployer/src/deploy.rs @@ -7,6 +7,14 @@ multiversx_sc::derive_imports!(); const DIVISION_SAFETY_CONST: u64 = 1_000_000_000_000_000_000; +pub struct CommonDeployVariables { + pub owner: ManagedAddress, + pub admins: MultiValueEncoded>, + pub template: ManagedAddress, + pub code_metadata: CodeMetadata, + pub timestamp_oracle: ManagedAddress, +} + #[multiversx_sc::module] pub trait DeployModule: crate::storage::StorageModule { #[endpoint(deployFarmStakingContract)] @@ -17,10 +25,7 @@ pub trait DeployModule: crate::storage::StorageModule { min_unbond_epochs: Epoch, ) -> ManagedAddress { self.require_correct_deployer_type(DeployerType::FarmStaking); - require!( - !self.all_used_tokens().contains(&farming_token_id), - "Token already used" - ); + self.require_token_not_used(&farming_token_id); let caller = self.get_caller_not_blacklisted(); let deployed_sc_address = self.deploy_farm_staking_from_source( @@ -34,6 +39,33 @@ pub trait DeployModule: crate::storage::StorageModule { deployed_sc_address } + #[endpoint(deployFarmWithTopUp)] + fn deploy_farm_with_top_up( + &self, + farming_token_id: TokenIdentifier, + reward_token_id: TokenIdentifier, + ) -> ManagedAddress { + self.require_correct_deployer_type(DeployerType::FarmWithTopUp); + self.require_token_not_used(&farming_token_id); + + let caller = self.get_caller_not_blacklisted(); + let deployed_sc_address = self.deploy_farm_with_top_up_from_source( + caller.clone(), + farming_token_id.clone(), + reward_token_id, + ); + self.add_new_contract(&caller, &deployed_sc_address, farming_token_id); + + deployed_sc_address + } + + fn require_token_not_used(&self, token_id: &TokenIdentifier) { + require!( + !self.all_used_tokens().contains(token_id), + "Token already used" + ); + } + fn get_caller_not_blacklisted(&self) -> ManagedAddress { let caller = self.blockchain().get_caller(); let caller_id = self.address_id().get_id_or_insert(&caller); @@ -52,18 +84,7 @@ pub trait DeployModule: crate::storage::StorageModule { max_apr: BigUint, min_unbond_epochs: Epoch, ) -> ManagedAddress { - let owner = self.blockchain().get_owner_address(); - - let own_sc_address = self.blockchain().get_sc_address(); - let mut admins = MultiValueEncoded::new(); - admins.push(caller); - admins.push(own_sc_address); - - let template = self.template_address().get(); - let code_metadata = - CodeMetadata::PAYABLE_BY_SC | CodeMetadata::READABLE | CodeMetadata::UPGRADEABLE; - let timestamp_oracle_address = self.timestamp_oracle_address().get(); - + let deploy_variables = self.get_common_deploy_variables(caller); let (deployed_sc_address, ()) = self .farm_staking_deploy_proxy() .init( @@ -71,15 +92,62 @@ pub trait DeployModule: crate::storage::StorageModule { DIVISION_SAFETY_CONST, max_apr, min_unbond_epochs, - owner, - timestamp_oracle_address, - admins, + deploy_variables.owner, + deploy_variables.timestamp_oracle, + deploy_variables.admins, + ) + .deploy_from_source(&deploy_variables.template, deploy_variables.code_metadata); + + deployed_sc_address + } + + fn deploy_farm_with_top_up_from_source( + &self, + caller: ManagedAddress, + farming_token_id: TokenIdentifier, + reward_token_id: TokenIdentifier, + ) -> ManagedAddress { + let deploy_variables = self.get_common_deploy_variables(caller); + let (deployed_sc_address, ()) = self + .farm_with_top_up_deploy_proxy() + .init( + reward_token_id, + farming_token_id, + DIVISION_SAFETY_CONST, + deploy_variables.owner, + deploy_variables.timestamp_oracle, + deploy_variables.admins, ) - .deploy_from_source(&template, code_metadata); + .deploy_from_source(&deploy_variables.template, deploy_variables.code_metadata); deployed_sc_address } + fn get_common_deploy_variables( + &self, + caller: ManagedAddress, + ) -> CommonDeployVariables { + let owner = self.blockchain().get_owner_address(); + + let own_sc_address = self.blockchain().get_sc_address(); + let mut admins = MultiValueEncoded::new(); + admins.push(caller); + admins.push(own_sc_address); + + let template = self.template_address().get(); + let code_metadata = + CodeMetadata::PAYABLE_BY_SC | CodeMetadata::READABLE | CodeMetadata::UPGRADEABLE; + let timestamp_oracle = self.timestamp_oracle_address().get(); + + CommonDeployVariables { + owner, + admins, + template, + code_metadata, + timestamp_oracle, + } + } + fn add_new_contract( &self, caller: &ManagedAddress, @@ -104,4 +172,7 @@ pub trait DeployModule: crate::storage::StorageModule { #[proxy] fn farm_staking_deploy_proxy(&self) -> farm_staking::Proxy; + + #[proxy] + fn farm_with_top_up_deploy_proxy(&self) -> farm_with_top_up::Proxy; } diff --git a/dex/proxy-deployer/src/set_contract_active.rs b/dex/proxy-deployer/src/set_contract_active.rs index 9f819a6a3..873c56bb4 100644 --- a/dex/proxy-deployer/src/set_contract_active.rs +++ b/dex/proxy-deployer/src/set_contract_active.rs @@ -84,6 +84,7 @@ pub trait SetContractActiveModule: .execute_on_dest_context() } + // Both contracts have the same code for this at the moment. If this ever changes, make a separate endpoint and proxy for each #[proxy] fn set_contract_active_proxy( &self, diff --git a/dex/proxy-deployer/tests/proxy_deployer_farm_with_top_up_setup/mod.rs b/dex/proxy-deployer/tests/proxy_deployer_farm_with_top_up_setup/mod.rs new file mode 100644 index 000000000..7e756684f --- /dev/null +++ b/dex/proxy-deployer/tests/proxy_deployer_farm_with_top_up_setup/mod.rs @@ -0,0 +1,127 @@ +use common_structs::Timestamp; +use farm_boosted_yields::boosted_yields_factors::BoostedYieldsFactors; +use multiversx_sc::types::Address; +use multiversx_sc_scenario::{ + imports::{BlockchainStateWrapper, ContractObjWrapper}, + managed_address, managed_biguint, managed_token_id, rust_biguint, DebugApi, +}; +use proxy_deployer::{ + deploy::DeployModule, set_contract_active::SetContractActiveModule, storage::DeployerType, + ProxyDeployer, +}; +use timestamp_oracle::{epoch_to_timestamp::EpochToTimestampModule, TimestampOracle}; + +pub const TIMESTAMP_PER_EPOCH: Timestamp = 24 * 60 * 60; + +pub struct ProxyDeployerFarmStakingSetup +where + ProxyDeployerBuilder: 'static + Copy + Fn() -> proxy_deployer::ContractObj, + FarmWithTopUpBuilder: 'static + Copy + Fn() -> farm_with_top_up::ContractObj, +{ + pub b_mock: BlockchainStateWrapper, + pub owner: Address, + pub user: Address, + pub proxy_deployer_wrapper: + ContractObjWrapper, ProxyDeployerBuilder>, + pub template_wrapper: + ContractObjWrapper, FarmWithTopUpBuilder>, +} + +impl + ProxyDeployerFarmStakingSetup +where + ProxyDeployerBuilder: 'static + Copy + Fn() -> proxy_deployer::ContractObj, + FarmWithTopUpBuilder: 'static + Copy + Fn() -> farm_with_top_up::ContractObj, +{ + pub fn new( + proxy_builder: ProxyDeployerBuilder, + farm_with_top_up_builder: FarmWithTopUpBuilder, + ) -> Self { + let rust_zero = rust_biguint!(0); + let mut b_mock = BlockchainStateWrapper::new(); + let owner = b_mock.create_user_account(&rust_zero); + let user = b_mock.create_user_account(&rust_zero); + let proxy_deployer_wrapper = + b_mock.create_sc_account(&rust_zero, Some(&owner), proxy_builder, "proxy deployer"); + let template_wrapper = b_mock.create_sc_account( + &rust_zero, + Some(&owner), + farm_with_top_up_builder, + "farm top up template", + ); + + let timestamp_oracle_wrapper = b_mock.create_sc_account( + &rust_zero, + Some(&owner), + timestamp_oracle::contract_obj, + "timestamp oracle", + ); + b_mock + .execute_tx(&owner, ×tamp_oracle_wrapper, &rust_zero, |sc| { + sc.init(0); + + for i in 0..=21 { + sc.set_start_timestamp_for_epoch(i, i * TIMESTAMP_PER_EPOCH + 1); + } + }) + .assert_ok(); + + b_mock + .execute_tx(&owner, &proxy_deployer_wrapper, &rust_zero, |sc| { + sc.init( + managed_address!(template_wrapper.address_ref()), + DeployerType::FarmWithTopUp, + managed_address!(timestamp_oracle_wrapper.address_ref()), + BoostedYieldsFactors { + max_rewards_factor: managed_biguint!(10), + user_rewards_energy_const: managed_biguint!(3), + user_rewards_farm_const: managed_biguint!(2), + min_energy_amount: managed_biguint!(1), + min_farm_amount: managed_biguint!(1), + }, + ); + }) + .assert_ok(); + + Self { + b_mock, + owner, + user, + proxy_deployer_wrapper, + template_wrapper, + } + } + + pub fn deploy_farm_with_top_up(&mut self, farming_token_id: &[u8], reward_token_id: &[u8]) { + self.b_mock + .execute_tx( + &self.user, + &self.proxy_deployer_wrapper, + &rust_biguint!(0), + |sc| { + let _ = sc.deploy_farm_with_top_up( + managed_token_id!(farming_token_id), + managed_token_id!(reward_token_id), + ); + }, + ) + .assert_ok(); + } + + pub fn set_contract_active(&mut self, contract: &Address) { + self.b_mock + .execute_tx( + &self.user, + &self.proxy_deployer_wrapper, + &rust_biguint!(0), + |sc| { + sc.set_contract_active( + managed_address!(contract), + managed_biguint!(1_000), + 1_000, // 10% + ); + }, + ) + .assert_ok(); + } +} diff --git a/dex/proxy-deployer/tests/proxy_deployer_farm_with_top_up_tests.rs b/dex/proxy-deployer/tests/proxy_deployer_farm_with_top_up_tests.rs new file mode 100644 index 000000000..c59751696 --- /dev/null +++ b/dex/proxy-deployer/tests/proxy_deployer_farm_with_top_up_tests.rs @@ -0,0 +1,143 @@ +use farm_token::FarmTokenModule; +use farm_with_top_up::{custom_rewards::CustomRewardsModule, FarmWithTopUp}; +use multiversx_sc::{ + codec::Empty, + imports::{OptionalValue, StorageTokenWrapper}, + types::EsdtLocalRole, +}; +use multiversx_sc_scenario::{managed_address, managed_biguint, managed_token_id, rust_biguint}; +use proxy_deployer::remove_contracts::RemoveContractsModule; +use proxy_deployer_farm_with_top_up_setup::ProxyDeployerFarmStakingSetup; + +pub mod proxy_deployer_farm_with_top_up_setup; + +#[test] +fn setup_test() { + let _ = ProxyDeployerFarmStakingSetup::new( + proxy_deployer::contract_obj, + farm_with_top_up::contract_obj, + ); +} + +#[test] +fn deploy_farm_staking_test() { + let mut setup = ProxyDeployerFarmStakingSetup::new( + proxy_deployer::contract_obj, + farm_with_top_up::contract_obj, + ); + + let new_sc_wrapper = setup.b_mock.prepare_deploy_from_sc( + setup.proxy_deployer_wrapper.address_ref(), + farm_with_top_up::contract_obj, + ); + setup.deploy_farm_with_top_up(&b"COOLTOK-123456"[..], &b"COOLERTOK-123456"[..]); + + // user call admin function on new farm + setup + .b_mock + .execute_tx(&setup.user, &new_sc_wrapper, &rust_biguint!(0), |sc| { + sc.farm_token() + .set_token_id(managed_token_id!(b"MYCOOLFARM-123456")); + + sc.set_per_block_rewards(managed_biguint!(1_000)); + }) + .assert_ok(); + + // owner remove the contracts + let user_addr = setup.user.clone(); + setup + .b_mock + .execute_tx( + &setup.owner, + &setup.proxy_deployer_wrapper, + &rust_biguint!(0), + |sc| { + sc.remove_all_by_deployer(managed_address!(&user_addr), 1); + }, + ) + .assert_ok(); + + // user try call admin function after removed + setup + .b_mock + .execute_tx(&setup.user, &new_sc_wrapper, &rust_biguint!(0), |sc| { + sc.set_per_block_rewards(managed_biguint!(1_000)); + }) + .assert_user_error("Permission denied"); +} + +#[test] +fn set_contract_active_test() { + let mut setup = ProxyDeployerFarmStakingSetup::new( + proxy_deployer::contract_obj, + farm_with_top_up::contract_obj, + ); + + let new_sc_wrapper = setup.b_mock.prepare_deploy_from_sc( + setup.proxy_deployer_wrapper.address_ref(), + farm_with_top_up::contract_obj, + ); + let farming_token_id = b"COOLTOK-123456"; + let farm_token_id = b"MYCOOLFARM-123456"; + setup.deploy_farm_with_top_up(&farming_token_id[..], &farming_token_id[..]); + + // simulate farm token issue + setup + .b_mock + .execute_tx(&setup.user, &new_sc_wrapper, &rust_biguint!(0), |sc| { + sc.farm_token() + .set_token_id(managed_token_id!(farm_token_id)); + }) + .assert_ok(); + + setup.b_mock.set_esdt_local_roles( + new_sc_wrapper.address_ref(), + farm_token_id, + &[EsdtLocalRole::NftCreate, EsdtLocalRole::NftBurn], + ); + + // set user balance + setup + .b_mock + .set_esdt_balance(&setup.user, &farming_token_id[..], &rust_biguint!(1_000)); + + // user try enter farm before it's ready + setup + .b_mock + .execute_esdt_transfer( + &setup.user, + &new_sc_wrapper, + &farming_token_id[..], + 0, + &rust_biguint!(1_000), + |sc| { + sc.enter_farm_endpoint(OptionalValue::None); + }, + ) + .assert_user_error("Not active"); + + setup.set_contract_active(new_sc_wrapper.address_ref()); + + // user enter farm again + setup + .b_mock + .execute_esdt_transfer( + &setup.user, + &new_sc_wrapper, + &farming_token_id[..], + 0, + &rust_biguint!(1_000), + |sc| { + sc.enter_farm_endpoint(OptionalValue::None); + }, + ) + .assert_ok(); + + setup.b_mock.check_nft_balance( + &setup.user, + &farm_token_id[..], + 1, + &rust_biguint!(1_000), + Option::<&Empty>::None, + ); +} diff --git a/dex/proxy-deployer/wasm/Cargo.lock b/dex/proxy-deployer/wasm/Cargo.lock index eef769ff7..36028fb17 100644 --- a/dex/proxy-deployer/wasm/Cargo.lock +++ b/dex/proxy-deployer/wasm/Cargo.lock @@ -190,6 +190,36 @@ dependencies = [ "weekly-rewards-splitting", ] +[[package]] +name = "farm-with-top-up" +version = "0.0.0" +dependencies = [ + "common_errors", + "common_structs", + "config", + "contexts", + "energy-query", + "events", + "farm-boosted-yields", + "farm_base_impl", + "farm_token", + "fixed-supply-token", + "mergeable", + "multiversx-sc", + "multiversx-sc-modules", + "original_owner_helper", + "pair", + "pausable", + "permissions-hub", + "permissions_hub_module", + "permissions_module", + "rewards", + "sc_whitelist_module", + "utils", + "week-timekeeping", + "weekly-rewards-splitting", +] + [[package]] name = "farm_base_impl" version = "0.0.0" @@ -457,6 +487,7 @@ dependencies = [ "common_structs", "farm-boosted-yields", "farm-staking", + "farm-with-top-up", "multiversx-sc", "pausable", "permissions_module", diff --git a/dex/proxy-deployer/wasm/src/lib.rs b/dex/proxy-deployer/wasm/src/lib.rs index de33813fe..7157ca4f6 100644 --- a/dex/proxy-deployer/wasm/src/lib.rs +++ b/dex/proxy-deployer/wasm/src/lib.rs @@ -6,9 +6,9 @@ // Init: 1 // Upgrade: 1 -// Endpoints: 17 +// Endpoints: 18 // Async Callback (empty): 1 -// Total number of exported functions: 20 +// Total number of exported functions: 21 #![no_std] @@ -21,6 +21,7 @@ multiversx_sc_wasm_adapter::endpoints! { init => init upgrade => upgrade deployFarmStakingContract => deploy_farm_staking_contract + deployFarmWithTopUp => deploy_farm_with_top_up setContractActive => set_contract_active blacklistUser => blacklist_user removeAllByDeployer => remove_all_by_deployer From 0f909408172a25bc2de375c72bab8ed1ecb0cd4e Mon Sep 17 00:00:00 2001 From: Dorin Marian Iancu Date: Wed, 4 Dec 2024 10:57:24 +0200 Subject: [PATCH 5/5] remove comment --- dex/farm-with-top-up/src/custom_rewards.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dex/farm-with-top-up/src/custom_rewards.rs b/dex/farm-with-top-up/src/custom_rewards.rs index 39e1c688f..f34943cc0 100644 --- a/dex/farm-with-top-up/src/custom_rewards.rs +++ b/dex/farm-with-top-up/src/custom_rewards.rs @@ -40,7 +40,7 @@ pub trait CustomRewardsModule: + weekly_rewards_splitting::global_info::WeeklyRewardsGlobalInfo + weekly_rewards_splitting::locked_token_buckets::WeeklyRewardsLockedTokenBucketsModule + weekly_rewards_splitting::update_claim_progress_energy::UpdateClaimProgressEnergyModule - + energy_query::EnergyQueryModule // + crate::base_functions::BaseFunctionsModule + + energy_query::EnergyQueryModule { #[payable("*")] #[endpoint(topUpRewards)]