diff --git a/Cargo.lock b/Cargo.lock index a7674adddf..452564a7c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6057,7 +6057,7 @@ dependencies = [ "pallet-evm-precompile-assets-erc20", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", - "pallet-evm-precompile-dapps-staking", + "pallet-evm-precompile-dapp-staking-v3", "pallet-evm-precompile-dispatch", "pallet-evm-precompile-ed25519", "pallet-evm-precompile-modexp", @@ -7970,6 +7970,34 @@ dependencies = [ "substrate-bn", ] +[[package]] +name = "pallet-evm-precompile-dapp-staking-v3" +version = "0.1.0" +dependencies = [ + "assert_matches", + "astar-primitives", + "derive_more", + "fp-evm", + "frame-support", + "frame-system", + "log", + "num_enum 0.5.11", + "pallet-balances", + "pallet-dapp-staking-v3", + "pallet-evm", + "pallet-timestamp", + "parity-scale-codec", + "precompile-utils", + "scale-info", + "serde", + "sha3", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-evm-precompile-dapps-staking" version = "3.6.3" @@ -13173,7 +13201,6 @@ dependencies = [ "pallet-balances", "pallet-block-rewards-hybrid", "pallet-chain-extension-assets", - "pallet-chain-extension-dapps-staking", "pallet-chain-extension-unified-accounts", "pallet-chain-extension-xvm", "pallet-collator-selection", diff --git a/Cargo.toml b/Cargo.toml index 41a2dead91..ca009583f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -295,6 +295,7 @@ pallet-evm-precompile-substrate-ecdsa = { path = "./precompiles/substrate-ecdsa" pallet-evm-precompile-xcm = { path = "./precompiles/xcm", default-features = false } pallet-evm-precompile-xvm = { path = "./precompiles/xvm", default-features = false } pallet-evm-precompile-dapps-staking = { path = "./precompiles/dapps-staking", default-features = false } +pallet-evm-precompile-dapp-staking-v3 = { path = "./precompiles/dapp-staking-v3", default-features = false } pallet-evm-precompile-unified-accounts = { path = "./precompiles/unified-accounts", default-features = false } pallet-chain-extension-dapps-staking = { path = "./chain-extensions/dapps-staking", default-features = false } diff --git a/pallets/dapp-staking-migration/src/mock.rs b/pallets/dapp-staking-migration/src/mock.rs index 062de7f1fa..c33aa09620 100644 --- a/pallets/dapp-staking-migration/src/mock.rs +++ b/pallets/dapp-staking-migration/src/mock.rs @@ -24,14 +24,13 @@ use frame_support::{ weights::Weight, PalletId, }; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use sp_arithmetic::fixed_point::FixedU64; use sp_core::H256; use sp_io::TestExternalities; use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; use astar_primitives::{ - dapp_staking::{CycleConfiguration, StakingRewardHandler}, + dapp_staking::{CycleConfiguration, SmartContract, StakingRewardHandler}, testing::Header, Balance, BlockNumber, }; @@ -134,17 +133,7 @@ impl StakingRewardHandler for DummyStakingRewardHandler { } } -#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] -pub enum MockSmartContract { - Wasm(AccountId), - Other(AccountId), -} - -impl Default for MockSmartContract { - fn default() -> Self { - MockSmartContract::Wasm(1) - } -} +pub(crate) type MockSmartContract = SmartContract; #[cfg(feature = "runtime-benchmarks")] pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md index c8de9b0036..48a5b531aa 100644 --- a/pallets/dapp-staking-v3/README.md +++ b/pallets/dapp-staking-v3/README.md @@ -227,7 +227,8 @@ be left out of tiers and won't earn **any** reward. In a special and unlikely case that two or more dApps have the exact same score and satisfy tier entry threshold, but there isn't enough leftover tier capacity to accomodate them all, this is considered _undefined_ behavior. Some of the dApps will manage to enter the tier, while others will be left out. There is no strict rule which defines this behavior - instead dApps are encouraged to ensure their tier entry by -having a larger stake than the other dApp(s). +having a larger stake than the other dApp(s). Tehnically, at the moment, the dApp with the lower `dApp Id` will have the advantage over a dApp with +the larger Id. ### Reward Expiry diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 715b4df674..a99b36af95 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -51,7 +51,7 @@ use sp_runtime::{ pub use sp_std::vec::Vec; use astar_primitives::{ - dapp_staking::{CycleConfiguration, StakingRewardHandler}, + dapp_staking::{CycleConfiguration, SmartContractHandle, StakingRewardHandler}, Balance, BlockNumber, }; @@ -118,7 +118,10 @@ pub mod pallet { >; /// Describes smart contract in the context required by dApp staking. - type SmartContract: Parameter + Member + MaxEncodedLen; + type SmartContract: Parameter + + Member + + MaxEncodedLen + + SmartContractHandle; /// Privileged origin for managing dApp staking pallet. type ManagerOrigin: EnsureOrigin<::RuntimeOrigin>; @@ -795,7 +798,7 @@ pub mod pallet { ledger.subtract_lock_amount(amount_to_unlock); let current_block = frame_system::Pallet::::block_number(); - let unlock_block = current_block.saturating_add(Self::unlock_period()); + let unlock_block = current_block.saturating_add(Self::unlocking_period()); ledger .add_unlocking_chunk(amount_to_unlock, unlock_block) .map_err(|_| Error::::TooManyUnlockingChunks)?; @@ -1137,8 +1140,9 @@ pub mod pallet { let earliest_staked_era = ledger .earliest_staked_era() .ok_or(Error::::InternalClaimStakerError)?; - let era_rewards = EraRewards::::get(Self::era_reward_index(earliest_staked_era)) - .ok_or(Error::::NoClaimableRewards)?; + let era_rewards = + EraRewards::::get(Self::era_reward_span_index(earliest_staked_era)) + .ok_or(Error::::NoClaimableRewards)?; // The last era for which we can theoretically claim rewards. // And indicator if we know the period's ending era. @@ -1545,7 +1549,7 @@ pub mod pallet { } /// Calculates the `EraRewardSpan` index for the specified era. - pub(crate) fn era_reward_index(era: EraNumber) -> EraNumber { + pub fn era_reward_span_index(era: EraNumber) -> EraNumber { era.saturating_sub(era % T::EraRewardSpanLength::get()) } @@ -1556,7 +1560,7 @@ pub mod pallet { } /// Unlocking period expressed in the number of blocks. - pub(crate) fn unlock_period() -> BlockNumber { + pub fn unlocking_period() -> BlockNumber { T::CycleConfiguration::blocks_per_era().saturating_mul(T::UnlockingPeriod::get().into()) } @@ -1655,7 +1659,9 @@ pub mod pallet { // In case when tier has 1 more free slot, but two dApps with exactly same score satisfy the threshold, // one of them will be assigned to the tier, and the other one will be assigned to the lower tier, if it exists. // - // There is no explicit definition of which dApp gets the advantage - it's decided by dApp IDs hash & the unstable sort algorithm. + // In the current implementation, the dApp with the lower dApp Id has the advantage. + // There is no guarantee this will persist in the future, so it's best for dApps to do their + // best to avoid getting themselves into such situations. // 4. Calculate rewards. let tier_rewards = tier_config @@ -1842,7 +1848,7 @@ pub mod pallet { CurrentEraInfo::::put(era_info); - let era_span_index = Self::era_reward_index(current_era); + let era_span_index = Self::era_reward_span_index(current_era); let mut span = EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpan::new()); if let Err(_) = span.push(current_era, era_reward) { // This must never happen but we log the error just in case. diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 8be66cd86a..3f116c6bbc 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -30,7 +30,6 @@ use frame_support::{ }, weights::Weight, }; -use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use sp_arithmetic::fixed_point::FixedU64; use sp_core::H256; use sp_io::TestExternalities; @@ -40,7 +39,7 @@ use sp_runtime::{ }; use sp_std::cell::RefCell; -use astar_primitives::{testing::Header, Balance, BlockNumber}; +use astar_primitives::{dapp_staking::SmartContract, testing::Header, Balance, BlockNumber}; pub(crate) type AccountId = u64; @@ -146,17 +145,7 @@ impl StakingRewardHandler for DummyStakingRewardHandler { } } -#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, TypeInfo, MaxEncodedLen, Hash)] -pub enum MockSmartContract { - Wasm(AccountId), - Other(AccountId), -} - -impl Default for MockSmartContract { - fn default() -> Self { - MockSmartContract::Wasm(1) - } -} +pub(crate) type MockSmartContract = SmartContract; #[cfg(feature = "runtime-benchmarks")] pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); @@ -165,7 +154,7 @@ impl crate::BenchmarkHelper for BenchmarkHelper { fn get_smart_contract(id: u32) -> MockSmartContract { - MockSmartContract::Wasm(id as AccountId) + MockSmartContract::wasm(id as AccountId) } fn set_balance(account: &AccountId, amount: Balance) { diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 29cfd9e40d..a973bba7ac 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -1288,7 +1288,7 @@ pub(crate) fn assert_block_bump(pre_snapshot: &MemorySnapshot) { } // 4. Verify era reward - let era_span_index = DappStaking::era_reward_index(pre_protoc_state.era); + let era_span_index = DappStaking::era_reward_span_index(pre_protoc_state.era); let maybe_pre_era_reward_span = pre_snapshot.era_rewards.get(&era_span_index); let post_era_reward_span = post_snapshot .era_rewards @@ -1467,10 +1467,10 @@ pub(crate) fn required_number_of_reward_claims(account: AccountId) -> u32 { }; let era_span_length: EraNumber = ::EraRewardSpanLength::get(); - let first = DappStaking::era_reward_index(range.0) + let first = DappStaking::era_reward_span_index(range.0) .checked_div(era_span_length) .unwrap(); - let second = DappStaking::era_reward_index(range.1) + let second = DappStaking::era_reward_span_index(range.1) .checked_div(era_span_length) .unwrap(); diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 40dba6ab31..d6154a319a 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -29,7 +29,10 @@ use frame_support::{ }; use sp_runtime::traits::Zero; -use astar_primitives::{dapp_staking::CycleConfiguration, Balance, BlockNumber}; +use astar_primitives::{ + dapp_staking::{CycleConfiguration, SmartContractHandle}, + Balance, BlockNumber, +}; #[test] fn maintenace_mode_works() { @@ -104,11 +107,19 @@ fn maintenace_mode_call_filtering_works() { Error::::Disabled ); assert_noop!( - DappStaking::stake(RuntimeOrigin::signed(1), MockSmartContract::default(), 100), + DappStaking::stake( + RuntimeOrigin::signed(1), + MockSmartContract::wasm(1 as AccountId), + 100 + ), Error::::Disabled ); assert_noop!( - DappStaking::unstake(RuntimeOrigin::signed(1), MockSmartContract::default(), 100), + DappStaking::unstake( + RuntimeOrigin::signed(1), + MockSmartContract::wasm(1 as AccountId), + 100 + ), Error::::Disabled ); assert_noop!( @@ -116,13 +127,16 @@ fn maintenace_mode_call_filtering_works() { Error::::Disabled ); assert_noop!( - DappStaking::claim_bonus_reward(RuntimeOrigin::signed(1), MockSmartContract::default()), + DappStaking::claim_bonus_reward( + RuntimeOrigin::signed(1), + MockSmartContract::wasm(1 as AccountId) + ), Error::::Disabled ); assert_noop!( DappStaking::claim_dapp_reward( RuntimeOrigin::signed(1), - MockSmartContract::default(), + MockSmartContract::wasm(1 as AccountId), 1 ), Error::::Disabled @@ -130,7 +144,7 @@ fn maintenace_mode_call_filtering_works() { assert_noop!( DappStaking::unstake_from_unregistered( RuntimeOrigin::signed(1), - MockSmartContract::default() + MockSmartContract::wasm(1 as AccountId) ), Error::::Disabled ); @@ -671,7 +685,7 @@ fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { #[test] fn claim_unlocked_is_ok() { ExtBuilder::build().execute_with(|| { - let unlocking_blocks = DappStaking::unlock_period(); + let unlocking_blocks = DappStaking::unlocking_period(); // Lock some amount in a few eras let account = 2; @@ -721,7 +735,7 @@ fn claim_unlocked_no_eligible_chunks_fails() { // Cannot claim if unlock period hasn't passed yet let lock_amount = 103; assert_lock(account, lock_amount); - let unlocking_blocks = DappStaking::unlock_period(); + let unlocking_blocks = DappStaking::unlocking_period(); run_for_blocks(unlocking_blocks - 1); assert_noop!( DappStaking::claim_unlocked(RuntimeOrigin::signed(account)), @@ -799,7 +813,7 @@ fn relock_unlocking_insufficient_lock_amount_fails() { }); // Make sure only one chunk is left - let unlocking_blocks = DappStaking::unlock_period(); + let unlocking_blocks = DappStaking::unlocking_period(); run_for_blocks(unlocking_blocks - 1); assert_claim_unlocked(account); @@ -815,7 +829,7 @@ fn stake_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -834,7 +848,7 @@ fn stake_after_expiry_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); // Lock & stake some amount @@ -867,7 +881,7 @@ fn stake_after_expiry_is_ok() { fn stake_with_zero_amount_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; assert_lock(account, 300); @@ -886,7 +900,7 @@ fn stake_on_invalid_dapp_fails() { assert_lock(account, 300); // Try to stake on non-existing contract - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_noop!( DappStaking::stake(RuntimeOrigin::signed(account), smart_contract, 100), Error::::NotOperatedDApp @@ -906,7 +920,7 @@ fn stake_on_invalid_dapp_fails() { fn stake_in_final_era_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); let account = 2; assert_register(1, &smart_contract); assert_lock(account, 300); @@ -929,7 +943,7 @@ fn stake_in_final_era_fails() { fn stake_fails_if_unclaimed_staker_rewards_from_past_remain() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); let account = 2; assert_register(1, &smart_contract); assert_lock(account, 300); @@ -957,7 +971,7 @@ fn stake_fails_if_unclaimed_staker_rewards_from_past_remain() { fn stake_fails_if_claimable_bonus_rewards_from_past_remain() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); let account = 2; assert_register(1, &smart_contract); assert_lock(account, 300); @@ -1101,7 +1115,7 @@ fn unstake_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1123,7 +1137,7 @@ fn unstake_with_leftover_amount_below_minimum_works() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1142,7 +1156,7 @@ fn unstake_with_leftover_amount_below_minimum_works() { fn unstake_with_zero_amount_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract & lock some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; assert_lock(account, 300); @@ -1162,7 +1176,7 @@ fn unstake_on_invalid_dapp_fails() { assert_lock(account, 300); // Try to unstake from non-existing contract - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_noop!( DappStaking::unstake(RuntimeOrigin::signed(account), smart_contract, 100), Error::::NotOperatedDApp @@ -1289,7 +1303,7 @@ fn claim_staker_rewards_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1324,7 +1338,7 @@ fn claim_staker_rewards_double_call_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1351,7 +1365,7 @@ fn claim_staker_rewards_no_claimable_rewards_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1391,7 +1405,7 @@ fn claim_staker_rewards_after_expiry_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1440,7 +1454,7 @@ fn claim_staker_rewards_after_expiry_fails() { fn claim_staker_rewards_fails_due_to_payout_failure() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1469,7 +1483,7 @@ fn claim_bonus_reward_works() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1504,7 +1518,7 @@ fn claim_bonus_reward_double_call_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1529,7 +1543,7 @@ fn claim_bonus_reward_when_nothing_to_claim_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1557,7 +1571,7 @@ fn claim_bonus_reward_with_only_build_and_earn_stake_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1587,7 +1601,7 @@ fn claim_bonus_reward_after_expiry_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1622,7 +1636,7 @@ fn claim_bonus_reward_after_expiry_fails() { fn claim_bonus_reward_fails_due_to_payout_failure() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1651,7 +1665,7 @@ fn claim_dapp_reward_works() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; @@ -1684,7 +1698,7 @@ fn claim_dapp_reward_works() { #[test] fn claim_dapp_reward_from_non_existing_contract_fails() { ExtBuilder::build().execute_with(|| { - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_noop!( DappStaking::claim_dapp_reward(RuntimeOrigin::signed(1), smart_contract, 1), Error::::ContractNotFound, @@ -1696,7 +1710,7 @@ fn claim_dapp_reward_from_non_existing_contract_fails() { fn claim_dapp_reward_from_invalid_era_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1758,7 +1772,7 @@ fn claim_dapp_reward_if_dapp_not_in_any_tier_fails() { fn claim_dapp_reward_twice_for_same_era_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1791,7 +1805,7 @@ fn claim_dapp_reward_twice_for_same_era_fails() { fn claim_dapp_reward_for_expired_era_fails() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1821,7 +1835,7 @@ fn claim_dapp_reward_for_expired_era_fails() { fn claim_dapp_reward_fails_due_to_payout_failure() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1857,7 +1871,7 @@ fn claim_dapp_reward_fails_due_to_payout_failure() { fn unstake_from_unregistered_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1875,7 +1889,7 @@ fn unstake_from_unregistered_is_ok() { fn unstake_from_unregistered_fails_for_active_contract() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -1894,7 +1908,7 @@ fn unstake_from_unregistered_fails_for_active_contract() { fn unstake_from_unregistered_fails_for_not_staked_contract() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); assert_unregister(&smart_contract); @@ -1909,7 +1923,7 @@ fn unstake_from_unregistered_fails_for_not_staked_contract() { fn unstake_from_unregistered_fails_for_past_period() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -2384,7 +2398,7 @@ fn advance_for_some_periods_works() { fn unlock_after_staked_period_ends_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let account = 2; @@ -2445,7 +2459,7 @@ fn stake_and_unstake_after_reward_claim_is_ok() { ExtBuilder::build().execute_with(|| { // Register smart contract, lock&stake some amount let dev_account = 1; - let smart_contract = MockSmartContract::default(); + let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(dev_account, &smart_contract); let account = 2; diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index a634e2ad75..73c26974ee 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -84,6 +84,12 @@ pub type AccountLedgerFor = AccountLedger<::MaxUnlockingChunks>; pub type DAppTierRewardsFor = DAppTierRewards<::MaxNumberOfContracts, ::NumberOfTiers>; +// Convenience type for `EraRewardSpan` usage. +pub type EraRewardSpanFor = EraRewardSpan<::EraRewardSpanLength>; + +// Convenience type for `DAppInfo` usage. +pub type DAppInfoFor = DAppInfo<::AccountId>; + /// Era number type pub type EraNumber = u32; /// Period number type diff --git a/precompiles/dapp-staking-v3/Cargo.toml b/precompiles/dapp-staking-v3/Cargo.toml new file mode 100644 index 0000000000..5c40d95a5f --- /dev/null +++ b/precompiles/dapp-staking-v3/Cargo.toml @@ -0,0 +1,62 @@ +[package] +name = "pallet-evm-precompile-dapp-staking-v3" +version = "0.1.0" +license = "GPL-3.0-or-later" +description = "dApp Staking EVM precompiles" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +log = { workspace = true } +num_enum = { workspace = true } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } + +frame-support = { workspace = true } +frame-system = { workspace = true } + +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Astar +astar-primitives = { workspace = true } +pallet-dapp-staking-v3 = { workspace = true } +precompile-utils = { workspace = true, default-features = false } + +# Frontier +fp-evm = { workspace = true } +pallet-evm = { workspace = true } + +[dev-dependencies] +assert_matches = { workspace = true } +derive_more = { workspace = true } +pallet-balances = { workspace = true, features = ["std"] } +pallet-timestamp = { workspace = true } +precompile-utils = { workspace = true, features = ["testing"] } +serde = { workspace = true } +sha3 = { workspace = true } +sp-arithmetic = { workspace = true } +sp-io = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "scale-info/std", + "astar-primitives/std", + "sp-std/std", + "sp-core/std", + "sp-runtime/std", + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "pallet-dapp-staking-v3/std", + "pallet-evm/std", + "precompile-utils/std", + "pallet-balances/std", + "sp-arithmetic/std", +] +runtime-benchmarks = ["pallet-dapp-staking-v3/runtime-benchmarks"] diff --git a/precompiles/dapp-staking-v3/DappsStakingV1.sol b/precompiles/dapp-staking-v3/DappsStakingV1.sol new file mode 100644 index 0000000000..9b13429413 --- /dev/null +++ b/precompiles/dapp-staking-v3/DappsStakingV1.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: BSD-3-Clause + +pragma solidity >=0.8.0; + +/// Predeployed at the address 0x0000000000000000000000000000000000005001 +/// For better understanding check the source code: +/// repo: https://github.com/AstarNetwork/Astar +/// +/// **NOTE:** This is a soft-deprecated interface used by the old dApps staking v2. +/// It is still supported by the network, but doesn't reflect how dApp staking v3 should be used. +/// Please refer to the `v2` interface for the latest version of the dApp staking contract. +/// +/// It is possible that dApp staking feature will once again evolve in the future so all developers are encouraged +/// to keep their smart contracts which utilize dApp staking precompile interface as upgradable, or implement their logic +/// in such a way it's relatively simple to migrate to the new version of the interface. +interface DappsStaking { + + // Types + + /// Instruction how to handle reward payout for staker. + /// `FreeBalance` - Reward will be paid out to the staker (free balance). + /// `StakeBalance` - Reward will be paid out to the staker and is immediately restaked (locked balance) + enum RewardDestination {FreeBalance, StakeBalance} + + // Storage getters + + /// @notice Read current era. + /// @return era: The current era + function read_current_era() external view returns (uint256); + + /// @notice Read the unbonding period (or unlocking period) in the number of eras. + /// @return period: The unbonding period in eras + function read_unbonding_period() external view returns (uint256); + + /// @notice Read Total network reward for the given era - sum of staker & dApp rewards. + /// @return reward: Total network reward for the given era + function read_era_reward(uint32 era) external view returns (uint128); + + /// @notice Read Total staked amount for the given era + /// @return staked: Total staked amount for the given era + function read_era_staked(uint32 era) external view returns (uint128); + + /// @notice Read Staked amount for the staker + /// @param staker: The staker address in form of 20 or 32 hex bytes + /// @return amount: Staked amount by the staker + function read_staked_amount(bytes calldata staker) external view returns (uint128); + + /// @notice Read Staked amount on a given contract for the staker + /// @param contract_id: The smart contract address used for staking + /// @param staker: The staker address in form of 20 or 32 hex bytes + /// @return amount: Staked amount by the staker + function read_staked_amount_on_contract(address contract_id, bytes calldata staker) external view returns (uint128); + + /// @notice Read the staked amount from the era when the amount was last staked/unstaked + /// @return total: The most recent total staked amount on contract + function read_contract_stake(address contract_id) external view returns (uint128); + + + // Extrinsic calls + + /// @notice Register is root origin only and not allowed via evm precompile. + /// This should always fail. + function register(address) external returns (bool); + + /// @notice Stake provided amount on the contract. + function bond_and_stake(address, uint128) external returns (bool); + + /// @notice Start unbonding process and unstake balance from the contract. + function unbond_and_unstake(address, uint128) external returns (bool); + + /// @notice Withdraw all funds that have completed the unbonding process. + function withdraw_unbonded() external returns (bool); + + /// @notice Claim earned staker rewards for the oldest unclaimed era. + /// In order to claim multiple eras, this call has to be called multiple times. + /// Staker account is derived from the caller address. + /// @param smart_contract: The smart contract address used for staking + function claim_staker(address smart_contract) external returns (bool); + + /// @notice Claim one era of unclaimed dapp rewards for the specified contract and era. + /// @param smart_contract: The smart contract address used for staking + /// @param era: The era to be claimed + function claim_dapp(address smart_contract, uint128 era) external returns (bool); + + /// @notice Set reward destination for staker rewards + /// @param reward_destination: The instruction on how the reward payout should be handled + function set_reward_destination(RewardDestination reward_destination) external returns (bool); + + /// @notice Withdraw staked funds from an unregistered contract. + /// @param smart_contract: The smart contract address used for staking + function withdraw_from_unregistered(address smart_contract) external returns (bool); + + /// @notice Transfer part or entire nomination from origin smart contract to target smart contract + /// @param origin_smart_contract: The origin smart contract address + /// @param amount: The amount to transfer from origin to target + /// @param target_smart_contract: The target smart contract address + function nomination_transfer(address origin_smart_contract, uint128 amount, address target_smart_contract) external returns (bool); +} diff --git a/precompiles/dapp-staking-v3/DappsStakingV2.sol b/precompiles/dapp-staking-v3/DappsStakingV2.sol new file mode 100644 index 0000000000..40c55af6c4 --- /dev/null +++ b/precompiles/dapp-staking-v3/DappsStakingV2.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: BSD-3-Clause + +pragma solidity >=0.8.0; + +/// Predeployed at the address 0x0000000000000000000000000000000000005001 +/// For better understanding check the source code: +/// repo: https://github.com/AstarNetwork/Astar +/// code: pallets/dapp-staking-v3 +interface DAppStaking { + + // Types + + /// Describes the subperiod in which the protocol currently is. + enum Subperiod {Voting, BuildAndEarn} + + /// Describes current smart contract types supported by the network. + enum SmartContractType {EVM, WASM} + + /// @notice Describes protocol state. + /// @param era: Ongoing era number. + /// @param period: Ongoing period number. + /// @param subperiod: Ongoing subperiod type. + struct ProtocolState { + uint256 era; + uint256 period; + Subperiod subperiod; + } + + /// @notice Used to describe smart contract. Astar supports both EVM & WASM smart contracts + /// so it's important to differentiate between the two. This approach also allows + /// easy extensibility in the future. + /// @param contract_type: Type of the smart contract to be used + struct SmartContract { + SmartContractType contract_type; + bytes contract_address; + } + + // Storage getters + + /// @notice Get the current protocol state. + /// @return (current era, current period number, current subperiod type). + function protocol_state() external view returns (ProtocolState memory); + + /// @notice Get the unlocking period expressed in the number of blocks. + /// @return period: The unlocking period expressed in the number of blocks. + function unlocking_period() external view returns (uint256); + + + // Extrinsic calls + + /// @notice Lock the given amount of tokens into dApp staking protocol. + /// @param amount: The amount of tokens to be locked. + function lock(uint128 amount) external returns (bool); + + /// @notice Start the unlocking process for the given amount of tokens. + /// @param amount: The amount of tokens to be unlocked. + function unlock(uint128 amount) external returns (bool); + + /// @notice Claims unlocked tokens, if there are any + function claim_unlocked() external returns (bool); + + /// @notice Stake the given amount of tokens on the specified smart contract. + /// The amount specified must be precise, otherwise the call will fail. + /// @param smart_contract: The smart contract to be staked on. + /// @param amount: The amount of tokens to be staked. + function stake(SmartContract calldata smart_contract, uint128 amount) external returns (bool); + + /// @notice Unstake the given amount of tokens from the specified smart contract. + /// The amount specified must be precise, otherwise the call will fail. + /// @param smart_contract: The smart contract to be unstaked from. + /// @param amount: The amount of tokens to be unstaked. + function unstake(SmartContract calldata smart_contract, uint128 amount) external returns (bool); + + /// @notice Claims one or more pending staker rewards. + function claim_staker_rewards() external returns (bool); + + /// @notice Claim the bonus reward for the specified smart contract. + /// @param smart_contract: The smart contract for which the bonus reward should be claimed. + function claim_bonus_reward(SmartContract calldata smart_contract) external returns (bool); + + /// @notice Claim dApp reward for the specified smart contract & era. + /// @param smart_contract: The smart contract for which the dApp reward should be claimed. + /// @param era: The era for which the dApp reward should be claimed. + function claim_dapp_reward(SmartContract calldata smart_contract, uint256 era) external returns (bool); + + /// @notice Unstake all funds from the unregistered smart contract. + /// @param smart_contract: The smart contract which was unregistered and from which all funds should be unstaked. + function unstake_from_unregistered(SmartContract calldata smart_contract) external returns (bool); + + /// @notice Used to cleanup all expired contract stake entries from the caller. + function cleanup_expired_entries() external returns (bool); +} diff --git a/precompiles/dapp-staking-v3/src/lib.rs b/precompiles/dapp-staking-v3/src/lib.rs new file mode 100644 index 0000000000..5178395be5 --- /dev/null +++ b/precompiles/dapp-staking-v3/src/lib.rs @@ -0,0 +1,832 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +//! Astar dApp staking interface. + +#![cfg_attr(not(feature = "std"), no_std)] + +use fp_evm::PrecompileHandle; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use parity_scale_codec::MaxEncodedLen; + +use frame_support::{ + dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}, + ensure, + traits::ConstU32, +}; + +use pallet_evm::AddressMapping; +use precompile_utils::{ + prelude::*, + solidity::{ + codec::{Reader, Writer}, + Codec, + }, +}; +use sp_core::{Get, H160, U256}; +use sp_runtime::traits::Zero; +use sp_std::{marker::PhantomData, prelude::*}; +extern crate alloc; + +use astar_primitives::{dapp_staking::SmartContractHandle, AccountId, Balance}; +use pallet_dapp_staking_v3::{ + AccountLedgerFor, ActiveProtocolState, ContractStake, ContractStakeAmount, CurrentEraInfo, + DAppInfoFor, EraInfo, EraRewardSpanFor, EraRewards, IntegratedDApps, Ledger, + Pallet as DAppStaking, ProtocolState, SingularStakingInfo, StakerInfo, Subperiod, +}; + +pub const STAKER_BYTES_LIMIT: u32 = 32; +type GetStakerBytesLimit = ConstU32; + +pub type DynamicAddress = BoundedBytes; + +#[cfg(test)] +mod test; + +/// Helper struct used to encode protocol state. +#[derive(Debug, Clone, solidity::Codec)] +pub(crate) struct PrecompileProtocolState { + era: U256, + period: U256, + subperiod: u8, +} + +/// Helper struct used to encode different smart contract types for the v2 interface. +#[derive(Debug, Clone, solidity::Codec)] +pub struct SmartContractV2 { + contract_type: SmartContractTypes, + address: DynamicAddress, +} + +/// Convenience type for smart contract type handling. +#[derive(Clone, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +pub(crate) enum SmartContractTypes { + Evm, + Wasm, +} + +impl Codec for SmartContractTypes { + fn read(reader: &mut Reader) -> MayRevert { + let value256: U256 = reader + .read() + .map_err(|_| RevertReason::read_out_of_bounds(Self::signature()))?; + + let value_as_u8: u8 = value256 + .try_into() + .map_err(|_| RevertReason::value_is_too_large(Self::signature()))?; + + value_as_u8 + .try_into() + .map_err(|_| RevertReason::custom("Unknown smart contract type").into()) + } + + fn write(writer: &mut Writer, value: Self) { + let value_as_u8: u8 = value.into(); + U256::write(writer, value_as_u8.into()); + } + + fn has_static_size() -> bool { + true + } + + fn signature() -> String { + "uint8".into() + } +} + +pub struct DappStakingV3Precompile(PhantomData); +#[precompile_utils::precompile] +impl DappStakingV3Precompile +where + R: pallet_evm::Config + + pallet_dapp_staking_v3::Config + + frame_system::Config, + ::RuntimeOrigin: From>, + R::RuntimeCall: Dispatchable + GetDispatchInfo, + R::RuntimeCall: From>, +{ + // v1 functions + + /// Read the ongoing `era` number. + #[precompile::public("read_current_era()")] + #[precompile::view] + fn read_current_era(handle: &mut impl PrecompileHandle) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + handle.record_db_read::(8 + ProtocolState::max_encoded_len())?; + + let current_era = ActiveProtocolState::::get().era; + + Ok(current_era.into()) + } + + /// Read the `unbonding period` or `unlocking period` expressed in the number of eras. + #[precompile::public("read_unbonding_period()")] + #[precompile::view] + fn read_unbonding_period(_: &mut impl PrecompileHandle) -> EvmResult { + // constant, no DB read + Ok(::UnlockingPeriod::get().into()) + } + + /// Read the total assigned reward pool for the given era. + /// + /// Total amount is sum of staker & dApp rewards. + #[precompile::public("read_era_reward(uint32)")] + #[precompile::view] + fn read_era_reward(handle: &mut impl PrecompileHandle, era: u32) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: EraRewards: + // Twox64Concat(8) + EraIndex(4) + EraRewardSpanFor::max_encoded_len + handle.record_db_read::(12 + EraRewardSpanFor::::max_encoded_len())?; + + // Get the appropriate era reward span + let era_span_index = DAppStaking::::era_reward_span_index(era); + let reward_span = + EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpanFor::::new()); + + // Sum up staker & dApp reward pools for the era + let reward = reward_span.get(era).map_or(Zero::zero(), |r| { + r.staker_reward_pool.saturating_add(r.dapp_reward_pool) + }); + + Ok(reward) + } + + /// Read the total staked amount for the given era. + /// + /// In case era is very far away in history, it's possible that the information is not available. + /// In that case, zero is returned. + /// + /// This is safe to use for current era and the next one. + #[precompile::public("read_era_staked(uint32)")] + #[precompile::view] + fn read_era_staked(handle: &mut impl PrecompileHandle, era: u32) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + handle.record_db_read::(8 + ProtocolState::max_encoded_len())?; + + let current_era = ActiveProtocolState::::get().era; + + // There are few distinct scenenarios: + // 1. Era is in the past so the value might exist. + // 2. Era is current or the next one, in which case we definitely have that information. + // 3. Era is from the future (more than the next era), in which case we don't have that information. + if era < current_era { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: EraRewards: + // Twox64Concat(8) + Twox64Concat(8 + EraIndex(4)) + EraRewardSpanFor::max_encoded_len + handle.record_db_read::(20 + EraRewardSpanFor::::max_encoded_len())?; + + let era_span_index = DAppStaking::::era_reward_span_index(era); + let reward_span = + EraRewards::::get(&era_span_index).unwrap_or(EraRewardSpanFor::::new()); + + let staked = reward_span.get(era).map_or(Zero::zero(), |r| r.staked); + + Ok(staked.into()) + } else if era == current_era || era == current_era.saturating_add(1) { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: CurrentEraInfo: + // Twox64Concat(8) + EraInfo::max_encoded_len + handle.record_db_read::(8 + EraInfo::max_encoded_len())?; + + let current_era_info = CurrentEraInfo::::get(); + + if era == current_era { + Ok(current_era_info.current_stake_amount.total()) + } else { + Ok(current_era_info.next_stake_amount.total()) + } + } else { + Err(RevertReason::custom("Era is in the future").into()) + } + } + + /// Read the total staked amount by the given account. + #[precompile::public("read_staked_amount(bytes)")] + #[precompile::view] + fn read_staked_amount( + handle: &mut impl PrecompileHandle, + staker: DynamicAddress, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + // Storage item: Ledger: + // Blake2_128Concat(16 + SmartContract::max_encoded_len) + Ledger::max_encoded_len + handle.record_db_read::( + 24 + AccountLedgerFor::::max_encoded_len() + + ProtocolState::max_encoded_len() + + ::SmartContract::max_encoded_len(), + )?; + + let staker = Self::parse_input_address(staker.into())?; + + // read the account's ledger + let ledger = Ledger::::get(&staker); + log::trace!(target: "ds-precompile", "read_staked_amount for account: {:?}, ledger: {:?}", staker, ledger); + + // Make sure to check staked amount against the ongoing period (past period stakes are reset to zero). + let current_period_number = ActiveProtocolState::::get().period_number(); + + Ok(ledger.staked_amount(current_period_number)) + } + + /// Read the total staked amount by the given staker on the given contract. + #[precompile::public("read_staked_amount_on_contract(address,bytes)")] + #[precompile::view] + fn read_staked_amount_on_contract( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + staker: DynamicAddress, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + // Storage item: StakerInfo: + // Blake2_128Concat(16 + SmartContract::max_encoded_len) + SingularStakingInfo::max_encoded_len + handle.record_db_read::( + 24 + ProtocolState::max_encoded_len() + + ::SmartContract::max_encoded_len() + + SingularStakingInfo::max_encoded_len(), + )?; + + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + + // parse the staker account + let staker = Self::parse_input_address(staker.into())?; + + // Get staking info for the staker/contract combination + let staking_info = StakerInfo::::get(&staker, &smart_contract).unwrap_or_default(); + log::trace!(target: "ds-precompile", "read_staked_amount_on_contract for account:{:?}, staking_info: {:?}", staker, staking_info); + + // Ensure that the staking info is checked against the current period (stakes from past periods are reset) + let current_period_number = ActiveProtocolState::::get().period_number(); + + if staking_info.period_number() == current_period_number { + Ok(staking_info.total_staked_amount()) + } else { + Ok(0_u128) + } + } + + /// Read the total amount staked on the given contract right now. + #[precompile::public("read_contract_stake(address)")] + #[precompile::view] + fn read_contract_stake( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + // Storage item: IntegratedDApps: + // Blake2_128Concat(16 + SmartContract::max_encoded_len) + DAppInfoFor::max_encoded_len + // Storage item: ContractStake: + // Twox64Concat(8) + EraIndex(4) + ContractStakeAmount::max_encoded_len + handle.record_db_read::( + 36 + ProtocolState::max_encoded_len() + + ::SmartContract::max_encoded_len() + + DAppInfoFor::::max_encoded_len() + + ContractStakeAmount::max_encoded_len(), + )?; + + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + + let current_period_number = ActiveProtocolState::::get().period_number(); + let dapp_info = match IntegratedDApps::::get(&smart_contract) { + Some(dapp_info) => dapp_info, + None => { + // If the contract is not registered, return 0 to keep the legacy behavior. + return Ok(0_u128); + } + }; + + // call pallet-dapps-staking + let contract_stake = ContractStake::::get(&dapp_info.id); + + Ok(contract_stake.total_staked_amount(current_period_number)) + } + + /// Register contract with the dapp-staking pallet + /// Register is root origin only. This should always fail when called via evm precompile. + #[precompile::public("register(address)")] + fn register(_: &mut impl PrecompileHandle, _address: Address) -> EvmResult { + // register is root-origin call. it should always fail when called via evm precompiles. + Err(RevertReason::custom("register via evm precompile is not allowed").into()) + } + + /// Lock & stake some amount on the specified contract. + /// + /// In case existing `stakeable` is sufficient to cover the given `amount`, only the `stake` operation is performed. + /// Otherwise, best effort is done to lock the additional amount so `stakeable` amount can cover the given `amount`. + #[precompile::public("bond_and_stake(address,uint128)")] + fn bond_and_stake( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + amount: u128, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + // Storage item: Ledger: + // Blake2_128Concat(16 + SmartContract::max_encoded_len()) + Ledger::max_encoded_len + handle.record_db_read::( + 24 + AccountLedgerFor::::max_encoded_len() + + ProtocolState::max_encoded_len() + + ::SmartContract::max_encoded_len(), + )?; + + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + log::trace!(target: "ds-precompile", "bond_and_stake {:?}, {:?}", smart_contract, amount); + + // Read total locked & staked amounts + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let protocol_state = ActiveProtocolState::::get(); + let ledger = Ledger::::get(&origin); + + // Check if stakeable amount is enough to cover the given `amount` + let stakeable_amount = ledger.stakeable_amount(protocol_state.period_number()); + + // If it isn't, we need to first lock the additional amount. + if stakeable_amount < amount { + let delta = amount.saturating_sub(stakeable_amount); + + let lock_call = pallet_dapp_staking_v3::Call::::lock { amount: delta }; + RuntimeHelper::::try_dispatch(handle, Some(origin.clone()).into(), lock_call)?; + } + + // Now, with best effort, we can try & stake the given `value`. + let stake_call = pallet_dapp_staking_v3::Call::::stake { + smart_contract, + amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), stake_call)?; + + Ok(true) + } + + /// Start unbonding process and unstake balance from the contract. + #[precompile::public("unbond_and_unstake(address,uint128)")] + fn unbond_and_unstake( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + amount: u128, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + // Storage item: StakerInfo: + // Blake2_128Concat(16 + SmartContract::max_encoded_len) + SingularStakingInfo::max_encoded_len + handle.record_db_read::( + 24 + ProtocolState::max_encoded_len() + + ::SmartContract::max_encoded_len() + + SingularStakingInfo::max_encoded_len(), + )?; + + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + let origin = R::AddressMapping::into_account_id(handle.context().caller); + log::trace!(target: "ds-precompile", "unbond_and_unstake {:?}, {:?}", smart_contract, amount); + + // Find out if there is something staked on the contract + let protocol_state = ActiveProtocolState::::get(); + let staker_info = StakerInfo::::get(&origin, &smart_contract).unwrap_or_default(); + + // If there is, we need to unstake it before calling `unlock` + if staker_info.period_number() == protocol_state.period_number() { + let unstake_call = pallet_dapp_staking_v3::Call::::unstake { + smart_contract, + amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin.clone()).into(), unstake_call)?; + } + + // Now we can try and `unlock` the given `amount` + let unlock_call = pallet_dapp_staking_v3::Call::::unlock { amount }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), unlock_call)?; + + Ok(true) + } + + /// Claim back the unbonded (or unlocked) funds. + #[precompile::public("withdraw_unbonded()")] + fn withdraw_unbonded(handle: &mut impl PrecompileHandle) -> EvmResult { + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let call = pallet_dapp_staking_v3::Call::::claim_unlocked {}; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(true) + } + + /// Claim dApp rewards for the given era + #[precompile::public("claim_dapp(address,uint128)")] + fn claim_dapp( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + era: u128, + ) -> EvmResult { + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + + // parse era + let era = era + .try_into() + .map_err::(|_| RevertReason::value_is_too_large("era type").into()) + .in_field("era")?; + + log::trace!(target: "ds-precompile", "claim_dapp {:?}, era {:?}", smart_contract, era); + + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let call = pallet_dapp_staking_v3::Call::::claim_dapp_reward { + smart_contract, + era, + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(true) + } + + /// Claim staker rewards. + /// + /// Smart contract argument is legacy & is ignored in the new implementation. + #[precompile::public("claim_staker(address)")] + fn claim_staker( + handle: &mut impl PrecompileHandle, + _contract_h160: Address, + ) -> EvmResult { + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let call = pallet_dapp_staking_v3::Call::::claim_staker_rewards {}; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(true) + } + + /// Set claim reward destination for the caller. + /// + /// This call has been deprecated by dApp staking v3. + #[precompile::public("set_reward_destination(uint8)")] + fn set_reward_destination(_: &mut impl PrecompileHandle, _destination: u8) -> EvmResult { + Err(RevertReason::custom("Setting reward destination is no longer supported.").into()) + } + + /// Withdraw staked funds from the unregistered contract + #[precompile::public("withdraw_from_unregistered(address)")] + fn withdraw_from_unregistered( + handle: &mut impl PrecompileHandle, + contract_h160: Address, + ) -> EvmResult { + let smart_contract = + ::SmartContract::evm(contract_h160.into()); + log::trace!(target: "ds-precompile", "withdraw_from_unregistered {:?}", smart_contract); + + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let call = pallet_dapp_staking_v3::Call::::unstake_from_unregistered { smart_contract }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(true) + } + + /// Transfers stake from one contract to another. + /// This is a legacy functionality that is no longer supported via direct call to dApp staking v3. + /// However, it can be achieved by chaining `unstake` and `stake` calls. + #[precompile::public("nomination_transfer(address,uint128,address)")] + fn nomination_transfer( + handle: &mut impl PrecompileHandle, + origin_contract_h160: Address, + amount: u128, + target_contract_h160: Address, + ) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: StakerInfo: + // Blake2_128Concat(16 + SmartContract::max_encoded_len) + SingularStakingInfo::max_encoded_len + handle.record_db_read::( + 16 + ::SmartContract::max_encoded_len() + + SingularStakingInfo::max_encoded_len(), + )?; + + let origin_smart_contract = + ::SmartContract::evm(origin_contract_h160.into()); + let target_smart_contract = + ::SmartContract::evm(target_contract_h160.into()); + log::trace!(target: "ds-precompile", "nomination_transfer {:?} {:?} {:?}", origin_smart_contract, amount, target_smart_contract); + + // Find out how much staker has staked on the origin contract + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let staker_info = StakerInfo::::get(&origin, &origin_smart_contract).unwrap_or_default(); + + // We don't care from which period the staked amount is, the logic takes care of the situation + // if value comes from the past period. + let staked_amount = staker_info.total_staked_amount(); + let minimum_allowed_stake_amount = + ::MinimumStakeAmount::get(); + + // In case the remaining staked amount on the origin contract is less than the minimum allowed stake amount, + // everything will be unstaked. To keep in line with legacy `nomination_transfer` behavior, we should transfer + // the entire amount from the origin to target contract. + // + // In case value comes from the past period, we don't care, since the `unstake` call will fall apart. + let stake_amount = if staked_amount > 0 + && staked_amount.saturating_sub(amount) < minimum_allowed_stake_amount + { + staked_amount + } else { + amount + }; + + // First call unstake from the origin smart contract + let unstake_call = pallet_dapp_staking_v3::Call::::unstake { + smart_contract: origin_smart_contract, + amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin.clone()).into(), unstake_call)?; + + // Then call stake on the target smart contract + let stake_call = pallet_dapp_staking_v3::Call::::stake { + smart_contract: target_smart_contract, + amount: stake_amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), stake_call)?; + + Ok(true) + } + + // v2 functions + + /// Read the current protocol state. + #[precompile::public("protocol_state()")] + #[precompile::view] + fn protocol_state(handle: &mut impl PrecompileHandle) -> EvmResult { + // TODO: benchmark this function so we can measure ref time & PoV correctly + // Storage item: ActiveProtocolState: + // Twox64(8) + ProtocolState::max_encoded_len + handle.record_db_read::(8 + ProtocolState::max_encoded_len())?; + + let protocol_state = ActiveProtocolState::::get(); + + Ok(PrecompileProtocolState { + era: protocol_state.era.into(), + period: protocol_state.period_number().into(), + subperiod: subperiod_id(&protocol_state.subperiod()), + }) + } + + /// Read the `unbonding period` or `unlocking period` expressed in the number of eras. + #[precompile::public("unlocking_period()")] + #[precompile::view] + fn unlocking_period(_: &mut impl PrecompileHandle) -> EvmResult { + // constant, no DB read + Ok(DAppStaking::::unlocking_period().into()) + } + + /// Attempt to lock the given amount into the dApp staking protocol. + #[precompile::public("lock(uint128)")] + fn lock(handle: &mut impl PrecompileHandle, amount: u128) -> EvmResult { + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let lock_call = pallet_dapp_staking_v3::Call::::lock { amount }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), lock_call)?; + + Ok(true) + } + + /// Attempt to unlock the given amount from the dApp staking protocol. + #[precompile::public("unlock(uint128)")] + fn unlock(handle: &mut impl PrecompileHandle, amount: u128) -> EvmResult { + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let unlock_call = pallet_dapp_staking_v3::Call::::unlock { amount }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), unlock_call)?; + + Ok(true) + } + + /// Attempts to claim unlocking chunks which have undergone the entire unlocking period. + #[precompile::public("claim_unlocked()")] + fn claim_unlocked(handle: &mut impl PrecompileHandle) -> EvmResult { + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let claim_unlocked_call = pallet_dapp_staking_v3::Call::::claim_unlocked {}; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), claim_unlocked_call)?; + + Ok(true) + } + + /// Attempts to stake the given amount on the given smart contract. + #[precompile::public("stake((uint8,bytes),uint128)")] + fn stake( + handle: &mut impl PrecompileHandle, + smart_contract: SmartContractV2, + amount: Balance, + ) -> EvmResult { + let smart_contract = Self::decode_smart_contract(smart_contract)?; + + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let stake_call = pallet_dapp_staking_v3::Call::::stake { + smart_contract, + amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), stake_call)?; + + Ok(true) + } + + /// Attempts to unstake the given amount from the given smart contract. + #[precompile::public("unstake((uint8,bytes),uint128)")] + fn unstake( + handle: &mut impl PrecompileHandle, + smart_contract: SmartContractV2, + amount: Balance, + ) -> EvmResult { + let smart_contract = Self::decode_smart_contract(smart_contract)?; + + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let unstake_call = pallet_dapp_staking_v3::Call::::unstake { + smart_contract, + amount, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), unstake_call)?; + + Ok(true) + } + + /// Attempts to claim one or more pending staker rewards. + #[precompile::public("claim_staker_rewards()")] + fn claim_staker_rewards(handle: &mut impl PrecompileHandle) -> EvmResult { + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let claim_staker_rewards_call = pallet_dapp_staking_v3::Call::::claim_staker_rewards {}; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), claim_staker_rewards_call)?; + + Ok(true) + } + + /// Attempts to claim bonus reward for being a loyal staker of the given dApp. + #[precompile::public("claim_bonus_reward((uint8,bytes))")] + fn claim_bonus_reward( + handle: &mut impl PrecompileHandle, + smart_contract: SmartContractV2, + ) -> EvmResult { + let smart_contract = Self::decode_smart_contract(smart_contract)?; + + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let claim_bonus_reward_call = + pallet_dapp_staking_v3::Call::::claim_bonus_reward { smart_contract }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), claim_bonus_reward_call)?; + + Ok(true) + } + + /// Attempts to claim dApp reward for the given dApp in the given era. + #[precompile::public("claim_bonus_reward((uint8,bytes),uint256)")] + fn claim_dapp_reward( + handle: &mut impl PrecompileHandle, + smart_contract: SmartContractV2, + era: U256, + ) -> EvmResult { + let smart_contract = Self::decode_smart_contract(smart_contract)?; + let era = era + .try_into() + .map_err::(|_| RevertReason::value_is_too_large("Era number.").into()) + .in_field("era")?; + + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let claim_dapp_reward_call = pallet_dapp_staking_v3::Call::::claim_dapp_reward { + smart_contract, + era, + }; + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), claim_dapp_reward_call)?; + + Ok(true) + } + + /// Attempts to unstake everything from an unregistered contract. + #[precompile::public("unstake_from_unregistered((uint8,bytes))")] + fn unstake_from_unregistered( + handle: &mut impl PrecompileHandle, + smart_contract: SmartContractV2, + ) -> EvmResult { + let smart_contract = Self::decode_smart_contract(smart_contract)?; + + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let unstake_from_unregistered_call = + pallet_dapp_staking_v3::Call::::unstake_from_unregistered { smart_contract }; + RuntimeHelper::::try_dispatch( + handle, + Some(origin).into(), + unstake_from_unregistered_call, + )?; + + Ok(true) + } + + /// Attempts to cleanup expired entries for the staker. + #[precompile::public("cleanup_expired_entries()")] + fn cleanup_expired_entries(handle: &mut impl PrecompileHandle) -> EvmResult { + // Prepare call & dispatch it + let origin = R::AddressMapping::into_account_id(handle.context().caller); + let cleanup_expired_entries_call = + pallet_dapp_staking_v3::Call::::cleanup_expired_entries {}; + RuntimeHelper::::try_dispatch( + handle, + Some(origin).into(), + cleanup_expired_entries_call, + )?; + + Ok(true) + } + + // Utility functions + + /// Helper method to decode smart contract struct for v2 calls + pub(crate) fn decode_smart_contract( + smart_contract: SmartContractV2, + ) -> EvmResult<::SmartContract> { + let smart_contract = match smart_contract.contract_type { + SmartContractTypes::Evm => { + ensure!( + smart_contract.address.as_bytes().len() == 20, + revert("Invalid address length for Astar EVM smart contract.") + ); + let h160_address = H160::from_slice(smart_contract.address.as_bytes()); + ::SmartContract::evm(h160_address) + } + SmartContractTypes::Wasm => { + ensure!( + smart_contract.address.as_bytes().len() == 32, + revert("Invalid address length for Astar WASM smart contract.") + ); + let mut staker_bytes = [0_u8; 32]; + staker_bytes[..].clone_from_slice(&smart_contract.address.as_bytes()); + + ::SmartContract::wasm(staker_bytes.into()) + } + }; + + Ok(smart_contract) + } + + /// Helper method to parse H160 or SS58 address + pub(crate) fn parse_input_address(staker_vec: Vec) -> EvmResult { + let staker: R::AccountId = match staker_vec.len() { + // public address of the ss58 account has 32 bytes + 32 => { + let mut staker_bytes = [0_u8; 32]; + staker_bytes[..].clone_from_slice(&staker_vec[0..32]); + + staker_bytes.into() + } + // public address of the H160 account has 20 bytes + 20 => { + let mut staker_bytes = [0_u8; 20]; + staker_bytes[..].clone_from_slice(&staker_vec[0..20]); + + R::AddressMapping::into_account_id(staker_bytes.into()) + } + _ => { + // Return err if account length is wrong + return Err(revert("Error while parsing staker's address")); + } + }; + + Ok(staker) + } +} + +/// Numeric Id of the subperiod enum value. +pub(crate) fn subperiod_id(subperiod: &Subperiod) -> u8 { + match subperiod { + Subperiod::Voting => 0, + Subperiod::BuildAndEarn => 1, + } +} diff --git a/precompiles/dapp-staking-v3/src/test/mock.rs b/precompiles/dapp-staking-v3/src/test/mock.rs new file mode 100644 index 0000000000..2015ba6736 --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/mock.rs @@ -0,0 +1,461 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::*; + +use fp_evm::{IsPrecompileResult, Precompile}; +use frame_support::{ + assert_ok, construct_runtime, parameter_types, + traits::{ + fungible::{Mutate as FunMutate, Unbalanced as FunUnbalanced}, + ConstU128, ConstU64, GenesisBuild, Hooks, + }, + weights::{RuntimeDbWeight, Weight}, +}; +use frame_system::RawOrigin; +use pallet_evm::{ + AddressMapping, EnsureAddressNever, EnsureAddressRoot, PrecompileResult, PrecompileSet, +}; +use sp_arithmetic::{fixed_point::FixedU64, Permill}; +use sp_core::{H160, H256}; +use sp_io::TestExternalities; +use sp_runtime::traits::{BlakeTwo256, ConstU32, IdentityLookup}; +extern crate alloc; + +use astar_primitives::{ + dapp_staking::{CycleConfiguration, SmartContract, StakingRewardHandler}, + testing::Header, + AccountId, Balance, BlockNumber, +}; +use pallet_dapp_staking_v3::{EraNumber, PeriodNumber, PriceProvider, TierThreshold}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +pub struct AddressMapper; +impl AddressMapping for AddressMapper { + fn into_account_id(account: H160) -> AccountId { + let mut account_id = [0u8; 32]; + account_id[0..20].clone_from_slice(&account.as_bytes()); + + account_id + .try_into() + .expect("H160 is 20 bytes long so it must fit into 32 bytes; QED") + } +} + +pub const READ_WEIGHT: u64 = 3; +pub const WRITE_WEIGHT: u64 = 7; + +parameter_types! { + pub const BlockHashCount: BlockNumber = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); + pub const TestWeights: RuntimeDbWeight = RuntimeDbWeight { + read: READ_WEIGHT, + write: WRITE_WEIGHT, + }; +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type MaxLocks = ConstU32<4>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type HoldIdentifier = (); + type FreezeIdentifier = RuntimeFreezeReason; + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<1>; + type WeightInfo = (); +} + +pub fn precompile_address() -> H160 { + H160::from_low_u64_be(0x5001) +} + +#[derive(Debug, Clone, Copy)] +pub struct DappStakingPrecompile(PhantomData); +impl PrecompileSet for DappStakingPrecompile +where + R: pallet_evm::Config, + DappStakingV3Precompile: Precompile, +{ + fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { + match handle.code_address() { + a if a == precompile_address() => Some(DappStakingV3Precompile::::execute(handle)), + _ => None, + } + } + + fn is_precompile(&self, address: sp_core::H160, _gas: u64) -> IsPrecompileResult { + IsPrecompileResult::Answer { + is_precompile: address == precompile_address(), + extra_cost: 0, + } + } +} + +pub type PrecompileCall = DappStakingV3PrecompileCall; + +parameter_types! { + pub PrecompilesValue: DappStakingPrecompile = DappStakingPrecompile(Default::default()); + pub WeightPerGas: Weight = Weight::from_parts(1, 0); +} + +impl pallet_evm::Config for Test { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AddressMapper; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = DappStakingPrecompile; + type PrecompilesValue = PrecompilesValue; + type Timestamp = Timestamp; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = (); + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type WeightInfo = (); + type GasLimitPovSizeRatio = ConstU64<4>; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<5>; + type WeightInfo = (); +} + +type MockSmartContract = SmartContract<::AccountId>; + +pub struct DummyPriceProvider; +impl PriceProvider for DummyPriceProvider { + fn average_price() -> FixedU64 { + FixedU64::from_rational(1, 10) + } +} + +pub struct DummyStakingRewardHandler; +impl StakingRewardHandler for DummyStakingRewardHandler { + fn staker_and_dapp_reward_pools(_total_staked_value: Balance) -> (Balance, Balance) { + ( + Balance::from(1_000_000_000_000_u128), + Balance::from(1_000_000_000_u128), + ) + } + + fn bonus_reward_pool() -> Balance { + Balance::from(3_000_000_u128) + } + + fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()> { + let _ = Balances::mint_into(beneficiary, reward); + Ok(()) + } +} + +pub struct DummyCycleConfiguration; +impl CycleConfiguration for DummyCycleConfiguration { + fn periods_per_cycle() -> u32 { + 4 + } + + fn eras_per_voting_subperiod() -> u32 { + 8 + } + + fn eras_per_build_and_earn_subperiod() -> u32 { + 16 + } + + fn blocks_per_era() -> u32 { + 10 + } +} + +// Just to satsify the trait bound +#[cfg(feature = "runtime-benchmarks")] +pub struct BenchmarkHelper(sp_std::marker::PhantomData<(SC, ACC)>); +#[cfg(feature = "runtime-benchmarks")] +impl pallet_dapp_staking_v3::BenchmarkHelper + for BenchmarkHelper +{ + fn get_smart_contract(id: u32) -> MockSmartContract { + MockSmartContract::evm(H160::from_low_u64_be(id as u64)) + } + + fn set_balance(_account: &AccountId, _amount: Balance) {} +} + +impl pallet_dapp_staking_v3::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type Currency = Balances; + type SmartContract = MockSmartContract; + type ManagerOrigin = frame_system::EnsureRoot; + type NativePriceProvider = DummyPriceProvider; + type StakingRewardHandler = DummyStakingRewardHandler; + type CycleConfiguration = DummyCycleConfiguration; + type EraRewardSpanLength = ConstU32<8>; + type RewardRetentionInPeriods = ConstU32<2>; + type MaxNumberOfContracts = ConstU32<10>; + type MaxUnlockingChunks = ConstU32<5>; + type MinimumLockedAmount = ConstU128<10>; + type UnlockingPeriod = ConstU32<2>; + type MaxNumberOfStakedContracts = ConstU32<5>; + type MinimumStakeAmount = ConstU128<3>; + type NumberOfTiers = ConstU32<4>; + type WeightInfo = pallet_dapp_staking_v3::weights::SubstrateWeight; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = BenchmarkHelper; +} + +construct_runtime!( + pub struct Test + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + DappStaking: pallet_dapp_staking_v3, + } +); + +pub struct ExternalityBuilder; +impl ExternalityBuilder { + pub fn build() -> TestExternalities { + let mut storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + >::assimilate_storage( + &pallet_dapp_staking_v3::GenesisConfig { + reward_portion: vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ], + slot_distribution: vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ], + tier_thresholds: vec![ + TierThreshold::DynamicTvlAmount { + amount: 100, + minimum_amount: 80, + }, + TierThreshold::DynamicTvlAmount { + amount: 50, + minimum_amount: 40, + }, + TierThreshold::DynamicTvlAmount { + amount: 20, + minimum_amount: 20, + }, + TierThreshold::FixedTvlAmount { amount: 10 }, + ], + slots_per_tier: vec![10, 20, 30, 40], + }, + &mut storage, + ) + .ok(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| { + System::set_block_number(1); + + let alice_native = AddressMapper::into_account_id(ALICE); + assert_ok!( + ::Currency::write_balance( + &alice_native, + 1000_000_000_000_000_000_000 as Balance, + ) + ); + }); + ext + } +} + +pub fn precompiles() -> DappStakingPrecompile { + PrecompilesValue::get() +} + +// Utility functions + +pub const ALICE: H160 = H160::repeat_byte(0xAA); + +/// Used to register a smart contract, and stake some funds on it. +pub fn register_and_stake( + account: H160, + smart_contract: ::SmartContract, + amount: Balance, +) { + let alice_native = AddressMapper::into_account_id(account); + + // 1. Register smart contract + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + alice_native.clone(), + smart_contract.clone() + )); + + // 2. Lock some amount + assert_ok!(DappStaking::lock( + RawOrigin::Signed(alice_native.clone()).into(), + amount, + )); + + // 3. Stake the locked amount + assert_ok!(DappStaking::stake( + RawOrigin::Signed(alice_native.clone()).into(), + smart_contract.clone(), + amount, + )); +} + +/// Utility function used to create `DynamicAddress` out of the given `H160` address. +/// The first one is simply byte representation of the H160 address. +/// The second one is byte representation of the derived `AccountId` from the H160 address. +pub fn into_dynamic_addresses(address: H160) -> [DynamicAddress; 2] { + [ + address.as_bytes().try_into().unwrap(), + >::as_ref(&AddressMapper::into_account_id(address)) + .try_into() + .unwrap(), + ] +} + +/// Initialize first block. +/// This method should only be called once in a UT otherwise the first block will get initialized multiple times. +pub fn initialize() { + // This assert prevents method misuse + assert_eq!(System::block_number(), 1 as BlockNumber); + DappStaking::on_initialize(System::block_number()); + run_to_block(2); +} + +/// Run to the specified block number. +/// Function assumes first block has been initialized. +pub(crate) fn run_to_block(n: BlockNumber) { + while System::block_number() < n { + DappStaking::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + DappStaking::on_initialize(System::block_number()); + } +} + +/// Run for the specified number of blocks. +/// Function assumes first block has been initialized. +pub(crate) fn run_for_blocks(n: BlockNumber) { + run_to_block(System::block_number() + n); +} + +/// Advance blocks until the specified era has been reached. +/// +/// Function has no effect if era is already passed. +pub(crate) fn advance_to_era(era: EraNumber) { + assert!(era >= ActiveProtocolState::::get().era); + while ActiveProtocolState::::get().era < era { + run_for_blocks(1); + } +} + +/// Advance blocks until next era has been reached. +pub(crate) fn advance_to_next_era() { + advance_to_era(ActiveProtocolState::::get().era + 1); +} + +/// Advance blocks until next period type has been reached. +pub(crate) fn advance_to_next_subperiod() { + let subperiod = ActiveProtocolState::::get().subperiod(); + while ActiveProtocolState::::get().subperiod() == subperiod { + run_for_blocks(1); + } +} + +/// Advance blocks until the specified period has been reached. +/// +/// Function has no effect if period is already passed. +pub(crate) fn advance_to_period(period: PeriodNumber) { + assert!(period >= ActiveProtocolState::::get().period_number()); + while ActiveProtocolState::::get().period_number() < period { + run_for_blocks(1); + } +} + +/// Advance blocks until next period has been reached. +pub(crate) fn advance_to_next_period() { + advance_to_period(ActiveProtocolState::::get().period_number() + 1); +} + +// Return all dApp staking events from the event buffer. +pub fn dapp_staking_events() -> Vec> { + System::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + ::RuntimeEvent::from(e) + .try_into() + .ok() + }) + .collect::>() +} diff --git a/precompiles/dapp-staking-v3/src/test/mod.rs b/precompiles/dapp-staking-v3/src/test/mod.rs new file mode 100644 index 0000000000..a33eb22954 --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/mod.rs @@ -0,0 +1,22 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +mod mock; +mod tests_v1; +mod tests_v2; +mod types; diff --git a/precompiles/dapp-staking-v3/src/test/tests_v1.rs b/precompiles/dapp-staking-v3/src/test/tests_v1.rs new file mode 100644 index 0000000000..8d93b56198 --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/tests_v1.rs @@ -0,0 +1,867 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +extern crate alloc; +use crate::{test::mock::*, *}; +use frame_support::assert_ok; +use frame_system::RawOrigin; +use precompile_utils::testing::*; +use sp_core::H160; +use sp_runtime::traits::Zero; + +use assert_matches::assert_matches; + +use pallet_dapp_staking_v3::{ActiveProtocolState, EraNumber, EraRewards}; + +#[test] +fn read_current_era_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_current_era {}, + ) + .expect_no_logs() + .execute_returns(ActiveProtocolState::::get().era); + + // advance a few eras, check value again + advance_to_era(7); + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_current_era {}, + ) + .expect_no_logs() + .execute_returns(ActiveProtocolState::::get().era); + }); +} + +#[test] +fn read_unbonding_period_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let unlocking_period_in_eras: EraNumber = + ::UnlockingPeriod::get(); + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_unbonding_period {}, + ) + .expect_no_logs() + .execute_returns(unlocking_period_in_eras); + }); +} + +#[test] +fn read_era_reward_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Check historic era for rewards + let era = 3; + advance_to_era(era + 1); + + let span_index = DAppStaking::::era_reward_span_index(era); + + let era_rewards_span = EraRewards::::get(span_index).expect("Entry must exist."); + let expected_reward = era_rewards_span + .get(era) + .map(|r| r.staker_reward_pool + r.dapp_reward_pool) + .expect("It's history era so it must exist."); + assert!(expected_reward > 0, "Sanity check."); + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_era_reward { era }, + ) + .expect_no_logs() + .execute_returns(expected_reward); + + // Check current era for rewards, must be zero + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::read_era_reward { era: era + 1 }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + }); +} + +#[test] +fn read_era_staked_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + let anchor_era = ActiveProtocolState::::get().era; + + // 1. Current era stake must be zero, since stake is only valid from the next era. + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { era: anchor_era }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { + era: anchor_era + 1, + }, + ) + .expect_no_logs() + .execute_returns(amount); + + // 2. Advance to next era, and check next era after the anchor. + advance_to_era(anchor_era + 5); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { + era: anchor_era + 1, + }, + ) + .expect_no_logs() + .execute_returns(amount); + + // 3. Check era after the next one, must throw an error. + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_era_staked { + era: ActiveProtocolState::::get().era + 2, + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"Era is in the future"); + }); +} + +#[test] +fn read_staked_amount_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let staker_h160 = ALICE; + let dynamic_addresses = into_dynamic_addresses(staker_h160); + + // 1. Sanity checks - must be zero before anything is staked. + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount { + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + + // 2. Stake some amount and check again + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount { + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(amount); + } + + // 3. Advance into next period, it should be reset back to zero + advance_to_next_period(); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount { + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + }); +} + +#[test] +fn read_staked_amount_on_contract_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let dynamic_addresses = into_dynamic_addresses(staker_h160); + + // 1. Sanity checks - must be zero before anything is staked. + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount_on_contract { + contract_h160: smart_contract_address.into(), + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + + // 2. Stake some amount and check again + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount_on_contract { + contract_h160: smart_contract_address.into(), + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(amount); + } + + // 3. Advance into next period, it should be reset back to zero + advance_to_next_period(); + for staker in &dynamic_addresses { + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_staked_amount_on_contract { + contract_h160: smart_contract_address.into(), + staker: staker.clone(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + } + }); +} + +#[test] +fn read_contract_stake_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + + // 1. Sanity checks - must be zero before anything is staked. + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_contract_stake { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + + // 2. Stake some amount and check again + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_contract_stake { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(amount); + + // 3. Advance into next period, it should be reset back to zero + advance_to_next_period(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::read_contract_stake { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(Balance::zero()); + }); +} + +#[test] +fn register_is_unsupported() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::register { + _address: Default::default(), + }, + ) + .expect_no_logs() + .execute_reverts(|output| output == b"register via evm precompile is not allowed"); + }); +} + +#[test] +fn set_reward_destination_is_unsupported() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::set_reward_destination { _destination: 0 }, + ) + .expect_no_logs() + .execute_reverts(|output| { + output == b"Setting reward destination is no longer supported." + }); + }); +} + +#[test] +fn bond_and_stake_with_two_calls_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() + )); + + // Lock some amount, but not enough to cover the `bond_and_stake` call. + let pre_lock_amount = 500; + let stake_amount = 1_000_000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + pre_lock_amount, + )); + + // Execute legacy call, expect missing funds to be locked. + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::bond_and_stake { + contract_h160: smart_contract_address.into(), + amount: stake_amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 2); + let additional_lock_amount = stake_amount - pre_lock_amount; + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Locked { + amount, + .. + } if amount == additional_lock_amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == stake_amount + ); + }); +} + +#[test] +fn bond_and_stake_with_single_call_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() + )); + + // Lock enough amount to cover `bond_and_stake` call. + let amount = 3000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + amount, + )); + + // Execute legacy call, expect only single stake to be executed. + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::bond_and_stake { + contract_h160: smart_contract_address.into(), + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn unbond_and_unstake_with_two_calls_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Execute legacy call, expect funds to first unstaked, and then unlocked + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::unbond_and_unstake { + contract_h160: smart_contract_address.into(), + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 2); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + }if smart_contract == smart_contract && amount == amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Unlocking { amount, .. } if amount == amount + ); + }); +} + +#[test] +fn unbond_and_unstake_with_single_calls_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Unstake the entire amount, so only unlock call is expected. + assert_ok!(DappStaking::unstake( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + smart_contract.clone(), + amount, + )); + + // Execute legacy call, expect funds to be unlocked + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::unbond_and_unstake { + contract_h160: smart_contract_address.into(), + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unlocking { amount, .. } if amount == amount + ); + }); +} + +#[test] +fn withdraw_unbonded_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let staker_native = AddressMapper::into_account_id(staker_h160); + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Unlock some amount + assert_ok!(DappStaking::unstake( + RawOrigin::Signed(staker_native.clone()).into(), + smart_contract.clone(), + amount, + )); + let unlock_amount = amount / 7; + assert_ok!(DappStaking::unlock( + RawOrigin::Signed(staker_native.clone()).into(), + unlock_amount, + )); + + // Advance enough into time so unlocking chunk can be claimed + let unlock_block = Ledger::::get(&staker_native).unlocking[0].unlock_block; + run_to_block(unlock_block); + + // Execute legacy call, expect unlocked funds to be claimed back + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::withdraw_unbonded {}, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::ClaimedUnlocked { + amount, + .. + } if amount == unlock_amount + ); + }); +} + +#[test] +fn claim_dapp_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance enough eras so we can claim dApp reward + advance_to_era(3); + let claim_era = 2; + + // Execute legacy call, expect dApp rewards to be claimed + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_dapp { + contract_h160: smart_contract_address.into(), + era: claim_era, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::DAppReward { + era, + smart_contract, + .. + } if era as u128 == claim_era && smart_contract == smart_contract + ); + }); +} + +#[test] +fn claim_staker_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance enough eras so we can claim dApp reward + let target_era = 5; + advance_to_era(target_era); + let number_of_claims = (2..target_era).count(); + + // Execute legacy call, expect dApp rewards to be claimed + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_staker { + _contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect multiple reward to be claimed + let events = dapp_staking_events(); + assert_eq!(events.len(), number_of_claims as usize); + for era in 2..target_era { + assert_matches!( + events[era as usize - 2].clone(), + pallet_dapp_staking_v3::Event::Reward { era, .. } if era == era + ); + } + }); +} + +#[test] +fn withdraw_from_unregistered_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_address); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Unregister the dApp + assert_ok!(DappStaking::unregister( + RawOrigin::Root.into(), + smart_contract.clone() + )); + + // Execute legacy call, expect funds to be unstaked & withdrawn + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::withdraw_from_unregistered { + contract_h160: smart_contract_address.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::UnstakeFromUnregistered { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn nomination_transfer_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register the first dApp, and stke on it. + let staker_h160 = ALICE; + let staker_native = AddressMapper::into_account_id(staker_h160); + let smart_contract_address_1 = H160::repeat_byte(0xFA); + let smart_contract_1 = + ::SmartContract::evm(smart_contract_address_1); + let amount = 1_000_000_000_000; + register_and_stake(staker_h160, smart_contract_1.clone(), amount); + + // Register the second dApp. + let smart_contract_address_2 = H160::repeat_byte(0xBF); + let smart_contract_2 = + ::SmartContract::evm(smart_contract_address_2); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + staker_native.clone(), + smart_contract_2.clone() + )); + + // 1st scenario - transfer enough amount from the first to second dApp to cover the stake, + // but not enough for full unstake. + let minimum_stake_amount: Balance = + ::MinimumStakeAmount::get(); + + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::nomination_transfer { + origin_contract_h160: smart_contract_address_1.into(), + amount: minimum_stake_amount, + target_contract_h160: smart_contract_address_2.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect the same amount to be staked on the second contract + let events = dapp_staking_events(); + assert_eq!(events.len(), 2); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_1 && amount == minimum_stake_amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_2 && amount == minimum_stake_amount + ); + + // 2nd scenario - transfer almost the entire amount from the first to second dApp. + // The amount is large enough to trigger full unstake of the first contract. + let unstake_amount = amount - minimum_stake_amount - 1; + let expected_stake_unstake_amount = amount - minimum_stake_amount; + + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::nomination_transfer { + origin_contract_h160: smart_contract_address_1.into(), + amount: unstake_amount, + target_contract_h160: smart_contract_address_2.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect the same amount to be staked on the second contract + let events = dapp_staking_events(); + assert_eq!(events.len(), 2); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_1 && amount == expected_stake_unstake_amount + ); + assert_matches!( + events[1].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract_2 && amount == expected_stake_unstake_amount + ); + }); +} diff --git a/precompiles/dapp-staking-v3/src/test/tests_v2.rs b/precompiles/dapp-staking-v3/src/test/tests_v2.rs new file mode 100644 index 0000000000..5977417b5e --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/tests_v2.rs @@ -0,0 +1,526 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +extern crate alloc; +use crate::{test::mock::*, *}; +use frame_support::assert_ok; +use frame_system::RawOrigin; +use precompile_utils::testing::*; +use sp_core::H160; + +use assert_matches::assert_matches; + +use astar_primitives::{dapp_staking::CycleConfiguration, BlockNumber}; +use pallet_dapp_staking_v3::{ActiveProtocolState, EraNumber}; + +#[test] +fn protocol_state_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Prepare some mixed state in the future so not all entries are 'zero' + advance_to_next_period(); + advance_to_next_era(); + + let state = ActiveProtocolState::::get(); + + let expected_outcome = PrecompileProtocolState { + era: state.era.into(), + period: state.period_number().into(), + subperiod: subperiod_id(&state.subperiod()), + }; + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::protocol_state {}, + ) + .expect_no_logs() + .execute_returns(expected_outcome); + }); +} + +#[test] +fn unlocking_period_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let unlocking_period_in_eras: EraNumber = + ::UnlockingPeriod::get(); + let era_length: BlockNumber = + ::CycleConfiguration::blocks_per_era(); + + let expected_outcome = era_length * unlocking_period_in_eras; + + precompiles() + .prepare_test( + Alice, + precompile_address(), + PrecompileCall::unlocking_period {}, + ) + .expect_no_logs() + .execute_returns(expected_outcome); + }); +} + +#[test] +fn lock_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Lock some amount and verify event + let amount = 1234; + System::reset_events(); + precompiles() + .prepare_test(ALICE, precompile_address(), PrecompileCall::lock { amount }) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Locked { + amount, + .. + } if amount == amount + ); + }); +} + +#[test] +fn unlock_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + let lock_amount = 1234; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), + lock_amount, + )); + + // Unlock some amount and verify event + System::reset_events(); + let unlock_amount = 1234 / 7; + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::unlock { + amount: unlock_amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unlocking { + amount, + .. + } if amount == unlock_amount + ); + }); +} + +#[test] +fn claim_unlocked_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Lock/unlock some amount to create unlocking chunk + let amount = 1234; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), + amount, + )); + assert_ok!(DappStaking::unlock( + RawOrigin::Signed(AddressMapper::into_account_id(ALICE)).into(), + amount, + )); + + // Advance enough into time so unlocking chunk can be claimed + let unlock_block = + Ledger::::get(&AddressMapper::into_account_id(ALICE)).unlocking[0].unlock_block; + run_to_block(unlock_block); + + // Claim unlocked chunk and verify event + System::reset_events(); + precompiles() + .prepare_test( + ALICE, + precompile_address(), + PrecompileCall::claim_unlocked {}, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::ClaimedUnlocked { + amount, + .. + } if amount == amount + ); + }); +} + +#[test] +fn stake_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_h160 = H160::repeat_byte(0xFA); + let smart_contract = + ::SmartContract::evm(smart_contract_h160); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() + )); + + // Lock some amount which will be used for staking + let amount = 2000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + amount, + )); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Evm, + address: smart_contract_h160.as_bytes().try_into().unwrap(), + }; + + // Stake some amount and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::stake { + smart_contract: smart_contract_v2, + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Stake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn unstake_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp for staking + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + assert_ok!(DappStaking::register( + RawOrigin::Root.into(), + AddressMapper::into_account_id(staker_h160), + smart_contract.clone() + )); + + // Lock & stake some amount + let amount = 2000; + assert_ok!(DappStaking::lock( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + amount, + )); + assert_ok!(DappStaking::stake( + RawOrigin::Signed(AddressMapper::into_account_id(staker_h160)).into(), + smart_contract.clone(), + amount, + )); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Unstake some amount and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::unstake { + smart_contract: smart_contract_v2, + amount, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::Unstake { + smart_contract, + amount, + .. + } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn claim_staker_rewards_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance a few eras so we can claim a few rewards + let target_era = 7; + advance_to_era(target_era); + let number_of_claims = (2..target_era).count(); + + // Claim staker rewards and verify events + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_staker_rewards {}, + ) + .expect_no_logs() + .execute_returns(true); + + // We expect multiple reward to be claimed + let events = dapp_staking_events(); + assert_eq!(events.len(), number_of_claims as usize); + for era in 2..target_era { + assert_matches!( + events[era as usize - 2].clone(), + pallet_dapp_staking_v3::Event::Reward { era, .. } if era == era + ); + } + }); +} + +#[test] +fn claim_bonus_reward_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it, loyally + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance to the next period + advance_to_next_period(); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Claim bonus reward and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_bonus_reward { + smart_contract: smart_contract_v2, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::BonusReward { smart_contract, .. } if smart_contract == smart_contract + ); + }); +} + +#[test] +fn claim_dapp_reward_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance to 3rd era so we claim rewards for the 2nd era + advance_to_era(3); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Claim dApp reward and verify event + let claim_era: EraNumber = 2; + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::claim_dapp_reward { + smart_contract: smart_contract_v2, + era: claim_era.into(), + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::DAppReward { era, smart_contract, .. } if era == claim_era && smart_contract == smart_contract + ); + }); +} + +#[test] +fn unstake_from_unregistered_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Unregister the dApp + assert_ok!(DappStaking::unregister( + RawOrigin::Root.into(), + smart_contract.clone() + )); + + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: smart_contract_address.into(), + }; + + // Unstake from the unregistered dApp and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::unstake_from_unregistered { + smart_contract: smart_contract_v2, + }, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::UnstakeFromUnregistered { smart_contract, amount, .. } if smart_contract == smart_contract && amount == amount + ); + }); +} + +#[test] +fn cleanup_expired_entries_is_ok() { + ExternalityBuilder::build().execute_with(|| { + initialize(); + + // Advance over to the Build&Earn subperiod + advance_to_next_subperiod(); + assert_eq!( + ActiveProtocolState::::get().subperiod(), + Subperiod::BuildAndEarn, + "Sanity check." + ); + + // Register a dApp and stake on it + let staker_h160 = ALICE; + let smart_contract_address = [0xAF; 32]; + let smart_contract = ::SmartContract::wasm( + smart_contract_address.into(), + ); + let amount = 1234; + register_and_stake(staker_h160, smart_contract.clone(), amount); + + // Advance over to the next period so the entry for dApp becomes expired + advance_to_next_period(); + + // Cleanup single expired entry and verify event + System::reset_events(); + precompiles() + .prepare_test( + staker_h160, + precompile_address(), + PrecompileCall::cleanup_expired_entries {}, + ) + .expect_no_logs() + .execute_returns(true); + + let events = dapp_staking_events(); + assert_eq!(events.len(), 1); + assert_matches!( + events[0].clone(), + pallet_dapp_staking_v3::Event::ExpiredEntriesRemoved { count, .. } if count == 1 + ); + }); +} diff --git a/precompiles/dapp-staking-v3/src/test/types.rs b/precompiles/dapp-staking-v3/src/test/types.rs new file mode 100644 index 0000000000..186300e677 --- /dev/null +++ b/precompiles/dapp-staking-v3/src/test/types.rs @@ -0,0 +1,154 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +extern crate alloc; +use crate::{test::mock::*, *}; + +use assert_matches::assert_matches; + +#[test] +fn smart_contract_types_are_ok() { + // Verify Astar EVM smart contract type + { + let index: u8 = SmartContractTypes::Evm.into(); + assert_eq!(index, 0); + assert_eq!(Ok(SmartContractTypes::Evm), index.try_into()); + } + + // Verify Astar WASM smart contract type + { + let index: u8 = SmartContractTypes::Wasm.into(); + assert_eq!(index, 1); + assert_eq!(Ok(SmartContractTypes::Wasm), index.try_into()); + } + + // Negative case + { + let index: u8 = 2; + let maybe_smart_contract: Result = index.try_into(); + assert_matches!(maybe_smart_contract, Err(_)); + } +} + +#[test] +fn decode_smart_contract_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // Astar EVM smart contract decoding + { + let address = H160::repeat_byte(0xCA); + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Evm, + address: address.as_bytes().into(), + }; + + assert_eq!( + Ok(::SmartContract::evm(address)), + DappStakingV3Precompile::::decode_smart_contract(smart_contract_v2) + ); + } + + // Astar WASM smart contract decoding + { + let address = [0x6E; 32]; + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: address.into(), + }; + + assert_eq!( + Ok(::SmartContract::wasm(address.into())), + DappStakingV3Precompile::::decode_smart_contract(smart_contract_v2) + ); + } + }); +} + +#[test] +fn decode_smart_contract_fails_when_type_and_address_mismatch() { + ExternalityBuilder::build().execute_with(|| { + // H160 address for Wasm smart contract type + { + let address = H160::repeat_byte(0xCA); + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Wasm, + address: address.as_bytes().into(), + }; + + assert_matches!( + DappStakingV3Precompile::::decode_smart_contract(smart_contract_v2), + Err(_) + ); + } + + // Native address for EVM smart contract type + { + let address = [0x6E; 32]; + let smart_contract_v2 = SmartContractV2 { + contract_type: SmartContractTypes::Evm, + address: address.into(), + }; + + assert_matches!( + DappStakingV3Precompile::::decode_smart_contract(smart_contract_v2), + Err(_) + ); + } + }); +} + +#[test] +fn parse_input_address_is_ok() { + ExternalityBuilder::build().execute_with(|| { + // H160 address + { + let address_h160 = H160::repeat_byte(0xCA); + let address_native = AddressMapper::into_account_id(address_h160); + + assert_eq!( + DappStakingV3Precompile::::parse_input_address( + address_h160.as_bytes().into() + ), + Ok(address_native) + ); + } + + // Native address + { + let address_native = [0x6E; 32]; + + assert_eq!( + DappStakingV3Precompile::::parse_input_address(address_native.into()), + Ok(address_native.into()) + ); + } + }); +} + +#[test] +fn parse_input_address_fails_with_incorrect_address_length() { + ExternalityBuilder::build().execute_with(|| { + let addresses: Vec<&[u8]> = vec![&[0x6E; 19], &[0xA1; 21], &[0xC3; 31], &[0x99; 33]]; + + for address in addresses { + assert_matches!( + DappStakingV3Precompile::::parse_input_address(address.into()), + Err(_) + ); + } + }); +} diff --git a/primitives/src/dapp_staking.rs b/primitives/src/dapp_staking.rs index 92ead96b15..baf7ff34b5 100644 --- a/primitives/src/dapp_staking.rs +++ b/primitives/src/dapp_staking.rs @@ -18,6 +18,12 @@ use super::{Balance, BlockNumber}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; + +use frame_support::RuntimeDebug; +use sp_core::H160; +use sp_std::hash::Hash; + /// Configuration for cycles, periods, subperiods & eras. /// /// * `cycle` - Time unit similar to 'year' in the real world. Consists of one or more periods. At the beginning of each cycle, inflation is recalculated. @@ -83,3 +89,50 @@ pub trait StakingRewardHandler { /// Attempts to pay out the rewards to the beneficiary. fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()>; } + +/// Trait defining the interface for dApp staking `smart contract types` handler. +/// +/// It can be used to create a representation of the specified smart contract instance type. +pub trait SmartContractHandle { + /// Create a new smart contract representation for the specified EVM address. + fn evm(address: H160) -> Self; + /// Create a new smart contract representation for the specified Wasm address. + fn wasm(address: AccountId) -> Self; +} + +/// Multi-VM pointer to smart contract instance. +#[derive( + PartialEq, + Eq, + Copy, + Clone, + Encode, + Decode, + RuntimeDebug, + MaxEncodedLen, + Hash, + scale_info::TypeInfo, +)] +pub enum SmartContract { + /// EVM smart contract instance. + Evm(H160), + /// Wasm smart contract instance. + Wasm(AccountId), +} + +// TODO: remove this once dApps staking v2 has been removed. +impl Default for SmartContract { + fn default() -> Self { + Self::evm([0x01; 20].into()) + } +} + +impl SmartContractHandle for SmartContract { + fn evm(address: H160) -> Self { + Self::Evm(address) + } + + fn wasm(address: AccountId) -> Self { + Self::Wasm(address) + } +} diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index 0f31ebec72..de1e2ac1a6 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -76,7 +76,7 @@ pallet-dapp-staking-v3 = { workspace = true } pallet-dapps-staking = { workspace = true } pallet-dynamic-evm-base-fee = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } -pallet-evm-precompile-dapps-staking = { workspace = true } +pallet-evm-precompile-dapp-staking-v3 = { workspace = true } pallet-evm-precompile-sr25519 = { workspace = true } pallet-evm-precompile-substrate-ecdsa = { workspace = true } pallet-evm-precompile-unified-accounts = { workspace = true } @@ -142,7 +142,7 @@ std = [ "pallet-evm-precompile-ed25519/std", "pallet-evm-precompile-modexp/std", "pallet-evm-precompile-sha3fips/std", - "pallet-evm-precompile-dapps-staking/std", + "pallet-evm-precompile-dapp-staking-v3/std", "pallet-evm-precompile-sr25519/std", "pallet-evm-precompile-substrate-ecdsa/std", "pallet-evm-precompile-unified-accounts/std", diff --git a/runtime/local/src/chain_extensions.rs b/runtime/local/src/chain_extensions.rs index 516bc8040b..87fc75da1a 100644 --- a/runtime/local/src/chain_extensions.rs +++ b/runtime/local/src/chain_extensions.rs @@ -28,10 +28,6 @@ pub use pallet_chain_extension_xvm::XvmExtension; // Following impls defines chain extension IDs. -impl RegisteredChainExtension for DappsStakingExtension { - const ID: u16 = 00; -} - impl RegisteredChainExtension for XvmExtension { const ID: u16 = 01; } diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index 71a002fef6..c5d8abae9a 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -62,7 +62,7 @@ use sp_runtime::{ use sp_std::prelude::*; pub use astar_primitives::{ - dapp_staking::{CycleConfiguration, StakingRewardHandler}, + dapp_staking::{CycleConfiguration, SmartContract, StakingRewardHandler}, evm::{EvmRevertCodeHandler, HashedDefaultMappings}, AccountId, Address, AssetId, Balance, BlockNumber, Hash, Header, Index, Signature, }; @@ -482,29 +482,6 @@ impl pallet_dapps_staking::Config for Runtime { type ForcePalletDisabled = ConstBool; // This will be set to `true` when needed } -/// Multi-VM pointer to smart contract instance. -#[derive( - PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, MaxEncodedLen, scale_info::TypeInfo, -)] -pub enum SmartContract { - /// EVM smart contract instance. - Evm(sp_core::H160), - /// Wasm smart contract instance. - Wasm(AccountId), -} - -impl Default for SmartContract { - fn default() -> Self { - SmartContract::Evm(H160::repeat_byte(0x00)) - } -} - -impl> From<[u8; 32]> for SmartContract { - fn from(input: [u8; 32]) -> Self { - SmartContract::Wasm(input.into()) - } -} - pub struct DummyPriceProvider; impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { fn average_price() -> FixedU64 { @@ -943,7 +920,7 @@ impl pallet_contracts::Config for Runtime { type WeightPrice = pallet_transaction_payment::Pallet; type WeightInfo = pallet_contracts::weights::SubstrateWeight; type ChainExtension = ( - DappsStakingExtension, + // DappsStakingExtension, XvmExtension, AssetsExtension>, UnifiedAccountsExtension, diff --git a/runtime/local/src/precompiles.rs b/runtime/local/src/precompiles.rs index 151c7ca577..d207068f4b 100644 --- a/runtime/local/src/precompiles.rs +++ b/runtime/local/src/precompiles.rs @@ -24,7 +24,7 @@ use frame_support::{parameter_types, traits::Contains}; use pallet_evm_precompile_assets_erc20::Erc20AssetsPrecompileSet; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; -use pallet_evm_precompile_dapps_staking::DappsStakingWrapper; +use pallet_evm_precompile_dapp_staking_v3::DappStakingV3Precompile; use pallet_evm_precompile_dispatch::Dispatch; use pallet_evm_precompile_ed25519::Ed25519Verify; use pallet_evm_precompile_modexp::Modexp; @@ -92,7 +92,7 @@ pub type LocalPrecompilesSetAt = ( // Local specific precompiles: PrecompileAt< AddressU64<20481>, - DappsStakingWrapper, + DappStakingV3Precompile, (CallableByContract, CallableByPrecompile), >, PrecompileAt< diff --git a/runtime/shibuya/Cargo.toml b/runtime/shibuya/Cargo.toml index 49eb2d6c01..73c2b42296 100644 --- a/runtime/shibuya/Cargo.toml +++ b/runtime/shibuya/Cargo.toml @@ -97,7 +97,6 @@ orml-xtokens = { workspace = true } # Astar pallets astar-primitives = { workspace = true } pallet-block-rewards-hybrid = { workspace = true } -pallet-chain-extension-dapps-staking = { workspace = true } pallet-chain-extension-unified-accounts = { workspace = true } pallet-chain-extension-xvm = { workspace = true } pallet-collator-selection = { workspace = true } @@ -165,7 +164,6 @@ std = [ "pallet-block-rewards-hybrid/std", "pallet-contracts/std", "pallet-contracts-primitives/std", - "pallet-chain-extension-dapps-staking/std", "pallet-chain-extension-xvm/std", "pallet-chain-extension-unified-accounts/std", "pallet-dynamic-evm-base-fee/std", diff --git a/runtime/shibuya/src/chain_extensions.rs b/runtime/shibuya/src/chain_extensions.rs index 516bc8040b..a368fa8d18 100644 --- a/runtime/shibuya/src/chain_extensions.rs +++ b/runtime/shibuya/src/chain_extensions.rs @@ -22,16 +22,11 @@ use super::{Runtime, UnifiedAccounts, Xvm}; pub use pallet_chain_extension_assets::AssetsExtension; use pallet_contracts::chain_extension::RegisteredChainExtension; -pub use pallet_chain_extension_dapps_staking::DappsStakingExtension; pub use pallet_chain_extension_unified_accounts::UnifiedAccountsExtension; pub use pallet_chain_extension_xvm::XvmExtension; // Following impls defines chain extension IDs. -impl RegisteredChainExtension for DappsStakingExtension { - const ID: u16 = 00; -} - impl RegisteredChainExtension for XvmExtension { const ID: u16 = 01; } diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index 38a59fe5ed..3a4a0a3119 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -667,7 +667,6 @@ impl pallet_contracts::Config for Runtime { type WeightPrice = pallet_transaction_payment::Pallet; type WeightInfo = pallet_contracts::weights::SubstrateWeight; type ChainExtension = ( - DappsStakingExtension, XvmExtension, AssetsExtension>, UnifiedAccountsExtension,