diff --git a/farming/contracts/farming/Cargo.toml b/farming/contracts/master_chef/Cargo.toml similarity index 89% rename from farming/contracts/farming/Cargo.toml rename to farming/contracts/master_chef/Cargo.toml index 2f3d162..3c72bfd 100644 --- a/farming/contracts/farming/Cargo.toml +++ b/farming/contracts/master_chef/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "farming_contract" +name = "master_chef_contract" version = "0.1.0" authors = ["Stake Technologies "] edition = "2021" @@ -19,7 +19,7 @@ openbrush = { version = "2.2.0", default-features = false, features = ["ownable" farming = { path = "../../logics", default-features = false } [lib] -name = "farming_contract" +name = "master_chef_contract" path = "lib.rs" crate-type = ["cdylib"] @@ -38,4 +38,9 @@ std = [ "openbrush/std", "farming/std" ] -ink-as-dependency = [] + +[profile.dev] +overflow-checks = false + +[profile.release] +overflow-checks = false diff --git a/farming/contracts/farming/lib.rs b/farming/contracts/master_chef/lib.rs similarity index 93% rename from farming/contracts/farming/lib.rs rename to farming/contracts/master_chef/lib.rs index 0436cf7..6c49eff 100644 --- a/farming/contracts/farming/lib.rs +++ b/farming/contracts/master_chef/lib.rs @@ -2,8 +2,8 @@ #![feature(min_specialization)] #[openbrush::contract] -pub mod farming { - use farming::traits::{ +pub mod master_chef_contract { + use farming::traits::master_chef::{ events::*, farming::*, getters::*, diff --git a/farming/contracts/rewarder/Cargo.toml b/farming/contracts/rewarder/Cargo.toml new file mode 100644 index 0000000..66fea6e --- /dev/null +++ b/farming/contracts/rewarder/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "rewarder_contract" +version = "0.1.0" +authors = ["Stake Technologies "] +edition = "2021" + +[dependencies] +ink_primitives = { version = "~3.3.0", default-features = false } +ink_metadata = { version = "~3.3.0", default-features = false, features = ["derive"], optional = true } +ink_env = { version = "~3.3.0", default-features = false } +ink_storage = { version = "~3.3.0", default-features = false } +ink_lang = { version = "~3.3.0", default-features = false } +ink_prelude = { version = "~3.3.0", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"], optional = true } + +openbrush = { version = "2.2.0", default-features = false, features = ["psp22"] } +farming = { path = "../../logics", default-features = false } + +[lib] +name = "rewarder_contract" +path = "lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["std"] +std = [ + "ink_primitives/std", + "ink_metadata", + "ink_metadata/std", + "ink_env/std", + "ink_storage/std", + "ink_lang/std", + "scale/std", + "scale-info", + "scale-info/std", + "openbrush/std", + "farming/std" +] +[profile.dev] +overflow-checks = false + +[profile.release] +overflow-checks = false diff --git a/farming/contracts/rewarder/lib.rs b/farming/contracts/rewarder/lib.rs new file mode 100644 index 0000000..882db94 --- /dev/null +++ b/farming/contracts/rewarder/lib.rs @@ -0,0 +1,42 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![feature(min_specialization)] + +#[openbrush::contract] +pub mod rewarder { + use farming::traits::rewarder::{ + data::*, + getters::*, + rewarder::*, + }; + use ink_storage::traits::SpreadAllocate; + use openbrush::traits::Storage; + + #[ink(storage)] + #[derive(Default, SpreadAllocate, Storage)] + pub struct Rewarderontract { + #[storage_field] + rewarder: Data, + } + + impl Rewarder for Rewarderontract {} + + impl RewarderGetters for Rewarderontract {} + + impl Rewarderontract { + #[ink(constructor)] + pub fn new( + reward_multiplier: u32, + reward_token: AccountId, + master_chef: AccountId, + ) -> Self { + ink_lang::codegen::initialize_contract(|instance: &mut Self| { + instance.rewarder.reward_multiplier = reward_multiplier; + instance.rewarder.reward_token = reward_token; + instance.rewarder.master_chef = master_chef; + }) + } + + #[ink(message)] + pub fn dummy(&self) {} + } +} diff --git a/farming/logics/Cargo.toml b/farming/logics/Cargo.toml index 7f1eeaf..17e24cf 100644 --- a/farming/logics/Cargo.toml +++ b/farming/logics/Cargo.toml @@ -15,6 +15,7 @@ ink_prelude = { version = "~3.3.0", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2", default-features = false, features = ["derive"], optional = true } +primitive-types = { version = "0.11.1", default-features = false, features = ["codec"] } openbrush = { version = "2.2.0", default-features = false, features = ["ownable"] } [lib] @@ -37,4 +38,5 @@ std = [ "scale-info", "scale-info/std", "openbrush/std", + "primitive-types/std" ] diff --git a/farming/logics/helpers/helper.rs b/farming/logics/helpers/helper.rs new file mode 100644 index 0000000..f42bcef --- /dev/null +++ b/farming/logics/helpers/helper.rs @@ -0,0 +1,8 @@ +#[macro_export] +macro_rules! ensure { + ( $x:expr, $y:expr $(,)? ) => {{ + if !$x { + return Err($y.into()) + } + }}; +} diff --git a/farming/logics/helpers/math.rs b/farming/logics/helpers/math.rs new file mode 100644 index 0000000..7874c8c --- /dev/null +++ b/farming/logics/helpers/math.rs @@ -0,0 +1,5 @@ +use primitive_types::U256; + +pub fn casted_mul(a: u128, b: u128) -> U256 { + U256::from(a) * U256::from(b) +} diff --git a/farming/logics/helpers/mod.rs b/farming/logics/helpers/mod.rs new file mode 100644 index 0000000..97d3d60 --- /dev/null +++ b/farming/logics/helpers/mod.rs @@ -0,0 +1,2 @@ +pub mod helper; +pub mod math; diff --git a/farming/logics/lib.rs b/farming/logics/lib.rs index 1b54fff..8dadbc2 100644 --- a/farming/logics/lib.rs +++ b/farming/logics/lib.rs @@ -1,4 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] #![feature(min_specialization)] +#![feature(mixed_integer_ops)] +pub mod helpers; pub mod traits; diff --git a/farming/logics/traits/farming.rs b/farming/logics/traits/farming.rs deleted file mode 100644 index 2384cb3..0000000 --- a/farming/logics/traits/farming.rs +++ /dev/null @@ -1,235 +0,0 @@ -pub use crate::traits::{ - data::{ - Data, - Pool, - }, - events::FarmingEvents, - getters::FarmingGetters, -}; -use ink_prelude::vec::Vec; -use openbrush::{ - contracts::{ - ownable::*, - traits::psp22::{ - PSP22Error, - PSP22Ref, - }, - }, - modifiers, - traits::{ - AccountId, - Balance, - Storage, - }, -}; - -pub const ACC_ARSW_PRECISION: u8 = 12; -pub const ARTHSWAP_ORIGIN_BLOCK: u32 = 1u32; -pub const BLOCK_PER_PERIOD: u32 = 215000u32; -pub const MAX_PERIOD: u8 = 23u8; -pub const FIRST_PERIOD_REWERD_SUPPLY: Balance = 151629858171523000000u128; - -#[openbrush::trait_definition] -pub trait Farming: Storage + Storage + FarmingGetters + FarmingEvents { - #[ink(message)] - #[modifiers(only_owner)] - fn add( - &mut self, - alloc_point: u32, - lp_token: AccountId, - rewarder: Option, - ) -> Result<(), FarmingError> { - self._check_pool_duplicate(lp_token)?; - self._update_all_pools()?; - self.data::().total_alloc_point = self - .data::() - .total_alloc_point - .checked_add(alloc_point) - .ok_or(FarmingError::AddOverflow2)?; - self.data::().lp_tokens.push(lp_token); - let pool_length = self.pool_length(); - if let Some(rewarder_address) = rewarder { - self.data::() - .rewarders - .insert(&pool_length, &rewarder_address); - } - self.data::().pool_info.insert( - &pool_length, - &Pool { - acc_arsw_per_share: 0, - last_reward_block: Self::env().block_number(), - alloc_point, - }, - ); - self.data::().pool_info_length = pool_length - .checked_add(1) - .ok_or(FarmingError::AddOverflow2)?; - self._emit_log_pool_addition_event(pool_length, alloc_point, lp_token, rewarder); - Ok(()) - } - - #[ink(message)] - fn pending_arsw(&self, _pool_id: u32, _user: AccountId) -> Result { - Ok(1_000_000_000_000_000_000) - } - - #[ink(message)] - fn deposit( - &mut self, - pool_id: u32, - amount: Balance, - to: AccountId, - ) -> Result<(), FarmingError> { - let (prev_amount, prev_reward_debt) = self.get_user_info(pool_id, to).unwrap_or((0, 0)); - // TODO: Fix reward_debt - self.data::().user_info.insert( - &(pool_id, to), - &( - prev_amount - .checked_add(amount) - .ok_or(FarmingError::AddOverflow1)?, - prev_reward_debt, - ), - ); - let lp_token = self - .get_lp_token(pool_id) - .ok_or(FarmingError::PoolNotFound2)?; - PSP22Ref::transfer_from( - &lp_token, - Self::env().caller(), - Self::env().account_id(), - amount, - Vec::new(), - )?; - Ok(()) - } - - #[ink(message)] - fn withdraw( - &mut self, - pool_id: u32, - amount: Balance, - to: AccountId, - ) -> Result<(), FarmingError> { - if amount == 0 { - return Err(FarmingError::ZeroWithdrawal) - } - let caller = Self::env().caller(); - let (prev_amount, prev_reward_debt) = self - .get_user_info(pool_id, caller) - .ok_or(FarmingError::UserNotFound)?; - // TODO: Fix reward_debt - self.data::().user_info.insert( - &(pool_id, caller), - &( - prev_amount - .checked_sub(amount) - .ok_or(FarmingError::SubUnderflow2)?, - prev_reward_debt, - ), - ); - let lp_token = self - .get_lp_token(pool_id) - .ok_or(FarmingError::PoolNotFound3)?; - PSP22Ref::transfer(&lp_token, to, amount, Vec::new())?; - Ok(()) - } - - #[ink(message)] - fn harvest(&mut self, _pool_id: u32, _to: AccountId) -> Result<(), FarmingError> { - Ok(()) - } - - fn _check_pool_duplicate(&self, lp_token: AccountId) -> Result<(), FarmingError> { - let lp_tokens = &self.data::().lp_tokens; - if lp_tokens.iter().any(|lp| *lp == lp_token) { - return Err(FarmingError::DuplicateLPToken) - } - Ok(()) - } - - fn _update_all_pools(&mut self) -> Result<(), FarmingError> { - let lp_tokens = &self.data::().lp_tokens; - for i in 0..lp_tokens.len() { - self._update_pool(i as u32)?; - } - Ok(()) - } - - fn _update_pool(&mut self, pool_id: u32) -> Result<(), FarmingError> { - let pool = self - .get_pool_infos(pool_id) - .ok_or(FarmingError::PoolNotFound1)?; - let current_block = Self::env().block_number(); - if current_block > pool.last_reward_block { - let lp_token = self - .get_lp_token(pool_id) - .ok_or(FarmingError::LpTokenNotFound)?; - let lp_supply = PSP22Ref::balance_of(&lp_token, Self::env().account_id()); - if lp_supply > 0 { - let additional_acc_arsw_per_share = - self._calculate_additional_acc_arsw_per_share(pool, current_block, lp_supply)?; - } - } - Ok(()) - } - - fn _calculate_additional_acc_arsw_per_share( - &mut self, - pool_info: Pool, - current_block: u32, - lp_supply: Balance, - ) -> Result { - if lp_supply == 0 { - return Err(FarmingError::LpSupplyIsZero) - } - let last_reward_block_period = self._get_period(pool_info.last_reward_block)?; - let current_period = self._get_period(Self::env().block_number())?; - Ok(10u128) - } - - fn _get_period(&self, block_number: u32) -> Result { - if block_number < ARTHSWAP_ORIGIN_BLOCK { - return Err(FarmingError::BlockNumberLowerThanOriginBlock) - } - - // BLOCK_PER_PERIOD is never 0 - return Ok(block_number - .checked_sub(ARTHSWAP_ORIGIN_BLOCK) - .ok_or(FarmingError::SubUnderflow1)? - / BLOCK_PER_PERIOD) - } -} - -#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum FarmingError { - OwnableError(OwnableError), - PSP22Error(PSP22Error), - DuplicateLPToken, - PoolNotFound1, - PoolNotFound2, - PoolNotFound3, - UserNotFound, - ZeroWithdrawal, - LpTokenNotFound, - LpSupplyIsZero, - BlockNumberLowerThanOriginBlock, - SubUnderflow1, - SubUnderflow2, - AddOverflow1, - AddOverflow2, - AddOverflow3, -} - -impl From for FarmingError { - fn from(error: OwnableError) -> Self { - FarmingError::OwnableError(error) - } -} - -impl From for FarmingError { - fn from(error: PSP22Error) -> Self { - FarmingError::PSP22Error(error) - } -} diff --git a/farming/logics/traits/data.rs b/farming/logics/traits/master_chef/data.rs similarity index 82% rename from farming/logics/traits/data.rs rename to farming/logics/traits/master_chef/data.rs index 3dc1aa9..8ca6f19 100644 --- a/farming/logics/traits/data.rs +++ b/farming/logics/traits/master_chef/data.rs @@ -3,7 +3,10 @@ use ink_storage::{ traits::*, Mapping, }; -use openbrush::traits::AccountId; +use openbrush::traits::{ + AccountId, + Balance, +}; use scale::{ Decode, Encode, @@ -19,6 +22,13 @@ pub struct Pool { pub alloc_point: u32, } +#[derive(Encode, Decode, SpreadLayout, PackedLayout, SpreadAllocate, Default)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, StorageLayout))] +pub struct UserInfo { + pub amount: Balance, + pub reward_debt: i128, +} + #[derive(Default, Debug)] #[openbrush::upgradeable_storage(STORAGE_KEY)] pub struct Data { @@ -30,7 +40,7 @@ pub struct Data { /// Value (`amount`: u128, `reward_debt`: u128) /// `amount` LP token amount the user has provided. /// `reward_debt` The amount of ARSW entitled to the user. - pub user_info: Mapping<(u32, AccountId), (u128, i128)>, + pub user_info: Mapping<(u32, AccountId), UserInfo>, /// Info of each MasterChef pool. /// Key `pool_id`: u32 diff --git a/farming/logics/traits/master_chef/errors.rs b/farming/logics/traits/master_chef/errors.rs new file mode 100644 index 0000000..ec3ad5b --- /dev/null +++ b/farming/logics/traits/master_chef/errors.rs @@ -0,0 +1,74 @@ +use crate::traits::rewarder::errors::RewarderError; +use openbrush::contracts::{ + ownable::*, + traits::psp22::PSP22Error, +}; + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum FarmingError { + OwnableError(OwnableError), + PSP22Error(PSP22Error), + RewarderError(RewarderError), + DuplicateLPToken, + PoolNotFound, + UserNotFound, + ZeroWithdrawal, + LpTokenNotFound, + LpSupplyIsZero, + BlockNumberLowerThanOriginBlock, + CastTou128Error1, + CastTou128Error2, + CastTou128Error3, + CastTou128Error4, + CastTou128Error5, + CastTou128Error6, + CastTou128Error7, + CastToi128Error1, + CastToi128Error2, + CastToi128Error3, + RewarderNotFound, + SubUnderflow1, + SubUnderflow2, + SubUnderflow3, + SubUnderflow4, + SubUnderflow5, + SubUnderflow6, + SubUnderflow7, + SubUnderflow8, + AddOverflow1, + AddOverflow2, + AddOverflow3, + AddOverflow4, + AddOverflow5, + AddOverflow6, + AddOverflow7, + AddOverflow8, + AddOverflow9, + AddOverflow10, + AddOverflow11, + AddOverflow12, + MulOverflow1, + PowOverflow1, + PowOverflow2, + DivByZero1, + DivByZero2, +} + +impl From for FarmingError { + fn from(error: OwnableError) -> Self { + FarmingError::OwnableError(error) + } +} + +impl From for FarmingError { + fn from(error: PSP22Error) -> Self { + FarmingError::PSP22Error(error) + } +} + +impl From for FarmingError { + fn from(error: RewarderError) -> Self { + FarmingError::RewarderError(error) + } +} diff --git a/farming/logics/traits/events.rs b/farming/logics/traits/master_chef/events.rs similarity index 84% rename from farming/logics/traits/events.rs rename to farming/logics/traits/master_chef/events.rs index b199a24..beaba93 100644 --- a/farming/logics/traits/events.rs +++ b/farming/logics/traits/master_chef/events.rs @@ -32,14 +32,7 @@ pub trait FarmingEvents { ) { } - fn _emit_harvest_event( - &self, - _user: AccountId, - _pool_id: u32, - _amount: Balance, - _to: AccountId, - ) { - } + fn _emit_harvest_event(&self, _user: AccountId, _pool_id: u32, _amount: Balance) {} fn _emit_log_pool_addition_event( &self, @@ -53,8 +46,8 @@ pub trait FarmingEvents { fn _emit_log_set_pool_event( &self, _pool_id: u32, - _alloc_point: u128, - _rewardes: AccountId, + _alloc_point: u32, + _rewarder: Option, _overwrite: bool, ) { } diff --git a/farming/logics/traits/master_chef/farming.rs b/farming/logics/traits/master_chef/farming.rs new file mode 100644 index 0000000..afc580d --- /dev/null +++ b/farming/logics/traits/master_chef/farming.rs @@ -0,0 +1,585 @@ +pub use crate::traits::{ + master_chef::{ + data::{ + Data, + Pool, + }, + events::FarmingEvents, + getters::FarmingGetters, + }, + rewarder::rewarder::RewarderRef, +}; +use crate::{ + ensure, + helpers::math::casted_mul, + traits::master_chef::{ + data::UserInfo, + errors::FarmingError, + }, +}; +use ink_env::CallFlags; +use ink_prelude::vec::Vec; +use openbrush::{ + contracts::{ + ownable::*, + traits::psp22::PSP22Ref, + }, + modifiers, + traits::{ + AccountId, + Balance, + Storage, + }, +}; +use primitive_types::U256; + +// Cannot be 0 or it will panic! +pub const ACC_ARSW_PRECISION: u128 = 1_000_000_000_000; +pub const ARTHSWAP_ORIGIN_BLOCK: u32 = 1u32; +pub const BLOCK_PER_PERIOD: u32 = 215000u32; +pub const MAX_PERIOD: u32 = 23u32; +pub const FIRST_PERIOD_REWERD_SUPPLY: Balance = 151629858171523000000u128; + +#[openbrush::trait_definition] +pub trait Farming: Storage + Storage + FarmingGetters + FarmingEvents { + #[ink(message)] + #[modifiers(only_owner)] + fn add( + &mut self, + alloc_point: u32, + lp_token: AccountId, + rewarder: Option, + ) -> Result<(), FarmingError> { + self._check_pool_duplicate(lp_token)?; + self._update_all_pools()?; + + self.data::().total_alloc_point = self + .get_total_alloc_point() + .checked_add(alloc_point) + .ok_or(FarmingError::AddOverflow1)?; + self.data::().lp_tokens.push(lp_token); + let pool_length = self.pool_length(); + + if let Some(rewarder_address) = rewarder { + self.data::() + .rewarders + .insert(&pool_length, &rewarder_address); + } + self.data::().pool_info.insert( + &pool_length, + &Pool { + acc_arsw_per_share: 0, + last_reward_block: Self::env().block_number(), + alloc_point, + }, + ); + self.data::().pool_info_length = pool_length + .checked_add(1) + .ok_or(FarmingError::AddOverflow1)?; + + self._emit_log_pool_addition_event(pool_length, alloc_point, lp_token, rewarder); + Ok(()) + } + + #[ink(message)] + #[modifiers(only_owner)] + fn set( + &mut self, + pool_id: u32, + alloc_point: u32, + rewarder: Option, + overwrite: bool, + ) -> Result<(), FarmingError> { + let pool_info = self + .get_pool_info(pool_id) + .ok_or(FarmingError::PoolNotFound)?; + self._update_all_pools()?; + self.data::().total_alloc_point = self + .get_total_alloc_point() + .checked_sub(pool_info.alloc_point) + .ok_or(FarmingError::SubUnderflow4)? + .checked_add(alloc_point) + .ok_or(FarmingError::AddOverflow7)?; + + self.data::().pool_info.insert( + &pool_id, + &Pool { + alloc_point, + ..pool_info + }, + ); + let mut rewarder = rewarder; + if overwrite { + match rewarder { + Some(rewarder_address) => { + self.data::() + .rewarders + .insert(&pool_id, &rewarder_address) + } + None => self.data::().rewarders.remove(&pool_id), + } + } else { + rewarder = self.get_rewarder(pool_id); + } + self._emit_log_set_pool_event(pool_id, alloc_point, rewarder, overwrite); + Ok(()) + } + + #[ink(message)] + fn pending_arsw(&self, pool_id: u32, user: AccountId) -> Result { + let pool = self + .get_pool_info(pool_id) + .ok_or(FarmingError::PoolNotFound)?; + let user_info = self + .get_user_info(pool_id, user) + .ok_or(FarmingError::UserNotFound)?; + let mut acc_arsw_per_share = pool.acc_arsw_per_share; + + let lp_supply = self._get_lp_supply(pool_id)?; + let current_block = Self::env().block_number(); + + if current_block > pool.last_reward_block && lp_supply != 0 { + let additional_acc_arsw_per_share = + self._calculate_additional_acc_arsw_per_share(&pool, current_block, lp_supply)?; + acc_arsw_per_share = acc_arsw_per_share + .checked_add(additional_acc_arsw_per_share) + .ok_or(FarmingError::AddOverflow6)?; + } + + let pending = >::try_into( + casted_mul(user_info.amount, acc_arsw_per_share) / ACC_ARSW_PRECISION, + ) + .map_err(|_| FarmingError::CastTou128Error7)? + .checked_add_signed(-user_info.reward_debt) + .ok_or(FarmingError::AddOverflow10)?; + + Ok(pending) + } + + #[ink(message)] + fn deposit( + &mut self, + pool_id: u32, + amount: Balance, + to: AccountId, + ) -> Result<(), FarmingError> { + let pool = self + .get_pool_info(pool_id) + .ok_or(FarmingError::PoolNotFound)?; + self._update_pool(pool_id)?; + let user = self.get_user_info(pool_id, to).unwrap_or_default(); + let user_amount = user + .amount + .checked_add(amount) + .ok_or(FarmingError::AddOverflow8)?; + let user_reward_debt = user + .reward_debt + .checked_add( + >::try_into( + casted_mul(amount, pool.acc_arsw_per_share) / ACC_ARSW_PRECISION, + ) + .map_err(|_| FarmingError::CastTou128Error1)?, + ) + .ok_or(FarmingError::AddOverflow9)?; + + self.data::().user_info.insert( + &(pool_id, to), + &UserInfo { + amount: user_amount, + reward_debt: user_reward_debt, + }, + ); + + if let Some(rewarder_address) = self.get_rewarder(pool_id) { + RewarderRef::on_arsw_reward(&rewarder_address, pool_id, to, to, 0, user_amount)?; + } + + let lp_token = self + .get_lp_token(pool_id) + .ok_or(FarmingError::PoolNotFound)?; + PSP22Ref::transfer_from( + &lp_token, + Self::env().caller(), + Self::env().account_id(), + amount, + Vec::new(), + )?; + self._emit_deposit_event(Self::env().caller(), pool_id, amount, to); + Ok(()) + } + + #[ink(message)] + fn withdraw( + &mut self, + pool_id: u32, + amount: Balance, + to: AccountId, + ) -> Result<(), FarmingError> { + ensure!(amount > 0, FarmingError::ZeroWithdrawal); + let pool = self + .get_pool_info(pool_id) + .ok_or(FarmingError::PoolNotFound)?; + self._update_pool(pool_id)?; + let caller = Self::env().caller(); + let user = self + .get_user_info(pool_id, caller) + .ok_or(FarmingError::UserNotFound)?; + let user_reward_debt = user + .reward_debt + .checked_sub( + >::try_into( + casted_mul(amount, pool.acc_arsw_per_share) / ACC_ARSW_PRECISION, + ) + .map_err(|_| FarmingError::CastTou128Error2)?, + ) + .ok_or(FarmingError::SubUnderflow5)?; + + let user_amount = user + .amount + .checked_sub(amount) + .ok_or(FarmingError::SubUnderflow8)?; + + self.data::().user_info.insert( + &(pool_id, to), + &UserInfo { + amount: user_amount, + reward_debt: user_reward_debt, + }, + ); + + if let Some(rewarder_address) = self.get_rewarder(pool_id) { + RewarderRef::on_arsw_reward(&rewarder_address, pool_id, caller, to, 0, user_amount)?; + } + + let lp_token = self + .get_lp_token(pool_id) + .ok_or(FarmingError::LpTokenNotFound)?; + PSP22Ref::transfer(&lp_token, to, amount, Vec::new())?; + self._emit_withdraw_event(caller, pool_id, amount, to); + Ok(()) + } + + #[ink(message)] + fn harvest(&mut self, pool_id: u32, to: AccountId) -> Result<(), FarmingError> { + let pool = self + .get_pool_info(pool_id) + .ok_or(FarmingError::PoolNotFound)?; + self._update_pool(pool_id)?; + let caller = Self::env().caller(); + let user = self + .get_user_info(pool_id, caller) + .ok_or(FarmingError::UserNotFound)?; + + let accumulated_arsw = >::try_into( + casted_mul(user.amount, pool.acc_arsw_per_share) / ACC_ARSW_PRECISION, + ) + .map_err(|_| FarmingError::CastTou128Error3)?; + + let pending_arsw = accumulated_arsw + .checked_sub(user.reward_debt) + .ok_or(FarmingError::SubUnderflow7)? as u128; + + self.data::().user_info.insert( + &(pool_id, to), + &UserInfo { + reward_debt: accumulated_arsw, + ..user + }, + ); + + if pending_arsw != 0 { + PSP22Ref::transfer( + &mut self.data::().arsw_token, + to, + pending_arsw, + Vec::new(), + )?; + } + + if let Some(rewarder_address) = self.get_rewarder(pool_id) { + RewarderRef::on_arsw_reward( + &rewarder_address, + pool_id, + caller, + to, + pending_arsw, + user.amount, + )?; + } + + self._emit_harvest_event(caller, pool_id, pending_arsw); + Ok(()) + } + + #[ink(message)] + fn withdraw_and_harvest( + &mut self, + pool_id: u32, + amount: Balance, + to: AccountId, + ) -> Result<(), FarmingError> { + let pool = self + .get_pool_info(pool_id) + .ok_or(FarmingError::PoolNotFound)?; + self._update_pool(pool_id)?; + let caller = Self::env().caller(); + let user = self + .get_user_info(pool_id, caller) + .ok_or(FarmingError::UserNotFound)?; + + let accumulated_arsw = + casted_mul(user.amount, pool.acc_arsw_per_share) / ACC_ARSW_PRECISION; + + let pending_arsw = >::try_into(accumulated_arsw) + .map_err(|_| FarmingError::CastTou128Error6)? + .checked_add_signed(-user.reward_debt) + .ok_or(FarmingError::AddOverflow11)?; + + let user_reward_debt: i128 = accumulated_arsw + .checked_sub(casted_mul(amount, pool.acc_arsw_per_share) / ACC_ARSW_PRECISION) + .ok_or(FarmingError::SubUnderflow6)? + .try_into() + .map_err(|_| FarmingError::CastTou128Error5)?; + + let user_amount = user + .amount + .checked_sub(amount) + .ok_or(FarmingError::AddOverflow12)?; + + self.data::().user_info.insert( + &(pool_id, to), + &UserInfo { + reward_debt: user_reward_debt, + amount: user_amount, + }, + ); + + PSP22Ref::transfer( + &self.data::().arsw_token, + to, + pending_arsw, + Vec::new(), + )?; + if let Some(rewarder_address) = self.get_rewarder(pool_id) { + RewarderRef::on_arsw_reward( + &rewarder_address, + pool_id, + caller, + to, + pending_arsw, + user.amount, + )?; + } + let lp_token = self + .get_lp_token(pool_id) + .ok_or(FarmingError::LpTokenNotFound)?; + PSP22Ref::transfer(&lp_token, to, amount, Vec::new())?; + + self._emit_withdraw_event(caller, pool_id, amount, to); + self._emit_harvest_event(caller, pool_id, pending_arsw); + Ok(()) + } + + #[ink(message)] + fn emergency_withdraw(&mut self, pool_id: u32, to: AccountId) -> Result<(), FarmingError> { + let caller = Self::env().caller(); + let user = self + .get_user_info(pool_id, caller) + .ok_or(FarmingError::UserNotFound)?; + let amount = user.amount; + self.data::().user_info.insert( + &(pool_id, to), + &UserInfo { + reward_debt: 0, + amount: 0, + }, + ); + + if let Some(rewarder_address) = self.get_rewarder(pool_id) { + RewarderRef::on_arsw_reward(&rewarder_address, pool_id, caller, to, 0, 0)?; + } + + let lp_token = self + .get_lp_token(pool_id) + .ok_or(FarmingError::LpTokenNotFound)?; + PSP22Ref::transfer(&lp_token, to, amount, Vec::new())?; + + self._emit_emergency_withdraw_event(caller, pool_id, amount, to); + Ok(()) + } + + #[ink(message)] + #[modifiers(only_owner)] + fn deposit_arsw(&mut self, amount: Balance) -> Result<(), FarmingError> { + ensure!(amount > 0, FarmingError::ZeroWithdrawal); + let caller = Self::env().caller(); + PSP22Ref::transfer_from_builder( + &mut self.data::().arsw_token, + caller, + Self::env().account_id(), + amount, + Vec::new(), + ) + .call_flags(CallFlags::default().set_allow_reentry(true)) + .fire() + .unwrap()?; + self._emit_deposit_arsw_event(Self::env().block_number(), amount); + Ok(()) + } + + fn _check_pool_duplicate(&self, lp_token: AccountId) -> Result<(), FarmingError> { + let lp_tokens = &self.data::().lp_tokens; + ensure!( + !lp_tokens.iter().any(|lp| *lp == lp_token), + FarmingError::DuplicateLPToken + ); + Ok(()) + } + + fn _update_all_pools(&mut self) -> Result<(), FarmingError> { + let lp_tokens = &self.data::().lp_tokens; + for i in 0..lp_tokens.len() { + self._update_pool(i as u32)?; + } + Ok(()) + } + + fn _update_pool(&mut self, pool_id: u32) -> Result<(), FarmingError> { + let mut pool = self + .get_pool_info(pool_id) + .ok_or(FarmingError::PoolNotFound)?; + let current_block = Self::env().block_number(); + if current_block > pool.last_reward_block { + let lp_supply = self._get_lp_supply(pool_id)?; + if lp_supply > 0 { + let additional_acc_arsw_per_share = + self._calculate_additional_acc_arsw_per_share(&pool, current_block, lp_supply)?; + pool.acc_arsw_per_share = pool + .acc_arsw_per_share + .checked_add(additional_acc_arsw_per_share) + .ok_or(FarmingError::AddOverflow6)?; + } + pool.last_reward_block = current_block; + self.data::().pool_info.insert(&pool_id, &pool); + + self._emit_log_update_pool_event( + pool_id, + pool.last_reward_block, + lp_supply, + pool.acc_arsw_per_share, + ); + } + Ok(()) + } + + fn _calculate_additional_acc_arsw_per_share( + &self, + pool_info: &Pool, + current_block: u32, + lp_supply: Balance, + ) -> Result { + ensure!(lp_supply > 0, FarmingError::LpSupplyIsZero); + let current_period = self._get_period(current_block)?; + let mut arsw_reward: Balance = 0; + let mut last_block = pool_info.last_reward_block; + let last_reward_block_period = self._get_period(last_block)?; + let mut period = last_reward_block_period; + let total_alloc_point = self.get_total_alloc_point(); + while period <= current_period { + if period > MAX_PERIOD { + break + } + if current_block <= self._period_max(period)? { + arsw_reward = arsw_reward + .checked_add( + casted_mul( + current_block + .checked_sub(last_block) + .ok_or(FarmingError::SubUnderflow2)? + as u128 + * pool_info.alloc_point as u128, + self._arsw_per_block(period)?, + ) + .checked_div(total_alloc_point.into()) + .ok_or(FarmingError::DivByZero1)? + .try_into() + .map_err(|_| FarmingError::CastTou128Error1)?, + ) + .ok_or(FarmingError::AddOverflow4)?; + } else { + arsw_reward = arsw_reward + .checked_add( + casted_mul( + self._period_max(period)? + .checked_sub(last_block.into()) + .ok_or(FarmingError::SubUnderflow3)? + as u128 + * pool_info.alloc_point as u128, + self._arsw_per_block(period)? as u128, + ) + .checked_div(total_alloc_point.into()) + .ok_or(FarmingError::DivByZero2)? + .try_into() + .map_err(|_| FarmingError::CastTou128Error2)?, + ) + .ok_or(FarmingError::AddOverflow5)?; + + last_block = self._period_max(period)?; + } + + period += 1; + } + Ok( + (casted_mul(arsw_reward, ACC_ARSW_PRECISION) / U256::from(lp_supply)) + .try_into() + .map_err(|_| FarmingError::CastTou128Error4)?, + ) + } + + fn _get_period(&self, block_number: u32) -> Result { + ensure!( + block_number >= ARTHSWAP_ORIGIN_BLOCK, + FarmingError::BlockNumberLowerThanOriginBlock + ); + + // BLOCK_PER_PERIOD is never 0 + return Ok(block_number + .checked_sub(ARTHSWAP_ORIGIN_BLOCK) + .ok_or(FarmingError::SubUnderflow1)? + / BLOCK_PER_PERIOD) + } + + fn _period_max(&self, period: u32) -> Result { + Ok(ARTHSWAP_ORIGIN_BLOCK + .checked_add( + BLOCK_PER_PERIOD + .checked_mul(period.checked_add(1).ok_or(FarmingError::AddOverflow2)?) + .ok_or(FarmingError::MulOverflow1)?, + ) + .ok_or(FarmingError::AddOverflow3)? + - 1) + } + + fn _arsw_per_block(&self, period: u32) -> Result { + if period > MAX_PERIOD { + return Ok(0) + } + Ok((casted_mul( + FIRST_PERIOD_REWERD_SUPPLY, + 9u128 + .checked_pow(period) + .ok_or(FarmingError::PowOverflow1)?, + ) / 10u128 + .checked_pow(period) + .ok_or(FarmingError::PowOverflow2)?) + .try_into() + .map_err(|_| FarmingError::CastTou128Error3)?) + } + + fn _get_lp_supply(&self, pool_id: u32) -> Result { + let lp_token = self + .get_lp_token(pool_id) + .ok_or(FarmingError::LpTokenNotFound)?; + Ok(PSP22Ref::balance_of(&lp_token, Self::env().account_id())) + } +} diff --git a/farming/logics/traits/getters.rs b/farming/logics/traits/master_chef/getters.rs similarity index 60% rename from farming/logics/traits/getters.rs rename to farming/logics/traits/master_chef/getters.rs index b8394ac..6bc7da4 100644 --- a/farming/logics/traits/getters.rs +++ b/farming/logics/traits/master_chef/getters.rs @@ -1,5 +1,8 @@ -use crate::traits::{ - data::Pool, +use crate::traits::master_chef::{ + data::{ + Pool, + UserInfo, + }, farming::Data, }; use ink_env::AccountId; @@ -13,12 +16,12 @@ pub trait FarmingGetters: Storage { } #[ink(message)] - fn get_pool_infos(&self, pool_id: u32) -> Option { + fn get_pool_info(&self, pool_id: u32) -> Option { self.data::().pool_info.get(&pool_id) } #[ink(message)] - fn get_user_info(&self, pool_id: u32, user: AccountId) -> Option<(u128, i128)> { + fn get_user_info(&self, pool_id: u32, user: AccountId) -> Option { self.data::().user_info.get(&(pool_id, user)) } @@ -26,4 +29,14 @@ pub trait FarmingGetters: Storage { fn get_lp_token(&self, pool_id: u32) -> Option { self.data::().lp_tokens.get(pool_id as usize).copied() } + + #[ink(message)] + fn get_rewarder(&self, pool_id: u32) -> Option { + self.data::().rewarders.get(pool_id) + } + + #[ink(message)] + fn get_total_alloc_point(&self) -> u32 { + self.data::().total_alloc_point + } } diff --git a/farming/logics/traits/master_chef/mod.rs b/farming/logics/traits/master_chef/mod.rs new file mode 100644 index 0000000..38029af --- /dev/null +++ b/farming/logics/traits/master_chef/mod.rs @@ -0,0 +1,5 @@ +pub mod data; +pub mod errors; +pub mod events; +pub mod farming; +pub mod getters; diff --git a/farming/logics/traits/mod.rs b/farming/logics/traits/mod.rs index 321934a..207f3e2 100644 --- a/farming/logics/traits/mod.rs +++ b/farming/logics/traits/mod.rs @@ -1,4 +1,2 @@ -pub mod data; -pub mod events; -pub mod farming; -pub mod getters; +pub mod master_chef; +pub mod rewarder; diff --git a/farming/logics/traits/rewarder/data.rs b/farming/logics/traits/rewarder/data.rs new file mode 100644 index 0000000..55fcc4a --- /dev/null +++ b/farming/logics/traits/rewarder/data.rs @@ -0,0 +1,11 @@ +use ink_env::AccountId; + +pub const STORAGE_KEY: u32 = openbrush::storage_unique_key!(Data); + +#[derive(Default, Debug)] +#[openbrush::upgradeable_storage(STORAGE_KEY)] +pub struct Data { + pub reward_multiplier: u32, + pub reward_token: AccountId, + pub master_chef: AccountId, +} diff --git a/farming/logics/traits/rewarder/errors.rs b/farming/logics/traits/rewarder/errors.rs new file mode 100644 index 0000000..8942614 --- /dev/null +++ b/farming/logics/traits/rewarder/errors.rs @@ -0,0 +1,16 @@ +use openbrush::contracts::traits::psp22::PSP22Error; + +#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum RewarderError { + PSP22Error(PSP22Error), + CallerIsNotMasterChef, + MulOverflow1, + MulOverflow2, +} + +impl From for RewarderError { + fn from(error: PSP22Error) -> Self { + RewarderError::PSP22Error(error) + } +} diff --git a/farming/logics/traits/rewarder/getters.rs b/farming/logics/traits/rewarder/getters.rs new file mode 100644 index 0000000..c358ed5 --- /dev/null +++ b/farming/logics/traits/rewarder/getters.rs @@ -0,0 +1,21 @@ +use crate::traits::rewarder::rewarder::Data; +use ink_env::AccountId; +use openbrush::traits::Storage; + +#[openbrush::trait_definition] +pub trait RewarderGetters: Storage { + #[ink(message)] + fn reward_multiplier(&self) -> u32 { + self.data::().reward_multiplier + } + + #[ink(message)] + fn reward_token(&self) -> AccountId { + self.data::().reward_token + } + + #[ink(message)] + fn master_chef(&self) -> AccountId { + self.data::().master_chef + } +} diff --git a/farming/logics/traits/rewarder/mod.rs b/farming/logics/traits/rewarder/mod.rs new file mode 100644 index 0000000..38b2235 --- /dev/null +++ b/farming/logics/traits/rewarder/mod.rs @@ -0,0 +1,4 @@ +pub mod data; +pub mod errors; +pub mod getters; +pub mod rewarder; diff --git a/farming/logics/traits/rewarder/rewarder.rs b/farming/logics/traits/rewarder/rewarder.rs new file mode 100644 index 0000000..946b747 --- /dev/null +++ b/farming/logics/traits/rewarder/rewarder.rs @@ -0,0 +1,76 @@ +pub use crate::traits::rewarder::{ + data::Data, + errors::RewarderError, + getters::RewarderGetters, +}; +use ink_env::AccountId; +use ink_prelude::vec::Vec; +use openbrush::{ + contracts::traits::psp22::PSP22Ref, + modifier_definition, + modifiers, + traits::{ + Balance, + Storage, + }, +}; + +// Cannot be 0 or it will panic! +pub const REWARD_TOKEN_DIVISOR: u128 = 1_000_000_000_000_000_000; + +#[openbrush::wrapper] +pub type RewarderRef = dyn Rewarder; + +#[openbrush::trait_definition] +pub trait Rewarder: Storage + RewarderGetters { + #[ink(message)] + #[modifiers(only_master_chef)] + fn on_arsw_reward( + &self, + _pool_id: u32, + _user: AccountId, + to: AccountId, + arsw_amount: Balance, + _new_lp_amount: Balance, + ) -> Result<(), RewarderError> { + let pending_reward = arsw_amount + .checked_mul(self.reward_multiplier().into()) + .ok_or(RewarderError::MulOverflow1)? + / REWARD_TOKEN_DIVISOR; + let reward_token = self.data::().reward_token; + let reward_bal = PSP22Ref::balance_of(&reward_token, Self::env().account_id()); + if pending_reward < reward_bal { + PSP22Ref::transfer(&reward_token, to, reward_bal, Vec::new())?; + } else { + PSP22Ref::transfer(&reward_token, to, pending_reward, Vec::new())?; + } + Ok(()) + } + + #[ink(message)] + fn pending_tokens( + &self, + _pool_id: u32, + _user: AccountId, + arsw_amount: Balance, + ) -> Result<(AccountId, Balance), RewarderError> { + let reward_amount = arsw_amount + .checked_mul(self.reward_multiplier().into()) + .ok_or(RewarderError::MulOverflow2)? + / REWARD_TOKEN_DIVISOR; + Ok((self.reward_token(), reward_amount)) + } +} + +#[modifier_definition] +pub fn only_master_chef(instance: &T, body: F) -> Result +where + T: Storage, + F: FnOnce(&T) -> Result, + E: From, +{ + if instance.data().master_chef != T::env().caller() { + return Err(From::from(RewarderError::CallerIsNotMasterChef)) + } + body(instance) +}