Skip to content

Commit

Permalink
add migration flows + protection for remaining amounts during undeleg…
Browse files Browse the repository at this point in the history
…ate/delegate
  • Loading branch information
mihaieremia committed Nov 4, 2024
1 parent d54e4ff commit f8dcbca
Show file tree
Hide file tree
Showing 6 changed files with 894 additions and 529 deletions.
1,186 changes: 682 additions & 504 deletions lcov.info

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions liquid-staking/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ pub static ERROR_MAX_SELECTED_PROVIDERS: &[u8] = b"Max selected providers must b
pub static ERROR_MAX_CHANGED_DELEGATION_ADDRESSES: &[u8] = b"Max delegation addresses must be greater than 0";

pub static ERROR_MIN_EGLD_TO_DELEGATE: &[u8] = b"Minimum EGLD to delegate must be greater than 1 EGLD";
pub static ERROR_MIGRATION_SC_NOT_SET: &[u8] = b"Migration SC not set";
pub static ERROR_MIGRATION_NOT_ALLOWED: &[u8] = b"Migration not allowed";
53 changes: 44 additions & 9 deletions liquid-staking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub mod utils_delegation;
pub mod utils_un_delegation;
pub mod views;

pub mod migrate;

mod contexts;
mod events;
mod liquidity_pool;
Expand Down Expand Up @@ -150,15 +152,48 @@ pub trait LiquidStaking<ContractReader>:
ERROR_UNSTAKE_PERIOD_NOT_PASSED
);

require!(
storage_cache.total_withdrawn_egld >= payment.amount,
ERROR_INSUFFICIENT_UNBONDED_AMOUNT
);

self.burn_unstake_tokens(payment.token_nonce, &payment.amount);

storage_cache.total_withdrawn_egld -= &payment.amount;
to_send += payment.amount;
if storage_cache.total_withdrawn_egld >= payment.amount {
self.burn_unstake_tokens(payment.token_nonce, &payment.amount);

storage_cache.total_withdrawn_egld -= &payment.amount;
to_send += payment.amount;
} else {
if storage_cache.total_withdrawn_egld > BigUint::zero() {
// In this case the required amount of the MetaESDT is higher than the available amount
// This case can happen only when the amount from the providers didn't arrive yet in the protocol
// In this case we partially give to the user the available amount and return the un claimed MetaESDT to the user
self.burn_unstake_tokens(
payment.token_nonce,
&storage_cache.total_withdrawn_egld,
);

// Burn the used amount from the MetaESDT
self.burn_unstake_tokens(
payment.token_nonce,
&storage_cache.total_withdrawn_egld,
);

let remaining_amount = payment.amount - &storage_cache.total_withdrawn_egld;

// Send the remaining amount to the user
self.tx()
.to(&caller)
.single_esdt(
&payment.token_identifier,
payment.token_nonce,
&remaining_amount,
)
.transfer();

// Send the amount to the user
to_send += storage_cache.total_withdrawn_egld.clone();

// Reset the total withdrawn amount to 0
storage_cache.total_withdrawn_egld = BigUint::zero();
} else {
sc_panic!(ERROR_INSUFFICIENT_UNBONDED_AMOUNT);
}
}
}

if to_send > BigUint::zero() {
Expand Down
11 changes: 8 additions & 3 deletions liquid-staking/src/manage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ pub trait ManageModule:
OptionalValue::None => storage_cache.pending_egld.clone(),
};

let delegation_contract = self.get_delegation_contract_for_delegate(&amount_to_delegate);
let delegation_contract =
self.get_delegation_contract_for_delegate(&amount_to_delegate, &mut storage_cache);

// Important before delegating the amount to the new contracts, set the reserve to 0 or deduct the amount delegated when not full
storage_cache.pending_egld -= amount_to_delegate;
Expand Down Expand Up @@ -129,7 +130,8 @@ pub trait ManageModule:
OptionalValue::None => storage_cache.pending_egld_for_unstake.clone(),
};

let delegation_contract = self.get_delegation_contract_for_undelegate(&amount_to_unstake);
let delegation_contract =
self.get_delegation_contract_for_undelegate(&amount_to_unstake, &mut storage_cache);

// Important before un delegating the amount from the new contracts, set the amount to 0
storage_cache.pending_egld_for_unstake -= amount_to_unstake;
Expand Down Expand Up @@ -238,8 +240,11 @@ pub trait ManageModule:
self.protocol_revenue_event(&fees, self.blockchain().get_block_epoch());
}

let amount_to_delegate = storage_cache.rewards_reserve.clone();

let delegation_contract =
self.get_delegation_contract_for_delegate(&storage_cache.rewards_reserve);
self.get_delegation_contract_for_delegate(&amount_to_delegate, &mut storage_cache);

// Important before delegating the rewards to the new contracts, set the rewards reserve to 0
storage_cache.rewards_reserve = BigUint::zero();

