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: account_info_v2 APY #4112

Merged
merged 8 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions state-chain/cf-integration-tests/src/authorities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ use state_chain_runtime::{
};

// Helper function that creates a network, funds backup nodes, and have them join the auction.
fn fund_authorities_and_join_auction(
pub fn fund_authorities_and_join_auction(
max_authorities: AuthorityCount,
) -> (network::Network, BTreeSet<NodeId>, BTreeSet<NodeId>) {
// Create MAX_AUTHORITIES backup nodes and fund them above our genesis
// authorities The result will be our newly created nodes will be authorities
// and the genesis authorities will become backup nodes
let genesis_authorities = Validator::current_authorities();
let genesis_authorities: BTreeSet<AccountId32> = Validator::current_authorities();
let (mut testnet, init_backup_nodes) =
network::Network::create(max_authorities as u8, &genesis_authorities);

Expand Down
104 changes: 102 additions & 2 deletions state-chain/cf-integration-tests/src/funding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ use crate::{

use super::{genesis, network, *};
use cf_primitives::{AccountRole, GENESIS_EPOCH};
use cf_traits::{offence_reporting::OffenceReporter, EpochInfo};
use cf_traits::{offence_reporting::OffenceReporter, AccountInfo, Bid, EpochInfo};
use mock_runtime::MIN_FUNDING;
use pallet_cf_funding::pallet::Error;
use pallet_cf_validator::{Backups, CurrentRotationPhase};
use state_chain_runtime::chainflip::Offence;
use sp_runtime::Permill;
use state_chain_runtime::chainflip::{
backup_node_rewards::calculate_backup_rewards, calculate_account_apy, Offence,
};

#[test]
// Nodes cannot redeem when we are out of the redeeming period (50% of the epoch)
Expand Down Expand Up @@ -136,3 +139,100 @@ fn funded_node_is_added_to_backups() {
assert_eq!(backups_map.get(&new_backup).unwrap(), &NEW_FUNDING_AMOUNT);
});
}

#[test]
fn backup_reward_is_calculated_linearly() {
const EPOCH_BLOCKS: u32 = 1_000;
const MAX_AUTHORITIES: u32 = 10;
const NUM_BACKUPS: u32 = 20;
super::genesis::default()
.blocks_per_epoch(EPOCH_BLOCKS)
.max_authorities(MAX_AUTHORITIES)
.build()
.execute_with(|| {
let (mut network, _, _) =
crate::authorities::fund_authorities_and_join_auction(NUM_BACKUPS);
network.move_to_the_next_epoch();

// 3 backup will split the backup reward.
assert_eq!(Validator::highest_funded_qualified_backup_node_bids().count(), 3);

let rewards_per_block = &calculate_backup_rewards::<AccountId, FlipBalance>(
Validator::highest_funded_qualified_backup_node_bids().collect::<Vec<_>>(),
Validator::bond(),
1u128,
Emissions::backup_node_emission_per_block(),
Emissions::current_authority_emission_per_block(),
Validator::current_authority_count() as u128,
);

let rewards_per_heartbeat = &calculate_backup_rewards::<AccountId, FlipBalance>(
Validator::highest_funded_qualified_backup_node_bids().collect::<Vec<_>>(),
Validator::bond(),
HEARTBEAT_BLOCK_INTERVAL as u128,
Emissions::backup_node_emission_per_block(),
Emissions::current_authority_emission_per_block(),
Validator::current_authority_count() as u128,
);

for i in 0..rewards_per_block.len() {
// Validator account should match
assert_eq!(rewards_per_block[i].0, rewards_per_heartbeat[i].0);
// Reward per heartbeat should be scaled linearly.
assert_eq!(
rewards_per_heartbeat[i].1,
rewards_per_block[i].1 * HEARTBEAT_BLOCK_INTERVAL as u128
);
}
});
}

#[test]
fn can_calculate_account_apy() {
const EPOCH_BLOCKS: u32 = 1_000;
const MAX_AUTHORITIES: u32 = 10;
const NUM_BACKUPS: u32 = 20;
super::genesis::default()
.blocks_per_epoch(EPOCH_BLOCKS)
.max_authorities(MAX_AUTHORITIES)
.build()
.execute_with(|| {
let (mut network, _, _) =
crate::authorities::fund_authorities_and_join_auction(NUM_BACKUPS);
network.move_to_the_next_epoch();

let mut backup_earning_rewards = Validator::highest_funded_qualified_backup_node_bids();
let all_backups = Validator::backups();
let validator = Validator::current_authorities().into_iter().next().unwrap();
let Bid { bidder_id: backup, amount: backup_staked } =
backup_earning_rewards.next().unwrap();

// Normal account returns None
let no_reward = AccountId::from([0xff; 32]);
assert!(!Validator::current_authorities().contains(&no_reward));
assert!(!Validator::backups().contains_key(&no_reward));
assert!(calculate_account_apy(&no_reward).is_none());

// Backups that are not qualified to earn rewards are returned None
let backup_no_reward = AccountId::from([0x01; 32]);
assert!(all_backups.contains_key(&backup_no_reward));
assert!(!backup_earning_rewards
.any(|Bid { bidder_id, amount: _ }| bidder_id == backup_no_reward));
assert!(calculate_account_apy(&backup_no_reward).is_none());

// APY rate is correct for current Authority
let total = Flip::balance(&validator);
let reward = Emissions::current_authority_emission_per_block() * YEAR as u128 / 10u128;
let apy_basis_point = Permill::from_rational(reward, total) * 10_000u32;
assert_eq!(apy_basis_point, 49u32);
assert_eq!(calculate_account_apy(&validator), Some(apy_basis_point));

// APY rate is correct for backup that are earning rewards.
// Since all 3 backup validators has the same staked amount, and the award is capped by
// Emission rewards are split evenly between 3 validators.
let reward = Emissions::backup_node_emission_per_block() / 3u128 * YEAR as u128;
let apy_basis_point = Permill::from_rational(reward, backup_staked) * 10_000u32;
assert_eq!(apy_basis_point, 35u32);
assert_eq!(calculate_account_apy(&backup), Some(apy_basis_point));
});
}
6 changes: 6 additions & 0 deletions state-chain/custom-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ pub enum RpcAccountInfo {
is_online: bool,
is_bidding: bool,
bound_redeem_address: Option<EthereumAddress>,
apy_bp: Option<u32>,
restricted_balances: BTreeMap<EthereumAddress, NumberOrHex>,
},
}
Expand Down Expand Up @@ -108,6 +109,7 @@ impl RpcAccountInfo {
is_online: info.is_online,
is_bidding: info.is_bidding,
bound_redeem_address: info.bound_redeem_address,
apy_bp: info.apy_bp,
restricted_balances: info
.restricted_balances
.into_iter()
Expand All @@ -131,6 +133,7 @@ pub struct RpcAccountInfoV2 {
pub is_online: bool,
pub is_bidding: bool,
pub bound_redeem_address: Option<EthereumAddress>,
pub apy_bp: Option<u32>,
pub restricted_balances: BTreeMap<EthereumAddress, u128>,
}

Expand Down Expand Up @@ -580,6 +583,7 @@ where
is_online: account_info.is_online,
is_bidding: account_info.is_bidding,
bound_redeem_address: account_info.bound_redeem_address,
apy_bp: account_info.apy_bp,
restricted_balances: account_info.restricted_balances,
})
}
Expand Down Expand Up @@ -994,6 +998,7 @@ mod test {
is_online: true,
is_qualified: true,
bound_redeem_address: Some(H160::from([1; 20])),
apy_bp: Some(100u32),
restricted_balances: BTreeMap::from_iter(vec![(H160::from([1; 20]), 10u128.pow(18))]),
});
assert_eq!(
Expand All @@ -1012,6 +1017,7 @@ mod test {
"online_credits": 0,
"reputation_points": 0,
"role": "validator",
"apy_bp": 100,
"restricted_balances": {
"0x0101010101010101010101010101010101010101": "0xde0b6b3a7640000"
}
Expand Down
2 changes: 1 addition & 1 deletion state-chain/pallets/cf-emissions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub mod pallet {
#[pallet::constant]
type CompoundingInterval: Get<BlockNumberFor<Self>>;

/// Something that can provide the state chain gatweay address.
/// Something that can provide the state chain gateway address.
type EthEnvironment: EthEnvironmentProvider;

/// The interface for accessing the amount of Flip we want burn.
Expand Down
5 changes: 2 additions & 3 deletions state-chain/pallets/cf-emissions/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ fn no_update_of_update_total_supply_during_safe_mode_code_red() {
<MockRuntimeSafeMode as SetSafeMode<MockRuntimeSafeMode>>::set_code_green();
// Try send a broadcast to update the total supply
Emissions::on_initialize((SUPPLY_UPDATE_INTERVAL * 2).into());
// Expect the broadcast to be sendt
// Expect the broadcast to be sent
assert_eq!(
MockBroadcast::get_called().unwrap().new_total_supply,
Flip::<Test>::total_issuance()
Expand All @@ -126,7 +126,7 @@ fn no_update_of_update_total_supply_during_safe_mode_code_red() {
}

#[test]
fn test_example_block_reward_calcaulation() {
fn test_example_block_reward_calculation() {
use crate::calculate_inflation_to_block_reward;
let issuance: u128 = 100_000_000_000_000_000_000_000_000; // 100m Flip
let inflation: u128 = 2720; // perbill
Expand All @@ -135,7 +135,6 @@ fn test_example_block_reward_calcaulation() {
}

const BLOCKS_PER_YEAR: u64 = (365 * 24 + 6) * 60 * 60 / SECONDS_PER_BLOCK;

#[test]
fn rewards_calculation_compounding() {
const INITIAL_ISSUANCE: u128 = 100_000_000_000_000_000_000_000_000; // 100m Flip
Expand Down
45 changes: 39 additions & 6 deletions state-chain/runtime/src/chainflip.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Configuration, utilities and helpers for the Chainflip runtime.
pub mod address_derivation;
pub mod all_vaults_rotator;
mod backup_node_rewards;
pub mod backup_node_rewards;
pub mod chain_instances;
pub mod decompose_recompose;
pub mod epoch_transition;
Expand All @@ -12,7 +12,7 @@ use crate::{
AccountId, AccountRoles, Authorship, BitcoinChainTracking, BitcoinIngressEgress, BitcoinVault,
BlockNumber, Emissions, Environment, EthereumBroadcaster, EthereumChainTracking,
EthereumIngressEgress, Flip, FlipBalance, PolkadotBroadcaster, PolkadotChainTracking,
PolkadotIngressEgress, Runtime, RuntimeCall, System, Validator,
PolkadotIngressEgress, Runtime, RuntimeCall, System, Validator, YEAR,
};
use backup_node_rewards::calculate_backup_rewards;
use cf_chains::{
Expand Down Expand Up @@ -44,16 +44,16 @@ use cf_chains::{
};
use cf_primitives::{chains::assets, AccountRole, Asset, BasisPoints, ChannelId, EgressId};
use cf_traits::{
AccountRoleRegistry, BlockEmissions, BroadcastAnyChainGovKey, Broadcaster, Chainflip,
CommKeyBroadcaster, DepositApi, DepositHandler, EgressApi, EpochInfo, Heartbeat, Issuance,
KeyProvider, OnBroadcastReady, QualifyNode, RewardsDistribution, RuntimeUpgrade,
AccountInfo, AccountRoleRegistry, BlockEmissions, BroadcastAnyChainGovKey, Broadcaster,
Chainflip, CommKeyBroadcaster, DepositApi, DepositHandler, EgressApi, EpochInfo, Heartbeat,
Issuance, KeyProvider, OnBroadcastReady, QualifyNode, RewardsDistribution, RuntimeUpgrade,
};
use codec::{Decode, Encode};
use frame_support::{
dispatch::{DispatchError, DispatchErrorWithPostInfo, PostDispatchInfo},
sp_runtime::{
traits::{BlockNumberProvider, One, UniqueSaturatedFrom, UniqueSaturatedInto},
FixedU64,
FixedU64, Permill,
},
traits::Get,
};
Expand Down Expand Up @@ -601,3 +601,36 @@ impl QualifyNode<<Runtime as Chainflip>::ValidatorId> for ValidatorRoleQualifica
AccountRoles::has_account_role(id, AccountRole::Validator)
}
}

// Calculates the APY of a given account, returned in Basis Points (1 b.p. = 0.01%)
// Returns Some(APY) if the account is a Validator/backup validator.
// Otherwise returns None.
pub fn calculate_account_apy(account_id: &AccountId) -> Option<u32> {
if pallet_cf_validator::CurrentAuthorities::<Runtime>::get().contains(account_id) {
// Authority: reward is earned by authoring a block.
Some(
Emissions::current_authority_emission_per_block() * YEAR as u128 /
pallet_cf_validator::CurrentAuthorities::<Runtime>::decode_len()
.expect("Current authorities must exists and non-empty.") as u128,
)
} else if Validator::backups().contains_key(account_id) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could search for our account_id in the highest_funded_qualified_backup_node_bids() instead of all the backups since non qualified backups have apy of 0 and the function should return none for them which is already covered by the else case below. A little performance improvement.

// Calculate backup validator reward for the current block, then scaled linearly into YEAR.
calculate_backup_rewards::<AccountId, FlipBalance>(
Validator::highest_funded_qualified_backup_node_bids().collect::<Vec<_>>(),
Validator::bond(),
One::one(),
Emissions::backup_node_emission_per_block(),
Emissions::current_authority_emission_per_block(),
u128::from(Validator::current_authority_count()),
)
.into_iter()
.find(|(id, _reward)| *id == *account_id)
.map(|(_id, reward)| reward * YEAR as u128)
} else {
None
}
.map(|reward_pa| {
// Convert Permill to Basis Point.
Permill::from_rational(reward_pa, Flip::balance(account_id)) * 10_000u32
})
}
2 changes: 1 addition & 1 deletion state-chain/runtime/src/chainflip/backup_node_rewards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use cf_traits::Bid;
use frame_support::sp_runtime::{helpers_128bit::multiply_by_rational_with_rounding, Rounding};
use sp_std::{cmp::min, prelude::*};

//TODO: The u128 is not big enough for some calculations (for example this one) which involve
// TODO: The u128 is not big enough for some calculations (for example this one) which involve
// intermediate steps of the calculation create values that saturate the u128. In this and in
// similar cases we might have to convert the values to BigInt for calculation and then convert it
// back to u128 after calculation. In this case, the saturation problem can lead to upto 0.03 - 0.05
Expand Down
1 change: 1 addition & 0 deletions state-chain/runtime/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub mod common {
pub const MINUTES: BlockNumber = 60_000 / (MILLISECONDS_PER_BLOCK as BlockNumber);
pub const HOURS: BlockNumber = MINUTES * 60;
pub const DAYS: BlockNumber = HOURS * 24;
pub const YEAR: BlockNumber = DAYS * 365;

pub const EXPIRY_SPAN_IN_SECONDS: u64 = 80000;

Expand Down
6 changes: 4 additions & 2 deletions state-chain/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub mod safe_mode;
pub mod test_runner;
mod weights;
use crate::{
chainflip::Offence,
chainflip::{calculate_account_apy, Offence},
runtime_apis::{AuctionState, LiquidityProviderInfo, RuntimeApiPenalty},
};
use cf_amm::{
Expand Down Expand Up @@ -82,7 +82,7 @@ use sp_version::RuntimeVersion;
pub use cf_primitives::{
AccountRole, Asset, AssetAmount, BlockNumber, FlipBalance, SemVer, SwapOutput,
};
pub use cf_traits::{EpochInfo, QualifyNode, SessionKeysRegistered, SwappingApi};
pub use cf_traits::{AccountInfo, EpochInfo, QualifyNode, SessionKeysRegistered, SwappingApi};

pub use chainflip::chain_instances::*;
use chainflip::{
Expand Down Expand Up @@ -920,6 +920,7 @@ impl_runtime_apis! {
let is_current_authority = pallet_cf_validator::CurrentAuthorities::<Runtime>::get().contains(&account_id);
let is_bidding = pallet_cf_funding::ActiveBidder::<Runtime>::get(&account_id);
let bound_redeem_address = pallet_cf_funding::BoundRedeemAddress::<Runtime>::get(&account_id);
let apy_bp = calculate_account_apy(&account_id);
let reputation_info = pallet_cf_reputation::Reputations::<Runtime>::get(&account_id);
let account_info = pallet_cf_flip::Account::<Runtime>::get(&account_id);
let restricted_balances = pallet_cf_funding::RestrictedBalances::<Runtime>::get(&account_id);
Expand All @@ -936,6 +937,7 @@ impl_runtime_apis! {
is_online: Reputation::is_qualified(&account_id),
is_bidding,
bound_redeem_address,
apy_bp,
restricted_balances,
}
}
Expand Down
1 change: 1 addition & 0 deletions state-chain/runtime/src/runtime_apis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub struct RuntimeApiAccountInfoV2 {
pub is_online: bool,
pub is_bidding: bool,
pub bound_redeem_address: Option<EthereumAddress>,
pub apy_bp: Option<u32>, // APY for validator/back only. In Basis points.
pub restricted_balances: BTreeMap<EthereumAddress, u128>,
}

Expand Down