//! # Unit Staking Workers Pallet
//! <!-- Original author of paragraph: @matthiew23
#![cfg_attr(not(feature = "std"), no_std)]
/// Edit this file to define custom logic or remove it if it is not needed.
/// Learn more about FRAME and the core library of Substrate FRAME pallets:
/// <https://docs.substrate.io/reference/frame-pallets/>
//use traits::profile::ProfileInterface;
/// Main ideas :
///
/// You can register and stake UNIT to be a worker. You will be able to get back your staked
/// amount only once all the work you did has been checked.
///
/// For deposit / withdraw / prices, workers will submit data. Once we reach 80% of agreements,
/// based on the amount staked, bad workers will be slashed (their shares will be 0 and
/// distributed as rewards for right workers). Then, after some time, workers will penalize no
/// workers (1 % less). There is a delay between the first consensus and the second one, so
/// that every workers will have time to work.
pub use pallet::*;
mod types;
pub use types::*;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use sp_std::prelude::Vec;
use frame_support::{
pallet_prelude::*,
sp_runtime::traits::AccountIdConversion,
traits::{
fungibles,
tokens::{ fungibles::{ Inspect, Mutate }, Balance, Preservation::Expendable },
UnixTime,
},
Blake2_128Concat,
PalletId,
};
use sp_runtime::BoundedVec;
use sp_runtime::traits::{ AtLeast32BitUnsigned, CheckedAdd };
use sp_runtime::{ DispatchError, Perbill, SaturatedConversion, Saturating };
use frame_system::pallet_prelude::*;
use pallet_deposit::{ Deposit, StatCode };
use pallet_oracle::WorkersFeedData;
use pallet_vault::VaultAddressManager;
use pallet_withdraw::{ StatCode as WithdrawStatCode, Withdraw, WorkersAcceptWithdraw };
use sp_runtime::{ traits::Zero, FixedPointOperand };
use sp_runtime::traits::{ CheckedDiv, CheckedMul };
use traits::{
asset::{ AssetInterface, TokenType },
deposit::WorkersAcceptDeposit,
oracle::OracleInterface,
stakingworkers::GetAmountStaked,
subaccounts::{ AccountOrigin, SubAccounts },
};
/// Account id type alias.
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
/// Type used to represent the token decimals.
pub type Decimals = u8;
pub type DepositId = u64;
pub type WithdrawId = u64;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type to access the Assets Pallet.
type Fungibles: fungibles::Inspect<
Self::AccountId,
AssetId = Self::AssetId,
Balance = Self::AssetBalance
> + //, AssetId = u32> // Hash this
fungibles::Mutate<Self::AccountId> +
fungibles::Create<Self::AccountId> +
fungibles::roles::Inspect<Self::AccountId> +
AssetInterface<Self::AssetId, Self::AssetBalance, Self::AccountId, Decimals, TokenType>;
type AssetBalance: Balance +
FixedPointOperand +
MaxEncodedLen +
MaybeSerializeDeserialize +
TypeInfo;
/// Type to access the Oracle Pallet interface.
type OracleInterface: OracleInterface<
<Self::Fungibles as Inspect<Self::AccountId>>::AssetId,
<Self::Fungibles as Inspect<Self::AccountId>>::Balance,
Decimals
>;
type AssetId: Member +
Parameter +
Copy +
MaybeSerializeDeserialize +
MaxEncodedLen +
Default +
Zero +
From<u32>;
/// Limit of Address Legnth
#[pallet::constant]
type AddressLengthLimit: Get<u32>;
/// Vault Handler trait
type VaultManager: VaultAddressManager<Self::AssetId, Self::AccountId, Self::AssetBalance>;
// Provides actual time
type TimeProvider: UnixTime;
/// Type to access the sub account pallet.
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;
#[pallet::constant]
type ProfileStringLimit: Get<u32>;
/// Helper trait for benchmarks.
/* #[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper: ProfileInterface<Self::AccountId, Self::ProfileStringLimit>; */
// Pallet Id
#[pallet::constant]
type PalletId: Get<PalletId>;
// Id of UNIT
#[pallet::constant]
type UnitId: Get<u32>;
// To access trait of deposit
type WorkersAcceptDeposit: WorkersAcceptDeposit<
Self::AssetId,
Self::AccountId,
Self::AssetBalance,
DepositId,
Self::AddressLengthLimit,
Deposit<Self::AccountId, Self::AssetId, Self::AssetBalance, DepositId>
>;
type WorkersAcceptWithdraw: WorkersAcceptWithdraw<
Self::AssetId,
Self::AccountId,
Self::AssetBalance,
WithdrawId,
Self::AddressLengthLimit,
Withdraw<
Self::AccountId,
Self::AssetId,
Self::AssetBalance,
WithdrawId,
Self::ProfileStringLimit
>
>;
/// The percentage of total shares that a result needs to get to reach consensus.
#[pallet::constant]
type ConsensusPercentage: Get<u32>;
/// The percentage of total shares that will be slashed for been malicious.
#[pallet::constant]
type SlashPercentageMalicious: Get<u32>;
/// The percentage of total shares that will be slashed for not working.
#[pallet::constant]
type SlashPercentageInactive: Get<u32>;
/// The data key type
type OracleKey: Parameter + Member + MaxEncodedLen + MaybeSerializeDeserialize + From<u32>;
/// The data value type
type OracleValue: Parameter +
Member +
Ord +
MaxEncodedLen +
MaybeSerializeDeserialize +
AtLeast32BitUnsigned +
TryInto<<Self::Fungibles as Inspect<Self::AccountId>>::AssetId>;
/// The data supply type
type OracleSupply: Parameter +
Member +
MaxEncodedLen +
MaybeSerializeDeserialize +
AtLeast32BitUnsigned;
// Type to feed data in oracle pallet
type WorkersFeedData: WorkersFeedData<
Self::AccountId,
Self::OracleKey,
Self::OracleValue,
Self::OracleSupply
>;
}
/// Store addresses registered as workers.
/// account_id -> option<bool>
#[pallet::storage]
#[pallet::getter(fn get_registered_workers)]
pub type RegisteredWorkers<T: Config> = StorageMap<
_,
Blake2_128Concat,
AccountIdOf<T>,
WorkerInfo<T::AssetBalance, WorkerState>
>;
/// Number of shares in the pool
#[pallet::storage]
#[pallet::getter(fn pool_share)]
pub type PoolShare<T: Config> = StorageValue<_, T::AssetBalance, ValueQuery>;
// Amount of UNIT in the pool
#[pallet::storage]
#[pallet::getter(fn liquidity_pool)]
pub type LiquidityPool<T: Config> = StorageValue<_, T::AssetBalance, ValueQuery>;
// Number of registered workers
#[pallet::storage]
#[pallet::getter(fn number_of_registered_workers)]
pub type NumberOfRegisteredWorkers<T: Config> = StorageValue<_, u32, ValueQuery>;
// By deposit ID and by workers, the response for a given deposit
#[pallet::storage]
#[pallet::getter(fn response_for_deposit)]
pub(super) type WorkerDepositResponse<T: Config> = StorageMap<
_,
Blake2_128Concat,
(DepositId, T::AccountId),
DepositResponse<Responses, BoundedVec<u8, T::AddressLengthLimit>>
>;
// By withdraw ID and by workers, the response for a given withdraw.
#[pallet::storage]
#[pallet::getter(fn response_for_withdraw)]
pub(super) type WorkerWithdrawResponse<T: Config> = StorageMap<
_,
Blake2_128Concat,
(WithdrawId, T::AccountId),
WithdrawResponse<Responses, BoundedVec<u8, T::AddressLengthLimit>>
>;
// The result of a deposit after consensus is reached
#[pallet::storage]
pub(super) type DepositResultConsensus<T: Config> = StorageMap<
_,
Blake2_128Concat,
DepositId,
DepositResult<Responses, BoundedVec<u8, T::AddressLengthLimit>, T::AssetBalance>
>;
// The result for a withdraw after consensus is reached
#[pallet::storage]
pub(super) type WithdrawResultConsensus<T: Config> = StorageMap<
_,
Blake2_128Concat,
WithdrawId,
WithdrawResult<Responses, BoundedVec<u8, T::AddressLengthLimit>, T::AssetBalance>
>;
// The slash info for a deposit id and worker
#[pallet::storage]
pub(super) type SlashInfoForDeposit<T: Config> = StorageMap<
_,
Blake2_128Concat,
(DepositId, T::AccountId),
SlashInfoDeposit<T::AccountId, SlashReason, T::AssetBalance>
>;
// The slash info for a deposit id and worker
#[pallet::storage]
pub(super) type SlashInfoForWithdraw<T: Config> = StorageMap<
_,
Blake2_128Concat,
(WithdrawId, T::AccountId),
SlashInfoWithdraw<T::AccountId, SlashReason, T::AssetBalance>
>;
// By block number, value of the higher staker
#[pallet::storage]
pub(super) type ValuesbyBlockNumber<T: Config> = StorageMap<
_,
Blake2_128Concat,
BlockNumberFor<T>,
Vec<(T::OracleKey, T::OracleValue, T::OracleSupply)>,
OptionQuery
>;
// By block number, if a consensus has been reached, and if yes at which block and the result of
// the consensus (true of false)
#[pallet::storage]
pub(super) type ConsensusbyBlockNumber<T: Config> = StorageMap<
_,
Blake2_128Concat,
BlockNumberFor<T>,
(bool, BlockNumberFor<T>, bool),
OptionQuery
>;
// The higher staker
#[pallet::storage]
pub(super) type LastBlockPricesValidated<T: Config> = StorageValue<
_,
BlockNumberFor<T>,
ValueQuery
>;
// Answer of workers, by block number : accepted or rejected
#[pallet::storage]
pub(super) type AnswerbyBlockNumber<T: Config> = StorageMap<
_,
Blake2_128Concat,
(BlockNumberFor<T>, T::AccountId),
bool,
OptionQuery
>;
// The price proposer of a given block
#[pallet::storage]
#[pallet::getter(fn price_proposer)]
pub(super) type PriceProposer<T: Config> = StorageMap<
_,
Blake2_128Concat,
BlockNumberFor<T>,
T::AccountId,
OptionQuery
>;
// The slash info for a deposit id and worker
#[pallet::storage]
pub(super) type SlashInfoForPrices<T: Config> = StorageMap<
_,
Blake2_128Concat,
(BlockNumberFor<T>, T::AccountId),
SlashInfoPrices<T::AccountId, SlashReason, T::AssetBalance>
>;
// The higher staker
#[pallet::storage]
pub(super) type HigherStaker<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
// Pallets use events to inform users when important changes are made.
// https://docs.substrate.io/main-docs/build/events-errors/
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A `worker` was registered.
WorkerRegistered {
who: AccountIdOf<T>,
},
/// A `worker` was unregistered.
WorkerUnregistered {
who: AccountIdOf<T>,
},
}
// Errors inform users that something went wrong.
#[pallet::error]
pub enum Error<T> {
/// The withdraw request is not been processed
WithdrawNotExecuted,
/// No workers registered
NoWorkersRegistered,
/// Field has not been initialized
UnregisterAtNotFound,
/// Workers have to wait 2 hours after unregistration before reclaiming their tokens
ReclaimPeriodNotOver,
/// Worker state is not set to `Disable`
WorkerNotDisable,
/// The slashing stage is not open yet for this deposit.
CannotSlashYet,
/// The worker was already punished
AlreadySlashed,
/// Cannot slash the worker because voted correctly.
WorkerVotedCorrectly,
/// Deposit result not found
ResultNotFound,
/// Worker didn't had to vote for the given deposit.
DidNotVote,
/// Cannot vote to reject the deposit yet.
CannotReject,
/// There are missing votes to reach the consensus
MissingVotes,
/// Consensus not reached for a tx hash
ConsensusForHashNotReached,
/// The deposit was already processed
DepositAlreadyProcessed,
/// The worker is inactive
WorkerInactive,
/// You need to provide a tx hash to accept a deposit
InvalidHash,
/// The worker has been registered after the deposit was created
CannotWork,
/// Voting period finished
VotingExpired,
/// Workers is already registerd
WorkerAlreadyRegistered,
/// Workers has already worked
AlreadyAnswered,
/// Workers is not registerd
WorkerNotRegistered,
/// Wait that all your work have been approved before getting your staking amount,
HavetoWaitValidationOfDeposit,
// Too small amount
Underflow,
/// Too big amount
Overflow,
/// Invalid block number
InvalidBlockNumber,
/// Only the higher staker should be able to submit prices
NotHighestShareholder,
/// Consensus not reach for prices submission
ConsensusNotReached,
/// No values got to send to the oracle pallet
NoValues,
/// Need to wait 3 blocks before slashing
NeedToWaitBeforeSlashingPrices,
/// Consensus has not been reached
NotConsensus,
/// Proposer of specific block was not found
ProposerNotFound,
/// Consensys already reach
ConsensusAlreadyReached,
}
// Dispatchable functions allows users to interact with the pallet and invoke state changes.
// These functions materialize as "extrinsics", which are often compared to transactions.
// Dispatchable functions must be annotated with a weight and must return a DispatchResult.
#[pallet::call]
impl<T: Config> Pallet<T> {
/// The origin can register as a worker.
///
/// Emits `WorkerRegistered` event when successful.
#[pallet::call_index(0)]
#[pallet::weight({ 0 })]
pub fn register_worker(
origin: OriginFor<T>,
stakedamount: T::AssetBalance
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
// Get the ID if the pool, how much UNIT are in the pool currently and how many shares
// we have in the pool.
let pool_id = Self::account_id();
let current_pool = LiquidityPool::<T>::get();
let pool_shares = PoolShare::<T>::get();
// Calculation of the share the user will receive once registered
let receiving_share_count = if
current_pool == (0u128).saturated_into::<T::AssetBalance>()
{
stakedamount
} else {
stakedamount
.checked_mul(&pool_shares)
.ok_or(Error::<T>::Overflow)?
.checked_div(¤t_pool)
.ok_or(Error::<T>::Underflow)?
};
// Transfer of UNIT from the user to the pool.
T::Fungibles::transfer(
T::UnitId::get().into(),
&who,
&pool_id,
stakedamount,
Expendable
)?;
// Update total shares
PoolShare::<T>::try_mutate(
|current_shares| -> Result<(), DispatchError> {
*current_shares = current_shares.saturating_add(receiving_share_count);
Ok(())
}
)?;
let current_block = <frame_system::Pallet<T>>::block_number();
// Update registered workers
RegisteredWorkers::<T>::try_mutate(&who, |worker_info| -> DispatchResult {
if worker_info.is_some() {
Err(Error::<T>::WorkerAlreadyRegistered.into())
} else {
*worker_info = Some(WorkerInfo {
registered_at: current_block.saturated_into::<u64>(),
shares: receiving_share_count,
state: WorkerState::Active,
unregister_at: None,
was_slashed: WasSlashed::No,
});
Ok(())
}
})?;
// When the first worker register, we get the pool id of withdraw and deposit.
if NumberOfRegisteredWorkers::<T>::get() == 0 {
T::WorkersAcceptWithdraw::get_pool_id_worker(pool_id.clone())?;
T::WorkersAcceptDeposit::get_pool_id_worker(pool_id)?;
}
NumberOfRegisteredWorkers::<T>::mutate(|count| {
*count = count.saturating_add(1);
});
// Update the liquidity pool with the staked amount
LiquidityPool::<T>::put(current_pool.saturating_add(stakedamount));
Self::update_higher_staker();
Self::deposit_event(Event::WorkerRegistered { who });
Ok(())
}
/// The origin can unregister as a worker iff they are currently registered.
///
/// Only users that have `active` state can call this extrinsic
/// User state will be switched to `Disable` making them eligible to
/// request their funds back and prevent them the access to vote
///
/// Emits `WorkerUnregistered` event when successful.
#[pallet::call_index(1)]
#[pallet::weight({ 0 })]
pub fn unregister_worker(origin: OriginFor<T>) -> DispatchResult {
let mut worker = ensure_signed(origin)?;
worker = T::SubAccounts::get_main_account(worker)?;
ensure!(Self::is_worker_active(&worker), Error::<T>::WorkerNotRegistered);
// Update worker state to `Disable` and `unregister_at` field to current block timestamp
let current_block = <frame_system::Pallet<T>>::block_number();
RegisteredWorkers::<T>::try_mutate(&worker, |worker_info| -> DispatchResult {
if let Some(worker_info) = worker_info {
worker_info.state = WorkerState::Disable;
worker_info.unregister_at = Some(current_block.saturated_into::<u64>());
return Ok(());
}
Ok(())
})?;
// Decrease the number of registered workers
NumberOfRegisteredWorkers::<T>::mutate(|count| {
*count = count.saturating_sub(1);
});
// We update the storage ; now the user just need to wait to retrieve his staking that
// his work was validated and then call the get_back_staking_after_unregister extrinsic.
// This allows to set a delay while waiting everything has been handled.
if HigherStaker::<T>::get() == Some(worker.clone()) {
Self::update_higher_staker();
}
Self::deposit_event(Event::WorkerUnregistered { who: worker });
Ok(())
}
/// The origin can increase their shares (voting power) by staking more Unit.
///
/// Only users that have `active` state can call this extrinsic.
///
/// Parameters:
/// - `amount_to_stake`: The amount of Unit that the user will lock to get shares for
/// voting.
///
/// TODO emit event
#[pallet::call_index(2)]
#[pallet::weight({ 0 })]
pub fn increase_shares(
origin: OriginFor<T>,
amount_to_stake: T::AssetBalance
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
who = T::SubAccounts::get_main_account(who)?;
ensure!(Self::is_worker_active(&who), Error::<T>::WorkerNotRegistered);
// Transfer staking amount
let pool_id = Self::account_id();
T::Fungibles::transfer(
T::UnitId::get().into(),
&who,
&pool_id,
amount_to_stake,
Expendable
)?;
// Calculate and increase shares
let current_pool = LiquidityPool::<T>::get();
let additional_shares = amount_to_stake
.checked_mul(&Self::pool_share())
.ok_or(Error::<T>::Overflow)?
.checked_div(¤t_pool)
.ok_or(Error::<T>::Underflow)?;
// Update total shares
PoolShare::<T>::try_mutate(
|current_shares| -> Result<(), DispatchError> {
*current_shares = current_shares.saturating_add(additional_shares);
Ok(())
}
)?;
// Update worker's shares
RegisteredWorkers::<T>::try_mutate(&who, |worker_info| -> DispatchResult {
if let Some(worker_info) = worker_info {
worker_info.shares = worker_info.shares.saturating_add(additional_shares);
return Ok(());
}
Ok(())
})?;
// Update the liquidity pool with the staked amount
LiquidityPool::<T>::put(current_pool.saturating_add(amount_to_stake));
Self::update_higher_staker();
Ok(())
}
/// The origin can get back funds iff their state is `Disable`
#[pallet::call_index(3)]
#[pallet::weight({ 0 })]
pub fn get_back_staking_after_unregister(origin: OriginFor<T>) -> DispatchResult {
let mut worker = ensure_signed(origin)?;
worker = T::SubAccounts::get_main_account(worker)?;
// We check the worker state is disable
ensure!(Self::is_worker_disable(&worker), Error::<T>::WorkerNotDisable);
let disable_time_period = 1200u64;
// Need to handle specific case where you can not retrieve your funds if a consensus was
// reach but "no workers" were not punished
let worker_info = RegisteredWorkers::<T>
::get(&worker)
.ok_or(Error::<T>::WorkerNotRegistered)?;
let worker_unregister_at = worker_info.unregister_at.ok_or(
Error::<T>::WorkerNotDisable
)?;
let block_number = <frame_system::Pallet<T>>::block_number().saturated_into::<u64>();
if block_number - worker_unregister_at < disable_time_period {
return Err(Error::<T>::ReclaimPeriodNotOver.into());
}
// Update worker shares
let current_pool = LiquidityPool::<T>::get();
let pool_shares = PoolShare::<T>::get();
let pool_id = Self::account_id();
// Calculate what the user should receive based on their shares
let receiving_amount = if current_pool != (0u128).saturated_into::<T::AssetBalance>() {
worker_info.shares
.checked_mul(¤t_pool)
.ok_or(Error::<T>::Overflow)?
.checked_div(&pool_shares)
.ok_or(Error::<T>::Underflow)?
} else {
(0u128).saturated_into::<T::AssetBalance>()
};
// Transfer amount to worker
T::Fungibles::transfer(
T::UnitId::get().into(),
&pool_id,
&worker,
receiving_amount,
Expendable
)?;
// Delete worker info storage
RegisteredWorkers::<T>::remove(&worker);
// Update the liquidity pool by reducing the worker's staked amount
LiquidityPool::<T>::try_mutate(|current_pool| -> DispatchResult {
*current_pool = current_pool.saturating_sub(receiving_amount);
Ok(())
})?;
// Update the shares pool by reducing the worker's shares
PoolShare::<T>::try_mutate(|pool_shares| -> DispatchResult {
*pool_shares = pool_shares.saturating_sub(worker_info.shares);
Ok(())
})?;
Self::deposit_event(Event::WorkerUnregistered { who: worker });
Ok(())
}
/// The origin can submit his vote for processing a deposit.
///
/// Workers can vote until 10 block has passed from reaching consensus
/// If the caller submit a tx_hash the pallet counts the vote as positive.
/// If the caller submit None as a paremeter for tx_hash his vote will be considered as
/// rejecting the deposit. Only users that have `active` state can call this extrinsic.
///
/// Parameters:
/// - `deposit_id`: The deposit for which the user is voting.
/// - `deposit_result`: Enum for the type of voting. "Accepted" or "Rejected".
/// - `tx_hash`: The hash of the tx on the external chain that validates that the transfer
/// was made to the vault.
///
/// TODO emit event
#[pallet::call_index(4)]
#[pallet::weight({ 0 })]
pub fn vote_for_deposit(
origin: OriginFor<T>,
deposit_id: DepositId,
deposit_result: Responses,
tx_hash: Option<BoundedVec<u8, T::AddressLengthLimit>>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
// Workers can answer once to each deposit
ensure!(
!WorkerDepositResponse::<T>::contains_key((&deposit_id, &who)),
Error::<T>::AlreadyAnswered
);
// Worker should be registered and active
let worker_info = RegisteredWorkers::<T>
::get(&who)
.ok_or(Error::<T>::WorkerNotRegistered)?;
ensure!(worker_info.state == WorkerState::Active, Error::<T>::WorkerInactive);
let deposit_info = T::WorkersAcceptDeposit::get_deposit_info(deposit_id.clone())?;
// The workers can only submit a response for deposits created after their registration
ensure!(worker_info.registered_at < deposit_info.created_at, Error::<T>::CannotWork);
let current_block = <frame_system::Pallet<T>>::block_number();
// Check if the worker can vote for this deposit
if let Some(x) = deposit_info.can_vote_until {
ensure!(x > current_block.saturated_into::<u64>(), Error::<T>::VotingExpired);
}
if let Responses::Accepted = deposit_result {
// Vote to accept the deposit
// Check if the tx_hash has a value
let hash = tx_hash.ok_or(Error::<T>::InvalidHash)?;
// Update 'WorkerDepositResponse'
let worker_deposit_response = DepositResponse {
response: deposit_result,
tx_hash: Some(hash.clone()),
};
WorkerDepositResponse::<T>::set(
(deposit_id, who.clone()),
Some(worker_deposit_response)
);
} else {
// Vote to reject the deposit
// Update 'WorkerDepositResponse'
let worker_deposit_response = DepositResponse {
response: deposit_result,
tx_hash: None,
};
WorkerDepositResponse::<T>::set(
(deposit_id, who.clone()),
Some(worker_deposit_response)
);
}
// TODO emit events
Ok(())
}
/// The origin can set the result of a deposit.
///
/// Workers can call this function after reaching the consensus for the deposit.
/// After setting the result, the workers that haven't voted yet will have 10 blocks to
/// submit their vote. After 10 blocks has passed from setting the result, the workers can
/// be slashed. The hash passed as a parameter is proposed as the result and the pallet
/// checks the amount of votes for this hash to validate is consensus has reached.
/// Only users that have `active` state can call this extrinsic.
///
/// Parameters:
/// - `deposit_id`: The deposit for which the user is checking whether consensus was
/// reached.
/// - `tx_hash`: The hash proposed has a result, could be None.
///
/// TODO emit event
#[pallet::call_index(5)]
#[pallet::weight({ 0 })]
pub fn set_deposit_result(
origin: OriginFor<T>,
deposit_id: DepositId,
tx_hash: Option<BoundedVec<u8, T::AddressLengthLimit>>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
ensure!(Self::is_worker_active(&who), Error::<T>::WorkerNotRegistered);
// Get the deposit info
let deposit_info = T::WorkersAcceptDeposit::get_deposit_info(deposit_id.clone())?;
// ensure that the deposit is pending
ensure!(
deposit_info.statcode == StatCode::Pending,
Error::<T>::DepositAlreadyProcessed
);
// To get all the shares participating on for this deposit we need to iterate over all
// the workers and check their state To corroborate if we reach a consensus we need to
// add all the shares that voted for the given deposit id and check
let mut votes: T::AssetBalance = Zero::zero();
let mut total_shares: T::AssetBalance = Zero::zero();
let all_workers = RegisteredWorkers::<T>::iter();
for (address, worker) in all_workers {
let mut should_have_worked = false;
// Add shares if the worker should have worked
if worker.state == WorkerState::Disable {
let unregister_at = worker.unregister_at.ok_or(
Error::<T>::UnregisterAtNotFound
)?;
if unregister_at > deposit_info.created_at {
if worker.was_slashed == WasSlashed::No {
total_shares = total_shares.saturating_add(worker.shares);
should_have_worked = true;
}
}
} else {
if worker.registered_at < deposit_info.created_at {
total_shares = total_shares.saturating_add(worker.shares);
should_have_worked = true;
}
}
// Add shares to consensus if the user vote for the given hash
if should_have_worked {
if
let Some(response) = WorkerDepositResponse::<T>::get((
&deposit_id,
&address,
))
{
if response.tx_hash == tx_hash {
votes = votes.saturating_add(worker.shares);
}
}
}
}
let shares_to_consensus =
Perbill::from_percent(T::ConsensusPercentage::get()) * total_shares;
// Check if we reach consensus to accept or reject
if shares_to_consensus <= votes {
if tx_hash.is_some() {
DepositResultConsensus::<T>::set(
deposit_id,
Some(DepositResult {
response: Responses::Accepted,
total_shares: votes,
tx_hash: tx_hash.clone(),
})
);
let hash = tx_hash.ok_or(Error::<T>::InvalidHash)?;
// Call to mint the tokens
T::WorkersAcceptDeposit::accept_deposit(deposit_id, hash)?;
} else {
DepositResultConsensus::<T>::set(
deposit_id,
Some(DepositResult {
response: Responses::Rejected,
total_shares: votes,
tx_hash: None,
})
);
T::WorkersAcceptDeposit::decline_deposit(deposit_id)?;
}
} else {
return Err(Error::<T>::MissingVotes.into());
}
Ok(())
}
//// The origin can slash a worker if possible.
///
/// Workers can call this function after 10 blocks has passed from reaching the consensus
/// for the deposit. The functions checks if the malicious worker should have worked for the
/// given deposit, and if so, it compares his vote with the result and if there is any
/// difference he is slashed. If worker is malicious, 50% of it's shares are slashed.
/// If worker didn't vote, 5% of it's shares are slashed
/// Only users that have `active` state can call this extrinsic.
///
/// Parameters:
/// - `deposit_id`: The deposit for which the user is checking if the malicious user voted.
/// - `malicious`: The user that acted against the consensus.
///
/// TODO emit event
#[pallet::call_index(6)]
#[pallet::weight({ 0 })]
pub fn slash_worker_for_deposit(
origin: OriginFor<T>,
deposit_id: DepositId,
malicious: T::AccountId
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
// Caller should be registered and active
ensure!(Self::is_worker_active(&who), Error::<T>::WorkerNotRegistered);
ensure!(
!SlashInfoForDeposit::<T>::contains_key((&deposit_id, &malicious)),
Error::<T>::AlreadySlashed
);
let deposit_result = DepositResultConsensus::<T>
::get(deposit_id)
.ok_or(Error::<T>::ResultNotFound)?;
// Worker should be registered and active
let mut worker_info = RegisteredWorkers::<T>
::get(&malicious)
.ok_or(Error::<T>::WorkerNotRegistered)?;
// We should not slash someone that was already slashed
ensure!(worker_info.was_slashed == WasSlashed::No, Error::<T>::CannotSlashYet);
// Get the deposit info
let deposit_info = T::WorkersAcceptDeposit::get_deposit_info(deposit_id.clone())?;
// Check if we are in the slashing stage
let current_block = <frame_system::Pallet<T>>::block_number();
// Get the block after which the workers can be slashed
let starting_block_slash_period = deposit_info.can_vote_until.ok_or(
Error::<T>::ResultNotFound
)?;
ensure!(
current_block.saturated_into::<u64>() > starting_block_slash_period,
Error::<T>::CannotSlashYet
);
// If the worker was registered before a deposit was created, he should vote.
// If the worker was unregistered before a deposit was create, he shouldn't vote.
if let Some(x) = worker_info.unregister_at {
ensure!(x > deposit_info.created_at, Error::<T>::DidNotVote);
}
let slash_amount: T::AssetBalance;
// Get the worker response
let worker_response = WorkerDepositResponse::<T>::get((&deposit_id, &malicious));
if let Some(x) = worker_response {
slash_amount =
Perbill::from_percent(T::SlashPercentageMalicious::get()) * worker_info.shares;
// Check if the response is the same as the voted result
if x.response != deposit_result.response || x.tx_hash != deposit_result.tx_hash {
worker_info.shares = worker_info.shares.saturating_sub(slash_amount);
worker_info.state = WorkerState::Disable;
let current_block = <frame_system::Pallet<T>>::block_number();
worker_info.unregister_at = Some(current_block.saturated_into::<u64>());
worker_info.was_slashed = WasSlashed::Yes;
RegisteredWorkers::<T>::insert(&malicious, worker_info);
SlashInfoForDeposit::<T>::insert((&deposit_id, &malicious), SlashInfoDeposit {
worker: malicious.clone(),
reason: SlashReason::DifferentResponse,
shares_slashed: slash_amount,
deposit_id,
});
} else {
return Err(Error::<T>::WorkerVotedCorrectly.into());
}
} else {
ensure!(
worker_info.registered_at < deposit_info.created_at,
Error::<T>::DidNotVote
);
// If the respone is null after the previous checks, the worker can be slashed
// for not participating
slash_amount =
Perbill::from_percent(T::SlashPercentageInactive::get()) * worker_info.shares;
worker_info.shares = worker_info.shares.saturating_sub(slash_amount);
worker_info.state = WorkerState::Disable;
let current_block = <frame_system::Pallet<T>>::block_number();
worker_info.unregister_at = Some(current_block.saturated_into::<u64>());
worker_info.was_slashed = WasSlashed::Yes;
RegisteredWorkers::<T>::insert(&malicious, worker_info);
SlashInfoForDeposit::<T>::insert((deposit_id, &malicious), SlashInfoDeposit {
worker: malicious.clone(),
reason: SlashReason::MissingResponse,
shares_slashed: slash_amount,
deposit_id,
});
}
// Decrease the number of registered workers
NumberOfRegisteredWorkers::<T>::mutate(|count| {
*count = count.saturating_sub(1);
});
PoolShare::<T>::mutate(|total_shares| {
*total_shares = total_shares.saturating_sub(slash_amount);
});
if HigherStaker::<T>::get() == Some(malicious.clone()) {
Self::update_higher_staker();
}
// TODO emit events
Ok(())
}
/// The origin can submit his vote for processing a withdraw.
///
/// Workers can vote until 10 block has passed from reaching consensus.
/// If the caller submit a tx_hash the pallet counts the vote as positive.
/// If the caller submit None as a paremeter for tx_hash his vote will be considered as
/// rejecting the withdraw. Only users that have `active` state can call this extrinsic.
///
/// Parameters:
/// - `withdraw_id`: The withdraw for which the user is voting.
/// - `tx_hash`: The hash of the tx on the external chain that validates that the transfer
/// was made to the user.
///
/// TODO emit event
#[pallet::call_index(7)]
#[pallet::weight({ 0 })]
pub fn vote_for_withdraw(
origin: OriginFor<T>,
withdraw_id: WithdrawId,
tx_hash: Option<BoundedVec<u8, T::AddressLengthLimit>>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
// Workers can answer once to each withdrawal
ensure!(
!WorkerWithdrawResponse::<T>::contains_key((&withdraw_id, &who)),
Error::<T>::AlreadyAnswered
);
// Worker should be registered and active
let worker_info = RegisteredWorkers::<T>
::get(&who)
.ok_or(Error::<T>::WorkerNotRegistered)?;
ensure!(worker_info.state == WorkerState::Active, Error::<T>::WorkerInactive);
let withdraw_info = T::WorkersAcceptWithdraw::get_withdraw_info(withdraw_id.clone())?;
// The workers can only submit a response for withdraws created after their registration
ensure!(worker_info.registered_at < withdraw_info.created_at, Error::<T>::CannotWork);
let current_block = <frame_system::Pallet<T>>::block_number();
// Check if the worker can vote for this withdraw depending if we reached the consensus
if let Some(x) = withdraw_info.can_vote_until {
ensure!(x > current_block.saturated_into::<u64>(), Error::<T>::VotingExpired);
}
// Vote for the withdraw
if let Some(_) = tx_hash.clone() {
// If the worker passed a tx hash as a paremeter it means that he accepted the
// withdraw
// Update 'WorkerWithdrawResponse'
let worker_withdraw_response = WithdrawResponse {
response: Responses::Accepted,
tx_hash,
};
WorkerWithdrawResponse::<T>::set(
(withdraw_id, who.clone()),
Some(worker_withdraw_response)
);
} else {
// Vote to reject the withdraw
// Update 'WorkerWithdrawResponse'
let worker_withdraw_response = WithdrawResponse {
response: Responses::Rejected,
tx_hash: None,
};
WorkerWithdrawResponse::<T>::set(
(withdraw_id, who.clone()),
Some(worker_withdraw_response)
);
}
// TODO emit events
Ok(())
}
/// The origin can set the result of a withdraw.
///
/// Workers can call this function after reaching the consensus for the withdraw.
/// After setting the result, the workers that haven't voted yet will have 10 blocks to
/// submit their vote. After 10 blocks has passed from setting the result, the workers can
/// be slashed. The hash passed as a parameter is proposed as the result and the pallet
/// checks the amount of votes for this hash to validate is consensus has reached.
/// Only users that have `active` state can call this extrinsic.
///
/// Parameters:
/// - `withdraw_id`: The withdraw for which the user is checking whether the consensus was
/// reached.
/// - `tx_hash`: The hash proposed has a result, could be None.
///
/// TODO emit event
#[pallet::call_index(8)]
#[pallet::weight({ 0 })]
pub fn set_withdraw_result(
origin: OriginFor<T>,
withdraw_id: WithdrawId,
tx_hash: Option<BoundedVec<u8, T::AddressLengthLimit>>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
ensure!(Self::is_worker_active(&who), Error::<T>::WorkerNotRegistered);
// Get the deposit info
let withdraw_info = T::WorkersAcceptWithdraw::get_withdraw_info(withdraw_id.clone())?;
// ensure that the deposit is pending
ensure!(
withdraw_info.statcode == WithdrawStatCode::Executing,
Error::<T>::WithdrawNotExecuted
);
// To get all the shares participating for this withdraw we need to iterate over all
// the workers and check their state to corroborate.
// If we reach a consensus we need to add all the shares that voted for the given
// withdraw id and check if more than 80% of shares voted for the same result.
let mut votes: T::AssetBalance = Zero::zero();
let mut total_shares: T::AssetBalance = Zero::zero();
let all_workers = RegisteredWorkers::<T>::iter();
for (address, worker) in all_workers {
let mut should_have_worked = false;
// Add shares if the worker should have worked
if worker.state == WorkerState::Disable {
let unregister_at = worker.unregister_at.ok_or(
Error::<T>::UnregisterAtNotFound
)?;
if unregister_at > withdraw_info.created_at {
if worker.was_slashed == WasSlashed::No {
total_shares = total_shares.saturating_add(worker.shares);
should_have_worked = true;
}
}
} else {
if worker.registered_at < withdraw_info.created_at {
total_shares = total_shares.saturating_add(worker.shares);
should_have_worked = true;
}
}
// Add shares to consensus if the user vote for the given hash
if should_have_worked {
if
let Some(response) = WorkerWithdrawResponse::<T>::get((
&withdraw_id,
&address,
))
{
if response.tx_hash == tx_hash {
votes = votes.saturating_add(worker.shares);
}
}
}
}
let shares_to_consensus =
Perbill::from_percent(T::ConsensusPercentage::get()) * total_shares;
// Check if we reach consensus to accept or reject
if shares_to_consensus <= votes {
if tx_hash.is_some() {
WithdrawResultConsensus::<T>::set(
withdraw_id,
Some(WithdrawResult {
response: Responses::Accepted,
total_shares: votes,
tx_hash: tx_hash.clone(),
})
);
let hash = tx_hash.ok_or(Error::<T>::InvalidHash)?;
// Call to mint the tokens
T::WorkersAcceptWithdraw::accept_withdraw(withdraw_id, hash)?;
// Update the liquidity pool based on rewards sent by the withdraw pallet (which
// were in the pool but not in the storage of the liquidity pool)
let current_pool = LiquidityPool::<T>::get();
let pool_id = Self::account_id();
let amount_to_add =
T::Fungibles::balance(T::UnitId::get().into(), &pool_id) - current_pool;
match current_pool.checked_add(&amount_to_add) {
Some(new_pool_value) => LiquidityPool::<T>::put(new_pool_value),
None => {
return Err(Error::<T>::Overflow.into());
}
}
} else {
WithdrawResultConsensus::<T>::set(
withdraw_id,
Some(WithdrawResult {
response: Responses::Rejected,
total_shares: votes,
tx_hash: None,
})
);
T::WorkersAcceptWithdraw::decline_withdraw(withdraw_id)?;
}
} else {
return Err(Error::<T>::MissingVotes.into());
}
Ok(())
}
//// The origin can slash a worker if possible.
///
/// Workers can call this function after 10 blocks has passed from reaching the consensus
/// for the withdraw. The functions checks if the malicious worker should have worked for
/// the given withdraw, and if so, it compares his vote with the result and if there is any
/// difference he is slashed. If worker is malicious, 50% of it's shares are slashed.
/// If worker didn't vote, 5% of it's shares are slashed
/// Only users that have `active` state can call this extrinsic.
///
/// Parameters:
/// - `withdraw_id`: The withdraw for which the user is checking if the malicious user
/// voted.
/// - `malicious`: The user that acted against the consensus.
///
/// TODO emit event
#[pallet::call_index(9)]
#[pallet::weight({ 0 })]
pub fn slash_worker_for_withdraw(
origin: OriginFor<T>,
withdraw_id: WithdrawId,
malicious: T::AccountId
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
// Caller should be registered and active
ensure!(Self::is_worker_active(&who), Error::<T>::WorkerNotRegistered);
ensure!(
!SlashInfoForWithdraw::<T>::contains_key((&withdraw_id, &malicious)),
Error::<T>::AlreadySlashed
);
let withdraw_result = WithdrawResultConsensus::<T>
::get(withdraw_id)
.ok_or(Error::<T>::ResultNotFound)?;
// Worker should be registered and active
let mut worker_info = RegisteredWorkers::<T>
::get(&malicious)
.ok_or(Error::<T>::WorkerNotRegistered)?;
// We should not slash someone that was already slashed
ensure!(worker_info.was_slashed == WasSlashed::No, Error::<T>::CannotSlashYet);
// Get the deposit info
let withdraw_info = T::WorkersAcceptWithdraw::get_withdraw_info(withdraw_id.clone())?;
// Check if we are in the slashing stage
let current_block = <frame_system::Pallet<T>>::block_number();
// Get the block after which the workers can be slashed
let starting_block_slash_period = withdraw_info.can_vote_until.ok_or(
Error::<T>::ResultNotFound
)?;
ensure!(
current_block.saturated_into::<u64>() > starting_block_slash_period,
Error::<T>::CannotSlashYet
);
// If the worker was registered before a deposit was created, he should vote.
// If the worker was unregistered before a deposit was create, he shouldn't vote.
if let Some(x) = worker_info.unregister_at {
ensure!(x > withdraw_info.created_at, Error::<T>::DidNotVote);
}
let slash_amount: T::AssetBalance;
// Get the worker response
let worker_response = WorkerDepositResponse::<T>::get((&withdraw_id, &malicious));
if let Some(x) = worker_response {
slash_amount =
Perbill::from_percent(T::SlashPercentageMalicious::get()) * worker_info.shares;
// Check if the response is the same as the voted result
if x.response != withdraw_result.response || x.tx_hash != withdraw_result.tx_hash {
worker_info.shares = worker_info.shares.saturating_sub(slash_amount);
worker_info.state = WorkerState::Disable;
let current_block = <frame_system::Pallet<T>>::block_number();
worker_info.unregister_at = Some(current_block.saturated_into::<u64>());
worker_info.was_slashed = WasSlashed::Yes;
RegisteredWorkers::<T>::insert(&malicious, worker_info);
SlashInfoForWithdraw::<T>::insert(
(&withdraw_id, &malicious),
SlashInfoWithdraw {
worker: malicious.clone(),
reason: SlashReason::DifferentResponse,
shares_slashed: slash_amount,
withdraw_id,
}
);
} else {
return Err(Error::<T>::WorkerVotedCorrectly.into());
}
} else {
ensure!(
worker_info.registered_at < withdraw_info.created_at,
Error::<T>::DidNotVote
);
// If the respone is null after the previous checks, the worker can be slashed
// for not participating
slash_amount =
Perbill::from_percent(T::SlashPercentageInactive::get()) * worker_info.shares;
worker_info.shares = worker_info.shares.saturating_sub(slash_amount);
worker_info.state = WorkerState::Disable;
let current_block = <frame_system::Pallet<T>>::block_number();
worker_info.unregister_at = Some(current_block.saturated_into::<u64>());
worker_info.was_slashed = WasSlashed::Yes;
RegisteredWorkers::<T>::insert(&malicious, worker_info);
SlashInfoForWithdraw::<T>::insert((withdraw_id, &malicious), SlashInfoWithdraw {
worker: malicious.clone(),
reason: SlashReason::MissingResponse,
shares_slashed: slash_amount,
withdraw_id,
});
}
// Decrease the number of registered workers
NumberOfRegisteredWorkers::<T>::mutate(|count| {
*count = count.saturating_sub(1);
});
PoolShare::<T>::mutate(|total_shares| {
*total_shares = total_shares.saturating_sub(slash_amount);
});
if HigherStaker::<T>::get() == Some(malicious.clone()) {
Self::update_higher_staker();
}
// TODO emit events
Ok(())
}
// Only the higher staker can submit prices. The workers have to check within the
// UserPoolShares if they are this worker. They need to ensure that the higher staker is not
// waiting his funds orelse he should not be the higher staker
#[pallet::call_index(10)]
#[pallet::weight({ 0 })]
pub fn propose_prices(
origin: OriginFor<T>,
values: Vec<(T::OracleKey, T::OracleValue, T::OracleSupply)>,
time: BlockNumberFor<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
// Ensure block number is divisible by 4
ensure!(time % (4u32).into() == (0u32).into(), Error::<T>::InvalidBlockNumber);
// let current_block = <frame_system::Pallet<T>>::block_number();
// let block_difference = current_block.saturating_sub(time);
// ensure!(
// block_difference == (1u32).into() || block_difference == (2u32).into(),
// Error::<T>::InvalidBlockNumber
// );
let worker_info = RegisteredWorkers::<T>
::get(&who)
.ok_or(Error::<T>::WorkerNotRegistered)?;
ensure!(worker_info.state == WorkerState::Active, Error::<T>::WorkerInactive);
// Ensure caller has the highest shares in UserPoolShares. We do not take into account
// the one that is waiting for his funds.
let highest_shares_owner = HigherStaker::<T>::get();
let worker_account_id = Some(who.clone());
ensure!(worker_account_id == highest_shares_owner, Error::<T>::NotHighestShareholder);
// Update storage to express work done
ValuesbyBlockNumber::<T>::try_mutate(time, |work_option| -> DispatchResult {
match work_option {
Some(_) => Err(Error::<T>::AlreadyAnswered.into()),
None => {
*work_option = Some(values);
Ok(())
}
}
})?;
// Express the work for the higher staker
AnswerbyBlockNumber::<T>::try_mutate(
&(time, who.clone()),
|answer_option| -> DispatchResult {
match answer_option {
Some(_) => Err(Error::<T>::AlreadyAnswered.into()),
None => {
*answer_option = Some(true);
Ok(())
}
}
}
)?;
// Update price proposer
PriceProposer::<T>::set(time, Some(who.clone()));
Ok(())
}
#[pallet::call_index(11)]
#[pallet::weight({ 0 })]
pub fn give_agreement(
origin: OriginFor<T>,
time: BlockNumberFor<T>,
answer: bool
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
// Ensure block number is divisible by 4
ensure!(time % (4u32).into() == (0u32).into(), Error::<T>::InvalidBlockNumber);
let worker_info = RegisteredWorkers::<T>
::get(&who)
.ok_or(Error::<T>::WorkerNotRegistered)?;
ensure!(worker_info.state == WorkerState::Active, Error::<T>::WorkerInactive);
// Give answer
AnswerbyBlockNumber::<T>::try_mutate(
&(time, who.clone()),
|work_option| -> DispatchResult {
match work_option {
Some(_) => Err(Error::<T>::AlreadyAnswered.into()),
None => {
*work_option = Some(answer);
Ok(())
}
}
}
)?;
Ok(())
}
#[pallet::call_index(12)]
#[pallet::weight({ 0 })]
pub fn request_consensus_prices(
origin: OriginFor<T>,
time: BlockNumberFor<T>,
answer_consensus: bool
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
ensure!(
!ConsensusbyBlockNumber::<T>::contains_key(&time),
Error::<T>::ConsensusAlreadyReached
);
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
// Ensure block number is divisible by 4
ensure!(time % (4u32).into() == (0u32).into(), Error::<T>::InvalidBlockNumber);
ensure!(time > LastBlockPricesValidated::<T>::get(), Error::<T>::InvalidBlockNumber);
let worker_info = RegisteredWorkers::<T>
::get(&who)
.ok_or(Error::<T>::WorkerNotRegistered)?;
ensure!(worker_info.state == WorkerState::Active, Error::<T>::WorkerInactive);
// To get all the shares participating on for this deposit we need to iterate over all
// the workers and check their state To corroborate if we reach a consensus we need to
// add all the shares that voted for the given deposit id and check
let mut votes: T::AssetBalance = Zero::zero();
let mut total_shares: T::AssetBalance = Zero::zero();
let all_workers = RegisteredWorkers::<T>::iter();
for (address, worker) in all_workers {
let mut should_have_worked = false;
if worker.state == WorkerState::Disable {
let unregister_at = worker.unregister_at.ok_or(
Error::<T>::UnregisterAtNotFound
)?;
if unregister_at > time.saturated_into::<u64>() {
if worker.was_slashed == WasSlashed::No {
total_shares = total_shares.saturating_add(worker.shares);
should_have_worked = true;
}
}
} else {
if worker.registered_at < time.saturated_into::<u64>() {
total_shares = total_shares.saturating_add(worker.shares);
should_have_worked = true;
} else if let Some(_) = AnswerbyBlockNumber::<T>::get((time, &address)) {
total_shares = total_shares.saturating_add(worker.shares);
should_have_worked = true;
}
}
if should_have_worked {
if let Some(response) = AnswerbyBlockNumber::<T>::get((time, &address)) {
if response == answer_consensus {
votes = votes.saturating_add(worker.shares);
}
}
}
}
// Check if consensus shares are greater than the required consensus percentage of total
// shares
let consensus_required =
Perbill::from_percent(T::ConsensusPercentage::get()) * total_shares;
if consensus_required <= votes {
// Send right data to the oracle pallet
let values_opt = ValuesbyBlockNumber::<T>::get(time);
if let Some(values) = values_opt {
if answer_consensus {
let price_proposer = PriceProposer::<T>
::get(time)
.ok_or(Error::<T>::ProposerNotFound)?;
T::WorkersFeedData::feed_values(price_proposer, values.clone())?;
}
LastBlockPricesValidated::<T>::set(time);
ConsensusbyBlockNumber::<T>::insert(time, (
true,
<frame_system::Pallet<T>>::block_number(),
answer_consensus,
));
} else {
if answer_consensus == false {
LastBlockPricesValidated::<T>::set(time);
ConsensusbyBlockNumber::<T>::insert(time, (
true,
<frame_system::Pallet<T>>::block_number(),
false,
));
}
}
} else {
return Err(Error::<T>::ConsensusNotReached.into());
}
Ok(())
}
// After few blocks (time to let workers work at least 3), we check who has not worked
#[pallet::call_index(13)]
#[pallet::weight({ 0 })]
pub fn manage_slashing_prices(
origin: OriginFor<T>,
time: BlockNumberFor<T>,
malicious: T::AccountId
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account
who = T::SubAccounts::get_main_account(who)?;
// Caller should be registered and active
ensure!(Self::is_worker_active(&who), Error::<T>::WorkerNotRegistered);
ensure!(
!SlashInfoForPrices::<T>::contains_key((&time, &malicious)),
Error::<T>::AlreadySlashed
);
// Ensure block number is divisible by 4
ensure!(time % (4u32).into() == (0u32).into(), Error::<T>::InvalidBlockNumber);
// Ensure the consensus has been reached and that we let enough time for workers to work
// (5 blocks)
// Worker should be registered and active
let mut worker_info = RegisteredWorkers::<T>
::get(&malicious)
.ok_or(Error::<T>::WorkerNotRegistered)?;
// We should not slash someone that was already slashed
ensure!(worker_info.was_slashed == WasSlashed::No, Error::<T>::CannotSlashYet);
// Get current block number
let current_block = <frame_system::Pallet<T>>::block_number();
// Retrieve the consensus status and the associated block number
let (consensus_reached, consensus_block, answer_from_consensus) =
ConsensusbyBlockNumber::<T>::get(time).ok_or(Error::<T>::NotConsensus)?;
// Ensure the consensus has been reached for the block number and check block difference
ensure!(
consensus_reached && current_block - consensus_block > (5u32).into(),
Error::<T>::NotConsensus
);
if let Some(unregister_at) = worker_info.unregister_at {
ensure!(
unregister_at < consensus_block.saturated_into::<u64>(),
Error::<T>::DidNotVote
);
}
let slash_amount: T::AssetBalance;
let worker_response = AnswerbyBlockNumber::<T>::get((time, &malicious));
// Get the worker response
if let Some(x) = worker_response {
slash_amount =
Perbill::from_percent(T::SlashPercentageMalicious::get()) * worker_info.shares;
// Check if the response is the same as the voted result
if x != answer_from_consensus {
worker_info.shares = worker_info.shares.saturating_sub(slash_amount);
worker_info.state = WorkerState::Disable;
let current_block = <frame_system::Pallet<T>>::block_number();
worker_info.unregister_at = Some(current_block.saturated_into::<u64>());
worker_info.was_slashed = WasSlashed::Yes;
RegisteredWorkers::<T>::insert(&malicious, worker_info);
SlashInfoForPrices::<T>::insert((&time, &malicious), SlashInfoPrices {
worker: malicious.clone(),
reason: SlashReason::DifferentResponse,
shares_slashed: slash_amount,
blocknumber: Some(time.saturated_into()),
});
} else {
return Err(Error::<T>::WorkerVotedCorrectly.into());
}
} else {
// If the respone is null after the previous checks, the worker can be slashed
// for not participating
ensure!(
worker_info.registered_at < time.saturated_into::<u64>(),
Error::<T>::DidNotVote
);
slash_amount =
Perbill::from_percent(T::SlashPercentageInactive::get()) * worker_info.shares;
worker_info.shares = worker_info.shares.saturating_sub(slash_amount);
worker_info.state = WorkerState::Disable;
let current_block = <frame_system::Pallet<T>>::block_number();
worker_info.unregister_at = Some(current_block.saturated_into::<u64>());
worker_info.was_slashed = WasSlashed::Yes;
RegisteredWorkers::<T>::insert(&malicious, worker_info);
SlashInfoForPrices::<T>::insert((time, &malicious), SlashInfoPrices {
worker: malicious.clone(),
reason: SlashReason::MissingResponse,
shares_slashed: slash_amount,
blocknumber: Some(time.saturated_into()),
});
}
// Decrease the number of registered workers
NumberOfRegisteredWorkers::<T>::mutate(|count| {
*count = count.saturating_sub(1);
});
PoolShare::<T>::mutate(|total_shares| {
*total_shares = total_shares.saturating_sub(slash_amount);
});
if HigherStaker::<T>::get() == Some(malicious.clone()) {
Self::update_higher_staker();
}
// TODO emit events
Ok(())
}
}
impl<T: Config> Pallet<T> {
pub fn u32_to_asset_balance(input: u32) -> T::AssetBalance {
input.into()
}
pub fn asset_balance_to_u32(input: T::AssetBalance) -> u32 {
input.saturated_into::<u32>()
}
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
pub fn is_worker_active(account: &T::AccountId) -> bool {
// Get worker info associated with the given `account`
if let Some(worker_info) = RegisteredWorkers::<T>::get(account) {
// Compare the state of the worker with `WorkerState::Active`
worker_info.state == WorkerState::Active
} else {
// Handle case when worker is not registered
false
}
}
pub fn is_worker_disable(account: &T::AccountId) -> bool {
// Get worker info associated with the given `account`
if let Some(worker_info) = RegisteredWorkers::<T>::get(account) {
// Compare the state of the worker with `WorkerState::Disable`
worker_info.state == WorkerState::Disable
} else {
// Handle case when worker is not registered
false
}
}
pub fn update_higher_staker() {
let highest_shares_owner = RegisteredWorkers::<T>
::iter()
.filter_map(|(account_id, worker_info)| {
if
worker_info.state == WorkerState::Active &&
worker_info.was_slashed == WasSlashed::No
{
Some((account_id, worker_info.shares))
} else {
None
}
})
.max_by_key(|&(_, shares)| shares)
.map(|(account_id, _)| account_id);
if let Some(owner) = highest_shares_owner {
HigherStaker::<T>::put(owner);
}
}
}
impl<T: Config> GetAmountStaked<T::AssetBalance> for Pallet<T> {
fn get_amount_pool() -> T::AssetBalance {
LiquidityPool::<T>::get()
}
fn get_total_shares() -> T::AssetBalance {
PoolShare::<T>::get()
}
}
}