Skip to content

feat(solana): support token icons and descriptions on wrapped tokens - #199

Draft
jackchuma wants to merge 1 commit into
mainfrom
jack/wrapped-token-metadata-uri
Draft

jackchuma wants to merge 1 commit into
mainfrom
jack/wrapped-token-metadata-uri

Conversation

@jackchuma

@jackchuma jackchuma commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Part 1 of 3. Stacked: #199 (program) → client → CLI.

Context

An ecosystem partner asked whether a token icon and description can be set when creating a wrapped token on Solana through the bridge. They can't today.

Token-2022 has no icon or description fields at all. Its metadata extension is { name, symbol, uri, additional_metadata }, and both live in the off-chain JSON document that uri points at, under the image and description keys that wallets fetch. So this is one field rather than two — and uri was being initialized to an empty string:

token_metadata.name,
token_metadata.symbol,
Default::default(),   // <- the uri argument

The constraint that shaped this

The wrapped mint address is a PDA seeded by keccak(name, symbol, remote_token, scaler_exponent), and the program recomputes that hash from onchain metadata to authenticate a mint as a wrapped token. uri is deliberately excluded from the preimage. Adding it would move every already-deployed wrapped mint to an address the program no longer derives, which would:

  • stop bridge_wrapped_token from working, stranding those tokens on Solana;
  • stop finalize_wrapped_token_transfer, so inbound transfers could be proven but never relayed;
  • invert the MintIsWrappedToken guard in bridge_spl, routing legacy mints through the lock-in-vault path instead of the burn path and breaking the supply accounting behind the Base-side escrow.

deployed_mint_still_derives guards this using the real mainnet wrapped ETH mint (2ZCFyWM6WthDLBo41zJsMQmjJ4Kvb6yumvrbLpVh9LMX) as a fixture, so the test fails if the preimage ever changes.

Instructions

wrap_token_v2 is added and accepts the uri. wrap_token is deprecated but keeps its exact wire format and delegates with an empty uri.

Its argument is a new PartialTokenMetadataV1 whose field layout is identical to the previous PartialTokenMetadata, and the discriminator is unchanged at [203, 83, 204, 83, 225, 109, 44, 6]. Only the IDL's label for that type differs, so clients built before uri existed keep working untouched. Both wrap instructions derive the same mint, so a token wrapped through either is the same asset.

A wrapped token's metadata is immutable after creation, so the uri can only be supplied at wrap time. There is no instruction to change or backfill it.

MAX_URI_LEN is 200, matching Metaplex's limit for its own uri field, and is exported as an Anchor #[constant] so clients read it from the IDL rather than keeping a second copy.

Upgrade safety

Fully additive. Verified against origin/main: one instruction added, none removed, no discriminator changed, one error code added (UriTooLong, 12706) with no existing code shifted.

Known limitation

Because the uri is write-at-creation only, a token wrapped without one keeps an empty uri permanently. That covers every wrapped token already live on mainnet and anything wrapped through the deprecated wrap_token. For those, wallet and indexer token lists remain the only way to supply an icon.

It also means the first caller to wrap a given token decides its uri for good. The exposure is a wrong icon or description on a canonical mint rather than a fund-loss path, since name, symbol, and remote token are all committed in the mint address.

Also open: whether the deprecated wrap_token should be made to revert in a later upgrade. Keeping it indefinitely means the shorter, more discoverable name stays the one that produces uri-less tokens.

Testing

111 tests pass; clippy clean.

  • test_wrap_token_legacy_wire_format_is_unchanged hand-writes the legacy instruction bytes, discriminator included, rather than serializing from a type, so renaming the instruction or reordering its arguments fails here instead of silently breaking old clients.
  • test_wrap_token_v2_stores_uri_on_mint also proves the mint is funded for the bytes the uri adds, since Token-2022 rejects the realloc otherwise. wrap_token previously had no instruction-level tests.
  • deployed_mint_still_derives and v1_hashes_identically_and_converts_to_an_empty_uri cover the hash invariants.

One fixture fix is included: create_mock_wrapped_mint built its metadata through TokenMetadata::from, which leaves update_authority and mint at their defaults, unlike a real wrapped mint. It now mirrors the real layout. Account sizes are unchanged, since both are fixed-width fields.

Scope

Program and both generated IDL artifacts (solana/programs/bridge/idl.json and scripts/src/internal/sol/bridge.idl.ts, emitted by the same generate-idl run — separating them would leave the repo's two copies of the IDL out of sync).

The regenerated TypeScript client and the wrap-token CLI changes are in the two follow-up PRs.

Follow-ups before merge

  • Program upgrade across testnet-alpha, testnet-prod, and mainnet.
  • Audit delta against the existing Cantina reports, with attention to the two wrap entry points sharing one handler.

@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

Token-2022 has no icon or description fields; both are served from the
off-chain JSON document that its `uri` field points at. That field was
initialized to an empty string, so wrapped tokens could never carry either.

Adds `uri` to `PartialTokenMetadata` and passes it through to the metadata
initialize CPI. It is deliberately excluded from the mint's PDA hash: that
hash is recomputed from onchain metadata to authenticate a mint as a wrapped
token, so extending the preimage would move every already-deployed mint to an
address the program no longer derives.

`wrap_token` keeps its exact wire format, taking a `PartialTokenMetadataV1`
argument whose layout is byte-identical to the previous type, and delegates
with an empty uri. `wrap_token_v2` accepts the uri. Both derive the same mint.

Also fixes `create_mock_wrapped_mint`, which left `update_authority` and `mint`
at their defaults rather than mirroring a real wrapped mint.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants