diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd3a9ed6f..5801abc8b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The minor version will be incremented upon a breaking change and the patch versi - lang: Add `program_id` verification to CPI return values ([#4411](https://github.com/solana-foundation/anchor/pull/4411)). - spl: Add pausable mint extension support ([#4092](https://github.com/solana-foundation/anchor/pull/4092)). - cli: Resolve the target directory via `cargo metadata` to support target directory overrides ([#3817](https://github.com/solana-foundation/anchor/pull/3817)). +- spl: Add `Interest Bearing Config` Extension ([#4464](https://github.com/solana-foundation/anchor/pull/4464)). - spl: Added `token_metadata_remove_key` to support removing keys from token metadata extension ([#3717](https://github.com/solana-foundation/anchor/pull/3717)). - lang: Derive `Clone`, `Debug`, `Copy`, and `Default` on generated client / CPI account structs and instruction args where the field types allow it ([#4085](https://github.com/solana-foundation/anchor/pull/4085)). - lang: Add `AccountLoader::new_unchecked` for constructing an `AccountLoader` without performing owner or discriminator checks ([#4162](https://github.com/solana-foundation/anchor/pull/4162)). diff --git a/lang/error/src/lib.rs b/lang/error/src/lib.rs index 4279b5a890..050cd19699 100644 --- a/lang/error/src/lib.rs +++ b/lang/error/src/lib.rs @@ -199,6 +199,16 @@ pub enum ErrorCode { #[msg("A pausable extension authority constraint was violated")] ConstraintMintPausableAuthority, + /// 2045 - An interest-bearing extension constraint was violated + #[msg("An interest-bearing extension constraint was violated")] + ConstraintMintInterestBearingExtension, + /// 2046 - An interest-bearing extension authority constraint was violated + #[msg("An interest-bearing extension authority constraint was violated")] + ConstraintMintInterestBearingAuthority, + /// 2047 - An interest-bearing extension rate constraint was violated + #[msg("An interest-bearing extension rate constraint was violated")] + ConstraintMintInterestBearingRate, + // Require /// 2500 - A require expression was violated #[msg("A require expression was violated")] diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index 4eb69fd734..505462dc86 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -784,6 +784,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, pausable_authority, @@ -856,6 +858,24 @@ fn generate_constraint_init_group( None => quote! {}, }; + let interest_bearing_mint_rate_check = match interest_bearing_mint_rate { + Some(r) => check_scope.generate_check(r), + None => quote! {}, + }; + + let interest_bearing_mint_authority_check = match interest_bearing_mint_authority { + Some(a) => check_scope.generate_check(a), + None => quote! {}, + }; + + let if_needed_interest_bearing_check = generate_interest_bearing_extension_check( + &account_ref, + &name_str, + interest_bearing_mint_rate.as_ref(), + interest_bearing_mint_authority.as_ref(), + &mut check_scope, + ); + 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); @@ -877,6 +897,8 @@ fn generate_constraint_init_group( #transfer_hook_program_id_check #permanent_delegate_check #pausable_authority_check + #interest_bearing_mint_rate_check + #interest_bearing_mint_authority_check }; let payer_optional_check = check_scope.generate_check(payer); @@ -912,6 +934,10 @@ fn generate_constraint_init_group( extensions.push(quote! {::anchor_spl::token_interface::spl_token_2022::extension::ExtensionType::Pausable}) } + if interest_bearing_mint_rate.is_some() || interest_bearing_mint_authority.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 { @@ -971,6 +997,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! { #ibmr }, + None => quote! { 0i16 }, + }; + + 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 }, @@ -1068,6 +1106,12 @@ fn generate_constraint_init_group( mint: #field.to_account_info(), }), #pausable_authority.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_mint_authority, #interest_bearing_mint_rate)?; + }, // All extensions specified by the user should be implemented. // If this line runs, it means there is a bug in the codegen. _ => unimplemented!("{e:?}"), @@ -1100,6 +1144,7 @@ fn generate_constraint_init_group( if owner_program != &#token_program.key() { return Err(anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::ConstraintMintTokenProgram).with_account_name(#name_str).with_pubkeys((*owner_program, #token_program.key()))); } + #if_needed_interest_bearing_check } Ok(pa) }})()?; @@ -1647,6 +1692,14 @@ fn generate_constraint_mint( None => quote! {}, }; + let interest_bearing_check = generate_interest_bearing_extension_check( + &account_ref, + &name.to_string(), + c.interest_bearing_mint_rate.as_ref(), + c.interest_bearing_mint_authority.as_ref(), + &mut optional_check_scope, + ); + quote! { { #decimal_check @@ -1664,10 +1717,56 @@ fn generate_constraint_mint( #transfer_hook_authority_check #transfer_hook_program_id_check #pausable_authority_check + #interest_bearing_check } } } +fn generate_interest_bearing_extension_check( + account_ref: &proc_macro2::TokenStream, + name_str: &str, + rate: Option<&Expr>, + authority: Option<&Expr>, + optional_check_scope: &mut OptionalCheckScope, +) -> proc_macro2::TokenStream { + if rate.is_none() && authority.is_none() { + return quote! {}; + } + let rate_check = match rate { + Some(rate) => quote! { + if i16::from(interest_bearing.current_rate) != #rate { + return Err(anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMintInterestBearingRate, + ).with_account_name(#name_str)); + } + }, + None => quote! {}, + }; + let authority_check = match authority { + Some(authority) => { + let authority_optional_check = optional_check_scope.generate_check(authority); + quote! { + #authority_optional_check + if interest_bearing.rate_authority != ::anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey::try_from(Some(#authority.key()))? { + return Err(anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMintInterestBearingAuthority, + ).with_account_name(#name_str)); + } + } + } + None => quote! {}, + }; + 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).map_err(|_| anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::ConstraintMintInterestBearingExtension, + ).with_account_name(#name_str))?; + #rate_check + #authority_check + } +} + #[derive(Clone, Debug)] pub struct OptionalCheckScope<'a> { seen: HashSet, diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index 8665f8482e..669e158e60 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -857,6 +857,8 @@ pub enum ConstraintToken { ExtensionTokenHookProgramId(Context), ExtensionPermanentDelegate(Context), ExtensionPausableAuthority(Context), + ExtensionInterestBearingMintRate(Context), + ExtensionInterestBearingMintAuthority(Context), } impl Parse for ConstraintToken { @@ -1093,6 +1095,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 { @@ -1128,6 +1135,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, pausable_authority: Option, @@ -1250,6 +1259,8 @@ pub struct ConstraintTokenMintGroup { pub transfer_hook_authority: Option, pub transfer_hook_program_id: Option, pub pausable_authority: Option, + pub interest_bearing_mint_rate: Option, + pub interest_bearing_mint_authority: 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 2be18e8b30..444994db95 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -235,6 +235,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::()?; @@ -582,6 +611,9 @@ pub struct ConstraintGroupBuilder<'ty> { pub extension_transfer_hook_program_id: Option>, pub extension_permanent_delegate: Option>, pub extension_pausable_authority: 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>, @@ -629,6 +661,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_hook_program_id: None, extension_permanent_delegate: None, extension_pausable_authority: None, + extension_interest_bearing_mint_rate: None, + extension_interest_bearing_mint_authority: None, bump: None, program_seed: None, realloc: None, @@ -847,6 +881,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { extension_transfer_hook_program_id, extension_permanent_delegate, extension_pausable_authority, + extension_interest_bearing_mint_rate, + extension_interest_bearing_mint_authority, bump, program_seed, realloc, @@ -956,6 +992,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { &extension_transfer_hook_program_id, &extension_permanent_delegate, &extension_pausable_authority, + &extension_interest_bearing_mint_rate, + &extension_interest_bearing_mint_authority, ) { ( None, @@ -973,6 +1011,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> { None, None, None, + None, + None, ) => None, _ => Some(ConstraintTokenMintGroup { decimals: mint_decimals @@ -1021,6 +1061,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> { pausable_authority: extension_pausable_authority .as_ref() .map(|a| a.clone().into_inner().authority), + interest_bearing_mint_rate: extension_interest_bearing_mint_rate + .as_ref() + .map(|a| a.clone().into_inner().rate), + interest_bearing_mint_authority: extension_interest_bearing_mint_authority + .as_ref() + .map(|a| a.clone().into_inner().authority), }), }; @@ -1103,6 +1149,11 @@ impl<'ty> ConstraintGroupBuilder<'ty> { .map(|thpid| thpid.into_inner().program_id), pausable_authority: extension_pausable_authority .map(|ca| ca.into_inner().authority), + 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), } } else { InitKind::Program { @@ -1206,6 +1257,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> { ConstraintToken::ExtensionPausableAuthority(c) => { self.add_extension_pausable_authority(c) } + ConstraintToken::ExtensionInterestBearingMintRate(c) => { + self.add_extension_interest_bearing_mint_rate(c) + } + ConstraintToken::ExtensionInterestBearingMintAuthority(c) => { + self.add_extension_interest_bearing_mint_authority(c) + } } } @@ -1811,6 +1868,34 @@ impl<'ty> ConstraintGroupBuilder<'ty> { self.extension_pausable_authority.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 authority already provided", + )); + } + self.extension_interest_bearing_mint_authority.replace(c); + Ok(()) + } } #[cfg(test)] 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 47e3ad4c30..735ce9bd4b 100644 --- a/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs +++ b/tests/spl/token-extensions/programs/token-extensions/src/instructions.rs @@ -9,9 +9,13 @@ use { token_2022::{ self, spl_token_2022::extension::{ - group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, - mint_close_authority::MintCloseAuthority, pausable::PausableConfig, - permanent_delegate::PermanentDelegate, transfer_hook::TransferHook, + group_member_pointer::GroupMemberPointer, + interest_bearing_mint::{BasisPoints, InterestBearingConfig}, + metadata_pointer::MetadataPointer, + mint_close_authority::MintCloseAuthority, + pausable::PausableConfig, + permanent_delegate::PermanentDelegate, + transfer_hook::TransferHook, }, }, token_2022_extensions, @@ -61,6 +65,8 @@ pub struct CreateMintAccount<'info> { extensions::close_authority::authority = authority, extensions::permanent_delegate::delegate = authority, extensions::pausable::authority = authority, + extensions::interest_bearing_mint::authority = authority, + extensions::interest_bearing_mint::rate = 100, )] pub mint: Box>, #[account( @@ -158,6 +164,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(), diff --git a/tests/spl/token-extensions/tests/token-extensions.ts b/tests/spl/token-extensions/tests/token-extensions.ts index 2d3c2c3731..78a555ddfd 100644 --- a/tests/spl/token-extensions/tests/token-extensions.ts +++ b/tests/spl/token-extensions/tests/token-extensions.ts @@ -9,6 +9,7 @@ import { createMint, ExtensionType, getExtensionTypes, + getInterestBearingMintConfigState, getMint, getMintLen, NATIVE_MINT_2022, @@ -80,6 +81,19 @@ describe("token extensions", () => { .rpc(); }); + it("Interest bearing mint extension is initialized on the mint", async () => { + const mintAccount = await getMint( + provider.connection, + mint.publicKey, + "confirmed", + TOKEN_2022_PROGRAM_ID + ); + const config = getInterestBearingMintConfigState(mintAccount); + assert.ok(config !== null); + assert.equal(config!.rateAuthority.toBase58(), payer.publicKey.toBase58()); + assert.equal(config!.currentRate, 100); + }); + it("mint extension constraints test passes", async () => { await program.methods .checkMintExtensionsConstraints()