penumbra_sdk_stake/component/action_handler/
undelegate.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use cnidarium::StateWrite;
4use penumbra_sdk_proto::{DomainType as _, StateWriteProto};
5use penumbra_sdk_sct::component::clock::EpochRead;
6use penumbra_sdk_shielded_pool::component::AssetRegistry;
7
8use crate::{
9    component::action_handler::ActionHandler,
10    component::{validator_handler::ValidatorDataRead, StateWriteExt as _},
11    event, Undelegate,
12};
13
14#[async_trait]
15impl ActionHandler for Undelegate {
16    type CheckStatelessContext = ();
17    async fn check_stateless(&self, _context: ()) -> Result<()> {
18        Ok(())
19    }
20
21    async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> Result<()> {
22        // These checks all formerly happened in the `check_historical` method,
23        // if profiling shows that they cause a bottleneck we could (CAREFULLY)
24        // move some of them back.
25        let u = self;
26
27        // Check that the undelegation was prepared for the current epoch.
28        // This let us provide a more helpful error message if an epoch boundary was crossed.
29        // And it ensures that the unbonding delay is enforced correctly.
30        let current_epoch = state.get_current_epoch().await?;
31        let prepared_for_current_epoch = u.from_epoch == current_epoch;
32        if !prepared_for_current_epoch {
33            tracing::error!(
34                from = ?u.from_epoch,
35                current = ?current_epoch,
36                "undelegation was prepared for a different epoch",
37            );
38            anyhow::bail!(
39                "undelegation was prepared for epoch {} but the current epoch is {}",
40                u.from_epoch.index,
41                current_epoch.index
42            );
43        }
44
45        // For undelegations, we enforce correct computation (with rounding)
46        // of the *unbonded amount based on the delegation amount*, because
47        // users (should be) starting with the amount of delegation tokens they
48        // wish to undelegate, and computing the amount of unbonded stake
49        // they receive.
50        //
51        // The direction of the computation matters because the computation
52        // involves rounding, so while both
53        //
54        // (unbonded amount, rates) -> delegation amount
55        // (delegation amount, rates) -> unbonded amount
56        //
57        // should give approximately the same results, they may not give
58        // exactly the same results.
59        let rate_data = state
60            .get_validator_rate(&u.validator_identity)
61            .await?
62            .ok_or_else(|| {
63                anyhow::anyhow!("unknown validator identity {}", u.validator_identity)
64            })?;
65        let expected_unbonded_amount = rate_data.unbonded_amount(u.delegation_amount);
66
67        if u.unbonded_amount != expected_unbonded_amount {
68            tracing::error!(
69                actual = %u.unbonded_amount,
70                expected = %expected_unbonded_amount,
71                "undelegation amount does not match expected amount",
72            );
73            anyhow::bail!(
74                "undelegation amount {} does not match expected amount {}",
75                u.unbonded_amount,
76                expected_unbonded_amount,
77            );
78        }
79
80        /* ----- execution ------ */
81
82        // Register the undelegation's denom, so clients can look it up later.
83        state.register_denom(&self.unbonding_token().denom()).await;
84
85        tracing::debug!(?self, "queuing undelegation for next epoch");
86        state.push_undelegation(self.clone());
87
88        state.record_proto(event::EventUndelegate::from(self).to_proto());
89
90        Ok(())
91    }
92}