diff --git a/Cargo.lock b/Cargo.lock index 278ee1d9..1e9b170d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3330,6 +3330,12 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "solana-security-txt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" + [[package]] name = "solana-streamer" version = "1.14.16" @@ -3532,7 +3538,7 @@ dependencies = [ [[package]] name = "squads-multisig" -version = "0.0.10" +version = "0.0.11" dependencies = [ "solana-client", "squads-multisig-program", @@ -3541,11 +3547,12 @@ dependencies = [ [[package]] name = "squads-multisig-program" -version = "0.1.2" +version = "0.2.0" dependencies = [ "anchor-lang", "anchor-spl", "solana-address-lookup-table-program", + "solana-security-txt", ] [[package]] diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..1ff90d5d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,88 @@ +## Security Policy & Bug Bounty +Learn more about Squads perpetual bug bounty program +1. Reporting security problems +2. Incident response process +3. Security bug bounties +### Reporting security problems +DO NOT CREATE AN ISSUE to report a security problem. Instead, please send an email to security@sqds.io and provide your GitHub username so we can add you to a new draft security advisory for further discussion. +For security reasons, we do not accept vulnerability disclosures presently via email, nor do we accept attachments when provided via email for vulnerability disclosures. +We suggest that you ensure that multi-factor authentication is enabled on your account prior to submitting. +### Incident response process +In case an incident is discovered or reported, the following process will be followed to contain, respond and remediate: +1. Establish a new draft security advisory +In response to an email to security@sqds.so, a member of the Squads team will: +* create a new draft security advisory for the incident at +* add the reporter's Github user and the squads-protocol/security-incident-response-team group to the draft security advisory +* create a private fork of the repository (grey button towards the bottom of the page) +* respond to the reporter by email, sharing a link to the draft security advisory. +If the advisory is the result of an audit finding, a similar but slightly modified process is followed: +follow the same process as above but add the auditor's GitHub username and begin the title with "[Audit]". +2. Triage +Within the draft security advisory, the Squads protocol team and the reporter will discuss and determine the severity of the issue. The Squads Protocol team will ultimately be the determining party for any aspects related to the severity of the issue (and any associated bounty). +If necessary, members of the squads-protocol/security-incident-response-team group may add other GitHub users to the advisory to assist with triage. +In the event of a non-critical advisory, the Squads team will work to communicate as broadly where possible with relevant stakeholders and interested parties. +3. Prepare fixes +For the affected branches prepare a fix for the issue and push them to the corresponding branch in the private repository associated with the draft security advisory. +Normal CI procedures will not be present within the private repository so you must build from source and manually verify fixes. +Code review from the reporter is ideal, as well as from multiple members of the Squads development team. +4. Ship the patch +Once the fix is accepted, a member of the squads-protocol/security-incident-response-team group should prepare a single patch file for each affected branch. +The commit title for the patch should only contain the advisory id, and not disclose any further details about the incident. +5. Public disclosure and release +Once the fix has been deployed, the patches from the security advisory may be merged into the main source repository. At this time, more broad public disclosure may occur via the official Squads Twitter account and other official mediums of communication. +A new official release for each affected branch should be shipped and upgraded to as quickly as possible. +6. Security advisory bounty accounting and cleanup +If this issue is eligible for a bounty, prefix the title of the security advisory with one of the following, depending on the severity: +* being able to steal funds +* being able to freeze funds or render them inaccessible by their owners +* being able to perform replay attacks on the same chain +* being able to change Squad settings or module settings without consent of owners +Confirm with the reporter that they agree with the severity assessment, and discuss as required to reach a conclusion. +### Security bug bounties +We offer bounties for critical security issues. Please see below for more details. Either a demonstration or a valid bug report is all that's necessary to submit a bug bounty. +A patch to fix the issue isn't required. +#### Ability to Steal Funds +$300,000 USD in locked SOL tokens (locked for 12 months) +* theft of funds without users signature from any account +* theft of funds without users interaction with the Multisig program +* theft of funds that requires users signature - creating a Multisig program that drains funds. +#### Loss of Availability / Ability to Freeze Funds +$200,000 USD in locked SOL tokens (locked for 12 months): +* Ability to freeze a User’s ability to claim funds from a Multisig +#### Replay Attacks +$25,000 USD in locked SOL tokens (locked for 12 months): +* Ability to replay a previously executed transaction involving a Squads Multisig +#### Settings Modifications +$10,000 USD in locked SOL tokens (locked for 12 months): +* Modification of any Multisig or module settings without proper authorization by the owners of the Multisig +### In Scope +Squads V3 on-chain program () is in scope for the bounty program. +### Out of Scope +The following components are out of scope for the bounty program: +* any encrypted credentials, auth tokens, etc. checked into the repo +* bugs in dependencies, please take them upstream! +* attacks that require social engineering +* any files, modules or libraries other than the ones mentioned above +* any points listed as an already known weaknesses +* any points listed in the audit reports +* any points fixed in a newer version. +### Eligibility +The participant submitting the bug report shall follow the process outlined within this document. +Multiple submissions for the same class of exploit are still eligible for compensation, though may be compensated at a lower rate, however these will be assessed on a case-by-case basis. +Participants located in OFAC sanctioned countries may not participate in the bug bounty program at this time. +Duplicate reports +Compensation for duplicative reports will be split among reporters with first to report taking priority using the following equation:\ +`R: total reports `\ +`ri: report priority`\ +`bi: bounty share`\ +`bi = 2 ^ (R - ri) / ((2^R) - 1)` +### Payment of Bug Bounties +Bounties are paid out NET and are reviewed on a rolling basis. We try to respond to every submission within 24 hours, but some may take longer as we assess relevance and impact. +Responsible Disclosure Policy +If you comply with the policies below when reporting a security issue to us, we will not undergo legal action or a law enforcement investigation against you in response to your report. +We ask that: +* You give us reasonable time to investigate and mitigate an issue you report before making public any information about the report or sharing such information with others. +* You make a good faith effort to avoid security violations and disruptions to others, including (but not limited to) destruction of data and interruption or degradation of our services. +* You do not exploit a security issue you discover for any reason. This includes demonstrating additional risk, such as an attempted compromise of sensitive company data or probing for additional issues. +* You have not violated any other applicable laws or regulations. +* You are not currently subject to any U.S. sanctions administered by the Office of Foreign Assets Control of the U.S. Department of the Treasury (“OFAC”). diff --git a/programs/squads_multisig_program/Cargo.toml b/programs/squads_multisig_program/Cargo.toml index 26bb65c0..c2695acf 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 = "0.1.2" +version = "0.2.0" description = "Squads Multisig Program V4" edition = "2021" license-file = "../../LICENSE" @@ -20,3 +20,4 @@ default = [] anchor-lang = { version = "=0.27.0", features = ["allow-missing-optionals"] } anchor-spl = { version="=0.27.0", features=["token"] } solana-address-lookup-table-program = "=1.14.16" +solana-security-txt = "1.1.1" diff --git a/programs/squads_multisig_program/src/errors.rs b/programs/squads_multisig_program/src/errors.rs index 0cdeb052..1467ee5a 100644 --- a/programs/squads_multisig_program/src/errors.rs +++ b/programs/squads_multisig_program/src/errors.rs @@ -62,4 +62,6 @@ pub enum MultisigError { UnknownPermission, #[msg("Account is protected, it cannot be passed into a CPI as writable")] ProtectedAccount, + #[msg("Time lock exceeds the maximum allowed (90 days)")] + TimeLockExceedsMaxAllowed, } diff --git a/programs/squads_multisig_program/src/instructions/batch_add_transaction.rs b/programs/squads_multisig_program/src/instructions/batch_add_transaction.rs index d39b4190..685661cb 100644 --- a/programs/squads_multisig_program/src/instructions/batch_add_transaction.rs +++ b/programs/squads_multisig_program/src/instructions/batch_add_transaction.rs @@ -79,6 +79,7 @@ impl BatchAddTransaction<'_> { multisig, member, proposal, + batch, .. } = self; @@ -91,6 +92,8 @@ impl BatchAddTransaction<'_> { multisig.member_has_permission(member.key(), Permission::Initiate), MultisigError::Unauthorized ); + // Only batch creator can add transactions to it. + require!(member.key() == batch.creator, MultisigError::Unauthorized); // `proposal` require!( diff --git a/programs/squads_multisig_program/src/instructions/config_transaction_create.rs b/programs/squads_multisig_program/src/instructions/config_transaction_create.rs index 369266bd..aac2f8b0 100644 --- a/programs/squads_multisig_program/src/instructions/config_transaction_create.rs +++ b/programs/squads_multisig_program/src/instructions/config_transaction_create.rs @@ -44,7 +44,7 @@ pub struct ConfigTransactionCreate<'info> { } impl ConfigTransactionCreate<'_> { - fn validate(&self) -> Result<()> { + fn validate(&self, args: &ConfigTransactionCreateArgs) -> Result<()> { // multisig require_keys_eq!( self.multisig.config_authority, @@ -63,17 +63,30 @@ impl ConfigTransactionCreate<'_> { MultisigError::Unauthorized ); + // args + + // Config transaction must have at least one action + require!(!args.actions.is_empty(), MultisigError::NoActions); + + // time_lock must not exceed the maximum allowed. + for action in &args.actions { + if let ConfigAction::SetTimeLock { new_time_lock, .. } = action { + require!( + *new_time_lock <= MAX_TIME_LOCK, + MultisigError::TimeLockExceedsMaxAllowed + ); + } + } + Ok(()) } /// Create a new config transaction. - #[access_control(ctx.accounts.validate())] + #[access_control(ctx.accounts.validate(&args))] pub fn config_transaction_create( ctx: Context, args: ConfigTransactionCreateArgs, ) -> Result<()> { - require!(!args.actions.is_empty(), MultisigError::NoActions); - let multisig = &mut ctx.accounts.multisig; let transaction = &mut ctx.accounts.transaction; let creator = &mut ctx.accounts.creator; diff --git a/programs/squads_multisig_program/src/instructions/multisig_config.rs b/programs/squads_multisig_program/src/instructions/multisig_config.rs index 8650b441..85057353 100644 --- a/programs/squads_multisig_program/src/instructions/multisig_config.rs +++ b/programs/squads_multisig_program/src/instructions/multisig_config.rs @@ -105,10 +105,10 @@ impl MultisigConfig<'_> { multisig.add_member(new_member); - multisig.invariant()?; - multisig.invalidate_prior_transactions(); + multisig.invariant()?; + Ok(()) } @@ -136,10 +136,10 @@ impl MultisigConfig<'_> { .expect("didn't expect more that `u16::MAX` members"); }; - multisig.invariant()?; - multisig.invalidate_prior_transactions(); + multisig.invariant()?; + Ok(()) } @@ -156,10 +156,10 @@ impl MultisigConfig<'_> { multisig.threshold = new_threshold; - multisig.invariant()?; - multisig.invalidate_prior_transactions(); + multisig.invariant()?; + Ok(()) } @@ -173,10 +173,10 @@ impl MultisigConfig<'_> { multisig.time_lock = args.time_lock; - multisig.invariant()?; - multisig.invalidate_prior_transactions(); + multisig.invariant()?; + Ok(()) } @@ -193,10 +193,10 @@ impl MultisigConfig<'_> { multisig.config_authority = args.config_authority; - multisig.invariant()?; - multisig.invalidate_prior_transactions(); + multisig.invariant()?; + Ok(()) } } diff --git a/programs/squads_multisig_program/src/instructions/multisig_create.rs b/programs/squads_multisig_program/src/instructions/multisig_create.rs index 4fe3679d..1b1ad389 100644 --- a/programs/squads_multisig_program/src/instructions/multisig_create.rs +++ b/programs/squads_multisig_program/src/instructions/multisig_create.rs @@ -29,9 +29,9 @@ pub struct MultisigCreate<'info> { )] pub multisig: Account<'info, Multisig>, - /// A random public key that is used as a seed for the Multisig PDA. - /// CHECK: This can be any random public key. - pub create_key: AccountInfo<'info>, + /// 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)] diff --git a/programs/squads_multisig_program/src/lib.rs b/programs/squads_multisig_program/src/lib.rs index 27610e42..400d8bb1 100644 --- a/programs/squads_multisig_program/src/lib.rs +++ b/programs/squads_multisig_program/src/lib.rs @@ -20,6 +20,20 @@ pub mod instructions; pub mod state; mod utils; +#[cfg(not(feature = "no-entrypoint"))] +use solana_security_txt::security_txt; + +#[cfg(not(feature = "no-entrypoint"))] +security_txt! { + name: "Squads Multisig Program", + project_url: "https://squads.so", + contacts: "email:security@sqds.io,email:contact@osec.io", + policy: "https://github.com/Squads-Protocol/v4/blob/main/SECURITY.md", + preferred_languages: "en", + source_code: "https://github.com/squads-protocol/v4", + auditors: "OtterSec, Neodyme" +} + declare_id!("SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf"); #[program] @@ -55,6 +69,14 @@ pub mod squads_multisig_program { MultisigConfig::multisig_set_time_lock(ctx, args) } + /// Set the `threshold` config parameter for the controlled multisig. + pub fn multisig_change_threshold( + ctx: Context, + args: MultisigChangeThresholdArgs, + ) -> Result<()> { + MultisigConfig::multisig_change_threshold(ctx, args) + } + /// Set the multisig `config_authority`. pub fn multisig_set_config_authority( ctx: Context, diff --git a/programs/squads_multisig_program/src/state/multisig.rs b/programs/squads_multisig_program/src/state/multisig.rs index 86607727..a298c407 100644 --- a/programs/squads_multisig_program/src/state/multisig.rs +++ b/programs/squads_multisig_program/src/state/multisig.rs @@ -5,6 +5,8 @@ use anchor_lang::system_program; use crate::errors::*; +pub const MAX_TIME_LOCK: u32 = 3 * 30 * 24 * 60 * 60; // 3 months + #[account] pub struct Multisig { /// Key that is used to seed the multisig PDA. @@ -169,6 +171,12 @@ impl Multisig { MultisigError::InvalidStaleTransactionIndex ); + // Time Lock must not exceed the maximum allowed to prevent bricking the multisig. + require!( + self.time_lock <= MAX_TIME_LOCK, + MultisigError::TimeLockExceedsMaxAllowed + ); + Ok(()) } diff --git a/sdk/multisig/idl/squads_multisig_program.json b/sdk/multisig/idl/squads_multisig_program.json index c29c0b44..673e44f2 100644 --- a/sdk/multisig/idl/squads_multisig_program.json +++ b/sdk/multisig/idl/squads_multisig_program.json @@ -1,5 +1,5 @@ { - "version": "0.1.2", + "version": "0.2.0", "name": "squads_multisig_program", "instructions": [ { @@ -16,9 +16,10 @@ { "name": "createKey", "isMut": false, - "isSigner": false, + "isSigner": true, "docs": [ - "A random public key that is used as a seed for the Multisig PDA." + "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." ] }, { @@ -191,6 +192,55 @@ } ] }, + { + "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": [ @@ -2406,6 +2456,11 @@ "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)" } ], "metadata": { diff --git a/sdk/multisig/package.json b/sdk/multisig/package.json index 00b28216..8392353a 100644 --- a/sdk/multisig/package.json +++ b/sdk/multisig/package.json @@ -1,6 +1,6 @@ { "name": "@sqds/multisig", - "version": "1.9.0", + "version": "1.10.0", "description": "SDK for Squads Multisig Program v4", "main": "lib/index.js", "license": "MIT", diff --git a/sdk/multisig/src/generated/errors/index.ts b/sdk/multisig/src/generated/errors/index.ts index dd14f7e8..4163e95b 100644 --- a/sdk/multisig/src/generated/errors/index.ts +++ b/sdk/multisig/src/generated/errors/index.ts @@ -676,6 +676,32 @@ createErrorFromNameLookup.set( () => new ProtectedAccountError() ) +/** + * TimeLockExceedsMaxAllowed: 'Time lock exceeds the maximum allowed (90 days)' + * + * @category Errors + * @category generated + */ +export class TimeLockExceedsMaxAllowedError extends Error { + readonly code: number = 0x178e + readonly name: string = 'TimeLockExceedsMaxAllowed' + constructor() { + super('Time lock exceeds the maximum allowed (90 days)') + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, TimeLockExceedsMaxAllowedError) + } + } +} + +createErrorFromCodeLookup.set( + 0x178e, + () => new TimeLockExceedsMaxAllowedError() +) +createErrorFromNameLookup.set( + 'TimeLockExceedsMaxAllowed', + () => new TimeLockExceedsMaxAllowedError() +) + /** * 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 82f83f08..846d448e 100644 --- a/sdk/multisig/src/generated/instructions/index.ts +++ b/sdk/multisig/src/generated/instructions/index.ts @@ -5,6 +5,7 @@ export * from './configTransactionCreate' export * from './configTransactionExecute' export * from './multisigAddMember' export * from './multisigAddSpendingLimit' +export * from './multisigChangeThreshold' export * from './multisigCreate' export * from './multisigRemoveMember' export * from './multisigRemoveSpendingLimit' diff --git a/sdk/multisig/src/generated/instructions/multisigChangeThreshold.ts b/sdk/multisig/src/generated/instructions/multisigChangeThreshold.ts new file mode 100644 index 00000000..121467dc --- /dev/null +++ b/sdk/multisig/src/generated/instructions/multisigChangeThreshold.ts @@ -0,0 +1,130 @@ +/** + * 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 { + MultisigChangeThresholdArgs, + multisigChangeThresholdArgsBeet, +} from '../types/MultisigChangeThresholdArgs' + +/** + * @category Instructions + * @category MultisigChangeThreshold + * @category generated + */ +export type MultisigChangeThresholdInstructionArgs = { + args: MultisigChangeThresholdArgs +} +/** + * @category Instructions + * @category MultisigChangeThreshold + * @category generated + */ +export const multisigChangeThresholdStruct = new beet.FixableBeetArgsStruct< + MultisigChangeThresholdInstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */ + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['args', multisigChangeThresholdArgsBeet], + ], + 'MultisigChangeThresholdInstructionArgs' +) +/** + * Accounts required by the _multisigChangeThreshold_ instruction + * + * @property [_writable_] multisig + * @property [**signer**] configAuthority + * @property [_writable_, **signer**] rentPayer (optional) + * @category Instructions + * @category MultisigChangeThreshold + * @category generated + */ +export type MultisigChangeThresholdInstructionAccounts = { + multisig: web3.PublicKey + configAuthority: web3.PublicKey + rentPayer?: web3.PublicKey + systemProgram?: web3.PublicKey + anchorRemainingAccounts?: web3.AccountMeta[] +} + +export const multisigChangeThresholdInstructionDiscriminator = [ + 141, 42, 15, 126, 169, 92, 62, 181, +] + +/** + * Creates a _MultisigChangeThreshold_ instruction. + * + * Optional accounts that are not provided will be omitted from the accounts + * array passed with the instruction. + * An optional account that is set cannot follow an optional account that is unset. + * Otherwise an Error is raised. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category MultisigChangeThreshold + * @category generated + */ +export function createMultisigChangeThresholdInstruction( + accounts: MultisigChangeThresholdInstructionAccounts, + args: MultisigChangeThresholdInstructionArgs, + programId = new web3.PublicKey('SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf') +) { + const [data] = multisigChangeThresholdStruct.serialize({ + instructionDiscriminator: multisigChangeThresholdInstructionDiscriminator, + ...args, + }) + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.multisig, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.configAuthority, + isWritable: false, + isSigner: true, + }, + ] + + if (accounts.rentPayer != null) { + keys.push({ + pubkey: accounts.rentPayer, + isWritable: true, + isSigner: true, + }) + } + if (accounts.systemProgram != null) { + if (accounts.rentPayer == null) { + throw new Error( + "When providing 'systemProgram' then 'accounts.rentPayer' need(s) to be provided as well." + ) + } + keys.push({ + pubkey: accounts.systemProgram, + 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/multisigCreate.ts b/sdk/multisig/src/generated/instructions/multisigCreate.ts index 55d72695..6391bf3c 100644 --- a/sdk/multisig/src/generated/instructions/multisigCreate.ts +++ b/sdk/multisig/src/generated/instructions/multisigCreate.ts @@ -40,7 +40,7 @@ export const multisigCreateStruct = new beet.FixableBeetArgsStruct< * Accounts required by the _multisigCreate_ instruction * * @property [_writable_] multisig - * @property [] createKey + * @property [**signer**] createKey * @property [_writable_, **signer**] creator * @category Instructions * @category MultisigCreate @@ -86,7 +86,7 @@ export function createMultisigCreateInstruction( { pubkey: accounts.createKey, isWritable: false, - isSigner: false, + isSigner: true, }, { pubkey: accounts.creator, diff --git a/sdk/multisig/src/rpc/multisigCreate.ts b/sdk/multisig/src/rpc/multisigCreate.ts index 15f6888c..06498d04 100644 --- a/sdk/multisig/src/rpc/multisigCreate.ts +++ b/sdk/multisig/src/rpc/multisigCreate.ts @@ -23,7 +23,7 @@ export async function multisigCreate({ sendOptions, }: { connection: Connection; - createKey: PublicKey; + createKey: Signer; creator: Signer; multisigPda: PublicKey; configAuthority: PublicKey | null; @@ -37,7 +37,7 @@ export async function multisigCreate({ const tx = transactions.multisigCreate({ blockhash, - createKey: createKey, + createKey: createKey.publicKey, creator: creator.publicKey, multisigPda, configAuthority, @@ -47,7 +47,7 @@ export async function multisigCreate({ memo, }); - tx.sign([creator]); + tx.sign([creator, createKey]); try { return await connection.sendTransaction(tx, sendOptions); diff --git a/sdk/rs/Cargo.toml b/sdk/rs/Cargo.toml index eb11ea75..0cae1b9c 100644 --- a/sdk/rs/Cargo.toml +++ b/sdk/rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "squads-multisig" -version = "0.0.10" +version = "0.0.11" description = "An SDK for building automated programs on Solana" edition = "2021" license = "MIT OR Apache-2.0" @@ -14,7 +14,7 @@ keywords = ["solana", "multisig", "anchor", "squads", "cpi"] name = "squads_multisig" [dependencies] -squads-multisig-program = { path = "../../programs/squads_multisig_program", features =["cpi", "no-entrypoint"], version = "0.1.2" } +squads-multisig-program = { path = "../../programs/squads_multisig_program", features =["cpi", "no-entrypoint"], version = "0.2.0" } solana-client = "1.14.16" thiserror = "1.0.48" diff --git a/tests/suites/multisig-sdk.ts b/tests/suites/multisig-sdk.ts index b65f1d17..0fc8a2ca 100644 --- a/tests/suites/multisig-sdk.ts +++ b/tests/suites/multisig-sdk.ts @@ -35,9 +35,9 @@ describe("Multisig SDK", () => { it("error: duplicate member", async () => { const creator = await generateFundedKeypair(connection); - const createKey = Keypair.generate().publicKey; + const createKey = Keypair.generate(); const [multisigPda] = multisig.getMultisigPda({ - createKey, + createKey: createKey.publicKey, }); await assert.rejects( @@ -66,12 +66,49 @@ describe("Multisig SDK", () => { ); }); + it("error: missing signature from `createKey`", async () => { + const creator = await generateFundedKeypair(connection); + + const createKey = Keypair.generate(); + const [multisigPda] = multisig.getMultisigPda({ + createKey: createKey.publicKey, + }); + + const tx = multisig.transactions.multisigCreate({ + blockhash: (await connection.getLatestBlockhash()).blockhash, + createKey: createKey.publicKey, + creator: creator.publicKey, + multisigPda, + configAuthority: null, + timeLock: 0, + threshold: 1, + members: [ + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + { + key: members.almighty.publicKey, + permissions: Permissions.all(), + }, + ], + }); + + // Missing signature from `createKey`. + tx.sign([creator]); + + await assert.rejects( + () => connection.sendTransaction(tx, { skipPreflight: true }), + /Transaction signature verification failure/ + ); + }); + it("error: empty members", async () => { const creator = await generateFundedKeypair(connection); - const createKey = Keypair.generate().publicKey; + const createKey = Keypair.generate(); const [multisigPda] = multisig.getMultisigPda({ - createKey, + createKey: createKey.publicKey, }); await assert.rejects( @@ -95,9 +132,9 @@ describe("Multisig SDK", () => { const creator = await generateFundedKeypair(connection); const member = Keypair.generate(); - const createKey = Keypair.generate().publicKey; + const createKey = Keypair.generate(); const [multisigPda] = multisig.getMultisigPda({ - createKey, + createKey: createKey.publicKey, }); await assert.rejects( @@ -130,9 +167,9 @@ describe("Multisig SDK", () => { it("error: invalid threshold (< 1)", async () => { const creator = await generateFundedKeypair(connection); - const createKey = Keypair.generate().publicKey; + const createKey = Keypair.generate(); const [multisigPda] = multisig.getMultisigPda({ - createKey, + createKey: createKey.publicKey, }); await assert.rejects( @@ -158,9 +195,9 @@ describe("Multisig SDK", () => { it("error: invalid threshold (> members with permission to Vote)", async () => { const creator = await generateFundedKeypair(connection); - const createKey = Keypair.generate().publicKey; + const createKey = Keypair.generate(); const [multisigPda] = multisig.getMultisigPda({ - createKey: createKey, + createKey: createKey.publicKey, }); await assert.rejects( @@ -202,7 +239,7 @@ describe("Multisig SDK", () => { }); it("create a new autonomous multisig", async () => { - const createKey = Keypair.generate().publicKey; + const createKey = Keypair.generate(); const [multisigPda, multisigBump] = await createAutonomousMultisig({ connection, @@ -255,13 +292,13 @@ describe("Multisig SDK", () => { assert.strictEqual(multisigAccount.staleTransactionIndex.toString(), "0"); assert.strictEqual( multisigAccount.createKey.toBase58(), - createKey.toBase58() + createKey.publicKey.toBase58() ); assert.strictEqual(multisigAccount.bump, multisigBump); }); it("create a new controlled multisig", async () => { - const createKey = Keypair.generate().publicKey; + const createKey = Keypair.generate(); const configAuthority = await generateFundedKeypair(connection); const [multisigPda] = await createControlledMultisig({ @@ -307,7 +344,7 @@ describe("Multisig SDK", () => { multisigPda = ( await createControlledMultisig({ connection, - createKey: Keypair.generate().publicKey, + createKey: Keypair.generate(), configAuthority: configAuthority.publicKey, members, threshold: 2, @@ -839,7 +876,7 @@ describe("Multisig SDK", () => { let multisigPda: PublicKey; before(async () => { - const msCreateKey = Keypair.generate().publicKey; + const msCreateKey = Keypair.generate(); // Create new autonomous multisig. multisigPda = ( @@ -1009,7 +1046,7 @@ describe("Multisig SDK", () => { let multisigPda: PublicKey; before(async () => { - const msCreateKey = Keypair.generate().publicKey; + const msCreateKey = Keypair.generate(); // Create new autonomous multisig. multisigPda = ( @@ -1196,7 +1233,7 @@ describe("Multisig SDK", () => { before(async () => { const feePayer = await generateFundedKeypair(connection); - const msCreateKey = Keypair.generate().publicKey; + const msCreateKey = Keypair.generate(); // Create new autonomous multisig. multisigPda = ( @@ -1367,7 +1404,7 @@ describe("Multisig SDK", () => { before(async () => { const feePayer = await generateFundedKeypair(connection); - const msCreateKey = Keypair.generate().publicKey; + const msCreateKey = Keypair.generate(); // Create new autonomous multisig. multisigPda = ( @@ -1587,7 +1624,7 @@ describe("Multisig SDK", () => { before(async () => { const feePayer = await generateFundedKeypair(connection); - const msCreateKey = Keypair.generate().publicKey; + const msCreateKey = Keypair.generate(); // Create new autonomous multisig. multisigPda = ( @@ -2000,9 +2037,9 @@ describe("Multisig SDK", () => { describe("getAvailableMemoSize", () => { it("provides estimates for available size to use for memo", async () => { const multisigCreator = await generateFundedKeypair(connection); - const createKey = Keypair.generate().publicKey; + const createKey = Keypair.generate(); const [multisigPda] = multisig.getMultisigPda({ - createKey, + createKey: createKey.publicKey, }); const [configAuthority] = multisig.getVaultPda({ multisigPda, @@ -2013,7 +2050,7 @@ describe("Multisig SDK", () => { typeof multisig.transactions.multisigCreate >[0] = { blockhash: (await connection.getLatestBlockhash()).blockhash, - createKey, + createKey: createKey.publicKey, creator: multisigCreator.publicKey, multisigPda, configAuthority, @@ -2043,7 +2080,7 @@ describe("Multisig SDK", () => { // The transaction with memo should have the maximum allowed size. assert.strictEqual(createMultisigTxWithMemo.serialize().length, 1232); // The transaction should work. - createMultisigTxWithMemo.sign([multisigCreator]); + createMultisigTxWithMemo.sign([multisigCreator, createKey]); const signature = await connection.sendTransaction( createMultisigTxWithMemo ); diff --git a/tests/utils.ts b/tests/utils.ts index cfbeb3f7..85b25cbf 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -48,12 +48,12 @@ export async function generateMultisigMembers( export async function createAutonomousMultisig({ connection, - createKey = Keypair.generate().publicKey, + createKey = Keypair.generate(), members, threshold, timeLock, }: { - createKey?: PublicKey; + createKey?: Keypair; members: TestMembers; threshold: number; timeLock: number; @@ -62,7 +62,7 @@ export async function createAutonomousMultisig({ const creator = await generateFundedKeypair(connection); const [multisigPda, multisigBump] = multisig.getMultisigPda({ - createKey, + createKey: createKey.publicKey, }); const signature = await multisig.rpc.multisigCreate({ @@ -87,7 +87,7 @@ export async function createAutonomousMultisig({ permissions: Permissions.fromPermissions([Permission.Execute]), }, ], - createKey, + createKey: createKey, sendOptions: { skipPreflight: true }, }); @@ -98,13 +98,13 @@ export async function createAutonomousMultisig({ export async function createControlledMultisig({ connection, - createKey = Keypair.generate().publicKey, + createKey = Keypair.generate(), configAuthority, members, threshold, timeLock, }: { - createKey?: PublicKey; + createKey?: Keypair; configAuthority: PublicKey; members: TestMembers; threshold: number; @@ -114,7 +114,7 @@ export async function createControlledMultisig({ const creator = await generateFundedKeypair(connection); const [multisigPda, multisigBump] = multisig.getMultisigPda({ - createKey, + createKey: createKey.publicKey, }); const signature = await multisig.rpc.multisigCreate({ @@ -139,7 +139,7 @@ export async function createControlledMultisig({ permissions: Permissions.fromPermissions([Permission.Execute]), }, ], - createKey, + createKey: createKey, sendOptions: { skipPreflight: true }, }); diff --git a/yarn.lock b/yarn.lock index ee2f966d..e33fa2a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -127,7 +127,7 @@ dependencies: buffer "~6.0.3" -"@solana/spl-token@*", "@solana/spl-token@0.3.6": +"@solana/spl-token@*", "@solana/spl-token@0.3.6", "@solana/spl-token@^0.3.6": version "0.3.6" resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.6.tgz#35473ad2ed71fe91e5754a2ac72901e1b8b26a42" integrity sha512-P9pTXjDIRvVbjr3J0mCnSamYqLnICeds7IoH1/Ro2R9OBuOHdp5pqKZoscfZ3UYrgnCWUc1bc9M2m/YPHjw+1g== @@ -136,7 +136,7 @@ "@solana/buffer-layout-utils" "^0.2.0" buffer "^6.0.3" -"@solana/web3.js@*", "@solana/web3.js@1.70.3", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.56.2": +"@solana/web3.js@1.70.3", "@solana/web3.js@^1.32.0", "@solana/web3.js@^1.56.2", "@solana/web3.js@^1.70.3": version "1.70.3" resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.70.3.tgz#44040a78d1f86ee6a0a9dbe391b5f891bb404265" integrity sha512-9JAFXAWB3yhUHnoahzemTz4TcsGqmITPArNlm9795e+LA/DYkIEJIXIosV4ImzDMfqolymZeRgG3O8ewNgYTTA==