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
10 changes: 10 additions & 0 deletions lang/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,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")]
Expand Down
75 changes: 75 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 @@ -855,6 +857,16 @@ 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 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);
Expand All @@ -876,6 +888,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);
Expand Down Expand Up @@ -911,6 +925,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() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I understand it correctly if only rate supplied, token-2022 cannot change a rate without an authority right? — so the rate is permanently locked at initial setup value? was it intended design?

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 +988,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::<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 +1097,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:?}"),
Expand Down Expand Up @@ -1646,6 +1682,44 @@ fn generate_constraint_mint(
None => quote! {},
};

let interest_bearing_check = match (
&c.interest_bearing_mint_rate,
&c.interest_bearing_mint_authority,
) {
(None, None) => quote! {},
(rate, authority) => {
let rate_check = match rate {
Some(rate) => quote! {
if i16::from(interest_bearing.current_rate) != #rate {
return Err(anchor_lang::error::ErrorCode::ConstraintMintInterestBearingRate.into());
}
},
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::ErrorCode::ConstraintMintInterestBearingAuthority.into());
}
}
}
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);
if interest_bearing.is_err() {
return Err(anchor_lang::error::ErrorCode::ConstraintMintInterestBearingExtension.into());
}
let interest_bearing = interest_bearing.unwrap();
#rate_check
#authority_check
}
}
};

quote! {
{
#decimal_check
Expand All @@ -1663,6 +1737,7 @@ fn generate_constraint_mint(
#transfer_hook_authority_check
#transfer_hook_program_id_check
#pausable_authority_check
#interest_bearing_check
}
}
}
Expand Down
11 changes: 11 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 Expand Up @@ -1254,6 +1263,8 @@ pub struct ConstraintTokenMintGroup {
pub transfer_hook_authority: Option<Expr>,
pub transfer_hook_program_id: Option<Expr>,
pub pausable_authority: Option<Expr>,
pub interest_bearing_mint_rate: Option<Expr>,
pub interest_bearing_mint_authority: Option<Expr>,
}

// Syntax context object for preserving metadata about the inner item.
Expand Down
85 changes: 85 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 @@ -940,6 +976,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,
Expand All @@ -957,6 +995,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
None,
None,
None,
None,
None,
) => None,
_ => Some(ConstraintTokenMintGroup {
decimals: mint_decimals
Expand Down Expand Up @@ -1005,6 +1045,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),
}),
};

Expand Down Expand Up @@ -1087,6 +1133,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 +1241,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 +1852,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 authority already provided",
));
}
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
Loading