Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vara): Maintain total supply consistency in offchain election provider #3389

Merged
merged 3 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pallets/staking-rewards/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pallet-staking-reward-fn.workspace = true
sp-core = { workspace = true, features = ["std"] }
sp-io = { workspace = true, features = ["std"] }
sp-authority-discovery = { workspace = true, features = ["std"] }
sp-npos-elections = { workspace = true, features = ["std"] }
frame-election-provider-support = { workspace = true, features = ["std"] }
pallet-treasury = { workspace = true, features = ["std"] }
pallet-authorship = { workspace = true, features = ["std"] }
Expand All @@ -41,6 +42,7 @@ pallet-session = { workspace = true, features = ["std"] }
pallet-sudo = { workspace = true, features = ["std"] }
pallet-bags-list = { workspace = true, features = ["std"] }
pallet-utility = { workspace = true, features = ["std"] }
pallet-election-provider-multi-phase = { workspace = true, features = ["std"] }
frame-executive = { workspace = true, features = ["std"] }
env_logger.workspace = true

Expand Down
8 changes: 5 additions & 3 deletions pallets/staking-rewards/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,11 @@ impl<T: Config> OnUnbalanced<PositiveImbalanceOf<T>> for Pallet<T> {

/// A type to be plugged into the Staking pallet as the `RewardRemainder` associated type.
///
/// Implements the `OnUnbalanced<NegativeImbalance>` trait in a way that would try to offset
/// the input negative imbalance against the staking rewards pool so that the total
/// token supply is not affected by the rewards-in-excess that are sent to Treasury.
/// Implements the `OnUnbalanced<NegativeImbalance>` trait in a way that would try to burn
/// the amount equivalent to that provided in the input `NegativeImbalance` from the rewards
/// pool in order to keep the token total supply intact. It is assumed that the subsequent
/// `OnUnbalanced` handler (e.g. Treasury) would `resolve` the imbalance and not drop it -
/// otherwise the the total supply will decrease.
pub struct RewardsStash<T, U>(sp_std::marker::PhantomData<(T, U)>);

impl<T: Config, U> OnUnbalanced<NegativeImbalanceOf<T>> for RewardsStash<T, U>
Expand Down
134 changes: 126 additions & 8 deletions pallets/staking-rewards/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use crate as pallet_gear_staking_rewards;
use frame_election_provider_support::{onchain, SequentialPhragmen, VoteWeight};
use frame_election_provider_support::{
onchain, ElectionDataProvider, SequentialPhragmen, VoteWeight,
};
use frame_support::{
construct_runtime, parameter_types,
traits::{
ConstU32, Contains, FindAuthor, GenesisBuild, OnFinalize, OnInitialize, U128CurrencyToVote,
ConstU32, Contains, Currency, Everything, FindAuthor, GenesisBuild, NeverEnsureOrigin,
OnFinalize, OnInitialize, U128CurrencyToVote,
},
weights::constants::RocksDbWeight,
weights::{constants::RocksDbWeight, Weight},
PalletId,
};
use frame_system::{self as system, pallet_prelude::BlockNumberFor, EnsureRoot};
use pallet_election_provider_multi_phase::{self as multi_phase};
use pallet_session::historical::{self as pallet_session_historical};
use sp_core::{crypto::key_types, H256};
use sp_runtime::{
Expand Down Expand Up @@ -95,6 +99,7 @@ construct_runtime!(
BagsList: pallet_bags_list::<Instance1>::{Pallet, Event<T>},
Sudo: pallet_sudo::{Pallet, Call, Storage, Config<T>, Event<T>},
Utility: pallet_utility::{Pallet, Call, Event},
ElectionProviderMultiPhase: multi_phase::{Pallet, Call, Event<T>},
}
);

Expand All @@ -117,7 +122,7 @@ parameter_types! {
}

impl system::Config for Test {
type BaseCallFilter = frame_support::traits::Everything;
type BaseCallFilter = Everything;
type BlockWeights = ();
type BlockLength = ();
type DbWeight = RocksDbWeight;
Expand Down Expand Up @@ -301,6 +306,16 @@ parameter_types! {
pub const MaxOnChainElectableTargets: u16 = 100;
}

frame_election_provider_support::generate_solution_type!(
#[compact]
pub struct TestNposSolution::<
VoterIndex = u32,
TargetIndex = u16,
Accuracy = sp_runtime::PerU16,
MaxVoters = ConstU32::<2_000>,
>(16)
);

pub struct OnChainSeqPhragmen;
impl onchain::Config for OnChainSeqPhragmen {
type System = Test;
Expand Down Expand Up @@ -414,7 +429,93 @@ impl pallet_treasury::Config for Test {
type SpendFunds = ();
type WeightInfo = ();
type MaxApprovals = MaxApprovals;
type SpendOrigin = frame_support::traits::NeverEnsureOrigin<u128>;
type SpendOrigin = NeverEnsureOrigin<u128>;
}

parameter_types! {
// phase durations. 1/4 of the last session for each.
pub static SignedPhase: u64 = SESSION_DURATION / 4;
pub static UnsignedPhase: u64 = SESSION_DURATION / 4;

// signed config
pub static SignedRewardBase: Balance = 50 * UNITS;
pub static SignedDepositBase: Balance = 50 * UNITS;
pub static SignedDepositByte: Balance = 0;
pub static SignedMaxSubmissions: u32 = 5;
pub static SignedMaxRefunds: u32 = 2;
pub BetterUnsignedThreshold: Perbill = Perbill::zero();
pub SignedMaxWeight: Weight = Weight::from_parts(u64::MAX, u64::MAX);

// miner configs
pub static MinerTxPriority: u64 = 100;
pub static MinerMaxWeight: Weight = Weight::from_parts(u64::MAX, u64::MAX);
pub static MinerMaxLength: u32 = 256;
}

impl multi_phase::MinerConfig for Test {
type AccountId = AccountId;
type MaxLength = MinerMaxLength;
type MaxWeight = MinerMaxWeight;
type MaxVotesPerVoter = <Staking as ElectionDataProvider>::MaxVotesPerVoter;
type MaxWinners = MaxActiveValidators;
type Solution = TestNposSolution;

fn solution_weight(v: u32, t: u32, a: u32, d: u32) -> Weight {
<<Self as multi_phase::Config>::WeightInfo as multi_phase::WeightInfo>::submit_unsigned(
v, t, a, d,
)
}
}

pub struct TestBenchmarkingConfig;
impl multi_phase::BenchmarkingConfig for TestBenchmarkingConfig {
const VOTERS: [u32; 2] = [1000, 2000];
const TARGETS: [u32; 2] = [500, 1000];
const ACTIVE_VOTERS: [u32; 2] = [500, 800];
const DESIRED_TARGETS: [u32; 2] = [200, 400];
const SNAPSHOT_MAXIMUM_VOTERS: u32 = 1000;
const MINER_MAXIMUM_VOTERS: u32 = 1000;
const MAXIMUM_TARGETS: u32 = 300;
}

impl multi_phase::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type EstimateCallFee = ConstU32<1_000>;
type SignedPhase = SignedPhase;
type UnsignedPhase = UnsignedPhase;
type BetterUnsignedThreshold = BetterUnsignedThreshold;
type BetterSignedThreshold = ();
type OffchainRepeat = OffchainRepeat;
type MinerTxPriority = MinerTxPriority;
type SignedRewardBase = SignedRewardBase;
type SignedDepositBase = SignedDepositBase;
type SignedDepositByte = ();
type SignedDepositWeight = ();
type SignedMaxWeight = SignedMaxWeight;
type SignedMaxSubmissions = SignedMaxSubmissions;
type SignedMaxRefunds = SignedMaxRefunds;
type SlashHandler = Treasury;
type RewardHandler = StakingRewards;
type DataProvider = Staking;
type Fallback = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GovernanceFallback = onchain::OnChainExecution<OnChainSeqPhragmen>;
type ForceOrigin = frame_system::EnsureRoot<AccountId>;
type MaxElectableTargets = MaxElectableTargets;
type MaxWinners = MaxActiveValidators;
type MaxElectingVoters = MaxElectingVoters;
type WeightInfo = ();
type BenchmarkingConfig = TestBenchmarkingConfig;
type MinerConfig = Self;
type Solver = SequentialPhragmen<AccountId, multi_phase::SolutionAccuracyOf<Self>, ()>;
}

impl<C> frame_system::offchain::SendTransactionTypes<C> for Test
where
RuntimeCall: From<C>,
{
type OverarchingCall = RuntimeCall;
type Extrinsic = TestXt;
}

pub type ValidatorAccountId = (
Expand Down Expand Up @@ -573,9 +674,7 @@ impl ExtBuilder {
if total_supply < self.total_supply {
// Mint the difference to SIGNER user
let diff = self.total_supply.saturating_sub(total_supply);
let _ = <Balances as frame_support::traits::Currency<_>>::deposit_creating(
&SIGNER, diff,
);
let _ = <Balances as Currency<_>>::deposit_creating(&SIGNER, diff);
}
});
ext
Expand Down Expand Up @@ -607,11 +706,30 @@ pub(crate) fn run_for_n_blocks(n: u64) {
}
}

pub fn run_to_unsigned() {
while !matches!(
ElectionProviderMultiPhase::current_phase(),
multi_phase::Phase::Unsigned(_)
) {
run_to_block(System::block_number() + 1);
}
}

pub fn run_to_signed() {
while !matches!(
ElectionProviderMultiPhase::current_phase(),
multi_phase::Phase::Signed
) {
run_to_block(System::block_number() + 1);
}
}

// Run on_initialize hooks in order as they appear in AllPalletsWithSystem.
pub(crate) fn on_initialize(new_block_number: BlockNumberFor<Test>) {
Timestamp::set_timestamp(new_block_number.saturating_mul(MILLISECS_PER_BLOCK));
Authorship::on_initialize(new_block_number);
Session::on_initialize(new_block_number);
ElectionProviderMultiPhase::on_initialize(new_block_number);
ekovalev marked this conversation as resolved.
Show resolved Hide resolved
}

// Run on_finalize hooks (in pallets reverse order, as they appear in AllPalletsWithSystem)
Expand Down
115 changes: 115 additions & 0 deletions pallets/staking-rewards/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,121 @@ fn empty_rewards_pool_causes_inflation() {
});
}

