waffles_solana_program/stake/
state.rs

1#![allow(clippy::integer_arithmetic)]
2use {
3    crate::{
4        clock::{Clock, Epoch, UnixTimestamp},
5        instruction::InstructionError,
6        pubkey::Pubkey,
7        stake::{
8            config::Config,
9            instruction::{LockupArgs, StakeError},
10        },
11        stake_history::{StakeHistory, StakeHistoryEntry},
12    },
13    borsh::{maybestd::io, BorshDeserialize, BorshSchema, BorshSerialize},
14    std::collections::HashSet,
15};
16
17pub type StakeActivationStatus = StakeHistoryEntry;
18
19#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)]
20#[allow(clippy::large_enum_variant)]
21pub enum StakeState {
22    #[default]
23    Uninitialized,
24    Initialized(Meta),
25    Stake(Meta, Stake),
26    RewardsPool,
27}
28
29impl BorshDeserialize for StakeState {
30    fn deserialize(buf: &mut &[u8]) -> io::Result<Self> {
31        let enum_value: u32 = BorshDeserialize::deserialize(buf)?;
32        match enum_value {
33            0 => Ok(StakeState::Uninitialized),
34            1 => {
35                let meta: Meta = BorshDeserialize::deserialize(buf)?;
36                Ok(StakeState::Initialized(meta))
37            }
38            2 => {
39                let meta: Meta = BorshDeserialize::deserialize(buf)?;
40                let stake: Stake = BorshDeserialize::deserialize(buf)?;
41                Ok(StakeState::Stake(meta, stake))
42            }
43            3 => Ok(StakeState::RewardsPool),
44            _ => Err(io::Error::new(
45                io::ErrorKind::InvalidData,
46                "Invalid enum value",
47            )),
48        }
49    }
50}
51
52impl BorshSerialize for StakeState {
53    fn serialize<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
54        match self {
55            StakeState::Uninitialized => writer.write_all(&0u32.to_le_bytes()),
56            StakeState::Initialized(meta) => {
57                writer.write_all(&1u32.to_le_bytes())?;
58                meta.serialize(writer)
59            }
60            StakeState::Stake(meta, stake) => {
61                writer.write_all(&2u32.to_le_bytes())?;
62                meta.serialize(writer)?;
63                stake.serialize(writer)
64            }
65            StakeState::RewardsPool => writer.write_all(&3u32.to_le_bytes()),
66        }
67    }
68}
69
70impl StakeState {
71    /// The fixed number of bytes used to serialize each stake account
72    pub const fn size_of() -> usize {
73        200 // see test_size_of
74    }
75
76    pub fn stake(&self) -> Option<Stake> {
77        match self {
78            StakeState::Stake(_meta, stake) => Some(*stake),
79            _ => None,
80        }
81    }
82
83    pub fn delegation(&self) -> Option<Delegation> {
84        match self {
85            StakeState::Stake(_meta, stake) => Some(stake.delegation),
86            _ => None,
87        }
88    }
89
90    pub fn authorized(&self) -> Option<Authorized> {
91        match self {
92            StakeState::Stake(meta, _stake) => Some(meta.authorized),
93            StakeState::Initialized(meta) => Some(meta.authorized),
94            _ => None,
95        }
96    }
97
98    pub fn lockup(&self) -> Option<Lockup> {
99        self.meta().map(|meta| meta.lockup)
100    }
101
102    pub fn meta(&self) -> Option<Meta> {
103        match self {
104            StakeState::Stake(meta, _stake) => Some(*meta),
105            StakeState::Initialized(meta) => Some(*meta),
106            _ => None,
107        }
108    }
109}
110
111#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, AbiExample)]
112pub enum StakeAuthorize {
113    Staker,
114    Withdrawer,
115}
116
117#[derive(
118    Default,
119    Debug,
120    Serialize,
121    Deserialize,
122    PartialEq,
123    Eq,
124    Clone,
125    Copy,
126    AbiExample,
127    BorshDeserialize,
128    BorshSchema,
129    BorshSerialize,
130)]
131pub struct Lockup {
132    /// UnixTimestamp at which this stake will allow withdrawal, unless the
133    ///   transaction is signed by the custodian
134    pub unix_timestamp: UnixTimestamp,
135    /// epoch height at which this stake will allow withdrawal, unless the
136    ///   transaction is signed by the custodian
137    pub epoch: Epoch,
138    /// custodian signature on a transaction exempts the operation from
139    ///  lockup constraints
140    pub custodian: Pubkey,
141}
142
143impl Lockup {
144    pub fn is_in_force(&self, clock: &Clock, custodian: Option<&Pubkey>) -> bool {
145        if custodian == Some(&self.custodian) {
146            return false;
147        }
148        self.unix_timestamp > clock.unix_timestamp || self.epoch > clock.epoch
149    }
150}
151
152#[derive(
153    Default,
154    Debug,
155    Serialize,
156    Deserialize,
157    PartialEq,
158    Eq,
159    Clone,
160    Copy,
161    AbiExample,
162    BorshDeserialize,
163    BorshSchema,
164    BorshSerialize,
165)]
166pub struct Authorized {
167    pub staker: Pubkey,
168    pub withdrawer: Pubkey,
169}
170
171impl Authorized {
172    pub fn auto(authorized: &Pubkey) -> Self {
173        Self {
174            staker: *authorized,
175            withdrawer: *authorized,
176        }
177    }
178    pub fn check(
179        &self,
180        signers: &HashSet<Pubkey>,
181        stake_authorize: StakeAuthorize,
182    ) -> Result<(), InstructionError> {
183        match stake_authorize {
184            StakeAuthorize::Staker if signers.contains(&self.staker) => Ok(()),
185            StakeAuthorize::Withdrawer if signers.contains(&self.withdrawer) => Ok(()),
186            _ => Err(InstructionError::MissingRequiredSignature),
187        }
188    }
189
190    pub fn authorize(
191        &mut self,
192        signers: &HashSet<Pubkey>,
193        new_authorized: &Pubkey,
194        stake_authorize: StakeAuthorize,
195        lockup_custodian_args: Option<(&Lockup, &Clock, Option<&Pubkey>)>,
196    ) -> Result<(), InstructionError> {
197        match stake_authorize {
198            StakeAuthorize::Staker => {
199                // Allow either the staker or the withdrawer to change the staker key
200                if !signers.contains(&self.staker) && !signers.contains(&self.withdrawer) {
201                    return Err(InstructionError::MissingRequiredSignature);
202                }
203                self.staker = *new_authorized
204            }
205            StakeAuthorize::Withdrawer => {
206                if let Some((lockup, clock, custodian)) = lockup_custodian_args {
207                    if lockup.is_in_force(clock, None) {
208                        match custodian {
209                            None => {
210                                return Err(StakeError::CustodianMissing.into());
211                            }
212                            Some(custodian) => {
213                                if !signers.contains(custodian) {
214                                    return Err(StakeError::CustodianSignatureMissing.into());
215                                }
216
217                                if lockup.is_in_force(clock, Some(custodian)) {
218                                    return Err(StakeError::LockupInForce.into());
219                                }
220                            }
221                        }
222                    }
223                }
224                self.check(signers, stake_authorize)?;
225                self.withdrawer = *new_authorized
226            }
227        }
228        Ok(())
229    }
230}
231
232#[derive(
233    Default,
234    Debug,
235    Serialize,
236    Deserialize,
237    PartialEq,
238    Eq,
239    Clone,
240    Copy,
241    AbiExample,
242    BorshDeserialize,
243    BorshSchema,
244    BorshSerialize,
245)]
246pub struct Meta {
247    pub rent_exempt_reserve: u64,
248    pub authorized: Authorized,
249    pub lockup: Lockup,
250}
251
252impl Meta {
253    pub fn set_lockup(
254        &mut self,
255        lockup: &LockupArgs,
256        signers: &HashSet<Pubkey>,
257        clock: &Clock,
258    ) -> Result<(), InstructionError> {
259        // post-stake_program_v4 behavior:
260        // * custodian can update the lockup while in force
261        // * withdraw authority can set a new lockup
262        if self.lockup.is_in_force(clock, None) {
263            if !signers.contains(&self.lockup.custodian) {
264                return Err(InstructionError::MissingRequiredSignature);
265            }
266        } else if !signers.contains(&self.authorized.withdrawer) {
267            return Err(InstructionError::MissingRequiredSignature);
268        }
269        if let Some(unix_timestamp) = lockup.unix_timestamp {
270            self.lockup.unix_timestamp = unix_timestamp;
271        }
272        if let Some(epoch) = lockup.epoch {
273            self.lockup.epoch = epoch;
274        }
275        if let Some(custodian) = lockup.custodian {
276            self.lockup.custodian = custodian;
277        }
278        Ok(())
279    }
280
281    pub fn auto(authorized: &Pubkey) -> Self {
282        Self {
283            authorized: Authorized::auto(authorized),
284            ..Meta::default()
285        }
286    }
287}
288
289#[derive(
290    Debug,
291    Serialize,
292    Deserialize,
293    PartialEq,
294    Clone,
295    Copy,
296    AbiExample,
297    BorshDeserialize,
298    BorshSchema,
299    BorshSerialize,
300)]
301pub struct Delegation {
302    /// to whom the stake is delegated
303    pub voter_pubkey: Pubkey,
304    /// activated stake amount, set at delegate() time
305    pub stake: u64,
306    /// epoch at which this stake was activated, std::Epoch::MAX if is a bootstrap stake
307    pub activation_epoch: Epoch,
308    /// epoch the stake was deactivated, std::Epoch::MAX if not deactivated
309    pub deactivation_epoch: Epoch,
310    /// how much stake we can activate per-epoch as a fraction of currently effective stake
311    pub warmup_cooldown_rate: f64,
312}
313
314impl Default for Delegation {
315    fn default() -> Self {
316        Self {
317            voter_pubkey: Pubkey::default(),
318            stake: 0,
319            activation_epoch: 0,
320            deactivation_epoch: std::u64::MAX,
321            warmup_cooldown_rate: Config::default().warmup_cooldown_rate,
322        }
323    }
324}
325
326impl Delegation {
327    pub fn new(
328        voter_pubkey: &Pubkey,
329        stake: u64,
330        activation_epoch: Epoch,
331        warmup_cooldown_rate: f64,
332    ) -> Self {
333        Self {
334            voter_pubkey: *voter_pubkey,
335            stake,
336            activation_epoch,
337            warmup_cooldown_rate,
338            ..Delegation::default()
339        }
340    }
341    pub fn is_bootstrap(&self) -> bool {
342        self.activation_epoch == std::u64::MAX
343    }
344
345    pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 {
346        self.stake_activating_and_deactivating(epoch, history)
347            .effective
348    }
349
350    #[allow(clippy::comparison_chain)]
351    pub fn stake_activating_and_deactivating(
352        &self,
353        target_epoch: Epoch,
354        history: Option<&StakeHistory>,
355    ) -> StakeActivationStatus {
356        // first, calculate an effective and activating stake
357        let (effective_stake, activating_stake) = self.stake_and_activating(target_epoch, history);
358
359        // then de-activate some portion if necessary
360        if target_epoch < self.deactivation_epoch {
361            // not deactivated
362            if activating_stake == 0 {
363                StakeActivationStatus::with_effective(effective_stake)
364            } else {
365                StakeActivationStatus::with_effective_and_activating(
366                    effective_stake,
367                    activating_stake,
368                )
369            }
370        } else if target_epoch == self.deactivation_epoch {
371            // can only deactivate what's activated
372            StakeActivationStatus::with_deactivating(effective_stake)
373        } else if let Some((history, mut prev_epoch, mut prev_cluster_stake)) =
374            history.and_then(|history| {
375                history
376                    .get(self.deactivation_epoch)
377                    .map(|cluster_stake_at_deactivation_epoch| {
378                        (
379                            history,
380                            self.deactivation_epoch,
381                            cluster_stake_at_deactivation_epoch,
382                        )
383                    })
384            })
385        {
386            // target_epoch > self.deactivation_epoch
387
388            // loop from my deactivation epoch until the target epoch
389            // current effective stake is updated using its previous epoch's cluster stake
390            let mut current_epoch;
391            let mut current_effective_stake = effective_stake;
392            loop {
393                current_epoch = prev_epoch + 1;
394                // if there is no deactivating stake at prev epoch, we should have been
395                // fully undelegated at this moment
396                if prev_cluster_stake.deactivating == 0 {
397                    break;
398                }
399
400                // I'm trying to get to zero, how much of the deactivation in stake
401                //   this account is entitled to take
402                let weight =
403                    current_effective_stake as f64 / prev_cluster_stake.deactivating as f64;
404
405                // portion of newly not-effective cluster stake I'm entitled to at current epoch
406                let newly_not_effective_cluster_stake =
407                    prev_cluster_stake.effective as f64 * self.warmup_cooldown_rate;
408                let newly_not_effective_stake =
409                    ((weight * newly_not_effective_cluster_stake) as u64).max(1);
410
411                current_effective_stake =
412                    current_effective_stake.saturating_sub(newly_not_effective_stake);
413                if current_effective_stake == 0 {
414                    break;
415                }
416
417                if current_epoch >= target_epoch {
418                    break;
419                }
420                if let Some(current_cluster_stake) = history.get(current_epoch) {
421                    prev_epoch = current_epoch;
422                    prev_cluster_stake = current_cluster_stake;
423                } else {
424                    break;
425                }
426            }
427
428            // deactivating stake should equal to all of currently remaining effective stake
429            StakeActivationStatus::with_deactivating(current_effective_stake)
430        } else {
431            // no history or I've dropped out of history, so assume fully deactivated
432            StakeActivationStatus::default()
433        }
434    }
435
436    // returned tuple is (effective, activating) stake
437    fn stake_and_activating(
438        &self,
439        target_epoch: Epoch,
440        history: Option<&StakeHistory>,
441    ) -> (u64, u64) {
442        let delegated_stake = self.stake;
443
444        if self.is_bootstrap() {
445            // fully effective immediately
446            (delegated_stake, 0)
447        } else if self.activation_epoch == self.deactivation_epoch {
448            // activated but instantly deactivated; no stake at all regardless of target_epoch
449            // this must be after the bootstrap check and before all-is-activating check
450            (0, 0)
451        } else if target_epoch == self.activation_epoch {
452            // all is activating
453            (0, delegated_stake)
454        } else if target_epoch < self.activation_epoch {
455            // not yet enabled
456            (0, 0)
457        } else if let Some((history, mut prev_epoch, mut prev_cluster_stake)) =
458            history.and_then(|history| {
459                history
460                    .get(self.activation_epoch)
461                    .map(|cluster_stake_at_activation_epoch| {
462                        (
463                            history,
464                            self.activation_epoch,
465                            cluster_stake_at_activation_epoch,
466                        )
467                    })
468            })
469        {
470            // target_epoch > self.activation_epoch
471
472            // loop from my activation epoch until the target epoch summing up my entitlement
473            // current effective stake is updated using its previous epoch's cluster stake
474            let mut current_epoch;
475            let mut current_effective_stake = 0;
476            loop {
477                current_epoch = prev_epoch + 1;
478                // if there is no activating stake at prev epoch, we should have been
479                // fully effective at this moment
480                if prev_cluster_stake.activating == 0 {
481                    break;
482                }
483
484                // how much of the growth in stake this account is
485                //  entitled to take
486                let remaining_activating_stake = delegated_stake - current_effective_stake;
487                let weight =
488                    remaining_activating_stake as f64 / prev_cluster_stake.activating as f64;
489
490                // portion of newly effective cluster stake I'm entitled to at current epoch
491                let newly_effective_cluster_stake =
492                    prev_cluster_stake.effective as f64 * self.warmup_cooldown_rate;
493                let newly_effective_stake =
494                    ((weight * newly_effective_cluster_stake) as u64).max(1);
495
496                current_effective_stake += newly_effective_stake;
497                if current_effective_stake >= delegated_stake {
498                    current_effective_stake = delegated_stake;
499                    break;
500                }
501
502                if current_epoch >= target_epoch || current_epoch >= self.deactivation_epoch {
503                    break;
504                }
505                if let Some(current_cluster_stake) = history.get(current_epoch) {
506                    prev_epoch = current_epoch;
507                    prev_cluster_stake = current_cluster_stake;
508                } else {
509                    break;
510                }
511            }
512
513            (
514                current_effective_stake,
515                delegated_stake - current_effective_stake,
516            )
517        } else {
518            // no history or I've dropped out of history, so assume fully effective
519            (delegated_stake, 0)
520        }
521    }
522}
523
524#[derive(
525    Debug,
526    Default,
527    Serialize,
528    Deserialize,
529    PartialEq,
530    Clone,
531    Copy,
532    AbiExample,
533    BorshDeserialize,
534    BorshSchema,
535    BorshSerialize,
536)]
537pub struct Stake {
538    pub delegation: Delegation,
539    /// credits observed is credits from vote account state when delegated or redeemed
540    pub credits_observed: u64,
541}
542
543impl Stake {
544    pub fn stake(&self, epoch: Epoch, history: Option<&StakeHistory>) -> u64 {
545        self.delegation.stake(epoch, history)
546    }
547
548    pub fn split(
549        &mut self,
550        remaining_stake_delta: u64,
551        split_stake_amount: u64,
552    ) -> Result<Self, StakeError> {
553        if remaining_stake_delta > self.delegation.stake {
554            return Err(StakeError::InsufficientStake);
555        }
556        self.delegation.stake -= remaining_stake_delta;
557        let new = Self {
558            delegation: Delegation {
559                stake: split_stake_amount,
560                ..self.delegation
561            },
562            ..*self
563        };
564        Ok(new)
565    }
566
567    pub fn deactivate(&mut self, epoch: Epoch) -> Result<(), StakeError> {
568        if self.delegation.deactivation_epoch != std::u64::MAX {
569            Err(StakeError::AlreadyDeactivated)
570        } else {
571            self.delegation.deactivation_epoch = epoch;
572            Ok(())
573        }
574    }
575}
576
577#[cfg(test)]
578mod test {
579    use {
580        super::*, crate::borsh::try_from_slice_unchecked, assert_matches::assert_matches,
581        bincode::serialize,
582    };
583
584    fn check_borsh_deserialization(stake: StakeState) {
585        let serialized = serialize(&stake).unwrap();
586        let deserialized = StakeState::try_from_slice(&serialized).unwrap();
587        assert_eq!(stake, deserialized);
588    }
589
590    fn check_borsh_serialization(stake: StakeState) {
591        let bincode_serialized = serialize(&stake).unwrap();
592        let borsh_serialized = StakeState::try_to_vec(&stake).unwrap();
593        assert_eq!(bincode_serialized, borsh_serialized);
594    }
595
596    #[test]
597    fn test_size_of() {
598        assert_eq!(StakeState::size_of(), std::mem::size_of::<StakeState>());
599    }
600
601    #[test]
602    fn bincode_vs_borsh_deserialization() {
603        check_borsh_deserialization(StakeState::Uninitialized);
604        check_borsh_deserialization(StakeState::RewardsPool);
605        check_borsh_deserialization(StakeState::Initialized(Meta {
606            rent_exempt_reserve: u64::MAX,
607            authorized: Authorized {
608                staker: Pubkey::new_unique(),
609                withdrawer: Pubkey::new_unique(),
610            },
611            lockup: Lockup::default(),
612        }));
613        check_borsh_deserialization(StakeState::Stake(
614            Meta {
615                rent_exempt_reserve: 1,
616                authorized: Authorized {
617                    staker: Pubkey::new_unique(),
618                    withdrawer: Pubkey::new_unique(),
619                },
620                lockup: Lockup::default(),
621            },
622            Stake {
623                delegation: Delegation {
624                    voter_pubkey: Pubkey::new_unique(),
625                    stake: u64::MAX,
626                    activation_epoch: Epoch::MAX,
627                    deactivation_epoch: Epoch::MAX,
628                    warmup_cooldown_rate: f64::MAX,
629                },
630                credits_observed: 1,
631            },
632        ));
633    }
634
635    #[test]
636    fn bincode_vs_borsh_serialization() {
637        check_borsh_serialization(StakeState::Uninitialized);
638        check_borsh_serialization(StakeState::RewardsPool);
639        check_borsh_serialization(StakeState::Initialized(Meta {
640            rent_exempt_reserve: u64::MAX,
641            authorized: Authorized {
642                staker: Pubkey::new_unique(),
643                withdrawer: Pubkey::new_unique(),
644            },
645            lockup: Lockup::default(),
646        }));
647        check_borsh_serialization(StakeState::Stake(
648            Meta {
649                rent_exempt_reserve: 1,
650                authorized: Authorized {
651                    staker: Pubkey::new_unique(),
652                    withdrawer: Pubkey::new_unique(),
653                },
654                lockup: Lockup::default(),
655            },
656            Stake {
657                delegation: Delegation {
658                    voter_pubkey: Pubkey::new_unique(),
659                    stake: u64::MAX,
660                    activation_epoch: Epoch::MAX,
661                    deactivation_epoch: Epoch::MAX,
662                    warmup_cooldown_rate: f64::MAX,
663                },
664                credits_observed: 1,
665            },
666        ));
667    }
668
669    #[test]
670    fn borsh_deserialization_live_data() {
671        let data = [
672            1, 0, 0, 0, 128, 213, 34, 0, 0, 0, 0, 0, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35,
673            119, 124, 168, 12, 120, 216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149,
674            224, 109, 52, 100, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35, 119, 124, 168, 12, 120,
675            216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149, 224, 109, 52, 100, 0, 0,
676            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
677            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
678            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
679            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
680            0, 0, 0, 0, 0, 0,
681        ];
682        // As long as we get the 4-byte enum and the first field right, then
683        // we're sure the rest works out
684        let deserialized = try_from_slice_unchecked::<StakeState>(&data).unwrap();
685        assert_matches!(
686            deserialized,
687            StakeState::Initialized(Meta {
688                rent_exempt_reserve: 2282880,
689                ..
690            })
691        );
692    }
693}