Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate Vault & StakePool improvements #308

Merged
merged 1 commit into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 116 additions & 87 deletions pallets/phala/src/compute/base_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ pub mod pallet {
pub nft_id: NftId,
}

/// Current withdrawing stats for a user
#[derive(Default)]
struct WithdrawEventStats<Balance: Default> {
amount: Balance,
shares: Balance,
burnt_shares: Balance,
}

#[pallet::config]
pub trait Config:
frame_system::Config
Expand Down Expand Up @@ -148,8 +156,11 @@ pub mod pallet {
pid: u64,
user: T::AccountId,
shares: BalanceOf<T>,
/// Target NFT to withdraw
nft_id: NftId,
as_vault: Option<u64>,
/// Splitted NFT for withdrawing
withdrawing_nft_id: NftId,
},
/// Some stake was withdrawn from a pool
///
Expand All @@ -164,6 +175,7 @@ pub mod pallet {
user: T::AccountId,
amount: BalanceOf<T>,
shares: BalanceOf<T>,
burnt_shares: BalanceOf<T>,
},
/// A pool contribution whitelist is added
///
Expand Down Expand Up @@ -653,13 +665,14 @@ pub mod pallet {
nft: &mut NftAttr<BalanceOf<T>>,
account_id: T::AccountId,
shares: BalanceOf<T>,
) -> DispatchResult {
) -> Result<Option<NftId>, DispatchError> {
if pool.share_price().is_none() {
// Pool bankrupt. Reduce the NFT share without doing real task.
nft.shares = nft
.shares
.checked_sub(&shares)
.ok_or(Error::<T>::InvalidShareToWithdraw)?;
return Ok(());
return Ok(None);
}

// Remove the existing withdraw request in the queue if there is any.
Expand All @@ -679,7 +692,7 @@ pub mod pallet {
Self::burn_nft(&pallet_id(), pool.cid, withdrawinfo.nft_id)
.expect("burn nft attr should always success; qed.");
}

// Move the shares in the NFT to the withdrawing NFT.
let split_nft_id = Self::mint_nft(pool.cid, pallet_id(), shares, pool.pid)
.expect("mint nft should always success");
nft.shares = nft
Expand All @@ -696,7 +709,7 @@ pub mod pallet {
nft_id: split_nft_id,
});

Ok(())
Ok(Some(split_nft_id))
}

/// Returns the new pid that will assigned to the creating pool
Expand Down Expand Up @@ -1006,19 +1019,26 @@ pub mod pallet {
nft_id: NftId,
as_vault: Option<u64>,
) -> DispatchResult {
Self::push_withdraw_in_queue(pool_info, nft, userid.clone(), shares)?;
Self::deposit_event(Event::<T>::WithdrawalQueued {
pid: pool_info.pid,
user: userid,
shares,
nft_id,
as_vault,
});
Self::try_process_withdraw_queue(pool_info);

let maybe_split_nft_id =
Self::push_withdraw_in_queue(pool_info, nft, userid.clone(), shares)?;
if let Some(split_nft_id) = maybe_split_nft_id {
// The pool is operating normally. Emit event and try to process withdraw queue.
Self::deposit_event(Event::<T>::WithdrawalQueued {
pid: pool_info.pid,
user: userid,
shares,
nft_id,
as_vault,
withdrawing_nft_id: split_nft_id,
});
Self::try_process_withdraw_queue(pool_info);
}
Ok(())
}

