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#[derive(Accounts)]
19#[instruction(num_accounts: u16, data_len: u16)]
20pub struct CreateInstructionBuffer<'info> {
21 #[account(mut)]
23 pub authority: Signer<'info>,
24 pub store: UncheckedAccount<'info>,
27 #[account(has_one = store)]
29 pub executor: AccountLoader<'info, Executor>,
30 #[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 pub instruction_program: UncheckedAccount<'info>,
40 pub store_program: Program<'info, GmsolStore>,
42 pub system_program: Program<'info, System>,
44}
45
46pub(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#[derive(Accounts)]
116#[instruction(role: String)]
117pub struct ApproveInstruction<'info> {
118 pub authority: Signer<'info>,
120 pub store: UncheckedAccount<'info>,
123 #[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 #[account(mut, has_one = executor)]
137 pub instruction: AccountLoader<'info, InstructionHeader>,
138 pub store_program: Program<'info, GmsolStore>,
140}
141
142pub(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#[derive(Accounts)]
173#[instruction(role: String)]
174pub struct ApproveInstructions<'info> {
175 pub authority: Signer<'info>,
177 pub store: UncheckedAccount<'info>,
180 #[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 pub store_program: Program<'info, GmsolStore>,
194}
195
196pub(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#[derive(Accounts)]
241pub struct CancelInstruction<'info> {
242 pub authority: Signer<'info>,
244 pub store: UncheckedAccount<'info>,
247 #[account(has_one = store)]
249 pub executor: AccountLoader<'info, Executor>,
250 #[account(mut)]
253 pub rent_receiver: UncheckedAccount<'info>,
254 #[account(
256 mut,
257 has_one = executor,
258 has_one = rent_receiver,
259 close = rent_receiver,
260 )]
261 pub instruction: AccountLoader<'info, InstructionHeader>,
262 pub store_program: Program<'info, GmsolStore>,
264}
265
266pub(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#[derive(Accounts)]
295pub struct CancelInstructions<'info> {
296 pub authority: Signer<'info>,
298 pub store: UncheckedAccount<'info>,
301 #[account(has_one = store)]
303 pub executor: AccountLoader<'info, Executor>,
304 #[account(mut)]
307 pub rent_receiver: UncheckedAccount<'info>,
308 pub store_program: Program<'info, GmsolStore>,
310}
311
312pub(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#[derive(Accounts)]
363pub struct ExecuteInstruction<'info> {
364 pub authority: Signer<'info>,
366 pub store: AccountLoader<'info, Store>,
368 #[account(has_one = store)]
370 pub timelock_config: AccountLoader<'info, TimelockConfig>,
371 #[account(has_one = store)]
373 pub executor: AccountLoader<'info, Executor>,
374 #[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 #[account(mut)]
386 pub rent_receiver: UncheckedAccount<'info>,
387 #[account(
389 mut,
390 has_one = executor,
391 has_one = rent_receiver,
392 close = rent_receiver,
393 )]
394 pub instruction: AccountLoader<'info, InstructionHeader>,
395 pub store_program: Program<'info, GmsolStore>,
397}
398
399pub(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 {
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}