diff --git a/Cargo.lock b/Cargo.lock index f4091b679..5ac027d5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,6 +853,31 @@ dependencies = [ "multiversx-sc-meta", ] +[[package]] +name = "farm-staking-legacy" +version = "0.0.0" +dependencies = [ + "common_errors", + "common_structs", + "config", + "farm_token", + "multiversx-sc", + "multiversx-sc-modules", + "multiversx-sc-scenario", + "num-bigint", + "rewards", + "token_merge_helper", + "token_send", +] + +[[package]] +name = "farm-staking-legacy-abi" +version = "0.0.0" +dependencies = [ + "farm-staking-legacy", + "multiversx-sc-meta", +] + [[package]] name = "farm-staking-proxy" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 0944e273b..f2e185b2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ members = [ "farm-staking/metabonding-staking", "farm-staking/metabonding-staking/meta", + "legacy-contracts/farm-staking-proxy-v1.3", + "legacy-contracts/farm-staking-proxy-v1.3/meta", "legacy-contracts/simple-lock-legacy", "legacy-contracts/simple-lock-legacy/meta", diff --git a/legacy-contracts/farm-staking-proxy-v1.3/Cargo.toml b/legacy-contracts/farm-staking-proxy-v1.3/Cargo.toml new file mode 100644 index 000000000..4f7255528 --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "farm-staking-legacy" +version = "0.0.0" +authors = [ "you",] +edition = "2018" +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies.config] +path = "../../common/modules/farm/config" + +[dependencies.farm_token] +path = "../../common/modules/farm/farm_token" + +[dependencies.rewards] +path = "../../common/modules/farm/rewards" + +[dependencies.token_send] +path = "../../common/modules/token_send" + +[dependencies.token_merge_helper] +path = "../../common/modules/token_merge_helper" + +[dependencies.common_structs] +path = "../../common/common_structs" + +[dependencies.common_errors] +path = "../../common/common_errors" + +[dev-dependencies.multiversx-sc-scenario] +version = "=0.48.1" + +[dev-dependencies.multiversx-sc-modules] +version = "=0.48.1" + +[dependencies.multiversx-sc] +version = "=0.48.1" +features = ["esdt-token-payment-legacy-decode"] + +[dev-dependencies] +num-bigint = "0.4.2" diff --git a/legacy-contracts/farm-staking-proxy-v1.3/docs/setup.md b/legacy-contracts/farm-staking-proxy-v1.3/docs/setup.md new file mode 100644 index 000000000..9c17aba50 --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/docs/setup.md @@ -0,0 +1,85 @@ +# Farm Staking setup steps + +## Deployment + +The init function takes the following arguments: +- farming_token_id - the farming token ID, which will also be the reward token +- division_safety_constant - used in some calculations for precision. I suggest something like 10^9. +- max_apr - a percentage of max APR, which will limit the user's rewards, with two decimals precision (i.e. 10_000 = 100%). Can be more than 100%. +- min_unbond_epochs - Number of epochs the user has to wait between unstake and unbond. +``` +#[init] +fn init( + &self, + farming_token_id: TokenIdentifier, + division_safety_constant: BigUint, + max_apr: BigUint, + min_unbond_epochs: u64, +) +``` + +## Additional config + +### Setup farm token + +You have to register/issue the farm token and set its local roles, which is the token used for positions in the staking contract. This is done through the following endpoints: + +``` +#[payable("EGLD")] +#[endpoint(registerFarmToken)] +fn register_farm_token( + &self, + #[payment_amount] register_cost: BigUint, + token_display_name: ManagedBuffer, + token_ticker: ManagedBuffer, + num_decimals: usize, +) +``` + +For issue parameters format restrictions, take a look here: https://docs.multiversx.com/tokens/esdt-tokens/#parameters-format + +payment_amount should be `0.05 EGLD`. + +``` +#[endpoint(setLocalRolesFarmToken)] +fn set_local_roles_farm_token(&self) +``` + +### Set per block rewards + +``` +#[endpoint(setPerBlockRewardAmount)] +fn set_per_block_rewards(&self, per_block_amount: BigUint) +``` + +Keep in mind amount has to take into consideration the token's decimals. So if you have a token with 18 decimals, you have to pass 10^18 for "1". + +### Add the reward tokens + +``` +#[payable("*")] +#[endpoint(topUpRewards)] +fn top_up_rewards( + &self, + #[payment_token] payment_token: TokenIdentifier, + #[payment_amount] payment_amount: BigUint, +) +``` + +No args needed, you only need to pay the reward tokens. In the staking farm, rewards are not minted, but added by the owner. + +### Final steps + +First, you have to enable rewards generation: + +``` +#[endpoint(startProduceRewards)] +fn start_produce_rewards(&self) +``` + +Then, you have to the set the state to active: + +``` +#[endpoint] +fn resume(&self) +``` diff --git a/legacy-contracts/farm-staking-proxy-v1.3/meta/Cargo.toml b/legacy-contracts/farm-staking-proxy-v1.3/meta/Cargo.toml new file mode 100644 index 000000000..01dcf699f --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/meta/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "farm-staking-legacy-abi" + +version = "0.0.0" +authors = [ "you",] +edition = "2018" +publish = false + +[dependencies.farm-staking-legacy] +path = ".." + +[dependencies.multiversx-sc-meta] +version = "0.48.1" +default-features = false \ No newline at end of file diff --git a/legacy-contracts/farm-staking-proxy-v1.3/meta/src/main.rs b/legacy-contracts/farm-staking-proxy-v1.3/meta/src/main.rs new file mode 100644 index 000000000..de7773d55 --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/meta/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + multiversx_sc_meta::cli_main::(); +} diff --git a/legacy-contracts/farm-staking-proxy-v1.3/multiversx.json b/legacy-contracts/farm-staking-proxy-v1.3/multiversx.json new file mode 100644 index 000000000..736553962 --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} \ No newline at end of file diff --git a/legacy-contracts/farm-staking-proxy-v1.3/src/custom_rewards.rs b/legacy-contracts/farm-staking-proxy-v1.3/src/custom_rewards.rs new file mode 100644 index 000000000..906088f7e --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/src/custom_rewards.rs @@ -0,0 +1,186 @@ +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +use common_structs::Nonce; +use config::MAX_PERCENT; + +pub const BLOCKS_IN_YEAR: u64 = 31_536_000 / 6; // seconds_in_year / 6_seconds_per_block + +#[multiversx_sc::module] +pub trait CustomRewardsModule: + config::ConfigModule + token_send::TokenSendModule + farm_token::FarmTokenModule +{ + fn calculate_extra_rewards_since_last_allocation(&self) -> BigUint { + let current_block_nonce = self.blockchain().get_block_nonce(); + let last_reward_nonce = self.last_reward_block_nonce().get(); + + if current_block_nonce > last_reward_nonce { + let extra_rewards_unbounded = + self.calculate_per_block_rewards(current_block_nonce, last_reward_nonce); + + let farm_token_supply = self.farm_token_supply().get(); + let extra_rewards_apr_bounded_per_block = + self.get_amount_apr_bounded(&farm_token_supply); + + let block_nonce_diff = current_block_nonce - last_reward_nonce; + let extra_rewards_apr_bounded = extra_rewards_apr_bounded_per_block * block_nonce_diff; + + self.last_reward_block_nonce().set(¤t_block_nonce); + + core::cmp::min(extra_rewards_unbounded, extra_rewards_apr_bounded) + } else { + BigUint::zero() + } + } + + fn generate_aggregated_rewards(&self) { + let mut extra_rewards = self.calculate_extra_rewards_since_last_allocation(); + if extra_rewards > 0 { + let mut accumulated_rewards = self.accumulated_rewards().get(); + let total_rewards = &accumulated_rewards + &extra_rewards; + let reward_capacity = self.reward_capacity().get(); + if total_rewards > reward_capacity { + let amount_over_capacity = total_rewards - reward_capacity; + extra_rewards -= amount_over_capacity; + } + + accumulated_rewards += &extra_rewards; + self.accumulated_rewards().set(&accumulated_rewards); + + self.update_reward_per_share(&extra_rewards); + } + } + + #[only_owner] + #[payable("*")] + #[endpoint(topUpRewards)] + fn top_up_rewards(&self) { + let (payment_amount, payment_token) = self.call_value().payment_token_pair(); + 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); + } + + #[only_owner] + #[endpoint] + fn end_produce_rewards(&self) { + self.generate_aggregated_rewards(); + self.produce_rewards_enabled().set(&false); + } + + #[only_owner] + #[endpoint(setPerBlockRewardAmount)] + fn set_per_block_rewards(&self, per_block_amount: BigUint) { + require!(per_block_amount != 0, "Amount cannot be zero"); + + self.generate_aggregated_rewards(); + self.per_block_reward_amount().set(&per_block_amount); + } + + #[only_owner] + #[endpoint(setMaxApr)] + fn set_max_apr(&self, max_apr: BigUint) { + require!(max_apr != 0, "Max APR cannot be zero"); + + self.max_annual_percentage_rewards().set(&max_apr); + } + + #[only_owner] + #[endpoint(setMinUnbondEpochs)] + fn set_min_unbond_epochs(&self, min_unbond_epochs: u64) { + self.min_unbond_epochs().set(&min_unbond_epochs); + } + + fn calculate_per_block_rewards( + &self, + current_block_nonce: Nonce, + last_reward_block_nonce: Nonce, + ) -> BigUint { + if current_block_nonce <= last_reward_block_nonce || !self.produces_per_block_rewards() { + return BigUint::zero(); + } + + let per_block_reward = self.per_block_reward_amount().get(); + let block_nonce_diff = current_block_nonce - last_reward_block_nonce; + + per_block_reward * block_nonce_diff + } + + fn update_reward_per_share(&self, reward_increase: &BigUint) { + let farm_token_supply = self.farm_token_supply().get(); + if farm_token_supply > 0 { + let increase = + self.calculate_reward_per_share_increase(reward_increase, &farm_token_supply); + self.reward_per_share().update(|r| *r += increase); + } + } + + fn calculate_reward_per_share_increase( + &self, + reward_increase: &BigUint, + farm_token_supply: &BigUint, + ) -> BigUint { + &(reward_increase * &self.division_safety_constant().get()) / farm_token_supply + } + + fn calculate_reward( + &self, + amount: &BigUint, + current_reward_per_share: &BigUint, + initial_reward_per_share: &BigUint, + ) -> BigUint { + if current_reward_per_share > initial_reward_per_share { + let reward_per_share_diff = current_reward_per_share - initial_reward_per_share; + amount * &reward_per_share_diff / self.division_safety_constant().get() + } else { + BigUint::zero() + } + } + + fn get_amount_apr_bounded(&self, amount: &BigUint) -> BigUint { + let max_apr = self.max_annual_percentage_rewards().get(); + amount * &max_apr / MAX_PERCENT / BLOCKS_IN_YEAR + } + + #[only_owner] + #[endpoint(startProduceRewards)] + fn start_produce_rewards(&self) { + require!( + self.per_block_reward_amount().get() != 0, + "Cannot produce zero reward amount" + ); + require!( + !self.produce_rewards_enabled().get(), + "Producing rewards is already enabled" + ); + let current_nonce = self.blockchain().get_block_nonce(); + self.produce_rewards_enabled().set(&true); + self.last_reward_block_nonce().set(¤t_nonce); + } + + #[inline(always)] + fn produces_per_block_rewards(&self) -> bool { + self.produce_rewards_enabled().get() + } + + #[view(getRewardPerShare)] + #[storage_mapper("reward_per_share")] + fn reward_per_share(&self) -> SingleValueMapper; + + #[view(getAccumulatedRewards)] + #[storage_mapper("accumulatedRewards")] + fn accumulated_rewards(&self) -> SingleValueMapper; + + #[view(getRewardCapacity)] + #[storage_mapper("reward_capacity")] + fn reward_capacity(&self) -> SingleValueMapper; + + #[view(getAnnualPercentageRewards)] + #[storage_mapper("annualPercentageRewards")] + fn max_annual_percentage_rewards(&self) -> SingleValueMapper; + + #[view(getMinUnbondEpochs)] + #[storage_mapper("minUnbondEpochs")] + fn min_unbond_epochs(&self) -> SingleValueMapper; +} diff --git a/legacy-contracts/farm-staking-proxy-v1.3/src/farm_token_merge.rs b/legacy-contracts/farm-staking-proxy-v1.3/src/farm_token_merge.rs new file mode 100644 index 000000000..90a878160 --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/src/farm_token_merge.rs @@ -0,0 +1,161 @@ +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +use token_merge_helper::ValueWeight; + +#[derive( + ManagedVecItem, + TopEncode, + TopDecode, + NestedEncode, + NestedDecode, + TypeAbi, + Clone, + PartialEq, + Debug, +)] +pub struct StakingFarmTokenAttributes { + pub reward_per_share: BigUint, + pub compounded_reward: BigUint, + pub current_farm_amount: BigUint, +} + +#[derive(ManagedVecItem, Clone)] +pub struct StakingFarmToken { + pub token_amount: EsdtTokenPayment, + pub attributes: StakingFarmTokenAttributes, +} + +#[multiversx_sc::module] +pub trait FarmTokenMergeModule: + token_send::TokenSendModule + + farm_token::FarmTokenModule + + config::ConfigModule + + token_merge_old::TokenMergeModule +{ + #[payable("*")] + #[endpoint(mergeFarmTokens)] + fn merge_farm_tokens(&self) -> EsdtTokenPayment { + let caller = self.blockchain().get_caller(); + let payments = self.call_value().all_esdt_transfers(); + + let attrs = self.get_merged_farm_token_attributes(&payments, None, None); + + let farm_token_id = self.farm_token_id().get(); + self.burn_farm_tokens_from_payments(&payments); + + let new_nonce = self.mint_farm_tokens(&farm_token_id, &attrs.current_farm_amount, &attrs); + let new_amount = attrs.current_farm_amount; + + self.send() + .direct(&caller, &farm_token_id, new_nonce, &new_amount, &[]); + + self.create_payment(&farm_token_id, new_nonce, &new_amount) + } + + fn get_merged_farm_token_attributes( + &self, + payments: &ManagedVec>, + replic: Option>, + opt_custom_attributes_for_payments: Option< + &ManagedVec>, + >, + ) -> StakingFarmTokenAttributes { + require!( + !payments.is_empty() || replic.is_some(), + "No tokens to merge" + ); + + let mut tokens = ManagedVec::new(); + let farm_token_id = self.farm_token_id().get(); + let empty_vec = ManagedVec::new(); + let custom_attributes = opt_custom_attributes_for_payments.unwrap_or(&empty_vec); + + for (i, payment) in payments.iter().enumerate() { + require!(payment.amount != 0u64, "zero entry amount"); + require!( + payment.token_identifier == farm_token_id, + "Not a farm token" + ); + + let attributes = match custom_attributes.try_get(i) { + Some(attr) => attr, + None => self.get_attributes(&payment.token_identifier, payment.token_nonce), + }; + tokens.push(StakingFarmToken { + token_amount: self.create_payment( + &payment.token_identifier, + payment.token_nonce, + &payment.amount, + ), + attributes, + }); + } + + if let Some(r) = replic { + tokens.push(r); + } + + if tokens.len() == 1 { + if let Some(t) = tokens.try_get(0) { + return t.attributes; + } + } + + StakingFarmTokenAttributes { + reward_per_share: self.aggregated_reward_per_share(&tokens), + compounded_reward: self.aggregated_compounded_reward(&tokens), + current_farm_amount: self.aggregated_current_farm_amount(&tokens), + } + } + + fn aggregated_reward_per_share( + &self, + tokens: &ManagedVec>, + ) -> BigUint { + let mut dataset = ManagedVec::new(); + tokens.iter().for_each(|x| { + dataset.push(ValueWeight { + value: x.attributes.reward_per_share.clone(), + weight: x.token_amount.amount, + }) + }); + self.weighted_average_ceil(dataset) + } + + fn aggregated_compounded_reward( + &self, + tokens: &ManagedVec>, + ) -> BigUint { + let mut sum = BigUint::zero(); + tokens.iter().for_each(|x| { + sum += &self.rule_of_three( + &x.token_amount.amount, + &x.attributes.current_farm_amount, + &x.attributes.compounded_reward, + ) + }); + sum + } + + fn aggregated_current_farm_amount( + &self, + tokens: &ManagedVec>, + ) -> BigUint { + let mut aggregated_amount = BigUint::zero(); + tokens + .iter() + .for_each(|x| aggregated_amount += &x.token_amount.amount); + aggregated_amount + } + + fn get_attributes(&self, token_id: &TokenIdentifier, token_nonce: u64) -> T { + let token_info = self.blockchain().get_esdt_token_data( + &self.blockchain().get_sc_address(), + token_id, + token_nonce, + ); + + token_info.decode_attributes() + } +} diff --git a/legacy-contracts/farm-staking-proxy-v1.3/src/lib.rs b/legacy-contracts/farm-staking-proxy-v1.3/src/lib.rs new file mode 100644 index 000000000..d6aca4865 --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/src/lib.rs @@ -0,0 +1,613 @@ +#![no_std] +#![feature(exact_size_is_empty)] +#![allow(clippy::too_many_arguments)] + +pub mod custom_rewards; +pub mod farm_token_merge; +pub mod whitelist; + +use common_structs_old::Nonce; +use config::State; + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +use crate::farm_token_merge::StakingFarmTokenAttributes; +use config::{ + DEFAULT_BURN_GAS_LIMIT, DEFAULT_MINUMUM_FARMING_EPOCHS, DEFAULT_TRANSFER_EXEC_GAS_LIMIT, +}; +use farm_token_merge::StakingFarmToken; + +pub type EnterFarmResultType = EsdtTokenPayment; +pub type CompoundRewardsResultType = EsdtTokenPayment; +pub type ClaimRewardsResultType = + MultiValue2, EsdtTokenPayment>; +pub type ExitFarmResultType = + MultiValue2, EsdtTokenPayment>; +pub type UnbondFarmResultType = EsdtTokenPayment; + +#[derive(TypeAbi, TopEncode, TopDecode, PartialEq, Debug)] +pub struct UnbondSftAttributes { + pub unlock_epoch: u64, +} + +#[multiversx_sc::contract] +pub trait Farm: + custom_rewards::CustomRewardsModule + + config::ConfigModule + + token_send::TokenSendModule + + token_merge_old::TokenMergeModule + + farm_token::FarmTokenModule + + farm_token_merge::FarmTokenMergeModule + + whitelist::WhitelistModule +{ + #[init] + fn init( + &self, + farming_token_id: TokenIdentifier, + division_safety_constant: BigUint, + max_apr: BigUint, + min_unbond_epochs: u64, + ) { + require!( + farming_token_id.is_esdt(), + "Farming token ID is not a valid esdt identifier" + ); + require!( + division_safety_constant != 0, + "Division constant cannot be 0" + ); + let farm_token = self.farm_token_id().get(); + require!( + farming_token_id != farm_token, + "Farming token ID cannot be farm token ID" + ); + require!(max_apr > 0u64, "Invalid max APR percentage"); + + self.state().set(&State::Inactive); + self.minimum_farming_epochs() + .set_if_empty(&DEFAULT_MINUMUM_FARMING_EPOCHS); + self.transfer_exec_gas_limit() + .set_if_empty(&DEFAULT_TRANSFER_EXEC_GAS_LIMIT); + self.burn_gas_limit().set_if_empty(&DEFAULT_BURN_GAS_LIMIT); + self.division_safety_constant() + .set_if_empty(&division_safety_constant); + + // farming and reward token are the same + self.reward_token_id().set(&farming_token_id); + self.farming_token_id().set(&farming_token_id); + self.max_annual_percentage_rewards().set(&max_apr); + self.min_unbond_epochs().set(&min_unbond_epochs); + } + + #[payable("*")] + #[endpoint(stakeFarmThroughProxy)] + fn stake_farm_through_proxy( + &self, + staked_token_amount: BigUint, + ) -> EnterFarmResultType { + let caller = self.blockchain().get_caller(); + self.require_whitelisted(&caller); + + let staked_token_id = self.farming_token_id().get(); + let staked_token_simulated_payment = + EsdtTokenPayment::new(staked_token_id, 0, staked_token_amount); + + let farm_tokens = self.call_value().all_esdt_transfers(); + let mut payments = ManagedVec::from_single_item(staked_token_simulated_payment); + payments.append_vec(farm_tokens); + + self.stake_farm(payments) + } + + #[payable("*")] + #[endpoint(stakeFarm)] + fn stake_farm_endpoint(&self) -> EnterFarmResultType { + let payments = self.call_value().all_esdt_transfers(); + + self.stake_farm(payments) + } + + fn stake_farm( + &self, + payments: ManagedVec>, + ) -> EnterFarmResultType { + require!(self.is_active(), "Not active"); + require!(!self.farm_token_id().is_empty(), "No farm token"); + + let payment_0 = payments + .try_get(0) + .unwrap_or_else(|| sc_panic!("empty payments")); + let additional_payments = payments.slice(1, payments.len()).unwrap_or_default(); + + let token_in = payment_0.token_identifier.clone(); + let enter_amount = payment_0.amount; + + let farming_token_id = self.farming_token_id().get(); + require!(token_in == farming_token_id, "Bad input token"); + require!(enter_amount > 0u32, "Cannot farm with amount of 0"); + + self.generate_aggregated_rewards(); + + let attributes = StakingFarmTokenAttributes { + reward_per_share: self.reward_per_share().get(), + compounded_reward: BigUint::zero(), + current_farm_amount: enter_amount.clone(), + }; + + let caller = self.blockchain().get_caller(); + let farm_token_id = self.farm_token_id().get(); + let (new_farm_token, _created_with_merge) = self.create_farm_tokens_by_merging( + &enter_amount, + &farm_token_id, + &attributes, + &additional_payments, + ); + self.send().direct( + &caller, + &farm_token_id, + new_farm_token.token_amount.token_nonce, + &new_farm_token.token_amount.amount, + &[], + ); + + new_farm_token.token_amount + } + + #[payable("*")] + #[endpoint(unstakeFarm)] + fn unstake_farm(&self) -> ExitFarmResultType { + let (amount, payment_token_id) = self.call_value().payment_token_pair(); + let token_nonce = self.call_value().esdt_token_nonce(); + + self.unstake_farm_common(payment_token_id, token_nonce, amount, None) + } + + #[payable("*")] + #[endpoint(unstakeFarmThroughProxy)] + fn unstake_farm_through_proxy( + &self, + original_caller: ManagedAddress, + ) -> ExitFarmResultType { + let caller = self.blockchain().get_caller(); + self.require_whitelisted(&caller); + + let payments = self.call_value().all_esdt_transfers(); + require!(payments.len() == 2, "Invalid payments amount"); + + // first payment are the staking tokens, taken from the liquidity pool + // they will be sent to the user on unbond + let first_payment = payments.get(0); + let staking_token_id = self.farming_token_id().get(); + require!( + first_payment.token_identifier == staking_token_id, + "Invalid first payment" + ); + + let second_payment = payments.get(1); + let farm_token_id = self.farm_token_id().get(); + require!( + second_payment.token_identifier == farm_token_id, + "Invalid second payment" + ); + + self.unstake_farm_common(original_caller, second_payment, Some(first_payment.amount)) + } + + fn unstake_farm_common( + &self, + payment_token_id: TokenIdentifier, + token_nonce: Nonce, + payment_amount: BigUint, + opt_unbond_amount: Option, + ) -> ExitFarmResultType { + require!(self.is_active(), "Not active"); + require!(!self.farm_token_id().is_empty(), "No farm token"); + + let farm_token_id = self.farm_token_id().get(); + require!(payment_token_id == farm_token_id, "Bad input token"); + require!(payment_amount > 0u32, "Payment amount cannot be zero"); + + let farm_attributes = self.get_attributes::>( + &payment_token_id, + token_nonce, + ); + let reward_token_id = self.reward_token_id().get(); + self.generate_aggregated_rewards(); + + let reward = self.calculate_reward( + &payment_amount, + &self.reward_per_share().get(), + &farm_attributes.reward_per_share, + ); + + let caller = self.blockchain().get_caller(); + self.burn_farm_tokens(&payment_token_id, token_nonce, &payment_amount); + + let unbond_token_amount = match opt_unbond_amount { + Some(amt) => amt, + None => payment_amount, // payment_amount = initial_farming + compounded_rewards + }; + let farm_token_payment = + self.create_and_send_unbond_tokens(&caller, farm_token_id, unbond_token_amount); + + self.send_rewards(&reward_token_id, &reward, &caller); + + MultiValue2::from(( + farm_token_payment, + EsdtTokenPayment::new(reward_token_id, 0, reward), + )) + } + + fn create_and_send_unbond_tokens( + &self, + to: &ManagedAddress, + farm_token_id: TokenIdentifier, + amount: BigUint, + ) -> EsdtTokenPayment { + let min_unbond_epochs = self.min_unbond_epochs().get(); + let current_epoch = self.blockchain().get_block_epoch(); + let nft_nonce = self.nft_create_tokens( + &farm_token_id, + &amount, + &UnbondSftAttributes { + unlock_epoch: current_epoch + min_unbond_epochs, + }, + ); + self.send() + .direct(to, &farm_token_id, nft_nonce, &amount, &[]); + + EsdtTokenPayment::new(farm_token_id, nft_nonce, amount) + } + + #[payable("*")] + #[endpoint(unbondFarm)] + fn unbond_farm(&self) -> UnbondFarmResultType { + require!(self.is_active(), "Not active"); + require!(!self.farm_token_id().is_empty(), "No farm token"); + + let (amount, payment_token_id) = self.call_value().payment_token_pair(); + let token_nonce = self.call_value().esdt_token_nonce(); + + let farm_token_id = self.farm_token_id().get(); + require!(payment_token_id == farm_token_id, "Bad input token"); + require!(amount > 0, "Payment amount cannot be zero"); + + let token_info = self.blockchain().get_esdt_token_data( + &self.blockchain().get_sc_address(), + &farm_token_id, + token_nonce, + ); + let unlock_epoch = token_info + .decode_attributes::() + .unlock_epoch; + let current_epoch = self.blockchain().get_block_epoch(); + require!(current_epoch >= unlock_epoch, "Unbond period not over"); + + self.send() + .esdt_local_burn(&farm_token_id, token_nonce, &amount); + + let caller = self.blockchain().get_caller(); + let farming_token_id = self.farming_token_id().get(); + self.send() + .direct(&caller, &farming_token_id, 0, &amount, &[]); + + EsdtTokenPayment::new(farming_token_id, 0, amount) + } + + #[payable("*")] + #[endpoint(claimRewards)] + fn claim_rewards(&self) -> ClaimRewardsResultType { + let payments = self.call_value().all_esdt_transfers(); + + self.claim_rewards_common(payments, None) + } + + #[payable("*")] + #[endpoint(claimRewardsWithNewValue)] + fn claim_rewards_with_new_value( + &self, + new_values: ManagedVec, + ) -> ClaimRewardsResultType { + let caller = self.blockchain().get_caller(); + self.require_whitelisted(&caller); + + let payments = self.call_value().all_esdt_transfers(); + require!( + payments.len() == new_values.len(), + "Arguments length mismatch" + ); + + self.claim_rewards_common(payments, Some(new_values)) + } + + fn claim_rewards_common( + &self, + payments: ManagedVec>, + opt_new_farm_values: Option>, + ) -> ClaimRewardsResultType { + require!(self.is_active(), "Not active"); + require!(!self.farm_token_id().is_empty(), "No farm token"); + + let payment_0 = payments + .try_get(0) + .unwrap_or_else(|| sc_panic!("empty payments")); + let additional_payments = payments.slice(1, payments.len()).unwrap_or_default(); + + let payment_token_id = payment_0.token_identifier.clone(); + let old_farming_amount = payment_0.amount.clone(); + let token_nonce = payment_0.token_nonce; + + require!(old_farming_amount > 0u32, "Zero amount"); + let farm_token_id = self.farm_token_id().get(); + require!(payment_token_id == farm_token_id, "Unknown farm token"); + let farm_attributes = self.get_attributes::>( + &payment_token_id, + token_nonce, + ); + + let reward_token_id = self.reward_token_id().get(); + self.generate_aggregated_rewards(); + + let reward = self.calculate_reward( + &old_farming_amount, + &self.reward_per_share().get(), + &farm_attributes.reward_per_share, + ); + let new_compound_reward_amount = self.rule_of_three( + &old_farming_amount, + &farm_attributes.current_farm_amount, + &farm_attributes.compounded_reward, + ); + let new_farming_amount = match &opt_new_farm_values { + Some(new_values) => (*new_values.get(0)).clone(), + None => old_farming_amount.clone(), + }; + + let new_attributes = StakingFarmTokenAttributes { + reward_per_share: self.reward_per_share().get(), + compounded_reward: new_compound_reward_amount, + current_farm_amount: new_farming_amount.clone(), + }; + + let caller = self.blockchain().get_caller(); + self.burn_farm_tokens(&payment_token_id, token_nonce, &old_farming_amount); + + let (new_farm_token, _created_with_merge) = match opt_new_farm_values { + Some(new_farm_values) => self.create_farm_tokens_with_new_value( + &additional_payments, + &new_farming_amount, + &new_attributes, + &new_farm_values, + ), + None => self.create_farm_tokens_by_merging( + &new_farming_amount, + &farm_token_id, + &new_attributes, + &additional_payments, + ), + }; + + self.send().direct( + &caller, + &farm_token_id, + new_farm_token.token_amount.token_nonce, + &new_farm_token.token_amount.amount, + &[], + ); + self.send_rewards(&reward_token_id, &reward, &caller); + + MultiValue2::from(( + new_farm_token.token_amount, + EsdtTokenPayment::new(reward_token_id, 0, reward), + )) + } + + fn create_farm_tokens_with_new_value( + &self, + additional_payments: &ManagedVec>, + new_farming_amount: &BigUint, + new_attributes: &StakingFarmTokenAttributes, + new_farm_values: &ManagedVec, + ) -> (StakingFarmToken, bool) { + let new_additional_values = new_farm_values + .slice(1, new_farm_values.len()) + .unwrap_or_default(); + + let mut additional_payments_attributes = ManagedVec::new(); + for (p, new_val) in additional_payments.iter().zip(new_additional_values.iter()) { + let mut attr = self.get_attributes::>( + &p.token_identifier, + p.token_nonce, + ); + attr.current_farm_amount = (*new_val).clone(); + + additional_payments_attributes.push(attr); + } + + let farm_token_id = self.farm_token_id().get(); + self.create_farm_tokens_by_merging_with_updated_attributes( + new_farming_amount, + &farm_token_id, + new_attributes, + additional_payments, + &additional_payments_attributes, + ) + } + + #[payable("*")] + #[endpoint(compoundRewards)] + fn compound_rewards(&self) -> CompoundRewardsResultType { + require!(self.is_active(), "Not active"); + + let payments_vec = self.call_value().all_esdt_transfers(); + let payment_0 = payments_vec + .try_get(0) + .unwrap_or_else(|| sc_panic!("empty payments")); + let additional_payments = payments_vec + .slice(1, payments_vec.len()) + .unwrap_or_default(); + + let payment_token_id = payment_0.token_identifier.clone(); + let payment_amount = payment_0.amount.clone(); + let payment_token_nonce = payment_0.token_nonce; + require!(payment_amount > 0u32, "Zero amount"); + + require!(!self.farm_token_id().is_empty(), "No farm token"); + let farm_token_id = self.farm_token_id().get(); + require!(payment_token_id == farm_token_id, "Unknown farm token"); + + let farming_token = self.farming_token_id().get(); + let reward_token = self.reward_token_id().get(); + require!( + farming_token == reward_token, + "Farming token differ from reward token" + ); + self.generate_aggregated_rewards(); + + let current_rps = self.reward_per_share().get(); + let farm_attributes = self.get_attributes::>( + &payment_token_id, + payment_token_nonce, + ); + let reward = self.calculate_reward( + &payment_amount, + ¤t_rps, + &farm_attributes.reward_per_share, + ); + + let new_farm_contribution = &payment_amount + &reward; + let new_compound_reward_amount = &self.rule_of_three( + &payment_amount, + &farm_attributes.current_farm_amount, + &farm_attributes.compounded_reward, + ) + &reward; + + let new_attributes = StakingFarmTokenAttributes { + reward_per_share: current_rps, + compounded_reward: new_compound_reward_amount, + current_farm_amount: new_farm_contribution.clone(), + }; + + self.burn_farm_tokens(&farm_token_id, payment_token_nonce, &payment_amount); + let caller = self.blockchain().get_caller(); + let (new_farm_token, _created_with_merge) = self.create_farm_tokens_by_merging( + &new_farm_contribution, + &farm_token_id, + &new_attributes, + &additional_payments, + ); + self.send().direct( + &caller, + &farm_token_id, + new_farm_token.token_amount.token_nonce, + &new_farm_token.token_amount.amount, + &[], + ); + + new_farm_token.token_amount + } + + fn create_farm_tokens_by_merging( + &self, + amount: &BigUint, + token_id: &TokenIdentifier, + attributes: &StakingFarmTokenAttributes, + additional_payments: &ManagedVec>, + ) -> (StakingFarmToken, bool) { + let current_position_replic = StakingFarmToken { + token_amount: self.create_payment(token_id, 0, amount), + attributes: attributes.clone(), + }; + + let additional_payments_len = additional_payments.len(); + let merged_attributes = self.get_merged_farm_token_attributes( + additional_payments, + Some(current_position_replic), + None, + ); + self.burn_farm_tokens_from_payments(additional_payments); + + let new_amount = &merged_attributes.current_farm_amount; + let new_nonce = self.mint_farm_tokens(token_id, new_amount, &merged_attributes); + + let new_farm_token = StakingFarmToken { + token_amount: self.create_payment(token_id, new_nonce, new_amount), + attributes: merged_attributes, + }; + let is_merged = additional_payments_len != 0; + + (new_farm_token, is_merged) + } + + fn create_farm_tokens_by_merging_with_updated_attributes( + &self, + amount: &BigUint, + token_id: &TokenIdentifier, + attributes: &StakingFarmTokenAttributes, + additional_payments: &ManagedVec>, + new_additional_attributes: &ManagedVec>, + ) -> (StakingFarmToken, bool) { + let current_position_replic = StakingFarmToken { + token_amount: self.create_payment(token_id, 0, amount), + attributes: attributes.clone(), + }; + + let additional_payments_len = additional_payments.len(); + let merged_attributes = self.get_merged_farm_token_attributes( + additional_payments, + Some(current_position_replic), + Some(new_additional_attributes), + ); + self.burn_farm_tokens_from_payments(additional_payments); + + let new_amount = &merged_attributes.current_farm_amount; + let new_nonce = self.mint_farm_tokens(token_id, new_amount, &merged_attributes); + + let new_farm_token = StakingFarmToken { + token_amount: self.create_payment(token_id, new_nonce, new_amount), + attributes: merged_attributes, + }; + let is_merged = additional_payments_len != 0; + + (new_farm_token, is_merged) + } + + fn send_rewards( + &self, + reward_token_id: &TokenIdentifier, + reward_amount: &BigUint, + destination: &ManagedAddress, + ) { + if reward_amount > &0 { + self.send() + .direct(destination, reward_token_id, 0, reward_amount, &[]); + } + } + + #[view(calculateRewardsForGivenPosition)] + fn calculate_rewards_for_given_position( + &self, + amount: BigUint, + attributes: StakingFarmTokenAttributes, + ) -> BigUint { + require!(amount > 0, "Zero liquidity input"); + let farm_token_supply = self.farm_token_supply().get(); + require!(farm_token_supply >= amount, "Not enough supply"); + + let last_reward_nonce = self.last_reward_block_nonce().get(); + let current_block_nonce = self.blockchain().get_block_nonce(); + let reward_increase = + self.calculate_per_block_rewards(current_block_nonce, last_reward_nonce); + let reward_per_share_increase = + self.calculate_reward_per_share_increase(&reward_increase, &farm_token_supply); + + let future_reward_per_share = self.reward_per_share().get() + reward_per_share_increase; + + self.calculate_reward( + &amount, + &future_reward_per_share, + &attributes.reward_per_share, + ) + } +} diff --git a/legacy-contracts/farm-staking-proxy-v1.3/src/whitelist.rs b/legacy-contracts/farm-staking-proxy-v1.3/src/whitelist.rs new file mode 100644 index 000000000..7e7d2d50e --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/src/whitelist.rs @@ -0,0 +1,31 @@ +multiversx_sc::imports!(); + +use common_errors::ERROR_PERMISSION_DENIED; + +#[multiversx_sc::module] +pub trait WhitelistModule { + #[only_owner] + #[endpoint(addAddressToWhitelist)] + fn add_address_to_whitelist(&self, address: ManagedAddress) { + self.whitelisted(&address).set(&true); + } + + #[only_owner] + #[endpoint(removeAddressFromWhitelist)] + fn remove_address_from_whitelist(&self, address: ManagedAddress) { + self.whitelisted(&address).clear(); + } + + #[inline] + fn is_whitelisted(&self, address: &ManagedAddress) -> bool { + self.whitelisted(address).get() + } + + fn require_whitelisted(&self, address: &ManagedAddress) { + require!(self.is_whitelisted(address), ERROR_PERMISSION_DENIED); + } + + #[view(isWhitelisted)] + #[storage_mapper("whitelisted")] + fn whitelisted(&self, address: &ManagedAddress) -> SingleValueMapper; +} diff --git a/legacy-contracts/farm-staking-proxy-v1.3/tests/farm_staking_test.rs b/legacy-contracts/farm-staking-proxy-v1.3/tests/farm_staking_test.rs new file mode 100644 index 000000000..c39e5c62c --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/tests/farm_staking_test.rs @@ -0,0 +1,638 @@ +use multiversx_sc::tx_mock::{TxContextStack, TxInputESDT}; +use multiversx_sc::types::{Address, EsdtLocalRole}; +use multiversx_sc::{ + managed_biguint, managed_token_id, rust_biguint, testing_framework::*, DebugApi, +}; + +type RustBigUint = num_bigint::BigUint; + +use config::*; +use farm_staking::custom_rewards::{CustomRewardsModule, BLOCKS_IN_YEAR}; +use farm_staking::farm_token_merge::StakingFarmTokenAttributes; +use farm_staking::*; + +const FARM_WASM_PATH: &'static str = "farm/output/farm-staking.wasm"; + +const REWARD_TOKEN_ID: &[u8] = b"RIDE-abcdef"; // reward token ID +const FARMING_TOKEN_ID: &[u8] = b"RIDE-abcdef"; // farming token ID +const FARM_TOKEN_ID: &[u8] = b"FARM-abcdef"; +const DIVISION_SAFETY_CONSTANT: u64 = 1_000_000_000_000; +const MIN_FARMING_EPOCHS: u8 = 2; +const MIN_UNBOND_EPOCHS: u64 = 5; +const PENALTY_PERCENT: u64 = 10; +const MAX_APR: u64 = 2_500; // 25% +const PER_BLOCK_REWARD_AMOUNT: u64 = 5_000; +const TOTAL_REWARDS_AMOUNT: u64 = 1_000_000_000_000; + +const USER_TOTAL_RIDE_TOKENS: u64 = 5_000_000_000; + +#[allow(dead_code)] // owner_address is unused, at least for now +struct FarmSetup +where + FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, +{ + pub blockchain_wrapper: BlockchainStateWrapper, + pub owner_address: Address, + pub user_address: Address, + pub farm_wrapper: ContractObjWrapper, FarmObjBuilder>, +} + +fn setup_farm(farm_builder: FarmObjBuilder) -> FarmSetup +where + FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, +{ + let rust_zero = rust_biguint!(0u64); + let mut blockchain_wrapper = BlockchainStateWrapper::new(); + let owner_addr = blockchain_wrapper.create_user_account(&rust_zero); + let farm_wrapper = blockchain_wrapper.create_sc_account( + &rust_zero, + Some(&owner_addr), + farm_builder, + FARM_WASM_PATH, + ); + + // init farm contract + + blockchain_wrapper + .execute_tx(&owner_addr, &farm_wrapper, &rust_zero, |sc| { + let farming_token_id = managed_token_id!(FARMING_TOKEN_ID); + let division_safety_constant = managed_biguint!(DIVISION_SAFETY_CONSTANT); + + sc.init( + farming_token_id, + division_safety_constant, + managed_biguint!(MAX_APR), + MIN_UNBOND_EPOCHS, + ); + + let farm_token_id = managed_token_id!(FARM_TOKEN_ID); + sc.farm_token_id().set(&farm_token_id); + + sc.per_block_reward_amount() + .set(&managed_biguint!(PER_BLOCK_REWARD_AMOUNT)); + sc.minimum_farming_epochs().set(&MIN_FARMING_EPOCHS); + sc.penalty_percent().set(&PENALTY_PERCENT); + + sc.state().set(&State::Active); + sc.produce_rewards_enabled().set(&true); + }) + .assert_ok(); + + blockchain_wrapper.set_esdt_balance(&owner_addr, REWARD_TOKEN_ID, &TOTAL_REWARDS_AMOUNT.into()); + blockchain_wrapper + .execute_esdt_transfer( + &owner_addr, + &farm_wrapper, + REWARD_TOKEN_ID, + 0, + &TOTAL_REWARDS_AMOUNT.into(), + |sc| { + sc.top_up_rewards(); + }, + ) + .assert_ok(); + + let farm_token_roles = [ + EsdtLocalRole::NftCreate, + EsdtLocalRole::NftAddQuantity, + EsdtLocalRole::NftBurn, + ]; + blockchain_wrapper.set_esdt_local_roles( + farm_wrapper.address_ref(), + FARM_TOKEN_ID, + &farm_token_roles[..], + ); + + let farming_token_roles = [EsdtLocalRole::Burn]; + blockchain_wrapper.set_esdt_local_roles( + farm_wrapper.address_ref(), + FARMING_TOKEN_ID, + &farming_token_roles[..], + ); + + let user_addr = blockchain_wrapper.create_user_account(&rust_biguint!(100_000_000)); + blockchain_wrapper.set_esdt_balance( + &user_addr, + FARMING_TOKEN_ID, + &rust_biguint!(USER_TOTAL_RIDE_TOKENS), + ); + + FarmSetup { + blockchain_wrapper, + owner_address: owner_addr, + user_address: user_addr, + farm_wrapper, + } +} + +fn stake_farm( + farm_setup: &mut FarmSetup, + farm_in_amount: u64, + additional_farm_tokens: &[TxInputESDT], + expected_farm_token_nonce: u64, + expected_reward_per_share: u64, + expected_compounded_reward: u64, +) where + FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, +{ + let mut payments = Vec::with_capacity(1 + additional_farm_tokens.len()); + payments.push(TxInputESDT { + token_identifier: FARMING_TOKEN_ID.to_vec(), + nonce: 0, + value: rust_biguint!(farm_in_amount), + }); + payments.extend_from_slice(additional_farm_tokens); + + let mut expected_total_out_amount = 0; + for payment in payments.iter() { + expected_total_out_amount += payment.value.to_u64_digits()[0]; + } + + let b_mock = &mut farm_setup.blockchain_wrapper; + b_mock + .execute_esdt_multi_transfer( + &farm_setup.user_address, + &farm_setup.farm_wrapper, + &payments, + |sc| { + let payment = sc.stake_farm_endpoint(); + assert_eq!(payment.token_identifier, managed_token_id!(FARM_TOKEN_ID)); + assert_eq!(payment.token_nonce, expected_farm_token_nonce); + assert_eq!(payment.amount, managed_biguint!(expected_total_out_amount)); + }, + ) + .assert_ok(); + + let _ = DebugApi::dummy(); + let expected_attributes = StakingFarmTokenAttributes:: { + reward_per_share: managed_biguint!(expected_reward_per_share), + compounded_reward: managed_biguint!(expected_compounded_reward), + current_farm_amount: managed_biguint!(expected_total_out_amount), + }; + b_mock.check_nft_balance( + &farm_setup.user_address, + FARM_TOKEN_ID, + expected_farm_token_nonce, + &rust_biguint!(expected_total_out_amount), + &expected_attributes, + ); + + let _ = TxContextStack::static_pop(); +} + +fn unbond_farm( + farm_setup: &mut FarmSetup, + farm_token_nonce: u64, + farm_tokem_amount: u64, + expected_farming_token_out: u64, + expected_user_farming_token_balance: u64, +) where + FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, +{ + let b_mock = &mut farm_setup.blockchain_wrapper; + b_mock + .execute_esdt_transfer( + &farm_setup.user_address, + &farm_setup.farm_wrapper, + FARM_TOKEN_ID, + farm_token_nonce, + &rust_biguint!(farm_tokem_amount), + |sc| { + let payment = sc.unbond_farm(); + assert_eq!( + payment.token_identifier, + managed_token_id!(FARMING_TOKEN_ID) + ); + assert_eq!(payment.token_nonce, 0); + assert_eq!(payment.amount, managed_biguint!(expected_farming_token_out)); + }, + ) + .assert_ok(); + + b_mock.check_esdt_balance( + &farm_setup.user_address, + FARMING_TOKEN_ID, + &rust_biguint!(expected_user_farming_token_balance), + ); +} + +fn unstake_farm( + farm_setup: &mut FarmSetup, + farm_token_amount: u64, + farm_token_nonce: u64, + expected_rewards_out: u64, + expected_user_reward_token_balance: &RustBigUint, + expected_user_farming_token_balance: &RustBigUint, + expected_new_farm_token_nonce: u64, + expected_new_farm_token_amount: u64, + expected_new_farm_token_attributes: &UnbondSftAttributes, +) where + FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, +{ + let b_mock = &mut farm_setup.blockchain_wrapper; + b_mock + .execute_esdt_transfer( + &farm_setup.user_address, + &farm_setup.farm_wrapper, + FARM_TOKEN_ID, + farm_token_nonce, + &rust_biguint!(farm_token_amount), + |sc| { + let multi_result = sc.unstake_farm(); + + let (first_result, second_result) = multi_result.into_tuple(); + + assert_eq!( + first_result.token_identifier, + managed_token_id!(FARM_TOKEN_ID) + ); + assert_eq!(first_result.token_nonce, expected_new_farm_token_nonce); + assert_eq!( + first_result.amount, + managed_biguint!(expected_new_farm_token_amount) + ); + + assert_eq!( + second_result.token_identifier, + managed_token_id!(REWARD_TOKEN_ID) + ); + assert_eq!(second_result.token_nonce, 0); + assert_eq!(second_result.amount, managed_biguint!(expected_rewards_out)); + }, + ) + .assert_ok(); + + b_mock.check_nft_balance( + &farm_setup.user_address, + FARM_TOKEN_ID, + expected_new_farm_token_nonce, + &rust_biguint!(expected_new_farm_token_amount), + expected_new_farm_token_attributes, + ); + + b_mock.check_esdt_balance( + &farm_setup.user_address, + REWARD_TOKEN_ID, + expected_user_reward_token_balance, + ); + b_mock.check_esdt_balance( + &farm_setup.user_address, + FARMING_TOKEN_ID, + expected_user_farming_token_balance, + ); +} + +fn claim_rewards( + farm_setup: &mut FarmSetup, + farm_token_amount: u64, + farm_token_nonce: u64, + expected_reward_token_out: u64, + expected_user_reward_token_balance: &RustBigUint, + expected_user_farming_token_balance: &RustBigUint, + expected_farm_token_nonce_out: u64, + expected_reward_per_share: u64, +) where + FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, +{ + let b_mock = &mut farm_setup.blockchain_wrapper; + b_mock + .execute_esdt_transfer( + &farm_setup.user_address, + &farm_setup.farm_wrapper, + FARM_TOKEN_ID, + farm_token_nonce, + &rust_biguint!(farm_token_amount), + |sc| { + let multi_result = sc.claim_rewards(); + let (first_result, second_result) = multi_result.into_tuple(); + + assert_eq!( + first_result.token_identifier, + managed_token_id!(FARM_TOKEN_ID) + ); + assert_eq!(first_result.token_nonce, expected_farm_token_nonce_out); + assert_eq!(first_result.amount, managed_biguint!(farm_token_amount)); + + assert_eq!( + second_result.token_identifier, + managed_token_id!(REWARD_TOKEN_ID) + ); + assert_eq!(second_result.token_nonce, 0); + assert_eq!( + second_result.amount, + managed_biguint!(expected_reward_token_out) + ); + }, + ) + .assert_ok(); + + let _ = DebugApi::dummy(); + let expected_attributes = StakingFarmTokenAttributes:: { + reward_per_share: managed_biguint!(expected_reward_per_share), + compounded_reward: managed_biguint!(0), + current_farm_amount: managed_biguint!(farm_token_amount), + }; + + b_mock.check_nft_balance( + &farm_setup.user_address, + FARM_TOKEN_ID, + expected_farm_token_nonce_out, + &rust_biguint!(farm_token_amount), + &expected_attributes, + ); + b_mock.check_esdt_balance( + &farm_setup.user_address, + REWARD_TOKEN_ID, + expected_user_reward_token_balance, + ); + b_mock.check_esdt_balance( + &farm_setup.user_address, + FARMING_TOKEN_ID, + expected_user_farming_token_balance, + ); + + let _ = TxContextStack::static_pop(); +} + +fn check_farm_token_supply( + farm_setup: &mut FarmSetup, + expected_farm_token_supply: u64, +) where + FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, +{ + let b_mock = &mut farm_setup.blockchain_wrapper; + b_mock + .execute_query(&farm_setup.farm_wrapper, |sc| { + let actual_farm_supply = sc.farm_token_supply().get(); + assert_eq!( + managed_biguint!(expected_farm_token_supply), + actual_farm_supply + ); + }) + .assert_ok(); +} + +fn set_block_nonce(farm_setup: &mut FarmSetup, block_nonce: u64) +where + FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, +{ + farm_setup.blockchain_wrapper.set_block_nonce(block_nonce); +} + +fn set_block_epoch(farm_setup: &mut FarmSetup, block_epoch: u64) +where + FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, +{ + farm_setup.blockchain_wrapper.set_block_epoch(block_epoch); +} + +#[test] +fn test_farm_setup() { + let _ = setup_farm(farm_staking::contract_obj); +} + +#[test] +fn test_enter_farm() { + let mut farm_setup = setup_farm(farm_staking::contract_obj); + + let farm_in_amount = 100_000_000; + let expected_farm_token_nonce = 1; + stake_farm( + &mut farm_setup, + farm_in_amount, + &[], + expected_farm_token_nonce, + 0, + 0, + ); + check_farm_token_supply(&mut farm_setup, farm_in_amount); +} + +#[test] +fn test_unstake_farm() { + let mut farm_setup = setup_farm(farm_staking::contract_obj); + + let farm_in_amount = 100_000_000; + let expected_farm_token_nonce = 1; + stake_farm( + &mut farm_setup, + farm_in_amount, + &[], + expected_farm_token_nonce, + 0, + 0, + ); + check_farm_token_supply(&mut farm_setup, farm_in_amount); + + let current_block = 10; + let current_epoch = 5; + set_block_epoch(&mut farm_setup, current_epoch); + set_block_nonce(&mut farm_setup, current_block); + + let block_diff = current_block - 0; + let expected_rewards_unbounded = block_diff * PER_BLOCK_REWARD_AMOUNT; + + // ~= 4 * 10 = 40 + let expected_rewards_max_apr = + farm_in_amount * MAX_APR / MAX_PERCENT / BLOCKS_IN_YEAR * block_diff; + let expected_rewards = core::cmp::min(expected_rewards_unbounded, expected_rewards_max_apr); + assert_eq!(expected_rewards, 40); + + let expected_ride_token_balance = + rust_biguint!(USER_TOTAL_RIDE_TOKENS) - farm_in_amount + expected_rewards; + unstake_farm( + &mut farm_setup, + farm_in_amount, + expected_farm_token_nonce, + expected_rewards, + &expected_ride_token_balance, + &expected_ride_token_balance, + expected_farm_token_nonce + 1, + farm_in_amount, + &UnbondSftAttributes { + unlock_epoch: current_epoch + MIN_UNBOND_EPOCHS, + }, + ); + check_farm_token_supply(&mut farm_setup, 0); +} + +#[test] +fn test_claim_rewards() { + let mut farm_setup = setup_farm(farm_staking::contract_obj); + + let farm_in_amount = 100_000_000; + let expected_farm_token_nonce = 1; + stake_farm( + &mut farm_setup, + farm_in_amount, + &[], + expected_farm_token_nonce, + 0, + 0, + ); + check_farm_token_supply(&mut farm_setup, farm_in_amount); + + set_block_epoch(&mut farm_setup, 5); + set_block_nonce(&mut farm_setup, 10); + + // value taken from the "test_unstake_farm" test + let expected_reward_token_out = 40; + let expected_farming_token_balance = + rust_biguint!(USER_TOTAL_RIDE_TOKENS - farm_in_amount + expected_reward_token_out); + let expected_reward_per_share = 400_000; + claim_rewards( + &mut farm_setup, + farm_in_amount, + expected_farm_token_nonce, + expected_reward_token_out, + &expected_farming_token_balance, + &expected_farming_token_balance, + expected_farm_token_nonce + 1, + expected_reward_per_share, + ); + check_farm_token_supply(&mut farm_setup, farm_in_amount); +} + +fn steps_enter_farm_twice(farm_builder: FarmObjBuilder) -> FarmSetup +where + FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, +{ + let mut farm_setup = setup_farm(farm_builder); + + let farm_in_amount = 100_000_000; + let expected_farm_token_nonce = 1; + stake_farm( + &mut farm_setup, + farm_in_amount, + &[], + expected_farm_token_nonce, + 0, + 0, + ); + check_farm_token_supply(&mut farm_setup, farm_in_amount); + + set_block_epoch(&mut farm_setup, 5); + set_block_nonce(&mut farm_setup, 10); + + let second_farm_in_amount = 200_000_000; + let prev_farm_tokens = [TxInputESDT { + token_identifier: FARM_TOKEN_ID.to_vec(), + nonce: expected_farm_token_nonce, + value: rust_biguint!(farm_in_amount), + }]; + + let total_amount = farm_in_amount + second_farm_in_amount; + let first_reward_share = 0; + let second_reward_share = 400_000; + let expected_reward_per_share = (first_reward_share * farm_in_amount + + second_reward_share * second_farm_in_amount + + total_amount + - 1) + / total_amount; + + stake_farm( + &mut farm_setup, + second_farm_in_amount, + &prev_farm_tokens, + expected_farm_token_nonce + 1, + expected_reward_per_share, + 0, + ); + check_farm_token_supply(&mut farm_setup, total_amount); + + farm_setup +} + +#[test] +fn test_enter_farm_twice() { + let _ = steps_enter_farm_twice(farm_staking::contract_obj); +} + +#[test] +fn test_exit_farm_after_enter_twice() { + let mut farm_setup = steps_enter_farm_twice(farm_staking::contract_obj); + let farm_in_amount = 100_000_000; + let second_farm_in_amount = 200_000_000; + + set_block_epoch(&mut farm_setup, 8); + set_block_nonce(&mut farm_setup, 25); + + let _current_farm_supply = farm_in_amount; + + let expected_rewards = 83; + let expected_ride_token_balance = + rust_biguint!(USER_TOTAL_RIDE_TOKENS) - farm_in_amount - second_farm_in_amount + + expected_rewards; + unstake_farm( + &mut farm_setup, + farm_in_amount, + 2, + expected_rewards, + &expected_ride_token_balance, + &expected_ride_token_balance, + 3, + farm_in_amount, + &UnbondSftAttributes { + unlock_epoch: 8 + MIN_UNBOND_EPOCHS, + }, + ); + check_farm_token_supply(&mut farm_setup, second_farm_in_amount); +} + +#[test] +fn test_unbond() { + let mut farm_setup = setup_farm(farm_staking::contract_obj); + + let farm_in_amount = 100_000_000; + let expected_farm_token_nonce = 1; + stake_farm( + &mut farm_setup, + farm_in_amount, + &[], + expected_farm_token_nonce, + 0, + 0, + ); + check_farm_token_supply(&mut farm_setup, farm_in_amount); + + let current_block = 10; + let current_epoch = 5; + set_block_epoch(&mut farm_setup, current_epoch); + set_block_nonce(&mut farm_setup, current_block); + + let block_diff = current_block - 0; + let expected_rewards_unbounded = block_diff * PER_BLOCK_REWARD_AMOUNT; + + // ~= 4 * 10 = 40 + let expected_rewards_max_apr = + farm_in_amount * MAX_APR / MAX_PERCENT / BLOCKS_IN_YEAR * block_diff; + let expected_rewards = core::cmp::min(expected_rewards_unbounded, expected_rewards_max_apr); + assert_eq!(expected_rewards, 40); + + let expected_ride_token_balance = + rust_biguint!(USER_TOTAL_RIDE_TOKENS) - farm_in_amount + expected_rewards; + unstake_farm( + &mut farm_setup, + farm_in_amount, + expected_farm_token_nonce, + expected_rewards, + &expected_ride_token_balance, + &expected_ride_token_balance, + expected_farm_token_nonce + 1, + farm_in_amount, + &UnbondSftAttributes { + unlock_epoch: current_epoch + MIN_UNBOND_EPOCHS, + }, + ); + check_farm_token_supply(&mut farm_setup, 0); + + set_block_epoch(&mut farm_setup, current_epoch + MIN_UNBOND_EPOCHS); + + unbond_farm( + &mut farm_setup, + expected_farm_token_nonce + 1, + farm_in_amount, + farm_in_amount, + USER_TOTAL_RIDE_TOKENS + expected_rewards, + ); +} diff --git a/legacy-contracts/farm-staking-proxy-v1.3/wasm/Cargo.toml b/legacy-contracts/farm-staking-proxy-v1.3/wasm/Cargo.toml new file mode 100644 index 000000000..6b46bff47 --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/wasm/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "farm-staking-legacy-wasm" +version = "0.0.0" +authors = [ "you",] +edition = "2018" +publish = false + +[lib] +crate-type = [ "cdylib",] + +[workspace] +members = [ ".",] + +[dev-dependencies] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" + +[dependencies.farm-staking-legacy] +default-features = false +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "=0.48.1" diff --git a/legacy-contracts/farm-staking-proxy-v1.3/wasm/src/lib.rs b/legacy-contracts/farm-staking-proxy-v1.3/wasm/src/lib.rs new file mode 100644 index 000000000..20748251d --- /dev/null +++ b/legacy-contracts/farm-staking-proxy-v1.3/wasm/src/lib.rs @@ -0,0 +1,59 @@ +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +#![no_std] + +multiversx_sc_wasm_adapter::endpoints! { + farm_staking + ( + callBack + addAddressToWhitelist + calculateRewardsForGivenPosition + claimRewards + claimRewardsWithNewValue + compoundRewards + end_produce_rewards + getAccumulatedRewards + getAnnualPercentageRewards + getBurnGasLimit + getDivisionSafetyConstant + getFarmTokenId + getFarmTokenSupply + getFarmingTokenId + getLastErrorMessage + getLastRewardBlockNonce + getLockedAssetFactoryManagedAddress + getMinUnbondEpochs + getMinimumFarmingEpoch + getPairContractManagedAddress + getPenaltyPercent + getPerBlockRewardAmount + getRewardCapacity + getRewardPerShare + getRewardTokenId + getState + getTransferExecGasLimit + isWhitelisted + mergeFarmTokens + pause + registerFarmToken + removeAddressFromWhitelist + resume + setLocalRolesFarmToken + setMaxApr + setMinUnbondEpochs + setPerBlockRewardAmount + set_burn_gas_limit + set_minimum_farming_epochs + set_penalty_percent + set_transfer_exec_gas_limit + stakeFarm + stakeFarmThroughProxy + startProduceRewards + topUpRewards + unbondFarm + unstakeFarm + unstakeFarmThroughProxy + ) +} diff --git a/locked-asset/interaction/testnet.snippets.sh b/locked-asset/interaction/testnet.snippets.sh index 856c79648..ce6841004 100644 --- a/locked-asset/interaction/testnet.snippets.sh +++ b/locked-asset/interaction/testnet.snippets.sh @@ -1,4 +1,4 @@ -WALLET_PEM="~/Documents/shared_folder/elrond_testnet_wallet.pem" +WALLET_PEM="~/Documents/shared_folder/multiversx_testnet_wallet.pem" DEPLOY_TRANSACTION=$(erdpy data load --key=deployTransaction-devnet) DEPLOY_GAS="1000000000" PROXY="https://testnet-gateway.multiversx.com"