/// Removes the share from the pool total_shares if it's a dust
///
/// Return true if the dust is removed
pub fn maybe_remove_dust(
pool_info: &mut BasePool<T::AccountId, BalanceOf<T>>,
nft: &NftAttr<BalanceOf<T>>,
Expand All @@ -1036,93 +1056,102 @@ pub mod pallet {
true
}

/// Removes withdrawing_shares from the nft
pub fn do_withdraw_shares(
withdrawing_shares: BalanceOf<T>,
pool_info: &mut BasePool<T::AccountId, BalanceOf<T>>,
nft: &mut NftAttr<BalanceOf<T>>,
userid: T::AccountId,
) {
// Overflow warning: remove_stake is carefully written to avoid precision error.
// (I hope so)
let (reduced, withdrawn_shares) =
Self::remove_stake_from_nft(pool_info, withdrawing_shares, nft, &userid)
.expect("There are enough withdrawing_shares; qed.");
Self::deposit_event(Event::<T>::Withdrawal {
pid: pool_info.pid,
user: userid,
amount: reduced,
shares: withdrawn_shares,
});
}

/// Tries to fulfill the withdraw queue with the newly freed stake
pub fn try_process_withdraw_queue(pool_info: &mut BasePool<T::AccountId, BalanceOf<T>>) {
use sp_std::collections::btree_map::BTreeMap;
// The share price shouldn't change at any point in this function. So we can calculate
// only once at the beginning.
let price = match pool_info.share_price() {
Some(price) => price,
None => return,
};

let wpha_min = wrapped_balances::Pallet::<T>::min_balance();
// Note: This function aggregates all the withdrawal stats and emit the events at the
// end. It's supposed the withdrawal process won't be interrupted by any error.
let mut withdrawing = BTreeMap::<T::AccountId, WithdrawEventStats<BalanceOf<T>>>::new();
while pool_info.get_free_stakes::<T>() > wpha_min {
if let Some(withdraw) = pool_info.withdraw_queue.front().cloned() {
// Must clear the pending reward before any stake change
let mut withdraw_nft_guard =
Self::get_nft_attr_guard(pool_info.cid, withdraw.nft_id)
.expect("get nftattr should always success; qed.");
let mut withdraw_nft = withdraw_nft_guard.attr.clone();
if Self::maybe_remove_dust(pool_info, &withdraw_nft) {
pool_info.withdraw_queue.pop_front();
Self::burn_nft(&pallet_id(), pool_info.cid, withdraw.nft_id)
.expect("burn nft should always success");
continue;
}
// Try to fulfill the withdraw requests as much as possible
let free_shares = if price == fp!(0) {
withdraw_nft.shares // 100% slashed
} else {
bdiv(pool_info.get_free_stakes::<T>(), &price)
};
// This is the shares to withdraw immedately. It should NOT contain any dust
// because we ensure (1) `free_shares` is not dust earlier, and (2) the shares
// in any withdraw request mustn't be dust when inserting and updating it.
let withdrawing_shares = free_shares.min(withdraw_nft.shares);
debug_assert!(
is_nondust_balance(withdrawing_shares),
"withdrawing_shares must be positive"
);
// Actually remove the fulfilled withdraw request. Dust in the user shares is
// considered but it in the request is ignored.
Self::do_withdraw_shares(
withdrawing_shares,
pool_info,
&mut withdraw_nft,
withdraw.user.clone(),
);
withdraw_nft_guard.attr = withdraw_nft.clone();
withdraw_nft_guard
.save()
.expect("save nft should always success");
// Update if the withdraw is partially fulfilled, otherwise pop it out of the
// queue
if withdraw_nft.shares == Zero::zero()
|| Self::maybe_remove_dust(pool_info, &withdraw_nft)
{
pool_info.withdraw_queue.pop_front();
Self::burn_nft(&pallet_id(), pool_info.cid, withdraw.nft_id)
.expect("burn nft should always success");
} else {
*pool_info
.withdraw_queue
.front_mut()
.expect("front exists as just checked; qed.") = withdraw;
let Some(withdraw) = pool_info.withdraw_queue.front().cloned() else {
// Stop if the queue is already empty
break;
};
let mut nft = Self::get_nft_attr_guard(pool_info.cid, withdraw.nft_id)
.expect("get nftattr should always success; qed.");
// Fast track to remove existing dust in the queue
if Self::maybe_remove_dust(pool_info, &nft.attr) {
// Account the burning NFT to emit events
withdrawing
.entry(withdraw.user.clone())
.or_default()
.burnt_shares += nft.attr.shares;
nft.unlock();
// Actually burn the NFT
pool_info.withdraw_queue.pop_front();
Self::burn_nft(&pallet_id(), pool_info.cid, withdraw.nft_id)
.expect("burn nft should always success");
continue;
}
// Try to fulfill the withdraw requests as much as possible
let free_shares = if price == fp!(0) {
nft.attr.shares // 100% slashed
} else {
bdiv(pool_info.get_free_stakes::<T>(), &price)
};
// This is the shares to withdraw immedately. It should NOT contain any dust
// because we ensure (1) `free_shares` is not dust earlier, and (2) the shares
// in any withdraw request mustn't be dust when inserting and updating it.
let withdrawing_shares = free_shares.min(nft.attr.shares);
debug_assert!(
is_nondust_balance(withdrawing_shares),
"withdrawing_shares must be positive"
);
// Actually remove the fulfilled withdraw request. Dust in the user shares is
// considered but it in the request is ignored.
let (reduced, withdrawn_shares) = Self::remove_stake_from_nft(
pool_info,
withdrawing_shares,
&mut nft.attr,
&withdraw.user,
)
.expect("There are enough withdrawing_shares; qed.");
// Account the withdrawn NFT to emit events
let event = withdrawing.entry(withdraw.user.clone()).or_default();
event.amount += reduced;
event.shares += withdrawn_shares;
// Update the withdraw NFT shares
let processed_nft = nft.attr.clone();
nft.save().expect("save nft should always success");
// Update if the withdraw is partially fulfilled, otherwise pop it out of the
// queue
if processed_nft.shares == Zero::zero()
|| Self::maybe_remove_dust(pool_info, &processed_nft)
{
if processed_nft.shares != Zero::zero() {
// Account the burning NFT to emit events
withdrawing
.entry(withdraw.user.clone())
.or_default()
.burnt_shares += processed_nft.shares;
}
pool_info.withdraw_queue.pop_front();
Self::burn_nft(&pallet_id(), pool_info.cid, withdraw.nft_id)
.expect("burn nft should always success");
} else {
break;
*pool_info
.withdraw_queue
.front_mut()
.expect("front exists as just checked; qed.") = withdraw;
}
}
// Emit the aggregated withdrawal events
for (user, stats) in withdrawing.into_iter() {
Self::deposit_event(Event::<T>::Withdrawal {
pid: pool_info.pid,
user,
amount: stats.amount,
shares: stats.shares,
burnt_shares: stats.burnt_shares,
})
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion pallets/phala/src/compute/computation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub mod pallet {
use fixed_sqrt::FixedSqrt;

#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};

const DEFAULT_EXPECTED_HEARTBEAT_COUNT: u32 = 20;
const COMPUTING_PALLETID: PalletId = PalletId(*b"phala/pp");
Expand Down
Loading
Loading