diff --git a/Cargo.lock b/Cargo.lock
index 1538a950752..2c970efdd1c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4597,6 +4597,8 @@ dependencies = [
"author-inherent",
"cumulus-pallet-parachain-system",
"cumulus-primitives-core",
+ "cumulus-primitives-parachain-inherent",
+ "cumulus-test-relay-sproof-builder",
"fp-rpc",
"frame-executive",
"frame-support",
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index a5da6064464..2e0b18afb88 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -64,6 +64,10 @@ cumulus-pallet-parachain-system = { git = "https://github.com/paritytech/cumulus
cumulus-primitives-core = { git = "https://github.com/paritytech/cumulus", default-features = false, branch = "rococo-v1" }
parachain-info = { git = "https://github.com/paritytech/cumulus", default-features = false, branch = "rococo-v1" }
+[dev-dependencies]
+cumulus-test-relay-sproof-builder = { git = "https://github.com/paritytech/cumulus", default-features = false, branch = "rococo-v1" }
+cumulus-primitives-parachain-inherent = { git = "https://github.com/paritytech/cumulus", default-features = false, branch = "rococo-v1" }
+
[build-dependencies]
substrate-wasm-builder = { version = "4.0.0", git = "https://github.com/paritytech/substrate", branch = "rococo-v1" }
diff --git a/runtime/tests/integration_test.rs b/runtime/tests/integration_test.rs
new file mode 100644
index 00000000000..fe1729033a2
--- /dev/null
+++ b/runtime/tests/integration_test.rs
@@ -0,0 +1,352 @@
+// 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 .
+
+//! Moonbeam Runtime Integration Tests
+
+#![cfg(test)]
+
+use cumulus_primitives_parachain_inherent::ParachainInherentData;
+use frame_support::{
+ assert_noop, assert_ok,
+ dispatch::Dispatchable,
+ traits::{GenesisBuild, OnFinalize, OnInitialize},
+};
+use moonbeam_runtime::{
+ AccountId, AuthorInherent, Balance, Balances, Call, Event, InflationInfo, ParachainStaking,
+ Range, Runtime, System, GLMR,
+};
+use parachain_staking::Bond;
+use sp_runtime::{DispatchError, Perbill};
+
+fn run_to_block(n: u32) {
+ while System::block_number() < n {
+ AuthorInherent::on_finalize(System::block_number());
+ ParachainStaking::on_finalize(System::block_number());
+ System::set_block_number(System::block_number() + 1);
+ AuthorInherent::on_initialize(System::block_number());
+ }
+}
+
+fn last_event() -> Event {
+ System::events().pop().expect("Event expected").event
+}
+
+struct ExtBuilder {
+ // endowed accounts with balances
+ balances: Vec<(AccountId, Balance)>,
+ // [collator, amount]
+ collators: Vec<(AccountId, Balance)>,
+ // [nominator, collator, nomination_amount]
+ nominators: Vec<(AccountId, AccountId, Balance)>,
+ // per-round inflation config
+ inflation: InflationInfo,
+}
+
+impl Default for ExtBuilder {
+ fn default() -> ExtBuilder {
+ ExtBuilder {
+ balances: vec![],
+ nominators: vec![],
+ collators: vec![],
+ inflation: InflationInfo {
+ expect: Range {
+ min: 100_000 * GLMR,
+ ideal: 200_000 * GLMR,
+ max: 500_000 * GLMR,
+ },
+ // unrealistically high parameterization, only for testing
+ round: Range {
+ min: Perbill::from_percent(5),
+ ideal: Perbill::from_percent(5),
+ max: Perbill::from_percent(5),
+ },
+ },
+ }
+ }
+}
+
+impl ExtBuilder {
+ fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self {
+ self.balances = balances;
+ self
+ }
+
+ fn with_collators(mut self, collators: Vec<(AccountId, Balance)>) -> Self {
+ self.collators = collators;
+ self
+ }
+
+ fn with_nominators(mut self, nominators: Vec<(AccountId, AccountId, Balance)>) -> Self {
+ self.nominators = nominators;
+ self
+ }
+
+ #[allow(dead_code)]
+ fn with_inflation(mut self, inflation: InflationInfo) -> Self {
+ self.inflation = inflation;
+ self
+ }
+
+ fn build(self) -> sp_io::TestExternalities {
+ let mut t = frame_system::GenesisConfig::default()
+ .build_storage::()
+ .unwrap();
+
+ pallet_balances::GenesisConfig:: {
+ balances: self.balances,
+ }
+ .assimilate_storage(&mut t)
+ .unwrap();
+
+ let mut stakers: Vec<(AccountId, Option, Balance)> = Vec::new();
+ for collator in self.collators {
+ stakers.push((collator.0, None, collator.1));
+ }
+ for nominator in self.nominators {
+ stakers.push((nominator.0, Some(nominator.1), nominator.2));
+ }
+ parachain_staking::GenesisConfig:: {
+ stakers,
+ inflation_config: self.inflation,
+ }
+ .assimilate_storage(&mut t)
+ .unwrap();
+
+ let mut ext = sp_io::TestExternalities::new(t);
+ ext.execute_with(|| System::set_block_number(1));
+ ext
+ }
+}
+
+const ALICE: [u8; 20] = [4u8; 20];
+const BOB: [u8; 20] = [5u8; 20];
+const CHARLIE: [u8; 20] = [6u8; 20];
+const DAVE: [u8; 20] = [7u8; 20];
+
+fn origin_of(account_id: AccountId) -> ::Origin {
+ ::Origin::signed(account_id)
+}
+
+fn inherent_origin() -> ::Origin {
+ ::Origin::none()
+}
+
+#[test]
+fn join_collator_candidates() {
+ ExtBuilder::default()
+ .with_balances(vec![
+ (AccountId::from(ALICE), 2_000 * GLMR),
+ (AccountId::from(BOB), 2_000 * GLMR),
+ (AccountId::from(CHARLIE), 1_100 * GLMR),
+ (AccountId::from(DAVE), 1_000 * GLMR),
+ ])
+ .with_collators(vec![
+ (AccountId::from(ALICE), 1_000 * GLMR),
+ (AccountId::from(BOB), 1_000 * GLMR),
+ ])
+ .with_nominators(vec![
+ (AccountId::from(CHARLIE), AccountId::from(ALICE), 50 * GLMR),
+ (AccountId::from(CHARLIE), AccountId::from(BOB), 50 * GLMR),
+ ])
+ .build()
+ .execute_with(|| {
+ assert_noop!(
+ ParachainStaking::join_candidates(origin_of(AccountId::from(ALICE)), 1_000 * GLMR,),
+ parachain_staking::Error::::CandidateExists
+ );
+ assert_noop!(
+ ParachainStaking::join_candidates(
+ origin_of(AccountId::from(CHARLIE)),
+ 1_000 * GLMR
+ ),
+ parachain_staking::Error::::NominatorExists
+ );
+ assert!(System::events().is_empty());
+ assert_ok!(ParachainStaking::join_candidates(
+ origin_of(AccountId::from(DAVE)),
+ 1_000 * GLMR,
+ ));
+ assert_eq!(
+ last_event(),
+ Event::parachain_staking(parachain_staking::Event::JoinedCollatorCandidates(
+ AccountId::from(DAVE),
+ 1_000 * GLMR,
+ 3_100 * GLMR
+ ))
+ );
+ let candidates = ParachainStaking::candidate_pool();
+ assert_eq!(
+ candidates.0[0],
+ Bond {
+ owner: AccountId::from(ALICE),
+ amount: 1_050 * GLMR
+ }
+ );
+ assert_eq!(
+ candidates.0[1],
+ Bond {
+ owner: AccountId::from(BOB),
+ amount: 1_050 * GLMR
+ }
+ );
+ assert_eq!(
+ candidates.0[2],
+ Bond {
+ owner: AccountId::from(DAVE),
+ amount: 1_000 * GLMR
+ }
+ );
+ });
+}
+
+#[test]
+fn transfer_through_evm_to_stake() {
+ ExtBuilder::default()
+ .with_balances(vec![(AccountId::from(ALICE), 3_000 * GLMR)])
+ .build()
+ .execute_with(|| {
+ // Charlie has no balance => fails to stake
+ assert_noop!(
+ ParachainStaking::join_candidates(
+ origin_of(AccountId::from(CHARLIE)),
+ 1_000 * GLMR,
+ ),
+ DispatchError::Module {
+ index: 3,
+ error: 3,
+ message: Some("InsufficientBalance")
+ }
+ );
+ // Alice stakes to become a collator candidate
+ assert_ok!(ParachainStaking::join_candidates(
+ origin_of(AccountId::from(ALICE)),
+ 1_000 * GLMR,
+ ));
+ // Alice transfer from free balance 1000 GLMR to Bob
+ assert_ok!(Balances::transfer(
+ origin_of(AccountId::from(ALICE)),
+ AccountId::from(BOB),
+ 2_000 * GLMR,
+ ));
+ assert_eq!(Balances::free_balance(AccountId::from(BOB)), 2_000 * GLMR,);
+ use sp_core::U256;
+ let gas_limit = 100000u64;
+ let gas_price: U256 = 1000.into();
+ // Bob transfers 1000 GLMR to Charlie via EVM
+ assert_ok!(Call::EVM(pallet_evm::Call::::call(
+ AccountId::from(BOB),
+ AccountId::from(CHARLIE),
+ Vec::new(),
+ (1_000 * GLMR).into(),
+ gas_limit,
+ gas_price,
+ None
+ ))
+ .dispatch(::Origin::root()));
+ assert_eq!(
+ Balances::free_balance(AccountId::from(CHARLIE)),
+ 1_000 * GLMR,
+ );
+ // Charlie can stake now
+ assert_ok!(ParachainStaking::join_candidates(
+ origin_of(AccountId::from(CHARLIE)),
+ 1_000 * GLMR,
+ ),);
+ let candidates = ParachainStaking::candidate_pool();
+ assert_eq!(
+ candidates.0[0],
+ Bond {
+ owner: AccountId::from(ALICE),
+ amount: 2_000 * GLMR
+ }
+ );
+ assert_eq!(
+ candidates.0[1],
+ Bond {
+ owner: AccountId::from(CHARLIE),
+ amount: 1_000 * GLMR
+ }
+ );
+ });
+}
+
+#[test]
+fn reward_block_authors() {
+ 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_nominators(vec![(
+ AccountId::from(BOB),
+ AccountId::from(ALICE),
+ 500 * GLMR,
+ )])
+ .build()
+ .execute_with(|| {
+ // set parachain inherent data
+ use cumulus_primitives_core::PersistedValidationData;
+ use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder;
+ let (relay_parent_storage_root, relay_chain_state) =
+ RelayStateSproofBuilder::default().into_state_root_and_proof();
+ let vfp = PersistedValidationData {
+ relay_parent_number: 1u32,
+ relay_parent_storage_root,
+ ..Default::default()
+ };
+ let parachain_inherent_data = ParachainInherentData {
+ validation_data: vfp,
+ relay_chain_state: relay_chain_state,
+ downward_messages: Default::default(),
+ horizontal_messages: Default::default(),
+ };
+ // Mock the inherent that sets validation data in ParachainSystem, which
+ // contains the `relay_chain_block_number`, which is used in `author-filter` as a
+ // source of randomness to filter valid authors at each block.
+ assert_ok!(Call::ParachainSystem(
+ cumulus_pallet_parachain_system::Call::::set_validation_data(
+ parachain_inherent_data
+ )
+ )
+ .dispatch(inherent_origin()));
+ // Mock the inherent that sets author in `author-inherent`
+ fn set_author(a: AccountId) {
+ assert_ok!(
+ Call::AuthorInherent(author_inherent::Call::::set_author(a))
+ .dispatch(inherent_origin())
+ );
+ }
+ for x in 2..1201 {
+ set_author(AccountId::from(ALICE));
+ run_to_block(x);
+ }
+ // no rewards doled out yet
+ assert_eq!(Balances::free_balance(AccountId::from(ALICE)), 1_000 * GLMR,);
+ assert_eq!(Balances::free_balance(AccountId::from(BOB)), 500 * GLMR,);
+ set_author(AccountId::from(ALICE));
+ run_to_block(1201);
+ // rewards minted and distributed
+ assert_eq!(
+ Balances::free_balance(AccountId::from(ALICE)),
+ 1109999999920000000000,
+ );
+ assert_eq!(
+ Balances::free_balance(AccountId::from(BOB)),
+ 539999999960000000000,
+ );
+ });
+}