🧱 Building Blocks
Introduction
It is very easy to make any contract algoz-compatible. We've created these snippets to help you get started.
Initiate
Copy and paste the following code into your rust contract for the multisig creation and proposal approving logic.
pub fn create_multisig( ctx: Context<CreateMultisig>, owners: Vec<Pubkey>, threshold: u64, nonce: u8, ) -> Result<()> { if owners.len() < 2 { return Err(Error::new(ErrorKind::InvalidInput, "Must have at least two owners")); } if owners.len() > 2 { return Err(Error::new(ErrorKind::InvalidInput, "Must have at least two owners")); } let multisig = &mut ctx.accounts.multisig; multisig.owners = owners multisig.threshold = 2; multisig.nonce = nonce; multisig.owner_set_seqno = 0; Ok(()) } pub fn approve(ctx: Context<Approve>) -> Result<()> { let owner_index = ctx .accounts .multisig .owners .iter() .position(|a| a == ctx.accounts.owner.key) .ok_or(ErrorCode::InvalidOwner)?; ctx.accounts.transaction.signers[owner_index] = true; let sig_count = ctx .accounts .transaction .signers .iter() .filter(|&did_sign| *did_sign) .count() as u64; if sig_count < ctx.accounts.multisig.threshold { return Ok(()); } if ctx.accounts.transaction.did_execute { return Err(ErrorCode::AlreadyExecuted.into()); } let mut ixs: Vec<Instruction> = (&*ctx.accounts.transaction).into(); for ix in ixs.iter_mut() { ix.accounts = ix .accounts .iter() .map(|acc| { let mut acc = acc.clone(); if &acc.pubkey == ctx.accounts.multisig_signer.key { acc.is_signer = true; } acc }) .collect(); } let seeds = &[ ctx.accounts.multisig.to_account_info().key.as_ref(), &[ctx.accounts.multisig.nonce], ]; let signer = &[&seeds[..]]; let accounts = ctx.remaining_accounts; for ix in ixs.iter() { solana_program::program::invoke_signed(ix, &accounts, signer)?; } ctx.accounts.transaction.did_execute = true; Ok(()) }
Types
These are the types you need to add to your contract.
pub struct Auth<'info> { #[account(mut)] multisig: ProgramAccount<'info, Multisig>, #[account( signer, seeds = [multisig.to_account_info().key.as_ref()], bump = multisig.nonce, )] multisig_signer: AccountInfo<'info>,}pub struct CreateMultisig<'info> { #[account(zero)] multisig: ProgramAccount<'info, Multisig>, rent: Sysvar<'info, Rent>,}pub struct CreateTransaction<'info> { multisig: ProgramAccount<'info, Multisig>, #[account(zero)] transaction: ProgramAccount<'info, Transaction>, // One of the owners. Checked in the handler. #[account(signer)] proposer: AccountInfo<'info>, rent: Sysvar<'info, Rent>,}pub struct Approve<'info> { #[account(constraint = multisig.owner_set_seqno == transaction.owner_set_seqno)] multisig: ProgramAccount<'info, Multisig>, #[account( seeds = [multisig.to_account_info().key.as_ref()], bump = multisig.nonce, )] multisig_signer: AccountInfo<'info>, #[account(mut, has_one = multisig)] transaction: ProgramAccount<'info, Transaction>, // One of the multisig owners. Checked in the handler. #[account(signer)] owner: AccountInfo<'info>,}pub struct Multisig { pub owners: Vec<Pubkey>, pub threshold: u64, pub nonce: u8, pub owner_set_seqno: u32,}pub struct Transaction { // The multisig account this transaction belongs to. pub multisig: Pubkey, // Target program to execute against. pub program_ids: Vec<Pubkey>, // Accounts requried for the transaction. pub accounts: Vec<Vec<TransactionAccount>>, // Instruction datas for the transaction. pub datas: Vec<Vec<u8>>, // signers[index] is true if multisig.owners[index] signed the transaction. pub signers: Vec<bool>, // Boolean ensuring one time execution. pub did_execute: bool, // Owner set sequence number. pub owner_set_seqno: u32,}impl From<&Transaction> for Vec<Instruction> { fn from(tx: &Transaction) -> Vec<Instruction> { let mut instructions: Vec<Instruction> = Vec::new(); for (i, _pid) in tx.program_ids.iter().enumerate() { instructions.push(Instruction { program_id: tx.program_ids[i], accounts: tx.accounts[i].iter().map(AccountMeta::from).collect(), data: tx.datas[i].clone(), }) } instructions }}pub struct TransactionAccount { pub pubkey: Pubkey, pub is_signer: bool, pub is_writable: bool,}impl From<&TransactionAccount> for AccountMeta { fn from(account: &TransactionAccount) -> AccountMeta { match account.is_writable { false => AccountMeta::new_readonly(account.pubkey, account.is_signer), true => AccountMeta::new(account.pubkey, account.is_signer), } }}impl From<&AccountMeta> for TransactionAccount { fn from(account_meta: &AccountMeta) -> TransactionAccount { TransactionAccount { pubkey: account_meta.pubkey, is_signer: account_meta.is_signer, is_writable: account_meta.is_writable, } }}pub enum ErrorCode { #[msg("The given owner is not part of this multisig.")] InvalidOwner, #[msg("The given transaction has already been executed.")] AlreadyExecuted, #[msg("program id account data must have same length")] ParamLength,}
Creating proposal
Adapt the following code snippet in any of the functions to enable multisig functionality. Any function which uses this logic will automatically be protected from bots.
if pids.len() != accs.len() || pids.len() != datas.len() { return Err(ErrorCode::ParamLength.into()); } let _ = ctx .accounts .multisig .owners .iter() .position(|a| a == ctx.accounts.proposer.key) .ok_or(ErrorCode::InvalidOwner)?; let mut signers = Vec::new(); signers.resize(2, false); signers[0] = true; let tx = &mut ctx.accounts.transaction; tx.program_ids = pids; tx.accounts = accs; tx.datas = datas; tx.signers = signers; tx.multisig = *ctx.accounts.multisig.to_account_info().key; tx.did_execute = false; tx.owner_set_seqno = ctx.accounts.multisig.owner_set_seqno; Ok(())