Skip to content

Commit c5def5d

Browse files
authored
Pointer-aware metadata sync (#234)
* Pointer-aware metadata sync * Simplify ix docs
1 parent 3ba64f3 commit c5def5d

File tree

20 files changed

+947
-167
lines changed

20 files changed

+947
-167
lines changed

Cargo.lock

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
[workspace]
22
resolver = "2"
3-
members = ["clients/cli", "program", "program/tests/programs/test-transfer-hook"]
3+
members = [
4+
"clients/cli",
5+
"program",
6+
"program/tests/programs/test-transfer-hook",
7+
"program/tests/programs/mock-metadata-owner"
8+
]
49

510
[workspace.metadata.cli]
611
solana = "2.3.4"
@@ -34,6 +39,7 @@ anyhow = "1.0.98"
3439
borsh = "0.10.4"
3540
bytemuck = { version = "1.23.2", features = ["derive"] }
3641
clap = { version = "3.2.25", features = ["derive"] }
42+
mock-metadata-owner = { path = "program/tests/programs/mock-metadata-owner" }
3743
mollusk-svm = "0.4.2"
3844
mollusk-svm-programs-token = "0.4.1"
3945
mpl-token-metadata = "5.1.1"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"private": true,
33
"scripts": {
4-
"programs:build": "cargo-build-sbf --manifest-path program/Cargo.toml && cargo-build-sbf --manifest-path program/tests/programs/test-transfer-hook/Cargo.toml",
4+
"programs:build": "cargo-build-sbf --manifest-path program/Cargo.toml && cargo-build-sbf --manifest-path program/tests/programs/test-transfer-hook/Cargo.toml && cargo-build-sbf --manifest-path program/tests/programs/mock-metadata-owner/Cargo.toml",
55
"programs:test": "zx ./scripts/rust/test-sbf.mjs program",
66
"programs:format": "zx ./scripts/rust/format.mjs program",
77
"programs:lint": "zx ./scripts/rust/lint.mjs program",

program/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ solana-instruction = { workspace = true }
2323
solana-msg = { workspace = true }
2424
solana-program-entrypoint = { workspace = true }
2525
solana-program-error = { workspace = true }
26-
solana-program-option = { workspace = true }
2726
solana-program-pack = { workspace = true }
2827
solana-pubkey = { workspace = true }
2928
solana-rent = { workspace = true }
@@ -34,17 +33,18 @@ spl-pod = { workspace = true }
3433
spl-token = { workspace = true }
3534
spl-token-metadata-interface = { workspace = true }
3635
spl-transfer-hook-interface = { workspace = true }
36+
spl-type-length-value = { workspace = true }
3737
thiserror = { workspace = true }
3838

3939
# Should depend on the next crate version after 7.0.0 when https://github.com/solana-program/token-2022/pull/253 is deployed
4040
spl-token-2022 = { workspace = true }
4141

4242
[dev-dependencies]
4343
borsh = { workspace = true }
44+
mock-metadata-owner = { workspace = true }
4445
mollusk-svm = { workspace = true }
4546
mollusk-svm-programs-token = { workspace = true }
4647
solana-account = { workspace = true }
47-
spl-type-length-value = { workspace = true }
4848
spl-tlv-account-resolution = { workspace = true }
4949
test-case = { workspace = true }
5050
test-transfer-hook = { workspace = true }

program/src/error.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ pub enum TokenWrapError {
5050
/// `Metaplex` metadata account address does not match expected PDA
5151
#[error("Metaplex metadata account address does not match expected PDA")]
5252
MetaplexMetadataMismatch,
53+
/// Metadata pointer extension missing on mint
54+
#[error("Metadata pointer extension missing on mint")]
55+
MetadataPointerMissing,
56+
/// Metadata pointer is unset (None)
57+
#[error("Metadata pointer is unset (None)")]
58+
MetadataPointerUnset,
59+
/// Provided source metadata account does not match pointer
60+
#[error("Provided source metadata account does not match pointer")]
61+
MetadataPointerMismatch,
62+
/// External metadata program returned no data
63+
#[error("External metadata program returned no data")]
64+
ExternalProgramReturnedNoData,
5365
}
5466

5567
impl From<TokenWrapError> for ProgramError {
@@ -83,6 +95,12 @@ impl ToStr for TokenWrapError {
8395
TokenWrapError::EscrowInGoodState => "Error: EscrowInGoodState",
8496
TokenWrapError::UnwrappedMintHasNoMetadata => "Error: UnwrappedMintHasNoMetadata",
8597
TokenWrapError::MetaplexMetadataMismatch => "Error: MetaplexMetadataMismatch",
98+
TokenWrapError::MetadataPointerMissing => "Error: MetadataPointerMissing",
99+
TokenWrapError::MetadataPointerUnset => "Error: MetadataPointerUnset",
100+
TokenWrapError::MetadataPointerMismatch => "Error: MetadataPointerMismatch",
101+
&TokenWrapError::ExternalProgramReturnedNoData => {
102+
"Error: ExternalProgramReturnedNoData"
103+
}
86104
}
87105
}
88106
}

program/src/instruction.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ pub enum TokenWrapInstruction {
116116
/// - Token-2022 to Token-2022
117117
/// - SPL-token to Token-2022
118118
///
119+
/// If source mint is a Token-2022, it must have a `MetadataPointer` and the
120+
/// account it points to must be provided. If source mint is an SPL-Token,
121+
/// the `Metaplex` PDA must be provided.
122+
///
119123
/// If the `TokenMetadata` extension on the wrapped mint if not present, it
120124
/// will initialize it. The client is responsible for funding the wrapped
121125
/// mint account with enough lamports to cover the rent for the
@@ -128,8 +132,10 @@ pub enum TokenWrapInstruction {
128132
/// 1. `[]` Wrapped mint authority PDA
129133
/// 2. `[]` Unwrapped mint
130134
/// 3. `[]` Token-2022 program
131-
/// 4. `[]` (Optional) `Metaplex` Metadata PDA. Required if the unwrapped
132-
/// mint is an `spl-token` mint.
135+
/// 4. `[]` (Optional) Source metadata account. Required if metadata pointer
136+
/// indicates external account.
137+
/// 5. `[]` (Optional) Owner program. Required when metadata account is
138+
/// owned by a third-party program.
133139
SyncMetadataToToken2022,
134140
}
135141

@@ -312,7 +318,8 @@ pub fn sync_metadata_to_token_2022(
312318
wrapped_mint: &Pubkey,
313319
wrapped_mint_authority: &Pubkey,
314320
unwrapped_mint: &Pubkey,
315-
metaplex_metadata: Option<&Pubkey>,
321+
source_metadata: Option<&Pubkey>,
322+
owner_program: Option<&Pubkey>,
316323
) -> Instruction {
317324
let mut accounts = vec![
318325
AccountMeta::new(*wrapped_mint, false),
@@ -321,10 +328,14 @@ pub fn sync_metadata_to_token_2022(
321328
AccountMeta::new_readonly(spl_token_2022::id(), false),
322329
];
323330

324-
if let Some(pubkey) = metaplex_metadata {
331+
if let Some(pubkey) = source_metadata {
325332
accounts.push(AccountMeta::new_readonly(*pubkey, false));
326333
}
327334

335+
if let Some(owner) = owner_program {
336+
accounts.push(AccountMeta::new_readonly(*owner, false));
337+
}
338+
328339
let data = TokenWrapInstruction::SyncMetadataToToken2022.pack();
329340
Instruction::new_with_bytes(*program_id, &data, accounts)
330341
}

program/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
mod entrypoint;
66
pub mod error;
77
pub mod instruction;
8+
pub mod metadata;
89
pub mod metaplex;
910
pub mod mint_customizer;
1011
pub mod processor;

program/src/metadata.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Metadata resolution helpers for pointer-aware metadata sync
2+
3+
use {
4+
crate::{error::TokenWrapError, metaplex::metaplex_to_token_2022_metadata},
5+
mpl_token_metadata::ID as MPL_TOKEN_METADATA_ID,
6+
solana_account_info::AccountInfo,
7+
solana_cpi::{get_return_data, invoke},
8+
solana_program_error::ProgramError,
9+
spl_token_2022::{
10+
extension::{
11+
metadata_pointer::MetadataPointer, BaseStateWithExtensions, PodStateWithExtensions,
12+
},
13+
id as token_2022_id,
14+
pod::PodMint,
15+
},
16+
spl_token_metadata_interface::{instruction::emit, state::TokenMetadata},
17+
spl_type_length_value::variable_len_pack::VariableLenPack,
18+
};
19+
20+
/// Fetches metadata from a third-party program implementing
21+
/// `TokenMetadataInstruction` by invoking its `Emit` instruction and decoding
22+
/// the `TokenMetadata` struct from the return data.
23+
pub fn cpi_emit_and_decode<'a>(
24+
owner_program_info: &AccountInfo<'a>,
25+
metadata_info: &AccountInfo<'a>,
26+
) -> Result<TokenMetadata, ProgramError> {
27+
invoke(
28+
&emit(owner_program_info.key, metadata_info.key, None, None),
29+
&[metadata_info.clone()],
30+
)?;
31+
32+
if let Some((program_key, data)) = get_return_data() {
33+
// This check ensures this data comes from the program we just called
34+
if program_key == *owner_program_info.key {
35+
return TokenMetadata::unpack_from_slice(&data);
36+
}
37+
}
38+
39+
Err(TokenWrapError::ExternalProgramReturnedNoData.into())
40+
}
41+
42+
/// Resolve the canonical metadata source for an unwrapped Token-2022 mint
43+
/// by following its `MetadataPointer`.
44+
///
45+
/// Supported pointer targets:
46+
/// - Self
47+
/// - Token-2022 account
48+
/// - `Metaplex` PDA
49+
/// - Third-party program
50+
pub fn resolve_token_2022_source_metadata<'a>(
51+
unwrapped_mint_info: &AccountInfo<'a>,
52+
maybe_source_metadata_info: Option<&AccountInfo<'a>>,
53+
maybe_owner_program_info: Option<&AccountInfo<'a>>,
54+
) -> Result<TokenMetadata, ProgramError> {
55+
let data = unwrapped_mint_info.try_borrow_data()?;
56+
let mint_state = PodStateWithExtensions::<PodMint>::unpack(&data)?;
57+
let pointer = mint_state
58+
.get_extension::<MetadataPointer>()
59+
.map_err(|_| TokenWrapError::MetadataPointerMissing)?;
60+
let metadata_addr =
61+
Option::from(pointer.metadata_address).ok_or(TokenWrapError::MetadataPointerUnset)?;
62+
63+
// Scenario 1: points to self, read off unwrapped mint
64+
if metadata_addr == *unwrapped_mint_info.key {
65+
return mint_state.get_variable_len_extension::<TokenMetadata>();
66+
}
67+
68+
// Metadata account must be passed by this point
69+
let metadata_info = maybe_source_metadata_info.ok_or(ProgramError::NotEnoughAccountKeys)?;
70+
if metadata_info.key != &metadata_addr {
71+
return Err(TokenWrapError::MetadataPointerMismatch.into());
72+
}
73+
74+
if metadata_info.owner == &token_2022_id() {
75+
// Scenario 2: points to another token-2022 mint
76+
let data = metadata_info.try_borrow_data()?;
77+
let state = PodStateWithExtensions::<PodMint>::unpack(&data)?;
78+
state.get_variable_len_extension::<TokenMetadata>()
79+
} else if metadata_info.owner == &MPL_TOKEN_METADATA_ID {
80+
// Scenario 3: points to a Metaplex PDA
81+
metaplex_to_token_2022_metadata(unwrapped_mint_info, metadata_info)
82+
} else {
83+
// Scenario 4: points to an external program
84+
let owner_program_info =
85+
maybe_owner_program_info.ok_or(ProgramError::NotEnoughAccountKeys)?;
86+
if owner_program_info.key != metadata_info.owner {
87+
return Err(ProgramError::InvalidAccountOwner);
88+
}
89+
cpi_emit_and_decode(owner_program_info, metadata_info)
90+
}
91+
}

program/src/processor.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use {
88
get_wrapped_mint_backpointer_address_signer_seeds,
99
get_wrapped_mint_backpointer_address_with_seed, get_wrapped_mint_signer_seeds,
1010
instruction::TokenWrapInstruction,
11+
metadata::resolve_token_2022_source_metadata,
1112
metaplex::metaplex_to_token_2022_metadata,
1213
mint_customizer::{
1314
default_token_2022::DefaultToken2022Customizer, interface::MintCustomizer,
@@ -533,6 +534,8 @@ pub fn process_sync_metadata_to_token_2022(accounts: &[AccountInfo]) -> ProgramR
533534
let wrapped_mint_authority_info = next_account_info(account_info_iter)?;
534535
let unwrapped_mint_info = next_account_info(account_info_iter)?;
535536
let token_program_info = next_account_info(account_info_iter)?;
537+
let source_metadata_info = account_info_iter.next();
538+
let owner_program_info = account_info_iter.next();
536539

537540
if *token_program_info.key != spl_token_2022::id() {
538541
return Err(ProgramError::IncorrectProgramId);
@@ -554,15 +557,16 @@ pub fn process_sync_metadata_to_token_2022(accounts: &[AccountInfo]) -> ProgramR
554557
}
555558

556559
let unwrapped_metadata = if *unwrapped_mint_info.owner == spl_token_2022::id() {
557-
// Source is Token-2022: read from extension
558-
let unwrapped_mint_data = unwrapped_mint_info.try_borrow_data()?;
559-
let unwrapped_mint_state = PodStateWithExtensions::<PodMint>::unpack(&unwrapped_mint_data)?;
560-
unwrapped_mint_state
561-
.get_variable_len_extension::<TokenMetadata>()
562-
.map_err(|_| TokenWrapError::UnwrappedMintHasNoMetadata)?
560+
// Source is Token-2022: resolve metadata pointer
561+
resolve_token_2022_source_metadata(
562+
unwrapped_mint_info,
563+
source_metadata_info,
564+
owner_program_info,
565+
)?
563566
} else if *unwrapped_mint_info.owner == spl_token::id() {
564567
// Source is spl-token: read from Metaplex PDA
565-
let metaplex_metadata_info = next_account_info(account_info_iter)?;
568+
let metaplex_metadata_info =
569+
source_metadata_info.ok_or(ProgramError::NotEnoughAccountKeys)?;
566570
let (expected_metaplex_pda, _) = MetaplexMetadata::find_pda(unwrapped_mint_info.key);
567571
if *metaplex_metadata_info.owner != mpl_token_metadata::ID {
568572
return Err(ProgramError::InvalidAccountOwner);

program/tests/helpers/common.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ pub fn init_mollusk() -> Mollusk {
6464
"test_transfer_hook",
6565
&mollusk_svm::program::loader_keys::LOADER_V3,
6666
);
67+
mollusk.add_program(
68+
&mock_metadata_owner::ID,
69+
"mock_metadata_owner",
70+
&mollusk_svm::program::loader_keys::LOADER_V3,
71+
);
6772
mollusk
6873
}
6974

0 commit comments

Comments
 (0)