Skip to content
This repository was archived by the owner on Apr 25, 2026. It is now read-only.

Commit 94bfb9b

Browse files
authored
chore: add extrinsic to update credits rate (#1007)
1 parent fe82287 commit 94bfb9b

13 files changed

Lines changed: 1976 additions & 618 deletions

File tree

Cargo.lock

Lines changed: 1424 additions & 251 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pallets/credits/src/lib.rs

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ pub mod pallet {
145145
#[pallet::constant]
146146
type MaxStakeTiers: Get<u32>;
147147

148+
/// Type for the origin that is allowed to update stake tiers.
149+
type ForceOrigin: frame_support::traits::EnsureOrigin<Self::RuntimeOrigin>;
150+
148151
/// The weight information for the pallet.
149152
type WeightInfo: WeightInfo;
150153
}
@@ -202,20 +205,22 @@ pub mod pallet {
202205
#[pallet::generate_deposit(pub(super) fn deposit_event)]
203206
pub enum Event<T: Config> {
204207
/// TNT tokens were successfully burned, granting potential off-chain credits.
205-
/// \[who, tnt_burned, credits_granted]
208+
/// Credits granted = amount_burned * conversion_rate.
209+
/// [who, amount_burned, credits_granted, offchain_account_id]
206210
CreditsGrantedFromBurn {
207211
who: T::AccountId,
208212
tnt_burned: BalanceOf<T>,
209213
credits_granted: BalanceOf<T>,
210214
},
211-
/// A user successfully claimed credits, emitting details for off-chain processing.
212-
/// The amount is the value requested by the user, verified against the claimable window.
213-
/// \[who, amount_claimed, offchain_account_id]
215+
/// Credits were claimed from staking rewards, within the allowed window.
216+
/// [who, amount_claimed, offchain_account_id]
214217
CreditsClaimed {
215218
who: T::AccountId,
216219
amount_claimed: BalanceOf<T>,
217220
offchain_account_id: OffchainAccountIdOf<T>,
218221
},
222+
/// Stake tiers were updated.
223+
StakeTiersUpdated,
219224
}
220225

221226
// --- Errors ---
@@ -225,16 +230,20 @@ pub mod pallet {
225230
InsufficientTntBalance,
226231
/// The requested claim amount exceeds the maximum calculated within the allowed window.
227232
ClaimAmountExceedsWindowAllowance,
228-
/// The provided off-chain account ID exceeds the maximum allowed length.
229-
OffchainAccountIdTooLong,
230-
/// An arithmetic operation resulted in an overflow.
231-
Overflow,
232-
/// No staking tiers are configured in the runtime.
233-
NoStakeTiersConfigured,
233+
/// Invalid claim ID (e.g., too long).
234+
InvalidClaimId,
235+
/// No stake tiers are configured or the stake amount is below the lowest tier threshold.
236+
NoValidTier,
234237
/// Amount specified for burn or claim must be greater than zero.
235238
AmountZero,
236239
/// Cannot transfer burned tokens to target account (feature not fully implemented).
237240
BurnTransferNotImplemented,
241+
/// The stake tiers are not properly sorted by threshold.
242+
StakeTiersNotSorted,
243+
/// There are no stake tiers provided for the update.
244+
EmptyStakeTiers,
245+
/// Amount overflowed.
246+
Overflow,
238247
}
239248

240249
#[pallet::call]
@@ -279,7 +288,7 @@ pub mod pallet {
279288
ensure!(amount_to_claim > Zero::zero(), Error::<T>::AmountZero);
280289
ensure!(
281290
offchain_account_id.len() <= T::MaxOffchainAccountIdLength::get() as usize,
282-
Error::<T>::OffchainAccountIdTooLong
291+
Error::<T>::InvalidClaimId
283292
);
284293

285294
let current_block = frame_system::Pallet::<T>::block_number();
@@ -302,6 +311,50 @@ pub mod pallet {
302311
});
303312
Ok(())
304313
}
314+
315+
/// Update the stake tiers. This function can only be called by the configured ForceOrigin.
316+
/// Stake tiers must be provided in ascending order by threshold.
317+
///
318+
/// Parameters:
319+
/// - `origin`: Must be the ForceOrigin
320+
/// - `new_tiers`: A vector of StakeTier structs representing the new tiers configuration
321+
///
322+
/// Emits `StakeTiersUpdated` on success.
323+
///
324+
/// Weight: O(n) where n is the number of tiers
325+
#[pallet::call_index(2)]
326+
#[pallet::weight(T::WeightInfo::burn())]
327+
pub fn set_stake_tiers(
328+
origin: OriginFor<T>,
329+
new_tiers: Vec<StakeTier<BalanceOf<T>>>,
330+
) -> DispatchResult {
331+
// Ensure the call is from the configured ForceOrigin
332+
T::ForceOrigin::ensure_origin(origin)?;
333+
334+
// Check that we have at least one tier
335+
ensure!(!new_tiers.is_empty(), Error::<T>::EmptyStakeTiers);
336+
337+
// Ensure tiers are properly sorted by threshold in ascending order
338+
for i in 1..new_tiers.len() {
339+
ensure!(
340+
new_tiers[i - 1].threshold <= new_tiers[i].threshold,
341+
Error::<T>::StakeTiersNotSorted
342+
);
343+
}
344+
345+
// Try to create a bounded vector
346+
let bounded_tiers =
347+
BoundedVec::<StakeTier<BalanceOf<T>>, T::MaxStakeTiers>::try_from(new_tiers)
348+
.map_err(|_| Error::<T>::EmptyStakeTiers)?; // Reusing error since we don't have a specific one for exceeding max tiers
349+
350+
// Update storage
351+
StoredStakeTiers::<T>::set(bounded_tiers);
352+
353+
// Emit event
354+
Self::deposit_event(Event::<T>::StakeTiersUpdated);
355+
356+
Ok(())
357+
}
305358
}
306359

