diff --git a/Cargo.lock b/Cargo.lock index 8e8b388e66..94c785d58e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1349,6 +1349,36 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crowdloan-rewards-precompiles" +version = "0.6.0" +dependencies = [ + "cumulus-pallet-parachain-system", + "cumulus-primitives-core", + "cumulus-primitives-parachain-inherent", + "cumulus-test-relay-sproof-builder", + "derive_more", + "evm", + "frame-support", + "frame-system", + "log", + "max-encoded-len", + "pallet-balances", + "pallet-crowdloan-rewards", + "pallet-evm", + "pallet-scheduler", + "pallet-timestamp", + "parity-scale-codec", + "precompile-utils", + "rustc-hex", + "serde", + "sha3 0.9.1", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -2161,6 +2191,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "faster-hex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02a61fea82de8484f9d7ce99b698b0e190ef8de71035690a961a43d7b18a2ad" + [[package]] name = "fastrand" version = "1.5.0" @@ -4788,6 +4824,7 @@ name = "moonbase-runtime" version = "0.8.4" dependencies = [ "account", + "crowdloan-rewards-precompiles", "cumulus-pallet-parachain-system", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", @@ -5112,6 +5149,7 @@ name = "moonbeam-runtime" version = "0.8.4" dependencies = [ "account", + "crowdloan-rewards-precompiles", "cumulus-pallet-parachain-system", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", @@ -5303,6 +5341,7 @@ name = "moonriver-runtime" version = "0.8.4" dependencies = [ "account", + "crowdloan-rewards-precompiles", "cumulus-pallet-parachain-system", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", @@ -5376,6 +5415,7 @@ name = "moonshadow-runtime" version = "0.8.4" dependencies = [ "account", + "crowdloan-rewards-precompiles", "cumulus-pallet-parachain-system", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", @@ -8245,6 +8285,22 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "precompile-utils" +version = "0.1.0" +dependencies = [ + "evm", + "frame-support", + "frame-system", + "log", + "pallet-evm", + "parity-scale-codec", + "slices", + "sp-core", + "sp-io", + "sp-std", +] + [[package]] name = "predicates" version = "1.0.8" @@ -10528,6 +10584,18 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +[[package]] +name = "slices" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2086e458a369cdca838e9f6ed04b4cc2e3ce636d99abb80c9e2eada107749cf" +dependencies = [ + "faster-hex", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "slog" version = "2.7.0" diff --git a/Cargo.toml b/Cargo.toml index e20d54cb15..fe75ed6ed5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ 'node', 'node/cli', 'node/service', - 'bin/utils/moonkey' + 'bin/utils/moonkey', ] exclude = [ 'bin/utils/moonkey' diff --git a/precompiles/crowdloan-rewards/Cargo.toml b/precompiles/crowdloan-rewards/Cargo.toml new file mode 100644 index 0000000000..4a2bce724d --- /dev/null +++ b/precompiles/crowdloan-rewards/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "crowdloan-rewards-precompiles" +version = "0.6.0" +authors = ["PureStake"] +edition = "2018" +description = "A Precompile to make crowdloan rewards accessible to pallet-evm" + +[dependencies] +log = "0.4" +rustc-hex = { version = "2.0.1", default-features = false } + +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.8", default-features = false } +evm = { version = "0.27.0", default-features = false, features = ["with-codec"] } +sp-std = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.8" } +sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.8" } +pallet-evm = { git = "https://github.com/purestake/frontier", default-features = false, branch = "moonbeam-polkadot-v0.9.8" } +pallet-crowdloan-rewards = { git = "https://github.com/purestake/crowdloan-rewards", default-features = false, branch = "main" } +frame-system = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.8" } +precompile-utils = { path = "../utils", default-features = false } + +[dev-dependencies] +sha3 = "0.9" +sp-io = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.8" } +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false } +sp-runtime = { git="https://github.com/paritytech/substrate", branch="polkadot-v0.9.8" } +pallet-balances = { git="https://github.com/paritytech/substrate", branch="polkadot-v0.9.8" } +pallet-timestamp = { git="https://github.com/paritytech/substrate", branch="polkadot-v0.9.8" } +pallet-scheduler = { git="https://github.com/paritytech/substrate", branch="polkadot-v0.9.8" } +max-encoded-len = { git="https://github.com/paritytech/substrate", branch="polkadot-v0.9.8", features=["derive"] } +serde = "1.0.100" +derive_more = "0.99" +cumulus-primitives-parachain-inherent = { git = "https://github.com/purestake/cumulus", default-features = false, branch = "joshy-np098" } +cumulus-pallet-parachain-system = { git = "https://github.com/purestake/cumulus", default-features = false, branch = "joshy-np098" } +cumulus-primitives-core = { git = "https://github.com/purestake/cumulus", default-features = false, branch = "joshy-np098" } +cumulus-test-relay-sproof-builder = { git = "https://github.com/purestake/cumulus", default-features = false, branch = "joshy-np098" } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "evm/std", + "sp-std/std", + "sp-core/std", + "pallet-crowdloan-rewards/std", + "frame-system/std", + "precompile-utils/std", + "pallet-evm/std", +] diff --git a/precompiles/crowdloan-rewards/CrowdloanInterface.sol b/precompiles/crowdloan-rewards/CrowdloanInterface.sol new file mode 100644 index 0000000000..81578d2923 --- /dev/null +++ b/precompiles/crowdloan-rewards/CrowdloanInterface.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.0; + +/// @author The Moonbeam Team +/// @title The interface through which solidity contracts will interact with Crowdloan Rewards +/// We follow this same interface including four-byte function selectors, in the precompile that +/// wraps the pallet +interface CrowdloanRewards { + // First some simple accessors + + /// @dev Checks whether the address is a contributor + /// @param contributor the address that we want to confirm is a contributor + /// @return A boolean confirming whether the address is a contributor + function is_contributor(address contributor) external view returns (bool); + + /// @dev Retrieve total reward and claimed reward for an address + /// @param contributor the address for which we want to retrieve the information + /// @return A u256 tuple, reflecting (total_rewards, claimed_rewards) + function reward_info(address contributor) external view returns (uint256, uint256); + + // Now the dispatchables + + /// @dev Claim the vested amount from the crowdloan rewards + function claim() external; + + /// @dev Update reward address to receive crowdloan rewards + /// @param new_address, the new_address where to receive the rewards from now on + function update_reward_address(address new_address) external; + +} + +// These are the selectors generated by remix following this advice +// https://ethereum.stackexchange.com/a/73405/9963 +// Eventually we will probably want a better way of generating these and copying them to Rust + +//{ +// "53440c90": "is_contributor(address)" +// "76f70249": "reward_info(address)" +// "4e71d92d": "claim()" +// "aaac61d6": "update_reward_address(address)" +//} \ No newline at end of file diff --git a/precompiles/crowdloan-rewards/src/lib.rs b/precompiles/crowdloan-rewards/src/lib.rs new file mode 100644 index 0000000000..f184ac6178 --- /dev/null +++ b/precompiles/crowdloan-rewards/src/lib.rs @@ -0,0 +1,273 @@ +// Copyright 2019-2021 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +//! Precompile to call pallet-crowdloan-rewards runtime methods via the EVM + +#![cfg_attr(not(feature = "std"), no_std)] + +use evm::{executor::PrecompileOutput, Context, ExitError, ExitSucceed}; +use frame_support::{ + dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}, + traits::{Currency, Get}, +}; +use pallet_evm::{AddressMapping, GasWeightMapping, Precompile}; +use precompile_utils::{error, InputReader, OutputBuilder}; + +use sp_core::{H160, U256}; +use sp_std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + marker::PhantomData, +}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +type BalanceOf = + <::RewardCurrency as Currency< + ::AccountId, + >>::Balance; + +/// A precompile to wrap the functionality from pallet_crowdloan_rewards. +pub struct CrowdloanRewardsWrapper(PhantomData); + +impl Precompile for CrowdloanRewardsWrapper +where + Runtime: pallet_crowdloan_rewards::Config + pallet_evm::Config, + Runtime::AccountId: From, + BalanceOf: TryFrom + Debug, + Runtime::Call: Dispatchable + GetDispatchInfo, + ::Origin: From>, + Runtime::Call: From>, +{ + fn execute( + input: &[u8], //Reminder this is big-endian + target_gas: Option, + context: &Context, + ) -> Result { + let input = InputReader::new(input)?; + + // Parse the function selector + // These are the four-byte function selectors calculated from the CrowdloanInterface.sol + // according to the solidity specification + // https://docs.soliditylang.org/en/v0.8.0/abi-spec.html#function-selector + let inner_call = match input.selector() { + // Check for accessor methods first. These return results immediately + [0x53, 0x44, 0x0c, 0x90] => { + return Self::is_contributor(input, target_gas); + } + [0x76, 0xf7, 0x02, 0x49] => { + return Self::reward_info(input, target_gas); + } + [0x4e, 0x71, 0xd9, 0x2d] => Self::claim()?, + + [0xaa, 0xac, 0x61, 0xd6] => Self::update_reward_address(input)?, + _ => { + log::trace!( + target: "crowdloan-rewards-precompile", + "Failed to match function selector in crowdloan rewards precompile" + ); + return Err(error( + "No crowdloan rewards wrapper method at given selector".into(), + )); + } + }; + + let outer_call: Runtime::Call = inner_call.into(); + let info = outer_call.get_dispatch_info(); + + // Make sure enough gas + if let Some(gas_limit) = target_gas { + let required_gas = Runtime::GasWeightMapping::weight_to_gas(info.weight); + if required_gas > gas_limit { + return Err(ExitError::OutOfGas); + } + } + log::trace!(target: "crowdloan-rewards-precompile", "Made it past gas check"); + + // Dispatch that call + let origin = Runtime::AddressMapping::into_account_id(context.caller); + + log::trace!(target: "crowdloan-rewards-precompile", "Gonna call with origin {:?}", origin); + + match outer_call.dispatch(Some(origin).into()) { + Ok(post_info) => { + let gas_used = Runtime::GasWeightMapping::weight_to_gas( + post_info.actual_weight.unwrap_or(info.weight), + ); + Ok(PrecompileOutput { + exit_status: ExitSucceed::Stopped, + cost: gas_used, + output: Default::default(), + logs: Default::default(), + }) + } + Err(e) => { + log::trace!( + target: "crowdloan-rewards-precompile", + "Crowdloan rewards call via evm failed {:?}", + e + ); + Err(ExitError::Other( + "Crowdloan rewards call via EVM failed".into(), + )) + } + } + } +} + +impl CrowdloanRewardsWrapper +where + Runtime: pallet_crowdloan_rewards::Config + pallet_evm::Config + frame_system::Config, + Runtime::AccountId: From, + BalanceOf: TryFrom + TryInto + Debug, + Runtime::Call: Dispatchable + GetDispatchInfo, + ::Origin: From>, + Runtime::Call: From>, +{ + // The accessors are first. They directly return their result. + fn is_contributor( + mut input: InputReader, + target_gas: Option, + ) -> Result { + // Bound check + input.expect_arguments(1)?; + + // parse the address + let contributor = input.read_address()?; + + log::trace!( + target: "crowdloan-rewards-precompile", + "Checking whether {:?} is a contributor", + contributor + ); + + let gas_consumed = ::GasWeightMapping::weight_to_gas( + ::DbWeight::get().read, + ); + + // Make sure enough gas + if let Some(gas_limit) = target_gas { + if gas_consumed > gas_limit { + return Err(ExitError::OutOfGas); + } + } + + let account: Runtime::AccountId = contributor.into(); + // fetch data from pallet + let is_contributor = + pallet_crowdloan_rewards::Pallet::::accounts_payable(account).is_some(); + + log::trace!(target: "crowldoan-rewards-precompile", "Result from pallet is {:?}", is_contributor); + + let gas_consumed = ::GasWeightMapping::weight_to_gas( + ::DbWeight::get().read, + ); + + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + cost: gas_consumed, + output: OutputBuilder::new().write_bool(is_contributor).build(), + logs: Default::default(), + }) + } + + fn reward_info( + mut input: InputReader, + target_gas: Option, + ) -> Result { + // Bound check + input.expect_arguments(1)?; + + // parse the address + let contributor = input.read_address()?; + + log::trace!( + target: "crowdloan-rewards-precompile", + "Checking reward info for {:?}", + contributor + ); + + let account: Runtime::AccountId = contributor.into(); + + let gas_consumed = ::GasWeightMapping::weight_to_gas( + ::DbWeight::get().read, + ); + + // Make sure enough gas + if let Some(gas_limit) = target_gas { + if gas_consumed > gas_limit { + return Err(ExitError::OutOfGas); + } + } + + // fetch data from pallet + let reward_info = pallet_crowdloan_rewards::Pallet::::accounts_payable(account); + + let (total, claimed): (U256, U256) = if let Some(reward_info) = reward_info { + let total_reward: u128 = reward_info.total_reward.try_into().map_err(|_| { + ExitError::Other("Amount is too large for provided balance type".into()) + })?; + let claimed_reward: u128 = reward_info.claimed_reward.try_into().map_err(|_| { + ExitError::Other("Amount is too large for provided balance type".into()) + })?; + + (total_reward.into(), claimed_reward.into()) + } else { + (0u128.into(), 0u128.into()) + }; + + log::trace!( + target: "crowldoan-rewards-precompile", "Result from pallet is {:?} {:?}", + total, claimed + ); + + let mut output = OutputBuilder::new().write_u256(total).build(); + output.extend(OutputBuilder::new().write_u256(claimed).build()); + + Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + cost: gas_consumed, + output: output, + logs: Default::default(), + }) + } + + fn claim() -> Result, ExitError> { + Ok(pallet_crowdloan_rewards::Call::::claim()) + } + + fn update_reward_address( + mut input: InputReader, + ) -> Result, ExitError> { + log::trace!( + target: "crowdloan-rewards-precompile", + "In update_reward_address dispatchable wrapper" + ); + + // Bound check + input.expect_arguments(1)?; + + // parse the address + let new_address = input.read_address()?; + + log::trace!(target: "crowdloan-rewards-precompile", "New account is {:?}", new_address); + + Ok(pallet_crowdloan_rewards::Call::::update_reward_address(new_address.into())) + } +} diff --git a/precompiles/crowdloan-rewards/src/mock.rs b/precompiles/crowdloan-rewards/src/mock.rs new file mode 100644 index 0000000000..71ed87af24 --- /dev/null +++ b/precompiles/crowdloan-rewards/src/mock.rs @@ -0,0 +1,362 @@ +// Copyright 2019-2021 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +//! Test utilities +use super::*; +use codec::{Decode, Encode}; +use cumulus_primitives_core::{ + relay_chain::BlockNumber as RelayChainBlockNumber, PersistedValidationData, +}; +use cumulus_primitives_parachain_inherent::ParachainInherentData; +use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; +use frame_support::{ + construct_runtime, + dispatch::UnfilteredDispatchable, + inherent::{InherentData, ProvideInherent}, + parameter_types, + traits::{GenesisBuild, MaxEncodedLen, OnFinalize, OnInitialize}, +}; +use frame_system::RawOrigin; +use pallet_evm::{AddressMapping, EnsureAddressNever, EnsureAddressRoot, PrecompileSet}; +use serde::{Deserialize, Serialize}; +use sp_core::H256; +use sp_io; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + Perbill, +}; +pub type AccountId = TestAccount; +pub type Balance = u128; +pub type BlockNumber = u64; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + Evm: pallet_evm::{Pallet, Call, Storage, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, + ParachainSystem: cumulus_pallet_parachain_system::{Pallet, Call, Storage, Inherent, Event}, + Crowdloan: pallet_crowdloan_rewards::{Pallet, Call, Storage, Event}, + } +); + +// FRom https://github.com/PureStake/moonbeam/pull/518. Merge to common once is merged +#[derive( + Eq, + PartialEq, + Ord, + PartialOrd, + Clone, + Encode, + Decode, + Debug, + MaxEncodedLen, + Serialize, + Deserialize, + derive_more::Display, +)] +pub enum TestAccount { + Alice, + Bob, + Charlie, + Bogus, +} + +impl Default for TestAccount { + fn default() -> Self { + Self::Bogus + } +} + +impl AddressMapping for TestAccount { + fn into_account_id(h160_account: H160) -> TestAccount { + match h160_account { + a if a == H160::repeat_byte(0xAA) => Self::Alice, + a if a == H160::repeat_byte(0xBB) => Self::Bob, + a if a == H160::repeat_byte(0xCC) => Self::Charlie, + _ => Self::Bogus, + } + } +} + +impl TestAccount { + pub(crate) fn to_h160(&self) -> H160 { + match self { + Self::Alice => H160::repeat_byte(0xAA), + Self::Bob => H160::repeat_byte(0xBB), + Self::Charlie => H160::repeat_byte(0xCC), + Self::Bogus => Default::default(), + } + } +} + +impl From for TestAccount { + fn from(x: H160) -> TestAccount { + TestAccount::into_account_id(x) + } +} + +parameter_types! { + pub ParachainId: cumulus_primitives_core::ParaId = 100.into(); +} + +impl cumulus_pallet_parachain_system::Config for Test { + type SelfParaId = ParachainId; + type Event = Event; + type OnValidationData = (); + type OutboundXcmpMessageSource = (); + type XcmpMessageHandler = (); + type ReservedXcmpWeight = (); + type DmpMessageHandler = (); + type ReservedDmpWeight = (); +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} +impl frame_system::Config for Test { + type BaseCallFilter = (); + type DbWeight = (); + type Origin = Origin; + type Index = u64; + type BlockNumber = BlockNumber; + type Call = Call; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = TestAccount; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = cumulus_pallet_parachain_system::ParachainSetCode; +} +parameter_types! { + pub const ExistentialDeposit: u128 = 0; +} +impl pallet_balances::Config for Test { + type MaxReserves = (); + type ReserveIdentifier = (); + type MaxLocks = (); + type Balance = Balance; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); +} + +/// The crowdloan precompile is available at address one in the mock runtime. +pub fn precompile_address() -> H160 { + H160::from_low_u64_be(1) +} + +#[derive(Debug, Clone, Copy)] +pub struct TestPrecompiles(PhantomData); + +impl PrecompileSet for TestPrecompiles +where + R::Call: Dispatchable + GetDispatchInfo + Decode, + ::Origin: From>, + R: pallet_crowdloan_rewards::Config + pallet_evm::Config, + R::AccountId: From, + BalanceOf: TryFrom + Debug, + R::Call: From>, +{ + fn execute( + address: H160, + input: &[u8], + target_gas: Option, + context: &Context, + ) -> Option> { + match address { + a if a == precompile_address() => Some(CrowdloanRewardsWrapper::::execute( + input, target_gas, context, + )), + _ => None, + } + } +} + +pub type Precompiles = TestPrecompiles; + +impl pallet_evm::Config for Test { + type FeeCalculator = (); + type GasWeightMapping = (); + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = TestAccount; + type Currency = Balances; + type Event = Event; + type Runner = pallet_evm::runner::stack::Runner; + type Precompiles = Precompiles; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +parameter_types! { + pub const TestMaxInitContributors: u32 = 8; + pub const TestMinimumReward: u128 = 0; + pub const TestInitialized: bool = false; + pub const TestInitializationPayment: Perbill = Perbill::from_percent(20); +} + +impl pallet_crowdloan_rewards::Config for Test { + type Event = Event; + type Initialized = TestInitialized; + type InitializationPayment = TestInitializationPayment; + type MaxInitContributors = TestMaxInitContributors; + type MinimumReward = TestMinimumReward; + type RewardCurrency = Balances; + type RelayChainAccountId = [u8; 32]; +} +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, + crowdloan_pot: Balance, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { + balances: vec![], + crowdloan_pot: 0u32.into(), + } + } +} + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + pub(crate) fn with_crowdloan_pot(mut self, pot: Balance) -> Self { + self.crowdloan_pot = pot; + self + } + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + pallet_crowdloan_rewards::GenesisConfig:: { + funded_amount: self.crowdloan_pot, + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +//TODO Add pallets here if necessary +pub(crate) fn roll_to(n: u64) { + while System::block_number() < n { + // Relay chain Stuff. I might actually set this to a number different than N + let sproof_builder = RelayStateSproofBuilder::default(); + let (relay_parent_storage_root, relay_chain_state) = + sproof_builder.into_state_root_and_proof(); + let vfp = PersistedValidationData { + relay_parent_number: (System::block_number() + 1u64) as RelayChainBlockNumber, + relay_parent_storage_root, + ..Default::default() + }; + let inherent_data = { + let mut inherent_data = InherentData::default(); + let system_inherent_data = ParachainInherentData { + validation_data: vfp.clone(), + relay_chain_state, + downward_messages: Default::default(), + horizontal_messages: Default::default(), + }; + inherent_data + .put_data( + cumulus_primitives_parachain_inherent::INHERENT_IDENTIFIER, + &system_inherent_data, + ) + .expect("failed to put VFP inherent"); + inherent_data + }; + + ParachainSystem::on_initialize(System::block_number()); + ParachainSystem::create_inherent(&inherent_data) + .expect("got an inherent") + .dispatch_bypass_filter(RawOrigin::None.into()) + .expect("dispatch succeeded"); + ParachainSystem::on_finalize(System::block_number()); + + Balances::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + Balances::on_initialize(System::block_number()); + } +} + +pub(crate) fn events() -> Vec { + System::events() + .into_iter() + .map(|r| r.event) + .collect::>() +} + +// Helper function to give a simple evm context suitable for tests. +// We can remove this once https://github.com/rust-blockchain/evm/pull/35 +// is in our dependency graph. +pub fn evm_test_context() -> evm::Context { + evm::Context { + address: Default::default(), + caller: Default::default(), + apparent_value: From::from(0), + } +} diff --git a/precompiles/crowdloan-rewards/src/tests.rs b/precompiles/crowdloan-rewards/src/tests.rs new file mode 100644 index 0000000000..e665cd1fbf --- /dev/null +++ b/precompiles/crowdloan-rewards/src/tests.rs @@ -0,0 +1,326 @@ +// Copyright 2019-2021 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +use crate::mock::Crowdloan; +use crate::mock::{ + events, evm_test_context, precompile_address, roll_to, Call, ExtBuilder, Origin, Precompiles, + TestAccount::Alice, TestAccount::Bob, TestAccount::Charlie, +}; +use crate::PrecompileOutput; +use frame_support::{assert_ok, dispatch::Dispatchable}; +use pallet_crowdloan_rewards::{Call as CrowdloanCall, Event as CrowdloanEvent}; +use pallet_evm::Call as EvmCall; +use pallet_evm::{ExitError, ExitSucceed, PrecompileSet}; +use precompile_utils::OutputBuilder; +use sha3::{Digest, Keccak256}; +use sp_core::U256; + +#[test] +fn selector_less_than_four_bytes() { + ExtBuilder::default().build().execute_with(|| { + // This selector is only three bytes long when four are required. + let bogus_selector = vec![1u8, 2u8, 3u8]; + + // Expected result is an error stating there are too few bytes + let expected_result = Some(Err(ExitError::Other( + "input must at least contain a selector".into(), + ))); + + assert_eq!( + Precompiles::execute( + precompile_address(), + &bogus_selector, + None, + &evm_test_context(), + ), + expected_result + ); + }); +} + +#[test] +fn no_selector_exists_but_length_is_right() { + ExtBuilder::default().build().execute_with(|| { + let bogus_selector = vec![1u8, 2u8, 3u8, 4u8]; + + // Expected result is an error stating there are such a selector does not exist + let expected_result = Some(Err(ExitError::Other( + "No crowdloan rewards wrapper method at given selector".into(), + ))); + + assert_eq!( + Precompiles::execute( + precompile_address(), + &bogus_selector, + None, + &evm_test_context(), + ), + expected_result + ); + }); +} + +#[test] +fn is_contributor_returns_false() { + ExtBuilder::default() + .with_balances(vec![(Alice, 1000)]) + .build() + .execute_with(|| { + let selector = &Keccak256::digest(b"is_contributor(address)")[0..4]; + + // Construct data to read prop count + let mut input_data = Vec::::from([0u8; 36]); + input_data[0..4].copy_from_slice(&selector); + input_data[16..36].copy_from_slice(&Alice.to_h160().0); + + // Expected result is one + let expected_one_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: OutputBuilder::new().write_bool(false).build(), + cost: Default::default(), + logs: Default::default(), + })); + + // Assert that no props have been opened. + assert_eq!( + Precompiles::execute(precompile_address(), &input_data, None, &evm_test_context()), + expected_one_result + ); + }); +} + +#[test] +fn is_contributor_returns_true() { + ExtBuilder::default() + .with_balances(vec![(Alice, 1000)]) + .with_crowdloan_pot(100u32.into()) + .build() + .execute_with(|| { + pub const VESTING: u32 = 8; + // The init relay block gets inserted + roll_to(2); + + let init_block = Crowdloan::init_relay_block(); + assert_ok!(Call::Crowdloan(CrowdloanCall::initialize_reward_vec(vec![ + ([1u8; 32].into(), Some(Alice), 50u32.into()), + ([2u8; 32].into(), Some(Bob), 50u32.into()), + ])) + .dispatch(Origin::root())); + + assert_ok!(Crowdloan::complete_initialization( + Origin::root(), + init_block + VESTING + )); + let selector = &Keccak256::digest(b"is_contributor(address)")[0..4]; + + // Construct data to read prop count + let mut input_data = Vec::::from([0u8; 36]); + input_data[0..4].copy_from_slice(&selector); + input_data[16..36].copy_from_slice(&Alice.to_h160().0); + + // Expected result is one + let expected_one_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: OutputBuilder::new().write_bool(true).build(), + cost: Default::default(), + logs: Default::default(), + })); + + // Assert that no props have been opened. + assert_eq!( + Precompiles::execute(precompile_address(), &input_data, None, &evm_test_context()), + expected_one_result + ); + }); +} + +#[test] +fn claim_works() { + ExtBuilder::default() + .with_balances(vec![(Alice, 1000)]) + .with_crowdloan_pot(100u32.into()) + .build() + .execute_with(|| { + pub const VESTING: u32 = 8; + // The init relay block gets inserted + roll_to(2); + + let init_block = Crowdloan::init_relay_block(); + assert_ok!(Call::Crowdloan(CrowdloanCall::initialize_reward_vec(vec![ + ([1u8; 32].into(), Some(Alice), 50u32.into()), + ([2u8; 32].into(), Some(Bob), 50u32.into()), + ])) + .dispatch(Origin::root())); + + assert_ok!(Crowdloan::complete_initialization( + Origin::root(), + init_block + VESTING + )); + + roll_to(5); + + let selector = &Keccak256::digest(b"claim()")[0..4]; + + // Construct data to read prop count + let mut input_data = Vec::::from([0u8; 4]); + input_data[0..4].copy_from_slice(&selector); + + // Make sure the call goes through successfully + assert_ok!(Call::Evm(EvmCall::call( + Alice.to_h160(), + precompile_address(), + input_data, + U256::zero(), // No value sent in EVM + u64::max_value(), + 0.into(), + None, // Use the next nonce + )) + .dispatch(Origin::root())); + + let expected: crate::mock::Event = CrowdloanEvent::RewardsPaid(Alice, 25).into(); + // Assert that the events vector contains the one expected + assert!(events().contains(&expected)); + }); +} + +#[test] +fn reward_info_works() { + ExtBuilder::default() + .with_balances(vec![(Alice, 1000)]) + .with_crowdloan_pot(100u32.into()) + .build() + .execute_with(|| { + pub const VESTING: u32 = 8; + // The init relay block gets inserted + roll_to(2); + + let init_block = Crowdloan::init_relay_block(); + assert_ok!(Call::Crowdloan(CrowdloanCall::initialize_reward_vec(vec![ + ([1u8; 32].into(), Some(Alice), 50u32.into()), + ([2u8; 32].into(), Some(Bob), 50u32.into()), + ])) + .dispatch(Origin::root())); + + assert_ok!(Crowdloan::complete_initialization( + Origin::root(), + init_block + VESTING + )); + + roll_to(5); + + let selector = &Keccak256::digest(b"reward_info(address)")[0..4]; + + // Construct data to read prop count + let mut input_data = Vec::::from([0u8; 36]); + input_data[0..4].copy_from_slice(&selector); + input_data[16..36].copy_from_slice(&Alice.to_h160().0); + + let mut output = OutputBuilder::new().write_u256(50u64).build(); + output.extend(OutputBuilder::new().write_u256(10u64).build()); // Expected result + let expected_buffer_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: output, + cost: Default::default(), + logs: Default::default(), + })); + + // Assert that no props have been opened. + assert_eq!( + Precompiles::execute(precompile_address(), &input_data, None, &evm_test_context()), + expected_buffer_result + ); + }); +} + +#[test] +fn update_reward_address_works() { + ExtBuilder::default() + .with_balances(vec![(Alice, 1000)]) + .with_crowdloan_pot(100u32.into()) + .build() + .execute_with(|| { + pub const VESTING: u32 = 8; + // The init relay block gets inserted + roll_to(2); + + let init_block = Crowdloan::init_relay_block(); + assert_ok!(Call::Crowdloan(CrowdloanCall::initialize_reward_vec(vec![ + ([1u8; 32].into(), Some(Alice), 50u32.into()), + ([2u8; 32].into(), Some(Bob), 50u32.into()), + ])) + .dispatch(Origin::root())); + + assert_ok!(Crowdloan::complete_initialization( + Origin::root(), + init_block + VESTING + )); + + roll_to(5); + + let selector = &Keccak256::digest(b"update_reward_address(address)")[0..4]; + + // Construct data to read prop count + let mut input_data = Vec::::from([0u8; 36]); + input_data[0..4].copy_from_slice(&selector); + input_data[16..36].copy_from_slice(&Charlie.to_h160().0); + + // Make sure the call goes through successfully + assert_ok!(Call::Evm(EvmCall::call( + Alice.to_h160(), + precompile_address(), + input_data, + U256::zero(), // No value sent in EVM + u64::max_value(), + 0.into(), + None, // Use the next nonce + )) + .dispatch(Origin::root())); + + let expected: crate::mock::Event = + CrowdloanEvent::RewardAddressUpdated(Alice, Charlie).into(); + // Assert that the events vector contains the one expected + assert!(events().contains(&expected)); + // Assert storage is correctly moved + assert!(Crowdloan::accounts_payable(Alice).is_none()); + assert!(Crowdloan::accounts_payable(Charlie).is_some()); + }); +} + +#[test] +fn test_bound_checks_for_address_parsing() { + ExtBuilder::default() + .with_balances(vec![(Alice, 1000)]) + .with_crowdloan_pot(100u32.into()) + .build() + .execute_with(|| { + let selector = &Keccak256::digest(b"update_reward_address(address)")[0..4]; + + // Construct data to read prop count + let mut input_data = Vec::::from([0u8; 20]); + input_data[0..4].copy_from_slice(&selector); + input_data[16..20].copy_from_slice(&[1u8; 4]); + + // Expected result is an error stating there are too few bytes + let expected_result = Some(Err(ExitError::Other( + "input doesn't match expected length".into(), + ))); + + assert_eq!( + Precompiles::execute(precompile_address(), &input_data, None, &evm_test_context(),), + expected_result + ); + }) +} diff --git a/precompiles/utils/Cargo.toml b/precompiles/utils/Cargo.toml index 0767966af2..6cc9af4927 100644 --- a/precompiles/utils/Cargo.toml +++ b/precompiles/utils/Cargo.toml @@ -30,4 +30,4 @@ std = [ "frame-system/std", "pallet-evm/std", "evm/std", -] \ No newline at end of file +] diff --git a/precompiles/utils/src/lib.rs b/precompiles/utils/src/lib.rs index f76c2f6878..a82108db39 100644 --- a/precompiles/utils/src/lib.rs +++ b/precompiles/utils/src/lib.rs @@ -120,6 +120,16 @@ impl OutputBuilder { self.data.extend_from_slice(&buffer); self } + + /// Push a U256 to the output. + pub fn write_bool>(mut self, value: T) -> Self { + let mut buffer = [0u8; 32]; + if value.into() { + buffer[31] = 1; + } + self.data.extend_from_slice(&buffer); + self + } } impl Default for OutputBuilder { diff --git a/runtime/moonbase/Cargo.toml b/runtime/moonbase/Cargo.toml index 1881d7602d..45785f83e6 100644 --- a/runtime/moonbase/Cargo.toml +++ b/runtime/moonbase/Cargo.toml @@ -71,6 +71,7 @@ pallet-proxy = { git = "https://github.com/paritytech/substrate", default-featur pallet-treasury = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.8" } pallet-crowdloan-rewards = { git = "https://github.com/purestake/crowdloan-rewards", default-features = false, branch = "main" } +crowdloan-rewards-precompiles = { path = "../../precompiles/crowdloan-rewards", default-features = false } moonbeam-evm-tracer = { path = "../evm_tracer", default-features = false } moonbeam-rpc-primitives-debug = { path = "../../primitives/rpc/debug", default-features = false } diff --git a/runtime/moonbase/src/precompiles.rs b/runtime/moonbase/src/precompiles.rs index 2d9b691d20..1280f23bc6 100644 --- a/runtime/moonbase/src/precompiles.rs +++ b/runtime/moonbase/src/precompiles.rs @@ -16,6 +16,7 @@ #![cfg_attr(not(feature = "std"), no_std)] +use crowdloan_rewards_precompiles::CrowdloanRewardsWrapper; use evm::{executor::PrecompileOutput, Context, ExitError}; use frame_support::dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}; use pallet_evm::{Precompile, PrecompileSet}; @@ -36,6 +37,11 @@ type BalanceOf = <::Currency as C ::AccountId, >>::Balance; +type RewardBalanceOf = + <::RewardCurrency as Currency< + ::AccountId, + >>::Balance; + /// The PrecompileSet installed in the Moonbase runtime. /// We include the nine Istanbul precompiles /// (https://github.com/ethereum/go-ethereum/blob/3c46f557/core/vm/contracts.go#L69) @@ -50,7 +56,7 @@ where /// Return all addresses that contain precompiles. This can be used to populate dummy code /// under the precompile. pub fn used_addresses() -> impl Iterator { - sp_std::vec![1, 2, 3, 4, 5, 6, 7, 8, 1024, 1025, 1026, 2048] + sp_std::vec![1, 2, 3, 4, 5, 6, 7, 8, 1024, 1025, 1026, 2048, 2049] .into_iter() .map(|x| hash(x).into()) } @@ -64,10 +70,11 @@ impl PrecompileSet for MoonbasePrecompiles where R::Call: Dispatchable + GetDispatchInfo + Decode, ::Origin: From>, - R: parachain_staking::Config + pallet_evm::Config, + R: parachain_staking::Config + pallet_evm::Config + pallet_crowdloan_rewards::Config, R::AccountId: From, BalanceOf: TryFrom + Debug, - R::Call: From>, + RewardBalanceOf: TryFrom + Debug, + R::Call: From> + From>, { fn execute( address: H160, @@ -93,6 +100,9 @@ where a if a == hash(2048) => Some(ParachainStakingWrapper::::execute( input, target_gas, context, )), + a if a == hash(2049) => Some(CrowdloanRewardsWrapper::::execute( + input, target_gas, context, + )), _ => None, } } diff --git a/runtime/moonbase/tests/integration_test.rs b/runtime/moonbase/tests/integration_test.rs index 00e7fe0108..1b987c32fe 100644 --- a/runtime/moonbase/tests/integration_test.rs +++ b/runtime/moonbase/tests/integration_test.rs @@ -532,6 +532,370 @@ fn initialize_crowdloan_addresses_with_batch_and_pay() { }); } +#[test] +fn claim_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * UNIT), + (AccountId::from(BOB), 1_000 * UNIT), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * UNIT)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * UNIT) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * UNIT + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * UNIT + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + // 30 percent initial payout + assert_eq!(Balances::balance(&AccountId::from(CHARLIE)), 450_000 * UNIT); + // 30 percent initial payout + assert_eq!(Balances::balance(&AccountId::from(DAVE)), 450_000 * UNIT); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Alice uses the crowdloan precompile to claim through the EVM + let gas_limit = 100000u64; + let gas_price: U256 = 1_000_000_000.into(); + + // Construct the call data (selector, amount) + let mut call_data = Vec::::from([0u8; 4]); + call_data[0..4].copy_from_slice(&Keccak256::digest(b"claim()")[0..4]); + + assert_ok!(Call::EVM(pallet_evm::Call::::call( + AccountId::from(CHARLIE), + crowdloan_precompile_address, + call_data, + U256::zero(), // No value sent in EVM + gas_limit, + gas_price, + None, // Use the next nonce + )) + .dispatch(::Origin::root())); + + let vesting_period = 4 * WEEKS as u128; + let per_block = (1_050_000 * UNIT) / vesting_period; + + assert_eq!( + CrowdloanRewards::accounts_payable(&AccountId::from(CHARLIE)) + .unwrap() + .claimed_reward, + (450_000 * UNIT) + per_block + ); + }) +} + +#[test] +fn is_contributor_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * UNIT), + (AccountId::from(BOB), 1_000 * UNIT), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * UNIT)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * UNIT) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * UNIT + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * UNIT + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Construct the input data to check if Bob is a contributor + let mut bob_input_data = Vec::::from([0u8; 36]); + bob_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"is_contributor(address)")[0..4]); + bob_input_data[16..36].copy_from_slice(&BOB); + + // Expected result is an EVM boolean false which is 256 bits long. + let mut expected_bytes = Vec::from([0u8; 32]); + expected_bytes[31] = 0; + let expected_false_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is not a contributor + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &bob_input_data, + None, // target_gas is not necessary right now because consumed none now + &evm_test_context(), + ), + expected_false_result + ); + + // Construct the input data to check if Charlie is a contributor + let mut charlie_input_data = Vec::::from([0u8; 36]); + charlie_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"is_contributor(address)")[0..4]); + charlie_input_data[16..36].copy_from_slice(&CHARLIE); + + // Expected result is an EVM boolean true which is 256 bits long. + let mut expected_bytes = Vec::from([0u8; 32]); + expected_bytes[31] = 1; + let expected_true_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is a nominator + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &charlie_input_data, + None, // target_gas is not necessary right now because consumed none now + &evm_test_context(), + ), + expected_true_result + ); + }) +} + +#[test] +fn reward_info_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * UNIT), + (AccountId::from(BOB), 1_000 * UNIT), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * UNIT)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * UNIT) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * UNIT + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * UNIT + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Construct the input data to check if Bob is a contributor + let mut charlie_input_data = Vec::::from([0u8; 36]); + charlie_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"reward_info(address)")[0..4]); + charlie_input_data[16..36].copy_from_slice(&CHARLIE); + + let expected_total: U256 = (1_500_000 * UNIT).into(); + let expected_claimed: U256 = (450_000 * UNIT).into(); + + // Expected result is two EVM u256 false which are 256 bits long. + let mut expected_bytes = Vec::from([0u8; 64]); + expected_total.to_big_endian(&mut expected_bytes[0..32]); + expected_claimed.to_big_endian(&mut expected_bytes[32..64]); + let expected_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is not a contributor + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &charlie_input_data, + None, // target_gas is not necessary right now because consumed none now + &evm_test_context(), + ), + expected_result + ); + }) +} + +#[test] +fn update_reward_address_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * UNIT), + (AccountId::from(BOB), 1_000 * UNIT), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * UNIT)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * UNIT) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * UNIT + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * UNIT + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Charlie uses the crowdloan precompile to update address through the EVM + let gas_limit = 100000u64; + let gas_price: U256 = 1_000_000_000.into(); + + // Construct the input data to check if Bob is a contributor + let mut call_data = Vec::::from([0u8; 36]); + call_data[0..4] + .copy_from_slice(&Keccak256::digest(b"update_reward_address(address)")[0..4]); + call_data[16..36].copy_from_slice(&ALICE); + + assert_ok!(Call::EVM(pallet_evm::Call::::call( + AccountId::from(CHARLIE), + crowdloan_precompile_address, + call_data, + U256::zero(), // No value sent in EVM + gas_limit, + gas_price, + None, // Use the next nonce + )) + .dispatch(::Origin::root())); + + assert!(CrowdloanRewards::accounts_payable(&AccountId::from(CHARLIE)).is_none()); + assert_eq!( + CrowdloanRewards::accounts_payable(&AccountId::from(ALICE)) + .unwrap() + .claimed_reward, + (450_000 * UNIT) + ); + }) +} + #[test] fn join_candidates_via_precompile() { ExtBuilder::default() diff --git a/runtime/moonbeam/Cargo.toml b/runtime/moonbeam/Cargo.toml index 3eef9fe417..b1801f40d9 100644 --- a/runtime/moonbeam/Cargo.toml +++ b/runtime/moonbeam/Cargo.toml @@ -70,6 +70,7 @@ pallet-proxy = { git = "https://github.com/paritytech/substrate", default-featur pallet-treasury = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.8" } pallet-crowdloan-rewards = { git = "https://github.com/purestake/crowdloan-rewards", default-features = false, branch = "main" } +crowdloan-rewards-precompiles = { path = "../../precompiles/crowdloan-rewards", default-features = false } moonbeam-evm-tracer = { path = "../evm_tracer", default-features = false } moonbeam-rpc-primitives-debug = { path = "../../primitives/rpc/debug", default-features = false } diff --git a/runtime/moonbeam/src/precompiles.rs b/runtime/moonbeam/src/precompiles.rs index 0fce981aea..0e39601a85 100644 --- a/runtime/moonbeam/src/precompiles.rs +++ b/runtime/moonbeam/src/precompiles.rs @@ -16,6 +16,7 @@ #![cfg_attr(not(feature = "std"), no_std)] +use crowdloan_rewards_precompiles::CrowdloanRewardsWrapper; use evm::{executor::PrecompileOutput, Context, ExitError}; use frame_support::dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}; use pallet_evm::{Precompile, PrecompileSet}; @@ -36,6 +37,11 @@ type BalanceOf = <::Currency as C ::AccountId, >>::Balance; +type RewardBalanceOf = + <::RewardCurrency as Currency< + ::AccountId, + >>::Balance; + /// The PrecompileSet installed in the Moonbeam runtime. /// We include the nine Istanbul precompiles /// (https://github.com/ethereum/go-ethereum/blob/3c46f557/core/vm/contracts.go#L69) @@ -50,7 +56,7 @@ where /// Return all addresses that contain precompiles. This can be used to populate dummy code /// under the precompile. pub fn used_addresses() -> impl Iterator { - sp_std::vec![1, 2, 3, 4, 5, 6, 7, 8, 1024, 1025, 1026, 2048] + sp_std::vec![1, 2, 3, 4, 5, 6, 7, 8, 1024, 1025, 1026, 2048, 2049] .into_iter() .map(|x| hash(x).into()) } @@ -64,10 +70,11 @@ impl PrecompileSet for MoonbeamPrecompiles where R::Call: Dispatchable + GetDispatchInfo + Decode, ::Origin: From>, - R: parachain_staking::Config + pallet_evm::Config, + R: parachain_staking::Config + pallet_evm::Config + pallet_crowdloan_rewards::Config, R::AccountId: From, BalanceOf: TryFrom + Debug, - R::Call: From>, + RewardBalanceOf: TryFrom + Debug, + R::Call: From> + From>, { fn execute( address: H160, @@ -93,6 +100,9 @@ where a if a == hash(2048) => Some(ParachainStakingWrapper::::execute( input, target_gas, context, )), + a if a == hash(2049) => Some(CrowdloanRewardsWrapper::::execute( + input, target_gas, context, + )), _ => None, } } diff --git a/runtime/moonbeam/tests/integration_test.rs b/runtime/moonbeam/tests/integration_test.rs index 1a7ff0b47f..c584b81754 100644 --- a/runtime/moonbeam/tests/integration_test.rs +++ b/runtime/moonbeam/tests/integration_test.rs @@ -533,6 +533,387 @@ fn initialize_crowdloan_addresses_with_batch_and_pay() { }); } +#[ignore] +#[test] +fn claim_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * GLMR), + (AccountId::from(BOB), 1_000 * GLMR), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * GLMR)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * GLMR) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * GLMR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * GLMR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + // 30 percent initial payout + assert_eq!(Balances::balance(&AccountId::from(CHARLIE)), 450_000 * GLMR); + // 30 percent initial payout + assert_eq!(Balances::balance(&AccountId::from(DAVE)), 450_000 * GLMR); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Alice uses the crowdloan precompile to claim through the EVM + let gas_limit = 100000u64; + let gas_price: U256 = 1_000_000_000.into(); + + // Construct the call data (selector, amount) + let mut call_data = Vec::::from([0u8; 4]); + call_data[0..4].copy_from_slice(&Keccak256::digest(b"claim()")[0..4]); + + assert_ok!(Call::EVM(pallet_evm::Call::::call( + AccountId::from(CHARLIE), + crowdloan_precompile_address, + call_data, + U256::zero(), // No value sent in EVM + gas_limit, + gas_price, + None, // Use the next nonce + )) + .dispatch(::Origin::root())); + + let vesting_period = 4 * WEEKS as u128; + let per_block = (1_050_000 * GLMR) / vesting_period; + + assert_eq!( + CrowdloanRewards::accounts_payable(&AccountId::from(CHARLIE)) + .unwrap() + .claimed_reward, + (450_000 * GLMR) + per_block + ); + }) +} + +#[test] +fn is_contributor_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * GLMR), + (AccountId::from(BOB), 1_000 * GLMR), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * GLMR)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * GLMR) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * GLMR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * GLMR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Construct the input data to check if Bob is a contributor + let mut bob_input_data = Vec::::from([0u8; 36]); + bob_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"is_contributor(address)")[0..4]); + bob_input_data[16..36].copy_from_slice(&BOB); + + // Expected result is an EVM boolean false which is 256 bits long. + let mut expected_bytes = Vec::from([0u8; 32]); + expected_bytes[31] = 0; + let expected_false_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is not a contributor + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &bob_input_data, + None, // target_gas is not necessary right now because consumed none now + &Context { + // This context copied from Sacrifice tests, it's not great. + address: Default::default(), + caller: Default::default(), + apparent_value: From::from(0), + }, + ), + expected_false_result + ); + + // Construct the input data to check if Charlie is a contributor + let mut charlie_input_data = Vec::::from([0u8; 36]); + charlie_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"is_contributor(address)")[0..4]); + charlie_input_data[16..36].copy_from_slice(&CHARLIE); + + // Expected result is an EVM boolean true which is 256 bits long. + let mut expected_bytes = Vec::from([0u8; 32]); + expected_bytes[31] = 1; + let expected_true_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is a nominator + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &charlie_input_data, + None, // target_gas is not necessary right now because consumed none now + &Context { + // This context copied from Sacrifice tests, it's not great. + address: Default::default(), + caller: Default::default(), + apparent_value: From::from(0), + }, + ), + expected_true_result + ); + }) +} + +#[test] +fn reward_info_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * GLMR), + (AccountId::from(BOB), 1_000 * GLMR), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * GLMR)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * GLMR) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * GLMR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * GLMR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Construct the input data to check if Bob is a contributor + let mut charlie_input_data = Vec::::from([0u8; 36]); + charlie_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"reward_info(address)")[0..4]); + charlie_input_data[16..36].copy_from_slice(&CHARLIE); + + let expected_total: U256 = (1_500_000 * GLMR).into(); + let expected_claimed: U256 = (450_000 * GLMR).into(); + + // Expected result is two EVM u256 false which are 256 bits long. + let mut expected_bytes = Vec::from([0u8; 64]); + expected_total.to_big_endian(&mut expected_bytes[0..32]); + expected_claimed.to_big_endian(&mut expected_bytes[32..64]); + let expected_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is not a contributor + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &charlie_input_data, + None, // target_gas is not necessary right now because consumed none now + &Context { + // This context copied from Sacrifice tests, it's not great. + address: Default::default(), + caller: Default::default(), + apparent_value: From::from(0), + }, + ), + expected_result + ); + }) +} + +#[ignore] +#[test] +fn update_reward_address_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * GLMR), + (AccountId::from(BOB), 1_000 * GLMR), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * GLMR)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * GLMR) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * GLMR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * GLMR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Charlie uses the crowdloan precompile to update address through the EVM + let gas_limit = 100000u64; + let gas_price: U256 = 1_000_000_000.into(); + + // Construct the input data to check if Bob is a contributor + let mut call_data = Vec::::from([0u8; 36]); + call_data[0..4] + .copy_from_slice(&Keccak256::digest(b"update_reward_address(address)")[0..4]); + call_data[16..36].copy_from_slice(&ALICE); + + assert_ok!(Call::EVM(pallet_evm::Call::::call( + AccountId::from(CHARLIE), + crowdloan_precompile_address, + call_data, + U256::zero(), // No value sent in EVM + gas_limit, + gas_price, + None, // Use the next nonce + )) + .dispatch(::Origin::root())); + + assert!(CrowdloanRewards::accounts_payable(&AccountId::from(CHARLIE)).is_none()); + assert_eq!( + CrowdloanRewards::accounts_payable(&AccountId::from(ALICE)) + .unwrap() + .claimed_reward, + (450_000 * GLMR) + ); + }) +} + #[test] fn join_candidates_via_precompile() { ExtBuilder::default() diff --git a/runtime/moonriver/Cargo.toml b/runtime/moonriver/Cargo.toml index ca63c92c33..6f40348807 100644 --- a/runtime/moonriver/Cargo.toml +++ b/runtime/moonriver/Cargo.toml @@ -70,6 +70,7 @@ pallet-proxy = { git = "https://github.com/paritytech/substrate", default-featur pallet-treasury = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.8" } pallet-crowdloan-rewards = { git = "https://github.com/purestake/crowdloan-rewards", default-features = false, branch = "main" } +crowdloan-rewards-precompiles = { path = "../../precompiles/crowdloan-rewards", default-features = false } moonbeam-evm-tracer = { path = "../evm_tracer", default-features = false } moonbeam-rpc-primitives-debug = { path = "../../primitives/rpc/debug", default-features = false } diff --git a/runtime/moonriver/src/precompiles.rs b/runtime/moonriver/src/precompiles.rs index f9d39da8ea..62a6ea506f 100644 --- a/runtime/moonriver/src/precompiles.rs +++ b/runtime/moonriver/src/precompiles.rs @@ -16,6 +16,7 @@ #![cfg_attr(not(feature = "std"), no_std)] +use crowdloan_rewards_precompiles::CrowdloanRewardsWrapper; use evm::{executor::PrecompileOutput, Context, ExitError}; use frame_support::dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}; use pallet_evm::{Precompile, PrecompileSet}; @@ -36,6 +37,11 @@ type BalanceOf = <::Currency as C ::AccountId, >>::Balance; +type RewardBalanceOf = + <::RewardCurrency as Currency< + ::AccountId, + >>::Balance; + /// The PrecompileSet installed in the Moonriver runtime. /// We include the nine Istanbul precompiles /// (https://github.com/ethereum/go-ethereum/blob/3c46f557/core/vm/contracts.go#L69) @@ -50,7 +56,7 @@ where /// Return all addresses that contain precompiles. This can be used to populate dummy code /// under the precompile. pub fn used_addresses() -> impl Iterator { - sp_std::vec![1, 2, 3, 4, 5, 6, 7, 8, 1024, 1025, 1026, 2048] + sp_std::vec![1, 2, 3, 4, 5, 6, 7, 8, 1024, 1025, 1026, 2048, 2049] .into_iter() .map(|x| hash(x).into()) } @@ -64,10 +70,11 @@ impl PrecompileSet for MoonriverPrecompiles where R::Call: Dispatchable + GetDispatchInfo + Decode, ::Origin: From>, - R: parachain_staking::Config + pallet_evm::Config, + R: parachain_staking::Config + pallet_evm::Config + pallet_crowdloan_rewards::Config, R::AccountId: From, BalanceOf: TryFrom + Debug, - R::Call: From>, + RewardBalanceOf: TryFrom + Debug, + R::Call: From> + From>, { fn execute( address: H160, @@ -93,6 +100,9 @@ where a if a == hash(2048) => Some(ParachainStakingWrapper::::execute( input, target_gas, context, )), + a if a == hash(2049) => Some(CrowdloanRewardsWrapper::::execute( + input, target_gas, context, + )), _ => None, } } diff --git a/runtime/moonriver/tests/integration_test.rs b/runtime/moonriver/tests/integration_test.rs index 4489bc10b5..030365c186 100644 --- a/runtime/moonriver/tests/integration_test.rs +++ b/runtime/moonriver/tests/integration_test.rs @@ -540,6 +540,390 @@ fn initialize_crowdloan_addresses_with_batch_and_pay() { }); } +#[ignore] +#[test] +fn claim_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * MOVR), + (AccountId::from(BOB), 1_000 * MOVR), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * MOVR)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * MOVR) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * MOVR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * MOVR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + assert!(CrowdloanRewards::initialized()); + + run_to_block(4); + // 30 percent initial payout + assert_eq!(Balances::balance(&AccountId::from(CHARLIE)), 450_000 * MOVR); + // 30 percent initial payout + assert_eq!(Balances::balance(&AccountId::from(DAVE)), 450_000 * MOVR); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Alice uses the staking precompile to go offline + let gas_limit = 100000u64; + let gas_price: U256 = 1_000_000_000.into(); + + // Construct the call data (selector, amount) + let mut call_data = Vec::::from([0u8; 4]); + call_data[0..4].copy_from_slice(&Keccak256::digest(b"claim()")[0..4]); + + assert_ok!(Call::EVM(pallet_evm::Call::::call( + AccountId::from(CHARLIE), + crowdloan_precompile_address, + call_data, + U256::zero(), // No value sent in EVM + gas_limit, + gas_price, + None, // Use the next nonce + )) + .dispatch(::Origin::root())); + + let vesting_period = 4 * WEEKS as u128; + let per_block = (1_050_000 * MOVR) / vesting_period; + + assert_eq!( + CrowdloanRewards::accounts_payable(&AccountId::from(CHARLIE)) + .unwrap() + .claimed_reward, + (450_000 * MOVR) + per_block + ); + }) +} + +#[test] +fn is_contributor_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * MOVR), + (AccountId::from(BOB), 1_000 * MOVR), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * MOVR)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * MOVR) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * MOVR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * MOVR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Construct the input data to check if Bob is a contributor + let mut bob_input_data = Vec::::from([0u8; 36]); + bob_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"is_contributor(address)")[0..4]); + bob_input_data[16..36].copy_from_slice(&BOB); + + // Expected result is an EVM boolean false which is 256 bits long. + let mut expected_bytes = Vec::from([0u8; 32]); + expected_bytes[31] = 0; + let expected_false_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is not a contributor + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &bob_input_data, + None, // target_gas is not necessary right now because consumed none now + &Context { + // This context copied from Sacrifice tests, it's not great. + address: Default::default(), + caller: Default::default(), + apparent_value: From::from(0), + } + ), + expected_false_result + ); + + // Construct the input data to check if Charlie is a contributor + let mut charlie_input_data = Vec::::from([0u8; 36]); + charlie_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"is_contributor(address)")[0..4]); + charlie_input_data[16..36].copy_from_slice(&CHARLIE); + + // Expected result is an EVM boolean true which is 256 bits long. + let mut expected_bytes = Vec::from([0u8; 32]); + expected_bytes[31] = 1; + let expected_true_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is a nominator + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &charlie_input_data, + None, // target_gas is not necessary right now because consumed none now + &Context { + // This context copied from Sacrifice tests, it's not great. + address: Default::default(), + caller: Default::default(), + apparent_value: From::from(0), + } + ), + expected_true_result + ); + }) +} + +#[test] +fn reward_info_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * MOVR), + (AccountId::from(BOB), 1_000 * MOVR), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * MOVR)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * MOVR) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * MOVR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * MOVR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Construct the input data to check if Bob is a contributor + let mut charlie_input_data = Vec::::from([0u8; 36]); + charlie_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"reward_info(address)")[0..4]); + charlie_input_data[16..36].copy_from_slice(&CHARLIE); + + let expected_total: U256 = (1_500_000 * MOVR).into(); + let expected_claimed: U256 = (450_000 * MOVR).into(); + + // Expected result is two EVM u256 false which are 256 bits long. + let mut expected_bytes = Vec::from([0u8; 64]); + expected_total.to_big_endian(&mut expected_bytes[0..32]); + expected_claimed.to_big_endian(&mut expected_bytes[32..64]); + let expected_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is not a contributor + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &charlie_input_data, + None, // target_gas is not necessary right now because consumed none now + &Context { + // This context copied from Sacrifice tests, it's not great. + address: Default::default(), + caller: Default::default(), + apparent_value: From::from(0), + }, + ), + expected_result + ); + }) +} + +#[ignore] +#[test] +fn update_reward_address_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * MOVR), + (AccountId::from(BOB), 1_000 * MOVR), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * MOVR)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * MOVR) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * MOVR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * MOVR + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Charlie uses the crowdloan precompile to update address through the EVM + let gas_limit = 100000u64; + let gas_price: U256 = 1_000_000_000.into(); + + // Construct the input data to check if Bob is a contributor + let mut call_data = Vec::::from([0u8; 36]); + call_data[0..4] + .copy_from_slice(&Keccak256::digest(b"update_reward_address(address)")[0..4]); + call_data[16..36].copy_from_slice(&ALICE); + + assert_ok!(Call::EVM(pallet_evm::Call::::call( + AccountId::from(CHARLIE), + crowdloan_precompile_address, + call_data, + U256::zero(), // No value sent in EVM + gas_limit, + gas_price, + None, // Use the next nonce + )) + .dispatch(::Origin::root())); + + assert!(CrowdloanRewards::accounts_payable(&AccountId::from(CHARLIE)).is_none()); + assert_eq!( + CrowdloanRewards::accounts_payable(&AccountId::from(ALICE)) + .unwrap() + .claimed_reward, + (450_000 * MOVR) + ); + }) +} + #[test] fn join_candidates_via_precompile() { ExtBuilder::default() diff --git a/runtime/moonshadow/Cargo.toml b/runtime/moonshadow/Cargo.toml index eff2cfd804..2d59942ca3 100644 --- a/runtime/moonshadow/Cargo.toml +++ b/runtime/moonshadow/Cargo.toml @@ -70,6 +70,7 @@ pallet-proxy = { git = "https://github.com/paritytech/substrate", default-featur pallet-treasury = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "polkadot-v0.9.8" } pallet-crowdloan-rewards = { git = "https://github.com/purestake/crowdloan-rewards", default-features = false, branch = "main" } +crowdloan-rewards-precompiles = { path = "../../precompiles/crowdloan-rewards", default-features = false } moonbeam-evm-tracer = { path = "../evm_tracer", default-features = false } moonbeam-rpc-primitives-debug = { path = "../../primitives/rpc/debug", default-features = false } diff --git a/runtime/moonshadow/src/precompiles.rs b/runtime/moonshadow/src/precompiles.rs index b855a1aea8..2dc3b5d125 100644 --- a/runtime/moonshadow/src/precompiles.rs +++ b/runtime/moonshadow/src/precompiles.rs @@ -16,6 +16,7 @@ #![cfg_attr(not(feature = "std"), no_std)] +use crowdloan_rewards_precompiles::CrowdloanRewardsWrapper; use evm::{executor::PrecompileOutput, Context, ExitError}; use frame_support::dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}; use pallet_evm::{Precompile, PrecompileSet}; @@ -36,6 +37,11 @@ type BalanceOf = <::Currency as C ::AccountId, >>::Balance; +type RewardBalanceOf = + <::RewardCurrency as Currency< + ::AccountId, + >>::Balance; + /// The PrecompileSet installed in the Moonshadow runtime. /// We include the nine Istanbul precompiles /// (https://github.com/ethereum/go-ethereum/blob/3c46f557/core/vm/contracts.go#L69) @@ -50,7 +56,7 @@ where /// Return all addresses that contain precompiles. This can be used to populate dummy code /// under the precompile. pub fn used_addresses() -> impl Iterator { - sp_std::vec![1, 2, 3, 4, 5, 6, 7, 8, 1024, 1025, 1026, 2048] + sp_std::vec![1, 2, 3, 4, 5, 6, 7, 8, 1024, 1025, 1026, 2048, 2049] .into_iter() .map(|x| hash(x).into()) } @@ -64,10 +70,11 @@ impl PrecompileSet for MoonshadowPrecompiles where R::Call: Dispatchable + GetDispatchInfo + Decode, ::Origin: From>, - R: parachain_staking::Config + pallet_evm::Config, + R: parachain_staking::Config + pallet_evm::Config + pallet_crowdloan_rewards::Config, R::AccountId: From, BalanceOf: TryFrom + Debug, - R::Call: From>, + RewardBalanceOf: TryFrom + Debug, + R::Call: From> + From>, { fn execute( address: H160, @@ -93,6 +100,9 @@ where a if a == hash(2048) => Some(ParachainStakingWrapper::::execute( input, target_gas, context, )), + a if a == hash(2049) => Some(CrowdloanRewardsWrapper::::execute( + input, target_gas, context, + )), _ => None, } } diff --git a/runtime/moonshadow/tests/integration_test.rs b/runtime/moonshadow/tests/integration_test.rs index 390e5da6d2..c806855f3f 100644 --- a/runtime/moonshadow/tests/integration_test.rs +++ b/runtime/moonshadow/tests/integration_test.rs @@ -530,6 +530,387 @@ fn initialize_crowdloan_addresses_with_batch_and_pay() { }); } +#[ignore] +#[test] +fn claim_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * MSHD), + (AccountId::from(BOB), 1_000 * MSHD), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * MSHD)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * MSHD) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * MSHD + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * MSHD + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + // 30 percent initial payout + assert_eq!(Balances::balance(&AccountId::from(CHARLIE)), 450_000 * MSHD); + // 30 percent initial payout + assert_eq!(Balances::balance(&AccountId::from(DAVE)), 450_000 * MSHD); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Alice uses the crowdloan precompile to claim through the EVM + let gas_limit = 100000u64; + let gas_price: U256 = 1_000_000_000.into(); + + // Construct the call data (selector, amount) + let mut call_data = Vec::::from([0u8; 4]); + call_data[0..4].copy_from_slice(&Keccak256::digest(b"claim()")[0..4]); + + assert_ok!(Call::EVM(pallet_evm::Call::::call( + AccountId::from(CHARLIE), + crowdloan_precompile_address, + call_data, + U256::zero(), // No value sent in EVM + gas_limit, + gas_price, + None, // Use the next nonce + )) + .dispatch(::Origin::root())); + + let vesting_period = 4 * WEEKS as u128; + let per_block = (1_050_000 * MSHD) / vesting_period; + + assert_eq!( + CrowdloanRewards::accounts_payable(&AccountId::from(CHARLIE)) + .unwrap() + .claimed_reward, + (450_000 * MSHD) + per_block + ); + }) +} + +#[test] +fn is_contributor_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * MSHD), + (AccountId::from(BOB), 1_000 * MSHD), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * MSHD)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * MSHD) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * MSHD + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * MSHD + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Construct the input data to check if Bob is a contributor + let mut bob_input_data = Vec::::from([0u8; 36]); + bob_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"is_contributor(address)")[0..4]); + bob_input_data[16..36].copy_from_slice(&BOB); + + // Expected result is an EVM boolean false which is 256 bits long. + let mut expected_bytes = Vec::from([0u8; 32]); + expected_bytes[31] = 0; + let expected_false_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is not a contributor + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &bob_input_data, + None, // target_gas is not necessary right now because consumed none now + &Context { + // This context copied from Sacrifice tests, it's not great. + address: Default::default(), + caller: Default::default(), + apparent_value: From::from(0), + }, + ), + expected_false_result + ); + + // Construct the input data to check if Charlie is a contributor + let mut charlie_input_data = Vec::::from([0u8; 36]); + charlie_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"is_contributor(address)")[0..4]); + charlie_input_data[16..36].copy_from_slice(&CHARLIE); + + // Expected result is an EVM boolean true which is 256 bits long. + let mut expected_bytes = Vec::from([0u8; 32]); + expected_bytes[31] = 1; + let expected_true_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is a nominator + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &charlie_input_data, + None, // target_gas is not necessary right now because consumed none now + &Context { + // This context copied from Sacrifice tests, it's not great. + address: Default::default(), + caller: Default::default(), + apparent_value: From::from(0), + }, + ), + expected_true_result + ); + }) +} + +#[test] +fn reward_info_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * MSHD), + (AccountId::from(BOB), 1_000 * MSHD), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * MSHD)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * MSHD) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * MSHD + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * MSHD + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Construct the input data to check if Bob is a contributor + let mut charlie_input_data = Vec::::from([0u8; 36]); + charlie_input_data[0..4] + .copy_from_slice(&Keccak256::digest(b"reward_info(address)")[0..4]); + charlie_input_data[16..36].copy_from_slice(&CHARLIE); + + let expected_total: U256 = (1_500_000 * MSHD).into(); + let expected_claimed: U256 = (450_000 * MSHD).into(); + + // Expected result is two EVM u256 false which are 256 bits long. + let mut expected_bytes = Vec::from([0u8; 64]); + expected_total.to_big_endian(&mut expected_bytes[0..32]); + expected_claimed.to_big_endian(&mut expected_bytes[32..64]); + let expected_result = Some(Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: expected_bytes, + cost: 1000, + logs: Default::default(), + })); + + // Assert precompile reports Bob is not a contributor + assert_eq!( + Precompiles::execute( + crowdloan_precompile_address, + &charlie_input_data, + None, // target_gas is not necessary right now because consumed none now + &Context { + // This context copied from Sacrifice tests, it's not great. + address: Default::default(), + caller: Default::default(), + apparent_value: From::from(0), + }, + ), + expected_result + ); + }) +} + +#[ignore] +#[test] +fn update_reward_address_via_precompile() { + ExtBuilder::default() + .with_balances(vec![ + (AccountId::from(ALICE), 2_000 * MSHD), + (AccountId::from(BOB), 1_000 * MSHD), + ]) + .with_collators(vec![(AccountId::from(ALICE), 1_000 * MSHD)]) + .with_mappings(vec![( + NimbusId::from_slice(&ALICE_NIMBUS), + AccountId::from(ALICE), + )]) + .with_crowdloan_fund(3_000_000 * MSHD) + .build() + .execute_with(|| { + // set parachain inherent data + set_parachain_inherent_data(); + set_author(NimbusId::from_slice(&ALICE_NIMBUS)); + for x in 1..3 { + run_to_block(x); + } + let init_block = CrowdloanRewards::init_relay_block(); + // This matches the previous vesting + let end_block = init_block + 4 * WEEKS; + // Batch calls always succeed. We just need to check the inner event + assert_ok!( + Call::Utility(pallet_utility::Call::::batch_all(vec![ + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [4u8; 32].into(), + Some(AccountId::from(CHARLIE)), + 1_500_000 * MSHD + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::initialize_reward_vec(vec![( + [5u8; 32].into(), + Some(AccountId::from(DAVE)), + 1_500_000 * MSHD + )]) + ), + Call::CrowdloanRewards( + pallet_crowdloan_rewards::Call::::complete_initialization( + end_block + ) + ) + ])) + .dispatch(root_origin()) + ); + + let crowdloan_precompile_address = H160::from_low_u64_be(2049); + + // Charlie uses the crowdloan precompile to update address through the EVM + let gas_limit = 100000u64; + let gas_price: U256 = 1_000_000_000.into(); + + // Construct the input data to check if Bob is a contributor + let mut call_data = Vec::::from([0u8; 36]); + call_data[0..4] + .copy_from_slice(&Keccak256::digest(b"update_reward_address(address)")[0..4]); + call_data[16..36].copy_from_slice(&ALICE); + + assert_ok!(Call::EVM(pallet_evm::Call::::call( + AccountId::from(CHARLIE), + crowdloan_precompile_address, + call_data, + U256::zero(), // No value sent in EVM + gas_limit, + gas_price, + None, // Use the next nonce + )) + .dispatch(::Origin::root())); + + assert!(CrowdloanRewards::accounts_payable(&AccountId::from(CHARLIE)).is_none()); + assert_eq!( + CrowdloanRewards::accounts_payable(&AccountId::from(ALICE)) + .unwrap() + .claimed_reward, + (450_000 * MSHD) + ); + }) +} + #[test] fn join_candidates_via_precompile() { ExtBuilder::default()