1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! lockups generator
use solana_sdk::{clock::Epoch, epoch_schedule::EpochSchedule, timing::years_as_slots};
use 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
        );
    }
}