penumbra_sdk_stake/
undelegate.rs

1use penumbra_sdk_asset::{Balance, Value};
2use penumbra_sdk_num::Amount;
3use penumbra_sdk_proto::{penumbra::core::component::stake::v1 as pb, DomainType};
4use penumbra_sdk_sct::epoch::Epoch;
5use penumbra_sdk_txhash::{EffectHash, EffectingData};
6use serde::{Deserialize, Serialize};
7
8use crate::{DelegationToken, IdentityKey, UnbondingToken};
9
10/// A transaction action withdrawing stake from a validator's delegation pool.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(try_from = "pb::Undelegate", into = "pb::Undelegate")]
13pub struct Undelegate {
14    /// The identity key of the validator to undelegate from.
15    pub validator_identity: IdentityKey,
16    /// The epoch at which the undelegation was performed.
17    /// The undelegation takes effect after the unbonding period.
18    pub from_epoch: Epoch,
19    /// The amount to undelegate, in units of unbonding tokens.
20    pub unbonded_amount: Amount,
21    /// The amount of delegation tokens produced by this action.
22    ///
23    /// This is implied by the validator's exchange rate in the specified epoch
24    /// (and should be checked in transaction validation!), but including it allows
25    /// stateless verification that the transaction is internally consistent.
26    pub delegation_amount: Amount,
27}
28
29impl EffectingData for Undelegate {
30    fn effect_hash(&self) -> EffectHash {
31        // For undelegations, the entire action is considered effecting data.
32        EffectHash::from_proto_effecting_data(&self.to_proto())
33    }
34}
35
36impl Undelegate {
37    /// Return the balance after consuming delegation tokens, and producing unbonding tokens.
38    pub fn balance(&self) -> Balance {
39        let undelegation: Balance = self.unbonded_value().into();
40        let delegation: Balance = self.delegation_value().into();
41
42        // We consume the delegation tokens and produce the undelegation tokens.
43        undelegation - delegation
44    }
45
46    pub fn unbonding_token(&self) -> UnbondingToken {
47        // We produce undelegation tokens at a rate of 1:1 with the unbonded
48        // value of the delegated stake. When these tokens are claimed, we
49        // apply penalties that accumulated during the unbonding window.
50        UnbondingToken::new(
51            self.validator_identity.clone(),
52            self.from_epoch.start_height,
53        )
54    }
55
56    /// Returns the [`Value`] of the unbonded [`Amount`].
57    pub fn unbonded_value(&self) -> Value {
58        Value {
59            amount: self.unbonded_amount,
60            asset_id: self.unbonding_token().id(),
61        }
62    }
63
64    pub fn delegation_token(&self) -> DelegationToken {
65        DelegationToken::new(self.validator_identity.clone())
66    }
67
68    /// Returns the [`Value`] of the delegation [`Amount`].
69    pub fn delegation_value(&self) -> Value {
70        Value {
71            amount: self.delegation_amount,
72            asset_id: self.delegation_token().id(),
73        }
74    }
75}
76
77impl DomainType for Undelegate {
78    type Proto = pb::Undelegate;
79}
80
81impl From<Undelegate> for pb::Undelegate {
82    #[allow(deprecated)]
83    fn from(d: Undelegate) -> Self {
84        pb::Undelegate {
85            validator_identity: Some(d.validator_identity.into()),
86            unbonded_amount: Some(d.unbonded_amount.into()),
87            delegation_amount: Some(d.delegation_amount.into()),
88            from_epoch: Some(d.from_epoch.into()),
89            start_epoch_index: 0,
90        }
91    }
92}
93
94impl TryFrom<pb::Undelegate> for Undelegate {
95    type Error = anyhow::Error;
96    fn try_from(d: pb::Undelegate) -> Result<Self, Self::Error> {
97        Ok(Self {
98            validator_identity: d
99                .validator_identity
100                .ok_or_else(|| anyhow::anyhow!("missing validator_identity"))?
101                .try_into()?,
102            from_epoch: d
103                .from_epoch
104                .ok_or_else(|| anyhow::anyhow!("missing from_epoch"))?
105                .try_into()?,
106            unbonded_amount: d
107                .unbonded_amount
108                .ok_or_else(|| anyhow::anyhow!("missing unbonded_amount"))?
109                .try_into()?,
110            delegation_amount: d
111                .delegation_amount
112                .ok_or_else(|| anyhow::anyhow!("missing delegation_amount"))?
113                .try_into()?,
114        })
115    }
116}