penumbra_sdk_governance/proposal_deposit_claim/
action.rs

1use serde::{Deserialize, Serialize};
2
3use penumbra_sdk_asset::{
4    asset::{self, Metadata},
5    Balance, Value, STAKING_TOKEN_ASSET_ID,
6};
7use penumbra_sdk_num::Amount;
8use penumbra_sdk_proto::{penumbra::core::component::governance::v1 as pb, DomainType};
9use penumbra_sdk_txhash::{EffectHash, EffectingData};
10
11use crate::proposal_state::{Outcome, Withdrawn};
12
13use crate::ProposalNft;
14
15/// A claim for the initial submission deposit for a proposal.
16#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
17#[serde(
18    try_from = "pb::ProposalDepositClaim",
19    into = "pb::ProposalDepositClaim"
20)]
21pub struct ProposalDepositClaim {
22    /// The proposal ID to claim the deposit for.
23    pub proposal: u64,
24    /// The amount of the deposit.
25    pub deposit_amount: Amount,
26    /// The outcome of the proposal.
27    pub outcome: Outcome<()>,
28}
29
30impl EffectingData for ProposalDepositClaim {
31    fn effect_hash(&self) -> EffectHash {
32        EffectHash::from_proto_effecting_data(&self.to_proto())
33    }
34}
35
36impl DomainType for ProposalDepositClaim {
37    type Proto = pb::ProposalDepositClaim;
38}
39
40impl From<ProposalDepositClaim> for pb::ProposalDepositClaim {
41    fn from(value: ProposalDepositClaim) -> pb::ProposalDepositClaim {
42        pb::ProposalDepositClaim {
43            proposal: value.proposal,
44            deposit_amount: Some(value.deposit_amount.into()),
45            outcome: Some(value.outcome.into()),
46        }
47    }
48}
49
50impl TryFrom<pb::ProposalDepositClaim> for ProposalDepositClaim {
51    type Error = anyhow::Error;
52
53    fn try_from(msg: pb::ProposalDepositClaim) -> Result<Self, Self::Error> {
54        Ok(ProposalDepositClaim {
55            proposal: msg.proposal,
56            deposit_amount: msg
57                .deposit_amount
58                .ok_or_else(|| anyhow::anyhow!("missing deposit amount in `ProposalDepositClaim`"))?
59                .try_into()?,
60            outcome: msg
61                .outcome
62                .ok_or_else(|| anyhow::anyhow!("missing outcome in `ProposalDepositClaim`"))?
63                .try_into()?,
64        })
65    }
66}
67
68impl ProposalDepositClaim {
69    /// Compute the balance contributed to the transaction by this proposal deposit claim.
70    pub fn balance(&self) -> Balance {
71        let deposit = Value {
72            amount: self.deposit_amount,
73            asset_id: *STAKING_TOKEN_ASSET_ID,
74        };
75
76        let (voting_or_withdrawn_proposal_denom, claimed_proposal_denom): (Metadata, Metadata) =
77            match self.outcome {
78                // Outcomes without withdrawal consume `deposit`:
79                Outcome::Passed => (
80                    ProposalNft::deposit(self.proposal).denom(),
81                    ProposalNft::passed(self.proposal).denom(),
82                ),
83                Outcome::Failed {
84                    withdrawn: Withdrawn::No,
85                } => (
86                    ProposalNft::deposit(self.proposal).denom(),
87                    ProposalNft::failed(self.proposal).denom(),
88                ),
89                Outcome::Slashed {
90                    withdrawn: Withdrawn::No,
91                } => (
92                    ProposalNft::deposit(self.proposal).denom(),
93                    ProposalNft::slashed(self.proposal).denom(),
94                ),
95                // Outcomes after withdrawal consume `unbonding_deposit`:
96                Outcome::Failed {
97                    withdrawn: Withdrawn::WithReason { .. },
98                } => (
99                    ProposalNft::unbonding_deposit(self.proposal).denom(),
100                    ProposalNft::failed(self.proposal).denom(),
101                ),
102                Outcome::Slashed {
103                    withdrawn: Withdrawn::WithReason { .. },
104                } => (
105                    ProposalNft::unbonding_deposit(self.proposal).denom(),
106                    ProposalNft::slashed(self.proposal).denom(),
107                ),
108            };
109
110        // NFT to be consumed
111        let voting_or_withdrawn_proposal_nft = Value {
112            amount: Amount::from(1u64),
113            asset_id: asset::Id::from(voting_or_withdrawn_proposal_denom),
114        };
115
116        // NFT to be created
117        let claimed_proposal_nft = Value {
118            amount: Amount::from(1u64),
119            asset_id: asset::Id::from(claimed_proposal_denom),
120        };
121
122        // Proposal deposit claims consume the submitted or withdrawn proposal and produce a claimed
123        // proposal and the deposit:
124        let mut balance =
125            Balance::from(claimed_proposal_nft) - Balance::from(voting_or_withdrawn_proposal_nft);
126
127        // Only issue a refund if the proposal was not slashed
128        if self.outcome.should_be_refunded() {
129            balance += Balance::from(deposit);
130        }
131
132        balance
133    }
134}