Skip to content

Commit

Permalink
feat(spending_limits): add SpendingLimit account
Browse files Browse the repository at this point in the history
  • Loading branch information
vovacodes committed May 9, 2023
1 parent 7ecd3de commit 88e3486
Show file tree
Hide file tree
Showing 19 changed files with 1,075 additions and 157 deletions.
376 changes: 342 additions & 34 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions programs/multisig/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ default = []

[dependencies]
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"
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub struct ConfigTransactionCreate<'info> {
#[account(
init,
payer = creator,
space = ConfigTransaction::size(args.actions.len()),
space = ConfigTransaction::size(&args.actions),
seeds = [
SEED_PREFIX,
multisig.key().as_ref(),
Expand Down
76 changes: 58 additions & 18 deletions programs/multisig/src/instructions/config_transaction_execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,16 @@ pub struct ConfigTransactionExecute<'info> {
pub transaction: Account<'info, ConfigTransaction>,

/// The account that will be charged in case the multisig account needs to reallocate space,
/// for example when adding a new member.
/// for example when adding a new member or a spending limit.
/// This is usually the same as `member`, but can be a different account if needed.
#[account(mut)]
pub rent_payer: Signer<'info>,
pub rent_payer: Option<Signer<'info>>,

/// We might need it in case reallocation is needed.
pub system_program: Program<'info, System>,
pub system_program: Option<Program<'info, System>>,
// In case the transaction contains Add(Remove)SpendingLimit actions,
// `remaining_accounts` must contain the SpendingLimit accounts to be initialized/closed.
// remaining_accounts
}

impl ConfigTransactionExecute<'_> {
Expand Down Expand Up @@ -95,20 +98,39 @@ impl ConfigTransactionExecute<'_> {
let multisig = &mut ctx.accounts.multisig;
let transaction = &mut ctx.accounts.transaction;
let proposal = &mut ctx.accounts.proposal;
let rent_payer = &ctx.accounts.rent_payer;
let system_program = &ctx.accounts.system_program;

let mut spending_limits: Vec<Account<SpendingLimit>> = vec![];
for remaining_account in ctx.remaining_accounts {
if let Some(spending_limit) = Account::<SpendingLimit>::try_from(remaining_account).ok()
{
spending_limits.push(spending_limit)
}
}

// Check applying the config actions will require reallocation of space for the multisig account.
let new_members_length =
members_length_after_actions(multisig.members.len(), &transaction.actions);
let reallocated = Multisig::realloc_if_needed(
multisig.to_account_info(),
new_members_length,
rent_payer.to_account_info(),
system_program.to_account_info(),
)?;
if reallocated {
multisig.reload()?;
if new_members_length > multisig.members.len() {
let rent_payer = &ctx
.accounts
.rent_payer
.as_ref()
.ok_or(MultisigError::MissingAccount)?;
let system_program = &ctx
.accounts
.system_program
.as_ref()
.ok_or(MultisigError::MissingAccount)?;

let reallocated = Multisig::realloc_if_needed(
multisig.to_account_info(),
new_members_length,
rent_payer.to_account_info(),
system_program.to_account_info(),
)?;
if reallocated {
multisig.reload()?;
}
}

// Execute the actions one by one.
Expand All @@ -117,25 +139,42 @@ impl ConfigTransactionExecute<'_> {
ConfigAction::AddMember { new_member } => {
multisig.add_member(new_member.to_owned());

multisig.config_updated();
multisig.invalidate_prior_transactions();
}

ConfigAction::RemoveMember { old_member } => {
multisig.remove_member(old_member.to_owned())?;

multisig.config_updated();
multisig.invalidate_prior_transactions();
}

ConfigAction::ChangeThreshold { new_threshold } => {
multisig.threshold = *new_threshold;

multisig.config_updated();
multisig.invalidate_prior_transactions();
}

ConfigAction::SetTimeLock { new_time_lock } => {
multisig.time_lock = *new_time_lock;

multisig.config_updated();
multisig.invalidate_prior_transactions();
}

ConfigAction::AddSpendingLimit {
create_key,
vault_index,
mint,
amount,
period,
members,
destinations,
} => {
let spending_limit = spending_limits
.iter()
.find(|l| l.create_key == *create_key)
.ok_or(MultisigError::MissingAccount)?;

todo!()
}
}
}
Expand All @@ -146,7 +185,7 @@ impl ConfigTransactionExecute<'_> {
.members
.len()
.try_into()
.expect("didn't expect more that `u16::MAX` members");
.expect("didn't expect more than `u16::MAX` members");
};

