--- a/pallets/app-promotion/src/lib.rs +++ b/pallets/app-promotion/src/lib.rs @@ -37,9 +37,10 @@ pub mod weights; use sp_std::{vec::Vec, iter::Sum, borrow::ToOwned}; +use sp_core::H160; use codec::EncodeLike; use pallet_balances::BalanceLock; -pub use types::ExtendedLockableCurrency; +pub use types::*; // use up_common::constants::{DAYS, UNIQUE}; use up_data_structs::CollectionId; @@ -78,7 +79,6 @@ use super::*; use frame_support::{Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, PalletId}; use frame_system::pallet_prelude::*; - use types::CollectionHandler; #[pallet::config] pub trait Config: frame_system::Config + pallet_evm::account::Config { @@ -89,6 +89,8 @@ CollectionId = CollectionId, >; + type ContractHandler: ContractHandler; + type TreasuryAccountId: Get; /// The app's pallet id, used for deriving its sovereign account ID. @@ -98,7 +100,7 @@ /// In relay blocks. #[pallet::constant] type RecalculationInterval: Get; - /// In chain blocks. + /// In relay blocks. #[pallet::constant] type PendingInterval: Get; @@ -120,13 +122,6 @@ /// Events compatible with [`frame_system::Config::Event`]. type Event: IsType<::Event> + From>; - - // /// Number of blocks that pass between treasury balance updates due to inflation - // #[pallet::constant] - // type InterestBlockInterval: Get; - - // // Weight information for functions of this pallet. - // type WeightInfo: WeightInfo; } #[pallet::pallet] @@ -146,13 +141,14 @@ #[pallet::error] pub enum Error { + /// Error due to action requiring admin to be set AdminNotSet, - /// No permission to perform action + /// No permission to perform an action NoPermission, /// Insufficient funds to perform an action NotSufficientFounds, + /// An error related to the fact that an invalid argument was passed to perform an action InvalidArgument, - AlreadySponsored, } #[pallet::storage] @@ -183,7 +179,7 @@ QueryKind = ValueQuery, >; - /// A block when app-promotion has started + /// A block when app-promotion has started .I think this is redundant, because we only need `NextInterestBlock`. #[pallet::storage] pub type StartBlock = StorageValue; @@ -285,6 +281,21 @@ Ok(()) } + #[pallet::weight(0)] + pub fn stop_app_promotion(origin: OriginFor) -> DispatchResult + where + ::BlockNumber: From, + { + ensure_root(origin)?; + + if >::get() != 0u32.into() { + >::set(T::BlockNumber::default()); + >::set(T::BlockNumber::default()); + } + + Ok(()) + } + #[pallet::weight(T::WeightInfo::stake())] pub fn stake(staker: OriginFor, amount: BalanceOf) -> DispatchResult { let staker_id = ensure_signed(staker)?; @@ -341,7 +352,8 @@ .ok_or(ArithmeticError::Underflow)?, ); - let block = frame_system::Pallet::::block_number() + T::PendingInterval::get(); + let block = + T::RelayBlockNumberProvider::current_block_number() + T::PendingInterval::get(); >::insert( (&staker_id, block), >::get((&staker_id, block)) @@ -415,113 +427,46 @@ ); T::CollectionHandler::remove_collection_sponsor(collection_id) } - } -} - -impl Pallet { - // pub fn stake(staker: &T::AccountId, amount: BalanceOf) -> DispatchResult { - // let balance = <::Currency as Currency>::free_balance(staker); - - // ensure!(balance >= amount, ArithmeticError::Underflow); - - // Self::set_lock_unchecked(staker, amount); - - // let block_number = ::current_block_number(); - // >::insert( - // (staker, block_number), - // >::get((staker, block_number)) - // .checked_add(&amount) - // .ok_or(ArithmeticError::Overflow)?, - // ); - - // >::set( - // >::get() - // .checked_add(&amount) - // .ok_or(ArithmeticError::Overflow)?, - // ); - - // Ok(()) - // } - - // pub fn unstake(staker: &T::AccountId, amount: BalanceOf) -> DispatchResult { - // let mut stakes = Staked::::iter_prefix((staker,)).collect::>(); - - // let total_staked = stakes - // .iter() - // .fold(>::default(), |acc, (_, amount)| acc + *amount); - - // ensure!(total_staked >= amount, ArithmeticError::Underflow); - - // >::set( - // >::get() - // .checked_sub(&amount) - // .ok_or(ArithmeticError::Underflow)?, - // ); - - // let block = ::current_block_number() + WEEK.into(); - // >::insert( - // (staker, block), - // >::get((staker, block)) - // .checked_add(&amount) - // .ok_or(ArithmeticError::Overflow)?, - // ); - - // stakes.sort_by_key(|(block, _)| *block); - - // let mut acc_amount = amount; - // let new_state = stakes - // .into_iter() - // .map_while(|(block, balance_per_block)| { - // if acc_amount == >::default() { - // return None; - // } - // if acc_amount <= balance_per_block { - // let res = (block, balance_per_block - acc_amount, acc_amount); - // acc_amount = >::default(); - // return Some(res); - // } else { - // acc_amount -= balance_per_block; - // return Some((block, >::default(), acc_amount)); - // } - // }) - // .collect::>(); - - // new_state - // .into_iter() - // .for_each(|(block, to_staked, _to_pending)| { - // if to_staked == >::default() { - // >::remove((staker, block)); - // } else { - // >::insert((staker, block), to_staked); - // } - // }); + #[pallet::weight(0)] + pub fn sponsor_conract(admin: OriginFor, contract_id: H160) -> DispatchResult { + let admin_id = ensure_signed(admin)?; - // Ok(()) - // } + ensure!( + admin_id == Admin::::get().ok_or(Error::::AdminNotSet)?, + Error::::NoPermission + ); - // pub fn sponsor_collection(admin: T::AccountId, collection_id: u32) -> DispatchResult { - // Ok(()) - // } + T::ContractHandler::set_sponsor( + T::CrossAccountId::from_sub(Self::account_id()), + contract_id, + ) + } - // pub fn stop_sponsorign_collection(admin: T::AccountId, collection_id: u32) -> DispatchResult { - // Ok(()) - // } + #[pallet::weight(0)] + pub fn stop_sponsorign_contract(admin: OriginFor, contract_id: H160) -> DispatchResult { + let admin_id = ensure_signed(admin)?; - pub fn sponsor_conract(admin: T::AccountId, app_id: u32) -> DispatchResult { - Ok(()) - } + ensure!( + admin_id == Admin::::get().ok_or(Error::::AdminNotSet)?, + Error::::NoPermission + ); - pub fn stop_sponsorign_contract(admin: T::AccountId, app_id: u32) -> DispatchResult { - Ok(()) + ensure!( + T::ContractHandler::get_sponsor(contract_id)?.ok_or(>::InvalidArgument)? + == T::CrossAccountId::from_sub(Self::account_id()), + >::NoPermission + ); + T::ContractHandler::remove_contract_sponsor(contract_id) + } } +} +impl Pallet { pub fn account_id() -> T::AccountId { T::PalletId::get().into_account_truncating() } -} -impl Pallet { fn unlock_balance_unchecked(staker: &T::AccountId, amount: BalanceOf) { let mut locked_balance = Self::get_locked_balance(staker).map(|l| l.amount).unwrap(); locked_balance -= amount; --- a/pallets/app-promotion/src/types.rs +++ b/pallets/app-promotion/src/types.rs @@ -9,7 +9,7 @@ use sp_runtime::DispatchError; use up_data_structs::{CollectionId, SponsorshipState}; use sp_std::borrow::ToOwned; - +use pallet_evm_contract_helpers::{Pallet as EvmHelpersPallet, Config as EvmHelpersConfig, Sponsoring}; pub trait ExtendedLockableCurrency: LockableCurrency { fn locks(who: KArg) -> WeakBoundedVec, Self::MaxLocks> @@ -94,3 +94,40 @@ .map(|acc| acc.to_owned())) } } + +pub trait ContractHandler { + type ContractId; + type AccountId; + + fn set_sponsor(sponsor_id: Self::AccountId, contract_id: Self::ContractId) -> DispatchResult; + + fn remove_contract_sponsor(collection_id: Self::ContractId) -> DispatchResult; + + fn get_sponsor(contract_id: Self::ContractId) + -> Result, DispatchError>; +} + +impl ContractHandler for EvmHelpersPallet { + type ContractId = sp_core::H160; + + type AccountId = T::CrossAccountId; + + fn set_sponsor(sponsor_id: Self::AccountId, contract_id: Self::ContractId) -> DispatchResult { + Sponsoring::::insert( + contract_id, + SponsorshipState::::Confirmed(sponsor_id), + ); + Ok(()) + } + + fn remove_contract_sponsor(contract_id: Self::ContractId) -> DispatchResult { + Sponsoring::::remove(contract_id); + Ok(()) + } + + fn get_sponsor( + contract_id: Self::ContractId, + ) -> Result, DispatchError> { + Ok(Self::get_sponsor(contract_id)) + } +} --- a/pallets/evm-contract-helpers/src/lib.rs +++ b/pallets/evm-contract-helpers/src/lib.rs @@ -75,7 +75,7 @@ /// * **Key** - contract address. /// * **Value** - sponsorship state. #[pallet::storage] - pub(super) type Sponsoring = StorageMap< + pub type Sponsoring = StorageMap< Hasher = Twox64Concat, Key = H160, Value = SponsorshipState, --- a/primitives/common/src/constants.rs +++ b/primitives/common/src/constants.rs @@ -22,6 +22,7 @@ use crate::types::{BlockNumber, Balance}; pub const MILLISECS_PER_BLOCK: u64 = 12000; +pub const MILLISECS_PER_RELAY_BLOCK: u64 = 6000; pub const SLOT_DURATION: u64 = MILLISECS_PER_BLOCK; @@ -30,6 +31,11 @@ pub const HOURS: BlockNumber = MINUTES * 60; pub const DAYS: BlockNumber = HOURS * 24; +// These time units are defined in number of relay blocks. +pub const RELAY_MINUTES: BlockNumber = 60_000 / (MILLISECS_PER_RELAY_BLOCK as BlockNumber); +pub const RELAY_HOURS: BlockNumber = RELAY_MINUTES * 60; +pub const RELAY_DAYS: BlockNumber = RELAY_HOURS * 24; + pub const MICROUNIQUE: Balance = 1_000_000_000_000; pub const MILLIUNIQUE: Balance = 1_000 * MICROUNIQUE; pub const CENTIUNIQUE: Balance = 10 * MILLIUNIQUE; --- a/runtime/common/config/pallets/app_promotion.rs +++ b/runtime/common/config/pallets/app_promotion.rs @@ -16,28 +16,40 @@ use crate::{ runtime_common::config::pallets::{TreasuryAccountId, RelayChainBlockNumberProvider}, - Runtime, Balances, BlockNumber, Unique, Event, + Runtime, Balances, BlockNumber, Unique, Event, EvmContractHelpers, }; use frame_support::{parameter_types, PalletId}; use sp_arithmetic::Perbill; use up_common::{ - constants::{DAYS, UNIQUE}, + constants::{DAYS, UNIQUE, RELAY_DAYS}, types::Balance, }; +#[cfg(all(not(feature = "unique-runtime"), not(feature = "quartz-runtime")))] parameter_types! { pub const AppPromotionId: PalletId = PalletId(*b"appstake"); pub const RecalculationInterval: BlockNumber = 20; - pub const PendingInterval: BlockNumber = 10; + pub const PendingInterval: BlockNumber = 20; pub const Nominal: Balance = UNIQUE; pub const Day: BlockNumber = DAYS; - pub IntervalIncome: Perbill = Perbill::from_rational(RecalculationInterval::get(), 2 * DAYS) * Perbill::from_rational(5u32, 10_000); + pub IntervalIncome: Perbill = Perbill::from_rational(RecalculationInterval::get(), RELAY_DAYS) * Perbill::from_rational(5u32, 10_000); +} + +#[cfg(any(feature = "unique-runtime", feature = "quartz-runtime"))] +parameter_types! { + pub const AppPromotionId: PalletId = PalletId(*b"appstake"); + pub const RecalculationInterval: BlockNumber = RELAY_DAYS; + pub const PendingInterval: BlockNumber = 7 * RELAY_DAYS; + pub const Nominal: Balance = UNIQUE; + pub const Day: BlockNumber = RELAY_DAYS; + pub IntervalIncome: Perbill = Perbill::from_rational(5u32, 10_000); } impl pallet_app_promotion::Config for Runtime { type PalletId = AppPromotionId; type CollectionHandler = Unique; + type ContractHandler = EvmContractHelpers; type Currency = Balances; type WeightInfo = pallet_app_promotion::weights::SubstrateWeight; type TreasuryAccountId = TreasuryAccountId; --- a/tests/src/app-promotion.test.ts +++ b/tests/src/app-promotion.test.ts @@ -706,6 +706,12 @@ nominal = helper.balance.getOneTokenNominal(); }); }); + + after(async function () { + await usingPlaygrounds(async (helper) => { + await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion())); + }); + }); it('will credit 0.05% for staking period', async () => { // arrange: bob.stake(10000); @@ -770,39 +776,24 @@ const staker = await createUser(40n * nominal); await waitForRecalculationBlock(helper.api!); - // const foo = await helper.api!.registry.getChainProperties(). + await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled; - // await waitNewBlocks(helper.api!, 1); + await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled; - // await waitNewBlocks(helper.api!, 1); + await expect(helper.signTransaction(staker, helper.api!.tx.promotion.stake(10n * nominal))).to.be.eventually.fulfilled; - // console.log(await helper.balance.getSubstrate(staker.address)); - // await waitNewBlocks(helper.api!, 17); + await waitForRelayBlock(helper.api!, 34); expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))) .map(([_, amount]) => amount.toBigInt())) .to.be.deep.equal([calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n), calculateIncome(10n * nominal, 10n)]); - // console.log(await getBlockNumber(helper.api!)); - // console.log((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([block, amount]) => [block.toBigInt(), amount.toBigInt()])); - // console.log(`${calculateIncome(10n * nominal, 10n)} || ${calculateIncome(10n * nominal, 10n, 2)}`); - // await waitNewBlocks(helper.api!, 10); await waitForRelayBlock(helper.api!, 20); - // console.log((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())); - // console.log(await helper.balance.getSubstrate(staker.address)); await expect(helper.signTransaction(staker, helper.api!.tx.promotion.unstake(calculateIncome(10n * nominal, 10n, 2) - 10n * nominal))).to.be.eventually.fulfilled; - // console.log(calculateIncome(10n * nominal, 10n, 2)); - // console.log(calculateIncome(10n * nominal, 10n, 3)); - // console.log(calculateIncome(10n * nominal, 10n, 4)); - // console.log(calculateIncome(10n * nominal, 10n, 5)); expect((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))) .map(([_, amount]) => amount.toBigInt())) .to.be.deep.equal([10n * nominal, calculateIncome(10n * nominal, 10n, 2), calculateIncome(10n * nominal, 10n, 2)]); - - // console.log((await helper.api!.rpc.unique.totalStakedPerBlock(normalizeAccountId(staker))).map(([_, amount]) => amount.toBigInt())); - - // console.log(await helper.balance.getSubstrate(staker.address)); }); }); --- a/tests/src/interfaces/augment-api-consts.ts +++ b/tests/src/interfaces/augment-api-consts.ts @@ -78,7 +78,7 @@ **/ palletId: FrameSupportPalletId & AugmentedConst; /** - * In chain blocks. + * In relay blocks. **/ pendingInterval: u32 & AugmentedConst; /** --- a/tests/src/interfaces/augment-api-errors.ts +++ b/tests/src/interfaces/augment-api-errors.ts @@ -430,11 +430,16 @@ [key: string]: AugmentedError; }; promotion: { + /** + * Error due to action requiring admin to be set + **/ AdminNotSet: AugmentedError; - AlreadySponsored: AugmentedError; + /** + * An error related to the fact that an invalid argument was passed to perform an action + **/ InvalidArgument: AugmentedError; /** - * No permission to perform action + * No permission to perform an action **/ NoPermission: AugmentedError; /** --- a/tests/src/interfaces/augment-api-query.ts +++ b/tests/src/interfaces/augment-api-query.ts @@ -525,7 +525,7 @@ **/ staked: AugmentedQuery Observable, [AccountId32, u32]> & QueryableStorageEntry; /** - * A block when app-promotion has started + * A block when app-promotion has started .I think this is redundant, because we only need `NextInterestBlock`. **/ startBlock: AugmentedQuery Observable, []> & QueryableStorageEntry; totalStaked: AugmentedQuery Observable, []> & QueryableStorageEntry; --- a/tests/src/interfaces/augment-api-tx.ts +++ b/tests/src/interfaces/augment-api-tx.ts @@ -365,9 +365,12 @@ promotion: { setAdminAddress: AugmentedSubmittable<(admin: PalletEvmAccountBasicCrossAccountIdRepr | { Substrate: any } | { Ethereum: any } | string | Uint8Array) => SubmittableExtrinsic, [PalletEvmAccountBasicCrossAccountIdRepr]>; sponsorCollection: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array) => SubmittableExtrinsic, [u32]>; + sponsorConract: AugmentedSubmittable<(contractId: H160 | string | Uint8Array) => SubmittableExtrinsic, [H160]>; stake: AugmentedSubmittable<(amount: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic, [u128]>; startAppPromotion: AugmentedSubmittable<(promotionStartRelayBlock: Option | null | Uint8Array | u32 | AnyNumber) => SubmittableExtrinsic, [Option]>; + stopAppPromotion: AugmentedSubmittable<() => SubmittableExtrinsic, []>; stopSponsorignCollection: AugmentedSubmittable<(collectionId: u32 | AnyNumber | Uint8Array) => SubmittableExtrinsic, [u32]>; + stopSponsorignContract: AugmentedSubmittable<(contractId: H160 | string | Uint8Array) => SubmittableExtrinsic, [H160]>; unstake: AugmentedSubmittable<(amount: u128 | AnyNumber | Uint8Array) => SubmittableExtrinsic, [u128]>; /** * Generic tx --- a/tests/src/interfaces/default/types.ts +++ b/tests/src/interfaces/default/types.ts @@ -816,6 +816,7 @@ readonly asStartAppPromotion: { readonly promotionStartRelayBlock: Option; } & Struct; + readonly isStopAppPromotion: boolean; readonly isStake: boolean; readonly asStake: { readonly amount: u128; @@ -832,7 +833,15 @@ readonly asStopSponsorignCollection: { readonly collectionId: u32; } & Struct; - readonly type: 'SetAdminAddress' | 'StartAppPromotion' | 'Stake' | 'Unstake' | 'SponsorCollection' | 'StopSponsorignCollection'; + readonly isSponsorConract: boolean; + readonly asSponsorConract: { + readonly contractId: H160; + } & Struct; + readonly isStopSponsorignContract: boolean; + readonly asStopSponsorignContract: { + readonly contractId: H160; + } & Struct; + readonly type: 'SetAdminAddress' | 'StartAppPromotion' | 'StopAppPromotion' | 'Stake' | 'Unstake' | 'SponsorCollection' | 'StopSponsorignCollection' | 'SponsorConract' | 'StopSponsorignContract'; } /** @name PalletAppPromotionError */ @@ -841,8 +850,7 @@ readonly isNoPermission: boolean; readonly isNotSufficientFounds: boolean; readonly isInvalidArgument: boolean; - readonly isAlreadySponsored: boolean; - readonly type: 'AdminNotSet' | 'NoPermission' | 'NotSufficientFounds' | 'InvalidArgument' | 'AlreadySponsored'; + readonly type: 'AdminNotSet' | 'NoPermission' | 'NotSufficientFounds' | 'InvalidArgument'; } /** @name PalletAppPromotionEvent */ --- a/tests/src/interfaces/lookup.ts +++ b/tests/src/interfaces/lookup.ts @@ -2463,6 +2463,7 @@ start_app_promotion: { promotionStartRelayBlock: 'Option', }, + stop_app_promotion: 'Null', stake: { amount: 'u128', }, @@ -2473,7 +2474,13 @@ collectionId: 'u32', }, stop_sponsorign_collection: { - collectionId: 'u32' + collectionId: 'u32', + }, + sponsor_conract: { + contractId: 'H160', + }, + stop_sponsorign_contract: { + contractId: 'H160' } } }, @@ -3098,7 +3105,7 @@ * Lookup410: pallet_app_promotion::pallet::Error **/ PalletAppPromotionError: { - _enum: ['AdminNotSet', 'NoPermission', 'NotSufficientFounds', 'InvalidArgument', 'AlreadySponsored'] + _enum: ['AdminNotSet', 'NoPermission', 'NotSufficientFounds', 'InvalidArgument'] }, /** * Lookup413: pallet_evm::pallet::Error --- a/tests/src/interfaces/types-lookup.ts +++ b/tests/src/interfaces/types-lookup.ts @@ -2669,6 +2669,7 @@ readonly asStartAppPromotion: { readonly promotionStartRelayBlock: Option; } & Struct; + readonly isStopAppPromotion: boolean; readonly isStake: boolean; readonly asStake: { readonly amount: u128; @@ -2685,7 +2686,15 @@ readonly asStopSponsorignCollection: { readonly collectionId: u32; } & Struct; - readonly type: 'SetAdminAddress' | 'StartAppPromotion' | 'Stake' | 'Unstake' | 'SponsorCollection' | 'StopSponsorignCollection'; + readonly isSponsorConract: boolean; + readonly asSponsorConract: { + readonly contractId: H160; + } & Struct; + readonly isStopSponsorignContract: boolean; + readonly asStopSponsorignContract: { + readonly contractId: H160; + } & Struct; + readonly type: 'SetAdminAddress' | 'StartAppPromotion' | 'StopAppPromotion' | 'Stake' | 'Unstake' | 'SponsorCollection' | 'StopSponsorignCollection' | 'SponsorConract' | 'StopSponsorignContract'; } /** @name PalletEvmCall (305) */ @@ -3288,8 +3297,7 @@ readonly isNoPermission: boolean; readonly isNotSufficientFounds: boolean; readonly isInvalidArgument: boolean; - readonly isAlreadySponsored: boolean; - readonly type: 'AdminNotSet' | 'NoPermission' | 'NotSufficientFounds' | 'InvalidArgument' | 'AlreadySponsored'; + readonly type: 'AdminNotSet' | 'NoPermission' | 'NotSufficientFounds' | 'InvalidArgument'; } /** @name PalletEvmError (413) */