Parcourir la source

examples: Add multisig (#56)

Armani Ferrante il y a 4 ans
Parent
commit
bba2771962

+ 1 - 0
.travis.yml

@@ -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

+ 2 - 0
examples/multisig/Anchor.toml

@@ -0,0 +1,2 @@
+cluster = "devnet"
+wallet = "~/.config/solana/id.json"

+ 4 - 0
examples/multisig/Cargo.toml

@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "programs/*"
+]

+ 13 - 0
examples/multisig/migrations/deploy.js

@@ -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.
+}

+ 18 - 0
examples/multisig/programs/multisig/Cargo.toml

@@ -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"] }

+ 2 - 0
examples/multisig/programs/multisig/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 229 - 0
examples/multisig/programs/multisig/src/lib.rs

@@ -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,
+}

+ 134 - 0
examples/multisig/tests/multisig.js

@@ -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);
+  });
+});