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 multisig.is_member(member.key()).is_some(),
102 MultisigError::NotAMember
103 );
104 require!(
106 spending_limit.members.contains(&member.key()),
107 MultisigError::Unauthorized
108 );
109
110 if spending_limit.mint == Pubkey::default() {
114 require!(mint.is_none(), MultisigError::InvalidMint);
116 } else {
117 require!(
119 spending_limit.mint == mint.as_ref().unwrap().key(),
120 MultisigError::InvalidMint
121 );
122 }
123
124 if !spending_limit.destinations.is_empty() {
130 require!(
131 spending_limit
132 .destinations
133 .contains(&self.destination.key()),
134 MultisigError::InvalidDestination
135 );
136 }
137
138 Ok(())
141 }
142
143 #[access_control(ctx.accounts.validate())]
145 pub fn spending_limit_use(ctx: Context<Self>, args: SpendingLimitUseArgs) -> Result<()> {
146 let spending_limit = &mut ctx.accounts.spending_limit;
147 let vault = &mut ctx.accounts.vault;
148 let destination = &mut ctx.accounts.destination;
149
150 let multisig_key = ctx.accounts.multisig.key();
151 let vault_bump = ctx.bumps.vault;
152 let now = Clock::get()?.unix_timestamp;
153
154 if let Some(reset_period) = spending_limit.period.to_seconds() {
156 let passed_since_last_reset = now.checked_sub(spending_limit.last_reset).unwrap();
157
158 if passed_since_last_reset > reset_period {
159 spending_limit.remaining_amount = spending_limit.amount;
160
161 let periods_passed = passed_since_last_reset.checked_div(reset_period).unwrap();
162
163 spending_limit.last_reset = spending_limit
165 .last_reset
166 .checked_add(periods_passed.checked_mul(reset_period).unwrap())
167 .unwrap();
168 }
169 }
170
171 spending_limit.remaining_amount = spending_limit
174 .remaining_amount
175 .checked_sub(args.amount)
176 .ok_or(MultisigError::SpendingLimitExceeded)?;
177
178 if spending_limit.mint == Pubkey::default() {
180 let system_program = &ctx
182 .accounts
183 .system_program
184 .as_ref()
185 .ok_or(MultisigError::MissingAccount)?;
186
187 require!(args.decimals == 9, MultisigError::DecimalsMismatch);
189
190 anchor_lang::system_program::transfer(
191 CpiContext::new_with_signer(
192 system_program.to_account_info(),
193 anchor_lang::system_program::Transfer {
194 from: vault.clone(),
195 to: destination.clone(),
196 },
197 &[&[
198 SEED_PREFIX,
199 multisig_key.as_ref(),
200 SEED_VAULT,
201 &spending_limit.vault_index.to_le_bytes(),
202 &[vault_bump],
203 ]],
204 ),
205 args.amount,
206 )?
207 } else {
208 let mint = &ctx
210 .accounts
211 .mint
212 .as_ref()
213 .ok_or(MultisigError::MissingAccount)?;
214 let vault_token_account = &ctx
215 .accounts
216 .vault_token_account
217 .as_ref()
218 .ok_or(MultisigError::MissingAccount)?;
219 let destination_token_account = &ctx
220 .accounts
221 .destination_token_account
222 .as_ref()
223 .ok_or(MultisigError::MissingAccount)?;
224 let token_program = &ctx
225 .accounts
226 .token_program
227 .as_ref()
228 .ok_or(MultisigError::MissingAccount)?;
229
230 msg!(
231 "token_program {} mint {} vault {} destination {} amount {} decimals {}",
232 &token_program.key,
233 &mint.key(),
234 &vault.key,
235 &destination.key,
236 &args.amount,
237 &args.decimals
238 );
239
240 token_interface::transfer_checked(
241 CpiContext::new_with_signer(
242 token_program.to_account_info(),
243 TransferChecked {
244 from: vault_token_account.to_account_info(),
245 mint: mint.to_account_info(),
246 to: destination_token_account.to_account_info(),
247 authority: vault.clone(),
248 },
249 &[&[
250 SEED_PREFIX,
251 multisig_key.as_ref(),
252 SEED_VAULT,
253 &spending_limit.vault_index.to_le_bytes(),
254 &[vault_bump],
255 ]],
256 ),
257 args.amount,
258 args.decimals,
259 )?;
260 }
261
262 Ok(())
263 }
264}