Skip to content

RFC: Deferred Dispatch for pallet-whitelist #11199

@muharem

Description

@muharem

Summary

Extend pallet-whitelist so that dispatch_whitelisted_call and dispatch_whitelisted_call_with_preimage automatically defer execution when the call is not yet whitelisted, instead of failing. The deferred call can be dispatched later by anyone once the whitelist entry arrives, or cleaned up after expiration.

Problem

Today, dispatch_whitelisted_call* requires the call to already be whitelisted at the time of dispatch. This creates a sequencing problem in governance:

  1. A governance proposal to dispatch a whitelisted call can only succeed if the whitelist entry is already in place.
  2. If the whitelist entry hasn't been set yet (e.g., the Fellowship hasn't whitelisted it), the governance-enacted dispatch simply fails and is lost.
  3. There is no mechanism to "park" an authorized dispatch and have it execute once the whitelist condition is met.

This forces tight coordination between the whitelisting body (e.g., Fellowship) and the dispatching body (e.g., general governance referendum), with no tolerance for ordering mismatches.

Details

Current Architecture

pallet-whitelist has two origin types:

  • WhitelistOrigin — can whitelist/remove call hashes (typically the Fellowship).
  • DispatchWhitelistedOrigin — can dispatch a whitelisted call with root origin (typically governance referenda).

The current calls are:

  • whitelist_call(call_hash) — adds a call hash to the whitelist.
  • remove_whitelisted_call(call_hash) — removes a call hash from the whitelist.
  • dispatch_whitelisted_call(call_hash, call_encoded_len, call_weight_witness) — dispatches a whitelisted call from preimage storage.
  • dispatch_whitelisted_call_with_preimage(call) — dispatches a whitelisted call with inline preimage.

All dispatch calls require the call to be present in the WhitelistedCall storage map at the time of execution. If the call is not whitelisted, the dispatch fails and the governance authorization is lost permanently.

The Sequencing Problem

Consider the typical governance flow:

  1. Fellowship whitelists a call hash via whitelist_call.
  2. A public referendum enacts dispatch_whitelisted_call for that hash.

If step 2 is enacted before step 1, the dispatch fails permanently. The referendum has already been decided and enacted — its authorization is consumed and lost. There is no retry mechanism.

Proposal

Modified Behavior of dispatch_whitelisted_call and dispatch_whitelisted_call_with_preimage

Both existing dispatch calls are modified with two changes:

1. Automatic deferral when not yet whitelisted

When called with DispatchWhitelistedOrigin and the call is not yet whitelisted, instead of returning an error, the call is stored as a deferred dispatch:

  • Stores the call hash in the DeferredDispatch storage map with expire_at = now + T::DeferredDispatchExpiration.
  • For dispatch_whitelisted_call_with_preimage, the preimage is noted in the preimage store via T::Preimages::note so it is available for later dispatch.
  • Emits DispatchDeferred { call_hash }.
  • Returns actual weight reflecting only the deferral cost.

2. Relaxed origin when a deferred entry exists

The origin check is relaxed:

  • Current: Requires DispatchWhitelistedOrigin.
  • New: Requires DispatchWhitelistedOrigin OR any origin if a DeferredDispatch entry exists for the call hash and not expired.

This allows anyone (including pallet origins) to trigger dispatch of a deferred call once it has been whitelisted, without needing to go through governance again.

The call must still be present in the WhitelistedCall storage — this requirement is unchanged.

When a deferred entry exists and the call is dispatched, the deferred entry is cleaned up as part of clean_and_dispatch.

New Call: remove_deferred_dispatch

#[pallet::call_index(4)]
pub fn remove_deferred_dispatch(
    origin: OriginFor<T>,
    call_hash: T::Hash,
) -> DispatchResultWithPostInfo
  • Origin: any signed origin (permissionless).
  • Behavior:
    • Fails if no deferred entry exists for call_hash (DeferredDispatchNotFound).
    • Fails if the entry has not yet expired (DeferredDispatchNotExpired).
    • Removes the entry from DeferredDispatch storage.
    • Emits DeferredDispatchRemoved { call_hash }.
    • Refunds transaction fees on success to incentivize cleanup.

New Config Parameter

/// The expiration duration for deferred dispatches. When a dispatch is
/// deferred, it expires at `now + DeferredDispatchExpiration`. After
/// expiration, anyone can remove the deferred entry via
/// `remove_deferred_dispatch`.
type DeferredDispatchExpiration: Get<BlockNumberFor<Self>>;

New Storage

/// Map of call hashes to their deferred dispatch expiration block.
#[pallet::storage]
pub type DeferredDispatch<T: Config> =
    StorageMap<_, Twox64Concat, T::Hash, BlockNumberFor<T>, OptionQuery>;

New Events

/// A dispatch was deferred for future execution because the call
/// was not yet whitelisted.
DispatchDeferred { call_hash: T::Hash },
/// A deferred dispatch entry was removed after expiration.
DeferredDispatchRemoved { call_hash: T::Hash },

New Errors

/// No deferred dispatch entry exists for this call hash.
DeferredDispatchNotFound,
/// The deferred dispatch entry has not yet expired.
DeferredDispatchNotExpired,

Conclusion

This proposal adds automatic deferred dispatch to pallet-whitelist by modifying the two existing dispatch calls. When a governance-authorized dispatch arrives before the whitelist entry, it is silently stored instead of failing. The deferred call can then be triggered by anyone once whitelisted, or cleaned up after expiration.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions