Runtime Misconfigurations in Polkadot: Risks and Mitigations

This post is the result of collaborative work between the Parity Security team and Security Research Labs.

In our previous post “Security insights and trends from Alpha 2022-2024”, we talked about the Alpha program, which enables SRLabs to perform security assessment of the parachains in the Polkadot community. We shared insights on the issues found during the assessments between 2022-2024.
As promised, we would like to proceed with detailed blogs for each of the ‘Substrate Top 10’ vulnerabilities and their mitigations. So, let’s dive-in together into common runtime misconfigurations identified through the Alpha program within Polkadot.

What are runtime misconfigurations?

The Runtime configuration is the backbone of any Polkadot-based blockchain and thus plays a critical role in the security and functionality of blockchain networks. Misconfigurations in runtime parameters such as existential deposit, runtime weights, XCM fees, etc. introduce risks that can lead to spamming and storage bloating.

One of the common issues we identified during the Alpha program is that parachains inherit Polkadot’s default runtime templates without updating the parameters with their own requirements. These subtle and overlooked runtime configurations leave the parachain vulnerable, opening the door to multiple attacks.

In the following sections, we explore common runtime misconfigurations, their implications, and the best practices to avoid them.

Runtime misconfigurations that could break your parachain

1. The pitfall of incorrect runtime weight configuration

Since parachain developers often start with Polkadot’s parachain template, they sometimes overlook a critical step: benchmarking and properly configuring weights for each pallet in their runtime. Every extrinsic in a Polkadot-based chain needs to have an accurate weight assigned, which is calculated using a benchmarking system to reflect the actual computation and storage costs. However, the Alpha program identified that many teams leave the weights unconfigured (resulting in a zero-value weight) as shown below:

impl pallet_name::Config for Runtime {
  ...
  type WeightInfo = ();
  ...
}

Another common case we identified is leaving the configuration to the default benchmarks from external runtimes rather than testing against their own parachain’s logic:

impl pallet_name::Config for Runtime {
  ...
  type WeightInfo = pallet_name::weights::SubstrateWeight<Runtime>; 
  ...
}

Note: This issue also falls under the “Incorrect benchmarking” category.

Huh! Incorrect weights, so what?

This misconfiguration can lead to severe consequences for the parachain and the users. Underestimated weights can cause blocks to process slower than expected, resulting in spam attacks, validators missing their slots, increase in chain storage, and eventually destabilizing the chain. On the other hand, overestimated weights result in overpriced extrinsics making the parachain expensive to the users.

How can this be fixed?

If your parachain hasn’t run custom benchmarking for every pallet, you might be operating with incorrect assumptions. The Benchmarking guide from Polkadot is a good place to start with benchmarking best practices.

Takeaway: Always benchmark your runtime. Don’t just inherit, validate.

2. Free XCM isn’t free

Cross-chain messaging (XCM) enables communication between parachains and external networks. An important XCM runtime configuration that is easy to miss when using the standard parachain template is setting the message fee. There’s a couple of instances where this happens:

  • XCM FeeManager being set to (), which completely waives all message delivery fees.
impl xcm_executor::Config for XcmConfig {
  ...
  type FeeManager = ();
  ...
}
  • The configuration for pallet cumulus-pallet-xcmp-queue setting PriceForSiblingDelivery = NoPriceForMessageDelivery, meaning no fees are charged for sending XCM messages across parachains.
impl cumulus_pallet_xcmp_queue::Config for Runtime {
  ...
  type PriceForSiblingDelivery = NoPriceForMessageDelivery<ParaId>;
  ...
}

How big is the risk?

This seemingly small mistake has big repercussions; it effectively disables all fee-based congestion control, leaving the chain wide open to abuse. Attackers can exploit this by flooding your XCM message queue with unlimited free messages, potentially causing dangerous congestion, storage exhaustion, message delays, or even dropped transactions.

The Fix: Charge smart fees

The fix is straightforward but crucial: never waive XCM fees entirely. Instead, implement proper fee management. An example is shown below:

impl xcm_executor::Config for XcmConfig {
  ...
  type FeeManager = XcmFeesToAccount<Self, SystemParachains, AccountId, TreasuryAccount>;
  ...
}

For message fees across sibling parachains, configure XCM dynamic fees in the runtime. A good reference is the exponential fee mechanism in Kusama, which scales up with the message volume.

Pro Tip: Polkadot’s templates are starters. Always customize fee logic for your parachain’s needs!

3. Missing dynamic fee adjustment for the transactions

Dynamic fee adjustment (via FeeMultiplierUpdate) is a transaction-level security mechanism that automatically scales transaction fees based on network congestion. If your FeeMultiplierUpdate is set to ConstFeeMultiplier (instead of a dynamic adjuster like TargetedFeeAdjustment, which is the case in the default templates), your network fees won’t scale with congestion. This means that during traffic spikes—whether organic or malicious—transaction fees stay flat, leaving your chain defenseless against DoS attacks.

