diff --git a/Cargo.lock b/Cargo.lock index 25e7523..34e3c67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,7 +382,6 @@ dependencies = [ name = "delegation-mock" version = "0.0.0" dependencies = [ - "itertools 0.10.5", "multiversx-sc", "multiversx-sc-scenario", ] @@ -986,15 +985,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -1057,20 +1047,11 @@ name = "liquid-staking" version = "0.0.0" dependencies = [ "delegation-mock", - "itertools 0.10.5", "multiversx-sc", "multiversx-sc-modules", "multiversx-sc-scenario", ] -[[package]] -name = "liquid-staking-abi" -version = "0.0.0" -dependencies = [ - "liquid-staking", - "multiversx-sc-meta-lib", -] - [[package]] name = "liquid-staking-interactor" version = "0.0.0" @@ -1083,6 +1064,14 @@ dependencies = [ "toml", ] +[[package]] +name = "liquid-staking-meta" +version = "0.0.0" +dependencies = [ + "liquid-staking", + "multiversx-sc-meta-lib", +] + [[package]] name = "litemap" version = "0.7.4" @@ -1174,7 +1163,7 @@ dependencies = [ "ed25519-dalek", "hex", "hex-literal", - "itertools 0.13.0", + "itertools", "multiversx-chain-core", "multiversx-chain-vm-executor", "num-bigint", @@ -1284,7 +1273,7 @@ dependencies = [ "bech32", "colored", "hex", - "itertools 0.13.0", + "itertools", "log", "multiversx-chain-scenario-format", "multiversx-chain-vm", @@ -1333,7 +1322,7 @@ dependencies = [ "ctr", "hex", "hmac", - "itertools 0.13.0", + "itertools", "log", "multiversx-chain-core", "pbkdf2", @@ -1357,7 +1346,7 @@ checksum = "08809666d2cfdf3c57e487a1749a18464898ade2cbab73c02e3ea7bbc55026f8" dependencies = [ "anyhow", "hex", - "itertools 0.13.0", + "itertools", "log", "multiversx-sdk", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 8789d5f..ef2e5d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,5 @@ members = [ "liquid-staking/meta", "liquid-staking/interactor", "delegation-mock", - "delegation-mock/meta" + "delegation-mock/meta", ] diff --git a/delegation-mock/Cargo.toml b/delegation-mock/Cargo.toml index b9d4cae..eebfa2f 100644 --- a/delegation-mock/Cargo.toml +++ b/delegation-mock/Cargo.toml @@ -11,9 +11,5 @@ path = "src/delegation.rs" [dependencies.multiversx-sc] version = "0.54.4" -[dependencies.itertools] -version = "0.10.1" -default-features = false - [dev-dependencies.multiversx-sc-scenario] version = "0.54.4" diff --git a/delegation-mock/wasm/Cargo.lock b/delegation-mock/wasm/Cargo.lock index da4b1aa..6896fe1 100644 --- a/delegation-mock/wasm/Cargo.lock +++ b/delegation-mock/wasm/Cargo.lock @@ -24,7 +24,6 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" name = "delegation-mock" version = "0.0.0" dependencies = [ - "itertools", "multiversx-sc", ] @@ -36,12 +35,6 @@ dependencies = [ "multiversx-sc-wasm-adapter", ] -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - [[package]] name = "endian-type" version = "0.1.2" @@ -60,15 +53,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "multiversx-chain-core" version = "0.11.1" diff --git a/liquid-staking/Cargo.toml b/liquid-staking/Cargo.toml index 38812b9..bb7fe33 100644 --- a/liquid-staking/Cargo.toml +++ b/liquid-staking/Cargo.toml @@ -8,10 +8,6 @@ publish = false [lib] path = "src/lib.rs" -[dependencies.itertools] -version = "0.10.1" -default-features = false - [dependencies.multiversx-sc] version = "0.54.4" diff --git a/liquid-staking/interactor/src/proxy.rs b/liquid-staking/interactor/src/proxy.rs index 53565f0..4530a11 100644 --- a/liquid-staking/interactor/src/proxy.rs +++ b/liquid-staking/interactor/src/proxy.rs @@ -81,70 +81,6 @@ where To: TxTo, Gas: TxGas, { - pub fn add_liquidity( - self, - ) -> TxTypedCall { - self.wrapped_tx - .raw_call("addLiquidity") - .original_result() - } - - pub fn remove_liquidity( - self, - ) -> TxTypedCall { - self.wrapped_tx - .raw_call("removeLiquidity") - .original_result() - } - - pub fn unbond_tokens( - self, - ) -> TxTypedCall { - self.wrapped_tx - .raw_call("unbondTokens") - .original_result() - } - - pub fn withdraw_all< - Arg0: ProxyArg>, - >( - self, - delegation_contract: Arg0, - ) -> TxTypedCall { - self.wrapped_tx - .payment(NotPayable) - .raw_call("withdrawAll") - .argument(&delegation_contract) - .original_result() - } - - pub fn claim_rewards( - self, - ) -> TxTypedCall { - self.wrapped_tx - .payment(NotPayable) - .raw_call("claimRewards") - .original_result() - } - - pub fn recompute_token_reserve( - self, - ) -> TxTypedCall { - self.wrapped_tx - .payment(NotPayable) - .raw_call("recomputeTokenReserve") - .original_result() - } - - pub fn delegate_rewards( - self, - ) -> TxTypedCall { - self.wrapped_tx - .payment(NotPayable) - .raw_call("delegateRewards") - .original_result() - } - pub fn get_ls_value_for_position< Arg0: ProxyArg>, >( @@ -430,17 +366,74 @@ where .argument(&contract_address) .original_result() } -} -#[type_abi] -#[derive(TopEncode, TopDecode, PartialEq, Eq, Copy, Clone, Debug)] -pub enum State { - Inactive, - Active, + pub fn claim_rewards( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("claimRewards") + .original_result() + } + + pub fn delegate_rewards( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("delegateRewards") + .original_result() + } + + pub fn recompute_token_reserve( + self, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("recomputeTokenReserve") + .original_result() + } + + pub fn unbond_tokens( + self, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("unbondTokens") + .original_result() + } + + pub fn withdraw_all< + Arg0: ProxyArg>, + >( + self, + delegation_contract: Arg0, + ) -> TxTypedCall { + self.wrapped_tx + .payment(NotPayable) + .raw_call("withdrawAll") + .argument(&delegation_contract) + .original_result() + } + + pub fn add_liquidity( + self, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("addLiquidity") + .original_result() + } + + pub fn remove_liquidity( + self, + ) -> TxTypedCall { + self.wrapped_tx + .raw_call("removeLiquidity") + .original_result() + } } #[type_abi] -#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, PartialEq, Eq, Clone)] +#[derive(TopEncode)] pub struct AddLiquidityEvent where Api: ManagedTypeApi, @@ -457,7 +450,7 @@ where } #[type_abi] -#[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, PartialEq, Eq, Clone)] +#[derive(TopEncode)] pub struct RemoveLiquidityEvent where Api: ManagedTypeApi, @@ -475,6 +468,13 @@ where pub timestamp: u64, } +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Eq, Copy, Clone, Debug)] +pub enum State { + Inactive, + Active, +} + #[type_abi] #[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, PartialEq, Eq, Clone)] pub enum ClaimStatusType { diff --git a/liquid-staking/meta/Cargo.toml b/liquid-staking/meta/Cargo.toml index b7930db..4fa1c46 100644 --- a/liquid-staking/meta/Cargo.toml +++ b/liquid-staking/meta/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "liquid-staking-abi" +name = "liquid-staking-meta" version = "0.0.0" authors = ["Sorin Petreasca "] edition = "2021" diff --git a/liquid-staking/src/basics/constants.rs b/liquid-staking/src/basics/constants.rs new file mode 100644 index 0000000..1bd95d9 --- /dev/null +++ b/liquid-staking/src/basics/constants.rs @@ -0,0 +1,18 @@ +pub type GasLimit = u64; +pub type Blocks = u64; + +pub const DEFAULT_MIN_GAS_TO_SAVE_PROGRESS: GasLimit = 30_000_000; +pub const DEFAULT_GAS_TO_CLAIM_REWARDS: GasLimit = 6_000_000; +pub const MIN_GAS_FOR_ASYNC_CALL: GasLimit = 12_000_000; +pub const MIN_GAS_FOR_CALLBACK: GasLimit = 12_000_000; +pub const MIN_GAS_FINISH_EXEC: GasLimit = 20_000_000; + +pub const MIN_BLOCKS_BEFORE_CLEAR_ONGOING_OP: Blocks = 10; +pub const RECOMPUTE_BLOCK_OFFSET: Blocks = 10; + +pub const MIN_EGLD_TO_DELEGATE: u64 = 1_000_000_000_000_000_000; // 1 EGLD +pub const EGLD_TO_WHITELIST: u64 = 1_000_000_000_000_000_000; // 1 EGLD + +pub const MINIMUM_LIQUIDITY: u64 = 1_000; + +pub const MAX_DELEGATION_ADDRESSES: usize = 20; diff --git a/liquid-staking/src/errors.rs b/liquid-staking/src/basics/errors.rs similarity index 83% rename from liquid-staking/src/errors.rs rename to liquid-staking/src/basics/errors.rs index 92712a0..1781f80 100644 --- a/liquid-staking/src/errors.rs +++ b/liquid-staking/src/basics/errors.rs @@ -1,11 +1,9 @@ -pub static ERROR_ACTIVE: &[u8] = b"Active state"; pub static ERROR_NOT_ACTIVE: &[u8] = b"Not active"; pub static ERROR_LS_TOKEN_NOT_ISSUED: &[u8] = b"LS token not issued"; pub static ERROR_DELEGATION_CONTRACT_NOT_INITIALIZED: &[u8] = b"Delegation contract was not initialized yet"; pub static ERROR_INSUFFICIENT_GAS: &[u8] = b"Insufficient gas remaining for the callback"; -pub static ERROR_CLAIM_START: &[u8] = b"Claim operation must be new or pending"; pub static ERROR_CLAIM_IN_PROGRESS: &[u8] = b"Claim operation is already in progress"; pub static ERROR_OLD_CLAIM_START: &[u8] = b"Previous claimed rewards must be redelegated or lesser than 1 EGLD"; @@ -19,13 +17,9 @@ pub static ERROR_BAD_PAYMENT_TOKEN: &[u8] = b"Bad payment token"; pub static ERROR_BAD_PAYMENT_AMOUNT: &[u8] = b"Insufficient delegated amount"; pub static ERROR_BAD_WHITELIST_FEE: &[u8] = b"Whitelisting contracts costs 1 EGLD"; pub static ERROR_INSUFFICIENT_UNSTAKE_AMOUNT: &[u8] = b"Insufficient unstake amount"; -pub static ERROR_INSUFFICIENT_UNBONDED_AMOUNT: &[u8] = b"Insufficient incoming withdraw amount"; pub static ERROR_INSUFFICIENT_LIQUIDITY: &[u8] = b"Insufficient liquidity minted"; pub static ERROR_INSUFFICIENT_LIQ_BURNED: &[u8] = b"Insufficient liquidity burned"; -pub static ERROR_NOT_ENOUGH_RESERVE: &[u8] = b"Not enough reserve"; -pub static ERROR_NOT_ENOUGH_LP: &[u8] = b"Not enough LP token supply"; - pub static ERROR_BAD_DELEGATION_ADDRESS: &[u8] = b"No delegation contract available"; pub static ERROR_BAD_DELEGATION_AMOUNT: &[u8] = b"Delegation amount must be at least 1 EGLD"; pub static ERROR_NO_DELEGATION_CONTRACTS: &[u8] = b"There are no delegation contracts whitelisted"; @@ -36,4 +30,3 @@ pub static ERROR_DELEGATION_CAP: &[u8] = b"Delegation cap must be higher than the total staked amount"; pub static ERROR_ONLY_DELEGATION_ADMIN: &[u8] = b"Only the admin of the delegation contract can change the status"; -pub static ERROR_NOTHING_TO_UNBOND: &[u8] = b"Nothing to unbond"; diff --git a/liquid-staking/src/events.rs b/liquid-staking/src/basics/events.rs similarity index 90% rename from liquid-staking/src/events.rs rename to liquid-staking/src/basics/events.rs index 2774552..3160029 100644 --- a/liquid-staking/src/events.rs +++ b/liquid-staking/src/basics/events.rs @@ -1,8 +1,8 @@ -use super::contexts::base::StorageCache; - multiversx_sc::imports!(); multiversx_sc::derive_imports!(); +use crate::contexts::base::StorageCache; + #[type_abi] #[derive(TopEncode)] pub struct AddLiquidityEvent { @@ -35,7 +35,7 @@ pub struct RemoveLiquidityEvent { #[multiversx_sc::module] pub trait EventsModule: - super::config::ConfigModule + crate::config::ConfigModule + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule { fn emit_add_liquidity_event( @@ -108,4 +108,14 @@ pub trait EventsModule: #[indexed] epoch: u64, remove_liquidity_event: &RemoveLiquidityEvent, ); + + #[event("successful_claim")] + fn successful_claim_event( + &self, + amount_available_to_claim: BigUint, + #[indexed] caller: &ManagedAddress, + ); + + #[event("failed_claim")] + fn failed_claim_event(&self, #[indexed] caller: &ManagedAddress); } diff --git a/liquid-staking/src/basics/mod.rs b/liquid-staking/src/basics/mod.rs new file mode 100644 index 0000000..4b646a9 --- /dev/null +++ b/liquid-staking/src/basics/mod.rs @@ -0,0 +1,4 @@ +pub mod constants; +pub mod errors; +pub mod events; +pub mod views; diff --git a/liquid-staking/src/basics/views.rs b/liquid-staking/src/basics/views.rs new file mode 100644 index 0000000..6940145 --- /dev/null +++ b/liquid-staking/src/basics/views.rs @@ -0,0 +1,22 @@ +multiversx_sc::imports!(); + +use crate::{basics::errors::ERROR_INSUFFICIENT_LIQ_BURNED, liquidity_pool}; + +#[multiversx_sc::module] +pub trait ViewsModule: + crate::config::ConfigModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + liquidity_pool::LiquidityPoolModule +{ + // views + #[view(getLsValueForPosition)] + fn get_ls_value_for_position(&self, ls_token_amount: BigUint) -> BigUint { + let ls_token_supply = self.ls_token_supply().get(); + let virtual_egld_reserve = self.virtual_egld_reserve().get(); + + let egld_amount = ls_token_amount * &virtual_egld_reserve / ls_token_supply; + require!(egld_amount > 0u64, ERROR_INSUFFICIENT_LIQ_BURNED); + + egld_amount + } +} diff --git a/liquid-staking/src/config.rs b/liquid-staking/src/config.rs index 07a421e..3a85142 100644 --- a/liquid-staking/src/config.rs +++ b/liquid-staking/src/config.rs @@ -1,7 +1,7 @@ multiversx_sc::imports!(); multiversx_sc::derive_imports!(); -use super::liquidity_pool::State; +use crate::liquidity_pool::State; pub const MAX_PERCENTAGE: u64 = 100_000; pub const UNBOND_PERIOD: u64 = 10; diff --git a/liquid-staking/src/contexts/base.rs b/liquid-staking/src/contexts/base.rs index 3bf36ae..9ba962e 100644 --- a/liquid-staking/src/contexts/base.rs +++ b/liquid-staking/src/contexts/base.rs @@ -13,7 +13,6 @@ where pub ls_token_id: TokenIdentifier, pub ls_token_supply: BigUint, pub virtual_egld_reserve: BigUint, - pub skip_commit: bool, } impl<'a, C> StorageCache<'a, C> @@ -27,19 +26,15 @@ where ls_token_supply: sc_ref.ls_token_supply().get(), virtual_egld_reserve: sc_ref.virtual_egld_reserve().get(), sc_ref, - skip_commit: false, } } } -impl<'a, C> Drop for StorageCache<'a, C> +impl Drop for StorageCache<'_, C> where C: ConfigModule, { fn drop(&mut self) { - if self.skip_commit { - return; - } // commit changes to storage for the mutable fields self.sc_ref.ls_token_supply().set(&self.ls_token_supply); self.sc_ref diff --git a/liquid-staking/src/delegation.rs b/liquid-staking/src/delegation.rs index 3d77de7..60e0518 100644 --- a/liquid-staking/src/delegation.rs +++ b/liquid-staking/src/delegation.rs @@ -1,18 +1,23 @@ -use crate::{delegation_proxy, ERROR_BAD_WHITELIST_FEE}; +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +use crate::basics::constants::{ + GasLimit, EGLD_TO_WHITELIST, MAX_DELEGATION_ADDRESSES, MIN_BLOCKS_BEFORE_CLEAR_ONGOING_OP, + MIN_GAS_FINISH_EXEC, MIN_GAS_FOR_ASYNC_CALL, MIN_GAS_FOR_CALLBACK, +}; +use crate::{ + basics::errors::{ERROR_BAD_WHITELIST_FEE, ERROR_INSUFFICIENT_GAS}, + delegation_proxy, +}; -use super::errors::{ +use crate::basics::errors::{ ERROR_ALREADY_WHITELISTED, ERROR_BAD_DELEGATION_ADDRESS, ERROR_CLAIM_EPOCH, ERROR_CLAIM_IN_PROGRESS, ERROR_DELEGATION_CAP, ERROR_FIRST_DELEGATION_NODE, ERROR_NOT_WHITELISTED, ERROR_NO_DELEGATION_CONTRACTS, ERROR_OLD_CLAIM_START, ERROR_ONLY_DELEGATION_ADMIN, }; -multiversx_sc::imports!(); -multiversx_sc::derive_imports!(); -pub const MAX_DELEGATION_ADDRESSES: usize = 20; -pub const EGLD_TO_WHITELIST: u64 = 1_000_000_000_000_000_000; -pub const MIN_BLOCKS_BEFORE_CLEAR_ONGOING_OP: u64 = 10; -use super::liquidity_pool::State; +use crate::liquidity_pool::State; #[type_abi] #[derive(NestedEncode, NestedDecode, TopEncode, TopDecode, PartialEq, Eq, Clone)] @@ -57,7 +62,7 @@ pub struct DelegationContractData { #[multiversx_sc::module] pub trait DelegationModule: - super::config::ConfigModule + crate::config::ConfigModule + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule { #[only_owner] @@ -260,6 +265,14 @@ pub trait DelegationModule: } } + fn get_gas_for_async_call(&self) -> GasLimit { + let gas_left = self.blockchain().get_gas_left(); + require!( + gas_left > MIN_GAS_FOR_ASYNC_CALL + MIN_GAS_FOR_CALLBACK + MIN_GAS_FINISH_EXEC, + ERROR_INSUFFICIENT_GAS + ); + gas_left - MIN_GAS_FOR_CALLBACK - MIN_GAS_FINISH_EXEC + } fn move_delegation_contract_to_back(&self, delegation_contract: ManagedAddress) { self.remove_delegation_address_from_list(&delegation_contract); self.delegation_addresses_list() diff --git a/liquid-staking/src/funds/claim.rs b/liquid-staking/src/funds/claim.rs new file mode 100644 index 0000000..1bedad3 --- /dev/null +++ b/liquid-staking/src/funds/claim.rs @@ -0,0 +1,91 @@ +multiversx_sc::imports!(); + +use crate::{ + basics::{ + constants::{DEFAULT_GAS_TO_CLAIM_REWARDS, DEFAULT_MIN_GAS_TO_SAVE_PROGRESS}, + errors::{ERROR_NOT_ACTIVE, ERROR_NO_DELEGATION_CONTRACTS}, + events, + }, + config::{self}, + delegation::{self, ClaimStatusType}, + delegation_proxy, StorageCache, +}; + +#[multiversx_sc::module] +pub trait ClaimModule: + config::ConfigModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + delegation::DelegationModule + + events::EventsModule +{ + #[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(); + + self.check_claim_operation(old_claim_status, current_epoch); + let mut delegation_addresses = self.addresses_to_claim(); + if delegation_addresses.is_empty() { + self.prepare_claim_operation(); + } + + while !delegation_addresses.is_empty() { + let gas_left = self.blockchain().get_gas_left(); + if gas_left < DEFAULT_MIN_GAS_TO_SAVE_PROGRESS { + break; + } + + let current_node = delegation_addresses.pop_back().unwrap(); + let address = current_node.clone().into_value(); + + self.tx() + .to(address.clone()) + .typed(delegation_proxy::DelegationMockProxy) + .claim_rewards() + .gas(DEFAULT_GAS_TO_CLAIM_REWARDS) + .callback(ClaimModule::callbacks(self).claim_rewards_callback(address)) + .register_promise(); + + delegation_addresses.remove_node(¤t_node); + } + + if delegation_addresses.is_empty() { + claim_status_mapper.update(|claim_status| { + claim_status.status = ClaimStatusType::Finished; + claim_status.last_claim_block = self.blockchain().get_block_nonce(); + claim_status.last_claim_epoch = self.blockchain().get_block_epoch(); + }); + } + } + + #[promises_callback] + fn claim_rewards_callback( + &self, + delegation_address: ManagedAddress, + #[call_result] result: ManagedAsyncCallResult<()>, + ) { + match result { + ManagedAsyncCallResult::Ok(_) => { + let payment = self.call_value().egld_value().clone_value(); + self.rewards_reserve().update(|value| *value += &payment); + self.successful_claim_event(payment, &delegation_address); + } + ManagedAsyncCallResult::Err(_) => { + self.failed_claim_event(&delegation_address); + } + } + } +} diff --git a/liquid-staking/src/funds/delegate_rewards.rs b/liquid-staking/src/funds/delegate_rewards.rs new file mode 100644 index 0000000..f8de87f --- /dev/null +++ b/liquid-staking/src/funds/delegate_rewards.rs @@ -0,0 +1,89 @@ +multiversx_sc::imports!(); + +use crate::{ + basics, + basics::constants::{MIN_EGLD_TO_DELEGATE, MIN_GAS_FOR_CALLBACK}, + basics::errors::{ERROR_BAD_DELEGATION_AMOUNT, ERROR_CLAIM_REDELEGATE, ERROR_NOT_ACTIVE}, + config, + delegation::{self, ClaimStatusType}, + delegation_proxy, StorageCache, +}; + +#[multiversx_sc::module] +pub trait DelegateRewardsModule: + config::ConfigModule + + delegation::DelegationModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + basics::events::EventsModule +{ + #[endpoint(delegateRewards)] + fn delegate_rewards(&self) { + let 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 = self.rewards_reserve().take(); + + require!( + rewards_reserve >= MIN_EGLD_TO_DELEGATE, + ERROR_BAD_DELEGATION_AMOUNT + ); + self.call_delegate(rewards_reserve); + } + + #[promises_callback] + fn delegate_rewards_callback( + &self, + delegation_contract: ManagedAddress, + staked_tokens: BigUint, + #[call_result] result: ManagedAsyncCallResult<()>, + ) { + 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); + + let mut storage_cache = StorageCache::new(self); + 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(_) => { + self.move_delegation_contract_to_back(delegation_contract); + self.rewards_reserve() + .update(|value| *value += staked_tokens) + } + } + } + + fn call_delegate(&self, rewards_reserve: BigUint) { + let delegation_contract = self.get_delegation_contract_for_delegate(&rewards_reserve); + + let gas_for_async_call = self.get_gas_for_async_call(); + self.tx() + .to(delegation_contract.clone()) + .typed(delegation_proxy::DelegationMockProxy) + .delegate() + .egld(rewards_reserve.clone()) + .gas(gas_for_async_call) + .callback( + DelegateRewardsModule::callbacks(self) + .delegate_rewards_callback(delegation_contract, rewards_reserve), + ) + .gas_for_callback(MIN_GAS_FOR_CALLBACK) + .register_promise(); + } +} diff --git a/liquid-staking/src/funds/mod.rs b/liquid-staking/src/funds/mod.rs new file mode 100644 index 0000000..2e33a74 --- /dev/null +++ b/liquid-staking/src/funds/mod.rs @@ -0,0 +1,5 @@ +pub mod claim; +pub mod delegate_rewards; +pub mod recompute_token_reserve; +pub mod unbond; +pub mod withdraw; diff --git a/liquid-staking/src/funds/recompute_token_reserve.rs b/liquid-staking/src/funds/recompute_token_reserve.rs new file mode 100644 index 0000000..2ff3370 --- /dev/null +++ b/liquid-staking/src/funds/recompute_token_reserve.rs @@ -0,0 +1,46 @@ +multiversx_sc::imports!(); + +use crate::{ + basics::constants::{MIN_EGLD_TO_DELEGATE, RECOMPUTE_BLOCK_OFFSET}, + basics::errors::{ERROR_NOT_ACTIVE, ERROR_RECOMPUTE_RESERVES, ERROR_RECOMPUTE_TOO_SOON}, + config, + delegation::{self, ClaimStatusType}, + StorageCache, +}; + +#[multiversx_sc::module] +pub trait RecomputeTokenReserveModule: + config::ConfigModule + + delegation::DelegationModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule +{ + #[endpoint(recomputeTokenReserve)] + fn recompute_token_reserve(&self) { + let 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 + ); + + if self.rewards_reserve().get() >= 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/funds/unbond.rs b/liquid-staking/src/funds/unbond.rs new file mode 100644 index 0000000..7778c8d --- /dev/null +++ b/liquid-staking/src/funds/unbond.rs @@ -0,0 +1,76 @@ +multiversx_sc::imports!(); + +use crate::{ + basics::errors::{ + ERROR_BAD_PAYMENT_AMOUNT, ERROR_BAD_PAYMENT_TOKEN, ERROR_NOT_ACTIVE, + ERROR_UNSTAKE_PERIOD_NOT_PASSED, + }, + config::{self, UnstakeTokenAttributes}, + delegation, liquidity_pool, StorageCache, +}; + +#[multiversx_sc::module] +pub trait UnbondModule: + config::ConfigModule + + delegation::DelegationModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + liquidity_pool::LiquidityPoolModule +{ + #[payable("*")] + #[endpoint(unbondTokens)] + fn unbond_tokens(&self) { + self.blockchain().check_caller_is_user_account(); + let storage_cache = StorageCache::new(self); + let payment = self.call_value().single_esdt(); + let caller = self.blockchain().get_caller(); + + 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 total_unstake_amount = + self.handle_unstake_amount(unstake_token_attributes, payment.token_nonce); + + self.send().direct_egld(&caller, &total_unstake_amount); + } + + fn handle_unstake_amount( + &self, + unstake_token_attributes: UnstakeTokenAttributes, + nonce: u64, + ) -> BigUint { + let delegation_contract = unstake_token_attributes.delegation_contract.clone(); + let unstake_amount = unstake_token_attributes.unstake_amount.clone(); + let delegation_contract_mapper = self.delegation_contract_data(&delegation_contract); + let delegation_contract_data = delegation_contract_mapper.get(); + + require!( + delegation_contract_data.total_unbonded_from_ls_contract >= unstake_amount, + "Nothing to unbond" + ); + delegation_contract_mapper.update(|contract_data| { + contract_data.total_unstaked_from_ls_contract -= &unstake_amount; + contract_data.total_unbonded_from_ls_contract -= &unstake_amount + }); + + self.burn_unstake_tokens(nonce); + unstake_amount + } +} diff --git a/liquid-staking/src/funds/withdraw.rs b/liquid-staking/src/funds/withdraw.rs new file mode 100644 index 0000000..10173af --- /dev/null +++ b/liquid-staking/src/funds/withdraw.rs @@ -0,0 +1,61 @@ +multiversx_sc::imports!(); + +use crate::{ + basics::constants::MIN_GAS_FOR_CALLBACK, + basics::errors::ERROR_NOT_ACTIVE, + config::{self}, + delegation, delegation_proxy, StorageCache, +}; + +#[multiversx_sc::module] +pub trait WithdrawModule: + config::ConfigModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + delegation::DelegationModule +{ + #[endpoint(withdrawAll)] + fn withdraw_all(&self, delegation_contract: ManagedAddress) { + self.blockchain().check_caller_is_user_account(); + let storage_cache = StorageCache::new(self); + + require!( + self.is_state_active(storage_cache.contract_state), + ERROR_NOT_ACTIVE + ); + self.call_withdraw(delegation_contract); + } + + #[promises_callback] + fn withdraw_tokens_callback( + &self, + provider: ManagedAddress, + #[call_result] result: ManagedAsyncCallResult<()>, + ) { + match result { + ManagedAsyncCallResult::Ok(()) => { + let withdraw_amount = self.call_value().egld_value().clone_value(); + if withdraw_amount == 0u64 { + return; + } + + self.delegation_contract_data(&provider) + .update(|contract_data| { + contract_data.total_unbonded_from_ls_contract += &withdraw_amount + }); + } + ManagedAsyncCallResult::Err(_) => {} + } + } + + fn call_withdraw(&self, delegation_contract: ManagedAddress) { + let gas_for_async_call = self.get_gas_for_async_call(); + self.tx() + .to(delegation_contract.clone()) + .typed(delegation_proxy::DelegationMockProxy) + .withdraw() + .gas(gas_for_async_call) + .callback(WithdrawModule::callbacks(self).withdraw_tokens_callback(delegation_contract)) + .gas_for_callback(MIN_GAS_FOR_CALLBACK) + .register_promise(); + } +} diff --git a/liquid-staking/src/lib.rs b/liquid-staking/src/lib.rs index 38b64fe..1662f27 100644 --- a/liquid-staking/src/lib.rs +++ b/liquid-staking/src/lib.rs @@ -3,38 +3,34 @@ multiversx_sc::imports!(); multiversx_sc::derive_imports!(); -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 MINIMUM_LIQUIDITY: u64 = 1_000; -pub const DEFAULT_MIN_GAS_TO_SAVE_PROGRESS: u64 = 30_000_000; -pub const MIN_GAS_FINISH_EXEC: u64 = 20_000_000; - +pub mod basics; pub mod config; -mod contexts; +pub mod contexts; pub mod delegation; -mod delegation_proxy; -pub mod errors; -mod events; -mod liquidity_pool; +pub mod delegation_proxy; +pub mod funds; +pub mod liquidity; +pub mod liquidity_pool; -use { - delegation::{ClaimStatus, ClaimStatusType}, - errors::*, -}; +use delegation::{ClaimStatus, ClaimStatusType}; -use config::{UnstakeTokenAttributes, UNBOND_PERIOD}; use contexts::base::*; use liquidity_pool::State; #[multiversx_sc::contract] -pub trait LiquidStaking: - liquidity_pool::LiquidityPoolModule +pub trait LiquidStaking: + basics::events::EventsModule + + basics::views::ViewsModule + config::ConfigModule - + events::EventsModule + delegation::DelegationModule + + funds::claim::ClaimModule + + funds::delegate_rewards::DelegateRewardsModule + + funds::recompute_token_reserve::RecomputeTokenReserveModule + + funds::unbond::UnbondModule + + funds::withdraw::WithdrawModule + + liquidity::add_liquidity::AddLiquidityModule + + liquidity::remove_liquidity::RemoveLiquidityModule + + liquidity_pool::LiquidityPoolModule + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule { #[init] @@ -53,470 +49,4 @@ pub trait LiquidStaking: #[upgrade] fn upgrade(&self) {} - - #[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 - ); - if storage_cache.ls_token_supply == 0 { - require!( - caller == self.blockchain().get_owner_address(), - ERROR_DELEGATION_CONTRACT_NOT_INITIALIZED - ); - } - require!(payment > MIN_EGLD_TO_DELEGATE, ERROR_BAD_PAYMENT_AMOUNT); - - let delegation_contract = self.get_delegation_contract_for_delegate(&payment); - - drop(storage_cache); - - let gas_for_async_call = self.get_gas_for_async_call(); - self.tx() - .to(delegation_contract.clone()) - .typed(delegation_proxy::DelegationMockProxy) - .delegate() - .egld(payment.clone()) - .gas(gas_for_async_call) - .callback(LiquidStaking::callbacks(self).add_liquidity_callback( - caller, - delegation_contract, - payment, - )) - .gas_for_callback(MIN_GAS_FOR_CALLBACK) - .register_promise(); - } - - #[promises_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 mut ls_token_amount_before_add = BigUint::zero(); - if storage_cache.ls_token_supply == 0 { - ls_token_amount_before_add += MINIMUM_LIQUIDITY; - } - let ls_token_amount = self.pool_add_liquidity(&staked_tokens, &mut storage_cache) - - ls_token_amount_before_add; - 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 delegation_contract_mapper = self.delegation_contract_data(&delegation_contract); - delegation_contract_mapper - .update(|contract_data| contract_data.egld_in_ongoing_undelegation += &egld_to_unstake); - drop(storage_cache); - - let gas_for_async_call = self.get_gas_for_async_call(); - self.tx() - .to(delegation_contract.clone()) - .typed(delegation_proxy::DelegationMockProxy) - .undelegate(egld_to_unstake.clone()) - .gas(gas_for_async_call) - .callback(LiquidStaking::callbacks(self).remove_liquidity_callback( - caller, - delegation_contract, - egld_to_unstake, - payment.amount, - )) - .gas_for_callback(MIN_GAS_FOR_CALLBACK) - .register_promise(); - } - - #[promises_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); - let delegation_contract_mapper = self.delegation_contract_data(&delegation_contract); - - delegation_contract_mapper.update(|contract_data| { - contract_data.egld_in_ongoing_undelegation -= &egld_to_unstake; - }); - - match result { - ManagedAsyncCallResult::Ok(()) => { - let current_epoch = self.blockchain().get_block_epoch(); - let unbond_epoch = current_epoch + UNBOND_PERIOD; - - delegation_contract_mapper.update(|contract_data| { - contract_data.total_staked_from_ls_contract -= &egld_to_unstake; - contract_data.total_unstaked_from_ls_contract += &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 storage_cache = StorageCache::new(self); - let payment = self.call_value().single_esdt(); - let caller = self.blockchain().get_caller(); - - 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 mut total_unstake_amount = BigUint::zero(); - - 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.clone(); - let unstake_amount = unstake_token_attributes.unstake_amount.clone(); - let delegation_contract_mapper = self.delegation_contract_data(&delegation_contract); - let delegation_contract_data = delegation_contract_mapper.get(); - - require!( - delegation_contract_data.total_unbonded_from_ls_contract >= unstake_amount, - "Nothing to unbond" - ); - delegation_contract_mapper.update(|contract_data| { - contract_data.total_unstaked_from_ls_contract -= &unstake_amount; - contract_data.total_unbonded_from_ls_contract -= &unstake_amount - }); - - total_unstake_amount += unstake_amount; - self.burn_unstake_tokens(payment.token_nonce); - self.send().direct_egld(&caller, &total_unstake_amount); - } - - #[endpoint(withdrawAll)] - fn withdraw_all(&self, delegation_contract: ManagedAddress) { - self.blockchain().check_caller_is_user_account(); - let storage_cache = StorageCache::new(self); - - require!( - self.is_state_active(storage_cache.contract_state), - ERROR_NOT_ACTIVE - ); - - drop(storage_cache); - - let gas_for_async_call = self.get_gas_for_async_call(); - self.tx() - .to(delegation_contract.clone()) - .typed(delegation_proxy::DelegationMockProxy) - .withdraw() - .gas(gas_for_async_call) - .callback(LiquidStaking::callbacks(self).withdraw_tokens_callback(delegation_contract)) - .gas_for_callback(MIN_GAS_FOR_CALLBACK) - .register_promise(); - } - - #[promises_callback] - fn withdraw_tokens_callback( - &self, - provider: ManagedAddress, - #[call_result] result: ManagedAsyncCallResult<()>, - ) { - match result { - ManagedAsyncCallResult::Ok(()) => { - let withdraw_amount = self.call_value().egld_value().clone_value(); - let delegation_contract_mapper = self.delegation_contract_data(&provider); - if withdraw_amount > 0u64 { - delegation_contract_mapper.update(|contract_data| { - contract_data.total_unbonded_from_ls_contract += &withdraw_amount - }); - } - } - ManagedAsyncCallResult::Err(_) => {} - } - } - - #[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(); - - self.check_claim_operation(old_claim_status, current_epoch); - let mut delegation_addresses = self.addresses_to_claim(); - if delegation_addresses.is_empty() { - self.prepare_claim_operation(); - } - - while !delegation_addresses.is_empty() { - let gas_left = self.blockchain().get_gas_left(); - if gas_left < DEFAULT_MIN_GAS_TO_SAVE_PROGRESS { - break; - } - - let current_node = delegation_addresses.pop_back().unwrap(); - let address = current_node.clone().into_value(); - - self.tx() - .to(&address) - .typed(delegation_proxy::DelegationMockProxy) - .claim_rewards() - .gas(DEFAULT_GAS_TO_CLAIM_REWARDS) - .callback(LiquidStaking::callbacks(self).claim_rewards_callback()) - .register_promise(); - - delegation_addresses.remove_node(¤t_node); - } - - if delegation_addresses.is_empty() { - claim_status_mapper.update(|claim_status| { - claim_status.status = ClaimStatusType::Finished; - claim_status.last_claim_block = self.blockchain().get_block_nonce(); - claim_status.last_claim_epoch = self.blockchain().get_block_epoch(); - }); - } - } - - #[promises_callback] - fn claim_rewards_callback(&self, #[call_result] result: ManagedAsyncCallResult<()>) { - match result { - ManagedAsyncCallResult::Ok(_) => { - let payment = self.call_value().egld_value().clone_value(); - self.rewards_reserve().update(|value| *value += payment); - } - ManagedAsyncCallResult::Err(_) => {} - } - } - - #[endpoint(recomputeTokenReserve)] - fn recompute_token_reserve(&self) { - let 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 - ); - - if self.rewards_reserve().get() >= 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 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 = self.rewards_reserve().get(); - self.rewards_reserve().set(BigUint::zero()); - - require!( - rewards_reserve >= MIN_EGLD_TO_DELEGATE, - ERROR_BAD_DELEGATION_AMOUNT - ); - - let delegation_contract = self.get_delegation_contract_for_delegate(&rewards_reserve); - - drop(storage_cache); - - let gas_for_async_call = self.get_gas_for_async_call(); - self.tx() - .to(delegation_contract.clone()) - .typed(delegation_proxy::DelegationMockProxy) - .delegate() - .egld(rewards_reserve.clone()) - .gas(gas_for_async_call) - .callback( - LiquidStaking::callbacks(self) - .delegate_rewards_callback(delegation_contract, rewards_reserve), - ) - .gas_for_callback(MIN_GAS_FOR_CALLBACK) - .register_promise(); - } - - #[promises_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(_) => { - self.move_delegation_contract_to_back(delegation_contract); - self.rewards_reserve() - .update(|value| *value += staked_tokens) - } - } - } - - 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 + MIN_GAS_FINISH_EXEC, - ERROR_INSUFFICIENT_GAS - ); - gas_left - MIN_GAS_FOR_CALLBACK - MIN_GAS_FINISH_EXEC - } - - // views - #[view(getLsValueForPosition)] - fn get_ls_value_for_position(&self, ls_token_amount: BigUint) -> BigUint { - let mut storage_cache = StorageCache::new(self); - storage_cache.skip_commit = true; - self.get_egld_amount(&ls_token_amount, &storage_cache) - } } diff --git a/liquid-staking/src/liquidity/add_liquidity.rs b/liquid-staking/src/liquidity/add_liquidity.rs new file mode 100644 index 0000000..fe76e5a --- /dev/null +++ b/liquid-staking/src/liquidity/add_liquidity.rs @@ -0,0 +1,101 @@ +multiversx_sc::imports!(); + +use crate::{ + basics, + basics::constants::{MINIMUM_LIQUIDITY, MIN_EGLD_TO_DELEGATE, MIN_GAS_FOR_CALLBACK}, + basics::errors::{ + ERROR_BAD_PAYMENT_AMOUNT, ERROR_DELEGATION_CONTRACT_NOT_INITIALIZED, ERROR_NOT_ACTIVE, + }, + config, delegation, delegation_proxy, liquidity_pool, StorageCache, +}; + +#[multiversx_sc::module] +pub trait AddLiquidityModule: + config::ConfigModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + delegation::DelegationModule + + liquidity_pool::LiquidityPoolModule + + basics::events::EventsModule +{ + #[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 + ); + if storage_cache.ls_token_supply == 0 { + require!( + caller == self.blockchain().get_owner_address(), + ERROR_DELEGATION_CONTRACT_NOT_INITIALIZED + ); + } + require!(payment > MIN_EGLD_TO_DELEGATE, ERROR_BAD_PAYMENT_AMOUNT); + + self.call_delegate(caller, payment); + } + + #[promises_callback] + fn add_liquidity_callback( + &self, + caller: ManagedAddress, + delegation_contract: ManagedAddress, + staked_tokens: BigUint, + #[call_result] result: ManagedAsyncCallResult<()>, + ) { + match result { + ManagedAsyncCallResult::Ok(()) => { + self.delegation_contract_data(&delegation_contract) + .update(|contract_data| { + contract_data.total_staked_from_ls_contract += &staked_tokens; + }); + + let mut storage_cache = StorageCache::new(self); + let mut ls_token_amount_before_add = BigUint::zero(); + if storage_cache.ls_token_supply == 0 { + ls_token_amount_before_add += MINIMUM_LIQUIDITY; + } + + let ls_token_amount = self.pool_add_liquidity(&staked_tokens, &mut storage_cache) + - ls_token_amount_before_add; + 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); + } + } + } + + fn call_delegate(&self, caller: ManagedAddress, payment: BigUint) { + let delegation_contract = self.get_delegation_contract_for_delegate(&payment); + + let gas_for_async_call = self.get_gas_for_async_call(); + self.tx() + .to(delegation_contract.clone()) + .typed(delegation_proxy::DelegationMockProxy) + .delegate() + .egld(payment.clone()) + .gas(gas_for_async_call) + .callback(AddLiquidityModule::callbacks(self).add_liquidity_callback( + caller, + delegation_contract, + payment, + )) + .gas_for_callback(MIN_GAS_FOR_CALLBACK) + .register_promise(); + } +} diff --git a/liquid-staking/src/liquidity/mod.rs b/liquid-staking/src/liquidity/mod.rs new file mode 100644 index 0000000..06c8187 --- /dev/null +++ b/liquid-staking/src/liquidity/mod.rs @@ -0,0 +1,2 @@ +pub mod add_liquidity; +pub mod remove_liquidity; diff --git a/liquid-staking/src/liquidity/remove_liquidity.rs b/liquid-staking/src/liquidity/remove_liquidity.rs new file mode 100644 index 0000000..3fd0137 --- /dev/null +++ b/liquid-staking/src/liquidity/remove_liquidity.rs @@ -0,0 +1,145 @@ +multiversx_sc::imports!(); + +use crate::{ + basics, + basics::constants::{MIN_EGLD_TO_DELEGATE, MIN_GAS_FOR_CALLBACK}, + basics::errors::{ + ERROR_BAD_PAYMENT_AMOUNT, ERROR_BAD_PAYMENT_TOKEN, ERROR_INSUFFICIENT_UNSTAKE_AMOUNT, + ERROR_LS_TOKEN_NOT_ISSUED, ERROR_NOT_ACTIVE, + }, + config::{self, UnstakeTokenAttributes, UNBOND_PERIOD}, + delegation, delegation_proxy, liquidity_pool, StorageCache, +}; + +#[multiversx_sc::module] +pub trait RemoveLiquidityModule: + config::ConfigModule + + multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule + + delegation::DelegationModule + + liquidity_pool::LiquidityPoolModule + + basics::events::EventsModule +{ + #[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); + + self.call_undelegate(egld_to_unstake, caller, payment.amount); + } + + #[promises_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); + let delegation_contract_mapper = self.delegation_contract_data(&delegation_contract); + + match result { + ManagedAsyncCallResult::Ok(()) => { + let current_epoch = self.blockchain().get_block_epoch(); + let unbond_epoch = current_epoch + UNBOND_PERIOD; + + delegation_contract_mapper.update(|contract_data| { + contract_data.total_staked_from_ls_contract -= &egld_to_unstake; + contract_data.total_unstaked_from_ls_contract += &egld_to_unstake; + contract_data.egld_in_ongoing_undelegation -= &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(_) => { + delegation_contract_mapper.update(|contract_data| { + contract_data.egld_in_ongoing_undelegation -= &egld_to_unstake; + }); + + 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); + } + } + } + + fn call_undelegate( + &self, + egld_to_unstake: BigUint, + caller: ManagedAddress, + ls_tokens_to_be_burned: BigUint, + ) { + let delegation_contract = self.get_delegation_contract_for_undelegate(&egld_to_unstake); + + let delegation_contract_mapper = self.delegation_contract_data(&delegation_contract); + delegation_contract_mapper + .update(|contract_data| contract_data.egld_in_ongoing_undelegation += &egld_to_unstake); + + let gas_for_async_call = self.get_gas_for_async_call(); + self.tx() + .to(delegation_contract.clone()) + .typed(delegation_proxy::DelegationMockProxy) + .undelegate(egld_to_unstake.clone()) + .gas(gas_for_async_call) + .callback( + RemoveLiquidityModule::callbacks(self).remove_liquidity_callback( + caller, + delegation_contract, + egld_to_unstake, + ls_tokens_to_be_burned, + ), + ) + .gas_for_callback(MIN_GAS_FOR_CALLBACK) + .register_promise(); + } +} diff --git a/liquid-staking/src/liquidity_pool.rs b/liquid-staking/src/liquidity_pool.rs index f36f5da..c0a2ce5 100644 --- a/liquid-staking/src/liquidity_pool.rs +++ b/liquid-staking/src/liquidity_pool.rs @@ -1,10 +1,10 @@ multiversx_sc::imports!(); multiversx_sc::derive_imports!(); -use super::contexts::base::StorageCache; -use super::errors::*; +use crate::basics::errors::{ERROR_INSUFFICIENT_LIQUIDITY, ERROR_INSUFFICIENT_LIQ_BURNED}; +use crate::contexts::base::StorageCache; -use super::config; +use crate::config; #[type_abi] #[derive(TopEncode, TopDecode, PartialEq, Eq, Copy, Clone, Debug)] diff --git a/liquid-staking/tests/contract_interactions/mod.rs b/liquid-staking/tests/contract_interactions/mod.rs index 5bb4537..37540df 100644 --- a/liquid-staking/tests/contract_interactions/mod.rs +++ b/liquid-staking/tests/contract_interactions/mod.rs @@ -1,10 +1,15 @@ use crate::contract_setup::LiquidStakingContractSetup; +use basics::views::ViewsModule; +use config::{ConfigModule, UnstakeTokenAttributes}; +use delegation::DelegationModule; use delegation_mock::DelegationMock; -use liquid_staking::{ - config::{ConfigModule, UnstakeTokenAttributes}, - delegation::DelegationModule, - LiquidStaking, +use funds::{ + claim::ClaimModule, delegate_rewards::DelegateRewardsModule, + recompute_token_reserve::RecomputeTokenReserveModule, unbond::UnbondModule, + withdraw::WithdrawModule, }; +use liquid_staking::*; +use liquidity::{add_liquidity::AddLiquidityModule, remove_liquidity::RemoveLiquidityModule}; use multiversx_sc::types::Address; use multiversx_sc_scenario::{managed_address, num_bigint, rust_biguint, DebugApi}; diff --git a/liquid-staking/wasm/Cargo.lock b/liquid-staking/wasm/Cargo.lock index 7ee21e0..7e5083f 100644 --- a/liquid-staking/wasm/Cargo.lock +++ b/liquid-staking/wasm/Cargo.lock @@ -24,16 +24,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" name = "delegation-mock" version = "0.0.0" dependencies = [ - "itertools", "multiversx-sc", ] -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - [[package]] name = "endian-type" version = "0.1.2" @@ -52,21 +45,11 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "liquid-staking" version = "0.0.0" dependencies = [ "delegation-mock", - "itertools", "multiversx-sc", "multiversx-sc-modules", ] diff --git a/liquid-staking/wasm/src/lib.rs b/liquid-staking/wasm/src/lib.rs index 8b59c83..e094b74 100644 --- a/liquid-staking/wasm/src/lib.rs +++ b/liquid-staking/wasm/src/lib.rs @@ -21,13 +21,6 @@ multiversx_sc_wasm_adapter::endpoints! { ( init => init upgrade => upgrade - addLiquidity => add_liquidity - removeLiquidity => remove_liquidity - unbondTokens => unbond_tokens - withdrawAll => withdraw_all - claimRewards => claim_rewards - recomputeTokenReserve => recompute_token_reserve - delegateRewards => delegate_rewards getLsValueForPosition => get_ls_value_for_position registerLsToken => register_ls_token registerUnstakeToken => register_unstake_token @@ -51,11 +44,18 @@ multiversx_sc_wasm_adapter::endpoints! { getAddressesToClaim => addresses_to_claim getDelegationClaimStatus => delegation_claim_status getDelegationContractData => delegation_contract_data - add_liquidity_callback => add_liquidity_callback - remove_liquidity_callback => remove_liquidity_callback - withdraw_tokens_callback => withdraw_tokens_callback + claimRewards => claim_rewards + delegateRewards => delegate_rewards + recomputeTokenReserve => recompute_token_reserve + unbondTokens => unbond_tokens + withdrawAll => withdraw_all + addLiquidity => add_liquidity + removeLiquidity => remove_liquidity claim_rewards_callback => claim_rewards_callback delegate_rewards_callback => delegate_rewards_callback + withdraw_tokens_callback => withdraw_tokens_callback + add_liquidity_callback => add_liquidity_callback + remove_liquidity_callback => remove_liquidity_callback ) }