Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,8 @@ cluster = "localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "../node_modules/.bin/mocha --import=tsx -t 1000000 tests/**/*.ts"
# cargo test runs both the tx_hook.rs unit tests and the litesvm
# integration test - CI's Anchor workflow only runs this [scripts] test
# command (never a separate `cargo test`), so without this the Rust-side
# tests were never actually executed.
test = "cargo test -p abl-token && ../node_modules/.bin/mocha --import=tsx -t 1000000 tests/**/*.ts"
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ spl-discriminator = "0.5.1"

[dev-dependencies]
litesvm = "0.11.0"
solana-account = "3.2.0"
solana-instruction = "3.0.0"
solana-keypair = "3.0.1"
solana-message = "3.1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use anchor_spl::{
Token2022,
},
token_interface::{
spl_token_metadata_interface::state::Field, token_metadata_update_field,
Mint as MintAccount, TokenMetadataUpdateField,
spl_token_metadata_interface::state::Field, token_metadata_update_field, Mint as MintAccount,
TokenMetadataUpdateField,
},
};

Expand Down Expand Up @@ -50,11 +50,7 @@ impl ChangeMode<'_> {
token_metadata_update_field(cpi_ctx, Field::Key("AB".to_string()), args.mode.to_string())?;

if args.mode == Mode::Mixed || self.has_threshold()? {
let threshold = if args.mode == Mode::Mixed {
args.threshold
} else {
0
};
let threshold = if args.mode == Mode::Mixed { args.threshold } else { 0 };

let cpi_accounts = TokenMetadataUpdateField {
metadata: self.mint.to_account_info(),
Expand All @@ -64,11 +60,7 @@ impl ChangeMode<'_> {
let cpi_program = self.token_program.key();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);

token_metadata_update_field(
cpi_ctx,
Field::Key("threshold".to_string()),
threshold.to_string(),
)?;
token_metadata_update_field(cpi_ctx, Field::Key("threshold".to_string()), threshold.to_string())?;
}

let data = self.mint.to_account_info().data_len();
Expand All @@ -80,11 +72,7 @@ impl ChangeMode<'_> {
&self.mint.to_account_info().key(),
min_balance - self.mint.to_account_info().get_lamports(),
),
&[
self.authority.to_account_info(),
self.mint.to_account_info(),
self.system_program.to_account_info(),
],
&[self.authority.to_account_info(), self.mint.to_account_info(), self.system_program.to_account_info()],
)?;
}

Expand All @@ -96,11 +84,6 @@ impl ChangeMode<'_> {
let mint_data = mint_info.data.borrow();
let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
let metadata = mint.get_variable_len_extension::<TokenMetadata>();
Ok(metadata.is_ok()
&& metadata
.unwrap()
.additional_metadata
.iter()
.any(|(key, _)| key == "threshold"))
Ok(metadata.is_ok() && metadata.unwrap().additional_metadata.iter().any(|(key, _)| key == "threshold"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ pub struct InitConfig<'info> {

impl InitConfig<'_> {
pub fn init_config(&mut self, config_bump: u8) -> Result<()> {
self.config.set_inner(Config {
authority: self.payer.key(),
bump: config_bump,
});
self.config.set_inner(Config { authority: self.payer.key(), bump: config_bump });

Ok(())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
use anchor_lang::{
prelude::*, solana_program::program::invoke, solana_program::system_instruction::transfer,
};
use anchor_lang::{prelude::*, solana_program::program::invoke, solana_program::system_instruction::transfer};
use anchor_spl::{
token_2022::Token2022,
token_interface::{
spl_token_metadata_interface::state::Field, token_metadata_initialize,
token_metadata_update_field, Mint, TokenMetadataInitialize, TokenMetadataUpdateField,
spl_token_metadata_interface::state::Field, token_metadata_initialize, token_metadata_update_field, Mint,
TokenMetadataInitialize, TokenMetadataUpdateField,
},
};

Expand Down Expand Up @@ -80,11 +78,7 @@ impl InitMint<'_> {
};
let cpi_ctx = CpiContext::new(self.token_program.key(), cpi_accounts);

token_metadata_update_field(
cpi_ctx,
Field::Key("threshold".to_string()),
args.threshold.to_string(),
)?;
token_metadata_update_field(cpi_ctx, Field::Key("threshold".to_string()), args.threshold.to_string())?;
}

let data = self.mint.to_account_info().data_len();
Expand All @@ -96,11 +90,7 @@ impl InitMint<'_> {
&self.mint.to_account_info().key(),
min_balance - self.mint.to_account_info().get_lamports(),
),
&[
self.payer.to_account_info(),
self.mint.to_account_info(),
self.system_program.to_account_info(),
],
&[self.payer.to_account_info(), self.mint.to_account_info(), self.system_program.to_account_info()],
)?;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod init_config;
pub mod init_mint;
pub mod init_wallet;
pub mod remove_wallet;
pub mod resize_meta_list;
pub mod tx_hook;

