Skip to content

Commit

Permalink
feat(spending_limits): spending_limit_use
Browse files Browse the repository at this point in the history
  • Loading branch information
vovacodes committed May 12, 2023
1 parent a3c6f4b commit 98c4043
Show file tree
Hide file tree
Showing 22 changed files with 1,252 additions and 43 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"deploy": "anchor deploy --provider.cluster mainnet-beta --provider.wallet .keys/upgrade-authority.json --program-name multisig"
},
"devDependencies": {
"@solana/spl-token": "0.3.6",
"@solana/spl-token": "*",
"@types/bn.js": "5.1.0",
"@types/mocha": "10.0.1",
"@types/node-fetch": "2.6.2",
Expand All @@ -22,6 +22,7 @@
},
"resolutions": {
"@solana/web3.js": "1.70.3",
"@solana/spl-token": "0.3.6",
"typescript": "4.9.4"
}
}
8 changes: 8 additions & 0 deletions programs/multisig/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,12 @@ pub enum MultisigError {
NoActions,
#[msg("Missing account")]
MissingAccount,
#[msg("Invalid mint")]
InvalidMint,
#[msg("Invalid destination")]
InvalidDestination,
#[msg("Spending limit exceeded")]
SpendingLimitExceeded,
#[msg("Decimals don't match the mint")]
DecimalsMismatch,
}
2 changes: 2 additions & 0 deletions programs/multisig/src/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub use multisig_create::*;
pub use proposal_activate::*;
pub use proposal_create::*;
pub use proposal_vote::*;
pub use spending_limit_use::*;
pub use vault_transaction_create::*;
pub use vault_transaction_execute::*;

Expand All @@ -21,5 +22,6 @@ mod multisig_create;
mod proposal_activate;
mod proposal_create;
mod proposal_vote;
mod spending_limit_use;
mod vault_transaction_create;
mod vault_transaction_execute;
265 changes: 265 additions & 0 deletions programs/multisig/src/instructions/spending_limit_use.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
use anchor_lang::prelude::*;
use anchor_spl::token_2022::TransferChecked;
use anchor_spl::token_interface;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};

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

#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct SpendingLimitUseArgs {
/// Amount of tokens to transfer.
pub amount: u64,
/// Decimals of the token mint. Used for double-checking against incorrect order of magnitude of `amount`.
pub decimals: u8,
/// Memo used for indexing.
pub memo: Option<String>,
}

#[derive(Accounts)]
pub struct SpendingLimitUse<'info> {
/// The multisig account the `spending_limit` is for.
#[account(
mut,
seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()],
bump = multisig.bump,
)]
pub multisig: Box<Account<'info, Multisig>>,

pub member: Signer<'info>,

/// The SpendingLimit account to use.
#[account(
mut,
seeds = [
SEED_PREFIX,
multisig.key().as_ref(),
SEED_SPENDING_LIMIT,
spending_limit.create_key.key().as_ref(),
],
bump = spending_limit.bump,
)]
pub spending_limit: Account<'info, SpendingLimit>,

/// Multisig vault account to transfer tokens from.
/// CHECK: All the required checks are done by checking the seeds.
#[account(
mut,
seeds = [
SEED_PREFIX,
multisig.key().as_ref(),
SEED_VAULT,
&spending_limit.vault_index.to_le_bytes(),
],
bump
)]
pub vault: AccountInfo<'info>,

/// Destination account to transfer tokens to.
/// CHECK: We do the checks in `SpendingLimitUse::validate`.
#[account(mut)]
pub destination: AccountInfo<'info>,

/// In case `spending_limit.mint` is SOL.
pub system_program: Option<Program<'info, System>>,

/// The mint of the tokens to transfer in case `spending_limit.mint` is an SPL token.
/// CHECK: We do the checks in `SpendingLimitUse::validate`.
pub mint: Option<InterfaceAccount<'info, Mint>>,

/// Multisig vault token account to transfer tokens from in case `spending_limit.mint` is an SPL token.
#[account(
mut,
token::mint = mint,
token::authority = vault,
)]
pub vault_token_account: Option<InterfaceAccount<'info, TokenAccount>>,

/// Destination token account in case `spending_limit.mint` is an SPL token.
#[account(
mut,
token::mint = mint,
token::authority = destination,
)]
pub destination_token_account: Option<InterfaceAccount<'info, TokenAccount>>,

/// In case `spending_limit.mint` is an SPL token.
pub token_program: Option<Interface<'info, TokenInterface>>,
}

impl SpendingLimitUse<'_> {
fn validate(&self) -> Result<()> {
let Self {
multisig,
member,
spending_limit,
mint,
..
} = 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
);

// spending_limit - needs no checking.

// mint
if spending_limit.mint == Pubkey::default() {
// SpendingLimit is for SOL, there should be no mint account in this case.
require!(mint.is_none(), MultisigError::InvalidMint);
} else {
// SpendingLimit is for an SPL token, `mint` must match `spending_limit.mint`.
require!(
spending_limit.mint == mint.as_ref().unwrap().key(),
MultisigError::InvalidMint
);
}

// vault - checked in the #[account] attribute.

// vault_token_account - checked in the #[account] attribute.

// destination
if !spending_limit.destinations.is_empty() {
require!(
spending_limit
.destinations
.contains(&self.destination.key()),
MultisigError::InvalidDestination
);
}

