Skip to content
Merged
70 changes: 67 additions & 3 deletions authority/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ use frame_support::{
},
weights::GetDispatchInfo,
};
use frame_system::pallet_prelude::*;
use frame_system::{pallet_prelude::*, EnsureOneOf, EnsureRoot, EnsureSigned};
use sp_runtime::{
traits::{CheckedSub, Dispatchable, Saturating},
ArithmeticError, DispatchError, DispatchResult, RuntimeDebug,
traits::{CheckedSub, Dispatchable, Hash, Saturating},
ArithmeticError, DispatchError, DispatchResult, Either, RuntimeDebug,
};
use sp_std::prelude::*;

Expand Down Expand Up @@ -172,6 +172,10 @@ pub mod module {
FailedToFastTrack,
/// Failed to delay a task.
FailedToDelay,
/// Call is not authorized.
CallNotAuthorized,
/// Triggering the call is not permitted.
TriggerCallNotPermitted,
}

#[pallet::event]
Expand All @@ -187,12 +191,22 @@ pub mod module {
Delayed(T::PalletsOrigin, ScheduleTaskIndex, T::BlockNumber),
/// A scheduled call is cancelled. [origin, index]
Cancelled(T::PalletsOrigin, ScheduleTaskIndex),
/// A call is authorized. \[hash, caller\]
AuthorizedCall(T::Hash, Option<T::AccountId>),
/// An authorized call was removed. \[hash\]
RemovedAuthorizedCall(T::Hash),
/// An authorized call was triggered. \[hash, caller\]
TriggeredCallBy(T::Hash, T::AccountId),
}

#[pallet::storage]
#[pallet::getter(fn next_task_index)]
pub type NextTaskIndex<T: Config> = StorageValue<_, ScheduleTaskIndex, ValueQuery>;

#[pallet::storage]
#[pallet::getter(fn saved_calls)]
pub type SavedCalls<T: Config> = StorageMap<_, Identity, T::Hash, (CallOf<T>, Option<T::AccountId>), OptionQuery>;

#[pallet::pallet]
pub struct Pallet<T>(_);

Expand Down Expand Up @@ -325,5 +339,55 @@ pub mod module {
Self::deposit_event(Event::Cancelled(*initial_origin, task_id));
Ok(())
}

#[pallet::weight(0)]
Comment thread
ermalkaleci marked this conversation as resolved.
Outdated
pub fn authorize_call(
origin: OriginFor<T>,
call: Box<CallOf<T>>,
caller: Option<T::AccountId>,
) -> DispatchResult {
ensure_root(origin)?;
let hash = T::Hashing::hash_of(&call);
SavedCalls::<T>::insert(hash, (call, caller.clone()));
Comment thread
ermalkaleci marked this conversation as resolved.
Self::deposit_event(Event::AuthorizedCall(hash, caller));
Ok(())
}

#[pallet::weight(0)]
pub fn remove_authorized_call(origin: OriginFor<T>, hash: T::Hash) -> DispatchResult {
let root_or_sigend =
EnsureOneOf::<T::AccountId, EnsureRoot<T::AccountId>, EnsureSigned<T::AccountId>>::ensure_origin(
origin,
)?;

SavedCalls::<T>::try_mutate_exists(hash, |maybe_call| {
let (_, maybe_caller) = maybe_call.take().ok_or(Error::<T>::CallNotAuthorized)?;
match root_or_sigend {
Either::Left(_) => {} // root, do nothing
Either::Right(who) => {
// signed, ensure it's the caller
let caller = maybe_caller.ok_or(Error::<T>::CallNotAuthorized)?;
ensure!(who == caller, Error::<T>::CallNotAuthorized);
}
}
Self::deposit_event(Event::RemovedAuthorizedCall(hash));
Ok(())
})
}

#[pallet::weight(0)]
pub fn trigger_call(origin: OriginFor<T>, hash: T::Hash) -> DispatchResult {
let who = ensure_signed(origin)?;
SavedCalls::<T>::try_mutate_exists(hash, |maybe_call| {
let (call, maybe_caller) = maybe_call.take().ok_or(Error::<T>::CallNotAuthorized)?;
if let Some(caller) = maybe_caller {
ensure!(who == caller, Error::<T>::TriggerCallNotPermitted);
}
let result = call.dispatch(OriginFor::<T>::root());

@apopiak apopiak Sep 14, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you skip weighing the call
you'll probably want to pass a weight as param and then ensure that the executed call has lower or equal weight to it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you skip weighing the call
you'll probably want to pass a weight as param and then ensure that the executed call has lower or equal weight to it

pushed an update to handle call post_info

Self::deposit_event(Event::TriggeredCallBy(hash, who));
Self::deposit_event(Event::Dispatched(result.map(|_| ()).map_err(|e| e.error)));
Ok(())
})
}
}
}
120 changes: 120 additions & 0 deletions authority/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,123 @@ fn call_size_limit() {
If the limit is too strong, maybe consider increasing the limit",
);
}

