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 135 136 137 138 139 140 141 142 143 144 145 146 147 148
use anchor_lang::prelude::*;
use crate::errors::*;
/// Stores the data required for tracking the status of a multisig proposal.
/// Each `Proposal` has a 1:1 association with a transaction account, e.g. a `VaultTransaction` or a `ConfigTransaction`;
/// the latter can be executed only after the `Proposal` has been approved and its time lock is released.
#[account]
pub struct Proposal {
/// The multisig this belongs to.
pub multisig: Pubkey,
/// Index of the multisig transaction this proposal is associated with.
pub transaction_index: u64,
/// The status of the transaction.
pub status: ProposalStatus,
/// PDA bump.
pub bump: u8,
/// Keys that have approved/signed.
pub approved: Vec<Pubkey>,
/// Keys that have rejected.
pub rejected: Vec<Pubkey>,
/// Keys that have cancelled (Approved only).
pub cancelled: Vec<Pubkey>,
}
impl Proposal {
pub fn size(members_len: usize) -> usize {
8 + // anchor account discriminator
32 + // multisig
8 + // index
1 + // status enum variant
8 + // status enum wrapped timestamp (i64)
1 + // bump
(4 + (members_len * 32)) + // approved vec
(4 + (members_len * 32)) + // rejected vec
(4 + (members_len * 32)) // cancelled vec
}
/// Register an approval vote.
pub fn approve(&mut self, member: Pubkey, threshold: usize) -> Result<()> {
// If `member` has previously voted to reject, remove that vote.
if let Some(vote_index) = self.has_voted_reject(member.key()) {
self.remove_rejection_vote(vote_index);
}
// Insert the vote of approval.
match self.approved.binary_search(&member) {
Ok(_) => return err!(MultisigError::AlreadyApproved),
Err(pos) => self.approved.insert(pos, member),
};
// If current number of approvals reaches threshold, mark the transaction as `Approved`.
if self.approved.len() >= threshold {
self.status = ProposalStatus::Approved {
timestamp: Clock::get()?.unix_timestamp,
};
}
Ok(())
}
/// Register a rejection vote.
pub fn reject(&mut self, member: Pubkey, cutoff: usize) -> Result<()> {
// If `member` has previously voted to approve, remove that vote.
if let Some(vote_index) = self.has_voted_approve(member.key()) {
self.remove_approval_vote(vote_index);
}
// Insert the vote of rejection.
match self.rejected.binary_search(&member) {
Ok(_) => return err!(MultisigError::AlreadyRejected),
Err(pos) => self.rejected.insert(pos, member),
};
// If current number of rejections reaches cutoff, mark the transaction as `Rejected`.
if self.rejected.len() >= cutoff {
self.status = ProposalStatus::Rejected {
timestamp: Clock::get()?.unix_timestamp,
};
}
Ok(())
}
/// Registers a cancellation vote.
pub fn cancel(&mut self, member: Pubkey, threshold: usize) -> Result<()> {
// Insert the vote of cancellation.
match self.cancelled.binary_search(&member) {
Ok(_) => return err!(MultisigError::AlreadyCancelled),
Err(pos) => self.cancelled.insert(pos, member),
};
// If current number of cancellations reaches threshold, mark the transaction as `Cancelled`.
if self.cancelled.len() >= threshold {
self.status = ProposalStatus::Cancelled {
timestamp: Clock::get()?.unix_timestamp,
};
}
Ok(())
}
/// Check if the member approved the transaction.
/// Returns `Some(index)` if `member` has approved the transaction, with `index` into the `approved` vec.
fn has_voted_approve(&self, member: Pubkey) -> Option<usize> {
self.approved.binary_search(&member).ok()
}
/// Check if the member rejected the transaction.
/// Returns `Some(index)` if `member` has rejected the transaction, with `index` into the `rejected` vec.
fn has_voted_reject(&self, member: Pubkey) -> Option<usize> {
self.rejected.binary_search(&member).ok()
}
/// Delete the vote of rejection at the `index`.
fn remove_rejection_vote(&mut self, index: usize) {
self.rejected.remove(index);
}
/// Delete the vote of approval at the `index`.
fn remove_approval_vote(&mut self, index: usize) {
self.approved.remove(index);
}
}
/// The status of a proposal.
/// Each variant wraps a timestamp of when the status was set.
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum ProposalStatus {
/// Proposal is in the draft mode and can be voted on.
Draft { timestamp: i64 },
/// Proposal is live and ready for voting.
Active { timestamp: i64 },
/// Proposal has been rejected.
Rejected { timestamp: i64 },
/// Proposal has been approved and is pending execution.
Approved { timestamp: i64 },
/// Proposal is being executed. This is a transient state that always transitions to `Executed` in the span of a single transaction.
#[deprecated(
note = "This status used to be used to prevent reentrancy attacks. It is no longer needed."
)]
Executing,
/// Proposal has been executed.
Executed { timestamp: i64 },
/// Proposal has been cancelled.
Cancelled { timestamp: i64 },
}