// destination_token_account - checked in the #[account] attribute.

Ok(())
}

/// Use a spending limit to transfer tokens from a multisig vault to a destination account.
#[access_control(ctx.accounts.validate())]
pub fn spending_limit_use(ctx: Context<Self>, args: SpendingLimitUseArgs) -> Result<()> {
let spending_limit = &mut ctx.accounts.spending_limit;
let vault = &mut ctx.accounts.vault;
let destination = &mut ctx.accounts.destination;

let multisig_key = ctx.accounts.multisig.key();
let vault_bump = *ctx.bumps.get("vault").unwrap();
let now = Clock::get()?.unix_timestamp;

// Reset `spending_limit.remaining_amount` if the `spending_limit.period` has passed.
if let Some(reset_period) = spending_limit.period.to_seconds() {
let passed_since_last_reset = now.checked_sub(spending_limit.last_reset).unwrap();

if passed_since_last_reset > reset_period {
spending_limit.remaining_amount = spending_limit.amount;

let periods_passed = passed_since_last_reset.checked_div(reset_period).unwrap();

// last_reset = last_reset + periods_passed * reset_period,
spending_limit.last_reset = spending_limit
.last_reset
.checked_add(periods_passed.checked_mul(reset_period).unwrap())
.unwrap();
}
}

// Update `spending_limit.remaining_amount`.
// This will also check if `amount` doesn't exceed `spending_limit.remaining_amount`.
spending_limit.remaining_amount = spending_limit
.remaining_amount
.checked_sub(args.amount)
.ok_or(MultisigError::SpendingLimitExceeded)?;

// Transfer tokens.
if spending_limit.mint == Pubkey::default() {
// Transfer using the system_program::transfer.
let system_program = &ctx
.accounts
.system_program
.as_ref()
.ok_or(MultisigError::MissingAccount)?;

// Sanity check for the decimals. Similar to the one in token_interface::transfer_checked.
require!(args.decimals == 9, MultisigError::DecimalsMismatch);

anchor_lang::system_program::transfer(
CpiContext::new_with_signer(
system_program.to_account_info(),
anchor_lang::system_program::Transfer {
from: vault.clone(),
to: destination.clone(),
},
&[&[
&SEED_PREFIX,
multisig_key.as_ref(),
&SEED_VAULT,
&spending_limit.vault_index.to_le_bytes(),
&[vault_bump],
]],
),
args.amount,
)?
} else {
// Transfer using the token_program::transfer_checked.
let mint = &ctx
.accounts
.mint
.as_ref()
.ok_or(MultisigError::MissingAccount)?;
let vault_token_account = &ctx
.accounts
.vault_token_account
.as_ref()
.ok_or(MultisigError::MissingAccount)?;
let destination_token_account = &ctx
.accounts
.destination_token_account
.as_ref()
.ok_or(MultisigError::MissingAccount)?;
let token_program = &ctx
.accounts
.token_program
.as_ref()
.ok_or(MultisigError::MissingAccount)?;

msg!(
"token_program {} mint {} vault {} destination {} amount {} decimals {}",
&token_program.key,
&mint.key(),
&vault.key,
&destination.key,
&args.amount,
&args.decimals
);

token_interface::transfer_checked(
CpiContext::new_with_signer(
token_program.to_account_info(),
TransferChecked {
from: vault_token_account.to_account_info(),
mint: mint.to_account_info(),
to: destination_token_account.to_account_info(),
authority: vault.clone(),
},
&[&[
&SEED_PREFIX,
multisig_key.as_ref(),
&SEED_VAULT,
&spending_limit.vault_index.to_le_bytes(),
&[vault_bump],
]],
),
args.amount,
args.decimals,
)?;
}

Ok(())
}
}
9 changes: 9 additions & 0 deletions programs/multisig/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![allow(clippy::result_large_err)]
#![deny(arithmetic_overflow)]
#![deny(unused_must_use)]
// #![deny(clippy::arithmetic_side_effects)]
// #![deny(clippy::integer_arithmetic)]
Expand Down Expand Up @@ -133,4 +134,12 @@ pub mod multisig {
pub fn proposal_cancel(ctx: Context<ProposalVote>, args: ProposalVoteArgs) -> Result<()> {
ProposalVote::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<SpendingLimitUse>,
args: SpendingLimitUseArgs,
) -> Result<()> {
SpendingLimitUse::spending_limit_use(ctx, args)
}
}
13 changes: 13 additions & 0 deletions programs/multisig/src/state/spending_limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub struct SpendingLimit {
pub vault_index: u8,

/// The token mint the spending limit is for.
/// Pubkey::default() means SOL.
/// use NATIVE_MINT for Wrapped SOL.
pub mint: Pubkey,

/// The amount of tokens that can be spent in a period.
Expand Down Expand Up @@ -74,3 +76,14 @@ pub enum Period {
/// The spending limit is reset every month (30 days).
Month,
}

impl Period {
pub fn to_seconds(&self) -> Option<i64> {
match self {
Period::OneTime => None,
Period::Day => Some(24 * 60 * 60),
Period::Week => Some(7 * 24 * 60 * 60),
Period::Month => Some(30 * 24 * 60 * 60),
}
}
}
Loading

0 comments on commit 98c4043

Please sign in to comment.