From cfaabb5308b075ac9a437cd2b9e61b92d7755632 Mon Sep 17 00:00:00 2001 From: Joe C Date: Fri, 1 Sep 2023 11:40:34 -0600 Subject: [PATCH] SPL errors from hashes (#5169) * SPL errors from hashes * hashed error code is first variant only * add check for collision error codes * address feedback! * stupid `0`! --- Cargo.lock | 6 +- libraries/program-error/derive/Cargo.toml | 1 + libraries/program-error/derive/src/lib.rs | 61 +++++++-- .../program-error/derive/src/macro_impl.rs | 122 +++++++++++++++--- libraries/program-error/derive/src/parser.rs | 64 +++++++++ libraries/program-error/src/lib.rs | 12 +- libraries/program-error/tests/decode.rs | 2 +- libraries/program-error/tests/into.rs | 2 +- libraries/program-error/tests/mod.rs | 14 +- libraries/program-error/tests/print.rs | 2 +- libraries/program-error/tests/spl.rs | 56 +++++++- libraries/tlv-account-resolution/src/error.rs | 2 +- libraries/type-length-value/src/error.rs | 2 +- token-metadata/interface/src/error.rs | 2 +- .../transfer-hook-example/tests/functional.rs | 11 +- token/transfer-hook-interface/Cargo.toml | 5 +- token/transfer-hook-interface/src/error.rs | 44 +------ 17 files changed, 300 insertions(+), 108 deletions(-) create mode 100644 libraries/program-error/derive/src/parser.rs diff --git a/Cargo.lock b/Cargo.lock index d9436487cd6..c4afa47b9b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6799,6 +6799,7 @@ version = "0.2.0" dependencies = [ "proc-macro2 1.0.63", "quote 1.0.29", + "solana-program", "syn 2.0.28", ] @@ -7269,15 +7270,12 @@ version = "0.1.0" dependencies = [ "arrayref", "bytemuck", - "num-derive 0.4.0", - "num-traits", - "num_enum 0.7.0", "solana-program", "spl-discriminator", "spl-pod", + "spl-program-error", "spl-tlv-account-resolution", "spl-type-length-value", - "thiserror", ] [[package]] diff --git a/libraries/program-error/derive/Cargo.toml b/libraries/program-error/derive/Cargo.toml index 9eba3c1b637..4a3aaea5048 100644 --- a/libraries/program-error/derive/Cargo.toml +++ b/libraries/program-error/derive/Cargo.toml @@ -13,4 +13,5 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" +solana-program = "1.16.3" syn = { version = "2.0", features = ["full"] } diff --git a/libraries/program-error/derive/src/lib.rs b/libraries/program-error/derive/src/lib.rs index af9e53dadad..3be1bd7a3a3 100644 --- a/libraries/program-error/derive/src/lib.rs +++ b/libraries/program-error/derive/src/lib.rs @@ -14,39 +14,72 @@ extern crate proc_macro; mod macro_impl; +mod parser; -use macro_impl::MacroType; -use proc_macro::TokenStream; -use syn::{parse_macro_input, ItemEnum}; +use { + crate::parser::SplProgramErrorArgs, + macro_impl::MacroType, + proc_macro::TokenStream, + syn::{parse_macro_input, ItemEnum}, +}; -/// Derive macro to add `Into` traits +/// Derive macro to add `Into` +/// trait #[proc_macro_derive(IntoProgramError)] pub fn into_program_error(input: TokenStream) -> TokenStream { - MacroType::IntoProgramError - .generate_tokens(parse_macro_input!(input as ItemEnum)) + let ItemEnum { ident, .. } = parse_macro_input!(input as ItemEnum); + MacroType::IntoProgramError { ident } + .generate_tokens() .into() } /// Derive macro to add `solana_program::decode_error::DecodeError` trait #[proc_macro_derive(DecodeError)] pub fn decode_error(input: TokenStream) -> TokenStream { - MacroType::DecodeError - .generate_tokens(parse_macro_input!(input as ItemEnum)) - .into() + let ItemEnum { ident, .. } = parse_macro_input!(input as ItemEnum); + MacroType::DecodeError { ident }.generate_tokens().into() } /// Derive macro to add `solana_program::program_error::PrintProgramError` trait #[proc_macro_derive(PrintProgramError)] pub fn print_program_error(input: TokenStream) -> TokenStream { - MacroType::PrintProgramError - .generate_tokens(parse_macro_input!(input as ItemEnum)) + let ItemEnum { + ident, variants, .. + } = parse_macro_input!(input as ItemEnum); + MacroType::PrintProgramError { ident, variants } + .generate_tokens() .into() } /// Proc macro attribute to turn your enum into a Solana Program Error +/// +/// Adds: +/// - `Clone` +/// - `Debug` +/// - `Eq` +/// - `PartialEq` +/// - `thiserror::Error` +/// - `num_derive::FromPrimitive` +/// - `Into` +/// - `solana_program::decode_error::DecodeError` +/// - `solana_program::program_error::PrintProgramError` +/// +/// Optionally, you can add `hash_error_code_start: u32` argument to create +/// a unique `u32` _starting_ error codes from the names of the enum variants. +/// Notes: +/// - The _error_ variant will start at this value, and the rest will be +/// incremented by one +/// - The value provided is only for code readability, the actual error code +/// will be a hash of the input string and is checked against your input +/// +/// Syntax: `#[spl_program_error(hash_error_code_start = 1275525928)]` +/// Hash Input: `spl_program_error::` +/// Value: `u32::from_le_bytes([13..17])` #[proc_macro_attribute] -pub fn spl_program_error(_: TokenStream, input: TokenStream) -> TokenStream { - MacroType::SplProgramError - .generate_tokens(parse_macro_input!(input as ItemEnum)) +pub fn spl_program_error(attr: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as SplProgramErrorArgs); + let item_enum = parse_macro_input!(input as ItemEnum); + MacroType::SplProgramError { args, item_enum } + .generate_tokens() .into() } diff --git a/libraries/program-error/derive/src/macro_impl.rs b/libraries/program-error/derive/src/macro_impl.rs index f017728432e..114aa699af7 100644 --- a/libraries/program-error/derive/src/macro_impl.rs +++ b/libraries/program-error/derive/src/macro_impl.rs @@ -1,32 +1,52 @@ //! The actual token generator for the macro -use quote::quote; -use syn::{punctuated::Punctuated, token::Comma, Ident, ItemEnum, LitStr, Variant}; + +use { + crate::parser::SplProgramErrorArgs, + proc_macro2::Span, + quote::quote, + syn::{ + punctuated::Punctuated, token::Comma, Expr, ExprLit, Ident, ItemEnum, Lit, LitInt, LitStr, + Token, Variant, + }, +}; + +const SPL_ERROR_HASH_NAMESPACE: &str = "spl_program_error"; +const SPL_ERROR_HASH_MIN_VALUE: u32 = 7_000; /// The type of macro being called, thus directing which tokens to generate #[allow(clippy::enum_variant_names)] pub enum MacroType { - IntoProgramError, - DecodeError, - PrintProgramError, - SplProgramError, + IntoProgramError { + ident: Ident, + }, + DecodeError { + ident: Ident, + }, + PrintProgramError { + ident: Ident, + variants: Punctuated, + }, + SplProgramError { + args: SplProgramErrorArgs, + item_enum: ItemEnum, + }, } impl MacroType { /// Generates the corresponding tokens based on variant selection - pub fn generate_tokens(&self, item_enum: ItemEnum) -> proc_macro2::TokenStream { + pub fn generate_tokens(&mut self) -> proc_macro2::TokenStream { match self { - MacroType::IntoProgramError => into_program_error(&item_enum.ident), - MacroType::DecodeError => decode_error(&item_enum.ident), - MacroType::PrintProgramError => { - print_program_error(&item_enum.ident, &item_enum.variants) - } - MacroType::SplProgramError => spl_program_error(item_enum), + Self::IntoProgramError { ident } => into_program_error(ident), + Self::DecodeError { ident } => decode_error(ident), + Self::PrintProgramError { ident, variants } => print_program_error(ident, variants), + Self::SplProgramError { args, item_enum } => spl_program_error(args, item_enum), } } } -/// Builds the implementation of `Into` -/// More specifically, implements `From for solana_program::program_error::ProgramError` +/// Builds the implementation of +/// `Into` More specifically, +/// implements `From for solana_program::program_error::ProgramError` pub fn into_program_error(ident: &Ident) -> proc_macro2::TokenStream { quote! { impl From<#ident> for solana_program::program_error::ProgramError { @@ -48,7 +68,8 @@ pub fn decode_error(ident: &Ident) -> proc_macro2::TokenStream { } } -/// Builds the implementation of `solana_program::program_error::PrintProgramError` +/// Builds the implementation of +/// `solana_program::program_error::PrintProgramError` pub fn print_program_error( ident: &Ident, variants: &Punctuated, @@ -96,16 +117,25 @@ fn get_error_message(variant: &Variant) -> Option { /// The main function that produces the tokens required to turn your /// error enum into a Solana Program Error -pub fn spl_program_error(input: ItemEnum) -> proc_macro2::TokenStream { - let ident = &input.ident; - let variants = &input.variants; +pub fn spl_program_error( + args: &SplProgramErrorArgs, + item_enum: &mut ItemEnum, +) -> proc_macro2::TokenStream { + if let Some(error_code_start) = args.hash_error_code_start { + set_first_discriminant(item_enum, error_code_start); + } + + let ident = &item_enum.ident; + let variants = &item_enum.variants; let into_program_error = into_program_error(ident); let decode_error = decode_error(ident); let print_program_error = print_program_error(ident, variants); + quote! { + #[repr(u32)] #[derive(Clone, Debug, Eq, thiserror::Error, num_derive::FromPrimitive, PartialEq)] #[num_traits = "num_traits"] - #input + #item_enum #into_program_error @@ -114,3 +144,55 @@ pub fn spl_program_error(input: ItemEnum) -> proc_macro2::TokenStream { #print_program_error } } + +/// This function adds a discriminant to the first enum variant based on the +/// hash of the `SPL_ERROR_HASH_NAMESPACE` constant, the enum name and variant +/// name. +/// It will then check to make sure the provided `hash_error_code_start` is +/// equal to the hash-produced `u32`. +/// +/// See https://docs.rs/syn/latest/syn/struct.Variant.html +fn set_first_discriminant(item_enum: &mut ItemEnum, error_code_start: u32) { + let enum_ident = &item_enum.ident; + if item_enum.variants.is_empty() { + panic!("Enum must have at least one variant"); + } + let first_variant = &mut item_enum.variants[0]; + let discriminant = u32_from_hash(enum_ident); + if discriminant == error_code_start { + let eq = Token![=](Span::call_site()); + let expr = Expr::Lit(ExprLit { + attrs: Vec::new(), + lit: Lit::Int(LitInt::new(&discriminant.to_string(), Span::call_site())), + }); + first_variant.discriminant = Some((eq, expr)); + } else { + panic!( + "Error code start value from hash must be {0}. Update your macro attribute to \ + `#[spl_program_error(hash_error_code_start = {0})]`.", + discriminant + ); + } +} + +/// Hashes the `SPL_ERROR_HASH_NAMESPACE` constant, the enum name and variant +/// name and returns four middle bytes (13 through 16) as a u32. +fn u32_from_hash(enum_ident: &Ident) -> u32 { + let hash_input = format!("{}:{}", SPL_ERROR_HASH_NAMESPACE, enum_ident); + + // We don't want our error code to start at any number below + // `SPL_ERROR_HASH_MIN_VALUE`! + let mut nonce: u32 = 0; + loop { + let hash = solana_program::hash::hashv(&[hash_input.as_bytes(), &nonce.to_le_bytes()]); + let d = u32::from_le_bytes( + hash.to_bytes()[13..17] + .try_into() + .expect("Unable to convert hash to u32"), + ); + if d >= SPL_ERROR_HASH_MIN_VALUE { + return d; + } + nonce += 1; + } +} diff --git a/libraries/program-error/derive/src/parser.rs b/libraries/program-error/derive/src/parser.rs new file mode 100644 index 00000000000..a09ffc7ba7c --- /dev/null +++ b/libraries/program-error/derive/src/parser.rs @@ -0,0 +1,64 @@ +//! Token parsing + +use { + proc_macro2::Ident, + syn::{ + parse::{Parse, ParseStream}, + token::Comma, + LitInt, Token, + }, +}; + +/// Possible arguments to the `#[spl_program_error]` attribute +pub struct SplProgramErrorArgs { + /// Whether to hash the error codes using `solana_program::hash` + /// or to use the default error code assigned by `num_traits`. + pub hash_error_code_start: Option, +} + +impl Parse for SplProgramErrorArgs { + fn parse(input: ParseStream) -> syn::Result { + if input.is_empty() { + return Ok(Self { + hash_error_code_start: None, + }); + } + match SplProgramErrorArgParser::parse(input)? { + SplProgramErrorArgParser::HashErrorCodes { value, .. } => Ok(Self { + hash_error_code_start: Some(value.base10_parse::()?), + }), + } + } +} + +/// Parser for args to the `#[spl_program_error]` attribute +/// ie. `#[spl_program_error(hash_error_code_start = 1275525928)]` +enum SplProgramErrorArgParser { + HashErrorCodes { + _ident: Ident, + _equals_sign: Token![=], + value: LitInt, + _comma: Option, + }, +} + +impl Parse for SplProgramErrorArgParser { + fn parse(input: ParseStream) -> syn::Result { + let _ident = { + let ident = input.parse::()?; + if ident != "hash_error_code_start" { + return Err(input.error("Expected argument 'hash_error_code_start'")); + } + ident + }; + let _equals_sign = input.parse::()?; + let value = input.parse::()?; + let _comma: Option = input.parse().unwrap_or(None); + Ok(Self::HashErrorCodes { + _ident, + _equals_sign, + value, + _comma, + }) + } +} diff --git a/libraries/program-error/src/lib.rs b/libraries/program-error/src/lib.rs index 8c1fb7c0b7f..739995dd197 100644 --- a/libraries/program-error/src/lib.rs +++ b/libraries/program-error/src/lib.rs @@ -8,10 +8,10 @@ extern crate self as spl_program_error; // Make these available downstream for the macro to work without // additional imports -pub use num_derive; -pub use num_traits; -pub use solana_program; -pub use spl_program_error_derive::{ - spl_program_error, DecodeError, IntoProgramError, PrintProgramError, +pub use { + num_derive, num_traits, solana_program, + spl_program_error_derive::{ + spl_program_error, DecodeError, IntoProgramError, PrintProgramError, + }, + thiserror, }; -pub use thiserror; diff --git a/libraries/program-error/tests/decode.rs b/libraries/program-error/tests/decode.rs index 28aeac56b61..0c7c209bc14 100644 --- a/libraries/program-error/tests/decode.rs +++ b/libraries/program-error/tests/decode.rs @@ -1,5 +1,5 @@ //! Tests `#[derive(DecodeError)]` -//! + use spl_program_error::*; /// Example error diff --git a/libraries/program-error/tests/into.rs b/libraries/program-error/tests/into.rs index 97e8868a9be..0f32b8f40d3 100644 --- a/libraries/program-error/tests/into.rs +++ b/libraries/program-error/tests/into.rs @@ -1,5 +1,5 @@ //! Tests `#[derive(IntoProgramError)]` -//! + use spl_program_error::*; /// Example error diff --git a/libraries/program-error/tests/mod.rs b/libraries/program-error/tests/mod.rs index ae50368fd35..413587758e9 100644 --- a/libraries/program-error/tests/mod.rs +++ b/libraries/program-error/tests/mod.rs @@ -6,13 +6,15 @@ pub mod spl; #[cfg(test)] mod tests { - use super::*; - use serial_test::serial; - use solana_program::{ - decode_error::DecodeError, - program_error::{PrintProgramError, ProgramError}, + use { + super::*, + serial_test::serial, + solana_program::{ + decode_error::DecodeError, + program_error::{PrintProgramError, ProgramError}, + }, + std::sync::{Arc, RwLock}, }; - use std::sync::{Arc, RwLock}; // Used to capture output for `PrintProgramError` for testing lazy_static::lazy_static! { diff --git a/libraries/program-error/tests/print.rs b/libraries/program-error/tests/print.rs index ddb62cf60b6..8b68f66a581 100644 --- a/libraries/program-error/tests/print.rs +++ b/libraries/program-error/tests/print.rs @@ -1,5 +1,5 @@ //! Tests `#[derive(PrintProgramError)]` -//! + use spl_program_error::*; /// Example error diff --git a/libraries/program-error/tests/spl.rs b/libraries/program-error/tests/spl.rs index 8cd557b0f36..bbdb9af0aca 100644 --- a/libraries/program-error/tests/spl.rs +++ b/libraries/program-error/tests/spl.rs @@ -1,5 +1,5 @@ //! Tests `#[spl_program_error]` -//! + use spl_program_error::*; /// Example error @@ -18,3 +18,57 @@ pub enum ExampleError { fn test_macros_compile() { let _ = ExampleError::MintHasNoMintAuthority; } + +/// Example library error with namespace +#[spl_program_error(hash_error_code_start = 2_056_342_880)] +enum ExampleLibraryError { + /// This is a very informative error + #[error("This is a very informative error")] + VeryInformativeError, + /// This is a super important error + #[error("This is a super important error")] + SuperImportantError, + /// This is a mega serious error + #[error("This is a mega serious error")] + MegaSeriousError, + /// You are toast + #[error("You are toast")] + YouAreToast, +} + +/// Tests hashing of error codes into unique `u32` values +#[test] +fn test_library_error_codes() { + fn get_error_code_check(hash_input: &str) -> u32 { + let mut nonce: u32 = 0; + loop { + let hash = solana_program::hash::hashv(&[hash_input.as_bytes(), &nonce.to_le_bytes()]); + let mut bytes = [0u8; 4]; + bytes.copy_from_slice(&hash.to_bytes()[13..17]); + let error_code = u32::from_le_bytes(bytes); + if error_code >= 10_000 { + return error_code; + } + nonce += 1; + } + } + + let first_error_as_u32 = ExampleLibraryError::VeryInformativeError as u32; + + assert_eq!( + ExampleLibraryError::VeryInformativeError as u32, + get_error_code_check("spl_program_error:ExampleLibraryError"), + ); + assert_eq!( + ExampleLibraryError::SuperImportantError as u32, + first_error_as_u32 + 1, + ); + assert_eq!( + ExampleLibraryError::MegaSeriousError as u32, + first_error_as_u32 + 2, + ); + assert_eq!( + ExampleLibraryError::YouAreToast as u32, + first_error_as_u32 + 3, + ); +} diff --git a/libraries/tlv-account-resolution/src/error.rs b/libraries/tlv-account-resolution/src/error.rs index 3fab843e07c..be152b2dbf1 100644 --- a/libraries/tlv-account-resolution/src/error.rs +++ b/libraries/tlv-account-resolution/src/error.rs @@ -3,7 +3,7 @@ use spl_program_error::*; /// Errors that may be returned by the Account Resolution library. -#[spl_program_error] +#[spl_program_error(hash_error_code_start = 2_724_315_840)] pub enum AccountResolutionError { /// Incorrect account provided #[error("Incorrect account provided")] diff --git a/libraries/type-length-value/src/error.rs b/libraries/type-length-value/src/error.rs index b2d24246294..48e5d9efa34 100644 --- a/libraries/type-length-value/src/error.rs +++ b/libraries/type-length-value/src/error.rs @@ -3,7 +3,7 @@ use spl_program_error::*; /// Errors that may be returned by the Token program. -#[spl_program_error] +#[spl_program_error(hash_error_code_start = 1_202_666_432)] pub enum TlvError { /// Type not found in TLV data #[error("Type not found in TLV data")] diff --git a/token-metadata/interface/src/error.rs b/token-metadata/interface/src/error.rs index a0ebd0b64f3..3f5b2437865 100644 --- a/token-metadata/interface/src/error.rs +++ b/token-metadata/interface/src/error.rs @@ -3,7 +3,7 @@ use spl_program_error::*; /// Errors that may be returned by the interface. -#[spl_program_error] +#[spl_program_error(hash_error_code_start = 901_952_957)] pub enum TokenMetadataError { /// Incorrect account provided #[error("Incorrect account provided")] diff --git a/token/transfer-hook-example/tests/functional.rs b/token/transfer-hook-example/tests/functional.rs index f60ce1927ff..989b6f901ce 100644 --- a/token/transfer-hook-example/tests/functional.rs +++ b/token/transfer-hook-example/tests/functional.rs @@ -18,7 +18,8 @@ use { transaction::{Transaction, TransactionError}, }, spl_tlv_account_resolution::{ - account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, + account::ExtraAccountMeta, error::AccountResolutionError, seeds::Seed, + state::ExtraAccountMetaList, }, spl_token_2022::{ extension::{transfer_hook::TransferHookAccount, ExtensionType, StateWithExtensionsMut}, @@ -270,7 +271,7 @@ async fn success_execute() { error, TransactionError::InstructionError( 0, - InstructionError::Custom(TransferHookError::IncorrectAccount as u32), + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), ) ); } @@ -309,7 +310,7 @@ async fn success_execute() { error, TransactionError::InstructionError( 0, - InstructionError::Custom(TransferHookError::IncorrectAccount as u32), + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), ) ); } @@ -356,7 +357,7 @@ async fn success_execute() { error, TransactionError::InstructionError( 0, - InstructionError::Custom(TransferHookError::IncorrectAccount as u32), + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), ) ); } @@ -395,7 +396,7 @@ async fn success_execute() { error, TransactionError::InstructionError( 0, - InstructionError::Custom(TransferHookError::IncorrectAccount as u32), + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), ) ); } diff --git a/token/transfer-hook-interface/Cargo.toml b/token/transfer-hook-interface/Cargo.toml index 449e55ad6db..62e943ffd8d 100644 --- a/token/transfer-hook-interface/Cargo.toml +++ b/token/transfer-hook-interface/Cargo.toml @@ -10,15 +10,12 @@ edition = "2021" [dependencies] arrayref = "0.3.7" bytemuck = { version = "1.13.1", features = ["derive"] } -num-derive = "0.4" -num-traits = "0.2" -num_enum = "0.7.0" solana-program = "1.16.3" spl-discriminator = { version = "0.1.0" , path = "../../libraries/discriminator" } +spl-program-error = { version = "0.2.0" , path = "../../libraries/program-error" } spl-tlv-account-resolution = { version = "0.2.0" , path = "../../libraries/tlv-account-resolution" } spl-type-length-value = { version = "0.2.0" , path = "../../libraries/type-length-value" } spl-pod = { version = "0.1.0", path = "../../libraries/pod" } -thiserror = "1.0" [lib] crate-type = ["cdylib", "lib"] diff --git a/token/transfer-hook-interface/src/error.rs b/token/transfer-hook-interface/src/error.rs index c387b8b15b3..aaf25e19627 100644 --- a/token/transfer-hook-interface/src/error.rs +++ b/token/transfer-hook-interface/src/error.rs @@ -1,17 +1,9 @@ //! Error types -use { - num_derive::FromPrimitive, - solana_program::{ - decode_error::DecodeError, - msg, - program_error::{PrintProgramError, ProgramError}, - }, - thiserror::Error, -}; +use spl_program_error::*; /// Errors that may be returned by the interface. -#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +#[spl_program_error(hash_error_code_start = 2_110_272_652)] pub enum TransferHookError { /// Incorrect account provided #[error("Incorrect account provided")] @@ -26,35 +18,3 @@ pub enum TransferHookError { #[error("Program called outside of a token transfer")] ProgramCalledOutsideOfTransfer, } -impl From for ProgramError { - fn from(e: TransferHookError) -> Self { - ProgramError::Custom(e as u32) - } -} -impl DecodeError for TransferHookError { - fn type_of() -> &'static str { - "TransferHookError" - } -} - -impl PrintProgramError for TransferHookError { - fn print(&self) - where - E: 'static - + std::error::Error - + DecodeError - + PrintProgramError - + num_traits::FromPrimitive, - { - match self { - Self::IncorrectAccount => msg!("Incorrect account provided"), - Self::MintHasNoMintAuthority => msg!("Mint has no mint authority"), - Self::IncorrectMintAuthority => { - msg!("Incorrect mint authority has signed the instruction") - } - Self::ProgramCalledOutsideOfTransfer => { - msg!("Program called outside of a token transfer") - } - } - } -}