Skip to content

Commit

Permalink
fix(cancellation_realloc)
Browse files Browse the repository at this point in the history
* Cancelation realloc and current member state check/retain

* added comment about realloc

* move new vote logic to a v2 ix to preserve backwards compatibility

* new proposal cancel instruction (v2)

* new account context labeled as ProposalCancel specifically

* add the retain old member keys to existing cancel logic
  • Loading branch information
ogmedia authored Aug 12, 2024
1 parent 8264ed0 commit e801095
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 0 deletions.
78 changes: 78 additions & 0 deletions programs/squads_multisig_program/src/instructions/proposal_vote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,33 @@ pub struct ProposalVote<'info> {
pub proposal: Account<'info, Proposal>,
}

#[derive(Accounts)]
pub struct ProposalCancel<'info> {
#[account(
seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()],
bump = multisig.bump,
)]
pub multisig: Account<'info, Multisig>,

#[account(mut)]
pub member: Signer<'info>,

#[account(
mut,
seeds = [
SEED_PREFIX,
multisig.key().as_ref(),
SEED_TRANSACTION,
&proposal.transaction_index.to_le_bytes(),
SEED_PROPOSAL,
],
bump = proposal.bump,
)]
pub proposal: Account<'info, Proposal>,

pub system_program: Program<'info, System>,
}

impl ProposalVote<'_> {
fn validate(&self, vote: Vote) -> Result<()> {
let Self {
Expand Down Expand Up @@ -113,8 +140,59 @@ impl ProposalVote<'_> {
let proposal = &mut ctx.accounts.proposal;
let member = &mut ctx.accounts.member;

proposal.cancelled.retain(|k| multisig.is_member(*k).is_some());

proposal.cancel(member.key(), usize::from(multisig.threshold))?;

Ok(())
}
}

impl ProposalCancel<'_> {
fn validate(&self) -> Result<()> {
let Self {
multisig,
proposal,
member,
..
} = self;

// member
require!(
multisig.is_member(member.key()).is_some(),
MultisigError::NotAMember
);
require!(
multisig.member_has_permission(member.key(), Permission::Vote),
MultisigError::Unauthorized
);


require!(
matches!(proposal.status, ProposalStatus::Approved { .. }),
MultisigError::InvalidProposalStatus
);
// CAN cancel a stale proposal.

Ok(())
}

/// Cancel a multisig proposal on behalf of the `member`.
/// The proposal must be `Approved`.
#[access_control(ctx.accounts.validate())]
pub fn proposal_cancel(ctx: Context<Self>, _args: ProposalVoteArgs) -> Result<()> {
let multisig = &mut ctx.accounts.multisig;
let proposal = &mut ctx.accounts.proposal;
let member = &mut ctx.accounts.member;
let system_program = &ctx.accounts.system_program;

// ensure that the cancel array contains no keys that are not currently members
proposal.cancelled.retain(|k| multisig.is_member(*k).is_some());

proposal.cancel(member.key(), usize::from(multisig.threshold))?;

// reallocate the proposal size if needed
Proposal::realloc_if_needed(proposal.to_account_info(), multisig.members.len(), Some(member.to_account_info()), Some(system_program.to_account_info()))?;
Ok(())
}
}
Expand Down
11 changes: 11 additions & 0 deletions programs/squads_multisig_program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,17 @@ pub mod squads_multisig_program {
ProposalVote::proposal_cancel(ctx, args)
}

/// Cancel a multisig proposal on behalf of the `member`.
/// The proposal must be `Approved`.
/// This was introduced to incorporate proper state update, as old multisig members
/// may have lingering votes, and the proposal size may need to be reallocated to
/// accommodate the new amount of cancel votes.
/// The previous implemenation still works if the proposal size is in line with the
/// thresholdhold size.
pub fn proposal_cancel_v2(ctx: Context<ProposalCancel>, args: ProposalVoteArgs) -> Result<()> {
ProposalCancel::proposal_cancel(ctx, args)
}

/// Use a spending limit to transfer tokens from a multisig vault to a destination account.
pub fn spending_limit_use(
ctx: Context<SpendingLimitUse>,
Expand Down
56 changes: 56 additions & 0 deletions programs/squads_multisig_program/src/state/proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
use anchor_lang::prelude::*;

use crate::errors::*;
use crate::id;

use anchor_lang::system_program;

/// 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`;
Expand Down Expand Up @@ -122,6 +125,59 @@ impl Proposal {
fn remove_approval_vote(&mut self, index: usize) {
self.approved.remove(index);
}

/// Check if the proposal account space needs to be reallocated to accommodate `cancelled` vec.
/// Proposal size is crated at creation, and thus may not accomodate enough space for all members to cancel if more are added or changed
/// Returns `true` if the account was reallocated.
pub fn realloc_if_needed<'a>(
proposal: AccountInfo<'a>,
members_length: usize,
rent_payer: Option<AccountInfo<'a>>,
system_program: Option<AccountInfo<'a>>,
) -> Result<bool> {
// Sanity checks
require_keys_eq!(*proposal.owner, id(), MultisigError::IllegalAccountOwner);

let current_account_size = proposal.data.borrow().len();
let account_size_to_fit_members = Proposal::size(members_length);

// Check if we need to reallocate space.
if current_account_size >= account_size_to_fit_members {
return Ok(false);
}

// Reallocate more space.
AccountInfo::realloc(&proposal, account_size_to_fit_members, false)?;

// If more lamports are needed, transfer them to the account.
let rent_exempt_lamports = Rent::get().unwrap().minimum_balance(account_size_to_fit_members).max(1);
let top_up_lamports =
rent_exempt_lamports.saturating_sub(proposal.to_account_info().lamports());

if top_up_lamports > 0 {
let system_program = system_program.ok_or(MultisigError::MissingAccount)?;
require_keys_eq!(
*system_program.key,
system_program::ID,
MultisigError::InvalidAccount
);

let rent_payer = rent_payer.ok_or(MultisigError::MissingAccount)?;

system_program::transfer(
CpiContext::new(
system_program,
system_program::Transfer {
from: rent_payer,
to: proposal,
},
),
top_up_lamports,
)?;
}

Ok(true)
}
}

/// The status of a proposal.
Expand Down

0 comments on commit e801095

Please sign in to comment.