Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)).

### Fixes

Expand Down
24 changes: 24 additions & 0 deletions lang/syn/src/codegen/accounts/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,8 @@ fn generate_constraint_init_group(
metadata_pointer_metadata_address,
close_authority,
permanent_delegate,
interest_bearing_mint_rate,
Comment thread
jamie-osec marked this conversation as resolved.
interest_bearing_mint_authority,
transfer_hook_authority,
transfer_hook_program_id,
pausable_authority,
Expand Down Expand Up @@ -911,6 +913,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() {
Comment thread
jamie-osec marked this conversation as resolved.
Outdated
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 {
Expand Down Expand Up @@ -970,6 +976,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::<i16>::Some(#ibmr) },
None => quote! { Option::<i16>::None },
};

let interest_bearing_mint_authority = match interest_bearing_mint_authority {
Some(ibma) => {
quote! { Option::<anchor_lang::prelude::Pubkey>::Some(#ibma.key()) }
}
None => quote! { Option::<anchor_lang::prelude::Pubkey>::None },
};

let transfer_hook_authority = match transfer_hook_authority {
Some(tha) => quote! { Option::<anchor_lang::prelude::Pubkey>::Some(#tha.key()) },
None => quote! { Option::<anchor_lang::prelude::Pubkey>::None },
Expand Down Expand Up @@ -1067,6 +1085,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.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:?}"),
Expand Down
9 changes: 9 additions & 0 deletions lang/syn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,8 @@ pub enum ConstraintToken {
ExtensionTokenHookProgramId(Context<ConstraintExtensionTokenHookProgramId>),
ExtensionPermanentDelegate(Context<ConstraintExtensionPermanentDelegate>),
ExtensionPausableAuthority(Context<ConstraintExtensionAuthority>),
ExtensionInterestBearingMintRate(Context<ConstraintExtensionInterestBearingMintRate>),
ExtensionInterestBearingMintAuthority(Context<ConstraintExtensionAuthority>),
}

impl Parse for ConstraintToken {
Expand Down Expand Up @@ -1097,6 +1099,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 {
Expand Down Expand Up @@ -1132,6 +1139,8 @@ pub enum InitKind {
metadata_pointer_metadata_address: Option<Expr>,
close_authority: Option<Expr>,
permanent_delegate: Option<Expr>,
interest_bearing_mint_rate: Option<Expr>,
interest_bearing_mint_authority: Option<Expr>,
transfer_hook_authority: Option<Expr>,
transfer_hook_program_id: Option<Expr>,
pausable_authority: Option<Expr>,
Expand Down
75 changes: 75 additions & 0 deletions lang/syn/src/parser/accounts/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,35 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
_ => return Err(ParseError::new(ident.span(), "Invalid attribute")),
}
}
"interest_bearing_mint" => {
stream.parse::<Token![:]>()?;
stream.parse::<Token![:]>()?;
let kw = stream.call(Ident::parse_any)?.to_string();
stream.parse::<Token![=]>()?;

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::<Token![:]>()?;
stream.parse::<Token![:]>()?;
Expand Down Expand Up @@ -566,6 +595,9 @@ pub struct ConstraintGroupBuilder<'ty> {
pub extension_transfer_hook_program_id: Option<Context<ConstraintExtensionTokenHookProgramId>>,
pub extension_permanent_delegate: Option<Context<ConstraintExtensionPermanentDelegate>>,
pub extension_pausable_authority: Option<Context<ConstraintExtensionAuthority>>,
pub extension_interest_bearing_mint_rate:
Option<Context<ConstraintExtensionInterestBearingMintRate>>,
pub extension_interest_bearing_mint_authority: Option<Context<ConstraintExtensionAuthority>>,
pub bump: Option<Context<ConstraintTokenBump>>,
pub program_seed: Option<Context<ConstraintProgramSeed>>,
pub realloc: Option<Context<ConstraintRealloc>>,
Expand Down Expand Up @@ -613,6 +645,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,
Expand Down Expand Up @@ -831,6 +865,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,
Expand Down Expand Up @@ -1087,6 +1123,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 {
Expand Down Expand Up @@ -1190,6 +1231,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)
}
}
}

Expand Down Expand Up @@ -1795,4 +1842,32 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
self.extension_pausable_authority.replace(c);
Ok(())
}

fn add_extension_interest_bearing_mint_rate(
&mut self,
c: Context<ConstraintExtensionInterestBearingMintRate>,
) -> 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<ConstraintExtensionAuthority>,
) -> ParseResult<()> {
if self.extension_interest_bearing_mint_authority.is_some() {
return Err(ParseError::new(
c.span(),
"extension interest bearing mint rate authority already provided",
Comment thread
jamie-osec marked this conversation as resolved.
Outdated
));
}
self.extension_interest_bearing_mint_authority.replace(c);
Ok(())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ use {
anchor_spl::{
associated_token::AssociatedToken,
token_2022::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,
token_interface::{
Expand Down Expand Up @@ -56,6 +60,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<InterfaceAccount<'info, Mint>>,
#[account(
Expand Down Expand Up @@ -153,6 +159,12 @@ pub fn handler(ctx: Context<CreateMintAccount>, args: CreateMintAccountArgs) ->
group_member_pointer.member_address,
OptionalNonZeroPubkey::try_from(mint_key)?
);
let interest_bearing_config = get_mint_extension_data::<InterestBearingConfig>(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(),
Expand Down
15 changes: 15 additions & 0 deletions tests/spl/token-extensions/tests/token-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
createMint,
ExtensionType,
getAccountLen,
getInterestBearingMintConfigState,
getMint,
} from "@solana/spl-token";
import { it } from "node:test";

Expand Down Expand Up @@ -84,6 +86,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()
Expand Down
Loading