How does this affect the parachain?

Attackers can exploit constant fees by flooding the network with transactions at minimal cost, potentially crowding out legitimate users for extended periods. Unlike systems with adaptive fees (where spam becomes prohibitively expensive), a static fee multiplier lets attackers sustain congestion indefinitely.

The Fix

Switch to a TargetedFeeAdjustment type (e.g., SlowAdjustingFeeUpdate). This automatically adjusts fees based on network load, creating an economic barrier against spam while keeping costs reasonable during normal operation. For example:

impl pallet_transaction_payment::Config for Runtime {
 ...
 type FeeMultiplierUpdate = TargetedFeeAdjustment<Self, SlowAdjustingFeeUpdate<Self>>;
 ...
}

Don’t rely on the template’s defaults; proactively configure this to protect your chain. Your users (and validators) will thank you!

4. Why proper freeze / hold configuration matters

Freeze / hold configurations let your parachain temporarily lock assets (freeze) or reserve them mid-operation (hold) for security purposes, like pausing suspicious transfers during an attack.

Here’s a subtle but important configuration oversight that could haunt your parachain later: if your runtime sets RuntimeFreezeReason = () or RuntimeHoldReason = (), you’re essentially disabling critical safety mechanisms. These empty configurations bypass the MaxFreezes integrity check and may cause unexpected failures when your chain eventually implements features that rely on freezing or holding assets; like governance interventions, security measures, or treasury controls.

This can make future upgrades or emergency actions to fail silently or panic when attempting to use freeze / hold functionality that your runtime claims doesn’t exist.

The Fix

Proactively configure these enums using the Polkadot SDK parachain template as your guide. For example:

#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]
pub enum RuntimeFreezeReason {
  GovernanceIntervention,
  SecurityEmergency,
  // ...your custom variants
}

Don’t wait until you need these features. Implement them properly from the start.

Remember: Empty configs might compile today, but they could fail catastrophically tomorrow.

5. Risk in insecure randomness

In blockchain, randomness isn’t just a nice thing to have, it is critical. A dangerous shortcut many developers take is using an insecure randomness pallet, such as pallet_insecure_randomness_collective_flip. While this pseudorandom number generator (PRNG) might seem convenient (it’s built into Substrate after all), its output is dangerously predictable. It simply hashes the last 81 blocks, meaning anyone can reverse-engineer future “random” values. Even if you’re not currently using randomness in your pallets, keeping this insecure import creates a silent vulnerability. The moment any pallet or future upgrade accidentally taps into this pseudo-randomness, you open the door to manipulation. Malicious collators could game the system by strategically influencing block production.

The Solution

Rip out this security liability entirely and implement VRF-based solutions as described in the Polkadot’s Randomness document or proper on-chain randomness through the BABE pallet (used by Kusama) as shown below:

impl pallet_name::Config for Runtime {
  ...
  type Randomness = pallet_babe::RandomnessFromOneEpochAgo<Runtime>;
  ...
}

6. The hidden cost of zero Existential Deposit

When your ExistentialDeposit is set to zero, you’re essentially allowing attackers to permanently bloat your chain’s state with dust accounts. In Polkadot SDK, the existential deposit acts as a “minimum balance” requirement. Accounts dipping below this threshold get automatically cleaned up to optimize storage. But with ExistentialDeposit = 0, even accounts holding microscopic balances become permanent storage liabilities.

Attackers could exploit this by distributing tiny balance amounts across thousands of accounts, gradually clogging your chain’s storage with unprunable data. While transaction fees might slow this attack, they won’t stop it entirely as storage costs aren’t factored into extrinsic weights.

The Fix

Set a sensible non-zero existential deposit (e.g., equivalent to ~$1 in your native token). This creates an economic barrier: filling storage would require locking real value per account, making attacks prohibitively expensive. The Polkadot Kusama configuration can be used as an example as shown below:

/// Money matters.
pub mod currency {
  use polkadot_primitives::Balance;
  /// The existential deposit.
  pub const EXISTENTIAL_DEPOSIT: Balance = CENTS;
  pub const UNITS: Balance = 1_000_000_000_000;
  pub const QUID: Balance = UNITS / 30;
  pub const CENTS: Balance = QUID / 100;
  ...
}

Default templates often use zero existential deposit for testing. Never deploy this to production.

Takeaways

These misconfigurations are not just theoretical risks, they are real issues we’ve observed in nearly every parachain audit. Most of the issues originate from using the default templates “as-is” and not customizing them with the parachains’ needs. The good news is that they are completely preventable with proper awareness and early audit.

That’s all for the misconfigurations, in our next post we will cover the issues with incorrect benchmarking.

4 Likes