squads_multisig_program/instructions/
spending_limit_use.rs1use anchor_lang::prelude::*;
2use anchor_spl::token_2022::TransferChecked;
3use anchor_spl::token_interface;
4use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
5
6use crate::errors::*;
7use crate::state::*;
8
9#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
10pub struct SpendingLimitUseArgs {
11 pub amount: u64,
13 pub decimals: u8,
15 pub memo: Option<String>,
17}
18
19#[derive(Accounts)]
20pub struct SpendingLimitUse<'info> {
21 #[account(
23 seeds = [SEED_PREFIX, SEED_MULTISIG, multisig.create_key.as_ref()],
24 bump = multisig.bump,
25 )]
26 pub multisig: Box<Account<'info, Multisig>>,
27
28 pub member: Signer<'info>,
29
30 #[account(
32 mut,
33 seeds = [
34 SEED_PREFIX,
35 multisig.key().as_ref(),
36 SEED_SPENDING_LIMIT,
37 spending_limit.create_key.key().as_ref(),
38 ],
39 bump = spending_limit.bump,
40 )]
41 pub spending_limit: Account<'info, SpendingLimit>,
42
43 #[account(
46 mut,
47 seeds = [
48 SEED_PREFIX,
49 multisig.key().as_ref(),
50 SEED_VAULT,
51 &spending_limit.vault_index.to_le_bytes(),
52 ],
53 bump
54 )]
55 pub vault: AccountInfo<'info>,
56
57 #[account(mut)]
60 pub destination: AccountInfo<'info>,
61
62 pub system_program: Option<Program<'info, System>>,
64
65 pub mint: Option<InterfaceAccount<'info, Mint>>,
68
69 #[account(
71 mut,
72 token::mint = mint,
73 token::authority = vault,
74 )]
75 pub vault_token_account: Option<InterfaceAccount<'info, TokenAccount>>,
76
77 #[account(
79 mut,
80 token::mint = mint,
81 token::authority = destination,
82 )]
83 pub destination_token_account: Option<InterfaceAccount<'info, TokenAccount>>,
84
85 pub token_program: Option<Interface<'info, TokenInterface>>,
87}
88
89impl SpendingLimitUse<'_> {
90 fn validate(&self) -> Result<()> {
91 let Self {
92 multisig,
93 member,
94 spending_limit,
95 mint,
96 ..
97 } = self;
98
99 require!(
101 spending_limit.members.contains(&member.key()),
102 MultisigError::Unauthorized
103 );
104
105 if spending_limit.mint == Pubkey::default() {
109 require!(mint.is_none(), MultisigError::InvalidMint);
111 } else {
112 require!(
114 spending_limit.mint == mint.as_ref().unwrap().key(),
115 MultisigError::InvalidMint
116 );
117 }
118
119 if !spending_limit.destinations.is_empty() {
125 require!(
126 spending_limit
127 .destinations
128 .contains(&self.destination.key()),
129 MultisigError::InvalidDestination
130 );
131 }
132
133 Ok(())
136 }
137
138 #[access_control(ctx.accounts.validate())]
140 pub fn spending_limit_use(ctx: Context<Self>, args: SpendingLimitUseArgs) -> Result<()> {
141 let spending_limit = &mut ctx.accounts.spending_limit;
142 let vault = &mut ctx.accounts.vault;
143 let destination = &mut ctx.accounts.destination;
144
145 let multisig_key = ctx.accounts.multisig.key();
146 let vault_bump = ctx.bumps.vault;
147 let now = Clock::get()?.unix_timestamp;
148
149 if let Some(reset_period) = spending_limit.period.to_seconds() {
151 let passed_since_last_reset = now.checked_sub(spending_limit.last_reset).unwrap();
152
153 if passed_since_last_reset > reset_period {
154 spending_limit.remaining_amount = spending_limit.amount;
155
156 let periods_passed = passed_since_last_reset.checked_div(reset_period).unwrap();
157
158 spending_limit.last_reset = spending_limit
160 .last_reset
161 .checked_add(periods_passed.checked_mul(reset_period).unwrap())
162 .unwrap();
163 }
164 }
165
166 spending_limit.remaining_amount = spending_limit
169 .remaining_amount
170 .checked_sub(args.amount)
171 .ok_or(MultisigError::SpendingLimitExceeded)?;
172
173 if spending_limit.mint == Pubkey::default() {
175 let system_program = &ctx
177 .accounts
178 .system_program
179 .as_ref()
180 .ok_or(MultisigError::MissingAccount)?;
181
182 require!(args.decimals == 9, MultisigError::DecimalsMismatch);
184
185 anchor_lang::system_program::transfer(
186 CpiContext::new_with_signer(
187 system_program.to_account_info(),
188 anchor_lang::system_program::Transfer {
189 from: vault.clone(),
190 to: destination.clone(),
191 },
192 &[&[
193 SEED_PREFIX,
194 multisig_key.as_ref(),
195 SEED_VAULT,
196 &spending_limit.vault_index.to_le_bytes(),
197 &[vault_bump],
198 ]],
199 ),
200 args.amount,
201 )?
202 } else {
203 let mint = &ctx
205 .accounts
206 .mint
207 .as_ref()
208 .ok_or(MultisigError::MissingAccount)?;
209 let vault_token_account = &ctx
210 .accounts
211 .vault_token_account
212 .as_ref()
213 .ok_or(MultisigError::MissingAccount)?;
214 let destination_token_account = &ctx
215 .accounts
216 .destination_token_account
217 .as_ref()
218 .ok_or(MultisigError::MissingAccount)?;
219 let token_program = &ctx
220 .accounts
221 .token_program
222 .as_ref()
223 .ok_or(MultisigError::MissingAccount)?;
224
225 msg!(
226 "token_program {} mint {} vault {} destination {} amount {} decimals {}",
227 &token_program.key,
228 &mint.key(),
229 &vault.key,
230 &destination.key,
231 &args.amount,
232 &args.decimals
233 );
234
235 token_interface::transfer_checked(
236 CpiContext::new_with_signer(
237 token_program.to_account_info(),
238 TransferChecked {
239 from: vault_token_account.to_account_info(),
240 mint: mint.to_account_info(),
241 to: destination_token_account.to_account_info(),
242 authority: vault.clone(),
243 },
244 &[&[
245 SEED_PREFIX,
246 multisig_key.as_ref(),
247 SEED_VAULT,
248 &spending_limit.vault_index.to_le_bytes(),
249 &[vault_bump],
250 ]],
251 ),
252 args.amount,
253 args.decimals,
254 )?;
255 }
256
257 Ok(())
258 }
259}