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
use anyhow::Result;
use async_trait::async_trait;
use cnidarium::StateWrite;
use penumbra_sct::component::clock::EpochRead;
use penumbra_shielded_pool::component::AssetRegistry;

use crate::{
    component::action_handler::ActionHandler,
    component::{validator_handler::ValidatorDataRead, StateWriteExt as _},
    event, Undelegate,
};

#[async_trait]
impl ActionHandler for Undelegate {
    type CheckStatelessContext = ();
    async fn check_stateless(&self, _context: ()) -> Result<()> {
        Ok(())
    }

    async fn check_and_execute<S: StateWrite>(&self, mut state: S) -> Result<()> {
        // These checks all formerly happened in the `check_historical` method,
        // if profiling shows that they cause a bottleneck we could (CAREFULLY)
        // move some of them back.
        let u = self;

        // Check that the undelegation was prepared for the current epoch.
        // This let us provide a more helpful error message if an epoch boundary was crossed.
        // And it ensures that the unbonding delay is enforced correctly.
        let current_epoch = state.get_current_epoch().await?;
        let prepared_for_current_epoch = u.from_epoch == current_epoch;
        if !prepared_for_current_epoch {
            tracing::error!(
                from = ?u.from_epoch,
                current = ?current_epoch,
                "undelegation was prepared for a different epoch",
            );
            anyhow::bail!(
                "undelegation was prepared for epoch {} but the current epoch is {}",
                u.from_epoch.index,
                current_epoch.index
            );
        }

        // For undelegations, we enforce correct computation (with rounding)
        // of the *unbonded amount based on the delegation amount*, because
        // users (should be) starting with the amount of delegation tokens they
        // wish to undelegate, and computing the amount of unbonded stake
        // they receive.
        //
        // The direction of the computation matters because the computation
        // involves rounding, so while both
        //
        // (unbonded amount, rates) -> delegation amount
        // (delegation amount, rates) -> unbonded amount
        //
        // should give approximately the same results, they may not give
        // exactly the same results.
        let rate_data = state
            .get_validator_rate(&u.validator_identity)
            .await?
            .ok_or_else(|| {
                anyhow::anyhow!("unknown validator identity {}", u.validator_identity)
            })?;
        let expected_unbonded_amount = rate_data.unbonded_amount(u.delegation_amount);

        if u.unbonded_amount != expected_unbonded_amount {
            tracing::error!(
                actual = %u.unbonded_amount,
                expected = %expected_unbonded_amount,
                "undelegation amount does not match expected amount",
            );
            anyhow::bail!(
                "undelegation amount {} does not match expected amount {}",
                u.unbonded_amount,
                expected_unbonded_amount,
            );
        }

        /* ----- execution ------ */

        // Register the undelegation's denom, so clients can look it up later.
        state.register_denom(&self.unbonding_token().denom()).await;

        tracing::debug!(?self, "queuing undelegation for next epoch");
        state.push_undelegation(self.clone());

        state.record(event::undelegate(self));

        Ok(())
    }
}