squads_multisig_program/instructions/
spending_limit_use.rs

1use 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    /// Amount of tokens to transfer.
12    pub amount: u64,
13    /// Decimals of the token mint. Used for double-checking against incorrect order of magnitude of `amount`.
14    pub decimals: u8,
15    /// Memo used for indexing.
16    pub memo: Option<String>,
17}
18
19#[derive(Accounts)]
20pub struct SpendingLimitUse<'info> {
21    /// The multisig account the `spending_limit` is for.
22    #[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    /// The SpendingLimit account to use.
31    #[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    /// Multisig vault account to transfer tokens from.
44    /// CHECK: All the required checks are done by checking the seeds.
45    #[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    /// Destination account to transfer tokens to.
58    /// CHECK: We do the checks in `SpendingLimitUse::validate`.
59    #[account(mut)]
60    pub destination: AccountInfo<'info>,
61
62    /// In case `spending_limit.mint` is SOL.
63    pub system_program: Option<Program<'info, System>>,
64
65    /// The mint of the tokens to transfer in case `spending_limit.mint` is an SPL token.
66    /// CHECK: We do the checks in `SpendingLimitUse::validate`.
67    pub mint: Option<InterfaceAccount<'info, Mint>>,
68
69    /// Multisig vault token account to transfer tokens from in case `spending_limit.mint` is an SPL token.
70    #[account(
71        mut,
72        token::mint = mint,
73        token::authority = vault,
74    )]
75    pub vault_token_account: Option<InterfaceAccount<'info, TokenAccount>>,
76
77    /// Destination token account in case `spending_limit.mint` is an SPL token.
78    #[account(
79        mut,
80        token::mint = mint,
81        token::authority = destination,
82    )]
83    pub destination_token_account: Option<InterfaceAccount<'info, TokenAccount>>,
84
85    /// In case `spending_limit.mint` is an SPL token.
86    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        // member
100        require!(
101            multisig.is_member(member.key()).is_some(),
102            MultisigError::NotAMember
103        );
104        // We don't check member's permissions here but we check if the spending_limit is for the member.
105        require!(
106            spending_limit.members.contains(&member.key()),
107            MultisigError::Unauthorized
108        );
109
110        // spending_limit - needs no checking.
111
112        // mint
113        if spending_limit.mint == Pubkey::default() {
114            // SpendingLimit is for SOL, there should be no mint account in this case.
115            require!(mint.is_none(), MultisigError::InvalidMint);
116        } else {
117            // SpendingLimit is for an SPL token, `mint` must match `spending_limit.mint`.
118            require!(
119                spending_limit.mint == mint.as_ref().unwrap().key(),
120                MultisigError::InvalidMint
121            );
122        }
123
124        // vault - checked in the #[account] attribute.
125
126        // vault_token_account - checked in the #[account] attribute.
127
128        // destination
129        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        // destination_token_account - checked in the #[account] attribute.
139
140        Ok(())
141    }
142
143    /// Use a spending limit to transfer tokens from a multisig vault to a destination account.
144    #[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        // Reset `spending_limit.remaining_amount` if the `spending_limit.period` has passed.
155        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                // last_reset = last_reset + periods_passed * reset_period,
164                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        // Update `spending_limit.remaining_amount`.
172        // This will also check if `amount` doesn't exceed `spending_limit.remaining_amount`.
173        spending_limit.remaining_amount = spending_limit
174            .remaining_amount
175            .checked_sub(args.amount)
176            .ok_or(MultisigError::SpendingLimitExceeded)?;
177
178        // Transfer tokens.
179        if spending_limit.mint == Pubkey::default() {
180            // Transfer using the system_program::transfer.
181            let system_program = &ctx
182                .accounts
183                .system_program
184                .as_ref()
185                .ok_or(MultisigError::MissingAccount)?;
186
187            // Sanity check for the decimals. Similar to the one in token_interface::transfer_checked.
188            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            // Transfer using the token_program::transfer_checked.
209            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}