Skip to content

Commit

Permalink
feat(vara): Maintain total supply consistency in offchain election pr…
Browse files Browse the repository at this point in the history
…ovider (#3389)
  • Loading branch information
ekovalev authored Oct 5, 2023
1 parent ae31dbb commit 71168c5
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 13 deletions.
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);
}

// 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

0 comments on commit 71168c5

Please sign in to comment.