#[test]
fn election_solution_rewards_add_up() {
use pallet_election_provider_multi_phase::{Config as MPConfig, RawSolution};
use sp_npos_elections::ElectionScore;

let (target_inflation, ideal_stake, pool_balance, non_stakeable) = sensible_defaults();
// Solutions submitters
let accounts = (0_u64..5).map(|i| 100 + i).collect::<Vec<_>>();
let mut ext = ExtBuilder::default()
.initial_authorities(vec![
(VAL_1_STASH, VAL_1_CONTROLLER, VAL_1_AUTH_ID),
(VAL_2_STASH, VAL_2_CONTROLLER, VAL_2_AUTH_ID),
(VAL_3_STASH, VAL_3_CONTROLLER, VAL_3_AUTH_ID),
])
.stash(VALIDATOR_STAKE)
.endowment(ENDOWMENT)
.endowed_accounts(accounts)
.total_supply(INITIAL_TOTAL_TOKEN_SUPPLY)
.non_stakeable(non_stakeable)
.pool_balance(pool_balance)
.ideal_stake(ideal_stake)
.target_inflation(target_inflation)
.build();
ext.execute_with(|| {
// Initial chain state
let (initial_total_issuance, _, initial_treasury_balance, initial_rewards_pool_balance) =
chain_state();
assert_eq!(initial_rewards_pool_balance, pool_balance);

// Running chain until the signing phase begins
run_to_signed();
assert!(ElectionProviderMultiPhase::current_phase().is_signed());
assert_eq!(<Test as MPConfig>::SignedMaxRefunds::get(), 2_u32);
assert!(<Test as MPConfig>::SignedMaxSubmissions::get() > 3_u32);

// Submit 3 election solutions candidates:
// 2 good solutions and 1 incorrect one (with higher score, so that it is going to run
// through feasibility check as the best candidate but eventually be rejected and slashed).
let good_solution = RawSolution {
solution: TestNposSolution {
votes1: vec![(0, 0), (1, 1), (2, 2)],
..Default::default()
},
score: ElectionScore {
minimal_stake: VALIDATOR_STAKE,
sum_stake: 3 * VALIDATOR_STAKE,
sum_stake_squared: 3 * VALIDATOR_STAKE * VALIDATOR_STAKE,
},
round: 1,
};
let bad_solution = RawSolution {
solution: TestNposSolution {
votes1: vec![(0, 0), (1, 1), (2, 2)],
..Default::default()
},
score: ElectionScore {
minimal_stake: VALIDATOR_STAKE + 100_u128,
sum_stake: 3 * VALIDATOR_STAKE,
sum_stake_squared: 3 * VALIDATOR_STAKE * VALIDATOR_STAKE,
},
round: 1,
};
let solutions = vec![good_solution.clone(), bad_solution, good_solution];
for (i, s) in solutions.into_iter().enumerate() {
let account = 100_u64 + i as u64;
assert_ok!(ElectionProviderMultiPhase::submit(
RuntimeOrigin::signed(account),
Box::new(s)
));
assert_eq!(
Balances::free_balance(account),
ENDOWMENT - <Test as MPConfig>::SignedDepositBase::get()
);
}

run_to_unsigned();

// Measure current stats
let (total_issuance, _, treasury_balance, rewards_pool_balance) = chain_state();

// Check all balances consistency:
// 1. `total_issuance` hasn't change despite rewards having been minted
assert_eq!(total_issuance, initial_total_issuance);
// 2. the account whose solution was accepted got reward + tx fee rebate
assert_eq!(
Balances::free_balance(102),
ENDOWMENT
+ <Test as MPConfig>::SignedRewardBase::get()
+ <<Test as MPConfig>::EstimateCallFee as Get<u32>>::get() as u128
);
// 3. the account whose solution was rejected got slashed and lost the deposit and fee
assert_eq!(
Balances::free_balance(101),
ENDOWMENT - <Test as MPConfig>::SignedDepositBase::get()
);
// 4. the third account got deposit unreserved and tx fee returned
assert_eq!(
Balances::free_balance(100),
ENDOWMENT + <<Test as MPConfig>::EstimateCallFee as Get<u32>>::get() as u128
);
// 5. the slashed deposit went to `Treasury`
assert_eq!(
treasury_balance,
initial_treasury_balance + <Test as MPConfig>::SignedDepositBase::get()
);
// 6. the rewards offset pool's balanced decreased to compensate for reward and rebates.
assert_eq!(
rewards_pool_balance,
initial_rewards_pool_balance
- <Test as MPConfig>::SignedRewardBase::get()
- <<Test as MPConfig>::EstimateCallFee as Get<u32>>::get() as u128 * 2
);
});
}

fn sensible_defaults() -> (Perquintill, Perquintill, u128, Perquintill) {
(
Perquintill::from_rational(578_u64, 10_000_u64),
Expand Down
4 changes: 2 additions & 2 deletions runtime/vara/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,8 +555,8 @@ impl pallet_election_provider_multi_phase::Config for Runtime {
type SignedMaxRefunds = ConstU32<3>;
type SignedDepositWeight = ();
type SignedMaxWeight = MinerMaxWeight;
type SlashHandler = (); // burn slashes
type RewardHandler = (); // nothing to do upon rewards
type SlashHandler = Treasury;
type RewardHandler = StakingRewards;
type DataProvider = Staking;
type Fallback = onchain::OnChainExecution<OnChainSeqPhragmen>;
type GovernanceFallback = onchain::OnChainExecution<OnChainSeqPhragmen>;
Expand Down