solana/genesis/src/unlocks.rs

215 lines
6.6 KiB
Rust

//! lockups generator
use {
solana_sdk::{clock::Epoch, epoch_schedule::EpochSchedule, timing::years_as_slots},
std::time::Duration,
};
#[derive(Debug)]
pub struct UnlockInfo {
pub cliff_fraction: f64,
pub cliff_years: f64,
pub unlocks: usize,
pub unlock_years: f64,
pub custodian: &'static str,
}
#[derive(Debug, Default, Clone)]
pub struct Unlocks {
/// where in iteration over unlocks, loop var
i: usize,
/// number of unlocks after the first cliff
unlocks: usize,
/// fraction unlocked as of last event
prev_fraction: f64,
/// first cliff
/// fraction of unlocked at first cliff
cliff_fraction: f64,
/// time of cliff, in epochs, 0-based
cliff_epoch: Epoch,
/// post cliff
/// fraction unlocked at each post-cliff unlock
unlock_fraction: f64,
/// time between each post-cliff unlock, in Epochs
unlock_epochs: Epoch,
}
impl Unlocks {
pub fn new(
cliff_fraction: f64, // first cliff fraction
cliff_year: f64, // first cliff time, starting from genesis, in years
unlocks: usize, // number of follow-on unlocks
unlock_years: f64, // years between each following unlock
epoch_schedule: &EpochSchedule,
tick_duration: &Duration,
ticks_per_slot: u64,
) -> Self {
// convert cliff year to a slot height, as the cliff_year is considered from genesis
let cliff_slot = years_as_slots(cliff_year, tick_duration, ticks_per_slot) as u64;
// get the first cliff epoch from that slot height
let cliff_epoch = epoch_schedule.get_epoch(cliff_slot);
// assumes that the first cliff is after any epoch warmup and that follow-on
// epochs are uniform in length
let first_unlock_slot =
years_as_slots(cliff_year + unlock_years, tick_duration, ticks_per_slot) as u64;
let unlock_epochs = epoch_schedule.get_epoch(first_unlock_slot) - cliff_epoch;
Self::from_epochs(cliff_fraction, cliff_epoch, unlocks, unlock_epochs)
}
pub fn from_epochs(
cliff_fraction: f64, // first cliff fraction
cliff_epoch: Epoch, // first cliff epoch
unlocks: usize, // number of follow-on unlocks
unlock_epochs: Epoch, // epochs between each following unlock
) -> Self {
let unlock_fraction = if unlocks != 0 {
(1.0 - cliff_fraction) / unlocks as f64
} else {
0.0
};
Self {
prev_fraction: 0.0,
i: 0,
unlocks,
cliff_fraction,
cliff_epoch,
unlock_fraction,
unlock_epochs,
}
}
}
impl Iterator for Unlocks {
type Item = Unlock;
fn next(&mut self) -> Option<Self::Item> {
let i = self.i;
if i == 0 {
self.i += 1;
self.prev_fraction = self.cliff_fraction;
Some(Unlock {
prev_fraction: 0.0,
fraction: self.cliff_fraction,
epoch: self.cliff_epoch,
})
} else if i <= self.unlocks {
self.i += 1;
let prev_fraction = self.prev_fraction;
// move forward, tortured-looking math comes from wanting to reach 1.0 by the last
// unlock
self.prev_fraction = 1.0 - (self.unlocks - i) as f64 * self.unlock_fraction;
Some(Unlock {
prev_fraction,
fraction: self.prev_fraction,
epoch: self.cliff_epoch + i as u64 * self.unlock_epochs,
})
} else {
None
}
}
}
/// describes an unlock event
#[derive(Debug, Default)]
pub struct Unlock {
/// the epoch height at which this unlock occurs
pub epoch: Epoch,
/// the fraction that was unlocked last iteration
pub prev_fraction: f64,
/// the fraction unlocked this iteration
pub fraction: f64,
}
impl Unlock {
/// the number of lamports unlocked at this event
#[allow(clippy::float_cmp)]
pub fn amount(&self, total: u64) -> u64 {
if self.fraction == 1.0 {
total - (self.prev_fraction * total as f64) as u64
} else {
(self.fraction * total as f64) as u64 - (self.prev_fraction * total as f64) as u64
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(clippy::float_cmp)]
fn test_make_lockups() {
// this number just a random val
let total_lamports: u64 = 1_725_987_234_408_923;
// expected config
const EPOCHS_PER_MONTH: Epoch = 2;
assert_eq!(
Unlocks::from_epochs(0.20, 6 * EPOCHS_PER_MONTH, 24, EPOCHS_PER_MONTH)
.map(|unlock| unlock.amount(total_lamports))
.sum::<u64>(),
total_lamports
);
// one tick/sec
let tick_duration = Duration::new(1, 0);
// one tick per slot
let ticks_per_slot = 1;
// two-week epochs at one second per slot
let epoch_schedule = EpochSchedule::custom(14 * 24 * 60 * 60, 0, false);
assert_eq!(
// 30 "month" schedule is 1/5th at 6 months
// 1/24 at each 1/12 of a year thereafter
Unlocks::new(
0.20,
0.5,
24,
1.0 / 12.0,
&epoch_schedule,
&tick_duration,
ticks_per_slot,
)
.map(|unlock| {
if unlock.prev_fraction == 0.0 {
assert_eq!(unlock.epoch, 13); // 26 weeks is 1/2 year, first cliff
} else if unlock.prev_fraction == 0.2 {
assert_eq!(unlock.epoch, 15); // subsequent unlocks are separated by 2 weeks
}
unlock.amount(total_lamports)
})
.sum::<u64>(),
total_lamports
);
assert_eq!(
Unlocks::new(
0.20,
1.5, // start 1.5 years after genesis
24,
1.0 / 12.0,
&epoch_schedule,
&tick_duration,
ticks_per_slot,
)
.map(|unlock| {
if unlock.prev_fraction == 0.0 {
assert_eq!(unlock.epoch, 26 + 13); // 26 weeks is 1/2 year, first cliff is 1.5 years
} else if unlock.prev_fraction == 0.2 {
assert_eq!(unlock.epoch, 26 + 15); // subsequent unlocks are separated by 2 weeks
}
unlock.amount(total_lamports)
})
.sum::<u64>(),
total_lamports
);
}
}