Lines
100 %
Functions
Branches
// Copyright 2019-2022 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 <http://www.gnu.org/licenses/>.
//! Unit testing
use super::*;
use crate::mock::*;
use crate::pallet::{EndVestingBlock, InitVestingBlock, Initialized};
use frame_support::{assert_noop, assert_ok};
use parity_scale_codec::Encode;
use sp_core::{crypto::AccountId32, Pair};
use sp_runtime::traits::AccountIdConversion;
use sp_runtime::MultiSignature;
// Constant that reflects the desired vesting period for the tests
// Most tests complete initialization passing initRelayBlock + VESTING as the endRelayBlock
const VESTING: u32 = 8;
#[test]
fn geneses() {
ExtBuilder::empty().execute_with(|| {
// Fund pallet with exact amount needed (total rewards + small dust)
assert!(System::events().is_empty());
// Insert contributors
let pairs = get_ed25519_pairs(3);
let init_block = CrowdloanRewards::init_vesting_block();
assert_ok!(CrowdloanRewards::initialize_reward_vec(vec![
([1u8; 32].into(), Some(account(1)), 500u32.into()),
([2u8; 32].into(), Some(account(2)), 500u32.into()),
(pairs[0].public().into(), None, 500u32.into()),
(pairs[1].public().into(), None, 500u32.into()),
(pairs[2].public().into(), None, 500u32.into())
]));
assert_ok!(CrowdloanRewards::complete_initialization(
init_block + VESTING
));
assert_eq!(CrowdloanRewards::total_contributors(), 5);
// accounts_payable
assert!(CrowdloanRewards::accounts_payable(&account(1)).is_some());
assert!(CrowdloanRewards::accounts_payable(&account(2)).is_some());
assert!(CrowdloanRewards::accounts_payable(&account(3)).is_none());
assert!(CrowdloanRewards::accounts_payable(&account(4)).is_none());
assert!(CrowdloanRewards::accounts_payable(&account(5)).is_none());
// claimed address existence
assert!(CrowdloanRewards::claimed_relay_chain_ids(account(1)).is_some());
assert!(CrowdloanRewards::claimed_relay_chain_ids(account(2)).is_some());
assert!(
CrowdloanRewards::claimed_relay_chain_ids(AccountId32::from(pairs[0].public()))
.is_none()
);
CrowdloanRewards::claimed_relay_chain_ids(AccountId32::from(pairs[1].public()))
CrowdloanRewards::claimed_relay_chain_ids(AccountId32::from(pairs[2].public()))
// unassociated_contributions
assert!(CrowdloanRewards::unassociated_contributions(account(1)).is_none());
assert!(CrowdloanRewards::unassociated_contributions(account(2)).is_none());
CrowdloanRewards::unassociated_contributions(AccountId32::from(pairs[0].public()))
.is_some()
CrowdloanRewards::unassociated_contributions(AccountId32::from(pairs[1].public()))
CrowdloanRewards::unassociated_contributions(AccountId32::from(pairs[2].public()))
});
}
fn proving_assignation_works() {
let mut payload = WRAPPED_BYTES_PREFIX.to_vec();
payload.append(&mut SignatureNetworkIdentifier::get().to_vec());
payload.append(&mut account(3).encode());
payload.append(&mut WRAPPED_BYTES_POSTFIX.to_vec());
let signature: MultiSignature = pairs[0].sign(&payload).into();
let mut already_associated_payload = WRAPPED_BYTES_PREFIX.to_vec();
already_associated_payload.append(&mut SignatureNetworkIdentifier::get().to_vec());
already_associated_payload.append(&mut account(1).encode());
already_associated_payload.append(&mut WRAPPED_BYTES_POSTFIX.to_vec());
let alread_associated_signature: MultiSignature =
pairs[0].sign(&already_associated_payload).into();
],));
// 4 is not payable first
assert!(CrowdloanRewards::accounts_payable(account(3)).is_none());
assert_eq!(
CrowdloanRewards::accounts_payable(account(1))
.unwrap()
.contributed_relay_addresses,
vec![account(1)]
roll_to(4);
// Signature is wrong, prove fails
assert_noop!(
CrowdloanRewards::associate_native_identity(
RuntimeOrigin::signed(account(4)),
account(4),
pairs[0].public().into(),
signature.clone()
),
Error::<Test>::InvalidClaimSignature
// Signature is right, but address already claimed
account(1),
alread_associated_signature
Error::<Test>::AlreadyAssociated
// Signature is right, prove passes
assert_ok!(CrowdloanRewards::associate_native_identity(
account(3),
// Signature is right, but relay address is no longer on unassociated
signature
Error::<Test>::NoAssociatedClaim
// now three is payable
assert!(CrowdloanRewards::accounts_payable(account(3)).is_some());
CrowdloanRewards::accounts_payable(account(3))
vec![AccountId32::from(*pairs[0].public().as_array_ref())]
// Only events from current block (after roll_to(4)) are preserved
let expected = vec![
crate::Event::InitialPaymentMade(account(3), 100),
crate::Event::NativeIdentityAssociated(pairs[0].public().into(), account(3), 500),
];
assert_eq!(events(), expected);
fn initializing_multi_relay_to_single_native_address_works() {
// The init relay block gets inserted
roll_to(2);
([2u8; 32].into(), Some(account(1)), 500u32.into()),
// 1 is payable
assert!(CrowdloanRewards::accounts_payable(account(1)).is_some());
vec![account(1), account(2)]
assert_ok!(CrowdloanRewards::claim(RuntimeOrigin::signed(account(1))));
.claimed_reward,
400
CrowdloanRewards::claim(RuntimeOrigin::signed(account(3))),
crate::Event::InitialPaymentMade(account(1), 100),
crate::Event::RewardsPaid(account(1), 200),
fn paying_works_step_by_step() {
CrowdloanRewards::accounts_payable(&account(1))
200
roll_to(5);
250
roll_to(6);
300
roll_to(7);
350
roll_to(8);
roll_to(9);
450
roll_to(10);
500
roll_to(11);
CrowdloanRewards::claim(RuntimeOrigin::signed(account(1))),
Error::<Test>::RewardsAlreadyClaimed
crate::Event::InitialPaymentMade(account(2), 100),
crate::Event::RewardsPaid(account(1), 100),
crate::Event::RewardsPaid(account(1), 50),
fn paying_works_after_unclaimed_period() {
roll_to(330);
crate::Event::RewardsPaid(account(1), 150),
fn paying_late_joiner_works() {
roll_to(12);
assert_ok!(CrowdloanRewards::claim(RuntimeOrigin::signed(account(3))));
CrowdloanRewards::accounts_payable(&account(3))
// Note: account(1) and account(2) don't get InitialPaymentMade events during initialization
// because they are already associated with native accounts. Only account(3) gets it
// when associate_native_identity is called.
crate::Event::RewardsPaid(account(3), 400),
fn update_address_works() {
CrowdloanRewards::claim(RuntimeOrigin::signed(account(8))),
assert_ok!(CrowdloanRewards::update_reward_address(
RuntimeOrigin::signed(account(1)),
account(8)
CrowdloanRewards::accounts_payable(&account(8))
assert_ok!(CrowdloanRewards::claim(RuntimeOrigin::signed(account(8))));
// The initial payment is not
crate::Event::RewardAddressUpdated(account(1), account(8)),
crate::Event::RewardsPaid(account(8), 100),
fn update_address_with_existing_address_fails() {
assert_ok!(CrowdloanRewards::claim(RuntimeOrigin::signed(account(2))));
CrowdloanRewards::update_reward_address(RuntimeOrigin::signed(account(1)), account(2)),
fn update_address_with_existing_with_multi_address_works() {
// We make sure all rewards go to the new address
account(2)
CrowdloanRewards::accounts_payable(&account(2))
.total_reward,
1000
fn initialize_new_addresses() {
assert_eq!(CrowdloanRewards::initialized(), true);
CrowdloanRewards::initialize_reward_vec(vec![(
[1u8; 32].into(),
Some(account(1)),
500u32.into()
)]),
Error::<Test>::RewardVecAlreadyInitialized,
CrowdloanRewards::complete_initialization(init_block + VESTING * 2),
fn initialize_new_addresses_handle_dust() {
let pairs = get_ed25519_pairs(2);
(pairs[1].public().into(), None, 999u32.into()),
fn initialize_new_addresses_not_matching_funds() {
// Total supply is 2500. Lets ensure inserting 2495 is not working.
(pairs[1].public().into(), None, 995u32.into()),
CrowdloanRewards::complete_initialization(init_block + VESTING),
Error::<Test>::RewardsDoNotMatchFund
fn initialize_new_addresses_with_batch() {
// This time should succeed trully
assert_ok!(CrowdloanRewards::initialize_reward_vec(vec![(
[4u8; 32].into(),
Some(account(3)),
1250
)]));
[5u8; 32].into(),
assert_eq!(CrowdloanRewards::total_contributors(), 2);
// Verify that the second ending block provider had no effect
assert_eq!(CrowdloanRewards::end_vesting_block(), init_block + VESTING);
// After complete_initialization, initialize_reward_vec should fail
Error::<Test>::RewardVecAlreadyInitialized
fn floating_point_arithmetic_works() {
1190
Some(account(2)),
1185
// We will work with this. This has 100/8=12.5 payable per block
[3u8; 32].into(),
125
assert_eq!(CrowdloanRewards::total_contributors(), 3);
25u128
// Block relay number is 2 post init initialization
// In this case there is no problem. Here we pay 12.5*2=25
// Total claimed reward: 25+25 = 50
50u128
// If we claim now we have to pay 12.5. 12 will be paid.
62u128
// Now we should pay 12.5. However the calculus will be:
// Account 3 should have claimed 50 + 25 at this block, but
// he only claimed 62. The payment is 13
75u128
crate::Event::InitialPaymentMade(account(1), 238),
crate::Event::InitialPaymentMade(account(2), 237),
crate::Event::InitialPaymentMade(account(3), 25),
crate::Event::RewardsPaid(account(3), 25),
crate::Event::RewardsPaid(account(3), 12),
crate::Event::RewardsPaid(account(3), 13),
fn reward_below_vesting_period_works() {
1247
// We will work with this. This has 5/8=0.625 payable per block
6
1u128
// Here we should pay floor(0.625*2)=1
// Total claimed reward: 1+1 = 2
2u128
// If we claim now we have to pay floor(0.625) = 0
// Now we should pay 1 again. The claimer should have claimed floor(0.625*4) + 1
// but he only claimed 2
3u128
// We pay the remaining
6u128
// Nothing more to claim
crate::Event::InitialPaymentMade(account(1), 249),
crate::Event::InitialPaymentMade(account(2), 249),
crate::Event::InitialPaymentMade(account(3), 1),
crate::Event::RewardsPaid(account(3), 1),
crate::Event::RewardsPaid(account(3), 0),
crate::Event::RewardsPaid(account(3), 3),
fn test_initialization_errors() {
let pot = CrowdloanRewards::pot();
// Too many contributors
CrowdloanRewards::initialize_reward_vec(vec![
([1u8; 32].into(), Some(account(1)), 1),
([2u8; 32].into(), Some(account(2)), 1),
([3u8; 32].into(), Some(account(3)), 1),
([4u8; 32].into(), Some(account(4)), 1),
([5u8; 32].into(), Some(account(5)), 1),
([6u8; 32].into(), Some(account(6)), 1),
([7u8; 32].into(), Some(account(7)), 1),
([8u8; 32].into(), Some(account(8)), 1),
([9u8; 32].into(), Some(account(9)), 1)
]),
Error::<Test>::TooManyContributors
// Go beyond fund pot
pot + 1
Error::<Test>::BatchBeyondFundPot
// Dont fill rewards
pot - 1
// Fill rewards
[2u8; 32].into(),
1
// Insert a non-valid vesting period
CrowdloanRewards::complete_initialization(init_block),
Error::<Test>::VestingPeriodNonValid
// Cannot claim if we dont complete initialization
Error::<Test>::RewardVecNotFullyInitializedYet
// Complete
// Cannot initialize again
fn test_relay_signatures_can_change_reward_addresses() {
// 5 relay keys
let pairs = get_ed25519_pairs(5);
// We will have all pointint to the same reward account
(pairs[0].public().into(), Some(account(1)), 500u32.into()),
(pairs[1].public().into(), Some(account(1)), 500u32.into()),
(pairs[2].public().into(), Some(account(1)), 500u32.into()),
(pairs[3].public().into(), Some(account(1)), 500u32.into()),
(pairs[4].public().into(), Some(account(1)), 500u32.into())
let reward_info = CrowdloanRewards::accounts_payable(&account(1)).unwrap();
// We should have all of them as contributors
for pair in pairs.clone() {
assert!(reward_info
.contributed_relay_addresses
.contains(&pair.public().into()))
// Threshold is set to 50%, so we need at least 3 votes to pass
// Let's make sure that we dont pass with 2
payload.append(&mut account(2).encode());
payload.append(&mut account(1).encode());
let mut insufficient_proofs: Vec<(AccountId, MultiSignature)> = vec![];
for i in 0..2 {
insufficient_proofs.push((pairs[i].public().into(), pairs[i].sign(&payload).into()));
// Not sufficient proofs presented
CrowdloanRewards::change_association_with_relay_keys(
account(2),
insufficient_proofs.clone()
Error::<Test>::InsufficientNumberOfValidProofs
// With three votes we should passs
let mut sufficient_proofs = insufficient_proofs.clone();
// We push one more
sufficient_proofs.push((pairs[2].public().into(), pairs[2].sign(&payload).into()));
// This time should pass
assert_ok!(CrowdloanRewards::change_association_with_relay_keys(
sufficient_proofs.clone()
// 1 should no longer be payable
assert!(CrowdloanRewards::accounts_payable(&account(1)).is_none());
// 2 should be now payable
let reward_info_2 = CrowdloanRewards::accounts_payable(&account(2)).unwrap();
// The reward info should be identical
assert_eq!(reward_info, reward_info_2);
fn test_claim_works_with_full_vesting() {
let reward_account = account(1);
let relay_account = account(10);
let total_reward = 10_000u128;
ExtBuilder::default()
.with_funded_accounts(vec![(
relay_account.clone(),
Some(reward_account.clone()),
total_reward,
)])
.build()
.execute_with(|| {
// Move to end of vesting period
run_to_block(100);
let initial_balance = Balances::free_balance(&reward_account);
let pallet_initial_balance = Balances::free_balance(&CrowdloanRewards::account_id());
// Claim rewards
assert_ok!(CrowdloanRewards::claim(RuntimeOrigin::signed(
reward_account.clone()
)));
// Check that rewards were paid
let reward_info = AccountsPayable::<Test>::get(&reward_account).unwrap();
assert_eq!(reward_info.claimed_reward, total_reward);
// Check balances
let final_balance = Balances::free_balance(&reward_account);
let pallet_final_balance = Balances::free_balance(&CrowdloanRewards::account_id());
// Should have received remaining vested amount
let initialization_payment = InitializationPayment::get() * total_reward;
let expected_claim = total_reward - initialization_payment;
assert_eq!(final_balance, initial_balance + expected_claim);
pallet_final_balance,
pallet_initial_balance - expected_claim
// Note: Event checking could be added here if needed
fn test_claim_works_with_partial_vesting() {
// Move to 50% of vesting period (block 50 out of 100)
run_to_block(50);
// Check that partial rewards were paid
// Calculate expected vested amount
let remaining_to_vest = total_reward - initialization_payment;
let vesting_period = 100u32 - 1u32; // 99 blocks
let elapsed_period = 50u32 - 1u32; // 49 blocks
let expected_vested =
remaining_to_vest * elapsed_period as u128 / vesting_period as u128;
let expected_total_claimed = initialization_payment + expected_vested;
assert_eq!(reward_info.claimed_reward, expected_total_claimed);
// Check balance increased by the vested amount
assert_eq!(final_balance, initial_balance + expected_vested);
fn test_claim_fails_when_no_rewards() {
// Use default genesis which initializes with account(1) having rewards
// But we'll try to claim with a different account
ExtBuilder::default().build().execute_with(|| {
let reward_account = account(2);
// Try to claim without having any rewards
CrowdloanRewards::claim(RuntimeOrigin::signed(reward_account)),
fn test_claim_fails_when_not_initialized() {
// Use empty genesis config which will still set Initialized to true
// We need to manually set it to false after
// Manually insert reward data and mark as not initialized
let reward_info = RewardInfo {
claimed_reward: InitializationPayment::get() * total_reward,
contributed_relay_addresses: vec![relay_account.clone()],
};
AccountsPayable::<Test>::insert(&reward_account, &reward_info);
ClaimedRelayChainIds::<Test>::insert(&relay_account, ());
Initialized::<Test>::put(false);
// Try to claim
fn test_claim_fails_when_all_rewards_claimed() {
// Setup reward data with all rewards already claimed
claimed_reward: total_reward, // All claimed
InitVestingBlock::<Test>::put(1u32);
EndVestingBlock::<Test>::put(100u32);
Initialized::<Test>::put(true);
fn test_update_reward_address_works() {
let old_reward_account = account(1);
let new_reward_account = account(2);
Some(old_reward_account.clone()),
// Update reward address
RuntimeOrigin::signed(old_reward_account.clone()),
new_reward_account.clone()
// Check that old account no longer has rewards
assert!(AccountsPayable::<Test>::get(&old_reward_account).is_none());
// Check that new account has the rewards
let reward_info = AccountsPayable::<Test>::get(&new_reward_account).unwrap();
assert_eq!(reward_info.total_reward, total_reward);
fn test_update_reward_address_fails_when_no_rewards() {
// Try to update address without having rewards
CrowdloanRewards::update_reward_address(
RuntimeOrigin::signed(old_reward_account),
new_reward_account
fn test_update_reward_address_fails_when_new_account_already_has_rewards() {
let relay_account1 = account(10);
let relay_account2 = account(11);
.with_funded_accounts(vec![
(
relay_account1.clone(),
relay_account2.clone(),
Some(new_reward_account.clone()),
])
// Try to update address to an account that already has rewards
fn test_pot_returns_correct_balance() {
// Total rewards minus the initial payment to the native accounts
// Default genesis has 10_000 reward with 20% initial payment = 2000
// Pot is set to total_rewards + 1 (dust) = 10_001
let expected_balance = 10_001u128 - 2000u128;
assert_eq!(CrowdloanRewards::pot(), expected_balance);
fn test_account_id_returns_correct_account() {
let expected_account = CrowdloanPalletId::get().into_account_truncating();
assert_eq!(CrowdloanRewards::account_id(), expected_account);
fn test_vesting_calculation_with_zero_period() {
// Setup with zero vesting period
EndVestingBlock::<Test>::put(1u32); // Same as init = zero period
run_to_block(10);
// Claim with zero vesting period should pay everything immediately
let remaining_reward = total_reward - initialization_payment;
assert_eq!(final_balance, initial_balance + remaining_reward);
fn test_multiple_claims_during_vesting() {
// First claim at 25% vesting
run_to_block(25);
let balance_after_first = Balances::free_balance(&reward_account);
// Second claim at 50% vesting
let balance_after_second = Balances::free_balance(&reward_account);
// Third claim at 100% vesting
// Should have received all rewards by the end
final_balance,
initial_balance + total_reward - initialization_payment
// Each claim should increase balance
assert!(balance_after_first > initial_balance);
assert!(balance_after_second > balance_after_first);
assert!(final_balance > balance_after_second);
// Final check: all rewards should be claimed