Expand Down
109 changes: 109 additions & 0 deletions liquid-staking/src/migrate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use crate::{StorageCache, ERROR_MIGRATION_NOT_ALLOWED, ERROR_MIGRATION_SC_NOT_SET};

multiversx_sc::imports!();
multiversx_sc::derive_imports!();

#[multiversx_sc::module]
pub trait ManageModule:
crate::config::ConfigModule
+ crate::events::EventsModule
+ crate::callback::CallbackModule
+ crate::delegation::DelegationModule
+ crate::storage::StorageModule
+ crate::utils::UtilsModule
+ crate::liquidity_pool::LiquidityPoolModule
+ multiversx_sc_modules::default_issue_callbacks::DefaultIssueCallbacksModule
{
#[endpoint(migrate)]
fn migrate(&self, virtual_egld_amount: &BigUint) {
let mut storage_cache = StorageCache::new(self);
let migration_sc_address = self.migration_sc_address().get();

// Check that the migration SC address is set
require!(!migration_sc_address.is_zero(), ERROR_MIGRATION_SC_NOT_SET);

let caller = self.blockchain().get_caller();

// Check that the caller is the migration SC
require!(caller == migration_sc_address, ERROR_MIGRATION_NOT_ALLOWED);

// Double check that the caller is a smart contract
require!(
self.blockchain().is_smart_contract(&caller),
ERROR_MIGRATION_NOT_ALLOWED
);

let ls_amount = self.pool_add_liquidity(virtual_egld_amount, &mut storage_cache);
let user_payment = self.mint_ls_token(ls_amount);

// Emit the add liquidity event
self.emit_add_liquidity_event(&storage_cache, virtual_egld_amount);
// Send the final amount to the user
self.tx().to(&caller).esdt(user_payment).transfer();
}

#[payable("EGLD")]
#[endpoint(migratePending)]
fn migrate_pending(&self) {
let mut storage_cache = StorageCache::new(self);
let migration_sc_address = self.migration_sc_address().get();

// Check that the migration SC address is set
require!(!migration_sc_address.is_zero(), ERROR_MIGRATION_SC_NOT_SET);

let caller = self.blockchain().get_caller();

// Check that the caller is the migration SC
require!(caller == migration_sc_address, ERROR_MIGRATION_NOT_ALLOWED);

// Double check that the caller is a smart contract
require!(
self.blockchain().is_smart_contract(&caller),
ERROR_MIGRATION_NOT_ALLOWED
);

let amount = self.call_value().egld_value();

storage_cache.pending_egld += amount.clone_value();
}

#[payable("EGLD")]
#[endpoint(migrateRewards)]
fn migrate_rewards(&self) {
let mut storage_cache = StorageCache::new(self);
let migration_sc_address = self.migration_sc_address().get();

// Check that the migration SC address is set
require!(!migration_sc_address.is_zero(), ERROR_MIGRATION_SC_NOT_SET);

let caller = self.blockchain().get_caller();

// Check that the caller is the migration SC
require!(caller == migration_sc_address, ERROR_MIGRATION_NOT_ALLOWED);

// Double check that the caller is a smart contract
require!(
self.blockchain().is_smart_contract(&caller),
ERROR_MIGRATION_NOT_ALLOWED
);

let amount = self.call_value().egld_value();

storage_cache.rewards_reserve += amount.clone_value();
}

#[only_owner]
#[endpoint(addMigrationScAddress)]
fn add_migration_sc_address(&self, address: &ManagedAddress) {
// Double check that the caller is a smart contract
require!(
self.blockchain().is_smart_contract(address),
ERROR_MIGRATION_NOT_ALLOWED
);
self.migration_sc_address().set(address);
}

#[view(getMigrationScAddress)]
#[storage_mapper("migrationScAddress")]
fn migration_sc_address(&self) -> SingleValueMapper<ManagedAddress>;
}
62 changes: 49 additions & 13 deletions liquid-staking/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
structs::{DelegationContractInfo, DelegationContractSelectionInfo, DelegatorSelection},
ERROR_BAD_DELEGATION_ADDRESS, ERROR_FAILED_TO_DISTRIBUTE, ERROR_NO_DELEGATION_CONTRACTS,
MIN_EGLD_TO_DELEGATE,
StorageCache, ERROR_BAD_DELEGATION_ADDRESS, ERROR_FAILED_TO_DISTRIBUTE,
ERROR_NO_DELEGATION_CONTRACTS, MIN_EGLD_TO_DELEGATE,
};

