Skip to content

Commit

Permalink
Seventh iteration of the Court pallet
Browse files Browse the repository at this point in the history
  • Loading branch information
c410-f3r committed Aug 31, 2021
1 parent 17d3de8 commit 7b1ac20
Show file tree
Hide file tree
Showing 15 changed files with 357 additions and 66 deletions.
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

0 comments on commit 7b1ac20

Please sign in to comment.