1use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
20use frame_support::traits::{
21 fungible::{Balanced, Credit},
22 tokens::imbalance::ResolveTo,
23 Contains, ContainsPair, Imbalance, OnUnbalanced,
24};
25use pallet_treasury::TreasuryAccountId;
26use polkadot_primitives::Balance;
27use sp_runtime::{traits::TryConvert, Perquintill};
28use xcm::VersionedLocation;
29
30pub struct ToAuthor<R>(core::marker::PhantomData<R>);
32impl<R> OnUnbalanced<Credit<R::AccountId, pallet_balances::Pallet<R>>> for ToAuthor<R>
33where
34 R: pallet_balances::Config + pallet_authorship::Config,
35 <R as frame_system::Config>::AccountId: From<polkadot_primitives::AccountId>,
36 <R as frame_system::Config>::AccountId: Into<polkadot_primitives::AccountId>,
37{
38 fn on_nonzero_unbalanced(
39 amount: Credit<<R as frame_system::Config>::AccountId, pallet_balances::Pallet<R>>,
40 ) {
41 if let Some(author) = <pallet_authorship::Pallet<R>>::author() {
42 let _ = <pallet_balances::Pallet<R>>::resolve(&author, amount);
43 }
44 }
45}
46
47pub struct DealWithFees<R>(core::marker::PhantomData<R>);
48impl<R> OnUnbalanced<Credit<R::AccountId, pallet_balances::Pallet<R>>> for DealWithFees<R>
49where
50 R: pallet_balances::Config + pallet_authorship::Config + pallet_treasury::Config,
51 <R as frame_system::Config>::AccountId: From<polkadot_primitives::AccountId>,
52 <R as frame_system::Config>::AccountId: Into<polkadot_primitives::AccountId>,
53{
54 fn on_unbalanceds(
55 mut fees_then_tips: impl Iterator<Item = Credit<R::AccountId, pallet_balances::Pallet<R>>>,
56 ) {
57 if let Some(fees) = fees_then_tips.next() {
58 let mut split = fees.ration(80, 20);
60 if let Some(tips) = fees_then_tips.next() {
61 tips.merge_into(&mut split.1);
63 }
64 ResolveTo::<TreasuryAccountId<R>, pallet_balances::Pallet<R>>::on_unbalanced(split.0);
65 <ToAuthor<R> as OnUnbalanced<_>>::on_unbalanced(split.1);
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct EraPayoutParams {
73 pub total_staked: Balance,
75 pub total_stakable: Balance,
80 pub ideal_stake: Perquintill,
82 pub max_annual_inflation: Perquintill,
84 pub min_annual_inflation: Perquintill,
86 pub falloff: Perquintill,
88 pub period_fraction: Perquintill,
90 pub legacy_auction_proportion: Option<Perquintill>,
92}
93
94pub fn relay_era_payout(params: EraPayoutParams) -> (Balance, Balance) {
97 use sp_runtime::traits::Saturating;
98
99 let EraPayoutParams {
100 total_staked,
101 total_stakable,
102 ideal_stake,
103 max_annual_inflation,
104 min_annual_inflation,
105 falloff,
106 period_fraction,
107 legacy_auction_proportion,
108 } = params;
109
110 let delta_annual_inflation = max_annual_inflation.saturating_sub(min_annual_inflation);
111
112 let ideal_stake = ideal_stake.saturating_sub(legacy_auction_proportion.unwrap_or_default());
113
114 let stake = Perquintill::from_rational(total_staked, total_stakable);
115 let adjustment = pallet_staking_reward_fn::compute_inflation(stake, ideal_stake, falloff);
116 let staking_inflation =
117 min_annual_inflation.saturating_add(delta_annual_inflation * adjustment);
118
119 let max_payout = period_fraction * max_annual_inflation * total_stakable;
120 let staking_payout = (period_fraction * staking_inflation) * total_stakable;
121 let rest = max_payout.saturating_sub(staking_payout);
122
123 let other_issuance = total_stakable.saturating_sub(total_staked);
124 if total_staked > other_issuance {
125 let _cap_rest = Perquintill::from_rational(other_issuance, total_staked) * staking_payout;
126 }
129 (staking_payout, rest)
130}
131
132#[derive(
135 Encode,
136 Decode,
137 DecodeWithMemTracking,
138 Eq,
139 PartialEq,
140 Clone,
141 Debug,
142 scale_info::TypeInfo,
143 MaxEncodedLen,
144)]
145pub enum VersionedLocatableAsset {
146 #[codec(index = 3)]
147 V3 { location: xcm::v3::Location, asset_id: xcm::v3::AssetId },
148 #[codec(index = 4)]
149 V4 { location: xcm::v4::Location, asset_id: xcm::v4::AssetId },
150 #[codec(index = 5)]
151 V5 { location: xcm::v5::Location, asset_id: xcm::v5::AssetId },
152}
153
154impl From<(xcm::latest::Location, xcm::latest::AssetId)> for VersionedLocatableAsset {
156 fn from(value: (xcm::latest::Location, xcm::latest::AssetId)) -> Self {
157 VersionedLocatableAsset::V5 { location: value.0, asset_id: value.1 }
158 }
159}
160
161pub struct LocatableAssetConverter;
163impl TryConvert<VersionedLocatableAsset, xcm_builder::LocatableAssetId>
164 for LocatableAssetConverter
165{
166 fn try_convert(
167 asset: VersionedLocatableAsset,
168 ) -> Result<xcm_builder::LocatableAssetId, VersionedLocatableAsset> {
169 match asset {
170 VersionedLocatableAsset::V3 { location, asset_id } => {
171 let v4_location: xcm::v4::Location =
172 location.try_into().map_err(|_| asset.clone())?;
173 let v4_asset_id: xcm::v4::AssetId =
174 asset_id.try_into().map_err(|_| asset.clone())?;
175 Ok(xcm_builder::LocatableAssetId {
176 location: v4_location.try_into().map_err(|_| asset.clone())?,
177 asset_id: v4_asset_id.try_into().map_err(|_| asset.clone())?,
178 })
179 },
180 VersionedLocatableAsset::V4 { ref location, ref asset_id } => {
181 Ok(xcm_builder::LocatableAssetId {
182 location: location.clone().try_into().map_err(|_| asset.clone())?,
183 asset_id: asset_id.clone().try_into().map_err(|_| asset.clone())?,
184 })
185 },
186 VersionedLocatableAsset::V5 { location, asset_id } => {
187 Ok(xcm_builder::LocatableAssetId { location, asset_id })
188 },
189 }
190 }
191}
192
193pub struct VersionedLocationConverter;
195impl TryConvert<&VersionedLocation, xcm::latest::Location> for VersionedLocationConverter {
196 fn try_convert(
197 location: &VersionedLocation,
198 ) -> Result<xcm::latest::Location, &VersionedLocation> {
199 let latest = match location.clone() {
200 VersionedLocation::V3(l) => {
201 let v4_location: xcm::v4::Location = l.try_into().map_err(|_| location)?;
202 v4_location.try_into().map_err(|_| location)?
203 },
204 VersionedLocation::V4(l) => l.try_into().map_err(|_| location)?,
205 VersionedLocation::V5(l) => l,
206 };
207 Ok(latest)
208 }
209}
210
211pub struct ContainsParts<C>(core::marker::PhantomData<C>);
214impl<C> Contains<VersionedLocatableAsset> for ContainsParts<C>
215where
216 C: ContainsPair<xcm::latest::Location, xcm::latest::Location>,
217{
218 fn contains(asset: &VersionedLocatableAsset) -> bool {
219 use VersionedLocatableAsset::*;
220 let (location, asset_id) = match asset.clone() {
221 V3 { location, asset_id } => {
222 let v4_location: xcm::v4::Location = match location.try_into() {
223 Ok(l) => l,
224 Err(_) => return false,
225 };
226 let v4_asset_id: xcm::v4::AssetId = match asset_id.try_into() {
227 Ok(a) => a,
228 Err(_) => return false,
229 };
230 match (v4_location.try_into(), v4_asset_id.try_into()) {
231 (Ok(l), Ok(a)) => (l, a),
232 _ => return false,
233 }
234 },
235 V4 { location, asset_id } => match (location.try_into(), asset_id.try_into()) {
236 (Ok(l), Ok(a)) => (l, a),
237 _ => return false,
238 },
239 V5 { location, asset_id } => (location, asset_id),
240 };
241 C::contains(&location, &asset_id.0)
242 }
243}
244
245#[cfg(feature = "runtime-benchmarks")]
246pub mod benchmarks {
247 use super::VersionedLocatableAsset;
248 use core::marker::PhantomData;
249 use frame_support::traits::Get;
250 use pallet_asset_rate::AssetKindFactory;
251 use pallet_treasury::ArgumentsFactory as TreasuryArgumentsFactory;
252 use sp_core::{ConstU32, ConstU8};
253 use xcm::prelude::*;
254
255 pub struct AssetRateArguments;
258 impl AssetKindFactory<VersionedLocatableAsset> for AssetRateArguments {
259 fn create_asset_kind(seed: u32) -> VersionedLocatableAsset {
260 (
261 Location::new(0, [Parachain(seed)]),
262 AssetId(Location::new(
263 0,
264 [PalletInstance(seed.try_into().unwrap()), GeneralIndex(seed.into())],
265 )),
266 )
267 .into()
268 }
269 }
270
271 pub struct TreasuryArguments<Parents = ConstU8<0>, ParaId = ConstU32<0>>(
275 PhantomData<(Parents, ParaId)>,
276 );
277 impl<Parents: Get<u8>, ParaId: Get<u32>>
278 TreasuryArgumentsFactory<VersionedLocatableAsset, VersionedLocation>
279 for TreasuryArguments<Parents, ParaId>
280 {
281 fn create_asset_kind(seed: u32) -> VersionedLocatableAsset {
282 (
283 Location::new(Parents::get(), [Junction::Parachain(ParaId::get())]),
284 AssetId(Location::new(
285 0,
286 [PalletInstance(seed.try_into().unwrap()), GeneralIndex(seed.into())],
287 )),
288 )
289 .into()
290 }
291 fn create_beneficiary(seed: [u8; 32]) -> VersionedLocation {
292 VersionedLocation::from(Location::new(0, [AccountId32 { network: None, id: seed }]))
293 }
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use frame_support::{
301 derive_impl,
302 dispatch::DispatchClass,
303 parameter_types,
304 traits::{
305 tokens::{PayFromAccount, UnityAssetBalanceConversion},
306 FindAuthor,
307 },
308 weights::Weight,
309 PalletId,
310 };
311 use frame_system::limits;
312 use polkadot_primitives::AccountId;
313 use sp_core::{ConstU64, H256};
314 use sp_runtime::{
315 traits::{BlakeTwo256, IdentityLookup},
316 BuildStorage, Perbill,
317 };
318
319 type Block = frame_system::mocking::MockBlock<Test>;
320 const TEST_ACCOUNT: AccountId = AccountId::new([1; 32]);
321
322 frame_support::construct_runtime!(
323 pub enum Test
324 {
325 System: frame_system,
326 Authorship: pallet_authorship,
327 Balances: pallet_balances,
328 Treasury: pallet_treasury,
329 }
330 );
331
332 parameter_types! {
333 pub BlockWeights: limits::BlockWeights = limits::BlockWeights::builder()
334 .base_block(Weight::from_parts(10, 0))
335 .for_class(DispatchClass::all(), |weight| {
336 weight.base_extrinsic = Weight::from_parts(100, 0);
337 })
338 .for_class(DispatchClass::non_mandatory(), |weight| {
339 weight.max_total = Some(Weight::from_parts(1024, u64::MAX));
340 })
341 .build_or_panic();
342 pub BlockLength: limits::BlockLength = limits::BlockLength::builder()
343 .max_length(2 * 1024)
344 .build();
345 pub const AvailableBlockRatio: Perbill = Perbill::one();
346 }
347
348 #[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
349 impl frame_system::Config for Test {
350 type BaseCallFilter = frame_support::traits::Everything;
351 type RuntimeOrigin = RuntimeOrigin;
352 type Nonce = u64;
353 type RuntimeCall = RuntimeCall;
354 type Hash = H256;
355 type Hashing = BlakeTwo256;
356 type AccountId = AccountId;
357 type Lookup = IdentityLookup<Self::AccountId>;
358 type Block = Block;
359 type RuntimeEvent = RuntimeEvent;
360 type BlockLength = BlockLength;
361 type BlockWeights = BlockWeights;
362 type DbWeight = ();
363 type Version = ();
364 type PalletInfo = PalletInfo;
365 type AccountData = pallet_balances::AccountData<u64>;
366 type OnNewAccount = ();
367 type OnKilledAccount = ();
368 type SystemWeightInfo = ();
369 type SS58Prefix = ();
370 type OnSetCode = ();
371 type MaxConsumers = frame_support::traits::ConstU32<16>;
372 }
373
374 #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
375 impl pallet_balances::Config for Test {
376 type AccountStore = System;
377 }
378
379 parameter_types! {
380 pub const TreasuryPalletId: PalletId = PalletId(*b"py/trsry");
381 pub const MaxApprovals: u32 = 100;
382 pub TreasuryAccount: AccountId = Treasury::account_id();
383 }
384
385 impl pallet_treasury::Config for Test {
386 type Currency = pallet_balances::Pallet<Test>;
387 type RejectOrigin = frame_system::EnsureRoot<AccountId>;
388 type RuntimeEvent = RuntimeEvent;
389 type SpendPeriod = ();
390 type Burn = ();
391 type BurnDestination = ();
392 type PalletId = TreasuryPalletId;
393 type SpendFunds = ();
394 type MaxApprovals = MaxApprovals;
395 type WeightInfo = ();
396 type SpendOrigin = frame_support::traits::NeverEnsureOrigin<u64>;
397 type AssetKind = ();
398 type Beneficiary = Self::AccountId;
399 type BeneficiaryLookup = IdentityLookup<Self::AccountId>;
400 type Paymaster = PayFromAccount<Balances, TreasuryAccount>;
401 type BalanceConverter = UnityAssetBalanceConversion;
402 type PayoutPeriod = ConstU64<0>;
403 type BlockNumberProvider = System;
404 #[cfg(feature = "runtime-benchmarks")]
405 type BenchmarkHelper = ();
406 }
407
408 pub struct OneAuthor;
409 impl FindAuthor<AccountId> for OneAuthor {
410 fn find_author<'a, I>(_: I) -> Option<AccountId>
411 where
412 I: 'a,
413 {
414 Some(TEST_ACCOUNT)
415 }
416 }
417 impl pallet_authorship::Config for Test {
418 type FindAuthor = OneAuthor;
419 type EventHandler = ();
420 }
421
422 pub fn new_test_ext() -> sp_io::TestExternalities {
423 let mut t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
424 pallet_balances::GenesisConfig::<Test>::default()
426 .assimilate_storage(&mut t)
427 .unwrap();
428 t.into()
429 }
430
431 pub fn deprecated_era_payout(
432 total_staked: Balance,
433 total_stakable: Balance,
434 max_annual_inflation: Perquintill,
435 period_fraction: Perquintill,
436 auctioned_slots: u64,
437 ) -> (Balance, Balance) {
438 use pallet_staking_reward_fn::compute_inflation;
439 use sp_runtime::traits::Saturating;
440
441 let min_annual_inflation = Perquintill::from_rational(25u64, 1000u64);
442 let delta_annual_inflation = max_annual_inflation.saturating_sub(min_annual_inflation);
443
444 let auction_proportion = Perquintill::from_rational(auctioned_slots.min(60), 200u64);
446
447 let ideal_stake = Perquintill::from_percent(75).saturating_sub(auction_proportion);
450
451 let stake = Perquintill::from_rational(total_staked, total_stakable);
452 let falloff = Perquintill::from_percent(5);
453 let adjustment = compute_inflation(stake, ideal_stake, falloff);
454 let staking_inflation =
455 min_annual_inflation.saturating_add(delta_annual_inflation * adjustment);
456
457 let max_payout = period_fraction * max_annual_inflation * total_stakable;
458 let staking_payout = (period_fraction * staking_inflation) * total_stakable;
459 let rest = max_payout.saturating_sub(staking_payout);
460
461 let other_issuance = total_stakable.saturating_sub(total_staked);
462 if total_staked > other_issuance {
463 let _cap_rest =
464 Perquintill::from_rational(other_issuance, total_staked) * staking_payout;
465 }
468 (staking_payout, rest)
469 }
470
471 #[test]
472 fn test_fees_and_tip_split() {
473 new_test_ext().execute_with(|| {
474 let fee =
475 <pallet_balances::Pallet<Test> as frame_support::traits::fungible::Balanced<
476 AccountId,
477 >>::issue(10);
478 let tip =
479 <pallet_balances::Pallet<Test> as frame_support::traits::fungible::Balanced<
480 AccountId,
481 >>::issue(20);
482
483 assert_eq!(Balances::free_balance(Treasury::account_id()), 0);
484 assert_eq!(Balances::free_balance(TEST_ACCOUNT), 0);
485
486 DealWithFees::on_unbalanceds(vec![fee, tip].into_iter());
487
488 assert_eq!(Balances::free_balance(TEST_ACCOUNT), 22);
490 assert_eq!(Balances::free_balance(Treasury::account_id()), 8);
492 });
493 }
494
495 #[test]
496 fn compute_inflation_should_give_sensible_results() {
497 assert_eq!(
498 pallet_staking_reward_fn::compute_inflation(
499 Perquintill::from_percent(75),
500 Perquintill::from_percent(75),
501 Perquintill::from_percent(5),
502 ),
503 Perquintill::one()
504 );
505 assert_eq!(
506 pallet_staking_reward_fn::compute_inflation(
507 Perquintill::from_percent(50),
508 Perquintill::from_percent(75),
509 Perquintill::from_percent(5),
510 ),
511 Perquintill::from_rational(2u64, 3u64)
512 );
513 assert_eq!(
514 pallet_staking_reward_fn::compute_inflation(
515 Perquintill::from_percent(80),
516 Perquintill::from_percent(75),
517 Perquintill::from_percent(5),
518 ),
519 Perquintill::from_rational(1u64, 2u64)
520 );
521 }
522
523 #[test]
524 fn era_payout_should_give_sensible_results() {
525 let payout =
526 deprecated_era_payout(75, 100, Perquintill::from_percent(10), Perquintill::one(), 0);
527 assert_eq!(payout, (10, 0));
528
529 let payout =
530 deprecated_era_payout(80, 100, Perquintill::from_percent(10), Perquintill::one(), 0);
531 assert_eq!(payout, (6, 4));
532 }
533
534 #[test]
535 fn relay_era_payout_should_give_sensible_results() {
536 let params = EraPayoutParams {
537 total_staked: 75,
538 total_stakable: 100,
539 ideal_stake: Perquintill::from_percent(75),
540 max_annual_inflation: Perquintill::from_percent(10),
541 min_annual_inflation: Perquintill::from_rational(25u64, 1000u64),
542 falloff: Perquintill::from_percent(5),
543 period_fraction: Perquintill::one(),
544 legacy_auction_proportion: None,
545 };
546 assert_eq!(relay_era_payout(params), (10, 0));
547
548 let params = EraPayoutParams {
549 total_staked: 80,
550 total_stakable: 100,
551 ideal_stake: Perquintill::from_percent(75),
552 max_annual_inflation: Perquintill::from_percent(10),
553 min_annual_inflation: Perquintill::from_rational(25u64, 1000u64),
554 falloff: Perquintill::from_percent(5),
555 period_fraction: Perquintill::one(),
556 legacy_auction_proportion: None,
557 };
558 assert_eq!(relay_era_payout(params), (6, 4));
559 }
560
561 #[test]
562 fn relay_era_payout_should_give_same_results_as_era_payout() {
563 let total_staked = 1_000_000;
564 let total_stakable = 2_000_000;
565 let max_annual_inflation = Perquintill::from_percent(10);
566 let period_fraction = Perquintill::from_percent(25);
567 let auctioned_slots = 30;
568
569 let params = EraPayoutParams {
570 total_staked,
571 total_stakable,
572 ideal_stake: Perquintill::from_percent(75),
573 max_annual_inflation,
574 min_annual_inflation: Perquintill::from_rational(25u64, 1000u64),
575 falloff: Perquintill::from_percent(5),
576 period_fraction,
577 legacy_auction_proportion: Some(Perquintill::from_rational(
578 auctioned_slots.min(60),
579 200u64,
580 )),
581 };
582
583 let payout = deprecated_era_payout(
584 total_staked,
585 total_stakable,
586 max_annual_inflation,
587 period_fraction,
588 auctioned_slots,
589 );
590 assert_eq!(relay_era_payout(params), payout);
591
592 let total_staked = 1_900_000;
593 let total_stakable = 2_000_000;
594 let auctioned_slots = 60;
595
596 let params = EraPayoutParams {
597 total_staked,
598 total_stakable,
599 ideal_stake: Perquintill::from_percent(75),
600 max_annual_inflation,
601 min_annual_inflation: Perquintill::from_rational(25u64, 1000u64),
602 falloff: Perquintill::from_percent(5),
603 period_fraction,
604 legacy_auction_proportion: Some(Perquintill::from_rational(
605 auctioned_slots.min(60),
606 200u64,
607 )),
608 };
609
610 let payout = deprecated_era_payout(
611 total_staked,
612 total_stakable,
613 max_annual_inflation,
614 period_fraction,
615 auctioned_slots,
616 );
617
618 assert_eq!(relay_era_payout(params), payout);
619 }
620}