307360
impl<T: Config> Pallet<T> {

pallets/credits/src/mock.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,7 @@ impl pallet_credits::Config for Runtime {
557557
type CreditBurnRecipient = CreditBurnRecipient;
558558
type MaxOffchainAccountIdLength = ConstU32<100>;
559559
type MaxStakeTiers = MaxStakeTiers;
560+
type ForceOrigin = frame_system::EnsureRoot<AccountId>;
560561
type WeightInfo = ();
561562
}
562563

pallets/credits/src/tests.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,93 @@ fn burn_and_claim_interact_correctly_via_last_update_block() {
575575
assert_eq!(last_reward_update(user), 150);
576576
});
577577
}
578+
579+
#[test]
580+
fn set_stake_tiers_works() {
581+
new_test_ext(vec![]).execute_with(|| {
582+
// Get initial stake tiers
583+
let initial_tiers = CreditsPallet::<Runtime>::stake_tiers();
584+
assert_eq!(initial_tiers.len(), 3, "Should have 3 initial tiers");
585+
586+
// Create new stake tiers
587+
let new_tiers = vec![
588+
StakeTier { threshold: 100, rate_per_block: 2 },
589+
StakeTier { threshold: 500, rate_per_block: 10 },
590+
StakeTier { threshold: 2000, rate_per_block: 25 },
591+
StakeTier { threshold: 10000, rate_per_block: 100 },
592+
];
593+
594+
// Verify non-root origin is rejected
595+
assert_noop!(
596+
CreditsPallet::<Runtime>::set_stake_tiers(
597+
RuntimeOrigin::signed(ALICE),
598+
new_tiers.clone()
599+
),
600+
frame_support::error::BadOrigin,
601+
);
602+
603+
// Verify empty tiers list is rejected
604+
assert_noop!(
605+
CreditsPallet::<Runtime>::set_stake_tiers(RuntimeOrigin::root(), vec![]),
606+
Error::<Runtime>::EmptyStakeTiers,
607+
);
608+
609+
// Verify unsorted tiers are rejected
610+
let unsorted_tiers = vec![
611+
StakeTier { threshold: 500, rate_per_block: 10 },
612+
StakeTier {
613+
threshold: 100, // This is less than the previous tier threshold
614+
rate_per_block: 2,
615+
},
616+
];
617+
assert_noop!(
618+
CreditsPallet::<Runtime>::set_stake_tiers(RuntimeOrigin::root(), unsorted_tiers),
619+
Error::<Runtime>::StakeTiersNotSorted,
620+
);
621+
622+
// Update stake tiers with root origin
623+
let set_tiers_call =
624+
CreditsPallet::<Runtime>::set_stake_tiers(RuntimeOrigin::root(), new_tiers.clone());
625+
assert_ok!(set_tiers_call);
626+
627+
// Verify event was emitted
628+
System::assert_has_event(Event::StakeTiersUpdated.into());
629+
630+
// Verify tiers were updated in storage
631+
let updated_tiers = CreditsPallet::<Runtime>::stake_tiers();
632+
assert_eq!(updated_tiers.len(), 4, "Should now have 4 tiers");
633+
634+
for (i, tier) in updated_tiers.iter().enumerate() {
635+
assert_eq!(tier.threshold, new_tiers[i].threshold, "Tier threshold should match");
636+
assert_eq!(tier.rate_per_block, new_tiers[i].rate_per_block, "Tier rate should match");
637+
}
638+
639+
// Set some tiers that have the same threshold but different rates
640+
let same_threshold_tiers = vec![
641+
StakeTier { threshold: 100, rate_per_block: 1 },
642+
StakeTier {
643+
threshold: 100, // Same threshold as previous tier
644+
rate_per_block: 2,
645+
},
646+
];
647+
648+
// Should be accepted since thresholds are considered properly sorted if they are <=
649+
assert_ok!(CreditsPallet::<Runtime>::set_stake_tiers(
650+
RuntimeOrigin::root(),
651+
same_threshold_tiers.clone()
652+
));
653+
654+
// Verify tiers were updated
655+
let final_tiers = CreditsPallet::<Runtime>::stake_tiers();
656+
assert_eq!(final_tiers.len(), 2, "Should now have 2 tiers");
657+
658+
// Test tier-based reward calculation with the new tiers
659+
let stake_amount_tier1 = 50; // Below first tier
660+
let rate_tier1 = CreditsPallet::<Runtime>::get_current_rate(stake_amount_tier1);
661+
assert_eq!(rate_tier1, 0, "Rate should be 0 for stake below lowest tier");
662+
663+
let stake_amount_tier2 = 100; // At first tier
664+
let rate_tier2 = CreditsPallet::<Runtime>::get_current_rate(stake_amount_tier2);
665+
assert_eq!(rate_tier2, 2, "Rate should match the tier 2 rate");
666+
});
667+
}

