diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index c166a6cfb..f50d01216 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -20,9 +20,9 @@ permissions: jobs: contracts: name: Contracts - uses: multiversx/mx-sc-actions/.github/workflows/contracts.yml@v3.3.1 + uses: multiversx/mx-sc-actions/.github/workflows/contracts.yml@v4.2.1 with: - rust-toolchain: nightly-2024-05-22 + rust-toolchain: stable coverage-args: --ignore-filename-regex='/.cargo/git' --output ./coverage.md secrets: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3bb9fa310..8672e0210 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ permissions: jobs: build: - uses: multiversx/mx-sc-actions/.github/workflows/reproducible-build.yml@v3.3.1 + uses: multiversx/mx-sc-actions/.github/workflows/reproducible-build.yml@v4.2.1 with: image_tag: v7.0.0 attach_to_existing_release: true diff --git a/Cargo.lock b/Cargo.lock index 724f6ca9d..4b4c525d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -530,8 +530,11 @@ dependencies = [ "multiversx-sc-modules", "multiversx-sc-scenario", "num-bigint", + "original_owner_helper", "pair", "pausable", + "permissions-hub", + "permissions_hub_module", "permissions_module", "rewards", "sc_whitelist_module", @@ -586,8 +589,11 @@ dependencies = [ "multiversx-sc-modules", "multiversx-sc-scenario", "num-bigint", + "original_owner_helper", "pair", "pausable", + "permissions-hub", + "permissions_hub_module", "permissions_module", "rewards", "sc_whitelist_module", @@ -628,6 +634,8 @@ dependencies = [ "num-bigint", "pair", "pausable", + "permissions-hub", + "permissions_hub_module", "rewards", "sc_whitelist_module", "simple-lock", @@ -766,7 +774,10 @@ dependencies = [ "multiversx-sc-modules", "multiversx-sc-scenario", "num-bigint", + "original_owner_helper", "pausable", + "permissions-hub", + "permissions_hub_module", "permissions_module", "rewards", "sc_whitelist_module", @@ -1365,6 +1376,14 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "original_owner_helper" +version = "0.0.0" +dependencies = [ + "common_structs", + "multiversx-sc", +] + [[package]] name = "pair" version = "0.0.0" @@ -1443,6 +1462,31 @@ dependencies = [ "pause-all", ] +[[package]] +name = "permissions-hub" +version = "0.0.0" +dependencies = [ + "multiversx-sc", + "multiversx-sc-scenario", + "num-bigint", +] + +[[package]] +name = "permissions-hub-meta" +version = "0.0.0" +dependencies = [ + "multiversx-sc-meta-lib", + "permissions-hub", +] + +[[package]] +name = "permissions_hub_module" +version = "0.0.0" +dependencies = [ + "multiversx-sc", + "permissions-hub", +] + [[package]] name = "permissions_module" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 81e48c99e..b36732c3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ members = [ "dex/proxy-deployer/meta", "dex/pair-mock", "dex/pair-mock/meta", + "dex/permissions-hub", + "dex/permissions-hub/meta", "energy-integration/energy-factory-mock", "energy-integration/energy-factory-mock/meta", diff --git a/common/common_structs/src/farm_types.rs b/common/common_structs/src/farm_types.rs index d7d85e20b..676b26e36 100644 --- a/common/common_structs/src/farm_types.rs +++ b/common/common_structs/src/farm_types.rs @@ -80,6 +80,8 @@ pub trait FarmToken { fn get_compounded_rewards(&self) -> BigUint; fn get_initial_farming_tokens(&self) -> BigUint; + + fn get_original_owner(&self) -> ManagedAddress; } impl FarmToken for FarmTokenAttributes { @@ -97,4 +99,9 @@ impl FarmToken for FarmTokenAttributes { fn get_initial_farming_tokens(&self) -> BigUint { &self.current_farm_amount - &self.compounded_reward } + + #[inline] + fn get_original_owner(&self) -> ManagedAddress { + self.original_owner.clone() + } } diff --git a/common/modules/original_owner_helper/Cargo.toml b/common/modules/original_owner_helper/Cargo.toml new file mode 100644 index 000000000..21fa393dc --- /dev/null +++ b/common/modules/original_owner_helper/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "original_owner_helper" +version = "0.0.0" +authors = ["MultiversX "] +edition = "2021" + +[lib] +path = "src/lib.rs" + +[dependencies.multiversx-sc] +version = "=0.53.2" +features = ["esdt-token-payment-legacy-decode"] + +[dependencies.common_structs] +path = "../../common_structs" diff --git a/common/modules/original_owner_helper/src/lib.rs b/common/modules/original_owner_helper/src/lib.rs new file mode 100644 index 000000000..3f5126929 --- /dev/null +++ b/common/modules/original_owner_helper/src/lib.rs @@ -0,0 +1,62 @@ +#![no_std] + +multiversx_sc::imports!(); + +use common_structs::{FarmToken, PaymentsVec}; + +#[multiversx_sc::module] +pub trait OriginalOwnerHelperModule { + fn check_and_return_original_owner + TopDecode>( + &self, + payments: &PaymentsVec, + farm_token_mapper: &NonFungibleTokenMapper, + ) -> ManagedAddress { + let mut original_owner = ManagedAddress::zero(); + for payment in payments.iter() { + let attributes: T = farm_token_mapper.get_token_attributes(payment.token_nonce); + let payment_original_owner = attributes.get_original_owner(); + + if original_owner.is_zero() { + original_owner = payment_original_owner; + } else { + require!( + original_owner == payment_original_owner, + "All position must have the same original owner" + ); + } + } + + require!( + !original_owner.is_zero(), + "Original owner could not be identified" + ); + + original_owner + } + + fn check_additional_payments_original_owner + TopDecode>( + &self, + user: &ManagedAddress, + payments: &PaymentsVec, + farm_token_mapper: &NonFungibleTokenMapper, + ) { + if payments.len() == 1 { + return; + } + + let farm_token_id = farm_token_mapper.get_token_id(); + for payment in payments.into_iter() { + if payment.token_identifier != farm_token_id { + continue; + } + + let attributes: T = farm_token_mapper.get_token_attributes(payment.token_nonce); + let payment_original_owner = attributes.get_original_owner(); + + require!( + user == &payment_original_owner, + "Provided address is not the same as the original owner" + ); + } + } +} diff --git a/common/modules/permissions_hub_module/Cargo.toml b/common/modules/permissions_hub_module/Cargo.toml new file mode 100644 index 000000000..503fe9d60 --- /dev/null +++ b/common/modules/permissions_hub_module/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "permissions_hub_module" +version = "0.0.0" +authors = ["MultiversX "] +edition = "2021" + +[lib] +path = "src/permissions_hub_module.rs" + +[dependencies.permissions-hub] +path = "../../../dex/permissions-hub" + +[dependencies.multiversx-sc] +version = "=0.53.2" +features = ["esdt-token-payment-legacy-decode"] diff --git a/common/modules/permissions_hub_module/src/permissions_hub_module.rs b/common/modules/permissions_hub_module/src/permissions_hub_module.rs new file mode 100644 index 000000000..3b7832d06 --- /dev/null +++ b/common/modules/permissions_hub_module/src/permissions_hub_module.rs @@ -0,0 +1,32 @@ +#![no_std] + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +#[multiversx_sc::module] +pub trait PermissionsHubModule { + fn require_user_whitelisted(&self, user: &ManagedAddress, authorized_address: &ManagedAddress) { + let permissions_hub_address = self.permissions_hub_address().get(); + let is_whitelisted: bool = self + .permissions_hub_proxy(permissions_hub_address) + .is_whitelisted(user, authorized_address) + .execute_on_dest_context(); + + require!(is_whitelisted, "Caller is not whitelisted by the user"); + } + + #[only_owner] + #[endpoint(setPermissionsHubAddress)] + fn set_permissions_hub_address(&self, address: ManagedAddress) { + self.permissions_hub_address().set(&address); + } + + #[proxy] + fn permissions_hub_proxy( + &self, + sc_address: ManagedAddress, + ) -> permissions_hub::Proxy; + + #[storage_mapper("permissionsHubAddress")] + fn permissions_hub_address(&self) -> SingleValueMapper; +} diff --git a/dex/farm-with-locked-rewards/Cargo.toml b/dex/farm-with-locked-rewards/Cargo.toml index eedd45925..d39dd6ed1 100644 --- a/dex/farm-with-locked-rewards/Cargo.toml +++ b/dex/farm-with-locked-rewards/Cargo.toml @@ -41,6 +41,12 @@ path = "../../common/modules/utils" [dependencies.permissions_module] path = "../../common/modules/permissions_module" +[dependencies.permissions_hub_module] +path = "../../common/modules/permissions_hub_module" + +[dependencies.original_owner_helper] +path = "../../common/modules/original_owner_helper" + [dependencies.sc_whitelist_module] path = "../../common/modules/sc_whitelist_module" @@ -74,6 +80,9 @@ path = "../../locked-asset/energy-factory" [dependencies.energy-query] path = "../../energy-integration/common-modules/energy-query" +[dependencies.permissions-hub] +path = "../permissions-hub" + [dependencies.multiversx-sc] version = "=0.53.2" features = ["esdt-token-payment-legacy-decode"] diff --git a/dex/farm-with-locked-rewards/README.md b/dex/farm-with-locked-rewards/README.md index fd70b0aa2..4b06389fe 100644 --- a/dex/farm-with-locked-rewards/README.md +++ b/dex/farm-with-locked-rewards/README.md @@ -110,3 +110,72 @@ The interaction scripts for this contract are located in the dex subdirectory of ## Deployment The deployment of this contract is done using interaction scripts and it is managed by its admin. + +# Farm With Locked Rewards OnBehalf Operations + +## Abstract + +The Farm With Locked Rewards OnBehalf operations extend the Farm With Locked Rewards smart contract with the ability to allow whitelisted contracts to perform actions on behalf of users, enabling enhanced protocol composability while maintaining security through integration with the Permissions Hub. + +## Introduction + +This module allows third-party contracts to perform farm operations on behalf of users, after being explicitly whitelisted through the Permissions Hub. Users maintain full control over their assets by managing contract permissions, while protocols can build more complex DeFi interactions. + +## Endpoints + +### enterFarmOnBehalf + +```rust +#[payable("*")] +#[endpoint(enterFarmOnBehalf)] +fn enter_farm_on_behalf(&self, user: ManagedAddress) -> EnterFarmResultType +``` + +The enterFarmOnBehalf function allows whitelisted contracts to enter farm positions on behalf of users. It receives several arguments: + +- __user__ - The address of the user for whom the operation is being performed. This address must have whitelisted the caller contract through the Permissions Hub. +- __payment__ - The tokens to be used are received as payment in the transaction. + +The function performs the following steps: +1. Validates that the caller is whitelisted by the user through Permissions Hub +2. Processes the farming tokens payment +3. Claims any pending boosted rewards for the original owner +4. Performs the enter farm operation on behalf of the original owner +5. Sends the new farm token to the caller +6. Sends the locked rewards, if any, to the original owner +7. Updates energy and progress for the original owner + +### claimRewardsOnBehalf + +```rust +#[payable("*")] +#[endpoint(claimRewardsOnBehalf)] +fn claim_rewards_on_behalf(&self) -> ClaimRewardsResultType +``` + +The claimRewardsOnBehalf function enables whitelisted contracts to claim rewards on behalf of the users. This function does not require any address parameter, as the original owner is read from the farm position metadata. The operation requires: + +- __payment__ - The farm token must be received as payment in the transaction. + +The function performs these steps: +1. Processes the farm token payment +2. Extracts the original owner from the farm token attributes +3. Validates that the caller is whitelisted by the original owner +4. Claims and sends locked rewards to the original owner +5. Sends the new farm token to the caller + +## exitOnBehalf +The exit operation remains under the direct control of the position owner to ensure maximum security. When third-party contracts interact with farming positions through onBehalf operations, they receive and hold the position tokens. These tokens maintain the original owner information in their attributes, protecting the user's ownership rights. To exit their position, users must first reclaim their position tokens from the third-party contract through that protocol's specific mechanisms. Once users have regained control of their position tokens, they can perform the standard exit operation directly through the specific xExchange contract. +This design ensures users maintain ultimate control over their funds while allowing protocols to build complex DeFi interactions. + +## Storage + +The contract relies on the Permissions Hub for permission management, thus no additional storage, other than the one holding the Permissions Hub SC address, is required. All whitelisting data is managed through the Permissions Hub contract. + +## Deployment + +The onBehalf features are part of the core farm contract and require: + +1. A deployed Permissions Hub contract +2. Configuration of the Permissions Hub address in the farm contract +3. User whitelisting of contracts that will perform onBehalf operations diff --git a/dex/farm-with-locked-rewards/src/external_interaction.rs b/dex/farm-with-locked-rewards/src/external_interaction.rs new file mode 100644 index 000000000..be29004f0 --- /dev/null +++ b/dex/farm-with-locked-rewards/src/external_interaction.rs @@ -0,0 +1,110 @@ +multiversx_sc::imports!(); + +use common_structs::FarmTokenAttributes; +use farm::{ + base_functions::{self, ClaimRewardsResultType}, + exit_penalty, EnterFarmResultType, +}; + +use crate::NoMintWrapper; + +#[multiversx_sc::module] +pub trait ExternalInteractionsModule: + rewards::RewardsModule + + config::ConfigModule + + token_send::TokenSendModule + + farm_token::FarmTokenModule + + pausable::PausableModule + + permissions_module::PermissionsModule + + permissions_hub_module::PermissionsHubModule + + original_owner_helper::OriginalOwnerHelperModule + + sc_whitelist_module::SCWhitelistModule + + events::EventsModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + base_functions::BaseFunctionsModule + + exit_penalty::ExitPenaltyModule + + locking_module::lock_with_energy_module::LockWithEnergyModule + + 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 + + farm_boosted_yields::FarmBoostedYieldsModule + + farm_boosted_yields::boosted_yields_factors::BoostedYieldsFactorsModule + + 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 + + utils::UtilsModule +{ + #[payable("*")] + #[endpoint(enterFarmOnBehalf)] + fn enter_farm_on_behalf(&self, user: ManagedAddress) -> EnterFarmResultType { + let caller = self.blockchain().get_caller(); + self.require_user_whitelisted(&user, &caller); + + let payments = self.get_non_empty_payments(); + let farm_token_mapper = self.farm_token(); + self.check_additional_payments_original_owner::>( + &user, + &payments, + &farm_token_mapper, + ); + + let boosted_rewards = self.claim_only_boosted_payment(&user); + let new_farm_token = self.enter_farm::>(user.clone()); + self.send_payment_non_zero(&caller, &new_farm_token); + + let locked_rewards_payment = if boosted_rewards == 0 { + let locked_token_id = self.get_locked_token_id(); + EsdtTokenPayment::new(locked_token_id, 0, boosted_rewards) + } else { + self.lock_virtual( + self.reward_token_id().get(), + boosted_rewards, + user.clone(), + user.clone(), + ) + }; + + self.update_energy_and_progress(&user); + + (new_farm_token, locked_rewards_payment).into() + } + + #[payable("*")] + #[endpoint(claimRewardsOnBehalf)] + fn claim_rewards_on_behalf(&self) -> ClaimRewardsResultType { + let payments = self.get_non_empty_payments(); + let farm_token_mapper = self.farm_token(); + let caller = self.blockchain().get_caller(); + let user = self.check_and_return_original_owner::>( + &payments, + &farm_token_mapper, + ); + self.require_user_whitelisted(&user, &caller); + + let claim_rewards_result = self.claim_rewards::>(user.clone()); + + self.send_payment_non_zero(&caller, &claim_rewards_result.new_farm_token); + + let rewards_payment = claim_rewards_result.rewards; + let locked_rewards_payment = if rewards_payment.amount == 0 { + let locked_token_id = self.get_locked_token_id(); + EsdtTokenPayment::new(locked_token_id, 0, rewards_payment.amount) + } else { + self.lock_virtual( + rewards_payment.token_identifier, + rewards_payment.amount, + user.clone(), + user, + ) + }; + + (claim_rewards_result.new_farm_token, locked_rewards_payment).into() + } +} diff --git a/dex/farm-with-locked-rewards/src/lib.rs b/dex/farm-with-locked-rewards/src/lib.rs index 7434704d1..69b5dc4a1 100644 --- a/dex/farm-with-locked-rewards/src/lib.rs +++ b/dex/farm-with-locked-rewards/src/lib.rs @@ -3,6 +3,8 @@ multiversx_sc::imports!(); multiversx_sc::derive_imports!(); +pub mod external_interaction; + use common_structs::FarmTokenAttributes; use contexts::storage_cache::StorageCache; use core::marker::PhantomData; @@ -27,11 +29,14 @@ pub trait Farm: + utils::UtilsModule + pausable::PausableModule + permissions_module::PermissionsModule + + permissions_hub_module::PermissionsHubModule + + original_owner_helper::OriginalOwnerHelperModule + sc_whitelist_module::SCWhitelistModule + events::EventsModule + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + farm::base_functions::BaseFunctionsModule + farm::exit_penalty::ExitPenaltyModule + + external_interaction::ExternalInteractionsModule + farm_base_impl::base_farm_init::BaseFarmInitModule + farm_base_impl::base_farm_validation::BaseFarmValidationModule + farm_base_impl::enter_farm::BaseEnterFarmModule diff --git a/dex/farm-with-locked-rewards/tests/farm_with_locked_rewards_setup/mod.rs b/dex/farm-with-locked-rewards/tests/farm_with_locked_rewards_setup/mod.rs index 5f6015658..4d2a2819a 100644 --- a/dex/farm-with-locked-rewards/tests/farm_with_locked_rewards_setup/mod.rs +++ b/dex/farm-with-locked-rewards/tests/farm_with_locked_rewards_setup/mod.rs @@ -8,6 +8,7 @@ use multiversx_sc::{ types::{Address, BigInt, EsdtLocalRole, MultiValueEncoded}, }; use multiversx_sc_scenario::{ + imports::TxTokenTransfer, managed_address, managed_biguint, managed_token_id, rust_biguint, whitebox_legacy::{BlockchainStateWrapper, ContractObjWrapper}, DebugApi, @@ -20,10 +21,12 @@ use energy_factory::{energy::EnergyModule, SimpleLockEnergy}; use energy_query::{Energy, EnergyQueryModule}; use farm_boosted_yields::boosted_yields_factors::BoostedYieldsFactorsModule; use farm_token::FarmTokenModule; -use farm_with_locked_rewards::Farm; +use farm_with_locked_rewards::{external_interaction::ExternalInteractionsModule, Farm}; use locking_module::lock_with_energy_module::LockWithEnergyModule; use multiversx_sc_modules::pause::PauseModule; use pausable::{PausableModule, State}; +use permissions_hub::PermissionsHub; +use permissions_hub_module::PermissionsHubModule; use rewards::RewardsModule; use sc_whitelist_module::SCWhitelistModule; use simple_lock::locked_token::LockedTokenModule; @@ -35,8 +38,9 @@ pub static LEGACY_LOCKED_TOKEN_ID: &[u8] = b"LEGACY-123456"; pub static FARMING_TOKEN_ID: &[u8] = b"LPTOK-123456"; pub static FARM_TOKEN_ID: &[u8] = b"FARM-123456"; const DIV_SAFETY: u64 = 1_000_000_000_000; -const PER_BLOCK_REWARD_AMOUNT: u64 = 1_000; +pub const PER_BLOCK_REWARD_AMOUNT: u64 = 1_000; const FARMING_TOKEN_BALANCE: u64 = 100_000_000; +pub const MAX_PERCENTAGE: u64 = 10_000; // 100% pub const BOOSTED_YIELDS_PERCENTAGE: u64 = 2_500; // 25% pub const USER_REWARDS_BASE_CONST: u64 = 10; pub const USER_REWARDS_ENERGY_CONST: u64 = 3; @@ -57,10 +61,11 @@ pub struct RawFarmTokenAttributes { pub original_owner_bytes: [u8; 32], } -pub struct FarmSetup +pub struct FarmSetup where FarmObjBuilder: 'static + Copy + Fn() -> farm_with_locked_rewards::ContractObj, EnergyFactoryBuilder: 'static + Copy + Fn() -> energy_factory::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, { pub b_mock: BlockchainStateWrapper, pub owner: Address, @@ -72,14 +77,22 @@ where ContractObjWrapper, FarmObjBuilder>, pub energy_factory_wrapper: ContractObjWrapper, EnergyFactoryBuilder>, + pub permissions_hub_wrapper: + ContractObjWrapper, PermissionsHubObjBuilder>, } -impl FarmSetup +impl + FarmSetup where FarmObjBuilder: 'static + Copy + Fn() -> farm_with_locked_rewards::ContractObj, EnergyFactoryBuilder: 'static + Copy + Fn() -> energy_factory::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, { - pub fn new(farm_builder: FarmObjBuilder, energy_factory_builder: EnergyFactoryBuilder) -> Self { + pub fn new( + farm_builder: FarmObjBuilder, + energy_factory_builder: EnergyFactoryBuilder, + 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); @@ -105,6 +118,19 @@ where "fees collector mock", ); + 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, &energy_factory_wrapper, &rust_zero, |sc| { let mut lock_options = MultiValueEncoded::new(); @@ -162,6 +188,9 @@ where sc.set_energy_factory_address(managed_address!( energy_factory_wrapper.address_ref() )); + sc.set_permissions_hub_address(managed_address!( + permissions_hub_wrapper.address_ref() + )); }) .assert_ok(); @@ -227,6 +256,7 @@ where last_farm_token_nonce: 0, farm_wrapper, energy_factory_wrapper, + permissions_hub_wrapper, } } @@ -450,6 +480,99 @@ where result } + pub fn enter_farm_on_behalf( + &mut self, + caller: &Address, + user: &Address, + farming_token_amount: u64, + farm_token_nonce: u64, + farm_token_amount: u64, + ) { + let mut payments = Vec::new(); + payments.push(TxTokenTransfer { + token_identifier: FARMING_TOKEN_ID.to_vec(), + nonce: 0, + value: rust_biguint!(farming_token_amount), + }); + + if farm_token_nonce > 0 { + payments.push(TxTokenTransfer { + token_identifier: FARM_TOKEN_ID.to_vec(), + nonce: farm_token_nonce, + value: rust_biguint!(farm_token_amount), + }); + } + + let b_mock = &mut self.b_mock; + b_mock + .execute_esdt_multi_transfer(caller, &self.farm_wrapper, &payments, |sc| { + let enter_farm_result = sc.enter_farm_on_behalf(managed_address!(user)); + let (out_farm_token, _reward_token) = enter_farm_result.into_tuple(); + assert_eq!( + out_farm_token.token_identifier, + managed_token_id!(FARM_TOKEN_ID) + ); + assert_eq!( + out_farm_token.amount, + managed_biguint!(farming_token_amount + farm_token_amount) + ); + }) + .assert_ok(); + } + + pub fn claim_rewards_on_behalf( + &mut self, + caller: &Address, + farm_token_nonce: u64, + farm_token_amount: u64, + expected_reward_token_nonce: u64, + ) -> u64 { + let mut result = 0; + self.b_mock + .execute_esdt_transfer( + caller, + &self.farm_wrapper, + FARM_TOKEN_ID, + farm_token_nonce, + &rust_biguint!(farm_token_amount), + |sc| { + let (out_farm_token, out_reward_token) = + sc.claim_rewards_on_behalf().into_tuple(); + assert_eq!( + out_farm_token.token_identifier, + managed_token_id!(FARM_TOKEN_ID) + ); + assert_eq!(out_farm_token.amount, managed_biguint!(farm_token_amount)); + + assert_eq!( + out_reward_token.token_identifier, + managed_token_id!(LOCKED_REWARD_TOKEN_ID) + ); + assert_eq!(out_reward_token.token_nonce, expected_reward_token_nonce); + + result = out_reward_token.amount.to_u64().unwrap(); + }, + ) + .assert_ok(); + + result + } + + pub fn whitelist_address_on_behalf(&mut self, user: &Address, address_to_whitelist: &Address) { + self.b_mock + .execute_tx( + user, + &self.permissions_hub_wrapper, + &rust_biguint!(0), + |sc| { + let mut addresses = MultiValueEncoded::new(); + addresses.push(managed_address!(address_to_whitelist)); + sc.whitelist(addresses); + }, + ) + .assert_ok(); + } + pub fn check_farm_token_supply(&mut self, expected_farm_token_supply: u64) { let b_mock = &mut self.b_mock; b_mock diff --git a/dex/farm-with-locked-rewards/tests/farm_with_locked_rewards_test.rs b/dex/farm-with-locked-rewards/tests/farm_with_locked_rewards_test.rs index 649461f18..fea8f4fcd 100644 --- a/dex/farm-with-locked-rewards/tests/farm_with_locked_rewards_test.rs +++ b/dex/farm-with-locked-rewards/tests/farm_with_locked_rewards_test.rs @@ -2,6 +2,9 @@ use common_structs::FarmTokenAttributes; use farm_with_locked_rewards::Farm; +use farm_with_locked_rewards_setup::{ + FARMING_TOKEN_ID, MAX_PERCENTAGE, PER_BLOCK_REWARD_AMOUNT, REWARD_TOKEN_ID, +}; use multiversx_sc::{codec::Empty, imports::OptionalValue}; use multiversx_sc_scenario::{managed_address, managed_biguint, rust_biguint, DebugApi}; use simple_lock::locked_token::LockedTokenAttributes; @@ -18,6 +21,7 @@ fn farm_with_no_boost_no_proxy_test() { let mut farm_setup = FarmSetup::new( farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, ); // first user enter farm @@ -118,6 +122,7 @@ fn farm_with_boosted_yields_no_proxy_test() { let mut farm_setup = FarmSetup::new( farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -237,6 +242,7 @@ fn total_farm_position_claim_with_locked_rewards_test() { let mut farm_setup = FarmSetup::new( farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -344,6 +350,7 @@ fn claim_only_boosted_rewards_per_week_test() { let mut farm_setup = FarmSetup::new( farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -407,6 +414,7 @@ fn claim_rewards_per_week_test() { let mut farm_setup = FarmSetup::new( farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -473,6 +481,7 @@ fn claim_boosted_rewards_with_zero_position_test() { let mut farm_setup = FarmSetup::new( farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -544,6 +553,7 @@ fn claim_boosted_rewards_user_energy_not_registered_test() { let mut farm_setup = FarmSetup::new( farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -575,3 +585,127 @@ fn claim_boosted_rewards_user_energy_not_registered_test() { // Rewards computation is out of scope farm_setup.claim_boosted_rewards_for_user(&first_user, &first_user, 0); } + +#[test] +fn test_multiple_positions_on_behalf() { + DebugApi::dummy(); + + let mut farm_setup = FarmSetup::new( + farm_with_locked_rewards::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); + + farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); + farm_setup.set_boosted_yields_factors(); + let mut block_nonce = 0u64; + farm_setup.b_mock.set_block_nonce(block_nonce); + + // new external user + let external_user = farm_setup.b_mock.create_user_account(&rust_biguint!(0)); + farm_setup.set_user_energy(&external_user, 1_000, 1, 1); + + // authorized address + let farm_token_amount = 100_000_000; + let authorized_address = farm_setup.first_user.clone(); + farm_setup.b_mock.set_esdt_balance( + &authorized_address, + FARMING_TOKEN_ID, + &rust_biguint!(farm_token_amount * 2), + ); + + farm_setup.whitelist_address_on_behalf(&external_user, &authorized_address); + + farm_setup.check_farm_token_supply(0); + farm_setup.enter_farm_on_behalf(&authorized_address, &external_user, farm_token_amount, 0, 0); + farm_setup.check_farm_token_supply(farm_token_amount); + + let block_nonce_diff = 10u64; + block_nonce += block_nonce_diff; + farm_setup.b_mock.set_block_nonce(block_nonce); + + // 1000 rewards per block + let total_rewards = PER_BLOCK_REWARD_AMOUNT * block_nonce_diff; + let base_rewards = + total_rewards * (MAX_PERCENTAGE - BOOSTED_YIELDS_PERCENTAGE) / MAX_PERCENTAGE; + let boosted_rewards = total_rewards * BOOSTED_YIELDS_PERCENTAGE / MAX_PERCENTAGE; + + // Only base rewards are given + farm_setup + .b_mock + .check_esdt_balance(&external_user, REWARD_TOKEN_ID, &rust_biguint!(0)); + farm_setup.claim_rewards_on_behalf(&authorized_address, 1, farm_token_amount, 1); + farm_setup + .b_mock + .check_nft_balance::>( + &external_user, + LOCKED_REWARD_TOKEN_ID, + 1, + &rust_biguint!(base_rewards), + None, + ); + + // random tx on end of week 1, to cummulate rewards + farm_setup.b_mock.set_block_epoch(6); + let temp_user = farm_setup.third_user.clone(); + farm_setup.set_user_energy(&external_user, 1_000, 6, 1); + farm_setup.set_user_energy(&temp_user, 1, 6, 1); + farm_setup.last_farm_token_nonce = 2; + farm_setup.enter_farm(&temp_user, 1); + farm_setup.exit_farm(&temp_user, 3, 1); + + // advance 1 week + block_nonce += block_nonce_diff; + farm_setup.b_mock.set_block_nonce(block_nonce); + farm_setup.b_mock.set_block_epoch(10); + farm_setup.set_user_energy(&external_user, 1_000, 10, 1); + + // enter farm again for the same user (with additional payment) + farm_setup.check_farm_token_supply(farm_token_amount); + farm_setup.enter_farm_on_behalf( + &authorized_address, + &external_user, + farm_token_amount, + 2, // nonce 2 as the user already claimed with this position + farm_token_amount, + ); + farm_setup.check_farm_token_supply(farm_token_amount * 2); + farm_setup + .b_mock + .check_nft_balance::>( + &external_user, + LOCKED_REWARD_TOKEN_ID, + 1, + &rust_biguint!(base_rewards + boosted_rewards), + None, + ); + + farm_setup.claim_rewards_on_behalf(&authorized_address, 4, farm_token_amount * 2, 1); + farm_setup.check_farm_token_supply(farm_token_amount * 2); + + farm_setup + .b_mock + .check_nft_balance::>( + &external_user, + LOCKED_REWARD_TOKEN_ID, + 1, + &rust_biguint!(total_rewards + base_rewards), + None, + ); + + let farm_token_attributes: FarmTokenAttributes = FarmTokenAttributes { + reward_per_share: managed_biguint!(150_000_000u64), + entering_epoch: 10u64, + compounded_reward: managed_biguint!(0), + current_farm_amount: managed_biguint!(farm_token_amount * 2), + original_owner: managed_address!(&external_user), + }; + + farm_setup.b_mock.check_nft_balance( + &authorized_address, + FARM_TOKEN_ID, + 5, + &rust_biguint!(farm_token_amount * 2), + Some(&farm_token_attributes), + ); +} diff --git a/dex/farm-with-locked-rewards/wasm/Cargo.lock b/dex/farm-with-locked-rewards/wasm/Cargo.lock index 897ce8c8e..1ec1e747a 100644 --- a/dex/farm-with-locked-rewards/wasm/Cargo.lock +++ b/dex/farm-with-locked-rewards/wasm/Cargo.lock @@ -136,8 +136,11 @@ dependencies = [ "mergeable", "multiversx-sc", "multiversx-sc-modules", + "original_owner_helper", "pair", "pausable", + "permissions-hub", + "permissions_hub_module", "permissions_module", "rewards", "sc_whitelist_module", @@ -181,7 +184,10 @@ dependencies = [ "mergeable", "multiversx-sc", "multiversx-sc-modules", + "original_owner_helper", "pausable", + "permissions-hub", + "permissions_hub_module", "permissions_module", "rewards", "sc_whitelist_module", @@ -399,6 +405,14 @@ dependencies = [ "autocfg", ] +[[package]] +name = "original_owner_helper" +version = "0.0.0" +dependencies = [ + "common_structs", + "multiversx-sc", +] + [[package]] name = "pair" version = "0.0.0" @@ -423,6 +437,21 @@ dependencies = [ "permissions_module", ] +[[package]] +name = "permissions-hub" +version = "0.0.0" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "permissions_hub_module" +version = "0.0.0" +dependencies = [ + "multiversx-sc", + "permissions-hub", +] + [[package]] name = "permissions_module" version = "0.0.0" diff --git a/dex/farm-with-locked-rewards/wasm/src/lib.rs b/dex/farm-with-locked-rewards/wasm/src/lib.rs index e8aa3099f..81ef9c901 100644 --- a/dex/farm-with-locked-rewards/wasm/src/lib.rs +++ b/dex/farm-with-locked-rewards/wasm/src/lib.rs @@ -6,9 +6,9 @@ // Init: 1 // Upgrade: 1 -// Endpoints: 66 +// Endpoints: 69 // Async Callback: 1 -// Total number of exported functions: 69 +// Total number of exported functions: 72 #![no_std] @@ -56,6 +56,7 @@ multiversx_sc_wasm_adapter::endpoints! { removeAdmin => remove_admin_endpoint updateOwnerOrAdmin => update_owner_or_admin_endpoint getPermissions => permissions + setPermissionsHubAddress => set_permissions_hub_address addSCAddressToWhitelist => add_sc_address_to_whitelist removeSCAddressFromWhitelist => remove_sc_address_from_whitelist isSCAddressWhitelisted => is_sc_address_whitelisted @@ -66,6 +67,8 @@ multiversx_sc_wasm_adapter::endpoints! { getMinimumFarmingEpoch => minimum_farming_epochs getBurnGasLimit => burn_gas_limit getPairContractManagedAddress => pair_contract_address + enterFarmOnBehalf => enter_farm_on_behalf + claimRewardsOnBehalf => claim_rewards_on_behalf collectUndistributedBoostedRewards => collect_undistributed_boosted_rewards getBoostedYieldsRewardsPercentage => boosted_yields_rewards_percentage getAccumulatedRewardsForWeek => accumulated_rewards_for_week diff --git a/dex/farm/Cargo.toml b/dex/farm/Cargo.toml index f883c4579..b91a99066 100644 --- a/dex/farm/Cargo.toml +++ b/dex/farm/Cargo.toml @@ -38,6 +38,12 @@ path = "../../common/modules/pausable" [dependencies.permissions_module] path = "../../common/modules/permissions_module" +[dependencies.permissions_hub_module] +path = "../../common/modules/permissions_hub_module" + +[dependencies.original_owner_helper] +path = "../../common/modules/original_owner_helper" + [dependencies.sc_whitelist_module] path = "../../common/modules/sc_whitelist_module" @@ -68,6 +74,9 @@ path = "../../energy-integration/common-modules/weekly-rewards-splitting" [dependencies.energy-query] path = "../../energy-integration/common-modules/energy-query" +[dependencies.permissions-hub] +path = "../permissions-hub" + [dependencies.multiversx-sc] version = "=0.53.2" features = ["esdt-token-payment-legacy-decode"] diff --git a/dex/farm/README.md b/dex/farm/README.md index fcc860299..3a33264b8 100644 --- a/dex/farm/README.md +++ b/dex/farm/README.md @@ -103,3 +103,75 @@ The interaction scripts for this contract are located in the dex subdirectory of ## 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). + + +# Farm OnBehalf Operations + +## Abstract + +The Farm OnBehalf operations extend the Farm smart contract with the ability to allow whitelisted contracts to perform actions on behalf of users, enabling enhanced protocol composability while maintaining security through integration with the Permissions Hub. + +## Introduction + +This module allows third-party contracts to perform farm operations on behalf of users, after being explicitly whitelisted through the Permissions Hub. Users maintain full control over their assets by managing contract permissions, while protocols can build more complex DeFi interactions. + +## Endpoints + +### enterFarmOnBehalf + +```rust +#[payable("*")] +#[endpoint(enterFarmOnBehalf)] +fn enter_farm_on_behalf(&self, user: ManagedAddress) -> EnterFarmResultType +``` + +The enterFarmOnBehalf function allows whitelisted contracts to enter farm positions on behalf of users. It receives several arguments: + +- __user__ - The address of the user for whom the operation is being performed. This address must have whitelisted the caller contract through the Permissions Hub. +- __payment__ - The tokens to be used are received as payment in the transaction. + +The function performs the following steps: +1. Validates that the caller is whitelisted by the user through Permissions Hub +2. Processes the farming tokens payment +3. Claims any pending boosted rewards for the original owner +4. Performs the enter farm operation on behalf of the original owner +5. Sends the new farm token to the caller +6. Sends the collected rewards, if any, to the original owner +7. Updates energy and progress for the original owner + +### claimRewardsOnBehalf + +```rust +#[payable("*")] +#[endpoint(claimRewardsOnBehalf)] +fn claim_rewards_on_behalf(&self) -> ClaimRewardsResultType +``` + +The claimRewardsOnBehalf function enables whitelisted contracts to claim rewards on behalf of the users. This function does not require any address parameter, as the original owner is read from the farm position metadata. The operation requires: + +- __payment__ - The farm token must be received as payment in the transaction. + +The function performs these steps: +1. Processes the farm token payment +2. Extracts the original owner from the farm token attributes +3. Validates that the caller is whitelisted by the original owner +4. Claims and sends rewards to the original owner +5. Sends the new farm token to the caller + +## exitOnBehalf +The exit operation remains under the direct control of the position owner to ensure maximum security. When third-party contracts interact with farming positions through onBehalf operations, they receive and hold the position tokens. These tokens maintain the original owner information in their attributes, protecting the user's ownership rights. To exit their position, users must first reclaim their position tokens from the third-party contract through that protocol's specific mechanisms. Once users have regained control of their position tokens, they can perform the standard exit operation directly through the specific xExchange contract. +This design ensures users maintain ultimate control over their funds while allowing protocols to build complex DeFi interactions. + +## Storage + +The contract relies on the Permissions Hub for permission management, thus no additional storage, other than the one holding the Permissions Hub SC address, is required. All whitelisting data is managed through the Permissions Hub contract. + + +## Deployment + +The onBehalf features are part of the core farm contract and require: + +1. A deployed Permissions Hub contract +2. Configuration of the Permissions Hub address in the farm contract +3. User whitelisting of contracts that will perform onBehalf operations + diff --git a/dex/farm/src/external_interaction.rs b/dex/farm/src/external_interaction.rs new file mode 100644 index 000000000..0af8fe5a5 --- /dev/null +++ b/dex/farm/src/external_interaction.rs @@ -0,0 +1,90 @@ +multiversx_sc::imports!(); + +use common_structs::FarmTokenAttributes; + +use crate::{ + base_functions::{self, ClaimRewardsResultType, Wrapper}, + exit_penalty, EnterFarmResultType, +}; + +#[multiversx_sc::module] +pub trait ExternalInteractionsModule: + rewards::RewardsModule + + config::ConfigModule + + token_send::TokenSendModule + + farm_token::FarmTokenModule + + pausable::PausableModule + + permissions_module::PermissionsModule + + permissions_hub_module::PermissionsHubModule + + original_owner_helper::OriginalOwnerHelperModule + + sc_whitelist_module::SCWhitelistModule + + events::EventsModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + base_functions::BaseFunctionsModule + + exit_penalty::ExitPenaltyModule + + 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 + + farm_boosted_yields::FarmBoostedYieldsModule + + farm_boosted_yields::boosted_yields_factors::BoostedYieldsFactorsModule + + 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 + + utils::UtilsModule +{ + #[payable("*")] + #[endpoint(enterFarmOnBehalf)] + fn enter_farm_on_behalf(&self, user: ManagedAddress) -> EnterFarmResultType { + let caller = self.blockchain().get_caller(); + self.require_user_whitelisted(&user, &caller); + + let payments = self.get_non_empty_payments(); + let farm_token_mapper = self.farm_token(); + self.check_additional_payments_original_owner::>( + &user, + &payments, + &farm_token_mapper, + ); + + let boosted_rewards = self.claim_only_boosted_payment(&user); + + let boosted_rewards_payment = + EsdtTokenPayment::new(self.reward_token_id().get(), 0, boosted_rewards); + + let new_farm_token = self.enter_farm::>(user.clone()); + self.send_payment_non_zero(&caller, &new_farm_token); + self.send_payment_non_zero(&user, &boosted_rewards_payment); + + self.update_energy_and_progress(&user); + + (new_farm_token, boosted_rewards_payment).into() + } + + #[payable("*")] + #[endpoint(claimRewardsOnBehalf)] + fn claim_rewards_on_behalf(&self) -> ClaimRewardsResultType { + let payments = self.get_non_empty_payments(); + let farm_token_mapper = self.farm_token(); + + let caller = self.blockchain().get_caller(); + let user = self.check_and_return_original_owner::>( + &payments, + &farm_token_mapper, + ); + self.require_user_whitelisted(&user, &caller); + + let claim_rewards_result = self.claim_rewards::>(user.clone()); + + self.send_payment_non_zero(&caller, &claim_rewards_result.new_farm_token); + self.send_payment_non_zero(&user, &claim_rewards_result.rewards); + + claim_rewards_result.into() + } +} diff --git a/dex/farm/src/lib.rs b/dex/farm/src/lib.rs index 2ca53489d..5eb7ea834 100644 --- a/dex/farm/src/lib.rs +++ b/dex/farm/src/lib.rs @@ -5,6 +5,7 @@ multiversx_sc::derive_imports!(); pub mod base_functions; pub mod exit_penalty; +pub mod external_interaction; use base_functions::{ClaimRewardsResultType, DoubleMultiPayment, Wrapper}; use common_structs::FarmTokenAttributes; @@ -29,11 +30,14 @@ pub trait Farm: + farm_token::FarmTokenModule + pausable::PausableModule + permissions_module::PermissionsModule + + permissions_hub_module::PermissionsHubModule + + original_owner_helper::OriginalOwnerHelperModule + sc_whitelist_module::SCWhitelistModule + events::EventsModule + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + base_functions::BaseFunctionsModule + exit_penalty::ExitPenaltyModule + + external_interaction::ExternalInteractionsModule + farm_base_impl::base_farm_init::BaseFarmInitModule + farm_base_impl::base_farm_validation::BaseFarmValidationModule + farm_base_impl::enter_farm::BaseEnterFarmModule diff --git a/dex/farm/tests/energy_update_test.rs b/dex/farm/tests/energy_update_test.rs index 1d8f78a98..a15fbc5e2 100644 --- a/dex/farm/tests/energy_update_test.rs +++ b/dex/farm/tests/energy_update_test.rs @@ -10,6 +10,7 @@ fn test_farm_setup() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); } @@ -19,6 +20,7 @@ fn test_energy_update() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); let first_farm_token_amount = 100_000_000; @@ -39,6 +41,7 @@ fn test_energy_update_no_claim_current_week() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); let first_farm_token_amount = 100_000_000; diff --git a/dex/farm/tests/external_interaction_test.rs b/dex/farm/tests/external_interaction_test.rs new file mode 100644 index 000000000..6a36ae680 --- /dev/null +++ b/dex/farm/tests/external_interaction_test.rs @@ -0,0 +1,309 @@ +#![allow(deprecated)] + +mod farm_setup; + +use common_structs::FarmTokenAttributes; +use farm::external_interaction::ExternalInteractionsModule; +use farm_setup::multi_user_farm_setup::{ + MultiUserFarmSetup, BOOSTED_YIELDS_PERCENTAGE, FARMING_TOKEN_ID, FARM_TOKEN_ID, MAX_PERCENTAGE, + PER_BLOCK_REWARD_AMOUNT, REWARD_TOKEN_ID, +}; +use multiversx_sc_scenario::{ + imports::TxTokenTransfer, managed_address, managed_biguint, rust_biguint, DebugApi, +}; + +#[test] +fn test_enter_and_claim_farm_on_behalf() { + let mut farm_setup = MultiUserFarmSetup::new( + farm::contract_obj, + energy_factory_mock::contract_obj, + energy_update::contract_obj, + permissions_hub::contract_obj, + ); + + farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); + farm_setup.set_boosted_yields_factors(); + + // new external user + let external_user = farm_setup.b_mock.create_user_account(&rust_biguint!(0)); + + // authorized address + let farm_token_amount = 100_000_000; + let farm_token_nonce = 1u64; + let authorized_address = farm_setup.first_user.clone(); + + farm_setup.whitelist_address_on_behalf(&external_user, &authorized_address); + + farm_setup.check_farm_token_supply(0); + farm_setup.enter_farm_on_behalf(&authorized_address, &external_user, farm_token_amount, 0, 0); + farm_setup.check_farm_token_supply(farm_token_amount); + + let block_nonce = 10u64; + farm_setup.b_mock.set_block_nonce(block_nonce); + + // 1000 rewards per block + let total_rewards = 1000 * block_nonce; + + // Only base rewards are given + let base_rewards = + total_rewards * (MAX_PERCENTAGE - BOOSTED_YIELDS_PERCENTAGE) / MAX_PERCENTAGE; + farm_setup + .b_mock + .check_esdt_balance(&external_user, REWARD_TOKEN_ID, &rust_biguint!(0)); + farm_setup.claim_rewards_on_behalf(&authorized_address, farm_token_nonce, farm_token_amount); + farm_setup.b_mock.check_esdt_balance( + &external_user, + REWARD_TOKEN_ID, + &rust_biguint!(base_rewards), + ); +} + +#[test] +fn test_multiple_positions_on_behalf() { + DebugApi::dummy(); + + let mut farm_setup = MultiUserFarmSetup::new( + farm::contract_obj, + energy_factory_mock::contract_obj, + energy_update::contract_obj, + permissions_hub::contract_obj, + ); + + farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); + farm_setup.set_boosted_yields_factors(); + let mut block_nonce = 0u64; + farm_setup.b_mock.set_block_nonce(block_nonce); + + // new external user + let external_user = farm_setup.b_mock.create_user_account(&rust_biguint!(0)); + farm_setup.set_user_energy(&external_user, 1_000, 1, 1); + + // authorized address + let farm_token_amount = 100_000_000; + let authorized_address = farm_setup.first_user.clone(); + + farm_setup.whitelist_address_on_behalf(&external_user, &authorized_address); + + farm_setup.check_farm_token_supply(0); + farm_setup.enter_farm_on_behalf(&authorized_address, &external_user, farm_token_amount, 0, 0); + farm_setup.check_farm_token_supply(farm_token_amount); + + let block_nonce_diff = 10u64; + block_nonce += block_nonce_diff; + farm_setup.b_mock.set_block_nonce(block_nonce); + + // 1000 rewards per block + let total_rewards = PER_BLOCK_REWARD_AMOUNT * block_nonce_diff; + let base_rewards = + total_rewards * (MAX_PERCENTAGE - BOOSTED_YIELDS_PERCENTAGE) / MAX_PERCENTAGE; + let boosted_rewards = total_rewards * BOOSTED_YIELDS_PERCENTAGE / MAX_PERCENTAGE; + + // Only base rewards are given + farm_setup + .b_mock + .check_esdt_balance(&external_user, REWARD_TOKEN_ID, &rust_biguint!(0)); + farm_setup.claim_rewards_on_behalf(&authorized_address, 1, farm_token_amount); + farm_setup.b_mock.check_esdt_balance( + &external_user, + REWARD_TOKEN_ID, + &rust_biguint!(base_rewards), + ); + + // random tx on end of week 1, to cummulate rewards + farm_setup.b_mock.set_block_epoch(6); + let temp_user = farm_setup.third_user.clone(); + farm_setup.set_user_energy(&external_user, 1_000, 6, 1); + farm_setup.set_user_energy(&temp_user, 1, 6, 1); + farm_setup.last_farm_token_nonce = 2; + farm_setup.enter_farm(&temp_user, 1); + farm_setup.exit_farm(&temp_user, 3, 1); + + // advance 1 week + block_nonce += block_nonce_diff; + farm_setup.b_mock.set_block_nonce(block_nonce); + farm_setup.b_mock.set_block_epoch(10); + farm_setup.set_user_energy(&external_user, 1_000, 10, 1); + + // enter farm again for the same user (with additional payment) + farm_setup.check_farm_token_supply(farm_token_amount); + farm_setup.enter_farm_on_behalf( + &authorized_address, + &external_user, + farm_token_amount, + 2, // nonce 2 as the user already claimed with this position + farm_token_amount, + ); + farm_setup.check_farm_token_supply(farm_token_amount * 2); + farm_setup.b_mock.check_esdt_balance( + &external_user, + REWARD_TOKEN_ID, + &rust_biguint!(base_rewards + boosted_rewards), + ); + + farm_setup.claim_rewards_on_behalf(&authorized_address, 4, farm_token_amount * 2); + farm_setup.check_farm_token_supply(farm_token_amount * 2); + farm_setup.b_mock.check_esdt_balance( + &external_user, + REWARD_TOKEN_ID, + &rust_biguint!(total_rewards + base_rewards), + ); + + let farm_token_attributes: FarmTokenAttributes = FarmTokenAttributes { + reward_per_share: managed_biguint!(150_000_000u64), + entering_epoch: 10u64, + compounded_reward: managed_biguint!(0), + current_farm_amount: managed_biguint!(farm_token_amount * 2), + original_owner: managed_address!(&external_user), + }; + + farm_setup.b_mock.check_nft_balance( + &authorized_address, + FARM_TOKEN_ID, + 5, + &rust_biguint!(farm_token_amount * 2), + Some(&farm_token_attributes), + ); +} + +#[test] +fn test_enter_and_claim_farm_on_behalf_not_whitelisted_error() { + let mut farm_setup = MultiUserFarmSetup::new( + farm::contract_obj, + energy_factory_mock::contract_obj, + energy_update::contract_obj, + permissions_hub::contract_obj, + ); + + farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); + farm_setup.set_boosted_yields_factors(); + + // new external user + let external_user = farm_setup.b_mock.create_user_account(&rust_biguint!(0)); + + // authorized address + let authorized_address = farm_setup.first_user.clone(); + + // Try enter without whitelist + farm_setup + .b_mock + .execute_tx( + &authorized_address, + &farm_setup.farm_wrapper, + &rust_biguint!(0), + |sc| { + sc.enter_farm_on_behalf(managed_address!(&external_user)); + }, + ) + .assert_error(4, "Caller is not whitelisted by the user"); + + let farm_token_amount = 100_000_000; + farm_setup.whitelist_address_on_behalf(&external_user, &authorized_address); + farm_setup.enter_farm_on_behalf(&authorized_address, &external_user, farm_token_amount, 0, 0); + + // Try claim without whitelist + farm_setup.remove_whitelist_address_on_behalf(&external_user, &authorized_address); + farm_setup + .b_mock + .execute_esdt_transfer( + &authorized_address, + &farm_setup.farm_wrapper, + FARM_TOKEN_ID, + 1, + &rust_biguint!(farm_token_amount), + |sc| { + sc.claim_rewards_on_behalf(); + }, + ) + .assert_error(4, "Caller is not whitelisted by the user"); +} + +#[test] +fn test_wrong_original_owner_on_behalf_validation() { + let mut farm_setup = MultiUserFarmSetup::new( + farm::contract_obj, + energy_factory_mock::contract_obj, + energy_update::contract_obj, + permissions_hub::contract_obj, + ); + + farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); + farm_setup.set_boosted_yields_factors(); + + // new external users + let external_user1 = farm_setup.b_mock.create_user_account(&rust_biguint!(0)); + let external_user2 = farm_setup.b_mock.create_user_account(&rust_biguint!(0)); + + // authorized address + let authorized_address = farm_setup.first_user.clone(); + + let farm_token_amount = 100_000_000; + farm_setup.whitelist_address_on_behalf(&external_user1, &authorized_address); + farm_setup.whitelist_address_on_behalf(&external_user2, &authorized_address); + farm_setup.enter_farm_on_behalf( + &authorized_address, + &external_user1, + farm_token_amount, + 0, + 0, + ); + farm_setup.enter_farm_on_behalf( + &authorized_address, + &external_user2, + farm_token_amount, + 0, + 0, + ); + + // Try enter farm with wrong position + farm_setup.b_mock.set_esdt_balance( + &authorized_address, + FARMING_TOKEN_ID, + &rust_biguint!(farm_token_amount), + ); + let mut enter_farm_payments = Vec::new(); + enter_farm_payments.push(TxTokenTransfer { + token_identifier: FARMING_TOKEN_ID.to_vec(), + nonce: 0, + value: rust_biguint!(farm_token_amount), + }); + enter_farm_payments.push(TxTokenTransfer { + token_identifier: FARM_TOKEN_ID.to_vec(), + nonce: 2, // external_user2 position + value: rust_biguint!(farm_token_amount), + }); + farm_setup + .b_mock + .execute_esdt_multi_transfer( + &authorized_address, + &farm_setup.farm_wrapper, + &enter_farm_payments, + |sc| { + sc.enter_farm_on_behalf(managed_address!(&external_user1)); + }, + ) + .assert_error(4, "Provided address is not the same as the original owner"); + + // Try claim with different original owners + let mut claim_payments = Vec::new(); + claim_payments.push(TxTokenTransfer { + token_identifier: FARM_TOKEN_ID.to_vec(), + nonce: 1, // external_user1 position + value: rust_biguint!(farm_token_amount), + }); + claim_payments.push(TxTokenTransfer { + token_identifier: FARM_TOKEN_ID.to_vec(), + nonce: 2, // external_user2 position + value: rust_biguint!(farm_token_amount), + }); + farm_setup + .b_mock + .execute_esdt_multi_transfer( + &authorized_address, + &farm_setup.farm_wrapper, + &claim_payments, + |sc| { + sc.claim_rewards_on_behalf(); + }, + ) + .assert_error(4, "All position must have the same original owner"); +} diff --git a/dex/farm/tests/farm_multi_user_test.rs b/dex/farm/tests/farm_multi_user_test.rs index 03315b184..d37bee046 100644 --- a/dex/farm/tests/farm_multi_user_test.rs +++ b/dex/farm/tests/farm_multi_user_test.rs @@ -18,6 +18,7 @@ fn farm_with_no_boost_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); // first user enter farm @@ -116,6 +117,7 @@ fn farm_with_boosted_yields_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -228,6 +230,7 @@ fn farm_change_boosted_yields_factors_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -291,6 +294,7 @@ fn farm_boosted_yields_claim_with_different_user_pos_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -397,6 +401,7 @@ fn farm_known_proxy_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); // first user enter farm @@ -496,6 +501,7 @@ fn farm_multiple_claim_weeks_with_collect_undistributed_rewards_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -746,6 +752,7 @@ fn farm_enter_with_multiple_farm_token() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -861,6 +868,7 @@ fn farm_claim_with_minimum_tokens() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); diff --git a/dex/farm/tests/farm_setup/multi_user_farm_setup.rs b/dex/farm/tests/farm_setup/multi_user_farm_setup.rs index cb12a2a1e..3398fd679 100644 --- a/dex/farm/tests/farm_setup/multi_user_farm_setup.rs +++ b/dex/farm/tests/farm_setup/multi_user_farm_setup.rs @@ -3,6 +3,7 @@ use common_structs::FarmTokenAttributes; use config::ConfigModule; +use farm::external_interaction::ExternalInteractionsModule; use multiversx_sc::codec::multi_types::OptionalValue; use multiversx_sc::{ storage::mappers::StorageTokenWrapper, @@ -23,6 +24,8 @@ use farm_boosted_yields::boosted_yields_factors::BoostedYieldsFactorsModule; use farm_boosted_yields::FarmBoostedYieldsModule; use farm_token::FarmTokenModule; use pausable::{PausableModule, State}; +use permissions_hub::PermissionsHub; +use permissions_hub_module::PermissionsHubModule; use sc_whitelist_module::SCWhitelistModule; use week_timekeeping::Epoch; use weekly_rewards_splitting::update_claim_progress_energy::UpdateClaimProgressEnergyModule; @@ -54,11 +57,16 @@ pub struct NonceAmountPair { pub amount: u64, } -pub struct MultiUserFarmSetup -where +pub struct MultiUserFarmSetup< + FarmObjBuilder, + EnergyFactoryBuilder, + EnergyUpdateObjBuilder, + PermissionsHubObjBuilder, +> where FarmObjBuilder: 'static + Copy + Fn() -> farm::ContractObj, EnergyFactoryBuilder: 'static + Copy + Fn() -> energy_factory_mock::ContractObj, EnergyUpdateObjBuilder: 'static + Copy + Fn() -> energy_update::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, { pub b_mock: BlockchainStateWrapper, pub owner: Address, @@ -71,19 +79,28 @@ where ContractObjWrapper, EnergyFactoryBuilder>, pub eu_wrapper: ContractObjWrapper, EnergyUpdateObjBuilder>, + pub permissions_hub_wrapper: + ContractObjWrapper, PermissionsHubObjBuilder>, } -impl - MultiUserFarmSetup +impl + MultiUserFarmSetup< + FarmObjBuilder, + EnergyFactoryBuilder, + EnergyUpdateObjBuilder, + PermissionsHubObjBuilder, + > where FarmObjBuilder: 'static + Copy + Fn() -> farm::ContractObj, EnergyFactoryBuilder: 'static + Copy + Fn() -> energy_factory_mock::ContractObj, EnergyUpdateObjBuilder: 'static + Copy + Fn() -> energy_update::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, { pub fn new( farm_builder: FarmObjBuilder, energy_factory_builder: EnergyFactoryBuilder, eu_builder: EnergyUpdateObjBuilder, + permissions_hub_builder: PermissionsHubObjBuilder, ) -> Self { let rust_zero = rust_biguint!(0); let mut b_mock = BlockchainStateWrapper::new(); @@ -108,6 +125,19 @@ where }) .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); @@ -135,6 +165,10 @@ where sc.set_energy_factory_address(managed_address!( energy_factory_wrapper.address_ref() )); + + sc.set_permissions_hub_address(managed_address!( + permissions_hub_wrapper.address_ref() + )); }) .assert_ok(); @@ -189,6 +223,7 @@ where farm_wrapper, energy_factory_wrapper, eu_wrapper, + permissions_hub_wrapper, } } @@ -616,6 +651,117 @@ where .assert_ok(); } + pub fn whitelist_address_on_behalf(&mut self, user: &Address, address_to_whitelist: &Address) { + self.b_mock + .execute_tx( + user, + &self.permissions_hub_wrapper, + &rust_biguint!(0), + |sc| { + let mut addresses = MultiValueEncoded::new(); + addresses.push(managed_address!(address_to_whitelist)); + sc.whitelist(addresses); + }, + ) + .assert_ok(); + } + + pub fn remove_whitelist_address_on_behalf( + &mut self, + user: &Address, + address_to_remove: &Address, + ) { + self.b_mock + .execute_tx( + user, + &self.permissions_hub_wrapper, + &rust_biguint!(0), + |sc| { + let mut addresses = MultiValueEncoded::new(); + addresses.push(managed_address!(address_to_remove)); + sc.remove_whitelist(addresses); + }, + ) + .assert_ok(); + } + + pub fn enter_farm_on_behalf( + &mut self, + caller: &Address, + user: &Address, + farming_token_amount: u64, + farm_token_nonce: u64, + farm_token_amount: u64, + ) { + let mut payments = Vec::new(); + payments.push(TxTokenTransfer { + token_identifier: FARMING_TOKEN_ID.to_vec(), + nonce: 0, + value: rust_biguint!(farming_token_amount), + }); + + if farm_token_nonce > 0 { + payments.push(TxTokenTransfer { + token_identifier: FARM_TOKEN_ID.to_vec(), + nonce: farm_token_nonce, + value: rust_biguint!(farm_token_amount), + }); + } + + let b_mock = &mut self.b_mock; + b_mock + .execute_esdt_multi_transfer(caller, &self.farm_wrapper, &payments, |sc| { + let enter_farm_result = sc.enter_farm_on_behalf(managed_address!(user)); + let (out_farm_token, _reward_token) = enter_farm_result.into_tuple(); + assert_eq!( + out_farm_token.token_identifier, + managed_token_id!(FARM_TOKEN_ID) + ); + assert_eq!( + out_farm_token.amount, + managed_biguint!(farming_token_amount + farm_token_amount) + ); + }) + .assert_ok(); + } + + pub fn claim_rewards_on_behalf( + &mut self, + caller: &Address, + farm_token_nonce: u64, + farm_token_amount: u64, + ) -> u64 { + let mut result = 0; + self.b_mock + .execute_esdt_transfer( + caller, + &self.farm_wrapper, + FARM_TOKEN_ID, + farm_token_nonce, + &rust_biguint!(farm_token_amount), + |sc| { + let (out_farm_token, out_reward_token) = + sc.claim_rewards_on_behalf().into_tuple(); + assert_eq!( + out_farm_token.token_identifier, + managed_token_id!(FARM_TOKEN_ID) + ); + assert_eq!(out_farm_token.amount, managed_biguint!(farm_token_amount)); + + assert_eq!( + out_reward_token.token_identifier, + managed_token_id!(REWARD_TOKEN_ID) + ); + assert_eq!(out_reward_token.token_nonce, 0); + + result = out_reward_token.amount.to_u64().unwrap(); + }, + ) + .assert_ok(); + + result + } + pub fn update_energy_for_user(&mut self) { let b_mock = &mut self.b_mock; let user_addr = &self.first_user; diff --git a/dex/farm/tests/total_farm_position_test.rs b/dex/farm/tests/total_farm_position_test.rs index b7ca1be7b..4bee74227 100644 --- a/dex/farm/tests/total_farm_position_test.rs +++ b/dex/farm/tests/total_farm_position_test.rs @@ -23,6 +23,7 @@ fn total_farm_position_claim_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -128,6 +129,7 @@ fn allow_external_claim_rewards_setting_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -194,6 +196,7 @@ fn total_farm_position_claim_for_other_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -297,6 +300,7 @@ fn farm_total_position_migration_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -414,6 +418,7 @@ fn farm_total_position_exit_migration_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -494,6 +499,7 @@ fn farm_total_position_on_claim_migration_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -584,6 +590,7 @@ fn farm_total_position_on_merge_migration_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -678,6 +685,7 @@ fn no_boosted_rewards_penalty_for_no_energy_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -758,6 +766,7 @@ fn total_farm_position_owner_change_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); farm_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -982,6 +991,7 @@ fn total_farm_position_through_simple_lock_test() { farm::contract_obj, energy_factory_mock::contract_obj, energy_update::contract_obj, + permissions_hub::contract_obj, ); let rust_zero = rust_biguint!(0); diff --git a/dex/farm/wasm/Cargo.lock b/dex/farm/wasm/Cargo.lock index c3dea8cdc..c85e89764 100644 --- a/dex/farm/wasm/Cargo.lock +++ b/dex/farm/wasm/Cargo.lock @@ -136,8 +136,11 @@ dependencies = [ "mergeable", "multiversx-sc", "multiversx-sc-modules", + "original_owner_helper", "pair", "pausable", + "permissions-hub", + "permissions_hub_module", "permissions_module", "rewards", "sc_whitelist_module", @@ -369,6 +372,14 @@ dependencies = [ "autocfg", ] +[[package]] +name = "original_owner_helper" +version = "0.0.0" +dependencies = [ + "common_structs", + "multiversx-sc", +] + [[package]] name = "pair" version = "0.0.0" @@ -393,6 +404,21 @@ dependencies = [ "permissions_module", ] +[[package]] +name = "permissions-hub" +version = "0.0.0" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "permissions_hub_module" +version = "0.0.0" +dependencies = [ + "multiversx-sc", + "permissions-hub", +] + [[package]] name = "permissions_module" version = "0.0.0" diff --git a/dex/farm/wasm/src/lib.rs b/dex/farm/wasm/src/lib.rs index 525d76239..235653731 100644 --- a/dex/farm/wasm/src/lib.rs +++ b/dex/farm/wasm/src/lib.rs @@ -6,9 +6,9 @@ // Init: 1 // Upgrade: 1 -// Endpoints: 63 +// Endpoints: 66 // Async Callback: 1 -// Total number of exported functions: 66 +// Total number of exported functions: 69 #![no_std] @@ -53,6 +53,7 @@ multiversx_sc_wasm_adapter::endpoints! { removeAdmin => remove_admin_endpoint updateOwnerOrAdmin => update_owner_or_admin_endpoint getPermissions => permissions + setPermissionsHubAddress => set_permissions_hub_address addSCAddressToWhitelist => add_sc_address_to_whitelist removeSCAddressFromWhitelist => remove_sc_address_from_whitelist isSCAddressWhitelisted => is_sc_address_whitelisted @@ -63,6 +64,8 @@ multiversx_sc_wasm_adapter::endpoints! { getMinimumFarmingEpoch => minimum_farming_epochs getBurnGasLimit => burn_gas_limit getPairContractManagedAddress => pair_contract_address + enterFarmOnBehalf => enter_farm_on_behalf + claimRewardsOnBehalf => claim_rewards_on_behalf collectUndistributedBoostedRewards => collect_undistributed_boosted_rewards getBoostedYieldsRewardsPercentage => boosted_yields_rewards_percentage getAccumulatedRewardsForWeek => accumulated_rewards_for_week diff --git a/dex/permissions-hub/Cargo.toml b/dex/permissions-hub/Cargo.toml new file mode 100644 index 000000000..cf8673610 --- /dev/null +++ b/dex/permissions-hub/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "permissions-hub" +version = "0.0.0" +publish = false +edition = "2021" +authors = ["you"] + +[lib] +path = "src/lib.rs" + +[dependencies.multiversx-sc] +version = "0.53.2" + +[dev-dependencies] +num-bigint = "0.4" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.53.2" diff --git a/dex/permissions-hub/README.md b/dex/permissions-hub/README.md new file mode 100644 index 000000000..87c5fd091 --- /dev/null +++ b/dex/permissions-hub/README.md @@ -0,0 +1,124 @@ +# Permissions Hub Smart Contract + +## Overview +The Permissions Hub is a security-focused smart contract that manages permissions for on-behalf operations across the MultiversX DeFi ecosystem. It allows users to whitelist specific contracts that can perform operations on their behalf, enabling secure contract-to-contract interactions while maintaining user control over their assets. + +## Features +- User-controlled whitelisting of trusted contracts +- Administrative blacklisting for security purposes +- Granular permission management +- Efficient permission checking through optimized storage +- Integration support for other smart contracts + +## Core Functionality + +### User Operations + +#### Whitelisting Contracts +Users can whitelist multiple contract addresses that they trust to operate on their behalf: +```rust +#[endpoint] +fn whitelist(&self, addresses_to_whitelist: MultiValueEncoded) +``` +- Allows users to add multiple trusted contracts in a single transaction +- Prevents duplicate whitelisting through built-in validation +- Each user maintains their own whitelist independently + +#### Removing Whitelisted Contracts +Users can remove previously whitelisted contracts: +```rust +#[endpoint(removeWhitelist)] +fn remove_whitelist(&self, addresses_to_remove: MultiValueEncoded) +``` +- Allows batch removal of whitelisted addresses +- Validates that addresses were previously whitelisted +- Maintains user control over their permissions + +### Administrative Functions + +#### Blacklisting +Contract owner can blacklist potentially malicious addresses: +```rust +#[only_owner] +#[endpoint(blacklist)] +fn blacklist(&self, address_to_blacklist: ManagedAddress) +``` +- Restricted to contract owner +- Global blacklist affecting all users +- Security measure against identified threats + +#### Removing from Blacklist +Contract owner can remove addresses from the blacklist: +```rust +#[only_owner] +#[endpoint(removeBlacklist)] +fn remove_blacklist(&self, address_to_remove: ManagedAddress) +``` + +### View Functions + +#### Permission Checking +Contracts can verify if they have permission to operate on behalf of a user: +```rust +#[view(isWhitelisted)] +fn is_whitelisted(&self, user: &ManagedAddress, address_to_check: &ManagedAddress) -> bool +``` +- Returns true only if: + 1. The address is not blacklisted + 2. The address is in the user's whitelist +- Efficient for integration with other contracts + +#### Blacklist Viewing +```rust +#[view(getBlacklistedAddresses)] +fn blacklisted_addresses(&self) -> UnorderedSetMapper +``` +- Public view of globally blacklisted addresses +- Useful for transparency and integration purposes + +## Storage + +The contract uses two main storage mappers: + +1. User Whitelists: +```rust +#[storage_mapper("whitelistedAddresses")] +fn user_whitelisted_addresses(&self, user: &ManagedAddress) -> UnorderedSetMapper +``` +- Separate whitelist for each user +- Implemented as an UnorderedSetMapper for efficient operations + +2. Global Blacklist: +```rust +#[storage_mapper("blacklistedAddresses")] +fn blacklisted_addresses(&self) -> UnorderedSetMapper +``` +- Single global blacklist +- Managed by contract owner + +## Integration Guide + +### For Smart Contracts +To integrate with the Permissions Hub: + +1. Add the Permissions Hub address as a configurable parameter in your contract +2. Before performing operations on behalf of a user, check permissions: +```rust +let is_allowed = permissions_hub_proxy.is_whitelisted(user_address, caller_address); +require!(is_allowed, "Not authorized to perform operations on behalf of user"); +``` + +### For Users +To enable contracts to operate on your behalf: + +1. Call the `whitelist` endpoint with the contract address(es) you want to authorize +2. Monitor your active whitelisted addresses +3. Remove permissions using `removeWhitelist` when they're no longer needed + +## Security Considerations + +- Users should carefully verify contract addresses before whitelisting +- Regular auditing of whitelisted addresses is recommended +- The blacklist provides an additional security layer managed by the contract owner +- All permission changes are permanent until explicitly modified +- Users maintain full control over their whitelist diff --git a/dex/permissions-hub/meta/Cargo.toml b/dex/permissions-hub/meta/Cargo.toml new file mode 100644 index 000000000..c939cfe71 --- /dev/null +++ b/dex/permissions-hub/meta/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "permissions-hub-meta" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies.permissions-hub] +path = ".." + +[dependencies.multiversx-sc-meta-lib] +version = "0.53.2" +default-features = false diff --git a/dex/permissions-hub/meta/src/main.rs b/dex/permissions-hub/meta/src/main.rs new file mode 100644 index 000000000..984508b6c --- /dev/null +++ b/dex/permissions-hub/meta/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + multiversx_sc_meta_lib::cli_main::(); +} diff --git a/dex/permissions-hub/multiversx.json b/dex/permissions-hub/multiversx.json new file mode 100644 index 000000000..736553962 --- /dev/null +++ b/dex/permissions-hub/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} \ No newline at end of file diff --git a/dex/permissions-hub/src/lib.rs b/dex/permissions-hub/src/lib.rs new file mode 100644 index 000000000..7dfe4392d --- /dev/null +++ b/dex/permissions-hub/src/lib.rs @@ -0,0 +1,67 @@ +#![no_std] + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +#[multiversx_sc::contract] +pub trait PermissionsHub { + #[init] + fn init(&self) {} + + #[upgrade] + fn upgrade(&self) {} + + #[endpoint] + fn whitelist(&self, addresses_to_whitelist: MultiValueEncoded) { + let caller = self.blockchain().get_caller(); + for address_to_whitelist in addresses_to_whitelist.into_iter() { + require!( + self.user_whitelisted_addresses(&caller) + .insert(address_to_whitelist), + "Address is already whitelisted" + ); + } + } + + #[endpoint(removeWhitelist)] + fn remove_whitelist(&self, addresses_to_remove: MultiValueEncoded) { + let caller = self.blockchain().get_caller(); + for address_to_remove in addresses_to_remove.into_iter() { + require!( + self.user_whitelisted_addresses(&caller) + .swap_remove(&address_to_remove), + "Address is not whitelisted" + ); + } + } + + #[only_owner] + #[endpoint(blacklist)] + fn blacklist(&self, address_to_blacklist: ManagedAddress) { + self.blacklisted_addresses().insert(address_to_blacklist); + } + + #[only_owner] + #[endpoint(removeBlacklist)] + fn remove_blacklist(&self, address_to_remove: ManagedAddress) { + self.blacklisted_addresses().swap_remove(&address_to_remove); + } + + #[view(isWhitelisted)] + fn is_whitelisted(&self, user: &ManagedAddress, address_to_check: &ManagedAddress) -> bool { + !self.blacklisted_addresses().contains(address_to_check) + && self + .user_whitelisted_addresses(user) + .contains(address_to_check) + } + + #[storage_mapper("whitelistedAddresses")] + fn user_whitelisted_addresses( + &self, + user: &ManagedAddress, + ) -> UnorderedSetMapper; + + #[view(getBlacklistedAddresses)] + #[storage_mapper("blacklistedAddresses")] + fn blacklisted_addresses(&self) -> UnorderedSetMapper; +} diff --git a/dex/permissions-hub/wasm/Cargo.lock b/dex/permissions-hub/wasm/Cargo.lock new file mode 100644 index 000000000..a5b82e4db --- /dev/null +++ b/dex/permissions-hub/wasm/Cargo.lock @@ -0,0 +1,188 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "multiversx-sc" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ea89a26f0aacda21437a8ae5ccfbefab99d8191942b3d2eddbcbf84f9866d7" +dependencies = [ + "bitflags", + "hex-literal", + "multiversx-sc-codec", + "multiversx-sc-derive", + "num-traits", + "unwrap-infallible", +] + +[[package]] +name = "multiversx-sc-codec" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d7a5a8534e5dc9128cb8f15a65a21dd378e135c6016c7cd1491cd012bc8cb" +dependencies = [ + "arrayvec", + "multiversx-sc-codec-derive", + "unwrap-infallible", +] + +[[package]] +name = "multiversx-sc-codec-derive" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffba1dce273ed5b61ee1b90aeea5c8c744617d0f12624f620768c144d83e753" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multiversx-sc-derive" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c17fdf90fafca2f19085ae67b0502d9f71bf8ab1be3c83808eb88e02a8c18b9" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "radix_trie", + "syn", +] + +[[package]] +name = "multiversx-sc-wasm-adapter" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20659915a4377d375c46d7f237e810053a03f7e084fad6362dd5748a7233defb" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "permissions-hub" +version = "0.0.0" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "permissions-hub-wasm" +version = "0.0.0" +dependencies = [ + "multiversx-sc-wasm-adapter", + "permissions-hub", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unwrap-infallible" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151ac09978d3c2862c4e39b557f4eceee2cc72150bc4cb4f16abf061b6e381fb" diff --git a/dex/permissions-hub/wasm/Cargo.toml b/dex/permissions-hub/wasm/Cargo.toml new file mode 100644 index 000000000..dd9a073c3 --- /dev/null +++ b/dex/permissions-hub/wasm/Cargo.toml @@ -0,0 +1,34 @@ +# Code generated by the multiversx-sc build system. DO NOT EDIT. + +# ########################################## +# ############## AUTO-GENERATED ############# +# ########################################## + +[package] +name = "permissions-hub-wasm" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +overflow-checks = false + +[profile.dev] +panic = "abort" + +[dependencies.permissions-hub] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "0.53.2" + +[workspace] +members = ["."] diff --git a/dex/permissions-hub/wasm/src/lib.rs b/dex/permissions-hub/wasm/src/lib.rs new file mode 100644 index 000000000..4ea948115 --- /dev/null +++ b/dex/permissions-hub/wasm/src/lib.rs @@ -0,0 +1,32 @@ +// Code generated by the multiversx-sc build system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Upgrade: 1 +// Endpoints: 6 +// Async Callback (empty): 1 +// Total number of exported functions: 9 + +#![no_std] + +multiversx_sc_wasm_adapter::allocator!(); +multiversx_sc_wasm_adapter::panic_handler!(); + +multiversx_sc_wasm_adapter::endpoints! { + permissions_hub + ( + init => init + upgrade => upgrade + whitelist => whitelist + removeWhitelist => remove_whitelist + blacklist => blacklist + removeBlacklist => remove_blacklist + isWhitelisted => is_whitelisted + getBlacklistedAddresses => blacklisted_addresses + ) +} + +multiversx_sc_wasm_adapter::async_callback_empty! {} diff --git a/farm-staking/farm-staking-proxy/Cargo.toml b/farm-staking/farm-staking-proxy/Cargo.toml index 34ee079f9..34d8c7151 100644 --- a/farm-staking/farm-staking-proxy/Cargo.toml +++ b/farm-staking/farm-staking-proxy/Cargo.toml @@ -63,6 +63,12 @@ path = "../../common/modules/sc_whitelist_module" [dependencies.energy-query] path = "../../energy-integration/common-modules/energy-query" +[dependencies.permissions-hub] +path = "../../dex/permissions-hub" + +[dependencies.permissions_hub_module] +path = "../../common/modules/permissions_hub_module" + [dev-dependencies] num-bigint = "0.4.2" diff --git a/farm-staking/farm-staking-proxy/README.md b/farm-staking/farm-staking-proxy/README.md index 158d30351..edf67e811 100644 --- a/farm-staking/farm-staking-proxy/README.md +++ b/farm-staking/farm-staking-proxy/README.md @@ -98,3 +98,84 @@ One thing to note here is that between claiming rewards in the farming contract To unstake his current position, a user must send the desired amount of dual yield tokens to the proxy contract. At this moment, the proxy contract knows, based on the sent dual yield token, both the farm token position and staking token position. The first step is for the proxy contract to withdraw the LP tokens from the farms and the liquidity from the pair contract. After that all the harvested rewards, the resulting eGLD from removing the LP token and the unstake position of the staking token are all sent to the user. The unstaking process is ended with the burning of the dual yield tokens. It is important to note that because of the user’s unstaked position, an unbonding period is not needed. + +# Farm Staking Proxy OnBehalf Operations + +## Abstract + +The Farm Staking Proxy contract enables complex yield strategies by managing dual yield positions. The OnBehalf operations allow whitelisted contracts to manage these positions for users, combining LP farming and staking rewards while maintaining proper ownership and security through the Permissions Hub. + +## Introduction + +This feature extends the dual yield functionality with delegated operations, allowing third-party contracts to manage composite farming positions. It maintains the security of underlying positions, proper reward distribution, and ownership tracking while enabling more complex DeFi integrations through the Permissions Hub. + +## Endpoints + +### stakeFarmOnBehalf + +```rust +#[payable("*")] +#[endpoint(stakeFarmOnBehalf)] +fn stake_farm_on_behalf(&self, original_owner: ManagedAddress) -> StakeProxyResult +``` + +The stakeFarmOnBehalf function enables whitelisted contracts to create dual yield positions. It receives: + +- __original_owner__ - The address of the user for whom the position is being created +- __payments__ - Multiple token payments required for the dual yield position: + - First payment must be an LP farm token + - Additional payments must belong to the same original owner + +The function performs these operations: +1. Validates caller's whitelist status through Permissions Hub +2. Verifies ownership of all provided tokens +3. Creates the dual yield position +4. Distributes the results: + - LP farm boosted rewards to original owner + - Staking boosted rewards to original owner + - Dual yield tokens to caller + +### claimDualYieldOnBehalf + +```rust +#[payable("*")] +#[endpoint(claimDualYieldOnBehalf)] +fn claim_dual_yield_on_behalf(&self) -> ClaimDualYieldResult +``` + +The claimDualYieldOnBehalf function allows whitelisted contracts to claim rewards from dual yield positions. It requires: + +- __payment__ - A dual yield token payment + +The function performs these steps: +1. Extracts original owner from underlying farm position +2. Validates caller's whitelist status for the token owner +3. Claims both LP farming and staking rewards +4. Distributes rewards: + - LP farm rewards to original owner + - Staking farm rewards to original owner + - New dual yield tokens to caller + +## exitOnBehalf +The exit operation remains under the direct control of the position owner to ensure maximum security. When third-party contracts interact with farming or staking positions through onBehalf operations, they receive and hold the position tokens. These tokens maintain the original owner information in their attributes, protecting the user's ownership rights. To exit their position, users must first reclaim their position tokens from the third-party contract through that protocol's specific mechanisms. Once users have regained control of their position tokens, they can perform the standard exit operation directly through the specific xExchange contract. +This design ensures users maintain ultimate control over their funds while allowing protocols to build complex DeFi interactions. + +## Storage + +The contract maintains its standard dual yield token storage and relies on underlying contracts and the Permissions Hub for any additional data. + +## Deployment + +The onBehalf features require: + +1. Proper configuration of: + - Permissions Hub address + - LP Farm contract address + - Staking Farm contract address + - Token IDs and roles + +2. Required external contracts: + - Active Permissions Hub + - Active LP Farm contract + - Active Staking Farm contract + diff --git a/farm-staking/farm-staking-proxy/src/lib.rs b/farm-staking/farm-staking-proxy/src/lib.rs index 7bfc66bab..f28d5d083 100644 --- a/farm-staking/farm-staking-proxy/src/lib.rs +++ b/farm-staking/farm-staking-proxy/src/lib.rs @@ -14,6 +14,7 @@ pub trait FarmStakingProxy: + external_contracts_interactions::ExternalContractsInteractionsModule + lp_farm_token::LpFarmTokenModule + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + permissions_hub_module::PermissionsHubModule + utils::UtilsModule + token_send::TokenSendModule + energy_query::EnergyQueryModule @@ -21,6 +22,7 @@ pub trait FarmStakingProxy: + proxy_actions::stake::ProxyStakeModule + proxy_actions::claim::ProxyClaimModule + proxy_actions::unstake::ProxyUnstakeModule + + proxy_actions::external_interaction::ProxyExternalInteractionsModule { #[init] fn init( diff --git a/farm-staking/farm-staking-proxy/src/proxy_actions/claim.rs b/farm-staking/farm-staking-proxy/src/proxy_actions/claim.rs index 01df77c36..a3a40a5de 100644 --- a/farm-staking/farm-staking-proxy/src/proxy_actions/claim.rs +++ b/farm-staking/farm-staking-proxy/src/proxy_actions/claim.rs @@ -25,43 +25,26 @@ pub trait ProxyClaimModule: &self, opt_orig_caller: OptionalValue, ) -> ClaimDualYieldResult { - let payment = self.call_value().single_esdt(); - let dual_yield_token_mapper = self.dual_yield_token(); - dual_yield_token_mapper.require_same_token(&payment.token_identifier); - let caller = self.blockchain().get_caller(); - let attributes: DualYieldTokenAttributes = - self.get_attributes_as_part_of_fixed_supply(&payment, &dual_yield_token_mapper); - let internal_claim_result = self.claim_dual_yield( - &caller, - opt_orig_caller, - attributes.staking_farm_token_amount.clone(), - attributes, - ); + let orig_caller = self.get_orig_caller_from_opt(&caller, opt_orig_caller); - let new_dual_yield_tokens = self.create_dual_yield_tokens( - &dual_yield_token_mapper, - &internal_claim_result.new_dual_yield_attributes, - ); - let claim_result = ClaimDualYieldResult { - lp_farm_rewards: internal_claim_result.lp_farm_rewards, - staking_farm_rewards: internal_claim_result.staking_farm_rewards, - new_dual_yield_tokens, - }; + let payment = self.call_value().single_esdt(); - dual_yield_token_mapper.nft_burn(payment.token_nonce, &payment.amount); + let claim_result = self.claim_dual_yield_common(orig_caller, payment); claim_result.send_and_return(self, &caller) } - fn claim_dual_yield( + fn claim_dual_yield_common( &self, - caller: &ManagedAddress, - opt_orig_caller: OptionalValue, - staking_claim_amount: BigUint, - attributes: DualYieldTokenAttributes, - ) -> InternalClaimResult { - let orig_caller = self.get_orig_caller_from_opt(caller, opt_orig_caller); + orig_caller: ManagedAddress, + payment: EsdtTokenPayment, + ) -> ClaimDualYieldResult { + let dual_yield_token_mapper = self.dual_yield_token(); + dual_yield_token_mapper.require_same_token(&payment.token_identifier); + + let attributes: DualYieldTokenAttributes = + self.get_attributes_as_part_of_fixed_supply(&payment, &dual_yield_token_mapper); let lp_tokens_in_position = self.get_lp_tokens_in_farm_position( attributes.lp_farm_token_nonce, @@ -81,23 +64,31 @@ pub trait ProxyClaimModule: orig_caller, staking_farm_token_id, attributes.staking_farm_token_nonce, - staking_claim_amount, + attributes.staking_farm_token_amount, new_staking_farm_value, ); let new_lp_farm_tokens = lp_farm_claim_rewards_result.new_lp_farm_tokens; let new_staking_farm_tokens = staking_farm_claim_rewards_result.new_staking_farm_tokens; - let new_attributes = DualYieldTokenAttributes { + let new_dual_yield_attributes = DualYieldTokenAttributes { lp_farm_token_nonce: new_lp_farm_tokens.token_nonce, lp_farm_token_amount: new_lp_farm_tokens.amount, staking_farm_token_nonce: new_staking_farm_tokens.token_nonce, staking_farm_token_amount: new_staking_farm_tokens.amount, }; - InternalClaimResult { - lp_farm_rewards: lp_farm_claim_rewards_result.lp_farm_rewards, - staking_farm_rewards: staking_farm_claim_rewards_result.staking_farm_rewards, - new_dual_yield_attributes: new_attributes, + let new_dual_yield_tokens = + self.create_dual_yield_tokens(&dual_yield_token_mapper, &new_dual_yield_attributes); + + let lp_farm_rewards = lp_farm_claim_rewards_result.lp_farm_rewards; + let staking_farm_rewards = staking_farm_claim_rewards_result.staking_farm_rewards; + + dual_yield_token_mapper.nft_burn(payment.token_nonce, &payment.amount); + + ClaimDualYieldResult { + lp_farm_rewards, + staking_farm_rewards, + new_dual_yield_tokens, } } } diff --git a/farm-staking/farm-staking-proxy/src/proxy_actions/external_interaction.rs b/farm-staking/farm-staking-proxy/src/proxy_actions/external_interaction.rs new file mode 100644 index 000000000..722bc2eb8 --- /dev/null +++ b/farm-staking/farm-staking-proxy/src/proxy_actions/external_interaction.rs @@ -0,0 +1,119 @@ +multiversx_sc::imports!(); + +use common_structs::FarmTokenAttributes; + +use crate::{ + dual_yield_token::DualYieldTokenAttributes, + result_types::{ClaimDualYieldResult, StakeProxyResult}, +}; + +#[multiversx_sc::module] +pub trait ProxyExternalInteractionsModule: + crate::dual_yield_token::DualYieldTokenModule + + crate::external_contracts_interactions::ExternalContractsInteractionsModule + + crate::lp_farm_token::LpFarmTokenModule + + crate::proxy_actions::stake::ProxyStakeModule + + crate::proxy_actions::claim::ProxyClaimModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + permissions_hub_module::PermissionsHubModule + + utils::UtilsModule + + token_send::TokenSendModule + + energy_query::EnergyQueryModule + + sc_whitelist_module::SCWhitelistModule +{ + #[payable("*")] + #[endpoint(stakeFarmOnBehalf)] + fn stake_farm_on_behalf(&self, original_owner: ManagedAddress) -> StakeProxyResult { + let caller = self.blockchain().get_caller(); + self.require_user_whitelisted(&original_owner, &caller); + + let payments = self.get_non_empty_payments(); + self.check_stake_farm_payments(&original_owner, &payments); + + let output_payments = self.stake_farm_tokens_common(original_owner.clone(), payments); + + self.send_payment_non_zero(&original_owner, &output_payments.lp_farm_boosted_rewards); + self.send_payment_non_zero(&original_owner, &output_payments.staking_boosted_rewards); + self.send_payment_non_zero(&caller, &output_payments.dual_yield_tokens); + + output_payments + } + + #[payable("*")] + #[endpoint(claimDualYieldOnBehalf)] + fn claim_dual_yield_on_behalf(&self) -> ClaimDualYieldResult { + let payment = self.call_value().single_esdt(); + + let caller = self.blockchain().get_caller(); + let original_owner = self.get_underlying_farm_position_original_owner(&payment); + self.require_user_whitelisted(&original_owner, &caller); + + let claim_result = self.claim_dual_yield_common(original_owner.clone(), payment); + + self.send_payment_non_zero(&original_owner, &claim_result.lp_farm_rewards); + self.send_payment_non_zero(&original_owner, &claim_result.staking_farm_rewards); + self.send_payment_non_zero(&caller, &claim_result.new_dual_yield_tokens); + + claim_result + } + + fn check_stake_farm_payments( + &self, + original_owner: &ManagedAddress, + payments: &ManagedVec, + ) { + let lp_farm_token_payment = payments.get(0); + let additional_payments = payments.slice(1, payments.len()).unwrap_or_default(); + + let lp_farm_token_id = self.lp_farm_token_id().get(); + require!( + lp_farm_token_payment.token_identifier == lp_farm_token_id, + "Invalid first payment" + ); + + let attributes = self + .blockchain() + .get_token_attributes::>( + &lp_farm_token_payment.token_identifier, + lp_farm_token_payment.token_nonce, + ); + + require!( + &attributes.original_owner == original_owner, + "Provided address is not the same as the original owner" + ); + + for payment in additional_payments.into_iter() { + require!( + &self.get_underlying_farm_position_original_owner(&payment) == original_owner, + "Provided address is not the same as the original owner" + ); + } + } + + fn get_underlying_farm_position_original_owner( + &self, + payment: &EsdtTokenPayment, + ) -> ManagedAddress { + let dual_yield_token_mapper = self.dual_yield_token(); + dual_yield_token_mapper.require_same_token(&payment.token_identifier); + + let attributes: DualYieldTokenAttributes = + self.get_attributes_as_part_of_fixed_supply(payment, &dual_yield_token_mapper); + + let lp_farm_token_id = self.lp_farm_token_id().get(); + let attributes = self + .blockchain() + .get_token_attributes::>( + &lp_farm_token_id, + attributes.lp_farm_token_nonce, + ); + + require!( + attributes.original_owner != ManagedAddress::zero(), + "Invalid original owner" + ); + + attributes.original_owner + } +} diff --git a/farm-staking/farm-staking-proxy/src/proxy_actions/mod.rs b/farm-staking/farm-staking-proxy/src/proxy_actions/mod.rs index 3270b0f79..88511f2ac 100644 --- a/farm-staking/farm-staking-proxy/src/proxy_actions/mod.rs +++ b/farm-staking/farm-staking-proxy/src/proxy_actions/mod.rs @@ -1,3 +1,4 @@ pub mod claim; +pub mod external_interaction; pub mod stake; pub mod unstake; diff --git a/farm-staking/farm-staking-proxy/src/proxy_actions/stake.rs b/farm-staking/farm-staking-proxy/src/proxy_actions/stake.rs index b32f62b76..26e9b2af7 100644 --- a/farm-staking/farm-staking-proxy/src/proxy_actions/stake.rs +++ b/farm-staking/farm-staking-proxy/src/proxy_actions/stake.rs @@ -22,6 +22,17 @@ pub trait ProxyStakeModule: let caller = self.blockchain().get_caller(); let orig_caller = self.get_orig_caller_from_opt(&caller, opt_orig_caller); let payments = self.get_non_empty_payments(); + + let output_payments = self.stake_farm_tokens_common(orig_caller, payments); + + output_payments.send_and_return(self, &caller) + } + + fn stake_farm_tokens_common( + &self, + original_caller: ManagedAddress, + payments: ManagedVec, + ) -> StakeProxyResult { let lp_farm_token_payment = payments.get(0); let additional_payments = payments.slice(1, payments.len()).unwrap_or_default(); @@ -62,7 +73,7 @@ pub trait ProxyStakeModule: ); let staking_token_amount = self.get_lp_tokens_safe_price(lp_tokens_in_farm); let staking_farm_enter_result = self.staking_farm_enter( - orig_caller.clone(), + original_caller.clone(), staking_token_amount, additional_staking_farm_tokens, ); @@ -70,7 +81,7 @@ pub trait ProxyStakeModule: let (merged_lp_farm_tokens, lp_farm_boosted_rewards) = self .merge_lp_farm_tokens( - orig_caller, + original_caller, lp_farm_token_payment, additional_lp_farm_tokens, ) @@ -90,6 +101,6 @@ pub trait ProxyStakeModule: lp_farm_boosted_rewards, }; - output_payments.send_and_return(self, &caller) + output_payments } } diff --git a/farm-staking/farm-staking-proxy/tests/staking_farm_with_lp.rs b/farm-staking/farm-staking-proxy/tests/staking_farm_with_lp.rs index 50b62cb93..9a790bb19 100644 --- a/farm-staking/farm-staking-proxy/tests/staking_farm_with_lp.rs +++ b/farm-staking/farm-staking-proxy/tests/staking_farm_with_lp.rs @@ -31,6 +31,7 @@ fn test_all_setup() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -42,6 +43,7 @@ fn test_stake_farm_proxy() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -57,6 +59,7 @@ fn test_claim_rewards_farm_proxy_full() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -85,6 +88,7 @@ fn test_claim_rewards_farm_proxy_half() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -113,6 +117,7 @@ fn test_claim_rewards_farm_proxy_twice() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -156,6 +161,7 @@ fn test_unstake_through_proxy_no_claim() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -187,6 +193,7 @@ fn unstake_through_proxy_after_claim() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -228,6 +235,7 @@ fn unstake_partial_position_test() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -344,6 +352,7 @@ fn unbond_test() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -389,6 +398,7 @@ fn farm_staking_compound_rewards_and_unstake_test() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -420,6 +430,7 @@ fn test_stake_farm_through_proxy_with_merging() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -496,6 +507,7 @@ fn test_farm_stake_proxy_merging_boosted_rewards() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -641,6 +653,7 @@ fn original_caller_negative_test() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -683,6 +696,7 @@ fn claim_for_others_positive_test() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -879,6 +893,7 @@ fn stake_farm_through_proxy_migration_test() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -1013,6 +1028,7 @@ fn total_farm_position_after_claim_and_exit_metastaking_test() { pair::contract_obj, farm_with_locked_rewards::contract_obj, energy_factory::contract_obj, + permissions_hub::contract_obj, farm_staking::contract_obj, farm_staking_proxy::contract_obj, ); @@ -1216,3 +1232,170 @@ fn total_farm_position_after_claim_and_exit_metastaking_test() { // Total farm position should be 0 after full unstake setup.check_user_total_staking_farm_position(&user_address, 0); } + +#[test] +fn test_multiple_positions_on_behalf() { + DebugApi::dummy(); + + let mut setup = FarmStakingSetup::new( + pair::contract_obj, + farm_with_locked_rewards::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + farm_staking::contract_obj, + farm_staking_proxy::contract_obj, + ); + + // Boosted rewards setup + setup + .b_mock + .execute_tx( + &setup.owner_addr, + &setup.staking_farm_wrapper, + &rust_biguint!(0), + |sc| { + sc.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); + }, + ) + .assert_ok(); + + setup.set_lp_farm_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); + let farm_amount = 100_000_000u64; + let user_address = setup.user_addr.clone(); + let authorized_address = setup.b_mock.create_user_account(&rust_biguint!(0)); + let temp_user = setup + .b_mock + .create_user_account(&rust_biguint!(farm_amount)); + setup.exit_lp_farm(&user_address, 1, USER_TOTAL_LP_TOKENS); + setup + .b_mock + .set_esdt_balance(&setup.user_addr, LP_TOKEN_ID, &rust_biguint!(farm_amount)); + setup + .b_mock + .set_esdt_balance(&temp_user, LP_TOKEN_ID, &rust_biguint!(1)); + + let mut block_nonce = 2u64; + setup.b_mock.set_block_epoch(2u64); + + setup.set_user_energy(&user_address, 1_000, 2, 1); + setup + .b_mock + .set_esdt_balance(&user_address, LP_TOKEN_ID, &rust_biguint!(farm_amount * 2)); + setup + .b_mock + .set_esdt_balance(&user_address, STAKING_REWARD_TOKEN_ID, &rust_biguint!(0)); + let farm_token_nonce = setup.enter_lp_farm(&user_address, farm_amount * 2); + + setup.check_user_total_staking_farm_position(&user_address, 0); + + // authorize address + setup.whitelist_address_on_behalf(&user_address, &authorized_address); + + setup.send_farm_position( + &user_address, + &authorized_address, + farm_token_nonce, + farm_amount * 2, + 0, + block_nonce, + ); + + setup.b_mock.check_esdt_balance( + &authorized_address, + STAKING_REWARD_TOKEN_ID, + &rust_biguint!(0), // should always be 0 + ); + + setup.stake_farm_on_behalf( + &authorized_address, + &user_address, + farm_token_nonce, + farm_amount, + 0, + 0, + 1, + farm_amount, + ); + setup.check_user_total_staking_farm_position(&user_address, farm_amount); + + let block_nonce_diff = 100; + block_nonce += block_nonce_diff; + + setup.b_mock.set_block_nonce(block_nonce); + + // Only base rewards are given + setup + .b_mock + .check_esdt_balance(&user_address, STAKING_REWARD_TOKEN_ID, &rust_biguint!(0)); + setup.claim_rewards_on_behalf(&authorized_address, 1, farm_amount); + setup.b_mock.check_esdt_balance( + &user_address, + STAKING_REWARD_TOKEN_ID, + &rust_biguint!(14u64), + ); + + // User total farm position should still be the same + setup.check_user_total_staking_farm_position(&user_address, farm_amount); + + // random tx on end of week 1, to cummulate rewards + setup.b_mock.set_block_epoch(6); + setup.set_user_energy(&user_address, 1_000, 6, 1); + setup.set_user_energy(&temp_user, 1, 6, 1); + let temp_user_farm_token_nonce = setup.enter_lp_farm(&temp_user, 1); + setup.exit_lp_farm(&temp_user, temp_user_farm_token_nonce, 1); + + // advance 1 week + block_nonce += block_nonce_diff; + setup.b_mock.set_block_nonce(block_nonce); + setup.b_mock.set_block_epoch(10); + setup.set_user_energy(&user_address, 1_000, 10, 1); + + // enter farm again for the same user (with additional payment) + setup.stake_farm_on_behalf( + &authorized_address, + &user_address, + farm_token_nonce, + farm_amount, + 2, // nonce 2 as the user already claimed with this position + farm_amount, + 3, + farm_amount * 2, + ); + setup.b_mock.check_esdt_balance( + &user_address, + STAKING_REWARD_TOKEN_ID, + &rust_biguint!(14u64 + 4u64), + ); + + setup.check_user_total_staking_farm_position(&user_address, farm_amount * 2); + setup.claim_rewards_on_behalf(&authorized_address, 3, farm_amount * 2); + setup.check_user_total_staking_farm_position(&user_address, farm_amount * 2); + + // Check reward token balances + setup.b_mock.check_esdt_balance( + &user_address, + STAKING_REWARD_TOKEN_ID, + &rust_biguint!(693), // actual amount computation out of scope for this unit test + ); + setup.b_mock.check_esdt_balance( + &authorized_address, + STAKING_REWARD_TOKEN_ID, + &rust_biguint!(0), // should always be 0 + ); + + let dual_yield_token_attributes: DualYieldTokenAttributes = + DualYieldTokenAttributes { + lp_farm_token_nonce: 6, + lp_farm_token_amount: managed_biguint!(farm_amount * 2u64), + staking_farm_token_nonce: 4, + staking_farm_token_amount: managed_biguint!(farm_amount * 2u64), + }; + + setup.b_mock.check_nft_balance( + &authorized_address, + DUAL_YIELD_TOKEN_ID, + 4, + &rust_biguint!(farm_amount * 2u64), + Some(&dual_yield_token_attributes), + ); +} diff --git a/farm-staking/farm-staking-proxy/tests/staking_farm_with_lp_staking_contract_interactions/mod.rs b/farm-staking/farm-staking-proxy/tests/staking_farm_with_lp_staking_contract_interactions/mod.rs index aba5f5713..7bc22b29a 100644 --- a/farm-staking/farm-staking-proxy/tests/staking_farm_with_lp_staking_contract_interactions/mod.rs +++ b/farm-staking/farm-staking-proxy/tests/staking_farm_with_lp_staking_contract_interactions/mod.rs @@ -1,12 +1,13 @@ #![allow(deprecated)] +use common_structs::FarmTokenAttributes; use config::ConfigModule; use energy_factory::energy::EnergyModule; use energy_query::Energy; use farm_with_locked_rewards::Farm; use multiversx_sc::{ codec::multi_types::OptionalValue, - types::{Address, BigInt}, + types::{Address, BigInt, MultiValueEncoded}, }; use multiversx_sc_scenario::{ managed_address, managed_biguint, managed_token_id, rust_biguint, @@ -21,12 +22,17 @@ use farm_staking::{ token_attributes::UnbondSftAttributes, unbond_farm::UnbondFarmModule, unstake_farm::UnstakeFarmModule, }; -use farm_staking_proxy::dual_yield_token::DualYieldTokenAttributes; use farm_staking_proxy::proxy_actions::claim::ProxyClaimModule; +use farm_staking_proxy::{ + dual_yield_token::DualYieldTokenAttributes, + proxy_actions::external_interaction::ProxyExternalInteractionsModule, +}; use farm_staking_proxy::proxy_actions::stake::ProxyStakeModule; use farm_staking_proxy::proxy_actions::unstake::ProxyUnstakeModule; +use permissions_hub::PermissionsHub; +use permissions_hub_module::PermissionsHubModule; use sc_whitelist_module::SCWhitelistModule; use crate::{ @@ -46,12 +52,14 @@ pub struct FarmStakingSetup< PairObjBuilder, FarmObjBuilder, EnergyFactoryBuilder, + PermissionsHubObjBuilder, StakingContractObjBuilder, ProxyContractObjBuilder, > where PairObjBuilder: 'static + Copy + Fn() -> pair::ContractObj, FarmObjBuilder: 'static + Copy + Fn() -> farm_with_locked_rewards::ContractObj, EnergyFactoryBuilder: 'static + Copy + Fn() -> energy_factory::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, StakingContractObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, ProxyContractObjBuilder: 'static + Copy + Fn() -> farm_staking_proxy::ContractObj, { @@ -63,6 +71,8 @@ pub struct FarmStakingSetup< ContractObjWrapper, FarmObjBuilder>, pub energy_factory_wrapper: ContractObjWrapper, EnergyFactoryBuilder>, + pub permissions_hub_wrapper: + ContractObjWrapper, PermissionsHubObjBuilder>, pub staking_farm_wrapper: ContractObjWrapper, StakingContractObjBuilder>, pub proxy_wrapper: @@ -73,6 +83,7 @@ impl< PairObjBuilder, FarmObjBuilder, EnergyFactoryBuilder, + PermissionsHubObjBuilder, StakingContractObjBuilder, ProxyContractObjBuilder, > @@ -80,6 +91,7 @@ impl< PairObjBuilder, FarmObjBuilder, EnergyFactoryBuilder, + PermissionsHubObjBuilder, StakingContractObjBuilder, ProxyContractObjBuilder, > @@ -87,6 +99,7 @@ where PairObjBuilder: 'static + Copy + Fn() -> pair::ContractObj, FarmObjBuilder: 'static + Copy + Fn() -> farm_with_locked_rewards::ContractObj, EnergyFactoryBuilder: 'static + Copy + Fn() -> energy_factory::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, StakingContractObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, ProxyContractObjBuilder: 'static + Copy + Fn() -> farm_staking_proxy::ContractObj, { @@ -94,6 +107,7 @@ where pair_builder: PairObjBuilder, lp_farm_builder: FarmObjBuilder, energy_factory_builder: EnergyFactoryBuilder, + permissions_hub_builder: PermissionsHubObjBuilder, staking_farm_builder: StakingContractObjBuilder, proxy_builder: ProxyContractObjBuilder, ) -> Self { @@ -135,6 +149,27 @@ where &staking_farm_wrapper, ); + let permissions_hub_wrapper = b_mock.create_sc_account( + &rust_zero, + Some(&owner_addr), + permissions_hub_builder, + "permissions_hub.wasm", + ); + + b_mock + .execute_tx(&owner_addr, &proxy_wrapper, &rust_zero, |sc| { + sc.set_permissions_hub_address(managed_address!( + permissions_hub_wrapper.address_ref() + )); + }) + .assert_ok(); + + b_mock + .execute_tx(&owner_addr, &permissions_hub_wrapper, &rust_zero, |sc| { + sc.init(); + }) + .assert_ok(); + b_mock .execute_tx(&owner_addr, &lp_farm_wrapper, &rust_zero, |sc| { sc.add_sc_address_to_whitelist(managed_address!(proxy_wrapper.address_ref())); @@ -153,6 +188,7 @@ where pair_wrapper, lp_farm_wrapper, energy_factory_wrapper, + permissions_hub_wrapper, staking_farm_wrapper, proxy_wrapper, } @@ -603,6 +639,84 @@ where ) } + #[allow(clippy::too_many_arguments)] + pub fn stake_farm_on_behalf( + &mut self, + caller: &Address, + user: &Address, + lp_farm_token_nonce: u64, + lp_farm_token_amount: u64, + additional_dual_yield_token_nonce: u64, + additional_dual_yield_token_amount: u64, + expected_dual_yield_token_nonce: u64, + expected_dual_yield_token_amount: u64, + ) { + let mut payments = Vec::new(); + payments.push(TxTokenTransfer { + token_identifier: LP_FARM_TOKEN_ID.to_vec(), + nonce: lp_farm_token_nonce, + value: rust_biguint!(lp_farm_token_amount), + }); + + if additional_dual_yield_token_nonce > 0 { + payments.push(TxTokenTransfer { + token_identifier: DUAL_YIELD_TOKEN_ID.to_vec(), + nonce: additional_dual_yield_token_nonce, + value: rust_biguint!(additional_dual_yield_token_amount), + }); + } + + let b_mock = &mut self.b_mock; + b_mock + .execute_esdt_multi_transfer(caller, &self.proxy_wrapper, &payments, |sc| { + let stake_farm_result = sc.stake_farm_on_behalf(managed_address!(user)); + assert_eq!( + stake_farm_result.dual_yield_tokens.token_nonce, + expected_dual_yield_token_nonce + ); + assert_eq!( + stake_farm_result.dual_yield_tokens.amount, + managed_biguint!(expected_dual_yield_token_amount) + ); + }) + .assert_ok(); + } + + pub fn claim_rewards_on_behalf( + &mut self, + caller: &Address, + dual_yield_token_nonce: u64, + dual_yield_token_amount: u64, + ) { + self.b_mock + .execute_esdt_transfer( + caller, + &self.proxy_wrapper, + DUAL_YIELD_TOKEN_ID, + dual_yield_token_nonce, + &rust_biguint!(dual_yield_token_amount), + |sc| { + let _claim_dual_yield_result = sc.claim_dual_yield_on_behalf(); + }, + ) + .assert_ok(); + } + + pub fn whitelist_address_on_behalf(&mut self, user: &Address, address_to_whitelist: &Address) { + self.b_mock + .execute_tx( + user, + &self.permissions_hub_wrapper, + &rust_biguint!(0), + |sc| { + let mut addresses = MultiValueEncoded::new(); + addresses.push(managed_address!(address_to_whitelist)); + sc.whitelist(addresses); + }, + ) + .assert_ok(); + } + pub fn set_user_energy( &mut self, user: &Address, @@ -700,6 +814,90 @@ where .assert_ok(); } + pub fn send_farm_position( + &mut self, + sender: &Address, + receiver: &Address, + nonce: u64, + amount: u64, + attr_reward_per_share: u64, + attr_entering_epoch: u64, + ) { + self.b_mock.check_nft_balance( + sender, + LP_FARM_TOKEN_ID, + nonce, + &rust_biguint!(amount), + Some(&FarmTokenAttributes:: { + reward_per_share: managed_biguint!(attr_reward_per_share), + entering_epoch: attr_entering_epoch, + compounded_reward: managed_biguint!(0), + current_farm_amount: managed_biguint!(amount), + original_owner: managed_address!(&sender), + }), + ); + + self.b_mock + .check_nft_balance::>( + receiver, + LP_FARM_TOKEN_ID, + nonce, + &rust_biguint!(0), + None, + ); + + self.b_mock.set_nft_balance( + sender, + LP_FARM_TOKEN_ID, + nonce, + &rust_biguint!(0), + &FarmTokenAttributes:: { + reward_per_share: managed_biguint!(attr_reward_per_share), + entering_epoch: attr_entering_epoch, + compounded_reward: managed_biguint!(0), + current_farm_amount: managed_biguint!(amount), + original_owner: managed_address!(&sender), + }, + ); + + self.b_mock.set_nft_balance( + receiver, + LP_FARM_TOKEN_ID, + nonce, + &rust_biguint!(amount), + &FarmTokenAttributes:: { + reward_per_share: managed_biguint!(attr_reward_per_share), + entering_epoch: attr_entering_epoch, + compounded_reward: managed_biguint!(0), + current_farm_amount: managed_biguint!(amount), + original_owner: managed_address!(&sender), + }, + ); + + self.b_mock + .check_nft_balance::>( + sender, + LP_FARM_TOKEN_ID, + nonce, + &rust_biguint!(0), + None, + ); + + self.b_mock.check_nft_balance( + receiver, + LP_FARM_TOKEN_ID, + nonce, + &rust_biguint!(amount), + Some(&FarmTokenAttributes:: { + reward_per_share: managed_biguint!(attr_reward_per_share), + entering_epoch: attr_entering_epoch, + compounded_reward: managed_biguint!(0), + current_farm_amount: managed_biguint!(amount), + original_owner: managed_address!(&sender), + }), + ); + } + pub fn check_user_total_staking_farm_position( &mut self, user_addr: &Address, diff --git a/farm-staking/farm-staking-proxy/wasm/Cargo.lock b/farm-staking/farm-staking-proxy/wasm/Cargo.lock index 1f79bbe16..7fe70065c 100644 --- a/farm-staking/farm-staking-proxy/wasm/Cargo.lock +++ b/farm-staking/farm-staking-proxy/wasm/Cargo.lock @@ -138,6 +138,7 @@ dependencies = [ "multiversx-sc-modules", "pair", "pausable", + "permissions-hub", "permissions_module", "rewards", "sc_whitelist_module", @@ -183,6 +184,7 @@ dependencies = [ "multiversx-sc-modules", "pair", "pausable", + "permissions-hub", "permissions_module", "rewards", "sc_whitelist_module", @@ -209,6 +211,7 @@ dependencies = [ "multiversx-sc-modules", "pair", "pausable", + "permissions-hub", "rewards", "sc_whitelist_module", "token_send", @@ -245,6 +248,7 @@ dependencies = [ "multiversx-sc", "multiversx-sc-modules", "pausable", + "permissions-hub", "permissions_module", "rewards", "sc_whitelist_module", @@ -484,6 +488,13 @@ dependencies = [ "permissions_module", ] +[[package]] +name = "permissions-hub" +version = "0.0.0" +dependencies = [ + "multiversx-sc", +] + [[package]] name = "permissions_module" version = "0.0.0" diff --git a/farm-staking/farm-staking-proxy/wasm/src/lib.rs b/farm-staking/farm-staking-proxy/wasm/src/lib.rs index 3f9abd0a4..3632ff09e 100644 --- a/farm-staking/farm-staking-proxy/wasm/src/lib.rs +++ b/farm-staking/farm-staking-proxy/wasm/src/lib.rs @@ -6,9 +6,9 @@ // Init: 1 // Upgrade: 1 -// Endpoints: 17 +// Endpoints: 20 // Async Callback: 1 -// Total number of exported functions: 20 +// Total number of exported functions: 23 #![no_std] @@ -37,6 +37,9 @@ multiversx_sc_wasm_adapter::endpoints! { stakeFarmTokens => stake_farm_tokens claimDualYield => claim_dual_yield_endpoint unstakeFarmTokens => unstake_farm_tokens + stakeFarmOnBehalf => stake_farm_on_behalf + claimDualYieldOnBehalf => claim_dual_yield_on_behalf + setPermissionsHubAddress => set_permissions_hub_address ) } diff --git a/farm-staking/farm-staking/Cargo.toml b/farm-staking/farm-staking/Cargo.toml index 8f1817a89..51876247b 100644 --- a/farm-staking/farm-staking/Cargo.toml +++ b/farm-staking/farm-staking/Cargo.toml @@ -53,6 +53,12 @@ path = "../../common/modules/pausable" [dependencies.permissions_module] path = "../../common/modules/permissions_module" +[dependencies.permissions_hub_module] +path = "../../common/modules/permissions_hub_module" + +[dependencies.original_owner_helper] +path = "../../common/modules/original_owner_helper" + [dependencies.sc_whitelist_module] path = "../../common/modules/sc_whitelist_module" @@ -77,6 +83,9 @@ path = "../../common/common_structs" [dependencies.common_errors] path = "../../common/common_errors" +[dependencies.permissions-hub] +path = "../../dex/permissions-hub" + [dependencies.multiversx-sc] version = "=0.53.2" features = ["esdt-token-payment-legacy-decode"] diff --git a/farm-staking/farm-staking/README.md b/farm-staking/farm-staking/README.md index a3b188322..b2f755e63 100644 --- a/farm-staking/farm-staking/README.md +++ b/farm-staking/farm-staking/README.md @@ -222,3 +222,76 @@ As stated in the proxy contract documentation, the staking farm tokens are also ``` Payable endpoint that allows the caller to harvest the rewards generated by the staking farm and reinvest them seamlessly, within a single endpoint. It burns the current farm tokens and computes the actual position with the rewards included. + + +# Farm Staking onBehalf Operations + +## Abstract + +The Farm Staking smart contract extends its functionality with onBehalf operations, enabling whitelisted contracts to stake tokens and manage rewards on behalf of users. This feature integrates with the Permissions Hub to provide secure delegation of staking operations while maintaining proper reward distribution and energy system mechanics. + +## Introduction + +The onBehalf operations in Farm Staking allow third-party contracts to manage staking positions for users, facilitating complex DeFi integrations. The contract maintains all standard staking mechanics, including energy-based boosted rewards and weekly reward distributions, while adding secure delegation capabilities through the Permissions Hub integration. + +## Endpoints + +### stakeFarmOnBehalf + +```rust +#[payable("*")] +#[endpoint(stakeFarmOnBehalf)] +fn stake_farm_on_behalf(&self, user: ManagedAddress) -> EnterFarmResultType +``` + +The stakeFarmOnBehalf function enables whitelisted contracts to create staking positions for users. It receives: + +- __user__ - The address of the user for whom the staking position is being created +- __payment__ - The tokens to be staked are received as payment in the transaction + +The function performs these operations: +1. Validates caller's whitelist status through Permissions Hub +2. Processes any pending boosted rewards for the user +3. Creates the staking position for the original owner +4. Sends the staking position to the caller +5. Sends the rewards, if any, to the original owner +6. Maintains energy system and progress tracking +7. Emits appropriate events with both caller and user information + +### claimRewardsOnBehalf + +```rust +#[payable("*")] +#[endpoint(claimRewardsOnBehalf)] +fn claim_rewards_on_behalf(&self) -> ClaimRewardsResultType +``` + +The claimRewardsOnBehalf function allows whitelisted contracts to claim staking rewards. This function does not require any address parameter, as the original owner is read from the staking position metadata. It requires: + +- __payment__ - The farm staking token must be received as payment + +The function executes the following steps: +1. Validates the farm token and extracts the original owner +2. Verifies caller's whitelist status for the token owner +3. Claims any pending rewards +4. Creates new farm token with updated attributes +5. Updates weekly farm supply +6. Maintains energy and progress tracking +7. Distributes new farm token to caller and rewards to the original owner +8. Emits claim events with relevant information + +## exitOnBehalf +The exit operation remains under the direct control of the position owner to ensure maximum security. When third-party contracts interact with staking positions through onBehalf operations, they receive and hold the position tokens. These tokens maintain the original owner information in their attributes, protecting the user's ownership rights. To exit their position, users must first reclaim their position tokens from the third-party contract through that protocol's specific mechanisms. Once users have regained control of their position tokens, they can perform the standard exit operation directly through the specific xExchange contract. +This design ensures users maintain ultimate control over their funds while allowing protocols to build complex DeFi interactions. + +## Storage + +The contract relies on the Permissions Hub for permission management, thus no additional storage, other than the one holding the Permissions Hub SC address, is required. All whitelisting data is managed through the Permissions Hub contract. + +## Deployment + +The onBehalf features are part of the core farm contract and require: + +1. A deployed Permissions Hub contract +2. Configuration of the Permissions Hub address in the staking contract +3. User whitelisting of contracts that will perform onBehalf operations \ No newline at end of file diff --git a/farm-staking/farm-staking/src/external_interaction.rs b/farm-staking/farm-staking/src/external_interaction.rs new file mode 100644 index 000000000..1ece48d9a --- /dev/null +++ b/farm-staking/farm-staking/src/external_interaction.rs @@ -0,0 +1,133 @@ +multiversx_sc::imports!(); + +use farm::{base_functions::ClaimRewardsResultType, EnterFarmResultType}; + +use crate::{ + base_impl_wrapper::FarmStakingWrapper, claim_only_boosted_staking_rewards, + claim_stake_farm_rewards, compound_stake_farm_rewards, custom_rewards, farm_token_roles, + stake_farm, token_attributes::StakingFarmTokenAttributes, unbond_farm, unstake_farm, +}; + +#[multiversx_sc::module] +pub trait ExternalInteractionsModule: + custom_rewards::CustomRewardsModule + + rewards::RewardsModule + + config::ConfigModule + + events::EventsModule + + token_send::TokenSendModule + + farm_token::FarmTokenModule + + sc_whitelist_module::SCWhitelistModule + + pausable::PausableModule + + permissions_module::PermissionsModule + + permissions_hub_module::PermissionsHubModule + + original_owner_helper::OriginalOwnerHelperModule + + 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_token_roles::FarmTokenRolesModule + + stake_farm::StakeFarmModule + + claim_stake_farm_rewards::ClaimStakeFarmRewardsModule + + compound_stake_farm_rewards::CompoundStakeFarmRewardsModule + + unstake_farm::UnstakeFarmModule + + unbond_farm::UnbondFarmModule + + claim_only_boosted_staking_rewards::ClaimOnlyBoostedStakingRewardsModule + + farm_boosted_yields::FarmBoostedYieldsModule + + farm_boosted_yields::boosted_yields_factors::BoostedYieldsFactorsModule + + 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 +{ + #[payable("*")] + #[endpoint(stakeFarmOnBehalf)] + fn stake_farm_on_behalf(&self, user: ManagedAddress) -> EnterFarmResultType { + let caller = self.blockchain().get_caller(); + self.require_user_whitelisted(&user, &caller); + + let payments = self.get_non_empty_payments(); + let farm_token_mapper = self.farm_token(); + self.check_additional_payments_original_owner::>( + &user, + &payments, + &farm_token_mapper, + ); + + let boosted_rewards = self.claim_only_boosted_payment(&user); + let boosted_rewards_payment = + EsdtTokenPayment::new(self.reward_token_id().get(), 0, boosted_rewards); + + let enter_result = self.enter_farm_base::>(user.clone(), payments); + + let new_farm_token = enter_result.new_farm_token.payment.clone(); + self.send_payment_non_zero(&caller, &new_farm_token); + self.send_payment_non_zero(&user, &boosted_rewards_payment); + + self.set_farm_supply_for_current_week(&enter_result.storage_cache.farm_token_supply); + + self.update_energy_and_progress(&user); + + self.emit_enter_farm_event( + &caller, + enter_result.context.farming_token_payment, + enter_result.new_farm_token, + enter_result.created_with_merge, + enter_result.storage_cache, + ); + + (new_farm_token, boosted_rewards_payment).into() + } + + #[payable("*")] + #[endpoint(claimRewardsOnBehalf)] + fn claim_rewards_on_behalf(&self) -> ClaimRewardsResultType { + let payments = self.get_non_empty_payments(); + let farm_token_mapper = self.farm_token(); + let caller = self.blockchain().get_caller(); + let user = self.check_and_return_original_owner::>( + &payments, + &farm_token_mapper, + ); + self.require_user_whitelisted(&user, &caller); + + let claim_result = self.claim_rewards_base_no_farm_token_mint::>( + user.clone(), + payments, + ); + + let mut virtual_farm_token = claim_result.new_farm_token.clone(); + + self.set_farm_supply_for_current_week(&claim_result.storage_cache.farm_token_supply); + + self.update_energy_and_progress(&user); + + let new_farm_token_nonce = self.send().esdt_nft_create_compact( + &virtual_farm_token.payment.token_identifier, + &virtual_farm_token.payment.amount, + &virtual_farm_token.attributes, + ); + virtual_farm_token.payment.token_nonce = new_farm_token_nonce; + + let caller = self.blockchain().get_caller(); + self.send_payment_non_zero(&caller, &virtual_farm_token.payment); + self.send_payment_non_zero(&user, &claim_result.rewards); + + self.emit_claim_rewards_event( + &caller, + claim_result.context, + virtual_farm_token.clone(), + claim_result.rewards.clone(), + claim_result.created_with_merge, + claim_result.storage_cache, + ); + + (virtual_farm_token.payment, claim_result.rewards).into() + } +} diff --git a/farm-staking/farm-staking/src/lib.rs b/farm-staking/farm-staking/src/lib.rs index f783c7831..4cf1c3ebf 100644 --- a/farm-staking/farm-staking/src/lib.rs +++ b/farm-staking/farm-staking/src/lib.rs @@ -18,6 +18,7 @@ pub mod claim_only_boosted_staking_rewards; pub mod claim_stake_farm_rewards; pub mod compound_stake_farm_rewards; pub mod custom_rewards; +pub mod external_interaction; pub mod farm_token_roles; pub mod stake_farm; pub mod token_attributes; @@ -35,6 +36,8 @@ pub trait FarmStaking: + sc_whitelist_module::SCWhitelistModule + pausable::PausableModule + permissions_module::PermissionsModule + + permissions_hub_module::PermissionsHubModule + + original_owner_helper::OriginalOwnerHelperModule + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + farm_base_impl::base_farm_init::BaseFarmInitModule + farm_base_impl::base_farm_validation::BaseFarmValidationModule @@ -49,6 +52,7 @@ pub trait FarmStaking: + compound_stake_farm_rewards::CompoundStakeFarmRewardsModule + unstake_farm::UnstakeFarmModule + unbond_farm::UnbondFarmModule + + external_interaction::ExternalInteractionsModule + claim_only_boosted_staking_rewards::ClaimOnlyBoostedStakingRewardsModule + farm_boosted_yields::FarmBoostedYieldsModule + farm_boosted_yields::boosted_yields_factors::BoostedYieldsFactorsModule diff --git a/farm-staking/farm-staking/src/token_attributes.rs b/farm-staking/farm-staking/src/token_attributes.rs index 59f8c6326..0fcabeba3 100644 --- a/farm-staking/farm-staking/src/token_attributes.rs +++ b/farm-staking/farm-staking/src/token_attributes.rs @@ -82,6 +82,10 @@ impl FarmToken for StakingFarmTokenAttributes { fn get_initial_farming_tokens(&self) -> BigUint { &self.current_farm_amount - &self.compounded_reward } + + fn get_original_owner(&self) -> ManagedAddress { + self.original_owner.clone() + } } impl FixedSupplyToken for StakingFarmTokenAttributes { diff --git a/farm-staking/farm-staking/tests/farm_staking_energy_test.rs b/farm-staking/farm-staking/tests/farm_staking_energy_test.rs index a4046f03e..e1a885355 100644 --- a/farm-staking/farm-staking/tests/farm_staking_energy_test.rs +++ b/farm-staking/farm-staking/tests/farm_staking_energy_test.rs @@ -18,8 +18,11 @@ use multiversx_sc_scenario::{ #[test] fn farm_staking_with_energy_setup_test() { - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); fs_setup.set_boosted_yields_factors(); fs_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -28,8 +31,11 @@ fn farm_staking_with_energy_setup_test() { #[test] fn farm_staking_boosted_rewards_no_energy_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = fs_setup.user_address.clone(); @@ -74,8 +80,11 @@ fn farm_staking_boosted_rewards_no_energy_test() { #[test] fn farm_staking_other_user_enter_negative_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = fs_setup.user_address.clone(); let rand_user = fs_setup.b_mock.create_user_account(&rust_biguint!(0)); @@ -120,8 +129,11 @@ fn farm_staking_other_user_enter_negative_test() { #[test] fn farm_staking_boosted_rewards_with_energy_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = fs_setup.user_address.clone(); let user_address2 = fs_setup.user_address2.clone(); @@ -345,8 +357,11 @@ fn farm_staking_boosted_rewards_with_energy_test() { #[test] fn farm_staking_partial_position_handling_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = fs_setup.user_address.clone(); @@ -507,8 +522,11 @@ fn farm_staking_partial_position_handling_test() { #[test] fn farm_staking_claim_boosted_rewards_for_user_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = fs_setup.user_address.clone(); @@ -621,8 +639,11 @@ fn farm_staking_claim_boosted_rewards_for_user_test() { #[test] fn farm_staking_full_position_boosted_rewards_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = fs_setup.user_address.clone(); @@ -736,8 +757,11 @@ fn farm_staking_full_position_boosted_rewards_test() { #[test] fn position_owner_change_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let first_user = fs_setup.user_address.clone(); let second_user = fs_setup.user_address2.clone(); @@ -1028,8 +1052,11 @@ fn position_owner_change_test() { #[test] fn farm_staking_farm_position_migration_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user = fs_setup.user_address.clone(); @@ -1122,8 +1149,11 @@ fn farm_staking_farm_position_migration_test() { #[test] fn boosted_rewards_config_change_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let first_user = fs_setup.user_address.clone(); let second_user = fs_setup.user_address2.clone(); @@ -1432,8 +1462,11 @@ fn boosted_rewards_config_change_test() { #[test] fn claim_only_boosted_rewards_per_week_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); fs_setup.set_boosted_yields_factors(); fs_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -1527,8 +1560,11 @@ fn claim_only_boosted_rewards_per_week_test() { #[test] fn claim_rewards_per_week_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); fs_setup.set_boosted_yields_factors(); fs_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -1621,8 +1657,11 @@ fn claim_rewards_per_week_test() { #[test] fn claim_boosted_rewards_with_zero_position_test() { DebugApi::dummy(); - let mut fs_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); fs_setup.set_boosted_yields_factors(); fs_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); @@ -1717,3 +1756,115 @@ fn claim_boosted_rewards_with_zero_position_test() { Some(&expected_attributes), ); } + +#[test] +fn test_multiple_positions_on_behalf() { + DebugApi::dummy(); + + let mut fs_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); + + fs_setup.set_boosted_yields_rewards_percentage(BOOSTED_YIELDS_PERCENTAGE); + fs_setup.set_boosted_yields_factors(); + let mut block_nonce = 0u64; + fs_setup.b_mock.set_block_nonce(block_nonce); + + // new external user + let external_user = fs_setup.b_mock.create_user_account(&rust_biguint!(0)); + fs_setup.set_user_energy(&external_user, 1_000, 1, 1); + + // authorized address + let farm_token_amount = 100_000_000; + let authorized_address = fs_setup.user_address.clone(); + fs_setup.b_mock.set_esdt_balance( + &authorized_address, + FARMING_TOKEN_ID, + &rust_biguint!(farm_token_amount * 2), + ); + + fs_setup.whitelist_address_on_behalf(&external_user, &authorized_address); + + fs_setup.check_farm_token_supply(0); + fs_setup.stake_farm_on_behalf(&authorized_address, &external_user, farm_token_amount, 0, 0); + fs_setup.check_farm_token_supply(farm_token_amount); + + let block_nonce_diff = 10u64; + block_nonce += block_nonce_diff; + fs_setup.b_mock.set_block_nonce(block_nonce); + + let base_rewards = 30u64; + let boosted_rewards = 10u64; + let total_rewards = base_rewards + boosted_rewards; + + // Only base rewards are given + fs_setup + .b_mock + .check_esdt_balance(&external_user, REWARD_TOKEN_ID, &rust_biguint!(0)); + fs_setup.claim_rewards_on_behalf(&authorized_address, 1, farm_token_amount); + fs_setup.b_mock.check_esdt_balance( + &external_user, + REWARD_TOKEN_ID, + &rust_biguint!(base_rewards), + ); + + // random tx on end of week 1, to cummulate rewards + fs_setup.b_mock.set_block_epoch(6); + let temp_user = fs_setup.b_mock.create_user_account(&rust_biguint!(0)); + fs_setup.b_mock.set_esdt_balance( + &temp_user, + FARMING_TOKEN_ID, + &rust_biguint!(USER_TOTAL_RIDE_TOKENS), + ); + fs_setup.set_user_energy(&external_user, 1_000, 6, 1); + fs_setup.set_user_energy(&temp_user, 1, 6, 1); + fs_setup.stake_farm(&temp_user, 10, &[], 3, 300_000u64, 0); + fs_setup.unstake_farm_no_checks(&temp_user, 10, 3); + + // advance 1 week + block_nonce += block_nonce_diff; + fs_setup.b_mock.set_block_nonce(block_nonce); + fs_setup.b_mock.set_block_epoch(10); + fs_setup.set_user_energy(&external_user, 1_000, 10, 1); + + // enter farm again for the same user (with additional payment) + fs_setup.check_farm_token_supply(farm_token_amount); + fs_setup.stake_farm_on_behalf( + &authorized_address, + &external_user, + farm_token_amount, + 2, // nonce 2 as the user already claimed with this position + farm_token_amount, + ); + fs_setup.check_farm_token_supply(farm_token_amount * 2); + fs_setup.b_mock.check_esdt_balance( + &external_user, + REWARD_TOKEN_ID, + &rust_biguint!(base_rewards + boosted_rewards), + ); + + fs_setup.claim_rewards_on_behalf(&authorized_address, 5, farm_token_amount * 2); + fs_setup.check_farm_token_supply(farm_token_amount * 2); + fs_setup.b_mock.check_esdt_balance( + &external_user, + REWARD_TOKEN_ID, + &rust_biguint!(total_rewards + base_rewards), + ); + + let farm_token_attributes: StakingFarmTokenAttributes = StakingFarmTokenAttributes { + reward_per_share: managed_biguint!(600_000u64), + compounded_reward: managed_biguint!(0), + current_farm_amount: managed_biguint!(farm_token_amount * 2), + original_owner: managed_address!(&external_user), + }; + + fs_setup.b_mock.check_nft_balance( + &authorized_address, + FARM_TOKEN_ID, + 6, + &rust_biguint!(farm_token_amount * 2), + Some(&farm_token_attributes), + ); +} diff --git a/farm-staking/farm-staking/tests/farm_staking_setup/mod.rs b/farm-staking/farm-staking/tests/farm_staking_setup/mod.rs index a27b2d0d0..35ed665bc 100644 --- a/farm-staking/farm-staking/tests/farm_staking_setup/mod.rs +++ b/farm-staking/farm-staking/tests/farm_staking_setup/mod.rs @@ -1,5 +1,6 @@ #![allow(deprecated)] +use external_interaction::ExternalInteractionsModule; use farm_staking::claim_only_boosted_staking_rewards::ClaimOnlyBoostedStakingRewardsModule; use farm_staking::compound_stake_farm_rewards::CompoundStakeFarmRewardsModule; use multiversx_sc::codec::multi_types::OptionalValue; @@ -25,6 +26,8 @@ use farm_staking::unstake_farm::UnstakeFarmModule; use farm_staking::*; use farm_token::FarmTokenModule; use pausable::{PausableModule, State}; +use permissions_hub::PermissionsHub; +use permissions_hub_module::PermissionsHubModule; use rewards::RewardsModule; pub static REWARD_TOKEN_ID: &[u8] = b"RIDE-abcdef"; // reward token ID @@ -38,6 +41,7 @@ pub const TOTAL_REWARDS_AMOUNT: u64 = 1_000_000_000_000; pub const USER_TOTAL_RIDE_TOKENS: u64 = 5_000_000_000; +pub const MAX_PERCENTAGE: u64 = 10_000; // 100% pub const BOOSTED_YIELDS_PERCENTAGE: u64 = 2_500; // 25% pub const MAX_REWARDS_FACTOR: u64 = 10; pub const USER_REWARDS_ENERGY_CONST: u64 = 3; @@ -52,10 +56,11 @@ pub struct NonceAmountPair { pub amount: u64, } -pub struct FarmStakingSetup +pub struct FarmStakingSetup where FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, EnergyFactoryBuilder: 'static + Copy + Fn() -> energy_factory::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, { pub b_mock: BlockchainStateWrapper, pub owner_address: Address, @@ -64,14 +69,22 @@ where pub farm_wrapper: ContractObjWrapper, FarmObjBuilder>, pub energy_factory_wrapper: ContractObjWrapper, EnergyFactoryBuilder>, + pub permissions_hub_wrapper: + ContractObjWrapper, PermissionsHubObjBuilder>, } -impl FarmStakingSetup +impl + FarmStakingSetup where FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, EnergyFactoryBuilder: 'static + Copy + Fn() -> energy_factory::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, { - pub fn new(farm_builder: FarmObjBuilder, energy_factory_builder: EnergyFactoryBuilder) -> Self { + pub fn new( + farm_builder: FarmObjBuilder, + energy_factory_builder: EnergyFactoryBuilder, + permissions_hub_builder: PermissionsHubObjBuilder, + ) -> Self { let rust_zero = rust_biguint!(0u64); let mut b_mock = BlockchainStateWrapper::new(); let owner_addr = b_mock.create_user_account(&rust_zero); @@ -85,6 +98,19 @@ where "energy_factory.wasm", ); + let permissions_hub_wrapper = b_mock.create_sc_account( + &rust_zero, + Some(&owner_addr), + permissions_hub_builder, + "permissions_hub.wasm", + ); + + b_mock + .execute_tx(&owner_addr, &permissions_hub_wrapper, &rust_zero, |sc| { + sc.init(); + }) + .assert_ok(); + // init farm contract b_mock @@ -112,6 +138,10 @@ where sc.energy_factory_address() .set(managed_address!(energy_factory_wrapper.address_ref())); + + sc.set_permissions_hub_address(managed_address!( + permissions_hub_wrapper.address_ref() + )); }) .assert_ok(); @@ -167,6 +197,7 @@ where user_address2: user_addr2, farm_wrapper, energy_factory_wrapper, + permissions_hub_wrapper, } } @@ -566,6 +597,97 @@ where ); } + pub fn stake_farm_on_behalf( + &mut self, + caller: &Address, + user: &Address, + farming_token_amount: u64, + farm_token_nonce: u64, + farm_token_amount: u64, + ) { + let mut payments = Vec::new(); + payments.push(TxTokenTransfer { + token_identifier: FARMING_TOKEN_ID.to_vec(), + nonce: 0, + value: rust_biguint!(farming_token_amount), + }); + + if farm_token_nonce > 0 { + payments.push(TxTokenTransfer { + token_identifier: FARM_TOKEN_ID.to_vec(), + nonce: farm_token_nonce, + value: rust_biguint!(farm_token_amount), + }); + } + + let b_mock = &mut self.b_mock; + b_mock + .execute_esdt_multi_transfer(caller, &self.farm_wrapper, &payments, |sc| { + let stake_farm_result = sc.stake_farm_on_behalf(managed_address!(user)); + let (out_farm_token, _reward_token) = stake_farm_result.into_tuple(); + assert_eq!( + out_farm_token.token_identifier, + managed_token_id!(FARM_TOKEN_ID) + ); + assert_eq!( + out_farm_token.amount, + managed_biguint!(farming_token_amount + farm_token_amount) + ); + }) + .assert_ok(); + } + + pub fn claim_rewards_on_behalf( + &mut self, + caller: &Address, + farm_token_nonce: u64, + farm_token_amount: u64, + ) -> u64 { + let mut result = 0; + self.b_mock + .execute_esdt_transfer( + caller, + &self.farm_wrapper, + FARM_TOKEN_ID, + farm_token_nonce, + &rust_biguint!(farm_token_amount), + |sc| { + let (out_farm_token, out_reward_token) = + sc.claim_rewards_on_behalf().into_tuple(); + assert_eq!( + out_farm_token.token_identifier, + managed_token_id!(FARM_TOKEN_ID) + ); + assert_eq!(out_farm_token.amount, managed_biguint!(farm_token_amount)); + + assert_eq!( + out_reward_token.token_identifier, + managed_token_id!(REWARD_TOKEN_ID) + ); + + result = out_reward_token.amount.to_u64().unwrap(); + }, + ) + .assert_ok(); + + result + } + + pub fn whitelist_address_on_behalf(&mut self, user: &Address, address_to_whitelist: &Address) { + self.b_mock + .execute_tx( + user, + &self.permissions_hub_wrapper, + &rust_biguint!(0), + |sc| { + let mut addresses = MultiValueEncoded::new(); + addresses.push(managed_address!(address_to_whitelist)); + sc.whitelist(addresses); + }, + ) + .assert_ok(); + } + pub fn check_farm_token_supply(&mut self, expected_farm_token_supply: u64) { self.b_mock .execute_query(&self.farm_wrapper, |sc| { diff --git a/farm-staking/farm-staking/tests/farm_staking_test.rs b/farm-staking/farm-staking/tests/farm_staking_test.rs index 26a9d065b..24ab2bef7 100644 --- a/farm-staking/farm-staking/tests/farm_staking_test.rs +++ b/farm-staking/farm-staking/tests/farm_staking_test.rs @@ -11,14 +11,21 @@ use farm_staking_setup::*; #[test] fn test_farm_setup() { - let _ = FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let _ = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); } #[test] fn test_enter_farm() { DebugApi::dummy(); - let mut farm_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut farm_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = farm_setup.user_address.clone(); @@ -38,8 +45,11 @@ fn test_enter_farm() { #[test] fn test_unstake_farm() { DebugApi::dummy(); - let mut farm_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut farm_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = farm_setup.user_address.clone(); @@ -90,8 +100,11 @@ fn test_unstake_farm() { #[test] fn test_claim_rewards() { DebugApi::dummy(); - let mut farm_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut farm_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = farm_setup.user_address.clone(); @@ -128,15 +141,21 @@ fn test_claim_rewards() { farm_setup.check_farm_token_supply(farm_in_amount); } -fn steps_enter_farm_twice( +fn steps_enter_farm_twice( farm_builder: FarmObjBuilder, energy_factory_builder: EnergyFactoryBuilder, -) -> FarmStakingSetup + permissions_hub_builder: PermissionsHubObjBuilder, +) -> FarmStakingSetup where FarmObjBuilder: 'static + Copy + Fn() -> farm_staking::ContractObj, EnergyFactoryBuilder: 'static + Copy + Fn() -> energy_factory::ContractObj, + PermissionsHubObjBuilder: 'static + Copy + Fn() -> permissions_hub::ContractObj, { - let mut farm_setup = FarmStakingSetup::new(farm_builder, energy_factory_builder); + let mut farm_setup = FarmStakingSetup::new( + farm_builder, + energy_factory_builder, + permissions_hub_builder, + ); let user_address = farm_setup.user_address.clone(); @@ -187,14 +206,21 @@ where #[test] fn test_enter_farm_twice() { DebugApi::dummy(); - let _ = steps_enter_farm_twice(farm_staking::contract_obj, energy_factory::contract_obj); + let _ = steps_enter_farm_twice( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); } #[test] fn test_exit_farm_after_enter_twice() { DebugApi::dummy(); - let mut farm_setup = - steps_enter_farm_twice(farm_staking::contract_obj, energy_factory::contract_obj); + let mut farm_setup = steps_enter_farm_twice( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = farm_setup.user_address.clone(); @@ -227,8 +253,11 @@ fn test_exit_farm_after_enter_twice() { #[test] fn test_unbond() { DebugApi::dummy(); - let mut farm_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut farm_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = farm_setup.user_address.clone(); @@ -288,8 +317,11 @@ fn test_unbond() { #[test] fn test_withdraw_rewards() { DebugApi::dummy(); - let mut farm_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut farm_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let initial_rewards_capacity = 1_000_000_000_000u64; farm_setup.check_rewards_capacity(initial_rewards_capacity); @@ -304,8 +336,11 @@ fn test_withdraw_rewards() { #[test] fn test_withdraw_after_produced_rewards() { DebugApi::dummy(); - let mut farm_setup = - FarmStakingSetup::new(farm_staking::contract_obj, energy_factory::contract_obj); + let mut farm_setup = FarmStakingSetup::new( + farm_staking::contract_obj, + energy_factory::contract_obj, + permissions_hub::contract_obj, + ); let user_address = farm_setup.user_address.clone(); diff --git a/farm-staking/farm-staking/wasm/Cargo.lock b/farm-staking/farm-staking/wasm/Cargo.lock index 465de9d30..7da9c7389 100644 --- a/farm-staking/farm-staking/wasm/Cargo.lock +++ b/farm-staking/farm-staking/wasm/Cargo.lock @@ -136,8 +136,11 @@ dependencies = [ "mergeable", "multiversx-sc", "multiversx-sc-modules", + "original_owner_helper", "pair", "pausable", + "permissions-hub", + "permissions_hub_module", "permissions_module", "rewards", "sc_whitelist_module", @@ -181,8 +184,11 @@ dependencies = [ "mergeable", "multiversx-sc", "multiversx-sc-modules", + "original_owner_helper", "pair", "pausable", + "permissions-hub", + "permissions_hub_module", "permissions_module", "rewards", "sc_whitelist_module", @@ -400,6 +406,14 @@ dependencies = [ "autocfg", ] +[[package]] +name = "original_owner_helper" +version = "0.0.0" +dependencies = [ + "common_structs", + "multiversx-sc", +] + [[package]] name = "pair" version = "0.0.0" @@ -424,6 +438,21 @@ dependencies = [ "permissions_module", ] +[[package]] +name = "permissions-hub" +version = "0.0.0" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "permissions_hub_module" +version = "0.0.0" +dependencies = [ + "multiversx-sc", + "permissions-hub", +] + [[package]] name = "permissions_module" version = "0.0.0" diff --git a/farm-staking/farm-staking/wasm/src/lib.rs b/farm-staking/farm-staking/wasm/src/lib.rs index 9489b1d7e..e0fdd5bb3 100644 --- a/farm-staking/farm-staking/wasm/src/lib.rs +++ b/farm-staking/farm-staking/wasm/src/lib.rs @@ -6,9 +6,9 @@ // Init: 1 // Upgrade: 1 -// Endpoints: 69 +// Endpoints: 72 // Async Callback: 1 -// Total number of exported functions: 72 +// Total number of exported functions: 75 #![no_std] @@ -59,6 +59,7 @@ multiversx_sc_wasm_adapter::endpoints! { removeAdmin => remove_admin_endpoint updateOwnerOrAdmin => update_owner_or_admin_endpoint getPermissions => permissions + setPermissionsHubAddress => set_permissions_hub_address setBurnRoleForAddress => set_burn_role_for_address stakeFarmThroughProxy => stake_farm_through_proxy stakeFarm => stake_farm_endpoint @@ -68,6 +69,8 @@ multiversx_sc_wasm_adapter::endpoints! { unstakeFarm => unstake_farm unstakeFarmThroughProxy => unstake_farm_through_proxy unbondFarm => unbond_farm + stakeFarmOnBehalf => stake_farm_on_behalf + claimRewardsOnBehalf => claim_rewards_on_behalf claimBoostedRewards => claim_boosted_rewards collectUndistributedBoostedRewards => collect_undistributed_boosted_rewards getBoostedYieldsRewardsPercentage => boosted_yields_rewards_percentage