penumbra_governance/proposal_deposit_claim/
action.rs

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
use serde::{Deserialize, Serialize};

use penumbra_asset::{
    asset::{self, Metadata},
    Balance, Value, STAKING_TOKEN_ASSET_ID,
};
use penumbra_num::Amount;
use penumbra_proto::{penumbra::core::component::governance::v1 as pb, DomainType};
use penumbra_txhash::{EffectHash, EffectingData};

use crate::proposal_state::{Outcome, Withdrawn};

use crate::ProposalNft;

/// A claim for the initial submission deposit for a proposal.
#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
#[serde(
    try_from = "pb::ProposalDepositClaim",
    into = "pb::ProposalDepositClaim"
)]
pub struct ProposalDepositClaim {
    /// The proposal ID to claim the deposit for.
    pub proposal: u64,
    /// The amount of the deposit.
    pub deposit_amount: Amount,
    /// The outcome of the proposal.
    pub outcome: Outcome<()>,
}

impl EffectingData for ProposalDepositClaim {
    fn effect_hash(&self) -> EffectHash {
        EffectHash::from_proto_effecting_data(&self.to_proto())
    }
}

impl DomainType for ProposalDepositClaim {
    type Proto = pb::ProposalDepositClaim;
}

impl From<ProposalDepositClaim> for pb::ProposalDepositClaim {
    fn from(value: ProposalDepositClaim) -> pb::ProposalDepositClaim {
        pb::ProposalDepositClaim {
            proposal: value.proposal,
            deposit_amount: Some(value.deposit_amount.into()),
            outcome: Some(value.outcome.into()),
        }
    }
}

impl TryFrom<pb::ProposalDepositClaim> for ProposalDepositClaim {
    type Error = anyhow::Error;

    fn try_from(msg: pb::ProposalDepositClaim) -> Result<Self, Self::Error> {
        Ok(ProposalDepositClaim {
            proposal: msg.proposal,
            deposit_amount: msg
                .deposit_amount
                .ok_or_else(|| anyhow::anyhow!("missing deposit amount in `ProposalDepositClaim`"))?
                .try_into()?,
            outcome: msg
                .outcome
                .ok_or_else(|| anyhow::anyhow!("missing outcome in `ProposalDepositClaim`"))?
                .try_into()?,
        })
    }
}

impl ProposalDepositClaim {
    /// Compute the balance contributed to the transaction by this proposal deposit claim.
    pub fn balance(&self) -> Balance {
        let deposit = Value {
            amount: self.deposit_amount,
            asset_id: *STAKING_TOKEN_ASSET_ID,
        };

        let (voting_or_withdrawn_proposal_denom, claimed_proposal_denom): (Metadata, Metadata) =
            match self.outcome {
                // Outcomes without withdrawal consume `deposit`:
                Outcome::Passed => (
                    ProposalNft::deposit(self.proposal).denom(),
                    ProposalNft::passed(self.proposal).denom(),
                ),
                Outcome::Failed {
                    withdrawn: Withdrawn::No,
                } => (
                    ProposalNft::deposit(self.proposal).denom(),
                    ProposalNft::failed(self.proposal).denom(),
                ),
                Outcome::Slashed {
                    withdrawn: Withdrawn::No,
                } => (
                    ProposalNft::deposit(self.proposal).denom(),
                    ProposalNft::slashed(self.proposal).denom(),
                ),
                // Outcomes after withdrawal consume `unbonding_deposit`:
                Outcome::Failed {
                    withdrawn: Withdrawn::WithReason { .. },
                } => (
                    ProposalNft::unbonding_deposit(self.proposal).denom(),
                    ProposalNft::failed(self.proposal).denom(),
                ),
                Outcome::Slashed {
                    withdrawn: Withdrawn::WithReason { .. },
                } => (
                    ProposalNft::unbonding_deposit(self.proposal).denom(),
                    ProposalNft::slashed(self.proposal).denom(),
                ),
            };

        // NFT to be consumed
        let voting_or_withdrawn_proposal_nft = Value {
            amount: Amount::from(1u64),
            asset_id: asset::Id::from(voting_or_withdrawn_proposal_denom),
        };

        // NFT to be created
        let claimed_proposal_nft = Value {
            amount: Amount::from(1u64),
            asset_id: asset::Id::from(claimed_proposal_denom),
        };

        // Proposal deposit claims consume the submitted or withdrawn proposal and produce a claimed
        // proposal and the deposit:
        let mut balance =
            Balance::from(claimed_proposal_nft) - Balance::from(voting_or_withdrawn_proposal_nft);

        // Only issue a refund if the proposal was not slashed
        if self.outcome.should_be_refunded() {
            balance += Balance::from(deposit);
        }

        balance
    }
}