From 0ed59c6f69ae0269d42c66c7015e2388cff6c72b Mon Sep 17 00:00:00 2001 From: Joe Date: Fri, 13 Oct 2023 15:45:25 +0200 Subject: [PATCH] token group: init interface --- Cargo.lock | 12 + Cargo.toml | 1 + token-group/interface/Cargo.toml | 24 ++ token-group/interface/src/error.rs | 14 + token-group/interface/src/instruction.rs | 336 +++++++++++++++++++++++ token-group/interface/src/lib.rs | 11 + token-group/interface/src/state.rs | 180 ++++++++++++ 7 files changed, 578 insertions(+) create mode 100644 token-group/interface/Cargo.toml create mode 100644 token-group/interface/src/error.rs create mode 100644 token-group/interface/src/instruction.rs create mode 100644 token-group/interface/src/lib.rs create mode 100644 token-group/interface/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 7590192215f..88f13457416 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7176,6 +7176,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "spl-token-group-interface" +version = "0.1.0" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator 0.1.0", + "spl-pod 0.1.0", + "spl-program-error 0.3.0", + "spl-type-length-value 0.3.0", +] + [[package]] name = "spl-token-lending" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 45fd58854fd..209c1a091e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ members = [ "stake-pool/cli", "stake-pool/program", "stateless-asks/program", + "token-group/interface", "token-lending/cli", "token-lending/program", "token-metadata/example", diff --git a/token-group/interface/Cargo.toml b/token-group/interface/Cargo.toml new file mode 100644 index 00000000000..40271ad1092 --- /dev/null +++ b/token-group/interface/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "spl-token-group-interface" +version = "0.1.0" +description = "Solana Program Library Token Group Interface" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2021" + +[dependencies] +bytemuck = "1.14.0" +solana-program = "1.16.3" +spl-discriminator = { version = "0.1.0" , path = "../../libraries/discriminator" } +spl-pod = { version = "0.1.0" , path = "../../libraries/pod", features = ["borsh"] } +spl-program-error = { version = "0.3.0" , path = "../../libraries/program-error" } + +[dev-dependencies] +spl-type-length-value = { version = "0.3.0", path = "../../libraries/type-length-value", features = ["derive"] } + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/token-group/interface/src/error.rs b/token-group/interface/src/error.rs new file mode 100644 index 00000000000..f2eff4d60f2 --- /dev/null +++ b/token-group/interface/src/error.rs @@ -0,0 +1,14 @@ +//! Interface error types + +use spl_program_error::*; + +/// Errors that may be returned by the interface. +#[spl_program_error] +pub enum TokenGroupError { + /// Size is greater than proposed max size + #[error("Size is greater than proposed max size")] + SizeExceedsNewMaxSize, + /// Size is greater than max size + #[error("Size is greater than max size")] + SizeExceedsMaxSize, +} diff --git a/token-group/interface/src/instruction.rs b/token-group/interface/src/instruction.rs new file mode 100644 index 00000000000..cac4002d589 --- /dev/null +++ b/token-group/interface/src/instruction.rs @@ -0,0 +1,336 @@ +//! Instruction types + +use { + bytemuck::{Pod, Zeroable}, + solana_program::{ + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + }, + spl_discriminator::{ArrayDiscriminator, SplDiscriminate}, + spl_pod::{ + bytemuck::{pod_bytes_of, pod_from_bytes}, + optional_keys::OptionalNonZeroPubkey, + }, +}; + +/// Instruction data for initializing a new `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:initialize_group")] +pub struct InitializeGroup { + /// Update authority for the group + pub update_authority: OptionalNonZeroPubkey, + /// The maximum number of group members + pub max_size: u32, +} + +/// Instruction data for updating the max size of a `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:update_group_max_size")] +pub struct UpdateGroupMaxSize { + /// New max size for the group + pub max_size: u32, +} + +/// Instruction data for updating the authority of a `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:update_group_authority")] +pub struct UpdateGroupAuthority { + /// New authority for the group, or unset if `None` + pub new_authority: OptionalNonZeroPubkey, +} + +/// Instruction data for initializing a new `Member` of a `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:initialize_member")] +pub struct InitializeMember { + /// The pubkey of the `Group` + pub group: Pubkey, + /// The member number + pub member_number: u32, +} + +/// All instructions that must be implemented in the SPL Token Group Interface +/// +/// Note: Any instruction can be extended using additional required accounts by +/// using the `InitializeExtraAccountMetaList` instruction to write +/// configurations for extra required accounts into validation data +/// corresponding to an instruction's unique discriminator. +#[derive(Clone, Debug, PartialEq)] +pub enum TokenGroupInterfaceInstruction { + /// Initialize a new `Group` + /// + /// Assumes one has already initialized a mint for the + /// group. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[w]` Group + /// 1. `[]` Mint + /// 2. `[s]` Mint authority + InitializeGroup(InitializeGroup), + + /// Update the max size of a `Group` + /// + /// Accounts expected by this instruction: + /// + /// 0. `[w]` Group + /// 1. `[s]` Update authority + UpdateGroupMaxSize(UpdateGroupMaxSize), + + /// Update the authority of a `Group` + /// + /// Accounts expected by this instruction: + /// + /// 0. `[w]` Group + /// 1. `[s]` Current update authority + UpdateGroupAuthority(UpdateGroupAuthority), + + /// Initialize a new `Member` of a `Group` + /// + /// Assumes the `Group` has already been initialized, + /// as well as the mint for the member. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[w]` Member + /// 1. `[]` Member Mint + /// 2. `[w]` Group + /// 3. `[]` Group Mint + /// 4. `[s]` Group Mint authority + InitializeMember(InitializeMember), +} +impl TokenGroupInterfaceInstruction { + /// Unpacks a byte buffer into a `TokenGroupInterfaceInstruction` + pub fn unpack(input: &[u8]) -> Result { + if input.len() < ArrayDiscriminator::LENGTH { + return Err(ProgramError::InvalidInstructionData); + } + let (discriminator, rest) = input.split_at(ArrayDiscriminator::LENGTH); + Ok(match discriminator { + InitializeGroup::SPL_DISCRIMINATOR_SLICE => { + let data = pod_from_bytes::(rest)?; + Self::InitializeGroup(*data) + } + UpdateGroupMaxSize::SPL_DISCRIMINATOR_SLICE => { + let data = pod_from_bytes::(rest)?; + Self::UpdateGroupMaxSize(*data) + } + UpdateGroupAuthority::SPL_DISCRIMINATOR_SLICE => { + let data = pod_from_bytes::(rest)?; + Self::UpdateGroupAuthority(*data) + } + InitializeMember::SPL_DISCRIMINATOR_SLICE => { + let data = pod_from_bytes::(rest)?; + Self::InitializeMember(*data) + } + _ => return Err(ProgramError::InvalidInstructionData), + }) + } + + /// Packs a `TokenGroupInterfaceInstruction` into a byte buffer. + pub fn pack(&self) -> Vec { + let mut buf = vec![]; + match self { + Self::InitializeGroup(data) => { + buf.extend_from_slice(InitializeGroup::SPL_DISCRIMINATOR_SLICE); + buf.extend_from_slice(pod_bytes_of(data)); + } + Self::UpdateGroupMaxSize(data) => { + buf.extend_from_slice(UpdateGroupMaxSize::SPL_DISCRIMINATOR_SLICE); + buf.extend_from_slice(pod_bytes_of(data)); + } + Self::UpdateGroupAuthority(data) => { + buf.extend_from_slice(UpdateGroupAuthority::SPL_DISCRIMINATOR_SLICE); + buf.extend_from_slice(pod_bytes_of(data)); + } + Self::InitializeMember(data) => { + buf.extend_from_slice(InitializeMember::SPL_DISCRIMINATOR_SLICE); + buf.extend_from_slice(pod_bytes_of(data)); + } + }; + buf + } +} + +/// Creates a `InitializeGroup` instruction +pub fn initialize_group( + program_id: &Pubkey, + group: &Pubkey, + mint: &Pubkey, + mint_authority: &Pubkey, + update_authority: Option, + max_size: u32, + extra_account_metas: &[AccountMeta], +) -> Instruction { + let update_authority = OptionalNonZeroPubkey::try_from(update_authority) + .expect("Failed to deserialize `Option`"); + let data = TokenGroupInterfaceInstruction::InitializeGroup(InitializeGroup { + update_authority, + max_size, + }) + .pack(); + let mut accounts = vec![ + AccountMeta::new(*group, false), + AccountMeta::new_readonly(*mint, false), + AccountMeta::new_readonly(*mint_authority, true), + ]; + accounts.extend_from_slice(extra_account_metas); + + Instruction { + program_id: *program_id, + accounts, + data, + } +} + +/// Creates a `UpdateGroupMaxSize` instruction +pub fn update_group_max_size( + program_id: &Pubkey, + group: &Pubkey, + update_authority: &Pubkey, + max_size: u32, +) -> Instruction { + let data = + TokenGroupInterfaceInstruction::UpdateGroupMaxSize(UpdateGroupMaxSize { max_size }).pack(); + Instruction { + program_id: *program_id, + accounts: vec![ + AccountMeta::new(*group, false), + AccountMeta::new_readonly(*update_authority, true), + ], + data, + } +} + +/// Creates a `UpdateGroupAuthority` instruction +pub fn update_group_authority( + program_id: &Pubkey, + group: &Pubkey, + current_authority: &Pubkey, + new_authority: Option, +) -> Instruction { + let new_authority = OptionalNonZeroPubkey::try_from(new_authority) + .expect("Failed to deserialize `Option`"); + let data = TokenGroupInterfaceInstruction::UpdateGroupAuthority(UpdateGroupAuthority { + new_authority, + }) + .pack(); + Instruction { + program_id: *program_id, + accounts: vec![ + AccountMeta::new(*group, false), + AccountMeta::new_readonly(*current_authority, true), + ], + data, + } +} + +/// Creates a `InitializeMember` instruction +#[allow(clippy::too_many_arguments)] +pub fn initialize_member( + program_id: &Pubkey, + group: &Pubkey, + group_mint: &Pubkey, + group_mint_authority: &Pubkey, + member: &Pubkey, + member_mint: &Pubkey, + member_mint_authority: &Pubkey, + member_number: u32, + extra_account_metas: &[AccountMeta], +) -> Instruction { + let data = TokenGroupInterfaceInstruction::InitializeMember(InitializeMember { + group: *group, + member_number, + }) + .pack(); + let mut accounts = vec![ + AccountMeta::new(*member, false), + AccountMeta::new_readonly(*member_mint, false), + AccountMeta::new_readonly(*member_mint_authority, true), + AccountMeta::new(*group, false), + AccountMeta::new_readonly(*group_mint, false), + AccountMeta::new_readonly(*group_mint_authority, true), + ]; + accounts.extend_from_slice(extra_account_metas); + + Instruction { + program_id: *program_id, + accounts, + data, + } +} + +#[cfg(test)] +mod test { + use {super::*, crate::NAMESPACE, solana_program::hash}; + + #[repr(C)] + #[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)] + #[discriminator_hash_input("mock_group")] + struct MockGroup; + + fn instruction_pack_unpack( + instruction: TokenGroupInterfaceInstruction, + discriminator: &[u8], + data: I, + ) where + I: core::fmt::Debug + PartialEq + Pod + Zeroable + SplDiscriminate, + { + let mut expect = vec![]; + expect.extend_from_slice(discriminator.as_ref()); + expect.extend_from_slice(pod_bytes_of(&data)); + let packed = instruction.pack(); + assert_eq!(packed, expect); + let unpacked = TokenGroupInterfaceInstruction::unpack(&expect).unwrap(); + assert_eq!(unpacked, instruction); + } + + #[test] + fn initialize_group_pack() { + let data = InitializeGroup { + update_authority: OptionalNonZeroPubkey::default(), + max_size: 100, + }; + let instruction = TokenGroupInterfaceInstruction::InitializeGroup(data); + let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_group").as_bytes()]); + let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; + instruction_pack_unpack::(instruction, discriminator, data); + } + + #[test] + fn update_group_max_size_pack() { + let data = UpdateGroupMaxSize { max_size: 200 }; + let instruction = TokenGroupInterfaceInstruction::UpdateGroupMaxSize(data); + let preimage = hash::hashv(&[format!("{NAMESPACE}:update_group_max_size").as_bytes()]); + let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; + instruction_pack_unpack::(instruction, discriminator, data); + } + + #[test] + fn update_authority_pack() { + let data = UpdateGroupAuthority { + new_authority: OptionalNonZeroPubkey::default(), + }; + let instruction = TokenGroupInterfaceInstruction::UpdateGroupAuthority(data); + let preimage = hash::hashv(&[format!("{NAMESPACE}:update_group_authority").as_bytes()]); + let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; + instruction_pack_unpack::(instruction, discriminator, data); + } + + #[test] + fn initialize_member_pack() { + let data = InitializeMember { + group: Pubkey::new_unique(), + member_number: 100, + }; + let instruction = TokenGroupInterfaceInstruction::InitializeMember(data); + let preimage = hash::hashv(&[format!("{NAMESPACE}:initialize_member").as_bytes()]); + let discriminator = &preimage.as_ref()[..ArrayDiscriminator::LENGTH]; + instruction_pack_unpack::(instruction, discriminator, data); + } +} diff --git a/token-group/interface/src/lib.rs b/token-group/interface/src/lib.rs new file mode 100644 index 00000000000..6867b86acf8 --- /dev/null +++ b/token-group/interface/src/lib.rs @@ -0,0 +1,11 @@ +//! Crate defining the SPL Token Group Interface + +#![deny(missing_docs)] +#![cfg_attr(not(test), forbid(unsafe_code))] + +pub mod error; +pub mod instruction; +pub mod state; + +/// Namespace for all programs implementing spl-token-group +pub const NAMESPACE: &str = "spl_token_group_interface"; diff --git a/token-group/interface/src/state.rs b/token-group/interface/src/state.rs new file mode 100644 index 00000000000..b4a40cb0534 --- /dev/null +++ b/token-group/interface/src/state.rs @@ -0,0 +1,180 @@ +//! Interface state types + +use { + crate::error::TokenGroupError, + bytemuck::{Pod, Zeroable}, + solana_program::{program_error::ProgramError, pubkey::Pubkey}, + spl_discriminator::SplDiscriminate, + spl_pod::{error::PodSliceError, optional_keys::OptionalNonZeroPubkey}, +}; + +/// Data struct for a `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:group")] +pub struct Group { + /// The authority that can sign to update the group + pub update_authority: OptionalNonZeroPubkey, + /// The current number of group members + pub size: u32, + /// The maximum number of group members + pub max_size: u32, +} + +impl Group { + /// Creates a new `Group` state + pub fn new(update_authority: OptionalNonZeroPubkey, max_size: u32) -> Self { + Self { + update_authority, + size: 0, + max_size, + } + } + + /// Updates the max size for a group + pub fn update_max_size(&mut self, new_max_size: u32) -> Result<(), ProgramError> { + // The new max size cannot be less than the current size + if new_max_size < self.size { + return Err(TokenGroupError::SizeExceedsNewMaxSize.into()); + } + self.max_size = new_max_size; + Ok(()) + } + + /// Increment the size for a group, returning the new size + pub fn increment_size(&mut self) -> Result { + // The new size cannot be greater than the max size + let new_size = self + .size + .checked_add(1) + .ok_or::(PodSliceError::CalculationFailure.into())?; + if new_size > self.max_size { + return Err(TokenGroupError::SizeExceedsMaxSize.into()); + } + self.size = new_size; + Ok(self.size) + } +} + +/// Data struct for a `Member` of a `Group` +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable, SplDiscriminate)] +#[discriminator_hash_input("spl_token_group_interface:member")] +pub struct Member { + /// The pubkey of the `Group` + pub group: Pubkey, + /// The member number + pub member_number: u32, +} +impl Member { + /// Creates a new `Member` state + pub fn new(group: Pubkey, member_number: u32) -> Self { + Self { + group, + member_number, + } + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::NAMESPACE, + solana_program::hash, + spl_discriminator::ArrayDiscriminator, + spl_type_length_value::state::{TlvState, TlvStateBorrowed, TlvStateMut}, + std::mem::size_of, + }; + + #[test] + fn discriminators() { + let preimage = hash::hashv(&[format!("{NAMESPACE}:group").as_bytes()]); + let discriminator = + ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap(); + assert_eq!(Group::SPL_DISCRIMINATOR, discriminator); + + let preimage = hash::hashv(&[format!("{NAMESPACE}:member").as_bytes()]); + let discriminator = + ArrayDiscriminator::try_from(&preimage.as_ref()[..ArrayDiscriminator::LENGTH]).unwrap(); + assert_eq!(Member::SPL_DISCRIMINATOR, discriminator); + } + + #[test] + fn tlv_state_pack() { + // Make sure we can pack more than one instance of each type + let group = Group { + update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), + size: 10, + max_size: 20, + }; + + let member = Member { + group: Pubkey::new_unique(), + member_number: 0, + }; + + let account_size = TlvStateBorrowed::get_base_len() + + size_of::() + + TlvStateBorrowed::get_base_len() + + size_of::(); + let mut buffer = vec![0; account_size]; + let mut state = TlvStateMut::unpack(&mut buffer).unwrap(); + + let group_data = state.init_value::(false).unwrap().0; + *group_data = group; + + let member_data = state.init_value::(false).unwrap().0; + *member_data = member; + + assert_eq!(state.get_first_value::().unwrap(), &group); + assert_eq!(state.get_first_value::().unwrap(), &member); + } + + #[test] + fn update_max_size() { + // Test with a `Some` max size + let max_size = 10; + let mut group = Group { + update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), + size: 0, + max_size, + }; + + let new_max_size = 30; + group.update_max_size(new_max_size).unwrap(); + assert_eq!(group.max_size, new_max_size); + + // Change the current size to 30 + group.size = 30; + + // Try to set the max size to 20, which is less than the current size + let new_max_size = 20; + assert_eq!( + group.update_max_size(new_max_size), + Err(ProgramError::from(TokenGroupError::SizeExceedsNewMaxSize)) + ); + + let new_max_size = 30; + group.update_max_size(new_max_size).unwrap(); + assert_eq!(group.max_size, new_max_size); + } + + #[test] + fn increment_current_size() { + let mut group = Group { + update_authority: OptionalNonZeroPubkey::try_from(Some(Pubkey::new_unique())).unwrap(), + size: 0, + max_size: 1, + }; + + group.increment_size().unwrap(); + assert_eq!(group.size, 1); + + // Try to increase the current size to 2, which is greater than the max size + assert_eq!( + group.increment_size(), + Err(ProgramError::from(TokenGroupError::SizeExceedsMaxSize)) + ); + } +}