diff --git a/CHANGELOG.md b/CHANGELOG.md index c93f47e583..d04f20bd2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ The minor version will be incremented upon a breaking change and the patch versi - cli: Add short alias for the `idl build` command ([#3283](https://github.com/coral-xyz/anchor/pull/3283)). - cli: Add `--program-id` option to `idl convert` command ([#3309](https://github.com/coral-xyz/anchor/pull/3309)). - lang: Generate documentation of constants in `declare_program!` ([#3311](https://github.com/coral-xyz/anchor/pull/3311)). +- spl: Add 'Interest Bearing Config` Extension ([#3278](https://github.com/coral-xyz/anchor/pull/3278)). - cli: Add support for fetching legacy IDLs ([#3324](https://github.com/coral-xyz/anchor/pull/3324)). - avm: Add short alias for `install` and `list` commands ([#3326](https://github.com/coral-xyz/anchor/pull/3326)). - avm: Add Windows support for renaming anchor binary ([#3325](https://github.com/coral-xyz/anchor/pull/3325)). diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index f210a0680e..e311cf9b4c 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -688,6 +688,8 @@ fn generate_constraint_init_group( metadata_pointer_metadata_address, close_authority, permanent_delegate, + interest_bearing_mint_rate, + interest_bearing_mint_authority, transfer_hook_authority, transfer_hook_program_id, } => { @@ -805,6 +807,10 @@ fn generate_constraint_init_group( extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::PermanentDelegate}); } + if interest_bearing_mint_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 { @@ -864,6 +870,18 @@ fn generate_constraint_init_group( None => quote! { Option::<&anchor_lang::prelude::Pubkey>::None }, }; + let interest_bearing_mint_rate = match interest_bearing_mint_rate { + Some(ibmr) => quote! { Option::::Some(#ibmr) }, + None => quote! { Option::::None }, + }; + + let interest_bearing_mint_authority = match interest_bearing_mint_authority { + Some(ibma) => { + quote! { Option::::Some(#ibma.key()) } + } + None => quote! { Option::::None }, + }; + let transfer_hook_authority = match transfer_hook_authority { Some(tha) => quote! { Option::::Some(#tha.key()) }, None => quote! { Option::::None }, @@ -946,6 +964,12 @@ fn generate_constraint_init_group( mint: #field.to_account_info(), }), #permanent_delegate.unwrap())?; }, + ::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::InterestBearingConfig => { + ::anchor_spl::token_interface::interest_bearing_mint_initialize(anchor_lang::context::CpiContext::new(#token_program.to_account_info(), ::anchor_spl::token_interface::InterestBearingMintInitialize { + token_program_id: #token_program.to_account_info(), + mint: #field.to_account_info(), + }), #interest_bearing_mint_authority, #interest_bearing_mint_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:?}"), diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 33bc08b7cd..0e086d3d75 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -783,6 +783,8 @@ pub enum ConstraintToken { ExtensionTokenHookAuthority(Context), ExtensionTokenHookProgramId(Context), ExtensionPermanentDelegate(Context), + ExtensionInterestBearingMintRate(Context), + ExtensionInterestBearingMintAuthority(Context), } impl Parse for ConstraintToken { @@ -905,6 +907,11 @@ pub struct ConstraintExtensionAuthority { pub authority: Expr, } +#[derive(Debug, Clone)] +pub struct ConstraintBool { + pub enable: Expr, +} + #[derive(Debug, Clone)] pub struct ConstraintExtensionGroupPointerGroupAddress { pub group_address: Expr, @@ -930,6 +937,11 @@ pub struct ConstraintExtensionPermanentDelegate { pub permanent_delegate: Expr, } +#[derive(Debug, Clone)] +pub struct ConstraintExtensionInterestBearingMintRate { + pub rate: Expr, +} + #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub enum InitKind { @@ -965,6 +977,8 @@ pub enum InitKind { metadata_pointer_metadata_address: Option, close_authority: Option, permanent_delegate: Option, + interest_bearing_mint_rate: Option, + interest_bearing_mint_authority: Option, transfer_hook_authority: Option, transfer_hook_program_id: Option, }, diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index 936cf2057b..6734603592 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -230,6 +230,35 @@ pub fn parse_token(stream: ParseStream) -> ParseResult { _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), } } + "interest_bearing_mint" => { + 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() { + "rate" => ConstraintToken::ExtensionInterestBearingMintRate(Context::new( + span, + ConstraintExtensionInterestBearingMintRate { + rate: stream.parse()?, + }, + )), + "authority" => { + ConstraintToken::ExtensionInterestBearingMintAuthority(Context::new( + span, + ConstraintExtensionAuthority { + authority: stream.parse()?, + }, + )) + } + _ => return Err(ParseError::new(ident.span(), "Invalid attribute")), + } + } "transfer_hook" => { stream.parse::()?; stream.parse::()?; @@ -525,6 +554,7 @@ pub struct ConstraintGroupBuilder<'ty> { pub mint_freeze_authority: Option>, pub mint_decimals: Option>, pub mint_token_program: Option>, + // mint extensions. pub extension_group_pointer_authority: Option>, pub extension_group_pointer_group_address: Option>, @@ -538,6 +568,9 @@ pub struct ConstraintGroupBuilder<'ty> { pub extension_transfer_hook_authority: Option>, pub extension_transfer_hook_program_id: Option>, pub extension_permanent_delegate: Option>, + pub extension_interest_bearing_mint_rate: + Option>, + pub extension_interest_bearing_mint_authority: Option>, pub bump: Option>, pub program_seed: Option>, pub realloc: Option>, @@ -583,6 +616,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_hook_authority: None, extension_transfer_hook_program_id: None, extension_permanent_delegate: None, + extension_interest_bearing_mint_rate: None, + extension_interest_bearing_mint_authority: None, bump: None, program_seed: None, realloc: None, @@ -795,6 +830,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_hook_authority, extension_transfer_hook_program_id, extension_permanent_delegate, + extension_interest_bearing_mint_rate, + extension_interest_bearing_mint_authority, bump, program_seed, realloc, @@ -975,6 +1012,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { )), }, token_program: token_token_program.map(|tp| tp.into_inner().token_program), + } } else if let Some(at) = &associated_token { InitKind::AssociatedToken { @@ -1003,6 +1041,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { metadata_pointer_metadata_address: extension_metadata_pointer_metadata_address.map(|mpma| mpma.into_inner().metadata_address), close_authority: extension_close_authority.map(|ca| ca.into_inner().authority), permanent_delegate: extension_permanent_delegate.map(|pd| pd.into_inner().permanent_delegate), + interest_bearing_mint_rate: extension_interest_bearing_mint_rate.map(|ibmr| ibmr.into_inner().rate), + interest_bearing_mint_authority: extension_interest_bearing_mint_authority.map(|ibma| ibma.into_inner().authority), transfer_hook_authority: extension_transfer_hook_authority.map(|tha| tha.into_inner().authority), transfer_hook_program_id: extension_transfer_hook_program_id.map(|thpid| thpid.into_inner().program_id), } @@ -1093,6 +1133,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> { ConstraintToken::ExtensionPermanentDelegate(c) => { self.add_extension_permanent_delegate(c) } + ConstraintToken::ExtensionInterestBearingMintRate(c) => { + self.add_extension_interest_bearing_mint_rate(c) + } + ConstraintToken::ExtensionInterestBearingMintAuthority(c) => { + self.add_extension_interest_bearing_mint_authority(c) + } } } @@ -1675,4 +1721,32 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.extension_permanent_delegate.replace(c); Ok(()) } + + fn add_extension_interest_bearing_mint_rate( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_interest_bearing_mint_rate.is_some() { + return Err(ParseError::new( + c.span(), + "extension interest bearing mint rate already provided", + )); + } + self.extension_interest_bearing_mint_rate.replace(c); + Ok(()) + } + + fn add_extension_interest_bearing_mint_authority( + &mut self, + c: Context, + ) -> ParseResult<()> { + if self.extension_interest_bearing_mint_authority.is_some() { + return Err(ParseError::new( + c.span(), + "extension interest bearing mint rate authority already provided", + )); + } + self.extension_interest_bearing_mint_authority.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 a721120dc8..624461eb5b 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, interest_bearing_mint::{InterestBearingConfig, BasisPoints}, }, token_interface::{ get_mint_extension_data, spl_token_metadata_interface::state::TokenMetadata, @@ -53,6 +53,8 @@ pub struct CreateMintAccount<'info> { extensions::transfer_hook::program_id = crate::ID, extensions::close_authority::authority = authority, extensions::permanent_delegate::delegate = authority, + extensions::interest_bearing_mint::authority = authority, + extensions::interest_bearing_mint::rate = 100, )] pub mint: Box>, #[account( @@ -150,6 +152,12 @@ pub fn handler(ctx: Context, args: CreateMintAccountArgs) -> group_member_pointer.member_address, OptionalNonZeroPubkey::try_from(mint_key)? ); + let interest_bearing_config = get_mint_extension_data::(mint_data)?; + assert_eq!( + interest_bearing_config.rate_authority, + OptionalNonZeroPubkey::try_from(authority_key)? + ); + assert_eq!(interest_bearing_config.current_rate, BasisPoints::from(100)); // transfer minimum rent to mint account update_account_lamports_to_minimum_balance( ctx.accounts.mint.to_account_info(),