Skip to content

Commit

Permalink
feat(transaction_reject): instruction, sdk and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
vovacodes committed Jan 30, 2023
1 parent 29233e1 commit ceef604
Show file tree
Hide file tree
Showing 16 changed files with 576 additions and 63 deletions.
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
},
"devDependencies": {
"@solana/spl-token": "0.3.6",
"@solana/web3.js": "*",
"@sqds/multisig": "*",
"@types/bn.js": "5.1.0",
"@types/mocha": "10.0.1",
"@types/node-fetch": "2.6.2",
Expand Down
4 changes: 3 additions & 1 deletion programs/multisig/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub enum MultisigError {
TooManyMembers,
#[msg("Maximum number of members already reached")]
MaxMembersReached,
#[msg("Invalid threshold, must be between 1 and number of members")]
#[msg("Invalid threshold, must be between 1 and number of members with Vote permission")]
InvalidThreshold,
#[msg("Attempted to perform an unauthorized action")]
Unauthorized,
Expand All @@ -28,6 +28,8 @@ pub enum MultisigError {
TransactionNotForMultisig,
#[msg("Member already approved the transaction")]
AlreadyApproved,
#[msg("Member already rejected the transaction")]
AlreadyRejected,
#[msg("Wrong number of accounts provided")]
InvalidNumberOfAccounts,
#[msg("Invalid account provided")]
Expand Down
14 changes: 13 additions & 1 deletion programs/multisig/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub struct TransactionCreated {
pub memo: Option<String>,
}

/// New multisig transaction account is created.
/// Transaction is approved.
#[event]
pub struct TransactionApproved {
/// The multisig account.
Expand All @@ -57,6 +57,18 @@ pub struct TransactionApproved {
pub memo: Option<String>,
}

/// Transaction is rejected.
#[event]
pub struct TransactionRejected {
/// The multisig account.
pub multisig: Pubkey,
/// The transaction account.
pub transaction: Pubkey,
#[index]
/// Memo that was added by the creator.
pub memo: Option<String>,
}

/// New multisig transaction account is created.
#[event]
pub struct TransactionExecuted {
Expand Down
3 changes: 2 additions & 1 deletion programs/multisig/src/instructions/multisig_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ impl MultisigCreate<'_> {

// Make sure threshold is within bounds.
let threshold = usize::from(args.threshold);
let num_voters = Multisig::num_voters(&members);
require!(threshold > 0, MultisigError::InvalidThreshold);
require!(threshold <= num_members, MultisigError::InvalidThreshold);
require!(threshold <= num_voters, MultisigError::InvalidThreshold);

// Initialize the multisig.
let multisig = &mut ctx.accounts.multisig;
Expand Down
40 changes: 40 additions & 0 deletions programs/multisig/src/instructions/transaction_vote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,44 @@ impl TransactionVote<'_> {

Ok(())
}

/// Reject the transaction on behalf of the `member`.
/// The transaction must be `Active`.
pub fn transaction_reject(ctx: Context<Self>, args: TransactionVoteArgs) -> Result<()> {
let multisig = &mut ctx.accounts.multisig;
let transaction = &mut ctx.accounts.transaction;
let member = &mut ctx.accounts.member;

require!(
transaction.has_voted_reject(member.key()).is_none(),
MultisigError::AlreadyApproved
);

// If `member` has previously voted to approve, remove that vote.
if let Some(vote_index) = transaction.has_voted_approve(member.key()) {
transaction.remove_approval_vote(vote_index);
}

transaction.reject(member.key());

// How many "reject" votes are enough to make the transaction "Rejected".
// The cutoff must be such that it is impossible for the remaining voters to reach the approval threshold.
// For example: total voters = 7, threshold = 3, cutoff = 5.
let cutoff = Multisig::num_voters(&multisig.members)
.checked_sub(usize::from(multisig.threshold))
.unwrap()
.checked_add(1)
.unwrap();
if transaction.rejected.len() >= cutoff {
transaction.status = TransactionStatus::Rejected;
}

emit!(TransactionRejected {
multisig: multisig.key(),
transaction: transaction.key(),
memo: args.memo,
});

Ok(())
}
}
9 changes: 9 additions & 0 deletions programs/multisig/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ pub mod multisig {
TransactionVote::transaction_approve(ctx, args)
}

/// Reject the transaction on behalf of the `member`.
/// The transaction must be `Active`.
pub fn transaction_reject(
ctx: Context<TransactionVote>,
args: TransactionVoteArgs,
) -> Result<()> {
TransactionVote::transaction_reject(ctx, args)
}

/// Execute the multisig transaction.
/// The transaction must be `ExecuteReady`.
pub fn transaction_execute(ctx: Context<TransactionExecute>) -> Result<()> {
Expand Down
7 changes: 7 additions & 0 deletions programs/multisig/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ impl Multisig {
}
}

pub fn num_voters(members: &[Member]) -> usize {
members
.iter()
.filter(|m| m.permissions.has(Permission::Vote))
.count()
}

pub fn add_member_if_not_exists(&mut self, new_member: Member) {
if self.is_member(new_member.key).is_none() {
self.members.push(new_member);
Expand Down
63 changes: 61 additions & 2 deletions sdk/multisig/idl/multisig.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,38 @@
}
]
},
{
"name": "transactionReject",
"docs": [
"Reject the transaction on behalf of the `member`.",
"The transaction must be `Active`."
],
"accounts": [
{
"name": "multisig",
"isMut": false,
"isSigner": false
},
{
"name": "transaction",
"isMut": true,
"isSigner": false
},
{
"name": "member",
"isMut": true,
"isSigner": true
}
],
"args": [
{
"name": "args",
"type": {
"defined": "TransactionVoteArgs"
}
}
]
},
{
"name": "transactionExecute",
"docs": [
Expand Down Expand Up @@ -764,6 +796,28 @@
}
]
},
{
"name": "TransactionRejected",
"fields": [
{
"name": "multisig",
"type": "publicKey",
"index": false
},
{
"name": "transaction",
"type": "publicKey",
"index": false
},
{
"name": "memo",
"type": {
"option": "string"
},
"index": true
}
]
},
{
"name": "TransactionExecuted",
"fields": [
Expand Down Expand Up @@ -809,7 +863,7 @@
{
"code": 6005,
"name": "InvalidThreshold",
"msg": "Invalid threshold, must be between 1 and number of members"
"msg": "Invalid threshold, must be between 1 and number of members with Vote permission"
},
{
"code": 6006,
Expand Down Expand Up @@ -848,11 +902,16 @@
},
{
"code": 6013,
"name": "AlreadyRejected",
"msg": "Member already rejected the transaction"
},
{
"code": 6014,
"name": "InvalidNumberOfAccounts",
"msg": "Wrong number of accounts provided"
},
{
"code": 6014,
"code": 6015,
"name": "InvalidAccount",
"msg": "Invalid account provided"
}
Expand Down
37 changes: 31 additions & 6 deletions sdk/multisig/src/generated/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ createErrorFromNameLookup.set(
)

/**
* InvalidThreshold: 'Invalid threshold, must be between 1 and number of members'
* InvalidThreshold: 'Invalid threshold, must be between 1 and number of members with Vote permission'
*
* @category Errors
* @category generated
Expand All @@ -130,7 +130,9 @@ export class InvalidThresholdError extends Error {
readonly code: number = 0x1775
readonly name: string = 'InvalidThreshold'
constructor() {
super('Invalid threshold, must be between 1 and number of members')
super(
'Invalid threshold, must be between 1 and number of members with Vote permission'
)
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, InvalidThresholdError)
}
Expand Down Expand Up @@ -304,14 +306,37 @@ createErrorFromNameLookup.set(
() => new AlreadyApprovedError()
)

/**
* AlreadyRejected: 'Member already rejected the transaction'
*
* @category Errors
* @category generated
*/
export class AlreadyRejectedError extends Error {
readonly code: number = 0x177d
readonly name: string = 'AlreadyRejected'
constructor() {
super('Member already rejected the transaction')
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, AlreadyRejectedError)
}
}
}

createErrorFromCodeLookup.set(0x177d, () => new AlreadyRejectedError())
createErrorFromNameLookup.set(
'AlreadyRejected',
() => new AlreadyRejectedError()
)

/**
* InvalidNumberOfAccounts: 'Wrong number of accounts provided'
*
* @category Errors
* @category generated
*/
export class InvalidNumberOfAccountsError extends Error {
readonly code: number = 0x177d
readonly code: number = 0x177e
readonly name: string = 'InvalidNumberOfAccounts'
constructor() {
super('Wrong number of accounts provided')
Expand All @@ -321,7 +346,7 @@ export class InvalidNumberOfAccountsError extends Error {
}
}

createErrorFromCodeLookup.set(0x177d, () => new InvalidNumberOfAccountsError())
createErrorFromCodeLookup.set(0x177e, () => new InvalidNumberOfAccountsError())
createErrorFromNameLookup.set(
'InvalidNumberOfAccounts',
() => new InvalidNumberOfAccountsError()
Expand All @@ -334,7 +359,7 @@ createErrorFromNameLookup.set(
* @category generated
*/
export class InvalidAccountError extends Error {
readonly code: number = 0x177e
readonly code: number = 0x177f
readonly name: string = 'InvalidAccount'
constructor() {
super('Invalid account provided')
Expand All @@ -344,7 +369,7 @@ export class InvalidAccountError extends Error {
}
}

createErrorFromCodeLookup.set(0x177e, () => new InvalidAccountError())
createErrorFromCodeLookup.set(0x177f, () => new InvalidAccountError())
createErrorFromNameLookup.set('InvalidAccount', () => new InvalidAccountError())

/**
Expand Down
1 change: 1 addition & 0 deletions sdk/multisig/src/generated/instructions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './multisigCreate'
export * from './transactionApprove'
export * from './transactionCreate'
export * from './transactionExecute'
export * from './transactionReject'
Loading

0 comments on commit ceef604

Please sign in to comment.