#[test]
fn authorize_call_works() {
ExtBuilder::default().build().execute_with(|| {
run_to_block(1);
let ensure_root_call = Call::System(frame_system::Call::fill_block(Perbill::one()));
let call = Call::Authority(authority::Call::dispatch_as(
MockAsOriginId::Root,
Box::new(ensure_root_call),
));
let hash = <Runtime as frame_system::Config>::Hashing::hash_of(&call);

// works without account
assert_ok!(Authority::authorize_call(Origin::root(), Box::new(call.clone()), None));
assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), None)));
System::assert_last_event(mock::Event::Authority(Event::AuthorizedCall(hash, None)));

// works with account
assert_ok!(Authority::authorize_call(
Origin::root(),
Box::new(call.clone()),
Some(1)
));
assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), Some(1))));
System::assert_last_event(mock::Event::Authority(Event::AuthorizedCall(hash, Some(1))));
});
}

#[test]
fn trigger_call_works() {
ExtBuilder::default().build().execute_with(|| {
run_to_block(1);
let ensure_root_call = Call::System(frame_system::Call::fill_block(Perbill::one()));
let call = Call::Authority(authority::Call::dispatch_as(
MockAsOriginId::Root,
Box::new(ensure_root_call),
));
let hash = <Runtime as frame_system::Config>::Hashing::hash_of(&call);

// call not authorized yet
assert_noop!(
Authority::trigger_call(Origin::signed(1), hash),
Error::<Runtime>::CallNotAuthorized
);

// works without caller
assert_ok!(Authority::authorize_call(Origin::root(), Box::new(call.clone()), None));
assert_ok!(Authority::trigger_call(Origin::signed(1), hash));
assert_eq!(Authority::saved_calls(&hash), None);
System::assert_has_event(mock::Event::Authority(Event::TriggeredCallBy(hash, 1)));
System::assert_last_event(mock::Event::Authority(Event::Dispatched(Ok(()))));

// works with caller 1
assert_ok!(Authority::authorize_call(
Origin::root(),
Box::new(call.clone()),
Some(1)
));
// caller 2 is not permitted to trigger the call
assert_noop!(
Authority::trigger_call(Origin::signed(2), hash),
Error::<Runtime>::TriggerCallNotPermitted
);
assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), Some(1))));

// caller 1 triggering the call
assert_ok!(Authority::trigger_call(Origin::signed(1), hash));
assert_eq!(Authority::saved_calls(&hash), None);
System::assert_has_event(mock::Event::Authority(Event::TriggeredCallBy(hash, 1)));
System::assert_last_event(mock::Event::Authority(Event::Dispatched(Ok(()))));
});
}

#[test]
fn remove_authorized_call_works() {
ExtBuilder::default().build().execute_with(|| {
run_to_block(1);
let ensure_root_call = Call::System(frame_system::Call::fill_block(Perbill::one()));
let call = Call::Authority(authority::Call::dispatch_as(
MockAsOriginId::Root,
Box::new(ensure_root_call),
));
let hash = <Runtime as frame_system::Config>::Hashing::hash_of(&call);

assert_noop!(
Authority::remove_authorized_call(Origin::root(), hash),
Error::<Runtime>::CallNotAuthorized
);

assert_ok!(Authority::authorize_call(Origin::root(), Box::new(call.clone()), None));
assert_noop!(
Authority::remove_authorized_call(Origin::signed(1), hash),
Error::<Runtime>::CallNotAuthorized
);
assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), None)));
assert_ok!(Authority::remove_authorized_call(Origin::root(), hash));
assert_eq!(Authority::saved_calls(&hash), None);

assert_ok!(Authority::authorize_call(
Origin::root(),
Box::new(call.clone()),
Some(1)
));
assert_ok!(Authority::remove_authorized_call(Origin::root(), hash));
assert_eq!(Authority::saved_calls(&hash), None);

assert_ok!(Authority::authorize_call(
Origin::root(),
Box::new(call.clone()),
Some(1)
));
assert_noop!(
Authority::remove_authorized_call(Origin::signed(2), hash),
Error::<Runtime>::CallNotAuthorized
);
assert_eq!(Authority::saved_calls(&hash), Some((call.clone(), Some(1))));
assert_ok!(Authority::remove_authorized_call(Origin::signed(1), hash));
assert_eq!(Authority::saved_calls(&hash), None);
});
}