squads_multisig_program/state/
proposal.rs

1#![allow(deprecated)]
2use anchor_lang::prelude::*;
3
4use crate::errors::*;
5
6/// Stores the data required for tracking the status of a multisig proposal.
7/// Each `Proposal` has a 1:1 association with a transaction account, e.g. a `VaultTransaction` or a `ConfigTransaction`;
8/// the latter can be executed only after the `Proposal` has been approved and its time lock is released.
9#[account]
10pub struct Proposal {
11    /// The multisig this belongs to.
12    pub multisig: Pubkey,
13    /// Index of the multisig transaction this proposal is associated with.
14    pub transaction_index: u64,
15    /// The status of the transaction.
16    pub status: ProposalStatus,
17    /// PDA bump.
18    pub bump: u8,
19    /// Keys that have approved/signed.
20    pub approved: Vec<Pubkey>,
21    /// Keys that have rejected.
22    pub rejected: Vec<Pubkey>,
23    /// Keys that have cancelled (Approved only).
24    pub cancelled: Vec<Pubkey>,
25}
26
27impl Proposal {
28    pub fn size(members_len: usize) -> usize {
29        8 +   // anchor account discriminator
30        32 +  // multisig
31        8 +   // index
32        1 +   // status enum variant
33        8 +   // status enum wrapped timestamp (i64)
34        1 +   // bump
35        (4 + (members_len * 32)) + // approved vec
36        (4 + (members_len * 32)) + // rejected vec
37        (4 + (members_len * 32)) // cancelled vec
38    }
39
40    /// Register an approval vote.
41    pub fn approve(&mut self, member: Pubkey, threshold: usize) -> Result<()> {
42        // If `member` has previously voted to reject, remove that vote.
43        if let Some(vote_index) = self.has_voted_reject(member.key()) {
44            self.remove_rejection_vote(vote_index);
45        }
46
47        // Insert the vote of approval.
48        match self.approved.binary_search(&member) {
49            Ok(_) => return err!(MultisigError::AlreadyApproved),
50            Err(pos) => self.approved.insert(pos, member),
51        };
52
53        // If current number of approvals reaches threshold, mark the transaction as `Approved`.
54        if self.approved.len() >= threshold {
55            self.status = ProposalStatus::Approved {
56                timestamp: Clock::get()?.unix_timestamp,
57            };
58        }
59
60        Ok(())
61    }
62
63    /// Register a rejection vote.
64    pub fn reject(&mut self, member: Pubkey, cutoff: usize) -> Result<()> {
65        // If `member` has previously voted to approve, remove that vote.
66        if let Some(vote_index) = self.has_voted_approve(member.key()) {
67            self.remove_approval_vote(vote_index);
68        }
69
70        // Insert the vote of rejection.
71        match self.rejected.binary_search(&member) {
72            Ok(_) => return err!(MultisigError::AlreadyRejected),
73            Err(pos) => self.rejected.insert(pos, member),
74        };
75
76        // If current number of rejections reaches cutoff, mark the transaction as `Rejected`.
77        if self.rejected.len() >= cutoff {
78            self.status = ProposalStatus::Rejected {
79                timestamp: Clock::get()?.unix_timestamp,
80            };
81        }
82
83        Ok(())
84    }
85
86    /// Registers a cancellation vote.
87    pub fn cancel(&mut self, member: Pubkey, threshold: usize) -> Result<()> {
88        // Insert the vote of cancellation.
89        match self.cancelled.binary_search(&member) {
90            Ok(_) => return err!(MultisigError::AlreadyCancelled),
91            Err(pos) => self.cancelled.insert(pos, member),
92        };
93
94        // If current number of cancellations reaches threshold, mark the transaction as `Cancelled`.
95        if self.cancelled.len() >= threshold {
96            self.status = ProposalStatus::Cancelled {
97                timestamp: Clock::get()?.unix_timestamp,
98            };
99        }
100
101        Ok(())
102    }
103
104    /// Check if the member approved the transaction.
105    /// Returns `Some(index)` if `member` has approved the transaction, with `index` into the `approved` vec.
106    fn has_voted_approve(&self, member: Pubkey) -> Option<usize> {
107        self.approved.binary_search(&member).ok()
108    }
109
110    /// Check if the member rejected the transaction.
111    /// Returns `Some(index)` if `member` has rejected the transaction, with `index` into the `rejected` vec.
112    fn has_voted_reject(&self, member: Pubkey) -> Option<usize> {
113        self.rejected.binary_search(&member).ok()
114    }
115
116    /// Delete the vote of rejection at the `index`.
117    fn remove_rejection_vote(&mut self, index: usize) {
118        self.rejected.remove(index);
119    }
120
121    /// Delete the vote of approval at the `index`.
122    fn remove_approval_vote(&mut self, index: usize) {
123        self.approved.remove(index);
124    }
125}
126
127/// The status of a proposal.
128/// Each variant wraps a timestamp of when the status was set.
129#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
130#[non_exhaustive]
131pub enum ProposalStatus {
132    /// Proposal is in the draft mode and can be voted on.
133    Draft { timestamp: i64 },
134    /// Proposal is live and ready for voting.
135    Active { timestamp: i64 },
136    /// Proposal has been rejected.
137    Rejected { timestamp: i64 },
138    /// Proposal has been approved and is pending execution.
139    Approved { timestamp: i64 },
140    /// Proposal is being executed. This is a transient state that always transitions to `Executed` in the span of a single transaction.
141    #[deprecated(
142        note = "This status used to be used to prevent reentrancy attacks. It is no longer needed."
143    )]
144    Executing,
145    /// Proposal has been executed.
146    Executed { timestamp: i64 },
147    /// Proposal has been cancelled.
148    Cancelled { timestamp: i64 },
149}