From 19dcb3e61b41c8a8d3c0156f4f24ff3339a8f460 Mon Sep 17 00:00:00 2001 From: Vladimir Guguiev <1524432+vovacodes@users.noreply.github.com> Date: Fri, 9 Aug 2024 00:07:34 +0200 Subject: [PATCH 01/47] feat(spending_limits): allow spending limits for non-members --- .../config_transaction_execute.rs | 8 --- .../multisig_add_spending_limit.rs | 13 +--- .../src/instructions/spending_limit_use.rs | 5 -- sdk/multisig/idl/squads_multisig_program.json | 5 +- tests/suites/examples/spending-limits.ts | 62 ++++++++++++++++--- tests/suites/multisig-sdk.ts | 6 +- 6 files changed, 63 insertions(+), 36 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/config_transaction_execute.rs b/programs/squads_multisig_program/src/instructions/config_transaction_execute.rs index 6b9fb287..06851e1c 100644 --- a/programs/squads_multisig_program/src/instructions/config_transaction_execute.rs +++ b/programs/squads_multisig_program/src/instructions/config_transaction_execute.rs @@ -143,14 +143,6 @@ impl<'info> ConfigTransactionExecute<'info> { members, destinations, } => { - // SpendingLimit members must all be members of the multisig. - for sl_member in members.iter() { - require!( - multisig.is_member(*sl_member).is_some(), - MultisigError::NotAMember - ); - } - let (spending_limit_key, spending_limit_bump) = Pubkey::find_program_address( &[ SEED_PREFIX, diff --git a/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs b/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs index 1a0f34d4..8acda4b0 100644 --- a/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs +++ b/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs @@ -18,9 +18,8 @@ pub struct MultisigAddSpendingLimitArgs { /// The reset period of the spending limit. /// When it passes, the remaining amount is reset, unless it's `Period::OneTime`. pub period: Period, - /// Members of the multisig that can use the spending limit. - /// In case a member is removed from the multisig, the spending limit will remain existent - /// (until explicitly deleted), but the removed member will not be able to use it anymore. + /// Members of the Spending Limit that can use it. + /// Don't have to be members of the multisig. pub members: Vec, /// The destination addresses the spending limit is allowed to sent funds to. /// If empty, funds can be sent to any address. @@ -73,14 +72,6 @@ impl MultisigAddSpendingLimit<'_> { // `spending_limit` is partially checked via its seeds. - // SpendingLimit members must all be members of the multisig. - for sl_member in self.spending_limit.members.iter() { - require!( - self.multisig.is_member(*sl_member).is_some(), - MultisigError::NotAMember - ); - } - Ok(()) } diff --git a/programs/squads_multisig_program/src/instructions/spending_limit_use.rs b/programs/squads_multisig_program/src/instructions/spending_limit_use.rs index b357605a..0d929220 100644 --- a/programs/squads_multisig_program/src/instructions/spending_limit_use.rs +++ b/programs/squads_multisig_program/src/instructions/spending_limit_use.rs @@ -97,11 +97,6 @@ impl SpendingLimitUse<'_> { } = self; // member - require!( - multisig.is_member(member.key()).is_some(), - MultisigError::NotAMember - ); - // We don't check member's permissions here but we check if the spending_limit is for the member. require!( spending_limit.members.contains(&member.key()), MultisigError::Unauthorized diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index e6e1e167..d8b65009 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -2055,9 +2055,8 @@ { "name": "members", "docs": [ - "Members of the multisig that can use the spending limit.", - "In case a member is removed from the multisig, the spending limit will remain existent", - "(until explicitly deleted), but the removed member will not be able to use it anymore." + "Members of the Spending Limit that can use it.", + "Don't have to be members of the multisig." ], "type": { "vec": "publicKey" diff --git a/tests/suites/examples/spending-limits.ts b/tests/suites/examples/spending-limits.ts index 3cc41a5b..2760e77c 100644 --- a/tests/suites/examples/spending-limits.ts +++ b/tests/suites/examples/spending-limits.ts @@ -17,6 +17,7 @@ import { } from "@solana/spl-token"; import assert from "assert"; import { + comparePubkeys, createAutonomousMultisig, createLocalhostConnection, generateFundedKeypair, @@ -36,6 +37,7 @@ describe("Examples / Spending Limits", () => { let multisigPda: PublicKey; let members: TestMembers; + let nonMember: Keypair; let solSpendingLimitParams: multisig.types.ConfigActionRecord["AddSpendingLimit"]; let splSpendingLimitParams: multisig.types.ConfigActionRecord["AddSpendingLimit"]; let splMint: PublicKey; @@ -52,6 +54,8 @@ describe("Examples / Spending Limits", () => { }) )[0]; + nonMember = await generateFundedKeypair(connection); + // Set params for creating a Spending Limit for SOL tokens. solSpendingLimitParams = { createKey: Keypair.generate().publicKey, @@ -60,7 +64,7 @@ describe("Examples / Spending Limits", () => { mint: PublicKey.default, amount: 10 * LAMPORTS_PER_SOL, period: Period.OneTime, - members: [members.almighty.publicKey], + members: [members.almighty.publicKey, nonMember.publicKey], destinations: [ Keypair.generate().publicKey, Keypair.generate().publicKey, @@ -99,7 +103,7 @@ describe("Examples / Spending Limits", () => { mint: splMint, amount: 10 * 10 ** mintDecimals, period: Period.OneTime, - members: [members.almighty.publicKey], + members: [members.almighty.publicKey, nonMember.publicKey], destinations: [ Keypair.generate().publicKey, Keypair.generate().publicKey, @@ -272,8 +276,12 @@ describe("Examples / Spending Limits", () => { ); assert.strictEqual(solSpendingLimitAccount.bump, solSpendingLimitBump); assert.deepEqual( - solSpendingLimitAccount.members.map((k) => k.toBase58()), - solSpendingLimitParams.members.map((k) => k.toBase58()) + solSpendingLimitAccount.members + .sort(comparePubkeys) + .map((k) => k.toBase58()), + solSpendingLimitParams.members + .sort(comparePubkeys) + .map((k) => k.toBase58()) ); assert.deepEqual( solSpendingLimitAccount.destinations.map((k) => k.toBase58()), @@ -290,7 +298,8 @@ describe("Examples / Spending Limits", () => { programId, }); - const signature = await multisig.rpc + // Member of the multisig that can use the Spending Limit. + let signature = await multisig.rpc .spendingLimitUse({ connection, feePayer: members.almighty, @@ -302,7 +311,7 @@ describe("Examples / Spending Limits", () => { mint: undefined, vaultIndex: solSpendingLimitParams.vaultIndex, // Use the entire amount. - amount: solSpendingLimitParams.amount as number, + amount: (solSpendingLimitParams.amount as number) / 2, // SOL has 9 decimals. decimals: 9, // Transfer tokens to one of the allowed destinations. @@ -318,7 +327,46 @@ describe("Examples / Spending Limits", () => { await connection.confirmTransaction(signature); // Fetch the Spending Limit account. - const solSpendingLimitAccount = await SpendingLimit.fromAccountAddress( + let solSpendingLimitAccount = await SpendingLimit.fromAccountAddress( + connection, + solSpendingLimitPda + ); + + // We used the half of the amount. + assert.strictEqual( + solSpendingLimitAccount.remainingAmount.toString(), + String((solSpendingLimitParams.amount as number) / 2) + ); + + // Non-member of the multisig that can use the Spending Limit. + signature = await multisig.rpc + .spendingLimitUse({ + connection, + feePayer: members.almighty, + // A member that can use the Spending Limit. + member: nonMember, + multisigPda, + spendingLimit: solSpendingLimitPda, + // We don't need to specify the mint, because this Spending Limit is for SOL. + mint: undefined, + vaultIndex: solSpendingLimitParams.vaultIndex, + // Use the entire amount. + amount: (solSpendingLimitParams.amount as number) / 2, + // SOL has 9 decimals. + decimals: 9, + // Transfer tokens to one of the allowed destinations. + destination: solSpendingLimitParams.destinations[0], + // You can optionally add a memo. + memo: "Using my allowance!", + programId, + }) + .catch((err) => { + console.log(err.logs); + throw err; + }); + await connection.confirmTransaction(signature); + + solSpendingLimitAccount = await SpendingLimit.fromAccountAddress( connection, solSpendingLimitPda ); diff --git a/tests/suites/multisig-sdk.ts b/tests/suites/multisig-sdk.ts index acb61cd9..cd61ec65 100644 --- a/tests/suites/multisig-sdk.ts +++ b/tests/suites/multisig-sdk.ts @@ -843,7 +843,9 @@ describe("Multisig SDK", () => { ); }); - it("create a new Spending Limit for the controlled multisig", async () => { + it("create a new Spending Limit for the controlled multisig with member of the ms and non-member", async () => { + const nonMember = await generateFundedKeypair(connection); + const signature = await multisig.rpc.multisigAddSpendingLimit({ connection, feePayer: feePayer, @@ -856,7 +858,7 @@ describe("Multisig SDK", () => { period: multisig.generated.Period.Day, mint: Keypair.generate().publicKey, destinations: [Keypair.generate().publicKey], - members: [members.almighty.publicKey], + members: [members.almighty.publicKey, nonMember.publicKey], vaultIndex: 1, signers: [feePayer, members.almighty], sendOptions: { skipPreflight: true }, From dff1ac643a57ac7566a689b485e1bc00a56d87e5 Mon Sep 17 00:00:00 2001 From: slg Date: Mon, 12 Aug 2024 14:24:42 -0400 Subject: [PATCH 02/47] fix(multisig_add_member) Additional check for duplication upon member config action. --- .../src/instructions/multisig_config.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/programs/squads_multisig_program/src/instructions/multisig_config.rs b/programs/squads_multisig_program/src/instructions/multisig_config.rs index 00d534b2..461d799c 100644 --- a/programs/squads_multisig_program/src/instructions/multisig_config.rs +++ b/programs/squads_multisig_program/src/instructions/multisig_config.rs @@ -88,6 +88,9 @@ impl MultisigConfig<'_> { let multisig = &mut ctx.accounts.multisig; + // Make sure that the new member is not already in the multisig. + require!(multisig.is_member(new_member.key).is_none(), MultisigError::DuplicateMember); + multisig.add_member(new_member); // Make sure the multisig account can fit the newly set rent_collector. From 3b7e714e7f6e4e20cb219a7e1a2a145c38380499 Mon Sep 17 00:00:00 2001 From: slg Date: Mon, 12 Aug 2024 14:25:37 -0400 Subject: [PATCH 03/47] fix(multisig_add_spending_limit) In the case of a controlled multisig (with config_authority), we run a duplicate check on the member keys in the spending limit. --- .../src/instructions/multisig_add_spending_limit.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs b/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs index 1a0f34d4..efa6b0d1 100644 --- a/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs +++ b/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs @@ -93,6 +93,10 @@ impl MultisigAddSpendingLimit<'_> { args: MultisigAddSpendingLimitArgs, ) -> Result<()> { let spending_limit = &mut ctx.accounts.spending_limit; + + // Make sure there are no duplicate keys in this direct invocation by sorting so the invariant will catch + let mut sorted_members = args.members; + sorted_members.sort(); spending_limit.multisig = ctx.accounts.multisig.key(); spending_limit.create_key = args.create_key; @@ -103,7 +107,7 @@ impl MultisigAddSpendingLimit<'_> { spending_limit.remaining_amount = args.amount; spending_limit.last_reset = Clock::get()?.unix_timestamp; spending_limit.bump = ctx.bumps.spending_limit; - spending_limit.members = args.members; + spending_limit.members = sorted_members; spending_limit.destinations = args.destinations; spending_limit.invariant()?; From 8264ed0c0f44c64c800ca8edc5e570291dd92625 Mon Sep 17 00:00:00 2001 From: slg Date: Mon, 12 Aug 2024 14:26:40 -0400 Subject: [PATCH 04/47] fix(close_account) * updated account context for vault closing to incorporate missing proposal account (non init) * feat(config_transaction_accounts_close,vault_transaction_accounts_close): allow closing stale tx accounts with no proposal * feat(batch_accounts_close): allow closing stale tx accounts with no proposal --------- Co-authored-by: Vladimir Guguiev <1524432+vovacodes@users.noreply.github.com> --- .../transaction_accounts_close.rs | 304 +++++++++++------- .../src/utils/system.rs | 14 + sdk/multisig/idl/squads_multisig_program.json | 15 +- .../suites/instructions/batchAccountsClose.ts | 44 ++- .../configTransactionAccountsClose.ts | 82 ++++- .../vaultTransactionAccountsClose.ts | 88 ++++- tests/utils.ts | 40 ++- 7 files changed, 439 insertions(+), 148 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/transaction_accounts_close.rs b/programs/squads_multisig_program/src/instructions/transaction_accounts_close.rs index c7cadefc..08806845 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_accounts_close.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_accounts_close.rs @@ -13,6 +13,7 @@ use anchor_lang::prelude::*; use crate::errors::*; use crate::state::*; +use crate::utils; #[derive(Accounts)] pub struct ConfigTransactionAccountsClose<'info> { @@ -23,18 +24,25 @@ pub struct ConfigTransactionAccountsClose<'info> { )] pub multisig: Account<'info, Multisig>, + /// CHECK: `seeds` and `bump` verify that the account is the canonical Proposal, + /// the logic within `config_transaction_accounts_close` does the rest of the checks. #[account( mut, - has_one = multisig @ MultisigError::ProposalForAnotherMultisig, - close = rent_collector + seeds = [ + SEED_PREFIX, + multisig.key().as_ref(), + SEED_TRANSACTION, + &transaction.index.to_le_bytes(), + SEED_PROPOSAL, + ], + bump, )] - pub proposal: Account<'info, Proposal>, + pub proposal: AccountInfo<'info>, /// ConfigTransaction corresponding to the `proposal`. #[account( mut, has_one = multisig @ MultisigError::TransactionForAnotherMultisig, - constraint = transaction.index == proposal.transaction_index @ MultisigError::TransactionNotMatchingProposal, close = rent_collector )] pub transaction: Account<'info, ConfigTransaction>, @@ -51,47 +59,63 @@ pub struct ConfigTransactionAccountsClose<'info> { } impl ConfigTransactionAccountsClose<'_> { - fn validate(&self) -> Result<()> { - let Self { - multisig, proposal, .. - } = self; - - let is_stale = proposal.transaction_index <= multisig.stale_transaction_index; + /// Closes a `ConfigTransaction` and the corresponding `Proposal`. + /// `transaction` can be closed if either: + /// - the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`. + /// - the `proposal` is stale. + pub fn config_transaction_accounts_close(ctx: Context) -> Result<()> { + let multisig = &ctx.accounts.multisig; + let transaction = &ctx.accounts.transaction; + let proposal = &mut ctx.accounts.proposal; + let rent_collector = &ctx.accounts.rent_collector; + + let is_stale = transaction.index <= multisig.stale_transaction_index; + + let proposal_account = if proposal.data.borrow().is_empty() { + None + } else { + Some(Proposal::try_deserialize( + &mut &**proposal.data.borrow_mut(), + )?) + }; - // Has to be either stale or in a terminal state. #[allow(deprecated)] - let can_close = match proposal.status { - // Draft proposals can only be closed if stale, - // so they can't be activated anymore. - ProposalStatus::Draft { .. } => is_stale, - // Active proposals can only be closed if stale, - // so they can't be voted on anymore. - ProposalStatus::Active { .. } => is_stale, - // Approved proposals for ConfigTransactions can be closed if stale, - // because they cannot be executed anymore. - ProposalStatus::Approved { .. } => is_stale, - // Rejected proposals can be closed. - ProposalStatus::Rejected { .. } => true, - // Executed proposals can be closed. - ProposalStatus::Executed { .. } => true, - // Cancelled proposals can be closed. - ProposalStatus::Cancelled { .. } => true, - // Should never really be in this state. - ProposalStatus::Executing => false, + let can_close = if let Some(proposal_account) = &proposal_account { + match proposal_account.status { + // Draft proposals can only be closed if stale, + // so they can't be activated anymore. + ProposalStatus::Draft { .. } => is_stale, + // Active proposals can only be closed if stale, + // so they can't be voted on anymore. + ProposalStatus::Active { .. } => is_stale, + // Approved proposals for ConfigTransactions can be closed if stale, + // because they cannot be executed anymore. + ProposalStatus::Approved { .. } => is_stale, + // Rejected proposals can be closed. + ProposalStatus::Rejected { .. } => true, + // Executed proposals can be closed. + ProposalStatus::Executed { .. } => true, + // Cancelled proposals can be closed. + ProposalStatus::Cancelled { .. } => true, + // Should never really be in this state. + ProposalStatus::Executing => false, + } + } else { + // If no Proposal account exists then the ConfigTransaction can only be closed if stale + is_stale }; require!(can_close, MultisigError::InvalidProposalStatus); - Ok(()) - } + // Close the `proposal` account if exists. + if proposal_account.is_some() { + utils::close( + ctx.accounts.proposal.to_account_info(), + rent_collector.to_account_info(), + )?; + } - /// Closes a `ConfigTransaction` and the corresponding `Proposal`. - /// `transaction` can be closed if either: - /// - the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`. - /// - the `proposal` is stale. - #[access_control(_ctx.accounts.validate())] - pub fn config_transaction_accounts_close(_ctx: Context) -> Result<()> { - // Anchor will close the accounts for us. + // Anchor will close the `transaction` account for us. Ok(()) } } @@ -105,18 +129,25 @@ pub struct VaultTransactionAccountsClose<'info> { )] pub multisig: Account<'info, Multisig>, + /// CHECK: `seeds` and `bump` verify that the account is the canonical Proposal, + /// the logic within `vault_transaction_accounts_close` does the rest of the checks. #[account( mut, - has_one = multisig @ MultisigError::ProposalForAnotherMultisig, - close = rent_collector + seeds = [ + SEED_PREFIX, + multisig.key().as_ref(), + SEED_TRANSACTION, + &transaction.index.to_le_bytes(), + SEED_PROPOSAL, + ], + bump, )] - pub proposal: Account<'info, Proposal>, + pub proposal: AccountInfo<'info>, /// VaultTransaction corresponding to the `proposal`. #[account( mut, has_one = multisig @ MultisigError::TransactionForAnotherMultisig, - constraint = transaction.index == proposal.transaction_index @ MultisigError::TransactionNotMatchingProposal, close = rent_collector )] pub transaction: Account<'info, VaultTransaction>, @@ -133,46 +164,65 @@ pub struct VaultTransactionAccountsClose<'info> { } impl VaultTransactionAccountsClose<'_> { - fn validate(&self) -> Result<()> { - let Self { - multisig, proposal, .. - } = self; - - let is_stale = proposal.transaction_index <= multisig.stale_transaction_index; + /// Closes a `VaultTransaction` and the corresponding `Proposal`. + /// `transaction` can be closed if either: + /// - the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`. + /// - the `proposal` is stale and not `Approved`. + pub fn vault_transaction_accounts_close( + ctx: Context, + ) -> Result<()> { + let multisig = &ctx.accounts.multisig; + let transaction = &ctx.accounts.transaction; + let proposal = &mut ctx.accounts.proposal; + let rent_collector = &ctx.accounts.rent_collector; + + let is_stale = transaction.index <= multisig.stale_transaction_index; + + let proposal_account = if proposal.data.borrow().is_empty() { + None + } else { + Some(Proposal::try_deserialize( + &mut &**proposal.data.borrow_mut(), + )?) + }; #[allow(deprecated)] - let can_close = match proposal.status { - // Draft proposals can only be closed if stale, - // so they can't be activated anymore. - ProposalStatus::Draft { .. } => is_stale, - // Active proposals can only be closed if stale, - // so they can't be voted on anymore. - ProposalStatus::Active { .. } => is_stale, - // Approved proposals for VaultTransactions cannot be closed even if stale, - // because they still can be executed. - ProposalStatus::Approved { .. } => false, - // Rejected proposals can be closed. - ProposalStatus::Rejected { .. } => true, - // Executed proposals can be closed. - ProposalStatus::Executed { .. } => true, - // Cancelled proposals can be closed. - ProposalStatus::Cancelled { .. } => true, - // Should never really be in this state. - ProposalStatus::Executing => false, + let can_close = if let Some(proposal_account) = &proposal_account { + match proposal_account.status { + // Draft proposals can only be closed if stale, + // so they can't be activated anymore. + ProposalStatus::Draft { .. } => is_stale, + // Active proposals can only be closed if stale, + // so they can't be voted on anymore. + ProposalStatus::Active { .. } => is_stale, + // Approved proposals for VaultTransactions cannot be closed even if stale, + // because they still can be executed. + ProposalStatus::Approved { .. } => false, + // Rejected proposals can be closed. + ProposalStatus::Rejected { .. } => true, + // Executed proposals can be closed. + ProposalStatus::Executed { .. } => true, + // Cancelled proposals can be closed. + ProposalStatus::Cancelled { .. } => true, + // Should never really be in this state. + ProposalStatus::Executing => false, + } + } else { + // If no Proposal account exists then the VaultTransaction can only be closed if stale + is_stale }; require!(can_close, MultisigError::InvalidProposalStatus); - Ok(()) - } + // Close the `proposal` account if exists. + if proposal_account.is_some() { + utils::close( + ctx.accounts.proposal.to_account_info(), + rent_collector.to_account_info(), + )?; + } - /// Closes a `VaultTransaction` and the corresponding `Proposal`. - /// `transaction` can be closed if either: - /// - the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`. - /// - the `proposal` is stale and not `Approved`. - #[access_control(_ctx.accounts.validate())] - pub fn vault_transaction_accounts_close(_ctx: Context) -> Result<()> { - // Anchor will close the accounts for us. + // Anchor will close the `transaction` account for us. Ok(()) } } @@ -313,18 +363,26 @@ pub struct BatchAccountsClose<'info> { )] pub multisig: Account<'info, Multisig>, + // pub proposal: Account<'info, Proposal>, + /// CHECK: `seeds` and `bump` verify that the account is the canonical Proposal, + /// the logic within `batch_accounts_close` does the rest of the checks. #[account( mut, - has_one = multisig @ MultisigError::ProposalForAnotherMultisig, - close = rent_collector + seeds = [ + SEED_PREFIX, + multisig.key().as_ref(), + SEED_TRANSACTION, + &batch.index.to_le_bytes(), + SEED_PROPOSAL, + ], + bump, )] - pub proposal: Account<'info, Proposal>, + pub proposal: AccountInfo<'info>, /// `Batch` corresponding to the `proposal`. #[account( mut, has_one = multisig @ MultisigError::TransactionForAnotherMultisig, - constraint = batch.index == proposal.transaction_index @ MultisigError::TransactionNotMatchingProposal, close = rent_collector )] pub batch: Account<'info, Batch>, @@ -341,35 +399,51 @@ pub struct BatchAccountsClose<'info> { } impl BatchAccountsClose<'_> { - fn validate(&self) -> Result<()> { - let Self { - multisig, - proposal, - batch, - .. - } = self; - - let is_stale = proposal.transaction_index <= multisig.stale_transaction_index; + /// Closes Batch and the corresponding Proposal accounts for proposals in terminal states: + /// `Executed`, `Rejected`, or `Cancelled` or stale proposals that aren't `Approved`. + /// + /// This instruction is only allowed to be executed when all `VaultBatchTransaction` accounts + /// in the `batch` are already closed: `batch.size == 0`. + pub fn batch_accounts_close(ctx: Context) -> Result<()> { + let multisig = &ctx.accounts.multisig; + let batch = &ctx.accounts.batch; + let proposal = &mut ctx.accounts.proposal; + let rent_collector = &ctx.accounts.rent_collector; + + let is_stale = batch.index <= multisig.stale_transaction_index; + + let proposal_account = if proposal.data.borrow().is_empty() { + None + } else { + Some(Proposal::try_deserialize( + &mut &**proposal.data.borrow_mut(), + )?) + }; #[allow(deprecated)] - let can_close = match proposal.status { - // Draft proposals can only be closed if stale, - // so they can't be activated anymore. - ProposalStatus::Draft { .. } => is_stale, - // Active proposals can only be closed if stale, - // so they can't be voted on anymore. - ProposalStatus::Active { .. } => is_stale, - // Approved proposals for `Batch`s cannot be closed even if stale, - // because they still can be executed. - ProposalStatus::Approved { .. } => false, - // Rejected proposals can be closed. - ProposalStatus::Rejected { .. } => true, - // Executed proposals can be closed. - ProposalStatus::Executed { .. } => true, - // Cancelled proposals can be closed. - ProposalStatus::Cancelled { .. } => true, - // Should never really be in this state. - ProposalStatus::Executing => false, + let can_close = if let Some(proposal_account) = &proposal_account { + match proposal_account.status { + // Draft proposals can only be closed if stale, + // so they can't be activated anymore. + ProposalStatus::Draft { .. } => is_stale, + // Active proposals can only be closed if stale, + // so they can't be voted on anymore. + ProposalStatus::Active { .. } => is_stale, + // Approved proposals for `Batch`s cannot be closed even if stale, + // because they still can be executed. + ProposalStatus::Approved { .. } => false, + // Rejected proposals can be closed. + ProposalStatus::Rejected { .. } => true, + // Executed proposals can be closed. + ProposalStatus::Executed { .. } => true, + // Cancelled proposals can be closed. + ProposalStatus::Cancelled { .. } => true, + // Should never really be in this state. + ProposalStatus::Executing => false, + } + } else { + // If no Proposal account exists then the Batch can only be closed if stale + is_stale }; require!(can_close, MultisigError::InvalidProposalStatus); @@ -377,17 +451,15 @@ impl BatchAccountsClose<'_> { // Batch must be empty. require_eq!(batch.size, 0, MultisigError::BatchNotEmpty); - Ok(()) - } + // Close the `proposal` account if exists. + if proposal_account.is_some() { + utils::close( + ctx.accounts.proposal.to_account_info(), + rent_collector.to_account_info(), + )?; + } - /// Closes Batch and the corresponding Proposal accounts for proposals in terminal states: - /// `Executed`, `Rejected`, or `Cancelled` or stale proposals that aren't `Approved`. - /// - /// This instruction is only allowed to be executed when all `VaultBatchTransaction` accounts - /// in the `batch` are already closed: `batch.size == 0`. - #[access_control(_ctx.accounts.validate())] - pub fn batch_accounts_close(_ctx: Context) -> Result<()> { - // Anchor will close the accounts for us. + // Anchor will close the `batch` account for us. Ok(()) } } diff --git a/programs/squads_multisig_program/src/utils/system.rs b/programs/squads_multisig_program/src/utils/system.rs index 374d5552..f39054e4 100644 --- a/programs/squads_multisig_program/src/utils/system.rs +++ b/programs/squads_multisig_program/src/utils/system.rs @@ -99,3 +99,17 @@ pub fn create_account<'a, 'info>( ) } } + +/// Closes an account by transferring all lamports to the `sol_destination`. +/// +/// Lifted from private `anchor_lang::common::close`: https://github.com/coral-xyz/anchor/blob/714d5248636493a3d1db1481f16052836ee59e94/lang/src/common.rs#L6 +pub fn close<'info>(info: AccountInfo<'info>, sol_destination: AccountInfo<'info>) -> Result<()> { + // Transfer tokens from the account to the sol_destination. + let dest_starting_lamports = sol_destination.lamports(); + **sol_destination.lamports.borrow_mut() = + dest_starting_lamports.checked_add(info.lamports()).unwrap(); + **info.lamports.borrow_mut() = 0; + + info.assign(&system_program::ID); + info.realloc(0, false).map_err(Into::into) +} diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index e6e1e167..99616b33 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -1233,7 +1233,10 @@ { "name": "proposal", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "the logic within `config_transaction_accounts_close` does the rest of the checks." + ] }, { "name": "transaction", @@ -1276,7 +1279,10 @@ { "name": "proposal", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "the logic within `vault_transaction_accounts_close` does the rest of the checks." + ] }, { "name": "transaction", @@ -1373,7 +1379,10 @@ { "name": "proposal", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "the logic within `batch_accounts_close` does the rest of the checks." + ] }, { "name": "batch", diff --git a/tests/suites/instructions/batchAccountsClose.ts b/tests/suites/instructions/batchAccountsClose.ts index 41f9af80..492d7c34 100644 --- a/tests/suites/instructions/batchAccountsClose.ts +++ b/tests/suites/instructions/batchAccountsClose.ts @@ -269,13 +269,13 @@ describe("Instructions / batch_accounts_close", () => { multisig: multisigPda, rentCollector: vaultPda, proposal: multisig.getProposalPda({ - multisigPda: otherMultisig, + multisigPda, transactionIndex: 1n, programId, })[0], batch: multisig.getTransactionPda({ - multisigPda, - index: testMultisig.rejectedBatchIndex, + multisigPda: otherMultisig, + index: 1n, programId, })[0], }, @@ -297,7 +297,7 @@ describe("Instructions / batch_accounts_close", () => { connection .sendTransaction(tx) .catch(multisig.errors.translateAndThrowAnchorError), - /Proposal is for another multisig/ + /Transaction is for another multisig/ ); }); @@ -434,6 +434,42 @@ describe("Instructions / batch_accounts_close", () => { assert.equal(await connection.getAccountInfo(proposalPda), null); }); + it("close accounts for Stale batch with no Proposal", async () => { + const batchIndex = testMultisig.staleDraftBatchNoProposalIndex; + + const multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + + const proposalPda = multisig.getProposalPda({ + multisigPda, + transactionIndex: batchIndex, + programId, + })[0]; + + // Make sure proposal account doesn't exist. + assert.equal(await connection.getAccountInfo(proposalPda), null); + + let signature = await multisig.rpc.batchAccountsClose({ + connection, + feePayer: members.almighty, + multisigPda, + rentCollector: multisigAccount.rentCollector!, + batchIndex, + programId, + }); + await connection.confirmTransaction(signature); + + // Make sure batch and proposal accounts are closed. + const batchPda = multisig.getTransactionPda({ + multisigPda, + index: batchIndex, + programId, + })[0]; + assert.equal(await connection.getAccountInfo(batchPda), null); + }); + it("close accounts for Executed batch", async () => { const batchIndex = testMultisig.executedBatchIndex; diff --git a/tests/suites/instructions/configTransactionAccountsClose.ts b/tests/suites/instructions/configTransactionAccountsClose.ts index 78f8495b..455f63a0 100644 --- a/tests/suites/instructions/configTransactionAccountsClose.ts +++ b/tests/suites/instructions/configTransactionAccountsClose.ts @@ -25,11 +25,12 @@ describe("Instructions / config_transaction_accounts_close", () => { let members: TestMembers; let multisigPda: PublicKey; const staleTransactionIndex = 1n; - const executedTransactionIndex = 2n; - const activeTransactionIndex = 3n; - const approvedTransactionIndex = 4n; - const rejectedTransactionIndex = 5n; - const cancelledTransactionIndex = 6n; + const staleNoProposalTransactionIndex = 2n; + const executedTransactionIndex = 3n; + const activeTransactionIndex = 4n; + const approvedTransactionIndex = 5n; + const rejectedTransactionIndex = 6n; + const cancelledTransactionIndex = 7n; // Set up a multisig with config transactions. before(async () => { @@ -82,6 +83,24 @@ describe("Instructions / config_transaction_accounts_close", () => { // This transaction will become stale when the second config transaction is executed. //endregion + //region Stale and No Proposal + // Create a config transaction (Stale and No Proposal). + signature = await multisig.rpc.configTransactionCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex: staleNoProposalTransactionIndex, + creator: members.proposer.publicKey, + actions: [{ __kind: "ChangeThreshold", newThreshold: 1 }], + programId, + }); + await connection.confirmTransaction(signature); + + // No proposal created for this transaction. + + // This transaction will become stale when the config transaction is executed. + //endregion + //region Executed // Create a config transaction (Executed). signature = await multisig.rpc.configTransactionCreate({ @@ -524,7 +543,7 @@ describe("Instructions / config_transaction_accounts_close", () => { connection .sendTransaction(tx) .catch(multisig.errors.translateAndThrowAnchorError), - /Proposal is for another multisig/ + /A seeds constraint was violated/ ); }); @@ -621,7 +640,7 @@ describe("Instructions / config_transaction_accounts_close", () => { rentCollector: vaultPda, proposal: multisig.getProposalPda({ multisigPda, - transactionIndex: rejectedTransactionIndex, + transactionIndex: 1n, programId, })[0], transaction: multisig.getTransactionPda({ @@ -693,7 +712,7 @@ describe("Instructions / config_transaction_accounts_close", () => { connection .sendTransaction(tx) .catch(multisig.errors.translateAndThrowAnchorError), - /Transaction doesn't match proposal/ + /A seeds constraint was violated/ ); }); @@ -744,6 +763,53 @@ describe("Instructions / config_transaction_accounts_close", () => { assert.ok(postBalance === preBalance + accountsRent); }); + it("close accounts for Stale transaction with No Proposal", async () => { + const transactionIndex = staleNoProposalTransactionIndex; + + const multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + + // Make sure there's no proposal. + let proposalAccount = await connection.getAccountInfo( + multisig.getProposalPda({ + multisigPda, + transactionIndex, + programId, + })[0] + ); + assert.equal(proposalAccount, null); + + // Make sure the transaction is stale. + assert.ok( + transactionIndex <= + multisig.utils.toBigInt(multisigAccount.staleTransactionIndex) + ); + + const [vaultPda] = multisig.getVaultPda({ + multisigPda, + index: 0, + programId, + }); + + const preBalance = await connection.getBalance(vaultPda); + + const sig = await multisig.rpc.configTransactionAccountsClose({ + connection, + feePayer: members.almighty, + multisigPda, + rentCollector: vaultPda, + transactionIndex, + programId, + }); + await connection.confirmTransaction(sig); + + const postBalance = await connection.getBalance(vaultPda); + const accountsRent = 1_503_360; // Rent for the transaction account. + assert.equal(postBalance, preBalance + accountsRent); + }); + it("close accounts for Executed transaction", async () => { const transactionIndex = executedTransactionIndex; diff --git a/tests/suites/instructions/vaultTransactionAccountsClose.ts b/tests/suites/instructions/vaultTransactionAccountsClose.ts index 9f0a35ee..054b6995 100644 --- a/tests/suites/instructions/vaultTransactionAccountsClose.ts +++ b/tests/suites/instructions/vaultTransactionAccountsClose.ts @@ -27,13 +27,14 @@ describe("Instructions / vault_transaction_accounts_close", () => { let members: TestMembers; let multisigPda: PublicKey; const staleNonApprovedTransactionIndex = 1n; - const staleApprovedTransactionIndex = 2n; - const executedConfigTransactionIndex = 3n; - const executedVaultTransactionIndex = 4n; - const activeTransactionIndex = 5n; - const approvedTransactionIndex = 6n; - const rejectedTransactionIndex = 7n; - const cancelledTransactionIndex = 8n; + const staleNoProposalTransactionIndex = 2n; + const staleApprovedTransactionIndex = 3n; + const executedConfigTransactionIndex = 4n; + const executedVaultTransactionIndex = 5n; + const activeTransactionIndex = 6n; + const approvedTransactionIndex = 7n; + const rejectedTransactionIndex = 8n; + const cancelledTransactionIndex = 9n; // Set up a multisig with some transactions. before(async () => { @@ -109,6 +110,26 @@ describe("Instructions / vault_transaction_accounts_close", () => { // This transaction will become stale when the config transaction is executed. //endregion + //region Stale and No Proposal + // Create a vault transaction (Stale and Non-Approved). + signature = await multisig.rpc.vaultTransactionCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex: staleNoProposalTransactionIndex, + vaultIndex: 0, + transactionMessage: testTransferMessage, + ephemeralSigners: 0, + creator: members.proposer.publicKey, + programId, + }); + await connection.confirmTransaction(signature); + + // No proposal created for this transaction. + + // This transaction will become stale when the config transaction is executed. + //endregion + //region Stale and Approved // Create a vault transaction (Stale and Approved). signature = await multisig.rpc.vaultTransactionCreate({ @@ -710,7 +731,7 @@ describe("Instructions / vault_transaction_accounts_close", () => { connection .sendTransaction(tx) .catch(multisig.errors.translateAndThrowAnchorError), - /Proposal is for another multisig/ + /A seeds constraint was violated/ ); }); @@ -849,7 +870,7 @@ describe("Instructions / vault_transaction_accounts_close", () => { rentCollector: vaultPda, proposal: multisig.getProposalPda({ multisigPda, - transactionIndex: rejectedTransactionIndex, + transactionIndex: 1n, programId, })[0], transaction: multisig.getTransactionPda({ @@ -921,7 +942,7 @@ describe("Instructions / vault_transaction_accounts_close", () => { connection .sendTransaction(tx) .catch(multisig.errors.translateAndThrowAnchorError), - /Transaction doesn't match proposal/ + /A seeds constraint was violated/ ); }); @@ -973,6 +994,53 @@ describe("Instructions / vault_transaction_accounts_close", () => { assert.equal(postBalance, preBalance + accountsRent); }); + it("close accounts for Stale transaction with No Proposal", async () => { + const transactionIndex = staleNoProposalTransactionIndex; + + const multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + + // Make sure there's no proposal. + let proposalAccount = await connection.getAccountInfo( + multisig.getProposalPda({ + multisigPda, + transactionIndex, + programId, + })[0] + ); + assert.equal(proposalAccount, null); + + // Make sure the transaction is stale. + assert.ok( + transactionIndex <= + multisig.utils.toBigInt(multisigAccount.staleTransactionIndex) + ); + + const [vaultPda] = multisig.getVaultPda({ + multisigPda, + index: 0, + programId, + }); + + const preBalance = await connection.getBalance(vaultPda); + + const sig = await multisig.rpc.vaultTransactionAccountsClose({ + connection, + feePayer: members.almighty, + multisigPda, + rentCollector: vaultPda, + transactionIndex, + programId, + }); + await connection.confirmTransaction(sig); + + const postBalance = await connection.getBalance(vaultPda); + const accountsRent = 2_429_040; // Rent for the transaction account. + assert.equal(postBalance, preBalance + accountsRent); + }); + it("close accounts for Executed transaction", async () => { const transactionIndex = executedVaultTransactionIndex; diff --git a/tests/utils.ts b/tests/utils.ts index 1938cbe7..f9a21522 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -374,6 +374,12 @@ export type MultisigWithRentReclamationAndVariousBatches = { * The proposal is stale. */ staleDraftBatchIndex: bigint; + /** + * Index of a batch with a proposal in the Draft state. + * The batch contains 1 transaction, which is not executed. + * The proposal is stale. + */ + staleDraftBatchNoProposalIndex: bigint; /** * Index of a batch with a proposal in the Approved state. * The batch contains 2 transactions, the first of which is executed, the second is not. @@ -491,13 +497,14 @@ export async function createAutonomousMultisigWithRentReclamationAndVariousBatch //endregion const staleDraftBatchIndex = 1n; - const staleApprovedBatchIndex = 2n; - const executedConfigTransactionIndex = 3n; - const executedBatchIndex = 4n; - const activeBatchIndex = 5n; - const approvedBatchIndex = 6n; - const rejectedBatchIndex = 7n; - const cancelledBatchIndex = 8n; + const staleDraftBatchNoProposalIndex = 2n; + const staleApprovedBatchIndex = 3n; + const executedConfigTransactionIndex = 4n; + const executedBatchIndex = 5n; + const activeBatchIndex = 6n; + const approvedBatchIndex = 7n; + const rejectedBatchIndex = 8n; + const cancelledBatchIndex = 9n; //region Stale batch with proposal in Draft state // Create a batch (Stale and Non-Approved). @@ -541,6 +548,24 @@ export async function createAutonomousMultisigWithRentReclamationAndVariousBatch // This batch will become stale when the config transaction is executed. //endregion + //region Stale batch with No Proposal + // Create a batch (Stale and Non-Approved). + signature = await multisig.rpc.batchCreate({ + connection, + feePayer: members.proposer, + multisigPda, + batchIndex: staleDraftBatchNoProposalIndex, + vaultIndex: 0, + creator: members.proposer, + programId, + }); + await connection.confirmTransaction(signature); + + // No Proposal for this batch. + + // This batch will become stale when the config transaction is executed. + //endregion + //region Stale batch with Approved proposal // Create a batch (Stale and Approved). signature = await multisig.rpc.batchCreate({ @@ -1148,6 +1173,7 @@ export async function createAutonomousMultisigWithRentReclamationAndVariousBatch return { multisigPda, staleDraftBatchIndex, + staleDraftBatchNoProposalIndex, staleApprovedBatchIndex, executedConfigTransactionIndex, executedBatchIndex, From e801095228516223334d9070b73841f7260acb83 Mon Sep 17 00:00:00 2001 From: slg Date: Mon, 12 Aug 2024 14:27:29 -0400 Subject: [PATCH 05/47] fix(cancellation_realloc) * Cancelation realloc and current member state check/retain * added comment about realloc * move new vote logic to a v2 ix to preserve backwards compatibility * new proposal cancel instruction (v2) * new account context labeled as ProposalCancel specifically * add the retain old member keys to existing cancel logic --- .../src/instructions/proposal_vote.rs | 78 +++++++++++++++++++ programs/squads_multisig_program/src/lib.rs | 11 +++ .../src/state/proposal.rs | 56 +++++++++++++ 3 files changed, 145 insertions(+) diff --git a/programs/squads_multisig_program/src/instructions/proposal_vote.rs b/programs/squads_multisig_program/src/instructions/proposal_vote.rs index 87075dd8..67b83d69 100644 --- a/programs/squads_multisig_program/src/instructions/proposal_vote.rs +++ b/programs/squads_multisig_program/src/instructions/proposal_vote.rs @@ -33,6 +33,33 @@ pub struct ProposalVote<'info> { pub proposal: Account<'info, Proposal>, } +#[derive(Accounts)] +pub struct ProposalCancel<'info> { + #[account( + seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()], + bump = multisig.bump, + )] + pub multisig: Account<'info, Multisig>, + + #[account(mut)] + pub member: Signer<'info>, + + #[account( + mut, + seeds = [ + SEED_PREFIX, + multisig.key().as_ref(), + SEED_TRANSACTION, + &proposal.transaction_index.to_le_bytes(), + SEED_PROPOSAL, + ], + bump = proposal.bump, + )] + pub proposal: Account<'info, Proposal>, + + pub system_program: Program<'info, System>, +} + impl ProposalVote<'_> { fn validate(&self, vote: Vote) -> Result<()> { let Self { @@ -113,8 +140,59 @@ impl ProposalVote<'_> { let proposal = &mut ctx.accounts.proposal; let member = &mut ctx.accounts.member; + proposal.cancelled.retain(|k| multisig.is_member(*k).is_some()); + + proposal.cancel(member.key(), usize::from(multisig.threshold))?; + + Ok(()) + } +} + +impl ProposalCancel<'_> { + fn validate(&self) -> Result<()> { + let Self { + multisig, + proposal, + member, + .. + } = self; + + // member + require!( + multisig.is_member(member.key()).is_some(), + MultisigError::NotAMember + ); + require!( + multisig.member_has_permission(member.key(), Permission::Vote), + MultisigError::Unauthorized + ); + + + require!( + matches!(proposal.status, ProposalStatus::Approved { .. }), + MultisigError::InvalidProposalStatus + ); + // CAN cancel a stale proposal. + + Ok(()) + } + + /// Cancel a multisig proposal on behalf of the `member`. + /// The proposal must be `Approved`. + #[access_control(ctx.accounts.validate())] + pub fn proposal_cancel(ctx: Context, _args: ProposalVoteArgs) -> Result<()> { + let multisig = &mut ctx.accounts.multisig; + let proposal = &mut ctx.accounts.proposal; + let member = &mut ctx.accounts.member; + let system_program = &ctx.accounts.system_program; + + // ensure that the cancel array contains no keys that are not currently members + proposal.cancelled.retain(|k| multisig.is_member(*k).is_some()); + proposal.cancel(member.key(), usize::from(multisig.threshold))?; + // reallocate the proposal size if needed + Proposal::realloc_if_needed(proposal.to_account_info(), multisig.members.len(), Some(member.to_account_info()), Some(system_program.to_account_info()))?; Ok(()) } } diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index 130d24c6..338bfbf3 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -227,6 +227,17 @@ pub mod squads_multisig_program { ProposalVote::proposal_cancel(ctx, args) } + /// Cancel a multisig proposal on behalf of the `member`. + /// The proposal must be `Approved`. + /// This was introduced to incorporate proper state update, as old multisig members + /// may have lingering votes, and the proposal size may need to be reallocated to + /// accommodate the new amount of cancel votes. + /// The previous implemenation still works if the proposal size is in line with the + /// thresholdhold size. + pub fn proposal_cancel_v2(ctx: Context, args: ProposalVoteArgs) -> Result<()> { + ProposalCancel::proposal_cancel(ctx, args) + } + /// Use a spending limit to transfer tokens from a multisig vault to a destination account. pub fn spending_limit_use( ctx: Context, diff --git a/programs/squads_multisig_program/src/state/proposal.rs b/programs/squads_multisig_program/src/state/proposal.rs index 18a0fcc0..1f8c06bc 100644 --- a/programs/squads_multisig_program/src/state/proposal.rs +++ b/programs/squads_multisig_program/src/state/proposal.rs @@ -2,6 +2,9 @@ use anchor_lang::prelude::*; use crate::errors::*; +use crate::id; + +use anchor_lang::system_program; /// Stores the data required for tracking the status of a multisig proposal. /// Each `Proposal` has a 1:1 association with a transaction account, e.g. a `VaultTransaction` or a `ConfigTransaction`; @@ -122,6 +125,59 @@ impl Proposal { fn remove_approval_vote(&mut self, index: usize) { self.approved.remove(index); } + + /// Check if the proposal account space needs to be reallocated to accommodate `cancelled` vec. + /// Proposal size is crated at creation, and thus may not accomodate enough space for all members to cancel if more are added or changed + /// Returns `true` if the account was reallocated. + pub fn realloc_if_needed<'a>( + proposal: AccountInfo<'a>, + members_length: usize, + rent_payer: Option>, + system_program: Option>, + ) -> Result { + // Sanity checks + require_keys_eq!(*proposal.owner, id(), MultisigError::IllegalAccountOwner); + + let current_account_size = proposal.data.borrow().len(); + let account_size_to_fit_members = Proposal::size(members_length); + + // Check if we need to reallocate space. + if current_account_size >= account_size_to_fit_members { + return Ok(false); + } + + // Reallocate more space. + AccountInfo::realloc(&proposal, account_size_to_fit_members, false)?; + + // If more lamports are needed, transfer them to the account. + let rent_exempt_lamports = Rent::get().unwrap().minimum_balance(account_size_to_fit_members).max(1); + let top_up_lamports = + rent_exempt_lamports.saturating_sub(proposal.to_account_info().lamports()); + + if top_up_lamports > 0 { + let system_program = system_program.ok_or(MultisigError::MissingAccount)?; + require_keys_eq!( + *system_program.key, + system_program::ID, + MultisigError::InvalidAccount + ); + + let rent_payer = rent_payer.ok_or(MultisigError::MissingAccount)?; + + system_program::transfer( + CpiContext::new( + system_program, + system_program::Transfer { + from: rent_payer, + to: proposal, + }, + ), + top_up_lamports, + )?; + } + + Ok(true) + } } /// The status of a proposal. From ce9bd91ddfcf03877fa2689074200a752221fbe3 Mon Sep 17 00:00:00 2001 From: slg Date: Mon, 19 Aug 2024 08:11:21 -0400 Subject: [PATCH 06/47] Execute optimizations (#110) * fix(vault_seeds): removed iterations * pass vault seed slice directly * fix(execute-tx-message): optimization - moved signer seed iterations out of loop --- .../instructions/batch_execute_transaction.rs | 5 +-- .../instructions/vault_transaction_execute.rs | 5 +-- .../utils/executable_transaction_message.rs | 33 +++++++++---------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/batch_execute_transaction.rs b/programs/squads_multisig_program/src/instructions/batch_execute_transaction.rs index 6d46ebd9..c3b9ac37 100644 --- a/programs/squads_multisig_program/src/instructions/batch_execute_transaction.rs +++ b/programs/squads_multisig_program/src/instructions/batch_execute_transaction.rs @@ -150,10 +150,7 @@ impl BatchExecuteTransaction<'_> { // Execute the transaction message instructions one-by-one. executable_message.execute_message( - &vault_seeds - .iter() - .map(|seed| seed.to_vec()) - .collect::>>(), + vault_seeds, &ephemeral_signer_seeds, protected_accounts, )?; diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_execute.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_execute.rs index edbb962c..4fea1162 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_execute.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_execute.rs @@ -130,10 +130,7 @@ impl VaultTransactionExecute<'_> { // Execute the transaction message instructions one-by-one. executable_message.execute_message( - &vault_seeds - .iter() - .map(|seed| seed.to_vec()) - .collect::>>(), + vault_seeds, &ephemeral_signer_seeds, protected_accounts, )?; diff --git a/programs/squads_multisig_program/src/utils/executable_transaction_message.rs b/programs/squads_multisig_program/src/utils/executable_transaction_message.rs index cf590c6e..d4e2768b 100644 --- a/programs/squads_multisig_program/src/utils/executable_transaction_message.rs +++ b/programs/squads_multisig_program/src/utils/executable_transaction_message.rs @@ -179,10 +179,24 @@ impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> { /// * `protected_accounts` - Accounts that must not be passed as writable to the CPI calls to prevent potential reentrancy attacks. pub fn execute_message( &self, - vault_seeds: &[Vec], + vault_seeds: &[&[u8]], ephemeral_signer_seeds: &[Vec>], protected_accounts: &[Pubkey], ) -> Result<()> { + + // First round of type conversion; from Vec>> to Vec>. + let ephemeral_signer_seeds = &ephemeral_signer_seeds + .iter() + .map(|seeds| seeds.iter().map(Vec::as_slice).collect::>()) + .collect::>>(); + // Second round of type conversion; from Vec> to Vec<&[&[u8]]>. + let mut signer_seeds = ephemeral_signer_seeds + .iter() + .map(Vec::as_slice) + .collect::>(); + // Add the vault seeds. + signer_seeds.push(&vault_seeds); + for (ix, account_infos) in self.to_instructions_and_accounts().iter() { // Make sure we don't pass protected accounts as writable to CPI calls. for account_meta in ix.accounts.iter().filter(|m| m.is_writable) { @@ -192,23 +206,6 @@ impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> { ); } - // Convert vault_seeds to Vec<&[u8]>. - let vault_seeds = vault_seeds.iter().map(Vec::as_slice).collect::>(); - - // First round of type conversion; from Vec>> to Vec>. - let ephemeral_signer_seeds = &ephemeral_signer_seeds - .iter() - .map(|seeds| seeds.iter().map(Vec::as_slice).collect::>()) - .collect::>>(); - // Second round of type conversion; from Vec> to Vec<&[&[u8]]>. - let mut signer_seeds = ephemeral_signer_seeds - .iter() - .map(Vec::as_slice) - .collect::>(); - - // Add the vault seeds. - signer_seeds.push(&vault_seeds); - invoke_signed(ix, account_infos, &signer_seeds)?; } From a0a8afa787d14c754ba6b889c6b148699663fc3f Mon Sep 17 00:00:00 2001 From: slg Date: Mon, 19 Aug 2024 08:13:36 -0400 Subject: [PATCH 07/47] chore(typo): fixed Fixed "threshold" typo --- programs/squads_multisig_program/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index 338bfbf3..dd7cce5c 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -233,7 +233,7 @@ pub mod squads_multisig_program { /// may have lingering votes, and the proposal size may need to be reallocated to /// accommodate the new amount of cancel votes. /// The previous implemenation still works if the proposal size is in line with the - /// thresholdhold size. + /// threshold size. pub fn proposal_cancel_v2(ctx: Context, args: ProposalVoteArgs) -> Result<()> { ProposalCancel::proposal_cancel(ctx, args) } From bb946c63350c112fc3bec4f0d5642cd43d1187a2 Mon Sep 17 00:00:00 2001 From: Orion <89707822+iceomatic@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:38:24 +0200 Subject: [PATCH 08/47] fix: Heap Optimization (#111) * heap optimization/removing inefficient clone * add: comments around explicit usage and consequences of `core::mem::take` --------- Co-authored-by: slg --- .../instructions/batch_execute_transaction.rs | 14 ++- .../instructions/vault_transaction_execute.rs | 16 ++- .../src/state/batch.rs | 7 ++ .../src/state/vault_transaction.rs | 12 +- .../utils/executable_transaction_message.rs | 23 ++-- sdk/multisig/idl/squads_multisig_program.json | 42 +++++++ .../src/generated/instructions/index.ts | 1 + .../instructions/proposalCancelV2.ts | 115 ++++++++++++++++++ 8 files changed, 211 insertions(+), 19 deletions(-) create mode 100644 sdk/multisig/src/generated/instructions/proposalCancelV2.ts diff --git a/programs/squads_multisig_program/src/instructions/batch_execute_transaction.rs b/programs/squads_multisig_program/src/instructions/batch_execute_transaction.rs index c3b9ac37..e0666dfa 100644 --- a/programs/squads_multisig_program/src/instructions/batch_execute_transaction.rs +++ b/programs/squads_multisig_program/src/instructions/batch_execute_transaction.rs @@ -108,7 +108,12 @@ impl BatchExecuteTransaction<'_> { let multisig = &mut ctx.accounts.multisig; let proposal = &mut ctx.accounts.proposal; let batch = &mut ctx.accounts.batch; - let transaction = &mut ctx.accounts.transaction; + + // NOTE: After `take()` is called, the VaultTransaction is reduced to + // its default empty value, which means it should no longer be referenced or + // used after this point to avoid faulty behavior. + // Instead only make use of the returned `transaction` value. + let transaction = ctx.accounts.transaction.take(); let multisig_key = multisig.key(); let batch_key = batch.key(); @@ -121,7 +126,7 @@ impl BatchExecuteTransaction<'_> { &[batch.vault_bump], ]; - let transaction_message = &transaction.message; + let transaction_message = transaction.message; let num_lookups = transaction_message.address_table_lookups.len(); let message_account_infos = ctx @@ -149,6 +154,11 @@ impl BatchExecuteTransaction<'_> { let protected_accounts = &[proposal.key(), batch_key]; // Execute the transaction message instructions one-by-one. + // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()` + // which in turn calls `take()` on + // `self.message.instructions`, therefore after this point no more + // references or usages of `self.message` should be made to avoid + // faulty behavior. executable_message.execute_message( vault_seeds, &ephemeral_signer_seeds, diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_execute.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_execute.rs index 4fea1162..a5ff077f 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_execute.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_execute.rs @@ -88,10 +88,15 @@ impl VaultTransactionExecute<'_> { pub fn vault_transaction_execute(ctx: Context) -> Result<()> { let multisig = &mut ctx.accounts.multisig; let proposal = &mut ctx.accounts.proposal; - let transaction = &mut ctx.accounts.transaction; + + // NOTE: After `take()` is called, the VaultTransaction is reduced to + // its default empty value, which means it should no longer be referenced or + // used after this point to avoid faulty behavior. + // Instead only make use of the returned `transaction` value. + let transaction = ctx.accounts.transaction.take(); let multisig_key = multisig.key(); - let transaction_key = transaction.key(); + let transaction_key = ctx.accounts.transaction.key(); let vault_seeds = &[ SEED_PREFIX, @@ -101,7 +106,7 @@ impl VaultTransactionExecute<'_> { &[transaction.vault_bump], ]; - let transaction_message = &transaction.message; + let transaction_message = transaction.message; let num_lookups = transaction_message.address_table_lookups.len(); let message_account_infos = ctx @@ -129,6 +134,11 @@ impl VaultTransactionExecute<'_> { let protected_accounts = &[proposal.key()]; // Execute the transaction message instructions one-by-one. + // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()` + // which in turn calls `take()` on + // `self.message.instructions`, therefore after this point no more + // references or usages of `self.message` should be made to avoid + // faulty behavior. executable_message.execute_message( vault_seeds, &ephemeral_signer_seeds, diff --git a/programs/squads_multisig_program/src/state/batch.rs b/programs/squads_multisig_program/src/state/batch.rs index 24793deb..810cb370 100644 --- a/programs/squads_multisig_program/src/state/batch.rs +++ b/programs/squads_multisig_program/src/state/batch.rs @@ -40,6 +40,7 @@ impl Batch { /// Stores data required for execution of one transaction from a batch. #[account] +#[derive(Default)] pub struct VaultBatchTransaction { /// PDA bump. pub bump: u8, @@ -69,4 +70,10 @@ impl VaultBatchTransaction { message_size, // message ) } + + /// Reduces the VaultBatchTransaction to its default empty value and moves + /// ownership of the data to the caller/return value. + pub fn take(&mut self) -> VaultBatchTransaction { + core::mem::take(self) + } } diff --git a/programs/squads_multisig_program/src/state/vault_transaction.rs b/programs/squads_multisig_program/src/state/vault_transaction.rs index 9f2a75a8..1e7cbac3 100644 --- a/programs/squads_multisig_program/src/state/vault_transaction.rs +++ b/programs/squads_multisig_program/src/state/vault_transaction.rs @@ -8,6 +8,7 @@ use crate::instructions::{CompiledInstruction, MessageAddressTableLookup, Transa /// Vault transaction is a transaction that's executed on behalf of the multisig vault PDA /// and wraps arbitrary Solana instructions, typically calling into other Solana programs. #[account] +#[derive(Default)] pub struct VaultTransaction { /// The multisig this belongs to. pub multisig: Pubkey, @@ -27,7 +28,7 @@ pub struct VaultTransaction { /// When wrapping such transactions into multisig ones, we replace these "ephemeral" signing keypairs /// with PDAs derived from the MultisigTransaction's `transaction_index` and controlled by the Multisig Program; /// during execution the program includes the seeds of these PDAs into the `invoke_signed` calls, - /// thus "signing" on behalf of these PDAs. + /// thus "signing" on behalf of these PDAs. pub ephemeral_signer_bumps: Vec, /// data required for executing the transaction. pub message: VaultTransactionMessage, @@ -44,16 +45,21 @@ impl VaultTransaction { 32 + // multisig 32 + // creator 8 + // index - 1 + // bump + 1 + // bump 1 + // vault_index 1 + // vault_bump (4 + usize::from(ephemeral_signers_length)) + // ephemeral_signers_bumps vec message_size, // message ) } + /// Reduces the VaultTransaction to its default empty value and moves + /// ownership of the data to the caller/return value. + pub fn take(&mut self) -> VaultTransaction { + core::mem::take(self) + } } -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] pub struct VaultTransactionMessage { /// The number of signer pubkeys in the account_keys vec. pub num_signers: u8, diff --git a/programs/squads_multisig_program/src/utils/executable_transaction_message.rs b/programs/squads_multisig_program/src/utils/executable_transaction_message.rs index d4e2768b..53a93251 100644 --- a/programs/squads_multisig_program/src/utils/executable_transaction_message.rs +++ b/programs/squads_multisig_program/src/utils/executable_transaction_message.rs @@ -13,7 +13,7 @@ use crate::state::*; /// Sanitized and validated combination of a `MsTransactionMessage` and `AccountInfo`s it references. pub struct ExecutableTransactionMessage<'a, 'info> { /// Message which loaded a collection of lookup table addresses. - message: &'a VaultTransactionMessage, + message: VaultTransactionMessage, /// Resolved `account_keys` of the message. static_accounts: Vec<&'a AccountInfo<'info>>, /// Concatenated vector of resolved `writable_indexes` from all address lookups. @@ -29,7 +29,7 @@ impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> { /// `address_lookup_table_account_infos` - AccountInfo's that are expected to correspond to the lookup tables mentioned in `message.address_table_lookups`. /// `vault_pubkey` - The vault PDA that is expected to sign the message. pub fn new_validated( - message: &'a VaultTransactionMessage, + message: VaultTransactionMessage, message_account_infos: &'a [AccountInfo<'info>], address_lookup_table_account_infos: &'a [AccountInfo<'info>], vault_pubkey: &'a Pubkey, @@ -178,12 +178,11 @@ impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> { /// * `ephemeral_signer_seeds` - Seeds for the ephemeral signer PDAs. /// * `protected_accounts` - Accounts that must not be passed as writable to the CPI calls to prevent potential reentrancy attacks. pub fn execute_message( - &self, + self, vault_seeds: &[&[u8]], ephemeral_signer_seeds: &[Vec>], protected_accounts: &[Pubkey], ) -> Result<()> { - // First round of type conversion; from Vec>> to Vec>. let ephemeral_signer_seeds = &ephemeral_signer_seeds .iter() @@ -196,7 +195,11 @@ impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> { .collect::>(); // Add the vault seeds. signer_seeds.push(&vault_seeds); - + + // NOTE: `self.to_instructions_and_accounts()` calls `take()` on + // `self.message.instructions`, therefore after this point no more + // references or usages of `self.message` should be made to avoid + // faulty behavior. for (ix, account_infos) in self.to_instructions_and_accounts().iter() { // Make sure we don't pass protected accounts as writable to CPI calls. for account_meta in ix.accounts.iter().filter(|m| m.is_writable) { @@ -205,10 +208,8 @@ impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> { MultisigError::ProtectedAccount ); } - - invoke_signed(ix, account_infos, &signer_seeds)?; + invoke_signed(&ix, &account_infos, &signer_seeds)?; } - Ok(()) } @@ -251,10 +252,10 @@ impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> { index < self.loaded_writable_accounts.len() } - pub fn to_instructions_and_accounts(&self) -> Vec<(Instruction, Vec>)> { + pub fn to_instructions_and_accounts(mut self) -> Vec<(Instruction, Vec>)> { let mut executable_instructions = vec![]; - for ms_compiled_instruction in self.message.instructions.iter() { + for ms_compiled_instruction in core::mem::take(&mut self.message.instructions) { let ix_accounts: Vec<(AccountInfo<'info>, AccountMeta)> = ms_compiled_instruction .account_indexes .iter() @@ -286,7 +287,7 @@ impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> { .iter() .map(|(_, account_meta)| account_meta.clone()) .collect(), - data: ms_compiled_instruction.data.clone(), + data: ms_compiled_instruction.data, }; let mut account_infos: Vec = ix_accounts diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index 45842fa6..0ce53974 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -1118,6 +1118,48 @@ } ] }, + { + "name": "proposalCancelV2", + "docs": [ + "Cancel a multisig proposal on behalf of the `member`.", + "The proposal must be `Approved`.", + "This was introduced to incorporate proper state update, as old multisig members", + "may have lingering votes, and the proposal size may need to be reallocated to", + "accommodate the new amount of cancel votes.", + "The previous implemenation still works if the proposal size is in line with the", + "thresholdhold size." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "member", + "isMut": true, + "isSigner": true + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProposalVoteArgs" + } + } + ] + }, { "name": "spendingLimitUse", "docs": [ diff --git a/sdk/multisig/src/generated/instructions/index.ts b/sdk/multisig/src/generated/instructions/index.ts index 23c7f196..30f3cedb 100644 --- a/sdk/multisig/src/generated/instructions/index.ts +++ b/sdk/multisig/src/generated/instructions/index.ts @@ -22,6 +22,7 @@ export * from './programConfigSetTreasury' export * from './proposalActivate' export * from './proposalApprove' export * from './proposalCancel' +export * from './proposalCancelV2' export * from './proposalCreate' export * from './proposalReject' export * from './spendingLimitUse' diff --git a/sdk/multisig/src/generated/instructions/proposalCancelV2.ts b/sdk/multisig/src/generated/instructions/proposalCancelV2.ts new file mode 100644 index 00000000..1b3d57d2 --- /dev/null +++ b/sdk/multisig/src/generated/instructions/proposalCancelV2.ts @@ -0,0 +1,115 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' +import { + ProposalVoteArgs, + proposalVoteArgsBeet, +} from '../types/ProposalVoteArgs' + +/** + * @category Instructions + * @category ProposalCancelV2 + * @category generated + */ +export type ProposalCancelV2InstructionArgs = { + args: ProposalVoteArgs +} +/** + * @category Instructions + * @category ProposalCancelV2 + * @category generated + */ +export const proposalCancelV2Struct = new beet.FixableBeetArgsStruct< + ProposalCancelV2InstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['args', proposalVoteArgsBeet], + ], + 'ProposalCancelV2InstructionArgs' +) +/** + * Accounts required by the _proposalCancelV2_ instruction + * + * @property [] multisig + * @property [_writable_, **signer**] member + * @property [_writable_] proposal + * @category Instructions + * @category ProposalCancelV2 + * @category generated + */ +export type ProposalCancelV2InstructionAccounts = { + multisig: web3.PublicKey + member: web3.PublicKey + proposal: web3.PublicKey + systemProgram?: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const proposalCancelV2InstructionDiscriminator = [ + 205, 41, 194, 61, 220, 139, 16, 247, +] + +/** + * Creates a _ProposalCancelV2_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category ProposalCancelV2 + * @category generated + */ +export function createProposalCancelV2Instruction( + accounts: ProposalCancelV2InstructionAccounts, + args: ProposalCancelV2InstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = proposalCancelV2Struct.serialize({ + instructionDiscriminator: proposalCancelV2InstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.multisig, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.member, + isWritable: true, + isSigner: true, + }, + { + pubkey: accounts.proposal, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} From f25fc922913fd49e288468df14bff9fe75fa19d0 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 19 Aug 2024 18:20:06 -0400 Subject: [PATCH 09/47] chore(cancel-tests): added test and sdk methods for cancel realloc --- sdk/multisig/src/instructions/index.ts | 1 + .../src/instructions/proposalCancelV2.ts | 29 ++ sdk/multisig/src/rpc/index.ts | 1 + sdk/multisig/src/rpc/proposalCancelV2.ts | 51 ++ sdk/multisig/src/transactions/index.ts | 1 + .../src/transactions/proposalCancelV2.ts | 46 ++ tests/index.ts | 1 + tests/suites/instructions/cancelRealloc.ts | 442 ++++++++++++++++++ tests/suites/multisig-sdk.ts | 95 ++++ 9 files changed, 667 insertions(+) create mode 100644 sdk/multisig/src/instructions/proposalCancelV2.ts create mode 100644 sdk/multisig/src/rpc/proposalCancelV2.ts create mode 100644 sdk/multisig/src/transactions/proposalCancelV2.ts create mode 100644 tests/suites/instructions/cancelRealloc.ts diff --git a/sdk/multisig/src/instructions/index.ts b/sdk/multisig/src/instructions/index.ts index d3f7d0a1..20bacba7 100644 --- a/sdk/multisig/src/instructions/index.ts +++ b/sdk/multisig/src/instructions/index.ts @@ -18,6 +18,7 @@ export * from "./multisigSetTimeLock.js"; export * from "./proposalActivate.js"; export * from "./proposalApprove.js"; export * from "./proposalCancel.js"; +export * from "./proposalCancelV2.js"; export * from "./proposalCreate.js"; export * from "./proposalReject.js"; export * from "./spendingLimitUse.js"; diff --git a/sdk/multisig/src/instructions/proposalCancelV2.ts b/sdk/multisig/src/instructions/proposalCancelV2.ts new file mode 100644 index 00000000..a10c5728 --- /dev/null +++ b/sdk/multisig/src/instructions/proposalCancelV2.ts @@ -0,0 +1,29 @@ +import { getProposalPda } from "../pda"; +import { createProposalCancelV2Instruction, PROGRAM_ID } from "../generated"; +import { PublicKey } from "@solana/web3.js"; + +export function proposalCancelV2({ + multisigPda, + transactionIndex, + member, + memo, + programId = PROGRAM_ID, +}: { + multisigPda: PublicKey; + transactionIndex: bigint; + member: PublicKey; + memo?: string; + programId?: PublicKey; +}) { + const [proposalPda] = getProposalPda({ + multisigPda, + transactionIndex, + programId, + }); + + return createProposalCancelV2Instruction( + { multisig: multisigPda, proposal: proposalPda, member }, + { args: { memo: memo ?? null } }, + programId + ); +} diff --git a/sdk/multisig/src/rpc/index.ts b/sdk/multisig/src/rpc/index.ts index a1537b96..2eed0925 100644 --- a/sdk/multisig/src/rpc/index.ts +++ b/sdk/multisig/src/rpc/index.ts @@ -17,6 +17,7 @@ export * from "./multisigSetTimeLock.js"; export * from "./proposalActivate.js"; export * from "./proposalApprove.js"; export * from "./proposalCancel.js"; +export * from "./proposalCancelV2.js"; export * from "./proposalCreate.js"; export * from "./proposalReject.js"; export * from "./spendingLimitUse.js"; diff --git a/sdk/multisig/src/rpc/proposalCancelV2.ts b/sdk/multisig/src/rpc/proposalCancelV2.ts new file mode 100644 index 00000000..2c0faea0 --- /dev/null +++ b/sdk/multisig/src/rpc/proposalCancelV2.ts @@ -0,0 +1,51 @@ +import { + Connection, + PublicKey, + SendOptions, + Signer, + TransactionSignature, + } from "@solana/web3.js"; + import * as transactions from "../transactions"; + import { translateAndThrowAnchorError } from "../errors"; + + /** Cancel a config transaction on behalf of the `member`. */ + export async function proposalCancelV2({ + connection, + feePayer, + member, + multisigPda, + transactionIndex, + memo, + sendOptions, + programId, + }: { + connection: Connection; + feePayer: Signer; + member: Signer; + multisigPda: PublicKey; + transactionIndex: bigint; + memo?: string; + sendOptions?: SendOptions; + programId?: PublicKey; + }): Promise { + const blockhash = (await connection.getLatestBlockhash()).blockhash; + + const tx = transactions.proposalCancelV2({ + blockhash, + feePayer: feePayer.publicKey, + multisigPda, + transactionIndex, + member: member.publicKey, + memo, + programId, + }); + + tx.sign([feePayer, member]); + + try { + return await connection.sendTransaction(tx, sendOptions); + } catch (err) { + translateAndThrowAnchorError(err); + } + } + \ No newline at end of file diff --git a/sdk/multisig/src/transactions/index.ts b/sdk/multisig/src/transactions/index.ts index 8beba2c6..437e9d12 100644 --- a/sdk/multisig/src/transactions/index.ts +++ b/sdk/multisig/src/transactions/index.ts @@ -18,6 +18,7 @@ export * from "./multisigSetTimeLock.js"; export * from "./proposalActivate.js"; export * from "./proposalApprove.js"; export * from "./proposalCancel.js"; +export * from "./proposalCancelV2.js"; export * from "./proposalCreate.js"; export * from "./proposalReject.js"; export * from "./spendingLimitUse.js"; diff --git a/sdk/multisig/src/transactions/proposalCancelV2.ts b/sdk/multisig/src/transactions/proposalCancelV2.ts new file mode 100644 index 00000000..4afbca17 --- /dev/null +++ b/sdk/multisig/src/transactions/proposalCancelV2.ts @@ -0,0 +1,46 @@ +import { + PublicKey, + TransactionMessage, + VersionedTransaction, + } from "@solana/web3.js"; + + import * as instructions from "../instructions/index.js"; + + /** + * Returns unsigned `VersionedTransaction` that needs to be + * signed by `member` and `feePayer` before sending it. + */ + export function proposalCancelV2({ + blockhash, + feePayer, + multisigPda, + transactionIndex, + member, + memo, + programId, + }: { + blockhash: string; + feePayer: PublicKey; + multisigPda: PublicKey; + transactionIndex: bigint; + member: PublicKey; + memo?: string; + programId?: PublicKey; + }): VersionedTransaction { + const message = new TransactionMessage({ + payerKey: feePayer, + recentBlockhash: blockhash, + instructions: [ + instructions.proposalCancelV2({ + member, + multisigPda, + transactionIndex, + memo, + programId, + }), + ], + }).compileToV0Message(); + + return new VersionedTransaction(message); + } + \ No newline at end of file diff --git a/tests/index.ts b/tests/index.ts index af1b1757..6a952c63 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -8,6 +8,7 @@ import "./suites/instructions/configTransactionAccountsClose"; import "./suites/instructions/vaultBatchTransactionAccountClose"; import "./suites/instructions/batchAccountsClose"; import "./suites/instructions/vaultTransactionAccountsClose"; +import "./suites/instructions/cancelRealloc"; import "./suites/multisig-sdk"; import "./suites/account-migrations"; import "./suites/examples/batch-sol-transfer"; diff --git a/tests/suites/instructions/cancelRealloc.ts b/tests/suites/instructions/cancelRealloc.ts new file mode 100644 index 00000000..2f76165a --- /dev/null +++ b/tests/suites/instructions/cancelRealloc.ts @@ -0,0 +1,442 @@ +import * as multisig from "@sqds/multisig"; +import assert from "assert"; +import { + createAutonomousMultisig, + createLocalhostConnection, + createTestTransferInstruction, + generateMultisigMembers, + getTestProgramId, + TestMembers, +} from "../../utils"; +import { Keypair, LAMPORTS_PER_SOL, PublicKey, TransactionMessage } from "@solana/web3.js"; + +const { Multisig, Proposal } = multisig.accounts; + +const programId = getTestProgramId(); +const connection = createLocalhostConnection(); + +describe("Instructions / proposal_cancel_v2", () => { + let members: TestMembers; + let multisigPda: PublicKey; + let newVotingMember = new Keypair(); + let newVotingMember2 = new Keypair(); + let newVotingMember3 = new Keypair(); + let newVotingMember4 = new Keypair(); + let addMemberCollection = [ + {key: newVotingMember.publicKey, permissions: multisig.types.Permissions.all()}, + {key: newVotingMember2.publicKey, permissions: multisig.types.Permissions.all()}, + {key: newVotingMember3.publicKey, permissions: multisig.types.Permissions.all()}, + {key: newVotingMember4.publicKey, permissions: multisig.types.Permissions.all()}, + ]; + let cancelVotesCollection = [ + newVotingMember, + newVotingMember2, + newVotingMember3, + newVotingMember4, + ]; + let originalCancel: Keypair; + + before(async () => { + members = await generateMultisigMembers(connection); + // Create new autonomous multisig. + multisigPda = ( + await createAutonomousMultisig({ + connection, + members, + threshold: 2, + timeLock: 0, + programId, + }) + )[0]; + + }); + + // multisig current has a threhsold of 2 with two voting members. + // create a proposal to add a member to the multisig (which we will cancel) + // the proposal size will be allocated to TOTAL members length + it("cancel basic config tx proposal", async () => { + // Create a config transaction. + const transactionIndex = 1n; + const [proposalPda] = multisig.getProposalPda({ + multisigPda, + transactionIndex, + programId, + }); + + let signature = await multisig.rpc.configTransactionCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex, + creator: members.proposer.publicKey, + actions: [{ __kind: "AddMember", newMember: {key: newVotingMember.publicKey, permissions: multisig.types.Permissions.all()} }], + programId, + }); + await connection.confirmTransaction(signature); + + // Create a proposal for the transaction. + signature = await multisig.rpc.proposalCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex, + creator: members.proposer, + programId, + }); + await connection.confirmTransaction(signature); + + // Proposal status must be "Cancelled". + let proposalAccount = await Proposal.fromAccountAddress( + connection, + proposalPda + ); + + // Approve the proposal 1. + signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.voter, + multisigPda, + transactionIndex, + member: members.voter, + programId, + }); + await connection.confirmTransaction(signature); + + // Approve the proposal 2. + signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex, + member: members.almighty, + programId, + }); + await connection.confirmTransaction(signature); + + // Proposal is now ready to execute, cast the 2 cancels using the new functionality. + signature = await multisig.rpc.proposalCancelV2({ + connection, + feePayer: members.voter, + member: members.voter, + multisigPda, + transactionIndex, + programId, + }); + await connection.confirmTransaction(signature); + + // Proposal is now ready to execute, cast the 2 cancels using the new functionality. + signature = await multisig.rpc.proposalCancelV2({ + connection, + feePayer: members.almighty, + member: members.almighty, + multisigPda, + transactionIndex, + programId, + }); + await connection.confirmTransaction(signature); + + // Proposal status must be "Cancelled". + proposalAccount = await Proposal.fromAccountAddress( + connection, + proposalPda + ); + assert.ok(multisig.types.isProposalStatusCancelled(proposalAccount.status)); + }); + + // in order to test this, we create a basic transfer transaction + // then we vote to approve it + // then we cast 1 cancel vote + // then we change the state of the multisig so the new amount of voting members is greater than the last total size + // then we change the threshold to be greater than the last total size + // then we change the state of the multisig so that one original cancel voter is removed + // then we vote to cancel (and be able to close the transfer transaction) + it("cancel config with stale state size", async () => { + // Create a config transaction. + let transactionIndex = 2n; + const [proposalPda] = multisig.getProposalPda({ + multisigPda, + transactionIndex, + programId, + }); + + // Default vault. + const [vaultPda, vaultBump] = multisig.getVaultPda({ + multisigPda, + index: 0, + programId, + }); + const testPayee = Keypair.generate(); + const testIx1 = await createTestTransferInstruction( + vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx1], + }); + + let signature = await multisig.rpc.vaultTransactionCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex, + creator: members.proposer.publicKey, + vaultIndex: 0, + ephemeralSigners: 0, + transactionMessage: testTransferMessage, + memo: "Transfer 1 SOL to a test account", + programId, + }); + await connection.confirmTransaction(signature); + + // Create a proposal for the transaction. + signature = await multisig.rpc.proposalCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex, + creator: members.proposer, + programId, + }); + await connection.confirmTransaction(signature); + + // Approve the proposal 1. + signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.voter, + multisigPda, + transactionIndex, + member: members.voter, + programId, + }); + await connection.confirmTransaction(signature); + + // Approve the proposal 2. + signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex, + member: members.almighty, + programId, + }); + await connection.confirmTransaction(signature); + + // Proposal status must be "Approved". + let proposalAccount = await Proposal.fromAccountAddress( + connection, + proposalPda + ); + assert.ok(multisig.types.isProposalStatusApproved(proposalAccount.status)); + // check the account size + + + // TX/Proposal is now in an approved/ready state. + // Now cancel vec has enough room for 4 votes. + + // Cast the 1 cancel using the new functionality and the 'voter' member. + signature = await multisig.rpc.proposalCancelV2({ + connection, + feePayer: members.voter, + member: members.voter, + multisigPda, + transactionIndex, + programId, + }); + await connection.confirmTransaction(signature); + // set the original cancel voter + originalCancel = members.voter; + + // ensure that the account size has not changed yet + + // Change the multisig state to have 5 voting members. + // loop through the process to add the 4 members + for (let i = 0; i < addMemberCollection.length; i++) { + const newMember = addMemberCollection[i]; + transactionIndex++; + signature = await multisig.rpc.configTransactionCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex, + creator: members.proposer.publicKey, + actions: [{ __kind: "AddMember", newMember }], + programId, + }); + await connection.confirmTransaction(signature); + + // Create a proposal for the transaction. + signature = await multisig.rpc.proposalCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex, + creator: members.proposer, + programId, + }); + await connection.confirmTransaction(signature); + + // Proposal status must be "Cancelled". + proposalAccount = await Proposal.fromAccountAddress( + connection, + proposalPda + ); + + // Approve the proposal 1. + signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.voter, + multisigPda, + transactionIndex, + member: members.voter, + programId, + }); + await connection.confirmTransaction(signature); + + // Approve the proposal 2. + signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex, + member: members.almighty, + programId, + }); + await connection.confirmTransaction(signature); + + // use the execute only member to execute + signature = await multisig.rpc.configTransactionExecute({ + connection, + feePayer: members.executor, + multisigPda, + transactionIndex, + member: members.executor, + rentPayer: members.executor, + programId, + }); + await connection.confirmTransaction(signature); + + } + + // assert that our member length is now 8 + let multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + assert.strictEqual(multisigAccount.members.length, 8); + + transactionIndex++; + // now remove the original cancel voter + signature = await multisig.rpc.configTransactionCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex, + creator: members.proposer.publicKey, + actions: [{ __kind: "RemoveMember", oldMember: originalCancel.publicKey }, { __kind: "ChangeThreshold", newThreshold: 5 }], + programId, + }); + await connection.confirmTransaction(signature); + // create the remove proposal + signature = await multisig.rpc.proposalCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex, + creator: members.proposer, + programId, + }); + await connection.confirmTransaction(signature); + // approve the proposal 1 + signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.voter, + multisigPda, + transactionIndex, + member: members.voter, + programId, + }); + await connection.confirmTransaction(signature); + // approve the proposal 2 + signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex, + member: members.almighty, + programId, + }); + await connection.confirmTransaction(signature); + // execute the proposal + signature = await multisig.rpc.configTransactionExecute({ + connection, + feePayer: members.executor, + multisigPda, + transactionIndex, + member: members.executor, + rentPayer: members.executor, + programId, + }); + await connection.confirmTransaction(signature); + // now assert we have 7 members + multisigAccount = await Multisig.fromAccountAddress( + connection, + multisigPda + ); + assert.strictEqual(multisigAccount.members.length, 7); + assert.strictEqual(multisigAccount.threshold, 5); + + // so now our threshold should be 5 for cancelling, which exceeds the original space allocated at the beginning + // get the original proposer and assert the originalCancel is in the cancel array + proposalAccount = await Proposal.fromAccountAddress( + connection, + proposalPda + ); + assert.strictEqual(proposalAccount.cancelled.length, 1); + let deprecatedCancelVote = proposalAccount.cancelled[0]; + assert.ok(deprecatedCancelVote.equals(originalCancel.publicKey)); + + // now cast a cancel against it with the first all perm key + signature = await multisig.rpc.proposalCancelV2({ + connection, + feePayer: members.almighty, + member: members.almighty, + multisigPda, + transactionIndex: 2n, + programId, + }); + await connection.confirmTransaction(signature); + // now assert that the cancelled array only has 1 key and it is the one that just voted + proposalAccount = await Proposal.fromAccountAddress( + connection, + proposalPda + ); + assert.strictEqual(proposalAccount.cancelled.length, 1); + let newCancelVote = proposalAccount.cancelled[0]; + assert.ok(newCancelVote.equals(members.almighty.publicKey)); + // now cast 4 more cancels with the new key + for (let i = 0; i < cancelVotesCollection.length; i++) { + signature = await multisig.rpc.proposalCancelV2({ + connection, + feePayer: members.executor, + member: cancelVotesCollection[i], + multisigPda, + transactionIndex: 2n, + programId, + }); + await connection.confirmTransaction(signature); + } + + // now assert the proposals is cancelled + proposalAccount = await Proposal.fromAccountAddress( + connection, + proposalPda + ); + assert.ok(multisig.types.isProposalStatusCancelled(proposalAccount.status)); + // assert there are 5 cancelled votes + assert.strictEqual(proposalAccount.cancelled.length, 5); + }); + + }); diff --git a/tests/suites/multisig-sdk.ts b/tests/suites/multisig-sdk.ts index cd61ec65..5bdaefdb 100644 --- a/tests/suites/multisig-sdk.ts +++ b/tests/suites/multisig-sdk.ts @@ -2079,6 +2079,101 @@ describe("Multisig SDK", () => { multisig.types.isProposalStatusCancelled(proposalAccount.status) ); }); + + it("proposal_cancel_v2", async () => { + // Create a config transaction. + const transactionIndex = 2n; + let newVotingMember = new Keypair(); + + const [proposalPda] = multisig.getProposalPda({ + multisigPda, + transactionIndex, + programId, + }); + + let signature = await multisig.rpc.configTransactionCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex, + creator: members.proposer.publicKey, + actions: [{ __kind: "AddMember", newMember: {key: newVotingMember.publicKey, permissions: multisig.types.Permissions.all()} }], + programId, + }); + await connection.confirmTransaction(signature); + + // Create a proposal for the transaction. + signature = await multisig.rpc.proposalCreate({ + connection, + feePayer: members.proposer, + multisigPda, + transactionIndex, + creator: members.proposer, + programId, + }); + await connection.confirmTransaction(signature); + + // Approve the proposal 1. + signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.voter, + multisigPda, + transactionIndex, + member: members.voter, + programId, + }); + await connection.confirmTransaction(signature); + + // Approve the proposal 2. + signature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex, + member: members.almighty, + programId, + }); + await connection.confirmTransaction(signature); + + let proposalAccount = await Proposal.fromAccountAddress( + connection, + proposalPda + ); + // Our threshold is 2, so after the first cancel, the proposal is still `Approved`. + assert.ok( + multisig.types.isProposalStatusApproved(proposalAccount.status) + ); + + // Proposal is now ready to execute, cast the 2 cancels using the new functionality. + signature = await multisig.rpc.proposalCancelV2({ + connection, + feePayer: members.voter, + member: members.voter, + multisigPda, + transactionIndex, + programId, + }); + await connection.confirmTransaction(signature); + + // Proposal is now ready to execute, cast the 2 cancels using the new functionality. + signature = await multisig.rpc.proposalCancelV2({ + connection, + feePayer: members.almighty, + member: members.almighty, + multisigPda, + transactionIndex, + programId, + }); + await connection.confirmTransaction(signature); + + // Proposal status must be "Cancelled". + proposalAccount = await Proposal.fromAccountAddress( + connection, + proposalPda + ); + assert.ok(multisig.types.isProposalStatusCancelled(proposalAccount.status)); + }); + }); describe("vault_transaction_execute", () => { From 12a12c88aaf2188c48128ed2fc487b68f537d8c6 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 20 Aug 2024 08:10:47 -0400 Subject: [PATCH 10/47] chore(cancel-test): verify account size changed --- tests/suites/instructions/cancelRealloc.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/suites/instructions/cancelRealloc.ts b/tests/suites/instructions/cancelRealloc.ts index 2f76165a..2ad7172f 100644 --- a/tests/suites/instructions/cancelRealloc.ts +++ b/tests/suites/instructions/cancelRealloc.ts @@ -150,7 +150,7 @@ describe("Instructions / proposal_cancel_v2", () => { // then we change the threshold to be greater than the last total size // then we change the state of the multisig so that one original cancel voter is removed // then we vote to cancel (and be able to close the transfer transaction) - it("cancel config with stale state size", async () => { + it("cancel tx with stale state size", async () => { // Create a config transaction. let transactionIndex = 2n; const [proposalPda] = multisig.getProposalPda({ @@ -398,6 +398,10 @@ describe("Instructions / proposal_cancel_v2", () => { let deprecatedCancelVote = proposalAccount.cancelled[0]; assert.ok(deprecatedCancelVote.equals(originalCancel.publicKey)); + // get the pre realloc size + const rawProposal = await connection.getAccountInfo(proposalPda); + const rawProposalData = rawProposal?.data.length; + // now cast a cancel against it with the first all perm key signature = await multisig.rpc.proposalCancelV2({ connection, @@ -413,6 +417,10 @@ describe("Instructions / proposal_cancel_v2", () => { connection, proposalPda ); + // check the data length to ensure it has changed + const updatedRawProposal = await connection.getAccountInfo(proposalPda); + const updatedRawProposalData = updatedRawProposal?.data.length; + assert.notStrictEqual(updatedRawProposalData, rawProposalData); assert.strictEqual(proposalAccount.cancelled.length, 1); let newCancelVote = proposalAccount.cancelled[0]; assert.ok(newCancelVote.equals(members.almighty.publicKey)); From 1fdb66f6f93d9df84cd15801055797f9c7eb1051 Mon Sep 17 00:00:00 2001 From: Orion <89707822+iceomatic@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:04:31 +0200 Subject: [PATCH 11/47] Feat: Incremental Transaction Uploading (#113) * wip: incremental tx uploading * add: incremental tx uploading ixns * generate sdk & add tests - createbuffer & extend passing* - createVaultTxFromBuffer failing with "Program Failed to Complete" * base case tests passing * add comments for clarity * expose buffer close & add test * add: extra tests -- buffer creation and extension * add: tests for transactionBufferClose * fix: uncomment tests * fix: space allocation buffer creation * add: additional helper for buffer size * add: buffer deserialization checks * dummy vercel commit * feat: tx buffer example & out of memory example --------- Co-authored-by: Joey Meere <100378695+joeymeere@users.noreply.github.com> --- .../squads_multisig_program/src/errors.rs | 6 + .../src/instructions/mod.rs | 8 + .../instructions/transaction_buffer_close.rs | 57 ++ .../instructions/transaction_buffer_create.rs | 107 ++++ .../instructions/transaction_buffer_extend.rs | 100 ++++ .../vault_transaction_create_from_buffer.rs | 160 ++++++ programs/squads_multisig_program/src/lib.rs | 32 ++ .../squads_multisig_program/src/state/mod.rs | 2 + .../src/state/seeds.rs | 1 + .../src/state/transaction_buffer.rs | 81 +++ sdk/multisig/idl/squads_multisig_program.json | 317 ++++++++++- .../generated/accounts/TransactionBuffer.ts | 211 +++++++ sdk/multisig/src/generated/accounts/index.ts | 3 + sdk/multisig/src/generated/errors/index.ts | 69 +++ .../src/generated/instructions/index.ts | 4 + .../instructions/transactionBufferClose.ts | 88 +++ .../instructions/transactionBufferCreate.ts | 122 ++++ .../instructions/transactionBufferExtend.ts | 109 ++++ .../vaultTransactionCreateFromBuffer.ts | 131 +++++ .../types/TransactionBufferCreateArgs.ts | 29 + .../types/TransactionBufferExtendArgs.ts | 21 + .../VaultTransactionCreateFromBufferArgs.ts | 25 + sdk/multisig/src/generated/types/index.ts | 3 + tests/index.ts | 5 + tests/suites/examples/transaction-buffer.ts | 355 ++++++++++++ .../instructions/transactionBufferClose.ts | 180 ++++++ .../instructions/transactionBufferCreate.ts | 528 ++++++++++++++++++ .../instructions/transactionBufferExtend.ts | 437 +++++++++++++++ .../vaultTransactionCreateFromBuffer.ts | 442 +++++++++++++++ tests/utils.ts | 8 + 30 files changed, 3640 insertions(+), 1 deletion(-) create mode 100644 programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs create mode 100644 programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs create mode 100644 programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs create mode 100644 programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs create mode 100644 programs/squads_multisig_program/src/state/transaction_buffer.rs create mode 100644 sdk/multisig/src/generated/accounts/TransactionBuffer.ts create mode 100644 sdk/multisig/src/generated/instructions/transactionBufferClose.ts create mode 100644 sdk/multisig/src/generated/instructions/transactionBufferCreate.ts create mode 100644 sdk/multisig/src/generated/instructions/transactionBufferExtend.ts create mode 100644 sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts create mode 100644 sdk/multisig/src/generated/types/TransactionBufferCreateArgs.ts create mode 100644 sdk/multisig/src/generated/types/TransactionBufferExtendArgs.ts create mode 100644 sdk/multisig/src/generated/types/VaultTransactionCreateFromBufferArgs.ts create mode 100644 tests/suites/examples/transaction-buffer.ts create mode 100644 tests/suites/instructions/transactionBufferClose.ts create mode 100644 tests/suites/instructions/transactionBufferCreate.ts create mode 100644 tests/suites/instructions/transactionBufferExtend.ts create mode 100644 tests/suites/instructions/vaultTransactionCreateFromBuffer.ts diff --git a/programs/squads_multisig_program/src/errors.rs b/programs/squads_multisig_program/src/errors.rs index 01a74a94..947ebcb9 100644 --- a/programs/squads_multisig_program/src/errors.rs +++ b/programs/squads_multisig_program/src/errors.rs @@ -82,4 +82,10 @@ pub enum MultisigError { BatchNotEmpty, #[msg("Invalid SpendingLimit amount")] SpendingLimitInvalidAmount, + #[msg("Final message buffer hash doesnt match the expected hash")] + FinalBufferHashMismatch, + #[msg("Final buffer size cannot exceed 4000 bytes")] + FinalBufferSizeExceeded, + #[msg("Final buffer size mismatch")] + FinalBufferSizeMismatch, } diff --git a/programs/squads_multisig_program/src/instructions/mod.rs b/programs/squads_multisig_program/src/instructions/mod.rs index d64dd9c1..ffb0b814 100644 --- a/programs/squads_multisig_program/src/instructions/mod.rs +++ b/programs/squads_multisig_program/src/instructions/mod.rs @@ -16,6 +16,10 @@ pub use spending_limit_use::*; pub use transaction_accounts_close::*; pub use vault_transaction_create::*; pub use vault_transaction_execute::*; +pub use transaction_buffer_create::*; +pub use transaction_buffer_extend::*; +pub use transaction_buffer_close::*; +pub use vault_transaction_create_from_buffer::*; mod batch_add_transaction; mod batch_create; @@ -35,3 +39,7 @@ mod spending_limit_use; mod transaction_accounts_close; mod vault_transaction_create; mod vault_transaction_execute; +mod transaction_buffer_create; +mod transaction_buffer_extend; +mod transaction_buffer_close; +mod vault_transaction_create_from_buffer; \ No newline at end of file diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs new file mode 100644 index 00000000..12115e32 --- /dev/null +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs @@ -0,0 +1,57 @@ +use anchor_lang::prelude::*; + +use crate::errors::*; +use crate::state::*; + +#[derive(Accounts)] +pub struct TransactionBufferClose<'info> { + #[account( + mut, + seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()], + bump = multisig.bump, + )] + pub multisig: Account<'info, Multisig>, + + #[account( + mut, + // Rent gets returned to the creator + close = creator, + // Only the creator can close the buffer + constraint = transaction_buffer.creator == creator.key() @ MultisigError::Unauthorized, + seeds = [ + SEED_PREFIX, + multisig.key().as_ref(), + SEED_TRANSACTION_BUFFER, + &multisig.transaction_index.checked_add(1).unwrap().to_le_bytes(), + ], + bump + )] + pub transaction_buffer: Account<'info, TransactionBuffer>, + + /// The member of the multisig that created the TransactionBuffer. + pub creator: Signer<'info>, +} + +impl TransactionBufferClose<'_> { + fn validate(&self) -> Result<()> { + let Self { + multisig, creator, .. + } = self; + + // creator is still a member in the multisig + require!( + multisig.is_member(creator.key()).is_some(), + MultisigError::NotAMember + ); + + Ok(()) + } + + /// Create a new vault transaction. + #[access_control(ctx.accounts.validate())] + pub fn transaction_buffer_close( + ctx: Context, + ) -> Result<()> { + Ok(()) + } +} diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs new file mode 100644 index 00000000..69ed7c67 --- /dev/null +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs @@ -0,0 +1,107 @@ +use anchor_lang::prelude::*; + +use crate::errors::*; +use crate::state::*; +use crate::state::MAX_BUFFER_SIZE; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct TransactionBufferCreateArgs { + /// Index of the vault this transaction belongs to. + pub vault_index: u8, + /// Hash of the final assembled transaction message. + pub final_buffer_hash: [u8; 32], + /// Final size of the buffer. + pub final_buffer_size: u16, + /// Initial slice of the buffer. + pub buffer: Vec, +} + +#[derive(Accounts)] +#[instruction(args: TransactionBufferCreateArgs)] +pub struct TransactionBufferCreate<'info> { + #[account( + mut, + seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()], + bump = multisig.bump, + )] + pub multisig: Account<'info, Multisig>, + + #[account( + init, + payer = rent_payer, + space = TransactionBuffer::size(args.final_buffer_size)?, + seeds = [ + SEED_PREFIX, + multisig.key().as_ref(), + SEED_TRANSACTION_BUFFER, + &multisig.transaction_index.checked_add(1).unwrap().to_le_bytes(), + ], + bump + )] + pub transaction_buffer: Account<'info, TransactionBuffer>, + + /// The member of the multisig that is creating the transaction. + pub creator: Signer<'info>, + + /// The payer for the transaction account rent. + #[account(mut)] + pub rent_payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +impl TransactionBufferCreate<'_> { + fn validate(&self, args: &TransactionBufferCreateArgs) -> Result<()> { + let Self { + multisig, creator, .. + } = self; + + // creator is a member in the multisig + require!( + multisig.is_member(creator.key()).is_some(), + MultisigError::NotAMember + ); + // creator has initiate permissions + require!( + multisig.member_has_permission(creator.key(), Permission::Initiate), + MultisigError::Unauthorized + ); + + // Final Buffer Size must not exceed 4000 bytes + require!(args.final_buffer_size as usize <= MAX_BUFFER_SIZE, MultisigError::FinalBufferSizeExceeded); + Ok(()) + } + + /// Create a new vault transaction. + #[access_control(ctx.accounts.validate(&args))] + pub fn transaction_buffer_create( + ctx: Context, + args: TransactionBufferCreateArgs, + ) -> Result<()> { + // Mutable Accounts + let transaction_buffer = &mut ctx.accounts.transaction_buffer; + + // Readonly Accounts + let multisig = &ctx.accounts.multisig; + let creator = &mut ctx.accounts.creator; + + // Get the transaction index. + let transaction_index = multisig.transaction_index.checked_add(1).unwrap(); + + // Initialize the transaction fields. + transaction_buffer.multisig = multisig.key(); + transaction_buffer.creator = creator.key(); + transaction_buffer.vault_index = args.vault_index; + transaction_buffer.transaction_index = transaction_index; + transaction_buffer.final_buffer_hash = args.final_buffer_hash; + transaction_buffer.final_buffer_size = args.final_buffer_size; + transaction_buffer.buffer = args.buffer; + + + // Invariant function on the transaction buffer + transaction_buffer.invariant()?; + + Ok(()) + } +} + diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs new file mode 100644 index 00000000..84a53d48 --- /dev/null +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs @@ -0,0 +1,100 @@ +use anchor_lang::prelude::*; + +use crate::errors::*; +use crate::state::*; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct TransactionBufferExtendArgs { + // Buffer to extend the TransactionBuffer with. + pub buffer: Vec, +} + +#[derive(Accounts)] +#[instruction(args: TransactionBufferExtendArgs)] +pub struct TransactionBufferExtend<'info> { + #[account( + mut, + seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()], + bump = multisig.bump, + )] + pub multisig: Account<'info, Multisig>, + + #[account( + mut, + // Only the creator can extend the buffer + constraint = transaction_buffer.creator == creator.key() @ MultisigError::Unauthorized, + seeds = [ + SEED_PREFIX, + multisig.key().as_ref(), + SEED_TRANSACTION_BUFFER, + &multisig.transaction_index.checked_add(1).unwrap().to_le_bytes(), + ], + bump + )] + pub transaction_buffer: Account<'info, TransactionBuffer>, + + /// The member of the multisig that created the TransactionBuffer. + pub creator: Signer<'info>, +} + +impl TransactionBufferExtend<'_> { + fn validate(&self, args: &TransactionBufferExtendArgs) -> Result<()> { + let Self { + multisig, + creator, + transaction_buffer, + .. + } = self; + + // creator is still a member in the multisig + require!( + multisig.is_member(creator.key()).is_some(), + MultisigError::NotAMember + ); + + // creator still has initiate permissions + require!( + multisig.member_has_permission(creator.key(), Permission::Initiate), + MultisigError::Unauthorized + ); + + // Extended Buffer size must not exceed final buffer size + // Calculate remaining space in the buffer + let current_buffer_size = transaction_buffer.buffer.len() as u16; + let remaining_space = transaction_buffer + .final_buffer_size + .checked_sub(current_buffer_size).unwrap(); + + // Check if the new data exceeds the remaining space + let new_data_size = args.buffer.len() as u16; + require!( + new_data_size <= remaining_space, + MultisigError::FinalBufferSizeExceeded + ); + + Ok(()) + } + + /// Create a new vault transaction. + #[access_control(ctx.accounts.validate(&args))] + pub fn transaction_buffer_extend( + ctx: Context, + args: TransactionBufferExtendArgs, + ) -> Result<()> { + // Mutable Accounts + let transaction_buffer = &mut ctx.accounts.transaction_buffer; + + // Required Data + let buffer_slice_extension = args.buffer; + + // Extend the Buffer inside the TransactionBuffer + transaction_buffer + .buffer + .extend_from_slice(&buffer_slice_extension); + + // Invariant function on the transaction buffer + transaction_buffer.invariant()?; + + Ok(()) + } +} diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs new file mode 100644 index 00000000..aaad4201 --- /dev/null +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -0,0 +1,160 @@ +use anchor_lang::prelude::*; + +use crate::errors::*; +use crate::state::*; +use crate::TransactionMessage; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct VaultTransactionCreateFromBufferArgs { + /// Number of ephemeral signing PDAs required by the transaction. + pub ephemeral_signers: u8, + /// Optional Memo + pub memo: Option, +} + +#[derive(Accounts)] +#[instruction(args: VaultTransactionCreateFromBufferArgs)] +pub struct VaultTransactionCreateFromBuffer<'info> { + #[account( + mut, + seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()], + bump = multisig.bump, + )] + pub multisig: Account<'info, Multisig>, + + #[account( + mut, + close = creator, + // Only the creator of the buffer can create a VaultTransaction from it + constraint = transaction_buffer.creator == creator.key(), + seeds = [ + SEED_PREFIX, + multisig.key().as_ref(), + SEED_TRANSACTION_BUFFER, + &multisig.transaction_index.checked_add(1).unwrap().to_le_bytes(), + ], + bump + )] + pub transaction_buffer: Box>, + + #[account( + init, + payer = rent_payer, + space = VaultTransaction::size(args.ephemeral_signers, &transaction_buffer.buffer)?, + seeds = [ + SEED_PREFIX, + multisig.key().as_ref(), + SEED_TRANSACTION, + &multisig.transaction_index.checked_add(1).unwrap().to_le_bytes(), + ], + bump + )] + pub transaction: Account<'info, VaultTransaction>, + + /// The member of the multisig that is creating the transaction. + pub creator: Signer<'info>, + + /// The payer for the transaction account rent. + #[account(mut)] + pub rent_payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +impl VaultTransactionCreateFromBuffer<'_> { + fn validate(&self) -> Result<()> { + let Self { + multisig, + creator, + transaction_buffer, + .. + } = self; + + // creator + require!( + multisig.is_member(creator.key()).is_some(), + MultisigError::NotAMember + ); + require!( + multisig.member_has_permission(creator.key(), Permission::Initiate), + MultisigError::Unauthorized + ); + + // Transaction buffer hash validation + transaction_buffer.validate_hash()?; + // Transaction buffer size validation + transaction_buffer.validate_size()?; + + Ok(()) + } + + /// Create a new vault transaction. + #[access_control(ctx.accounts.validate())] + pub fn vault_transaction_create_from_buffer( + ctx: Context, + args: VaultTransactionCreateFromBufferArgs, + ) -> Result<()> { + // Mutable Accounts + let multisig = &mut ctx.accounts.multisig; + let transaction = &mut ctx.accounts.transaction; + let transaction_buffer = &mut ctx.accounts.transaction_buffer; + + // Readonly Accounts + let creator = &mut ctx.accounts.creator; + + // Data + let vault_index = transaction_buffer.vault_index; + + let transaction_message = + TransactionMessage::deserialize(&mut transaction_buffer.buffer.as_slice())?; + + let multisig_key = multisig.key(); + let transaction_key = transaction.key(); + + let vault_seeds = &[ + SEED_PREFIX, + multisig_key.as_ref(), + SEED_VAULT, + &vault_index.to_le_bytes(), + ]; + let (_, vault_bump) = Pubkey::find_program_address(vault_seeds, ctx.program_id); + + let ephemeral_signer_bumps: Vec = (0..args.ephemeral_signers) + .map(|ephemeral_signer_index| { + let ephemeral_signer_seeds = &[ + SEED_PREFIX, + transaction_key.as_ref(), + SEED_EPHEMERAL_SIGNER, + &ephemeral_signer_index.to_le_bytes(), + ]; + + let (_, bump) = + Pubkey::find_program_address(ephemeral_signer_seeds, ctx.program_id); + bump + }) + .collect(); + + // Increment the transaction index. + let transaction_index = multisig.transaction_index.checked_add(1).unwrap(); + + // Initialize the transaction fields. + transaction.multisig = multisig_key; + transaction.creator = creator.key(); + transaction.index = transaction_index; + transaction.bump = ctx.bumps.transaction; + transaction.vault_index = vault_index; + transaction.vault_bump = vault_bump; + transaction.ephemeral_signer_bumps = ephemeral_signer_bumps; + transaction.message = transaction_message.try_into()?; + + // Updated last transaction index in the multisig account. + multisig.transaction_index = transaction_index; + + multisig.invariant()?; + + // Logs for indexing. + msg!("transaction index: {}", transaction_index); + + Ok(()) + } +} diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index dd7cce5c..fc5ffd87 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -175,6 +175,38 @@ pub mod squads_multisig_program { VaultTransactionCreate::vault_transaction_create(ctx, args) } + /// Create a transaction buffer account. + pub fn transaction_buffer_create( + ctx: Context, + args: TransactionBufferCreateArgs, + ) -> Result<()> { + TransactionBufferCreate::transaction_buffer_create(ctx, args) + } + + /// Close a transaction buffer account. + pub fn transaction_buffer_close( + ctx: Context, + ) -> Result<()> { + TransactionBufferClose::transaction_buffer_close(ctx) + } + + /// Extend a transaction buffer account. + pub fn transaction_buffer_extend( + ctx: Context, + args: TransactionBufferExtendArgs, + ) -> Result<()> { + TransactionBufferExtend::transaction_buffer_extend(ctx, args) + } + + /// Create a new vault transaction from a completed transaction buffer. + /// Finalized buffer hash must match `final_buffer_hash` + pub fn vault_transaction_create_from_buffer( + ctx: Context, + args: VaultTransactionCreateFromBufferArgs, + ) -> Result<()> { + VaultTransactionCreateFromBuffer::vault_transaction_create_from_buffer(ctx, args) + } + /// Execute a vault transaction. /// The transaction must be `Approved`. pub fn vault_transaction_execute(ctx: Context) -> Result<()> { diff --git a/programs/squads_multisig_program/src/state/mod.rs b/programs/squads_multisig_program/src/state/mod.rs index 85e19941..974f3ddf 100644 --- a/programs/squads_multisig_program/src/state/mod.rs +++ b/programs/squads_multisig_program/src/state/mod.rs @@ -6,6 +6,7 @@ pub use proposal::*; pub use seeds::*; pub use spending_limit::*; pub use vault_transaction::*; +pub use transaction_buffer::*; mod batch; mod config_transaction; @@ -15,3 +16,4 @@ mod proposal; mod seeds; mod spending_limit; mod vault_transaction; +mod transaction_buffer; \ No newline at end of file diff --git a/programs/squads_multisig_program/src/state/seeds.rs b/programs/squads_multisig_program/src/state/seeds.rs index e44e4ad0..c3b201b1 100644 --- a/programs/squads_multisig_program/src/state/seeds.rs +++ b/programs/squads_multisig_program/src/state/seeds.rs @@ -7,3 +7,4 @@ pub const SEED_BATCH_TRANSACTION: &[u8] = b"batch_transaction"; pub const SEED_VAULT: &[u8] = b"vault"; pub const SEED_EPHEMERAL_SIGNER: &[u8] = b"ephemeral_signer"; pub const SEED_SPENDING_LIMIT: &[u8] = b"spending_limit"; +pub const SEED_TRANSACTION_BUFFER: &[u8] = b"transaction_buffer"; \ No newline at end of file diff --git a/programs/squads_multisig_program/src/state/transaction_buffer.rs b/programs/squads_multisig_program/src/state/transaction_buffer.rs new file mode 100644 index 00000000..caea14e5 --- /dev/null +++ b/programs/squads_multisig_program/src/state/transaction_buffer.rs @@ -0,0 +1,81 @@ +use anchor_lang::prelude::*; +use anchor_lang::solana_program::hash::hash; + +use crate::errors::MultisigError; + +// Since VaultTransaction doesn't implement zero-copy, we are limited to +// deserializing the account onto the Stack. This means we are limited to a +// theoretical max size of 4KiB +pub const MAX_BUFFER_SIZE: usize = 4000; + +#[account] +#[derive(Default, Debug)] +pub struct TransactionBuffer { + /// The multisig this belongs to. + pub multisig: Pubkey, + /// Member of the Multisig who created the TransactionBuffer. + pub creator: Pubkey, + /// Vault index of the transaction this buffer belongs to. + pub vault_index: u8, + /// Index of the transaction this buffer belongs to. + pub transaction_index: u64, + /// Hash of the final assembled transaction message. + pub final_buffer_hash: [u8; 32], + /// The size of the final assembled transaction message. + pub final_buffer_size: u16, + /// The buffer of the transaction message. + pub buffer: Vec, +} + +impl TransactionBuffer { + pub fn size(final_message_buffer_size: u16) -> Result { + // Make sure final size is not greater than MAX_BUFFER_SIZE bytes. + if (final_message_buffer_size as usize) > MAX_BUFFER_SIZE { + return err!(MultisigError::FinalBufferSizeExceeded); + } + Ok( + 8 + // anchor account discriminator + 32 + // multisig + 32 + // creator + 8 + // vault_index + 8 + // transaction_index + 32 + // transaction_message_hash + 2 + // final_buffer_size + final_message_buffer_size as usize, // transaction_message + ) + } + + pub fn validate_hash(&self) -> Result<()> { + let message_buffer_hash = hash(&self.buffer); + require!( + message_buffer_hash.to_bytes() == self.final_buffer_hash, + MultisigError::FinalBufferHashMismatch + ); + Ok(()) + } + pub fn validate_size(&self) -> Result<()> { + require_eq!( + self.buffer.len(), + self.final_buffer_size as usize, + MultisigError::FinalBufferSizeMismatch + ); + Ok(()) + } + + pub fn invariant(&self) -> Result<()> { + require!( + self.final_buffer_size as usize <= MAX_BUFFER_SIZE, + MultisigError::FinalBufferSizeExceeded + ); + require!( + self.buffer.len() < MAX_BUFFER_SIZE, + MultisigError::FinalBufferSizeExceeded + ); + require!( + self.buffer.len() <= self.final_buffer_size as usize, + MultisigError::FinalBufferSizeMismatch + ); + + Ok(()) + } +} diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index 0ce53974..38b80e98 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -755,6 +755,167 @@ } ] }, + { + "name": "transactionBufferCreate", + "docs": [ + "Create a transaction buffer account." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transactionBuffer", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that is creating the transaction." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "The payer for the transaction account rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "TransactionBufferCreateArgs" + } + } + ] + }, + { + "name": "transactionBufferClose", + "docs": [ + "Close a transaction buffer account." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transactionBuffer", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that created the TransactionBuffer." + ] + } + ], + "args": [] + }, + { + "name": "transactionBufferExtend", + "docs": [ + "Extend a transaction buffer account." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transactionBuffer", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that created the TransactionBuffer." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "TransactionBufferExtendArgs" + } + } + ] + }, + { + "name": "vaultTransactionCreateFromBuffer", + "docs": [ + "Create a new vault transaction from a completed transaction buffer.", + "Finalized buffer hash must match `final_buffer_hash`" + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transactionBuffer", + "isMut": true, + "isSigner": false + }, + { + "name": "transaction", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that is creating the transaction." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "The payer for the transaction account rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "VaultTransactionCreateFromBufferArgs" + } + } + ] + }, { "name": "vaultTransactionExecute", "docs": [ @@ -1127,7 +1288,7 @@ "may have lingering votes, and the proposal size may need to be reallocated to", "accommodate the new amount of cancel votes.", "The previous implemenation still works if the proposal size is in line with the", - "thresholdhold size." + "threshold size." ], "accounts": [ { @@ -1917,6 +2078,68 @@ ] } }, + { + "name": "TransactionBuffer", + "type": { + "kind": "struct", + "fields": [ + { + "name": "multisig", + "docs": [ + "The multisig this belongs to." + ], + "type": "publicKey" + }, + { + "name": "creator", + "docs": [ + "Member of the Multisig who created the TransactionBuffer." + ], + "type": "publicKey" + }, + { + "name": "vaultIndex", + "docs": [ + "Vault index of the transaction this buffer belongs to." + ], + "type": "u8" + }, + { + "name": "transactionIndex", + "docs": [ + "Index of the transaction this buffer belongs to." + ], + "type": "u64" + }, + { + "name": "finalBufferHash", + "docs": [ + "Hash of the final assembled transaction message." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "finalBufferSize", + "docs": [ + "The size of the final assembled transaction message." + ], + "type": "u16" + }, + { + "name": "buffer", + "docs": [ + "The buffer of the transaction message." + ], + "type": "bytes" + } + ] + } + }, { "name": "VaultTransaction", "docs": [ @@ -2528,6 +2751,83 @@ ] } }, + { + "name": "TransactionBufferCreateArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vaultIndex", + "docs": [ + "Index of the vault this transaction belongs to." + ], + "type": "u8" + }, + { + "name": "finalBufferHash", + "docs": [ + "Hash of the final assembled transaction message." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "finalBufferSize", + "docs": [ + "Final size of the buffer." + ], + "type": "u16" + }, + { + "name": "buffer", + "docs": [ + "Initial slice of the buffer." + ], + "type": "bytes" + } + ] + } + }, + { + "name": "TransactionBufferExtendArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "buffer", + "type": "bytes" + } + ] + } + }, + { + "name": "VaultTransactionCreateFromBufferArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "ephemeralSigners", + "docs": [ + "Number of ephemeral signing PDAs required by the transaction." + ], + "type": "u8" + }, + { + "name": "memo", + "docs": [ + "Optional Memo" + ], + "type": { + "option": "string" + } + } + ] + } + }, { "name": "VaultTransactionCreateArgs", "type": { @@ -3168,6 +3468,21 @@ "code": 6039, "name": "SpendingLimitInvalidAmount", "msg": "Invalid SpendingLimit amount" + }, + { + "code": 6040, + "name": "FinalBufferHashMismatch", + "msg": "Final message buffer hash doesnt match the expected hash" + }, + { + "code": 6041, + "name": "FinalBufferSizeExceeded", + "msg": "Final buffer size cannot exceed 4000 bytes" + }, + { + "code": 6042, + "name": "FinalBufferSizeMismatch", + "msg": "Final buffer size mismatch" } ], "metadata": { diff --git a/sdk/multisig/src/generated/accounts/TransactionBuffer.ts b/sdk/multisig/src/generated/accounts/TransactionBuffer.ts new file mode 100644 index 00000000..7b5fba60 --- /dev/null +++ b/sdk/multisig/src/generated/accounts/TransactionBuffer.ts @@ -0,0 +1,211 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as web3 from '@solana/web3.js' +import * as beet from '@metaplex-foundation/beet' +import * as beetSolana from '@metaplex-foundation/beet-solana' + +/** + * Arguments used to create {@link TransactionBuffer} + * @category Accounts + * @category generated + */ +export type TransactionBufferArgs = { + multisig: web3.PublicKey + creator: web3.PublicKey + vaultIndex: number + transactionIndex: beet.bignum + finalBufferHash: number[] /* size: 32 */ + finalBufferSize: number + buffer: Uint8Array +} + +export const transactionBufferDiscriminator = [ + 90, 36, 35, 219, 93, 225, 110, 96, +] +/** + * Holds the data for the {@link TransactionBuffer} Account and provides de/serialization + * functionality for that data + * + * @category Accounts + * @category generated + */ +export class TransactionBuffer implements TransactionBufferArgs { + private constructor( + readonly multisig: web3.PublicKey, + readonly creator: web3.PublicKey, + readonly vaultIndex: number, + readonly transactionIndex: beet.bignum, + readonly finalBufferHash: number[] /* size: 32 */, + readonly finalBufferSize: number, + readonly buffer: Uint8Array + ) {} + + /** + * Creates a {@link TransactionBuffer} instance from the provided args. + */ + static fromArgs(args: TransactionBufferArgs) { + return new TransactionBuffer( + args.multisig, + args.creator, + args.vaultIndex, + args.transactionIndex, + args.finalBufferHash, + args.finalBufferSize, + args.buffer + ) + } + + /** + * Deserializes the {@link TransactionBuffer} from the data of the provided {@link web3.AccountInfo}. + * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. + */ + static fromAccountInfo( + accountInfo: web3.AccountInfo, + offset = 0 + ): [TransactionBuffer, number] { + return TransactionBuffer.deserialize(accountInfo.data, offset) + } + + /** + * Retrieves the account info from the provided address and deserializes + * the {@link TransactionBuffer} from its data. + * + * @throws Error if no account info is found at the address or if deserialization fails + */ + static async fromAccountAddress( + connection: web3.Connection, + address: web3.PublicKey, + commitmentOrConfig?: web3.Commitment | web3.GetAccountInfoConfig + ): Promise { + const accountInfo = await connection.getAccountInfo( + address, + commitmentOrConfig + ) + if (accountInfo == null) { + throw new Error(`Unable to find TransactionBuffer account at ${address}`) + } + return TransactionBuffer.fromAccountInfo(accountInfo, 0)[0] + } + + /** + * Provides a {@link web3.Connection.getProgramAccounts} config builder, + * to fetch accounts matching filters that can be specified via that builder. + * + * @param programId - the program that owns the accounts we are filtering + */ + static gpaBuilder( + programId: web3.PublicKey = new web3.PublicKey( + 'SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf' + ) + ) { + return beetSolana.GpaBuilder.fromStruct(programId, transactionBufferBeet) + } + + /** + * Deserializes the {@link TransactionBuffer} from the provided data Buffer. + * @returns a tuple of the account data and the offset up to which the buffer was read to obtain it. + */ + static deserialize(buf: Buffer, offset = 0): [TransactionBuffer, number] { + return transactionBufferBeet.deserialize(buf, offset) + } + + /** + * Serializes the {@link TransactionBuffer} into a Buffer. + * @returns a tuple of the created Buffer and the offset up to which the buffer was written to store it. + */ + serialize(): [Buffer, number] { + return transactionBufferBeet.serialize({ + accountDiscriminator: transactionBufferDiscriminator, + ...this, + }) + } + + /** + * Returns the byteSize of a {@link Buffer} holding the serialized data of + * {@link TransactionBuffer} for the provided args. + * + * @param args need to be provided since the byte size for this account + * depends on them + */ + static byteSize(args: TransactionBufferArgs) { + const instance = TransactionBuffer.fromArgs(args) + return transactionBufferBeet.toFixedFromValue({ + accountDiscriminator: transactionBufferDiscriminator, + ...instance, + }).byteSize + } + + /** + * Fetches the minimum balance needed to exempt an account holding + * {@link TransactionBuffer} data from rent + * + * @param args need to be provided since the byte size for this account + * depends on them + * @param connection used to retrieve the rent exemption information + */ + static async getMinimumBalanceForRentExemption( + args: TransactionBufferArgs, + connection: web3.Connection, + commitment?: web3.Commitment + ): Promise { + return connection.getMinimumBalanceForRentExemption( + TransactionBuffer.byteSize(args), + commitment + ) + } + + /** + * Returns a readable version of {@link TransactionBuffer} properties + * and can be used to convert to JSON and/or logging + */ + pretty() { + return { + multisig: this.multisig.toBase58(), + creator: this.creator.toBase58(), + vaultIndex: this.vaultIndex, + transactionIndex: (() => { + const x = <{ toNumber: () => number }>this.transactionIndex + if (typeof x.toNumber === 'function') { + try { + return x.toNumber() + } catch (_) { + return x + } + } + return x + })(), + finalBufferHash: this.finalBufferHash, + finalBufferSize: this.finalBufferSize, + buffer: this.buffer, + } + } +} + +/** + * @category Accounts + * @category generated + */ +export const transactionBufferBeet = new beet.FixableBeetStruct< + TransactionBuffer, + TransactionBufferArgs & { + accountDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['accountDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['multisig', beetSolana.publicKey], + ['creator', beetSolana.publicKey], + ['vaultIndex', beet.u8], + ['transactionIndex', beet.u64], + ['finalBufferHash', beet.uniformFixedSizeArray(beet.u8, 32)], + ['finalBufferSize', beet.u16], + ['buffer', beet.bytes], + ], + TransactionBuffer.fromArgs, + 'TransactionBuffer' +) diff --git a/sdk/multisig/src/generated/accounts/index.ts b/sdk/multisig/src/generated/accounts/index.ts index 10449a64..d5e0eab1 100644 --- a/sdk/multisig/src/generated/accounts/index.ts +++ b/sdk/multisig/src/generated/accounts/index.ts @@ -4,6 +4,7 @@ export * from './Multisig' export * from './ProgramConfig' export * from './Proposal' export * from './SpendingLimit' +export * from './TransactionBuffer' export * from './VaultBatchTransaction' export * from './VaultTransaction' @@ -14,6 +15,7 @@ import { Multisig } from './Multisig' import { ProgramConfig } from './ProgramConfig' import { Proposal } from './Proposal' import { SpendingLimit } from './SpendingLimit' +import { TransactionBuffer } from './TransactionBuffer' import { VaultTransaction } from './VaultTransaction' export const accountProviders = { @@ -24,5 +26,6 @@ export const accountProviders = { ProgramConfig, Proposal, SpendingLimit, + TransactionBuffer, VaultTransaction, } diff --git a/sdk/multisig/src/generated/errors/index.ts b/sdk/multisig/src/generated/errors/index.ts index c9050b8b..aa26cfb9 100644 --- a/sdk/multisig/src/generated/errors/index.ts +++ b/sdk/multisig/src/generated/errors/index.ts @@ -921,6 +921,75 @@ createErrorFromNameLookup.set( () => new SpendingLimitInvalidAmountError() ) +/** + * FinalBufferHashMismatch: 'Final message buffer hash doesnt match the expected hash' + * + * @category Errors + * @category generated + */ +export class FinalBufferHashMismatchError extends Error { + readonly code: number = 0x1798 + readonly name: string = 'FinalBufferHashMismatch' + constructor() { + super('Final message buffer hash doesnt match the expected hash') + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, FinalBufferHashMismatchError) + } + } +} + +createErrorFromCodeLookup.set(0x1798, () => new FinalBufferHashMismatchError()) +createErrorFromNameLookup.set( + 'FinalBufferHashMismatch', + () => new FinalBufferHashMismatchError() +) + +/** + * FinalBufferSizeExceeded: 'Final buffer size cannot exceed 4000 bytes' + * + * @category Errors + * @category generated + */ +export class FinalBufferSizeExceededError extends Error { + readonly code: number = 0x1799 + readonly name: string = 'FinalBufferSizeExceeded' + constructor() { + super('Final buffer size cannot exceed 4000 bytes') + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, FinalBufferSizeExceededError) + } + } +} + +createErrorFromCodeLookup.set(0x1799, () => new FinalBufferSizeExceededError()) +createErrorFromNameLookup.set( + 'FinalBufferSizeExceeded', + () => new FinalBufferSizeExceededError() +) + +/** + * FinalBufferSizeMismatch: 'Final buffer size mismatch' + * + * @category Errors + * @category generated + */ +export class FinalBufferSizeMismatchError extends Error { + readonly code: number = 0x179a + readonly name: string = 'FinalBufferSizeMismatch' + constructor() { + super('Final buffer size mismatch') + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, FinalBufferSizeMismatchError) + } + } +} + +createErrorFromCodeLookup.set(0x179a, () => new FinalBufferSizeMismatchError()) +createErrorFromNameLookup.set( + 'FinalBufferSizeMismatch', + () => new FinalBufferSizeMismatchError() +) + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/sdk/multisig/src/generated/instructions/index.ts b/sdk/multisig/src/generated/instructions/index.ts index 30f3cedb..9f89535d 100644 --- a/sdk/multisig/src/generated/instructions/index.ts +++ b/sdk/multisig/src/generated/instructions/index.ts @@ -26,7 +26,11 @@ export * from './proposalCancelV2' export * from './proposalCreate' export * from './proposalReject' export * from './spendingLimitUse' +export * from './transactionBufferClose' +export * from './transactionBufferCreate' +export * from './transactionBufferExtend' export * from './vaultBatchTransactionAccountClose' export * from './vaultTransactionAccountsClose' export * from './vaultTransactionCreate' +export * from './vaultTransactionCreateFromBuffer' export * from './vaultTransactionExecute' diff --git a/sdk/multisig/src/generated/instructions/transactionBufferClose.ts b/sdk/multisig/src/generated/instructions/transactionBufferClose.ts new file mode 100644 index 00000000..0e84493b --- /dev/null +++ b/sdk/multisig/src/generated/instructions/transactionBufferClose.ts @@ -0,0 +1,88 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' + +/** + * @category Instructions + * @category TransactionBufferClose + * @category generated + */ +export const transactionBufferCloseStruct = new beet.BeetArgsStruct<{ + instructionDiscriminator: number[] /* size: 8 */ +}>( + [['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)]], + 'TransactionBufferCloseInstructionArgs' +) +/** + * Accounts required by the _transactionBufferClose_ instruction + * + * @property [_writable_] multisig + * @property [_writable_] transactionBuffer + * @property [**signer**] creator + * @category Instructions + * @category TransactionBufferClose + * @category generated + */ +export type TransactionBufferCloseInstructionAccounts = { + multisig: web3.PublicKey + transactionBuffer: web3.PublicKey + creator: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const transactionBufferCloseInstructionDiscriminator = [ + 17, 182, 208, 228, 136, 24, 178, 102, +] + +/** + * Creates a _TransactionBufferClose_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @category Instructions + * @category TransactionBufferClose + * @category generated + */ +export function createTransactionBufferCloseInstruction( + accounts: TransactionBufferCloseInstructionAccounts, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = transactionBufferCloseStruct.serialize({ + instructionDiscriminator: transactionBufferCloseInstructionDiscriminator, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.multisig, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.transactionBuffer, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.creator, + isWritable: false, + isSigner: true, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/multisig/src/generated/instructions/transactionBufferCreate.ts b/sdk/multisig/src/generated/instructions/transactionBufferCreate.ts new file mode 100644 index 00000000..29b53256 --- /dev/null +++ b/sdk/multisig/src/generated/instructions/transactionBufferCreate.ts @@ -0,0 +1,122 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' +import { + TransactionBufferCreateArgs, + transactionBufferCreateArgsBeet, +} from '../types/TransactionBufferCreateArgs' + +/** + * @category Instructions + * @category TransactionBufferCreate + * @category generated + */ +export type TransactionBufferCreateInstructionArgs = { + args: TransactionBufferCreateArgs +} +/** + * @category Instructions + * @category TransactionBufferCreate + * @category generated + */ +export const transactionBufferCreateStruct = new beet.FixableBeetArgsStruct< + TransactionBufferCreateInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['args', transactionBufferCreateArgsBeet], + ], + 'TransactionBufferCreateInstructionArgs' +) +/** + * Accounts required by the _transactionBufferCreate_ instruction + * + * @property [_writable_] multisig + * @property [_writable_] transactionBuffer + * @property [**signer**] creator + * @property [_writable_, **signer**] rentPayer + * @category Instructions + * @category TransactionBufferCreate + * @category generated + */ +export type TransactionBufferCreateInstructionAccounts = { + multisig: web3.PublicKey + transactionBuffer: web3.PublicKey + creator: web3.PublicKey + rentPayer: web3.PublicKey + systemProgram?: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const transactionBufferCreateInstructionDiscriminator = [ + 245, 201, 113, 108, 37, 63, 29, 89, +] + +/** + * Creates a _TransactionBufferCreate_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category TransactionBufferCreate + * @category generated + */ +export function createTransactionBufferCreateInstruction( + accounts: TransactionBufferCreateInstructionAccounts, + args: TransactionBufferCreateInstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = transactionBufferCreateStruct.serialize({ + instructionDiscriminator: transactionBufferCreateInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.multisig, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.transactionBuffer, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.creator, + isWritable: false, + isSigner: true, + }, + { + pubkey: accounts.rentPayer, + isWritable: true, + isSigner: true, + }, + { + pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/multisig/src/generated/instructions/transactionBufferExtend.ts b/sdk/multisig/src/generated/instructions/transactionBufferExtend.ts new file mode 100644 index 00000000..0f1acec0 --- /dev/null +++ b/sdk/multisig/src/generated/instructions/transactionBufferExtend.ts @@ -0,0 +1,109 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' +import { + TransactionBufferExtendArgs, + transactionBufferExtendArgsBeet, +} from '../types/TransactionBufferExtendArgs' + +/** + * @category Instructions + * @category TransactionBufferExtend + * @category generated + */ +export type TransactionBufferExtendInstructionArgs = { + args: TransactionBufferExtendArgs +} +/** + * @category Instructions + * @category TransactionBufferExtend + * @category generated + */ +export const transactionBufferExtendStruct = new beet.FixableBeetArgsStruct< + TransactionBufferExtendInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['args', transactionBufferExtendArgsBeet], + ], + 'TransactionBufferExtendInstructionArgs' +) +/** + * Accounts required by the _transactionBufferExtend_ instruction + * + * @property [_writable_] multisig + * @property [_writable_] transactionBuffer + * @property [**signer**] creator + * @category Instructions + * @category TransactionBufferExtend + * @category generated + */ +export type TransactionBufferExtendInstructionAccounts = { + multisig: web3.PublicKey + transactionBuffer: web3.PublicKey + creator: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const transactionBufferExtendInstructionDiscriminator = [ + 230, 157, 67, 56, 5, 238, 245, 146, +] + +/** + * Creates a _TransactionBufferExtend_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category TransactionBufferExtend + * @category generated + */ +export function createTransactionBufferExtendInstruction( + accounts: TransactionBufferExtendInstructionAccounts, + args: TransactionBufferExtendInstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = transactionBufferExtendStruct.serialize({ + instructionDiscriminator: transactionBufferExtendInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.multisig, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.transactionBuffer, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.creator, + isWritable: false, + isSigner: true, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts b/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts new file mode 100644 index 00000000..6156118f --- /dev/null +++ b/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts @@ -0,0 +1,131 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' +import { + VaultTransactionCreateFromBufferArgs, + vaultTransactionCreateFromBufferArgsBeet, +} from '../types/VaultTransactionCreateFromBufferArgs' + +/** + * @category Instructions + * @category VaultTransactionCreateFromBuffer + * @category generated + */ +export type VaultTransactionCreateFromBufferInstructionArgs = { + args: VaultTransactionCreateFromBufferArgs +} +/** + * @category Instructions + * @category VaultTransactionCreateFromBuffer + * @category generated + */ +export const vaultTransactionCreateFromBufferStruct = + new beet.FixableBeetArgsStruct< + VaultTransactionCreateFromBufferInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } + >( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['args', vaultTransactionCreateFromBufferArgsBeet], + ], + 'VaultTransactionCreateFromBufferInstructionArgs' + ) +/** + * Accounts required by the _vaultTransactionCreateFromBuffer_ instruction + * + * @property [_writable_] multisig + * @property [_writable_] transactionBuffer + * @property [_writable_] transaction + * @property [**signer**] creator + * @property [_writable_, **signer**] rentPayer + * @category Instructions + * @category VaultTransactionCreateFromBuffer + * @category generated + */ +export type VaultTransactionCreateFromBufferInstructionAccounts = { + multisig: web3.PublicKey + transactionBuffer: web3.PublicKey + transaction: web3.PublicKey + creator: web3.PublicKey + rentPayer: web3.PublicKey + systemProgram?: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const vaultTransactionCreateFromBufferInstructionDiscriminator = [ + 222, 54, 149, 68, 87, 246, 48, 231, +] + +/** + * Creates a _VaultTransactionCreateFromBuffer_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category VaultTransactionCreateFromBuffer + * @category generated + */ +export function createVaultTransactionCreateFromBufferInstruction( + accounts: VaultTransactionCreateFromBufferInstructionAccounts, + args: VaultTransactionCreateFromBufferInstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = vaultTransactionCreateFromBufferStruct.serialize({ + instructionDiscriminator: + vaultTransactionCreateFromBufferInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.multisig, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.transactionBuffer, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.transaction, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.creator, + isWritable: false, + isSigner: true, + }, + { + pubkey: accounts.rentPayer, + isWritable: true, + isSigner: true, + }, + { + pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/multisig/src/generated/types/TransactionBufferCreateArgs.ts b/sdk/multisig/src/generated/types/TransactionBufferCreateArgs.ts new file mode 100644 index 00000000..480424d2 --- /dev/null +++ b/sdk/multisig/src/generated/types/TransactionBufferCreateArgs.ts @@ -0,0 +1,29 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +export type TransactionBufferCreateArgs = { + vaultIndex: number + finalBufferHash: number[] /* size: 32 */ + finalBufferSize: number + buffer: Uint8Array +} + +/** + * @category userTypes + * @category generated + */ +export const transactionBufferCreateArgsBeet = + new beet.FixableBeetArgsStruct( + [ + ['vaultIndex', beet.u8], + ['finalBufferHash', beet.uniformFixedSizeArray(beet.u8, 32)], + ['finalBufferSize', beet.u16], + ['buffer', beet.bytes], + ], + 'TransactionBufferCreateArgs' + ) diff --git a/sdk/multisig/src/generated/types/TransactionBufferExtendArgs.ts b/sdk/multisig/src/generated/types/TransactionBufferExtendArgs.ts new file mode 100644 index 00000000..404076e5 --- /dev/null +++ b/sdk/multisig/src/generated/types/TransactionBufferExtendArgs.ts @@ -0,0 +1,21 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +export type TransactionBufferExtendArgs = { + buffer: Uint8Array +} + +/** + * @category userTypes + * @category generated + */ +export const transactionBufferExtendArgsBeet = + new beet.FixableBeetArgsStruct( + [['buffer', beet.bytes]], + 'TransactionBufferExtendArgs' + ) diff --git a/sdk/multisig/src/generated/types/VaultTransactionCreateFromBufferArgs.ts b/sdk/multisig/src/generated/types/VaultTransactionCreateFromBufferArgs.ts new file mode 100644 index 00000000..4238cf48 --- /dev/null +++ b/sdk/multisig/src/generated/types/VaultTransactionCreateFromBufferArgs.ts @@ -0,0 +1,25 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +export type VaultTransactionCreateFromBufferArgs = { + ephemeralSigners: number + memo: beet.COption +} + +/** + * @category userTypes + * @category generated + */ +export const vaultTransactionCreateFromBufferArgsBeet = + new beet.FixableBeetArgsStruct( + [ + ['ephemeralSigners', beet.u8], + ['memo', beet.coption(beet.utf8String)], + ], + 'VaultTransactionCreateFromBufferArgs' + ) diff --git a/sdk/multisig/src/generated/types/index.ts b/sdk/multisig/src/generated/types/index.ts index c90dc772..5bb4d22a 100644 --- a/sdk/multisig/src/generated/types/index.ts +++ b/sdk/multisig/src/generated/types/index.ts @@ -25,6 +25,9 @@ export * from './ProposalCreateArgs' export * from './ProposalStatus' export * from './ProposalVoteArgs' export * from './SpendingLimitUseArgs' +export * from './TransactionBufferCreateArgs' +export * from './TransactionBufferExtendArgs' export * from './VaultTransactionCreateArgs' +export * from './VaultTransactionCreateFromBufferArgs' export * from './VaultTransactionMessage' export * from './Vote' diff --git a/tests/index.ts b/tests/index.ts index 6a952c63..74846b1a 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -8,6 +8,10 @@ import "./suites/instructions/configTransactionAccountsClose"; import "./suites/instructions/vaultBatchTransactionAccountClose"; import "./suites/instructions/batchAccountsClose"; import "./suites/instructions/vaultTransactionAccountsClose"; +import "./suites/instructions/transactionBufferClose"; +import "./suites/instructions/transactionBufferCreate"; +import "./suites/instructions/transactionBufferExtend"; +import "./suites/instructions/vaultTransactionCreateFromBuffer"; import "./suites/instructions/cancelRealloc"; import "./suites/multisig-sdk"; import "./suites/account-migrations"; @@ -15,3 +19,4 @@ import "./suites/examples/batch-sol-transfer"; import "./suites/examples/create-mint"; import "./suites/examples/immediate-execution"; import "./suites/examples/spending-limits"; +import "./suites/examples/transaction-buffer"; \ No newline at end of file diff --git a/tests/suites/examples/transaction-buffer.ts b/tests/suites/examples/transaction-buffer.ts new file mode 100644 index 00000000..3e9f7609 --- /dev/null +++ b/tests/suites/examples/transaction-buffer.ts @@ -0,0 +1,355 @@ +import * as multisig from "@sqds/multisig"; +import * as crypto from "crypto"; +import assert from "assert"; +import { BN } from "bn.js"; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + TransactionMessage, + VersionedTransaction, + SystemProgram, + Transaction, +} from "@solana/web3.js"; +import { + createAutonomousMultisigV2, + createLocalhostConnection, + createTestTransferInstruction, + generateMultisigMembers, + getLogs, + getTestProgramId, + TestMembers, +} from "../../utils"; +import { + TransactionBufferCreateArgs, + TransactionBufferCreateInstructionArgs, + TransactionBufferExtendArgs, + TransactionBufferExtendInstructionArgs, + VaultTransactionCreateFromBufferArgs, + VaultTransactionCreateFromBufferInstructionArgs, +} from "@sqds/multisig/lib/generated"; + +const programId = getTestProgramId(); + +describe("Examples / Transaction Buffers", () => { + const connection = createLocalhostConnection(); + + let members: TestMembers; + + const createKey = Keypair.generate(); + + let multisigPda = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + })[0]; + + const [vaultPda] = multisig.getVaultPda({ + multisigPda, + index: 0, + programId, + }); + + before(async () => { + members = await generateMultisigMembers(connection); + + multisigPda = ( + await createAutonomousMultisigV2({ + connection, + members: members, + createKey: createKey, + threshold: 1, + timeLock: 0, + programId, + rentCollector: vaultPda, + }) + )[0]; + + // Airdrop some SOL to the vault + let signature = await connection.requestAirdrop( + vaultPda, + 10 * LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(signature); + }); + + it("set buffer, extend, and create", async () => { + const transactionIndex = 1n; + + const testIx = createTestTransferInstruction(vaultPda, vaultPda, 1); + + let instructions = []; + + // Add 32 transfer instructions to the message. + for (let i = 0; i <= 22; i++) { + instructions.push(testIx); + } + + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: instructions, + }); + + // Serialize the message. Must be done with this util function + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + // Slice the message buffer into two parts. + const firstSlice = messageBuffer.slice(0, 400); + + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.almighty.publicKey, + rentPayer: members.almighty.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + // Must be a SHA256 hash of the message buffer. + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: firstSlice, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + const message = new TransactionMessage({ + payerKey: members.almighty.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + + tx.sign([members.almighty]); + + // Send first transaction. + const signature = await connection.sendTransaction(tx, { + skipPreflight: true, + }); + await connection.confirmTransaction(signature); + + const transactionBufferAccount = await connection.getAccountInfo( + transactionBuffer + ); + + // Check buffer account exists. + assert.notEqual(transactionBufferAccount, null); + assert.ok(transactionBufferAccount?.data.length! > 0); + + // Need to add some deserialization to check if it actually worked. + const transactionBufferInfo1 = await connection.getAccountInfo( + transactionBuffer + ); + const [txBufferDeser1] = + await multisig.generated.TransactionBuffer.fromAccountInfo( + transactionBufferInfo1! + ); + + // First chunk uploaded. Check that length is as expected. + assert.equal(txBufferDeser1.buffer.length, 400); + + const secondSlice = messageBuffer.slice(400, messageBuffer.byteLength); + + // Extned the buffer. + const secondIx = + multisig.generated.createTransactionBufferExtendInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.almighty.publicKey, + }, + { + args: { + buffer: secondSlice, + } as TransactionBufferExtendArgs, + } as TransactionBufferExtendInstructionArgs, + programId + ); + + const secondMessage = new TransactionMessage({ + payerKey: members.almighty.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [secondIx], + }).compileToV0Message(); + + const secondTx = new VersionedTransaction(secondMessage); + + secondTx.sign([members.almighty]); + + // Send second transaction to extend. + const secondSignature = await connection.sendTransaction(secondTx, { + skipPreflight: true, + }); + + await connection.confirmTransaction(secondSignature); + + // Need to add some deserialization to check if it actually worked. + const transactionBufferInfo2 = await connection.getAccountInfo( + transactionBuffer + ); + const [txBufferDeser2] = + await multisig.generated.TransactionBuffer.fromAccountInfo( + transactionBufferInfo2! + ); + + // Full buffer uploaded. Check that length is as expected. + assert.equal(txBufferDeser2.buffer.length, messageBuffer.byteLength); + + // Derive vault transaction PDA. + const [transactionPda] = multisig.getTransactionPda({ + multisigPda, + index: transactionIndex, + programId, + }); + + // Create final instruction. + const thirdIx = + multisig.generated.createVaultTransactionCreateFromBufferInstruction( + { + multisig: multisigPda, + transactionBuffer, + transaction: transactionPda, + creator: members.almighty.publicKey, + rentPayer: members.almighty.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + ephemeralSigners: 0, + memo: null, + } as VaultTransactionCreateFromBufferArgs, + } as VaultTransactionCreateFromBufferInstructionArgs, + programId + ); + + // Add third instruction to the message. + const thirdMessage = new TransactionMessage({ + payerKey: members.almighty.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [thirdIx], + }).compileToV0Message(); + + const thirdTx = new VersionedTransaction(thirdMessage); + + thirdTx.sign([members.almighty]); + + // Send final transaction. + const thirdSignature = await connection.sendTransaction(thirdTx, { + skipPreflight: true, + }); + + await connection.confirmTransaction(thirdSignature); + + const transactionInfo = + await multisig.accounts.VaultTransaction.fromAccountAddress( + connection, + transactionPda + ); + + // Ensure final vault transaction has 23 instructions + assert.equal(transactionInfo.message.instructions.length, 23); + }); + + it("create proposal, approve, execute from buffer derived transaction", async () => { + const transactionIndex = 1n; + + // Derive vault transaction PDA. + const [transactionPda] = multisig.getTransactionPda({ + multisigPda, + index: transactionIndex, + programId, + }); + + const transactionInfo = + await multisig.accounts.VaultTransaction.fromAccountAddress( + connection, + transactionPda + ); + + // Check that we're dealing with the same account from last test. + assert.equal(transactionInfo.message.instructions.length, 23); + + const [proposalPda] = multisig.getProposalPda({ + multisigPda, + transactionIndex, + programId, + }); + + const signature = await multisig.rpc.proposalCreate({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex, + creator: members.almighty, + isDraft: false, + programId, + }); + await connection.confirmTransaction(signature); + + const signature3 = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex, + member: members.almighty, + programId, + }); + await connection.confirmTransaction(signature3); + + // Fetch the proposal account. + let proposalAccount1 = await multisig.accounts.Proposal.fromAccountAddress( + connection, + proposalPda + ); + + const ix = await multisig.instructions.vaultTransactionExecute({ + connection, + multisigPda, + transactionIndex, + member: members.almighty.publicKey, + programId, + }); + + const tx = new Transaction().add(ix.instruction); + const signature4 = await connection.sendTransaction( + tx, + [members.almighty], + { skipPreflight: true } + ); + + await connection.confirmTransaction(signature4); + + // Fetch the proposal account. + let proposalAccount = await multisig.accounts.Proposal.fromAccountAddress( + connection, + proposalPda + ); + + // Check status. + assert.equal(proposalAccount.status.__kind, "Executed"); + }); +}); diff --git a/tests/suites/instructions/transactionBufferClose.ts b/tests/suites/instructions/transactionBufferClose.ts new file mode 100644 index 00000000..d9e6dce5 --- /dev/null +++ b/tests/suites/instructions/transactionBufferClose.ts @@ -0,0 +1,180 @@ +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import * as multisig from "@sqds/multisig"; +import { + TransactionBufferCreateArgs, + TransactionBufferCreateInstructionArgs, +} from "@sqds/multisig/lib/generated"; +import assert from "assert"; +import { BN } from "bn.js"; +import * as crypto from "crypto"; +import { + TestMembers, + createAutonomousMultisigV2, + createLocalhostConnection, + createTestTransferInstruction, + generateMultisigMembers, + getTestProgramId, +} from "../../utils"; + +const programId = getTestProgramId(); +const connection = createLocalhostConnection(); + +describe("Instructions / transaction_buffer_close", () => { + let members: TestMembers; + let multisigPda: PublicKey; + let vaultPda: PublicKey; + let transactionBuffer: PublicKey; + + const createKey = Keypair.generate(); + + before(async () => { + members = await generateMultisigMembers(connection); + + multisigPda = (await createAutonomousMultisigV2({ + connection, + createKey, + members, + threshold: 2, + timeLock: 0, + rentCollector: vaultPda, + programId, + }))[0]; + + [vaultPda] = multisig.getVaultPda({ + multisigPda, + index: 0, + programId, + }); + + let signature = await connection.requestAirdrop( + vaultPda, + 10 * LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(signature); + + const transactionIndex = 1n; + const testIx = await createTestTransferInstruction( + vaultPda, + Keypair.generate().publicKey, + 0.1 * LAMPORTS_PER_SOL + ); + + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], + }); + + const messageBuffer = multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + [transactionBuffer] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + const createIx = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + const createMessage = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [createIx], + }).compileToV0Message(); + + const createTx = new VersionedTransaction(createMessage); + createTx.sign([members.proposer]); + + const createSig = await connection.sendTransaction(createTx, { skipPreflight: true }); + await connection.confirmTransaction(createSig); + }); + + it("error: close buffer with non-creator signature", async () => { + const closeIx = multisig.generated.createTransactionBufferCloseInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.voter.publicKey, + }, + programId + ); + + const closeMessage = new TransactionMessage({ + payerKey: members.voter.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [closeIx], + }).compileToV0Message(); + + const closeTx = new VersionedTransaction(closeMessage); + closeTx.sign([members.voter]); + + await assert.rejects( + () => + connection + .sendTransaction(closeTx) + .catch(multisig.errors.translateAndThrowAnchorError), + /Unauthorized/ + ); + }); + + it("close buffer with creator signature", async () => { + const closeIx = multisig.generated.createTransactionBufferCloseInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + }, + programId + ); + + const closeMessage = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [closeIx], + }).compileToV0Message(); + + const closeTx = new VersionedTransaction(closeMessage); + closeTx.sign([members.proposer]); + + const closeSig = await connection.sendTransaction(closeTx, { skipPreflight: true }); + await connection.confirmTransaction(closeSig); + const transactionBufferAccount = await connection.getAccountInfo(transactionBuffer); + assert.equal(transactionBufferAccount, null, "Transaction buffer account should be closed"); + }); +}); \ No newline at end of file diff --git a/tests/suites/instructions/transactionBufferCreate.ts b/tests/suites/instructions/transactionBufferCreate.ts new file mode 100644 index 00000000..19abca29 --- /dev/null +++ b/tests/suites/instructions/transactionBufferCreate.ts @@ -0,0 +1,528 @@ +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + TransactionMessage, + VersionedTransaction, + SystemProgram, +} from "@solana/web3.js"; +import * as multisig from "@sqds/multisig"; +import assert from "assert"; +import { + createAutonomousMultisigV2, + createLocalhostConnection, + createTestTransferInstruction, + generateMultisigMembers, + getTestProgramId, + TestMembers, +} from "../../utils"; +import { BN } from "bn.js"; +import { + TransactionBufferCreateArgs, + TransactionBufferCreateInstructionArgs, +} from "@sqds/multisig/lib/generated"; +import * as crypto from "crypto"; + +const programId = getTestProgramId(); +const connection = createLocalhostConnection(); + +describe("Instructions / transaction_buffer_create", () => { + let members: TestMembers; + + const createKey = Keypair.generate(); + + let multisigPda = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + })[0]; + + const [vaultPda] = multisig.getVaultPda({ + multisigPda, + index: 0, + programId, + }); + + // Set up a multisig with some transactions. + before(async () => { + members = await generateMultisigMembers(connection); + + // Create new autonomous multisig with rentCollector set to its default vault. + multisigPda = ( + await createAutonomousMultisigV2({ + connection, + createKey, + members, + threshold: 2, + timeLock: 0, + rentCollector: vaultPda, + programId, + }) + )[0]; + + // Airdrop some SOL to the vault + let signature = await connection.requestAirdrop( + vaultPda, + 10 * LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(signature); + }); + + it("set transaction buffer", async () => { + const transactionIndex = 1n; + + const testPayee = Keypair.generate(); + const testIx = createTestTransferInstruction( + vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + // Initialize a transaction message with a single instruction. + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], + }); + + // Serialize with SDK util + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + // Convert to a SHA256 hash. + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + // Must be a SHA256 hash of the message buffer. + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + const message = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + + tx.sign([members.proposer]); + + // Send transaction. + const signature = await connection.sendTransaction(tx, { + skipPreflight: true, + }); + await connection.confirmTransaction(signature); + + const transactionBufferAccount = await connection.getAccountInfo( + transactionBuffer + ); + + // Verify account exists. + assert.notEqual(transactionBufferAccount, null); + assert.ok(transactionBufferAccount?.data.length! > 0); + }); + + it("close transaction buffer", async () => { + const transactionIndex = 1n; + + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + const ix = multisig.generated.createTransactionBufferCloseInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + }, + programId + ); + + const message = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + + tx.sign([members.proposer]); + + // Send transaction. + const signature = await connection.sendTransaction(tx, { + skipPreflight: true, + }); + await connection.confirmTransaction(signature); + + const transactionBufferAccount = await connection.getAccountInfo( + transactionBuffer + ); + + // Verify account is closed. + assert.equal(transactionBufferAccount, null); + }); + + // Test: Attempt to create a transaction buffer with a non-member +it("error: creating buffer as non-member", async () => { + const transactionIndex = 1n; + // Create a keypair that is not a member of the multisig + const nonMember = Keypair.generate(); + // Airdrop some SOL to the non-member + const airdropSig = await connection.requestAirdrop( + nonMember.publicKey, + 1 * LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(airdropSig); + + // Set up a test transaction + const testPayee = Keypair.generate(); + const testIx = await createTestTransferInstruction( + vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + // Create a transaction message + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], + }); + + // Serialize the message buffer + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + // Derive the transaction buffer PDA + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + // Create a hash of the message buffer + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + // Create the instruction to create a transaction buffer + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: nonMember.publicKey, + rentPayer: nonMember.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + // Create and sign the transaction + const message = new TransactionMessage({ + payerKey: nonMember.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + tx.sign([nonMember]); + + // Attempt to send the transaction and expect it to fail + await assert.rejects( + () => + connection + .sendTransaction(tx) + .catch(multisig.errors.translateAndThrowAnchorError), + /NotAMember/ + ); +}); + +// Test: Attempt to create a transaction buffer with a member without initiate permissions +it("error: creating buffer as member without proposer permissions", async () => { + const memberWithoutInitiatePermissions = members.voter; + + const transactionIndex = 1n; + + // Set up a test transaction + const testPayee = Keypair.generate(); + const testIx = await createTestTransferInstruction( + vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + // Create a transaction message + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], + }); + + // Serialize the message buffer + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + // Derive the transaction buffer PDA + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + // Create a hash of the message buffer + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + // Create the instruction to create a transaction buffer + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: memberWithoutInitiatePermissions.publicKey, + rentPayer: memberWithoutInitiatePermissions.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + // Create and sign the transaction + const message = new TransactionMessage({ + payerKey: memberWithoutInitiatePermissions.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + tx.sign([memberWithoutInitiatePermissions]); + + // Attempt to send the transaction and expect it to fail + await assert.rejects( + () => + connection + .sendTransaction(tx) + .catch(multisig.errors.translateAndThrowAnchorError), + /Unauthorized/ + ); +}); + +// Test: Attempt to create a transaction buffer with an invalid index +it("error: creating buffer for invalid index", async () => { + // Use an invalid transaction index (multisig.transaction_index + 2) + const invalidTransactionIndex = 3n; + + // Set up a test transaction + const testPayee = Keypair.generate(); + const testIx = await createTestTransferInstruction( + vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + // Create a transaction message + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], + }); + + // Serialize the message buffer + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + // Derive the transaction buffer PDA with the invalid index + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(invalidTransactionIndex)).toBuffer("le", 8), + ], + programId + ); + + // Create a hash of the message buffer + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + // Create the instruction to create a transaction buffer + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + // Create and sign the transaction + const message = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + tx.sign([members.proposer]); + + // Attempt to send the transaction and expect it to fail + await assert.rejects( + () => + connection + .sendTransaction(tx) + .catch(multisig.errors.translateAndThrowAnchorError), + /A seeds constraint was violated/ + ); +}); + + +it("error: creating buffer exceeding maximum size", async () => { + const transactionIndex = 1n; + + // Create a large buffer that exceeds the maximum size + const largeBuffer = Buffer.alloc(500, 1); // 500 bytes, filled with 1s + + // Derive the transaction buffer PDA + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + // Create a hash of the large buffer + const messageHash = crypto + .createHash("sha256") + .update(largeBuffer) + .digest(); + + // Create the instruction to create a transaction buffer + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: 4001, + buffer: largeBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + // Create and sign the transaction + const message = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + tx.sign([members.proposer]); + + // Attempt to send the transaction and expect it to fail + await assert.rejects( + () => + connection + .sendTransaction(tx) + .catch(multisig.errors.translateAndThrowAnchorError), + /FinalBufferSizeExceeded/ // Assuming this is the error thrown for exceeding buffer size + ); +}); +}); diff --git a/tests/suites/instructions/transactionBufferExtend.ts b/tests/suites/instructions/transactionBufferExtend.ts new file mode 100644 index 00000000..61c1aa23 --- /dev/null +++ b/tests/suites/instructions/transactionBufferExtend.ts @@ -0,0 +1,437 @@ +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import * as multisig from "@sqds/multisig"; +import { + TransactionBufferCreateArgs, + TransactionBufferCreateInstructionArgs, + TransactionBufferExtendArgs, + TransactionBufferExtendInstructionArgs, +} from "@sqds/multisig/lib/generated"; +import assert from "assert"; +import { BN } from "bn.js"; +import * as crypto from "crypto"; +import { + TestMembers, + createAutonomousMultisigV2, + createLocalhostConnection, + createTestTransferInstruction, + generateMultisigMembers, + getTestProgramId, +} from "../../utils"; + +const programId = getTestProgramId(); +const connection = createLocalhostConnection(); + +describe("Instructions / transaction_buffer_extend", () => { + let members: TestMembers; + let transactionBufferAccount: PublicKey; + + const createKey = Keypair.generate(); + + let multisigPda = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + })[0]; + + const [vaultPda] = multisig.getVaultPda({ + multisigPda, + index: 0, + programId, + }); + + // Set up a multisig with some transactions. + before(async () => { + members = await generateMultisigMembers(connection); + + // Create new autonomous multisig with rentCollector set to its default vault. + await createAutonomousMultisigV2({ + connection, + createKey, + members, + threshold: 1, + timeLock: 0, + rentCollector: vaultPda, + programId, + }); + + // Airdrop some SOL to the vault + let signature = await connection.requestAirdrop( + vaultPda, + 10 * LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(signature); + }); + + // Helper function to create a transaction buffer + async function createTransactionBuffer(creator: Keypair, transactionIndex: bigint) { + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + const testIx = await createTestTransferInstruction( + vaultPda, + Keypair.generate().publicKey, + 1 * LAMPORTS_PER_SOL + ); + + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], + }); + + const messageBuffer = multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + const messageHash = crypto.createHash("sha256").update(messageBuffer).digest(); + + const createIx = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: creator.publicKey, + rentPayer: creator.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer.slice(0, 750), + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + const createMessage = new TransactionMessage({ + payerKey: creator.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [createIx], + }).compileToV0Message(); + + const createTx = new VersionedTransaction(createMessage); + createTx.sign([creator]); + + const sig = await connection.sendTransaction(createTx, { skipPreflight: true }); + await connection.confirmTransaction(sig); + + return transactionBuffer; + } + + // Helper function to close a transaction buffer + async function closeTransactionBuffer(creator: Keypair, transactionBuffer: PublicKey) { + const closeIx = multisig.generated.createTransactionBufferCloseInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: creator.publicKey, + }, + programId + ); + + const closeMessage = new TransactionMessage({ + payerKey: creator.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [closeIx], + }).compileToV0Message(); + + const closeTx = new VersionedTransaction(closeMessage); + closeTx.sign([creator]); + + const sig = await connection.sendTransaction(closeTx, { skipPreflight: true }); + + await connection.confirmTransaction(sig); + } + + it("set transaction buffer and extend", async () => { + const transactionIndex = 1n; + + const testPayee = Keypair.generate(); + const testIx = await createTestTransferInstruction( + vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + let instructions = []; + + // Add 28 transfer instructions to the message. + for (let i = 0; i <= 42; i++) { + instructions.push(testIx); + } + + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: instructions, + }); + + // Serialize with SDK util + const messageBuffer = multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + // Convert message buffer to a SHA256 hash. + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + // Slice the first 750 bytes of the message buffer. + const firstHalf = messageBuffer.slice(0, 750); + + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + // Must be a SHA256 hash of the message buffer. + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: firstHalf, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + const message = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + + tx.sign([members.proposer]); + + // Send first transaction. + const signature = await connection.sendTransaction(tx, { + skipPreflight: true, + }); + await connection.confirmTransaction(signature); + + const transactionBufferAccount = await connection.getAccountInfo( + transactionBuffer + ); + + // Ensure the transaction buffer account exists. + assert.notEqual(transactionBufferAccount, null); + assert.ok(transactionBufferAccount?.data.length! > 0); + + // Need to add some deserialization to check if it actually worked. + const transactionBufferInfo1 = await connection.getAccountInfo(transactionBuffer); + const [txBufferDeser1] = await multisig.generated.TransactionBuffer.fromAccountInfo( + transactionBufferInfo1! + ); + + // First chunk uploaded. Check that length is as expected. + assert.equal(txBufferDeser1.buffer.length, 750); + + // Slice that last bytes of the message buffer. + const secondHalf = messageBuffer.slice( + 750, + messageBuffer.byteLength + ); + + const secondIx = + multisig.generated.createTransactionBufferExtendInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + }, + { + args: { + buffer: secondHalf, + } as TransactionBufferExtendArgs, + } as TransactionBufferExtendInstructionArgs, + programId + ); + + const secondMessage = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [secondIx], + }).compileToV0Message(); + + const secondTx = new VersionedTransaction(secondMessage); + + secondTx.sign([members.proposer]); + + // Send second transaction. + const secondSignature = await connection.sendTransaction(secondTx, { + skipPreflight: true, + }); + + await connection.confirmTransaction(secondSignature); + + // Need to add some deserialization to check if it actually worked. + const transactionBufferInfo2 = await connection.getAccountInfo(transactionBuffer); + const [txBufferDeser2] = await multisig.generated.TransactionBuffer.fromAccountInfo( + transactionBufferInfo2! + ); + + // Buffer fully uploaded. Check that length is as expected. + assert.equal(txBufferDeser2.buffer.length, messageBuffer.byteLength); + + // Close the transaction buffer account. + await closeTransactionBuffer(members.proposer, transactionBuffer); + + // Fetch the transaction buffer account. + const closedTransactionBufferInfo = await connection.getAccountInfo( + transactionBuffer + ); + assert.equal(closedTransactionBufferInfo, null); + }); + + // Test: Attempt to extend a transaction buffer as a non-member + it("error: extending buffer as non-member", async () => { + const transactionIndex = 1n; + const nonMember = Keypair.generate(); + await connection.requestAirdrop(nonMember.publicKey, 1 * LAMPORTS_PER_SOL); + + const transactionBuffer = await createTransactionBuffer(members.almighty, transactionIndex); + + const dummyData = Buffer.alloc(100, 1); + const ix = multisig.generated.createTransactionBufferExtendInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: nonMember.publicKey, + }, + { + args: { + buffer: dummyData, + } as TransactionBufferExtendArgs, + } as TransactionBufferExtendInstructionArgs, + programId + ); + + const message = new TransactionMessage({ + payerKey: nonMember.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + tx.sign([nonMember]); + + await assert.rejects( + () => connection.sendTransaction(tx).catch(multisig.errors.translateAndThrowAnchorError), + /Unauthorized/ + ); + + await closeTransactionBuffer(members.almighty, transactionBuffer); + }); + + // Test: Attempt to extend a transaction buffer past the 4000 byte limit + it("error: extending buffer past submitted byte value", async () => { + const transactionIndex = 1n; + + const transactionBuffer = await createTransactionBuffer(members.almighty, transactionIndex); + + const largeData = Buffer.alloc(500, 1); + const ix = multisig.generated.createTransactionBufferExtendInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.almighty.publicKey, + }, + { + args: { + buffer: largeData, + } as TransactionBufferExtendArgs, + } as TransactionBufferExtendInstructionArgs, + programId + ); + + const message = new TransactionMessage({ + payerKey: members.almighty.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + tx.sign([members.almighty]); + + await assert.rejects( + () => connection.sendTransaction(tx).catch(multisig.errors.translateAndThrowAnchorError), + /FinalBufferSizeExceeded/ + ); + + await closeTransactionBuffer(members.almighty, transactionBuffer); + }); + + // Test: Attempt to extend a transaction buffer by a member who is not the original creator + it("error: extending buffer by non-creator member", async () => { + const transactionIndex = 1n; + + const transactionBuffer = await createTransactionBuffer(members.proposer, transactionIndex); + + const dummyData = Buffer.alloc(100, 1); + const extendIx = multisig.generated.createTransactionBufferExtendInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.almighty.publicKey, + }, + { + args: { + buffer: dummyData, + } as TransactionBufferExtendArgs, + } as TransactionBufferExtendInstructionArgs, + programId + ); + + const extendMessage = new TransactionMessage({ + payerKey: members.almighty.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [extendIx], + }).compileToV0Message(); + + const extendTx = new VersionedTransaction(extendMessage); + extendTx.sign([members.almighty]); + + await assert.rejects( + () => connection.sendTransaction(extendTx).catch(multisig.errors.translateAndThrowAnchorError), + /Unauthorized/ + ); + + + await closeTransactionBuffer(members.proposer, transactionBuffer); + }); + +}); \ No newline at end of file diff --git a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts new file mode 100644 index 00000000..824cdcd2 --- /dev/null +++ b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts @@ -0,0 +1,442 @@ +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, + Transaction +} from "@solana/web3.js"; +import * as multisig from "@sqds/multisig"; +import { + TransactionBufferCreateArgs, + TransactionBufferCreateInstructionArgs, + TransactionBufferExtendArgs, + TransactionBufferExtendInstructionArgs, + VaultTransactionCreateFromBufferArgs, + VaultTransactionCreateFromBufferInstructionArgs +} from "@sqds/multisig/lib/generated"; +import assert from "assert"; +import { BN } from "bn.js"; +import * as crypto from "crypto"; +import { + TestMembers, + createAutonomousMultisigV2, + createLocalhostConnection, + createTestTransferInstruction, + generateMultisigMembers, + getLogs, + getTestProgramId, +} from "../../utils"; + +const programId = getTestProgramId(); +const connection = createLocalhostConnection(); + +describe("Instructions / vault_transaction_create_from_buffer", () => { + let members: TestMembers; + + const createKey = Keypair.generate(); + + let multisigPda = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + })[0]; + + const [vaultPda] = multisig.getVaultPda({ + multisigPda, + index: 0, + programId, + }); + + // Set up a multisig with some transactions. + before(async () => { + members = await generateMultisigMembers(connection); + + // Create new autonomous multisig with rentCollector set to its default vault. + await createAutonomousMultisigV2({ + connection, + createKey, + members, + threshold: 2, + timeLock: 0, + rentCollector: vaultPda, + programId, + }); + + // Airdrop some SOL to the vault + let signature = await connection.requestAirdrop( + vaultPda, + 10 * LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(signature); + }); + + it("set buffer, extend, and create", async () => { + const transactionIndex = 1n; + + const testPayee = Keypair.generate(); + const testIx = await createTestTransferInstruction( + vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + let instructions = []; + + // Add 48 transfer instructions to the message. + for (let i = 0; i <= 42; i++) { + instructions.push(testIx); + } + + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: instructions, + }); + + // Serialize the message. Must be done with this util function + const messageBuffer = multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + // Slice the message buffer into two parts. + const firstSlice = messageBuffer.slice(0, 700); + + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + // Must be a SHA256 hash of the message buffer. + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: firstSlice, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + const message = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + + tx.sign([members.proposer]); + + // Send first transaction. + const signature = await connection.sendTransaction(tx, { + skipPreflight: true, + }); + await connection.confirmTransaction(signature); + + const transactionBufferAccount = await connection.getAccountInfo( + transactionBuffer + ); + + // Check buffer account exists. + assert.notEqual(transactionBufferAccount, null); + assert.ok(transactionBufferAccount?.data.length! > 0); + + // Need to add some deserialization to check if it actually worked. + const transactionBufferInfo1 = await connection.getAccountInfo(transactionBuffer); + const [txBufferDeser1] = await multisig.generated.TransactionBuffer.fromAccountInfo( + transactionBufferInfo1! + ); + + // First chunk uploaded. Check that length is as expected. + assert.equal(txBufferDeser1.buffer.length, 700); + + const secondSlice = messageBuffer.slice( + 700, + messageBuffer.byteLength + ); + + // Extned the buffer. + const secondIx = + multisig.generated.createTransactionBufferExtendInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + }, + { + args: { + buffer: secondSlice, + } as TransactionBufferExtendArgs, + } as TransactionBufferExtendInstructionArgs, + programId + ); + + const secondMessage = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [secondIx], + }).compileToV0Message(); + + const secondTx = new VersionedTransaction(secondMessage); + + secondTx.sign([members.proposer]); + + // Send second transaction to extend. + const secondSignature = await connection.sendTransaction(secondTx, { + skipPreflight: true, + }); + + await connection.confirmTransaction(secondSignature); + + // Need to add some deserialization to check if it actually worked. + const transactionBufferInfo2 = await connection.getAccountInfo(transactionBuffer); + const [txBufferDeser2] = await multisig.generated.TransactionBuffer.fromAccountInfo( + transactionBufferInfo2! + ); + + // First chunk uploaded. Check that length is as expected. + assert.equal(txBufferDeser2.buffer.length, messageBuffer.byteLength); + + // Derive vault transaction PDA. + const [transactionPda] = multisig.getTransactionPda({ + multisigPda, + index: transactionIndex, + programId, + }); + + // Create final instruction. + const thirdIx = + multisig.generated.createVaultTransactionCreateFromBufferInstruction( + { + multisig: multisigPda, + transactionBuffer, + transaction: transactionPda, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + ephemeralSigners: 0, + memo: null, + } as VaultTransactionCreateFromBufferArgs, + } as VaultTransactionCreateFromBufferInstructionArgs, + programId + ); + + // Add third instruction to the message. + const thirdMessage = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [thirdIx], + }).compileToV0Message(); + + const thirdTx = new VersionedTransaction(thirdMessage); + + thirdTx.sign([members.proposer]); + + // Send final transaction. + const thirdSignature = await connection.sendTransaction(thirdTx, { + skipPreflight: true, + }); + + await connection.confirmTransaction(thirdSignature); + + const transactionInfo = + await multisig.accounts.VaultTransaction.fromAccountAddress( + connection, + transactionPda + ); + + // Ensure final vault transaction has 43 instructions + assert.equal(transactionInfo.message.instructions.length, 43); + }); + + it("error: create from buffer with mismatched hash", async () => { + const transactionIndex = 2n; + + // Create a simple transfer instruction + const testIx = await createTestTransferInstruction( + vaultPda, + Keypair.generate().publicKey, + 0.1 * LAMPORTS_PER_SOL + ); + + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], + }); + + const messageBuffer = multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + // Create a dummy hash of zeros + const dummyHash = new Uint8Array(32).fill(0); + + const createIx = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + finalBufferHash: Array.from(dummyHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + const createMessage = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [createIx], + }).compileToV0Message(); + + const createTx = new VersionedTransaction(createMessage); + createTx.sign([members.proposer]); + + const createBufferSig = await connection.sendTransaction(createTx, { skipPreflight: true }); + await connection.confirmTransaction(createBufferSig); + + const [transactionPda] = multisig.getTransactionPda({ + multisigPda, + index: transactionIndex, + programId, + }); + + const createFromBufferIx = multisig.generated.createVaultTransactionCreateFromBufferInstruction( + { + multisig: multisigPda, + transactionBuffer, + transaction: transactionPda, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + ephemeralSigners: 0, + memo: null, + } as VaultTransactionCreateFromBufferArgs, + } as VaultTransactionCreateFromBufferInstructionArgs, + programId + ); + + const createFromBufferMessage = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [createFromBufferIx], + }).compileToV0Message(); + + const createFromBufferTx = new VersionedTransaction(createFromBufferMessage); + createFromBufferTx.sign([members.proposer]); + + await assert.rejects( + () => connection.sendTransaction(createFromBufferTx).catch(multisig.errors.translateAndThrowAnchorError), + /FinalBufferHashMismatch/ + ); + }); + + // We expect the program to run out of memory in a base case, given 43 transfers. + it("error: out of memory (no allocator)", async () => { + const [transactionPda] = multisig.getTransactionPda({ + multisigPda, + index: 1n, + programId, + }); + + const transactionInfo = + await multisig.accounts.VaultTransaction.fromAccountAddress( + connection, + transactionPda + ); + + // Check that we're dealing with the same account from first test. + assert.equal(transactionInfo.message.instructions.length, 43); + + const fourthSignature = await multisig.rpc.proposalCreate({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex: 1n, + creator: members.almighty, + isDraft: false, + programId, + }); + await connection.confirmTransaction(fourthSignature); + + const fifthSignature = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex: 1n, + member: members.almighty, + programId, + }); + await connection.confirmTransaction(fifthSignature); + + const executeIx = await multisig.instructions.vaultTransactionExecute({ + connection, + multisigPda, + transactionIndex: 1n, + member: members.almighty.publicKey, + programId, + }); + + const executeTx = new Transaction().add(executeIx.instruction); + const signature4 = await connection.sendTransaction( + executeTx, + [members.almighty], + { skipPreflight: true } + ); + + await connection.confirmTransaction(signature4); + + assert.doesNotThrow( + async () => await getLogs(connection, signature4), + /Error: memory allocation failed, out of memory/ + ) + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts index f9a21522..ea153c07 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -124,6 +124,14 @@ export function createLocalhostConnection() { return new Connection("http://127.0.0.1:8899", "confirmed"); } +export const getLogs = async (connection: Connection, signature: string): Promise => { + const tx = await connection.getTransaction( + signature, + { commitment: "confirmed" } + ) + return tx!.meta!.logMessages || [] +} + export async function createAutonomousMultisig({ connection, createKey = Keypair.generate(), From 63c4975a0fdd81e4e72ad76d40a573d49c5b1440 Mon Sep 17 00:00:00 2001 From: Orion <89707822+iceomatic@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:05:09 +0200 Subject: [PATCH 12/47] Feat: Custom Bump Allocator (#114) * wip: incremental tx uploading * add: incremental tx uploading ixns * generate sdk & add tests - createbuffer & extend passing* - createVaultTxFromBuffer failing with "Program Failed to Complete" * base case tests passing * add comments for clarity * expose buffer close & add test * add: extra tests -- buffer creation and extension * add: tests for transactionBufferClose * fix: uncomment tests * fix: space allocation buffer creation * add: additional helper for buffer size * add: buffer deserialization checks * dummy vercel commit * feat: tx buffer example & out of memory example * initial commit * feat: flipped bump allocator on its head * feat: heap tests, buffer chunking util + refactor * remove: heap test ix & add: allocator docs * rm: heapTest testing suite * add: script for running anchor test in detach * revert: Cargo.toml anchor-lang change * linting: cargo fmt --------- Co-authored-by: Joey Meere <100378695+joeymeere@users.noreply.github.com> --- package.json | 55 +- programs/squads_multisig_program/Cargo.toml | 3 +- .../squads_multisig_program/src/allocator.rs | 138 + .../src/instructions/heap_test.rs | 28 + .../src/instructions/mod.rs | 20 +- .../multisig_add_spending_limit.rs | 2 +- .../src/instructions/multisig_config.rs | 5 +- .../src/instructions/proposal_vote.rs | 16 +- .../instructions/transaction_buffer_close.rs | 4 +- .../instructions/transaction_buffer_create.rs | 9 +- .../instructions/transaction_buffer_extend.rs | 3 +- .../instructions/vault_transaction_create.rs | 2 +- programs/squads_multisig_program/src/lib.rs | 9 +- .../squads_multisig_program/src/state/mod.rs | 4 +- .../src/state/proposal.rs | 5 +- .../src/state/seeds.rs | 2 +- .../utils/executable_transaction_message.rs | 2 +- sdk/multisig/idl/squads_multisig_program.json | 3494 ----------------- .../src/generated/instructions/heapTest.ts | 91 + .../src/generated/instructions/index.ts | 1 + tests/index.ts | 6 +- tests/suites/examples/custom-heap.ts | 331 ++ tests/suites/instructions/heapTest.ts | 65 + .../vaultTransactionCreateFromBuffer.ts | 152 +- tests/suites/program-config-init.ts | 16 +- tests/utils.ts | 55 + 26 files changed, 889 insertions(+), 3629 deletions(-) create mode 100644 programs/squads_multisig_program/src/allocator.rs create mode 100644 programs/squads_multisig_program/src/instructions/heap_test.rs delete mode 100644 sdk/multisig/idl/squads_multisig_program.json create mode 100644 sdk/multisig/src/generated/instructions/heapTest.ts create mode 100644 tests/suites/examples/custom-heap.ts create mode 100644 tests/suites/instructions/heapTest.ts diff --git a/package.json b/package.json index 97c06d20..e3ae5e79 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,30 @@ { - "private": true, - "workspaces": [ - "sdk/*" - ], - "scripts": { - "build": "turbo run build", - "test": "turbo run build && anchor test -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"", - "pretest": "mkdir -p target/deploy && cp ./test-program-keypair.json ./target/deploy/squads_multisig_program-keypair.json", - "ts": "turbo run ts && yarn tsc --noEmit" - }, - "devDependencies": { - "@solana/spl-token": "*", - "@solana/spl-memo": "^0.2.3", - "@types/bn.js": "5.1.0", - "@types/mocha": "10.0.1", - "@types/node-fetch": "2.6.2", - "mocha": "10.2.0", - "prettier": "2.6.2", - "ts-node": "10.9.1", - "turbo": "1.6.3", - "typescript": "*" - }, - "resolutions": { - "@solana/web3.js": "1.70.3", - "@solana/spl-token": "0.3.6", - "typescript": "4.9.4" - } + "private": true, + "workspaces": [ + "sdk/*" + ], + "scripts": { + "build": "turbo run build", + "test:detached": "turbo run build && anchor test --detach -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"", + "test": "turbo run build && anchor test -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"", + "pretest": "mkdir -p target/deploy && cp ./test-program-keypair.json ./target/deploy/squads_multisig_program-keypair.json", + "ts": "turbo run ts && yarn tsc --noEmit" + }, + "devDependencies": { + "@solana/spl-token": "*", + "@solana/spl-memo": "^0.2.3", + "@types/bn.js": "5.1.0", + "@types/mocha": "10.0.1", + "@types/node-fetch": "2.6.2", + "mocha": "10.2.0", + "prettier": "2.6.2", + "ts-node": "10.9.1", + "turbo": "1.6.3", + "typescript": "*" + }, + "resolutions": { + "@solana/web3.js": "1.70.3", + "@solana/spl-token": "0.3.6", + "typescript": "4.9.4" + } } diff --git a/programs/squads_multisig_program/Cargo.toml b/programs/squads_multisig_program/Cargo.toml index aa83782d..4fe06e76 100644 --- a/programs/squads_multisig_program/Cargo.toml +++ b/programs/squads_multisig_program/Cargo.toml @@ -10,12 +10,13 @@ crate-type = ["cdylib", "lib"] name = "squads_multisig_program" [features] +default = ["custom-heap"] +custom-heap = [] no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] testing = [] -default = [] [dependencies] anchor-lang = { version = "=0.29.0", features = ["allow-missing-optionals"] } diff --git a/programs/squads_multisig_program/src/allocator.rs b/programs/squads_multisig_program/src/allocator.rs new file mode 100644 index 00000000..4bdde984 --- /dev/null +++ b/programs/squads_multisig_program/src/allocator.rs @@ -0,0 +1,138 @@ +/* +Optimizing Bump Heap Allocation + +Objective: Increase available heap memory while maintaining flexibility in program invocation. + +1. Initial State: Default 32 KiB Heap + +Memory Layout: +0x300000000 0x300008000 + | | + v v + [--------------------] + ^ ^ + | | + VM Lower VM Upper + Boundary Boundary + +Default Allocator (Allocates Backwards / Top Down) (Default 32 KiB): +0x300000000 0x300008000 + | | + [--------------------] + ^ + | + Allocation starts here (SAFE) + +2. Naive Approach: Increase HEAP_LENGTH to 8 * 32 KiB + Default Allocator + +Memory Layout with Increased HEAP_LENGTH: +0x300000000 0x300008000 0x300040000 + | | | + v v v + [--------------------|------------------------------------|] + ^ ^ ^ + | | | + VM Lower VM Upper Allocation starts here + Boundary Boundary (ACCESS VIOLATION!) + +Issue: Access violation occurs without requestHeapFrame, requiring it for every transaction. + +3. Optimized Solution: Forward Allocation with Flexible Heap Usage + +Memory Layout (Same as Naive Approach): +0x300000000 0x300008000 0x300040000 + | | | + v v v + [--------------------|------------------------------------|] + ^ ^ ^ + | | | + VM Lower VM Upper Allocator & VM + Boundary Boundary Heap Limit + +Forward Allocator Behavior: + +a) Without requestHeapFrame: +0x300000000 0x300008000 + | | + [--------------------] + ^ ^ + | | + VM Lower VM Upper + Boundary Boundary + Allocation + starts here (SAFE) + +b) With requestHeapFrame: +0x300000000 0x300008000 0x300040000 + | | | + [--------------------|------------------------------------|] + ^ ^ ^ + | | | + VM Lower | VM Upper + Boundary Boundary + Allocation Allocation continues Maximum allocation + starts here with requestHeapFrame with requestHeapFrame +(SAFE) + +Key Advantages: +1. Compatibility: Functions without requestHeapFrame for allocations ≤32 KiB. +2. Extensibility: Supports larger allocations when requestHeapFrame is invoked. +3. Efficiency: Eliminates mandatory requestHeapFrame calls for all transactions. + +Conclusion: +The forward allocation strategy offers a robust solution, providing both backward +compatibility for smaller heap requirements and the flexibility to utilize extended +heap space when necessary. + +The following allocator is a copy of the bump allocator found in +solana_program::entrypoint and +https://github.com/solana-labs/solana-program-library/blob/master/examples/rust/custom-heap/src/entrypoint.rs + +but with changes to its HEAP_LENGTH and its +starting allocation address. +*/ + +use solana_program::entrypoint::HEAP_START_ADDRESS; +use std::{alloc::Layout, mem::size_of, ptr::null_mut}; + +/// Length of the memory region used for program heap. +pub const HEAP_LENGTH: usize = 8 * 32 * 1024; + +struct BumpAllocator; + +unsafe impl std::alloc::GlobalAlloc for BumpAllocator { + #[inline] + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + if layout.size() == isize::MAX as usize - 0x42 { + // Return test value + 0x42 as *mut u8 + } else { + const POS_PTR: *mut usize = HEAP_START_ADDRESS as *mut usize; + const TOP_ADDRESS: usize = HEAP_START_ADDRESS as usize + HEAP_LENGTH; + const BOTTOM_ADDRESS: usize = HEAP_START_ADDRESS as usize + size_of::<*mut u8>(); + let mut pos = *POS_PTR; + if pos == 0 { + // First time, set starting position to bottom address + pos = BOTTOM_ADDRESS; + } + // Align the position upwards + pos = (pos + layout.align() - 1) & !(layout.align() - 1); + let next_pos = pos.saturating_add(layout.size()); + if next_pos > TOP_ADDRESS { + return null_mut(); + } + *POS_PTR = next_pos; + pos as *mut u8 + } + } + + #[inline] + unsafe fn dealloc(&self, _: *mut u8, _: Layout) { + // I'm a bump allocator, I don't free + } +} + +// Only use the allocator if we're not in a no-entrypoint context +#[cfg(not(feature = "no-entrypoint"))] +#[global_allocator] +static A: BumpAllocator = BumpAllocator; diff --git a/programs/squads_multisig_program/src/instructions/heap_test.rs b/programs/squads_multisig_program/src/instructions/heap_test.rs new file mode 100644 index 00000000..526aac11 --- /dev/null +++ b/programs/squads_multisig_program/src/instructions/heap_test.rs @@ -0,0 +1,28 @@ +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct HeapTest<'info> { + /// CHECK: We only need to validate the address. + pub authority: AccountInfo<'info>, +} + +impl HeapTest<'_> { + pub fn handler(_ctx: Context, length: u64) -> Result<()> { + // Allocate the vector with the desired capacity + let mut vector = Vec::::with_capacity(length as usize); + + // Unsafe block to set the length of the vector without initialization + unsafe { + vector.set_len(length as usize); + } + + // If you need to set all elements to a specific value (e.g., 1), + // you can use `fill` method which is more efficient than iteration + vector.fill(1); + + // Log the vector's length + msg!("Vector allocated with length: {}", vector.len()); + + Ok(()) + } +} diff --git a/programs/squads_multisig_program/src/instructions/mod.rs b/programs/squads_multisig_program/src/instructions/mod.rs index ffb0b814..9c6330a5 100644 --- a/programs/squads_multisig_program/src/instructions/mod.rs +++ b/programs/squads_multisig_program/src/instructions/mod.rs @@ -3,43 +3,45 @@ pub use batch_create::*; pub use batch_execute_transaction::*; pub use config_transaction_create::*; pub use config_transaction_execute::*; +pub use heap_test::*; pub use multisig_add_spending_limit::*; pub use multisig_config::*; pub use multisig_create::*; pub use multisig_remove_spending_limit::*; -pub use program_config_init::*; pub use program_config::*; +pub use program_config_init::*; pub use proposal_activate::*; pub use proposal_create::*; pub use proposal_vote::*; pub use spending_limit_use::*; pub use transaction_accounts_close::*; -pub use vault_transaction_create::*; -pub use vault_transaction_execute::*; +pub use transaction_buffer_close::*; pub use transaction_buffer_create::*; pub use transaction_buffer_extend::*; -pub use transaction_buffer_close::*; +pub use vault_transaction_create::*; pub use vault_transaction_create_from_buffer::*; +pub use vault_transaction_execute::*; mod batch_add_transaction; mod batch_create; mod batch_execute_transaction; mod config_transaction_create; mod config_transaction_execute; +mod heap_test; mod multisig_add_spending_limit; mod multisig_config; mod multisig_create; mod multisig_remove_spending_limit; -mod program_config_init; mod program_config; +mod program_config_init; mod proposal_activate; mod proposal_create; mod proposal_vote; mod spending_limit_use; mod transaction_accounts_close; -mod vault_transaction_create; -mod vault_transaction_execute; +mod transaction_buffer_close; mod transaction_buffer_create; mod transaction_buffer_extend; -mod transaction_buffer_close; -mod vault_transaction_create_from_buffer; \ No newline at end of file +mod vault_transaction_create; +mod vault_transaction_create_from_buffer; +mod vault_transaction_execute; diff --git a/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs b/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs index a57951bd..944288b1 100644 --- a/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs +++ b/programs/squads_multisig_program/src/instructions/multisig_add_spending_limit.rs @@ -84,7 +84,7 @@ impl MultisigAddSpendingLimit<'_> { args: MultisigAddSpendingLimitArgs, ) -> Result<()> { let spending_limit = &mut ctx.accounts.spending_limit; - + // Make sure there are no duplicate keys in this direct invocation by sorting so the invariant will catch let mut sorted_members = args.members; sorted_members.sort(); diff --git a/programs/squads_multisig_program/src/instructions/multisig_config.rs b/programs/squads_multisig_program/src/instructions/multisig_config.rs index 461d799c..45516c4a 100644 --- a/programs/squads_multisig_program/src/instructions/multisig_config.rs +++ b/programs/squads_multisig_program/src/instructions/multisig_config.rs @@ -89,7 +89,10 @@ impl MultisigConfig<'_> { let multisig = &mut ctx.accounts.multisig; // Make sure that the new member is not already in the multisig. - require!(multisig.is_member(new_member.key).is_none(), MultisigError::DuplicateMember); + require!( + multisig.is_member(new_member.key).is_none(), + MultisigError::DuplicateMember + ); multisig.add_member(new_member); diff --git a/programs/squads_multisig_program/src/instructions/proposal_vote.rs b/programs/squads_multisig_program/src/instructions/proposal_vote.rs index 67b83d69..e2d26065 100644 --- a/programs/squads_multisig_program/src/instructions/proposal_vote.rs +++ b/programs/squads_multisig_program/src/instructions/proposal_vote.rs @@ -140,7 +140,9 @@ impl ProposalVote<'_> { let proposal = &mut ctx.accounts.proposal; let member = &mut ctx.accounts.member; - proposal.cancelled.retain(|k| multisig.is_member(*k).is_some()); + proposal + .cancelled + .retain(|k| multisig.is_member(*k).is_some()); proposal.cancel(member.key(), usize::from(multisig.threshold))?; @@ -167,7 +169,6 @@ impl ProposalCancel<'_> { MultisigError::Unauthorized ); - require!( matches!(proposal.status, ProposalStatus::Approved { .. }), MultisigError::InvalidProposalStatus @@ -187,12 +188,19 @@ impl ProposalCancel<'_> { let system_program = &ctx.accounts.system_program; // ensure that the cancel array contains no keys that are not currently members - proposal.cancelled.retain(|k| multisig.is_member(*k).is_some()); + proposal + .cancelled + .retain(|k| multisig.is_member(*k).is_some()); proposal.cancel(member.key(), usize::from(multisig.threshold))?; // reallocate the proposal size if needed - Proposal::realloc_if_needed(proposal.to_account_info(), multisig.members.len(), Some(member.to_account_info()), Some(system_program.to_account_info()))?; + Proposal::realloc_if_needed( + proposal.to_account_info(), + multisig.members.len(), + Some(member.to_account_info()), + Some(system_program.to_account_info()), + )?; Ok(()) } } diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs index 12115e32..ea1bf37c 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs @@ -49,9 +49,7 @@ impl TransactionBufferClose<'_> { /// Create a new vault transaction. #[access_control(ctx.accounts.validate())] - pub fn transaction_buffer_close( - ctx: Context, - ) -> Result<()> { + pub fn transaction_buffer_close(ctx: Context) -> Result<()> { Ok(()) } } diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs index 69ed7c67..ff216674 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs @@ -1,8 +1,8 @@ use anchor_lang::prelude::*; use crate::errors::*; -use crate::state::*; use crate::state::MAX_BUFFER_SIZE; +use crate::state::*; #[derive(AnchorSerialize, AnchorDeserialize)] pub struct TransactionBufferCreateArgs { @@ -68,7 +68,10 @@ impl TransactionBufferCreate<'_> { ); // Final Buffer Size must not exceed 4000 bytes - require!(args.final_buffer_size as usize <= MAX_BUFFER_SIZE, MultisigError::FinalBufferSizeExceeded); + require!( + args.final_buffer_size as usize <= MAX_BUFFER_SIZE, + MultisigError::FinalBufferSizeExceeded + ); Ok(()) } @@ -97,11 +100,9 @@ impl TransactionBufferCreate<'_> { transaction_buffer.final_buffer_size = args.final_buffer_size; transaction_buffer.buffer = args.buffer; - // Invariant function on the transaction buffer transaction_buffer.invariant()?; Ok(()) } } - diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs index 84a53d48..7fcbd6b1 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs @@ -63,7 +63,8 @@ impl TransactionBufferExtend<'_> { let current_buffer_size = transaction_buffer.buffer.len() as u16; let remaining_space = transaction_buffer .final_buffer_size - .checked_sub(current_buffer_size).unwrap(); + .checked_sub(current_buffer_size) + .unwrap(); // Check if the new data exceeds the remaining space let new_data_size = args.buffer.len() as u16; diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs index 2a12a58f..db21ec86 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs @@ -24,7 +24,7 @@ pub struct VaultTransactionCreate<'info> { )] pub multisig: Account<'info, Multisig>, - #[account( + #[account( init, payer = rent_payer, space = VaultTransaction::size(args.ephemeral_signers, &args.transaction_message)?, diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index fc5ffd87..e0a94b19 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -15,6 +15,7 @@ pub use instructions::*; pub use state::*; pub use utils::SmallVec; +pub mod allocator; pub mod errors; pub mod instructions; pub mod state; @@ -184,9 +185,7 @@ pub mod squads_multisig_program { } /// Close a transaction buffer account. - pub fn transaction_buffer_close( - ctx: Context, - ) -> Result<()> { + pub fn transaction_buffer_close(ctx: Context) -> Result<()> { TransactionBufferClose::transaction_buffer_close(ctx) } @@ -317,4 +316,8 @@ pub mod squads_multisig_program { pub fn batch_accounts_close(ctx: Context) -> Result<()> { BatchAccountsClose::batch_accounts_close(ctx) } + // Uncomment to enable the heap_test instruction + // pub fn heap_test(ctx: Context, length: u64) -> Result<()> { + // HeapTest::handler(ctx, length) + // } } diff --git a/programs/squads_multisig_program/src/state/mod.rs b/programs/squads_multisig_program/src/state/mod.rs index 974f3ddf..8eec0934 100644 --- a/programs/squads_multisig_program/src/state/mod.rs +++ b/programs/squads_multisig_program/src/state/mod.rs @@ -5,8 +5,8 @@ pub use program_config::*; pub use proposal::*; pub use seeds::*; pub use spending_limit::*; -pub use vault_transaction::*; pub use transaction_buffer::*; +pub use vault_transaction::*; mod batch; mod config_transaction; @@ -15,5 +15,5 @@ mod program_config; mod proposal; mod seeds; mod spending_limit; +mod transaction_buffer; mod vault_transaction; -mod transaction_buffer; \ No newline at end of file diff --git a/programs/squads_multisig_program/src/state/proposal.rs b/programs/squads_multisig_program/src/state/proposal.rs index 1f8c06bc..de5010cc 100644 --- a/programs/squads_multisig_program/src/state/proposal.rs +++ b/programs/squads_multisig_program/src/state/proposal.rs @@ -150,7 +150,10 @@ impl Proposal { AccountInfo::realloc(&proposal, account_size_to_fit_members, false)?; // If more lamports are needed, transfer them to the account. - let rent_exempt_lamports = Rent::get().unwrap().minimum_balance(account_size_to_fit_members).max(1); + let rent_exempt_lamports = Rent::get() + .unwrap() + .minimum_balance(account_size_to_fit_members) + .max(1); let top_up_lamports = rent_exempt_lamports.saturating_sub(proposal.to_account_info().lamports()); diff --git a/programs/squads_multisig_program/src/state/seeds.rs b/programs/squads_multisig_program/src/state/seeds.rs index c3b201b1..a4f6fb55 100644 --- a/programs/squads_multisig_program/src/state/seeds.rs +++ b/programs/squads_multisig_program/src/state/seeds.rs @@ -7,4 +7,4 @@ pub const SEED_BATCH_TRANSACTION: &[u8] = b"batch_transaction"; pub const SEED_VAULT: &[u8] = b"vault"; pub const SEED_EPHEMERAL_SIGNER: &[u8] = b"ephemeral_signer"; pub const SEED_SPENDING_LIMIT: &[u8] = b"spending_limit"; -pub const SEED_TRANSACTION_BUFFER: &[u8] = b"transaction_buffer"; \ No newline at end of file +pub const SEED_TRANSACTION_BUFFER: &[u8] = b"transaction_buffer"; diff --git a/programs/squads_multisig_program/src/utils/executable_transaction_message.rs b/programs/squads_multisig_program/src/utils/executable_transaction_message.rs index 53a93251..eb5ba797 100644 --- a/programs/squads_multisig_program/src/utils/executable_transaction_message.rs +++ b/programs/squads_multisig_program/src/utils/executable_transaction_message.rs @@ -195,7 +195,7 @@ impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> { .collect::>(); // Add the vault seeds. signer_seeds.push(&vault_seeds); - + // NOTE: `self.to_instructions_and_accounts()` calls `take()` on // `self.message.instructions`, therefore after this point no more // references or usages of `self.message` should be made to avoid diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json deleted file mode 100644 index 38b80e98..00000000 --- a/sdk/multisig/idl/squads_multisig_program.json +++ /dev/null @@ -1,3494 +0,0 @@ -{ - "version": "2.0.0", - "name": "squads_multisig_program", - "instructions": [ - { - "name": "programConfigInit", - "docs": [ - "Initialize the program config." - ], - "accounts": [ - { - "name": "programConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "initializer", - "isMut": true, - "isSigner": true, - "docs": [ - "The hard-coded account that is used to initialize the program config once." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ProgramConfigInitArgs" - } - } - ] - }, - { - "name": "programConfigSetAuthority", - "docs": [ - "Set the `authority` parameter of the program config." - ], - "accounts": [ - { - "name": "programConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ProgramConfigSetAuthorityArgs" - } - } - ] - }, - { - "name": "programConfigSetMultisigCreationFee", - "docs": [ - "Set the `multisig_creation_fee` parameter of the program config." - ], - "accounts": [ - { - "name": "programConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ProgramConfigSetMultisigCreationFeeArgs" - } - } - ] - }, - { - "name": "programConfigSetTreasury", - "docs": [ - "Set the `treasury` parameter of the program config." - ], - "accounts": [ - { - "name": "programConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ProgramConfigSetTreasuryArgs" - } - } - ] - }, - { - "name": "multisigCreate", - "docs": [ - "Create a multisig." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "createKey", - "isMut": false, - "isSigner": true, - "docs": [ - "An ephemeral signer that is used as a seed for the Multisig PDA.", - "Must be a signer to prevent front-running attack by someone else but the original creator." - ] - }, - { - "name": "creator", - "isMut": true, - "isSigner": true, - "docs": [ - "The creator of the multisig." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigCreateArgs" - } - } - ] - }, - { - "name": "multisigCreateV2", - "docs": [ - "Create a multisig." - ], - "accounts": [ - { - "name": "programConfig", - "isMut": false, - "isSigner": false, - "docs": [ - "Global program config account." - ] - }, - { - "name": "treasury", - "isMut": true, - "isSigner": false, - "docs": [ - "The treasury where the creation fee is transferred to." - ] - }, - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "createKey", - "isMut": false, - "isSigner": true, - "docs": [ - "An ephemeral signer that is used as a seed for the Multisig PDA.", - "Must be a signer to prevent front-running attack by someone else but the original creator." - ] - }, - { - "name": "creator", - "isMut": true, - "isSigner": true, - "docs": [ - "The creator of the multisig." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigCreateArgsV2" - } - } - ] - }, - { - "name": "multisigAddMember", - "docs": [ - "Add a new member to the controlled multisig." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "configAuthority", - "isMut": false, - "isSigner": true, - "docs": [ - "Multisig `config_authority` that must authorize the configuration change." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "isOptional": true, - "docs": [ - "The account that will be charged or credited in case the multisig account needs to reallocate space,", - "for example when adding a new member or a spending limit.", - "This is usually the same as `config_authority`, but can be a different account if needed." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "We might need it in case reallocation is needed." - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigAddMemberArgs" - } - } - ] - }, - { - "name": "multisigRemoveMember", - "docs": [ - "Remove a member/key from the controlled multisig." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "configAuthority", - "isMut": false, - "isSigner": true, - "docs": [ - "Multisig `config_authority` that must authorize the configuration change." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "isOptional": true, - "docs": [ - "The account that will be charged or credited in case the multisig account needs to reallocate space,", - "for example when adding a new member or a spending limit.", - "This is usually the same as `config_authority`, but can be a different account if needed." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "We might need it in case reallocation is needed." - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigRemoveMemberArgs" - } - } - ] - }, - { - "name": "multisigSetTimeLock", - "docs": [ - "Set the `time_lock` config parameter for the controlled multisig." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "configAuthority", - "isMut": false, - "isSigner": true, - "docs": [ - "Multisig `config_authority` that must authorize the configuration change." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "isOptional": true, - "docs": [ - "The account that will be charged or credited in case the multisig account needs to reallocate space,", - "for example when adding a new member or a spending limit.", - "This is usually the same as `config_authority`, but can be a different account if needed." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "We might need it in case reallocation is needed." - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigSetTimeLockArgs" - } - } - ] - }, - { - "name": "multisigChangeThreshold", - "docs": [ - "Set the `threshold` config parameter for the controlled multisig." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "configAuthority", - "isMut": false, - "isSigner": true, - "docs": [ - "Multisig `config_authority` that must authorize the configuration change." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "isOptional": true, - "docs": [ - "The account that will be charged or credited in case the multisig account needs to reallocate space,", - "for example when adding a new member or a spending limit.", - "This is usually the same as `config_authority`, but can be a different account if needed." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "We might need it in case reallocation is needed." - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigChangeThresholdArgs" - } - } - ] - }, - { - "name": "multisigSetConfigAuthority", - "docs": [ - "Set the multisig `config_authority`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "configAuthority", - "isMut": false, - "isSigner": true, - "docs": [ - "Multisig `config_authority` that must authorize the configuration change." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "isOptional": true, - "docs": [ - "The account that will be charged or credited in case the multisig account needs to reallocate space,", - "for example when adding a new member or a spending limit.", - "This is usually the same as `config_authority`, but can be a different account if needed." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "We might need it in case reallocation is needed." - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigSetConfigAuthorityArgs" - } - } - ] - }, - { - "name": "multisigSetRentCollector", - "docs": [ - "Set the multisig `rent_collector`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "configAuthority", - "isMut": false, - "isSigner": true, - "docs": [ - "Multisig `config_authority` that must authorize the configuration change." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "isOptional": true, - "docs": [ - "The account that will be charged or credited in case the multisig account needs to reallocate space,", - "for example when adding a new member or a spending limit.", - "This is usually the same as `config_authority`, but can be a different account if needed." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "We might need it in case reallocation is needed." - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigSetRentCollectorArgs" - } - } - ] - }, - { - "name": "multisigAddSpendingLimit", - "docs": [ - "Create a new spending limit for the controlled multisig." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "configAuthority", - "isMut": false, - "isSigner": true, - "docs": [ - "Multisig `config_authority` that must authorize the configuration change." - ] - }, - { - "name": "spendingLimit", - "isMut": true, - "isSigner": false - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "docs": [ - "This is usually the same as `config_authority`, but can be a different account if needed." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigAddSpendingLimitArgs" - } - } - ] - }, - { - "name": "multisigRemoveSpendingLimit", - "docs": [ - "Remove the spending limit from the controlled multisig." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "configAuthority", - "isMut": false, - "isSigner": true, - "docs": [ - "Multisig `config_authority` that must authorize the configuration change." - ] - }, - { - "name": "spendingLimit", - "isMut": true, - "isSigner": false - }, - { - "name": "rentCollector", - "isMut": true, - "isSigner": false, - "docs": [ - "This is usually the same as `config_authority`, but can be a different account if needed." - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigRemoveSpendingLimitArgs" - } - } - ] - }, - { - "name": "configTransactionCreate", - "docs": [ - "Create a new config transaction." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "transaction", - "isMut": true, - "isSigner": false - }, - { - "name": "creator", - "isMut": false, - "isSigner": true, - "docs": [ - "The member of the multisig that is creating the transaction." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "docs": [ - "The payer for the transaction account rent." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ConfigTransactionCreateArgs" - } - } - ] - }, - { - "name": "configTransactionExecute", - "docs": [ - "Execute a config transaction.", - "The transaction must be `Approved`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false, - "docs": [ - "The multisig account that owns the transaction." - ] - }, - { - "name": "member", - "isMut": false, - "isSigner": true, - "docs": [ - "One of the multisig members with `Execute` permission." - ] - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false, - "docs": [ - "The proposal account associated with the transaction." - ] - }, - { - "name": "transaction", - "isMut": false, - "isSigner": false, - "docs": [ - "The transaction to execute." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "isOptional": true, - "docs": [ - "The account that will be charged/credited in case the config transaction causes space reallocation,", - "for example when adding a new member, adding or removing a spending limit.", - "This is usually the same as `member`, but can be a different account if needed." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "We might need it in case reallocation is needed." - ] - } - ], - "args": [] - }, - { - "name": "vaultTransactionCreate", - "docs": [ - "Create a new vault transaction." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "transaction", - "isMut": true, - "isSigner": false - }, - { - "name": "creator", - "isMut": false, - "isSigner": true, - "docs": [ - "The member of the multisig that is creating the transaction." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "docs": [ - "The payer for the transaction account rent." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "VaultTransactionCreateArgs" - } - } - ] - }, - { - "name": "transactionBufferCreate", - "docs": [ - "Create a transaction buffer account." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "transactionBuffer", - "isMut": true, - "isSigner": false - }, - { - "name": "creator", - "isMut": false, - "isSigner": true, - "docs": [ - "The member of the multisig that is creating the transaction." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "docs": [ - "The payer for the transaction account rent." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "TransactionBufferCreateArgs" - } - } - ] - }, - { - "name": "transactionBufferClose", - "docs": [ - "Close a transaction buffer account." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "transactionBuffer", - "isMut": true, - "isSigner": false - }, - { - "name": "creator", - "isMut": false, - "isSigner": true, - "docs": [ - "The member of the multisig that created the TransactionBuffer." - ] - } - ], - "args": [] - }, - { - "name": "transactionBufferExtend", - "docs": [ - "Extend a transaction buffer account." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "transactionBuffer", - "isMut": true, - "isSigner": false - }, - { - "name": "creator", - "isMut": false, - "isSigner": true, - "docs": [ - "The member of the multisig that created the TransactionBuffer." - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "TransactionBufferExtendArgs" - } - } - ] - }, - { - "name": "vaultTransactionCreateFromBuffer", - "docs": [ - "Create a new vault transaction from a completed transaction buffer.", - "Finalized buffer hash must match `final_buffer_hash`" - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "transactionBuffer", - "isMut": true, - "isSigner": false - }, - { - "name": "transaction", - "isMut": true, - "isSigner": false - }, - { - "name": "creator", - "isMut": false, - "isSigner": true, - "docs": [ - "The member of the multisig that is creating the transaction." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "docs": [ - "The payer for the transaction account rent." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "VaultTransactionCreateFromBufferArgs" - } - } - ] - }, - { - "name": "vaultTransactionExecute", - "docs": [ - "Execute a vault transaction.", - "The transaction must be `Approved`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false, - "docs": [ - "The proposal account associated with the transaction." - ] - }, - { - "name": "transaction", - "isMut": false, - "isSigner": false, - "docs": [ - "The transaction to execute." - ] - }, - { - "name": "member", - "isMut": false, - "isSigner": true - } - ], - "args": [] - }, - { - "name": "batchCreate", - "docs": [ - "Create a new batch." - ], - "accounts": [ - { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "batch", - "isMut": true, - "isSigner": false - }, - { - "name": "creator", - "isMut": false, - "isSigner": true, - "docs": [ - "The member of the multisig that is creating the batch." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "docs": [ - "The payer for the batch account rent." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "BatchCreateArgs" - } - } - ] - }, - { - "name": "batchAddTransaction", - "docs": [ - "Add a transaction to the batch." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false, - "docs": [ - "Multisig account this batch belongs to." - ] - }, - { - "name": "proposal", - "isMut": false, - "isSigner": false, - "docs": [ - "The proposal account associated with the batch." - ] - }, - { - "name": "batch", - "isMut": true, - "isSigner": false - }, - { - "name": "transaction", - "isMut": true, - "isSigner": false, - "docs": [ - "`VaultBatchTransaction` account to initialize and add to the `batch`." - ] - }, - { - "name": "member", - "isMut": false, - "isSigner": true, - "docs": [ - "Member of the multisig." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "docs": [ - "The payer for the batch transaction account rent." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "BatchAddTransactionArgs" - } - } - ] - }, - { - "name": "batchExecuteTransaction", - "docs": [ - "Execute a transaction from the batch." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false, - "docs": [ - "Multisig account this batch belongs to." - ] - }, - { - "name": "member", - "isMut": false, - "isSigner": true, - "docs": [ - "Member of the multisig." - ] - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false, - "docs": [ - "The proposal account associated with the batch.", - "If `transaction` is the last in the batch, the `proposal` status will be set to `Executed`." - ] - }, - { - "name": "batch", - "isMut": true, - "isSigner": false - }, - { - "name": "transaction", - "isMut": false, - "isSigner": false, - "docs": [ - "Batch transaction to execute." - ] - } - ], - "args": [] - }, - { - "name": "proposalCreate", - "docs": [ - "Create a new multisig proposal." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false - }, - { - "name": "creator", - "isMut": false, - "isSigner": true, - "docs": [ - "The member of the multisig that is creating the proposal." - ] - }, - { - "name": "rentPayer", - "isMut": true, - "isSigner": true, - "docs": [ - "The payer for the proposal account rent." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ProposalCreateArgs" - } - } - ] - }, - { - "name": "proposalActivate", - "docs": [ - "Update status of a multisig proposal from `Draft` to `Active`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "member", - "isMut": true, - "isSigner": true - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "proposalApprove", - "docs": [ - "Approve a multisig proposal on behalf of the `member`.", - "The proposal must be `Active`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "member", - "isMut": true, - "isSigner": true - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ProposalVoteArgs" - } - } - ] - }, - { - "name": "proposalReject", - "docs": [ - "Reject a multisig proposal on behalf of the `member`.", - "The proposal must be `Active`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "member", - "isMut": true, - "isSigner": true - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ProposalVoteArgs" - } - } - ] - }, - { - "name": "proposalCancel", - "docs": [ - "Cancel a multisig proposal on behalf of the `member`.", - "The proposal must be `Approved`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "member", - "isMut": true, - "isSigner": true - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ProposalVoteArgs" - } - } - ] - }, - { - "name": "proposalCancelV2", - "docs": [ - "Cancel a multisig proposal on behalf of the `member`.", - "The proposal must be `Approved`.", - "This was introduced to incorporate proper state update, as old multisig members", - "may have lingering votes, and the proposal size may need to be reallocated to", - "accommodate the new amount of cancel votes.", - "The previous implemenation still works if the proposal size is in line with the", - "threshold size." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "member", - "isMut": true, - "isSigner": true - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "ProposalVoteArgs" - } - } - ] - }, - { - "name": "spendingLimitUse", - "docs": [ - "Use a spending limit to transfer tokens from a multisig vault to a destination account." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false, - "docs": [ - "The multisig account the `spending_limit` is for." - ] - }, - { - "name": "member", - "isMut": false, - "isSigner": true - }, - { - "name": "spendingLimit", - "isMut": true, - "isSigner": false, - "docs": [ - "The SpendingLimit account to use." - ] - }, - { - "name": "vault", - "isMut": true, - "isSigner": false, - "docs": [ - "Multisig vault account to transfer tokens from." - ] - }, - { - "name": "destination", - "isMut": true, - "isSigner": false, - "docs": [ - "Destination account to transfer tokens to." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "In case `spending_limit.mint` is SOL." - ] - }, - { - "name": "mint", - "isMut": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "The mint of the tokens to transfer in case `spending_limit.mint` is an SPL token." - ] - }, - { - "name": "vaultTokenAccount", - "isMut": true, - "isSigner": false, - "isOptional": true, - "docs": [ - "Multisig vault token account to transfer tokens from in case `spending_limit.mint` is an SPL token." - ] - }, - { - "name": "destinationTokenAccount", - "isMut": true, - "isSigner": false, - "isOptional": true, - "docs": [ - "Destination token account in case `spending_limit.mint` is an SPL token." - ] - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false, - "isOptional": true, - "docs": [ - "In case `spending_limit.mint` is an SPL token." - ] - } - ], - "args": [ - { - "name": "args", - "type": { - "defined": "SpendingLimitUseArgs" - } - } - ] - }, - { - "name": "configTransactionAccountsClose", - "docs": [ - "Closes a `ConfigTransaction` and the corresponding `Proposal`.", - "`transaction` can be closed if either:", - "- the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`.", - "- the `proposal` is stale." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false, - "docs": [ - "the logic within `config_transaction_accounts_close` does the rest of the checks." - ] - }, - { - "name": "transaction", - "isMut": true, - "isSigner": false, - "docs": [ - "ConfigTransaction corresponding to the `proposal`." - ] - }, - { - "name": "rentCollector", - "isMut": true, - "isSigner": false, - "docs": [ - "The rent collector." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "vaultTransactionAccountsClose", - "docs": [ - "Closes a `VaultTransaction` and the corresponding `Proposal`.", - "`transaction` can be closed if either:", - "- the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`.", - "- the `proposal` is stale and not `Approved`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false, - "docs": [ - "the logic within `vault_transaction_accounts_close` does the rest of the checks." - ] - }, - { - "name": "transaction", - "isMut": true, - "isSigner": false, - "docs": [ - "VaultTransaction corresponding to the `proposal`." - ] - }, - { - "name": "rentCollector", - "isMut": true, - "isSigner": false, - "docs": [ - "The rent collector." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "vaultBatchTransactionAccountClose", - "docs": [ - "Closes a `VaultBatchTransaction` belonging to the `batch` and `proposal`.", - "`transaction` can be closed if either:", - "- it's marked as executed within the `batch`;", - "- the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`.", - "- the `proposal` is stale and not `Approved`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "proposal", - "isMut": false, - "isSigner": false - }, - { - "name": "batch", - "isMut": true, - "isSigner": false, - "docs": [ - "`Batch` corresponding to the `proposal`." - ] - }, - { - "name": "transaction", - "isMut": true, - "isSigner": false, - "docs": [ - "`VaultBatchTransaction` account to close.", - "The transaction must be the current last one in the batch." - ] - }, - { - "name": "rentCollector", - "isMut": true, - "isSigner": false, - "docs": [ - "The rent collector." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "batchAccountsClose", - "docs": [ - "Closes Batch and the corresponding Proposal accounts for proposals in terminal states:", - "`Executed`, `Rejected`, or `Cancelled` or stale proposals that aren't `Approved`.", - "", - "This instruction is only allowed to be executed when all `VaultBatchTransaction` accounts", - "in the `batch` are already closed: `batch.size == 0`." - ], - "accounts": [ - { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false, - "docs": [ - "the logic within `batch_accounts_close` does the rest of the checks." - ] - }, - { - "name": "batch", - "isMut": true, - "isSigner": false, - "docs": [ - "`Batch` corresponding to the `proposal`." - ] - }, - { - "name": "rentCollector", - "isMut": true, - "isSigner": false, - "docs": [ - "The rent collector." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - } - ], - "accounts": [ - { - "name": "Batch", - "docs": [ - "Stores data required for serial execution of a batch of multisig vault transactions.", - "Vault transaction is a transaction that's executed on behalf of the multisig vault PDA", - "and wraps arbitrary Solana instructions, typically calling into other Solana programs.", - "The transactions themselves are stored in separate PDAs associated with the this account." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "multisig", - "docs": [ - "The multisig this belongs to." - ], - "type": "publicKey" - }, - { - "name": "creator", - "docs": [ - "Member of the Multisig who submitted the batch." - ], - "type": "publicKey" - }, - { - "name": "index", - "docs": [ - "Index of this batch within the multisig transactions." - ], - "type": "u64" - }, - { - "name": "bump", - "docs": [ - "PDA bump." - ], - "type": "u8" - }, - { - "name": "vaultIndex", - "docs": [ - "Index of the vault this batch belongs to." - ], - "type": "u8" - }, - { - "name": "vaultBump", - "docs": [ - "Derivation bump of the vault PDA this batch belongs to." - ], - "type": "u8" - }, - { - "name": "size", - "docs": [ - "Number of transactions in the batch." - ], - "type": "u32" - }, - { - "name": "executedTransactionIndex", - "docs": [ - "Index of the last executed transaction within the batch.", - "0 means that no transactions have been executed yet." - ], - "type": "u32" - } - ] - } - }, - { - "name": "VaultBatchTransaction", - "docs": [ - "Stores data required for execution of one transaction from a batch." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "bump", - "docs": [ - "PDA bump." - ], - "type": "u8" - }, - { - "name": "ephemeralSignerBumps", - "docs": [ - "Derivation bumps for additional signers.", - "Some transactions require multiple signers. Often these additional signers are \"ephemeral\" keypairs", - "that are generated on the client with a sole purpose of signing the transaction and be discarded immediately after.", - "When wrapping such transactions into multisig ones, we replace these \"ephemeral\" signing keypairs", - "with PDAs derived from the transaction's `transaction_index` and controlled by the Multisig Program;", - "during execution the program includes the seeds of these PDAs into the `invoke_signed` calls,", - "thus \"signing\" on behalf of these PDAs." - ], - "type": "bytes" - }, - { - "name": "message", - "docs": [ - "data required for executing the transaction." - ], - "type": { - "defined": "VaultTransactionMessage" - } - } - ] - } - }, - { - "name": "ConfigTransaction", - "docs": [ - "Stores data required for execution of a multisig configuration transaction.", - "Config transaction can perform a predefined set of actions on the Multisig PDA, such as adding/removing members,", - "changing the threshold, etc." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "multisig", - "docs": [ - "The multisig this belongs to." - ], - "type": "publicKey" - }, - { - "name": "creator", - "docs": [ - "Member of the Multisig who submitted the transaction." - ], - "type": "publicKey" - }, - { - "name": "index", - "docs": [ - "Index of this transaction within the multisig." - ], - "type": "u64" - }, - { - "name": "bump", - "docs": [ - "bump for the transaction seeds." - ], - "type": "u8" - }, - { - "name": "actions", - "docs": [ - "Action to be performed on the multisig." - ], - "type": { - "vec": { - "defined": "ConfigAction" - } - } - } - ] - } - }, - { - "name": "Multisig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "createKey", - "docs": [ - "Key that is used to seed the multisig PDA." - ], - "type": "publicKey" - }, - { - "name": "configAuthority", - "docs": [ - "The authority that can change the multisig config.", - "This is a very important parameter as this authority can change the members and threshold.", - "", - "The convention is to set this to `Pubkey::default()`.", - "In this case, the multisig becomes autonomous, so every config change goes through", - "the normal process of voting by the members.", - "", - "However, if this parameter is set to any other key, all the config changes for this multisig", - "will need to be signed by the `config_authority`. We call such a multisig a \"controlled multisig\"." - ], - "type": "publicKey" - }, - { - "name": "threshold", - "docs": [ - "Threshold for signatures." - ], - "type": "u16" - }, - { - "name": "timeLock", - "docs": [ - "How many seconds must pass between transaction voting settlement and execution." - ], - "type": "u32" - }, - { - "name": "transactionIndex", - "docs": [ - "Last transaction index. 0 means no transactions have been created." - ], - "type": "u64" - }, - { - "name": "staleTransactionIndex", - "docs": [ - "Last stale transaction index. All transactions up until this index are stale.", - "This index is updated when multisig config (members/threshold/time_lock) changes." - ], - "type": "u64" - }, - { - "name": "rentCollector", - "docs": [ - "The address where the rent for the accounts related to executed, rejected, or cancelled", - "transactions can be reclaimed. If set to `None`, the rent reclamation feature is turned off." - ], - "type": { - "option": "publicKey" - } - }, - { - "name": "bump", - "docs": [ - "Bump for the multisig PDA seed." - ], - "type": "u8" - }, - { - "name": "members", - "docs": [ - "Members of the multisig." - ], - "type": { - "vec": { - "defined": "Member" - } - } - } - ] - } - }, - { - "name": "ProgramConfig", - "docs": [ - "Global program configuration account." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "docs": [ - "The authority which can update the config." - ], - "type": "publicKey" - }, - { - "name": "multisigCreationFee", - "docs": [ - "The lamports amount charged for creating a new multisig account.", - "This fee is sent to the `treasury` account." - ], - "type": "u64" - }, - { - "name": "treasury", - "docs": [ - "The treasury account to send charged fees to." - ], - "type": "publicKey" - }, - { - "name": "reserved", - "docs": [ - "Reserved for future use." - ], - "type": { - "array": [ - "u8", - 64 - ] - } - } - ] - } - }, - { - "name": "Proposal", - "docs": [ - "Stores the data required for tracking the status of a multisig proposal.", - "Each `Proposal` has a 1:1 association with a transaction account, e.g. a `VaultTransaction` or a `ConfigTransaction`;", - "the latter can be executed only after the `Proposal` has been approved and its time lock is released." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "multisig", - "docs": [ - "The multisig this belongs to." - ], - "type": "publicKey" - }, - { - "name": "transactionIndex", - "docs": [ - "Index of the multisig transaction this proposal is associated with." - ], - "type": "u64" - }, - { - "name": "status", - "docs": [ - "The status of the transaction." - ], - "type": { - "defined": "ProposalStatus" - } - }, - { - "name": "bump", - "docs": [ - "PDA bump." - ], - "type": "u8" - }, - { - "name": "approved", - "docs": [ - "Keys that have approved/signed." - ], - "type": { - "vec": "publicKey" - } - }, - { - "name": "rejected", - "docs": [ - "Keys that have rejected." - ], - "type": { - "vec": "publicKey" - } - }, - { - "name": "cancelled", - "docs": [ - "Keys that have cancelled (Approved only)." - ], - "type": { - "vec": "publicKey" - } - } - ] - } - }, - { - "name": "SpendingLimit", - "type": { - "kind": "struct", - "fields": [ - { - "name": "multisig", - "docs": [ - "The multisig this belongs to." - ], - "type": "publicKey" - }, - { - "name": "createKey", - "docs": [ - "Key that is used to seed the SpendingLimit PDA." - ], - "type": "publicKey" - }, - { - "name": "vaultIndex", - "docs": [ - "The index of the vault that the spending limit is for." - ], - "type": "u8" - }, - { - "name": "mint", - "docs": [ - "The token mint the spending limit is for.", - "Pubkey::default() means SOL.", - "use NATIVE_MINT for Wrapped SOL." - ], - "type": "publicKey" - }, - { - "name": "amount", - "docs": [ - "The amount of tokens that can be spent in a period.", - "This amount is in decimals of the mint,", - "so 1 SOL would be `1_000_000_000` and 1 USDC would be `1_000_000`." - ], - "type": "u64" - }, - { - "name": "period", - "docs": [ - "The reset period of the spending limit.", - "When it passes, the remaining amount is reset, unless it's `Period::OneTime`." - ], - "type": { - "defined": "Period" - } - }, - { - "name": "remainingAmount", - "docs": [ - "The remaining amount of tokens that can be spent in the current period.", - "When reaches 0, the spending limit cannot be used anymore until the period reset." - ], - "type": "u64" - }, - { - "name": "lastReset", - "docs": [ - "Unix timestamp marking the last time the spending limit was reset (or created)." - ], - "type": "i64" - }, - { - "name": "bump", - "docs": [ - "PDA bump." - ], - "type": "u8" - }, - { - "name": "members", - "docs": [ - "Members of the multisig that can use the spending limit.", - "In case a member is removed from the multisig, the spending limit will remain existent", - "(until explicitly deleted), but the removed member will not be able to use it anymore." - ], - "type": { - "vec": "publicKey" - } - }, - { - "name": "destinations", - "docs": [ - "The destination addresses the spending limit is allowed to sent funds to.", - "If empty, funds can be sent to any address." - ], - "type": { - "vec": "publicKey" - } - } - ] - } - }, - { - "name": "TransactionBuffer", - "type": { - "kind": "struct", - "fields": [ - { - "name": "multisig", - "docs": [ - "The multisig this belongs to." - ], - "type": "publicKey" - }, - { - "name": "creator", - "docs": [ - "Member of the Multisig who created the TransactionBuffer." - ], - "type": "publicKey" - }, - { - "name": "vaultIndex", - "docs": [ - "Vault index of the transaction this buffer belongs to." - ], - "type": "u8" - }, - { - "name": "transactionIndex", - "docs": [ - "Index of the transaction this buffer belongs to." - ], - "type": "u64" - }, - { - "name": "finalBufferHash", - "docs": [ - "Hash of the final assembled transaction message." - ], - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "finalBufferSize", - "docs": [ - "The size of the final assembled transaction message." - ], - "type": "u16" - }, - { - "name": "buffer", - "docs": [ - "The buffer of the transaction message." - ], - "type": "bytes" - } - ] - } - }, - { - "name": "VaultTransaction", - "docs": [ - "Stores data required for tracking the voting and execution status of a vault transaction.", - "Vault transaction is a transaction that's executed on behalf of the multisig vault PDA", - "and wraps arbitrary Solana instructions, typically calling into other Solana programs." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "multisig", - "docs": [ - "The multisig this belongs to." - ], - "type": "publicKey" - }, - { - "name": "creator", - "docs": [ - "Member of the Multisig who submitted the transaction." - ], - "type": "publicKey" - }, - { - "name": "index", - "docs": [ - "Index of this transaction within the multisig." - ], - "type": "u64" - }, - { - "name": "bump", - "docs": [ - "bump for the transaction seeds." - ], - "type": "u8" - }, - { - "name": "vaultIndex", - "docs": [ - "Index of the vault this transaction belongs to." - ], - "type": "u8" - }, - { - "name": "vaultBump", - "docs": [ - "Derivation bump of the vault PDA this transaction belongs to." - ], - "type": "u8" - }, - { - "name": "ephemeralSignerBumps", - "docs": [ - "Derivation bumps for additional signers.", - "Some transactions require multiple signers. Often these additional signers are \"ephemeral\" keypairs", - "that are generated on the client with a sole purpose of signing the transaction and be discarded immediately after.", - "When wrapping such transactions into multisig ones, we replace these \"ephemeral\" signing keypairs", - "with PDAs derived from the MultisigTransaction's `transaction_index` and controlled by the Multisig Program;", - "during execution the program includes the seeds of these PDAs into the `invoke_signed` calls,", - "thus \"signing\" on behalf of these PDAs." - ], - "type": "bytes" - }, - { - "name": "message", - "docs": [ - "data required for executing the transaction." - ], - "type": { - "defined": "VaultTransactionMessage" - } - } - ] - } - } - ], - "types": [ - { - "name": "BatchAddTransactionArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "ephemeralSigners", - "docs": [ - "Number of ephemeral signing PDAs required by the transaction." - ], - "type": "u8" - }, - { - "name": "transactionMessage", - "type": "bytes" - } - ] - } - }, - { - "name": "BatchCreateArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "vaultIndex", - "docs": [ - "Index of the vault this transaction belongs to." - ], - "type": "u8" - }, - { - "name": "memo", - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "ConfigTransactionCreateArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "actions", - "type": { - "vec": { - "defined": "ConfigAction" - } - } - }, - { - "name": "memo", - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "MultisigAddSpendingLimitArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "createKey", - "docs": [ - "Key that is used to seed the SpendingLimit PDA." - ], - "type": "publicKey" - }, - { - "name": "vaultIndex", - "docs": [ - "The index of the vault that the spending limit is for." - ], - "type": "u8" - }, - { - "name": "mint", - "docs": [ - "The token mint the spending limit is for." - ], - "type": "publicKey" - }, - { - "name": "amount", - "docs": [ - "The amount of tokens that can be spent in a period.", - "This amount is in decimals of the mint,", - "so 1 SOL would be `1_000_000_000` and 1 USDC would be `1_000_000`." - ], - "type": "u64" - }, - { - "name": "period", - "docs": [ - "The reset period of the spending limit.", - "When it passes, the remaining amount is reset, unless it's `Period::OneTime`." - ], - "type": { - "defined": "Period" - } - }, - { - "name": "members", - "docs": [ - "Members of the Spending Limit that can use it.", - "Don't have to be members of the multisig." - ], - "type": { - "vec": "publicKey" - } - }, - { - "name": "destinations", - "docs": [ - "The destination addresses the spending limit is allowed to sent funds to.", - "If empty, funds can be sent to any address." - ], - "type": { - "vec": "publicKey" - } - }, - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "MultisigAddMemberArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "newMember", - "type": { - "defined": "Member" - } - }, - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "MultisigRemoveMemberArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "oldMember", - "type": "publicKey" - }, - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "MultisigChangeThresholdArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "newThreshold", - "type": "u16" - }, - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "MultisigSetTimeLockArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "timeLock", - "type": "u32" - }, - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "MultisigSetConfigAuthorityArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "configAuthority", - "type": "publicKey" - }, - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "MultisigSetRentCollectorArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "rentCollector", - "type": { - "option": "publicKey" - } - }, - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "MultisigCreateArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "configAuthority", - "docs": [ - "The authority that can configure the multisig: add/remove members, change the threshold, etc.", - "Should be set to `None` for autonomous multisigs." - ], - "type": { - "option": "publicKey" - } - }, - { - "name": "threshold", - "docs": [ - "The number of signatures required to execute a transaction." - ], - "type": "u16" - }, - { - "name": "members", - "docs": [ - "The members of the multisig." - ], - "type": { - "vec": { - "defined": "Member" - } - } - }, - { - "name": "timeLock", - "docs": [ - "How many seconds must pass between transaction voting, settlement, and execution." - ], - "type": "u32" - }, - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "MultisigCreateArgsV2", - "type": { - "kind": "struct", - "fields": [ - { - "name": "configAuthority", - "docs": [ - "The authority that can configure the multisig: add/remove members, change the threshold, etc.", - "Should be set to `None` for autonomous multisigs." - ], - "type": { - "option": "publicKey" - } - }, - { - "name": "threshold", - "docs": [ - "The number of signatures required to execute a transaction." - ], - "type": "u16" - }, - { - "name": "members", - "docs": [ - "The members of the multisig." - ], - "type": { - "vec": { - "defined": "Member" - } - } - }, - { - "name": "timeLock", - "docs": [ - "How many seconds must pass between transaction voting, settlement, and execution." - ], - "type": "u32" - }, - { - "name": "rentCollector", - "docs": [ - "The address where the rent for the accounts related to executed, rejected, or cancelled", - "transactions can be reclaimed. If set to `None`, the rent reclamation feature is turned off." - ], - "type": { - "option": "publicKey" - } - }, - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "MultisigRemoveSpendingLimitArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "ProgramConfigInitArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "docs": [ - "The authority that can configure the program config: change the treasury, etc." - ], - "type": "publicKey" - }, - { - "name": "multisigCreationFee", - "docs": [ - "The fee that is charged for creating a new multisig." - ], - "type": "u64" - }, - { - "name": "treasury", - "docs": [ - "The treasury where the creation fee is transferred to." - ], - "type": "publicKey" - } - ] - } - }, - { - "name": "ProgramConfigSetAuthorityArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "newAuthority", - "type": "publicKey" - } - ] - } - }, - { - "name": "ProgramConfigSetMultisigCreationFeeArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "newMultisigCreationFee", - "type": "u64" - } - ] - } - }, - { - "name": "ProgramConfigSetTreasuryArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "newTreasury", - "type": "publicKey" - } - ] - } - }, - { - "name": "ProposalCreateArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "transactionIndex", - "docs": [ - "Index of the multisig transaction this proposal is associated with." - ], - "type": "u64" - }, - { - "name": "draft", - "docs": [ - "Whether the proposal should be initialized with status `Draft`." - ], - "type": "bool" - } - ] - } - }, - { - "name": "ProposalVoteArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "memo", - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "SpendingLimitUseArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "docs": [ - "Amount of tokens to transfer." - ], - "type": "u64" - }, - { - "name": "decimals", - "docs": [ - "Decimals of the token mint. Used for double-checking against incorrect order of magnitude of `amount`." - ], - "type": "u8" - }, - { - "name": "memo", - "docs": [ - "Memo used for indexing." - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "TransactionBufferCreateArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "vaultIndex", - "docs": [ - "Index of the vault this transaction belongs to." - ], - "type": "u8" - }, - { - "name": "finalBufferHash", - "docs": [ - "Hash of the final assembled transaction message." - ], - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "finalBufferSize", - "docs": [ - "Final size of the buffer." - ], - "type": "u16" - }, - { - "name": "buffer", - "docs": [ - "Initial slice of the buffer." - ], - "type": "bytes" - } - ] - } - }, - { - "name": "TransactionBufferExtendArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "buffer", - "type": "bytes" - } - ] - } - }, - { - "name": "VaultTransactionCreateFromBufferArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "ephemeralSigners", - "docs": [ - "Number of ephemeral signing PDAs required by the transaction." - ], - "type": "u8" - }, - { - "name": "memo", - "docs": [ - "Optional Memo" - ], - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "VaultTransactionCreateArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "vaultIndex", - "docs": [ - "Index of the vault this transaction belongs to." - ], - "type": "u8" - }, - { - "name": "ephemeralSigners", - "docs": [ - "Number of ephemeral signing PDAs required by the transaction." - ], - "type": "u8" - }, - { - "name": "transactionMessage", - "type": "bytes" - }, - { - "name": "memo", - "type": { - "option": "string" - } - } - ] - } - }, - { - "name": "Member", - "type": { - "kind": "struct", - "fields": [ - { - "name": "key", - "type": "publicKey" - }, - { - "name": "permissions", - "type": { - "defined": "Permissions" - } - } - ] - } - }, - { - "name": "Permissions", - "docs": [ - "Bitmask for permissions." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "mask", - "type": "u8" - } - ] - } - }, - { - "name": "VaultTransactionMessage", - "type": { - "kind": "struct", - "fields": [ - { - "name": "numSigners", - "docs": [ - "The number of signer pubkeys in the account_keys vec." - ], - "type": "u8" - }, - { - "name": "numWritableSigners", - "docs": [ - "The number of writable signer pubkeys in the account_keys vec." - ], - "type": "u8" - }, - { - "name": "numWritableNonSigners", - "docs": [ - "The number of writable non-signer pubkeys in the account_keys vec." - ], - "type": "u8" - }, - { - "name": "accountKeys", - "docs": [ - "Unique account pubkeys (including program IDs) required for execution of the tx.", - "The signer pubkeys appear at the beginning of the vec, with writable pubkeys first, and read-only pubkeys following.", - "The non-signer pubkeys follow with writable pubkeys first and read-only ones following.", - "Program IDs are also stored at the end of the vec along with other non-signer non-writable pubkeys:", - "", - "```plaintext", - "[pubkey1, pubkey2, pubkey3, pubkey4, pubkey5, pubkey6, pubkey7, pubkey8]", - "|---writable---| |---readonly---| |---writable---| |---readonly---|", - "|------------signers-------------| |----------non-singers-----------|", - "```" - ], - "type": { - "vec": "publicKey" - } - }, - { - "name": "instructions", - "docs": [ - "List of instructions making up the tx." - ], - "type": { - "vec": { - "defined": "MultisigCompiledInstruction" - } - } - }, - { - "name": "addressTableLookups", - "docs": [ - "List of address table lookups used to load additional accounts", - "for this transaction." - ], - "type": { - "vec": { - "defined": "MultisigMessageAddressTableLookup" - } - } - } - ] - } - }, - { - "name": "MultisigCompiledInstruction", - "docs": [ - "Concise serialization schema for instructions that make up a transaction.", - "Closely mimics the Solana transaction wire format." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "programIdIndex", - "type": "u8" - }, - { - "name": "accountIndexes", - "docs": [ - "Indices into the tx's `account_keys` list indicating which accounts to pass to the instruction." - ], - "type": "bytes" - }, - { - "name": "data", - "docs": [ - "Instruction data." - ], - "type": "bytes" - } - ] - } - }, - { - "name": "MultisigMessageAddressTableLookup", - "docs": [ - "Address table lookups describe an on-chain address lookup table to use", - "for loading more readonly and writable accounts into a transaction." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "accountKey", - "docs": [ - "Address lookup table account key." - ], - "type": "publicKey" - }, - { - "name": "writableIndexes", - "docs": [ - "List of indexes used to load writable accounts." - ], - "type": "bytes" - }, - { - "name": "readonlyIndexes", - "docs": [ - "List of indexes used to load readonly accounts." - ], - "type": "bytes" - } - ] - } - }, - { - "name": "Vote", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Approve" - }, - { - "name": "Reject" - }, - { - "name": "Cancel" - } - ] - } - }, - { - "name": "ConfigAction", - "type": { - "kind": "enum", - "variants": [ - { - "name": "AddMember", - "fields": [ - { - "name": "newMember", - "type": { - "defined": "Member" - } - } - ] - }, - { - "name": "RemoveMember", - "fields": [ - { - "name": "oldMember", - "type": "publicKey" - } - ] - }, - { - "name": "ChangeThreshold", - "fields": [ - { - "name": "newThreshold", - "type": "u16" - } - ] - }, - { - "name": "SetTimeLock", - "fields": [ - { - "name": "newTimeLock", - "type": "u32" - } - ] - }, - { - "name": "AddSpendingLimit", - "fields": [ - { - "name": "createKey", - "docs": [ - "Key that is used to seed the SpendingLimit PDA." - ], - "type": "publicKey" - }, - { - "name": "vaultIndex", - "docs": [ - "The index of the vault that the spending limit is for." - ], - "type": "u8" - }, - { - "name": "mint", - "docs": [ - "The token mint the spending limit is for." - ], - "type": "publicKey" - }, - { - "name": "amount", - "docs": [ - "The amount of tokens that can be spent in a period.", - "This amount is in decimals of the mint,", - "so 1 SOL would be `1_000_000_000` and 1 USDC would be `1_000_000`." - ], - "type": "u64" - }, - { - "name": "period", - "docs": [ - "The reset period of the spending limit.", - "When it passes, the remaining amount is reset, unless it's `Period::OneTime`." - ], - "type": { - "defined": "Period" - } - }, - { - "name": "members", - "docs": [ - "Members of the multisig that can use the spending limit.", - "In case a member is removed from the multisig, the spending limit will remain existent", - "(until explicitly deleted), but the removed member will not be able to use it anymore." - ], - "type": { - "vec": "publicKey" - } - }, - { - "name": "destinations", - "docs": [ - "The destination addresses the spending limit is allowed to sent funds to.", - "If empty, funds can be sent to any address." - ], - "type": { - "vec": "publicKey" - } - } - ] - }, - { - "name": "RemoveSpendingLimit", - "fields": [ - { - "name": "spendingLimit", - "type": "publicKey" - } - ] - }, - { - "name": "SetRentCollector", - "fields": [ - { - "name": "newRentCollector", - "type": { - "option": "publicKey" - } - } - ] - } - ] - } - }, - { - "name": "ProposalStatus", - "docs": [ - "The status of a proposal.", - "Each variant wraps a timestamp of when the status was set." - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "Draft", - "fields": [ - { - "name": "timestamp", - "type": "i64" - } - ] - }, - { - "name": "Active", - "fields": [ - { - "name": "timestamp", - "type": "i64" - } - ] - }, - { - "name": "Rejected", - "fields": [ - { - "name": "timestamp", - "type": "i64" - } - ] - }, - { - "name": "Approved", - "fields": [ - { - "name": "timestamp", - "type": "i64" - } - ] - }, - { - "name": "Executing" - }, - { - "name": "Executed", - "fields": [ - { - "name": "timestamp", - "type": "i64" - } - ] - }, - { - "name": "Cancelled", - "fields": [ - { - "name": "timestamp", - "type": "i64" - } - ] - } - ] - } - }, - { - "name": "Period", - "docs": [ - "The reset period of the spending limit." - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "OneTime" - }, - { - "name": "Day" - }, - { - "name": "Week" - }, - { - "name": "Month" - } - ] - } - } - ], - "errors": [ - { - "code": 6000, - "name": "DuplicateMember", - "msg": "Found multiple members with the same pubkey" - }, - { - "code": 6001, - "name": "EmptyMembers", - "msg": "Members array is empty" - }, - { - "code": 6002, - "name": "TooManyMembers", - "msg": "Too many members, can be up to 65535" - }, - { - "code": 6003, - "name": "InvalidThreshold", - "msg": "Invalid threshold, must be between 1 and number of members with Vote permission" - }, - { - "code": 6004, - "name": "Unauthorized", - "msg": "Attempted to perform an unauthorized action" - }, - { - "code": 6005, - "name": "NotAMember", - "msg": "Provided pubkey is not a member of multisig" - }, - { - "code": 6006, - "name": "InvalidTransactionMessage", - "msg": "TransactionMessage is malformed." - }, - { - "code": 6007, - "name": "StaleProposal", - "msg": "Proposal is stale" - }, - { - "code": 6008, - "name": "InvalidProposalStatus", - "msg": "Invalid proposal status" - }, - { - "code": 6009, - "name": "InvalidTransactionIndex", - "msg": "Invalid transaction index" - }, - { - "code": 6010, - "name": "AlreadyApproved", - "msg": "Member already approved the transaction" - }, - { - "code": 6011, - "name": "AlreadyRejected", - "msg": "Member already rejected the transaction" - }, - { - "code": 6012, - "name": "AlreadyCancelled", - "msg": "Member already cancelled the transaction" - }, - { - "code": 6013, - "name": "InvalidNumberOfAccounts", - "msg": "Wrong number of accounts provided" - }, - { - "code": 6014, - "name": "InvalidAccount", - "msg": "Invalid account provided" - }, - { - "code": 6015, - "name": "RemoveLastMember", - "msg": "Cannot remove last member" - }, - { - "code": 6016, - "name": "NoVoters", - "msg": "Members don't include any voters" - }, - { - "code": 6017, - "name": "NoProposers", - "msg": "Members don't include any proposers" - }, - { - "code": 6018, - "name": "NoExecutors", - "msg": "Members don't include any executors" - }, - { - "code": 6019, - "name": "InvalidStaleTransactionIndex", - "msg": "`stale_transaction_index` must be <= `transaction_index`" - }, - { - "code": 6020, - "name": "NotSupportedForControlled", - "msg": "Instruction not supported for controlled multisig" - }, - { - "code": 6021, - "name": "TimeLockNotReleased", - "msg": "Proposal time lock has not been released" - }, - { - "code": 6022, - "name": "NoActions", - "msg": "Config transaction must have at least one action" - }, - { - "code": 6023, - "name": "MissingAccount", - "msg": "Missing account" - }, - { - "code": 6024, - "name": "InvalidMint", - "msg": "Invalid mint" - }, - { - "code": 6025, - "name": "InvalidDestination", - "msg": "Invalid destination" - }, - { - "code": 6026, - "name": "SpendingLimitExceeded", - "msg": "Spending limit exceeded" - }, - { - "code": 6027, - "name": "DecimalsMismatch", - "msg": "Decimals don't match the mint" - }, - { - "code": 6028, - "name": "UnknownPermission", - "msg": "Member has unknown permission" - }, - { - "code": 6029, - "name": "ProtectedAccount", - "msg": "Account is protected, it cannot be passed into a CPI as writable" - }, - { - "code": 6030, - "name": "TimeLockExceedsMaxAllowed", - "msg": "Time lock exceeds the maximum allowed (90 days)" - }, - { - "code": 6031, - "name": "IllegalAccountOwner", - "msg": "Account is not owned by Multisig program" - }, - { - "code": 6032, - "name": "RentReclamationDisabled", - "msg": "Rent reclamation is disabled for this multisig" - }, - { - "code": 6033, - "name": "InvalidRentCollector", - "msg": "Invalid rent collector address" - }, - { - "code": 6034, - "name": "ProposalForAnotherMultisig", - "msg": "Proposal is for another multisig" - }, - { - "code": 6035, - "name": "TransactionForAnotherMultisig", - "msg": "Transaction is for another multisig" - }, - { - "code": 6036, - "name": "TransactionNotMatchingProposal", - "msg": "Transaction doesn't match proposal" - }, - { - "code": 6037, - "name": "TransactionNotLastInBatch", - "msg": "Transaction is not last in batch" - }, - { - "code": 6038, - "name": "BatchNotEmpty", - "msg": "Batch is not empty" - }, - { - "code": 6039, - "name": "SpendingLimitInvalidAmount", - "msg": "Invalid SpendingLimit amount" - }, - { - "code": 6040, - "name": "FinalBufferHashMismatch", - "msg": "Final message buffer hash doesnt match the expected hash" - }, - { - "code": 6041, - "name": "FinalBufferSizeExceeded", - "msg": "Final buffer size cannot exceed 4000 bytes" - }, - { - "code": 6042, - "name": "FinalBufferSizeMismatch", - "msg": "Final buffer size mismatch" - } - ], - "metadata": { - "address": "SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf", - "origin": "anchor", - "binaryVersion": "0.29.0", - "libVersion": "=0.29.0" - } -} \ No newline at end of file diff --git a/sdk/multisig/src/generated/instructions/heapTest.ts b/sdk/multisig/src/generated/instructions/heapTest.ts new file mode 100644 index 00000000..786dd5fe --- /dev/null +++ b/sdk/multisig/src/generated/instructions/heapTest.ts @@ -0,0 +1,91 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as beet from '@metaplex-foundation/beet' +import * as web3 from '@solana/web3.js' + +/** + * @category Instructions + * @category HeapTest + * @category generated + */ +export type HeapTestInstructionArgs = { + length: beet.bignum +} +/** + * @category Instructions + * @category HeapTest + * @category generated + */ +export const heapTestStruct = new beet.BeetArgsStruct< + HeapTestInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['length', beet.u64], + ], + 'HeapTestInstructionArgs' +) +/** + * Accounts required by the _heapTest_ instruction + * + * @property [] authority + * @category Instructions + * @category HeapTest + * @category generated + */ +export type HeapTestInstructionAccounts = { + authority: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const heapTestInstructionDiscriminator = [ + 134, 205, 61, 111, 245, 170, 136, 43, +] + +/** + * Creates a _HeapTest_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category HeapTest + * @category generated + */ +export function createHeapTestInstruction( + accounts: HeapTestInstructionAccounts, + args: HeapTestInstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = heapTestStruct.serialize({ + instructionDiscriminator: heapTestInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.authority, + isWritable: false, + isSigner: false, + }, + ] + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc) + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }) + return ix +} diff --git a/sdk/multisig/src/generated/instructions/index.ts b/sdk/multisig/src/generated/instructions/index.ts index 9f89535d..9ca00bc8 100644 --- a/sdk/multisig/src/generated/instructions/index.ts +++ b/sdk/multisig/src/generated/instructions/index.ts @@ -5,6 +5,7 @@ export * from './batchExecuteTransaction' export * from './configTransactionAccountsClose' export * from './configTransactionCreate' export * from './configTransactionExecute' +export * from './heapTest' export * from './multisigAddMember' export * from './multisigAddSpendingLimit' export * from './multisigChangeThreshold' diff --git a/tests/index.ts b/tests/index.ts index 74846b1a..37dae815 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -19,4 +19,8 @@ import "./suites/examples/batch-sol-transfer"; import "./suites/examples/create-mint"; import "./suites/examples/immediate-execution"; import "./suites/examples/spending-limits"; -import "./suites/examples/transaction-buffer"; \ No newline at end of file +import "./suites/examples/transaction-buffer"; +// Uncomment to enable the heapTest instruction testing +//import "./suites/instructions/heapTest"; +import "./suites/examples/custom-heap"; + diff --git a/tests/suites/examples/custom-heap.ts b/tests/suites/examples/custom-heap.ts new file mode 100644 index 00000000..12a2679f --- /dev/null +++ b/tests/suites/examples/custom-heap.ts @@ -0,0 +1,331 @@ +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, + Transaction, + ComputeBudgetProgram, +} from "@solana/web3.js"; +import * as multisig from "@sqds/multisig"; +import { + TransactionBufferCreateArgs, + TransactionBufferCreateInstructionArgs, + VaultTransactionCreateFromBufferArgs, + VaultTransactionCreateFromBufferInstructionArgs, +} from "@sqds/multisig/lib/generated"; +import assert from "assert"; +import { BN } from "bn.js"; +import * as crypto from "crypto"; +import { + TestMembers, + createAutonomousMultisigV2, + createLocalhostConnection, + createTestTransferInstruction, + generateMultisigMembers, + getTestProgramId, + processBufferInChunks, +} from "../../utils"; + +const programId = getTestProgramId(); +const connection = createLocalhostConnection(); + +describe("Examples / Custom Heap Usage", () => { + let members: TestMembers; + + const createKey = Keypair.generate(); + + let multisigPda = multisig.getMultisigPda({ + createKey: createKey.publicKey, + programId, + })[0]; + + const [vaultPda] = multisig.getVaultPda({ + multisigPda, + index: 0, + programId, + }); + + // Set up a multisig with some transactions. + before(async () => { + members = await generateMultisigMembers(connection); + + // Create new autonomous multisig with rentCollector set to its default vault. + await createAutonomousMultisigV2({ + connection, + createKey, + members, + threshold: 1, + timeLock: 0, + rentCollector: vaultPda, + programId, + }); + + // Airdrop some SOL to the vault + let signature = await connection.requestAirdrop( + vaultPda, + 10 * LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(signature); + }); + + // We expect this to succeed when requesting extra heap. + it("execute large vault transaction (custom heap)", async () => { + const transactionIndex = 1n; + + const testIx = await createTestTransferInstruction(vaultPda, vaultPda, 1); + + let instructions = []; + + // Add 64 transfer instructions to the message. + for (let i = 0; i <= 59; i++) { + instructions.push(testIx); + } + + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: instructions, + }); + + //region Create & Upload Buffer + // Serialize the message. Must be done with this util function + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + const [transactionBuffer, _] = PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + new BN(Number(transactionIndex)).toBuffer("le", 8), + ], + programId + ); + + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + // Slice the message buffer into two parts. + const firstSlice = messageBuffer.slice(0, 700); + const bufferLength = messageBuffer.length; + + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + // Must be a SHA256 hash of the message buffer. + finalBufferHash: Array.from(messageHash), + finalBufferSize: bufferLength, + buffer: firstSlice, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + const message = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + tx.sign([members.proposer]); + + // Send first transaction. + const signature1 = await connection.sendRawTransaction(tx.serialize(), { + skipPreflight: true, + }); + await connection.confirmTransaction(signature1); + + const transactionBufferAccount = await connection.getAccountInfo( + transactionBuffer + ); + + const [txBufferDeser1] = + multisig.generated.TransactionBuffer.fromAccountInfo( + transactionBufferAccount! + ); + + // Check buffer account exists. + assert.notEqual(transactionBufferAccount, null); + assert.ok(transactionBufferAccount?.data.length! > 0); + + // First chunk uploaded. Check that length is as expected. + assert.equal(txBufferDeser1.buffer.length, 700); + + // Process the buffer in <=700 byte chunks. + await processBufferInChunks( + members.proposer as Keypair, + multisigPda, + transactionBuffer, + messageBuffer, + connection, + programId, + 700, + 700 + ); + + // Get account info and deserialize to run checks. + const transactionBufferInfo2 = await connection.getAccountInfo( + transactionBuffer + ); + const [txBufferDeser2] = + multisig.generated.TransactionBuffer.fromAccountInfo( + transactionBufferInfo2! + ); + + // Final chunk uploaded. Check that length is as expected. + assert.equal(txBufferDeser2.buffer.length, messageBuffer.byteLength); + + // Derive vault transaction PDA. + const [transactionPda] = multisig.getTransactionPda({ + multisigPda, + index: transactionIndex, + programId, + }); + //endregion + + //region Create Transaction From Buffer + // Create final instruction. + const thirdIx = + multisig.generated.createVaultTransactionCreateFromBufferInstruction( + { + multisig: multisigPda, + transactionBuffer, + transaction: transactionPda, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + ephemeralSigners: 0, + memo: null, + } as VaultTransactionCreateFromBufferArgs, + } as VaultTransactionCreateFromBufferInstructionArgs, + programId + ); + + // Request heap memory + const prelimHeap = ComputeBudgetProgram.requestHeapFrame({ + bytes: 8 * 32 * 1024, + }); + + const prelimHeapCU = ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_400_000, + }); + + const bufferConvertMessage = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [prelimHeap, prelimHeapCU, thirdIx], + }).compileToV0Message(); + + const bufferConvertTx = new VersionedTransaction(bufferConvertMessage); + + bufferConvertTx.sign([members.proposer]); + + // Send buffer conversion transaction. + const signature3 = await connection.sendRawTransaction( + bufferConvertTx.serialize(), + { + skipPreflight: true, + } + ); + await connection.confirmTransaction(signature3); + + const transactionInfo = + await multisig.accounts.VaultTransaction.fromAccountAddress( + connection, + transactionPda + ); + + // Ensure final vault transaction has 60 instructions + assert.equal(transactionInfo.message.instructions.length, 60); + //endregion + + //region Create, Vote, and Execute + // Create a proposal for the newly uploaded transaction. + const signature4 = await multisig.rpc.proposalCreate({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex: 1n, + creator: members.almighty, + isDraft: false, + programId, + }); + await connection.confirmTransaction(signature4); + + // Approve the proposal. + const signature5 = await multisig.rpc.proposalApprove({ + connection, + feePayer: members.almighty, + multisigPda, + transactionIndex: 1n, + member: members.almighty, + programId, + }); + await connection.confirmTransaction(signature5); + + // Execute the transaction. + const executeIx = await multisig.instructions.vaultTransactionExecute({ + connection, + multisigPda, + transactionIndex: 1n, + member: members.almighty.publicKey, + programId, + }); + + // Request heap for execution (it's very much needed here). + const computeBudgetIx = ComputeBudgetProgram.requestHeapFrame({ + bytes: 8 * 32 * 1024, + }); + + const computeBudgetCUIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_400_000, + }); + + const executeTx = new Transaction().add( + computeBudgetIx, + computeBudgetCUIx, + executeIx.instruction + ); + const signature6 = await connection.sendTransaction( + executeTx, + [members.almighty], + { skipPreflight: true } + ); + + await connection.confirmTransaction(signature6); + + const proposal = await multisig.getProposalPda({ + multisigPda, + transactionIndex: 1n, + programId, + })[0]; + + const proposalInfo = await multisig.accounts.Proposal.fromAccountAddress( + connection, + proposal + ); + + assert.equal(proposalInfo.status.__kind, "Executed"); + //endregion + }); +}); diff --git a/tests/suites/instructions/heapTest.ts b/tests/suites/instructions/heapTest.ts new file mode 100644 index 00000000..df4ed900 --- /dev/null +++ b/tests/suites/instructions/heapTest.ts @@ -0,0 +1,65 @@ +import { + ComputeBudgetProgram, + Keypair, + LAMPORTS_PER_SOL, + TransactionMessage, + VersionedTransaction +} from "@solana/web3.js"; +import * as multisig from "@sqds/multisig"; +import { + HeapTestInstructionArgs +} from "@sqds/multisig/lib/generated"; +import { + createLocalhostConnection, + getTestProgramId +} from "../../utils"; + +const programId = getTestProgramId(); +const connection = createLocalhostConnection(); + +describe("Instructions / transaction_buffer_close", () => { + it("heap test", async () => { + + let keypair = Keypair.generate(); + // Request airdrop + let signature = await connection.requestAirdrop( + keypair.publicKey, + 10 * LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(signature); + const createArgs: HeapTestInstructionArgs = { + length: 25000, + }; + const heapTestIx = multisig.generated.createHeapTestInstruction( + { + authority: keypair.publicKey, + }, + createArgs, + programId + ); + const computeBudgetIx = ComputeBudgetProgram.requestHeapFrame({ + bytes: 8 * 32 * 1024 + }); + const computeBudgetCUIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_400_000 + }); + // const heapTestMessage = new TransactionMessage({ + // payerKey: keypair.publicKey, + // recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + // instructions: [computeBudgetIx, computeBudgetCUIx, heapTestIx], + // }).compileToV0Message(); + const heapTestMessage = new TransactionMessage({ + payerKey: keypair.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [computeBudgetCUIx, heapTestIx], + }).compileToV0Message(); + const heapTestTx = new VersionedTransaction(heapTestMessage); + heapTestTx.sign([keypair]); + const heapTestSig = await connection.sendRawTransaction(heapTestTx.serialize(), { skipPreflight: true }); + console.log(heapTestSig); + await connection.confirmTransaction(heapTestSig); + + + + }); +}); \ No newline at end of file diff --git a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts index 824cdcd2..d87e53f4 100644 --- a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts +++ b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts @@ -5,7 +5,8 @@ import { SystemProgram, TransactionMessage, VersionedTransaction, - Transaction + Transaction, + ComputeBudgetProgram, } from "@solana/web3.js"; import * as multisig from "@sqds/multisig"; import { @@ -57,7 +58,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { connection, createKey, members, - threshold: 2, + threshold: 1, timeLock: 0, rentCollector: vaultPda, programId, @@ -95,11 +96,13 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { }); // Serialize the message. Must be done with this util function - const messageBuffer = multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ - message: testTransferMessage, - addressLookupTableAccounts: [], - vaultPda, - }); + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( [ @@ -164,18 +167,18 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { assert.ok(transactionBufferAccount?.data.length! > 0); // Need to add some deserialization to check if it actually worked. - const transactionBufferInfo1 = await connection.getAccountInfo(transactionBuffer); - const [txBufferDeser1] = await multisig.generated.TransactionBuffer.fromAccountInfo( - transactionBufferInfo1! + const transactionBufferInfo1 = await connection.getAccountInfo( + transactionBuffer ); + const [txBufferDeser1] = + await multisig.generated.TransactionBuffer.fromAccountInfo( + transactionBufferInfo1! + ); // First chunk uploaded. Check that length is as expected. assert.equal(txBufferDeser1.buffer.length, 700); - const secondSlice = messageBuffer.slice( - 700, - messageBuffer.byteLength - ); + const secondSlice = messageBuffer.slice(700, messageBuffer.byteLength); // Extned the buffer. const secondIx = @@ -211,12 +214,16 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { await connection.confirmTransaction(secondSignature); // Need to add some deserialization to check if it actually worked. - const transactionBufferInfo2 = await connection.getAccountInfo(transactionBuffer); - const [txBufferDeser2] = await multisig.generated.TransactionBuffer.fromAccountInfo( - transactionBufferInfo2! + const transactionBufferInfo2 = await connection.getAccountInfo( + transactionBuffer ); + const [txBufferDeser2] = + multisig.generated.TransactionBuffer.fromAccountInfo( + transactionBufferInfo2! + ); + + // Final chunk uploaded. Check that length is as expected. - // First chunk uploaded. Check that length is as expected. assert.equal(txBufferDeser2.buffer.length, messageBuffer.byteLength); // Derive vault transaction PDA. @@ -290,11 +297,13 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { instructions: [testIx], }); - const messageBuffer = multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ - message: testTransferMessage, - addressLookupTableAccounts: [], - vaultPda, - }); + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( [ @@ -309,24 +318,26 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { // Create a dummy hash of zeros const dummyHash = new Uint8Array(32).fill(0); - const createIx = multisig.generated.createTransactionBufferCreateInstruction( - { - multisig: multisigPda, - transactionBuffer, - creator: members.proposer.publicKey, - rentPayer: members.proposer.publicKey, - systemProgram: SystemProgram.programId, - }, - { - args: { - vaultIndex: 0, - finalBufferHash: Array.from(dummyHash), - finalBufferSize: messageBuffer.length, - buffer: messageBuffer, - } as TransactionBufferCreateArgs, - } as TransactionBufferCreateInstructionArgs, - programId - ); + const createIx = + multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + vaultIndex: 0, + finalBufferHash: Array.from(dummyHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + const createMessage = new TransactionMessage({ payerKey: members.proposer.publicKey, @@ -337,7 +348,9 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { const createTx = new VersionedTransaction(createMessage); createTx.sign([members.proposer]); - const createBufferSig = await connection.sendTransaction(createTx, { skipPreflight: true }); + const createBufferSig = await connection.sendTransaction(createTx, { + skipPreflight: true, + }); await connection.confirmTransaction(createBufferSig); const [transactionPda] = multisig.getTransactionPda({ @@ -346,23 +359,25 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { programId, }); - const createFromBufferIx = multisig.generated.createVaultTransactionCreateFromBufferInstruction( - { - multisig: multisigPda, - transactionBuffer, - transaction: transactionPda, - creator: members.proposer.publicKey, - rentPayer: members.proposer.publicKey, - systemProgram: SystemProgram.programId, - }, - { - args: { - ephemeralSigners: 0, - memo: null, - } as VaultTransactionCreateFromBufferArgs, - } as VaultTransactionCreateFromBufferInstructionArgs, - programId - ); + + const createFromBufferIx = + multisig.generated.createVaultTransactionCreateFromBufferInstruction( + { + multisig: multisigPda, + transactionBuffer, + transaction: transactionPda, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + ephemeralSigners: 0, + memo: null, + } as VaultTransactionCreateFromBufferArgs, + } as VaultTransactionCreateFromBufferInstructionArgs, + programId + ); const createFromBufferMessage = new TransactionMessage({ payerKey: members.proposer.publicKey, @@ -370,11 +385,17 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { instructions: [createFromBufferIx], }).compileToV0Message(); - const createFromBufferTx = new VersionedTransaction(createFromBufferMessage); + + const createFromBufferTx = new VersionedTransaction( + createFromBufferMessage + ); createFromBufferTx.sign([members.proposer]); await assert.rejects( - () => connection.sendTransaction(createFromBufferTx).catch(multisig.errors.translateAndThrowAnchorError), + () => + connection + .sendTransaction(createFromBufferTx) + .catch(multisig.errors.translateAndThrowAnchorError), /FinalBufferHashMismatch/ ); }); @@ -431,12 +452,11 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { [members.almighty], { skipPreflight: true } ); - await connection.confirmTransaction(signature4); - assert.doesNotThrow( - async () => await getLogs(connection, signature4), - /Error: memory allocation failed, out of memory/ - ) + const logs = (await getLogs(connection, signature4)).join(""); + + assert.match(logs, /Access violation in heap section at address/); + }); }); diff --git a/tests/suites/program-config-init.ts b/tests/suites/program-config-init.ts index e6ff2e5b..6f09f9e9 100644 --- a/tests/suites/program-config-init.ts +++ b/tests/suites/program-config-init.ts @@ -1,4 +1,11 @@ +import { + LAMPORTS_PER_SOL, + PublicKey, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; import * as multisig from "@sqds/multisig"; +import assert from "assert"; import { createLocalhostConnection, generateFundedKeypair, @@ -7,13 +14,6 @@ import { getTestProgramId, getTestProgramTreasury, } from "../utils"; -import { - LAMPORTS_PER_SOL, - PublicKey, - TransactionMessage, - VersionedTransaction, -} from "@solana/web3.js"; -import assert from "assert"; const programId = getTestProgramId(); const programConfigInitializer = getTestProgramConfigInitializer(); @@ -161,7 +161,7 @@ describe("Initialize Global ProgramConfig", () => { }).compileToV0Message(); const tx = new VersionedTransaction(message); tx.sign([programConfigInitializer]); - const sig = await connection.sendRawTransaction(tx.serialize()); + const sig = await connection.sendRawTransaction(tx.serialize(), { skipPreflight: true }); await connection.confirmTransaction(sig); const programConfigData = diff --git a/tests/utils.ts b/tests/utils.ts index ea153c07..0d8cd5a0 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -5,6 +5,7 @@ import { PublicKey, SystemProgram, TransactionMessage, + VersionedTransaction, } from "@solana/web3.js"; import * as multisig from "@sqds/multisig"; import { readFileSync } from "fs"; @@ -1225,3 +1226,57 @@ export function range(min: number, max: number, step: number = 1) { export function comparePubkeys(a: PublicKey, b: PublicKey) { return a.toBuffer().compare(b.toBuffer()); } + +export async function processBufferInChunks( + member: Keypair, + multisigPda: PublicKey, + bufferAccount: PublicKey, + buffer: Uint8Array, + connection: Connection, + programId: PublicKey, + chunkSize: number = 700, + startIndex: number = 0 +) { + const processChunk = async (startIndex: number) => { + if (startIndex >= buffer.length) { + return; + } + + const chunk = buffer.slice(startIndex, startIndex + chunkSize); + + const ix = multisig.generated.createTransactionBufferExtendInstruction( + { + multisig: multisigPda, + transactionBuffer:bufferAccount, + creator: member.publicKey, + }, + { + args: { + buffer: chunk, + }, + }, + programId + ); + + const message = new TransactionMessage({ + payerKey: member.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + + tx.sign([member]); + + const signature = await connection.sendRawTransaction(tx.serialize(), { + skipPreflight: true, + }); + + await connection.confirmTransaction(signature); + + // Move to next chunk + await processChunk(startIndex + chunkSize); + }; + + await processChunk(startIndex); +} \ No newline at end of file From ac11589343ad9b29b3d24f76e16d1bdb615b7714 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Thu, 5 Sep 2024 19:50:32 +0200 Subject: [PATCH 13/47] WIP --- .../src/instructions/mod.rs | 4 +- .../instructions/vault_transaction_create.rs | 59 +- .../vault_transaction_create_from_buffer.rs | 160 - programs/squads_multisig_program/src/lib.rs | 10 +- sdk/multisig/idl/squads_multisig_program.json | 3465 +++++++++++++++++ .../src/generated/instructions/heapTest.ts | 91 - .../src/generated/instructions/index.ts | 1 - .../vaultTransactionCreateFromBuffer.ts | 17 +- .../VaultTransactionCreateFromBufferArgs.ts | 25 - sdk/multisig/src/generated/types/index.ts | 1 - tests/index.ts | 44 +- tests/suites/examples/transaction-buffer.ts | 43 +- .../vaultTransactionCreateFromBuffer.ts | 50 +- 13 files changed, 3619 insertions(+), 351 deletions(-) delete mode 100644 programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs create mode 100644 sdk/multisig/idl/squads_multisig_program.json delete mode 100644 sdk/multisig/src/generated/instructions/heapTest.ts delete mode 100644 sdk/multisig/src/generated/types/VaultTransactionCreateFromBufferArgs.ts diff --git a/programs/squads_multisig_program/src/instructions/mod.rs b/programs/squads_multisig_program/src/instructions/mod.rs index 9c6330a5..f6352f8d 100644 --- a/programs/squads_multisig_program/src/instructions/mod.rs +++ b/programs/squads_multisig_program/src/instructions/mod.rs @@ -19,7 +19,7 @@ pub use transaction_buffer_close::*; pub use transaction_buffer_create::*; pub use transaction_buffer_extend::*; pub use vault_transaction_create::*; -pub use vault_transaction_create_from_buffer::*; + pub use vault_transaction_execute::*; mod batch_add_transaction; @@ -43,5 +43,5 @@ mod transaction_buffer_close; mod transaction_buffer_create; mod transaction_buffer_extend; mod vault_transaction_create; -mod vault_transaction_create_from_buffer; + mod vault_transaction_execute; diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs index db21ec86..8e66da41 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs @@ -48,8 +48,8 @@ pub struct VaultTransactionCreate<'info> { pub system_program: Program<'info, System>, } -impl VaultTransactionCreate<'_> { - fn validate(&self) -> Result<()> { +impl<'info> VaultTransactionCreate<'info> { + pub fn validate(&self) -> Result<()> { let Self { multisig, creator, .. } = self; @@ -129,6 +129,61 @@ impl VaultTransactionCreate<'_> { Ok(()) } + + pub fn build_args_from_buffer_account( + ctx: &Context<'_, '_, 'info, 'info, Self>, + args: &VaultTransactionCreateArgs, + ) -> Result { + let multisig = &ctx.accounts.multisig; + let remaining_accounts = ctx.remaining_accounts; + + // Determine valid buffer account address + let (transaction_buffer_address, _) = Pubkey::find_program_address( + &[ + SEED_PREFIX, + multisig.key().as_ref(), + SEED_TRANSACTION_BUFFER, + &multisig + .transaction_index + .checked_add(1) + .unwrap() + .to_le_bytes(), + ], + ctx.program_id, + ); + + // Find the buffer account in remaining accounts + let transaction_buffer_info = remaining_accounts + .iter() + .find(|acc| acc.key == &transaction_buffer_address) + .ok_or(MultisigError::MissingAccount)?; + let transaction_buffer_account: Account<'info, TransactionBuffer> = + Account::::try_from(transaction_buffer_info)?; + + // Check that the buffer creator is the same as the transaction creator + require_keys_eq!( + transaction_buffer_account.creator, + ctx.accounts.creator.key(), + MultisigError::InvalidAccount + ); + + // Check that the buffer is writable + require!( + transaction_buffer_info.is_writable, + MultisigError::InvalidAccount + ); + + // Close the buffer account + transaction_buffer_account.close(ctx.accounts.creator.to_account_info())?; + + // Build the args + Ok(VaultTransactionCreateArgs { + vault_index: transaction_buffer_account.vault_index, + ephemeral_signers: args.ephemeral_signers, + transaction_message: transaction_buffer_account.buffer.clone(), + memo: None, + }) + } } /// Unvalidated instruction data, must be treated as untrusted. diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs deleted file mode 100644 index aaad4201..00000000 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs +++ /dev/null @@ -1,160 +0,0 @@ -use anchor_lang::prelude::*; - -use crate::errors::*; -use crate::state::*; -use crate::TransactionMessage; - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct VaultTransactionCreateFromBufferArgs { - /// Number of ephemeral signing PDAs required by the transaction. - pub ephemeral_signers: u8, - /// Optional Memo - pub memo: Option, -} - -#[derive(Accounts)] -#[instruction(args: VaultTransactionCreateFromBufferArgs)] -pub struct VaultTransactionCreateFromBuffer<'info> { - #[account( - mut, - seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()], - bump = multisig.bump, - )] - pub multisig: Account<'info, Multisig>, - - #[account( - mut, - close = creator, - // Only the creator of the buffer can create a VaultTransaction from it - constraint = transaction_buffer.creator == creator.key(), - seeds = [ - SEED_PREFIX, - multisig.key().as_ref(), - SEED_TRANSACTION_BUFFER, - &multisig.transaction_index.checked_add(1).unwrap().to_le_bytes(), - ], - bump - )] - pub transaction_buffer: Box>, - - #[account( - init, - payer = rent_payer, - space = VaultTransaction::size(args.ephemeral_signers, &transaction_buffer.buffer)?, - seeds = [ - SEED_PREFIX, - multisig.key().as_ref(), - SEED_TRANSACTION, - &multisig.transaction_index.checked_add(1).unwrap().to_le_bytes(), - ], - bump - )] - pub transaction: Account<'info, VaultTransaction>, - - /// The member of the multisig that is creating the transaction. - pub creator: Signer<'info>, - - /// The payer for the transaction account rent. - #[account(mut)] - pub rent_payer: Signer<'info>, - - pub system_program: Program<'info, System>, -} - -impl VaultTransactionCreateFromBuffer<'_> { - fn validate(&self) -> Result<()> { - let Self { - multisig, - creator, - transaction_buffer, - .. - } = self; - - // creator - require!( - multisig.is_member(creator.key()).is_some(), - MultisigError::NotAMember - ); - require!( - multisig.member_has_permission(creator.key(), Permission::Initiate), - MultisigError::Unauthorized - ); - - // Transaction buffer hash validation - transaction_buffer.validate_hash()?; - // Transaction buffer size validation - transaction_buffer.validate_size()?; - - Ok(()) - } - - /// Create a new vault transaction. - #[access_control(ctx.accounts.validate())] - pub fn vault_transaction_create_from_buffer( - ctx: Context, - args: VaultTransactionCreateFromBufferArgs, - ) -> Result<()> { - // Mutable Accounts - let multisig = &mut ctx.accounts.multisig; - let transaction = &mut ctx.accounts.transaction; - let transaction_buffer = &mut ctx.accounts.transaction_buffer; - - // Readonly Accounts - let creator = &mut ctx.accounts.creator; - - // Data - let vault_index = transaction_buffer.vault_index; - - let transaction_message = - TransactionMessage::deserialize(&mut transaction_buffer.buffer.as_slice())?; - - let multisig_key = multisig.key(); - let transaction_key = transaction.key(); - - let vault_seeds = &[ - SEED_PREFIX, - multisig_key.as_ref(), - SEED_VAULT, - &vault_index.to_le_bytes(), - ]; - let (_, vault_bump) = Pubkey::find_program_address(vault_seeds, ctx.program_id); - - let ephemeral_signer_bumps: Vec = (0..args.ephemeral_signers) - .map(|ephemeral_signer_index| { - let ephemeral_signer_seeds = &[ - SEED_PREFIX, - transaction_key.as_ref(), - SEED_EPHEMERAL_SIGNER, - &ephemeral_signer_index.to_le_bytes(), - ]; - - let (_, bump) = - Pubkey::find_program_address(ephemeral_signer_seeds, ctx.program_id); - bump - }) - .collect(); - - // Increment the transaction index. - let transaction_index = multisig.transaction_index.checked_add(1).unwrap(); - - // Initialize the transaction fields. - transaction.multisig = multisig_key; - transaction.creator = creator.key(); - transaction.index = transaction_index; - transaction.bump = ctx.bumps.transaction; - transaction.vault_index = vault_index; - transaction.vault_bump = vault_bump; - transaction.ephemeral_signer_bumps = ephemeral_signer_bumps; - transaction.message = transaction_message.try_into()?; - - // Updated last transaction index in the multisig account. - multisig.transaction_index = transaction_index; - - multisig.invariant()?; - - // Logs for indexing. - msg!("transaction index: {}", transaction_index); - - Ok(()) - } -} diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index e0a94b19..36900a47 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -199,11 +199,13 @@ pub mod squads_multisig_program { /// Create a new vault transaction from a completed transaction buffer. /// Finalized buffer hash must match `final_buffer_hash` - pub fn vault_transaction_create_from_buffer( - ctx: Context, - args: VaultTransactionCreateFromBufferArgs, + pub fn vault_transaction_create_from_buffer<'info>( + ctx: Context<'_, '_, 'info, 'info, VaultTransactionCreate<'info>>, + args: VaultTransactionCreateArgs, ) -> Result<()> { - VaultTransactionCreateFromBuffer::vault_transaction_create_from_buffer(ctx, args) + msg!("got here"); + let create_args = VaultTransactionCreate::build_args_from_buffer_account(&ctx, &args)?; + VaultTransactionCreate::vault_transaction_create(ctx, create_args) } /// Execute a vault transaction. diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json new file mode 100644 index 00000000..9ff9c521 --- /dev/null +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -0,0 +1,3465 @@ +{ + "version": "2.0.0", + "name": "squads_multisig_program", + "instructions": [ + { + "name": "programConfigInit", + "docs": [ + "Initialize the program config." + ], + "accounts": [ + { + "name": "programConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "initializer", + "isMut": true, + "isSigner": true, + "docs": [ + "The hard-coded account that is used to initialize the program config once." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProgramConfigInitArgs" + } + } + ] + }, + { + "name": "programConfigSetAuthority", + "docs": [ + "Set the `authority` parameter of the program config." + ], + "accounts": [ + { + "name": "programConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProgramConfigSetAuthorityArgs" + } + } + ] + }, + { + "name": "programConfigSetMultisigCreationFee", + "docs": [ + "Set the `multisig_creation_fee` parameter of the program config." + ], + "accounts": [ + { + "name": "programConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProgramConfigSetMultisigCreationFeeArgs" + } + } + ] + }, + { + "name": "programConfigSetTreasury", + "docs": [ + "Set the `treasury` parameter of the program config." + ], + "accounts": [ + { + "name": "programConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProgramConfigSetTreasuryArgs" + } + } + ] + }, + { + "name": "multisigCreate", + "docs": [ + "Create a multisig." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "createKey", + "isMut": false, + "isSigner": true, + "docs": [ + "An ephemeral signer that is used as a seed for the Multisig PDA.", + "Must be a signer to prevent front-running attack by someone else but the original creator." + ] + }, + { + "name": "creator", + "isMut": true, + "isSigner": true, + "docs": [ + "The creator of the multisig." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigCreateArgs" + } + } + ] + }, + { + "name": "multisigCreateV2", + "docs": [ + "Create a multisig." + ], + "accounts": [ + { + "name": "programConfig", + "isMut": false, + "isSigner": false, + "docs": [ + "Global program config account." + ] + }, + { + "name": "treasury", + "isMut": true, + "isSigner": false, + "docs": [ + "The treasury where the creation fee is transferred to." + ] + }, + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "createKey", + "isMut": false, + "isSigner": true, + "docs": [ + "An ephemeral signer that is used as a seed for the Multisig PDA.", + "Must be a signer to prevent front-running attack by someone else but the original creator." + ] + }, + { + "name": "creator", + "isMut": true, + "isSigner": true, + "docs": [ + "The creator of the multisig." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigCreateArgsV2" + } + } + ] + }, + { + "name": "multisigAddMember", + "docs": [ + "Add a new member to the controlled multisig." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "configAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Multisig `config_authority` that must authorize the configuration change." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "isOptional": true, + "docs": [ + "The account that will be charged or credited in case the multisig account needs to reallocate space,", + "for example when adding a new member or a spending limit.", + "This is usually the same as `config_authority`, but can be a different account if needed." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "We might need it in case reallocation is needed." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigAddMemberArgs" + } + } + ] + }, + { + "name": "multisigRemoveMember", + "docs": [ + "Remove a member/key from the controlled multisig." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "configAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Multisig `config_authority` that must authorize the configuration change." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "isOptional": true, + "docs": [ + "The account that will be charged or credited in case the multisig account needs to reallocate space,", + "for example when adding a new member or a spending limit.", + "This is usually the same as `config_authority`, but can be a different account if needed." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "We might need it in case reallocation is needed." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigRemoveMemberArgs" + } + } + ] + }, + { + "name": "multisigSetTimeLock", + "docs": [ + "Set the `time_lock` config parameter for the controlled multisig." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "configAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Multisig `config_authority` that must authorize the configuration change." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "isOptional": true, + "docs": [ + "The account that will be charged or credited in case the multisig account needs to reallocate space,", + "for example when adding a new member or a spending limit.", + "This is usually the same as `config_authority`, but can be a different account if needed." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "We might need it in case reallocation is needed." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigSetTimeLockArgs" + } + } + ] + }, + { + "name": "multisigChangeThreshold", + "docs": [ + "Set the `threshold` config parameter for the controlled multisig." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "configAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Multisig `config_authority` that must authorize the configuration change." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "isOptional": true, + "docs": [ + "The account that will be charged or credited in case the multisig account needs to reallocate space,", + "for example when adding a new member or a spending limit.", + "This is usually the same as `config_authority`, but can be a different account if needed." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "We might need it in case reallocation is needed." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigChangeThresholdArgs" + } + } + ] + }, + { + "name": "multisigSetConfigAuthority", + "docs": [ + "Set the multisig `config_authority`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "configAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Multisig `config_authority` that must authorize the configuration change." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "isOptional": true, + "docs": [ + "The account that will be charged or credited in case the multisig account needs to reallocate space,", + "for example when adding a new member or a spending limit.", + "This is usually the same as `config_authority`, but can be a different account if needed." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "We might need it in case reallocation is needed." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigSetConfigAuthorityArgs" + } + } + ] + }, + { + "name": "multisigSetRentCollector", + "docs": [ + "Set the multisig `rent_collector`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "configAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Multisig `config_authority` that must authorize the configuration change." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "isOptional": true, + "docs": [ + "The account that will be charged or credited in case the multisig account needs to reallocate space,", + "for example when adding a new member or a spending limit.", + "This is usually the same as `config_authority`, but can be a different account if needed." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "We might need it in case reallocation is needed." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigSetRentCollectorArgs" + } + } + ] + }, + { + "name": "multisigAddSpendingLimit", + "docs": [ + "Create a new spending limit for the controlled multisig." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "configAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Multisig `config_authority` that must authorize the configuration change." + ] + }, + { + "name": "spendingLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "This is usually the same as `config_authority`, but can be a different account if needed." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigAddSpendingLimitArgs" + } + } + ] + }, + { + "name": "multisigRemoveSpendingLimit", + "docs": [ + "Remove the spending limit from the controlled multisig." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "configAuthority", + "isMut": false, + "isSigner": true, + "docs": [ + "Multisig `config_authority` that must authorize the configuration change." + ] + }, + { + "name": "spendingLimit", + "isMut": true, + "isSigner": false + }, + { + "name": "rentCollector", + "isMut": true, + "isSigner": false, + "docs": [ + "This is usually the same as `config_authority`, but can be a different account if needed." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "MultisigRemoveSpendingLimitArgs" + } + } + ] + }, + { + "name": "configTransactionCreate", + "docs": [ + "Create a new config transaction." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transaction", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that is creating the transaction." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "The payer for the transaction account rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ConfigTransactionCreateArgs" + } + } + ] + }, + { + "name": "configTransactionExecute", + "docs": [ + "Execute a config transaction.", + "The transaction must be `Approved`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false, + "docs": [ + "The multisig account that owns the transaction." + ] + }, + { + "name": "member", + "isMut": false, + "isSigner": true, + "docs": [ + "One of the multisig members with `Execute` permission." + ] + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false, + "docs": [ + "The proposal account associated with the transaction." + ] + }, + { + "name": "transaction", + "isMut": false, + "isSigner": false, + "docs": [ + "The transaction to execute." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "isOptional": true, + "docs": [ + "The account that will be charged/credited in case the config transaction causes space reallocation,", + "for example when adding a new member, adding or removing a spending limit.", + "This is usually the same as `member`, but can be a different account if needed." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "We might need it in case reallocation is needed." + ] + } + ], + "args": [] + }, + { + "name": "vaultTransactionCreate", + "docs": [ + "Create a new vault transaction." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transaction", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that is creating the transaction." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "The payer for the transaction account rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "VaultTransactionCreateArgs" + } + } + ] + }, + { + "name": "transactionBufferCreate", + "docs": [ + "Create a transaction buffer account." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transactionBuffer", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that is creating the transaction." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "The payer for the transaction account rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "TransactionBufferCreateArgs" + } + } + ] + }, + { + "name": "transactionBufferClose", + "docs": [ + "Close a transaction buffer account." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transactionBuffer", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that created the TransactionBuffer." + ] + } + ], + "args": [] + }, + { + "name": "transactionBufferExtend", + "docs": [ + "Extend a transaction buffer account." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transactionBuffer", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that created the TransactionBuffer." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "TransactionBufferExtendArgs" + } + } + ] + }, + { + "name": "vaultTransactionCreateFromBuffer", + "docs": [ + "Create a new vault transaction from a completed transaction buffer.", + "Finalized buffer hash must match `final_buffer_hash`" + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transaction", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that is creating the transaction." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "The payer for the transaction account rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "VaultTransactionCreateArgs" + } + } + ] + }, + { + "name": "vaultTransactionExecute", + "docs": [ + "Execute a vault transaction.", + "The transaction must be `Approved`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false, + "docs": [ + "The proposal account associated with the transaction." + ] + }, + { + "name": "transaction", + "isMut": false, + "isSigner": false, + "docs": [ + "The transaction to execute." + ] + }, + { + "name": "member", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "batchCreate", + "docs": [ + "Create a new batch." + ], + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "batch", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that is creating the batch." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "The payer for the batch account rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "BatchCreateArgs" + } + } + ] + }, + { + "name": "batchAddTransaction", + "docs": [ + "Add a transaction to the batch." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false, + "docs": [ + "Multisig account this batch belongs to." + ] + }, + { + "name": "proposal", + "isMut": false, + "isSigner": false, + "docs": [ + "The proposal account associated with the batch." + ] + }, + { + "name": "batch", + "isMut": true, + "isSigner": false + }, + { + "name": "transaction", + "isMut": true, + "isSigner": false, + "docs": [ + "`VaultBatchTransaction` account to initialize and add to the `batch`." + ] + }, + { + "name": "member", + "isMut": false, + "isSigner": true, + "docs": [ + "Member of the multisig." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "The payer for the batch transaction account rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "BatchAddTransactionArgs" + } + } + ] + }, + { + "name": "batchExecuteTransaction", + "docs": [ + "Execute a transaction from the batch." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false, + "docs": [ + "Multisig account this batch belongs to." + ] + }, + { + "name": "member", + "isMut": false, + "isSigner": true, + "docs": [ + "Member of the multisig." + ] + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false, + "docs": [ + "The proposal account associated with the batch.", + "If `transaction` is the last in the batch, the `proposal` status will be set to `Executed`." + ] + }, + { + "name": "batch", + "isMut": true, + "isSigner": false + }, + { + "name": "transaction", + "isMut": false, + "isSigner": false, + "docs": [ + "Batch transaction to execute." + ] + } + ], + "args": [] + }, + { + "name": "proposalCreate", + "docs": [ + "Create a new multisig proposal." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that is creating the proposal." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "The payer for the proposal account rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProposalCreateArgs" + } + } + ] + }, + { + "name": "proposalActivate", + "docs": [ + "Update status of a multisig proposal from `Draft` to `Active`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "member", + "isMut": true, + "isSigner": true + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "proposalApprove", + "docs": [ + "Approve a multisig proposal on behalf of the `member`.", + "The proposal must be `Active`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "member", + "isMut": true, + "isSigner": true + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProposalVoteArgs" + } + } + ] + }, + { + "name": "proposalReject", + "docs": [ + "Reject a multisig proposal on behalf of the `member`.", + "The proposal must be `Active`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "member", + "isMut": true, + "isSigner": true + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProposalVoteArgs" + } + } + ] + }, + { + "name": "proposalCancel", + "docs": [ + "Cancel a multisig proposal on behalf of the `member`.", + "The proposal must be `Approved`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "member", + "isMut": true, + "isSigner": true + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProposalVoteArgs" + } + } + ] + }, + { + "name": "proposalCancelV2", + "docs": [ + "Cancel a multisig proposal on behalf of the `member`.", + "The proposal must be `Approved`.", + "This was introduced to incorporate proper state update, as old multisig members", + "may have lingering votes, and the proposal size may need to be reallocated to", + "accommodate the new amount of cancel votes.", + "The previous implemenation still works if the proposal size is in line with the", + "threshold size." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "member", + "isMut": true, + "isSigner": true + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "ProposalVoteArgs" + } + } + ] + }, + { + "name": "spendingLimitUse", + "docs": [ + "Use a spending limit to transfer tokens from a multisig vault to a destination account." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false, + "docs": [ + "The multisig account the `spending_limit` is for." + ] + }, + { + "name": "member", + "isMut": false, + "isSigner": true + }, + { + "name": "spendingLimit", + "isMut": true, + "isSigner": false, + "docs": [ + "The SpendingLimit account to use." + ] + }, + { + "name": "vault", + "isMut": true, + "isSigner": false, + "docs": [ + "Multisig vault account to transfer tokens from." + ] + }, + { + "name": "destination", + "isMut": true, + "isSigner": false, + "docs": [ + "Destination account to transfer tokens to." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "In case `spending_limit.mint` is SOL." + ] + }, + { + "name": "mint", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "The mint of the tokens to transfer in case `spending_limit.mint` is an SPL token." + ] + }, + { + "name": "vaultTokenAccount", + "isMut": true, + "isSigner": false, + "isOptional": true, + "docs": [ + "Multisig vault token account to transfer tokens from in case `spending_limit.mint` is an SPL token." + ] + }, + { + "name": "destinationTokenAccount", + "isMut": true, + "isSigner": false, + "isOptional": true, + "docs": [ + "Destination token account in case `spending_limit.mint` is an SPL token." + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "isOptional": true, + "docs": [ + "In case `spending_limit.mint` is an SPL token." + ] + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SpendingLimitUseArgs" + } + } + ] + }, + { + "name": "configTransactionAccountsClose", + "docs": [ + "Closes a `ConfigTransaction` and the corresponding `Proposal`.", + "`transaction` can be closed if either:", + "- the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`.", + "- the `proposal` is stale." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false, + "docs": [ + "the logic within `config_transaction_accounts_close` does the rest of the checks." + ] + }, + { + "name": "transaction", + "isMut": true, + "isSigner": false, + "docs": [ + "ConfigTransaction corresponding to the `proposal`." + ] + }, + { + "name": "rentCollector", + "isMut": true, + "isSigner": false, + "docs": [ + "The rent collector." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "vaultTransactionAccountsClose", + "docs": [ + "Closes a `VaultTransaction` and the corresponding `Proposal`.", + "`transaction` can be closed if either:", + "- the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`.", + "- the `proposal` is stale and not `Approved`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false, + "docs": [ + "the logic within `vault_transaction_accounts_close` does the rest of the checks." + ] + }, + { + "name": "transaction", + "isMut": true, + "isSigner": false, + "docs": [ + "VaultTransaction corresponding to the `proposal`." + ] + }, + { + "name": "rentCollector", + "isMut": true, + "isSigner": false, + "docs": [ + "The rent collector." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "vaultBatchTransactionAccountClose", + "docs": [ + "Closes a `VaultBatchTransaction` belonging to the `batch` and `proposal`.", + "`transaction` can be closed if either:", + "- it's marked as executed within the `batch`;", + "- the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`.", + "- the `proposal` is stale and not `Approved`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "proposal", + "isMut": false, + "isSigner": false + }, + { + "name": "batch", + "isMut": true, + "isSigner": false, + "docs": [ + "`Batch` corresponding to the `proposal`." + ] + }, + { + "name": "transaction", + "isMut": true, + "isSigner": false, + "docs": [ + "`VaultBatchTransaction` account to close.", + "The transaction must be the current last one in the batch." + ] + }, + { + "name": "rentCollector", + "isMut": true, + "isSigner": false, + "docs": [ + "The rent collector." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "batchAccountsClose", + "docs": [ + "Closes Batch and the corresponding Proposal accounts for proposals in terminal states:", + "`Executed`, `Rejected`, or `Cancelled` or stale proposals that aren't `Approved`.", + "", + "This instruction is only allowed to be executed when all `VaultBatchTransaction` accounts", + "in the `batch` are already closed: `batch.size == 0`." + ], + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false, + "docs": [ + "the logic within `batch_accounts_close` does the rest of the checks." + ] + }, + { + "name": "batch", + "isMut": true, + "isSigner": false, + "docs": [ + "`Batch` corresponding to the `proposal`." + ] + }, + { + "name": "rentCollector", + "isMut": true, + "isSigner": false, + "docs": [ + "The rent collector." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "Batch", + "docs": [ + "Stores data required for serial execution of a batch of multisig vault transactions.", + "Vault transaction is a transaction that's executed on behalf of the multisig vault PDA", + "and wraps arbitrary Solana instructions, typically calling into other Solana programs.", + "The transactions themselves are stored in separate PDAs associated with the this account." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "multisig", + "docs": [ + "The multisig this belongs to." + ], + "type": "publicKey" + }, + { + "name": "creator", + "docs": [ + "Member of the Multisig who submitted the batch." + ], + "type": "publicKey" + }, + { + "name": "index", + "docs": [ + "Index of this batch within the multisig transactions." + ], + "type": "u64" + }, + { + "name": "bump", + "docs": [ + "PDA bump." + ], + "type": "u8" + }, + { + "name": "vaultIndex", + "docs": [ + "Index of the vault this batch belongs to." + ], + "type": "u8" + }, + { + "name": "vaultBump", + "docs": [ + "Derivation bump of the vault PDA this batch belongs to." + ], + "type": "u8" + }, + { + "name": "size", + "docs": [ + "Number of transactions in the batch." + ], + "type": "u32" + }, + { + "name": "executedTransactionIndex", + "docs": [ + "Index of the last executed transaction within the batch.", + "0 means that no transactions have been executed yet." + ], + "type": "u32" + } + ] + } + }, + { + "name": "VaultBatchTransaction", + "docs": [ + "Stores data required for execution of one transaction from a batch." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "docs": [ + "PDA bump." + ], + "type": "u8" + }, + { + "name": "ephemeralSignerBumps", + "docs": [ + "Derivation bumps for additional signers.", + "Some transactions require multiple signers. Often these additional signers are \"ephemeral\" keypairs", + "that are generated on the client with a sole purpose of signing the transaction and be discarded immediately after.", + "When wrapping such transactions into multisig ones, we replace these \"ephemeral\" signing keypairs", + "with PDAs derived from the transaction's `transaction_index` and controlled by the Multisig Program;", + "during execution the program includes the seeds of these PDAs into the `invoke_signed` calls,", + "thus \"signing\" on behalf of these PDAs." + ], + "type": "bytes" + }, + { + "name": "message", + "docs": [ + "data required for executing the transaction." + ], + "type": { + "defined": "VaultTransactionMessage" + } + } + ] + } + }, + { + "name": "ConfigTransaction", + "docs": [ + "Stores data required for execution of a multisig configuration transaction.", + "Config transaction can perform a predefined set of actions on the Multisig PDA, such as adding/removing members,", + "changing the threshold, etc." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "multisig", + "docs": [ + "The multisig this belongs to." + ], + "type": "publicKey" + }, + { + "name": "creator", + "docs": [ + "Member of the Multisig who submitted the transaction." + ], + "type": "publicKey" + }, + { + "name": "index", + "docs": [ + "Index of this transaction within the multisig." + ], + "type": "u64" + }, + { + "name": "bump", + "docs": [ + "bump for the transaction seeds." + ], + "type": "u8" + }, + { + "name": "actions", + "docs": [ + "Action to be performed on the multisig." + ], + "type": { + "vec": { + "defined": "ConfigAction" + } + } + } + ] + } + }, + { + "name": "Multisig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "createKey", + "docs": [ + "Key that is used to seed the multisig PDA." + ], + "type": "publicKey" + }, + { + "name": "configAuthority", + "docs": [ + "The authority that can change the multisig config.", + "This is a very important parameter as this authority can change the members and threshold.", + "", + "The convention is to set this to `Pubkey::default()`.", + "In this case, the multisig becomes autonomous, so every config change goes through", + "the normal process of voting by the members.", + "", + "However, if this parameter is set to any other key, all the config changes for this multisig", + "will need to be signed by the `config_authority`. We call such a multisig a \"controlled multisig\"." + ], + "type": "publicKey" + }, + { + "name": "threshold", + "docs": [ + "Threshold for signatures." + ], + "type": "u16" + }, + { + "name": "timeLock", + "docs": [ + "How many seconds must pass between transaction voting settlement and execution." + ], + "type": "u32" + }, + { + "name": "transactionIndex", + "docs": [ + "Last transaction index. 0 means no transactions have been created." + ], + "type": "u64" + }, + { + "name": "staleTransactionIndex", + "docs": [ + "Last stale transaction index. All transactions up until this index are stale.", + "This index is updated when multisig config (members/threshold/time_lock) changes." + ], + "type": "u64" + }, + { + "name": "rentCollector", + "docs": [ + "The address where the rent for the accounts related to executed, rejected, or cancelled", + "transactions can be reclaimed. If set to `None`, the rent reclamation feature is turned off." + ], + "type": { + "option": "publicKey" + } + }, + { + "name": "bump", + "docs": [ + "Bump for the multisig PDA seed." + ], + "type": "u8" + }, + { + "name": "members", + "docs": [ + "Members of the multisig." + ], + "type": { + "vec": { + "defined": "Member" + } + } + } + ] + } + }, + { + "name": "ProgramConfig", + "docs": [ + "Global program configuration account." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The authority which can update the config." + ], + "type": "publicKey" + }, + { + "name": "multisigCreationFee", + "docs": [ + "The lamports amount charged for creating a new multisig account.", + "This fee is sent to the `treasury` account." + ], + "type": "u64" + }, + { + "name": "treasury", + "docs": [ + "The treasury account to send charged fees to." + ], + "type": "publicKey" + }, + { + "name": "reserved", + "docs": [ + "Reserved for future use." + ], + "type": { + "array": [ + "u8", + 64 + ] + } + } + ] + } + }, + { + "name": "Proposal", + "docs": [ + "Stores the data required for tracking the status of a multisig proposal.", + "Each `Proposal` has a 1:1 association with a transaction account, e.g. a `VaultTransaction` or a `ConfigTransaction`;", + "the latter can be executed only after the `Proposal` has been approved and its time lock is released." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "multisig", + "docs": [ + "The multisig this belongs to." + ], + "type": "publicKey" + }, + { + "name": "transactionIndex", + "docs": [ + "Index of the multisig transaction this proposal is associated with." + ], + "type": "u64" + }, + { + "name": "status", + "docs": [ + "The status of the transaction." + ], + "type": { + "defined": "ProposalStatus" + } + }, + { + "name": "bump", + "docs": [ + "PDA bump." + ], + "type": "u8" + }, + { + "name": "approved", + "docs": [ + "Keys that have approved/signed." + ], + "type": { + "vec": "publicKey" + } + }, + { + "name": "rejected", + "docs": [ + "Keys that have rejected." + ], + "type": { + "vec": "publicKey" + } + }, + { + "name": "cancelled", + "docs": [ + "Keys that have cancelled (Approved only)." + ], + "type": { + "vec": "publicKey" + } + } + ] + } + }, + { + "name": "SpendingLimit", + "type": { + "kind": "struct", + "fields": [ + { + "name": "multisig", + "docs": [ + "The multisig this belongs to." + ], + "type": "publicKey" + }, + { + "name": "createKey", + "docs": [ + "Key that is used to seed the SpendingLimit PDA." + ], + "type": "publicKey" + }, + { + "name": "vaultIndex", + "docs": [ + "The index of the vault that the spending limit is for." + ], + "type": "u8" + }, + { + "name": "mint", + "docs": [ + "The token mint the spending limit is for.", + "Pubkey::default() means SOL.", + "use NATIVE_MINT for Wrapped SOL." + ], + "type": "publicKey" + }, + { + "name": "amount", + "docs": [ + "The amount of tokens that can be spent in a period.", + "This amount is in decimals of the mint,", + "so 1 SOL would be `1_000_000_000` and 1 USDC would be `1_000_000`." + ], + "type": "u64" + }, + { + "name": "period", + "docs": [ + "The reset period of the spending limit.", + "When it passes, the remaining amount is reset, unless it's `Period::OneTime`." + ], + "type": { + "defined": "Period" + } + }, + { + "name": "remainingAmount", + "docs": [ + "The remaining amount of tokens that can be spent in the current period.", + "When reaches 0, the spending limit cannot be used anymore until the period reset." + ], + "type": "u64" + }, + { + "name": "lastReset", + "docs": [ + "Unix timestamp marking the last time the spending limit was reset (or created)." + ], + "type": "i64" + }, + { + "name": "bump", + "docs": [ + "PDA bump." + ], + "type": "u8" + }, + { + "name": "members", + "docs": [ + "Members of the multisig that can use the spending limit.", + "In case a member is removed from the multisig, the spending limit will remain existent", + "(until explicitly deleted), but the removed member will not be able to use it anymore." + ], + "type": { + "vec": "publicKey" + } + }, + { + "name": "destinations", + "docs": [ + "The destination addresses the spending limit is allowed to sent funds to.", + "If empty, funds can be sent to any address." + ], + "type": { + "vec": "publicKey" + } + } + ] + } + }, + { + "name": "TransactionBuffer", + "type": { + "kind": "struct", + "fields": [ + { + "name": "multisig", + "docs": [ + "The multisig this belongs to." + ], + "type": "publicKey" + }, + { + "name": "creator", + "docs": [ + "Member of the Multisig who created the TransactionBuffer." + ], + "type": "publicKey" + }, + { + "name": "vaultIndex", + "docs": [ + "Vault index of the transaction this buffer belongs to." + ], + "type": "u8" + }, + { + "name": "transactionIndex", + "docs": [ + "Index of the transaction this buffer belongs to." + ], + "type": "u64" + }, + { + "name": "finalBufferHash", + "docs": [ + "Hash of the final assembled transaction message." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "finalBufferSize", + "docs": [ + "The size of the final assembled transaction message." + ], + "type": "u16" + }, + { + "name": "buffer", + "docs": [ + "The buffer of the transaction message." + ], + "type": "bytes" + } + ] + } + }, + { + "name": "VaultTransaction", + "docs": [ + "Stores data required for tracking the voting and execution status of a vault transaction.", + "Vault transaction is a transaction that's executed on behalf of the multisig vault PDA", + "and wraps arbitrary Solana instructions, typically calling into other Solana programs." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "multisig", + "docs": [ + "The multisig this belongs to." + ], + "type": "publicKey" + }, + { + "name": "creator", + "docs": [ + "Member of the Multisig who submitted the transaction." + ], + "type": "publicKey" + }, + { + "name": "index", + "docs": [ + "Index of this transaction within the multisig." + ], + "type": "u64" + }, + { + "name": "bump", + "docs": [ + "bump for the transaction seeds." + ], + "type": "u8" + }, + { + "name": "vaultIndex", + "docs": [ + "Index of the vault this transaction belongs to." + ], + "type": "u8" + }, + { + "name": "vaultBump", + "docs": [ + "Derivation bump of the vault PDA this transaction belongs to." + ], + "type": "u8" + }, + { + "name": "ephemeralSignerBumps", + "docs": [ + "Derivation bumps for additional signers.", + "Some transactions require multiple signers. Often these additional signers are \"ephemeral\" keypairs", + "that are generated on the client with a sole purpose of signing the transaction and be discarded immediately after.", + "When wrapping such transactions into multisig ones, we replace these \"ephemeral\" signing keypairs", + "with PDAs derived from the MultisigTransaction's `transaction_index` and controlled by the Multisig Program;", + "during execution the program includes the seeds of these PDAs into the `invoke_signed` calls,", + "thus \"signing\" on behalf of these PDAs." + ], + "type": "bytes" + }, + { + "name": "message", + "docs": [ + "data required for executing the transaction." + ], + "type": { + "defined": "VaultTransactionMessage" + } + } + ] + } + } + ], + "types": [ + { + "name": "BatchAddTransactionArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "ephemeralSigners", + "docs": [ + "Number of ephemeral signing PDAs required by the transaction." + ], + "type": "u8" + }, + { + "name": "transactionMessage", + "type": "bytes" + } + ] + } + }, + { + "name": "BatchCreateArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vaultIndex", + "docs": [ + "Index of the vault this transaction belongs to." + ], + "type": "u8" + }, + { + "name": "memo", + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "ConfigTransactionCreateArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "actions", + "type": { + "vec": { + "defined": "ConfigAction" + } + } + }, + { + "name": "memo", + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "MultisigAddSpendingLimitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "createKey", + "docs": [ + "Key that is used to seed the SpendingLimit PDA." + ], + "type": "publicKey" + }, + { + "name": "vaultIndex", + "docs": [ + "The index of the vault that the spending limit is for." + ], + "type": "u8" + }, + { + "name": "mint", + "docs": [ + "The token mint the spending limit is for." + ], + "type": "publicKey" + }, + { + "name": "amount", + "docs": [ + "The amount of tokens that can be spent in a period.", + "This amount is in decimals of the mint,", + "so 1 SOL would be `1_000_000_000` and 1 USDC would be `1_000_000`." + ], + "type": "u64" + }, + { + "name": "period", + "docs": [ + "The reset period of the spending limit.", + "When it passes, the remaining amount is reset, unless it's `Period::OneTime`." + ], + "type": { + "defined": "Period" + } + }, + { + "name": "members", + "docs": [ + "Members of the Spending Limit that can use it.", + "Don't have to be members of the multisig." + ], + "type": { + "vec": "publicKey" + } + }, + { + "name": "destinations", + "docs": [ + "The destination addresses the spending limit is allowed to sent funds to.", + "If empty, funds can be sent to any address." + ], + "type": { + "vec": "publicKey" + } + }, + { + "name": "memo", + "docs": [ + "Memo is used for indexing only." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "MultisigAddMemberArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newMember", + "type": { + "defined": "Member" + } + }, + { + "name": "memo", + "docs": [ + "Memo is used for indexing only." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "MultisigRemoveMemberArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "oldMember", + "type": "publicKey" + }, + { + "name": "memo", + "docs": [ + "Memo is used for indexing only." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "MultisigChangeThresholdArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newThreshold", + "type": "u16" + }, + { + "name": "memo", + "docs": [ + "Memo is used for indexing only." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "MultisigSetTimeLockArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "timeLock", + "type": "u32" + }, + { + "name": "memo", + "docs": [ + "Memo is used for indexing only." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "MultisigSetConfigAuthorityArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "configAuthority", + "type": "publicKey" + }, + { + "name": "memo", + "docs": [ + "Memo is used for indexing only." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "MultisigSetRentCollectorArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "rentCollector", + "type": { + "option": "publicKey" + } + }, + { + "name": "memo", + "docs": [ + "Memo is used for indexing only." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "MultisigCreateArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "configAuthority", + "docs": [ + "The authority that can configure the multisig: add/remove members, change the threshold, etc.", + "Should be set to `None` for autonomous multisigs." + ], + "type": { + "option": "publicKey" + } + }, + { + "name": "threshold", + "docs": [ + "The number of signatures required to execute a transaction." + ], + "type": "u16" + }, + { + "name": "members", + "docs": [ + "The members of the multisig." + ], + "type": { + "vec": { + "defined": "Member" + } + } + }, + { + "name": "timeLock", + "docs": [ + "How many seconds must pass between transaction voting, settlement, and execution." + ], + "type": "u32" + }, + { + "name": "memo", + "docs": [ + "Memo is used for indexing only." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "MultisigCreateArgsV2", + "type": { + "kind": "struct", + "fields": [ + { + "name": "configAuthority", + "docs": [ + "The authority that can configure the multisig: add/remove members, change the threshold, etc.", + "Should be set to `None` for autonomous multisigs." + ], + "type": { + "option": "publicKey" + } + }, + { + "name": "threshold", + "docs": [ + "The number of signatures required to execute a transaction." + ], + "type": "u16" + }, + { + "name": "members", + "docs": [ + "The members of the multisig." + ], + "type": { + "vec": { + "defined": "Member" + } + } + }, + { + "name": "timeLock", + "docs": [ + "How many seconds must pass between transaction voting, settlement, and execution." + ], + "type": "u32" + }, + { + "name": "rentCollector", + "docs": [ + "The address where the rent for the accounts related to executed, rejected, or cancelled", + "transactions can be reclaimed. If set to `None`, the rent reclamation feature is turned off." + ], + "type": { + "option": "publicKey" + } + }, + { + "name": "memo", + "docs": [ + "Memo is used for indexing only." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "MultisigRemoveSpendingLimitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "memo", + "docs": [ + "Memo is used for indexing only." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "ProgramConfigInitArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The authority that can configure the program config: change the treasury, etc." + ], + "type": "publicKey" + }, + { + "name": "multisigCreationFee", + "docs": [ + "The fee that is charged for creating a new multisig." + ], + "type": "u64" + }, + { + "name": "treasury", + "docs": [ + "The treasury where the creation fee is transferred to." + ], + "type": "publicKey" + } + ] + } + }, + { + "name": "ProgramConfigSetAuthorityArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newAuthority", + "type": "publicKey" + } + ] + } + }, + { + "name": "ProgramConfigSetMultisigCreationFeeArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newMultisigCreationFee", + "type": "u64" + } + ] + } + }, + { + "name": "ProgramConfigSetTreasuryArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "newTreasury", + "type": "publicKey" + } + ] + } + }, + { + "name": "ProposalCreateArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "transactionIndex", + "docs": [ + "Index of the multisig transaction this proposal is associated with." + ], + "type": "u64" + }, + { + "name": "draft", + "docs": [ + "Whether the proposal should be initialized with status `Draft`." + ], + "type": "bool" + } + ] + } + }, + { + "name": "ProposalVoteArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "memo", + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "SpendingLimitUseArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "amount", + "docs": [ + "Amount of tokens to transfer." + ], + "type": "u64" + }, + { + "name": "decimals", + "docs": [ + "Decimals of the token mint. Used for double-checking against incorrect order of magnitude of `amount`." + ], + "type": "u8" + }, + { + "name": "memo", + "docs": [ + "Memo used for indexing." + ], + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "TransactionBufferCreateArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vaultIndex", + "docs": [ + "Index of the vault this transaction belongs to." + ], + "type": "u8" + }, + { + "name": "finalBufferHash", + "docs": [ + "Hash of the final assembled transaction message." + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "finalBufferSize", + "docs": [ + "Final size of the buffer." + ], + "type": "u16" + }, + { + "name": "buffer", + "docs": [ + "Initial slice of the buffer." + ], + "type": "bytes" + } + ] + } + }, + { + "name": "TransactionBufferExtendArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "buffer", + "type": "bytes" + } + ] + } + }, + { + "name": "VaultTransactionCreateArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "vaultIndex", + "docs": [ + "Index of the vault this transaction belongs to." + ], + "type": "u8" + }, + { + "name": "ephemeralSigners", + "docs": [ + "Number of ephemeral signing PDAs required by the transaction." + ], + "type": "u8" + }, + { + "name": "transactionMessage", + "type": "bytes" + }, + { + "name": "memo", + "type": { + "option": "string" + } + } + ] + } + }, + { + "name": "Member", + "type": { + "kind": "struct", + "fields": [ + { + "name": "key", + "type": "publicKey" + }, + { + "name": "permissions", + "type": { + "defined": "Permissions" + } + } + ] + } + }, + { + "name": "Permissions", + "docs": [ + "Bitmask for permissions." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "mask", + "type": "u8" + } + ] + } + }, + { + "name": "VaultTransactionMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "numSigners", + "docs": [ + "The number of signer pubkeys in the account_keys vec." + ], + "type": "u8" + }, + { + "name": "numWritableSigners", + "docs": [ + "The number of writable signer pubkeys in the account_keys vec." + ], + "type": "u8" + }, + { + "name": "numWritableNonSigners", + "docs": [ + "The number of writable non-signer pubkeys in the account_keys vec." + ], + "type": "u8" + }, + { + "name": "accountKeys", + "docs": [ + "Unique account pubkeys (including program IDs) required for execution of the tx.", + "The signer pubkeys appear at the beginning of the vec, with writable pubkeys first, and read-only pubkeys following.", + "The non-signer pubkeys follow with writable pubkeys first and read-only ones following.", + "Program IDs are also stored at the end of the vec along with other non-signer non-writable pubkeys:", + "", + "```plaintext", + "[pubkey1, pubkey2, pubkey3, pubkey4, pubkey5, pubkey6, pubkey7, pubkey8]", + "|---writable---| |---readonly---| |---writable---| |---readonly---|", + "|------------signers-------------| |----------non-singers-----------|", + "```" + ], + "type": { + "vec": "publicKey" + } + }, + { + "name": "instructions", + "docs": [ + "List of instructions making up the tx." + ], + "type": { + "vec": { + "defined": "MultisigCompiledInstruction" + } + } + }, + { + "name": "addressTableLookups", + "docs": [ + "List of address table lookups used to load additional accounts", + "for this transaction." + ], + "type": { + "vec": { + "defined": "MultisigMessageAddressTableLookup" + } + } + } + ] + } + }, + { + "name": "MultisigCompiledInstruction", + "docs": [ + "Concise serialization schema for instructions that make up a transaction.", + "Closely mimics the Solana transaction wire format." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "programIdIndex", + "type": "u8" + }, + { + "name": "accountIndexes", + "docs": [ + "Indices into the tx's `account_keys` list indicating which accounts to pass to the instruction." + ], + "type": "bytes" + }, + { + "name": "data", + "docs": [ + "Instruction data." + ], + "type": "bytes" + } + ] + } + }, + { + "name": "MultisigMessageAddressTableLookup", + "docs": [ + "Address table lookups describe an on-chain address lookup table to use", + "for loading more readonly and writable accounts into a transaction." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "accountKey", + "docs": [ + "Address lookup table account key." + ], + "type": "publicKey" + }, + { + "name": "writableIndexes", + "docs": [ + "List of indexes used to load writable accounts." + ], + "type": "bytes" + }, + { + "name": "readonlyIndexes", + "docs": [ + "List of indexes used to load readonly accounts." + ], + "type": "bytes" + } + ] + } + }, + { + "name": "Vote", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Approve" + }, + { + "name": "Reject" + }, + { + "name": "Cancel" + } + ] + } + }, + { + "name": "ConfigAction", + "type": { + "kind": "enum", + "variants": [ + { + "name": "AddMember", + "fields": [ + { + "name": "newMember", + "type": { + "defined": "Member" + } + } + ] + }, + { + "name": "RemoveMember", + "fields": [ + { + "name": "oldMember", + "type": "publicKey" + } + ] + }, + { + "name": "ChangeThreshold", + "fields": [ + { + "name": "newThreshold", + "type": "u16" + } + ] + }, + { + "name": "SetTimeLock", + "fields": [ + { + "name": "newTimeLock", + "type": "u32" + } + ] + }, + { + "name": "AddSpendingLimit", + "fields": [ + { + "name": "createKey", + "docs": [ + "Key that is used to seed the SpendingLimit PDA." + ], + "type": "publicKey" + }, + { + "name": "vaultIndex", + "docs": [ + "The index of the vault that the spending limit is for." + ], + "type": "u8" + }, + { + "name": "mint", + "docs": [ + "The token mint the spending limit is for." + ], + "type": "publicKey" + }, + { + "name": "amount", + "docs": [ + "The amount of tokens that can be spent in a period.", + "This amount is in decimals of the mint,", + "so 1 SOL would be `1_000_000_000` and 1 USDC would be `1_000_000`." + ], + "type": "u64" + }, + { + "name": "period", + "docs": [ + "The reset period of the spending limit.", + "When it passes, the remaining amount is reset, unless it's `Period::OneTime`." + ], + "type": { + "defined": "Period" + } + }, + { + "name": "members", + "docs": [ + "Members of the multisig that can use the spending limit.", + "In case a member is removed from the multisig, the spending limit will remain existent", + "(until explicitly deleted), but the removed member will not be able to use it anymore." + ], + "type": { + "vec": "publicKey" + } + }, + { + "name": "destinations", + "docs": [ + "The destination addresses the spending limit is allowed to sent funds to.", + "If empty, funds can be sent to any address." + ], + "type": { + "vec": "publicKey" + } + } + ] + }, + { + "name": "RemoveSpendingLimit", + "fields": [ + { + "name": "spendingLimit", + "type": "publicKey" + } + ] + }, + { + "name": "SetRentCollector", + "fields": [ + { + "name": "newRentCollector", + "type": { + "option": "publicKey" + } + } + ] + } + ] + } + }, + { + "name": "ProposalStatus", + "docs": [ + "The status of a proposal.", + "Each variant wraps a timestamp of when the status was set." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Draft", + "fields": [ + { + "name": "timestamp", + "type": "i64" + } + ] + }, + { + "name": "Active", + "fields": [ + { + "name": "timestamp", + "type": "i64" + } + ] + }, + { + "name": "Rejected", + "fields": [ + { + "name": "timestamp", + "type": "i64" + } + ] + }, + { + "name": "Approved", + "fields": [ + { + "name": "timestamp", + "type": "i64" + } + ] + }, + { + "name": "Executing" + }, + { + "name": "Executed", + "fields": [ + { + "name": "timestamp", + "type": "i64" + } + ] + }, + { + "name": "Cancelled", + "fields": [ + { + "name": "timestamp", + "type": "i64" + } + ] + } + ] + } + }, + { + "name": "Period", + "docs": [ + "The reset period of the spending limit." + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "OneTime" + }, + { + "name": "Day" + }, + { + "name": "Week" + }, + { + "name": "Month" + } + ] + } + } + ], + "errors": [ + { + "code": 6000, + "name": "DuplicateMember", + "msg": "Found multiple members with the same pubkey" + }, + { + "code": 6001, + "name": "EmptyMembers", + "msg": "Members array is empty" + }, + { + "code": 6002, + "name": "TooManyMembers", + "msg": "Too many members, can be up to 65535" + }, + { + "code": 6003, + "name": "InvalidThreshold", + "msg": "Invalid threshold, must be between 1 and number of members with Vote permission" + }, + { + "code": 6004, + "name": "Unauthorized", + "msg": "Attempted to perform an unauthorized action" + }, + { + "code": 6005, + "name": "NotAMember", + "msg": "Provided pubkey is not a member of multisig" + }, + { + "code": 6006, + "name": "InvalidTransactionMessage", + "msg": "TransactionMessage is malformed." + }, + { + "code": 6007, + "name": "StaleProposal", + "msg": "Proposal is stale" + }, + { + "code": 6008, + "name": "InvalidProposalStatus", + "msg": "Invalid proposal status" + }, + { + "code": 6009, + "name": "InvalidTransactionIndex", + "msg": "Invalid transaction index" + }, + { + "code": 6010, + "name": "AlreadyApproved", + "msg": "Member already approved the transaction" + }, + { + "code": 6011, + "name": "AlreadyRejected", + "msg": "Member already rejected the transaction" + }, + { + "code": 6012, + "name": "AlreadyCancelled", + "msg": "Member already cancelled the transaction" + }, + { + "code": 6013, + "name": "InvalidNumberOfAccounts", + "msg": "Wrong number of accounts provided" + }, + { + "code": 6014, + "name": "InvalidAccount", + "msg": "Invalid account provided" + }, + { + "code": 6015, + "name": "RemoveLastMember", + "msg": "Cannot remove last member" + }, + { + "code": 6016, + "name": "NoVoters", + "msg": "Members don't include any voters" + }, + { + "code": 6017, + "name": "NoProposers", + "msg": "Members don't include any proposers" + }, + { + "code": 6018, + "name": "NoExecutors", + "msg": "Members don't include any executors" + }, + { + "code": 6019, + "name": "InvalidStaleTransactionIndex", + "msg": "`stale_transaction_index` must be <= `transaction_index`" + }, + { + "code": 6020, + "name": "NotSupportedForControlled", + "msg": "Instruction not supported for controlled multisig" + }, + { + "code": 6021, + "name": "TimeLockNotReleased", + "msg": "Proposal time lock has not been released" + }, + { + "code": 6022, + "name": "NoActions", + "msg": "Config transaction must have at least one action" + }, + { + "code": 6023, + "name": "MissingAccount", + "msg": "Missing account" + }, + { + "code": 6024, + "name": "InvalidMint", + "msg": "Invalid mint" + }, + { + "code": 6025, + "name": "InvalidDestination", + "msg": "Invalid destination" + }, + { + "code": 6026, + "name": "SpendingLimitExceeded", + "msg": "Spending limit exceeded" + }, + { + "code": 6027, + "name": "DecimalsMismatch", + "msg": "Decimals don't match the mint" + }, + { + "code": 6028, + "name": "UnknownPermission", + "msg": "Member has unknown permission" + }, + { + "code": 6029, + "name": "ProtectedAccount", + "msg": "Account is protected, it cannot be passed into a CPI as writable" + }, + { + "code": 6030, + "name": "TimeLockExceedsMaxAllowed", + "msg": "Time lock exceeds the maximum allowed (90 days)" + }, + { + "code": 6031, + "name": "IllegalAccountOwner", + "msg": "Account is not owned by Multisig program" + }, + { + "code": 6032, + "name": "RentReclamationDisabled", + "msg": "Rent reclamation is disabled for this multisig" + }, + { + "code": 6033, + "name": "InvalidRentCollector", + "msg": "Invalid rent collector address" + }, + { + "code": 6034, + "name": "ProposalForAnotherMultisig", + "msg": "Proposal is for another multisig" + }, + { + "code": 6035, + "name": "TransactionForAnotherMultisig", + "msg": "Transaction is for another multisig" + }, + { + "code": 6036, + "name": "TransactionNotMatchingProposal", + "msg": "Transaction doesn't match proposal" + }, + { + "code": 6037, + "name": "TransactionNotLastInBatch", + "msg": "Transaction is not last in batch" + }, + { + "code": 6038, + "name": "BatchNotEmpty", + "msg": "Batch is not empty" + }, + { + "code": 6039, + "name": "SpendingLimitInvalidAmount", + "msg": "Invalid SpendingLimit amount" + }, + { + "code": 6040, + "name": "FinalBufferHashMismatch", + "msg": "Final message buffer hash doesnt match the expected hash" + }, + { + "code": 6041, + "name": "FinalBufferSizeExceeded", + "msg": "Final buffer size cannot exceed 4000 bytes" + }, + { + "code": 6042, + "name": "FinalBufferSizeMismatch", + "msg": "Final buffer size mismatch" + } + ], + "metadata": { + "address": "SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf", + "origin": "anchor", + "binaryVersion": "0.29.0", + "libVersion": "=0.29.0" + } +} \ No newline at end of file diff --git a/sdk/multisig/src/generated/instructions/heapTest.ts b/sdk/multisig/src/generated/instructions/heapTest.ts deleted file mode 100644 index 786dd5fe..00000000 --- a/sdk/multisig/src/generated/instructions/heapTest.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * This code was GENERATED using the solita package. - * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. - * - * See: https://github.com/metaplex-foundation/solita - */ - -import * as beet from '@metaplex-foundation/beet' -import * as web3 from '@solana/web3.js' - -/** - * @category Instructions - * @category HeapTest - * @category generated - */ -export type HeapTestInstructionArgs = { - length: beet.bignum -} -/** - * @category Instructions - * @category HeapTest - * @category generated - */ -export const heapTestStruct = new beet.BeetArgsStruct< - HeapTestInstructionArgs & { - instructionDiscriminator: number[] /* size: 8 */ - } ->( - [ - ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], - ['length', beet.u64], - ], - 'HeapTestInstructionArgs' -) -/** - * Accounts required by the _heapTest_ instruction - * - * @property [] authority - * @category Instructions - * @category HeapTest - * @category generated - */ -export type HeapTestInstructionAccounts = { - authority: web3.PublicKey - anchorRemainingAccounts?: web3.AccountMeta[] -} - -export const heapTestInstructionDiscriminator = [ - 134, 205, 61, 111, 245, 170, 136, 43, -] - -/** - * Creates a _HeapTest_ instruction. - * - * @param accounts that will be accessed while the instruction is processed - * @param args to provide as instruction data to the program - * - * @category Instructions - * @category HeapTest - * @category generated - */ -export function createHeapTestInstruction( - accounts: HeapTestInstructionAccounts, - args: HeapTestInstructionArgs, - programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') -) { - const [data] = heapTestStruct.serialize({ - instructionDiscriminator: heapTestInstructionDiscriminator, - ...args, - }) - const keys: web3.AccountMeta[] = [ - { - pubkey: accounts.authority, - isWritable: false, - isSigner: false, - }, - ] - - if (accounts.anchorRemainingAccounts != null) { - for (const acc of accounts.anchorRemainingAccounts) { - keys.push(acc) - } - } - - const ix = new web3.TransactionInstruction({ - programId, - keys, - data, - }) - return ix -} diff --git a/sdk/multisig/src/generated/instructions/index.ts b/sdk/multisig/src/generated/instructions/index.ts index 9ca00bc8..9f89535d 100644 --- a/sdk/multisig/src/generated/instructions/index.ts +++ b/sdk/multisig/src/generated/instructions/index.ts @@ -5,7 +5,6 @@ export * from './batchExecuteTransaction' export * from './configTransactionAccountsClose' export * from './configTransactionCreate' export * from './configTransactionExecute' -export * from './heapTest' export * from './multisigAddMember' export * from './multisigAddSpendingLimit' export * from './multisigChangeThreshold' diff --git a/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts b/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts index 6156118f..d180ad82 100644 --- a/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts +++ b/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts @@ -8,9 +8,9 @@ import * as beet from '@metaplex-foundation/beet' import * as web3 from '@solana/web3.js' import { - VaultTransactionCreateFromBufferArgs, - vaultTransactionCreateFromBufferArgsBeet, -} from '../types/VaultTransactionCreateFromBufferArgs' + VaultTransactionCreateArgs, + vaultTransactionCreateArgsBeet, +} from '../types/VaultTransactionCreateArgs' /** * @category Instructions @@ -18,7 +18,7 @@ import { * @category generated */ export type VaultTransactionCreateFromBufferInstructionArgs = { - args: VaultTransactionCreateFromBufferArgs + args: VaultTransactionCreateArgs } /** * @category Instructions @@ -33,7 +33,7 @@ export const vaultTransactionCreateFromBufferStruct = >( [ ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], - ['args', vaultTransactionCreateFromBufferArgsBeet], + ['args', vaultTransactionCreateArgsBeet], ], 'VaultTransactionCreateFromBufferInstructionArgs' ) @@ -41,7 +41,6 @@ export const vaultTransactionCreateFromBufferStruct = * Accounts required by the _vaultTransactionCreateFromBuffer_ instruction * * @property [_writable_] multisig - * @property [_writable_] transactionBuffer * @property [_writable_] transaction * @property [**signer**] creator * @property [_writable_, **signer**] rentPayer @@ -51,7 +50,6 @@ export const vaultTransactionCreateFromBufferStruct = */ export type VaultTransactionCreateFromBufferInstructionAccounts = { multisig: web3.PublicKey - transactionBuffer: web3.PublicKey transaction: web3.PublicKey creator: web3.PublicKey rentPayer: web3.PublicKey @@ -89,11 +87,6 @@ export function createVaultTransactionCreateFromBufferInstruction( isWritable: true, isSigner: false, }, - { - pubkey: accounts.transactionBuffer, - isWritable: true, - isSigner: false, - }, { pubkey: accounts.transaction, isWritable: true, diff --git a/sdk/multisig/src/generated/types/VaultTransactionCreateFromBufferArgs.ts b/sdk/multisig/src/generated/types/VaultTransactionCreateFromBufferArgs.ts deleted file mode 100644 index 4238cf48..00000000 --- a/sdk/multisig/src/generated/types/VaultTransactionCreateFromBufferArgs.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * This code was GENERATED using the solita package. - * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. - * - * See: https://github.com/metaplex-foundation/solita - */ - -import * as beet from '@metaplex-foundation/beet' -export type VaultTransactionCreateFromBufferArgs = { - ephemeralSigners: number - memo: beet.COption -} - -/** - * @category userTypes - * @category generated - */ -export const vaultTransactionCreateFromBufferArgsBeet = - new beet.FixableBeetArgsStruct( - [ - ['ephemeralSigners', beet.u8], - ['memo', beet.coption(beet.utf8String)], - ], - 'VaultTransactionCreateFromBufferArgs' - ) diff --git a/sdk/multisig/src/generated/types/index.ts b/sdk/multisig/src/generated/types/index.ts index 5bb4d22a..b60c4a84 100644 --- a/sdk/multisig/src/generated/types/index.ts +++ b/sdk/multisig/src/generated/types/index.ts @@ -28,6 +28,5 @@ export * from './SpendingLimitUseArgs' export * from './TransactionBufferCreateArgs' export * from './TransactionBufferExtendArgs' export * from './VaultTransactionCreateArgs' -export * from './VaultTransactionCreateFromBufferArgs' export * from './VaultTransactionMessage' export * from './Vote' diff --git a/tests/index.ts b/tests/index.ts index 37dae815..3b9dbd09 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,26 +1,26 @@ -// The order of imports is the order the test suite will run in. +// // The order of imports is the order the test suite will run in. import "./suites/program-config-init"; -import "./suites/instructions/multisigCreate"; -import "./suites/instructions/multisigCreateV2"; -import "./suites/instructions/multisigSetRentCollector"; -import "./suites/instructions/configTransactionExecute"; -import "./suites/instructions/configTransactionAccountsClose"; -import "./suites/instructions/vaultBatchTransactionAccountClose"; -import "./suites/instructions/batchAccountsClose"; -import "./suites/instructions/vaultTransactionAccountsClose"; -import "./suites/instructions/transactionBufferClose"; -import "./suites/instructions/transactionBufferCreate"; -import "./suites/instructions/transactionBufferExtend"; +// import "./suites/instructions/multisigCreate"; +// import "./suites/instructions/multisigCreateV2"; +// import "./suites/instructions/multisigSetRentCollector"; +// import "./suites/instructions/configTransactionExecute"; +// import "./suites/instructions/configTransactionAccountsClose"; +// import "./suites/instructions/vaultBatchTransactionAccountClose"; +// import "./suites/instructions/batchAccountsClose"; +// import "./suites/instructions/vaultTransactionAccountsClose"; +// import "./suites/instructions/transactionBufferClose"; +// import "./suites/instructions/transactionBufferCreate"; +// import "./suites/instructions/transactionBufferExtend"; import "./suites/instructions/vaultTransactionCreateFromBuffer"; -import "./suites/instructions/cancelRealloc"; -import "./suites/multisig-sdk"; -import "./suites/account-migrations"; -import "./suites/examples/batch-sol-transfer"; -import "./suites/examples/create-mint"; -import "./suites/examples/immediate-execution"; -import "./suites/examples/spending-limits"; +// import "./suites/instructions/cancelRealloc"; +// import "./suites/multisig-sdk"; +// import "./suites/account-migrations"; +// import "./suites/examples/batch-sol-transfer"; +// import "./suites/examples/create-mint"; +// import "./suites/examples/immediate-execution"; +// import "./suites/examples/spending-limits"; import "./suites/examples/transaction-buffer"; -// Uncomment to enable the heapTest instruction testing -//import "./suites/instructions/heapTest"; -import "./suites/examples/custom-heap"; +// // Uncomment to enable the heapTest instruction testing +// //import "./suites/instructions/heapTest"; +// import "./suites/examples/custom-heap"; diff --git a/tests/suites/examples/transaction-buffer.ts b/tests/suites/examples/transaction-buffer.ts index 3e9f7609..32308e27 100644 --- a/tests/suites/examples/transaction-buffer.ts +++ b/tests/suites/examples/transaction-buffer.ts @@ -1,33 +1,33 @@ -import * as multisig from "@sqds/multisig"; -import * as crypto from "crypto"; -import assert from "assert"; -import { BN } from "bn.js"; import { + AccountMeta, Keypair, LAMPORTS_PER_SOL, PublicKey, - TransactionMessage, - VersionedTransaction, SystemProgram, Transaction, + TransactionMessage, + VersionedTransaction } from "@solana/web3.js"; -import { - createAutonomousMultisigV2, - createLocalhostConnection, - createTestTransferInstruction, - generateMultisigMembers, - getLogs, - getTestProgramId, - TestMembers, -} from "../../utils"; +import * as multisig from "@sqds/multisig"; import { TransactionBufferCreateArgs, TransactionBufferCreateInstructionArgs, TransactionBufferExtendArgs, TransactionBufferExtendInstructionArgs, - VaultTransactionCreateFromBufferArgs, + VaultTransactionCreateArgs, VaultTransactionCreateFromBufferInstructionArgs, } from "@sqds/multisig/lib/generated"; +import assert from "assert"; +import { BN } from "bn.js"; +import * as crypto from "crypto"; +import { + TestMembers, + createAutonomousMultisigV2, + createLocalhostConnection, + createTestTransferInstruction, + generateMultisigMembers, + getTestProgramId +} from "../../utils"; const programId = getTestProgramId(); @@ -226,22 +226,29 @@ describe("Examples / Transaction Buffers", () => { programId, }); + const transactionBufferMeta: AccountMeta = { + pubkey: transactionBuffer, + isWritable: true, + isSigner: false + } // Create final instruction. const thirdIx = multisig.generated.createVaultTransactionCreateFromBufferInstruction( { multisig: multisigPda, - transactionBuffer, transaction: transactionPda, creator: members.almighty.publicKey, rentPayer: members.almighty.publicKey, systemProgram: SystemProgram.programId, + anchorRemainingAccounts: [transactionBufferMeta] }, { args: { + vaultIndex: 0, + transactionMessage: Buffer.from("123"), ephemeralSigners: 0, memo: null, - } as VaultTransactionCreateFromBufferArgs, + } as VaultTransactionCreateArgs, } as VaultTransactionCreateFromBufferInstructionArgs, programId ); diff --git a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts index d87e53f4..67a1adee 100644 --- a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts +++ b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts @@ -1,12 +1,12 @@ import { + AccountMeta, Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, - TransactionMessage, - VersionedTransaction, Transaction, - ComputeBudgetProgram, + TransactionMessage, + VersionedTransaction } from "@solana/web3.js"; import * as multisig from "@sqds/multisig"; import { @@ -14,7 +14,7 @@ import { TransactionBufferCreateInstructionArgs, TransactionBufferExtendArgs, TransactionBufferExtendInstructionArgs, - VaultTransactionCreateFromBufferArgs, + VaultTransactionCreateArgs, VaultTransactionCreateFromBufferInstructionArgs } from "@sqds/multisig/lib/generated"; import assert from "assert"; @@ -156,6 +156,8 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { const signature = await connection.sendTransaction(tx, { skipPreflight: true, }); + console.log(signature); + await connection.confirmTransaction(signature); const transactionBufferAccount = await connection.getAccountInfo( @@ -233,30 +235,40 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { programId, }); + const transactionBufferMeta: AccountMeta = { + pubkey: transactionBuffer, + isWritable: true, + isSigner: false + } + // Create final instruction. const thirdIx = multisig.generated.createVaultTransactionCreateFromBufferInstruction( { multisig: multisigPda, - transactionBuffer, transaction: transactionPda, creator: members.proposer.publicKey, rentPayer: members.proposer.publicKey, systemProgram: SystemProgram.programId, + anchorRemainingAccounts: [transactionBufferMeta] }, { args: { + vaultIndex: 0, ephemeralSigners: 0, + transactionMessage: Buffer.from("123"), memo: null, - } as VaultTransactionCreateFromBufferArgs, + } as VaultTransactionCreateArgs, } as VaultTransactionCreateFromBufferInstructionArgs, programId ); // Add third instruction to the message. + const blockhash = await connection.getLatestBlockhash(); + const thirdMessage = new TransactionMessage({ payerKey: members.proposer.publicKey, - recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + recentBlockhash: blockhash.blockhash, instructions: [thirdIx], }).compileToV0Message(); @@ -265,18 +277,24 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { thirdTx.sign([members.proposer]); // Send final transaction. - const thirdSignature = await connection.sendTransaction(thirdTx, { + const thirdSignature = await connection.sendRawTransaction(thirdTx.serialize(), { skipPreflight: true, }); - await connection.confirmTransaction(thirdSignature); + console.log("Create Vault Tx:", thirdSignature); + + await connection.confirmTransaction({ + signature: thirdSignature, + blockhash: blockhash.blockhash, + lastValidBlockHeight: blockhash.lastValidBlockHeight, + }, "confirmed"); const transactionInfo = await multisig.accounts.VaultTransaction.fromAccountAddress( connection, transactionPda ); - + console.log(transactionInfo); // Ensure final vault transaction has 43 instructions assert.equal(transactionInfo.message.instructions.length, 43); }); @@ -358,23 +376,29 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { index: transactionIndex, programId, }); - + const transactionBufferMeta: AccountMeta = { + pubkey: transactionBuffer, + isWritable: true, + isSigner: false + } const createFromBufferIx = multisig.generated.createVaultTransactionCreateFromBufferInstruction( { multisig: multisigPda, - transactionBuffer, transaction: transactionPda, creator: members.proposer.publicKey, rentPayer: members.proposer.publicKey, systemProgram: SystemProgram.programId, + anchorRemainingAccounts: [transactionBufferMeta] }, { args: { + vaultIndex: 0, ephemeralSigners: 0, + transactionMessage: Buffer.from("123"), memo: null, - } as VaultTransactionCreateFromBufferArgs, + } as VaultTransactionCreateArgs, } as VaultTransactionCreateFromBufferInstructionArgs, programId ); From 63a068d362f2a4d459eb04c66ff00bb27fc98299 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Fri, 6 Sep 2024 20:19:40 +0200 Subject: [PATCH 14/47] refactor: vault_transaction_from_buffer --- .../squads_multisig_program/src/errors.rs | 2 + .../instructions/vault_transaction_create.rs | 92 +++++++++++++++---- programs/squads_multisig_program/src/lib.rs | 4 +- sdk/multisig/idl/squads_multisig_program.json | 9 +- sdk/multisig/src/generated/errors/index.ts | 35 +++++-- tests/index.ts | 41 +++++---- tests/suites/examples/transaction-buffer.ts | 2 +- .../vaultTransactionCreateFromBuffer.ts | 30 ++++-- 8 files changed, 159 insertions(+), 56 deletions(-) diff --git a/programs/squads_multisig_program/src/errors.rs b/programs/squads_multisig_program/src/errors.rs index 947ebcb9..933e229b 100644 --- a/programs/squads_multisig_program/src/errors.rs +++ b/programs/squads_multisig_program/src/errors.rs @@ -82,6 +82,8 @@ pub enum MultisigError { BatchNotEmpty, #[msg("Invalid SpendingLimit amount")] SpendingLimitInvalidAmount, + #[msg("Invalid Instruction Arguments")] + InvalidInstructionArgs, #[msg("Final message buffer hash doesnt match the expected hash")] FinalBufferHashMismatch, #[msg("Final buffer size cannot exceed 4000 bytes")] diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs index 8e66da41..1a0fc4de 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use anchor_lang::system_program; use crate::errors::*; use crate::state::*; @@ -130,13 +131,43 @@ impl<'info> VaultTransactionCreate<'info> { Ok(()) } - pub fn build_args_from_buffer_account( - ctx: &Context<'_, '_, 'info, 'info, Self>, - args: &VaultTransactionCreateArgs, - ) -> Result { + pub fn validate_transaction_buffer( + ctx: &Context, + transaction_buffer_account: &Account<'info, TransactionBuffer>, + transaction_buffer_info: &AccountInfo<'info>, + ) -> Result<()> { + // Check that the buffer creator is the same as the transaction creator + require_keys_eq!( + transaction_buffer_account.creator, + ctx.accounts.creator.key(), + MultisigError::InvalidAccount + ); + + // Check that the buffer is writable + require!( + transaction_buffer_info.is_writable, + MultisigError::InvalidAccount + ); + // Validate that the final hash matches the buffer + transaction_buffer_account.validate_hash()?; + + // Validate that the final size is correct + transaction_buffer_account.validate_size()?; + + Ok(()) + } + pub fn vault_transaction_create_from_buffer( + ctx: Context<'_, '_, 'info, 'info, Self>, + args: VaultTransactionCreateArgs, + ) -> Result<()> { + // Mutable Accounts let multisig = &ctx.accounts.multisig; - let remaining_accounts = ctx.remaining_accounts; + let vault_transaction_account = &mut ctx.accounts.transaction; + let vault_transaction_account_info = vault_transaction_account.to_account_info(); + let rent_payer = &ctx.accounts.rent_payer; + // Readonly Accounts + let remaining_accounts = ctx.remaining_accounts; // Determine valid buffer account address let (transaction_buffer_address, _) = Pubkey::find_program_address( &[ @@ -157,32 +188,57 @@ impl<'info> VaultTransactionCreate<'info> { .iter() .find(|acc| acc.key == &transaction_buffer_address) .ok_or(MultisigError::MissingAccount)?; + let transaction_buffer_account: Account<'info, TransactionBuffer> = Account::::try_from(transaction_buffer_info)?; - // Check that the buffer creator is the same as the transaction creator - require_keys_eq!( - transaction_buffer_account.creator, - ctx.accounts.creator.key(), - MultisigError::InvalidAccount - ); - - // Check that the buffer is writable + // Validate the Transaction Buffer + VaultTransactionCreate::validate_transaction_buffer( + &ctx, + &transaction_buffer_account, + &transaction_buffer_info, + )?; + + // Check that the transaction message is "empty" require!( - transaction_buffer_info.is_writable, - MultisigError::InvalidAccount + args.transaction_message == vec![0, 0, 0, 0, 0, 0], + MultisigError::InvalidInstructionArgs ); // Close the buffer account transaction_buffer_account.close(ctx.accounts.creator.to_account_info())?; - // Build the args - Ok(VaultTransactionCreateArgs { + // Calculate the new required length of the vault transaction account + let new_len = VaultTransaction::size( + args.ephemeral_signers, + transaction_buffer_account.buffer.as_slice(), + )?; + + // Calculate the rent exemption + let rent_exempt_lamports = Rent::get().unwrap().minimum_balance(new_len).max(1); + + // Check the difference between the rent exemption and the current lamports + let top_up_lamports = + rent_exempt_lamports.saturating_sub(vault_transaction_account_info.lamports()); + + // Top up the rent payer account with the difference + **rent_payer.try_borrow_mut_lamports()? -= top_up_lamports; + **vault_transaction_account_info.try_borrow_mut_lamports()? += top_up_lamports; + + // Reallocate the vault transaction account to the new length of the transaction message + AccountInfo::realloc(&vault_transaction_account_info, new_len, true)?; + + // Build the args to overwrite the blank vault transaction account + let create_args = VaultTransactionCreateArgs { vault_index: transaction_buffer_account.vault_index, ephemeral_signers: args.ephemeral_signers, transaction_message: transaction_buffer_account.buffer.clone(), memo: None, - }) + }; + // Populate the vault Transaction with the new args + Self::vault_transaction_create(ctx, create_args)?; + + Ok(()) } } diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index 36900a47..4038619d 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -203,9 +203,7 @@ pub mod squads_multisig_program { ctx: Context<'_, '_, 'info, 'info, VaultTransactionCreate<'info>>, args: VaultTransactionCreateArgs, ) -> Result<()> { - msg!("got here"); - let create_args = VaultTransactionCreate::build_args_from_buffer_account(&ctx, &args)?; - VaultTransactionCreate::vault_transaction_create(ctx, create_args) + VaultTransactionCreate::vault_transaction_create_from_buffer(ctx, args) } /// Execute a vault transaction. diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index 9ff9c521..d732c844 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -3442,16 +3442,21 @@ }, { "code": 6040, + "name": "InvalidInstructionArgs", + "msg": "Invalid Instruction Arguments" + }, + { + "code": 6041, "name": "FinalBufferHashMismatch", "msg": "Final message buffer hash doesnt match the expected hash" }, { - "code": 6041, + "code": 6042, "name": "FinalBufferSizeExceeded", "msg": "Final buffer size cannot exceed 4000 bytes" }, { - "code": 6042, + "code": 6043, "name": "FinalBufferSizeMismatch", "msg": "Final buffer size mismatch" } diff --git a/sdk/multisig/src/generated/errors/index.ts b/sdk/multisig/src/generated/errors/index.ts index aa26cfb9..4f32f4d2 100644 --- a/sdk/multisig/src/generated/errors/index.ts +++ b/sdk/multisig/src/generated/errors/index.ts @@ -921,6 +921,29 @@ createErrorFromNameLookup.set( () => new SpendingLimitInvalidAmountError() ) +/** + * InvalidInstructionArgs: 'Invalid Instruction Arguments' + * + * @category Errors + * @category generated + */ +export class InvalidInstructionArgsError extends Error { + readonly code: number = 0x1798 + readonly name: string = 'InvalidInstructionArgs' + constructor() { + super('Invalid Instruction Arguments') + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, InvalidInstructionArgsError) + } + } +} + +createErrorFromCodeLookup.set(0x1798, () => new InvalidInstructionArgsError()) +createErrorFromNameLookup.set( + 'InvalidInstructionArgs', + () => new InvalidInstructionArgsError() +) + /** * FinalBufferHashMismatch: 'Final message buffer hash doesnt match the expected hash' * @@ -928,7 +951,7 @@ createErrorFromNameLookup.set( * @category generated */ export class FinalBufferHashMismatchError extends Error { - readonly code: number = 0x1798 + readonly code: number = 0x1799 readonly name: string = 'FinalBufferHashMismatch' constructor() { super('Final message buffer hash doesnt match the expected hash') @@ -938,7 +961,7 @@ export class FinalBufferHashMismatchError extends Error { } } -createErrorFromCodeLookup.set(0x1798, () => new FinalBufferHashMismatchError()) +createErrorFromCodeLookup.set(0x1799, () => new FinalBufferHashMismatchError()) createErrorFromNameLookup.set( 'FinalBufferHashMismatch', () => new FinalBufferHashMismatchError() @@ -951,7 +974,7 @@ createErrorFromNameLookup.set( * @category generated */ export class FinalBufferSizeExceededError extends Error { - readonly code: number = 0x1799 + readonly code: number = 0x179a readonly name: string = 'FinalBufferSizeExceeded' constructor() { super('Final buffer size cannot exceed 4000 bytes') @@ -961,7 +984,7 @@ export class FinalBufferSizeExceededError extends Error { } } -createErrorFromCodeLookup.set(0x1799, () => new FinalBufferSizeExceededError()) +createErrorFromCodeLookup.set(0x179a, () => new FinalBufferSizeExceededError()) createErrorFromNameLookup.set( 'FinalBufferSizeExceeded', () => new FinalBufferSizeExceededError() @@ -974,7 +997,7 @@ createErrorFromNameLookup.set( * @category generated */ export class FinalBufferSizeMismatchError extends Error { - readonly code: number = 0x179a + readonly code: number = 0x179b readonly name: string = 'FinalBufferSizeMismatch' constructor() { super('Final buffer size mismatch') @@ -984,7 +1007,7 @@ export class FinalBufferSizeMismatchError extends Error { } } -createErrorFromCodeLookup.set(0x179a, () => new FinalBufferSizeMismatchError()) +createErrorFromCodeLookup.set(0x179b, () => new FinalBufferSizeMismatchError()) createErrorFromNameLookup.set( 'FinalBufferSizeMismatch', () => new FinalBufferSizeMismatchError() diff --git a/tests/index.ts b/tests/index.ts index 3b9dbd09..725fd878 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,25 +1,26 @@ // // The order of imports is the order the test suite will run in. -import "./suites/program-config-init"; -// import "./suites/instructions/multisigCreate"; -// import "./suites/instructions/multisigCreateV2"; -// import "./suites/instructions/multisigSetRentCollector"; -// import "./suites/instructions/configTransactionExecute"; -// import "./suites/instructions/configTransactionAccountsClose"; -// import "./suites/instructions/vaultBatchTransactionAccountClose"; -// import "./suites/instructions/batchAccountsClose"; -// import "./suites/instructions/vaultTransactionAccountsClose"; -// import "./suites/instructions/transactionBufferClose"; -// import "./suites/instructions/transactionBufferCreate"; -// import "./suites/instructions/transactionBufferExtend"; -import "./suites/instructions/vaultTransactionCreateFromBuffer"; -// import "./suites/instructions/cancelRealloc"; -// import "./suites/multisig-sdk"; -// import "./suites/account-migrations"; -// import "./suites/examples/batch-sol-transfer"; -// import "./suites/examples/create-mint"; -// import "./suites/examples/immediate-execution"; -// import "./suites/examples/spending-limits"; +import "./suites/program-config-init" +import "./suites/account-migrations"; +import "./suites/examples/batch-sol-transfer"; +import "./suites/examples/create-mint"; +import "./suites/examples/immediate-execution"; +import "./suites/examples/spending-limits"; import "./suites/examples/transaction-buffer"; +import "./suites/instructions/batchAccountsClose"; +import "./suites/instructions/cancelRealloc"; +import "./suites/instructions/configTransactionAccountsClose"; +import "./suites/instructions/configTransactionExecute"; +import "./suites/instructions/multisigCreate"; +import "./suites/instructions/multisigCreateV2"; +import "./suites/instructions/multisigSetRentCollector"; +import "./suites/instructions/transactionBufferClose"; +import "./suites/instructions/transactionBufferCreate"; +import "./suites/instructions/transactionBufferExtend"; +import "./suites/instructions/vaultBatchTransactionAccountClose"; +import "./suites/instructions/vaultTransactionAccountsClose"; +import "./suites/instructions/vaultTransactionCreateFromBuffer"; +import "./suites/multisig-sdk"; +import "./suites/program-config-init"; // // Uncomment to enable the heapTest instruction testing // //import "./suites/instructions/heapTest"; // import "./suites/examples/custom-heap"; diff --git a/tests/suites/examples/transaction-buffer.ts b/tests/suites/examples/transaction-buffer.ts index 32308e27..09c04749 100644 --- a/tests/suites/examples/transaction-buffer.ts +++ b/tests/suites/examples/transaction-buffer.ts @@ -245,7 +245,7 @@ describe("Examples / Transaction Buffers", () => { { args: { vaultIndex: 0, - transactionMessage: Buffer.from("123"), + transactionMessage: new Uint8Array(6).fill(0), ephemeralSigners: 0, memo: null, } as VaultTransactionCreateArgs, diff --git a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts index 67a1adee..6e6dc813 100644 --- a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts +++ b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts @@ -156,7 +156,6 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { const signature = await connection.sendTransaction(tx, { skipPreflight: true, }); - console.log(signature); await connection.confirmTransaction(signature); @@ -235,11 +234,31 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { programId, }); + const transactionAccountInfo = await connection.getAccountInfo(transactionPda); + + + const transactionBufferMeta: AccountMeta = { pubkey: transactionBuffer, isWritable: true, isSigner: false } + const mockTransferIx = SystemProgram.transfer({ + fromPubkey: members.proposer.publicKey, + toPubkey: members.almighty.publicKey, + lamports: 100 + }); + // const mockTransferMessage = new TransactionMessage({ + // payerKey: vaultPda, + // recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + // instructions: [mockTransferIx], + // }); + + // const bytes = multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + // message: mockTransferMessage, + // addressLookupTableAccounts: [], + // vaultPda, + // }); // Create final instruction. const thirdIx = @@ -256,7 +275,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { args: { vaultIndex: 0, ephemeralSigners: 0, - transactionMessage: Buffer.from("123"), + transactionMessage: new Uint8Array(6).fill(0), memo: null, } as VaultTransactionCreateArgs, } as VaultTransactionCreateFromBufferInstructionArgs, @@ -281,8 +300,6 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { skipPreflight: true, }); - console.log("Create Vault Tx:", thirdSignature); - await connection.confirmTransaction({ signature: thirdSignature, blockhash: blockhash.blockhash, @@ -294,7 +311,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { connection, transactionPda ); - console.log(transactionInfo); + // Ensure final vault transaction has 43 instructions assert.equal(transactionInfo.message.instructions.length, 43); }); @@ -396,8 +413,9 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { args: { vaultIndex: 0, ephemeralSigners: 0, - transactionMessage: Buffer.from("123"), + transactionMessage: new Uint8Array(6).fill(0), memo: null, + anchorRemainingAccounts: [transactionBufferMeta] } as VaultTransactionCreateArgs, } as VaultTransactionCreateFromBufferInstructionArgs, programId From a481def5a39c28708b7c49e96f8546f307f2500c Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:13:40 +0800 Subject: [PATCH 15/47] feat: wrap context for createFromBuffer --- .../src/instructions/mod.rs | 4 +- .../instructions/vault_transaction_create.rs | 109 ------------------ .../vault_transaction_create_from_buffer.rs | 106 +++++++++++++++++ programs/squads_multisig_program/src/lib.rs | 4 +- sdk/multisig/idl/squads_multisig_program.json | 57 +++++---- .../vaultTransactionCreateFromBuffer.ts | 41 ++++--- tests/index.ts | 4 +- tests/suites/examples/transaction-buffer.ts | 11 +- .../vaultTransactionCreateFromBuffer.ts | 22 ++-- 9 files changed, 194 insertions(+), 164 deletions(-) create mode 100644 programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs diff --git a/programs/squads_multisig_program/src/instructions/mod.rs b/programs/squads_multisig_program/src/instructions/mod.rs index f6352f8d..9c6330a5 100644 --- a/programs/squads_multisig_program/src/instructions/mod.rs +++ b/programs/squads_multisig_program/src/instructions/mod.rs @@ -19,7 +19,7 @@ pub use transaction_buffer_close::*; pub use transaction_buffer_create::*; pub use transaction_buffer_extend::*; pub use vault_transaction_create::*; - +pub use vault_transaction_create_from_buffer::*; pub use vault_transaction_execute::*; mod batch_add_transaction; @@ -43,5 +43,5 @@ mod transaction_buffer_close; mod transaction_buffer_create; mod transaction_buffer_extend; mod vault_transaction_create; - +mod vault_transaction_create_from_buffer; mod vault_transaction_execute; diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs index 1a0fc4de..ae265c38 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs @@ -131,115 +131,6 @@ impl<'info> VaultTransactionCreate<'info> { Ok(()) } - pub fn validate_transaction_buffer( - ctx: &Context, - transaction_buffer_account: &Account<'info, TransactionBuffer>, - transaction_buffer_info: &AccountInfo<'info>, - ) -> Result<()> { - // Check that the buffer creator is the same as the transaction creator - require_keys_eq!( - transaction_buffer_account.creator, - ctx.accounts.creator.key(), - MultisigError::InvalidAccount - ); - - // Check that the buffer is writable - require!( - transaction_buffer_info.is_writable, - MultisigError::InvalidAccount - ); - // Validate that the final hash matches the buffer - transaction_buffer_account.validate_hash()?; - - // Validate that the final size is correct - transaction_buffer_account.validate_size()?; - - Ok(()) - } - pub fn vault_transaction_create_from_buffer( - ctx: Context<'_, '_, 'info, 'info, Self>, - args: VaultTransactionCreateArgs, - ) -> Result<()> { - // Mutable Accounts - let multisig = &ctx.accounts.multisig; - let vault_transaction_account = &mut ctx.accounts.transaction; - let vault_transaction_account_info = vault_transaction_account.to_account_info(); - let rent_payer = &ctx.accounts.rent_payer; - - // Readonly Accounts - let remaining_accounts = ctx.remaining_accounts; - // Determine valid buffer account address - let (transaction_buffer_address, _) = Pubkey::find_program_address( - &[ - SEED_PREFIX, - multisig.key().as_ref(), - SEED_TRANSACTION_BUFFER, - &multisig - .transaction_index - .checked_add(1) - .unwrap() - .to_le_bytes(), - ], - ctx.program_id, - ); - - // Find the buffer account in remaining accounts - let transaction_buffer_info = remaining_accounts - .iter() - .find(|acc| acc.key == &transaction_buffer_address) - .ok_or(MultisigError::MissingAccount)?; - - let transaction_buffer_account: Account<'info, TransactionBuffer> = - Account::::try_from(transaction_buffer_info)?; - - // Validate the Transaction Buffer - VaultTransactionCreate::validate_transaction_buffer( - &ctx, - &transaction_buffer_account, - &transaction_buffer_info, - )?; - - // Check that the transaction message is "empty" - require!( - args.transaction_message == vec![0, 0, 0, 0, 0, 0], - MultisigError::InvalidInstructionArgs - ); - - // Close the buffer account - transaction_buffer_account.close(ctx.accounts.creator.to_account_info())?; - - // Calculate the new required length of the vault transaction account - let new_len = VaultTransaction::size( - args.ephemeral_signers, - transaction_buffer_account.buffer.as_slice(), - )?; - - // Calculate the rent exemption - let rent_exempt_lamports = Rent::get().unwrap().minimum_balance(new_len).max(1); - - // Check the difference between the rent exemption and the current lamports - let top_up_lamports = - rent_exempt_lamports.saturating_sub(vault_transaction_account_info.lamports()); - - // Top up the rent payer account with the difference - **rent_payer.try_borrow_mut_lamports()? -= top_up_lamports; - **vault_transaction_account_info.try_borrow_mut_lamports()? += top_up_lamports; - - // Reallocate the vault transaction account to the new length of the transaction message - AccountInfo::realloc(&vault_transaction_account_info, new_len, true)?; - - // Build the args to overwrite the blank vault transaction account - let create_args = VaultTransactionCreateArgs { - vault_index: transaction_buffer_account.vault_index, - ephemeral_signers: args.ephemeral_signers, - transaction_message: transaction_buffer_account.buffer.clone(), - memo: None, - }; - // Populate the vault Transaction with the new args - Self::vault_transaction_create(ctx, create_args)?; - - Ok(()) - } } /// Unvalidated instruction data, must be treated as untrusted. diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs new file mode 100644 index 00000000..99d09bf2 --- /dev/null +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -0,0 +1,106 @@ +use anchor_lang::prelude::*; + +use crate::errors::*; +use crate::instructions::*; +use crate::state::*; + + +#[derive(Accounts)] +pub struct VaultTransactionCreateFromBuffer<'info> { + pub vault_transaction_create: VaultTransactionCreate<'info>, + + #[account( + mut, + close = creator, + seeds = [ + SEED_PREFIX, + vault_transaction_create.multisig.key().as_ref(), + SEED_TRANSACTION_BUFFER, + &vault_transaction_create.multisig + .transaction_index + .checked_add(1) + .unwrap() + .to_le_bytes(), + ], + bump + )] + pub transaction_buffer: Box>, + + // Anchor doesn't allow us to use the creator inside + // vault_transaction_create, so just re-passing it here + #[account( + mut, + address = vault_transaction_create.creator.key(), + )] + pub creator: Signer<'info>, +} + +impl<'info> VaultTransactionCreateFromBuffer<'info> { + pub fn validate(&self, args: &VaultTransactionCreateArgs) -> Result<()> { + let transaction_buffer_account = &self.transaction_buffer; + + // Check that the transaction message is "empty" + require!( + args.transaction_message == vec![0, 0, 0, 0, 0, 0], + MultisigError::InvalidInstructionArgs + ); + + // Validate that the final hash matches the buffer + transaction_buffer_account.validate_hash()?; + + // Validate that the final size is correct + transaction_buffer_account.validate_size()?; + Ok(()) + } + + #[access_control(ctx.accounts.validate(&args))] + pub fn handler( + ctx: Context<'_, '_, 'info, 'info, Self>, + args: VaultTransactionCreateArgs, + ) -> Result<()> { + let transaction_buffer = &ctx.accounts.transaction_buffer; + let vault_transaction_account_info = &ctx.accounts.vault_transaction_create.transaction.to_account_info(); + let rent_payer_account_info = &ctx.accounts.vault_transaction_create.rent_payer.to_account_info(); + + + // Calculate the new required length of the vault transaction account + let new_len = VaultTransaction::size( + args.ephemeral_signers, + transaction_buffer.buffer.as_slice(), + )?; + + // Calculate the rent exemption + let rent_exempt_lamports = Rent::get().unwrap().minimum_balance(new_len).max(1); + + // Check the difference between the rent exemption and the current lamports + let top_up_lamports = + rent_exempt_lamports.saturating_sub(vault_transaction_account_info.lamports()); + + // Top up the account with the difference, paid by the rent payer + **rent_payer_account_info.try_borrow_mut_lamports()? -= top_up_lamports; + **vault_transaction_account_info.try_borrow_mut_lamports()? += top_up_lamports; + + // Reallocate the vault transaction account to the new length of the transaction message + AccountInfo::realloc(&vault_transaction_account_info, new_len, true)?; + + + // Create the args for the vault transaction create instruction + let create_args = VaultTransactionCreateArgs { + vault_index: transaction_buffer.vault_index, + ephemeral_signers: args.ephemeral_signers, + transaction_message: transaction_buffer.buffer.clone(), + memo: args.memo, + }; + // Create the context for the vault transaction create instruction + let context = Context::new( + ctx.program_id, + &mut ctx.accounts.vault_transaction_create, + ctx.remaining_accounts, + ctx.bumps.vault_transaction_create, + ); + + // Call the vault transaction create instruction + VaultTransactionCreate::vault_transaction_create(context, create_args)?; + Ok(()) + } +} diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index 4038619d..901823ed 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -200,10 +200,10 @@ pub mod squads_multisig_program { /// Create a new vault transaction from a completed transaction buffer. /// Finalized buffer hash must match `final_buffer_hash` pub fn vault_transaction_create_from_buffer<'info>( - ctx: Context<'_, '_, 'info, 'info, VaultTransactionCreate<'info>>, + ctx: Context<'_, '_, 'info, 'info, VaultTransactionCreateFromBuffer<'info>>, args: VaultTransactionCreateArgs, ) -> Result<()> { - VaultTransactionCreate::vault_transaction_create_from_buffer(ctx, args) + VaultTransactionCreateFromBuffer::handler(ctx, args) } /// Execute a vault transaction. diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index d732c844..7c7b45c0 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -871,35 +871,50 @@ ], "accounts": [ { - "name": "multisig", - "isMut": true, - "isSigner": false + "name": "vaultTransactionCreate", + "accounts": [ + { + "name": "multisig", + "isMut": true, + "isSigner": false + }, + { + "name": "transaction", + "isMut": true, + "isSigner": false + }, + { + "name": "creator", + "isMut": false, + "isSigner": true, + "docs": [ + "The member of the multisig that is creating the transaction." + ] + }, + { + "name": "rentPayer", + "isMut": true, + "isSigner": true, + "docs": [ + "The payer for the transaction account rent." + ] + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ] }, { - "name": "transaction", + "name": "transactionBuffer", "isMut": true, "isSigner": false }, { "name": "creator", - "isMut": false, - "isSigner": true, - "docs": [ - "The member of the multisig that is creating the transaction." - ] - }, - { - "name": "rentPayer", "isMut": true, - "isSigner": true, - "docs": [ - "The payer for the transaction account rent." - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false + "isSigner": true } ], "args": [ diff --git a/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts b/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts index d180ad82..74864079 100644 --- a/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts +++ b/sdk/multisig/src/generated/instructions/vaultTransactionCreateFromBuffer.ts @@ -40,20 +40,25 @@ export const vaultTransactionCreateFromBufferStruct = /** * Accounts required by the _vaultTransactionCreateFromBuffer_ instruction * - * @property [_writable_] multisig - * @property [_writable_] transaction - * @property [**signer**] creator - * @property [_writable_, **signer**] rentPayer + * @property [_writable_] vaultTransactionCreateItemMultisig + * @property [_writable_] vaultTransactionCreateItemTransaction + * @property [**signer**] vaultTransactionCreateItemCreator + * @property [_writable_, **signer**] vaultTransactionCreateItemRentPayer + * @property [] vaultTransactionCreateItemSystemProgram + * @property [_writable_] transactionBuffer + * @property [_writable_, **signer**] creator * @category Instructions * @category VaultTransactionCreateFromBuffer * @category generated */ export type VaultTransactionCreateFromBufferInstructionAccounts = { - multisig: web3.PublicKey - transaction: web3.PublicKey + vaultTransactionCreateItemMultisig: web3.PublicKey + vaultTransactionCreateItemTransaction: web3.PublicKey + vaultTransactionCreateItemCreator: web3.PublicKey + vaultTransactionCreateItemRentPayer: web3.PublicKey + vaultTransactionCreateItemSystemProgram: web3.PublicKey + transactionBuffer: web3.PublicKey creator: web3.PublicKey - rentPayer: web3.PublicKey - systemProgram?: web3.PublicKey anchorRemainingAccounts?: web3.AccountMeta[] } @@ -83,30 +88,40 @@ export function createVaultTransactionCreateFromBufferInstruction( }) const keys: web3.AccountMeta[] = [ { - pubkey: accounts.multisig, + pubkey: accounts.vaultTransactionCreateItemMultisig, isWritable: true, isSigner: false, }, { - pubkey: accounts.transaction, + pubkey: accounts.vaultTransactionCreateItemTransaction, isWritable: true, isSigner: false, }, { - pubkey: accounts.creator, + pubkey: accounts.vaultTransactionCreateItemCreator, isWritable: false, isSigner: true, }, { - pubkey: accounts.rentPayer, + pubkey: accounts.vaultTransactionCreateItemRentPayer, isWritable: true, isSigner: true, }, { - pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + pubkey: accounts.vaultTransactionCreateItemSystemProgram, isWritable: false, isSigner: false, }, + { + pubkey: accounts.transactionBuffer, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.creator, + isWritable: true, + isSigner: true, + }, ] if (accounts.anchorRemainingAccounts != null) { diff --git a/tests/index.ts b/tests/index.ts index 725fd878..5ea50433 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,4 +1,4 @@ -// // The order of imports is the order the test suite will run in. +// The order of imports is the order the test suite will run in. import "./suites/program-config-init" import "./suites/account-migrations"; import "./suites/examples/batch-sol-transfer"; @@ -20,7 +20,7 @@ import "./suites/instructions/vaultBatchTransactionAccountClose"; import "./suites/instructions/vaultTransactionAccountsClose"; import "./suites/instructions/vaultTransactionCreateFromBuffer"; import "./suites/multisig-sdk"; -import "./suites/program-config-init"; + // // Uncomment to enable the heapTest instruction testing // //import "./suites/instructions/heapTest"; // import "./suites/examples/custom-heap"; diff --git a/tests/suites/examples/transaction-buffer.ts b/tests/suites/examples/transaction-buffer.ts index 09c04749..c7ac7962 100644 --- a/tests/suites/examples/transaction-buffer.ts +++ b/tests/suites/examples/transaction-buffer.ts @@ -235,12 +235,13 @@ describe("Examples / Transaction Buffers", () => { const thirdIx = multisig.generated.createVaultTransactionCreateFromBufferInstruction( { - multisig: multisigPda, - transaction: transactionPda, + vaultTransactionCreateItemMultisig: multisigPda, + vaultTransactionCreateItemTransaction: transactionPda, + vaultTransactionCreateItemCreator: members.almighty.publicKey, + vaultTransactionCreateItemRentPayer: members.almighty.publicKey, + vaultTransactionCreateItemSystemProgram: SystemProgram.programId, creator: members.almighty.publicKey, - rentPayer: members.almighty.publicKey, - systemProgram: SystemProgram.programId, - anchorRemainingAccounts: [transactionBufferMeta] + transactionBuffer: transactionBuffer, }, { args: { diff --git a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts index 6e6dc813..e8a37db8 100644 --- a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts +++ b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts @@ -264,12 +264,13 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { const thirdIx = multisig.generated.createVaultTransactionCreateFromBufferInstruction( { - multisig: multisigPda, - transaction: transactionPda, + vaultTransactionCreateItemMultisig: multisigPda, + vaultTransactionCreateItemTransaction: transactionPda, + vaultTransactionCreateItemCreator: members.proposer.publicKey, + vaultTransactionCreateItemRentPayer: members.proposer.publicKey, + vaultTransactionCreateItemSystemProgram: SystemProgram.programId, creator: members.proposer.publicKey, - rentPayer: members.proposer.publicKey, - systemProgram: SystemProgram.programId, - anchorRemainingAccounts: [transactionBufferMeta] + transactionBuffer: transactionBuffer, }, { args: { @@ -402,12 +403,13 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { const createFromBufferIx = multisig.generated.createVaultTransactionCreateFromBufferInstruction( { - multisig: multisigPda, - transaction: transactionPda, + vaultTransactionCreateItemMultisig: multisigPda, + vaultTransactionCreateItemTransaction: transactionPda, + vaultTransactionCreateItemCreator: members.proposer.publicKey, + vaultTransactionCreateItemRentPayer: members.proposer.publicKey, + vaultTransactionCreateItemSystemProgram: SystemProgram.programId, creator: members.proposer.publicKey, - rentPayer: members.proposer.publicKey, - systemProgram: SystemProgram.programId, - anchorRemainingAccounts: [transactionBufferMeta] + transactionBuffer: transactionBuffer, }, { args: { From 5ab4abc456b9ec645b58f99190535e60d91d6d0c Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:19:54 +0800 Subject: [PATCH 16/47] add: comments --- .../vault_transaction_create_from_buffer.rs | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs index 99d09bf2..44140935 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -4,9 +4,9 @@ use crate::errors::*; use crate::instructions::*; use crate::state::*; - #[derive(Accounts)] pub struct VaultTransactionCreateFromBuffer<'info> { + // The context needed for the VaultTransactionCreate instruction pub vault_transaction_create: VaultTransactionCreate<'info>, #[account( @@ -27,7 +27,7 @@ pub struct VaultTransactionCreateFromBuffer<'info> { pub transaction_buffer: Box>, // Anchor doesn't allow us to use the creator inside - // vault_transaction_create, so just re-passing it here + // vault_transaction_create, so we just re-pass it here with the constraint #[account( mut, address = vault_transaction_create.creator.key(), @@ -58,18 +58,27 @@ impl<'info> VaultTransactionCreateFromBuffer<'info> { ctx: Context<'_, '_, 'info, 'info, Self>, args: VaultTransactionCreateArgs, ) -> Result<()> { + // Account infos necessary for reallocation + let vault_transaction_account_info = &ctx + .accounts + .vault_transaction_create + .transaction + .to_account_info(); + let rent_payer_account_info = &ctx + .accounts + .vault_transaction_create + .rent_payer + .to_account_info(); + + // Read-only accounts let transaction_buffer = &ctx.accounts.transaction_buffer; - let vault_transaction_account_info = &ctx.accounts.vault_transaction_create.transaction.to_account_info(); - let rent_payer_account_info = &ctx.accounts.vault_transaction_create.rent_payer.to_account_info(); - - // Calculate the new required length of the vault transaction account - let new_len = VaultTransaction::size( - args.ephemeral_signers, - transaction_buffer.buffer.as_slice(), - )?; + // Calculate the new required length of the vault transaction account, + // since it was initialized with an empty transaction message + let new_len = + VaultTransaction::size(args.ephemeral_signers, transaction_buffer.buffer.as_slice())?; - // Calculate the rent exemption + // Calculate the rent exemption for new length let rent_exempt_lamports = Rent::get().unwrap().minimum_balance(new_len).max(1); // Check the difference between the rent exemption and the current lamports @@ -80,13 +89,13 @@ impl<'info> VaultTransactionCreateFromBuffer<'info> { **rent_payer_account_info.try_borrow_mut_lamports()? -= top_up_lamports; **vault_transaction_account_info.try_borrow_mut_lamports()? += top_up_lamports; - // Reallocate the vault transaction account to the new length of the transaction message + // Reallocate the vault transaction account to the new length of the + // actual transaction message AccountInfo::realloc(&vault_transaction_account_info, new_len, true)?; - // Create the args for the vault transaction create instruction let create_args = VaultTransactionCreateArgs { - vault_index: transaction_buffer.vault_index, + vault_index: args.vault_index, ephemeral_signers: args.ephemeral_signers, transaction_message: transaction_buffer.buffer.clone(), memo: args.memo, @@ -101,6 +110,7 @@ impl<'info> VaultTransactionCreateFromBuffer<'info> { // Call the vault transaction create instruction VaultTransactionCreate::vault_transaction_create(context, create_args)?; + Ok(()) } } From c173a71066b055d8a55c139a441166be2c8b9546 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:20:17 +0800 Subject: [PATCH 17/47] chore: fmt --- .../src/instructions/vault_transaction_create.rs | 1 - .../src/instructions/vault_transaction_create_from_buffer.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs index ae265c38..b7c8f22e 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create.rs @@ -130,7 +130,6 @@ impl<'info> VaultTransactionCreate<'info> { Ok(()) } - } /// Unvalidated instruction data, must be treated as untrusted. diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs index 44140935..12346ca4 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -110,7 +110,7 @@ impl<'info> VaultTransactionCreateFromBuffer<'info> { // Call the vault transaction create instruction VaultTransactionCreate::vault_transaction_create(context, create_args)?; - + Ok(()) } } From abff4458924415d0ebcb315246b1693754750455 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Thu, 12 Sep 2024 01:29:18 +0800 Subject: [PATCH 18/47] refactor: wrap ProposalVote context inside ProposalCancelV2 --- .../src/instructions/proposal_vote.rs | 82 +++++-------------- .../vault_transaction_create_from_buffer.rs | 4 +- programs/squads_multisig_program/src/lib.rs | 4 +- sdk/multisig/idl/squads_multisig_program.json | 31 ++++--- .../instructions/proposalCancelV2.ts | 18 ++-- .../src/instructions/proposalCancelV2.ts | 6 +- 6 files changed, 53 insertions(+), 92 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/proposal_vote.rs b/programs/squads_multisig_program/src/instructions/proposal_vote.rs index e2d26065..667a0ce0 100644 --- a/programs/squads_multisig_program/src/instructions/proposal_vote.rs +++ b/programs/squads_multisig_program/src/instructions/proposal_vote.rs @@ -35,27 +35,8 @@ pub struct ProposalVote<'info> { #[derive(Accounts)] pub struct ProposalCancel<'info> { - #[account( - seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()], - bump = multisig.bump, - )] - pub multisig: Account<'info, Multisig>, - - #[account(mut)] - pub member: Signer<'info>, - - #[account( - mut, - seeds = [ - SEED_PREFIX, - multisig.key().as_ref(), - SEED_TRANSACTION, - &proposal.transaction_index.to_le_bytes(), - SEED_PROPOSAL, - ], - bump = proposal.bump, - )] - pub proposal: Account<'info, Proposal>, + // The context needed for the ProposalVote instruction + pub proposal_vote: ProposalVote<'info>, pub system_program: Program<'info, System>, } @@ -150,56 +131,31 @@ impl ProposalVote<'_> { } } -impl ProposalCancel<'_> { - fn validate(&self) -> Result<()> { - let Self { - multisig, - proposal, - member, - .. - } = self; - - // member - require!( - multisig.is_member(member.key()).is_some(), - MultisigError::NotAMember - ); - require!( - multisig.member_has_permission(member.key(), Permission::Vote), - MultisigError::Unauthorized - ); - - require!( - matches!(proposal.status, ProposalStatus::Approved { .. }), - MultisigError::InvalidProposalStatus - ); - // CAN cancel a stale proposal. - - Ok(()) - } +impl<'info> ProposalCancel<'info> { /// Cancel a multisig proposal on behalf of the `member`. /// The proposal must be `Approved`. - #[access_control(ctx.accounts.validate())] - pub fn proposal_cancel(ctx: Context, _args: ProposalVoteArgs) -> Result<()> { - let multisig = &mut ctx.accounts.multisig; - let proposal = &mut ctx.accounts.proposal; - let member = &mut ctx.accounts.member; - let system_program = &ctx.accounts.system_program; + pub fn proposal_cancel(ctx: Context<'_, '_, 'info, 'info, Self>, _args: ProposalVoteArgs) -> Result<()> { + // Readonly accounts + let multisig = &ctx.accounts.proposal_vote.multisig.clone(); - // ensure that the cancel array contains no keys that are not currently members - proposal - .cancelled - .retain(|k| multisig.is_member(*k).is_some()); + // Account infos necessary for reallocation + let proposal_account_info = &ctx.accounts.proposal_vote.proposal.to_account_info(); + let member_account_info = &ctx.accounts.proposal_vote.member.to_account_info(); + let system_program_account_info = &ctx.accounts.system_program.to_account_info(); - proposal.cancel(member.key(), usize::from(multisig.threshold))?; + // Create context for cancel instruction + let cancel_context = Context::new(ctx.program_id, &mut ctx.accounts.proposal_vote, ctx.remaining_accounts, ctx.bumps.proposal_vote); + + // Call cancel instruction + ProposalVote::proposal_cancel(cancel_context, _args)?; - // reallocate the proposal size if needed + // Reallocate the proposal size if needed Proposal::realloc_if_needed( - proposal.to_account_info(), + proposal_account_info.clone(), multisig.members.len(), - Some(member.to_account_info()), - Some(system_program.to_account_info()), + Some(member_account_info.clone()), + Some(system_program_account_info.clone()), )?; Ok(()) } diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs index 12346ca4..de035c74 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -52,9 +52,9 @@ impl<'info> VaultTransactionCreateFromBuffer<'info> { transaction_buffer_account.validate_size()?; Ok(()) } - + // Create a new vault transaction from a completed transaction buffer account. #[access_control(ctx.accounts.validate(&args))] - pub fn handler( + pub fn vault_transaction_create_from_buffer( ctx: Context<'_, '_, 'info, 'info, Self>, args: VaultTransactionCreateArgs, ) -> Result<()> { diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index 901823ed..09af03ce 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -203,7 +203,7 @@ pub mod squads_multisig_program { ctx: Context<'_, '_, 'info, 'info, VaultTransactionCreateFromBuffer<'info>>, args: VaultTransactionCreateArgs, ) -> Result<()> { - VaultTransactionCreateFromBuffer::handler(ctx, args) + VaultTransactionCreateFromBuffer::vault_transaction_create_from_buffer(ctx, args) } /// Execute a vault transaction. @@ -265,7 +265,7 @@ pub mod squads_multisig_program { /// accommodate the new amount of cancel votes. /// The previous implemenation still works if the proposal size is in line with the /// threshold size. - pub fn proposal_cancel_v2(ctx: Context, args: ProposalVoteArgs) -> Result<()> { + pub fn proposal_cancel_v2<'info>(ctx: Context<'_, '_, 'info, 'info, ProposalCancel<'info>>, args: ProposalVoteArgs) -> Result<()> { ProposalCancel::proposal_cancel(ctx, args) } diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index 7c7b45c0..609af796 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -1302,19 +1302,24 @@ ], "accounts": [ { - "name": "multisig", - "isMut": false, - "isSigner": false - }, - { - "name": "member", - "isMut": true, - "isSigner": true - }, - { - "name": "proposal", - "isMut": true, - "isSigner": false + "name": "proposalVote", + "accounts": [ + { + "name": "multisig", + "isMut": false, + "isSigner": false + }, + { + "name": "member", + "isMut": true, + "isSigner": true + }, + { + "name": "proposal", + "isMut": true, + "isSigner": false + } + ] }, { "name": "systemProgram", diff --git a/sdk/multisig/src/generated/instructions/proposalCancelV2.ts b/sdk/multisig/src/generated/instructions/proposalCancelV2.ts index 1b3d57d2..0be313c6 100644 --- a/sdk/multisig/src/generated/instructions/proposalCancelV2.ts +++ b/sdk/multisig/src/generated/instructions/proposalCancelV2.ts @@ -39,17 +39,17 @@ export const proposalCancelV2Struct = new beet.FixableBeetArgsStruct< /** * Accounts required by the _proposalCancelV2_ instruction * - * @property [] multisig - * @property [_writable_, **signer**] member - * @property [_writable_] proposal + * @property [] proposalVoteItemMultisig + * @property [_writable_, **signer**] proposalVoteItemMember + * @property [_writable_] proposalVoteItemProposal * @category Instructions * @category ProposalCancelV2 * @category generated */ export type ProposalCancelV2InstructionAccounts = { - multisig: web3.PublicKey - member: web3.PublicKey - proposal: web3.PublicKey + proposalVoteItemMultisig: web3.PublicKey + proposalVoteItemMember: web3.PublicKey + proposalVoteItemProposal: web3.PublicKey systemProgram?: web3.PublicKey anchorRemainingAccounts?: web3.AccountMeta[] } @@ -79,17 +79,17 @@ export function createProposalCancelV2Instruction( }) const keys: web3.AccountMeta[] = [ { - pubkey: accounts.multisig, + pubkey: accounts.proposalVoteItemMultisig, isWritable: false, isSigner: false, }, { - pubkey: accounts.member, + pubkey: accounts.proposalVoteItemMember, isWritable: true, isSigner: true, }, { - pubkey: accounts.proposal, + pubkey: accounts.proposalVoteItemProposal, isWritable: true, isSigner: false, }, diff --git a/sdk/multisig/src/instructions/proposalCancelV2.ts b/sdk/multisig/src/instructions/proposalCancelV2.ts index a10c5728..205f3a22 100644 --- a/sdk/multisig/src/instructions/proposalCancelV2.ts +++ b/sdk/multisig/src/instructions/proposalCancelV2.ts @@ -1,6 +1,6 @@ -import { getProposalPda } from "../pda"; -import { createProposalCancelV2Instruction, PROGRAM_ID } from "../generated"; import { PublicKey } from "@solana/web3.js"; +import { createProposalCancelV2Instruction, PROGRAM_ID } from "../generated"; +import { getProposalPda } from "../pda"; export function proposalCancelV2({ multisigPda, @@ -22,7 +22,7 @@ export function proposalCancelV2({ }); return createProposalCancelV2Instruction( - { multisig: multisigPda, proposal: proposalPda, member }, + { proposalVoteItemMultisig: multisigPda, proposalVoteItemProposal: proposalPda, proposalVoteItemMember: member }, { args: { memo: memo ?? null } }, programId ); From 305711624cf4a90edfc777553442040e867d9f30 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Thu, 12 Sep 2024 01:30:00 +0800 Subject: [PATCH 19/47] fix: typo --- .../src/instructions/vault_transaction_create_from_buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs index de035c74..374f98bf 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -52,7 +52,7 @@ impl<'info> VaultTransactionCreateFromBuffer<'info> { transaction_buffer_account.validate_size()?; Ok(()) } - // Create a new vault transaction from a completed transaction buffer account. + /// Create a new vault transaction from a completed transaction buffer account. #[access_control(ctx.accounts.validate(&args))] pub fn vault_transaction_create_from_buffer( ctx: Context<'_, '_, 'info, 'info, Self>, From 4a23a3d741921c6cf6d177a21dc156b40aec58a1 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:00:03 +0800 Subject: [PATCH 20/47] fix: comments from certora preliminary findings --- .../src/instructions/transaction_buffer_close.rs | 2 +- programs/squads_multisig_program/src/state/spending_limit.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs index ea1bf37c..cde577f1 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs @@ -47,7 +47,7 @@ impl TransactionBufferClose<'_> { Ok(()) } - /// Create a new vault transaction. + /// Close a transaction buffer account. #[access_control(ctx.accounts.validate())] pub fn transaction_buffer_close(ctx: Context) -> Result<()> { Ok(()) diff --git a/programs/squads_multisig_program/src/state/spending_limit.rs b/programs/squads_multisig_program/src/state/spending_limit.rs index f68894a0..c7567115 100644 --- a/programs/squads_multisig_program/src/state/spending_limit.rs +++ b/programs/squads_multisig_program/src/state/spending_limit.rs @@ -66,7 +66,7 @@ impl SpendingLimit { } pub fn invariant(&self) -> Result<()> { - // Amount must be positive. + // Amount must be a non-zero value. require_neq!(self.amount, 0, MultisigError::SpendingLimitInvalidAmount); require!(!self.members.is_empty(), MultisigError::EmptyMembers); From bfd31d845032c8302a6b0b195d28c7c760555748 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:08:42 +0800 Subject: [PATCH 21/47] fix: transaction_buffer_close seed constraints --- .../src/instructions/transaction_buffer_close.rs | 4 +++- .../src/instructions/transaction_buffer_extend.rs | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs index ea1bf37c..9f1fa76d 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs @@ -18,11 +18,13 @@ pub struct TransactionBufferClose<'info> { close = creator, // Only the creator can close the buffer constraint = transaction_buffer.creator == creator.key() @ MultisigError::Unauthorized, + // Account can be closed anytime by the creator, regardless of the + // current multisig transaction index seeds = [ SEED_PREFIX, multisig.key().as_ref(), SEED_TRANSACTION_BUFFER, - &multisig.transaction_index.checked_add(1).unwrap().to_le_bytes(), + &transaction_buffer.transaction_index.to_le_bytes(), ], bump )] diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs index 7fcbd6b1..0ea56193 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs @@ -23,6 +23,8 @@ pub struct TransactionBufferExtend<'info> { mut, // Only the creator can extend the buffer constraint = transaction_buffer.creator == creator.key() @ MultisigError::Unauthorized, + // Extending the buffer only work if it still represents the next + // transaction index in the multisig seeds = [ SEED_PREFIX, multisig.key().as_ref(), From 7b7c0c5d1aa39df7cdd7c94dd2d95148bd90a902 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:14:19 +0900 Subject: [PATCH 22/47] fix: confusing semantics --- .../src/instructions/proposal_vote.rs | 6 +++--- programs/squads_multisig_program/src/lib.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/proposal_vote.rs b/programs/squads_multisig_program/src/instructions/proposal_vote.rs index 667a0ce0..a00dec3b 100644 --- a/programs/squads_multisig_program/src/instructions/proposal_vote.rs +++ b/programs/squads_multisig_program/src/instructions/proposal_vote.rs @@ -34,7 +34,7 @@ pub struct ProposalVote<'info> { } #[derive(Accounts)] -pub struct ProposalCancel<'info> { +pub struct ProposalCancelV2<'info> { // The context needed for the ProposalVote instruction pub proposal_vote: ProposalVote<'info>, @@ -131,11 +131,11 @@ impl ProposalVote<'_> { } } -impl<'info> ProposalCancel<'info> { +impl<'info> ProposalCancelV2<'info> { /// Cancel a multisig proposal on behalf of the `member`. /// The proposal must be `Approved`. - pub fn proposal_cancel(ctx: Context<'_, '_, 'info, 'info, Self>, _args: ProposalVoteArgs) -> Result<()> { + pub fn proposal_cancel_v2(ctx: Context<'_, '_, 'info, 'info, Self>, _args: ProposalVoteArgs) -> Result<()> { // Readonly accounts let multisig = &ctx.accounts.proposal_vote.multisig.clone(); diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index 09af03ce..8be658b0 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -265,8 +265,8 @@ pub mod squads_multisig_program { /// accommodate the new amount of cancel votes. /// The previous implemenation still works if the proposal size is in line with the /// threshold size. - pub fn proposal_cancel_v2<'info>(ctx: Context<'_, '_, 'info, 'info, ProposalCancel<'info>>, args: ProposalVoteArgs) -> Result<()> { - ProposalCancel::proposal_cancel(ctx, args) + pub fn proposal_cancel_v2<'info>(ctx: Context<'_, '_, 'info, 'info, ProposalCancelV2<'info>>, args: ProposalVoteArgs) -> Result<()> { + ProposalCancelV2::proposal_cancel_v2(ctx, args) } /// Use a spending limit to transfer tokens from a multisig vault to a destination account. From 17983bc2eb2314f704580046caf52f6e5bc6a315 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:23:29 +0900 Subject: [PATCH 23/47] fix: buffer vec size --- .../squads_multisig_program/src/state/transaction_buffer.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/programs/squads_multisig_program/src/state/transaction_buffer.rs b/programs/squads_multisig_program/src/state/transaction_buffer.rs index caea14e5..2479a0f1 100644 --- a/programs/squads_multisig_program/src/state/transaction_buffer.rs +++ b/programs/squads_multisig_program/src/state/transaction_buffer.rs @@ -3,9 +3,6 @@ use anchor_lang::solana_program::hash::hash; use crate::errors::MultisigError; -// Since VaultTransaction doesn't implement zero-copy, we are limited to -// deserializing the account onto the Stack. This means we are limited to a -// theoretical max size of 4KiB pub const MAX_BUFFER_SIZE: usize = 4000; #[account] @@ -41,7 +38,8 @@ impl TransactionBuffer { 8 + // transaction_index 32 + // transaction_message_hash 2 + // final_buffer_size - final_message_buffer_size as usize, // transaction_message + 4 + // vec length bytes + final_message_buffer_size as usize, // buffer ) } From 7596c1a40e4878181370b8b4dfa0cc938965d35f Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Tue, 1 Oct 2024 00:24:24 +0900 Subject: [PATCH 24/47] small comment fixes --- .../src/instructions/vault_transaction_create_from_buffer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs index 374f98bf..9f85fcb5 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -26,8 +26,8 @@ pub struct VaultTransactionCreateFromBuffer<'info> { )] pub transaction_buffer: Box>, - // Anchor doesn't allow us to use the creator inside - // vault_transaction_create, so we just re-pass it here with the constraint + // Anchor doesn't allow us to use the creator inside of + // vault_transaction_create, so we just re-pass it here with the same constraint #[account( mut, address = vault_transaction_create.creator.key(), From 0996f21b1bec77c73dc1c2be44a37ca62c4af504 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:03:15 +0200 Subject: [PATCH 25/47] refactor: lamport mutation into system transfer --- .../vault_transaction_create_from_buffer.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs index 9f85fcb5..388085e4 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -1,8 +1,7 @@ -use anchor_lang::prelude::*; - use crate::errors::*; use crate::instructions::*; use crate::state::*; +use anchor_lang::{prelude::*, system_program}; #[derive(Accounts)] pub struct VaultTransactionCreateFromBuffer<'info> { @@ -70,6 +69,8 @@ impl<'info> VaultTransactionCreateFromBuffer<'info> { .rent_payer .to_account_info(); + let system_program = &ctx.accounts.vault_transaction_create.system_program.to_account_info(); + // Read-only accounts let transaction_buffer = &ctx.accounts.transaction_buffer; @@ -85,9 +86,15 @@ impl<'info> VaultTransactionCreateFromBuffer<'info> { let top_up_lamports = rent_exempt_lamports.saturating_sub(vault_transaction_account_info.lamports()); - // Top up the account with the difference, paid by the rent payer - **rent_payer_account_info.try_borrow_mut_lamports()? -= top_up_lamports; - **vault_transaction_account_info.try_borrow_mut_lamports()? += top_up_lamports; + // System Transfer the remaining difference to the vault transaction account + let transfer_context = CpiContext::new( + system_program.to_account_info(), + system_program::Transfer { + from: rent_payer_account_info.clone(), + to: vault_transaction_account_info.clone(), + }, + ); + system_program::transfer(transfer_context, top_up_lamports)?; // Reallocate the vault transaction account to the new length of the // actual transaction message From ca85338f6cfcfd619a1b4f5570f53d3d6d2d4335 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:09:08 +0200 Subject: [PATCH 26/47] feat: u8 for buffer seeding --- .../instructions/transaction_buffer_close.rs | 2 +- .../instructions/transaction_buffer_create.rs | 10 +- .../instructions/transaction_buffer_extend.rs | 4 +- .../vault_transaction_create_from_buffer.rs | 6 +- .../src/state/transaction_buffer.rs | 6 +- sdk/multisig/idl/squads_multisig_program.json | 17 +- .../generated/accounts/TransactionBuffer.ts | 22 +- .../types/TransactionBufferCreateArgs.ts | 2 + tests/suites/examples/transaction-buffer.ts | 6 +- .../instructions/transactionBufferClose.ts | 12 +- .../instructions/transactionBufferCreate.ts | 770 ++++++++++-------- .../instructions/transactionBufferExtend.ts | 8 +- .../vaultTransactionCreateFromBuffer.ts | 23 +- 13 files changed, 504 insertions(+), 384 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs index c1b946bd..9d28f3ef 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs @@ -24,7 +24,7 @@ pub struct TransactionBufferClose<'info> { SEED_PREFIX, multisig.key().as_ref(), SEED_TRANSACTION_BUFFER, - &transaction_buffer.transaction_index.to_le_bytes(), + &transaction_buffer.buffer_index.to_le_bytes() ], bump )] diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs index ff216674..2201b9a3 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs @@ -6,6 +6,8 @@ use crate::state::*; #[derive(AnchorSerialize, AnchorDeserialize)] pub struct TransactionBufferCreateArgs { + /// Index of the buffer account to seed the account derivation + pub buffer_index: u8, /// Index of the vault this transaction belongs to. pub vault_index: u8, /// Hash of the final assembled transaction message. @@ -34,7 +36,7 @@ pub struct TransactionBufferCreate<'info> { SEED_PREFIX, multisig.key().as_ref(), SEED_TRANSACTION_BUFFER, - &multisig.transaction_index.checked_add(1).unwrap().to_le_bytes(), + &args.buffer_index.to_le_bytes(), ], bump )] @@ -88,14 +90,14 @@ impl TransactionBufferCreate<'_> { let multisig = &ctx.accounts.multisig; let creator = &mut ctx.accounts.creator; - // Get the transaction index. - let transaction_index = multisig.transaction_index.checked_add(1).unwrap(); + // Get the buffer index. + let buffer_index = args.buffer_index; // Initialize the transaction fields. transaction_buffer.multisig = multisig.key(); transaction_buffer.creator = creator.key(); transaction_buffer.vault_index = args.vault_index; - transaction_buffer.transaction_index = transaction_index; + transaction_buffer.buffer_index = buffer_index; transaction_buffer.final_buffer_hash = args.final_buffer_hash; transaction_buffer.final_buffer_size = args.final_buffer_size; transaction_buffer.buffer = args.buffer; diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs index 0ea56193..84d6c43c 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs @@ -23,13 +23,11 @@ pub struct TransactionBufferExtend<'info> { mut, // Only the creator can extend the buffer constraint = transaction_buffer.creator == creator.key() @ MultisigError::Unauthorized, - // Extending the buffer only work if it still represents the next - // transaction index in the multisig seeds = [ SEED_PREFIX, multisig.key().as_ref(), SEED_TRANSACTION_BUFFER, - &multisig.transaction_index.checked_add(1).unwrap().to_le_bytes(), + &transaction_buffer.buffer_index.to_le_bytes() ], bump )] diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs index 388085e4..b9c5ab9f 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -15,11 +15,7 @@ pub struct VaultTransactionCreateFromBuffer<'info> { SEED_PREFIX, vault_transaction_create.multisig.key().as_ref(), SEED_TRANSACTION_BUFFER, - &vault_transaction_create.multisig - .transaction_index - .checked_add(1) - .unwrap() - .to_le_bytes(), + &transaction_buffer.buffer_index.to_le_bytes(), ], bump )] diff --git a/programs/squads_multisig_program/src/state/transaction_buffer.rs b/programs/squads_multisig_program/src/state/transaction_buffer.rs index 2479a0f1..2ed6b608 100644 --- a/programs/squads_multisig_program/src/state/transaction_buffer.rs +++ b/programs/squads_multisig_program/src/state/transaction_buffer.rs @@ -12,10 +12,10 @@ pub struct TransactionBuffer { pub multisig: Pubkey, /// Member of the Multisig who created the TransactionBuffer. pub creator: Pubkey, + /// Index to seed address derivation + pub buffer_index: u8, /// Vault index of the transaction this buffer belongs to. pub vault_index: u8, - /// Index of the transaction this buffer belongs to. - pub transaction_index: u64, /// Hash of the final assembled transaction message. pub final_buffer_hash: [u8; 32], /// The size of the final assembled transaction message. @@ -34,8 +34,8 @@ impl TransactionBuffer { 8 + // anchor account discriminator 32 + // multisig 32 + // creator + 8 + // buffer_index 8 + // vault_index - 8 + // transaction_index 32 + // transaction_message_hash 2 + // final_buffer_size 4 + // vec length bytes diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index 609af796..fed3c6fb 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -2113,18 +2113,18 @@ "type": "publicKey" }, { - "name": "vaultIndex", + "name": "bufferIndex", "docs": [ - "Vault index of the transaction this buffer belongs to." + "Index to seed address derivation" ], "type": "u8" }, { - "name": "transactionIndex", + "name": "vaultIndex", "docs": [ - "Index of the transaction this buffer belongs to." + "Vault index of the transaction this buffer belongs to." ], - "type": "u64" + "type": "u8" }, { "name": "finalBufferHash", @@ -2771,6 +2771,13 @@ "type": { "kind": "struct", "fields": [ + { + "name": "bufferIndex", + "docs": [ + "Index of the buffer account to seed the account derivation" + ], + "type": "u8" + }, { "name": "vaultIndex", "docs": [ diff --git a/sdk/multisig/src/generated/accounts/TransactionBuffer.ts b/sdk/multisig/src/generated/accounts/TransactionBuffer.ts index 7b5fba60..aa2e1f2b 100644 --- a/sdk/multisig/src/generated/accounts/TransactionBuffer.ts +++ b/sdk/multisig/src/generated/accounts/TransactionBuffer.ts @@ -6,8 +6,8 @@ */ import * as web3 from '@solana/web3.js' -import * as beet from '@metaplex-foundation/beet' import * as beetSolana from '@metaplex-foundation/beet-solana' +import * as beet from '@metaplex-foundation/beet' /** * Arguments used to create {@link TransactionBuffer} @@ -17,8 +17,8 @@ import * as beetSolana from '@metaplex-foundation/beet-solana' export type TransactionBufferArgs = { multisig: web3.PublicKey creator: web3.PublicKey + bufferIndex: number vaultIndex: number - transactionIndex: beet.bignum finalBufferHash: number[] /* size: 32 */ finalBufferSize: number buffer: Uint8Array @@ -38,8 +38,8 @@ export class TransactionBuffer implements TransactionBufferArgs { private constructor( readonly multisig: web3.PublicKey, readonly creator: web3.PublicKey, + readonly bufferIndex: number, readonly vaultIndex: number, - readonly transactionIndex: beet.bignum, readonly finalBufferHash: number[] /* size: 32 */, readonly finalBufferSize: number, readonly buffer: Uint8Array @@ -52,8 +52,8 @@ export class TransactionBuffer implements TransactionBufferArgs { return new TransactionBuffer( args.multisig, args.creator, + args.bufferIndex, args.vaultIndex, - args.transactionIndex, args.finalBufferHash, args.finalBufferSize, args.buffer @@ -167,18 +167,8 @@ export class TransactionBuffer implements TransactionBufferArgs { return { multisig: this.multisig.toBase58(), creator: this.creator.toBase58(), + bufferIndex: this.bufferIndex, vaultIndex: this.vaultIndex, - transactionIndex: (() => { - const x = <{ toNumber: () => number }>this.transactionIndex - if (typeof x.toNumber === 'function') { - try { - return x.toNumber() - } catch (_) { - return x - } - } - return x - })(), finalBufferHash: this.finalBufferHash, finalBufferSize: this.finalBufferSize, buffer: this.buffer, @@ -200,8 +190,8 @@ export const transactionBufferBeet = new beet.FixableBeetStruct< ['accountDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], ['multisig', beetSolana.publicKey], ['creator', beetSolana.publicKey], + ['bufferIndex', beet.u8], ['vaultIndex', beet.u8], - ['transactionIndex', beet.u64], ['finalBufferHash', beet.uniformFixedSizeArray(beet.u8, 32)], ['finalBufferSize', beet.u16], ['buffer', beet.bytes], diff --git a/sdk/multisig/src/generated/types/TransactionBufferCreateArgs.ts b/sdk/multisig/src/generated/types/TransactionBufferCreateArgs.ts index 480424d2..7c34fd6c 100644 --- a/sdk/multisig/src/generated/types/TransactionBufferCreateArgs.ts +++ b/sdk/multisig/src/generated/types/TransactionBufferCreateArgs.ts @@ -7,6 +7,7 @@ import * as beet from '@metaplex-foundation/beet' export type TransactionBufferCreateArgs = { + bufferIndex: number vaultIndex: number finalBufferHash: number[] /* size: 32 */ finalBufferSize: number @@ -20,6 +21,7 @@ export type TransactionBufferCreateArgs = { export const transactionBufferCreateArgsBeet = new beet.FixableBeetArgsStruct( [ + ['bufferIndex', beet.u8], ['vaultIndex', beet.u8], ['finalBufferHash', beet.uniformFixedSizeArray(beet.u8, 32)], ['finalBufferSize', beet.u16], diff --git a/tests/suites/examples/transaction-buffer.ts b/tests/suites/examples/transaction-buffer.ts index c7ac7962..156424fe 100644 --- a/tests/suites/examples/transaction-buffer.ts +++ b/tests/suites/examples/transaction-buffer.ts @@ -18,7 +18,6 @@ import { VaultTransactionCreateFromBufferInstructionArgs, } from "@sqds/multisig/lib/generated"; import assert from "assert"; -import { BN } from "bn.js"; import * as crypto from "crypto"; import { TestMembers, @@ -74,6 +73,7 @@ describe("Examples / Transaction Buffers", () => { it("set buffer, extend, and create", async () => { const transactionIndex = 1n; + const bufferIndex = 0; const testIx = createTestTransferInstruction(vaultPda, vaultPda, 1); @@ -103,7 +103,7 @@ describe("Examples / Transaction Buffers", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), + Buffer.from([bufferIndex]) ], programId ); @@ -122,10 +122,10 @@ describe("Examples / Transaction Buffers", () => { transactionBuffer, creator: members.almighty.publicKey, rentPayer: members.almighty.publicKey, - systemProgram: SystemProgram.programId, }, { args: { + bufferIndex: bufferIndex, vaultIndex: 0, // Must be a SHA256 hash of the message buffer. finalBufferHash: Array.from(messageHash), diff --git a/tests/suites/instructions/transactionBufferClose.ts b/tests/suites/instructions/transactionBufferClose.ts index d9e6dce5..e11a666d 100644 --- a/tests/suites/instructions/transactionBufferClose.ts +++ b/tests/suites/instructions/transactionBufferClose.ts @@ -32,11 +32,11 @@ describe("Instructions / transaction_buffer_close", () => { let vaultPda: PublicKey; let transactionBuffer: PublicKey; - const createKey = Keypair.generate(); before(async () => { members = await generateMultisigMembers(connection); + const createKey = Keypair.generate(); multisigPda = (await createAutonomousMultisigV2({ connection, createKey, @@ -59,7 +59,7 @@ describe("Instructions / transaction_buffer_close", () => { ); await connection.confirmTransaction(signature); - const transactionIndex = 1n; + const bufferIndex = 0; const testIx = await createTestTransferInstruction( vaultPda, Keypair.generate().publicKey, @@ -78,16 +78,15 @@ describe("Instructions / transaction_buffer_close", () => { vaultPda, }); - [transactionBuffer] = await PublicKey.findProgramAddressSync( + [transactionBuffer] = PublicKey.findProgramAddressSync( [ Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), + Uint8Array.from([bufferIndex]) ], programId ); - const messageHash = crypto .createHash("sha256") .update(messageBuffer) @@ -103,6 +102,7 @@ describe("Instructions / transaction_buffer_close", () => { }, { args: { + bufferIndex: Number(bufferIndex), vaultIndex: 0, finalBufferHash: Array.from(messageHash), finalBufferSize: messageBuffer.length, @@ -121,7 +121,7 @@ describe("Instructions / transaction_buffer_close", () => { const createTx = new VersionedTransaction(createMessage); createTx.sign([members.proposer]); - const createSig = await connection.sendTransaction(createTx, { skipPreflight: true }); + const createSig = await connection.sendRawTransaction(createTx.serialize(), { skipPreflight: true }); await connection.confirmTransaction(createSig); }); diff --git a/tests/suites/instructions/transactionBufferCreate.ts b/tests/suites/instructions/transactionBufferCreate.ts index 19abca29..a185203b 100644 --- a/tests/suites/instructions/transactionBufferCreate.ts +++ b/tests/suites/instructions/transactionBufferCreate.ts @@ -2,12 +2,17 @@ import { Keypair, LAMPORTS_PER_SOL, PublicKey, + SystemProgram, TransactionMessage, VersionedTransaction, - SystemProgram, } from "@solana/web3.js"; import * as multisig from "@sqds/multisig"; +import { + TransactionBufferCreateArgs, + TransactionBufferCreateInstructionArgs, +} from "@sqds/multisig/lib/generated"; import assert from "assert"; +import * as crypto from "crypto"; import { createAutonomousMultisigV2, createLocalhostConnection, @@ -16,12 +21,6 @@ import { getTestProgramId, TestMembers, } from "../../utils"; -import { BN } from "bn.js"; -import { - TransactionBufferCreateArgs, - TransactionBufferCreateInstructionArgs, -} from "@sqds/multisig/lib/generated"; -import * as crypto from "crypto"; const programId = getTestProgramId(); const connection = createLocalhostConnection(); @@ -68,7 +67,7 @@ describe("Instructions / transaction_buffer_create", () => { }); it("set transaction buffer", async () => { - const transactionIndex = 1n; + const bufferIndex = 0; const testPayee = Keypair.generate(); const testIx = createTestTransferInstruction( @@ -92,12 +91,12 @@ describe("Instructions / transaction_buffer_create", () => { vaultPda, }); - const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + const [transactionBuffer, _] = PublicKey.findProgramAddressSync( [ Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), + Uint8Array.from([bufferIndex]) ], programId ); @@ -108,6 +107,7 @@ describe("Instructions / transaction_buffer_create", () => { .update(messageBuffer) .digest(); + const ix = multisig.generated.createTransactionBufferCreateInstruction( { multisig: multisigPda, @@ -118,7 +118,9 @@ describe("Instructions / transaction_buffer_create", () => { }, { args: { + bufferIndex: bufferIndex, vaultIndex: 0, + createKey: Keypair.generate(), // Must be a SHA256 hash of the message buffer. finalBufferHash: Array.from(messageHash), finalBufferSize: messageBuffer.length, @@ -153,15 +155,17 @@ describe("Instructions / transaction_buffer_create", () => { assert.ok(transactionBufferAccount?.data.length! > 0); }); + + it("close transaction buffer", async () => { - const transactionIndex = 1n; + const bufferIndex = 0; - const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + const [transactionBuffer, _] = PublicKey.findProgramAddressSync( [ Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), + Uint8Array.from([bufferIndex]) ], programId ); @@ -199,330 +203,456 @@ describe("Instructions / transaction_buffer_create", () => { assert.equal(transactionBufferAccount, null); }); - // Test: Attempt to create a transaction buffer with a non-member -it("error: creating buffer as non-member", async () => { - const transactionIndex = 1n; - // Create a keypair that is not a member of the multisig - const nonMember = Keypair.generate(); - // Airdrop some SOL to the non-member - const airdropSig = await connection.requestAirdrop( - nonMember.publicKey, - 1 * LAMPORTS_PER_SOL - ); - await connection.confirmTransaction(airdropSig); - - // Set up a test transaction - const testPayee = Keypair.generate(); - const testIx = await createTestTransferInstruction( - vaultPda, - testPayee.publicKey, - 1 * LAMPORTS_PER_SOL - ); - - // Create a transaction message - const testTransferMessage = new TransactionMessage({ - payerKey: vaultPda, - recentBlockhash: (await connection.getLatestBlockhash()).blockhash, - instructions: [testIx], + it("reinitalize transaction buffer after its been closed", async () => { + const bufferIndex = 0; + + const testPayee = Keypair.generate(); + const testIx = createTestTransferInstruction( + vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + // Initialize a transaction message with a single instruction. + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], + }); + + // Serialize with SDK util + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + const [transactionBuffer, _] = PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + Uint8Array.from([bufferIndex]) + ], + programId + ); + + // Convert to a SHA256 hash. + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + bufferIndex: bufferIndex, + vaultIndex: 0, + createKey: Keypair.generate(), + // Must be a SHA256 hash of the message buffer. + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + const message = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + + tx.sign([members.proposer]); + + // Send transaction. + const signature = await connection.sendTransaction(tx, { + skipPreflight: true, + }); + await connection.confirmTransaction(signature); + + const transactionBufferAccount = await connection.getAccountInfo( + transactionBuffer + ); + + // Verify account exists. + assert.notEqual(transactionBufferAccount, null); + assert.ok(transactionBufferAccount?.data.length! > 0); + + const ix2 = multisig.generated.createTransactionBufferCloseInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + }, + programId + ); + + const message2 = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix2], + }).compileToV0Message(); + + const tx2 = new VersionedTransaction(message2); + + tx2.sign([members.proposer]); + + // Send transaction. + const signature2 = await connection.sendTransaction(tx2, { + skipPreflight: true, + }); + await connection.confirmTransaction(signature2); + + const transactionBufferAccount2 = await connection.getAccountInfo( + transactionBuffer + ); + + // Verify account is closed. + assert.equal(transactionBufferAccount2, null); }); - // Serialize the message buffer - const messageBuffer = - multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ - message: testTransferMessage, - addressLookupTableAccounts: [], + // Test: Attempt to create a transaction buffer with a non-member + it("error: creating buffer as non-member", async () => { + const bufferIndex = 0; + // Create a keypair that is not a member of the multisig + const nonMember = Keypair.generate(); + // Airdrop some SOL to the non-member + const airdropSig = await connection.requestAirdrop( + nonMember.publicKey, + 1 * LAMPORTS_PER_SOL + ); + await connection.confirmTransaction(airdropSig); + + // Set up a test transaction + const testPayee = Keypair.generate(); + const testIx = await createTestTransferInstruction( vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + // Create a transaction message + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], }); - // Derive the transaction buffer PDA - const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( - [ - Buffer.from("multisig"), - multisigPda.toBuffer(), - Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), - ], - programId - ); - - // Create a hash of the message buffer - const messageHash = crypto - .createHash("sha256") - .update(messageBuffer) - .digest(); - - // Create the instruction to create a transaction buffer - const ix = multisig.generated.createTransactionBufferCreateInstruction( - { - multisig: multisigPda, - transactionBuffer, - creator: nonMember.publicKey, - rentPayer: nonMember.publicKey, - systemProgram: SystemProgram.programId, - }, - { - args: { - vaultIndex: 0, - finalBufferHash: Array.from(messageHash), - finalBufferSize: messageBuffer.length, - buffer: messageBuffer, - } as TransactionBufferCreateArgs, - } as TransactionBufferCreateInstructionArgs, - programId - ); - - // Create and sign the transaction - const message = new TransactionMessage({ - payerKey: nonMember.publicKey, - recentBlockhash: (await connection.getLatestBlockhash()).blockhash, - instructions: [ix], - }).compileToV0Message(); - - const tx = new VersionedTransaction(message); - tx.sign([nonMember]); - - // Attempt to send the transaction and expect it to fail - await assert.rejects( - () => - connection - .sendTransaction(tx) - .catch(multisig.errors.translateAndThrowAnchorError), - /NotAMember/ - ); -}); + // Serialize the message buffer + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + // Derive the transaction buffer PDA + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + Uint8Array.from([bufferIndex]), + ], + programId + ); + + // Create a hash of the message buffer + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + // Create the instruction to create a transaction buffer + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: nonMember.publicKey, + rentPayer: nonMember.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + bufferIndex: bufferIndex, + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); -// Test: Attempt to create a transaction buffer with a member without initiate permissions -it("error: creating buffer as member without proposer permissions", async () => { - const memberWithoutInitiatePermissions = members.voter; - - const transactionIndex = 1n; - - // Set up a test transaction - const testPayee = Keypair.generate(); - const testIx = await createTestTransferInstruction( - vaultPda, - testPayee.publicKey, - 1 * LAMPORTS_PER_SOL - ); - - // Create a transaction message - const testTransferMessage = new TransactionMessage({ - payerKey: vaultPda, - recentBlockhash: (await connection.getLatestBlockhash()).blockhash, - instructions: [testIx], + // Create and sign the transaction + const message = new TransactionMessage({ + payerKey: nonMember.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + tx.sign([nonMember]); + + // Attempt to send the transaction and expect it to fail + await assert.rejects( + () => + connection + .sendTransaction(tx) + .catch(multisig.errors.translateAndThrowAnchorError), + /NotAMember/ + ); }); - // Serialize the message buffer - const messageBuffer = - multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ - message: testTransferMessage, - addressLookupTableAccounts: [], + // Test: Attempt to create a transaction buffer with a member without initiate permissions + it("error: creating buffer as member without proposer permissions", async () => { + const memberWithoutInitiatePermissions = members.voter; + + const bufferIndex = 0; + + // Set up a test transaction + const testPayee = Keypair.generate(); + const testIx = await createTestTransferInstruction( vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + // Create a transaction message + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], }); - // Derive the transaction buffer PDA - const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( - [ - Buffer.from("multisig"), - multisigPda.toBuffer(), - Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), - ], - programId - ); - - // Create a hash of the message buffer - const messageHash = crypto - .createHash("sha256") - .update(messageBuffer) - .digest(); - - // Create the instruction to create a transaction buffer - const ix = multisig.generated.createTransactionBufferCreateInstruction( - { - multisig: multisigPda, - transactionBuffer, - creator: memberWithoutInitiatePermissions.publicKey, - rentPayer: memberWithoutInitiatePermissions.publicKey, - systemProgram: SystemProgram.programId, - }, - { - args: { - vaultIndex: 0, - finalBufferHash: Array.from(messageHash), - finalBufferSize: messageBuffer.length, - buffer: messageBuffer, - } as TransactionBufferCreateArgs, - } as TransactionBufferCreateInstructionArgs, - programId - ); - - // Create and sign the transaction - const message = new TransactionMessage({ - payerKey: memberWithoutInitiatePermissions.publicKey, - recentBlockhash: (await connection.getLatestBlockhash()).blockhash, - instructions: [ix], - }).compileToV0Message(); - - const tx = new VersionedTransaction(message); - tx.sign([memberWithoutInitiatePermissions]); - - // Attempt to send the transaction and expect it to fail - await assert.rejects( - () => - connection - .sendTransaction(tx) - .catch(multisig.errors.translateAndThrowAnchorError), - /Unauthorized/ - ); -}); + // Serialize the message buffer + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + // Derive the transaction buffer PDA + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + Uint8Array.from([bufferIndex]), + ], + programId + ); + + // Create a hash of the message buffer + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + // Create the instruction to create a transaction buffer + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: memberWithoutInitiatePermissions.publicKey, + rentPayer: memberWithoutInitiatePermissions.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + bufferIndex: bufferIndex, + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); -// Test: Attempt to create a transaction buffer with an invalid index -it("error: creating buffer for invalid index", async () => { - // Use an invalid transaction index (multisig.transaction_index + 2) - const invalidTransactionIndex = 3n; - - // Set up a test transaction - const testPayee = Keypair.generate(); - const testIx = await createTestTransferInstruction( - vaultPda, - testPayee.publicKey, - 1 * LAMPORTS_PER_SOL - ); - - // Create a transaction message - const testTransferMessage = new TransactionMessage({ - payerKey: vaultPda, - recentBlockhash: (await connection.getLatestBlockhash()).blockhash, - instructions: [testIx], + // Create and sign the transaction + const message = new TransactionMessage({ + payerKey: memberWithoutInitiatePermissions.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + tx.sign([memberWithoutInitiatePermissions]); + + // Attempt to send the transaction and expect it to fail + await assert.rejects( + () => + connection + .sendTransaction(tx) + .catch(multisig.errors.translateAndThrowAnchorError), + /Unauthorized/ + ); }); - // Serialize the message buffer - const messageBuffer = - multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ - message: testTransferMessage, - addressLookupTableAccounts: [], + // Test: Attempt to create a transaction buffer with an invalid index + it("error: creating buffer for invalid index", async () => { + // Use an invalid buffer index (non-u8 value) + const invalidBufferIndex = "random_string"; + + // Set up a test transaction + const testPayee = Keypair.generate(); + const testIx = await createTestTransferInstruction( vaultPda, + testPayee.publicKey, + 1 * LAMPORTS_PER_SOL + ); + + // Create a transaction message + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [testIx], }); - // Derive the transaction buffer PDA with the invalid index - const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( - [ - Buffer.from("multisig"), - multisigPda.toBuffer(), - Buffer.from("transaction_buffer"), - new BN(Number(invalidTransactionIndex)).toBuffer("le", 8), - ], - programId - ); - - // Create a hash of the message buffer - const messageHash = crypto - .createHash("sha256") - .update(messageBuffer) - .digest(); - - // Create the instruction to create a transaction buffer - const ix = multisig.generated.createTransactionBufferCreateInstruction( - { - multisig: multisigPda, - transactionBuffer, - creator: members.proposer.publicKey, - rentPayer: members.proposer.publicKey, - systemProgram: SystemProgram.programId, - }, - { - args: { - vaultIndex: 0, - finalBufferHash: Array.from(messageHash), - finalBufferSize: messageBuffer.length, - buffer: messageBuffer, - } as TransactionBufferCreateArgs, - } as TransactionBufferCreateInstructionArgs, - programId - ); - - // Create and sign the transaction - const message = new TransactionMessage({ - payerKey: members.proposer.publicKey, - recentBlockhash: (await connection.getLatestBlockhash()).blockhash, - instructions: [ix], - }).compileToV0Message(); - - const tx = new VersionedTransaction(message); - tx.sign([members.proposer]); - - // Attempt to send the transaction and expect it to fail - await assert.rejects( - () => - connection - .sendTransaction(tx) - .catch(multisig.errors.translateAndThrowAnchorError), - /A seeds constraint was violated/ - ); -}); + // Serialize the message buffer + const messageBuffer = + multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + // Derive the transaction buffer PDA with the invalid index + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + Buffer.from(invalidBufferIndex), + ], + programId + ); -it("error: creating buffer exceeding maximum size", async () => { - const transactionIndex = 1n; - - // Create a large buffer that exceeds the maximum size - const largeBuffer = Buffer.alloc(500, 1); // 500 bytes, filled with 1s - - // Derive the transaction buffer PDA - const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( - [ - Buffer.from("multisig"), - multisigPda.toBuffer(), - Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), - ], - programId - ); - - // Create a hash of the large buffer - const messageHash = crypto - .createHash("sha256") - .update(largeBuffer) - .digest(); - - // Create the instruction to create a transaction buffer - const ix = multisig.generated.createTransactionBufferCreateInstruction( - { - multisig: multisigPda, - transactionBuffer, - creator: members.proposer.publicKey, - rentPayer: members.proposer.publicKey, - systemProgram: SystemProgram.programId, - }, - { - args: { - vaultIndex: 0, - finalBufferHash: Array.from(messageHash), - finalBufferSize: 4001, - buffer: largeBuffer, - } as TransactionBufferCreateArgs, - } as TransactionBufferCreateInstructionArgs, - programId - ); - - // Create and sign the transaction - const message = new TransactionMessage({ - payerKey: members.proposer.publicKey, - recentBlockhash: (await connection.getLatestBlockhash()).blockhash, - instructions: [ix], - }).compileToV0Message(); - - const tx = new VersionedTransaction(message); - tx.sign([members.proposer]); - - // Attempt to send the transaction and expect it to fail - await assert.rejects( - () => - connection - .sendTransaction(tx) - .catch(multisig.errors.translateAndThrowAnchorError), - /FinalBufferSizeExceeded/ // Assuming this is the error thrown for exceeding buffer size - ); -}); + // Create a hash of the message buffer + const messageHash = crypto + .createHash("sha256") + .update(messageBuffer) + .digest(); + + // Create the instruction to create a transaction buffer + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + bufferIndex: 0, + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: messageBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + // Create and sign the transaction + const message = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + // Not signing with the create_key on purpose + tx.sign([members.proposer]); + + // Attempt to send the transaction and expect it to fail + await assert.rejects( + () => + connection + .sendTransaction(tx) + .catch(multisig.errors.translateAndThrowAnchorError), + /A seeds constraint was violated/ + ); + }); + + + it("error: creating buffer exceeding maximum size", async () => { + const bufferIndex = 0; + + // Create a large buffer that exceeds the maximum size + const largeBuffer = Buffer.alloc(500, 1); // 500 bytes, filled with 1s + + // Derive the transaction buffer PDA + const [transactionBuffer, _] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + Uint8Array.from([bufferIndex]), + ], + programId + ); + + // Create a hash of the large buffer + const messageHash = crypto + .createHash("sha256") + .update(largeBuffer) + .digest(); + + // Create the instruction to create a transaction buffer + const ix = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + bufferIndex: bufferIndex, + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: 4001, + buffer: largeBuffer, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + // Create and sign the transaction + const message = new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new VersionedTransaction(message); + tx.sign([members.proposer]); + + // Attempt to send the transaction and expect it to fail + await assert.rejects( + () => + connection + .sendTransaction(tx) + .catch(multisig.errors.translateAndThrowAnchorError), + /FinalBufferSizeExceeded/ // Assuming this is the error thrown for exceeding buffer size + ); + }); }); diff --git a/tests/suites/instructions/transactionBufferExtend.ts b/tests/suites/instructions/transactionBufferExtend.ts index 61c1aa23..75ce62cc 100644 --- a/tests/suites/instructions/transactionBufferExtend.ts +++ b/tests/suites/instructions/transactionBufferExtend.ts @@ -14,7 +14,6 @@ import { TransactionBufferExtendInstructionArgs, } from "@sqds/multisig/lib/generated"; import assert from "assert"; -import { BN } from "bn.js"; import * as crypto from "crypto"; import { TestMembers, @@ -75,7 +74,7 @@ describe("Instructions / transaction_buffer_extend", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), + Buffer.from([Number(transactionIndex)]) ], programId ); @@ -110,6 +109,7 @@ describe("Instructions / transaction_buffer_extend", () => { }, { args: { + bufferIndex: Number(transactionIndex), vaultIndex: 0, finalBufferHash: Array.from(messageHash), finalBufferSize: messageBuffer.length, @@ -194,7 +194,7 @@ describe("Instructions / transaction_buffer_extend", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), + Buffer.from([Number(transactionIndex)]) ], programId ); @@ -217,6 +217,7 @@ describe("Instructions / transaction_buffer_extend", () => { }, { args: { + bufferIndex: Number(transactionIndex), vaultIndex: 0, // Must be a SHA256 hash of the message buffer. finalBufferHash: Array.from(messageHash), @@ -241,6 +242,7 @@ describe("Instructions / transaction_buffer_extend", () => { const signature = await connection.sendTransaction(tx, { skipPreflight: true, }); + await connection.confirmTransaction(signature); const transactionBufferAccount = await connection.getAccountInfo( diff --git a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts index e8a37db8..fd7f3a1b 100644 --- a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts +++ b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts @@ -74,9 +74,10 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { it("set buffer, extend, and create", async () => { const transactionIndex = 1n; + const bufferIndex = 0; const testPayee = Keypair.generate(); - const testIx = await createTestTransferInstruction( + const testIx = createTestTransferInstruction( vaultPda, testPayee.publicKey, 1 * LAMPORTS_PER_SOL @@ -109,7 +110,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), + Uint8Array.from([bufferIndex]), ], programId ); @@ -132,6 +133,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { }, { args: { + bufferIndex: bufferIndex, vaultIndex: 0, // Must be a SHA256 hash of the message buffer. finalBufferHash: Array.from(messageHash), @@ -156,7 +158,6 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { const signature = await connection.sendTransaction(tx, { skipPreflight: true, }); - await connection.confirmTransaction(signature); const transactionBufferAccount = await connection.getAccountInfo( @@ -248,17 +249,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { toPubkey: members.almighty.publicKey, lamports: 100 }); - // const mockTransferMessage = new TransactionMessage({ - // payerKey: vaultPda, - // recentBlockhash: (await connection.getLatestBlockhash()).blockhash, - // instructions: [mockTransferIx], - // }); - - // const bytes = multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ - // message: mockTransferMessage, - // addressLookupTableAccounts: [], - // vaultPda, - // }); + // Create final instruction. const thirdIx = @@ -319,6 +310,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { it("error: create from buffer with mismatched hash", async () => { const transactionIndex = 2n; + const bufferIndex = 0; // Create a simple transfer instruction const testIx = await createTestTransferInstruction( @@ -346,7 +338,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), - new BN(Number(transactionIndex)).toBuffer("le", 8), + Uint8Array.from([bufferIndex]), ], programId ); @@ -365,6 +357,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { }, { args: { + bufferIndex, vaultIndex: 0, finalBufferHash: Array.from(dummyHash), finalBufferSize: messageBuffer.length, From 74fafb1ba7890d8e54cf13673c6cd7a3cf4c764a Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Wed, 9 Oct 2024 08:07:09 +0200 Subject: [PATCH 27/47] fix: bind transaction_buffer seeds to creator --- .../src/instructions/transaction_buffer_close.rs | 1 + .../src/instructions/transaction_buffer_create.rs | 1 + .../src/instructions/transaction_buffer_extend.rs | 1 + .../src/instructions/vault_transaction_create_from_buffer.rs | 1 + 4 files changed, 4 insertions(+) diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs index 9d28f3ef..07f80385 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs @@ -24,6 +24,7 @@ pub struct TransactionBufferClose<'info> { SEED_PREFIX, multisig.key().as_ref(), SEED_TRANSACTION_BUFFER, + creator.key().as_ref(), &transaction_buffer.buffer_index.to_le_bytes() ], bump diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs index 2201b9a3..e36bd396 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs @@ -36,6 +36,7 @@ pub struct TransactionBufferCreate<'info> { SEED_PREFIX, multisig.key().as_ref(), SEED_TRANSACTION_BUFFER, + creator.key().as_ref(), &args.buffer_index.to_le_bytes(), ], bump diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs index 84d6c43c..77d0c2a5 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs @@ -27,6 +27,7 @@ pub struct TransactionBufferExtend<'info> { SEED_PREFIX, multisig.key().as_ref(), SEED_TRANSACTION_BUFFER, + creator.key().as_ref(), &transaction_buffer.buffer_index.to_le_bytes() ], bump diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs index b9c5ab9f..fb4048ba 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -15,6 +15,7 @@ pub struct VaultTransactionCreateFromBuffer<'info> { SEED_PREFIX, vault_transaction_create.multisig.key().as_ref(), SEED_TRANSACTION_BUFFER, + creator.key().as_ref(), &transaction_buffer.buffer_index.to_le_bytes(), ], bump From ff9c93e15fe0222d83176b1ea06d1cae50e88a34 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Wed, 9 Oct 2024 08:14:24 +0200 Subject: [PATCH 28/47] remove: current member check for transaction_buffer_close --- .../src/instructions/transaction_buffer_close.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs index 07f80385..fe0ca42b 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs @@ -37,16 +37,6 @@ pub struct TransactionBufferClose<'info> { impl TransactionBufferClose<'_> { fn validate(&self) -> Result<()> { - let Self { - multisig, creator, .. - } = self; - - // creator is still a member in the multisig - require!( - multisig.is_member(creator.key()).is_some(), - MultisigError::NotAMember - ); - Ok(()) } From 749a81d2524461be1700e16c1e502b663d91d149 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:06:09 +0200 Subject: [PATCH 29/47] fix: creator check on vault_tx_create_from_buffer --- .../src/instructions/vault_transaction_create_from_buffer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs index fb4048ba..46e31b7d 100644 --- a/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs +++ b/programs/squads_multisig_program/src/instructions/vault_transaction_create_from_buffer.rs @@ -11,6 +11,9 @@ pub struct VaultTransactionCreateFromBuffer<'info> { #[account( mut, close = creator, + // Only the creator can turn the buffer into a transaction and reclaim + // the rent + constraint = transaction_buffer.creator == creator.key() @ MultisigError::Unauthorized, seeds = [ SEED_PREFIX, vault_transaction_create.multisig.key().as_ref(), From 712ad05ecca40de5a044f5a0936ed745572ca7e9 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Wed, 9 Oct 2024 09:10:53 +0200 Subject: [PATCH 30/47] remove: multisig mut in transaction_buffer instructions --- .../src/instructions/transaction_buffer_close.rs | 1 - .../src/instructions/transaction_buffer_create.rs | 1 - .../src/instructions/transaction_buffer_extend.rs | 1 - 3 files changed, 3 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs index fe0ca42b..d38e3c9e 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_close.rs @@ -6,7 +6,6 @@ use crate::state::*; #[derive(Accounts)] pub struct TransactionBufferClose<'info> { #[account( - mut, seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()], bump = multisig.bump, )] diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs index e36bd396..6593fe3a 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_create.rs @@ -22,7 +22,6 @@ pub struct TransactionBufferCreateArgs { #[instruction(args: TransactionBufferCreateArgs)] pub struct TransactionBufferCreate<'info> { #[account( - mut, seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()], bump = multisig.bump, )] diff --git a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs index 77d0c2a5..9276dac8 100644 --- a/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs +++ b/programs/squads_multisig_program/src/instructions/transaction_buffer_extend.rs @@ -13,7 +13,6 @@ pub struct TransactionBufferExtendArgs { #[instruction(args: TransactionBufferExtendArgs)] pub struct TransactionBufferExtend<'info> { #[account( - mut, seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()], bump = multisig.bump, )] From 821e9a876d9945add4e389615c40ed872ddf9067 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:02:46 +0200 Subject: [PATCH 31/47] remove: testing code from allocator --- .../squads_multisig_program/src/allocator.rs | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/programs/squads_multisig_program/src/allocator.rs b/programs/squads_multisig_program/src/allocator.rs index 4bdde984..61c71288 100644 --- a/programs/squads_multisig_program/src/allocator.rs +++ b/programs/squads_multisig_program/src/allocator.rs @@ -103,27 +103,22 @@ struct BumpAllocator; unsafe impl std::alloc::GlobalAlloc for BumpAllocator { #[inline] unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - if layout.size() == isize::MAX as usize - 0x42 { - // Return test value - 0x42 as *mut u8 - } else { - const POS_PTR: *mut usize = HEAP_START_ADDRESS as *mut usize; - const TOP_ADDRESS: usize = HEAP_START_ADDRESS as usize + HEAP_LENGTH; - const BOTTOM_ADDRESS: usize = HEAP_START_ADDRESS as usize + size_of::<*mut u8>(); - let mut pos = *POS_PTR; - if pos == 0 { - // First time, set starting position to bottom address - pos = BOTTOM_ADDRESS; - } - // Align the position upwards - pos = (pos + layout.align() - 1) & !(layout.align() - 1); - let next_pos = pos.saturating_add(layout.size()); - if next_pos > TOP_ADDRESS { - return null_mut(); - } - *POS_PTR = next_pos; - pos as *mut u8 + const POS_PTR: *mut usize = HEAP_START_ADDRESS as *mut usize; + const TOP_ADDRESS: usize = HEAP_START_ADDRESS as usize + HEAP_LENGTH; + const BOTTOM_ADDRESS: usize = HEAP_START_ADDRESS as usize + size_of::<*mut u8>(); + let mut pos = *POS_PTR; + if pos == 0 { + // First time, set starting position to bottom address + pos = BOTTOM_ADDRESS; } + // Align the position upwards + pos = (pos + layout.align() - 1) & !(layout.align() - 1); + let next_pos = pos.saturating_add(layout.size()); + if next_pos > TOP_ADDRESS { + return null_mut(); + } + *POS_PTR = next_pos; + pos as *mut u8 } #[inline] From a663f7fb6a1383c8977f491404def286c44fd4e5 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:03:13 +0200 Subject: [PATCH 32/47] fix: tests --- sdk/multisig/idl/squads_multisig_program.json | 6 +++--- .../src/generated/instructions/transactionBufferClose.ts | 4 ++-- .../src/generated/instructions/transactionBufferCreate.ts | 4 ++-- .../src/generated/instructions/transactionBufferExtend.ts | 4 ++-- tests/suites/examples/transaction-buffer.ts | 1 + tests/suites/instructions/transactionBufferClose.ts | 3 ++- tests/suites/instructions/transactionBufferCreate.ts | 7 +++++++ tests/suites/instructions/transactionBufferExtend.ts | 6 ++++-- 8 files changed, 23 insertions(+), 12 deletions(-) diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index fed3c6fb..0224f2f0 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -763,7 +763,7 @@ "accounts": [ { "name": "multisig", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -810,7 +810,7 @@ "accounts": [ { "name": "multisig", - "isMut": true, + "isMut": false, "isSigner": false }, { @@ -837,7 +837,7 @@ "accounts": [ { "name": "multisig", - "isMut": true, + "isMut": false, "isSigner": false }, { diff --git a/sdk/multisig/src/generated/instructions/transactionBufferClose.ts b/sdk/multisig/src/generated/instructions/transactionBufferClose.ts index 0e84493b..b8ea8e86 100644 --- a/sdk/multisig/src/generated/instructions/transactionBufferClose.ts +++ b/sdk/multisig/src/generated/instructions/transactionBufferClose.ts @@ -22,7 +22,7 @@ export const transactionBufferCloseStruct = new beet.BeetArgsStruct<{ /** * Accounts required by the _transactionBufferClose_ instruction * - * @property [_writable_] multisig + * @property [] multisig * @property [_writable_] transactionBuffer * @property [**signer**] creator * @category Instructions @@ -58,7 +58,7 @@ export function createTransactionBufferCloseInstruction( const keys: web3.AccountMeta[] = [ { pubkey: accounts.multisig, - isWritable: true, + isWritable: false, isSigner: false, }, { diff --git a/sdk/multisig/src/generated/instructions/transactionBufferCreate.ts b/sdk/multisig/src/generated/instructions/transactionBufferCreate.ts index 29b53256..6887a94d 100644 --- a/sdk/multisig/src/generated/instructions/transactionBufferCreate.ts +++ b/sdk/multisig/src/generated/instructions/transactionBufferCreate.ts @@ -39,7 +39,7 @@ export const transactionBufferCreateStruct = new beet.FixableBeetArgsStruct< /** * Accounts required by the _transactionBufferCreate_ instruction * - * @property [_writable_] multisig + * @property [] multisig * @property [_writable_] transactionBuffer * @property [**signer**] creator * @property [_writable_, **signer**] rentPayer @@ -82,7 +82,7 @@ export function createTransactionBufferCreateInstruction( const keys: web3.AccountMeta[] = [ { pubkey: accounts.multisig, - isWritable: true, + isWritable: false, isSigner: false, }, { diff --git a/sdk/multisig/src/generated/instructions/transactionBufferExtend.ts b/sdk/multisig/src/generated/instructions/transactionBufferExtend.ts index 0f1acec0..50d2a126 100644 --- a/sdk/multisig/src/generated/instructions/transactionBufferExtend.ts +++ b/sdk/multisig/src/generated/instructions/transactionBufferExtend.ts @@ -39,7 +39,7 @@ export const transactionBufferExtendStruct = new beet.FixableBeetArgsStruct< /** * Accounts required by the _transactionBufferExtend_ instruction * - * @property [_writable_] multisig + * @property [] multisig * @property [_writable_] transactionBuffer * @property [**signer**] creator * @category Instructions @@ -79,7 +79,7 @@ export function createTransactionBufferExtendInstruction( const keys: web3.AccountMeta[] = [ { pubkey: accounts.multisig, - isWritable: true, + isWritable: false, isSigner: false, }, { diff --git a/tests/suites/examples/transaction-buffer.ts b/tests/suites/examples/transaction-buffer.ts index 156424fe..3178c389 100644 --- a/tests/suites/examples/transaction-buffer.ts +++ b/tests/suites/examples/transaction-buffer.ts @@ -103,6 +103,7 @@ describe("Examples / Transaction Buffers", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + members.almighty.publicKey.toBuffer(), Buffer.from([bufferIndex]) ], programId diff --git a/tests/suites/instructions/transactionBufferClose.ts b/tests/suites/instructions/transactionBufferClose.ts index e11a666d..9569f69e 100644 --- a/tests/suites/instructions/transactionBufferClose.ts +++ b/tests/suites/instructions/transactionBufferClose.ts @@ -83,6 +83,7 @@ describe("Instructions / transaction_buffer_close", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + members.proposer.publicKey.toBuffer(), Uint8Array.from([bufferIndex]) ], programId @@ -149,7 +150,7 @@ describe("Instructions / transaction_buffer_close", () => { connection .sendTransaction(closeTx) .catch(multisig.errors.translateAndThrowAnchorError), - /Unauthorized/ + /(Unauthorized|ConstraintSeeds)/ ); }); diff --git a/tests/suites/instructions/transactionBufferCreate.ts b/tests/suites/instructions/transactionBufferCreate.ts index a185203b..2ab1acc4 100644 --- a/tests/suites/instructions/transactionBufferCreate.ts +++ b/tests/suites/instructions/transactionBufferCreate.ts @@ -96,6 +96,7 @@ describe("Instructions / transaction_buffer_create", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + members.proposer.publicKey.toBuffer(), Uint8Array.from([bufferIndex]) ], programId @@ -165,6 +166,7 @@ describe("Instructions / transaction_buffer_create", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + members.proposer.publicKey.toBuffer(), Uint8Array.from([bufferIndex]) ], programId @@ -233,6 +235,7 @@ describe("Instructions / transaction_buffer_create", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + members.proposer.publicKey.toBuffer(), Uint8Array.from([bufferIndex]) ], programId @@ -365,6 +368,7 @@ describe("Instructions / transaction_buffer_create", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + nonMember.publicKey.toBuffer(), Uint8Array.from([bufferIndex]), ], programId @@ -452,6 +456,7 @@ describe("Instructions / transaction_buffer_create", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + memberWithoutInitiatePermissions.publicKey.toBuffer(), Uint8Array.from([bufferIndex]), ], programId @@ -538,6 +543,7 @@ describe("Instructions / transaction_buffer_create", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + members.proposer.publicKey.toBuffer(), Buffer.from(invalidBufferIndex), ], programId @@ -604,6 +610,7 @@ describe("Instructions / transaction_buffer_create", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + members.proposer.publicKey.toBuffer(), Uint8Array.from([bufferIndex]), ], programId diff --git a/tests/suites/instructions/transactionBufferExtend.ts b/tests/suites/instructions/transactionBufferExtend.ts index 75ce62cc..789bf0af 100644 --- a/tests/suites/instructions/transactionBufferExtend.ts +++ b/tests/suites/instructions/transactionBufferExtend.ts @@ -74,6 +74,7 @@ describe("Instructions / transaction_buffer_extend", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + creator.publicKey.toBuffer(), Buffer.from([Number(transactionIndex)]) ], programId @@ -194,6 +195,7 @@ describe("Instructions / transaction_buffer_extend", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + members.proposer.publicKey.toBuffer(), Buffer.from([Number(transactionIndex)]) ], programId @@ -353,7 +355,7 @@ describe("Instructions / transaction_buffer_extend", () => { await assert.rejects( () => connection.sendTransaction(tx).catch(multisig.errors.translateAndThrowAnchorError), - /Unauthorized/ + /(Unauthorized|ConstraintSeeds)/ ); await closeTransactionBuffer(members.almighty, transactionBuffer); @@ -429,7 +431,7 @@ describe("Instructions / transaction_buffer_extend", () => { await assert.rejects( () => connection.sendTransaction(extendTx).catch(multisig.errors.translateAndThrowAnchorError), - /Unauthorized/ + /(Unauthorized|ConstraintSeeds)/ ); From 7551bf073a12991926597e32b7b3bd88fd2ffa15 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:53:31 +0200 Subject: [PATCH 33/47] fix: MAX_BUFFER_SIZE saturation in invariant check --- .../squads_multisig_program/src/state/transaction_buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/programs/squads_multisig_program/src/state/transaction_buffer.rs b/programs/squads_multisig_program/src/state/transaction_buffer.rs index 2ed6b608..945276d1 100644 --- a/programs/squads_multisig_program/src/state/transaction_buffer.rs +++ b/programs/squads_multisig_program/src/state/transaction_buffer.rs @@ -66,7 +66,7 @@ impl TransactionBuffer { MultisigError::FinalBufferSizeExceeded ); require!( - self.buffer.len() < MAX_BUFFER_SIZE, + self.buffer.len() <= MAX_BUFFER_SIZE, MultisigError::FinalBufferSizeExceeded ); require!( From 676766d81256308ce122c8bfe9c83379993e97b4 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Thu, 10 Oct 2024 23:48:37 +0200 Subject: [PATCH 34/47] remove: heap testing instruction --- .../src/instructions/heap_test.rs | 28 ------------------- .../src/instructions/mod.rs | 2 -- programs/squads_multisig_program/src/lib.rs | 9 +++--- 3 files changed, 4 insertions(+), 35 deletions(-) delete mode 100644 programs/squads_multisig_program/src/instructions/heap_test.rs diff --git a/programs/squads_multisig_program/src/instructions/heap_test.rs b/programs/squads_multisig_program/src/instructions/heap_test.rs deleted file mode 100644 index 526aac11..00000000 --- a/programs/squads_multisig_program/src/instructions/heap_test.rs +++ /dev/null @@ -1,28 +0,0 @@ -use anchor_lang::prelude::*; - -#[derive(Accounts)] -pub struct HeapTest<'info> { - /// CHECK: We only need to validate the address. - pub authority: AccountInfo<'info>, -} - -impl HeapTest<'_> { - pub fn handler(_ctx: Context, length: u64) -> Result<()> { - // Allocate the vector with the desired capacity - let mut vector = Vec::::with_capacity(length as usize); - - // Unsafe block to set the length of the vector without initialization - unsafe { - vector.set_len(length as usize); - } - - // If you need to set all elements to a specific value (e.g., 1), - // you can use `fill` method which is more efficient than iteration - vector.fill(1); - - // Log the vector's length - msg!("Vector allocated with length: {}", vector.len()); - - Ok(()) - } -} diff --git a/programs/squads_multisig_program/src/instructions/mod.rs b/programs/squads_multisig_program/src/instructions/mod.rs index 9c6330a5..82e17d81 100644 --- a/programs/squads_multisig_program/src/instructions/mod.rs +++ b/programs/squads_multisig_program/src/instructions/mod.rs @@ -3,7 +3,6 @@ pub use batch_create::*; pub use batch_execute_transaction::*; pub use config_transaction_create::*; pub use config_transaction_execute::*; -pub use heap_test::*; pub use multisig_add_spending_limit::*; pub use multisig_config::*; pub use multisig_create::*; @@ -27,7 +26,6 @@ mod batch_create; mod batch_execute_transaction; mod config_transaction_create; mod config_transaction_execute; -mod heap_test; mod multisig_add_spending_limit; mod multisig_config; mod multisig_create; diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index 8be658b0..c1a5572d 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -265,7 +265,10 @@ pub mod squads_multisig_program { /// accommodate the new amount of cancel votes. /// The previous implemenation still works if the proposal size is in line with the /// threshold size. - pub fn proposal_cancel_v2<'info>(ctx: Context<'_, '_, 'info, 'info, ProposalCancelV2<'info>>, args: ProposalVoteArgs) -> Result<()> { + pub fn proposal_cancel_v2<'info>( + ctx: Context<'_, '_, 'info, 'info, ProposalCancelV2<'info>>, + args: ProposalVoteArgs, + ) -> Result<()> { ProposalCancelV2::proposal_cancel_v2(ctx, args) } @@ -316,8 +319,4 @@ pub mod squads_multisig_program { pub fn batch_accounts_close(ctx: Context) -> Result<()> { BatchAccountsClose::batch_accounts_close(ctx) } - // Uncomment to enable the heap_test instruction - // pub fn heap_test(ctx: Context, length: u64) -> Result<()> { - // HeapTest::handler(ctx, length) - // } } From 3afed8456fbcb7f9094a4ebc542c54ada9267df4 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:10:05 +0200 Subject: [PATCH 35/47] deprecate: multisig_create --- .../src/instructions/multisig_create.rs | 77 +------------------ programs/squads_multisig_program/src/lib.rs | 6 +- 2 files changed, 7 insertions(+), 76 deletions(-) diff --git a/programs/squads_multisig_program/src/instructions/multisig_create.rs b/programs/squads_multisig_program/src/instructions/multisig_create.rs index fd690db3..e4b058ec 100644 --- a/programs/squads_multisig_program/src/instructions/multisig_create.rs +++ b/programs/squads_multisig_program/src/instructions/multisig_create.rs @@ -6,80 +6,11 @@ use solana_program::native_token::LAMPORTS_PER_SOL; use crate::errors::MultisigError; use crate::state::*; -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct MultisigCreateArgs { - /// The authority that can configure the multisig: add/remove members, change the threshold, etc. - /// Should be set to `None` for autonomous multisigs. - pub config_authority: Option, - /// The number of signatures required to execute a transaction. - pub threshold: u16, - /// The members of the multisig. - pub members: Vec, - /// How many seconds must pass between transaction voting, settlement, and execution. - pub time_lock: u32, - /// Memo is used for indexing only. - pub memo: Option, -} - -#[deprecated( - since = "0.4.0", - note = "This instruction is deprecated and will be removed soon. Please use `multisig_create_v2` to ensure future compatibility." -)] +// Dummy Account context for multisigCreate, since Anchor doesn't allow empty instructions. #[derive(Accounts)] -#[instruction(args: MultisigCreateArgs)] -pub struct MultisigCreate<'info> { - #[account( - init, - payer = creator, - space = Multisig::size(args.members.len()), - seeds = [SEED_PREFIX, SEED_MULTISIG, create_key.key().as_ref()], - bump - )] - pub multisig: Account<'info, Multisig>, - - /// An ephemeral signer that is used as a seed for the Multisig PDA. - /// Must be a signer to prevent front-running attack by someone else but the original creator. - pub create_key: Signer<'info>, - - /// The creator of the multisig. - #[account(mut)] - pub creator: Signer<'info>, - - pub system_program: Program<'info, System>, -} - -#[allow(deprecated)] -impl MultisigCreate<'_> { - fn validate(&self) -> Result<()> { - Ok(()) - } - - /// Creates a multisig. - #[allow(deprecated)] - #[access_control(ctx.accounts.validate())] - pub fn multisig_create(ctx: Context, args: MultisigCreateArgs) -> Result<()> { - msg!("WARNING: This instruction is deprecated and will be removed soon. Please use `multisig_create_v2` to ensure future compatibility."); - - // Sort the members by pubkey. - let mut members = args.members; - members.sort_by_key(|m| m.key); - - // Initialize the multisig. - let multisig = &mut ctx.accounts.multisig; - multisig.config_authority = args.config_authority.unwrap_or_default(); - multisig.threshold = args.threshold; - multisig.time_lock = args.time_lock; - multisig.transaction_index = 0; - multisig.stale_transaction_index = 0; - multisig.create_key = ctx.accounts.create_key.key(); - multisig.bump = ctx.bumps.multisig; - multisig.members = members; - multisig.rent_collector = None; - - multisig.invariant()?; - - Ok(()) - } +pub struct Deprecated<'info> { + ///CHECK: Dummy Account + pub null: AccountInfo<'info>, } #[derive(AnchorSerialize, AnchorDeserialize)] diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index c1a5572d..e5931fb4 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -75,9 +75,9 @@ pub mod squads_multisig_program { } /// Create a multisig. - #[allow(deprecated)] - pub fn multisig_create(ctx: Context, args: MultisigCreateArgs) -> Result<()> { - MultisigCreate::multisig_create(ctx, args) + pub fn multisig_create(_ctx: Context) -> Result<()> { + msg!("WARNING: multisig_create has been deprecated. Please use multisig_create_V2 instead."); + Err(ErrorCode::Deprecated.into()) } /// Create a multisig. From 1fb5dc1fcf8461275f3ccb40d36053000f3dec4f Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:19:03 +0200 Subject: [PATCH 36/47] refactor: sdk for multisig_create deprecation --- sdk/multisig/idl/squads_multisig_program.json | 85 +------------------ .../generated/instructions/multisigCreate.ts | 55 ++---------- .../src/generated/types/MultisigCreateArgs.ts | 34 -------- sdk/multisig/src/generated/types/index.ts | 1 - .../src/instructions/multisigCreate.ts | 35 +++++--- sdk/multisig/src/rpc/multisigCreate.ts | 2 +- 6 files changed, 33 insertions(+), 179 deletions(-) delete mode 100644 sdk/multisig/src/generated/types/MultisigCreateArgs.ts diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index 0224f2f0..a60cc763 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -121,41 +121,12 @@ ], "accounts": [ { - "name": "multisig", - "isMut": true, - "isSigner": false - }, - { - "name": "createKey", - "isMut": false, - "isSigner": true, - "docs": [ - "An ephemeral signer that is used as a seed for the Multisig PDA.", - "Must be a signer to prevent front-running attack by someone else but the original creator." - ] - }, - { - "name": "creator", - "isMut": true, - "isSigner": true, - "docs": [ - "The creator of the multisig." - ] - }, - { - "name": "systemProgram", + "name": "null", "isMut": false, "isSigner": false } ], - "args": [ - { - "name": "args", - "type": { - "defined": "MultisigCreateArgs" - } - } - ] + "args": [] }, { "name": "multisigCreateV2", @@ -2503,58 +2474,6 @@ ] } }, - { - "name": "MultisigCreateArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "configAuthority", - "docs": [ - "The authority that can configure the multisig: add/remove members, change the threshold, etc.", - "Should be set to `None` for autonomous multisigs." - ], - "type": { - "option": "publicKey" - } - }, - { - "name": "threshold", - "docs": [ - "The number of signatures required to execute a transaction." - ], - "type": "u16" - }, - { - "name": "members", - "docs": [ - "The members of the multisig." - ], - "type": { - "vec": { - "defined": "Member" - } - } - }, - { - "name": "timeLock", - "docs": [ - "How many seconds must pass between transaction voting, settlement, and execution." - ], - "type": "u32" - }, - { - "name": "memo", - "docs": [ - "Memo is used for indexing only." - ], - "type": { - "option": "string" - } - } - ] - } - }, { "name": "MultisigCreateArgsV2", "type": { diff --git a/sdk/multisig/src/generated/instructions/multisigCreate.ts b/sdk/multisig/src/generated/instructions/multisigCreate.ts index 6391bf3c..3b0a7a04 100644 --- a/sdk/multisig/src/generated/instructions/multisigCreate.ts +++ b/sdk/multisig/src/generated/instructions/multisigCreate.ts @@ -7,50 +7,28 @@ import * as beet from '@metaplex-foundation/beet' import * as web3 from '@solana/web3.js' -import { - MultisigCreateArgs, - multisigCreateArgsBeet, -} from '../types/MultisigCreateArgs' /** * @category Instructions * @category MultisigCreate * @category generated */ -export type MultisigCreateInstructionArgs = { - args: MultisigCreateArgs -} -/** - * @category Instructions - * @category MultisigCreate - * @category generated - */ -export const multisigCreateStruct = new beet.FixableBeetArgsStruct< - MultisigCreateInstructionArgs & { - instructionDiscriminator: number[] /* size: 8 */ - } ->( - [ - ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], - ['args', multisigCreateArgsBeet], - ], +export const multisigCreateStruct = new beet.BeetArgsStruct<{ + instructionDiscriminator: number[] /* size: 8 */ +}>( + [['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)]], 'MultisigCreateInstructionArgs' ) /** * Accounts required by the _multisigCreate_ instruction * - * @property [_writable_] multisig - * @property [**signer**] createKey - * @property [_writable_, **signer**] creator + * @property [] null * @category Instructions * @category MultisigCreate * @category generated */ export type MultisigCreateInstructionAccounts = { - multisig: web3.PublicKey - createKey: web3.PublicKey - creator: web3.PublicKey - systemProgram?: web3.PublicKey + null: web3.PublicKey anchorRemainingAccounts?: web3.AccountMeta[] } @@ -62,39 +40,20 @@ export const multisigCreateInstructionDiscriminator = [ * Creates a _MultisigCreate_ instruction. * * @param accounts that will be accessed while the instruction is processed - * @param args to provide as instruction data to the program - * * @category Instructions * @category MultisigCreate * @category generated */ export function createMultisigCreateInstruction( accounts: MultisigCreateInstructionAccounts, - args: MultisigCreateInstructionArgs, programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') ) { const [data] = multisigCreateStruct.serialize({ instructionDiscriminator: multisigCreateInstructionDiscriminator, - ...args, }) const keys: web3.AccountMeta[] = [ { - pubkey: accounts.multisig, - isWritable: true, - isSigner: false, - }, - { - pubkey: accounts.createKey, - isWritable: false, - isSigner: true, - }, - { - pubkey: accounts.creator, - isWritable: true, - isSigner: true, - }, - { - pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + pubkey: accounts.null, isWritable: false, isSigner: false, }, diff --git a/sdk/multisig/src/generated/types/MultisigCreateArgs.ts b/sdk/multisig/src/generated/types/MultisigCreateArgs.ts deleted file mode 100644 index 0cb11d72..00000000 --- a/sdk/multisig/src/generated/types/MultisigCreateArgs.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * This code was GENERATED using the solita package. - * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. - * - * See: https://github.com/metaplex-foundation/solita - */ - -import * as web3 from '@solana/web3.js' -import * as beet from '@metaplex-foundation/beet' -import * as beetSolana from '@metaplex-foundation/beet-solana' -import { Member, memberBeet } from './Member' -export type MultisigCreateArgs = { - configAuthority: beet.COption - threshold: number - members: Member[] - timeLock: number - memo: beet.COption -} - -/** - * @category userTypes - * @category generated - */ -export const multisigCreateArgsBeet = - new beet.FixableBeetArgsStruct( - [ - ['configAuthority', beet.coption(beetSolana.publicKey)], - ['threshold', beet.u16], - ['members', beet.array(memberBeet)], - ['timeLock', beet.u32], - ['memo', beet.coption(beet.utf8String)], - ], - 'MultisigCreateArgs' - ) diff --git a/sdk/multisig/src/generated/types/index.ts b/sdk/multisig/src/generated/types/index.ts index b60c4a84..896e0cea 100644 --- a/sdk/multisig/src/generated/types/index.ts +++ b/sdk/multisig/src/generated/types/index.ts @@ -7,7 +7,6 @@ export * from './MultisigAddMemberArgs' export * from './MultisigAddSpendingLimitArgs' export * from './MultisigChangeThresholdArgs' export * from './MultisigCompiledInstruction' -export * from './MultisigCreateArgs' export * from './MultisigCreateArgsV2' export * from './MultisigMessageAddressTableLookup' export * from './MultisigRemoveMemberArgs' diff --git a/sdk/multisig/src/instructions/multisigCreate.ts b/sdk/multisig/src/instructions/multisigCreate.ts index c7b8bd22..92b03c50 100644 --- a/sdk/multisig/src/instructions/multisigCreate.ts +++ b/sdk/multisig/src/instructions/multisigCreate.ts @@ -29,18 +29,29 @@ export function multisigCreate({ }): TransactionInstruction { return createMultisigCreateInstruction( { - creator, - createKey, - multisig: multisigPda, - }, - { - args: { - configAuthority, - threshold, - members, - timeLock, - memo: memo ?? null, - }, + null: PublicKey.default, + anchorRemainingAccounts: [ + { + pubkey: creator, + isWritable: true, + isSigner: true, + }, + { + pubkey: createKey, + isWritable: false, + isSigner: true, + }, + { + pubkey: multisigPda, + isWritable: true, + isSigner: false, + }, + { + pubkey: createKey, + isWritable: false, + isSigner: true, + } + ] }, programId ); diff --git a/sdk/multisig/src/rpc/multisigCreate.ts b/sdk/multisig/src/rpc/multisigCreate.ts index 070150ed..4f1acc8e 100644 --- a/sdk/multisig/src/rpc/multisigCreate.ts +++ b/sdk/multisig/src/rpc/multisigCreate.ts @@ -57,7 +57,7 @@ export async function multisigCreate({ tx.sign([creator, createKey]); try { - return await connection.sendTransaction(tx, sendOptions); + return await connection.sendRawTransaction(tx.serialize(), sendOptions); } catch (err) { translateAndThrowAnchorError(err); } From a19df4905b34f038ad123c9f14cbdc1e6b260a3a Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:20:03 +0200 Subject: [PATCH 37/47] refactor: tests for multisig_create deprecation --- sdk/multisig/src/rpc/multisigCreate.ts | 2 +- tests/index.ts | 38 +++--- tests/suites/instructions/multisigCreate.ts | 120 +++++------------- .../vaultTransactionCreateFromBuffer.ts | 2 + 4 files changed, 56 insertions(+), 106 deletions(-) diff --git a/sdk/multisig/src/rpc/multisigCreate.ts b/sdk/multisig/src/rpc/multisigCreate.ts index 4f1acc8e..070150ed 100644 --- a/sdk/multisig/src/rpc/multisigCreate.ts +++ b/sdk/multisig/src/rpc/multisigCreate.ts @@ -57,7 +57,7 @@ export async function multisigCreate({ tx.sign([creator, createKey]); try { - return await connection.sendRawTransaction(tx.serialize(), sendOptions); + return await connection.sendTransaction(tx, sendOptions); } catch (err) { translateAndThrowAnchorError(err); } diff --git a/tests/index.ts b/tests/index.ts index 5ea50433..330f997b 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,25 +1,25 @@ // The order of imports is the order the test suite will run in. import "./suites/program-config-init" -import "./suites/account-migrations"; -import "./suites/examples/batch-sol-transfer"; -import "./suites/examples/create-mint"; -import "./suites/examples/immediate-execution"; -import "./suites/examples/spending-limits"; -import "./suites/examples/transaction-buffer"; -import "./suites/instructions/batchAccountsClose"; -import "./suites/instructions/cancelRealloc"; -import "./suites/instructions/configTransactionAccountsClose"; -import "./suites/instructions/configTransactionExecute"; +// import "./suites/account-migrations"; +// import "./suites/examples/batch-sol-transfer"; +// import "./suites/examples/create-mint"; +// import "./suites/examples/immediate-execution"; +// import "./suites/examples/spending-limits"; +// import "./suites/examples/transaction-buffer"; +// import "./suites/instructions/batchAccountsClose"; +// import "./suites/instructions/cancelRealloc"; +// import "./suites/instructions/configTransactionAccountsClose"; +// import "./suites/instructions/configTransactionExecute"; import "./suites/instructions/multisigCreate"; -import "./suites/instructions/multisigCreateV2"; -import "./suites/instructions/multisigSetRentCollector"; -import "./suites/instructions/transactionBufferClose"; -import "./suites/instructions/transactionBufferCreate"; -import "./suites/instructions/transactionBufferExtend"; -import "./suites/instructions/vaultBatchTransactionAccountClose"; -import "./suites/instructions/vaultTransactionAccountsClose"; -import "./suites/instructions/vaultTransactionCreateFromBuffer"; -import "./suites/multisig-sdk"; +// import "./suites/instructions/multisigCreateV2"; +// import "./suites/instructions/multisigSetRentCollector"; +// import "./suites/instructions/transactionBufferClose"; +// import "./suites/instructions/transactionBufferCreate"; +// import "./suites/instructions/transactionBufferExtend"; +// import "./suites/instructions/vaultBatchTransactionAccountClose"; +// import "./suites/instructions/vaultTransactionAccountsClose"; +// import "./suites/instructions/vaultTransactionCreateFromBuffer"; +// import "./suites/multisig-sdk"; // // Uncomment to enable the heapTest instruction testing // //import "./suites/instructions/heapTest"; diff --git a/tests/suites/instructions/multisigCreate.ts b/tests/suites/instructions/multisigCreate.ts index ba0d16c4..e9df7c90 100644 --- a/tests/suites/instructions/multisigCreate.ts +++ b/tests/suites/instructions/multisigCreate.ts @@ -1,16 +1,15 @@ +import { Keypair } from "@solana/web3.js"; import * as multisig from "@sqds/multisig"; +import assert from "assert"; import { - comparePubkeys, createAutonomousMultisig, createControlledMultisig, createLocalhostConnection, generateFundedKeypair, generateMultisigMembers, getTestProgramId, - TestMembers, + TestMembers } from "../../utils"; -import { Keypair, PublicKey } from "@solana/web3.js"; -import assert from "assert"; const { Multisig } = multisig.accounts; const { Permission, Permissions } = multisig.types; @@ -57,7 +56,7 @@ describe("Instructions / multisig_create", () => { sendOptions: { skipPreflight: true }, programId, }), - /Found multiple members with the same pubkey/ + /Deprecated/ ); }); @@ -123,7 +122,7 @@ describe("Instructions / multisig_create", () => { sendOptions: { skipPreflight: true }, programId, }), - /Members don't include any proposers/ + /Deprecated/ ); }); @@ -158,7 +157,7 @@ describe("Instructions / multisig_create", () => { sendOptions: { skipPreflight: true }, programId, }), - /Member has unknown permission/ + /Deprecated/ ); }); @@ -191,7 +190,7 @@ describe("Instructions / multisig_create", () => { sendOptions: { skipPreflight: true }, programId, }), - /Invalid threshold, must be between 1 and number of members/ + /Deprecated/ ); }); @@ -239,94 +238,43 @@ describe("Instructions / multisig_create", () => { sendOptions: { skipPreflight: true }, programId, }), - /Invalid threshold, must be between 1 and number of members with Vote permission/ + /Deprecated/ ); }); - it("create a new autonomous multisig", async () => { + it("error: create a new autonomous multisig (deprecated)", async () => { const createKey = Keypair.generate(); - - const [multisigPda, multisigBump] = await createAutonomousMultisig({ - connection, - createKey, - members, - threshold: 2, - timeLock: 0, - programId, - }); - - const multisigAccount = await Multisig.fromAccountAddress( - connection, - multisigPda - ); - assert.strictEqual( - multisigAccount.configAuthority.toBase58(), - PublicKey.default.toBase58() - ); - assert.strictEqual(multisigAccount.threshold, 2); - assert.deepEqual( - multisigAccount.members, - [ - { - key: members.almighty.publicKey, - permissions: { - mask: Permission.Initiate | Permission.Vote | Permission.Execute, - }, - }, - { - key: members.proposer.publicKey, - permissions: { - mask: Permission.Initiate, - }, - }, - { - key: members.voter.publicKey, - permissions: { - mask: Permission.Vote, - }, - }, - { - key: members.executor.publicKey, - permissions: { - mask: Permission.Execute, - }, - }, - ].sort((a, b) => comparePubkeys(a.key, b.key)) - ); - assert.strictEqual(multisigAccount.rentCollector, null); - assert.strictEqual(multisigAccount.transactionIndex.toString(), "0"); - assert.strictEqual(multisigAccount.staleTransactionIndex.toString(), "0"); - assert.strictEqual( - multisigAccount.createKey.toBase58(), - createKey.publicKey.toBase58() + assert.rejects( + () => + createAutonomousMultisig({ + connection, + createKey, + members, + threshold: 2, + timeLock: 0, + programId, + }), + /Deprecated/ ); - assert.strictEqual(multisigAccount.bump, multisigBump); + }); - it("create a new controlled multisig", async () => { + it("error: create a new controlled multisig (deprecated)", async () => { const createKey = Keypair.generate(); const configAuthority = await generateFundedKeypair(connection); - - const [multisigPda] = await createControlledMultisig({ - connection, - createKey, - configAuthority: configAuthority.publicKey, - members, - threshold: 2, - timeLock: 0, - programId, - }); - - const multisigAccount = await Multisig.fromAccountAddress( - connection, - multisigPda + assert.rejects( + () => + createControlledMultisig({ + connection, + createKey, + configAuthority: configAuthority.publicKey, + members, + threshold: 2, + timeLock: 0, + programId, + }), + /Deprecated/ ); - assert.strictEqual( - multisigAccount.configAuthority.toBase58(), - configAuthority.publicKey.toBase58() - ); - // We can skip the rest of the assertions because they are already tested - // in the previous case and will be the same here. }); }); diff --git a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts index fd7f3a1b..ab15fd10 100644 --- a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts +++ b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts @@ -110,6 +110,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + members.proposer.publicKey.toBuffer(), Uint8Array.from([bufferIndex]), ], programId @@ -338,6 +339,7 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { Buffer.from("multisig"), multisigPda.toBuffer(), Buffer.from("transaction_buffer"), + members.proposer.publicKey.toBuffer(), Uint8Array.from([bufferIndex]), ], programId From d7f16989b3d8f73ba3c1a7a9794d82af055df498 Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Mon, 14 Oct 2024 19:59:43 +0200 Subject: [PATCH 38/47] add: custom deprecation error --- programs/squads_multisig_program/src/errors.rs | 2 ++ programs/squads_multisig_program/src/lib.rs | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/programs/squads_multisig_program/src/errors.rs b/programs/squads_multisig_program/src/errors.rs index 933e229b..5384443c 100644 --- a/programs/squads_multisig_program/src/errors.rs +++ b/programs/squads_multisig_program/src/errors.rs @@ -90,4 +90,6 @@ pub enum MultisigError { FinalBufferSizeExceeded, #[msg("Final buffer size mismatch")] FinalBufferSizeMismatch, + #[msg("multisig_create has been deprecated. Use multisig_create_v2 instead.")] + MultisigCreateDeprecated, } diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index e5931fb4..2ae5b8d9 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -40,6 +40,8 @@ declare_id!("GyhGAqjokLwF9UXdQ2dR5Zwiup242j4mX4J1tSMKyAmD"); #[program] pub mod squads_multisig_program { + use errors::MultisigError; + use super::*; /// Initialize the program config. @@ -76,8 +78,8 @@ pub mod squads_multisig_program { /// Create a multisig. pub fn multisig_create(_ctx: Context) -> Result<()> { - msg!("WARNING: multisig_create has been deprecated. Please use multisig_create_V2 instead."); - Err(ErrorCode::Deprecated.into()) + msg!("multisig_create has been deprecated. Use multisig_create_v2 instead."); + Err(MultisigError::MultisigCreateDeprecated.into()) } /// Create a multisig. From e0b6c5542073886658409f683fce0d567dd65d1e Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Mon, 14 Oct 2024 20:03:15 +0200 Subject: [PATCH 39/47] tests: uncomment and fix --- sdk/multisig/idl/squads_multisig_program.json | 5 +++ sdk/multisig/src/generated/errors/index.ts | 25 ++++++++++++ tests/index.ts | 38 +++++++++---------- 3 files changed, 49 insertions(+), 19 deletions(-) diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index a60cc763..58a7aa4d 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -3405,6 +3405,11 @@ "code": 6043, "name": "FinalBufferSizeMismatch", "msg": "Final buffer size mismatch" + }, + { + "code": 6044, + "name": "MultisigCreateDeprecated", + "msg": "multisig_create has been deprecated. Use multisig_create_v2 instead." } ], "metadata": { diff --git a/sdk/multisig/src/generated/errors/index.ts b/sdk/multisig/src/generated/errors/index.ts index 4f32f4d2..6678b936 100644 --- a/sdk/multisig/src/generated/errors/index.ts +++ b/sdk/multisig/src/generated/errors/index.ts @@ -1013,6 +1013,31 @@ createErrorFromNameLookup.set( () => new FinalBufferSizeMismatchError() ) +/** + * MultisigCreateDeprecated: 'multisig_create has been deprecated. Use multisig_create_v2 instead.' + * + * @category Errors + * @category generated + */ +export class MultisigCreateDeprecatedError extends Error { + readonly code: number = 0x179c + readonly name: string = 'MultisigCreateDeprecated' + constructor() { + super( + 'multisig_create has been deprecated. Use multisig_create_v2 instead.' + ) + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, MultisigCreateDeprecatedError) + } + } +} + +createErrorFromCodeLookup.set(0x179c, () => new MultisigCreateDeprecatedError()) +createErrorFromNameLookup.set( + 'MultisigCreateDeprecated', + () => new MultisigCreateDeprecatedError() +) + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/tests/index.ts b/tests/index.ts index 330f997b..5ea50433 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -1,25 +1,25 @@ // The order of imports is the order the test suite will run in. import "./suites/program-config-init" -// import "./suites/account-migrations"; -// import "./suites/examples/batch-sol-transfer"; -// import "./suites/examples/create-mint"; -// import "./suites/examples/immediate-execution"; -// import "./suites/examples/spending-limits"; -// import "./suites/examples/transaction-buffer"; -// import "./suites/instructions/batchAccountsClose"; -// import "./suites/instructions/cancelRealloc"; -// import "./suites/instructions/configTransactionAccountsClose"; -// import "./suites/instructions/configTransactionExecute"; +import "./suites/account-migrations"; +import "./suites/examples/batch-sol-transfer"; +import "./suites/examples/create-mint"; +import "./suites/examples/immediate-execution"; +import "./suites/examples/spending-limits"; +import "./suites/examples/transaction-buffer"; +import "./suites/instructions/batchAccountsClose"; +import "./suites/instructions/cancelRealloc"; +import "./suites/instructions/configTransactionAccountsClose"; +import "./suites/instructions/configTransactionExecute"; import "./suites/instructions/multisigCreate"; -// import "./suites/instructions/multisigCreateV2"; -// import "./suites/instructions/multisigSetRentCollector"; -// import "./suites/instructions/transactionBufferClose"; -// import "./suites/instructions/transactionBufferCreate"; -// import "./suites/instructions/transactionBufferExtend"; -// import "./suites/instructions/vaultBatchTransactionAccountClose"; -// import "./suites/instructions/vaultTransactionAccountsClose"; -// import "./suites/instructions/vaultTransactionCreateFromBuffer"; -// import "./suites/multisig-sdk"; +import "./suites/instructions/multisigCreateV2"; +import "./suites/instructions/multisigSetRentCollector"; +import "./suites/instructions/transactionBufferClose"; +import "./suites/instructions/transactionBufferCreate"; +import "./suites/instructions/transactionBufferExtend"; +import "./suites/instructions/vaultBatchTransactionAccountClose"; +import "./suites/instructions/vaultTransactionAccountsClose"; +import "./suites/instructions/vaultTransactionCreateFromBuffer"; +import "./suites/multisig-sdk"; // // Uncomment to enable the heapTest instruction testing // //import "./suites/instructions/heapTest"; From 87ae2c977dd854d029d076c24ce8fe2df0f82d5c Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Tue, 15 Oct 2024 23:29:44 +0200 Subject: [PATCH 40/47] (tests)fix: broken tests from multisig_create deprecation --- tests/suites/multisig-sdk.ts | 27 ++++++++----- tests/utils.ts | 73 +++++++++--------------------------- 2 files changed, 34 insertions(+), 66 deletions(-) diff --git a/tests/suites/multisig-sdk.ts b/tests/suites/multisig-sdk.ts index 5bdaefdb..db2da09c 100644 --- a/tests/suites/multisig-sdk.ts +++ b/tests/suites/multisig-sdk.ts @@ -2084,7 +2084,7 @@ describe("Multisig SDK", () => { // Create a config transaction. const transactionIndex = 2n; let newVotingMember = new Keypair(); - + const [proposalPda] = multisig.getProposalPda({ multisigPda, transactionIndex, @@ -2101,7 +2101,7 @@ describe("Multisig SDK", () => { programId, }); await connection.confirmTransaction(signature); - + // Create a proposal for the transaction. signature = await multisig.rpc.proposalCreate({ connection, @@ -2123,7 +2123,7 @@ describe("Multisig SDK", () => { programId, }); await connection.confirmTransaction(signature); - + // Approve the proposal 2. signature = await multisig.rpc.proposalApprove({ connection, @@ -2154,7 +2154,7 @@ describe("Multisig SDK", () => { programId, }); await connection.confirmTransaction(signature); - + // Proposal is now ready to execute, cast the 2 cancels using the new functionality. signature = await multisig.rpc.proposalCancelV2({ connection, @@ -2165,7 +2165,7 @@ describe("Multisig SDK", () => { programId, }); await connection.confirmTransaction(signature); - + // Proposal status must be "Cancelled". proposalAccount = await Proposal.fromAccountAddress( connection, @@ -2173,7 +2173,7 @@ describe("Multisig SDK", () => { ); assert.ok(multisig.types.isProposalStatusCancelled(proposalAccount.status)); }); - + }); describe("vault_transaction_execute", () => { @@ -2353,13 +2353,20 @@ describe("Multisig SDK", () => { index: 0, programId, }); - + const programConfigPda = multisig.getProgramConfigPda({ programId })[0]; + const programConfig = await multisig.accounts.ProgramConfig.fromAccountAddress( + connection, + programConfigPda + ); + const treasury = programConfig.treasury; const multisigCreateArgs: Parameters< - typeof multisig.transactions.multisigCreate + typeof multisig.transactions.multisigCreateV2 >[0] = { blockhash: (await connection.getLatestBlockhash()).blockhash, createKey: createKey.publicKey, creator: multisigCreator.publicKey, + treasury: treasury, + rentCollector: null, multisigPda, configAuthority, timeLock: 0, @@ -2374,7 +2381,7 @@ describe("Multisig SDK", () => { }; const createMultisigTxWithoutMemo = - multisig.transactions.multisigCreate(multisigCreateArgs); + multisig.transactions.multisigCreateV2(multisigCreateArgs); const availableMemoSize = multisig.utils.getAvailableMemoSize( createMultisigTxWithoutMemo @@ -2382,7 +2389,7 @@ describe("Multisig SDK", () => { const memo = "a".repeat(availableMemoSize); - const createMultisigTxWithMemo = multisig.transactions.multisigCreate({ + const createMultisigTxWithMemo = multisig.transactions.multisigCreateV2({ ...multisigCreateArgs, memo, }); diff --git a/tests/utils.ts b/tests/utils.ts index 0d8cd5a0..8cb14d99 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,3 +1,4 @@ +import { createMemoInstruction } from "@solana/spl-memo"; import { Connection, Keypair, @@ -8,10 +9,9 @@ import { VersionedTransaction, } from "@solana/web3.js"; import * as multisig from "@sqds/multisig"; +import assert from "assert"; import { readFileSync } from "fs"; import path from "path"; -import { createMemoInstruction } from "@solana/spl-memo"; -import assert from "assert"; const { Permission, Permissions } = multisig.types; const { Proposal } = multisig.accounts; @@ -127,8 +127,8 @@ export function createLocalhostConnection() { export const getLogs = async (connection: Connection, signature: string): Promise => { const tx = await connection.getTransaction( - signature, - { commitment: "confirmed" } + signature, + { commitment: "confirmed" } ) return tx!.meta!.logMessages || [] } @@ -148,42 +148,22 @@ export async function createAutonomousMultisig({ connection: Connection; programId: PublicKey; }) { - const creator = await generateFundedKeypair(connection); const [multisigPda, multisigBump] = multisig.getMultisigPda({ createKey: createKey.publicKey, programId, }); - const signature = await multisig.rpc.multisigCreate({ + await createAutonomousMultisigV2({ connection, - creator, - multisigPda, - configAuthority: null, - timeLock, + createKey, + members, threshold, - members: [ - { key: members.almighty.publicKey, permissions: Permissions.all() }, - { - key: members.proposer.publicKey, - permissions: Permissions.fromPermissions([Permission.Initiate]), - }, - { - key: members.voter.publicKey, - permissions: Permissions.fromPermissions([Permission.Vote]), - }, - { - key: members.executor.publicKey, - permissions: Permissions.fromPermissions([Permission.Execute]), - }, - ], - createKey: createKey, - sendOptions: { skipPreflight: true }, + timeLock, + rentCollector: null, programId, }); - await connection.confirmTransaction(signature); - return [multisigPda, multisigBump] as const; } @@ -269,42 +249,23 @@ export async function createControlledMultisig({ connection: Connection; programId: PublicKey; }) { - const creator = await generateFundedKeypair(connection); const [multisigPda, multisigBump] = multisig.getMultisigPda({ createKey: createKey.publicKey, programId, }); - const signature = await multisig.rpc.multisigCreate({ + await createControlledMultisigV2({ connection, - creator, - multisigPda, - configAuthority, - timeLock, + createKey, + members, + rentCollector: null, threshold, - members: [ - { key: members.almighty.publicKey, permissions: Permissions.all() }, - { - key: members.proposer.publicKey, - permissions: Permissions.fromPermissions([Permission.Initiate]), - }, - { - key: members.voter.publicKey, - permissions: Permissions.fromPermissions([Permission.Vote]), - }, - { - key: members.executor.publicKey, - permissions: Permissions.fromPermissions([Permission.Execute]), - }, - ], - createKey: createKey, - sendOptions: { skipPreflight: true }, - programId, + configAuthority: configAuthority, + timeLock, + programId }); - await connection.confirmTransaction(signature); - return [multisigPda, multisigBump] as const; } @@ -1247,7 +1208,7 @@ export async function processBufferInChunks( const ix = multisig.generated.createTransactionBufferExtendInstruction( { multisig: multisigPda, - transactionBuffer:bufferAccount, + transactionBuffer: bufferAccount, creator: member.publicKey, }, { From 6d5235da621a2e9b7379ea358e48760e981053be Mon Sep 17 00:00:00 2001 From: Iceomatic <89707822+iceomatic@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:14:44 +0200 Subject: [PATCH 41/47] bump: program version to 2.1.0 --- Cargo.lock | 2 +- programs/squads_multisig_program/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3aa15976..ee960a8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4164,7 +4164,7 @@ dependencies = [ [[package]] name = "squads-multisig-program" -version = "2.0.0" +version = "2.1.0" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/programs/squads_multisig_program/Cargo.toml b/programs/squads_multisig_program/Cargo.toml index 4fe06e76..ac6ef1c4 100644 --- a/programs/squads_multisig_program/Cargo.toml +++ b/programs/squads_multisig_program/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "squads-multisig-program" -version = "2.0.0" +version = "2.1.0" description = "Squads Multisig Program V4" edition = "2021" license-file = "../../LICENSE" From 7360d8a59a11e702d4422e243d7771fe72d764db Mon Sep 17 00:00:00 2001 From: Iceomatic Date: Thu, 7 Nov 2024 20:59:39 +0100 Subject: [PATCH 42/47] fix(program): space calculation transaction_buffer --- .../squads_multisig_program/src/state/transaction_buffer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/programs/squads_multisig_program/src/state/transaction_buffer.rs b/programs/squads_multisig_program/src/state/transaction_buffer.rs index 945276d1..9207ea46 100644 --- a/programs/squads_multisig_program/src/state/transaction_buffer.rs +++ b/programs/squads_multisig_program/src/state/transaction_buffer.rs @@ -34,8 +34,8 @@ impl TransactionBuffer { 8 + // anchor account discriminator 32 + // multisig 32 + // creator - 8 + // buffer_index - 8 + // vault_index + 1 + // buffer_index + 1 + // vault_index 32 + // transaction_message_hash 2 + // final_buffer_size 4 + // vec length bytes From b9b1c368ba63c45005b81ac0ea1bf0d415e55b95 Mon Sep 17 00:00:00 2001 From: Iceomatic Date: Thu, 7 Nov 2024 21:02:02 +0100 Subject: [PATCH 43/47] bump: Anchor.toml solana-cli to 1.18.16 --- Anchor.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Anchor.toml b/Anchor.toml index e29658e6..e3ae6f21 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -1,6 +1,6 @@ [toolchain] anchor_version = "0.29.0" # `anchor-cli` version to use -solana_version = "1.17.0" # Solana version to use +solana_version = "1.18.16" # Solana version to use [features] seeds = false From 39c92c10dcc0b9cf7c8c6916845cf40d72daac5b Mon Sep 17 00:00:00 2001 From: Iceomatic Date: Thu, 7 Nov 2024 21:03:23 +0100 Subject: [PATCH 44/47] bump(idl): to 2.1.0 --- sdk/multisig/idl/squads_multisig_program.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index 58a7aa4d..ae557d20 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -1,5 +1,5 @@ { - "version": "2.0.0", + "version": "2.1.0", "name": "squads_multisig_program", "instructions": [ { From 2c4d16953504fd26798c4a7a3ff4092f1bbf6a70 Mon Sep 17 00:00:00 2001 From: Iceomatic Date: Tue, 19 Nov 2024 16:07:14 +0100 Subject: [PATCH 45/47] modify: max transaction_buffer size --- .../squads_multisig_program/src/state/transaction_buffer.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/programs/squads_multisig_program/src/state/transaction_buffer.rs b/programs/squads_multisig_program/src/state/transaction_buffer.rs index 9207ea46..bd9a8553 100644 --- a/programs/squads_multisig_program/src/state/transaction_buffer.rs +++ b/programs/squads_multisig_program/src/state/transaction_buffer.rs @@ -3,7 +3,9 @@ use anchor_lang::solana_program::hash::hash; use crate::errors::MultisigError; -pub const MAX_BUFFER_SIZE: usize = 4000; +// Maximum PDA allocation size in an inner ix is 10240 bytes. +// 10240 - account contents = 10128 bytes +pub const MAX_BUFFER_SIZE: usize = 10128 ; #[account] #[derive(Default, Debug)] From 64af7330413d5c85cbbccfd8c27a05d45b6e666f Mon Sep 17 00:00:00 2001 From: Iceomatic Date: Tue, 19 Nov 2024 20:51:46 +0100 Subject: [PATCH 46/47] fix(tests): accomodate new max buffer size --- .../instructions/transactionBufferCreate.ts | 2 +- .../vaultTransactionCreateFromBuffer.ts | 179 +++++++++++++++++- 2 files changed, 178 insertions(+), 3 deletions(-) diff --git a/tests/suites/instructions/transactionBufferCreate.ts b/tests/suites/instructions/transactionBufferCreate.ts index 2ab1acc4..2de22078 100644 --- a/tests/suites/instructions/transactionBufferCreate.ts +++ b/tests/suites/instructions/transactionBufferCreate.ts @@ -636,7 +636,7 @@ describe("Instructions / transaction_buffer_create", () => { bufferIndex: bufferIndex, vaultIndex: 0, finalBufferHash: Array.from(messageHash), - finalBufferSize: 4001, + finalBufferSize: 10128 + 1, buffer: largeBuffer, } as TransactionBufferCreateArgs, } as TransactionBufferCreateInstructionArgs, diff --git a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts index ab15fd10..82404864 100644 --- a/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts +++ b/tests/suites/instructions/vaultTransactionCreateFromBuffer.ts @@ -6,7 +6,8 @@ import { SystemProgram, Transaction, TransactionMessage, - VersionedTransaction + VersionedTransaction, + ComputeBudgetProgram } from "@solana/web3.js"; import * as multisig from "@sqds/multisig"; import { @@ -18,7 +19,6 @@ import { VaultTransactionCreateFromBufferInstructionArgs } from "@sqds/multisig/lib/generated"; import assert from "assert"; -import { BN } from "bn.js"; import * as crypto from "crypto"; import { TestMembers, @@ -498,4 +498,179 @@ describe("Instructions / vault_transaction_create_from_buffer", () => { assert.match(logs, /Access violation in heap section at address/); }); + + it("handles buffer sizes up to 10128 bytes", async () => { + const transactionIndex = 2n; + const bufferIndex = 1; + const CHUNK_SIZE = 900; // Safe chunk size for buffer extension + + // Create dummy instruction with 200 bytes of random data + function createLargeInstruction() { + const randomData = crypto.randomBytes(200); + return { + programId: SystemProgram.programId, + keys: [{ pubkey: vaultPda, isSigner: false, isWritable: true }], + data: randomData + }; + } + + // Create 45 instructions to get close to but not exceed 10128 bytes + const instructions = Array(45).fill(null).map(() => createLargeInstruction()); + + const testTransferMessage = new TransactionMessage({ + payerKey: vaultPda, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: instructions, + }); + + // Serialize the message + const messageBuffer = multisig.utils.transactionMessageToMultisigTransactionMessageBytes({ + message: testTransferMessage, + addressLookupTableAccounts: [], + vaultPda, + }); + + console.log(`Total message buffer size: ${messageBuffer.length} bytes`); + + // Verify buffer size is within limits + if (messageBuffer.length > 10128) { + throw new Error("Buffer size exceeds 10128 byte limit"); + } + + const [transactionBuffer] = await PublicKey.findProgramAddressSync( + [ + Buffer.from("multisig"), + multisigPda.toBuffer(), + Buffer.from("transaction_buffer"), + members.proposer.publicKey.toBuffer(), + Uint8Array.from([bufferIndex]), + ], + programId + ); + + const messageHash = crypto.createHash("sha256").update(messageBuffer).digest(); + + // Calculate number of chunks needed + const numChunks = Math.ceil(messageBuffer.length / CHUNK_SIZE); + console.log(`Uploading in ${numChunks} chunks`); + + // Initial buffer creation with first chunk + const firstChunk = messageBuffer.slice(0, CHUNK_SIZE); + const createIx = multisig.generated.createTransactionBufferCreateInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + rentPayer: members.proposer.publicKey, + systemProgram: SystemProgram.programId, + }, + { + args: { + bufferIndex, + vaultIndex: 0, + finalBufferHash: Array.from(messageHash), + finalBufferSize: messageBuffer.length, + buffer: firstChunk, + } as TransactionBufferCreateArgs, + } as TransactionBufferCreateInstructionArgs, + programId + ); + + // Send initial chunk + const createTx = new VersionedTransaction( + new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [createIx], + }).compileToV0Message() + ); + createTx.sign([members.proposer]); + const signature = await connection.sendTransaction(createTx, { skipPreflight: true }); + await connection.confirmTransaction(signature); + + // Extend buffer with remaining chunks + for (let i = 1; i < numChunks; i++) { + const start = i * CHUNK_SIZE; + const end = Math.min(start + CHUNK_SIZE, messageBuffer.length); + const chunk = messageBuffer.slice(start, end); + + const extendIx = multisig.generated.createTransactionBufferExtendInstruction( + { + multisig: multisigPda, + transactionBuffer, + creator: members.proposer.publicKey, + }, + { + args: { + buffer: chunk, + } as TransactionBufferExtendArgs, + } as TransactionBufferExtendInstructionArgs, + programId + ); + + const extendTx = new VersionedTransaction( + new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [extendIx], + }).compileToV0Message() + ); + extendTx.sign([members.proposer]); + const sig = await connection.sendRawTransaction(extendTx.serialize(), { skipPreflight: true }); + await connection.confirmTransaction(sig); + } + console.log("Buffer upload complete"); + // Verify final buffer size + const bufferAccount = await connection.getAccountInfo(transactionBuffer); + const [bufferData] = await multisig.generated.TransactionBuffer.fromAccountInfo(bufferAccount!); + assert.equal(bufferData.buffer.length, messageBuffer.length); + + // Create transaction from buffer + const [transactionPda] = multisig.getTransactionPda({ + multisigPda, + index: transactionIndex, + programId, + }); + + const createFromBufferIx = multisig.generated.createVaultTransactionCreateFromBufferInstruction( + { + vaultTransactionCreateItemMultisig: multisigPda, + vaultTransactionCreateItemTransaction: transactionPda, + vaultTransactionCreateItemCreator: members.proposer.publicKey, + vaultTransactionCreateItemRentPayer: members.proposer.publicKey, + vaultTransactionCreateItemSystemProgram: SystemProgram.programId, + creator: members.proposer.publicKey, + transactionBuffer: transactionBuffer, + }, + { + args: { + vaultIndex: 0, + ephemeralSigners: 0, + transactionMessage: new Uint8Array(6).fill(0), + memo: null, + } as VaultTransactionCreateArgs, + } as VaultTransactionCreateFromBufferInstructionArgs, + programId + ); + const requestHeapIx = ComputeBudgetProgram.requestHeapFrame({ + bytes: 262144 + }) + const finalTx = new VersionedTransaction( + new TransactionMessage({ + payerKey: members.proposer.publicKey, + recentBlockhash: (await connection.getLatestBlockhash()).blockhash, + instructions: [requestHeapIx, createFromBufferIx], + }).compileToV0Message() + ); + finalTx.sign([members.proposer]); + const finalSignature = await connection.sendRawTransaction(finalTx.serialize(), { skipPreflight: true }); + await connection.confirmTransaction(finalSignature); + + // Verify created transaction + const transactionInfo = await multisig.accounts.VaultTransaction.fromAccountAddress( + connection, + transactionPda + ); + assert.equal(transactionInfo.message.instructions.length, 45); + }); }); From dcac867070a3073929e2240a053780c324f4c29f Mon Sep 17 00:00:00 2001 From: Iceomatic Date: Fri, 22 Nov 2024 19:05:59 +0100 Subject: [PATCH 47/47] fix(tests): custom heap --- tests/suites/examples/custom-heap.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/suites/examples/custom-heap.ts b/tests/suites/examples/custom-heap.ts index 12a2679f..cfe99ad1 100644 --- a/tests/suites/examples/custom-heap.ts +++ b/tests/suites/examples/custom-heap.ts @@ -12,7 +12,7 @@ import * as multisig from "@sqds/multisig"; import { TransactionBufferCreateArgs, TransactionBufferCreateInstructionArgs, - VaultTransactionCreateFromBufferArgs, + VaultTransactionCreateArgs, VaultTransactionCreateFromBufferInstructionArgs, } from "@sqds/multisig/lib/generated"; import assert from "assert"; @@ -205,18 +205,19 @@ describe("Examples / Custom Heap Usage", () => { const thirdIx = multisig.generated.createVaultTransactionCreateFromBufferInstruction( { - multisig: multisigPda, + vaultTransactionCreateItemMultisig: multisigPda, transactionBuffer, - transaction: transactionPda, + vaultTransactionCreateItemTransaction: transactionPda, + vaultTransactionCreateItemCreator: members.proposer.publicKey, creator: members.proposer.publicKey, - rentPayer: members.proposer.publicKey, - systemProgram: SystemProgram.programId, + vaultTransactionCreateItemRentPayer: members.proposer.publicKey, + vaultTransactionCreateItemSystemProgram: SystemProgram.programId, }, { args: { ephemeralSigners: 0, memo: null, - } as VaultTransactionCreateFromBufferArgs, + } as VaultTransactionCreateArgs, } as VaultTransactionCreateFromBufferInstructionArgs, programId );