multiversx_sc::imports!();
Expand Down Expand Up @@ -31,6 +31,7 @@ pub trait UtilsModule:
fn get_delegation_contract_for_delegate(
&self,
amount_to_delegate: &BigUint,
storage_cache: &mut StorageCache<Self>,
) -> ManagedVec<DelegatorSelection<Self::Api>> {
self.get_delegation_contract(
amount_to_delegate,
Expand All @@ -55,7 +56,8 @@ pub trait UtilsModule:
min_egld,
total_stake,
total_nodes,
total_apy| {
total_apy,
storage_cache| {
self.distribute_amount(
selected_addresses,
amount_to_delegate,
Expand All @@ -64,14 +66,17 @@ pub trait UtilsModule:
total_nodes,
total_apy,
true,
storage_cache,
)
},
storage_cache,
)
}

fn get_delegation_contract_for_undelegate(
&self,
amount_to_undelegate: &BigUint,
storage_cache: &mut StorageCache<Self>,
) -> ManagedVec<DelegatorSelection<Self::Api>> {
self.get_delegation_contract(
amount_to_undelegate,
Expand All @@ -88,7 +93,8 @@ pub trait UtilsModule:
min_egld,
total_stake,
total_nodes,
total_apy| {
total_apy,
storage_cache| {
self.distribute_amount(
selected_addresses,
amount_to_undelegate,
Expand All @@ -97,8 +103,10 @@ pub trait UtilsModule:
total_nodes,
total_apy,
false,
storage_cache,
)
},
storage_cache,
)
}

Expand All @@ -107,6 +115,7 @@ pub trait UtilsModule:
amount: &BigUint,
filter_fn: F,
distribute_fn: D,
storage_cache: &mut StorageCache<Self>,
) -> ManagedVec<DelegatorSelection<Self::Api>>
where
F: Fn(&DelegationContractInfo<Self::Api>, &BigUint) -> bool,
Expand All @@ -117,6 +126,7 @@ pub trait UtilsModule:
&BigUint,
u64,
u64,
&mut StorageCache<Self>,
) -> ManagedVec<DelegatorSelection<Self::Api>>,
{
let map_list = self.delegation_addresses_list();
Expand All @@ -131,7 +141,7 @@ pub trait UtilsModule:
let mut total_nodes = 0;
let mut total_apy = 0;

for delegation_address in map_list.iter().take(max_providers) {
for delegation_address in map_list.iter() {
let contract_data = self.delegation_contract_data(&delegation_address).get();

if filter_fn(&contract_data, &amount_per_provider) {
Expand All @@ -153,6 +163,10 @@ pub trait UtilsModule:
total_staked_from_ls_contract: contract_data.total_staked_from_ls_contract,
});
}

if selected_addresses.len() == max_providers {
break;
}
}

require!(!selected_addresses.is_empty(), ERROR_BAD_DELEGATION_ADDRESS);
Expand All @@ -164,6 +178,7 @@ pub trait UtilsModule:
&total_stake,
total_nodes,
total_apy,
storage_cache,
)
}

Expand All @@ -176,6 +191,7 @@ pub trait UtilsModule:
total_nodes: u64,
total_apy: u64,
is_delegate: bool,
storage_cache: &mut StorageCache<Self>,
) -> ManagedVec<DelegatorSelection<Self::Api>> {
let mut result = ManagedVec::new();
let mut remaining_amount = amount.clone();
Expand Down Expand Up @@ -258,7 +274,13 @@ pub trait UtilsModule:

// In case of rounding dust due to math
// Most of the time this will add the remaining amount to the first provider
self._distribute_remaining_amount(&mut result, &mut remaining_amount, is_delegate);
self._distribute_remaining_amount(
&mut result,
&mut remaining_amount,
is_delegate,
min_egld,
storage_cache,
);

require!(!result.is_empty(), ERROR_BAD_DELEGATION_ADDRESS);

Expand All @@ -270,6 +292,8 @@ pub trait UtilsModule:
result: &mut ManagedVec<DelegatorSelection<Self::Api>>,
remaining_amount: &mut BigUint,
is_delegate: bool,
min_egld: &BigUint,
storage_cache: &mut StorageCache<Self>,
) {
// In case of rounding dust due to math
// Most of the time this will add the remaining amount to the first provider
Expand All @@ -286,9 +310,7 @@ pub trait UtilsModule:
if !is_delegate {
let left_over_amount = &available_space - &amount_to_add;
// If the left over amount is less than the required minimum or not zero, skip provider
if left_over_amount < BigUint::from(MIN_EGLD_TO_DELEGATE)
&& left_over_amount > BigUint::zero()
{
if left_over_amount < *min_egld && left_over_amount > BigUint::zero() {
continue;
}
}
Expand All @@ -310,10 +332,24 @@ pub trait UtilsModule:
}
}
}
require!(
*remaining_amount == BigUint::zero(),
ERROR_FAILED_TO_DISTRIBUTE
);

if *remaining_amount >= *min_egld {
// We can arrive here when for example we undelegate 20k EGLD and the entire 20k is not fitting in the first batch of providers
// In this case we need to add the remaining amount to the pending EGLD back and the next transaction will pick it up over a new batch of providers
// Both for delegate and undelegate
if is_delegate {
storage_cache.pending_egld += remaining_amount.clone();
return;
} else {
storage_cache.pending_egld_for_unstake += remaining_amount.clone();
return;
}
} else {
require!(
*remaining_amount == BigUint::zero(),
ERROR_FAILED_TO_DISTRIBUTE
);
}
}
}

Expand Down

0 comments on commit f8dcbca

Please sign in to comment.