diff --git a/Cargo.lock b/Cargo.lock index 3e354ea..9a87ef7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -599,6 +599,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" + [[package]] name = "hmac" version = "0.8.1" @@ -1211,6 +1217,7 @@ name = "squads-mpl" version = "0.1.1" dependencies = [ "anchor-lang", + "hex", ] [[package]] diff --git a/helpers/transactions.ts b/helpers/transactions.ts index cff3117..0f815a8 100644 --- a/helpers/transactions.ts +++ b/helpers/transactions.ts @@ -1,71 +1,82 @@ import * as anchor from '@project-serum/anchor'; -import { Program } from '@project-serum/anchor'; -import { Account } from '@solana/web3.js'; -import { SquadsMpl } from '../target/types/squads_mpl'; -import { ProgramManager } from '../target/types/program_manager'; +import {Program} from '@project-serum/anchor'; +import {Account} from '@solana/web3.js'; +import {SquadsMpl} from '../target/types/squads_mpl'; +import {ProgramManager} from '../target/types/program_manager'; // some TX/IX helper functions export const createTestTransferTransaction = async (authority: anchor.web3.PublicKey, recipient: anchor.web3.PublicKey, amount = 1000000) => { - return anchor.web3.SystemProgram.transfer( - { - fromPubkey: authority, - lamports: amount, - toPubkey: recipient - } - ); + return anchor.web3.SystemProgram.transfer( + { + fromPubkey: authority, + lamports: amount, + toPubkey: recipient + } + ); }; -export const createBlankTransaction = async (program: Program, feePayer: anchor.web3.PublicKey) =>{ - const {blockhash} = await program.provider.connection.getLatestBlockhash(); - const lastValidBlockHeight = await program.provider.connection.getBlockHeight(); +export const createBlankTransaction = async (program: Program, feePayer: anchor.web3.PublicKey) => { + const {blockhash} = await program.provider.connection.getLatestBlockhash(); + const lastValidBlockHeight = await program.provider.connection.getBlockHeight(); - return new anchor.web3.Transaction({ - blockhash, - lastValidBlockHeight, - feePayer - }); + return new anchor.web3.Transaction({ + blockhash, + lastValidBlockHeight, + feePayer + }); }; -export const createExecuteTransactionTx = async (program: Program, ms: anchor.web3.PublicKey, tx: anchor.web3.PublicKey, feePayer: anchor.web3.PublicKey) => { +export const createExecuteTransactionTx = async (program: Program, ms: anchor.web3.PublicKey, tx: anchor.web3.PublicKey, feePayer: anchor.web3.PublicKey) => { const txState = await program.account.msTransaction.fetch(tx); - const ixList = await Promise.all([...new Array(txState.instructionIndex)].map(async (a,i) => { - const ixIndexBN = new anchor.BN(i + 1,10); - const [ixKey] = await getIxPDA(tx, ixIndexBN, program.programId); - const ixAccount= await program.account.msInstruction.fetch(ixKey); - return {pubkey: ixKey, ixItem: ixAccount}; + const ixList = await Promise.all([...new Array(txState.instructionIndex)].map(async (a, i) => { + const ixIndexBN = new anchor.BN(i + 1, 10); + const [ixKey] = await getIxPDA(tx, ixIndexBN, program.programId); + const ixAccount = await program.account.msInstruction.fetch(ixKey); + return {pubkey: ixKey, ixItem: ixAccount}; })); - const ixKeysList= ixList.map(({pubkey, ixItem}, ixIndex) => { - const ixKeys: anchor.web3.AccountMeta[] = ixItem.keys as anchor.web3.AccountMeta[]; - - const formattedKeys = ixKeys.map((ixKey,keyInd) => { - return { - pubkey: ixKey.pubkey, - isSigner: false, - isWritable: ixKey.isWritable - }; - }); + const ixKeysList = ixList.map(({pubkey, ixItem}, ixIndex) => { + const ixKeys: anchor.web3.AccountMeta[] = ixItem.keys as anchor.web3.AccountMeta[]; + const sig = anchor.utils.sha256.hash("global:add_member"); + const ixDiscriminator = Buffer.from(sig, "hex"); + const data = Buffer.concat([ixDiscriminator.slice(0, 8)]); + const ixData = ixItem.data as any + + const formattedKeys = ixKeys.map((ixKey, keyInd) => { + if (ixData.includes(data) && keyInd === 2) { + return { + pubkey: feePayer, + isSigner: false, + isWritable: ixKey.isWritable + }; + } + return { + pubkey: ixKey.pubkey, + isSigner: false, + isWritable: ixKey.isWritable + }; + }); - return [ - {pubkey, isSigner: false, isWritable: false}, - {pubkey: ixItem.programId, isSigner: false, isWritable: false}, - ...formattedKeys - ]; - }).reduce((p,c) => p.concat(c),[]) + return [ + {pubkey, isSigner: false, isWritable: false}, + {pubkey: ixItem.programId, isSigner: false, isWritable: false}, + ...formattedKeys + ]; + }).reduce((p, c) => p.concat(c), []) // [ix ix_account, ix program_id, key1, key2 ...] - const keysUnique = ixKeysList.reduce((prev,curr) => { + const keysUnique = ixKeysList.reduce((prev, curr) => { const inList = prev.findIndex(a => a.pubkey.toBase58() === curr.pubkey.toBase58()); // if its already in the list, and has same write flag - if ( inList >= 0 && prev[inList].isWritable === curr.isWritable){ + if (inList >= 0 && prev[inList].isWritable === curr.isWritable) { return prev; - }else{ + } else { prev.push({pubkey: curr.pubkey, isWritable: curr.isWritable, isSigner: curr.isSigner}); return prev; } - },[]); + }, []); const keyIndexMap = ixKeysList.map(a => { return keysUnique.findIndex(k => { @@ -77,110 +88,110 @@ export const createExecuteTransactionTx = async (program: Program, m }); // console.log('ix key mapping', keyIndexMap); const keyIndexMapLengthBN = new anchor.BN(keyIndexMap.length, 10); - const keyIndexMapLengthBuffer = keyIndexMapLengthBN.toArrayLike(Buffer, "le",2); + const keyIndexMapLengthBuffer = keyIndexMapLengthBN.toArrayLike(Buffer, "le", 2); const keyIndexMapBuffer = Buffer.from(keyIndexMap); let executeKeys = [ - { - pubkey: ms, - isSigner: false, - isWritable: true - }, - { - pubkey: tx, - isSigner: false, - isWritable: true, - }, - { - pubkey: feePayer, - isSigner: true, - isWritable: true, - } - ]; + { + pubkey: ms, + isSigner: false, + isWritable: true + }, + { + pubkey: tx, + isSigner: false, + isWritable: true, + }, + { + pubkey: feePayer, + isSigner: true, + isWritable: true, + } + ]; // const keys = executeKeys.concat(ixKeysList); - const keys = executeKeys.concat(keysUnique); - const {blockhash} = await program.provider.connection.getLatestBlockhash(); - const lastValidBlockHeight = await program.provider.connection.getBlockHeight(); - - const executeTx = new anchor.web3.Transaction({ - blockhash, - lastValidBlockHeight, - feePayer - }); - - const sig = anchor.utils.sha256.hash("global:execute_transaction"); - const ixDiscriminator = Buffer.from(sig, "hex"); - - const data = Buffer.concat([ixDiscriminator.slice(0,16), keyIndexMapLengthBuffer, keyIndexMapBuffer]); - const executeIx = await program.methods.executeTransaction(Buffer.from(keyIndexMap)) - .accounts({multisig: ms, transaction: tx, member: feePayer}) - .instruction(); - executeIx.keys = executeIx.keys.concat(keysUnique); - executeTx.add(executeIx); - return executeTx; + const keys = executeKeys.concat(keysUnique); + const {blockhash} = await program.provider.connection.getLatestBlockhash(); + const lastValidBlockHeight = await program.provider.connection.getBlockHeight(); + + const executeTx = new anchor.web3.Transaction({ + blockhash, + lastValidBlockHeight, + feePayer + }); + + const sig = anchor.utils.sha256.hash("global:execute_transaction"); + const ixDiscriminator = Buffer.from(sig, "hex"); + + const data = Buffer.concat([ixDiscriminator.slice(0, 16), keyIndexMapLengthBuffer, keyIndexMapBuffer]); + const executeIx = await program.methods.executeTransaction(Buffer.from(keyIndexMap)) + .accounts({multisig: ms, transaction: tx, member: feePayer}) + .instruction(); + executeIx.keys = executeIx.keys.concat(keysUnique); + executeTx.add(executeIx); + return executeTx; }; // some PDA helper functions export const getMsPDA = (create_key: anchor.web3.PublicKey, programId: anchor.web3.PublicKey) => anchor.web3.PublicKey.findProgramAddressSync([ - anchor.utils.bytes.utf8.encode("squad"), - create_key.toBuffer(), - anchor.utils.bytes.utf8.encode("multisig") + anchor.utils.bytes.utf8.encode("squad"), + create_key.toBuffer(), + anchor.utils.bytes.utf8.encode("multisig") ], programId); export const getTxPDA = async (msPDA: anchor.web3.PublicKey, txIndexBN: anchor.BN, programId: anchor.web3.PublicKey) => await anchor.web3.PublicKey.findProgramAddress([ - anchor.utils.bytes.utf8.encode("squad"), - msPDA.toBuffer(), - txIndexBN.toBuffer("le",4), - anchor.utils.bytes.utf8.encode("transaction") + anchor.utils.bytes.utf8.encode("squad"), + msPDA.toBuffer(), + txIndexBN.toBuffer("le", 4), + anchor.utils.bytes.utf8.encode("transaction") ], programId); -export const getIxPDA = async(txPDA: anchor.web3.PublicKey, iXIndexBN: anchor.BN, programId: anchor.web3.PublicKey) => await anchor.web3.PublicKey.findProgramAddress([ - anchor.utils.bytes.utf8.encode("squad"), - txPDA.toBuffer(), - iXIndexBN.toBuffer("le",1), // note instruction index is an u8 (1 byte) - anchor.utils.bytes.utf8.encode("instruction") +export const getIxPDA = async (txPDA: anchor.web3.PublicKey, iXIndexBN: anchor.BN, programId: anchor.web3.PublicKey) => await anchor.web3.PublicKey.findProgramAddress([ + anchor.utils.bytes.utf8.encode("squad"), + txPDA.toBuffer(), + iXIndexBN.toBuffer("le", 1), // note instruction index is an u8 (1 byte) + anchor.utils.bytes.utf8.encode("instruction") ], programId); export const getAuthorityPDA = async (msPDA: anchor.web3.PublicKey, authorityIndexBN: anchor.BN, programId: anchor.web3.PublicKey) => await anchor.web3.PublicKey.findProgramAddress([ - anchor.utils.bytes.utf8.encode("squad"), - msPDA.toBuffer(), - authorityIndexBN.toBuffer("le",4), // note authority index is an u32 (4 byte) - anchor.utils.bytes.utf8.encode("authority") + anchor.utils.bytes.utf8.encode("squad"), + msPDA.toBuffer(), + authorityIndexBN.toBuffer("le", 4), // note authority index is an u32 (4 byte) + anchor.utils.bytes.utf8.encode("authority") ], programId); // basic helpers -export const getNextTxIndex = async (program: Program, msAddress: anchor.web3.PublicKey) => { - const msState = await program.account.ms.fetch(msAddress); - return msState.transactionIndex + 1; +export const getNextTxIndex = async (program: Program, msAddress: anchor.web3.PublicKey) => { + const msState = await program.account.ms.fetch(msAddress); + return msState.transactionIndex + 1; }; // program manager helpers export const getProgramManagerPDA = (msPDA: anchor.web3.PublicKey, programId: anchor.web3.PublicKey) => anchor.web3.PublicKey.findProgramAddressSync([ - anchor.utils.bytes.utf8.encode("squad"), - msPDA.toBuffer(), - anchor.utils.bytes.utf8.encode("pmanage") + anchor.utils.bytes.utf8.encode("squad"), + msPDA.toBuffer(), + anchor.utils.bytes.utf8.encode("pmanage") ], programId); export const getManagedProgramPDA = async (programManagerPDA: anchor.web3.PublicKey, managedProgramIndexBN: anchor.BN, programId: anchor.web3.PublicKey) => await anchor.web3.PublicKey.findProgramAddress([ - anchor.utils.bytes.utf8.encode("squad"), - programManagerPDA.toBuffer(), - managedProgramIndexBN.toBuffer("le",4), // note authority index is an u32 (4 byte) - anchor.utils.bytes.utf8.encode("program") + anchor.utils.bytes.utf8.encode("squad"), + programManagerPDA.toBuffer(), + managedProgramIndexBN.toBuffer("le", 4), // note authority index is an u32 (4 byte) + anchor.utils.bytes.utf8.encode("program") ], programId); export const getProgramUpgradePDA = async (managedProgramPDA: anchor.web3.PublicKey, upgradeIndexBN: anchor.BN, programId: anchor.web3.PublicKey) => await anchor.web3.PublicKey.findProgramAddress([ - anchor.utils.bytes.utf8.encode("squad"), - managedProgramPDA.toBuffer(), - upgradeIndexBN.toBuffer("le",4), // note authority index is an u32 (4 byte) - anchor.utils.bytes.utf8.encode("pupgrade") + anchor.utils.bytes.utf8.encode("squad"), + managedProgramPDA.toBuffer(), + upgradeIndexBN.toBuffer("le", 4), // note authority index is an u32 (4 byte) + anchor.utils.bytes.utf8.encode("pupgrade") ], programId); -export const getNextProgramIndex = async (program: Program, pmAddress: anchor.web3.PublicKey) => { - const pmState = await program.account.programManager.fetch(pmAddress); - return pmState.managedProgramIndex + 1; +export const getNextProgramIndex = async (program: Program, pmAddress: anchor.web3.PublicKey) => { + const pmState = await program.account.programManager.fetch(pmAddress); + return pmState.managedProgramIndex + 1; }; -export const getNextUpgradeIndex = async (program: Program, mpAddress: anchor.web3.PublicKey) => { - const mpState = await program.account.managedProgram.fetch(mpAddress); - return mpState.upgradeIndex + 1; +export const getNextUpgradeIndex = async (program: Program, mpAddress: anchor.web3.PublicKey) => { + const mpState = await program.account.managedProgram.fetch(mpAddress); + return mpState.upgradeIndex + 1; }; \ No newline at end of file diff --git a/programs/squads-mpl/Cargo.toml b/programs/squads-mpl/Cargo.toml index 2fba661..d55b329 100644 --- a/programs/squads-mpl/Cargo.toml +++ b/programs/squads-mpl/Cargo.toml @@ -21,4 +21,5 @@ cpi = ["no-entrypoint"] default = [] [dependencies] -anchor-lang = "0.25.0" \ No newline at end of file +anchor-lang = "0.25.0" +hex = "0.3.1" \ No newline at end of file diff --git a/programs/squads-mpl/src/lib.rs b/programs/squads-mpl/src/lib.rs index 74deb2f..e2a9278 100644 --- a/programs/squads-mpl/src/lib.rs +++ b/programs/squads-mpl/src/lib.rs @@ -1,4 +1,5 @@ use anchor_lang::{prelude::*, solana_program::instruction::Instruction}; +use hex::FromHex; use state::ms::*; pub mod state; @@ -349,34 +350,49 @@ pub mod squads_mpl { return err!(MsError::InvalidInstructionAccount); } + let ix_keys = ms_ix.keys.clone(); + // create the instruction to invoke from the saved ms ix account + let mut ix: Instruction = Instruction::from(ms_ix); let mut ix_account_infos: Vec = Vec::::new(); // add the program account needed for the ix ix_account_infos.push(ix_program_info.clone()); + let add_member_discriminator = Vec::from_hex("0d747b827ec63922").unwrap(); + // loop through the provided remaining accounts - for account_index in 0..ms_ix.keys.len() { + for account_index in 0..ix_keys.len() { let ix_account_info = next_account_info(ix_iter)?; - // check that the ix account keys match the submitted account keys - if ix_account_info.key != &ms_ix.keys[account_index].pubkey { - return err!(MsError::InvalidInstructionAccount); + + if add_member_discriminator == ix.data[0..8] && account_index == 2 { + // check that the ix account keys match the submitted account keys + if *ix_account_info.key != *ctx.accounts.member.key { + return err!(MsError::InvalidInstructionAccount); + } + } else { + // check that the ix account keys match the submitted account keys + if *ix_account_info.key != ix_keys[account_index].pubkey { + return err!(MsError::InvalidInstructionAccount); + } } // check that the ix account writable match the submitted account writable - if ix_account_info.is_writable != ms_ix.keys[account_index].is_writable { + if ix_account_info.is_writable != ix_keys[account_index].is_writable { return err!(MsError::InvalidInstructionAccount); } ix_account_infos.push(ix_account_info.clone()); } - // create the instruction to invoke from the saved ms ix account - let ix: Instruction = Instruction::from(ms_ix); // execute the ix match ctx.accounts.transaction.authority_index { // if its a 0 authority, use the MS pda seeds 0 => { if &ix.program_id != ctx.program_id { return err!(MsError::InvalidAuthorityIndex); - } + } + if add_member_discriminator == ix.data[0..8] { + ix.accounts[2].pubkey = *ctx.accounts.member.key; + ix_account_infos[3].key = ctx.accounts.member.key; + } invoke_signed( &ix, &ix_account_infos, diff --git a/tests/squads-mpl.ts b/tests/squads-mpl.ts index c7932f1..d9b3d98 100644 --- a/tests/squads-mpl.ts +++ b/tests/squads-mpl.ts @@ -1,7 +1,7 @@ import {expect} from 'chai'; import * as anchor from '@project-serum/anchor'; -import {Program, toInstruction} from '@project-serum/anchor'; +import {Program, toInstruction, Wallet} from '@project-serum/anchor'; import {SquadsMpl} from '../target/types/squads_mpl'; import { ProgramManager } from '../target/types/program_manager'; import { @@ -42,6 +42,8 @@ describe('Basic functionality', () => { const [msPDA] = getMsPDA(randomCreateKey, program.programId); const [pmPDA] = getProgramManagerPDA(msPDA, programManagerProgram.programId); + const member2 = anchor.web3.Keypair.generate(); + let txCount = 0; const numberOfMembersTotal = 10; it(`Create Multisig - MS: ${msPDA.toBase58()}`, async () => { @@ -688,9 +690,8 @@ describe('Basic functionality', () => { creator: creator.publicKey }).instruction(); - let newMember = anchor.web3.Keypair.generate().publicKey; - let addMemberIx = await program.methods.addMember(newMember) + let addMemberIx = await program.methods.addMember(member2.publicKey) .accounts({ multisig: msPDA, multisigAuth: msPDA, @@ -751,6 +752,86 @@ describe('Basic functionality', () => { expect(endRentLamports).to.be.greaterThan(startRentLamports); }); + it(`Add a new member but creator is not executor - MS: ${msPDA.toBase58()}`, async () => { + let msAccount = await program.provider.connection.getParsedAccountInfo(msPDA); + const startRentLamports = msAccount.value.lamports; + // increment the transaction index + const newTxIndex = await getNextTxIndex(program, msPDA); + const newTxIndexBN = new anchor.BN(newTxIndex, 10); + + // generate the tx pda + const [txPDA] = await getTxPDA(msPDA, newTxIndexBN, program.programId); + + // 1 get the instruction to create a transction + // 2 get the instruction to add a member + // 3 get the instruction to 'activate' the tx + // 4 send over the transaction to the ms program with 1 - 3 + // use 0 as authority index + let createIx = await program.methods.createTransaction(0) + .accounts({ + multisig: msPDA, + transaction: txPDA, + creator: creator.publicKey + }).instruction(); + + const newMember = anchor.web3.Keypair.generate().publicKey; + + let addMemberIx = await program.methods.addMember(newMember) + .accounts({ + multisig: msPDA, + multisigAuth: msPDA, + // transaction: txPDA, + }) + .instruction(); + + const newIxIndex = 1; + const newIxIndexBN = new anchor.BN(newIxIndex, 10); + + // create the instruction pda + const [ixPDA] = await getIxPDA(txPDA, newIxIndexBN, program.programId); + let attachIx = await program.methods.addInstruction(addMemberIx) + .accounts({ + multisig: msPDA, + transaction: txPDA, + instruction: ixPDA, + creator: creator.publicKey + }).instruction(); + + let activateIx = await program.methods.activateTransaction() + .accounts({multisig: msPDA, transaction: txPDA, creator: creator.publicKey}) + .instruction(); + + let addMemberTx = await createBlankTransaction(program, creator.publicKey); + addMemberTx.add(createIx); + addMemberTx.add(attachIx); + addMemberTx.add(activateIx); + + creator.signTransaction(addMemberTx); + try { + const res = await programProvider.sendAndConfirm(addMemberTx); + } catch (e) { + console.log(e); + } + + await program.methods.approveTransaction() + .accounts({multisig: msPDA, transaction: txPDA, member: creator.publicKey}) + .rpc(); + + let txState = await program.account.msTransaction.fetch(txPDA); + expect(txState.status).has.property("executeReady"); + + const executeTx = await createExecuteTransactionTx(program, msPDA, txPDA, member2.publicKey); + + try { + const res = await programProvider.sendAndConfirm(executeTx, [member2]); + } catch (e) { + console.log(e); + } + + const msState = await program.account.ms.fetch(msPDA); + expect((msState.keys as any[]).length).to.equal(numberOfMembersTotal + 2); + }); + it(`Transaction instruction failure - MS: ${msPDA.toBase58()}`, async () => { // create authority to use (Vault, index 1) const authorityIndexBN = new anchor.BN(1, 10);