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}