-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(spending_limits): spending_limit_use
- Loading branch information
Showing
22 changed files
with
1,252 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
265 changes: 265 additions & 0 deletions
265
programs/multisig/src/instructions/spending_limit_use.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.