// Make sure the multisig state is valid after applying the actions.
Expand All @@ -167,6 +206,7 @@ fn members_length_after_actions(members_length: usize, actions: &[ConfigAction])
ConfigAction::RemoveMember { .. } => acc.checked_sub(1).expect("overflow"),
ConfigAction::ChangeThreshold { .. } => acc,
ConfigAction::SetTimeLock { .. } => acc,
ConfigAction::AddSpendingLimit { .. } => acc,
});

let abs_members_delta =
Expand Down
46 changes: 24 additions & 22 deletions programs/multisig/src/instructions/multisig_config.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,41 @@
use anchor_lang::prelude::*;
use anchor_spl::token::Mint;

use crate::errors::*;
use crate::state::*;

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct MultisigAddMemberArgs {
pub new_member: Member,
/// Memo isn't used for anything, but is included in `AddMemberEvent` that can later be parsed and indexed.
/// Memo is used for indexing only.
pub memo: Option<String>,
}

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct MultisigRemoveMemberArgs {
pub old_member: Pubkey,
/// Memo isn't used for anything, but is included in `RemoveMemberEvent` that can later be parsed and indexed.
/// Memo is used for indexing only.
pub memo: Option<String>,
}

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct MultisigChangeThresholdArgs {
new_threshold: u16,
/// Memo isn't used for anything, but is included in `ChangeThreshold` that can later be parsed and indexed.
/// Memo is used for indexing only.
pub memo: Option<String>,
}

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct MultisigSetTimeLockArgs {
time_lock: u32,
/// Memo isn't used for anything, but is included in `ChangeThreshold` that can later be parsed and indexed.
/// Memo is used for indexing only.
pub memo: Option<String>,
}

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct MultisigSetConfigAuthorityArgs {
config_authority: Pubkey,
/// Memo isn't used for anything, but is included in `ChangeThreshold` that can later be parsed and indexed.
pub memo: Option<String>,
}

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct MultisigAddVaultArgs {
/// The next vault index to set as the latest used.
/// Must be the current `vault_index + 1`.
/// We pass it explicitly to make this instruction idempotent.
pub vault_index: u8,
/// Memo isn't used for anything, but is included in `ChangeThreshold` that can later be parsed and indexed.
/// Memo is used for indexing only.
pub memo: Option<String>,
}

Expand All @@ -60,8 +51,8 @@ pub struct MultisigConfig<'info> {
/// Multisig `config_authority` that must authorize the configuration change.
pub config_authority: Signer<'info>,

/// The account that will be charged in case the multisig account needs to reallocate space,
/// for example when adding a new member.
/// 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.
#[account(mut)]
pub rent_payer: Option<Signer<'info>>,
Expand Down Expand Up @@ -117,7 +108,7 @@ impl MultisigConfig<'_> {

multisig.invariant()?;

multisig.config_updated();
multisig.invalidate_prior_transactions();

Ok(())
}
Expand Down Expand Up @@ -148,7 +139,7 @@ impl MultisigConfig<'_> {

multisig.invariant()?;

multisig.config_updated();
multisig.invalidate_prior_transactions();

Ok(())
}
Expand All @@ -168,7 +159,7 @@ impl MultisigConfig<'_> {

multisig.invariant()?;

multisig.config_updated();
multisig.invalidate_prior_transactions();

Ok(())
}
Expand All @@ -185,7 +176,7 @@ impl MultisigConfig<'_> {

multisig.invariant()?;

multisig.config_updated();
multisig.invalidate_prior_transactions();

