Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Seventh iteration of the Court pallet #248

Merged
merged 1 commit into from
Sep 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion primitives/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ parameter_types! {
// Court
parameter_types! {
pub const CourtCaseDuration: u64 = BLOCKS_PER_DAY;
pub const CourtPalletId: PalletId = PalletId(*b"zge/cout");
pub const StakeWeight: u128 = 2 * BASE;
}

Expand All @@ -36,7 +37,7 @@ parameter_types! {

// Liquidity Mining parameters
parameter_types! {
pub const LiquidityMiningPalletId: PalletId = PalletId(*b"zrml/lmg");
pub const LiquidityMiningPalletId: PalletId = PalletId(*b"zge/lymg");
}

// Prediction Market parameters
Expand Down
11 changes: 10 additions & 1 deletion primitives/src/outcome_report.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
use crate::types::CategoryIndex;

/// The reported outcome of a market
#[derive(Clone, Debug, Eq, PartialEq, parity_scale_codec::Decode, parity_scale_codec::Encode)]
#[derive(
Clone,
Debug,
Eq,
Ord,
PartialEq,
PartialOrd,
parity_scale_codec::Decode,
parity_scale_codec::Encode,
)]
pub enum OutcomeReport {
Categorical(CategoryIndex),
Scalar(u128),
Expand Down
6 changes: 4 additions & 2 deletions primitives/src/traits/dispute_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ pub trait DisputeApi {

/// Disputes a reported outcome.
fn on_dispute(
bond: Self::Balance,
disputes: &[MarketDispute<Self::AccountId, Self::BlockNumber>],
market_id: Self::MarketId,
market_id: &Self::MarketId,
who: &Self::AccountId,
) -> DispatchResult;

/// Manages markets resolutions moving all reported markets to resolved.
fn on_resolution<D>(
dispute_bound: &D,
bound: &D,
disputes: &[MarketDispute<Self::AccountId, Self::BlockNumber>],
market_id: &Self::MarketId,
market: &Market<Self::AccountId, Self::BlockNumber, Self::Moment>,
Expand Down
192 changes: 176 additions & 16 deletions zrml/court/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,26 @@ pub use pallet::*;
#[frame_support::pallet]
mod pallet {
use crate::{Juror, JurorStatus};
use alloc::collections::BTreeMap;
use arrayvec::ArrayVec;
use core::marker::PhantomData;
use frame_support::{
dispatch::DispatchResult,
pallet_prelude::{StorageDoubleMap, StorageMap, StorageValue, ValueQuery},
traits::{Currency, Get, Hooks, IsType, Randomness, ReservableCurrency},
Blake2_128Concat,
traits::{
BalanceStatus, Currency, Get, Hooks, IsType, NamedReservableCurrency, Randomness,
ReservableCurrency,
},
Blake2_128Concat, PalletId,
};
use frame_system::{ensure_signed, pallet_prelude::OriginFor};
use rand::{rngs::StdRng, seq::SliceRandom, RngCore, SeedableRng};
use sp_runtime::{traits::Saturating, ArithmeticError, DispatchError, SaturatedConversion};
use sp_runtime::{
traits::{AccountIdConversion, Saturating},
ArithmeticError, DispatchError, SaturatedConversion,
};
use zeitgeist_primitives::{
constants::CourtPalletId,
traits::DisputeApi,
types::{Market, MarketDispute, OutcomeReport},
};
Expand All @@ -36,9 +44,13 @@ mod pallet {
// Number of jurors for an initial market dispute
const INITIAL_JURORS_NUM: usize = 3;
const MAX_RANDOM_JURORS: usize = 13;
const RESERVE_ID: [u8; 8] = CourtPalletId::get().0;
// Weight used to increase the number of jurors for subsequent disputes
// of the same market
const SUBSEQUENT_JURORS_FACTOR: usize = 2;
// Divides the reserved juror balance to calculate the slash amount. `5` here
// means that the output value will be 20% of the dividend.
const TARDY_PUNISHMENT_DIVISOR: u8 = 5;

pub(crate) type BalanceOf<T> =
<CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::Balance;
Expand All @@ -55,7 +67,7 @@ mod pallet {
let who = ensure_signed(origin)?;
let juror = Self::juror(&who)?;
Jurors::<T>::remove(&who);
CurrencyOf::<T>::unreserve(&who, juror.staked);
CurrencyOf::<T>::unreserve_named(&RESERVE_ID, &who, juror.staked);
Ok(())
}

Expand All @@ -70,7 +82,7 @@ mod pallet {
let jurors_num = Jurors::<T>::iter().count();
let jurors_num_plus_one = jurors_num.checked_add(1).ok_or(ArithmeticError::Overflow)?;
let stake = Self::current_required_stake(jurors_num_plus_one);
CurrencyOf::<T>::reserve(&who, stake)?;
CurrencyOf::<T>::reserve_named(&RESERVE_ID, &who, stake)?;
Jurors::<T>::insert(&who, Juror { staked: stake, status: JurorStatus::Ok });
Ok(())
}
Expand All @@ -88,8 +100,8 @@ mod pallet {
return Err(Error::<T>::OnlyJurorsCanVote.into());
}
Votes::<T>::insert(
who,
market_id,
who,
(<frame_system::Pallet<T>>::block_number(), outcome),
);
Ok(())
Expand All @@ -110,11 +122,17 @@ mod pallet {
BlockNumber = Self::BlockNumber,
>;

/// Identifier of this pallet
type PalletId: Get<PalletId>;

/// Randomness source
type Random: Randomness<Self::Hash, Self::BlockNumber>;

/// Weight used to calculate the necessary staking amount to become a juror
type StakeWeight: Get<BalanceOf<Self>>;

/// Slashed funds are send to the treasury
type TreasuryId: Get<PalletId>;
}

#[pallet::error]
Expand All @@ -123,6 +141,8 @@ mod pallet {
JurorAlreadyExists,
/// An account id does not exist on the jurors storage.
JurorDoesNotExists,
/// No-one voted on an outcome to resolve a market
NoVotes,
/// Forbids voting of unknown accounts
OnlyJurorsCanVote,
}
Expand Down Expand Up @@ -174,13 +194,29 @@ mod pallet {
StdRng::from_seed(seed)
}

pub(crate) fn set_juror_as_tardy(account_id: &T::AccountId) -> DispatchResult {
Self::mutate_juror(account_id, |juror| {
juror.status = JurorStatus::Tardy;
Ok(())
})
}

pub(crate) fn treasury_account_id() -> T::AccountId {
T::TreasuryId::get().into_account()
}

// No-one can stake more than BalanceOf::<T>::max(), therefore, this function saturates
// arithmetic operations.
fn current_required_stake(jurors_num: usize) -> BalanceOf<T> {
let jurors_len: BalanceOf<T> = jurors_num.saturated_into();
T::StakeWeight::get().saturating_mul(jurors_len)
}

// Retrieves a juror from the storage
fn juror(account_id: &T::AccountId) -> Result<Juror<BalanceOf<T>>, DispatchError> {
Jurors::<T>::get(account_id).ok_or_else(|| Error::<T>::JurorDoesNotExists.into())
}

// Calculates the necessary number of jurors depending on the number of market disputes.
//
// Result is capped to `usize::MAX` or in other words, capped to a very, very, very
Expand All @@ -190,9 +226,121 @@ mod pallet {
INITIAL_JURORS_NUM.saturating_add(SUBSEQUENT_JURORS_FACTOR.saturating_mul(len))
}

// * Jurors that didn't vote within `CourtCaseDuration` or didn't vote at all are
// placed as tardy.
//
// * Slashes 20% of staked funds and removes tardy jurors that didn't vote a second time.
fn manage_tardy_jurors(
requested_jurors: &[(T::AccountId, T::BlockNumber)],
votes: &[(T::AccountId, (T::BlockNumber, OutcomeReport))],
) -> DispatchResult {
let treasury_account_id = Self::treasury_account_id();

for (ai, max_block) in requested_jurors {
if let Some((_, (block, _))) = votes.iter().find(|el| &el.0 == ai) {
if block > max_block {
Self::set_juror_as_tardy(ai)?;
}
} else {
let juror = Self::juror(ai)?;
if let JurorStatus::Tardy = juror.status {
let reserved = CurrencyOf::<T>::reserved_balance_named(&RESERVE_ID, ai);
// Division will never overflow
let slash = reserved / BalanceOf::<T>::from(TARDY_PUNISHMENT_DIVISOR);
CurrencyOf::<T>::repatriate_reserved_named(
&RESERVE_ID,
ai,
&treasury_account_id,
slash,
BalanceStatus::Free,
)?;
CurrencyOf::<T>::unreserve_named(&RESERVE_ID, ai, reserved);
Jurors::<T>::remove(ai);
} else {
Self::set_juror_as_tardy(ai)?;
}
}
}

Ok(())
}

// Retrieves a juror from the storage
fn juror(account_id: &T::AccountId) -> Result<Juror<BalanceOf<T>>, DispatchError> {
Jurors::<T>::get(account_id).ok_or_else(|| Error::<T>::JurorDoesNotExists.into())
fn mutate_juror<F>(account_id: &T::AccountId, mut cb: F) -> DispatchResult
where
F: FnMut(&mut Juror<BalanceOf<T>>) -> DispatchResult,
{
Jurors::<T>::try_mutate(account_id, |opt| {
if let Some(el) = opt {
cb(el)?;
} else {
return Err(Error::<T>::JurorDoesNotExists.into());
}
Ok(())
})
}

// Jurors are only rewarded if sided on the most voted outcome but jurors that voted
// second most voted outcome (winner of the losing majority) are placed as tardy instead
// of being slashed
fn set_jurors_that_sided_on_the_second_most_voted_outcome_as_tardy(
second_most_voted_outcome: &Option<OutcomeReport>,
votes: &[(T::AccountId, (T::BlockNumber, OutcomeReport))],
) -> DispatchResult {
if let Some(el) = second_most_voted_outcome {
for (ai, (_, outcome_report)) in votes {
if outcome_report == el {
Self::set_juror_as_tardy(ai)?;
}
}
}
Ok(())
}

// For market resolution based on the votes of a market
fn two_best_outcomes(
votes: &[(T::AccountId, (T::BlockNumber, OutcomeReport))],
) -> Result<(OutcomeReport, Option<OutcomeReport>), DispatchError> {
let mut scores = BTreeMap::<OutcomeReport, u32>::new();

for (_, (_, outcome_report)) in votes {
if let Some(el) = scores.get_mut(outcome_report) {
*el = el.saturating_add(1);
} else {
scores.insert(outcome_report.clone(), 1);
}
}

let mut best_score;
let mut iter = scores.iter();

if let Some(first) = iter.next() {
best_score = first;
} else {
return Err(Error::<T>::NoVotes.into());
}

let mut second_best_score = if let Some(second) = iter.next() {
if second.1 > best_score.1 {
best_score = second;
best_score
} else {
second
}
} else {
return Ok((best_score.0.clone(), None));
};

for el in iter {
if el.1 > best_score.1 {
best_score = el;
second_best_score = best_score;
} else if el.1 > second_best_score.1 {
second_best_score = el;
}
}

Ok((best_score.0.clone(), Some(second_best_score.0.clone())))
}
}

Expand All @@ -208,31 +356,41 @@ mod pallet {
type Origin = T::Origin;

fn on_dispute(
bond: Self::Balance,
disputes: &[MarketDispute<Self::AccountId, Self::BlockNumber>],
market_id: Self::MarketId,
market_id: &Self::MarketId,
who: &Self::AccountId,
) -> DispatchResult {
CurrencyOf::<T>::reserve(who, bond)?;
let jurors: Vec<_> = Jurors::<T>::iter().collect();
let necessary_jurors_num = Self::necessary_jurors_num(disputes);
let mut rng = Self::rng();
let random_jurors = Self::random_jurors(&jurors, necessary_jurors_num, &mut rng);
let curr_block_num = <frame_system::Pallet<T>>::block_number();
let block_limit = curr_block_num.saturating_add(T::CourtCaseDuration::get());
for (ai, _) in random_jurors {
RequestedJurors::<T>::insert(ai, market_id, block_limit);
RequestedJurors::<T>::insert(market_id, ai, block_limit);
}
Ok(())
}

fn on_resolution<D>(
_: &D,
_: &[MarketDispute<Self::AccountId, Self::BlockNumber>],
_: &Self::MarketId,
market_id: &Self::MarketId,
_: &Market<Self::AccountId, Self::BlockNumber, Self::Moment>,
) -> Result<OutcomeReport, DispatchError>
where
D: Fn(usize) -> Self::Balance,
{
Ok(OutcomeReport::Scalar(Default::default()))
let requested_jurors: Vec<_> = RequestedJurors::<T>::iter_prefix(market_id).collect();
let votes: Vec<_> = Votes::<T>::iter_prefix(market_id).collect();
let (first, second) = Self::two_best_outcomes(&votes)?;
Self::manage_tardy_jurors(&requested_jurors, &votes)?;
Self::set_jurors_that_sided_on_the_second_most_voted_outcome_as_tardy(&second, &votes)?;
Votes::<T>::remove_prefix(market_id, None);
RequestedJurors::<T>::remove_prefix(market_id, None);
Ok(first)
}
}

Expand All @@ -249,20 +407,22 @@ mod pallet {
pub type RequestedJurors<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId,
Blake2_128Concat,
MarketIdOf<T>,
Blake2_128Concat,
T::AccountId,
T::BlockNumber,
>;

/// Votes of market outcomes for disputes
///
/// Stores the vote block number and the submitted outcome.
#[pallet::storage]
pub type Votes<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
T::AccountId,
Blake2_128Concat,
MarketIdOf<T>,
Blake2_128Concat,
T::AccountId,
(T::BlockNumber, OutcomeReport),
>;
}
Loading