diff --git a/actors/market/src/ext.rs b/actors/market/src/ext.rs index 987d50682..0be8f9d85 100644 --- a/actors/market/src/ext.rs +++ b/actors/market/src/ext.rs @@ -162,5 +162,7 @@ pub mod power { pub quality_adj_power: StoragePower, pub pledge_collateral: TokenAmount, pub quality_adj_power_smoothed: FilterEstimate, + pub ramp_start_epoch: i64, + pub ramp_duration_epochs: u64, } } diff --git a/actors/market/tests/harness.rs b/actors/market/tests/harness.rs index 1de246da2..b4ad54603 100644 --- a/actors/market/tests/harness.rs +++ b/actors/market/tests/harness.rs @@ -1150,6 +1150,8 @@ pub fn expect_query_network_info(rt: &MockRuntime) { quality_adj_power: power.clone(), pledge_collateral: TokenAmount::default(), quality_adj_power_smoothed: FilterEstimate::new(reward.atto().clone(), BigInt::zero()), + ramp_start_epoch: 0, + ramp_duration_epochs: 0, }; let current_reward = ThisEpochRewardReturn { this_epoch_baseline_power: power, diff --git a/actors/miner/src/ext.rs b/actors/miner/src/ext.rs index 031ccaf63..2f62e4274 100644 --- a/actors/miner/src/ext.rs +++ b/actors/miner/src/ext.rs @@ -101,6 +101,8 @@ pub mod power { pub quality_adj_power: StoragePower, pub pledge_collateral: TokenAmount, pub quality_adj_power_smoothed: FilterEstimate, + pub ramp_start_epoch: i64, + pub ramp_duration_epochs: u64, } #[derive(Serialize_tuple, Deserialize_tuple)] pub struct EnrollCronEventParams { diff --git a/actors/miner/src/lib.rs b/actors/miner/src/lib.rs index c1a132f20..01b9b5f85 100644 --- a/actors/miner/src/lib.rs +++ b/actors/miner/src/lib.rs @@ -817,6 +817,8 @@ impl Actor { network_baseline: rew.this_epoch_baseline_power, circulating_supply, epoch_reward: rew.this_epoch_reward_smoothed, + epochs_since_ramp_start: rt.curr_epoch() - pwr.ramp_start_epoch, + ramp_duration_epochs: pwr.ramp_duration_epochs, }; /* @@ -1908,6 +1910,8 @@ impl Actor { network_baseline: rew.this_epoch_baseline_power, circulating_supply, epoch_reward: rew.this_epoch_reward_smoothed, + epochs_since_ramp_start: rt.curr_epoch() - pwr.ramp_start_epoch, + ramp_duration_epochs: pwr.ramp_duration_epochs, }; activate_new_sector_infos( rt, @@ -1981,6 +1985,8 @@ impl Actor { network_baseline: params.reward_baseline_power, circulating_supply: rt.total_fil_circ_supply(), epoch_reward: params.reward_smoothed, + epochs_since_ramp_start: 0, + ramp_duration_epochs: 0, }; activate_new_sector_infos( rt, @@ -2093,6 +2099,8 @@ impl Actor { network_baseline: rew.this_epoch_baseline_power, circulating_supply, epoch_reward: rew.this_epoch_reward_smoothed, + epochs_since_ramp_start: rt.curr_epoch() - pwr.ramp_start_epoch, + ramp_duration_epochs: pwr.ramp_duration_epochs, }; let sector_day_reward = expected_reward_for_power( @@ -2115,6 +2123,8 @@ impl Actor { &pledge_inputs.epoch_reward, &pledge_inputs.network_qap, &pledge_inputs.circulating_supply, + pledge_inputs.epochs_since_ramp_start, + pledge_inputs.ramp_duration_epochs, ); let sectors_to_add = valid_sectors @@ -4116,6 +4126,8 @@ where network_baseline: rew.this_epoch_baseline_power, circulating_supply, epoch_reward: rew.this_epoch_reward_smoothed, + epochs_since_ramp_start: rt.curr_epoch() - pow.ramp_start_epoch, + ramp_duration_epochs: pow.ramp_duration_epochs, }; let mut power_delta = PowerPair::zero(); let mut pledge_delta = TokenAmount::zero(); @@ -4307,6 +4319,8 @@ fn update_existing_sector_info( &pledge_inputs.epoch_reward, &pledge_inputs.network_qap, &pledge_inputs.circulating_supply, + pledge_inputs.epochs_since_ramp_start, + pledge_inputs.ramp_duration_epochs, ), ); new_sector_info @@ -5546,6 +5560,8 @@ fn activate_new_sector_infos( &pledge_inputs.epoch_reward, &pledge_inputs.network_qap, &pledge_inputs.circulating_supply, + pledge_inputs.epochs_since_ramp_start, + pledge_inputs.ramp_duration_epochs, ); deposit_to_unlock += pci.pre_commit_deposit.clone(); @@ -5958,6 +5974,8 @@ struct NetworkPledgeInputs { pub network_baseline: StoragePower, pub circulating_supply: TokenAmount, pub epoch_reward: FilterEstimate, + pub epochs_since_ramp_start: i64, + pub ramp_duration_epochs: u64, } // Note: probably better to push this one level down into state diff --git a/actors/miner/src/monies.rs b/actors/miner/src/monies.rs index 9acefcbbd..9fdef8384 100644 --- a/actors/miner/src/monies.rs +++ b/actors/miner/src/monies.rs @@ -75,6 +75,8 @@ pub const TERMINATION_LIFETIME_CAP: ChainEpoch = 140; // Multiplier of whole per-winner rewards for a consensus fault penalty. const CONSENSUS_FAULT_FACTOR: u64 = 5; +const GAMMA_FIXED_POINT_FACTOR: u64 = 1000; // 3 decimal places + /// The projected block reward a sector would earn over some period. /// Also known as "BR(t)". /// BR(t) = ProjectedRewardFraction(t) * SectorQualityAdjustedPower @@ -255,6 +257,8 @@ pub fn initial_pledge_for_power( reward_estimate: &FilterEstimate, network_qa_power_estimate: &FilterEstimate, circulating_supply: &TokenAmount, + epochs_since_ramp_start: i64, + ramp_duration_epochs: u64, ) -> TokenAmount { let ip_base = expected_reward_for_power_clamped_at_atto_fil( reward_estimate, @@ -267,10 +271,41 @@ pub fn initial_pledge_for_power( let lock_target_denom = LOCK_TARGET_FACTOR_DENOM; let pledge_share_num = qa_power; let network_qa_power = network_qa_power_estimate.estimate(); - let pledge_share_denom = cmp::max(cmp::max(&network_qa_power, baseline_power), qa_power); + + // Once FIP-0081 has fully activated, additional pledge will be 70% baseline + // pledge + 30% simple pledge. + const FIP_0081_ACTIVATION_PERMILLE: i64 = 300; + // Gamma/GAMMA_FIXED_POINT_FACTOR is the share of pledge coming from the + // baseline formulation, with 1-(gamma/GAMMA_FIXED_POINT_FACTOR) coming from + // simple pledge. + // gamma = 1000 - 300 * (epochs_since_ramp_start / ramp_duration_epochs).max(0).min(1) + let skew = if epochs_since_ramp_start < 0 { + // No skew before ramp start + 0 + } else if ramp_duration_epochs == 0 || epochs_since_ramp_start >= ramp_duration_epochs as i64 { + // 100% skew after ramp end + FIP_0081_ACTIVATION_PERMILLE as u64 + } else { + ((epochs_since_ramp_start * FIP_0081_ACTIVATION_PERMILLE) / ramp_duration_epochs as i64) + as u64 + }; + let gamma = GAMMA_FIXED_POINT_FACTOR - skew; + let additional_ip_num = lock_target_num * pledge_share_num; - let additional_ip_denom = pledge_share_denom * lock_target_denom; - let additional_ip = additional_ip_num.div_floor(&additional_ip_denom); + + let pledge_share_denom_baseline = + cmp::max(cmp::max(&network_qa_power, baseline_power), qa_power); + let pledge_share_denom_simple = cmp::max(&network_qa_power, qa_power); + + let additional_ip_denom_baseline = pledge_share_denom_baseline * lock_target_denom; + let additional_ip_baseline = (gamma * &additional_ip_num) + .div_floor(&(additional_ip_denom_baseline * GAMMA_FIXED_POINT_FACTOR)); + let additional_ip_denom_simple = pledge_share_denom_simple * lock_target_denom; + let additional_ip_simple = ((GAMMA_FIXED_POINT_FACTOR - gamma) * &additional_ip_num) + .div_floor(&(additional_ip_denom_simple * GAMMA_FIXED_POINT_FACTOR)); + + // convex combination of simple and baseline pledge + let additional_ip = additional_ip_baseline + additional_ip_simple; let nominal_pledge = ip_base + TokenAmount::from_atto(additional_ip); let pledge_cap = TokenAmount::from_atto(INITIAL_PLEDGE_MAX_PER_BYTE.atto() * qa_power); diff --git a/actors/miner/tests/aggregate_prove_commit.rs b/actors/miner/tests/aggregate_prove_commit.rs index c999fceb6..5979690f9 100644 --- a/actors/miner/tests/aggregate_prove_commit.rs +++ b/actors/miner/tests/aggregate_prove_commit.rs @@ -100,6 +100,9 @@ fn valid_precommits_then_aggregate_provecommit() { &actor.epoch_reward_smooth, &actor.epoch_qa_power_smooth, &rt.total_fil_circ_supply(), + // NOTE: zero inputs here preserve the original pledge functionality + 0, + 0, ); let ten_sectors_initial_pledge = BigInt::from(10i32) * expected_initial_pledge.clone(); assert_eq!(ten_sectors_initial_pledge, st.initial_pledge); diff --git a/actors/miner/tests/fip0081_initial_pledge.rs b/actors/miner/tests/fip0081_initial_pledge.rs new file mode 100644 index 000000000..c4c3d1d44 --- /dev/null +++ b/actors/miner/tests/fip0081_initial_pledge.rs @@ -0,0 +1,232 @@ +use fil_actor_miner::initial_pledge_for_power; +use fil_actors_runtime::reward::FilterEstimate; +use fvm_shared::econ::TokenAmount; +use fvm_shared::sector::StoragePower; +use num_traits::zero; + +macro_rules! my_const { + ($name:ident, $ret_type:ty, $value:expr) => { + fn $name() -> $ret_type { + $value + } + }; +} + +my_const!(epoch_target_reward, TokenAmount, zero()); +my_const!(qa_sector_power, StoragePower, StoragePower::from(1u64 << 36)); +my_const!(network_qa_power, StoragePower, StoragePower::from(1u64 << 10)); +my_const!(power_rate_of_change, StoragePower, StoragePower::from(1u64 << 10)); +my_const!( + reward_estimate, + FilterEstimate, + FilterEstimate::new(epoch_target_reward().atto().clone(), zero()) +); +my_const!( + power_estimate, + FilterEstimate, + FilterEstimate::new(network_qa_power(), power_rate_of_change()) +); +my_const!(circulating_supply, TokenAmount, TokenAmount::from_whole(1)); + +// Pre-ramp where 'baseline power' dominates +#[test] +fn initial_pledge_pre_ramp_negative() { + let initial_pledge = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + -100, + 100, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1500).div_floor(10000), + initial_pledge + ); +} + +#[test] +fn initial_pledge_zero_ramp_duration() { + let initial_pledge = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + 0, + 0, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1950).div_floor(10000), + initial_pledge + ); + + let initial_pledge = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + 10, + 0, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1950).div_floor(10000), + initial_pledge + ); +} + +// Pre-ramp where 'baseline power' dominates +#[test] +fn initial_pledge_pre_ramp() { + let initial_pledge = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + 0, + 100, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1500).div_floor(10000), + initial_pledge + ); +} + +// On-ramp where 'baseline power' is at 85% and `simple power` is at 15%. +#[test] +fn initial_pledge_on_ramp_mid() { + let initial_pledge = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + 50, + 100, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1725).div_floor(10000), + initial_pledge + ); +} + +// After-ramp where 'baseline power' is at 70% and `simple power` is at 30%. +#[test] +fn initial_pledge_after_ramp() { + let initial_pledge = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + 150, + 100, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1950).div_floor(10000), + initial_pledge + ); +} + +// On-ramp where 'baseline power' has reduced effect (97%). +#[test] +fn initial_pledge_on_ramp_early() { + let initial_pledge = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + 10, + 100, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1545).div_floor(10000), + initial_pledge + ); +} + +// On-ramp, first epoch, pledge should be 97% 'baseline' + 3% simple. +#[test] +fn initial_pledge_on_ramp_step() { + let initial_pledge = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + 1, + 10, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1545).div_floor(10000), + initial_pledge + ); +} + +// Validate pledges 1 epoch before and after ramp start. +#[test] +fn initial_pledge_ramp_edges() { + let initial_pledge_before_ramp = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + -1, + 10, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1500).div_floor(10000), + initial_pledge_before_ramp + ); + + let initial_pledge_at_ramp = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + 0, + 10, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1500).div_floor(10000), + initial_pledge_at_ramp + ); + + let initial_pledge_on_ramp = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + 1, + 10, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1545).div_floor(10000), + initial_pledge_on_ramp + ); +} + +// Post-ramp where 'baseline power' has reduced effect (70%). +#[test] +fn initial_pledge_post_ramp() { + let initial_pledge = initial_pledge_for_power( + &qa_sector_power(), + &StoragePower::from(1u64 << 37), + &reward_estimate(), + &power_estimate(), + &circulating_supply(), + 500, + 100, + ); + assert_eq!( + TokenAmount::from_atto(1) + TokenAmount::from_whole(1950).div_floor(10000), + initial_pledge + ); +} diff --git a/actors/miner/tests/precommit_deposit_and_initial_pledge_positive_test.rs b/actors/miner/tests/precommit_deposit_and_initial_pledge_positive_test.rs index 15ac23e24..dd2e77e21 100644 --- a/actors/miner/tests/precommit_deposit_and_initial_pledge_positive_test.rs +++ b/actors/miner/tests/precommit_deposit_and_initial_pledge_positive_test.rs @@ -38,6 +38,10 @@ fn initial_pledge_clamped_at_one_attofil() { &reward_estimate(), &power_estimate(), &circulating_supply(), + // NOTE: setting this to zero preserves the original pledge definition (before baseline bug fix) + // so these inputs configure the function to return the original pledge + 0, + 0, ); assert_eq!(TokenAmount::from_atto(1), initial_pledge); } diff --git a/actors/miner/tests/util.rs b/actors/miner/tests/util.rs index 8a9bf9ded..93381e4af 100644 --- a/actors/miner/tests/util.rs +++ b/actors/miner/tests/util.rs @@ -769,6 +769,8 @@ impl ActorHarness { quality_adj_power: self.network_qa_power.clone(), pledge_collateral: self.network_pledge.clone(), quality_adj_power_smoothed: self.epoch_qa_power_smooth.clone(), + ramp_start_epoch: 0, + ramp_duration_epochs: 0, }; let current_reward = ThisEpochRewardReturn { this_epoch_baseline_power: self.baseline_power.clone(), @@ -2335,6 +2337,8 @@ impl ActorHarness { &self.epoch_reward_smooth, &self.epoch_qa_power_smooth, &rt.total_fil_circ_supply(), + 0, + 0, ) } diff --git a/actors/power/src/lib.rs b/actors/power/src/lib.rs index 45bc352e2..f89944951 100644 --- a/actors/power/src/lib.rs +++ b/actors/power/src/lib.rs @@ -267,6 +267,8 @@ impl Actor { quality_adj_power: st.this_epoch_quality_adj_power, pledge_collateral: st.this_epoch_pledge_collateral, quality_adj_power_smoothed: st.this_epoch_qa_power_smoothed, + ramp_start_epoch: st.ramp_start_epoch, + ramp_duration_epochs: st.ramp_duration_epochs, }) } diff --git a/actors/power/src/state.rs b/actors/power/src/state.rs index a30fa6760..673b077ea 100644 --- a/actors/power/src/state.rs +++ b/actors/power/src/state.rs @@ -68,6 +68,15 @@ pub struct State { /// Number of miners having proven the minimum consensus power. pub miner_above_min_power_count: i64, + /// FIP0081 changed pledge calculations, moving from ruleset A to ruleset B. + /// This change is spread over several epochs to avoid sharp jumps in pledge + /// amounts. At `ramp_start_epoch`, we use the old ruleset. At + /// `ramp_start_epoch + ramp_duration_epochs`, we use 70% old rules + 30% + /// new rules. See FIP0081 for more details. + pub ramp_start_epoch: i64, + /// Number of epochs over which the new pledge calculation is ramped up. + pub ramp_duration_epochs: u64, + /// A queue of events to be triggered by cron, indexed by epoch. pub cron_event_queue: Cid, // Multimap, (HAMT[ChainEpoch]AMT[CronEvent] diff --git a/actors/power/src/types.rs b/actors/power/src/types.rs index 983c64dc2..db5f9e077 100644 --- a/actors/power/src/types.rs +++ b/actors/power/src/types.rs @@ -68,6 +68,8 @@ pub struct CurrentTotalPowerReturn { pub quality_adj_power: StoragePower, pub pledge_collateral: TokenAmount, pub quality_adj_power_smoothed: FilterEstimate, + pub ramp_start_epoch: i64, + pub ramp_duration_epochs: u64, } #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone, Eq, PartialEq)]