Ok(())
}
Expand All @@ -205,8 +196,19 @@ impl MultisigConfig<'_> {

multisig.invariant()?;

multisig.config_updated();
multisig.invalidate_prior_transactions();

Ok(())
}

// /// Create a new spending limit for a vault.
// /// NOTE: This instruction must be called only by the `config_authority` if one is set (Controlled Multisig).
// /// Uncontrolled Mustisigs should use `config_transaction_create` instead.
// #[access_control(ctx.accounts.validate(&args))]
// pub fn multisig_add_spending_limit(
// ctx: Context<Self>,
// args: MultisigAddSpendingLimitArgs,
// ) -> Result<()> {
// todo!()
// }
}
4 changes: 2 additions & 2 deletions programs/multisig/src/instructions/multisig_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub struct MultisigCreateArgs {
pub members: Vec<Member>,
/// How many seconds must pass between transaction voting settlement and execution.
pub time_lock: u32,
/// Memo isn't used for anything, but is included in `CreatedEvent` that can later be parsed and indexed.
/// Memo is used for indexing only.
pub memo: Option<String>,
}

Expand Down Expand Up @@ -56,12 +56,12 @@ impl MultisigCreate<'_> {
let multisig = &mut ctx.accounts.multisig;
multisig.config_authority = args.config_authority.unwrap_or_default();
multisig.threshold = args.threshold;
multisig.members = members;
multisig.time_lock = args.time_lock;
multisig.transaction_index = 0;
multisig.stale_transaction_index = 0;
multisig.create_key = ctx.accounts.create_key.to_account_info().key();
multisig.bump = *ctx.bumps.get("multisig").unwrap();
multisig.members = members;

multisig.invariant()?;

Expand Down
2 changes: 1 addition & 1 deletion programs/multisig/src/instructions/proposal_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub struct ProposalCreate<'info> {

#[account(mut)]
pub rent_payer: Signer<'info>,

pub system_program: Program<'info, System>,
}

Expand Down
36 changes: 33 additions & 3 deletions programs/multisig/src/state/config_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anchor_lang::prelude::*;
use anchor_lang::solana_program::borsh::get_instance_packed_len;

use super::Member;
use super::*;

/// 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,
Expand All @@ -20,13 +21,19 @@ pub struct ConfigTransaction {
}

impl ConfigTransaction {
pub fn size(actions_len: usize) -> usize {
pub fn size(actions: &[ConfigAction]) -> usize {
let actions_size: usize = actions
.iter()
.map(|action| get_instance_packed_len(action).unwrap())
.sum();

8 + // anchor account discriminator
32 + // multisig
32 + // creator
8 + // index
1 + // bump
(4 + actions_len * (1 + Member::size())) // actions vec
4 + // actions vector length
actions_size
}
}

Expand All @@ -41,4 +48,27 @@ pub enum ConfigAction {
ChangeThreshold { new_threshold: u16 },
/// Change the `time_lock` of the multisig.
SetTimeLock { new_time_lock: u32 },
/// Change the `time_lock` of the multisig.
AddSpendingLimit {
/// Key that is used to seed the SpendingLimit PDA.
create_key: Pubkey,
/// The index of the vault that the spending limit is for.
vault_index: u8,
/// The token mint the spending limit is for.
mint: Pubkey,
/// 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`.
amount: u64,
/// The reset period of the spending limit.
/// When it passes, the remaining amount is reset, unless it's `Period::OneTime`.
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: Vec<Pubkey>,
/// The destination addresses the spending limit is allowed to sent funds to.
/// If empty, funds can be sent to any address.
destinations: Vec<Pubkey>,
},
}
2 changes: 2 additions & 0 deletions programs/multisig/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ pub use config_transaction::*;
pub use multisig::*;
pub use proposal::*;
pub use seeds::*;
pub use spending_limit::*;
pub use vault_transaction::*;

mod batch;
mod config_transaction;
mod multisig;
mod proposal;
mod seeds;
mod spending_limit;
mod vault_transaction;
Loading

0 comments on commit 88e3486

Please sign in to comment.