Skip to content

Commit bbc4947

Browse files
committed
solana/token_bridge: add support for token2022 metadata
1 parent 1438075 commit bbc4947

File tree

6 files changed

+111
-9
lines changed

6 files changed

+111
-9
lines changed

solana/Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

solana/modules/token_bridge/program/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ instructions = []
1919

2020
[dependencies]
2121
wormhole-bridge-solana = { path = "../../../bridge/program", features = ["no-entrypoint", "cpi"] }
22+
token-metadata-parser = { path = "../token_metadata_parser" }
2223
borsh = "=0.9.3"
2324
bstr = "0.2.16"
2425
byteorder = "1.4.3"

solana/modules/token_bridge/program/src/accounts.rs

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ impl<'b, const STATE: AccountState> Seeded<&EndpointDerivationData> for Endpoint
9090
}
9191
}
9292

93+
/// Metadata account for the token. This used to be exclusively the Metaplex
94+
/// metadata account, but with token2022's metadata pointer extension, this may be any account.
95+
/// `deserialize_and_verify_metadata` verifies that this account is what the token specifies (or falls back to Metaplex).
9396
pub type SplTokenMeta<'b> = Info<'b>;
9497

9598
pub struct SplTokenMetaDerivationData {
@@ -109,35 +112,125 @@ impl<'b> Seeded<&SplTokenMetaDerivationData> for SplTokenMeta<'b> {
109112
//New data length of spl token metadata account
110113
pub const NEW_MAX_METADATA_LEN: usize = 607;
111114

115+
/// Converts Token-2022 metadata to Metaplex metadata format for compatibility
116+
fn convert_token2022_to_metaplex_metadata(
117+
token_metadata: &token_metadata_parser::TokenMetadata,
118+
) -> spl_token_metadata::state::Metadata {
119+
use spl_token_metadata::state::{
120+
Data,
121+
Key,
122+
Metadata,
123+
};
124+
125+
let data = Data {
126+
name: token_metadata.name.clone(),
127+
symbol: token_metadata.symbol.clone(),
128+
uri: token_metadata.uri.clone(),
129+
seller_fee_basis_points: 0, // Token-2022 doesn't have this concept
130+
creators: None, // Token-2022 doesn't have creators
131+
};
132+
133+
Metadata {
134+
key: Key::MetadataV1,
135+
update_authority: token_metadata
136+
.update_authority
137+
.map(|pubkey| Pubkey::new(&pubkey.0))
138+
.unwrap_or_default(),
139+
mint: Pubkey::new(&token_metadata.mint.0),
140+
data,
141+
primary_sale_happened: false, // Default for Token-2022
142+
is_mutable: token_metadata.update_authority.is_some(),
143+
edition_nonce: None,
144+
token_standard: None,
145+
collection: None,
146+
uses: None,
147+
collection_details: None,
148+
programmable_config: None,
149+
}
150+
}
151+
112152
/// This method removes code duplication when checking token metadata. When metadata is read for
113153
/// attestation and transfers, Token Bridge does not invoke Metaplex's Token Metadata program, so
114154
/// it must validate the account the same way Token Metadata program does to ensure the correct
115155
/// account is passed into Token Bridge's instruction context.
116156
pub fn deserialize_and_verify_metadata(
117-
info: &Info,
157+
mint: &Info,
158+
metadata: &Info,
118159
derivation_data: SplTokenMetaDerivationData,
119160
) -> Result<spl_token_metadata::state::Metadata> {
120-
// Verify pda.
121-
info.verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
161+
let mint_metadata = token_metadata_parser::parse_token2022_metadata(
162+
token_metadata_parser::Pubkey::new(mint.key.to_bytes()),
163+
&mint.data.borrow(),
164+
)
165+
.map_err(|_| TokenBridgeError::InvalidMetadata)?;
166+
167+
// we constrain the `metadata` account in every case.
168+
// 1. if mint is token-2022 with embedded metadata, we return that metadata (in this case, `metadata` == `mint`. `token_metadata_parser` ensures this)
169+
// 2. if mint is token-2022 with external metadata pointer, we verify `metadata` matches the pointer
170+
// a. if `metadata` is owned by spl-token-metadata, we verify the pda and deserialise it as standard Metaplex metadata
171+
// b. if `metadata` is not owned by spl-token-metadata, we don't verify that it's a pda (we know it matches the pointer already)
172+
// 3. if mint doesn't include a metadata pointer, we ensure `metadata` is the metaplex pda.
173+
// this is the legacy case, but it applies to token2022 tokens as well (that have no metadata pointer extension)
174+
//
175+
// Note that in every case other than 1 (which is a well-defined spec via
176+
// the token metadata extension), we parse the `metadata` account following
177+
// the standard metaplex format.
178+
//
179+
// In case of 2b, this is a best-effort guess, because the metadata pointer
180+
// extension makes no guarantees about the shape of the metadata account. However, a common practice is to just follow the metaplex format.
181+
// What this means is that if the metadata account is not owned by the metaplex program, and is not in the metaplex format, the deserialisation will fail.
182+
// We just don't support these tokens.
183+
184+
match mint_metadata {
185+
// 1.
186+
token_metadata_parser::MintMetadata::Embedded(token_metadata) => {
187+
// token-2022 mint with embedded metadata
188+
return Ok(convert_token2022_to_metaplex_metadata(&token_metadata));
189+
}
190+
// 2.
191+
token_metadata_parser::MintMetadata::External(pointer) => {
192+
if pointer.metadata_address
193+
!= token_metadata_parser::Pubkey::new(metadata.key.to_bytes())
194+
{
195+
return Err(TokenBridgeError::WrongMetadataAccount.into());
196+
}
197+
198+
// 2a.
199+
if *metadata.owner == spl_token_metadata::id() {
200+
// Standard Metaplex metadata verification and parsing
201+
// Verify pda.
202+
metadata.verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
203+
// 2b.
204+
} else {
205+
// fall through
206+
}
207+
}
208+
// 3.
209+
token_metadata_parser::MintMetadata::None => {
210+
// Standard Metaplex metadata verification and parsing
211+
// Verify pda.
212+
metadata.verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
213+
}
214+
}
122215

123216
// There must be account data for token's metadata.
124-
if info.data_is_empty() {
217+
if metadata.data_is_empty() {
125218
return Err(TokenBridgeError::NonexistentTokenMetadataAccount.into());
126219
}
127220

128221
// Account must belong to Metaplex Token Metadata program.
129-
if *info.owner != spl_token_metadata::id() {
222+
if *metadata.owner != spl_token_metadata::id() {
130223
return Err(TokenBridgeError::WrongAccountOwner.into());
131224
}
132225

133226
// Account must be the expected Metadata length.
134-
if info.data_len() != spl_token_metadata::state::MAX_METADATA_LEN
135-
&& info.data_len() != NEW_MAX_METADATA_LEN
227+
if metadata.data_len() != spl_token_metadata::state::MAX_METADATA_LEN
228+
&& metadata.data_len() != NEW_MAX_METADATA_LEN
136229
{
137230
return Err(TokenBridgeError::InvalidMetadata.into());
138231
}
139232

140-
let mut data: &[u8] = &info.data.borrow_mut();
233+
let mut data: &[u8] = &metadata.data.borrow_mut();
141234

142235
// Unfortunately we cannot use `map_err` easily, so we will match certain deserialization conditions.
143236
match spl_token_metadata::utils::meta_deser_unchecked(&mut data) {

solana/modules/token_bridge/program/src/api/attest.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ pub fn attest_token(
117117

118118
// Assign metadata if an SPL Metadata account exists for the SPL token in question.
119119
if !accs.spl_metadata.data_is_empty() {
120-
let metadata = deserialize_and_verify_metadata(&accs.spl_metadata, (&*accs).into())?;
120+
let metadata =
121+
deserialize_and_verify_metadata(accs.mint.info(), &accs.spl_metadata, (&*accs).into())?;
121122
payload.name = metadata.data.name.clone();
122123
payload.symbol = metadata.data.symbol;
123124
}

solana/modules/token_bridge/program/src/api/create_wrapped.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ pub fn update_accounts(
205205
// Checks in this method are redundant with what occurs in `update_metadata_accounts_v2`, but we want to make
206206
// sure that the account we are deserializing is legitimate.
207207
let metadata = deserialize_and_verify_metadata(
208+
accs.mint.info(),
208209
&accs.spl_metadata,
209210
SplTokenMetaDerivationData {
210211
mint: *accs.mint.info().key,

solana/modules/token_bridge/program/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ pub enum TokenBridgeError {
9191
InvalidVAA,
9292
NonexistentTokenMetadataAccount,
9393
NotMetadataV1Account,
94+
WrongMetadataAccount,
9495
}
9596

9697
impl From<TokenBridgeError> for SolitaireError {

0 commit comments

Comments
 (0)