From f36bf8847beda38495653f60b9bada0e93b63ea7 Mon Sep 17 00:00:00 2001 From: Ikrk Date: Tue, 6 May 2025 13:24:51 +0200 Subject: [PATCH 1/5] Non-transferable token extension --- lang/src/error.rs | 5 ++++ lang/syn/src/codegen/accounts/constraints.rs | 24 +++++++++++++++ lang/syn/src/lib.rs | 6 ++++ lang/syn/src/parser/accounts/constraints.rs | 30 +++++++++++++++++++ .../token-extensions/src/instructions.rs | 5 +++- 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/lang/src/error.rs b/lang/src/error.rs index 2e6a04fe23..930e2cde76 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -183,6 +183,11 @@ pub enum ErrorCode { /// 2042 - Account must be migrated before exiting #[msg("Account must be migrated before exiting")] AccountNotMigrated, + /// Extension constraints - cont. + /// + /// 2043 - A non-transferable extension constraint was violated + #[msg("A non-transferable extension constraint was violated")] + ConstraintMintNonTransferableExtension, // Require /// 2500 - A require expression was violated diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index f89991d223..d9550a4055 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -769,6 +769,7 @@ fn generate_constraint_init_group( permanent_delegate, transfer_hook_authority, transfer_hook_program_id, + non_transferable, } => { let token_program = match token_program { Some(t) => t.to_token_stream(), @@ -884,6 +885,10 @@ fn generate_constraint_init_group( extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::PermanentDelegate}); } + if non_transferable.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::NonTransferable}); + } + let mint_space = if extensions.is_empty() { quote! { ::anchor_spl::token::Mint::LEN } } else { @@ -1028,6 +1033,12 @@ fn generate_constraint_init_group( mint: #field.to_account_info(), }), #permanent_delegate.unwrap())?; }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::NonTransferable => { + ::anchor_spl::token_interface::non_transferable_mint_initialize(anchor_lang::context::CpiContext::new(cpi_program_id, ::anchor_spl::token_interface::NonTransferableMintInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }))?; + }, // All extensions specified by the user should be implemented. // If this line runs, it means there is a bug in the codegen. _ => unimplemented!("{e:?}"), @@ -1588,6 +1599,18 @@ fn generate_constraint_mint( None => quote! {}, }; + let non_transferable_check = match &c.non_transferable { + Some(_) => { + quote! { + let non_transferable = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::non_transferable::NonTransferable>(#account_ref); + if non_transferable.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintNonTransferableExtension.into()); + } + } + } + None => quote! {}, + }; + quote! { { #decimal_check @@ -1604,6 +1627,7 @@ fn generate_constraint_mint( #permanent_delegate_check #transfer_hook_authority_check #transfer_hook_program_id_check + #non_transferable_check } } } diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 67c265a644..b2bf904b09 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -834,6 +834,7 @@ pub enum ConstraintToken { ExtensionTokenHookAuthority(Context), ExtensionTokenHookProgramId(Context), ExtensionPermanentDelegate(Context), + ExtensionNonTransferable(Context), } impl Parse for ConstraintToken { @@ -1074,6 +1075,9 @@ pub struct ConstraintExtensionPermanentDelegate { pub permanent_delegate: Expr, } +#[derive(Debug, Clone)] +pub struct ConstraintExtensionNonTransferable {} + #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum InitKind { @@ -1111,6 +1115,7 @@ pub enum InitKind { permanent_delegate: Option, transfer_hook_authority: Option, transfer_hook_program_id: Option, + non_transferable: Option<()>, }, } @@ -1229,6 +1234,7 @@ pub struct ConstraintTokenMintGroup { pub permanent_delegate: Option, pub transfer_hook_authority: Option, pub transfer_hook_program_id: Option, + pub non_transferable: Option<()>, } // Syntax context object for preserving metadata about the inner item. diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index fda6d48dae..6a12b3b018 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -262,6 +262,10 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } + "non_transferable" => ConstraintToken::ExtensionNonTransferable(Context::new( + ident.span(), + ConstraintExtensionNonTransferable {}, + )), _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } @@ -544,6 +548,7 @@ pub struct ConstraintGroupBuilder<'ty> { pub extension_transfer_hook_authority: Option>, pub extension_transfer_hook_program_id: Option>, pub extension_permanent_delegate: Option>, + pub extension_non_transferable: Option>, pub bump: Option>, pub program_seed: Option>, pub realloc: Option>, @@ -590,6 +595,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_hook_authority: None, extension_transfer_hook_program_id: None, extension_permanent_delegate: None, + extension_non_transferable: None, bump: None, program_seed: None, realloc: None, @@ -803,6 +809,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_hook_authority, extension_transfer_hook_program_id, extension_permanent_delegate, + extension_non_transferable, bump, program_seed, realloc, @@ -907,6 +914,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { &extension_transfer_hook_authority, &extension_transfer_hook_program_id, &extension_permanent_delegate, + &extension_non_transferable, ) { ( None, @@ -923,6 +931,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { None, None, None, + None, ) => None, _ => Some(ConstraintTokenMintGroup { decimals: mint_decimals @@ -968,6 +977,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> { transfer_hook_program_id: extension_transfer_hook_program_id .as_ref() .map(|a| a.clone().into_inner().program_id), + non_transferable: extension_non_transferable + .as_ref() + .map(|_| ()), }), }; @@ -1044,6 +1056,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { .map(|tha| tha.into_inner().authority), transfer_hook_program_id: extension_transfer_hook_program_id .map(|thpid| thpid.into_inner().program_id), + non_transferable: extension_non_transferable.map(|_| ()), } } else { InitKind::Program { @@ -1136,6 +1149,9 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.add_extension_permanent_delegate(c) } ConstraintToken::Dup(c) => self.add_dup(c), + ConstraintToken::ExtensionNonTransferable(c) => { + self.add_extension_non_transferable(c) + } } } @@ -1727,4 +1743,18 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.dup.replace(c); Ok(()) } + + fn add_extension_non_transferable( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_non_transferable.is_some() { + return Err(ParseError::new( + c.span(), + "extension non-transferable already provided", + )); + } + self.extension_non_transferable.replace(c); + Ok(()) + } } diff --git a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs index eaf98c5313..3a77d84719 100644 --- a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs +++ b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs @@ -5,7 +5,7 @@ use anchor_spl::{ token_2022::spl_token_2022::extension::{ group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, mint_close_authority::MintCloseAuthority, permanent_delegate::PermanentDelegate, - transfer_hook::TransferHook, + transfer_hook::TransferHook, non_transferable::NonTransferable, }, token_interface::{ get_mint_extension_data, spl_token_metadata_interface::state::TokenMetadata, @@ -53,6 +53,7 @@ pub struct CreateMintAccount<'info> { extensions::transfer_hook::program_id = crate::ID, extensions::close_authority::authority = authority, extensions::permanent_delegate::delegate = authority, + extensions::non_transferable, )] pub mint: Box>, #[account( @@ -150,6 +151,7 @@ pub fn handler(ctx: Context, args: CreateMintAccountArgs) -> group_member_pointer.member_address, OptionalNonZeroPubkey::try_from(mint_key)? ); + let _ = get_mint_extension_data::(mint_data)?; // transfer minimum rent to mint account update_account_lamports_to_minimum_balance( ctx.accounts.mint.to_account_info(), @@ -175,6 +177,7 @@ pub struct CheckMintExtensionConstraints<'info> { extensions::transfer_hook::program_id = crate::ID, extensions::close_authority::authority = authority, extensions::permanent_delegate::delegate = authority, + extensions::non_transferable, )] pub mint: Box>, } From ee8bb7c7d347945086286966f867f4fd5a8e4c26 Mon Sep 17 00:00:00 2001 From: Ikrk Date: Fri, 9 May 2025 23:06:04 +0200 Subject: [PATCH 2/5] Transfer fee token extension --- lang/src/error.rs | 9 + lang/syn/src/codegen/accounts/constraints.rs | 103 ++++++++ lang/syn/src/lib.rs | 22 ++ lang/syn/src/parser/accounts/constraints.rs | 219 +++++++++++++++--- .../token-extensions/src/instructions.rs | 29 ++- 5 files changed, 349 insertions(+), 33 deletions(-) diff --git a/lang/src/error.rs b/lang/src/error.rs index 930e2cde76..1505226796 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -188,6 +188,15 @@ pub enum ErrorCode { /// 2043 - A non-transferable extension constraint was violated #[msg("A non-transferable extension constraint was violated")] ConstraintMintNonTransferableExtension, + /// 2044 - A transfer fee extension constraint was violated + #[msg("A transfer fee extension constraint was violated")] + ConstraintMintTransferFeeExtension, + /// 2045 - A transfer fee extension config authority constraint was violated + #[msg("A transfer fee extension config authority constraint was violated")] + ConstraintMintTransferFeeConfigAuthority, + /// 2046 - A transfer fee extension withheld authority constraint was violated + #[msg("A transfer fee extension withheld authority constraint was violated")] + ConstraintMintTransferFeeWithheldAuthority, // Require /// 2500 - A require expression was violated diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index d9550a4055..fae3cdc7c2 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -770,6 +770,10 @@ fn generate_constraint_init_group( transfer_hook_authority, transfer_hook_program_id, non_transferable, + transfer_fee_config_authority, + transfer_fee_withheld_authority, + transfer_fee_basis_points, + transfer_fee_max_fee, } => { let token_program = match token_program { Some(t) => t.to_token_stream(), @@ -834,6 +838,26 @@ fn generate_constraint_init_group( None => quote! {}, }; + let transfer_fee_config_authority_check = match transfer_fee_config_authority { + Some(tfca) => check_scope.generate_check(tfca), + None => quote! {}, + }; + + let transfer_fee_withheld_authority_check = match transfer_fee_withheld_authority { + Some(tfwa) => check_scope.generate_check(tfwa), + None => quote! {}, + }; + + let transfer_fee_basis_points_check = match transfer_fee_basis_points { + Some(tfbp) => check_scope.generate_check(tfbp), + None => quote! {}, + }; + + let transfer_fee_max_fee_check = match transfer_fee_max_fee { + Some(tfmf) => check_scope.generate_check(tfmf), + None => quote! {}, + }; + let system_program_optional_check = check_scope.generate_check(system_program); let token_program_optional_check = check_scope.generate_check(&token_program); let rent_optional_check = check_scope.generate_check(rent); @@ -854,6 +878,10 @@ fn generate_constraint_init_group( #transfer_hook_authority_check #transfer_hook_program_id_check #permanent_delegate_check + #transfer_fee_config_authority_check + #transfer_fee_withheld_authority_check + #transfer_fee_basis_points_check + #transfer_fee_max_fee_check }; let payer_optional_check = check_scope.generate_check(payer); @@ -889,6 +917,11 @@ fn generate_constraint_init_group( extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::NonTransferable}); } + if transfer_fee_config_authority.is_some() || transfer_fee_withheld_authority.is_some() || + transfer_fee_basis_points.is_some() || transfer_fee_max_fee.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::TransferFeeConfig}); + } + let mint_space = if extensions.is_empty() { quote! { ::anchor_spl::token::Mint::LEN } } else { @@ -960,6 +993,26 @@ fn generate_constraint_init_group( None => quote! { Option::::None }, }; + let transfer_fee_config_authority = match transfer_fee_config_authority { + Some(tfca) => quote! { Option::::Some(#tfca.key()) }, + None => quote! { Option::::None }, + }; + + let transfer_fee_withheld_authority = match transfer_fee_withheld_authority { + Some(tfwa) => quote! { Option::::Some(#tfwa.key()) }, + None => quote! { Option::::None }, + }; + + let transfer_fee_basis_points = match transfer_fee_basis_points { + Some(tfbp) => quote! { Option::::Some(#tfbp) }, + None => quote! { Option::::None }, + }; + + let transfer_fee_max_fee = match transfer_fee_max_fee { + Some(tfmf) => quote! { Option::::Some(#tfmf) }, + None => quote! { Option::::None }, + }; + let create_account = generate_create_account( field, mint_space, @@ -1039,6 +1092,17 @@ fn generate_constraint_init_group( mint: #field.to_account_info(), }))?; }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::TransferFeeConfig => { + ::anchor_spl::token_interface::transfer_fee_initialize(anchor_lang::context::CpiContext::new(cpi_program_id, ::anchor_spl::token_interface::TransferFeeInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }), + #transfer_fee_config_authority.as_ref(), + #transfer_fee_withheld_authority.as_ref(), + #transfer_fee_basis_points.unwrap(), + #transfer_fee_max_fee.unwrap(), + )?; + }, // All extensions specified by the user should be implemented. // If this line runs, it means there is a bug in the codegen. _ => unimplemented!("{e:?}"), @@ -1611,6 +1675,43 @@ fn generate_constraint_mint( None => quote! {}, }; + let transfer_fee_config_authority_check = match &c.transfer_fee_config_authority { + Some(transfer_fee_config_authority) => { + let transfer_fee_config_authority_optional_check = + optional_check_scope.generate_check(transfer_fee_config_authority); + quote! { + let transfer_fee = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::transfer_fee::TransferFeeConfig>(#account_ref); + if transfer_fee.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferFeeExtension.into()); + } + #transfer_fee_config_authority_optional_check + if transfer_fee.unwrap().transfer_fee_config_authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#transfer_fee_config_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferFeeConfigAuthority.into()); + } + } + } + None => quote! {}, + }; + + let transfer_fee_withheld_authority_check = match &c.transfer_fee_withheld_authority { + Some(transfer_fee_withheld_authority) => { + let transfer_fee_withheld_authority_optional_check = + optional_check_scope.generate_check(transfer_fee_withheld_authority); + quote! { + let transfer_fee = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::transfer_fee::TransferFeeConfig>(#account_ref); + if transfer_fee.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferFeeExtension.into()); + } + #transfer_fee_withheld_authority_optional_check + if transfer_fee.unwrap().withdraw_withheld_authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#transfer_fee_withheld_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintTransferFeeWithheldAuthority.into()); + } + } + } + None => quote! {}, + }; + + quote! { { #decimal_check @@ -1628,6 +1729,8 @@ fn generate_constraint_mint( #transfer_hook_authority_check #transfer_hook_program_id_check #non_transferable_check + #transfer_fee_config_authority_check + #transfer_fee_withheld_authority_check } } } diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index b2bf904b09..8048648227 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -835,6 +835,10 @@ pub enum ConstraintToken { ExtensionTokenHookProgramId(Context), ExtensionPermanentDelegate(Context), ExtensionNonTransferable(Context), + ExtensionTransferFeeConfigAuthority(Context), + ExtensionTransferFeeWithheldAuthority(Context), + ExtensionTransferFeeBasisPoints(Context), + ExtensionTransferFeeMaxFee(Context), } impl Parse for ConstraintToken { @@ -1078,6 +1082,16 @@ pub struct ConstraintExtensionPermanentDelegate { #[derive(Debug, Clone)] pub struct ConstraintExtensionNonTransferable {} +#[derive(Debug, Clone)] +pub struct ConstraintExtensionTransferFeeBasisPoints { + pub basis_points: Expr, +} + +#[derive(Debug, Clone)] +pub struct ConstraintExtensionTransferFeeMaxFee { + pub max_fee: Expr, +} + #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum InitKind { @@ -1116,6 +1130,10 @@ pub enum InitKind { transfer_hook_authority: Option, transfer_hook_program_id: Option, non_transferable: Option<()>, + transfer_fee_config_authority: Option, + transfer_fee_withheld_authority: Option, + transfer_fee_basis_points: Option, + transfer_fee_max_fee: Option, }, } @@ -1235,6 +1253,10 @@ pub struct ConstraintTokenMintGroup { pub transfer_hook_authority: Option, pub transfer_hook_program_id: Option, pub non_transferable: Option<()>, + pub transfer_fee_config_authority: Option, + pub transfer_fee_withheld_authority: Option, + pub transfer_fee_basis_points: Option, + pub transfer_fee_max_fee: Option, } // Syntax context object for preserving metadata about the inner item. diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index 6a12b3b018..cb8a91cb96 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -266,6 +266,51 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { ident.span(), ConstraintExtensionNonTransferable {}, )), + "transfer_fee" => { + stream.parse::()?; + stream.parse::()?; + let kw = stream.call(Ident::parse_any)?.to_string(); + stream.parse::()?; + + let span = ident + .span() + .join(stream.span()) + .unwrap_or_else(|| ident.span()); + + match kw.as_str() { + "config_authority" => { + ConstraintToken::ExtensionTransferFeeConfigAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )) + } + "withheld_authority" => { + ConstraintToken::ExtensionTransferFeeWithheldAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )) + } + "basis_points" => { + ConstraintToken::ExtensionTransferFeeBasisPoints(Context::new( + span, + ConstraintExtensionTransferFeeBasisPoints { + basis_points: stream.parse()?, + }, + )) + } + "max_fee" => ConstraintToken::ExtensionTransferFeeMaxFee(Context::new( + span, + ConstraintExtensionTransferFeeMaxFee { + max_fee: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } @@ -549,6 +594,11 @@ pub struct ConstraintGroupBuilder<'ty> { pub extension_transfer_hook_program_id: Option>, pub extension_permanent_delegate: Option>, pub extension_non_transferable: Option>, + pub extension_transfer_fee_config_authority: Option>, + pub extension_transfer_fee_withheld_authority: Option>, + pub extension_transfer_fee_basis_points: + Option>, + pub extension_transfer_fee_max_fee: Option>, pub bump: Option>, pub program_seed: Option>, pub realloc: Option>, @@ -596,6 +646,10 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_hook_program_id: None, extension_permanent_delegate: None, extension_non_transferable: None, + extension_transfer_fee_config_authority: None, + extension_transfer_fee_withheld_authority: None, + extension_transfer_fee_basis_points: None, + extension_transfer_fee_max_fee: None, bump: None, program_seed: None, realloc: None, @@ -810,6 +864,10 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_hook_program_id, extension_permanent_delegate, extension_non_transferable, + extension_transfer_fee_config_authority, + extension_transfer_fee_withheld_authority, + extension_transfer_fee_basis_points, + extension_transfer_fee_max_fee, bump, program_seed, realloc, @@ -843,40 +901,35 @@ impl<'ty> ConstraintGroupBuilder<'ty> { .expect("bump must be provided with seeds"), program_seed: into_inner!(program_seed).map(|id| id.program_seed), }); - let associated_token = match ( - associated_token_mint, - associated_token_authority, - &associated_token_token_program, - ) { - (Some(mint), Some(auth), _) => Some(ConstraintAssociatedToken { - wallet: auth.into_inner().auth, - mint: mint.into_inner().mint, - token_program: associated_token_token_program - .as_ref() - .map(|a| a.clone().into_inner().token_program), - }), - (Some(mint), None, _) => { - return Err(ParseError::new( + let associated_token = + match ( + associated_token_mint, + associated_token_authority, + &associated_token_token_program, + ) { + (Some(mint), Some(auth), _) => Some(ConstraintAssociatedToken { + wallet: auth.into_inner().auth, + mint: mint.into_inner().mint, + token_program: associated_token_token_program + .as_ref() + .map(|a| a.clone().into_inner().token_program), + }), + (Some(mint), None, _) => return Err(ParseError::new( mint.span(), "authority must be provided to specify an associated token program derived \ address", - )) - } - (None, Some(auth), _) => { - return Err(ParseError::new( + )), + (None, Some(auth), _) => return Err(ParseError::new( auth.span(), "mint must be provided to specify an associated token program derived address", - )) - } - (None, None, Some(token_program)) => { - return Err(ParseError::new( + )), + (None, None, Some(token_program)) => return Err(ParseError::new( token_program.span(), "mint and authority must be provided to specify an associated token program \ derived address", - )) - } - _ => None, - }; + )), + _ => None, + }; if let Some(associated_token) = &associated_token { if seeds.is_some() { return Err(ParseError::new( @@ -915,6 +968,10 @@ impl<'ty> ConstraintGroupBuilder<'ty> { &extension_transfer_hook_program_id, &extension_permanent_delegate, &extension_non_transferable, + &extension_transfer_fee_config_authority, + &extension_transfer_fee_withheld_authority, + &extension_transfer_fee_basis_points, + &extension_transfer_fee_max_fee, ) { ( None, @@ -932,6 +989,10 @@ impl<'ty> ConstraintGroupBuilder<'ty> { None, None, None, + None, + None, + None, + None, ) => None, _ => Some(ConstraintTokenMintGroup { decimals: mint_decimals @@ -977,9 +1038,19 @@ impl<'ty> ConstraintGroupBuilder<'ty> { transfer_hook_program_id: extension_transfer_hook_program_id .as_ref() .map(|a| a.clone().into_inner().program_id), - non_transferable: extension_non_transferable + non_transferable: extension_non_transferable.as_ref().map(|_| ()), + transfer_fee_config_authority: extension_transfer_fee_config_authority .as_ref() - .map(|_| ()), + .map(|a| a.clone().into_inner().authority), + transfer_fee_withheld_authority: extension_transfer_fee_withheld_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), + transfer_fee_basis_points: extension_transfer_fee_basis_points + .as_ref() + .map(|a| a.clone().into_inner().basis_points), + transfer_fee_max_fee: extension_transfer_fee_max_fee + .as_ref() + .map(|a| a.clone().into_inner().max_fee), }), }; @@ -1016,6 +1087,10 @@ impl<'ty> ConstraintGroupBuilder<'ty> { .map(|tp| tp.into_inner().token_program), } } else if let Some(d) = &mint_decimals { + let init_transfer_fee = extension_transfer_fee_config_authority.is_some() || + extension_transfer_fee_withheld_authority.is_some() || + extension_transfer_fee_basis_points.is_some() || + extension_transfer_fee_max_fee.is_some(); InitKind::Mint { decimals: d.clone().into_inner().decimals, owner: match &mint_authority { @@ -1057,6 +1132,24 @@ impl<'ty> ConstraintGroupBuilder<'ty> { transfer_hook_program_id: extension_transfer_hook_program_id .map(|thpid| thpid.into_inner().program_id), non_transferable: extension_non_transferable.map(|_| ()), + transfer_fee_config_authority: extension_transfer_fee_config_authority.map(|tfca| tfca.into_inner().authority), + transfer_fee_withheld_authority: extension_transfer_fee_withheld_authority.map(|tfwa| tfwa.into_inner().authority), + transfer_fee_basis_points: match (&extension_transfer_fee_basis_points, init_transfer_fee) { + (Some(tfbp), _) => Some(tfbp.clone().into_inner().basis_points), + (None, true) => return Err(ParseError::new( + d.span(), + "fee basis points must be provided to initialize a mint with transfer fee extension" + )), + _ => None + }, + transfer_fee_max_fee: match (&extension_transfer_fee_max_fee, init_transfer_fee) { + (Some(tfmf), _) => Some(tfmf.clone().into_inner().max_fee), + (None, true) => return Err(ParseError::new( + d.span(), + "maximum fee must be provided to initialize a mint with transfer fee extension" + )), + _ => None + }, } } else { InitKind::Program { @@ -1149,8 +1242,18 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.add_extension_permanent_delegate(c) } ConstraintToken::Dup(c) => self.add_dup(c), - ConstraintToken::ExtensionNonTransferable(c) => { - self.add_extension_non_transferable(c) + ConstraintToken::ExtensionNonTransferable(c) => self.add_extension_non_transferable(c), + ConstraintToken::ExtensionTransferFeeConfigAuthority(c) => { + self.add_extension_transfer_fee_config_authority(c) + } + ConstraintToken::ExtensionTransferFeeWithheldAuthority(c) => { + self.add_extension_transfer_fee_withheld_authority(c) + } + ConstraintToken::ExtensionTransferFeeBasisPoints(c) => { + self.add_extension_transfer_fee_basis_points(c) + } + ConstraintToken::ExtensionTransferFeeMaxFee(c) => { + self.add_extension_transfer_fee_max_fee(c) } } } @@ -1757,4 +1860,60 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.extension_non_transferable.replace(c); Ok(()) } + + fn add_extension_transfer_fee_config_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_transfer_fee_config_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension transfer fee config authority already provided", + )); + } + self.extension_transfer_fee_config_authority.replace(c); + Ok(()) + } + + fn add_extension_transfer_fee_withheld_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_transfer_fee_withheld_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension transfer fee withheld authority already provided", + )); + } + self.extension_transfer_fee_withheld_authority.replace(c); + Ok(()) + } + + fn add_extension_transfer_fee_basis_points( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_transfer_fee_basis_points.is_some() { + return Err(ParseError::new( + c.span(), + "extension transfer fee basis points already provided", + )); + } + self.extension_transfer_fee_basis_points.replace(c); + Ok(()) + } + + fn add_extension_transfer_fee_max_fee( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_transfer_fee_max_fee.is_some() { + return Err(ParseError::new( + c.span(), + "extension transfer fee maximum fee already provided", + )); + } + self.extension_transfer_fee_max_fee.replace(c); + Ok(()) + } } diff --git a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs index 3a77d84719..c054539d78 100644 --- a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs +++ b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs @@ -3,9 +3,7 @@ use anchor_lang::{prelude::*, solana_program::entrypoint::ProgramResult}; use anchor_spl::{ associated_token::AssociatedToken, token_2022::spl_token_2022::extension::{ - group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, - mint_close_authority::MintCloseAuthority, permanent_delegate::PermanentDelegate, - transfer_hook::TransferHook, non_transferable::NonTransferable, + group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, mint_close_authority::MintCloseAuthority, non_transferable::NonTransferable, permanent_delegate::PermanentDelegate, transfer_fee::TransferFeeConfig, transfer_hook::TransferHook }, token_interface::{ get_mint_extension_data, spl_token_metadata_interface::state::TokenMetadata, @@ -19,6 +17,8 @@ use crate::{ update_account_lamports_to_minimum_balance, META_LIST_ACCOUNT_SEED, }; +const BASIS_POINTS: u16 = 100; +const MAX_FEE: u64 = 10000; #[derive(AnchorDeserialize, AnchorSerialize)] pub struct CreateMintAccountArgs { pub name: String, @@ -54,6 +54,10 @@ pub struct CreateMintAccount<'info> { extensions::close_authority::authority = authority, extensions::permanent_delegate::delegate = authority, extensions::non_transferable, + extensions::transfer_fee::config_authority = authority, + extensions::transfer_fee::withheld_authority = authority, + extensions::transfer_fee::basis_points = BASIS_POINTS, + extensions::transfer_fee::max_fee = MAX_FEE, )] pub mint: Box>, #[account( @@ -152,6 +156,23 @@ pub fn handler(ctx: Context, args: CreateMintAccountArgs) -> OptionalNonZeroPubkey::try_from(mint_key)? ); let _ = get_mint_extension_data::(mint_data)?; + let transfer_fee = get_mint_extension_data::(mint_data)?; + assert_eq!( + transfer_fee.transfer_fee_config_authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + assert_eq!( + transfer_fee.withdraw_withheld_authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + assert_eq!( + transfer_fee.newer_transfer_fee.transfer_fee_basis_points, + BASIS_POINTS.into() + ); + assert_eq!( + transfer_fee.newer_transfer_fee.maximum_fee, + MAX_FEE.into() + ); // transfer minimum rent to mint account update_account_lamports_to_minimum_balance( ctx.accounts.mint.to_account_info(), @@ -178,6 +199,8 @@ pub struct CheckMintExtensionConstraints<'info> { extensions::close_authority::authority = authority, extensions::permanent_delegate::delegate = authority, extensions::non_transferable, + extensions::transfer_fee::config_authority = authority, + extensions::transfer_fee::withheld_authority = authority )] pub mint: Box>, } From fef0353d878645fbf19d186f2e7d1d3e557ac678 Mon Sep 17 00:00:00 2001 From: Ikrk Date: Sat, 10 May 2025 01:21:20 +0200 Subject: [PATCH 3/5] Interest bearing token extension --- lang/src/error.rs | 6 ++ lang/syn/src/codegen/accounts/constraints.rs | 56 +++++++++++++ lang/syn/src/lib.rs | 11 +++ lang/syn/src/parser/accounts/constraints.rs | 83 +++++++++++++++++++ .../token-extensions/src/instructions.rs | 18 +++- 5 files changed, 172 insertions(+), 2 deletions(-) diff --git a/lang/src/error.rs b/lang/src/error.rs index 1505226796..515faa2aa0 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -197,6 +197,12 @@ pub enum ErrorCode { /// 2046 - A transfer fee extension withheld authority constraint was violated #[msg("A transfer fee extension withheld authority constraint was violated")] ConstraintMintTransferFeeWithheldAuthority, + /// 2047 - An interest bearing extension constraint was violated + #[msg("An interest bearing extension constraint was violated")] + ConstraintMintInterestBearingExtension, + /// 2048 - An interest bearing extension rate authority constraint was violated + #[msg("An interest bearing extension rate authority constraint was violated")] + ConstraintMintInterestBearingRateAuthority, // Require /// 2500 - A require expression was violated diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index fae3cdc7c2..038c3e751b 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -774,6 +774,8 @@ fn generate_constraint_init_group( transfer_fee_withheld_authority, transfer_fee_basis_points, transfer_fee_max_fee, + interest_bearing_authority, + interest_bearing_rate, } => { let token_program = match token_program { Some(t) => t.to_token_stream(), @@ -858,6 +860,16 @@ fn generate_constraint_init_group( None => quote! {}, }; + let interest_bearing_authority_check = match interest_bearing_authority { + Some(iba) => check_scope.generate_check(iba), + None => quote! {}, + }; + + let interest_bearing_rate_check = match interest_bearing_rate { + Some(ibr) => check_scope.generate_check(ibr), + None => quote! {}, + }; + let system_program_optional_check = check_scope.generate_check(system_program); let token_program_optional_check = check_scope.generate_check(&token_program); let rent_optional_check = check_scope.generate_check(rent); @@ -882,6 +894,8 @@ fn generate_constraint_init_group( #transfer_fee_withheld_authority_check #transfer_fee_basis_points_check #transfer_fee_max_fee_check + #interest_bearing_authority_check + #interest_bearing_rate_check }; let payer_optional_check = check_scope.generate_check(payer); @@ -922,6 +936,10 @@ fn generate_constraint_init_group( extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::TransferFeeConfig}); } + if interest_bearing_authority.is_some() || interest_bearing_rate.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::InterestBearingConfig}); + } + let mint_space = if extensions.is_empty() { quote! { ::anchor_spl::token::Mint::LEN } } else { @@ -1013,6 +1031,16 @@ fn generate_constraint_init_group( None => quote! { Option::::None }, }; + let interest_bearing_authority = match interest_bearing_authority { + Some(iba) => quote! { Option::::Some(#iba.key()) }, + None => quote! { Option::::None }, + }; + + let interest_bearing_rate = match interest_bearing_rate { + Some(ibr) => quote! { Option::::Some(#ibr) }, + None => quote! { Option::::None }, + }; + let create_account = generate_create_account( field, mint_space, @@ -1103,6 +1131,15 @@ fn generate_constraint_init_group( #transfer_fee_max_fee.unwrap(), )?; }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::InterestBearingConfig => { + ::anchor_spl::token_interface::interest_bearing_mint_initialize(anchor_lang::context::CpiContext::new(cpi_program_id, ::anchor_spl::token_interface::InterestBearingMintInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }), + #interest_bearing_authority, + #interest_bearing_rate.unwrap(), + )?; + }, // All extensions specified by the user should be implemented. // If this line runs, it means there is a bug in the codegen. _ => unimplemented!("{e:?}"), @@ -1711,6 +1748,24 @@ fn generate_constraint_mint( None => quote! {}, }; + let interest_bearing_authority_check = match &c.interest_bearing_authority { + Some(interest_bearing_authority) => { + let interest_bearing_authority_optional_check = + optional_check_scope.generate_check(interest_bearing_authority); + quote! { + let interest_bearing = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::interest_bearing_mint::InterestBearingConfig>(#account_ref); + if interest_bearing.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintInterestBearingExtension.into()); + } + #interest_bearing_authority_optional_check + if interest_bearing.unwrap().rate_authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#interest_bearing_authority.key()))? { + return Err(anchor_lang::error::ErrorCode::ConstraintMintInterestBearingRateAuthority.into()); + } + } + } + None => quote! {}, + }; + quote! { { @@ -1731,6 +1786,7 @@ fn generate_constraint_mint( #non_transferable_check #transfer_fee_config_authority_check #transfer_fee_withheld_authority_check + #interest_bearing_authority_check } } } diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 8048648227..cd7253a2cc 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -839,6 +839,8 @@ pub enum ConstraintToken { ExtensionTransferFeeWithheldAuthority(Context), ExtensionTransferFeeBasisPoints(Context), ExtensionTransferFeeMaxFee(Context), + ExtensionInterestBearingRateAuthority(Context), + ExtensionInterestBearingRate(Context), } impl Parse for ConstraintToken { @@ -1092,6 +1094,11 @@ pub struct ConstraintExtensionTransferFeeMaxFee { pub max_fee: Expr, } +#[derive(Debug, Clone)] +pub struct ConstraintExtensionInterestBearingRate { + pub rate: Expr, +} + #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum InitKind { @@ -1134,6 +1141,8 @@ pub enum InitKind { transfer_fee_withheld_authority: Option, transfer_fee_basis_points: Option, transfer_fee_max_fee: Option, + interest_bearing_authority: Option, + interest_bearing_rate: Option, }, } @@ -1257,6 +1266,8 @@ pub struct ConstraintTokenMintGroup { pub transfer_fee_withheld_authority: Option, pub transfer_fee_basis_points: Option, pub transfer_fee_max_fee: Option, + pub interest_bearing_authority: Option, + pub interest_bearing_rate: Option, } // Syntax context object for preserving metadata about the inner item. diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index cb8a91cb96..19d074e9fe 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -311,6 +311,33 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } + "interest_bearing" => { + stream.parse::()?; + stream.parse::()?; + let kw = stream.call(Ident::parse_any)?.to_string(); + stream.parse::()?; + + let span = ident + .span() + .join(stream.span()) + .unwrap_or_else(|| ident.span()); + + match kw.as_str() { + "authority" => ConstraintToken::ExtensionInterestBearingRateAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )), + "rate" => ConstraintToken::ExtensionInterestBearingRate(Context::new( + span, + ConstraintExtensionInterestBearingRate { + rate: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } @@ -599,6 +626,8 @@ pub struct ConstraintGroupBuilder<'ty> { pub extension_transfer_fee_basis_points: Option>, pub extension_transfer_fee_max_fee: Option>, + pub extension_interest_bearing_authority: Option>, + pub extension_interest_bearing_rate: Option>, pub bump: Option>, pub program_seed: Option>, pub realloc: Option>, @@ -650,6 +679,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_fee_withheld_authority: None, extension_transfer_fee_basis_points: None, extension_transfer_fee_max_fee: None, + extension_interest_bearing_authority: None, + extension_interest_bearing_rate: None, bump: None, program_seed: None, realloc: None, @@ -868,6 +899,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_fee_withheld_authority, extension_transfer_fee_basis_points, extension_transfer_fee_max_fee, + extension_interest_bearing_authority, + extension_interest_bearing_rate, bump, program_seed, realloc, @@ -972,6 +1005,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { &extension_transfer_fee_withheld_authority, &extension_transfer_fee_basis_points, &extension_transfer_fee_max_fee, + &extension_interest_bearing_authority, + &extension_interest_bearing_rate, ) { ( None, @@ -993,6 +1028,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { None, None, None, + None, + None, ) => None, _ => Some(ConstraintTokenMintGroup { decimals: mint_decimals @@ -1051,6 +1088,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> { transfer_fee_max_fee: extension_transfer_fee_max_fee .as_ref() .map(|a| a.clone().into_inner().max_fee), + interest_bearing_authority: extension_interest_bearing_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), + interest_bearing_rate: extension_interest_bearing_rate + .as_ref() + .map(|a| a.clone().into_inner().rate), }), }; @@ -1150,6 +1193,15 @@ impl<'ty> ConstraintGroupBuilder<'ty> { )), _ => None }, + interest_bearing_authority: extension_interest_bearing_authority.as_ref().map(|iba| iba.clone().into_inner().authority), + interest_bearing_rate: match (&extension_interest_bearing_rate, &extension_interest_bearing_authority) { + (Some(ibr), _) => Some(ibr.clone().into_inner().rate), + (None, Some(_)) => return Err(ParseError::new( + d.span(), + "rate must be provided to initialize a mint with interest bearing extension" + )), + _ => None + }, } } else { InitKind::Program { @@ -1255,6 +1307,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { ConstraintToken::ExtensionTransferFeeMaxFee(c) => { self.add_extension_transfer_fee_max_fee(c) } + ConstraintToken::ExtensionInterestBearingRateAuthority(c) => self.add_extension_interest_bearing_authority(c), + ConstraintToken::ExtensionInterestBearingRate(c) => self.add_extension_interest_bearing_rate(c), } } @@ -1916,4 +1970,33 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.extension_transfer_fee_max_fee.replace(c); Ok(()) } + + fn add_extension_interest_bearing_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_interest_bearing_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension interest bearing authority already provided", + )); + } + self.extension_interest_bearing_authority.replace(c); + Ok(()) + } + + fn add_extension_interest_bearing_rate( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_interest_bearing_rate.is_some() { + return Err(ParseError::new( + c.span(), + "extension interest bearing rate already provided", + )); + } + self.extension_interest_bearing_rate.replace(c); + Ok(()) + } + } diff --git a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs index c054539d78..55d1a9ff02 100644 --- a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs +++ b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs @@ -3,7 +3,7 @@ use anchor_lang::{prelude::*, solana_program::entrypoint::ProgramResult}; use anchor_spl::{ associated_token::AssociatedToken, token_2022::spl_token_2022::extension::{ - group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, mint_close_authority::MintCloseAuthority, non_transferable::NonTransferable, permanent_delegate::PermanentDelegate, transfer_fee::TransferFeeConfig, transfer_hook::TransferHook + group_member_pointer::GroupMemberPointer, interest_bearing_mint::InterestBearingConfig, metadata_pointer::MetadataPointer, mint_close_authority::MintCloseAuthority, non_transferable::NonTransferable, permanent_delegate::PermanentDelegate, transfer_fee::TransferFeeConfig, transfer_hook::TransferHook }, token_interface::{ get_mint_extension_data, spl_token_metadata_interface::state::TokenMetadata, @@ -19,6 +19,8 @@ use crate::{ const BASIS_POINTS: u16 = 100; const MAX_FEE: u64 = 10000; +const RATE: i16 = 100; + #[derive(AnchorDeserialize, AnchorSerialize)] pub struct CreateMintAccountArgs { pub name: String, @@ -58,6 +60,8 @@ pub struct CreateMintAccount<'info> { extensions::transfer_fee::withheld_authority = authority, extensions::transfer_fee::basis_points = BASIS_POINTS, extensions::transfer_fee::max_fee = MAX_FEE, + extensions::interest_bearing::authority = authority, + extensions::interest_bearing::rate = RATE, )] pub mint: Box>, #[account( @@ -173,6 +177,15 @@ pub fn handler(ctx: Context, args: CreateMintAccountArgs) -> transfer_fee.newer_transfer_fee.maximum_fee, MAX_FEE.into() ); + let interest_bearing = get_mint_extension_data::(mint_data)?; + assert_eq!( + interest_bearing.rate_authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + assert_eq!( + interest_bearing.current_rate, + RATE.into() + ); // transfer minimum rent to mint account update_account_lamports_to_minimum_balance( ctx.accounts.mint.to_account_info(), @@ -200,7 +213,8 @@ pub struct CheckMintExtensionConstraints<'info> { extensions::permanent_delegate::delegate = authority, extensions::non_transferable, extensions::transfer_fee::config_authority = authority, - extensions::transfer_fee::withheld_authority = authority + extensions::transfer_fee::withheld_authority = authority, + extensions::interest_bearing::authority = authority, )] pub mint: Box>, } From 0d046ff8cc81eb1c1d7cf0584ef02a52ee97a84c Mon Sep 17 00:00:00 2001 From: Ikrk Date: Sun, 11 May 2025 00:21:16 +0200 Subject: [PATCH 4/5] Default account state token extension --- lang/src/error.rs | 6 +++ lang/syn/src/codegen/accounts/constraints.rs | 41 ++++++++++++++++ lang/syn/src/lib.rs | 8 +++ lang/syn/src/parser/accounts/constraints.rs | 49 ++++++++++++++++++- .../token-extensions/src/instructions.rs | 13 +++-- 5 files changed, 112 insertions(+), 5 deletions(-) diff --git a/lang/src/error.rs b/lang/src/error.rs index 515faa2aa0..d8abdfa580 100644 --- a/lang/src/error.rs +++ b/lang/src/error.rs @@ -203,6 +203,12 @@ pub enum ErrorCode { /// 2048 - An interest bearing extension rate authority constraint was violated #[msg("An interest bearing extension rate authority constraint was violated")] ConstraintMintInterestBearingRateAuthority, + /// 2049 - A default account state extension constraint was violated + #[msg("A default account state extension constraint was violated")] + ConstraintMintDefaultAccountStateExtension, + /// 2050 - A default account state extension state constraint was violated + #[msg("A default account state extension state constraint was violated")] + ConstraintMintDefaultAccountState, // Require /// 2500 - A require expression was violated diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index 038c3e751b..cd1b4aad1b 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -776,6 +776,7 @@ fn generate_constraint_init_group( transfer_fee_max_fee, interest_bearing_authority, interest_bearing_rate, + default_account_state, } => { let token_program = match token_program { Some(t) => t.to_token_stream(), @@ -870,6 +871,11 @@ fn generate_constraint_init_group( None => quote! {}, }; + let default_account_state_check = match default_account_state { + Some(das) => check_scope.generate_check(das), + None => quote! {}, + }; + let system_program_optional_check = check_scope.generate_check(system_program); let token_program_optional_check = check_scope.generate_check(&token_program); let rent_optional_check = check_scope.generate_check(rent); @@ -896,6 +902,7 @@ fn generate_constraint_init_group( #transfer_fee_max_fee_check #interest_bearing_authority_check #interest_bearing_rate_check + #default_account_state_check }; let payer_optional_check = check_scope.generate_check(payer); @@ -940,6 +947,10 @@ fn generate_constraint_init_group( extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::InterestBearingConfig}); } + if default_account_state.is_some() { + extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::DefaultAccountState}); + } + let mint_space = if extensions.is_empty() { quote! { ::anchor_spl::token::Mint::LEN } } else { @@ -1041,6 +1052,11 @@ fn generate_constraint_init_group( None => quote! { Option::::None }, }; + let default_account_state = match default_account_state { + Some(das) => quote! { Option::::Some(#das) }, + None => quote! { Option::::None }, + }; + let create_account = generate_create_account( field, mint_space, @@ -1140,6 +1156,12 @@ fn generate_constraint_init_group( #interest_bearing_rate.unwrap(), )?; }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::DefaultAccountState => { + ::anchor_spl::token_interface::default_account_state_initialize(anchor_lang::context::CpiContext::new(cpi_program_id, ::anchor_spl::token_interface::DefaultAccountStateInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }), #default_account_state.as_ref().unwrap())?; + }, // All extensions specified by the user should be implemented. // If this line runs, it means there is a bug in the codegen. _ => unimplemented!("{e:?}"), @@ -1766,6 +1788,24 @@ fn generate_constraint_mint( None => quote! {}, }; + let default_account_state_check = match &c.default_account_state { + Some(default_account_state) => { + let default_account_state_optional_check = + optional_check_scope.generate_check(default_account_state); + quote! { + let account_state = ::anchor_spl::token_interface::get_mint_extension_data::<::anchor_spl::token_interface::spl_token_2022::extension::default_account_state::DefaultAccountState>(#account_ref); + if account_state.is_err() { + return Err(anchor_lang::error::ErrorCode::ConstraintMintDefaultAccountStateExtension.into()); + } + #default_account_state_optional_check + if account_state.unwrap().state != #default_account_state as u8 { + return Err(anchor_lang::error::ErrorCode::ConstraintMintDefaultAccountState.into()); + } + } + } + None => quote! {}, + }; + quote! { { @@ -1787,6 +1827,7 @@ fn generate_constraint_mint( #transfer_fee_config_authority_check #transfer_fee_withheld_authority_check #interest_bearing_authority_check + #default_account_state_check } } } diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index cd7253a2cc..94719a08b9 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -841,6 +841,7 @@ pub enum ConstraintToken { ExtensionTransferFeeMaxFee(Context), ExtensionInterestBearingRateAuthority(Context), ExtensionInterestBearingRate(Context), + ExtensionDefaultAccountState(Context), } impl Parse for ConstraintToken { @@ -1099,6 +1100,11 @@ pub struct ConstraintExtensionInterestBearingRate { pub rate: Expr, } +#[derive(Debug, Clone)] +pub struct ConstraintExtensionDefaultAccountState { + pub state: Expr, +} + #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum InitKind { @@ -1143,6 +1149,7 @@ pub enum InitKind { transfer_fee_max_fee: Option, interest_bearing_authority: Option, interest_bearing_rate: Option, + default_account_state: Option, }, } @@ -1268,6 +1275,7 @@ pub struct ConstraintTokenMintGroup { pub transfer_fee_max_fee: Option, pub interest_bearing_authority: Option, pub interest_bearing_rate: Option, + pub default_account_state: Option, } // Syntax context object for preserving metadata about the inner item. diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index 19d074e9fe..42b0e0efc8 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -338,6 +338,27 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } + "default_account_state" => { + stream.parse::()?; + stream.parse::()?; + let kw = stream.call(Ident::parse_any)?.to_string(); + stream.parse::()?; + + let span = ident + .span() + .join(stream.span()) + .unwrap_or_else(|| ident.span()); + + match kw.as_str() { + "state" => ConstraintToken::ExtensionDefaultAccountState(Context::new( + span, + ConstraintExtensionDefaultAccountState { + state: stream.parse()?, + }, + )), + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } @@ -628,6 +649,7 @@ pub struct ConstraintGroupBuilder<'ty> { pub extension_transfer_fee_max_fee: Option>, pub extension_interest_bearing_authority: Option>, pub extension_interest_bearing_rate: Option>, + pub extension_default_account_state: Option>, pub bump: Option>, pub program_seed: Option>, pub realloc: Option>, @@ -681,6 +703,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_fee_max_fee: None, extension_interest_bearing_authority: None, extension_interest_bearing_rate: None, + extension_default_account_state: None, bump: None, program_seed: None, realloc: None, @@ -901,6 +924,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_fee_max_fee, extension_interest_bearing_authority, extension_interest_bearing_rate, + extension_default_account_state, bump, program_seed, realloc, @@ -1007,6 +1031,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { &extension_transfer_fee_max_fee, &extension_interest_bearing_authority, &extension_interest_bearing_rate, + &extension_default_account_state ) { ( None, @@ -1030,6 +1055,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { None, None, None, + None, ) => None, _ => Some(ConstraintTokenMintGroup { decimals: mint_decimals @@ -1094,8 +1120,11 @@ impl<'ty> ConstraintGroupBuilder<'ty> { interest_bearing_rate: extension_interest_bearing_rate .as_ref() .map(|a| a.clone().into_inner().rate), - }), - }; + default_account_state: extension_default_account_state + .as_ref() + .map(|a| a.clone().into_inner().state), + }), + }; Ok(ConstraintGroup { init: init @@ -1202,6 +1231,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { )), _ => None }, + default_account_state: extension_default_account_state.map(|das| das.into_inner().state), } } else { InitKind::Program { @@ -1309,6 +1339,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { } ConstraintToken::ExtensionInterestBearingRateAuthority(c) => self.add_extension_interest_bearing_authority(c), ConstraintToken::ExtensionInterestBearingRate(c) => self.add_extension_interest_bearing_rate(c), + ConstraintToken::ExtensionDefaultAccountState(c) => self.add_extension_default_account_state(c), } } @@ -1999,4 +2030,18 @@ impl<'ty> ConstraintGroupBuilder<'ty> { Ok(()) } + fn add_extension_default_account_state( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_default_account_state.is_some() { + return Err(ParseError::new( + c.span(), + "extension default account state already provided", + )); + } + self.extension_default_account_state.replace(c); + Ok(()) + } + } diff --git a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs index 55d1a9ff02..b05fbab7a4 100644 --- a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs +++ b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs @@ -2,9 +2,9 @@ use anchor_lang::{prelude::*, solana_program::entrypoint::ProgramResult}; use anchor_spl::{ associated_token::AssociatedToken, - token_2022::spl_token_2022::extension::{ - group_member_pointer::GroupMemberPointer, interest_bearing_mint::InterestBearingConfig, metadata_pointer::MetadataPointer, mint_close_authority::MintCloseAuthority, non_transferable::NonTransferable, permanent_delegate::PermanentDelegate, transfer_fee::TransferFeeConfig, transfer_hook::TransferHook - }, + token_2022::spl_token_2022::{extension::{ + default_account_state::DefaultAccountState, group_member_pointer::GroupMemberPointer, interest_bearing_mint::InterestBearingConfig, metadata_pointer::MetadataPointer, mint_close_authority::MintCloseAuthority, non_transferable::NonTransferable, permanent_delegate::PermanentDelegate, transfer_fee::TransferFeeConfig, transfer_hook::TransferHook + }, state::AccountState}, token_interface::{ get_mint_extension_data, spl_token_metadata_interface::state::TokenMetadata, token_metadata_initialize, Mint, Token2022, TokenAccount, TokenMetadataInitialize, @@ -62,6 +62,7 @@ pub struct CreateMintAccount<'info> { extensions::transfer_fee::max_fee = MAX_FEE, extensions::interest_bearing::authority = authority, extensions::interest_bearing::rate = RATE, + extensions::default_account_state::state = AccountState::Frozen, )] pub mint: Box>, #[account( @@ -186,6 +187,11 @@ pub fn handler(ctx: Context, args: CreateMintAccountArgs) -> interest_bearing.current_rate, RATE.into() ); + let default_account_state = get_mint_extension_data::(mint_data)?; + assert_eq!( + default_account_state.state, + AccountState::Frozen as u8 + ); // transfer minimum rent to mint account update_account_lamports_to_minimum_balance( ctx.accounts.mint.to_account_info(), @@ -215,6 +221,7 @@ pub struct CheckMintExtensionConstraints<'info> { extensions::transfer_fee::config_authority = authority, extensions::transfer_fee::withheld_authority = authority, extensions::interest_bearing::authority = authority, + extensions::default_account_state::state = AccountState::Frozen, )] pub mint: Box>, } From f316c595fa7e7e36782a25c7a867ae546c443394 Mon Sep 17 00:00:00 2001 From: Ikrk Date: Sun, 11 May 2025 00:39:53 +0200 Subject: [PATCH 5/5] Formatting --- lang/src/lib.rs | 2 +- lang/syn/src/codegen/accounts/constraints.rs | 16 +- lang/syn/src/parser/accounts/constraints.rs | 158 ++++++++++++------- 3 files changed, 110 insertions(+), 66 deletions(-) diff --git a/lang/src/lib.rs b/lang/src/lib.rs index fb28f0d9f1..0ad3ab66dc 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -62,7 +62,7 @@ pub use { anchor_attribute_event::{emit, event}, anchor_attribute_program::{declare_program, instruction, program}, anchor_derive_accounts::Accounts, - anchor_derive_serde::{__erase, AnchorDeserialize, AnchorSerialize}, + anchor_derive_serde::{AnchorDeserialize, AnchorSerialize, __erase}, anchor_derive_space::InitSpace, borsh::ser::BorshSerialize as AnchorSerialize, const_crypto::ed25519::derive_program_address, diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index cd1b4aad1b..d1c010201e 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -938,8 +938,11 @@ fn generate_constraint_init_group( extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::NonTransferable}); } - if transfer_fee_config_authority.is_some() || transfer_fee_withheld_authority.is_some() || - transfer_fee_basis_points.is_some() || transfer_fee_max_fee.is_some() { + if transfer_fee_config_authority.is_some() + || transfer_fee_withheld_authority.is_some() + || transfer_fee_basis_points.is_some() + || transfer_fee_max_fee.is_some() + { extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::TransferFeeConfig}); } @@ -1053,8 +1056,12 @@ fn generate_constraint_init_group( }; let default_account_state = match default_account_state { - Some(das) => quote! { Option::::Some(#das) }, - None => quote! { Option::::None }, + Some(das) => { + quote! { Option::::Some(#das) } + } + None => { + quote! { Option::::None } + } }; let create_account = generate_create_account( @@ -1806,7 +1813,6 @@ fn generate_constraint_mint( None => quote! {}, }; - quote! { { #decimal_check diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index 42b0e0efc8..72c5107ee9 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -323,12 +323,14 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { .unwrap_or_else(|| ident.span()); match kw.as_str() { - "authority" => ConstraintToken::ExtensionInterestBearingRateAuthority(Context::new( - span, - ConstraintExtensionAuthority { - authority: stream.parse()?, - }, - )), + "authority" => { + ConstraintToken::ExtensionInterestBearingRateAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )) + } "rate" => ConstraintToken::ExtensionInterestBearingRate(Context::new( span, ConstraintExtensionInterestBearingRate { @@ -958,35 +960,40 @@ impl<'ty> ConstraintGroupBuilder<'ty> { .expect("bump must be provided with seeds"), program_seed: into_inner!(program_seed).map(|id| id.program_seed), }); - let associated_token = - match ( - associated_token_mint, - associated_token_authority, - &associated_token_token_program, - ) { - (Some(mint), Some(auth), _) => Some(ConstraintAssociatedToken { - wallet: auth.into_inner().auth, - mint: mint.into_inner().mint, - token_program: associated_token_token_program - .as_ref() - .map(|a| a.clone().into_inner().token_program), - }), - (Some(mint), None, _) => return Err(ParseError::new( + let associated_token = match ( + associated_token_mint, + associated_token_authority, + &associated_token_token_program, + ) { + (Some(mint), Some(auth), _) => Some(ConstraintAssociatedToken { + wallet: auth.into_inner().auth, + mint: mint.into_inner().mint, + token_program: associated_token_token_program + .as_ref() + .map(|a| a.clone().into_inner().token_program), + }), + (Some(mint), None, _) => { + return Err(ParseError::new( mint.span(), "authority must be provided to specify an associated token program derived \ address", - )), - (None, Some(auth), _) => return Err(ParseError::new( + )) + } + (None, Some(auth), _) => { + return Err(ParseError::new( auth.span(), "mint must be provided to specify an associated token program derived address", - )), - (None, None, Some(token_program)) => return Err(ParseError::new( + )) + } + (None, None, Some(token_program)) => { + return Err(ParseError::new( token_program.span(), "mint and authority must be provided to specify an associated token program \ derived address", - )), - _ => None, - }; + )) + } + _ => None, + }; if let Some(associated_token) = &associated_token { if seeds.is_some() { return Err(ParseError::new( @@ -1031,7 +1038,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { &extension_transfer_fee_max_fee, &extension_interest_bearing_authority, &extension_interest_bearing_rate, - &extension_default_account_state + &extension_default_account_state, ) { ( None, @@ -1123,8 +1130,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { default_account_state: extension_default_account_state .as_ref() .map(|a| a.clone().into_inner().state), - }), - }; + }), + }; Ok(ConstraintGroup { init: init @@ -1159,10 +1166,11 @@ impl<'ty> ConstraintGroupBuilder<'ty> { .map(|tp| tp.into_inner().token_program), } } else if let Some(d) = &mint_decimals { - let init_transfer_fee = extension_transfer_fee_config_authority.is_some() || - extension_transfer_fee_withheld_authority.is_some() || - extension_transfer_fee_basis_points.is_some() || - extension_transfer_fee_max_fee.is_some(); + let init_transfer_fee = extension_transfer_fee_config_authority + .is_some() + || extension_transfer_fee_withheld_authority.is_some() + || extension_transfer_fee_basis_points.is_some() + || extension_transfer_fee_max_fee.is_some(); InitKind::Mint { decimals: d.clone().into_inner().decimals, owner: match &mint_authority { @@ -1204,34 +1212,59 @@ impl<'ty> ConstraintGroupBuilder<'ty> { transfer_hook_program_id: extension_transfer_hook_program_id .map(|thpid| thpid.into_inner().program_id), non_transferable: extension_non_transferable.map(|_| ()), - transfer_fee_config_authority: extension_transfer_fee_config_authority.map(|tfca| tfca.into_inner().authority), - transfer_fee_withheld_authority: extension_transfer_fee_withheld_authority.map(|tfwa| tfwa.into_inner().authority), - transfer_fee_basis_points: match (&extension_transfer_fee_basis_points, init_transfer_fee) { + transfer_fee_config_authority: + extension_transfer_fee_config_authority + .map(|tfca| tfca.into_inner().authority), + transfer_fee_withheld_authority: + extension_transfer_fee_withheld_authority + .map(|tfwa| tfwa.into_inner().authority), + transfer_fee_basis_points: match ( + &extension_transfer_fee_basis_points, + init_transfer_fee, + ) { (Some(tfbp), _) => Some(tfbp.clone().into_inner().basis_points), - (None, true) => return Err(ParseError::new( - d.span(), - "fee basis points must be provided to initialize a mint with transfer fee extension" - )), - _ => None + (None, true) => { + return Err(ParseError::new( + d.span(), + "fee basis points must be provided to initialize a \ + mint with transfer fee extension", + )) + } + _ => None, }, - transfer_fee_max_fee: match (&extension_transfer_fee_max_fee, init_transfer_fee) { + transfer_fee_max_fee: match ( + &extension_transfer_fee_max_fee, + init_transfer_fee, + ) { (Some(tfmf), _) => Some(tfmf.clone().into_inner().max_fee), - (None, true) => return Err(ParseError::new( - d.span(), - "maximum fee must be provided to initialize a mint with transfer fee extension" - )), - _ => None + (None, true) => { + return Err(ParseError::new( + d.span(), + "maximum fee must be provided to initialize a mint \ + with transfer fee extension", + )) + } + _ => None, }, - interest_bearing_authority: extension_interest_bearing_authority.as_ref().map(|iba| iba.clone().into_inner().authority), - interest_bearing_rate: match (&extension_interest_bearing_rate, &extension_interest_bearing_authority) { + interest_bearing_authority: extension_interest_bearing_authority + .as_ref() + .map(|iba| iba.clone().into_inner().authority), + interest_bearing_rate: match ( + &extension_interest_bearing_rate, + &extension_interest_bearing_authority, + ) { (Some(ibr), _) => Some(ibr.clone().into_inner().rate), - (None, Some(_)) => return Err(ParseError::new( - d.span(), - "rate must be provided to initialize a mint with interest bearing extension" - )), - _ => None + (None, Some(_)) => { + return Err(ParseError::new( + d.span(), + "rate must be provided to initialize a mint with \ + interest bearing extension", + )) + } + _ => None, }, - default_account_state: extension_default_account_state.map(|das| das.into_inner().state), + default_account_state: extension_default_account_state + .map(|das| das.into_inner().state), } } else { InitKind::Program { @@ -1337,9 +1370,15 @@ impl<'ty> ConstraintGroupBuilder<'ty> { ConstraintToken::ExtensionTransferFeeMaxFee(c) => { self.add_extension_transfer_fee_max_fee(c) } - ConstraintToken::ExtensionInterestBearingRateAuthority(c) => self.add_extension_interest_bearing_authority(c), - ConstraintToken::ExtensionInterestBearingRate(c) => self.add_extension_interest_bearing_rate(c), - ConstraintToken::ExtensionDefaultAccountState(c) => self.add_extension_default_account_state(c), + ConstraintToken::ExtensionInterestBearingRateAuthority(c) => { + self.add_extension_interest_bearing_authority(c) + } + ConstraintToken::ExtensionInterestBearingRate(c) => { + self.add_extension_interest_bearing_rate(c) + } + ConstraintToken::ExtensionDefaultAccountState(c) => { + self.add_extension_default_account_state(c) + } } } @@ -2043,5 +2082,4 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.extension_default_account_state.replace(c); Ok(()) } - }