use bytemuck::{Pod, Zeroable};
use jito_bytemuck::{types::PodU64, AccountDeserialize, Discriminator};
use jito_vault_sdk::error::VaultError;
use shank::ShankAccount;
use solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
pubkey::Pubkey,
};
const RESERVED_SPACE_LEN: usize = 263;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct VaultNcnSlasherOperatorTicket {
pub vault: Pubkey,
pub ncn: Pubkey,
pub slasher: Pubkey,
pub operator: Pubkey,
epoch: PodU64,
slashed: PodU64,
pub bump: u8,
reserved: [u8; 263],
}
impl VaultNcnSlasherOperatorTicket {
pub fn new(
vault: Pubkey,
ncn: Pubkey,
slasher: Pubkey,
operator: Pubkey,
epoch: u64,
bump: u8,
) -> Self {
Self {
vault,
ncn,
slasher,
operator,
epoch: PodU64::from(epoch),
slashed: PodU64::from(0),
bump,
reserved: [0; RESERVED_SPACE_LEN],
}
}
pub fn slashed(&self) -> u64 {
self.slashed.into()
}
pub fn epoch(&self) -> u64 {
self.epoch.into()
}
pub fn increment_slashed(&mut self, amount: u64) -> Result<(), VaultError> {
let slashed = self
.slashed()
.checked_add(amount)
.ok_or(VaultError::VaultMaxSlashedPerOperatorExceeded)?;
self.slashed = PodU64::from(slashed);
Ok(())
}
#[inline(always)]
pub fn check_slashing_amount_not_exceeded(
&self,
slash_amount: u64,
max_slashable_per_epoch: u64,
) -> ProgramResult {
let amount_after_slash = self
.slashed()
.checked_add(slash_amount)
.ok_or(VaultError::ArithmeticOverflow)?;
if amount_after_slash > max_slashable_per_epoch {
msg!("Slash amount exceeds the maximum slashable amount per epoch");
return Err(VaultError::VaultMaxSlashedPerOperatorExceeded.into());
}
Ok(())
}
pub fn seeds(
vault: &Pubkey,
ncn: &Pubkey,
slasher: &Pubkey,
operator: &Pubkey,
epoch: u64,
) -> Vec<Vec<u8>> {
Vec::from_iter([
b"vault_ncn_slasher_operator".to_vec(),
vault.to_bytes().to_vec(),
ncn.to_bytes().to_vec(),
slasher.to_bytes().to_vec(),
operator.to_bytes().to_vec(),
epoch.to_le_bytes().to_vec(),
])
}
pub fn find_program_address(
program_id: &Pubkey,
vault: &Pubkey,
ncn: &Pubkey,
slasher: &Pubkey,
operator: &Pubkey,
epoch: u64,
) -> (Pubkey, u8, Vec<Vec<u8>>) {
let seeds = Self::seeds(vault, ncn, slasher, operator, epoch);
let seeds_iter: Vec<_> = seeds.iter().map(|s| s.as_slice()).collect();
let (pda, bump) = Pubkey::find_program_address(&seeds_iter, program_id);
(pda, bump, seeds)
}
#[allow(clippy::too_many_arguments)]
pub fn load(
program_id: &Pubkey,
vault_ncn_slasher_operator_ticket: &AccountInfo,
vault: &AccountInfo,
ncn: &AccountInfo,
slasher: &AccountInfo,
operator: &AccountInfo,
ncn_epoch: u64,
expect_writable: bool,
) -> Result<(), ProgramError> {
if vault_ncn_slasher_operator_ticket.owner.ne(program_id) {
msg!("Vault NCN slasher operator has an invalid owner");
return Err(ProgramError::InvalidAccountOwner);
}
if vault_ncn_slasher_operator_ticket.data_is_empty() {
msg!("Vault NCN slasher operator data is empty");
return Err(ProgramError::InvalidAccountData);
}
if expect_writable && !vault_ncn_slasher_operator_ticket.is_writable {
msg!("Vault NCN slasher operator is not writable");
return Err(ProgramError::InvalidAccountData);
}
if vault_ncn_slasher_operator_ticket.data.borrow()[0].ne(&Self::DISCRIMINATOR) {
msg!("Vault NCN slasher operator discriminator is invalid");
return Err(ProgramError::InvalidAccountData);
}
let expected_pubkey = Self::find_program_address(
program_id,
vault.key,
ncn.key,
slasher.key,
operator.key,
ncn_epoch,
)
.0;
if vault_ncn_slasher_operator_ticket.key.ne(&expected_pubkey) {
msg!("Vault NCN slasher operator is not at the correct PDA");
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vault_ncn_slasher_operator_ticket_no_padding() {
let vault_ncn_slasher_operator_ticket_size =
std::mem::size_of::<VaultNcnSlasherOperatorTicket>();
let sum_of_fields = size_of::<Pubkey>() + size_of::<Pubkey>() + size_of::<Pubkey>() + size_of::<Pubkey>() + size_of::<PodU64>() + size_of::<PodU64>() + size_of::<u8>() + RESERVED_SPACE_LEN; assert_eq!(vault_ncn_slasher_operator_ticket_size, sum_of_fields);
}
}