From d98ff39bbe437b35efbc8b49f99d755081f1e4e0 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 26 Oct 2022 10:26:18 -0400 Subject: [PATCH] fix(roles): tests updated, delegate for executetxproxy --- helpers/roles.ts | 111 +++++++++++++++++ programs/roles/src/lib.rs | 2 +- tests/squads-mpl.ts | 256 +++++++++++++++++++++++++++++++++++++- 3 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 helpers/roles.ts diff --git a/helpers/roles.ts b/helpers/roles.ts new file mode 100644 index 0000000..e7a6074 --- /dev/null +++ b/helpers/roles.ts @@ -0,0 +1,111 @@ +import * as anchor from "@project-serum/anchor"; +import { getIxPDA } from "@sqds/sdk"; +import { SquadsMpl } from "../idl/squads_mpl"; +import { Roles } from "../idl/roles"; + +export async function getExecuteProxyInstruction( + transactionPDA: anchor.web3.PublicKey, + member: anchor.web3.PublicKey, + user: anchor.web3.PublicKey, + delegate: anchor.web3.PublicKey, + squadsMplProgram: anchor.Program, + rolesProgram: anchor.Program, + ): Promise { + const transaction = await squadsMplProgram.account.msTransaction.fetch(transactionPDA); + const ixList = await Promise.all( + [...new Array(transaction.instructionIndex)].map(async (a, i) => { + const ixIndexBN = new anchor.BN(i + 1, 10); + const [ixKey] = getIxPDA( + transactionPDA, + ixIndexBN, + squadsMplProgram.programId + ); + const ixAccount = await squadsMplProgram.account.msInstruction.fetch(ixKey); + return { pubkey: ixKey, ixItem: ixAccount }; + }) + ); + + const ixKeysList: anchor.web3.AccountMeta[] = ixList + .map(({ pubkey, ixItem }) => { + const ixKeys: anchor.web3.AccountMeta[] = + ixItem.keys as anchor.web3.AccountMeta[]; + const addSig = anchor.utils.sha256.hash("global:add_member"); + const ixDiscriminator = Buffer.from(addSig, "hex"); + const addData = Buffer.concat([ixDiscriminator.slice(0, 8)]); + const addAndThreshSig = anchor.utils.sha256.hash( + "global:add_member_and_change_threshold" + ); + const ixAndThreshDiscriminator = Buffer.from(addAndThreshSig, "hex"); + const addAndThreshData = Buffer.concat([ + ixAndThreshDiscriminator.slice(0, 8), + ]); + const ixData = ixItem.data as any; + + const formattedKeys = ixKeys.map((ixKey, keyInd) => { + if ( + (ixData.includes(addData) || ixData.includes(addAndThreshData)) && + keyInd === 2 + ) { + return { + pubkey: member, + 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, + ] as anchor.web3.AccountMeta[]; + }) + .reduce((p, c) => p.concat(c), []); + + // [ix ix_account, ix program_id, key1, key2 ...] + const keysUnique: anchor.web3.AccountMeta[] = 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) { + return prev; + } else { + prev.push({ + pubkey: curr.pubkey, + isWritable: curr.isWritable, + isSigner: curr.isSigner, + }); + return prev; + } + }, + [] as anchor.web3.AccountMeta[] + ); + + const keyIndexMap = ixKeysList.map((a) => { + return keysUnique.findIndex( + (k) => + k.pubkey.toBase58() === a.pubkey.toBase58() && + k.isWritable === a.isWritable + ); + }); + + const executeIx = await rolesProgram.methods.executeTxProxy(Buffer.from(keyIndexMap)) + .accounts({ + multisig: transaction.ms, + transaction: transactionPDA, + member, + user, + delegate, + squadsProgram: squadsMplProgram.programId, + }) + .instruction(); + executeIx.keys = executeIx.keys.concat(keysUnique); + return executeIx; + } \ No newline at end of file diff --git a/programs/roles/src/lib.rs b/programs/roles/src/lib.rs index b024e8e..2d9a50c 100644 --- a/programs/roles/src/lib.rs +++ b/programs/roles/src/lib.rs @@ -572,7 +572,7 @@ impl<'info> ExecuteTxProxy<'info> { let cpi_accounts = ExecuteTransaction { multisig: self.multisig.to_account_info(), transaction: self.transaction.to_account_info(), - member: self.user.to_account_info(), + member: self.delegate.to_account_info(), }; CpiContext::new(cpi_program, cpi_accounts) } diff --git a/tests/squads-mpl.ts b/tests/squads-mpl.ts index ee5cf47..bb804c9 100644 --- a/tests/squads-mpl.ts +++ b/tests/squads-mpl.ts @@ -23,6 +23,7 @@ import Squads, { } from "@sqds/sdk"; import BN from "bn.js"; import { ASSOCIATED_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@project-serum/anchor/dist/cjs/utils/token"; +import { getExecuteProxyInstruction } from "../helpers/roles"; const BPF_UPGRADE_ID = new anchor.web3.PublicKey( "BPFLoaderUpgradeab1e11111111111111111111111" @@ -682,7 +683,7 @@ describe("Programs", function(){ }); }); - describe.skip("Program upgrades", () => { + describe("Program upgrades", () => { it(`Create a program manager`, async function(){ const newProgramManager = await squads.createProgramManager(msPDA); expect(newProgramManager.multisig.toBase58()).to.equal(msPDA.toBase58()); @@ -1250,6 +1251,167 @@ describe("Programs", function(){ expect(roleState.role).to.have.property("vote"); console.log(" ------ Successfully added vote role to user ------"); + + // add the execute-only role to the MS + const [userWithExecuteRolePDA] = await anchor.web3.PublicKey.findProgramAddress([ + anchor.utils.bytes.utf8.encode("squad"), + msPDA.toBuffer(), + userWithExecuteRole.publicKey.toBuffer(), + anchor.utils.bytes.utf8.encode("user-role") + ],rolesProgram.programId + ); + + const [userWithExecuteRoleDelegatePDA] = await anchor.web3.PublicKey.findProgramAddress([ + anchor.utils.bytes.utf8.encode("squad"), + msPDA.toBuffer(), + userWithExecuteRolePDA.toBuffer(), + anchor.utils.bytes.utf8.encode("delegate") + ], rolesProgram.programId) + + msState = await program.account.ms.fetch(msPDA); + nextTxIndex = msState.transactionIndex + 1; + // generate the txPDA + [txPDA] = await getTxPDA( + msPDA, + new BN(nextTxIndex, 10), + program.programId + ); + + // the actual transaction + try { + await program.methods.createTransaction(0) + .accounts({ + multisig: msPDA, + transaction: txPDA, + creator: provider.wallet.publicKey, + }) + .rpc(); + } catch (e) { + console.log("failed to create the authority 0 tx", e); + } + // generate the ixPDA + [ixPDA] = await getIxPDA( + txPDA, + new BN(1, 10), + program.programId + ); + + try { + // the ix that will create the user role + await rolesProgram.methods.addUser(userWithExecuteRole.publicKey, {execute:{}}) + .accounts({ + user: userWithExecuteRolePDA, + multisig: msPDA, + payer: provider.wallet.publicKey + }).rpc(); + }catch(e){ + console.log("failed to add the user role", e); + } + + try { + // the ix that will add the user to the MS + const addMemberIx = await program.methods.addMember(userWithExecuteRoleDelegatePDA) + .accounts({ + multisig: msPDA, + multisigAuth: msPDA, + member: provider.wallet.publicKey, + }) + .instruction(); + + await program.methods.addInstruction(addMemberIx) + .accounts({ + multisig: msPDA, + transaction: txPDA, + instruction: ixPDA, + creator: provider.wallet.publicKey + }) + .rpc(); + }catch(e){ + console.log("failed to add the add member ix", e); + } + + try { + // the activation ix + await program.methods.activateTransaction() + .accounts({ + multisig: msPDA, + transaction: txPDA, + creator: provider.wallet.publicKey + }) + .rpc(); + }catch(e){ + console.log("failed to activate the tx", e); + } + + try { + // the approve ix + await program.methods.approveTransaction() + .accounts({ + multisig: msPDA, + transaction: txPDA, + member: provider.wallet.publicKey, + }) + .rpc(); + }catch(e){ + console.log("failed to approve the tx", e); + } + + // sign to approve with the other members + msState = await squads.getMultisig(msPDA); + // get necessary signers + // if the threshold has changed, use the other members to approve as well + for (let i = 0; i < memberList.length; i++) { + // check to see if we need more signers + const approvalState = await squads.getTransaction(txPDA); + if (Object.keys(approvalState.status).indexOf("active") < 0) { + break; + } + + const inMultisig = (msState.keys as anchor.web3.PublicKey[]).findIndex( + (k) => { + return k.toBase58() == memberList[i].publicKey.toBase58(); + } + ); + if (inMultisig < 0) { + continue; + } + try { + await provider.connection.requestAirdrop( + memberList[i].publicKey, + anchor.web3.LAMPORTS_PER_SOL + ); + const approveTx = await program.methods + .approveTransaction() + .accounts({ + multisig: msPDA, + transaction: txPDA, + member: memberList[i].publicKey, + }) + .signers([memberList[i]]) + .transaction(); + try { + await provider.sendAndConfirm(approveTx, [memberList[i]]); + } catch (e) { + console.log(memberList[i].publicKey.toBase58(), " signing error"); + } + } catch (e) { + console.log(e); + } + } + txState = await program.account.msTransaction.fetch(txPDA); + expect(txState.status).to.have.property("executeReady"); + + try { + await squads.executeTransaction(txPDA); + }catch(e){ + console.log("failed to execute the tx", e); + } + txState = await program.account.msTransaction.fetch(txPDA); + expect(txState.status).to.have.property("executed"); + roleState = await rolesProgram.account.user.fetch(userWithExecuteRolePDA); + expect(roleState.originKey.toBase58()).to.equal(userWithExecuteRole.publicKey.toBase58()); + expect(roleState.role).to.have.property("execute"); + console.log(" ------ Successfully added execute role to user ------"); }); it("New user role initiate withdrawal & vote role", async function(){ @@ -1259,6 +1421,14 @@ describe("Programs", function(){ userWithRole.publicKey, anchor.web3.LAMPORTS_PER_SOL ); + await provider.connection.requestAirdrop( + userWithVoteRole.publicKey, + anchor.web3.LAMPORTS_PER_SOL + ); + await provider.connection.requestAirdrop( + userWithExecuteRole.publicKey, + anchor.web3.LAMPORTS_PER_SOL + ); await provider.connection.requestAirdrop( vault, anchor.web3.LAMPORTS_PER_SOL @@ -1400,13 +1570,95 @@ describe("Programs", function(){ txState = await program.account.msTransaction.fetch(txPDA); expect(txState.approved.length).to.equal(1); expect(txState.approved[0].toBase58()).to.equal(userWithVoteRoleDelegatePDA.toBase58()); + + // sign to approve with the other members + msState = await squads.getMultisig(msPDA); + // get necessary signers + // if the threshold has changed, use the other members to approve as well + for (let i = 0; i < memberList.length; i++) { + // check to see if we need more signers + const approvalState = await squads.getTransaction(txPDA); + if (Object.keys(approvalState.status).indexOf("active") < 0) { + break; + } + + const inMultisig = (msState.keys as anchor.web3.PublicKey[]).findIndex( + (k) => { + return k.toBase58() == memberList[i].publicKey.toBase58(); + } + ); + if (inMultisig < 0) { + continue; + } + try { + await provider.connection.requestAirdrop( + memberList[i].publicKey, + anchor.web3.LAMPORTS_PER_SOL + ); + const approveTx = await program.methods + .approveTransaction() + .accounts({ + multisig: msPDA, + transaction: txPDA, + member: memberList[i].publicKey, + }) + .signers([memberList[i]]) + .transaction(); + try { + await provider.sendAndConfirm(approveTx, [memberList[i]]); + } catch (e) { + console.log(memberList[i].publicKey.toBase58(), " signing error"); + } + } catch (e) { + console.log(e); + } + } + txState = await program.account.msTransaction.fetch(txPDA); + expect(txState.status).to.have.property("executeReady"); + + // test execute role + const [userWithExecutePDA] = await anchor.web3.PublicKey.findProgramAddress([ + anchor.utils.bytes.utf8.encode("squad"), + msPDA.toBuffer(), + userWithExecuteRole.publicKey.toBuffer(), + anchor.utils.bytes.utf8.encode("user-role") + ],rolesProgram.programId + ); + + const [userWithExecuteDelegatePDA] = await anchor.web3.PublicKey.findProgramAddress([ + anchor.utils.bytes.utf8.encode("squad"), + msPDA.toBuffer(), + userWithExecutePDA.toBuffer(), + anchor.utils.bytes.utf8.encode("delegate") + ], rolesProgram.programId) + + // get the proxied execute ix + const executeIx = await getExecuteProxyInstruction( + txPDA, + userWithExecuteRole.publicKey, + userWithExecutePDA, + userWithExecuteDelegatePDA, + program, + rolesProgram + ); + + const executeTx = new anchor.web3.Transaction(); + executeTx.add(executeIx); + try { + await provider.sendAndConfirm(executeTx, [userWithExecuteRole]); + }catch(e){ + console.log(e); + expect(true).to.equal(false); + } + txState = await program.account.msTransaction.fetch(txPDA); + expect(txState.status).to.have.property("executed"); }); }); }); // test suite for the mesh program - describe.skip("Mesh Program", function(){ + describe("Mesh Program", function(){ let meshProgram; let ms; let members = [