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:
- A governance proposal to dispatch a whitelisted call can only succeed if the whitelist entry is already in place.
- 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.
- 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:
- Fellowship whitelists a call hash via
whitelist_call.
- 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.
Summary
Extend
pallet-whitelistso thatdispatch_whitelisted_callanddispatch_whitelisted_call_with_preimageautomatically 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: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-whitelisthas 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
WhitelistedCallstorage 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:
whitelist_call.dispatch_whitelisted_callfor 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_callanddispatch_whitelisted_call_with_preimageBoth existing dispatch calls are modified with two changes:
1. Automatic deferral when not yet whitelisted
When called with
DispatchWhitelistedOriginand the call is not yet whitelisted, instead of returning an error, the call is stored as a deferred dispatch:DeferredDispatchstorage map withexpire_at = now + T::DeferredDispatchExpiration.dispatch_whitelisted_call_with_preimage, the preimage is noted in the preimage store viaT::Preimages::noteso it is available for later dispatch.DispatchDeferred { call_hash }.2. Relaxed origin when a deferred entry exists
The origin check is relaxed:
DispatchWhitelistedOrigin.DispatchWhitelistedOriginOR any origin if aDeferredDispatchentry 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
WhitelistedCallstorage — 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_dispatchcall_hash(DeferredDispatchNotFound).DeferredDispatchNotExpired).DeferredDispatchstorage.DeferredDispatchRemoved { call_hash }.New Config Parameter
New Storage
New Events
New Errors
Conclusion
This proposal adds automatic deferred dispatch to
pallet-whitelistby 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.