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)).
- 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)).
Expand Down
10 changes: 10 additions & 0 deletions lang/error/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
99 changes: 99 additions & 0 deletions lang/syn/src/codegen/accounts/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,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 @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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() {
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 @@ -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::<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 @@ -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:?}"),
Expand Down Expand Up @@ -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)
}})()?;
Expand Down Expand Up @@ -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
Expand All @@ -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<String>,
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 @@ -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 {
Expand Down Expand Up @@ -1128,6 +1135,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 @@ -1250,6 +1259,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 @@ -582,6 +611,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 @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -973,6 +1011,8 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
None,
None,
None,
None,
None,
) => None,
_ => Some(ConstraintTokenMintGroup {
decimals: mint_decimals
Expand Down Expand Up @@ -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),
}),
};

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -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<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(())
}
}

#[cfg(test)]
Expand Down
Loading
Loading