From f8629041ad1062233178572e24fd34054ed0af34 Mon Sep 17 00:00:00 2001 From: h4x3rotab Date: Fri, 27 Oct 2023 02:29:12 +0000 Subject: [PATCH] compute: settle vault reward when necessary --- pallets/phala/src/compute/vault.rs | 92 +++++++++++++++++++----------- pallets/phala/src/test.rs | 79 +++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 34 deletions(-) diff --git a/pallets/phala/src/compute/vault.rs b/pallets/phala/src/compute/vault.rs index 8e077636ea..df69215b37 100644 --- a/pallets/phala/src/compute/vault.rs +++ b/pallets/phala/src/compute/vault.rs @@ -309,41 +309,12 @@ pub mod pallet { #[pallet::weight({0})] pub fn maybe_gain_owner_shares(origin: OriginFor, vault_pid: u64) -> DispatchResult { let who = ensure_signed(origin)?; - let mut pool_info = ensure_vault::(vault_pid)?; - // Add pool owner's reward if applicable + let pool_info = ensure_vault::(vault_pid)?; ensure!( who == pool_info.basepool.owner, Error::::UnauthorizedPoolOwner ); - let current_price = match pool_info.basepool.share_price() { - Some(price) => BalanceOf::::from_fixed(&price), - None => return Ok(()), - }; - if pool_info.last_share_price_checkpoint == Zero::zero() { - pool_info.last_share_price_checkpoint = current_price; - base_pool::pallet::Pools::::insert(vault_pid, PoolProxy::Vault(pool_info)); - return Ok(()); - } - if current_price <= pool_info.last_share_price_checkpoint { - return Ok(()); - } - let delta_price = pool_info.commission.unwrap_or_default() - * (current_price - pool_info.last_share_price_checkpoint); - let new_price = current_price - delta_price; - let adjust_shares = bdiv(pool_info.basepool.total_value, &new_price.to_fixed()) - - pool_info.basepool.total_shares; - pool_info.basepool.total_shares += adjust_shares; - pool_info.owner_shares += adjust_shares; - pool_info.last_share_price_checkpoint = new_price; - - base_pool::pallet::Pools::::insert(vault_pid, PoolProxy::Vault(pool_info)); - Self::deposit_event(Event::::OwnerSharesGained { - pid: vault_pid, - shares: adjust_shares, - checkout_price: new_price, - }); - - Ok(()) + Self::do_gain_owner_share(vault_pid) } /// Let any user to launch a vault withdraw. Then check if the vault need to be forced withdraw all its contributions. @@ -488,9 +459,7 @@ pub mod pallet { #[frame_support::transactional] pub fn contribute(origin: OriginFor, pid: u64, amount: BalanceOf) -> DispatchResult { let who = ensure_signed(origin)?; - let mut pool_info = ensure_vault::(pid)?; let a = amount; // Alias to reduce confusion in the code below - ensure!( a >= T::MinContribution::get(), Error::::InsufficientContribution @@ -502,11 +471,15 @@ pub mod pallet { .ok_or(Error::::AssetAccountNotExist)?; ensure!(free >= a, Error::::InsufficientBalance); + // Trigger owner reward share distribution before contribution to ensure no harm to the + // contributor. + Self::do_gain_owner_share(pid)?; + let mut pool_info = ensure_vault::(pid)?; + let shares = base_pool::Pallet::::contribute(&mut pool_info.basepool, who.clone(), amount)?; // We have new free stake now, try to handle the waiting withdraw queue - base_pool::Pallet::::try_process_withdraw_queue(&mut pool_info.basepool); // Persist @@ -542,7 +515,12 @@ pub mod pallet { #[frame_support::transactional] pub fn withdraw(origin: OriginFor, pid: u64, shares: BalanceOf) -> DispatchResult { let who = ensure_signed(origin)?; + + // Trigger owner reward share distribution before withdrawal to ensure no harm to the + // pool owner. + Self::do_gain_owner_share(pid)?; let mut pool_info = ensure_vault::(pid)?; + let maybe_nft_id = base_pool::Pallet::::merge_nft_for_staker( pool_info.basepool.cid, who.clone(), @@ -605,4 +583,50 @@ pub mod pallet { Self::check_and_maybe_force_withdraw(origin, pid) } } + + impl Pallet + where + BalanceOf: sp_runtime::traits::AtLeast32BitUnsigned + Copy + FixedPointConvert + Display, + T: pallet_rmrk_core::Config, + T: pallet_assets::Config>, + { + /// Triggers owner reward share distribution + /// + /// Note 1: This function does mutate the pool info. After calling this function, the caller + /// must read the pool info again if it's accessed. + /// + /// Note 2: This function guarantees no-op when it returns error. + fn do_gain_owner_share(vault_pid: u64) -> DispatchResult { + let mut pool_info = ensure_vault::(vault_pid)?; + let current_price = match pool_info.basepool.share_price() { + Some(price) => BalanceOf::::from_fixed(&price), + None => return Ok(()), + }; + if pool_info.last_share_price_checkpoint == Zero::zero() { + pool_info.last_share_price_checkpoint = current_price; + base_pool::pallet::Pools::::insert(vault_pid, PoolProxy::Vault(pool_info)); + return Ok(()); + } + if current_price <= pool_info.last_share_price_checkpoint { + return Ok(()); + } + let delta_price = pool_info.commission.unwrap_or_default() + * (current_price - pool_info.last_share_price_checkpoint); + let new_price = current_price - delta_price; + let adjust_shares = bdiv(pool_info.basepool.total_value, &new_price.to_fixed()) + - pool_info.basepool.total_shares; + pool_info.basepool.total_shares += adjust_shares; + pool_info.owner_shares += adjust_shares; + pool_info.last_share_price_checkpoint = new_price; + + base_pool::pallet::Pools::::insert(vault_pid, PoolProxy::Vault(pool_info)); + Self::deposit_event(Event::::OwnerSharesGained { + pid: vault_pid, + shares: adjust_shares, + checkout_price: new_price, + }); + + Ok(()) + } + } } diff --git a/pallets/phala/src/test.rs b/pallets/phala/src/test.rs index 57d71f89d4..f25e8e0f29 100644 --- a/pallets/phala/src/test.rs +++ b/pallets/phala/src/test.rs @@ -2212,6 +2212,85 @@ fn vault_force_withdraw_after_3x_grace_period() { }); } +#[test] +fn vault_owner_reward_settle_when_contribute_withdraw() { + use crate::compute::computation::pallet::OnReward; + new_test_ext().execute_with(|| { + mock_asset_id(); + assert_ok!(PhalaWrappedBalances::wrap( + RuntimeOrigin::signed(1), + 500 * DOLLARS + )); + set_block_1(); + setup_workers(1); + setup_stake_pool_with_workers(1, &[1]); // pid = 0 + let vault1 = setup_vault(99); + assert_ok!(PhalaVault::set_payout_pref( + RuntimeOrigin::signed(99), + vault1, + Some(Permill::from_percent(100)), + )); + assert_ok!(PhalaVault::contribute( + RuntimeOrigin::signed(1), + 1, + 100 * DOLLARS, + )); + assert_ok!(PhalaStakePoolv2::contribute( + RuntimeOrigin::signed(99), + 0, + 100 * DOLLARS, + Some(vault1), + )); + // Checkpoint price = 1 + assert_ok!(PhalaVault::maybe_gain_owner_shares( + RuntimeOrigin::signed(99), + vault1, + )); + let pool0 = ensure_stake_pool::(0).unwrap(); + assert_eq!(pool0.basepool.share_price(), Some(fp!(1))); + let pool1 = ensure_vault::(1).unwrap(); + assert_eq!(pool1.basepool.share_price(), Some(fp!(1))); + assert_eq!(pool1.last_share_price_checkpoint, 1 * DOLLARS); + + // Current price = 2 + PhalaStakePoolv2::on_reward(&[SettleInfo { + pubkey: worker_pubkey(1), + v: FixedPoint::from_num(1u32).to_bits(), + payout: FixedPoint::from_num(100u32).to_bits(), + treasury: 0, + }]); + let pool1 = ensure_vault::(1).unwrap(); + assert_eq!(pool1.basepool.share_price(), Some(fp!(2))); + // Contribution should bring price back to 1 + assert_ok!(PhalaVault::contribute( + RuntimeOrigin::signed(1), + 1, + 100 * DOLLARS, + )); + let pool1 = ensure_vault::(1).unwrap(); + assert_eq!(pool1.basepool.share_price(), Some(fp!(1))); + + // Double the stake pool asset by adding 300 reward + // Current price = 2 again + PhalaStakePoolv2::on_reward(&[SettleInfo { + pubkey: worker_pubkey(1), + v: FixedPoint::from_num(1u32).to_bits(), + payout: FixedPoint::from_num(300u32).to_bits(), + treasury: 0, + }]); + let pool1 = ensure_vault::(1).unwrap(); + assert_eq!(pool1.basepool.share_price(), Some(fp!(2))); + // Withdrawal should bring the price back to 1 + assert_ok!(PhalaVault::withdraw( + RuntimeOrigin::signed(1), + 1, + 100 * DOLLARS, + )); + let pool1 = ensure_vault::(1).unwrap(); + assert_eq!(pool1.basepool.share_price(), Some(fp!(1))); + }); +} + fn mock_asset_id() { as Create>::create( ::WPhaAssetId::get(),