🧱 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(())