use std::collections::{HashMap, HashSet};
use std::ops::DerefMut;
use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::{invoke, invoke_signed};
use anchor_lang::solana_program::system_instruction;
#[cfg(not(feature = "localnet"))]
declare_id!("Ecycmji8eeggXrA3rD2cdEHpHDnP4btvVfcyTBS9cG9t");
#[cfg(feature = "localnet")]
declare_id!("AeAQKcvUbG6LmunEAiL2Vim5dN2uL5TNwJfgsGdyroQ3");
#[error_code]
pub enum Error {
#[msg("Multisig account is empty. Please create transactions")]
AccountEmpty,
#[msg("Multisig transaction queue is full. Please approve those.")]
AccountFull,
#[msg("Multisig account is locked. Please approve the transactions")]
AccountLocked,
#[msg("Missing transfer recipient AccountInfo")]
MissingRecipientAccountInfo,
#[msg("Fund account is not writable")]
FundAccountNotWritable,
#[msg("Fund account data is not empty")]
FundAccountIsNotEmpty,
#[msg("Invalid fund account")]
InvalidFundAddress,
#[msg("Invalid fund bump seed")]
InvalidFundBumpSeed,
#[msg("No signers provided")]
NoSigners,
#[msg("Too many signers provided")]
TooManySigners,
#[msg("Threshold too high")]
ThresholdTooHigh,
#[msg("Invalid signer")]
InvalidSigner,
#[msg("There is not enough fund balance")]
NotEnoughFundBalance,
}
#[account]
#[derive(Debug)]
pub struct State {
pub m: u8,
pub signers: Vec<Pubkey>,
pub signed: Vec<bool>,
pub fund: Pubkey,
pub balance: u64,
pub q: u8,
pub queue: Vec<Pubkey>,
}
impl State {
const MIN_SIGNERS: u8 = 1;
const MAX_SIGNERS: u8 = u8::MAX;
const MIN_QUEUE: u8 = 1;
const MAX_QUEUE: u8 = u8::MAX;
fn space(signers: &[Pubkey], q: u8) -> usize {
let n = Self::valid_n(signers.len() as u8) as usize;
let q = Self::valid_q(q) as usize;
8 + 1 + 4 + 32 * n + 4 + n + 32 + 8 + 1 + 4 + 32 * q
}
fn valid_n(n: u8) -> u8 {
n.clamp(Self::MIN_SIGNERS, Self::MAX_SIGNERS)
}
fn valid_q(q: u8) -> u8 {
q.clamp(Self::MIN_QUEUE, Self::MAX_QUEUE)
}
fn is_queue_empty(&self) -> bool {
self.queue.is_empty()
}
fn is_queue_full(&self) -> bool {
self.queue.len() == self.q as usize
}
fn is_locked(&self) -> bool {
self.signed.iter().any(|signed| *signed)
}
#[allow(clippy::result_large_err)]
fn validate_queue(&self) -> Result<()> {
require!(!self.is_queue_full(), Error::AccountFull);
Ok(())
}
#[allow(clippy::result_large_err)]
fn validate_fund<'info>(
state: &Account<'info, Self>,
fund: &UncheckedAccount<'info>,
bump: u8,
) -> Result<()> {
if !fund.is_writable {
Err(Error::FundAccountNotWritable)?;
}
if !fund.data_is_empty() {
Err(Error::FundAccountIsNotEmpty)?;
}
let state_key = state.key();
let seed = [b"fund", state_key.as_ref(), &[bump]];
let pda = match Pubkey::create_program_address(&seed, &id()) {
Err(_e) => Err(Error::InvalidFundBumpSeed)?,
Ok(pda) => pda,
};
require_keys_eq!(pda, fund.key(), Error::InvalidFundAddress);
Ok(())
}
#[allow(clippy::result_large_err)]
fn create_fund_account<'info>(
state: &Account<'info, Self>,
fund: &UncheckedAccount<'info>,
funder: &Signer<'info>,
bump: u8,
) -> Result<()> {
let lamports = Rent::get()?.minimum_balance(0);
let ix = system_instruction::create_account(&funder.key(), &fund.key(), lamports, 0, &id());
let state_key = state.key();
let accounts = [funder.to_account_info(), fund.to_account_info()];
let seed = [b"fund", state_key.as_ref(), &[bump]];
invoke_signed(&ix, &accounts, &[&seed])?;
Ok(())
}
#[allow(clippy::result_large_err)]
fn transfer_fund(
_state: &Account<'_, Self>,
from: &AccountInfo<'_>,
to: &AccountInfo<'_>,
lamports: u64,
_bump: u8,
) -> Result<()> {
**from.try_borrow_mut_lamports()? -= lamports;
**to.try_borrow_mut_lamports()? += lamports;
Ok(())
}
}
#[account]
#[derive(Debug)]
pub struct Transfer {
pub creator: Pubkey,
pub recipient: Pubkey,
pub lamports: u64,
}
impl Transfer {
const SPACE: usize = 8 + 32 + 32 + 8;
}
#[derive(Accounts)]
#[instruction(m: u8, signers: Vec<Pubkey>, q: u8, state_bump: u8, fund_bump: u8)]
pub struct Create<'info> {
#[account(mut)]
pub funder: Signer<'info>,
#[account(
init,
payer = funder,
space = State::space(&signers, q),
seeds = [b"state", funder.key.as_ref()],
bump,
)]
pub state: Account<'info, State>,
#[account(mut, seeds = [b"fund", state.key().as_ref()], bump = fund_bump)]
pub fund: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(lamports: u64, state_bump: u8, fund_bump: u8)]
pub struct Fund<'info> {
#[account(mut)]
pub funder: Signer<'info>,
#[account(mut, seeds = [b"state", funder.key.as_ref()], bump = state_bump)]
pub state: Box<Account<'info, State>>,
#[account(mut, seeds = [b"fund", state.key().as_ref()], bump = fund_bump)]
pub fund: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(recipient: Pubkey, lamports: u64, fund_bump: u8)]
pub struct CreateTransfer<'info> {
#[account(mut)]
pub creator: Signer<'info>,
#[account(mut)]
pub state: Box<Account<'info, State>>,
#[account(mut, seeds = [b"fund", state.key().as_ref()], bump = fund_bump)]
pub fund: UncheckedAccount<'info>,
#[account(init, payer = creator, space = Transfer::SPACE)]
pub transfer: Box<Account<'info, Transfer>>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(fund_bump: u8)]
pub struct Approve<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(mut)]
pub state: Box<Account<'info, State>>,
#[account(mut, seeds = [b"fund", state.key().as_ref()], bump = fund_bump)]
pub fund: UncheckedAccount<'info>,
}
#[derive(Accounts)]
#[instruction(state_bump: u8, fund_bump: u8)]
pub struct Close<'info> {
#[account(mut)]
pub funder: Signer<'info>,
#[account(mut, close = funder, seeds = [b"state", funder.key.as_ref()], bump = state_bump)]
pub state: Box<Account<'info, State>>,
#[account(mut, seeds = [b"fund", state.key().as_ref()], bump = fund_bump)]
pub fund: UncheckedAccount<'info>,
}
#[program]
pub mod multisig_lite {
use super::*;
#[allow(clippy::result_large_err)]
pub fn create(
ctx: Context<Create>,
m: u8,
signers: Vec<Pubkey>,
q: u8,
_state_bump: u8,
fund_bump: u8,
) -> Result<()> {
let funder = &mut ctx.accounts.funder;
let state = &mut ctx.accounts.state;
let fund = &mut ctx.accounts.fund;
require_gte!(m, State::MIN_SIGNERS, Error::NoSigners);
State::validate_fund(state, fund, fund_bump)?;
let signers: HashSet<_> = signers.into_iter().collect();
require_gte!(signers.len(), State::MIN_SIGNERS as usize, Error::NoSigners);
require_gte!(
State::MAX_SIGNERS as usize,
signers.len(),
Error::TooManySigners
);
let threshold = m as usize;
require_gte!(signers.len(), threshold, Error::ThresholdTooHigh);
State::create_fund_account(state, fund, funder, fund_bump)?;
state.m = m;
state.signers = signers.into_iter().collect();
state.signed = vec![false; state.signers.len()];
state.fund = fund.key();
state.balance = 0;
state.q = State::valid_q(q);
Ok(())
}
#[allow(clippy::result_large_err)]
pub fn fund(ctx: Context<Fund>, lamports: u64, _state_bump: u8, fund_bump: u8) -> Result<()> {
let funder = &ctx.accounts.funder;
let state = &mut ctx.accounts.state;
let fund = &mut ctx.accounts.fund;
State::validate_fund(state, fund, fund_bump)?;
let ix = system_instruction::transfer(&funder.key(), &fund.key(), lamports);
let accounts = [funder.to_account_info(), fund.to_account_info()];
invoke(&ix, &accounts)?;
state.balance += lamports;
Ok(())
}
#[allow(clippy::result_large_err)]
pub fn create_transfer(
ctx: Context<CreateTransfer>,
recipient: Pubkey,
lamports: u64,
fund_bump: u8,
) -> Result<()> {
let creator = &ctx.accounts.creator;
let state = &mut ctx.accounts.state;
let fund = &mut ctx.accounts.fund;
let transfer = &mut ctx.accounts.transfer;
require!(!state.is_locked(), Error::AccountLocked);
State::validate_fund(state, fund, fund_bump)?;
let creator_key = creator.key();
let signers = &state.signers;
require!(signers.contains(&creator_key), Error::InvalidSigner);
state.validate_queue()?;
require_gte!(state.balance, lamports, Error::NotEnoughFundBalance);
let from = fund.to_account_info();
let to = creator.to_account_info();
let rent = transfer.to_account_info().lamports();
State::transfer_fund(state, &from, &to, rent, fund_bump)?;
transfer.creator = creator_key;
transfer.recipient = recipient;
transfer.lamports = lamports;
state.balance -= lamports;
state.queue.push(transfer.key());
Ok(())
}
#[allow(clippy::result_large_err)]
pub fn approve(ctx: Context<Approve>, fund_bump: u8) -> Result<()> {
let signer = &ctx.accounts.signer;
let state = &mut ctx.accounts.state;
let fund = &mut ctx.accounts.fund;
let remaining_accounts: HashMap<_, _> = ctx
.remaining_accounts
.iter()
.map(|account| (account.key, account))
.collect();
State::validate_fund(state, fund, fund_bump)?;
require!(!state.is_queue_empty(), Error::AccountEmpty);
let signer_key = signer.key();
let signers = &state.signers;
let signer_index = match signers.iter().position(|pubkey| *pubkey == signer_key) {
None => return Err(Error::InvalidSigner.into()),
Some(signer_index) => signer_index,
};
if !state.signed[signer_index] {
state.signed[signer_index] = true;
}
let signed = state.signed.iter().filter(|&signed| *signed).count() as u8;
if signed < state.m {
return Ok(());
}
let mut executable = Vec::new();
let mut remaining = Vec::new();
for transfer_addr in &state.queue {
let transfer_info = match remaining_accounts.get(transfer_addr) {
Some(transfer) => transfer,
None => {
remaining.push(*transfer_addr);
continue;
}
};
let mut ref_data = transfer_info.try_borrow_mut_data()?;
let mut transfer_data: &[u8] = ref_data.deref_mut();
let tx = Transfer::try_deserialize(&mut transfer_data)?;
let to = match remaining_accounts.get(&tx.recipient) {
None => return Err(Error::MissingRecipientAccountInfo.into()),
Some(recipient) => recipient,
};
executable.push((transfer_info, to, tx.lamports));
}
if executable.is_empty() {
return Ok(());
}
let fund = fund.to_account_info();
for (transfer, to, lamports) in executable {
State::transfer_fund(state, &fund, to, lamports, fund_bump)?;
let lamports = transfer.lamports();
State::transfer_fund(state, transfer, &fund, lamports, fund_bump)?;
}
state.queue = remaining;
if state.is_queue_empty() {
state.signed.iter_mut().for_each(|signed| *signed = false);
}
Ok(())
}
#[allow(clippy::result_large_err)]
pub fn close(ctx: Context<Close>, _state_bump: u8, fund_bump: u8) -> Result<()> {
let funder = &mut ctx.accounts.funder;
let state = &mut ctx.accounts.state;
let fund = &mut ctx.accounts.fund;
let remaining_accounts: HashMap<_, _> = ctx
.remaining_accounts
.iter()
.map(|account| (account.key, account))
.collect();
State::validate_fund(state, fund, fund_bump)?;
let to = fund.to_account_info();
for transfer_addr in &state.queue {
let from = match remaining_accounts.get(transfer_addr) {
Some(transfer) => transfer,
None => continue,
};
let lamports = from.lamports();
State::transfer_fund(state, from, &to, lamports, fund_bump)?;
}
let from = fund.to_account_info();
let to = funder.to_account_info();
let lamports = fund.lamports();
State::transfer_fund(state, &from, &to, lamports, fund_bump)?;
Ok(())
}
}