//! # Contest Pallet
//! <!-- Original author of paragraph: @pablolteixeira
//!
//! ## Overview
//!
//! Pallet contests enables users to create, update, close, and designate a winner if they are the
//! contest owner, or participate in a contest as a participant.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Create a contest.
//! * Update a contest.
//! * Close a contest.
//! * Assign a contest winner(a contest have more than one winner).
//! * Enter a contest.
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! `contest_new`: This function creates a new contest.
//!
//! `update_contest`: This function updates an existing contest.
//!
//! `close_contest`: This function close an existing contest.
//!
//! `assign_contest_winner`: This function assigns a contest winner who will receive a portion of
//! the total contest prize.
//!
//! `create_contest_entry`: This function allows a user to enter a contest, providing a link that
//! showcases their work as
//! an entry to compete for the contest's victory.
//!
//!//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
#![cfg_attr(not(feature = "std"), no_std)]
pub use pallet::*;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use weights::WeightInfo;
use frame_support::{
pallet_prelude::*,
sp_runtime::{
traits::{AccountIdConversion, CheckedAdd, CheckedDiv, Zero},
FixedPointOperand, SaturatedConversion,
},
traits::{
tokens::{
currency::Currency,
fungibles::{Create, Inspect, Mutate},
Balance,
Preservation::Expendable,
},
UnixTime,
},
PalletId,
};
use frame_system::pallet_prelude::*;
use traits::subaccounts::{SubAccounts, AccountOrigin};
#[cfg(feature = "runtime-benchmarks")]
use traits::profile::ProfileInterface;
#[frame_support::pallet]
pub mod pallet {
use super::*;
/// Type alias used for interaction with fungibles(assets).
pub type AssetBalanceOf<T> =
<<T as Config>::Assets as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
/// Asset id type alias.
pub type AssetIdOf<T> =
<<T as Config>::Assets as Inspect<<T as frame_system::Config>::AccountId>>::AssetId;
/// Account id type alias.
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
/// Contest id type alias.
pub type ContestId = u64;
/// Entry id type alias.
pub type EntryId = u64;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[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 Currency trait implemented by the balances pallet.
type Currency: Currency<Self::AccountId>;
/// Type to access the Assets Pallet.
type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = Self::AssetBalance>
+ Mutate<Self::AccountId>
+ Create<Self::AccountId>;
/// Type representing the AssetBalance
type AssetBalance: Balance + FixedPointOperand + Zero;
/// Type representing the AssetId
type AssetId: Member
+ Parameter
+ Copy
+ MaybeSerializeDeserialize
+ MaxEncodedLen
+ Default
+ Zero;
/// Provides an interface to get the actual time in Unix Time.
type TimeProvider: UnixTime;
/// The minimum contest end date.
#[pallet::constant]
type MinContestEndDate: Get<u64>;
/// Contest pallet id.
#[pallet::constant]
type PalletId: Get<PalletId>;
/// The maximum title length.
#[pallet::constant]
type MaxTitleLength: Get<u32>;
/// The minimum title length.
#[pallet::constant]
type MinTitleLength: Get<u32>;
/// The maximum description length.
#[pallet::constant]
type MaxContestDescriptionLength: Get<u32>;
/// The minimum token amount inside the contest when creating it.
#[pallet::constant]
type MinTokenAmount: Get<u32>;
/// The minimum amount that each winner should receive.
#[pallet::constant]
type MinTokenWinner: Get<u32>;
/// The maximum link length.
#[pallet::constant]
type LinkLimit: Get<u32>;
/// 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 ProfileBenchmarkHelper: ProfileInterface<Self::AccountId, Self::ProfileStringLimit>;
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
/// This struct is used to represent the contest entity and store information about a contest,
/// including its title, user address, prize details, status, end date, and description.
#[derive(
Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, MaxEncodedLen, TypeInfo,
)]
#[scale_info(skip_type_params(T))]
pub struct Contest<T: Config> {
pub contest_id: ContestId,
pub title: BoundedVec<u8, T::MaxTitleLength>,
pub user_address: T::AccountId,
pub prize_token_id: AssetIdOf<T>,
pub prize_token_amount: AssetBalanceOf<T>,
pub prize_token_total_awarded: AssetBalanceOf<T>,
pub prize_token_winner: u32,
pub winners_amount: u32,
// statcode states -> true: open; false: closed.
pub statcode: bool,
// contest_end_date -> seconds
pub contest_end_date: u64,
pub description: BoundedVec<u8, T::MaxContestDescriptionLength>,
}
/// This struct represents the contest entry and stores information about a user's participation
/// in a contest, including their account address, the contest they entered, entry ID, a link to
/// their submission, and whether they are marked as a winner or not.
#[derive(
Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, Default, MaxEncodedLen, TypeInfo,
)]
#[scale_info(skip_type_params(T))]
pub struct ContestEntry<T: Config> {
pub user_address: T::AccountId,
pub contest_id: ContestId,
pub entry_id: EntryId,
pub link: BoundedVec<u8, T::LinkLimit>,
pub winner: bool,
}
/// Store the next contest id within each token.
/// asset_id -> contest_id
#[pallet::storage]
#[pallet::getter(fn get_next_contest_id)]
pub type NextContestId<T> =
StorageMap<_, Blake2_128Concat, AssetIdOf<T>, ContestId, ValueQuery>;
/// Store the next entry id within each token and contest.
/// asset_id -> contest_id -> entry_id
#[pallet::storage]
#[pallet::getter(fn get_next_entry_id)]
pub type NextEntryId<T> = StorageDoubleMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Blake2_128Concat,
ContestId,
EntryId,
ValueQuery,
>;
/// Store the contests within each token.
/// asset_id -> contest_id -> contest
#[pallet::storage]
#[pallet::getter(fn get_contests)]
pub type ContestsMap<T> = StorageDoubleMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Blake2_128Concat,
ContestId,
Contest<T>,
OptionQuery,
>;
/// Store the entries within each token and contest.
/// asset_id -> contest_id -> entry_id -> entry
#[pallet::storage]
#[pallet::getter(fn ger_entries)]
pub type EntriesMap<T> = StorageNMap<
_,
(
NMapKey<Blake2_128Concat, AssetIdOf<T>>,
NMapKey<Blake2_128Concat, ContestId>,
NMapKey<Blake2_128Concat, EntryId>,
),
ContestEntry<T>,
OptionQuery,
>;
// Code commented due to this problem is solved by the assets been 'sufficient'
/* #[pallet::genesis_config]
pub struct GenesisConfig;
#[cfg(feature = "std")]
impl Default for GenesisConfig {
fn default() -> Self {
Self{}
}
}
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig {
fn build(&self) {
// This snippet of code fixed the "TokenCannotCreate" error, that was due to the pallet account
// didn't exist because of not having the enough native currency to be activated.
T::Currency::deposit_creating(
&Pallet::<T>::account_id(),
T::Currency::minimum_balance()
);
}
} */
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Create a contest.
ContestCreated {
who: T::AccountId,
contest_id: ContestId,
title: BoundedVec<u8, T::MaxTitleLength>,
},
/// Update a contest.
ContestUpdated {
who: T::AccountId,
contest_id: ContestId,
title: BoundedVec<u8, T::MaxTitleLength>,
},
/// Enter a contest.
EntryCreated { who: T::AccountId, contest_id: ContestId, entry_id: EntryId },
/// Assign a winner in the contest.
ContestWinnerAssigned {
contest_id: ContestId,
winner: T::AccountId,
prize: AssetBalanceOf<T>,
},
/// Close a contest.
ContestClosed { who: T::AccountId, contest_id: ContestId },
}
#[pallet::error]
pub enum Error<T> {
/// Contest id does not exist.
ContestIdDontExist,
/// Contest cannot be closed because it already is.
ContestAlreadyClosed,
/// Entry id does not exist.
EntryIdDontExist,
/// Asset id does not exist.
AssetDontExist,
/// Title length is too small.
TitleTooSmall,
/// Winning token amount must be greater than zero
TokenAmountMustBeGreaterThanZero,
/// Number of winners is too small.
PrizeTokenWinnerTooSmall,
/// Prize amount per winner must be greater than zero.
PrizePerWinnerMustBeGreaterThanZero,
/// The users asset balance to deposit in the contest is insufficient.
AssetBalanceInsufficient,
/// Only the contests owner can update it.
OnlyOwnerCanChange,
/// Only the contests owner can assign a winner in it.
OnlyOwnerCanAssignContestWinner,
/// Only the contests owner can close it.
OnlyOwnerCanCloseContest,
/// Division error in a division operation.
DivisionError,
/// Overflow error in sum operation.
Overflow,
}
#[pallet::call(weight(<T as Config>::WeightInfo))]
impl<T: Config> Pallet<T> {
/// The origin can create a contest with a token having the ID 'token_id' with the title
/// 'title', a total prize amount of 'prize_token_amount', and a specified number of winners
/// 'prize_token_winner'.
///
/// Parameters:
/// - `token_id`: The token ID used as the prize in the contest.
/// - `title`: The title of the contest.
/// - `prize_token_amount`: The total prize amount of the token with the ID 'token_id' in
/// the contest.
/// - `prize_token_winner`: The number of winners the contest can have, and the total prize
/// amount
/// 'prize_token_amount' will be divided among them.
///
/// Emits `ContestCreated` event when successful.
#[pallet::call_index(0)]
pub fn contest_new(
origin: OriginFor<T>,
token_id: AssetIdOf<T>,
title: BoundedVec<u8, T::MaxTitleLength>,
prize_token_amount: AssetBalanceOf<T>,
prize_token_winner: u32,
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
ensure!(
prize_token_amount > (0u128).saturated_into::<AssetBalanceOf<T>>(),
Error::<T>::TokenAmountMustBeGreaterThanZero
);
let contest_id = NextContestId::<T>::get(token_id);
let five_days = 60 * 60 * 24 * 5;
let contest_end_date = T::TimeProvider::now()
.as_secs()
.checked_add(five_days)
.ok_or(Error::<T>::Overflow)?;
Self::validate_contest_new(
who.clone(),
contest_id,
title.clone(),
token_id,
prize_token_amount,
prize_token_winner,
)?;
let contest = Contest::<T> {
contest_id,
title: title.clone(),
user_address: who.clone(),
prize_token_id: token_id,
prize_token_amount,
prize_token_total_awarded: AssetBalanceOf::<T>::zero(),
prize_token_winner,
winners_amount: 0,
statcode: true,
contest_end_date,
description: BoundedVec::try_from("".as_bytes().to_vec())
.unwrap_or(BoundedVec::new()),
};
T::Assets::transfer(
token_id,
&who,
&Self::account_id(),
prize_token_amount,
Expendable,
)?;
let next_contest_id = contest_id + 1;
NextContestId::<T>::insert(token_id, next_contest_id);
ContestsMap::<T>::insert(token_id, contest_id, contest);
Self::deposit_event(Event::<T>::ContestCreated { who, contest_id, title });
Ok(())
}
/// The origin can update a contest with a token having the ID 'token_id', by modifying the
/// title to 'title', the description to 'description', and adjusting the contest end date
/// to 'contest_end_date'.
///
/// Parameters:
/// - `token_id`: The token ID of the contest.
/// - `contest_id`: The contest ID of the contest.
/// - `title`: The title of the contest.
/// - `description`: The description of the contest.
/// - `contest_end_date`: The contest end date of the contest.
///
/// Emits `ContestUpdated` event when successful.
#[pallet::call_index(1)]
pub fn update_contest(
origin: OriginFor<T>,
token_id: AssetIdOf<T>,
contest_id: ContestId,
title: BoundedVec<u8, T::MaxTitleLength>,
description: BoundedVec<u8, T::MaxContestDescriptionLength>,
contest_end_date: u64,
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
Self::validate_update_contest(
who.clone(),
token_id,
contest_id,
title.clone(),
)?;
let mut contest = ContestsMap::<T>::get(token_id, contest_id)
.ok_or(Error::<T>::ContestIdDontExist)?;
let contest_end_date_new = T::TimeProvider::now()
.as_secs()
.checked_add(contest_end_date)
.ok_or(Error::<T>::Overflow)?;
contest.title = title.clone();
contest.description = description.clone();
contest.contest_end_date = contest_end_date_new;
ContestsMap::<T>::insert(token_id, contest_id, contest);
Self::deposit_event(Event::<T>::ContestUpdated { who, contest_id, title });
Ok(())
}
/// The origin can create an entry in the contest with a token having the ID 'token_id' and
/// contest ID 'contest_id', by submitting a link that will be analyzed and potentially
/// chosen by the owner of the contest 'contest_id'.
///
/// Parameters:
/// - `token_id`: The token ID of the contest.
/// - `contest_id`: The contest ID of the contest.
/// - `link`: The link of the entry.
///
/// Emits `EntryCreated` event when successful.
#[pallet::call_index(2)]
pub fn create_contest_entry(
origin: OriginFor<T>,
token_id: AssetIdOf<T>,
contest_id: ContestId,
link: BoundedVec<u8, T::LinkLimit>,
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
let entry_id = NextEntryId::<T>::get(token_id, contest_id);
Self::validate_create_contest_entry(token_id, contest_id)?;
let entry_contest = ContestEntry::<T> {
user_address: who.clone(),
contest_id,
entry_id,
winner: false,
link,
};
EntriesMap::<T>::insert(
(token_id, contest_id, entry_id),
entry_contest,
);
NextEntryId::<T>::insert(token_id, contest_id, entry_id + 1);
Self::deposit_event(Event::<T>::EntryCreated { who, contest_id, entry_id });
Ok(())
}
/// The origin can close a contest with the token ID 'token_id' and contest ID 'contest_id'
/// if they are the owner, retrieving all the tokens that have not yet been awarded.
///
/// Parameters:
/// - `token_id`: The token ID of the contest.
/// - `contest_id`: The contest ID of the contest.
///
/// Emits `ContestClosed` event when successful.
#[pallet::call_index(3)]
pub fn close_contest(
origin: OriginFor<T>,
token_id: AssetIdOf<T>,
contest_id: ContestId,
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
let mut contest =
Self::validate_close_contest(who.clone(), token_id, contest_id)?;
let amount_to_withdraw =
contest.prize_token_amount - contest.prize_token_total_awarded;
T::Assets::transfer(
contest.prize_token_id,
&Self::account_id(),
&who,
amount_to_withdraw,
Expendable,
)?;
contest.statcode = false;
contest.prize_token_amount = AssetBalanceOf::<T>::zero();
let contest_id = contest.contest_id;
ContestsMap::<T>::insert(token_id, contest_id, contest);
Self::deposit_event(Event::<T>::ContestClosed { who, contest_id });
Ok(())
}
/// The origin can assign a winner of the contest with the token ID 'token_id' and contest
/// ID 'contest_id' if they are the owner, and the chosen entry with entry ID 'entry_id"
/// wins the contest prize amount 'prize_token_amount'.
///
/// Parameters:
/// - `token_id`: The token ID of the contest.
/// - `contest_id`: The contest ID of the contest.
/// - `entry_id`: The entry ID of the entry.
///
/// Emits `ContestWinnerAssigned` event when successful.
#[pallet::call_index(4)]
pub fn assign_contest_winner(
origin: OriginFor<T>,
token_id: AssetIdOf<T>,
contest_id: ContestId,
entry_id: EntryId,
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to transfer funds from the main account
who = T::SubAccounts::get_main_account(who)?;
let mut contest = Self::validate_assign_contest_winner(
who.clone(),
token_id,
contest_id,
entry_id,
)?;
let prize =
(match contest.prize_token_amount.checked_div(&contest.prize_token_winner.into()) {
Some(value) => Ok(value),
None => Err(Error::<T>::DivisionError),
})?;
let mut contest_entry = EntriesMap::<T>::get((token_id, contest_id, entry_id))
.ok_or(Error::<T>::EntryIdDontExist)?;
T::Assets::transfer(
contest.prize_token_id,
&Self::account_id(),
&contest_entry.user_address,
prize,
Expendable,
)?;
contest.winners_amount = contest.winners_amount.checked_add(1).unwrap_or(0);
contest.prize_token_total_awarded = contest
.prize_token_total_awarded
.checked_add(&prize)
.ok_or(Error::<T>::Overflow)?;
contest_entry.winner = true;
if contest.prize_token_winner == contest.winners_amount {
contest.statcode = false;
}
let winner = contest_entry.user_address.clone();
ContestsMap::<T>::insert(token_id, contest_id, contest);
EntriesMap::<T>::insert((token_id, contest_id, entry_id), contest_entry);
Self::deposit_event(Event::<T>::ContestWinnerAssigned { contest_id, winner, prize });
Ok(())
}
}
}
impl<T: Config> Pallet<T> {
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
fn validate_contest_new(
who: T::AccountId,
_contest_id: ContestId,
title: BoundedVec<u8, T::MaxTitleLength>,
prize_token_id: AssetIdOf<T>,
prize_token_amount: AssetBalanceOf<T>,
prize_token_winner: u32,
) -> DispatchResult {
ensure!(T::Assets::asset_exists(prize_token_id), Error::<T>::AssetDontExist);
ensure!(
prize_token_winner >= T::MinTokenWinner::get(),
Error::<T>::PrizeTokenWinnerTooSmall
);
ensure!((title.len() as u32) >= T::MinTitleLength::get(), Error::<T>::TitleTooSmall);
ensure!(
T::Assets::balance(prize_token_id, &who) >= T::MinTokenAmount::get().into() &&
prize_token_amount >= T::MinTokenAmount::get().into(),
Error::<T>::AssetBalanceInsufficient
);
// Ensure winner prize is greater than zero.
let prize = prize_token_amount
.checked_div(&prize_token_winner.into())
.ok_or(Error::<T>::DivisionError)?;
ensure!(
prize > (0u128).saturated_into::<AssetBalanceOf<T>>(),
Error::<T>::PrizePerWinnerMustBeGreaterThanZero
);
Ok(())
}
fn validate_update_contest(
who: T::AccountId,
token_id: AssetIdOf<T>,
contest_id: ContestId,
title: BoundedVec<u8, T::MaxTitleLength>,
) -> DispatchResult {
ensure!(T::Assets::asset_exists(token_id), Error::<T>::AssetDontExist);
ensure!(
ContestsMap::<T>::contains_key(token_id, contest_id),
Error::<T>::ContestIdDontExist
);
ensure!((title.len() as u32) >= T::MinTitleLength::get(), Error::<T>::TitleTooSmall);
// Unwrap used because there is a ensure! above testing that the element exist with
// contest_id key
let contest = ContestsMap::<T>::get(token_id, contest_id)
.ok_or(Error::<T>::ContestIdDontExist)?;
ensure!(contest.user_address == who, Error::<T>::OnlyOwnerCanChange);
ensure!(contest.statcode, Error::<T>::ContestAlreadyClosed);
Ok(())
}
fn validate_create_contest_entry(
token_id: AssetIdOf<T>,
contest_id: ContestId,
) -> DispatchResult {
ensure!(T::Assets::asset_exists(token_id), Error::<T>::AssetDontExist);
ensure!(
ContestsMap::<T>::contains_key(token_id, contest_id),
Error::<T>::ContestIdDontExist
);
let contest = ContestsMap::<T>::get(token_id, contest_id)
.ok_or(Error::<T>::ContestIdDontExist)?;
ensure!(contest.statcode, Error::<T>::ContestAlreadyClosed);
Ok(())
}
fn validate_assign_contest_winner(
who: T::AccountId,
token_id: AssetIdOf<T>,
contest_id: ContestId,
entry_id: EntryId,
) -> Result<Contest<T>, DispatchError> {
ensure!(T::Assets::asset_exists(token_id), Error::<T>::AssetDontExist);
ensure!(
ContestsMap::<T>::contains_key(token_id, contest_id),
Error::<T>::ContestIdDontExist
);
ensure!(
EntriesMap::<T>::contains_key((token_id, contest_id, entry_id)),
Error::<T>::EntryIdDontExist
);
let contest =
ContestsMap::get(token_id, contest_id).ok_or(Error::<T>::ContestIdDontExist)?;
ensure!(contest.statcode, Error::<T>::ContestAlreadyClosed);
ensure!(who == contest.user_address, Error::<T>::OnlyOwnerCanAssignContestWinner);
Ok(contest)
}
fn validate_close_contest(
who: T::AccountId,
token_id: AssetIdOf<T>,
contest_id: ContestId,
) -> Result<Contest<T>, DispatchError> {
ensure!(T::Assets::asset_exists(token_id), Error::<T>::AssetDontExist);
ensure!(
ContestsMap::<T>::contains_key(token_id, contest_id),
Error::<T>::ContestIdDontExist
);
let contest = ContestsMap::<T>::get(token_id, contest_id)
.ok_or(Error::<T>::ContestIdDontExist)?;
ensure!(contest.statcode, Error::<T>::ContestAlreadyClosed);
ensure!(contest.user_address == who, Error::<T>::OnlyOwnerCanCloseContest);
Ok(contest)
}
}