pub use attach_to_mint::*;
Expand All @@ -12,4 +13,5 @@ pub use init_config::*;
pub use init_mint::*;
pub use init_wallet::*;
pub use remove_wallet::*;
pub use resize_meta_list::*;
pub use tx_hook::*;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use anchor_lang::{prelude::*, solana_program::program::invoke, solana_program::system_instruction::transfer};
use anchor_spl::{
token_2022::Token2022,
token_interface::{transfer_hook_update, Mint, TransferHookUpdate},
};

use spl_tlv_account_resolution::state::ExtraAccountMetaList;
use spl_transfer_hook_interface::instruction::ExecuteInstruction;

use crate::{get_extra_account_metas, get_meta_list_size, META_LIST_ACCOUNT_SEED};

/// Rewrites an existing mint's extra-metas account to the current
/// `get_extra_account_metas()` layout, reallocating it if the size changed.
///
/// This exists because `extra_metas_account` is a fixed-size PDA created once
/// by `init_mint`/`attach_to_mint`: if the program's extra-account list is
/// ever extended (e.g. to add the source-wallet check), mints that were set
/// up under the old layout are left with a stale, undersized account and
/// their transfers start failing the hook's account-count check. Any mint
/// authority can call this to bring an existing mint's extra-metas account
/// back in sync after such an upgrade.
#[derive(Accounts)]
pub struct ResizeMetaList<'info> {
#[account(mut)]
pub payer: Signer<'info>,

#[account(mut, mint::token_program = token_program)]
pub mint: Box<InterfaceAccount<'info, Mint>>,

#[account(
mut,
seeds = [META_LIST_ACCOUNT_SEED, mint.key().as_ref()],
bump,
)]
/// CHECK: extra metas account
pub extra_metas_account: UncheckedAccount<'info>,

pub system_program: Program<'info, System>,

pub token_program: Program<'info, Token2022>,
}

impl ResizeMetaList<'_> {
pub fn resize_meta_list(&mut self) -> Result<()> {
// Re-setting the transfer hook to itself has no effect on the mint,
// but the CPI only succeeds if `payer` is the mint's current
// transfer-hook authority - the same check `attach_to_mint` relies
// on, reused here so this instruction can't be called by anyone
// other than whoever is already trusted to configure this mint's hook.
let tx_hook_accs = TransferHookUpdate {
token_program_id: self.token_program.to_account_info(),
mint: self.mint.to_account_info(),
authority: self.payer.to_account_info(),
};
let ctx = CpiContext::new(self.token_program.key(), tx_hook_accs);
transfer_hook_update(ctx, Some(crate::ID_CONST))?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Revoked authority blocks migration

If an existing mint revoked its transfer-hook authority or configured an authority that cannot sign directly, resize_meta_list always fails at transfer_hook_update before rewriting the deterministic program-owned metadata account. The mint therefore remains on the incompatible one-entry layout, causing every transfer through the upgraded hook to fail with no recovery path.

Knowledge Base Used: Tokens Directory Overview


let account_info = self.extra_metas_account.to_account_info();
let new_size = get_meta_list_size()?;

let min_balance = Rent::get()?.minimum_balance(new_size);
if min_balance > account_info.lamports() {
invoke(
&transfer(&self.payer.key(), account_info.key, min_balance - account_info.lamports()),
&[self.payer.to_account_info(), account_info.clone(), self.system_program.to_account_info()],
)?;
}
account_info.resize(new_size)?;

let metas = get_extra_account_metas()?;
let mut data = account_info.try_borrow_mut_data()?;
ExtraAccountMetaList::update::<ExecuteInstruction>(&mut data, &metas)
.map_err(|_| ProgramError::InvalidAccountData)?;

Ok(())
}
}
Loading
Loading