cronos_scheduler/state/
task.rs

1use crate::state::{ManagerAccount, QueueAccount, QueueStatus};
2
3use super::{Config, Fee, Manager};
4
5use {
6    super::Queue,
7    crate::{errors::CronosError, pda::PDA},
8    anchor_lang::{
9        prelude::borsh::BorshSchema, prelude::*, solana_program::instruction::Instruction,
10        AnchorDeserialize,
11    },
12    std::convert::TryFrom,
13};
14
15pub const SEED_TASK: &[u8] = b"task";
16
17/**
18 * Task
19 */
20
21#[account]
22#[derive(Debug)]
23pub struct Task {
24    pub id: u128,
25    pub ixs: Vec<InstructionData>,
26    pub queue: Pubkey,
27}
28
29impl Task {
30    pub fn pda(queue: Pubkey, id: u128) -> PDA {
31        Pubkey::find_program_address(
32            &[SEED_TASK, queue.as_ref(), id.to_be_bytes().as_ref()],
33            &crate::ID,
34        )
35    }
36}
37
38impl TryFrom<Vec<u8>> for Task {
39    type Error = Error;
40    fn try_from(data: Vec<u8>) -> std::result::Result<Self, Self::Error> {
41        Task::try_deserialize(&mut data.as_slice())
42    }
43}
44
45/**
46 * TaskAccount
47 */
48
49pub trait TaskAccount {
50    fn new(&mut self, ixs: Vec<InstructionData>, queue: &mut Account<Queue>) -> Result<()>;
51
52    fn exec(
53        &mut self,
54        account_infos: &Vec<AccountInfo>,
55        config: &Account<Config>,
56        delegate: &mut Signer,
57        fee: &mut Account<Fee>,
58        manager: &Account<Manager>,
59        manager_bump: u8,
60        queue: &mut Account<Queue>,
61    ) -> Result<()>;
62}
63
64impl TaskAccount for Account<'_, Task> {
65    fn new(&mut self, ixs: Vec<InstructionData>, queue: &mut Account<Queue>) -> Result<()> {
66        // Reject inner instructions if they have a signer other than the manager or delegate
67        for ix in ixs.iter() {
68            for acc in ix.accounts.iter() {
69                if acc.is_signer {
70                    require!(
71                        acc.pubkey == queue.manager || acc.pubkey == crate::payer::ID,
72                        CronosError::InvalidSignatory
73                    );
74                }
75            }
76        }
77
78        // Save data
79        self.id = queue.task_count;
80        self.ixs = ixs;
81        self.queue = queue.key();
82
83        // Increment the queue's task count
84        queue.task_count = queue.task_count.checked_add(1).unwrap();
85
86        Ok(())
87    }
88
89    fn exec(
90        &mut self,
91        account_infos: &Vec<AccountInfo>,
92        config: &Account<Config>,
93        delegate: &mut Signer,
94        fee: &mut Account<Fee>,
95        manager: &Account<Manager>,
96        manager_bump: u8,
97        queue: &mut Account<Queue>,
98    ) -> Result<()> {
99        // Validate the task id matches the queue's current execution state
100        require!(
101            self.id
102                == match queue.status {
103                    QueueStatus::Processing { task_id } => task_id,
104                    _ => return Err(CronosError::InvalidQueueStatus.into()),
105                },
106            CronosError::InvalidTask
107        );
108
109        // Validate the delegate data is empty
110        require!(delegate.data_is_empty(), CronosError::DelegateDataNotEmpty);
111
112        // Record the delegate's lamports before invoking inner ixs
113        let delegate_lamports_pre = delegate.lamports();
114
115        // Create an array of dynamic ixs to update the task for the next invocation
116        let dyanmic_ixs: &mut Vec<InstructionData> = &mut vec![];
117
118        // Process all of the task instructions
119        for ix in &self.ixs {
120            // If an inner ix account matches the Cronos delegate address (CronosDe1egate11111111111111111111111111111),
121            //  then inject the delegate account in its place. Dapp developers can use the delegate as a payer to initialize
122            //  new accouns in their queues. Delegates will be reimbursed for all SOL spent during the inner ixs.
123            //
124            // Because the delegate can be injected as the signer on inner ixs (written by presumed malicious parties),
125            //  node operators should not secure any assets or staking positions with their delegate wallets other than
126            //  an operational level of lamports needed to submit txns (~0.1 ⊚).
127            //
128            // TODO Update the network program to allow for split identity / delegate addresses so CRON stakes
129            //  are not secured by delegate signatures.
130            let accs: &mut Vec<AccountMetaData> = &mut vec![];
131            ix.accounts.iter().for_each(|acc| {
132                if acc.pubkey == crate::payer::ID {
133                    accs.push(AccountMetaData {
134                        pubkey: delegate.key(),
135                        is_signer: acc.is_signer,
136                        is_writable: acc.is_writable,
137                    });
138                } else {
139                    accs.push(acc.clone());
140                }
141            });
142
143            // Execute the inner ix and process the response. Note that even though the manager PDA is a signer
144            //  on this ix, Solana will not allow downstream programs to mutate accounts owned by this program
145            //  and explicitly forbids CPI reentrancy.
146            //
147            // TODO Can downstream programs mutate the manager account data?
148            let exec_response = manager.sign(
149                &account_infos,
150                manager_bump,
151                &InstructionData {
152                    program_id: ix.program_id,
153                    accounts: accs.clone(),
154                    data: ix.data.clone(),
155                },
156            )?;
157
158            // Process the exec response
159            match exec_response {
160                None => (),
161                Some(exec_response) => match exec_response.dynamic_accounts {
162                    None => (),
163                    Some(dynamic_accounts) => {
164                        require!(
165                            dynamic_accounts.len() == ix.accounts.len(),
166                            CronosError::InvalidDynamicAccounts
167                        );
168                        dyanmic_ixs.push(InstructionData {
169                            program_id: ix.program_id,
170                            accounts: dynamic_accounts
171                                .iter()
172                                .enumerate()
173                                .map(|(i, pubkey)| {
174                                    let acc = ix.accounts.get(i).unwrap();
175                                    AccountMetaData {
176                                        pubkey: match pubkey {
177                                            _ if *pubkey == delegate.key() => crate::payer::ID,
178                                            _ => *pubkey,
179                                        },
180                                        is_signer: acc.is_signer,
181                                        is_writable: acc.is_writable,
182                                    }
183                                })
184                                .collect::<Vec<AccountMetaData>>(),
185                            data: ix.data.clone(),
186                        });
187                    }
188                },
189            }
190        }
191
192        // Verify that inner ixs have not initialized data at the delegate address
193        require!(delegate.data_is_empty(), CronosError::DelegateDataNotEmpty);
194
195        // Update the actions's ixs for the next invocation
196        if !dyanmic_ixs.is_empty() {
197            self.ixs = dyanmic_ixs.clone();
198        }
199
200        // Track how many lamports the delegate spent in the inner ixs
201        let delegate_lamports_post = delegate.lamports();
202        let delegate_reimbursement = delegate_lamports_pre
203            .checked_sub(delegate_lamports_post)
204            .unwrap();
205
206        // Pay delegate fees
207        let total_delegate_fee = config
208            .delegate_fee
209            .checked_add(delegate_reimbursement)
210            .unwrap();
211        **manager.to_account_info().try_borrow_mut_lamports()? = manager
212            .to_account_info()
213            .lamports()
214            .checked_sub(total_delegate_fee)
215            .unwrap();
216        **delegate.to_account_info().try_borrow_mut_lamports()? = delegate
217            .to_account_info()
218            .lamports()
219            .checked_add(total_delegate_fee)
220            .unwrap();
221
222        // Pay program fees
223        **manager.to_account_info().try_borrow_mut_lamports()? = manager
224            .to_account_info()
225            .lamports()
226            .checked_sub(config.program_fee)
227            .unwrap();
228        **fee.to_account_info().try_borrow_mut_lamports()? = fee
229            .to_account_info()
230            .lamports()
231            .checked_add(config.program_fee)
232            .unwrap();
233
234        // Increment collectable fee balance
235        fee.balance = fee.balance.checked_add(config.program_fee).unwrap();
236
237        // Update the queue status
238        let next_task_id = self.id.checked_add(1).unwrap();
239        if next_task_id == queue.task_count {
240            queue.roll_forward()?;
241        } else {
242            queue.status = QueueStatus::Processing {
243                task_id: next_task_id,
244            };
245        }
246
247        Ok(())
248    }
249}
250
251/**
252 * InstructionData
253 */
254
255#[derive(AnchorDeserialize, AnchorSerialize, BorshSchema, Clone, Debug, PartialEq)]
256pub struct InstructionData {
257    /// Pubkey of the instruction processor that executes this instruction
258    pub program_id: Pubkey,
259    /// Metadata for what accounts should be passed to the instruction processor
260    pub accounts: Vec<AccountMetaData>,
261    /// Opaque data passed to the instruction processor
262    pub data: Vec<u8>,
263}
264
265impl From<Instruction> for InstructionData {
266    fn from(instruction: Instruction) -> Self {
267        InstructionData {
268            program_id: instruction.program_id,
269            accounts: instruction
270                .accounts
271                .iter()
272                .map(|a| AccountMetaData {
273                    pubkey: a.pubkey,
274                    is_signer: a.is_signer,
275                    is_writable: a.is_writable,
276                })
277                .collect(),
278            data: instruction.data,
279        }
280    }
281}
282
283impl From<&InstructionData> for Instruction {
284    fn from(instruction: &InstructionData) -> Self {
285        Instruction {
286            program_id: instruction.program_id,
287            accounts: instruction
288                .accounts
289                .iter()
290                .map(|a| AccountMeta {
291                    pubkey: a.pubkey,
292                    is_signer: a.is_signer,
293                    is_writable: a.is_writable,
294                })
295                .collect(),
296            data: instruction.data.clone(),
297        }
298    }
299}
300
301impl TryFrom<Vec<u8>> for InstructionData {
302    type Error = Error;
303    fn try_from(data: Vec<u8>) -> std::result::Result<Self, Self::Error> {
304        Ok(
305            borsh::try_from_slice_with_schema::<InstructionData>(data.as_slice())
306                .map_err(|_err| ErrorCode::AccountDidNotDeserialize)?,
307        )
308    }
309}
310
311/**
312 * AccountMetaData
313 */
314
315#[derive(AnchorDeserialize, AnchorSerialize, BorshSchema, Clone, Debug, PartialEq)]
316pub struct AccountMetaData {
317    /// An account's public key
318    pub pubkey: Pubkey,
319    /// True if an Instruction requires a Transaction signature matching `pubkey`.
320    pub is_signer: bool,
321    /// True if the `pubkey` can be loaded as a read-write account.
322    pub is_writable: bool,
323}