Skip to main content

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            spending_limit.members.contains(&member.key()),
102            MultisigError::Unauthorized
103        );
104
105        // spending_limit - needs no checking.
106
107        // mint
108        if spending_limit.mint == Pubkey::default() {
109            // SpendingLimit is for SOL, there should be no mint account in this case.
110            require!(mint.is_none(), MultisigError::InvalidMint);
111        } else {
112            // SpendingLimit is for an SPL token, `mint` must match `spending_limit.mint`.
113            require!(
114                spending_limit.mint == mint.as_ref().unwrap().key(),
115                MultisigError::InvalidMint
116            );
117        }
118
119        // vault - checked in the #[account] attribute.
120
121        // vault_token_account - checked in the #[account] attribute.
122
123        // destination
124        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        // destination_token_account - checked in the #[account] attribute.
134
135        Ok(())
136    }
137
138    /// Use a spending limit to transfer tokens from a multisig vault to a destination account.
139    #[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        // Reset `spending_limit.remaining_amount` if the `spending_limit.period` has passed.
150        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                // last_reset = last_reset + periods_passed * reset_period,
159                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        // Update `spending_limit.remaining_amount`.
167        // This will also check if `amount` doesn't exceed `spending_limit.remaining_amount`.
168        spending_limit.remaining_amount = spending_limit
169            .remaining_amount
170            .checked_sub(args.amount)
171            .ok_or(MultisigError::SpendingLimitExceeded)?;
172
173        // Transfer tokens.
174        if spending_limit.mint == Pubkey::default() {
175            // Transfer using the system_program::transfer.
176            let system_program = &ctx
177                .accounts
178                .system_program
179                .as_ref()
180                .ok_or(MultisigError::MissingAccount)?;
181
182            // Sanity check for the decimals. Similar to the one in token_interface::transfer_checked.
183            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            // Transfer using the token_program::transfer_checked.
204            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}