examples: Add multisig (#56)
This commit is contained in:
parent
809f6aa2fe
commit
bba2771962
|
@ -48,6 +48,7 @@ jobs:
|
|||
- pushd examples/composite && anchor test && popd
|
||||
- pushd examples/errors && anchor test && popd
|
||||
- pushd examples/spl/token-proxy && anchor test && popd
|
||||
- pushd examples/multisig && anchor test && popd
|
||||
- pushd examples/tutorial/basic-0 && anchor test && popd
|
||||
- pushd examples/tutorial/basic-1 && anchor test && popd
|
||||
- pushd examples/tutorial/basic-2 && anchor test && popd
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
cluster = "devnet"
|
||||
wallet = "~/.config/solana/id.json"
|
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
// Migrations are an early feature. Currently, they're nothing more than this
|
||||
// single deploy script that's invoked from the CLI, injecting a provider
|
||||
// configured from the workspace's Anchor.toml.
|
||||
|
||||
const anchor = require("@project-serum/anchor");
|
||||
|
||||
module.exports = async function (provider) {
|
||||
// Configure client to use the provider.
|
||||
anchor.setProvider(provider);
|
||||
|
||||
// Add your deploy script here.
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "multisig"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "multisig"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,229 @@
|
|||
//! An example of a multisig to execute arbitrary Solana transactions.
|
||||
|
||||
#![feature(proc_macro_hygiene)]
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program;
|
||||
use anchor_lang::solana_program::instruction::Instruction;
|
||||
use std::convert::Into;
|
||||
|
||||
#[program]
|
||||
pub mod multisig {
|
||||
use super::*;
|
||||
|
||||
pub fn create_multisig(
|
||||
ctx: Context<CreateMultisig>,
|
||||
owners: Vec<Pubkey>,
|
||||
threshold: u64,
|
||||
nonce: u8,
|
||||
) -> Result<()> {
|
||||
let multisig = &mut ctx.accounts.multisig;
|
||||
multisig.owners = owners;
|
||||
multisig.threshold = threshold;
|
||||
multisig.nonce = nonce;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_transaction(
|
||||
ctx: Context<CreateTransaction>,
|
||||
pid: Pubkey,
|
||||
accs: Vec<TransactionAccount>,
|
||||
data: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let owner_index = ctx
|
||||
.accounts
|
||||
.multisig
|
||||
.owners
|
||||
.iter()
|
||||
.position(|a| a == ctx.accounts.proposer.key)
|
||||
.ok_or(ErrorCode::InvalidOwner)?;
|
||||
|
||||
let mut signers = Vec::new();
|
||||
signers.resize(ctx.accounts.multisig.owners.len(), false);
|
||||
signers[owner_index] = true;
|
||||
|
||||
let tx = &mut ctx.accounts.transaction;
|
||||
tx.program_id = pid;
|
||||
tx.accounts = accs;
|
||||
tx.data = data;
|
||||
tx.signers = signers;
|
||||
tx.multisig = *ctx.accounts.multisig.to_account_info().key;
|
||||
tx.did_execute = false;
|
||||
|
||||
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;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Sets the owners field on the multisig. The only way this can be invoked
|
||||
// is via a recursive call from execute_transaction -> set_owners.
|
||||
pub fn set_owners(ctx: Context<Auth>, owners: Vec<Pubkey>) -> Result<()> {
|
||||
let multisig = &mut ctx.accounts.multisig;
|
||||
|
||||
if owners.len() as u64 > multisig.threshold {
|
||||
multisig.threshold = owners.len() as u64;
|
||||
}
|
||||
|
||||
multisig.owners = owners;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn change_threshold(ctx: Context<Auth>, threshold: u64) -> Result<()> {
|
||||
let multisig = &mut ctx.accounts.multisig;
|
||||
multisig.threshold = threshold;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn execute_transaction(ctx: Context<ExecuteTransaction>) -> Result<()> {
|
||||
// Check we have enough signers.
|
||||
let sig_count = ctx
|
||||
.accounts
|
||||
.transaction
|
||||
.signers
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
false => None,
|
||||
true => Some(true),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.len() as u64;
|
||||
if sig_count < ctx.accounts.multisig.threshold {
|
||||
return Err(ErrorCode::NotEnoughSigners.into());
|
||||
}
|
||||
|
||||
// Execute the multisig transaction.
|
||||
let ix: Instruction = ctx.accounts.transaction.account().into();
|
||||
let seeds = &[
|
||||
ctx.accounts.multisig.to_account_info().key.as_ref(),
|
||||
&[ctx.accounts.multisig.nonce],
|
||||
];
|
||||
let signer = &[&seeds[..]];
|
||||
let accounts = ctx.remaining_accounts;
|
||||
solana_program::program::invoke_signed(&ix, &accounts, signer)?;
|
||||
|
||||
// Burn the account to ensure one time use.
|
||||
ctx.accounts.transaction.did_execute = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CreateMultisig<'info> {
|
||||
#[account(init)]
|
||||
multisig: ProgramAccount<'info, Multisig>,
|
||||
rent: Sysvar<'info, Rent>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CreateTransaction<'info> {
|
||||
multisig: ProgramAccount<'info, Multisig>,
|
||||
#[account(init)]
|
||||
transaction: ProgramAccount<'info, Transaction>,
|
||||
#[account(signer)]
|
||||
proposer: AccountInfo<'info>,
|
||||
rent: Sysvar<'info, Rent>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Approve<'info> {
|
||||
multisig: ProgramAccount<'info, Multisig>,
|
||||
#[account(mut, belongs_to = multisig)]
|
||||
transaction: ProgramAccount<'info, Transaction>,
|
||||
// One of the multisig owners.
|
||||
#[account(signer)]
|
||||
owner: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Auth<'info> {
|
||||
#[account(mut)]
|
||||
multisig: ProgramAccount<'info, Multisig>,
|
||||
#[account(signer, seeds = [
|
||||
multisig.to_account_info().key.as_ref(),
|
||||
&[multisig.nonce],
|
||||
])]
|
||||
multisig_signer: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct ExecuteTransaction<'info> {
|
||||
multisig: ProgramAccount<'info, Multisig>,
|
||||
#[account(seeds = [
|
||||
multisig.to_account_info().key.as_ref(),
|
||||
&[multisig.nonce],
|
||||
])]
|
||||
multisig_signer: AccountInfo<'info>,
|
||||
#[account(mut, belongs_to = multisig)]
|
||||
transaction: ProgramAccount<'info, Transaction>,
|
||||
}
|
||||
|
||||
#[account]
|
||||
pub struct Multisig {
|
||||
owners: Vec<Pubkey>,
|
||||
threshold: u64,
|
||||
nonce: u8,
|
||||
}
|
||||
|
||||
#[account]
|
||||
pub struct Transaction {
|
||||
// Target program to execute against.
|
||||
program_id: Pubkey,
|
||||
// Accounts requried for the transaction.
|
||||
accounts: Vec<TransactionAccount>,
|
||||
// Instruction data for the transaction.
|
||||
data: Vec<u8>,
|
||||
// signers[index] is true iff multisig.owners[index] signed the transaction.
|
||||
signers: Vec<bool>,
|
||||
// The multisig account this transaction belongs to.
|
||||
multisig: Pubkey,
|
||||
// Boolean ensuring one time execution.
|
||||
did_execute: bool,
|
||||
}
|
||||
|
||||
impl From<&Transaction> for Instruction {
|
||||
fn from(tx: &Transaction) -> Instruction {
|
||||
Instruction {
|
||||
program_id: tx.program_id,
|
||||
accounts: tx.accounts.clone().into_iter().map(Into::into).collect(),
|
||||
data: tx.data.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
pub struct TransactionAccount {
|
||||
pubkey: Pubkey,
|
||||
is_signer: bool,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[error]
|
||||
pub enum ErrorCode {
|
||||
#[msg("The given owner is not part of this multisig.")]
|
||||
InvalidOwner,
|
||||
#[msg("Not enough owners signed this transaction.")]
|
||||
NotEnoughSigners,
|
||||
Unknown,
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
const anchor = require("@project-serum/anchor");
|
||||
const assert = require("assert");
|
||||
|
||||
describe("multisig", () => {
|
||||
// Configure the client to use the local cluster.
|
||||
anchor.setProvider(anchor.Provider.env());
|
||||
|
||||
const program = anchor.workspace.Multisig;
|
||||
|
||||
it("Is initialized!", async () => {
|
||||
const multisig = new anchor.web3.Account();
|
||||
const [
|
||||
multisigSigner,
|
||||
nonce,
|
||||
] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[multisig.publicKey.toBuffer()],
|
||||
program.programId
|
||||
);
|
||||
const multisigSize = 200; // Big enough.
|
||||
|
||||
const ownerA = new anchor.web3.Account();
|
||||
const ownerB = new anchor.web3.Account();
|
||||
const ownerC = new anchor.web3.Account();
|
||||
const owners = [ownerA.publicKey, ownerB.publicKey, ownerC.publicKey];
|
||||
|
||||
const threshold = new anchor.BN(2);
|
||||
await program.rpc.createMultisig(owners, threshold, nonce, {
|
||||
accounts: {
|
||||
multisig: multisig.publicKey,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
},
|
||||
instructions: [
|
||||
await program.account.multisig.createInstruction(
|
||||
multisig,
|
||||
multisigSize
|
||||
),
|
||||
],
|
||||
signers: [multisig],
|
||||
});
|
||||
|
||||
let multisigAccount = await program.account.multisig(multisig.publicKey);
|
||||
|
||||
assert.equal(multisigAccount.nonce, nonce);
|
||||
assert.ok(multisigAccount.threshold.eq(new anchor.BN(2)));
|
||||
assert.deepEqual(multisigAccount.owners, owners);
|
||||
|
||||
const pid = program.programId;
|
||||
const accounts = [
|
||||
{
|
||||
pubkey: multisig.publicKey,
|
||||
isWritable: true,
|
||||
isSigner: false,
|
||||
},
|
||||
{
|
||||
pubkey: multisigSigner,
|
||||
isWritable: false,
|
||||
isSigner: true,
|
||||
},
|
||||
];
|
||||
const newOwners = [ownerA.publicKey, ownerB.publicKey];
|
||||
const data = program.coder.instruction.encode({
|
||||
setOwners: {
|
||||
owners: newOwners,
|
||||
},
|
||||
});
|
||||
|
||||
const transaction = new anchor.web3.Account();
|
||||
const txSize = 1000; // Big enough, cuz I'm lazy.
|
||||
await program.rpc.createTransaction(pid, accounts, data, {
|
||||
accounts: {
|
||||
multisig: multisig.publicKey,
|
||||
transaction: transaction.publicKey,
|
||||
proposer: ownerA.publicKey,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
},
|
||||
instructions: [
|
||||
await program.account.transaction.createInstruction(
|
||||
transaction,
|
||||
txSize
|
||||
),
|
||||
],
|
||||
signers: [transaction, ownerA],
|
||||
});
|
||||
|
||||
const txAccount = await program.account.transaction(transaction.publicKey);
|
||||
|
||||
assert.ok(txAccount.programId.equals(pid));
|
||||
assert.deepEqual(txAccount.accounts, accounts);
|
||||
assert.deepEqual(txAccount.data, data);
|
||||
assert.ok(txAccount.multisig.equals(multisig.publicKey));
|
||||
assert.equal(txAccount.didExecute, false);
|
||||
|
||||
// Other owner approves transactoin.
|
||||
await program.rpc.approve({
|
||||
accounts: {
|
||||
multisig: multisig.publicKey,
|
||||
transaction: transaction.publicKey,
|
||||
owner: ownerB.publicKey,
|
||||
},
|
||||
signers: [ownerB],
|
||||
});
|
||||
|
||||
// Now that we've reached the threshold, send the transactoin.
|
||||
await program.rpc.executeTransaction({
|
||||
accounts: {
|
||||
multisig: multisig.publicKey,
|
||||
multisigSigner,
|
||||
transaction: transaction.publicKey,
|
||||
},
|
||||
remainingAccounts: program.instruction.setOwners
|
||||
.accounts({
|
||||
multisig: multisig.publicKey,
|
||||
multisigSigner,
|
||||
})
|
||||
// Change the signer status on the vendor signer since it's signed by the program, not the client.
|
||||
.map((meta) =>
|
||||
meta.pubkey.equals(multisigSigner)
|
||||
? { ...meta, isSigner: false }
|
||||
: meta
|
||||
)
|
||||
.concat({
|
||||
pubkey: program.programId,
|
||||
isWritable: false,
|
||||
isSigner: false,
|
||||
}),
|
||||
});
|
||||
|
||||
multisigAccount = await program.account.multisig(multisig.publicKey);
|
||||
|
||||
assert.equal(multisigAccount.nonce, nonce);
|
||||
assert.ok(multisigAccount.threshold.eq(new anchor.BN(2)));
|
||||
assert.deepEqual(multisigAccount.owners, newOwners);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue