diff --git a/liquid-staking/src/lib.rs b/liquid-staking/src/lib.rs index e2368eb..83668ab 100644 --- a/liquid-staking/src/lib.rs +++ b/liquid-staking/src/lib.rs @@ -3,15 +3,6 @@ multiversx_sc::imports!(); multiversx_sc::derive_imports!(); -use multiversx_sc::types::OperationCompletionStatus; -use multiversx_sc_modules::ongoing_operation::{ - CONTINUE_OP, DEFAULT_MIN_GAS_TO_SAVE_PROGRESS, STOP_OP, -}; -pub const DEFAULT_GAS_TO_CLAIM_REWARDS: u64 = 6_000_000; -pub const MIN_GAS_FOR_ASYNC_CALL: u64 = 12_000_000; -pub const MIN_GAS_FOR_CALLBACK: u64 = 12_000_000; -pub const MIN_EGLD_TO_DELEGATE: u64 = 1_000_000_000_000_000_000; -pub const RECOMPUTE_BLOCK_OFFSET: u64 = 10; pub const MAX_DELEGATION_ADDRESSES: usize = 50; pub type Epoch = u64; @@ -24,13 +15,13 @@ pub mod delegation_proxy; pub mod errors; mod events; mod liquidity_pool; +pub mod user_actions; use crate::{ delegation::{ClaimStatus, ClaimStatusType}, errors::*, }; -use config::{UnstakeTokenAttributes, UNBOND_PERIOD}; use contexts::base::*; use liquidity_pool::State; @@ -42,6 +33,13 @@ pub trait LiquidStaking: + delegation::DelegationModule + multiversx_sc_modules::ongoing_operation::OngoingOperationModule + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + user_actions::common::CommonModule + + user_actions::add_liquidity::AddLiquidityModule + + user_actions::remove_liquidity::RemoveLiquidityModule + + user_actions::unbond::UnbondModule + + user_actions::claim_rewards::ClaimRewardsModule + + user_actions::delegate_rewards::DelegateRewardsModule + + user_actions::recompute_token_reserve::RecomputeTokenReserveModule { #[init] fn init(&self) { @@ -58,469 +56,9 @@ pub trait LiquidStaking: starting_token_reserve: BigUint::zero(), }; - self.delegation_claim_status().set_if_empty(claim_status); - } - - #[payable("EGLD")] - #[endpoint(addLiquidity)] - fn add_liquidity(&self) { - self.blockchain().check_caller_is_user_account(); - let storage_cache = StorageCache::new(self); - let caller = self.blockchain().get_caller(); - - let payment = self.call_value().egld_value().clone_value(); - require!( - self.is_state_active(storage_cache.contract_state), - ERROR_NOT_ACTIVE - ); - require!(payment >= MIN_EGLD_TO_DELEGATE, ERROR_BAD_PAYMENT_AMOUNT); - - let delegation_contract = self.get_delegation_contract_for_delegate(&payment); - let gas_for_async_call = self.get_gas_for_async_call(); - - self.delegation_proxy_obj() - .contract(delegation_contract.clone()) - .delegate() - .with_gas_limit(gas_for_async_call) - .with_egld_transfer(payment.clone()) - .async_call() - .with_callback(LiquidStaking::callbacks(self).add_liquidity_callback( - caller, - delegation_contract, - payment, - )) - .call_and_exit() - } - - #[callback] - fn add_liquidity_callback( - &self, - caller: ManagedAddress, - delegation_contract: ManagedAddress, - staked_tokens: BigUint, - #[call_result] result: ManagedAsyncCallResult<()>, - ) { - match result { - ManagedAsyncCallResult::Ok(()) => { - let mut storage_cache = StorageCache::new(self); - self.delegation_contract_data(&delegation_contract) - .update(|contract_data| { - contract_data.total_staked_from_ls_contract += &staked_tokens; - }); - - let ls_token_amount = self.pool_add_liquidity(&staked_tokens, &mut storage_cache); - let user_payment = self.mint_ls_token(ls_token_amount); - self.send().direct_esdt( - &caller, - &user_payment.token_identifier, - user_payment.token_nonce, - &user_payment.amount, - ); - - self.emit_add_liquidity_event(&storage_cache, &caller, user_payment.amount); - } - ManagedAsyncCallResult::Err(_) => { - self.send().direct_egld(&caller, &staked_tokens); - self.move_delegation_contract_to_back(delegation_contract); - } - } - } - - #[payable("*")] - #[endpoint(removeLiquidity)] - fn remove_liquidity(&self) { - self.blockchain().check_caller_is_user_account(); - let mut storage_cache = StorageCache::new(self); - let caller = self.blockchain().get_caller(); - let payment = self.call_value().single_esdt(); - - require!( - self.is_state_active(storage_cache.contract_state), - ERROR_NOT_ACTIVE - ); - require!( - storage_cache.ls_token_id.is_valid_esdt_identifier(), - ERROR_LS_TOKEN_NOT_ISSUED - ); - require!( - payment.token_identifier == storage_cache.ls_token_id, - ERROR_BAD_PAYMENT_TOKEN - ); - require!(payment.amount > 0, ERROR_BAD_PAYMENT_AMOUNT); - - let egld_to_unstake = self.pool_remove_liquidity(&payment.amount, &mut storage_cache); - require!( - egld_to_unstake >= MIN_EGLD_TO_DELEGATE, - ERROR_INSUFFICIENT_UNSTAKE_AMOUNT - ); - self.burn_ls_token(&payment.amount); - - let delegation_contract = self.get_delegation_contract_for_undelegate(&egld_to_unstake); - let gas_for_async_call = self.get_gas_for_async_call(); - - self.delegation_proxy_obj() - .contract(delegation_contract.clone()) - .undelegate(egld_to_unstake.clone()) - .with_gas_limit(gas_for_async_call) - .async_call() - .with_callback(LiquidStaking::callbacks(self).remove_liquidity_callback( - caller, - delegation_contract, - egld_to_unstake, - payment.amount, - )) - .call_and_exit() - } - - #[callback] - fn remove_liquidity_callback( - &self, - caller: ManagedAddress, - delegation_contract: ManagedAddress, - egld_to_unstake: BigUint, - ls_tokens_to_be_burned: BigUint, - #[call_result] result: ManagedAsyncCallResult<()>, - ) { - let mut storage_cache = StorageCache::new(self); - match result { - ManagedAsyncCallResult::Ok(()) => { - let current_epoch = self.blockchain().get_block_epoch(); - let unbond_epoch = current_epoch + UNBOND_PERIOD; - - self.delegation_contract_data(&delegation_contract) - .update(|contract_data| { - contract_data.total_staked_from_ls_contract -= &egld_to_unstake; - contract_data.total_unstaked_from_ls_contract += &egld_to_unstake; - }); - self.unstake_token_supply() - .update(|x| *x += &egld_to_unstake); - - let virtual_position = UnstakeTokenAttributes { - delegation_contract, - unstake_epoch: current_epoch, - unstake_amount: egld_to_unstake, - unbond_epoch, - }; - - let user_payment = self.mint_unstake_tokens(&virtual_position); - self.send().direct_esdt( - &caller, - &user_payment.token_identifier, - user_payment.token_nonce, - &user_payment.amount, - ); - - self.emit_remove_liquidity_event( - &storage_cache, - ls_tokens_to_be_burned, - user_payment.amount, - ); - } - ManagedAsyncCallResult::Err(_) => { - let ls_token_amount = self.pool_add_liquidity(&egld_to_unstake, &mut storage_cache); - let user_payment = self.mint_ls_token(ls_token_amount); - self.send().direct_esdt( - &caller, - &user_payment.token_identifier, - user_payment.token_nonce, - &user_payment.amount, - ); - self.move_delegation_contract_to_back(delegation_contract); - } - } - } - - #[payable("*")] - #[endpoint(unbondTokens)] - fn unbond_tokens(&self) { - self.blockchain().check_caller_is_user_account(); - let mut storage_cache = StorageCache::new(self); - let caller = self.blockchain().get_caller(); - let payment = self.call_value().single_esdt(); - - require!( - self.is_state_active(storage_cache.contract_state), - ERROR_NOT_ACTIVE - ); - require!( - payment.token_identifier == self.unstake_token().get_token_id(), - ERROR_BAD_PAYMENT_TOKEN - ); - require!(payment.amount > 0, ERROR_BAD_PAYMENT_AMOUNT); - - let unstake_token_attributes: UnstakeTokenAttributes = self - .unstake_token() - .get_token_attributes(payment.token_nonce); - - let current_epoch = self.blockchain().get_block_epoch(); - require!( - current_epoch >= unstake_token_attributes.unbond_epoch, - ERROR_UNSTAKE_PERIOD_NOT_PASSED - ); - - let delegation_contract = unstake_token_attributes.delegation_contract; - let unstake_amount = unstake_token_attributes.unstake_amount; - let delegation_contract_mapper = self.delegation_contract_data(&delegation_contract); - let delegation_contract_data = delegation_contract_mapper.get(); - if delegation_contract_data.total_unbonded_from_ls_contract >= unstake_amount { - delegation_contract_mapper.update(|contract_data| { - contract_data.total_unstaked_from_ls_contract -= &unstake_amount; - contract_data.total_unbonded_from_ls_contract -= &unstake_amount - }); - - storage_cache.total_withdrawn_egld -= &unstake_amount; - self.unstake_token_supply() - .update(|x| *x -= &unstake_amount); - self.burn_unstake_tokens(payment.token_nonce); - self.send().direct_egld(&caller, &unstake_amount); - } else { - let gas_for_async_call = self.get_gas_for_async_call(); - self.delegation_proxy_obj() - .contract(delegation_contract.clone()) - .withdraw() - .with_gas_limit(gas_for_async_call) - .async_call() - .with_callback(LiquidStaking::callbacks(self).withdraw_tokens_callback( - caller, - delegation_contract, - payment.token_nonce, - unstake_amount, - )) - .call_and_exit(); - } - } - - #[callback] - fn withdraw_tokens_callback( - &self, - caller: ManagedAddress, - delegation_contract: ManagedAddress, - unstake_token_nonce: u64, - unstake_token_amount: BigUint, - #[call_result] result: ManagedAsyncCallResult<()>, - ) { - match result { - ManagedAsyncCallResult::Ok(()) => { - let withdraw_amount = self.call_value().egld_value().clone_value(); - let mut storage_cache = StorageCache::new(self); - let delegation_contract_mapper = - self.delegation_contract_data(&delegation_contract); - if withdraw_amount > 0u64 { - delegation_contract_mapper.update(|contract_data| { - contract_data.total_unbonded_from_ls_contract += &withdraw_amount - }); - storage_cache.total_withdrawn_egld += &withdraw_amount; - } - let delegation_contract_data = delegation_contract_mapper.get(); - if delegation_contract_data.total_unbonded_from_ls_contract >= unstake_token_amount - { - delegation_contract_mapper.update(|contract_data| { - contract_data.total_unstaked_from_ls_contract -= &unstake_token_amount; - contract_data.total_unbonded_from_ls_contract -= &unstake_token_amount; - }); - storage_cache.total_withdrawn_egld -= &unstake_token_amount; - self.unstake_token_supply() - .update(|x| *x -= &unstake_token_amount); - self.burn_unstake_tokens(unstake_token_nonce); - self.send().direct_egld(&caller, &unstake_token_amount); - } else { - self.send_back_unbond_nft(&caller, unstake_token_nonce); - } - } - ManagedAsyncCallResult::Err(_) => { - self.send_back_unbond_nft(&caller, unstake_token_nonce); - } - } - } - - #[endpoint(claimRewards)] - fn claim_rewards(&self) { - let storage_cache = StorageCache::new(self); - - require!( - self.is_state_active(storage_cache.contract_state), - ERROR_NOT_ACTIVE - ); - - let delegation_addresses_mapper = self.delegation_addresses_list(); - require!( - !delegation_addresses_mapper.is_empty(), - ERROR_NO_DELEGATION_CONTRACTS - ); - let claim_status_mapper = self.delegation_claim_status(); - let old_claim_status = claim_status_mapper.get(); - let current_epoch = self.blockchain().get_block_epoch(); - let mut current_claim_status = self.load_operation::>(); - - self.check_claim_operation(¤t_claim_status, old_claim_status, current_epoch); - self.prepare_claim_operation(&mut current_claim_status, current_epoch); - - let run_result = self.run_while_it_has_gas(DEFAULT_MIN_GAS_TO_SAVE_PROGRESS, || { - let delegation_address_node = delegation_addresses_mapper - .get_node_by_id(current_claim_status.current_node) - .unwrap(); - let next_node = delegation_address_node.get_next_node_id(); - let delegation_address = delegation_address_node.into_value(); - - self.delegation_proxy_obj() - .contract(delegation_address) - .claim_rewards() - .with_gas_limit(DEFAULT_GAS_TO_CLAIM_REWARDS) - .transfer_execute(); - - if next_node == 0 { - claim_status_mapper.set(current_claim_status.clone()); - return STOP_OP; - } else { - current_claim_status.current_node = next_node; - } - - CONTINUE_OP - }); - - match run_result { - OperationCompletionStatus::InterruptedBeforeOutOfGas => { - self.save_progress(¤t_claim_status); - } - OperationCompletionStatus::Completed => { - claim_status_mapper.update(|claim_status| { - claim_status.status = ClaimStatusType::Finished; - claim_status.last_claim_block = self.blockchain().get_block_nonce(); - }); - } - }; - } - - #[endpoint(recomputeTokenReserve)] - fn recompute_token_reserve(&self) { - let mut storage_cache = StorageCache::new(self); - let claim_status_mapper = self.delegation_claim_status(); - let mut claim_status = claim_status_mapper.get(); - - require!( - self.is_state_active(storage_cache.contract_state), - ERROR_NOT_ACTIVE - ); - require!( - claim_status.status == ClaimStatusType::Finished, - ERROR_RECOMPUTE_RESERVES - ); - - let current_block = self.blockchain().get_block_nonce(); - require!( - current_block >= claim_status.last_claim_block + RECOMPUTE_BLOCK_OFFSET, - ERROR_RECOMPUTE_TOO_SOON - ); - - let current_egld_balance = self - .blockchain() - .get_sc_balance(&EgldOrEsdtTokenIdentifier::egld(), 0); - if current_egld_balance - > &storage_cache.total_withdrawn_egld + &claim_status.starting_token_reserve - { - let rewards = ¤t_egld_balance - - &storage_cache.total_withdrawn_egld - - &claim_status.starting_token_reserve; - storage_cache.rewards_reserve += rewards; - } - - if storage_cache.rewards_reserve >= MIN_EGLD_TO_DELEGATE { - claim_status.status = ClaimStatusType::Delegable; - } else { - claim_status.status = ClaimStatusType::Insufficient; - } - - claim_status_mapper.set(claim_status); - } - - #[endpoint(delegateRewards)] - fn delegate_rewards(&self) { - let mut storage_cache = StorageCache::new(self); - let claim_status = self.delegation_claim_status().get(); - require!( - self.is_state_active(storage_cache.contract_state), - ERROR_NOT_ACTIVE - ); - require!( - claim_status.status == ClaimStatusType::Delegable, - ERROR_CLAIM_REDELEGATE - ); - - let rewards_reserve = storage_cache.rewards_reserve.clone(); - storage_cache.rewards_reserve = BigUint::zero(); - let delegation_contract = self.get_delegation_contract_for_delegate(&rewards_reserve); - let gas_for_async_call = self.get_gas_for_async_call(); - - self.delegation_proxy_obj() - .contract(delegation_contract.clone()) - .delegate() - .with_gas_limit(gas_for_async_call) - .with_egld_transfer(rewards_reserve.clone()) - .async_call() - .with_callback( - LiquidStaking::callbacks(self) - .delegate_rewards_callback(delegation_contract, rewards_reserve), - ) - .call_and_exit() - } - - #[callback] - fn delegate_rewards_callback( - &self, - delegation_contract: ManagedAddress, - staked_tokens: BigUint, - #[call_result] result: ManagedAsyncCallResult<()>, - ) { - let mut storage_cache = StorageCache::new(self); - match result { - ManagedAsyncCallResult::Ok(()) => { - self.delegation_contract_data(&delegation_contract) - .update(|contract_data| { - contract_data.total_staked_from_ls_contract += &staked_tokens; - }); - - self.delegation_claim_status() - .update(|claim_status| claim_status.status = ClaimStatusType::Redelegated); - - storage_cache.virtual_egld_reserve += &staked_tokens; - let sc_address = self.blockchain().get_sc_address(); - self.emit_add_liquidity_event(&storage_cache, &sc_address, BigUint::zero()); - } - ManagedAsyncCallResult::Err(_) => { - storage_cache.rewards_reserve = staked_tokens; - self.move_delegation_contract_to_back(delegation_contract); - } - } - } - - fn get_gas_for_async_call(&self) -> u64 { - let gas_left = self.blockchain().get_gas_left(); - require!( - gas_left > MIN_GAS_FOR_ASYNC_CALL + MIN_GAS_FOR_CALLBACK, - ERROR_INSUFFICIENT_GAS - ); - gas_left - MIN_GAS_FOR_CALLBACK - } - - fn send_back_unbond_nft(&self, caller: &ManagedAddress, unstake_token_nonce: u64) { - let unstake_token_id = self.unstake_token().get_token_id(); - self.send().direct_esdt( - caller, - &unstake_token_id, - unstake_token_nonce, - &BigUint::from(1u64), - ) - } - - // views - #[view(getLsValueForPosition)] - fn get_ls_value_for_position(&self, ls_token_amount: BigUint) -> BigUint { - let storage_cache = StorageCache::new(self); - self.get_egld_amount(&ls_token_amount, &storage_cache) + self.delegation_claim_status().set(claim_status); } - // proxy - - #[proxy] - fn delegation_proxy_obj(&self) -> delegation_proxy::Proxy; + #[upgrade] + fn upgrade(&self) {} } diff --git a/liquid-staking/src/user_actions/add_liquidity.rs b/liquid-staking/src/user_actions/add_liquidity.rs new file mode 100644 index 0000000..e92e11a --- /dev/null +++ b/liquid-staking/src/user_actions/add_liquidity.rs @@ -0,0 +1,83 @@ +use crate::{ + delegation_proxy::ProxyTrait as _, user_actions::common::MIN_EGLD_TO_DELEGATE, StorageCache, + ERROR_BAD_PAYMENT_AMOUNT, ERROR_NOT_ACTIVE, +}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait AddLiquidityModule: + crate::config::ConfigModule + + crate::events::EventsModule + + crate::delegation::DelegationModule + + crate::liquidity_pool::LiquidityPoolModule + + multiversx_sc_modules::ongoing_operation::OngoingOperationModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + super::common::CommonModule +{ + #[payable("EGLD")] + #[endpoint(addLiquidity)] + fn add_liquidity(&self) { + self.blockchain().check_caller_is_user_account(); + + let storage_cache = StorageCache::new(self); + let caller = self.blockchain().get_caller(); + + let payment = self.call_value().egld_value().clone_value(); + require!( + self.is_state_active(storage_cache.contract_state), + ERROR_NOT_ACTIVE + ); + require!(payment >= MIN_EGLD_TO_DELEGATE, ERROR_BAD_PAYMENT_AMOUNT); + + let delegation_contract = self.get_delegation_contract_for_delegate(&payment); + let gas_for_async_call = self.get_gas_for_async_call(); + + self.delegation_proxy_obj() + .contract(delegation_contract.clone()) + .delegate() + .with_gas_limit(gas_for_async_call) + .with_egld_transfer(payment.clone()) + .async_call() + .with_callback(AddLiquidityModule::callbacks(self).add_liquidity_callback( + caller, + delegation_contract, + payment, + )) + .call_and_exit() + } + + #[callback] + fn add_liquidity_callback( + &self, + caller: ManagedAddress, + delegation_contract: ManagedAddress, + staked_tokens: BigUint, + #[call_result] result: ManagedAsyncCallResult<()>, + ) { + match result { + ManagedAsyncCallResult::Ok(()) => { + let mut storage_cache = StorageCache::new(self); + self.delegation_contract_data(&delegation_contract) + .update(|contract_data| { + contract_data.total_staked_from_ls_contract += &staked_tokens; + }); + + let ls_token_amount = self.pool_add_liquidity(&staked_tokens, &mut storage_cache); + let user_payment = self.mint_ls_token(ls_token_amount); + self.send().direct_esdt( + &caller, + &user_payment.token_identifier, + user_payment.token_nonce, + &user_payment.amount, + ); + + self.emit_add_liquidity_event(&storage_cache, &caller, user_payment.amount); + } + ManagedAsyncCallResult::Err(_) => { + self.send().direct_egld(&caller, &staked_tokens); + self.move_delegation_contract_to_back(delegation_contract); + } + } + } +} diff --git a/liquid-staking/src/user_actions/claim_rewards.rs b/liquid-staking/src/user_actions/claim_rewards.rs new file mode 100644 index 0000000..b687adc --- /dev/null +++ b/liquid-staking/src/user_actions/claim_rewards.rs @@ -0,0 +1,82 @@ +use multiversx_sc_modules::ongoing_operation::{ + CONTINUE_OP, DEFAULT_MIN_GAS_TO_SAVE_PROGRESS, STOP_OP, +}; + +use crate::{ + delegation::{ClaimStatus, ClaimStatusType}, + delegation_proxy::ProxyTrait as _, + StorageCache, ERROR_NOT_ACTIVE, ERROR_NO_DELEGATION_CONTRACTS, +}; + +multiversx_sc::imports!(); + +pub const DEFAULT_GAS_TO_CLAIM_REWARDS: u64 = 6_000_000; + +#[multiversx_sc::module] +pub trait ClaimRewardsModule: + crate::config::ConfigModule + + crate::events::EventsModule + + crate::delegation::DelegationModule + + crate::liquidity_pool::LiquidityPoolModule + + multiversx_sc_modules::ongoing_operation::OngoingOperationModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + super::common::CommonModule +{ + #[endpoint(claimRewards)] + fn claim_rewards(&self) { + let storage_cache = StorageCache::new(self); + + require!( + self.is_state_active(storage_cache.contract_state), + ERROR_NOT_ACTIVE + ); + + let delegation_addresses_mapper = self.delegation_addresses_list(); + require!( + !delegation_addresses_mapper.is_empty(), + ERROR_NO_DELEGATION_CONTRACTS + ); + let claim_status_mapper = self.delegation_claim_status(); + let old_claim_status = claim_status_mapper.get(); + let current_epoch = self.blockchain().get_block_epoch(); + let mut current_claim_status = self.load_operation::>(); + + self.check_claim_operation(¤t_claim_status, old_claim_status, current_epoch); + self.prepare_claim_operation(&mut current_claim_status, current_epoch); + + let run_result = self.run_while_it_has_gas(DEFAULT_MIN_GAS_TO_SAVE_PROGRESS, || { + let delegation_address_node = delegation_addresses_mapper + .get_node_by_id(current_claim_status.current_node) + .unwrap(); + let next_node = delegation_address_node.get_next_node_id(); + let delegation_address = delegation_address_node.into_value(); + + self.delegation_proxy_obj() + .contract(delegation_address) + .claim_rewards() + .with_gas_limit(DEFAULT_GAS_TO_CLAIM_REWARDS) + .transfer_execute(); + + if next_node == 0 { + claim_status_mapper.set(current_claim_status.clone()); + return STOP_OP; + } else { + current_claim_status.current_node = next_node; + } + + CONTINUE_OP + }); + + match run_result { + OperationCompletionStatus::InterruptedBeforeOutOfGas => { + self.save_progress(¤t_claim_status); + } + OperationCompletionStatus::Completed => { + claim_status_mapper.update(|claim_status| { + claim_status.status = ClaimStatusType::Finished; + claim_status.last_claim_block = self.blockchain().get_block_nonce(); + }); + } + }; + } +} diff --git a/liquid-staking/src/user_actions/common.rs b/liquid-staking/src/user_actions/common.rs new file mode 100644 index 0000000..2fa401c --- /dev/null +++ b/liquid-staking/src/user_actions/common.rs @@ -0,0 +1,44 @@ +use crate::{delegation_proxy, StorageCache, ERROR_INSUFFICIENT_GAS}; + +multiversx_sc::imports!(); + +pub const MIN_GAS_FOR_CALLBACK: u64 = 12_000_000; +pub const MIN_GAS_FOR_ASYNC_CALL: u64 = 12_000_000; +pub const MIN_EGLD_TO_DELEGATE: u64 = 1_000_000_000_000_000_000; + +#[multiversx_sc::module] +pub trait CommonModule: + crate::config::ConfigModule + + crate::liquidity_pool::LiquidityPoolModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule +{ + // views + #[view(getLsValueForPosition)] + fn get_ls_value_for_position(&self, ls_token_amount: BigUint) -> BigUint { + let storage_cache = StorageCache::new(self); + self.get_egld_amount(&ls_token_amount, &storage_cache) + } + + fn get_gas_for_async_call(&self) -> u64 { + let gas_left = self.blockchain().get_gas_left(); + require!( + gas_left > MIN_GAS_FOR_ASYNC_CALL + MIN_GAS_FOR_CALLBACK, + ERROR_INSUFFICIENT_GAS + ); + + gas_left - MIN_GAS_FOR_CALLBACK + } + + fn send_back_unbond_nft(&self, caller: &ManagedAddress, unstake_token_nonce: u64) { + let unstake_token_id = self.unstake_token().get_token_id(); + self.send().direct_esdt( + caller, + &unstake_token_id, + unstake_token_nonce, + &BigUint::from(1u64), + ) + } + + #[proxy] + fn delegation_proxy_obj(&self) -> delegation_proxy::Proxy; +} diff --git a/liquid-staking/src/user_actions/delegate_rewards.rs b/liquid-staking/src/user_actions/delegate_rewards.rs new file mode 100644 index 0000000..f992e90 --- /dev/null +++ b/liquid-staking/src/user_actions/delegate_rewards.rs @@ -0,0 +1,77 @@ +use crate::{ + delegation::ClaimStatusType, delegation_proxy::ProxyTrait as _, StorageCache, + ERROR_CLAIM_REDELEGATE, ERROR_NOT_ACTIVE, +}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait DelegateRewardsModule: + crate::config::ConfigModule + + crate::events::EventsModule + + crate::delegation::DelegationModule + + crate::liquidity_pool::LiquidityPoolModule + + multiversx_sc_modules::ongoing_operation::OngoingOperationModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + super::common::CommonModule +{ + #[endpoint(delegateRewards)] + fn delegate_rewards(&self) { + let mut storage_cache = StorageCache::new(self); + let claim_status = self.delegation_claim_status().get(); + require!( + self.is_state_active(storage_cache.contract_state), + ERROR_NOT_ACTIVE + ); + require!( + claim_status.status == ClaimStatusType::Delegable, + ERROR_CLAIM_REDELEGATE + ); + + let rewards_reserve = storage_cache.rewards_reserve.clone(); + storage_cache.rewards_reserve = BigUint::zero(); + let delegation_contract = self.get_delegation_contract_for_delegate(&rewards_reserve); + let gas_for_async_call = self.get_gas_for_async_call(); + + self.delegation_proxy_obj() + .contract(delegation_contract.clone()) + .delegate() + .with_gas_limit(gas_for_async_call) + .with_egld_transfer(rewards_reserve.clone()) + .async_call() + .with_callback( + DelegateRewardsModule::callbacks(self) + .delegate_rewards_callback(delegation_contract, rewards_reserve), + ) + .call_and_exit() + } + + #[callback] + fn delegate_rewards_callback( + &self, + delegation_contract: ManagedAddress, + staked_tokens: BigUint, + #[call_result] result: ManagedAsyncCallResult<()>, + ) { + let mut storage_cache = StorageCache::new(self); + match result { + ManagedAsyncCallResult::Ok(()) => { + self.delegation_contract_data(&delegation_contract) + .update(|contract_data| { + contract_data.total_staked_from_ls_contract += &staked_tokens; + }); + + self.delegation_claim_status() + .update(|claim_status| claim_status.status = ClaimStatusType::Redelegated); + + storage_cache.virtual_egld_reserve += &staked_tokens; + let sc_address = self.blockchain().get_sc_address(); + self.emit_add_liquidity_event(&storage_cache, &sc_address, BigUint::zero()); + } + ManagedAsyncCallResult::Err(_) => { + storage_cache.rewards_reserve = staked_tokens; + self.move_delegation_contract_to_back(delegation_contract); + } + } + } +} diff --git a/liquid-staking/src/user_actions/mod.rs b/liquid-staking/src/user_actions/mod.rs new file mode 100644 index 0000000..cf1630d --- /dev/null +++ b/liquid-staking/src/user_actions/mod.rs @@ -0,0 +1,7 @@ +pub mod add_liquidity; +pub mod claim_rewards; +pub mod common; +pub mod delegate_rewards; +pub mod recompute_token_reserve; +pub mod remove_liquidity; +pub mod unbond; diff --git a/liquid-staking/src/user_actions/recompute_token_reserve.rs b/liquid-staking/src/user_actions/recompute_token_reserve.rs new file mode 100644 index 0000000..cd1e4bb --- /dev/null +++ b/liquid-staking/src/user_actions/recompute_token_reserve.rs @@ -0,0 +1,61 @@ +use crate::{ + delegation::ClaimStatusType, user_actions::common::MIN_EGLD_TO_DELEGATE, StorageCache, + ERROR_NOT_ACTIVE, ERROR_RECOMPUTE_RESERVES, ERROR_RECOMPUTE_TOO_SOON, +}; + +multiversx_sc::imports!(); + +pub const RECOMPUTE_BLOCK_OFFSET: u64 = 10; + +#[multiversx_sc::module] +pub trait RecomputeTokenReserveModule: + crate::config::ConfigModule + + crate::events::EventsModule + + crate::delegation::DelegationModule + + crate::liquidity_pool::LiquidityPoolModule + + multiversx_sc_modules::ongoing_operation::OngoingOperationModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + super::common::CommonModule +{ + #[endpoint(recomputeTokenReserve)] + fn recompute_token_reserve(&self) { + let mut storage_cache = StorageCache::new(self); + let claim_status_mapper = self.delegation_claim_status(); + let mut claim_status = claim_status_mapper.get(); + + require!( + self.is_state_active(storage_cache.contract_state), + ERROR_NOT_ACTIVE + ); + require!( + claim_status.status == ClaimStatusType::Finished, + ERROR_RECOMPUTE_RESERVES + ); + + let current_block = self.blockchain().get_block_nonce(); + require!( + current_block >= claim_status.last_claim_block + RECOMPUTE_BLOCK_OFFSET, + ERROR_RECOMPUTE_TOO_SOON + ); + + let current_egld_balance = self + .blockchain() + .get_sc_balance(&EgldOrEsdtTokenIdentifier::egld(), 0); + if current_egld_balance + > &storage_cache.total_withdrawn_egld + &claim_status.starting_token_reserve + { + let rewards = ¤t_egld_balance + - &storage_cache.total_withdrawn_egld + - &claim_status.starting_token_reserve; + storage_cache.rewards_reserve += rewards; + } + + if storage_cache.rewards_reserve >= MIN_EGLD_TO_DELEGATE { + claim_status.status = ClaimStatusType::Delegable; + } else { + claim_status.status = ClaimStatusType::Insufficient; + } + + claim_status_mapper.set(claim_status); + } +} diff --git a/liquid-staking/src/user_actions/remove_liquidity.rs b/liquid-staking/src/user_actions/remove_liquidity.rs new file mode 100644 index 0000000..1f8bb49 --- /dev/null +++ b/liquid-staking/src/user_actions/remove_liquidity.rs @@ -0,0 +1,126 @@ +use crate::{ + config::{UnstakeTokenAttributes, UNBOND_PERIOD}, + delegation_proxy::ProxyTrait as _, + user_actions::common::MIN_EGLD_TO_DELEGATE, + StorageCache, ERROR_BAD_PAYMENT_AMOUNT, ERROR_BAD_PAYMENT_TOKEN, + ERROR_INSUFFICIENT_UNSTAKE_AMOUNT, ERROR_LS_TOKEN_NOT_ISSUED, ERROR_NOT_ACTIVE, +}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait RemoveLiquidityModule: + crate::config::ConfigModule + + crate::events::EventsModule + + crate::delegation::DelegationModule + + crate::liquidity_pool::LiquidityPoolModule + + multiversx_sc_modules::ongoing_operation::OngoingOperationModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + super::common::CommonModule +{ + #[payable("*")] + #[endpoint(removeLiquidity)] + fn remove_liquidity(&self) { + self.blockchain().check_caller_is_user_account(); + let mut storage_cache = StorageCache::new(self); + let caller = self.blockchain().get_caller(); + let payment = self.call_value().single_esdt(); + + require!( + self.is_state_active(storage_cache.contract_state), + ERROR_NOT_ACTIVE + ); + require!( + storage_cache.ls_token_id.is_valid_esdt_identifier(), + ERROR_LS_TOKEN_NOT_ISSUED + ); + require!( + payment.token_identifier == storage_cache.ls_token_id, + ERROR_BAD_PAYMENT_TOKEN + ); + require!(payment.amount > 0, ERROR_BAD_PAYMENT_AMOUNT); + + let egld_to_unstake = self.pool_remove_liquidity(&payment.amount, &mut storage_cache); + require!( + egld_to_unstake >= MIN_EGLD_TO_DELEGATE, + ERROR_INSUFFICIENT_UNSTAKE_AMOUNT + ); + self.burn_ls_token(&payment.amount); + + let delegation_contract = self.get_delegation_contract_for_undelegate(&egld_to_unstake); + let gas_for_async_call = self.get_gas_for_async_call(); + + self.delegation_proxy_obj() + .contract(delegation_contract.clone()) + .undelegate(egld_to_unstake.clone()) + .with_gas_limit(gas_for_async_call) + .async_call() + .with_callback( + RemoveLiquidityModule::callbacks(self).remove_liquidity_callback( + caller, + delegation_contract, + egld_to_unstake, + payment.amount, + ), + ) + .call_and_exit() + } + + #[callback] + fn remove_liquidity_callback( + &self, + caller: ManagedAddress, + delegation_contract: ManagedAddress, + egld_to_unstake: BigUint, + ls_tokens_to_be_burned: BigUint, + #[call_result] result: ManagedAsyncCallResult<()>, + ) { + let mut storage_cache = StorageCache::new(self); + match result { + ManagedAsyncCallResult::Ok(()) => { + let current_epoch = self.blockchain().get_block_epoch(); + let unbond_epoch = current_epoch + UNBOND_PERIOD; + + self.delegation_contract_data(&delegation_contract) + .update(|contract_data| { + contract_data.total_staked_from_ls_contract -= &egld_to_unstake; + contract_data.total_unstaked_from_ls_contract += &egld_to_unstake; + }); + self.unstake_token_supply() + .update(|x| *x += &egld_to_unstake); + + let virtual_position = UnstakeTokenAttributes { + delegation_contract, + unstake_epoch: current_epoch, + unstake_amount: egld_to_unstake, + unbond_epoch, + }; + + let user_payment = self.mint_unstake_tokens(&virtual_position); + self.send().direct_esdt( + &caller, + &user_payment.token_identifier, + user_payment.token_nonce, + &user_payment.amount, + ); + + self.emit_remove_liquidity_event( + &storage_cache, + ls_tokens_to_be_burned, + user_payment.amount, + ); + } + ManagedAsyncCallResult::Err(_) => { + let ls_token_amount = self.pool_add_liquidity(&egld_to_unstake, &mut storage_cache); + let user_payment = self.mint_ls_token(ls_token_amount); + self.send().direct_esdt( + &caller, + &user_payment.token_identifier, + user_payment.token_nonce, + &user_payment.amount, + ); + self.move_delegation_contract_to_back(delegation_contract); + } + } + } +} diff --git a/liquid-staking/src/user_actions/unbond.rs b/liquid-staking/src/user_actions/unbond.rs new file mode 100644 index 0000000..090b72f --- /dev/null +++ b/liquid-staking/src/user_actions/unbond.rs @@ -0,0 +1,121 @@ +use crate::{ + config::UnstakeTokenAttributes, delegation_proxy::ProxyTrait as _, StorageCache, + ERROR_BAD_PAYMENT_AMOUNT, ERROR_BAD_PAYMENT_TOKEN, ERROR_NOT_ACTIVE, + ERROR_UNSTAKE_PERIOD_NOT_PASSED, +}; + +multiversx_sc::imports!(); + +#[multiversx_sc::module] +pub trait UnbondModule: + crate::config::ConfigModule + + crate::events::EventsModule + + crate::delegation::DelegationModule + + crate::liquidity_pool::LiquidityPoolModule + + multiversx_sc_modules::ongoing_operation::OngoingOperationModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + super::common::CommonModule +{ + #[payable("*")] + #[endpoint(unbondTokens)] + fn unbond_tokens(&self) { + self.blockchain().check_caller_is_user_account(); + let mut storage_cache = StorageCache::new(self); + let caller = self.blockchain().get_caller(); + let payment = self.call_value().single_esdt(); + + require!( + self.is_state_active(storage_cache.contract_state), + ERROR_NOT_ACTIVE + ); + require!( + payment.token_identifier == self.unstake_token().get_token_id(), + ERROR_BAD_PAYMENT_TOKEN + ); + require!(payment.amount > 0, ERROR_BAD_PAYMENT_AMOUNT); + + let unstake_token_attributes: UnstakeTokenAttributes = self + .unstake_token() + .get_token_attributes(payment.token_nonce); + + let current_epoch = self.blockchain().get_block_epoch(); + require!( + current_epoch >= unstake_token_attributes.unbond_epoch, + ERROR_UNSTAKE_PERIOD_NOT_PASSED + ); + + let delegation_contract = unstake_token_attributes.delegation_contract; + let unstake_amount = unstake_token_attributes.unstake_amount; + let delegation_contract_mapper = self.delegation_contract_data(&delegation_contract); + let delegation_contract_data = delegation_contract_mapper.get(); + if delegation_contract_data.total_unbonded_from_ls_contract >= unstake_amount { + delegation_contract_mapper.update(|contract_data| { + contract_data.total_unstaked_from_ls_contract -= &unstake_amount; + contract_data.total_unbonded_from_ls_contract -= &unstake_amount + }); + + storage_cache.total_withdrawn_egld -= &unstake_amount; + self.unstake_token_supply() + .update(|x| *x -= &unstake_amount); + self.burn_unstake_tokens(payment.token_nonce); + self.send().direct_egld(&caller, &unstake_amount); + } else { + let gas_for_async_call = self.get_gas_for_async_call(); + self.delegation_proxy_obj() + .contract(delegation_contract.clone()) + .withdraw() + .with_gas_limit(gas_for_async_call) + .async_call() + .with_callback(UnbondModule::callbacks(self).withdraw_tokens_callback( + caller, + delegation_contract, + payment.token_nonce, + unstake_amount, + )) + .call_and_exit(); + } + } + + #[callback] + fn withdraw_tokens_callback( + &self, + caller: ManagedAddress, + delegation_contract: ManagedAddress, + unstake_token_nonce: u64, + unstake_token_amount: BigUint, + #[call_result] result: ManagedAsyncCallResult<()>, + ) { + match result { + ManagedAsyncCallResult::Ok(()) => { + let withdraw_amount = self.call_value().egld_value().clone_value(); + let mut storage_cache = StorageCache::new(self); + let delegation_contract_mapper = + self.delegation_contract_data(&delegation_contract); + if withdraw_amount > 0u64 { + delegation_contract_mapper.update(|contract_data| { + contract_data.total_unbonded_from_ls_contract += &withdraw_amount + }); + storage_cache.total_withdrawn_egld += &withdraw_amount; + } + let delegation_contract_data = delegation_contract_mapper.get(); + if delegation_contract_data.total_unbonded_from_ls_contract >= unstake_token_amount + { + delegation_contract_mapper.update(|contract_data| { + contract_data.total_unstaked_from_ls_contract -= &unstake_token_amount; + contract_data.total_unbonded_from_ls_contract -= &unstake_token_amount; + }); + storage_cache.total_withdrawn_egld -= &unstake_token_amount; + self.unstake_token_supply() + .update(|x| *x -= &unstake_token_amount); + self.burn_unstake_tokens(unstake_token_nonce); + self.send().direct_egld(&caller, &unstake_token_amount); + } else { + self.send_back_unbond_nft(&caller, unstake_token_nonce); + } + } + ManagedAsyncCallResult::Err(_) => { + self.send_back_unbond_nft(&caller, unstake_token_nonce); + } + } + } +} diff --git a/liquid-staking/tests/contract_interactions/mod.rs b/liquid-staking/tests/contract_interactions/mod.rs index 6ce6702..9861e8a 100644 --- a/liquid-staking/tests/contract_interactions/mod.rs +++ b/liquid-staking/tests/contract_interactions/mod.rs @@ -1,6 +1,12 @@ use crate::contract_setup::LiquidStakingContractSetup; use liquid_staking::config::{ConfigModule, UnstakeTokenAttributes}; -use liquid_staking::LiquidStaking; +use liquid_staking::user_actions::add_liquidity::AddLiquidityModule; +use liquid_staking::user_actions::claim_rewards::ClaimRewardsModule; +use liquid_staking::user_actions::common::CommonModule; +use liquid_staking::user_actions::delegate_rewards::DelegateRewardsModule; +use liquid_staking::user_actions::recompute_token_reserve::RecomputeTokenReserveModule; +use liquid_staking::user_actions::remove_liquidity::RemoveLiquidityModule; +use liquid_staking::user_actions::unbond::UnbondModule; use multiversx_sc::types::Address; use multiversx_sc_scenario::{managed_address, num_bigint, rust_biguint, DebugApi}; diff --git a/liquid-staking/wasm/src/lib.rs b/liquid-staking/wasm/src/lib.rs index 0b09c8c..aed2896 100644 --- a/liquid-staking/wasm/src/lib.rs +++ b/liquid-staking/wasm/src/lib.rs @@ -5,9 +5,9 @@ //////////////////////////////////////////////////// // Init: 1 -// Endpoints: 31 +// Endpoints: 32 // Async Callback: 1 -// Total number of exported functions: 33 +// Total number of exported functions: 34 #![no_std] #![allow(internal_features)] @@ -20,13 +20,7 @@ multiversx_sc_wasm_adapter::endpoints! { liquid_staking ( init => init - addLiquidity => add_liquidity - removeLiquidity => remove_liquidity - unbondTokens => unbond_tokens - claimRewards => claim_rewards - recomputeTokenReserve => recompute_token_reserve - delegateRewards => delegate_rewards - getLsValueForPosition => get_ls_value_for_position + upgrade => upgrade registerLsToken => register_ls_token registerUnstakeToken => register_unstake_token setStateActive => set_state_active @@ -51,6 +45,13 @@ multiversx_sc_wasm_adapter::endpoints! { getDelegationClaimStatus => delegation_claim_status maxDelegationAddresses => max_delegation_addresses getDelegationContractData => delegation_contract_data + getLsValueForPosition => get_ls_value_for_position + addLiquidity => add_liquidity + removeLiquidity => remove_liquidity + unbondTokens => unbond_tokens + claimRewards => claim_rewards + delegateRewards => delegate_rewards + recomputeTokenReserve => recompute_token_reserve ) }