Skip to main content

jito_restaking_core/
operator.rs

1//! The Operator account stores global information for a particular operator
2//! including the admin, voter, and the number of NCN and vault accounts.
3
4use std::fmt::Debug;
5
6use bytemuck::{Pod, Zeroable};
7use jito_bytemuck::{
8    types::{PodU16, PodU64},
9    AccountDeserialize, Discriminator,
10};
11use jito_restaking_sdk::error::RestakingError;
12use shank::ShankAccount;
13use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey};
14
15const RESERVED_SPACE_LEN: usize = 261;
16
17/// The Operator account stores global information for a particular operator
18/// including the admin, voter, and the number of NCN and vault accounts.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable, AccountDeserialize, ShankAccount)]
20#[repr(C)]
21pub struct Operator {
22    /// The base pubkey used as a seed for the PDA
23    pub base: Pubkey,
24
25    /// The admin pubkey
26    pub admin: Pubkey,
27
28    /// The NCN admin can add and remove support for NCNs in the restaking protocol
29    pub ncn_admin: Pubkey,
30
31    /// The vault admin can add and remove support for vaults in the restaking protocol
32    pub vault_admin: Pubkey,
33
34    /// The delegate admin can delegate assets from the operator
35    pub delegate_admin: Pubkey,
36
37    /// ( For future use ) Authority to update the operators's metadata
38    pub metadata_admin: Pubkey,
39
40    /// The voter pubkey can be used as the voter for signing transactions for interacting
41    /// with various NCN programs. NCNs can also opt for their own signing infrastructure.
42    pub voter: Pubkey,
43
44    /// The operator index
45    index: PodU64,
46
47    /// The number of NcnOperatorTickets associated with the operator.
48    /// Helpful for indexing all available OperatorNcnTickets.
49    ncn_count: PodU64,
50
51    /// The number of OperatorVaultTickets associated with the operator.
52    /// Helpful for indexing all available OperatorVaultTickets.
53    vault_count: PodU64,
54
55    /// The operator fee in basis points
56    pub operator_fee_bps: PodU16,
57
58    /// The bump seed for the PDA
59    pub bump: u8,
60
61    /// Reserved space
62    reserved_space: [u8; 261],
63}
64
65impl Operator {
66    /// Create a new Operator account
67    /// # Arguments
68    /// * `base` - The base account used as a PDA seed
69    /// * `admin` - The admin of the Operator
70    /// * `index` - The index of the Operator
71    /// * `bump` - The bump seed for the PDA
72    pub fn new(base: Pubkey, admin: Pubkey, index: u64, operator_fee_bps: u16, bump: u8) -> Self {
73        Self {
74            base,
75            admin,
76            ncn_admin: admin,
77            vault_admin: admin,
78            delegate_admin: admin,
79            metadata_admin: admin,
80            voter: admin,
81            index: PodU64::from(index),
82            ncn_count: PodU64::from(0),
83            vault_count: PodU64::from(0),
84            operator_fee_bps: PodU16::from(operator_fee_bps),
85            bump,
86            reserved_space: [0; RESERVED_SPACE_LEN],
87        }
88    }
89
90    pub fn index(&self) -> u64 {
91        self.index.into()
92    }
93
94    pub fn ncn_count(&self) -> u64 {
95        self.ncn_count.into()
96    }
97
98    pub fn vault_count(&self) -> u64 {
99        self.vault_count.into()
100    }
101
102    pub fn increment_ncn_count(&mut self) -> Result<(), RestakingError> {
103        let mut ncn_count: u64 = self.ncn_count.into();
104        ncn_count = ncn_count
105            .checked_add(1)
106            .ok_or(RestakingError::NcnOverflow)?;
107        self.ncn_count = PodU64::from(ncn_count);
108        Ok(())
109    }
110
111    pub fn increment_vault_count(&mut self) -> Result<(), RestakingError> {
112        let mut vault_count: u64 = self.vault_count.into();
113        vault_count = vault_count
114            .checked_add(1)
115            .ok_or(RestakingError::VaultOverflow)?;
116        self.vault_count = PodU64::from(vault_count);
117        Ok(())
118    }
119
120    /// Validates the admin account and ensures it matches the expected admin.
121    ///
122    /// # Arguments
123    /// * `admin` - A reference to the [`Pubkey`] representing the admin Pubkey that is attempting
124    ///   to authorize the operation.
125    ///
126    /// # Returns
127    /// * `Result<(), RestakingError>` - Returns `Ok(())` if the admin account is valid.
128    ///
129    /// # Errors
130    /// This function will return a [`jito_restaking_sdk::error::RestakingError::OperatorAdminInvalid`] error in the following case:
131    /// * The `admin_info` account's public key does not match the expected admin public key stored in `self`.
132    pub fn check_admin(&self, admin: &Pubkey) -> Result<(), RestakingError> {
133        if self.admin.ne(admin) {
134            msg!(
135                "Incorrect admin provided, expected {}, received {}",
136                self.admin,
137                admin
138            );
139            return Err(RestakingError::OperatorAdminInvalid);
140        }
141        Ok(())
142    }
143
144    /// Validates the delegate_admin account and ensures it matches the expected delegate_admin.
145    ///
146    /// # Arguments
147    /// * `delegate_admin_info` - A reference to the [`Pubkey`] representing the delegate_admin Pubkey that is attempting
148    ///   to authorize the operation.
149    ///
150    /// # Returns
151    /// * `Result<(), RestakingError>` - Returns `Ok(())` if the delegate_admin account is valid.
152    ///
153    /// # Errors
154    /// This function will return a [`jito_restaking_sdk::error::RestakingError::OperatorDelegateAdminInvalid`] error in the following case:
155    /// * The `delegate_admin_info` account's public key does not match the expected admin public key stored in `self`.
156    pub fn check_delegate_admin(&self, delegate_admin: &Pubkey) -> Result<(), RestakingError> {
157        if self.delegate_admin.ne(delegate_admin) {
158            msg!(
159                "Incorrect delegate_admin provided, expected {}, received {}",
160                self.delegate_admin,
161                delegate_admin
162            );
163            return Err(RestakingError::OperatorDelegateAdminInvalid);
164        }
165        Ok(())
166    }
167
168    /// Replace all secondary admins that were equal to the old admin to the new admin
169    ///
170    /// # Arguments
171    /// * `old_admin` - The old admin Pubkey
172    /// * `new_admin` - The new admin Pubkey
173    pub fn update_secondary_admin(&mut self, old_admin: &Pubkey, new_admin: &Pubkey) {
174        if self.ncn_admin.eq(old_admin) {
175            self.ncn_admin = *new_admin;
176            msg!("NCN admin set to {:?}", new_admin);
177        }
178
179        if self.vault_admin.eq(old_admin) {
180            self.vault_admin = *new_admin;
181            msg!("Vault admin set to {:?}", new_admin);
182        }
183
184        if self.voter.eq(old_admin) {
185            self.voter = *new_admin;
186            msg!("Voter set to {:?}", new_admin);
187        }
188
189        if self.delegate_admin.eq(old_admin) {
190            self.delegate_admin = *new_admin;
191            msg!("Delegate admin set to {:?}", new_admin);
192        }
193
194        if self.metadata_admin.eq(old_admin) {
195            self.metadata_admin = *new_admin;
196            msg!("Metadata admin set to {:?}", new_admin);
197        }
198    }
199
200    /// Returns the seeds for the PDA
201    ///
202    /// # Arguments
203    /// * `base` - The base account used as a PDA seed
204    ///
205    /// # Returns
206    /// * `Vec<Vec<u8>>` - The seeds used to generate the PDA
207    pub fn seeds(base: &Pubkey) -> Vec<Vec<u8>> {
208        Vec::from_iter([b"operator".to_vec(), base.as_ref().to_vec()])
209    }
210
211    /// Find the program address for the Operator account
212    ///
213    /// # Arguments
214    /// * `program_id` - The program ID
215    /// * `base` - The base account used as a PDA seed
216    ///
217    /// # Returns
218    /// * `Pubkey` - The program address
219    /// * `u8` - The bump seed
220    /// * `Vec<Vec<u8>>` - The seeds used to generate the PDA
221    pub fn find_program_address(program_id: &Pubkey, base: &Pubkey) -> (Pubkey, u8, Vec<Vec<u8>>) {
222        let seeds = Self::seeds(base);
223        let seeds_iter: Vec<_> = seeds.iter().map(|s| s.as_slice()).collect();
224        let (pda, bump) = Pubkey::find_program_address(&seeds_iter, program_id);
225        (pda, bump, seeds)
226    }
227
228    /// Attempts to load the account as [`Operator`], returning an error if it's not valid.
229    ///
230    /// # Arguments
231    /// * `program_id` - The program ID
232    /// * `account` - The account to load the operator from
233    /// * `expect_writable` - Whether the account should be writable
234    ///
235    /// # Returns
236    /// * `Result<(), ProgramError>` - The result of the operation
237    pub fn load(
238        program_id: &Pubkey,
239        account: &AccountInfo,
240        expect_writable: bool,
241    ) -> Result<(), ProgramError> {
242        if account.owner.ne(program_id) {
243            msg!("Operator account has an invalid owner");
244            return Err(ProgramError::InvalidAccountOwner);
245        }
246        if account.data_is_empty() {
247            msg!("Operator account data is empty");
248            return Err(ProgramError::InvalidAccountData);
249        }
250        if expect_writable && !account.is_writable {
251            msg!("Operator account is not writable");
252            return Err(ProgramError::InvalidAccountData);
253        }
254        if account.data.borrow()[0].ne(&Self::DISCRIMINATOR) {
255            msg!("Operator account discriminator is invalid");
256            return Err(ProgramError::InvalidAccountData);
257        }
258        let base = Self::try_from_slice_unchecked(&account.data.borrow())?.base;
259        if account
260            .key
261            .ne(&Self::find_program_address(program_id, &base).0)
262        {
263            msg!("Operator account is not at the correct PDA");
264            return Err(ProgramError::InvalidAccountData);
265        }
266        Ok(())
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use jito_bytemuck::types::{PodU16, PodU64};
273    use solana_program::pubkey::Pubkey;
274
275    use super::{Operator, RESERVED_SPACE_LEN};
276
277    #[test]
278    fn test_operator_no_padding() {
279        let operator_size = std::mem::size_of::<Operator>();
280        let sum_of_fields = std::mem::size_of::<Pubkey>() + // base
281            std::mem::size_of::<Pubkey>() + // admin
282            std::mem::size_of::<Pubkey>() + // ncn_admin
283            std::mem::size_of::<Pubkey>() + // vault_admin
284            std::mem::size_of::<Pubkey>() + // delegate_admin
285            std::mem::size_of::<Pubkey>() + // metadata_admin
286            std::mem::size_of::<Pubkey>() + // voter
287            std::mem::size_of::<PodU64>() + // index
288            std::mem::size_of::<PodU64>() + // ncn_count
289            std::mem::size_of::<PodU64>() + // vault_count
290            std::mem::size_of::<PodU16>() + // operator_fee_bps
291            std::mem::size_of::<u8>() + // bump
292            RESERVED_SPACE_LEN; // reserved
293        assert_eq!(operator_size, sum_of_fields);
294    }
295
296    #[test]
297    fn test_update_secondary_admin_ok() {
298        let old_admin = Pubkey::new_unique();
299        let mut operator = Operator::new(Pubkey::new_unique(), old_admin, 0, 0, 0);
300
301        assert_eq!(operator.ncn_admin, old_admin);
302        assert_eq!(operator.vault_admin, old_admin);
303        assert_eq!(operator.voter, old_admin);
304        assert_eq!(operator.delegate_admin, old_admin);
305
306        let new_admin = Pubkey::new_unique();
307        operator.update_secondary_admin(&old_admin, &new_admin);
308
309        assert_eq!(operator.ncn_admin, new_admin);
310        assert_eq!(operator.vault_admin, new_admin);
311        assert_eq!(operator.voter, new_admin);
312        assert_eq!(operator.delegate_admin, new_admin);
313        assert_eq!(operator.metadata_admin, new_admin);
314    }
315}