Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancelation realloc and current member state check/retain #104

Merged
merged 6 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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<()> {
ogmedia marked this conversation as resolved.
Show resolved Hide resolved
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
Loading