gmsol_timelock/instructions/
instruction_buffer.rs

1use anchor_lang::{prelude::*, solana_program::program::invoke_signed};
2use gmsol_store::{
3    program::GmsolStore,
4    states::{Seed, Store, MAX_ROLE_NAME_LEN},
5    utils::{fixed_str::fixed_str_to_bytes, CpiAuthenticate, CpiAuthentication, WithStore},
6    CoreError,
7};
8
9use crate::{
10    roles,
11    states::{
12        config::TimelockConfig, Executor, ExecutorWalletSigner, InstructionAccess,
13        InstructionHeader, InstructionLoader,
14    },
15};
16
17/// The accounts definition for [`create_instruction_buffer`](crate::gmsol_timelock::create_instruction_buffer).
18#[derive(Accounts)]
19#[instruction(num_accounts: u16, data_len: u16)]
20pub struct CreateInstructionBuffer<'info> {
21    /// Authority.
22    #[account(mut)]
23    pub authority: Signer<'info>,
24    /// Store.
25    /// CHECK: check by CPI.
26    pub store: UncheckedAccount<'info>,
27    /// Expected executor.
28    #[account(has_one = store)]
29    pub executor: AccountLoader<'info, Executor>,
30    /// Instruction buffer to create.
31    #[account(
32        init,
33        payer = authority,
34        space = 8 + InstructionHeader::init_space(num_accounts, data_len),
35    )]
36    pub instruction_buffer: AccountLoader<'info, InstructionHeader>,
37    /// Instruction Program.
38    /// CHECK: only used as an address.
39    pub instruction_program: UncheckedAccount<'info>,
40    /// Store program.
41    pub store_program: Program<'info, GmsolStore>,
42    /// The system program.
43    pub system_program: Program<'info, System>,
44}
45
46/// Create instruction buffer.
47/// # CHECK
48/// Only [`TIMELOCK_KEEPER`](crate::roles::TIMELOCK_KEEPER) can use.
49pub(crate) fn unchecked_create_instruction_buffer<'info>(
50    ctx: Context<'_, '_, 'info, 'info, CreateInstructionBuffer<'info>>,
51    num_accounts: u16,
52    data_len: u16,
53    data: &[u8],
54    signers: &[u16],
55) -> Result<()> {
56    let remaining_accounts = ctx.remaining_accounts;
57    let num_accounts = usize::from(num_accounts);
58    require_gte!(
59        remaining_accounts.len(),
60        num_accounts,
61        CoreError::InvalidArgument
62    );
63
64    require_eq!(
65        data.len(),
66        usize::from(data_len),
67        CoreError::InvalidArgument
68    );
69
70    let wallet_bump = {
71        let executor = ctx.accounts.executor.load()?;
72        let wallet_bump = executor.wallet_bump;
73        msg!(
74            "[Timelock] creating instruction buffer for program {}, with executor `{}`",
75            ctx.accounts.instruction_program.key,
76            executor.role_name()?,
77        );
78        wallet_bump
79    };
80
81    ctx.accounts.instruction_buffer.load_and_init_instruction(
82        ctx.accounts.executor.key(),
83        wallet_bump,
84        ctx.accounts.authority.key(),
85        ctx.accounts.instruction_program.key(),
86        data,
87        &remaining_accounts[0..num_accounts],
88        signers,
89    )?;
90
91    Ok(())
92}
93
94impl<'info> WithStore<'info> for CreateInstructionBuffer<'info> {
95    fn store_program(&self) -> AccountInfo<'info> {
96        self.store_program.to_account_info()
97    }
98
99    fn store(&self) -> AccountInfo<'info> {
100        self.store.to_account_info()
101    }
102}
103
104impl<'info> CpiAuthentication<'info> for CreateInstructionBuffer<'info> {
105    fn authority(&self) -> AccountInfo<'info> {
106        self.authority.to_account_info()
107    }
108
109    fn on_error(&self) -> Result<()> {
110        err!(CoreError::PermissionDenied)
111    }
112}
113
114/// The accounts definition for [`approve_instruction`](crate::gmsol_timelock::approve_instruction).
115#[derive(Accounts)]
116#[instruction(role: String)]
117pub struct ApproveInstruction<'info> {
118    /// Authority.
119    pub authority: Signer<'info>,
120    /// Store.
121    /// CHECK: check by CPI.
122    pub store: UncheckedAccount<'info>,
123    /// Executor.
124    #[account(
125        has_one = store,
126        constraint = executor.load()?.role_name()? == role.as_str() @ CoreError::InvalidArgument,
127        seeds = [
128            Executor::SEED,
129            store.key.as_ref(),
130            &fixed_str_to_bytes::<MAX_ROLE_NAME_LEN>(&role)?,
131        ],
132        bump = executor.load()?.bump,
133    )]
134    pub executor: AccountLoader<'info, Executor>,
135    /// Instruction to approve.
136    #[account(mut, has_one = executor)]
137    pub instruction: AccountLoader<'info, InstructionHeader>,
138    /// Store program.
139    pub store_program: Program<'info, GmsolStore>,
140}
141
142/// Approve instruction.
143pub(crate) fn approve_instruction(ctx: Context<ApproveInstruction>, role: &str) -> Result<()> {
144    validate_timelocked_role(&ctx, role)?;
145    ctx.accounts
146        .instruction
147        .load_mut()?
148        .approve(ctx.accounts.authority.key())
149}
150
151impl<'info> WithStore<'info> for ApproveInstruction<'info> {
152    fn store_program(&self) -> AccountInfo<'info> {
153        self.store_program.to_account_info()
154    }
155
156    fn store(&self) -> AccountInfo<'info> {
157        self.store.to_account_info()
158    }
159}
160
161impl<'info> CpiAuthentication<'info> for ApproveInstruction<'info> {
162    fn authority(&self) -> AccountInfo<'info> {
163        self.authority.to_account_info()
164    }
165
166    fn on_error(&self) -> Result<()> {
167        err!(CoreError::PermissionDenied)
168    }
169}
170
171/// The accounts definition for [`approve_instructions`](crate::gmsol_timelock::approve_instructions).
172#[derive(Accounts)]
173#[instruction(role: String)]
174pub struct ApproveInstructions<'info> {
175    /// Authority.
176    pub authority: Signer<'info>,
177    /// Store.
178    /// CHECK: check by CPI.
179    pub store: UncheckedAccount<'info>,
180    /// Executor.
181    #[account(
182        has_one = store,
183        constraint = executor.load()?.role_name()? == role.as_str() @ CoreError::InvalidArgument,
184        seeds = [
185            Executor::SEED,
186            store.key.as_ref(),
187            &fixed_str_to_bytes::<MAX_ROLE_NAME_LEN>(&role)?,
188        ],
189        bump = executor.load()?.bump,
190    )]
191    pub executor: AccountLoader<'info, Executor>,
192    /// Store program.
193    pub store_program: Program<'info, GmsolStore>,
194}
195
196/// Approve instructions.
197pub(crate) fn approve_instructions<'info>(
198    ctx: Context<'_, '_, 'info, 'info, ApproveInstructions<'info>>,
199    role: &str,
200) -> Result<()> {
201    validate_timelocked_role(&ctx, role)?;
202
203    let executor = ctx.accounts.executor.key();
204    let approver = ctx.accounts.authority.key();
205    for account in ctx.remaining_accounts {
206        require!(account.is_writable, ErrorCode::AccountNotMutable);
207        let loader = AccountLoader::<InstructionHeader>::try_from(account)?;
208        require_keys_eq!(
209            *loader.load()?.executor(),
210            executor,
211            CoreError::InvalidArgument
212        );
213        loader.load_mut()?.approve(approver)?;
214    }
215
216    Ok(())
217}
218
219impl<'info> WithStore<'info> for ApproveInstructions<'info> {
220    fn store_program(&self) -> AccountInfo<'info> {
221        self.store_program.to_account_info()
222    }
223
224    fn store(&self) -> AccountInfo<'info> {
225        self.store.to_account_info()
226    }
227}
228
229impl<'info> CpiAuthentication<'info> for ApproveInstructions<'info> {
230    fn authority(&self) -> AccountInfo<'info> {
231        self.authority.to_account_info()
232    }
233
234    fn on_error(&self) -> Result<()> {
235        err!(CoreError::PermissionDenied)
236    }
237}
238
239/// The accounts definition for [`cancel_instruction`](crate::gmsol_timelock::cancel_instruction).
240#[derive(Accounts)]
241pub struct CancelInstruction<'info> {
242    /// Authority.
243    pub authority: Signer<'info>,
244    /// Store.
245    /// CHECK: check by CPI.
246    pub store: UncheckedAccount<'info>,
247    /// Executor.
248    #[account(has_one = store)]
249    pub executor: AccountLoader<'info, Executor>,
250    /// Rent receiver.
251    /// CHECK: only used to receive funds.
252    #[account(mut)]
253    pub rent_receiver: UncheckedAccount<'info>,
254    /// Instruction to cancel.
255    #[account(
256        mut,
257        has_one = executor,
258        has_one = rent_receiver,
259        close = rent_receiver,
260    )]
261    pub instruction: AccountLoader<'info, InstructionHeader>,
262    /// Store program.
263    pub store_program: Program<'info, GmsolStore>,
264}
265
266/// Cancel instruction.
267/// # CHECK
268/// Only [`TIMELOCK_ADMIN`](crate::roles::TIMELOCK_ADMIN) can use.
269pub(crate) fn unchecked_cancel_instruction(_ctx: Context<CancelInstruction>) -> Result<()> {
270    Ok(())
271}
272
273impl<'info> WithStore<'info> for CancelInstruction<'info> {
274    fn store_program(&self) -> AccountInfo<'info> {
275        self.store_program.to_account_info()
276    }
277
278    fn store(&self) -> AccountInfo<'info> {
279        self.store.to_account_info()
280    }
281}
282
283impl<'info> CpiAuthentication<'info> for CancelInstruction<'info> {
284    fn authority(&self) -> AccountInfo<'info> {
285        self.authority.to_account_info()
286    }
287
288    fn on_error(&self) -> Result<()> {
289        err!(CoreError::PermissionDenied)
290    }
291}
292
293/// The accounts definition for [`cancel_instructions`](crate::gmsol_timelock::cancel_instructions).
294#[derive(Accounts)]
295pub struct CancelInstructions<'info> {
296    /// Authority.
297    pub authority: Signer<'info>,
298    /// Store.
299    /// CHECK: check by CPI.
300    pub store: UncheckedAccount<'info>,
301    /// Executor.
302    #[account(has_one = store)]
303    pub executor: AccountLoader<'info, Executor>,
304    /// Rent receiver.
305    /// CHECK: only used to receive funds.
306    #[account(mut)]
307    pub rent_receiver: UncheckedAccount<'info>,
308    /// Store program.
309    pub store_program: Program<'info, GmsolStore>,
310}
311
312/// Cancel instructions that sharing the same `executor` and `rent_receiver`.
313/// # CHECK
314/// Only [`TIMELOCK_ADMIN`](crate::roles::TIMELOCK_ADMIN) can use.
315pub(crate) fn unchecked_cancel_instructions<'info>(
316    ctx: Context<'_, '_, 'info, 'info, CancelInstructions<'info>>,
317) -> Result<()> {
318    let executor = ctx.accounts.executor.key();
319    let rent_receiver = ctx.accounts.rent_receiver.key();
320
321    for account in ctx.remaining_accounts {
322        require!(account.is_writable, ErrorCode::AccountNotMutable);
323        let loader = AccountLoader::<InstructionHeader>::try_from(account)?;
324
325        {
326            let header = loader.load()?;
327            require_keys_eq!(*header.executor(), executor, CoreError::InvalidArgument);
328            require_keys_eq!(
329                *header.rent_receiver(),
330                rent_receiver,
331                CoreError::InvalidArgument
332            );
333        }
334
335        loader.close(ctx.accounts.rent_receiver.to_account_info())?;
336    }
337
338    Ok(())
339}
340
341impl<'info> WithStore<'info> for CancelInstructions<'info> {
342    fn store_program(&self) -> AccountInfo<'info> {
343        self.store_program.to_account_info()
344    }
345
346    fn store(&self) -> AccountInfo<'info> {
347        self.store.to_account_info()
348    }
349}
350
351impl<'info> CpiAuthentication<'info> for CancelInstructions<'info> {
352    fn authority(&self) -> AccountInfo<'info> {
353        self.authority.to_account_info()
354    }
355
356    fn on_error(&self) -> Result<()> {
357        err!(CoreError::PermissionDenied)
358    }
359}
360
361/// The acccounts definition for [`execute_instruction`](crate::gmsol_timelock::execute_instruction).
362#[derive(Accounts)]
363pub struct ExecuteInstruction<'info> {
364    /// Authority.
365    pub authority: Signer<'info>,
366    /// Store.
367    pub store: AccountLoader<'info, Store>,
368    /// Timelock config.
369    #[account(has_one = store)]
370    pub timelock_config: AccountLoader<'info, TimelockConfig>,
371    /// Executor.
372    #[account(has_one = store)]
373    pub executor: AccountLoader<'info, Executor>,
374    /// Executor Wallet.
375    /// CHECK: `wallet` doesn't have to be a system account, allowing
376    /// the instruction to close it.
377    #[account(
378        mut,
379        seeds = [Executor::WALLET_SEED, executor.key().as_ref()],
380        bump = executor.load()?.wallet_bump,
381    )]
382    pub wallet: UncheckedAccount<'info>,
383    /// Rent receiver.
384    /// CHECK: only used to receive funds.
385    #[account(mut)]
386    pub rent_receiver: UncheckedAccount<'info>,
387    /// Instruction to execute.
388    #[account(
389        mut,
390        has_one = executor,
391        has_one = rent_receiver,
392        close = rent_receiver,
393    )]
394    pub instruction: AccountLoader<'info, InstructionHeader>,
395    /// Store program.
396    pub store_program: Program<'info, GmsolStore>,
397}
398
399/// Execute instruction.
400/// # CHECK
401/// Only [`TIMELOCK_KEEPER`](crate::roles::TIMELOCK_KEEPER) can use.
402pub(crate) fn unchecked_execute_instruction(ctx: Context<ExecuteInstruction>) -> Result<()> {
403    let remaining_accounts = ctx.remaining_accounts;
404
405    let instruction = ctx.accounts.instruction.load_instruction()?;
406
407    // Validate that the approver still have the required role.
408    {
409        let store = ctx.accounts.store.load()?;
410        let approver = instruction
411            .header()
412            .apporver()
413            .ok_or_else(|| error!(CoreError::PreconditionsAreNotMet))?;
414        let timelocked_role = roles::timelocked_role(ctx.accounts.executor.load()?.role_name()?);
415        require!(
416            store.has_role(approver, &timelocked_role)?,
417            CoreError::PreconditionsAreNotMet
418        );
419    }
420
421    let delay = ctx.accounts.timelock_config.load()?.delay();
422    require!(
423        instruction.header().is_executable(delay)?,
424        CoreError::PreconditionsAreNotMet
425    );
426
427    let signer = ExecutorWalletSigner::new(
428        ctx.accounts.executor.key(),
429        ctx.accounts.executor.load()?.wallet_bump,
430    );
431
432    invoke_signed(
433        &instruction.to_instruction(false).map_err(|err| {
434            msg!("Instruction error: {}", err);
435            error!(CoreError::InvalidArgument)
436        })?,
437        remaining_accounts,
438        &[&signer.as_seeds()],
439    )?;
440    Ok(())
441}
442
443impl<'info> WithStore<'info> for ExecuteInstruction<'info> {
444    fn store_program(&self) -> AccountInfo<'info> {
445        self.store_program.to_account_info()
446    }
447
448    fn store(&self) -> AccountInfo<'info> {
449        self.store.to_account_info()
450    }
451}
452
453impl<'info> CpiAuthentication<'info> for ExecuteInstruction<'info> {
454    fn authority(&self) -> AccountInfo<'info> {
455        self.authority.to_account_info()
456    }
457
458    fn on_error(&self) -> Result<()> {
459        err!(CoreError::PermissionDenied)
460    }
461}
462
463fn validate_timelocked_role<'info>(
464    ctx: &Context<impl CpiAuthenticate<'info>>,
465    role: &str,
466) -> Result<()> {
467    let timelocked_role = roles::timelocked_role(role);
468    CpiAuthenticate::only(ctx, &timelocked_role)?;
469    msg!(
470        "[Timelock] approving `{}` instruction by a `{}`",
471        role,
472        timelocked_role
473    );
474    Ok(())
475}