This post discusses whether a potential solution should enable granular control over representing/casting/converting a pallet origin into a system-wide origin.
Problem Statement
Built-in guards and UX
Traditionally, pallet builders have used built-in checks (like ensure_signed
, ensure_root
, or ensure_none
) to implement guards on permissionless methods. The reasons behind this decision are pretty simple to explain:
- Checks are already there.
- They are simple, ergonomic, and intuitive. They provide the minimum essential guards without the need to think too much about it.
- Implementing custom guards (
EnsureOrigin
, custom implementations involvingOuterOrigin
s) are usually not necessary unless privileged calls/pallet origins come into place.
The need for more flexibility
While this isn’t necessarily problematic for most users (especially when pallet developers would expose their intended public APIs via traits), some edge cases require more flexibility.
One of those edge cases is where an arbitrary call is needed to be executed, either via the Proxy
or Schedule
pallets or directly using Dispatchable
s. An example of this case is what’s implemented on Collective
or Referenda
pallets, and more recently on Communities
pallet (currently under development).
Implementing such solutions requires designers to think about clean implementations that are limited to the scope of the pallet they’re building or, at least, to give runtime implementors some degree of manageability on which call origins may be used when dispatching an arbitrary call. This is where workarounds and tricks bloom.
Workarounds
For example, while Collective
s is opinionated on which origin to use when executing or proposing a poll and is therefore limited to the ensures that might be implemented as valid origins for some pallets, Referenda
exposes the TracksInfo
trait to let runtime implementors define whether an origin is bound to a track. This limits the ability of referenda proposals to what the runtime implementors decide. Also, it is less ergonomic, as it doesn’t allow for default behavior.
Converting a pallet origin into system origin is not possible (or at least, not that easy)
In the specific case of Communities
, the initially designed approach was to let a pallet origin be expressed in terms of frame_system
’s Origin
, as to be signed by the community’s AccountId, without intervention from the runtime end or without needing explicitly stating the origin to dispatching the call of the proposal, once approved, so it’s possible to execute some calls that don’t implement custom ensures (such as system::remark_with_event
).
However, this is not possible by default due to the current constraints of how FRAME is built, and the workaround was to follow a similar approach to what’s presented on the Referendum
pallet with TracksInfo
.
While this is not that bad (in the end, it’s a workaround and it works), it poses some challenges:
- Reduces ergonomy since it forces the users of the pallet to declare the signed origin for their community
AccountId
. - If a builder is unaware of these restrictions or how to circumvent them, it increases potential points of failure (or, better, points for attacks — let’s not forget how powerful a dispatchable is).
- While it is possible to implement a solution where a pallet origin defines a conversion to a system origin on their end, which is considered by the
RuntimeOrigin
, this requires implementing a customRuntimeOrigin
, not the one provided by FRAME. This implies redoing more boilerplate code, degrading the DX.
Solution
It is theoretically possible to implement a retro-compatible (thus, no need for a major breaking change) solution on FRAME
, that enables pallet builders to define a pallet’s origin conversion to system Origin that is considered when building RuntimeOrigin
.
An example of this potential solution goes like this:
// Define the structure for the pallet origin
pub struct RawOrigin {}
// Implement a failable conversion to `frame_syetem::Origin`
impl<T> trait TryInto<frame_system::Origin<T>> for RawOrigin
where
T: frame_system::Config
{
type Error = ();
fn try_into (self) -> Result<frame_system::Origin<T>, ()> {
todo!("Implement conversion to system origin here")
}
},
// Declare the structure as pallet origin
#[pallet::origin(as_system_origin)]
pub type Origin = RawOrigin;
This would tell FRAME to consider the origins coming from that pallet as possible candidates for an attempted conversion. An approach like this:
impl From<RuntimeOrigin> for #scrate::__private::sp_std::result::Result<#system_path::Origin<#runtime>, RuntimeOrigin> {
/// NOTE: converting to pallet origin loses the origin filter information.
fn from(val: RuntimeOrigin) -> Self {
match val.caller {
OriginCaller::system(l) => Ok(l),
// for #variant_name in #system_convertible_variants
OriginCaller::#variant_name(x) => x.try_into().map_err(|_| val),
// /for #variant_name in #system_convertible_variants
_ => Err(val)
}
}
}
Discussion
Of course, a post like this wouldn’t be complete without an invitation to leave feedback and think of the possibilities for this.