diff --git a/substrate/frame/nomination-pools/src/lib.rs b/substrate/frame/nomination-pools/src/lib.rs index f191126fbdd41..8715f59d8e070 100644 --- a/substrate/frame/nomination-pools/src/lib.rs +++ b/substrate/frame/nomination-pools/src/lib.rs @@ -544,23 +544,26 @@ impl PoolMember { fn total_balance(&self) -> BalanceOf { let pool = BondedPool::::get(self.pool_id).unwrap(); let active_balance = pool.points_to_balance(self.active_points()); + let unbonding_balance = self.unbonding_balance(); + active_balance + unbonding_balance + } + /// Unbonding balance of the member. Doesn't mutate state. + #[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))] + fn unbonding_balance(&self) -> BalanceOf { let sub_pools = match SubPoolsStorage::::get(self.pool_id) { Some(sub_pools) => sub_pools, - None => return active_balance, + None => return BalanceOf::::zero(), }; - let unbonding_balance = self.unbonding_eras.iter().fold( - BalanceOf::::zero(), - |accumulator, (era, unlocked_points)| { + self.unbonding_eras + .iter() + .fold(BalanceOf::::zero(), |acc, (era, unlocking_points)| { // if the `SubPools::with_era` has already been merged into the // `SubPools::no_era` use this pool instead. let era_pool = sub_pools.with_era.get(era).unwrap_or(&sub_pools.no_era); - accumulator + (era_pool.point_to_balance(*unlocked_points)) - }, - ); - - active_balance + unbonding_balance + acc + (era_pool.point_to_balance(*unlocking_points)) + }) } /// Total points of this member, both active and unbonding. @@ -1277,6 +1280,16 @@ impl BondedPool { Ok(points_issued) } + fn try_unbond_funds(&self, amount: BalanceOf) -> Result<(), DispatchError> { + let bonded_account = self.bonded_account(); + T::Staking::unbond(&bonded_account, amount)?; + // unbonding `amount` successes + TotalValueUnbonding::::mutate(|tvu| { + tvu.saturating_accrue(amount); + }); + Ok(()) + } + // Set the state of `self`, and deposit an event if the state changed. State should never be set // directly in in order to ensure a state change event is always correctly deposited. fn set_state(&mut self, state: PoolState) { @@ -1307,6 +1320,9 @@ impl BondedPool { TotalValueLocked::::mutate(|tvl| { tvl.saturating_reduce(diff); }); + TotalValueUnbonding::::mutate(|tvu| { + tvu.saturating_reduce(diff); + }); outcome } } @@ -1593,7 +1609,7 @@ pub mod pallet { use sp_runtime::Perbill; /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(8); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(9); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] @@ -1679,6 +1695,10 @@ pub mod pallet { #[pallet::storage] pub type TotalValueLocked = StorageValue<_, BalanceOf, ValueQuery>; + /// The sum of funds in the process of unbonding across all pools. + #[pallet::storage] + pub type TotalValueUnbonding = StorageValue<_, BalanceOf, ValueQuery>; + /// Minimum amount to bond to join a pool. #[pallet::storage] pub type MinJoinBond = StorageValue<_, BalanceOf, ValueQuery>; @@ -2150,7 +2170,7 @@ pub mod pallet { // Unbond in the actual underlying nominator. let unbonding_balance = bonded_pool.dissolve(unbonding_points); - T::Staking::unbond(&bonded_pool.bonded_account(), unbonding_balance)?; + bonded_pool.try_unbond_funds(unbonding_balance)?; // Note that we lazily create the unbonding pools here if they don't already exist let mut sub_pools = SubPoolsStorage::::get(member.pool_id) @@ -3351,7 +3371,8 @@ impl Pallet { let mut pools_members = BTreeMap::::new(); let mut pools_members_pending_rewards = BTreeMap::>::new(); let mut all_members = 0u32; - let mut total_balance_members = Default::default(); + let mut members_balance_total = Default::default(); + let mut members_balance_unbonding = Default::default(); PoolMembers::::iter().try_for_each(|(_, d)| -> Result<(), TryRuntimeError> { let bonded_pool = BondedPools::::get(d.pool_id).unwrap(); ensure!(!d.total_points().is_zero(), "No member should have zero points"); @@ -3367,7 +3388,8 @@ impl Pallet { let pending_rewards = d.pending_rewards(current_rc).unwrap(); *pools_members_pending_rewards.entry(d.pool_id).or_default() += pending_rewards; } // else this pool has been heavily slashed and cannot have any rewards anymore. - total_balance_members += d.total_balance(); + members_balance_total += d.total_balance(); + members_balance_unbonding += d.unbonding_balance(); Ok(()) })?; @@ -3392,6 +3414,8 @@ impl Pallet { })?; let mut expected_tvl: BalanceOf = Default::default(); + let mut expected_tvu: BalanceOf = Default::default(); + BondedPools::::iter().try_for_each(|(id, inner)| -> Result<(), TryRuntimeError> { let bonded_pool = BondedPool { id, inner }; ensure!( @@ -3413,8 +3437,12 @@ impl Pallet { pool is being destroyed and the depositor is the last member", ); - expected_tvl += - T::Staking::total_stake(&bonded_pool.bonded_account()).unwrap_or_default(); + let bonded_account = bonded_pool.bonded_account(); + let stake_total = T::Staking::total_stake(&bonded_account).unwrap_or_default(); + let stake_active = T::Staking::active_stake(&bonded_account).unwrap_or_default(); + + expected_tvl += stake_total; + expected_tvu += stake_total - stake_active; Ok(()) })?; @@ -3430,10 +3458,20 @@ impl Pallet { ); ensure!( - TotalValueLocked::::get() <= total_balance_members, + TotalValueLocked::::get() <= members_balance_total, "TVL must be equal to or less than the total balance of all PoolMembers." ); + ensure!( + TotalValueUnbonding::::get() == expected_tvu, + "TVU must equal the sum of unbonding funds of all pools." + ); + + ensure!( + TotalValueUnbonding::::get() <= members_balance_unbonding, + "TVU cannot surpass the sum of unbonding funds of members across all pools." + ); + if level <= 1 { return Ok(()) } @@ -3581,6 +3619,9 @@ impl sp_staking::OnStakingUpdate> for Pall pool_id, balance: *slashed_balance, }); + TotalValueUnbonding::::mutate(|tvu| { + tvu.defensive_saturating_reduce(*slashed_balance); + }) } }); SubPoolsStorage::::insert(pool_id, sub_pools); diff --git a/substrate/frame/nomination-pools/src/migration.rs b/substrate/frame/nomination-pools/src/migration.rs index 3adfd926d95cf..28351b5227317 100644 --- a/substrate/frame/nomination-pools/src/migration.rs +++ b/substrate/frame/nomination-pools/src/migration.rs @@ -27,6 +27,15 @@ use sp_runtime::TryRuntimeError; pub mod versioned { use super::*; + /// V9: Adds `TotalValueUnbonding`. + pub type V8ToV9 = frame_support::migrations::VersionedMigration< + 8, + 9, + v9::MigrateToV8, + crate::pallet::Pallet, + ::DbWeight, + >; + /// v8: Adds commission claim permissions to `BondedPools`. pub type V7ToV8 = frame_support::migrations::VersionedMigration< 7, @@ -56,6 +65,76 @@ pub mod versioned { >; } +mod v9 { + use super::*; + + pub struct MigrateToV8(sp_std::marker::PhantomData); + + impl MigrateToV8 { + fn calculate_tvu() -> BalanceOf { + BondedPools::::iter() + .map(|(id, inner)| { + let bonded_account = BondedPool { id, inner: inner.clone() }.bonded_account(); + let total_stake = T::Staking::total_stake(&bonded_account).unwrap_or_default(); + let active_stake = + T::Staking::active_stake(&bonded_account).unwrap_or_default(); + total_stake - active_stake + }) + .reduce(|acc, balance| acc + balance) + .unwrap_or_default() + } + } + + impl OnRuntimeUpgrade for MigrateToV8 { + fn on_runtime_upgrade() -> Weight { + let pool_count = BondedPools::::count(); + + // the sum of all funds that are in the unbonding process of each pool + let tvu = Self::calculate_tvu(); + TotalValueUnbonding::::set(tvu); + + log!(info, "Migrating {} pools with TotalValueUnbonding of {:?}", pool_count, tvu); + + // reads: pool_count * (BondedPool + total_stake + active_stake) + // + pool_count + storage_version + // writes: new storage_version + TotalValueUnbonding + T::DbWeight::get() + .reads_writes(pool_count.saturating_mul(3).saturating_add(2).into(), 2) + } + + // FIXME eagr this necessary? + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, TryRuntimeError> { + Ok(Vec::new()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_data: Vec) -> Result<(), TryRuntimeError> { + ensure!( + Pallet::::on_chain_storage_version() >= 9, + "nomination-pools::migration::v8: storage version must be no less than 8", + ); + + ensure!( + TotalValueUnbonding::::get() == Self::calculate_tvu(), + "TotalValueUnbonding must equal the sum of unbonding balance of all pools.", + ); + + let members_balance_unbonding = PoolMembers::::iter() + .map(|(_, member)| member.unbonding_balance()) + .reduce(|acc, balance| acc + balance) + .unwrap_or_default(); + + ensure!( + TotalValueUnbonding::::get() <= members_balance_unbonding, + "TotalValueUnbonding cannot surpass the sum of unbonding funds of members across all pools.", + ); + + Ok(()) + } + } +} + pub mod v8 { use super::*; diff --git a/substrate/frame/nomination-pools/src/tests.rs b/substrate/frame/nomination-pools/src/tests.rs index 7fe1e704bb13c..a63352943ce22 100644 --- a/substrate/frame/nomination-pools/src/tests.rs +++ b/substrate/frame/nomination-pools/src/tests.rs @@ -1090,6 +1090,7 @@ mod claim_payout { Event::Unbonded { member: 11, pool_id: 1, points: 11, balance: 11, era: 3 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 11); }); } @@ -2195,6 +2196,7 @@ mod claim_payout { Event::PaidOut { member: 10, pool_id: 1, payout: 7 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 10 + 10 + 5 + 5); }) } @@ -2714,6 +2716,7 @@ mod unbond { Event::Unbonded { member: 40, pool_id: 1, balance: 6, points: 6, era: 3 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 6); assert_eq!(StakingMock::active_stake(&default_bonded_account()).unwrap(), 94); assert_eq!( @@ -2763,6 +2766,7 @@ mod unbond { } ] ); + assert_eq!(TotalValueUnbonding::::get(), 6 + 92); // When CurrentEra::set(3); @@ -2802,6 +2806,7 @@ mod unbond { Event::Unbonded { member: 10, pool_id: 1, points: 2, balance: 2, era: 6 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 2); }); } @@ -2848,6 +2853,7 @@ mod unbond { Event::Unbonded { member: 10, pool_id: 1, points: 10, balance: 10, era: 9 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 10); }); } @@ -2890,6 +2896,7 @@ mod unbond { }, ] ); + assert_eq!(TotalValueUnbonding::::get(), 100); // When the bouncer kicks then its ok // Account with ID 200 is kicked. @@ -2905,6 +2912,7 @@ mod unbond { era: 3 }] ); + assert_eq!(TotalValueUnbonding::::get(), 100 + 200); assert_eq!( BondedPool::::get(1).unwrap(), @@ -2976,6 +2984,7 @@ mod unbond { Event::Unbonded { member: 100, pool_id: 1, points: 100, balance: 100, era: 3 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 100); // still permissionless unbond must be full assert_noop!( @@ -3004,6 +3013,7 @@ mod unbond { // but when everyone is unbonded it can.. CurrentEra::set(3); assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(10), 100, 0)); + assert_eq!(TotalValueUnbonding::::get(), 0); // still permissionless unbond must be full. assert_noop!( @@ -3018,6 +3028,7 @@ mod unbond { ); // but depositor itself can do it. assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(10), 10)); + assert_eq!(TotalValueUnbonding::::get(), 10); assert_eq!(BondedPools::::get(1).unwrap().points, 0); assert_eq!( @@ -3131,6 +3142,7 @@ mod unbond { Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 3 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 1); // when: casual further unbond, same era. assert_ok!(Pools::unbond(RuntimeOrigin::signed(10), 10, 5)); @@ -3156,6 +3168,7 @@ mod unbond { pool_events_since_last_call(), vec![Event::Unbonded { member: 10, pool_id: 1, points: 5, balance: 5, era: 3 }] ); + assert_eq!(TotalValueUnbonding::::get(), 1 + 5); // when: casual further unbond, next era. CurrentEra::set(1); @@ -3183,6 +3196,7 @@ mod unbond { pool_events_since_last_call(), vec![Event::Unbonded { member: 10, pool_id: 1, points: 1, balance: 1, era: 4 }] ); + assert_eq!(TotalValueUnbonding::::get(), 1 + 5 + 1); // when: unbonding more than our active: error assert_noop!( @@ -3218,6 +3232,7 @@ mod unbond { pool_events_since_last_call(), vec![Event::Unbonded { member: 10, pool_id: 1, points: 3, balance: 3, era: 4 }] ); + assert_eq!(TotalValueUnbonding::::get(), 1 + 5 + 1 + 3); }); } @@ -3246,6 +3261,8 @@ mod unbond { Error::::MaxUnbondingLimit ); + assert_eq!(TotalValueUnbonding::::get(), 2 + 3); + // when MaxUnbonding::set(3); assert_ok!(Pools::unbond(RuntimeOrigin::signed(20), 20, 1)); @@ -3266,6 +3283,7 @@ mod unbond { Event::Unbonded { member: 20, pool_id: 1, points: 1, balance: 1, era: 5 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 2 + 3 + 1); }) } @@ -3299,6 +3317,7 @@ mod unbond { Event::Unbonded { member: 10, pool_id: 1, points: 3, balance: 3, era: 3 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 3); }); } @@ -3342,6 +3361,7 @@ mod unbond { Event::Unbonded { member: 20, pool_id: 1, balance: 2, points: 2, era: 3 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 2); CurrentEra::set(1); Currency::set_balance(&default_reward_account(), 4 * Currency::minimum_balance()); @@ -3355,6 +3375,7 @@ mod unbond { Event::Unbonded { member: 20, pool_id: 1, points: 3, balance: 3, era: 4 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 2 + 3); CurrentEra::set(2); Currency::set_balance(&default_reward_account(), 4 * Currency::minimum_balance()); @@ -3367,6 +3388,7 @@ mod unbond { Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 5 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 2 + 3 + 5); assert_eq!( PoolMembers::::get(20).unwrap().unbonding_eras, @@ -3463,6 +3485,7 @@ mod withdraw_unbonded { // Sanity check assert_eq!(*unbond_pool, UnbondPool { points: 550 + 40, balance: 550 + 40 }); assert_eq!(TotalValueLocked::::get(), 600); + assert_eq!(TotalValueUnbonding::::get(), 590); // Simulate a slash to the pool with_era(current_era), decreasing the balance by // half @@ -3470,8 +3493,9 @@ mod withdraw_unbonded { unbond_pool.balance /= 2; // 295 SubPoolsStorage::::insert(1, sub_pools); - // Adjust the TVL for this non-api usage (direct sub-pool modification) + // Adjust TVL and TVU for this non-api usage (direct sub-pool modification) TotalValueLocked::::mutate(|x| *x -= 295); + TotalValueUnbonding::::mutate(|x| *x -= 295); // Update the equivalent of the unbonding chunks for the `StakingMock` let mut x = UnbondingBalanceMap::get(); @@ -3643,6 +3667,7 @@ mod withdraw_unbonded { } ] ); + assert_eq!(TotalValueUnbonding::::get(), 550 / 2 + 40 / 2); assert_eq!( balances_events_since_last_call(), vec![BEvent::Burned { who: default_bonded_account(), amount: 300 },] @@ -3687,6 +3712,7 @@ mod withdraw_unbonded { ] ); assert!(SubPoolsStorage::::get(1).unwrap().with_era.is_empty()); + assert_eq!(TotalValueUnbonding::::get(), 0); // now, finally, the depositor can take out its share. unsafe_set_state(1, PoolState::Destroying); @@ -3697,6 +3723,7 @@ mod withdraw_unbonded { SubPoolsStorage::::get(1).unwrap().with_era, unbonding_pools_with_era! { 6 => UnbondPool { points: 5, balance: 5 }} ); + assert_eq!(TotalValueUnbonding::::get(), 5); CurrentEra::set(CurrentEra::get() + 3); @@ -3731,6 +3758,7 @@ mod withdraw_unbonded { BEvent::Transfer { from: default_reward_account(), to: 10, amount: 5 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 0); }); } @@ -3849,6 +3877,7 @@ mod withdraw_unbonded { } ] ); + assert_eq!(TotalValueUnbonding::::get(), 100 + 200); // Given unsafe_set_state(1, PoolState::Blocked); @@ -3870,6 +3899,7 @@ mod withdraw_unbonded { assert!(!PoolMembers::::contains_key(100)); assert!(!PoolMembers::::contains_key(200)); assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default()); + assert_eq!(TotalValueUnbonding::::get(), 0); assert_eq!( pool_events_since_last_call(), vec![ @@ -3887,6 +3917,7 @@ mod withdraw_unbonded { ExtBuilder::default().add_members(vec![(100, 100)]).build_and_execute(|| { // Given assert_ok!(Pools::fully_unbond(RuntimeOrigin::signed(100), 100)); + assert_eq!(TotalValueUnbonding::::get(), 100); assert_eq!( BondedPool::::get(1).unwrap(), BondedPool { @@ -3918,6 +3949,7 @@ mod withdraw_unbonded { assert_eq!(SubPoolsStorage::::get(1).unwrap(), Default::default(),); assert_eq!(Currency::free_balance(&100), 100 + 100); assert!(!PoolMembers::::contains_key(100)); + assert_eq!(TotalValueUnbonding::::get(), 0); assert_eq!( pool_events_since_last_call(), vec![ @@ -3958,6 +3990,7 @@ mod withdraw_unbonded { ); assert_eq!(PoolMembers::::get(10).unwrap().active_points(), 13); assert_eq!(PoolMembers::::get(10).unwrap().unbonding_points(), 7); + assert_eq!(TotalValueUnbonding::::get(), 6 + 1); assert_eq!( pool_events_since_last_call(), vec![ @@ -3998,6 +4031,7 @@ mod withdraw_unbonded { pool_events_since_last_call(), vec![Event::Withdrawn { member: 10, pool_id: 1, points: 6, balance: 6 }] ); + assert_eq!(TotalValueUnbonding::::get(), 1); // when CurrentEra::set(4); @@ -4013,6 +4047,7 @@ mod withdraw_unbonded { pool_events_since_last_call(), vec![Event::Withdrawn { member: 10, pool_id: 1, points: 1, balance: 1 },] ); + assert_eq!(TotalValueUnbonding::::get(), 0); // when repeating: assert_noop!( @@ -4055,6 +4090,7 @@ mod withdraw_unbonded { Event::Unbonded { member: 11, pool_id: 1, points: 1, balance: 1, era: 4 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 6 + 1); // when CurrentEra::set(2); @@ -4085,6 +4121,7 @@ mod withdraw_unbonded { pool_events_since_last_call(), vec![Event::Withdrawn { member: 11, pool_id: 1, points: 6, balance: 6 }] ); + assert_eq!(TotalValueUnbonding::::get(), 1); // when CurrentEra::set(4); @@ -4100,6 +4137,7 @@ mod withdraw_unbonded { pool_events_since_last_call(), vec![Event::Withdrawn { member: 11, pool_id: 1, points: 1, balance: 1 }] ); + assert_eq!(TotalValueUnbonding::::get(), 0); // when repeating: assert_noop!( @@ -4158,6 +4196,7 @@ mod withdraw_unbonded { ); // tvl updated assert_eq!(TotalValueLocked::::get(), 35); + assert_eq!(TotalValueUnbonding::::get(), 25); // the 25 should be free now, and the member removed. CurrentEra::set(4); @@ -4169,6 +4208,7 @@ mod withdraw_unbonded { Event::MemberRemoved { pool_id: 1, member: 100 } ] ); + assert_eq!(TotalValueUnbonding::::get(), 0); }) } @@ -4212,6 +4252,7 @@ mod withdraw_unbonded { Event::Unbonded { member: 30, pool_id: 1, points: 5, balance: 5, era: 3 }, ] ); + assert_eq!(TotalValueUnbonding::::get(), 5 + 5); // when CurrentEra::set(1); @@ -4236,6 +4277,7 @@ mod withdraw_unbonded { pool_events_since_last_call(), vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 4 }] ); + assert_eq!(TotalValueUnbonding::::get(), 5 + 5 + 5); // when CurrentEra::set(2); @@ -4261,6 +4303,7 @@ mod withdraw_unbonded { pool_events_since_last_call(), vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 5 }] ); + assert_eq!(TotalValueUnbonding::::get(), 5 + 5 + 5 + 5); // when CurrentEra::set(5); @@ -4287,6 +4330,7 @@ mod withdraw_unbonded { pool_events_since_last_call(), vec![Event::Unbonded { member: 20, pool_id: 1, points: 5, balance: 5, era: 8 }] ); + assert_eq!(TotalValueUnbonding::::get(), 5 + 5 + 5 + 5 + 5); // now we start withdrawing unlocked bonds. @@ -4311,6 +4355,7 @@ mod withdraw_unbonded { pool_events_since_last_call(), vec![Event::Withdrawn { member: 20, pool_id: 1, points: 15, balance: 15 }] ); + assert_eq!(TotalValueUnbonding::::get(), 5); // when assert_ok!(Pools::withdraw_unbonded(RuntimeOrigin::signed(30), 30, 0)); @@ -4333,6 +4378,8 @@ mod withdraw_unbonded { pool_events_since_last_call(), vec![Event::Withdrawn { member: 30, pool_id: 1, points: 5, balance: 5 }] ); + // FIXME eagr actual value 5? + assert_eq!(TotalValueUnbonding::::get(), 0); }) } @@ -4395,6 +4442,7 @@ mod withdraw_unbonded { PoolMembers::::get(10).unwrap().unbonding_eras, member_unbonding_eras!(4 => 13) ); + assert_eq!(TotalValueUnbonding::::get(), 7 + 3 + 10 - 7); // the 13 should be free now, and the member removed. CurrentEra::set(4); @@ -4409,6 +4457,7 @@ mod withdraw_unbonded { ] ); assert!(!Metadata::::contains_key(1)); + assert_eq!(TotalValueUnbonding::::get(), 0); }) } @@ -4435,6 +4484,7 @@ mod withdraw_unbonded { Event::Unbonded { member: 20, pool_id: 1, balance: 20, points: 20, era: 4 }, ] ); + assert_eq!(TotalValueUnbonding::::get(), 20); CurrentEra::set(5); @@ -4452,6 +4502,7 @@ mod withdraw_unbonded { // Then assert_eq!(PoolMembers::::get(20), None); assert_eq!(ClaimPermissions::::contains_key(20), false); + assert_eq!(TotalValueUnbonding::::get(), 0); }); } }