precompiles/credits/src/mock.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ impl pallet_credits::Config for Runtime {
425425
type CreditBurnRecipient = CreditBurnRecipient;
426426
type MaxOffchainAccountIdLength = ConstU32<100>;
427427
type MaxStakeTiers = MaxStakeTiers;
428+
type ForceOrigin = frame_system::EnsureRoot<AccountId>;
428429
type WeightInfo = ();
429430
}
430431

runtime/mainnet/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,7 @@ impl pallet_credits::Config for Runtime {
13971397
type CreditBurnRecipient = CreditBurnRecipient;
13981398
type MaxOffchainAccountIdLength = ConstU32<100>;
13991399
type MaxStakeTiers = MaxStakeTiers;
1400+
type ForceOrigin = EnsureRoot<AccountId>;
14001401
type WeightInfo = ();
14011402
}
14021403

runtime/testnet/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,7 @@ impl pallet_credits::Config for Runtime {
12821282
type CreditBurnRecipient = CreditBurnRecipient;
12831283
type MaxOffchainAccountIdLength = ConstU32<100>;
12841284
type MaxStakeTiers = MaxStakeTiers;
1285+
type ForceOrigin = EnsureRoot<AccountId>;
12851286
type WeightInfo = ();
12861287
}
12871288

types/src/interfaces/augment-api-errors.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,22 +361,30 @@ declare module '@polkadot/api-base/types/errors' {
361361
* The requested claim amount exceeds the maximum calculated within the allowed window.
362362
**/
363363
ClaimAmountExceedsWindowAllowance: AugmentedError<ApiType>;
364+
/**
365+
* There are no stake tiers provided for the update.
366+
**/
367+
EmptyStakeTiers: AugmentedError<ApiType>;
364368
/**
365369
* Insufficient TNT balance to perform the burn operation.
366370
**/
367371
InsufficientTntBalance: AugmentedError<ApiType>;
368372
/**
369-
* No staking tiers are configured in the runtime.
373+
* Invalid claim ID (e.g., too long).
370374
**/
371-
NoStakeTiersConfigured: AugmentedError<ApiType>;
375+
InvalidClaimId: AugmentedError<ApiType>;
372376
/**
373-
* The provided off-chain account ID exceeds the maximum allowed length.
377+
* No stake tiers are configured or the stake amount is below the lowest tier threshold.
374378
**/
375-
OffchainAccountIdTooLong: AugmentedError<ApiType>;
379+
NoValidTier: AugmentedError<ApiType>;
376380
/**
377-
* An arithmetic operation resulted in an overflow.
381+
* Amount overflowed.
378382
**/
379383
Overflow: AugmentedError<ApiType>;
384+
/**
385+
* The stake tiers are not properly sorted by threshold.
386+
**/
387+
StakeTiersNotSorted: AugmentedError<ApiType>;
380388
/**
381389
* Generic error
382390
**/

types/src/interfaces/augment-api-events.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,16 +365,20 @@ declare module '@polkadot/api-base/types/events' {
365365
};
366366
credits: {
367367
/**
368-
* A user successfully claimed credits, emitting details for off-chain processing.
369-
* The amount is the value requested by the user, verified against the claimable window.
370-
* \[who, amount_claimed, offchain_account_id]
368+
* Credits were claimed from staking rewards, within the allowed window.
369+
* [who, amount_claimed, offchain_account_id]
371370
**/
372371
CreditsClaimed: AugmentedEvent<ApiType, [who: AccountId32, amountClaimed: u128, offchainAccountId: Bytes], { who: AccountId32, amountClaimed: u128, offchainAccountId: Bytes }>;
373372
/**
374373
* TNT tokens were successfully burned, granting potential off-chain credits.
375-
* \[who, tnt_burned, credits_granted]
374+
* Credits granted = amount_burned * conversion_rate.
375+
* [who, amount_burned, credits_granted, offchain_account_id]
376376
**/
377377
CreditsGrantedFromBurn: AugmentedEvent<ApiType, [who: AccountId32, tntBurned: u128, creditsGranted: u128], { who: AccountId32, tntBurned: u128, creditsGranted: u128 }>;
378+
/**
379+
* Stake tiers were updated.
380+
**/
381+
StakeTiersUpdated: AugmentedEvent<ApiType, []>;
378382
/**
379383
* Generic event
380384
**/

types/src/interfaces/augment-api-tx.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { Data } from '@polkadot/types';
1010
import type { BTreeMap, Bytes, Compact, Null, Option, U256, U8aFixed, Vec, bool, u128, u16, u32, u64, u8 } from '@polkadot/types-codec';
1111
import type { AnyNumber, IMethod, ITuple } from '@polkadot/types-codec/types';
1212
import type { AccountId32, Call, H160, H256, MultiAddress, Perbill, Percent, Permill } from '@polkadot/types/interfaces/runtime';
13-
import type { EthereumTransactionTransactionV2, FrameSupportPreimagesBounded, IsmpGrandpaAddStateMachine, IsmpHostStateMachine, IsmpMessagingCreateConsensusState, IsmpMessagingMessage, PalletAirdropClaimsStatementKind, PalletAirdropClaimsUtilsMultiAddress, PalletAirdropClaimsUtilsMultiAddressSignature, PalletBalancesAdjustmentDirection, PalletDemocracyConviction, PalletDemocracyMetadataOwner, PalletDemocracyVoteAccountVote, PalletElectionProviderMultiPhaseRawSolution, PalletElectionProviderMultiPhaseSolutionOrSnapshotSize, PalletElectionsPhragmenRenouncing, PalletIdentityJudgement, PalletIdentityLegacyIdentityInfo, PalletImOnlineHeartbeat, PalletImOnlineSr25519AppSr25519Signature, PalletIsmpUtilsFundMessageParams, PalletIsmpUtilsUpdateConsensusState, PalletMultiAssetDelegationDelegatorDelegatorBlueprintSelection, PalletMultisigTimepoint, PalletNominationPoolsBondExtra, PalletNominationPoolsClaimPermission, PalletNominationPoolsCommissionChangeRate, PalletNominationPoolsCommissionClaimPermission, PalletNominationPoolsConfigOpAccountId32, PalletNominationPoolsConfigOpPerbill, PalletNominationPoolsConfigOpU128, PalletNominationPoolsConfigOpU32, PalletNominationPoolsPoolState, PalletRewardsAssetAction, PalletRewardsRewardConfigForAssetVault, PalletStakingPalletConfigOpPerbill, PalletStakingPalletConfigOpPercent, PalletStakingPalletConfigOpU128, PalletStakingPalletConfigOpU32, PalletStakingRewardDestination, PalletStakingUnlockChunk, PalletStakingValidatorPrefs, PalletTangleLstBondExtra, PalletTangleLstCommissionCommissionChangeRate, PalletTangleLstCommissionCommissionClaimPermission, PalletTangleLstConfigOpAccountId32, PalletTangleLstConfigOpPerbill, PalletTangleLstConfigOpU128, PalletTangleLstConfigOpU32, PalletTangleLstPoolsPoolState, PalletTokenGatewayAssetRegistration, PalletTokenGatewayPrecisionUpdate, PalletTokenGatewayTeleportParams, PalletVestingVestingInfo, SpConsensusBabeDigestsNextConfigDescriptor, SpConsensusGrandpaEquivocationProof, SpConsensusSlotsEquivocationProof, SpCoreVoid, SpNposElectionsElectionScore, SpNposElectionsSupport, SpRuntimeMultiSignature, SpSessionMembershipProof, SpWeightsWeightV2Weight, TanglePrimitivesRewardsLockMultiplier, TanglePrimitivesServicesField, TanglePrimitivesServicesPricingPricingQuote, TanglePrimitivesServicesServiceServiceBlueprint, TanglePrimitivesServicesTypesAsset, TanglePrimitivesServicesTypesAssetSecurityCommitment, TanglePrimitivesServicesTypesAssetSecurityRequirement, TanglePrimitivesServicesTypesMembershipModel, TanglePrimitivesServicesTypesOperatorPreferences, TangleTestnetRuntimeOpaqueSessionKeys, TangleTestnetRuntimeOriginCaller, TangleTestnetRuntimeProxyType, TokenGatewayPrimitivesGatewayAssetUpdate } from '@polkadot/types/lookup';
13+
import type { EthereumTransactionTransactionV2, FrameSupportPreimagesBounded, IsmpGrandpaAddStateMachine, IsmpHostStateMachine, IsmpMessagingCreateConsensusState, IsmpMessagingMessage, PalletAirdropClaimsStatementKind, PalletAirdropClaimsUtilsMultiAddress, PalletAirdropClaimsUtilsMultiAddressSignature, PalletBalancesAdjustmentDirection, PalletCreditsStakeTier, PalletDemocracyConviction, PalletDemocracyMetadataOwner, PalletDemocracyVoteAccountVote, PalletElectionProviderMultiPhaseRawSolution, PalletElectionProviderMultiPhaseSolutionOrSnapshotSize, PalletElectionsPhragmenRenouncing, PalletIdentityJudgement, PalletIdentityLegacyIdentityInfo, PalletImOnlineHeartbeat, PalletImOnlineSr25519AppSr25519Signature, PalletIsmpUtilsFundMessageParams, PalletIsmpUtilsUpdateConsensusState, PalletMultiAssetDelegationDelegatorDelegatorBlueprintSelection, PalletMultisigTimepoint, PalletNominationPoolsBondExtra, PalletNominationPoolsClaimPermission, PalletNominationPoolsCommissionChangeRate, PalletNominationPoolsCommissionClaimPermission, PalletNominationPoolsConfigOpAccountId32, PalletNominationPoolsConfigOpPerbill, PalletNominationPoolsConfigOpU128, PalletNominationPoolsConfigOpU32, PalletNominationPoolsPoolState, PalletRewardsAssetAction, PalletRewardsRewardConfigForAssetVault, PalletStakingPalletConfigOpPerbill, PalletStakingPalletConfigOpPercent, PalletStakingPalletConfigOpU128, PalletStakingPalletConfigOpU32, PalletStakingRewardDestination, PalletStakingUnlockChunk, PalletStakingValidatorPrefs, PalletTangleLstBondExtra, PalletTangleLstCommissionCommissionChangeRate, PalletTangleLstCommissionCommissionClaimPermission, PalletTangleLstConfigOpAccountId32, PalletTangleLstConfigOpPerbill, PalletTangleLstConfigOpU128, PalletTangleLstConfigOpU32, PalletTangleLstPoolsPoolState, PalletTokenGatewayAssetRegistration, PalletTokenGatewayPrecisionUpdate, PalletTokenGatewayTeleportParams, PalletVestingVestingInfo, SpConsensusBabeDigestsNextConfigDescriptor, SpConsensusGrandpaEquivocationProof, SpConsensusSlotsEquivocationProof, SpCoreVoid, SpNposElectionsElectionScore, SpNposElectionsSupport, SpRuntimeMultiSignature, SpSessionMembershipProof, SpWeightsWeightV2Weight, TanglePrimitivesRewardsLockMultiplier, TanglePrimitivesServicesField, TanglePrimitivesServicesPricingPricingQuote, TanglePrimitivesServicesServiceServiceBlueprint, TanglePrimitivesServicesTypesAsset, TanglePrimitivesServicesTypesAssetSecurityCommitment, TanglePrimitivesServicesTypesAssetSecurityRequirement, TanglePrimitivesServicesTypesMembershipModel, TanglePrimitivesServicesTypesOperatorPreferences, TangleTestnetRuntimeOpaqueSessionKeys, TangleTestnetRuntimeOriginCaller, TangleTestnetRuntimeProxyType, TokenGatewayPrimitivesGatewayAssetUpdate } from '@polkadot/types/lookup';
1414

1515
export type __AugmentedSubmittable = AugmentedSubmittable<() => unknown>;
1616
export type __SubmittableExtrinsic<ApiType extends ApiTypes> = SubmittableExtrinsic<ApiType>;
@@ -1214,6 +1214,19 @@ declare module '@polkadot/api-base/types/submittable' {
12141214
* processing.
12151215
**/
12161216
claimCredits: AugmentedSubmittable<(amountToClaim: Compact<u128> | AnyNumber | Uint8Array, offchainAccountId: Bytes | string | Uint8Array) => SubmittableExtrinsic<ApiType>, [Compact<u128>, Bytes]>;
1217+
/**
1218+
* Update the stake tiers. This function can only be called by the configured ForceOrigin.
1219+
* Stake tiers must be provided in ascending order by threshold.
1220+
*
1221+
* Parameters:
1222+
* - `origin`: Must be the ForceOrigin
1223+
* - `new_tiers`: A vector of StakeTier structs representing the new tiers configuration
1224+
*
1225+
* Emits `StakeTiersUpdated` on success.
1226+
*
1227+
* Weight: O(n) where n is the number of tiers
1228+
**/
1229+
setStakeTiers: AugmentedSubmittable<(newTiers: Vec<PalletCreditsStakeTier> | (PalletCreditsStakeTier | { threshold?: any; ratePerBlock?: any } | string | Uint8Array)[]) => SubmittableExtrinsic<ApiType>, [Vec<PalletCreditsStakeTier>]>;
12171230
/**
12181231
* Generic tx
12191232
**/

0 commit comments

Comments
 (0)