#![cfg_attr(not(feature = "std"), no_std)]
#[allow(
deprecated,
unreachable_patterns,
clippy::let_unit_value,
clippy::cast_possible_truncation,
clippy::ignored_unit_patterns
)] #[frame_support::pallet]
pub mod pallet {
use scale_info::TypeInfo;
use sp_core::sr25519::Public;
use sp_io::hashing::blake2_256;
use frame_system::pallet_prelude::*;
#[allow(unused)]
use frame_support::{pallet_prelude::*, sp_runtime};
use serai_primitives::*;
use serai_signals_primitives::SignalId;
use validator_sets_pallet::{primitives::ValidatorSet, Config as VsConfig, Pallet as VsPallet};
use in_instructions_pallet::{Config as IiConfig, Pallet as InInstructions};
#[pallet::config]
pub trait Config:
frame_system::Config<AccountId = Public> + VsConfig + IiConfig + TypeInfo
{
type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;
type RetirementValidityDuration: Get<u32>;
type RetirementLockInDuration: Get<u32>;
}
#[pallet::genesis_config]
#[derive(Debug, Encode, Decode)]
pub struct GenesisConfig<T: Config> {
_config: PhantomData<T>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
GenesisConfig { _config: PhantomData }
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
assert!(T::RetirementValidityDuration::get() < T::RetirementLockInDuration::get());
}
}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct RegisteredRetirementSignal<T: Config> {
in_favor_of: [u8; 32],
registrant: T::AccountId,
registered_at: BlockNumberFor<T>,
}
impl<T: Config> RegisteredRetirementSignal<T> {
fn id(&self) -> [u8; 32] {
let mut preimage = b"Signal".to_vec();
preimage.extend(&self.encode());
blake2_256(&preimage)
}
}
#[pallet::storage]
type RegisteredRetirementSignals<T: Config> =
StorageMap<_, Blake2_128Concat, [u8; 32], RegisteredRetirementSignal<T>, OptionQuery>;
#[pallet::storage]
pub type Favors<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
(SignalId, NetworkId),
Blake2_128Concat,
T::AccountId,
(),
OptionQuery,
>;
#[pallet::storage]
pub type SetsInFavor<T: Config> =
StorageMap<_, Blake2_128Concat, (SignalId, ValidatorSet), (), OptionQuery>;
#[pallet::storage]
pub type LockedInRetirement<T: Config> =
StorageValue<_, ([u8; 32], BlockNumberFor<T>), OptionQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
RetirementSignalRegistered {
signal_id: [u8; 32],
in_favor_of: [u8; 32],
registrant: T::AccountId,
},
RetirementSignalRevoked {
signal_id: [u8; 32],
},
SignalFavored {
signal_id: SignalId,
by: T::AccountId,
for_network: NetworkId,
},
SetInFavor {
signal_id: SignalId,
set: ValidatorSet,
},
RetirementSignalLockedIn {
signal_id: [u8; 32],
},
SetNoLongerInFavor {
signal_id: SignalId,
set: ValidatorSet,
},
FavorRevoked {
signal_id: SignalId,
by: T::AccountId,
for_network: NetworkId,
},
AgainstSignal {
signal_id: SignalId,
who: T::AccountId,
for_network: NetworkId,
},
}
#[pallet::error]
pub enum Error<T> {
RetirementSignalLockedIn,
RetirementSignalAlreadyRegistered,
NotRetirementSignalRegistrant,
NonExistentRetirementSignal,
ExpiredRetirementSignal,
NotValidator,
RevokingNonExistentFavor,
}
const REQUIREMENT_NUMERATOR: u64 = 4;
const REQUIREMENT_DIVISOR: u64 = 5;
impl<T: Config> Pallet<T> {
fn tally_for_network(signal_id: SignalId, network: NetworkId) -> bool {
let this_network_session = VsPallet::<T>::latest_decided_session(network).unwrap();
let this_set = ValidatorSet { network, session: this_network_session };
let mut iter = Favors::<T>::iter_prefix_values((signal_id, network));
let mut needed_favor = (VsPallet::<T>::total_allocated_stake(network).unwrap().0 *
REQUIREMENT_NUMERATOR)
.div_ceil(REQUIREMENT_DIVISOR);
while iter.next().is_some() && (needed_favor != 0) {
let item_key = iter.last_raw_key();
let account = T::AccountId::decode(&mut &item_key[(item_key.len() - 32) ..]).unwrap();
if VsPallet::<T>::in_latest_decided_set(network, account) {
needed_favor =
needed_favor.saturating_sub(VsPallet::<T>::allocation((network, account)).unwrap().0);
}
}
if needed_favor == 0 {
if !SetsInFavor::<T>::contains_key((signal_id, this_set)) {
SetsInFavor::<T>::set((signal_id, this_set), Some(()));
Self::deposit_event(Event::SetInFavor { signal_id, set: this_set });
}
true
} else {
if SetsInFavor::<T>::contains_key((signal_id, this_set)) {
SetsInFavor::<T>::remove((signal_id, this_set));
Self::deposit_event(Event::SetNoLongerInFavor { signal_id, set: this_set });
}
false
}
}
fn tally_for_all_networks(signal_id: SignalId) -> bool {
let mut total_in_favor_stake = 0;
let mut total_allocated_stake = 0;
for network in serai_primitives::NETWORKS {
let Some(latest_decided_session) = VsPallet::<T>::latest_decided_session(network) else {
continue;
};
let network_stake = VsPallet::<T>::total_allocated_stake(network).unwrap();
if SetsInFavor::<T>::contains_key((
signal_id,
ValidatorSet { network, session: latest_decided_session },
)) {
total_in_favor_stake += network_stake.0;
}
total_allocated_stake += network_stake.0;
}
total_in_favor_stake >=
(total_allocated_stake * REQUIREMENT_NUMERATOR).div_ceil(REQUIREMENT_DIVISOR)
}
fn revoke_favor_internal(
account: T::AccountId,
signal_id: SignalId,
for_network: NetworkId,
) -> DispatchResult {
if !Favors::<T>::contains_key((signal_id, for_network), account) {
Err::<(), _>(Error::<T>::RevokingNonExistentFavor)?;
}
Favors::<T>::remove((signal_id, for_network), account);
Self::deposit_event(Event::<T>::FavorRevoked { signal_id, by: account, for_network });
Self::tally_for_network(signal_id, for_network);
Ok(())
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)] pub fn register_retirement_signal(
origin: OriginFor<T>,
in_favor_of: [u8; 32],
) -> DispatchResult {
if LockedInRetirement::<T>::exists() {
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
}
let account = ensure_signed(origin)?;
let signal = RegisteredRetirementSignal {
in_favor_of,
registrant: account,
registered_at: frame_system::Pallet::<T>::block_number(),
};
let signal_id = signal.id();
if RegisteredRetirementSignals::<T>::get(signal_id).is_some() {
Err::<(), _>(Error::<T>::RetirementSignalAlreadyRegistered)?;
}
Self::deposit_event(Event::<T>::RetirementSignalRegistered {
signal_id,
in_favor_of,
registrant: account,
});
RegisteredRetirementSignals::<T>::set(signal_id, Some(signal));
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(0)] pub fn revoke_retirement_signal(
origin: OriginFor<T>,
retirement_signal_id: [u8; 32],
) -> DispatchResult {
let account = ensure_signed(origin)?;
let Some(registered_signal) = RegisteredRetirementSignals::<T>::get(retirement_signal_id)
else {
return Err::<(), _>(Error::<T>::NonExistentRetirementSignal.into());
};
if account != registered_signal.registrant {
Err::<(), _>(Error::<T>::NotRetirementSignalRegistrant)?;
}
RegisteredRetirementSignals::<T>::remove(retirement_signal_id);
if LockedInRetirement::<T>::get().map(|(signal_id, _block_number)| signal_id) ==
Some(retirement_signal_id)
{
LockedInRetirement::<T>::kill();
}
Self::deposit_event(Event::<T>::RetirementSignalRevoked { signal_id: retirement_signal_id });
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight(0)] pub fn favor(
origin: OriginFor<T>,
signal_id: SignalId,
for_network: NetworkId,
) -> DispatchResult {
let account = ensure_signed(origin)?;
if let SignalId::Retirement(signal_id) = signal_id {
if LockedInRetirement::<T>::exists() {
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
}
let Some(registered_signal) = RegisteredRetirementSignals::<T>::get(signal_id) else {
return Err::<(), _>(Error::<T>::NonExistentRetirementSignal.into());
};
if (registered_signal.registered_at + T::RetirementValidityDuration::get().into()) <
frame_system::Pallet::<T>::block_number()
{
Err::<(), _>(Error::<T>::ExpiredRetirementSignal)?;
}
}
if !VsPallet::<T>::in_latest_decided_set(for_network, account) {
Err::<(), _>(Error::<T>::NotValidator)?;
}
if !Favors::<T>::contains_key((signal_id, for_network), account) {
Favors::<T>::set((signal_id, for_network), account, Some(()));
Self::deposit_event(Event::SignalFavored { signal_id, by: account, for_network });
}
let network_in_favor = Self::tally_for_network(signal_id, for_network);
if network_in_favor {
if Self::tally_for_all_networks(signal_id) {
match signal_id {
SignalId::Retirement(signal_id) => {
LockedInRetirement::<T>::set(Some((
signal_id,
frame_system::Pallet::<T>::block_number() +
T::RetirementLockInDuration::get().into(),
)));
Self::deposit_event(Event::RetirementSignalLockedIn { signal_id });
}
SignalId::Halt(network) => {
InInstructions::<T>::halt(network)?;
}
}
}
}
Ok(())
}
#[pallet::call_index(3)]
#[pallet::weight(0)] pub fn revoke_favor(
origin: OriginFor<T>,
signal_id: SignalId,
for_network: NetworkId,
) -> DispatchResult {
if matches!(&signal_id, SignalId::Retirement(_)) && LockedInRetirement::<T>::exists() {
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
}
Self::revoke_favor_internal(ensure_signed(origin)?, signal_id, for_network)
}
#[pallet::call_index(4)]
#[pallet::weight(0)] pub fn stand_against(
origin: OriginFor<T>,
signal_id: SignalId,
for_network: NetworkId,
) -> DispatchResult {
if LockedInRetirement::<T>::exists() {
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
}
let account = ensure_signed(origin)?;
if Favors::<T>::contains_key((signal_id, for_network), account) {
Self::revoke_favor_internal(account, signal_id, for_network)?;
} else {
if let SignalId::Retirement(signal_id) = signal_id {
if RegisteredRetirementSignals::<T>::get(signal_id).is_none() {
Err::<(), _>(Error::<T>::NonExistentRetirementSignal)?;
}
}
}
Self::deposit_event(Event::<T>::AgainstSignal { signal_id, who: account, for_network });
Ok(())
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(current_number: BlockNumberFor<T>) -> Weight {
if let Some((signal, block_number)) = LockedInRetirement::<T>::get() {
if block_number == current_number {
panic!(
"locked-in signal {} has been set for too long",
sp_core::hexdisplay::HexDisplay::from(&signal),
);
}
}
Weight::zero() }
}
}
pub use pallet::*;