logoSolana Program

Token Wrap

A program for wrapping SPL tokens to enable interoperability between token standards. If you are building an app with a mint/token and find yourself wishing you could take advantage of some of the latest features of a specific token program, this might be for you!

Features

  • Bidirectional Wrapping: Convert tokens between SPL Token and SPL Token 2022 standards in either direction, including conversions between different SPL Token 2022 mints.
  • Extensible Mint Creation: The CreateMint instruction is designed to be extensible through the MintCustomizer trait. By forking the program and implementing this trait, developers can add custom logic to:
    • Include any SPL Token 2022 extensions on the new wrapped mint.
    • Modify default properties like the freeze_authority and decimals.
  • Confidential Transfers by Default: All wrapped tokens created under the Token-2022 standard automatically include the ConfidentialTransferMint extension, enabling the option for privacy-preserving transactions. This feature is immutable and requires no additional configuration.
  • Transfer Hook Compatibility: Integrates with tokens that implement the SPL Transfer Hook interface, enabling custom logic on token transfers.
  • Multisignature Support: Compatible with multisig signers for both wrapping and unwrapping operations.
  • Metadata Synchronization: Syncs metadata from unwrapped tokens (both Metaplex and Token-2022 standards) to their wrapped counterparts.

How It Works

It supports the following primary operations:

  1. CreateMint: This operation initializes a new wrapped token mint and its associated backpointer account. Note, the caller must pre-fund this account with lamports. This is to avoid requiring writer+signer privileges on this instruction.

    • Wrapped Mint: An SPL Token or SPL Token 2022 mint account is created. The address of this mint is a PDA derived from the unwrapped token's mint address and the wrapped token program ID. This ensures a unique, deterministic relationship between the wrapped and unwrapped tokens. The wrapped mint's authority is also a PDA, controlled by the Token Wrap program.
    • Backpointer: An account (also a PDA, derived from the wrapped mint address) is created to store the address of the original unwrapped token mint. This allows anyone to easily determine the unwrapped token corresponding to a wrapped token, facilitating unwrapping.
  2. Wrap: This operation accepts deposits of unwrapped tokens and mints wrapped tokens.

    • Unwrapped tokens are transferred from the user's account to a specific escrow Associated Token Account (ATA). This ATA is for the unwrapped mint, and its authority is a Program Derived Address (PDA) controlled by the Token Wrap program (unique for each wrapped mint).
    • An equivalent amount of wrapped tokens is minted to the user's wrapped token account.
  3. Unwrap: This operation burns wrapped tokens and releases unwrapped token deposits.

    • Wrapped tokens are burned from the user's wrapped token account.
    • An equivalent amount of unwrapped tokens is transferred from the escrow account to the user's unwrapped token account.
  4. CloseStuckEscrow: This operation handles an edge case with re-creating a mint with the MintCloseAuthority extension.

    • The escrow ATA can get "stuck" when an unwrapped mint with a close authority is closed and then a new mint is created at the same address but with different extensions, leaving the escrow ATA (Associated Token Account) in an incompatible state.
    • The instruction closes the old escrow ATA and returns the lamports to a specified destination account.
    • This operation will only succeed if the current escrow has zero balance and has different extensions than the mint.
    • After closing the stuck escrow, the client is responsible for recreating the ATA with the correct extensions.
  5. SyncMetadataToToken2022: This operation copies metadata from an unwrapped mint to its wrapped Token-2022 mint's TokenMetadata extension.

    • It initializes the TokenMetadata extension on the wrapped mint if it doesn't already exist.
    • The caller is responsible for pre-funding the wrapped mint account with enough lamports to cover the rent for the added space.
    • Supports: SPL Token -> Token-2022 and Token-2022 -> Token-2022.
  6. SyncMetadataToSplToken: This operation copies metadata from an unwrapped mint to the Metaplex metadata account of its wrapped SPL Token mint.

    • It can create the Metaplex metadata account if it doesn't exist or update an existing one.
    • The wrapped_mint_authority PDA acts as the payer for the Metaplex program CPI and must be pre-funded with sufficient lamports to cover rent for the Metaplex account.
    • Supports: Token-2022 -> SPL Token and SPL Token -> SPL Token.

The 1:1 relationship between wrapped and unwrapped tokens is maintained through the escrow mechanism, ensuring that wrapped tokens are always fully backed by their unwrapped counterparts.

Permissionless design

The SPL Token Wrap program is designed to be permissionless. This means:

  • Anyone can create a wrapped mint: No special permissions or whitelisting is required to create a wrapped version of an existing mint. The CreateMint instruction is open to all users, provided they can pay the required rent for the new accounts.
  • Anyone can wrap and unwrap tokens: Once a wrapped mint has been created, any user holding the underlying unwrapped tokens can use the Wrap and Unwrap instructions. All transfers are controlled by PDAs owned by the Token Wrap program itself. However, it is important to note that if the unwrapped token has a freeze authority, that freeze authority is preserved in the wrapped token.

Confidential Transfer extension

The ConfidentialTransferMint extension is added to every Token-2022 wrapped mint and initialized with the following config:

  • No Authority: The confidential transfer authority is set to None, making the configuration immutable. This ensures that the privacy features cannot be disabled or altered after the wrapped mint is created.
  • No Auditor: The wrapped mints are created without a confidential transfer auditor. This means that there is no third party that can view the details of confidential transactions.
  • Automatic Account Approval: New token accounts are approved for confidential transfers by default. This allows users to make private transactions permissionlessly.

Customizing mint

If the current wrapped mint config does not suit your needs, please fork! A few places you are going to want to update:

  • Add a new struct that implements MintCustomizer in program/src/mint_customizer
  • Replace the current one in use within the processor: program/src/processor.rs
  • Re-run tests (see package.json) and update/remove assertions to accommodate new config
  • If wanting to make use of clients:
    • CLI: Update mint customizer type in clients/cli/src/create_mint.rs
    • JS: Update mint size in clients/js/src/create-mint.ts

Deployments

  • Program ID: TwRapQCDhWkZRrDaHfZGuHxkZ91gHDRkyuzNqeU5MgR
  • Mainnet: (not yet deployed)
  • Testnet: (not yet deployed)

Source

The Token Wrap Program's source is available on GitHub.

Security Audits

AuditorDateVersionReport
Zellic2025-05-1675c5529PDF
Runtime Verification2025-06-11dd71fc1PDF

SDK

  • Rust Crate: The program is written in Rust and available as the spl-token-wrap crate on crates.io and docs.rs.
  • JavaScript bindings for web development: @solana-program/token-wrap (source).
  • Command-Line Interface (CLI): The spl-token-wrap-cli utility allows direct interaction with the program via the command line for testing, scripting, or manual operations.

Reference Guide

Setup

The spl-token-wrap command-line utility can be used to interact with the Token Wrap program.

Install from crates.io

$ cargo install spl-token-wrap-cli

or, build the CLI from source:

$ git clone https://github.com/solana-program/token-wrap.git
$ cd token-wrap
$ cargo build --bin spl-token-wrap

Run spl-token-wrap --help for a full description of available commands.

The spl-token-wrap configuration is shared with the solana command-line tool.

Create a wrapped token mint

To create a new wrapped token mint, first you need to identify the unwrapped token mint address you want to wrap and the to/from token programs.

$ UNWRAPPED_MINT_ADDRESS=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap create-mint $UNWRAPPED_MINT_ADDRESS $WRAPPED_TOKEN_PROGRAM
 
Creating wrapped mint for BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
Funding wrapped_mint_account B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM with 1461600 lamports for rent
Funding backpointer_account CNjr898vsBdzWxrJApMSAQac4A7o7qLRcSseTb56X7C9 with 1113600 lamports for rent
Unwrapped mint address: BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
Wrapped mint address: B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM
Wrapped backpointer address: CNjr898vsBdzWxrJApMSAQac4A7o7qLRcSseTb56X7C9
Funded wrapped mint lamports: 1461600
Funded backpointer lamports: 1113600
Signature: 2UAPjhDogs8aTTfynWRi36KWez6jzmFJhAHPTBpYsamDvKRQ5Uqn2BXoz1mKfRwBPV8p1j1MSXLN7yZHLwb1wdnT

Find PDAs for a wrapped token

To interact with wrapped tokens, you need to know the PDAs (Program Derived Addresses) associated with them:

$ UNWRAPPED_MINT_ADDRESS=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap find-pdas $UNWRAPPED_MINT_ADDRESS $WRAPPED_TOKEN_PROGRAM
 
Wrapped mint address: B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM
Wrapped mint authority: 8WdYPmtq8c6ZfmHMZUwCQL2E8qVHEV8rG9MXkyax3joR
Wrapped backpointer address: CNjr898vsBdzWxrJApMSAQac4A7o7qLRcSseTb56X7C9
Unwrapped escrow address: QrzXtFZedQmg8AGu6AnUkPgmsLnR9ErsjNRLdCrRVWw

Create escrow account

Before wrapping tokens, if you are the first to do so for this wrapped mint, you may need to initialize the escrow account to custody the unwrapped tokens. The account must be an ATA whose owner is the mint authority PDA (see find-pdas command above). There is also a helper to initialize this account:

$ UNWRAPPED_MINT_ADDRESS=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap create-escrow-account $UNWRAPPED_MINT_ADDRESS $WRAPPED_TOKEN_PROGRAM
 
Creating escrow account under program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA for unwrapped mint BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e owned by PDA 8WdYPmtq8c6ZfmHMZUwCQL2E8qVHEV8rG9MXkyax3joR
Escrow Account Address: 4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3
Escrow Account Owner (PDA): 8WdYPmtq8c6ZfmHMZUwCQL2E8qVHEV8rG9MXkyax3joR
Escrow Token Program ID: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Signature: 3ysN6YEQcsYQBjnCPMas9xEGP53CSSvoL6CSJ1vJS1S5ZvtN5NbsUtDKQMs6hwCQHhsctcrEhLQBLTEBuQWEKqNE

Wrap tokens (single signer)

Escrows unwrapped tokens and mints wrapped tokens to recipient account.

$ UNWRAPPED_TOKEN_ACCOUNT=DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap wrap $UNWRAPPED_TOKEN_ACCOUNT $WRAPPED_TOKEN_PROGRAM 100
 
Wrapping 100 tokens from mint BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
Unwrapped mint address: BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
Wrapped mint address: B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM
Unwrapped token account: DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14
Recipient wrapped token account: HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt
Escrow account: 4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3
Amount: 100
Signers:
  26xTNzcurTuXQfHSCCuamxmrDXbkbA38JtGC9GhEcKgVZwxnyvXBD5AMH8TXmkfpNw64noDPaS4Ezm4RLMvfq3nF

You can specify a recipient token account with the --recipient-token-account option. If not provided, the associated token account of the fee payer will be used or created if it doesn't exist.

$ spl-token-wrap wrap $UNWRAPPED_TOKEN_ACCOUNT $WRAPPED_TOKEN_PROGRAM 100 \
    --recipient-token-account $RECIPIENT_WRAPPED_TOKEN_ACCOUNT

Wrap tokens (SPL Token Multisig)

An example wrapping tokens whose origin is a token account owned by an SPL Token multisig.

There are two parts to this. The first is having the multisig members sign the message independently. The second is the broadcaster collecting those signatures and sending the transaction to the network.

Let's pretend we have a 2 of 3 multisig and the broadcaster will be the fee payer. Here's what that would look like:

Get a recent blockhash. This will need to be the same for all signers.

$ solana block
Blockhash: E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm
⬆️ send this to all signers

First signer runs this command with their keypair:

Different for each signer
$ SIGNER_1=signer-1.json
$ SIGNER_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK
$ FEE_PAYER=2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5
 
Same for everyone
$ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm
$ UNWRAPPED_TOKEN_ACCOUNT=4jFsvSDhp9J67An6DUGwezTiunud11RXiaf2zqtG2yUL # owned by multisig
$ MULTISIG_ADDRESS=mgnqjedikMKaRtS5wrhVttuA12JaPXiqY619Gfef5eh
$ RECIPIENT_ACCOUNT=HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt
$ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
$ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap wrap $UNWRAPPED_TOKEN_ACCOUNT $WRAPPED_TOKEN_PROGRAM 23 \
    --transfer-authority $MULTISIG_ADDRESS \
    --recipient-token-account $RECIPIENT_ACCOUNT \
    --unwrapped-mint $UNWRAPPED_MINT \
    --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \
    --fee-payer $FEE_PAYER \
    --multisig-signer $SIGNER_1 \
    --multisig-signer $SIGNER_2 \
    --blockhash $BLOCKHASH \
    --sign-only
 
Signers (Pubkey=Signature):
  DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS=4sQFJg338zP9bxX4Gw4KS58eXkpBB2pwjwo4szxCEVQZxrApzgYMN7riBYUnbvZPb84tsThPE1aHApiCCC9PSSP7
Absent Signers (Pubkey):
  2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5
  42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK

Second signer uses their own keypair (note the change at the top):

Signer 2 uses their keypair and puts the pubkey for signer 1
$ SIGNER_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS
$ SIGNER_2=signer-2.json
$ FEE_PAYER=2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5
 
$ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm
$ UNWRAPPED_TOKEN_ACCOUNT=4jFsvSDhp9J67An6DUGwezTiunud11RXiaf2zqtG2yUL
$ MULTISIG_ADDRESS=mgnqjedikMKaRtS5wrhVttuA12JaPXiqY619Gfef5eh
$ RECIPIENT_ACCOUNT=HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt
$ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
$ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap wrap $UNWRAPPED_TOKEN_ACCOUNT $WRAPPED_TOKEN_PROGRAM 23 \
    --transfer-authority $MULTISIG_ADDRESS \
    --recipient-token-account $RECIPIENT_ACCOUNT \
    --unwrapped-mint $UNWRAPPED_MINT \
    --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \
    --fee-payer $FEE_PAYER \
    --multisig-signer $SIGNER_1 \
    --multisig-signer $SIGNER_2 \
    --blockhash $BLOCKHASH \
    --sign-only
 
Signers (Pubkey=Signature):
    42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK=4UPUAV9USLFp8CKJ9u6gXhvUUFkpL2FTMbu3eJyyZ8DonjHJBEjUchuaM7j7tTaNWWF7zaRFfK5TkYvBytbV5vUR
Absent Signers (Pubkey):
    2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5
    DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS

Now the broadcaster (and in this case, the fee payer as well) sends the last message with the Pubkey=Signature they have collected from Signer 1 and Signer 2:

$ SIGNER_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS
$ SIGNATURE_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS=4sQFJg338zP9bxX4Gw4KS58eXkpBB2pwjwo4szxCEVQZxrApzgYMN7riBYUnbvZPb84tsThPE1aHApiCCC9PSSP7
$ SIGNER_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK
$ SIGNATURE_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK=4UPUAV9USLFp8CKJ9u6gXhvUUFkpL2FTMbu3eJyyZ8DonjHJBEjUchuaM7j7tTaNWWF7zaRFfK5TkYvBytbV5vUR
$ FEE_PAYER="$HOME/.config/solana/id.json"
 
$ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm
$ UNWRAPPED_TOKEN_ACCOUNT=4jFsvSDhp9J67An6DUGwezTiunud11RXiaf2zqtG2yUL
$ MULTISIG_ADDRESS=mgnqjedikMKaRtS5wrhVttuA12JaPXiqY619Gfef5eh
$ RECIPIENT_ACCOUNT=HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt
$ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
$ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap wrap $UNWRAPPED_TOKEN_ACCOUNT $WRAPPED_TOKEN_PROGRAM 23 \
    --transfer-authority $MULTISIG_ADDRESS \
    --recipient-token-account $RECIPIENT_ACCOUNT \
    --unwrapped-mint $UNWRAPPED_MINT \
    --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \
    --fee-payer $FEE_PAYER \
    --multisig-signer $SIGNER_1 \
    --multisig-signer $SIGNER_2 \
    --blockhash $BLOCKHASH \
    --signer $SIGNATURE_1 \
    --signer $SIGNATURE_2
 
Wrapping 23 tokens from mint BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
Unwrapped mint address: BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
Wrapped mint address: B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM
Unwrapped token account: 4jFsvSDhp9J67An6DUGwezTiunud11RXiaf2zqtG2yUL
Recipient wrapped token account: HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt
Escrow account: 4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3
Amount: 23
Signers:
  5pBReBRzy8yWLbz5j5GNBVFTmwGy65d4BvzigUPRTWWRYx4SUceNWDb78h1ufaRdzyi7yNmKpdLHv2eNS7ziaH7L
  3KdzhMYjFxBFZtQzEqfeugg6LVGhApSRj8pAx8HpgqCRU7C3gA2Wm5Hvx55taMAcpDWaKSJdtpgUJ8ksBVo4PDJU
  259kbWfYYhhe4FjTWZCeCXhPN4q3VSKN4dRHMcb42i85jptfh82TocrEf13aj5qMMDux9btzL5RCV55AxCWJbu5Q

Note all three needed signers in final broadcasted message.

Unwrap tokens (single signer)

Burns wrapped tokens and releases unwrapped tokens from escrow.

$ WRAPPED_TOKEN_ACCOUNT=HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt
$ UNWRAPPED_TOKEN_RECIPIENT=DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14
 
$ spl-token-wrap unwrap $WRAPPED_TOKEN_ACCOUNT $UNWRAPPED_TOKEN_RECIPIENT 50
 
Unwrapping 50 tokens from mint BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
Unwrapped token program: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Unwrapped mint address: BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
Recipient unwrapped token account: DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14
Amount unwrapped: 50
Signers:
  4HjHkjpjZztvoYT95mKHy2wH7z7iAFqpxSMeeMdUpPTzsjZN3vKg1KXvTMV7VT3jK6CaePYYXYDCTm52KTWz6du

Unwrap tokens (SPL Token Multisig)

An example unwrapping tokens whose origin is a token account owned by an SPL Token multisig.

There are two parts to this. The first is having the multisig members sign the message independently. The second is the broadcaster collecting those signatures and sending the transaction to the network.

Let's pretend we have a 2 of 3 multisig and the broadcaster will be the fee payer. Here's what that would look like:

Get a recent blockhash. This will need to be the same for all signers.

$ solana block
Blockhash: E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm
⬆️ send this to all signers

First signer runs this command with their keypair:

Different for each signer
$ SIGNER_1=signer-1.json
$ SIGNER_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK
$ FEE_PAYER=2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5
 
Same for everyone
$ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm
$ WRAPPED_TOKEN_ACCOUNT=3FzdqSEo32BcFgTUqWL5QakZGQBRX91yBAQFo1vGsCji
$ MULTISIG_ADDRESS=FFQvYvhaWnHeGsCMfixccUMdnXPgDrkG3KkGzpfBHFPb # note this should have the same program-id as wrapped token account
$ UNWRAPPED_TOKEN_RECIPIENT=DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14
$ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
$ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap unwrap $WRAPPED_TOKEN_ACCOUNT $UNWRAPPED_TOKEN_RECIPIENT 5 \
    --transfer-authority $MULTISIG_ADDRESS \
    --fee-payer $FEE_PAYER \
    --unwrapped-mint $UNWRAPPED_MINT \
    --wrapped-token-program $WRAPPED_TOKEN_PROGRAM \
    --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \
    --multisig-signer $SIGNER_1 \
    --multisig-signer $SIGNER_2 \
    --blockhash $BLOCKHASH \
    --sign-only

Second signer uses their own keypair (note the change at the top):

Signer 2 uses their keypair and puts the pubkey for signer 1
$ SIGNER_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS
$ SIGNER_2=signer-2.json
$ FEE_PAYER=2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5
 
$ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm
$ WRAPPED_TOKEN_ACCOUNT=3FzdqSEo32BcFgTUqWL5QakZGQBRX91yBAQFo1vGsCji
$ MULTISIG_ADDRESS=FFQvYvhaWnHeGsCMfixccUMdnXPgDrkG3KkGzpfBHFPb
$ UNWRAPPED_TOKEN_RECIPIENT=DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14
$ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
$ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap unwrap $WRAPPED_TOKEN_ACCOUNT $UNWRAPPED_TOKEN_RECIPIENT 5 \
    --transfer-authority $MULTISIG_ADDRESS \
    --fee-payer $FEE_PAYER \
    --unwrapped-mint $UNWRAPPED_MINT \
    --wrapped-token-program $WRAPPED_TOKEN_PROGRAM \
    --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \
    --multisig-signer $SIGNER_1 \
    --multisig-signer $SIGNER_2 \
    --blockhash $BLOCKHASH \
    --sign-only

Now the broadcaster (and in this case, the fee payer as well) sends the last message with the Pubkey=Signature they have collected from Signer 1 and Signer 2:

$ SIGNER_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS
$ SIGNATURE_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS=4sQFJg338zP9bxX4Gw4KS58eXkpBB2pwjwo4szxCEVQZxrApzgYMN7riBYUnbvZPb84tsThPE1aHApiCCC9PSSP7
$ SIGNER_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK
$ SIGNATURE_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK=4UPUAV9USLFp8CKJ9u6gXhvUUFkpL2FTMbu3eJyyZ8DonjHJBEjUchuaM7j7tTaNWWF7zaRFfK5TkYvBytbV5vUR
$ FEE_PAYER="$HOME/.config/solana/id.json"
 
$ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm
$ WRAPPED_TOKEN_ACCOUNT=3FzdqSEo32BcFgTUqWL5QakZGQBRX91yBAQFo1vGsCji
$ MULTISIG_ADDRESS=FFQvYvhaWnHeGsCMfixccUMdnXPgDrkG3KkGzpfBHFPb
$ UNWRAPPED_TOKEN_RECIPIENT=DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14
$ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
$ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap unwrap $WRAPPED_TOKEN_ACCOUNT $UNWRAPPED_TOKEN_RECIPIENT 5 \
    --transfer-authority $MULTISIG_ADDRESS \
    --fee-payer $FEE_PAYER \
    --unwrapped-mint $UNWRAPPED_MINT \
    --wrapped-token-program $WRAPPED_TOKEN_PROGRAM \
    --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \
    --multisig-signer $SIGNER_1 \
    --multisig-signer $SIGNER_2 \
    --blockhash $BLOCKHASH \
    --signer $SIGNATURE_1 \
    --signer $SIGNATURE_2
 
Unwrapping 5 tokens from mint BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
Unwrapped token program: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
Unwrapped mint address: BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e
Recipient unwrapped token account: DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14
Amount unwrapped: 5
Signers:
  5s2gNCExGchZJcnDTHMubyRsjuNprhyzaNeSbc34sJSLt5AB8N6V6uBoAegnBF1zvm1s65CPtjVLNH7Eb2hFYLsM
  eEcQFqdsDZzKL5CAm43kofVvnXCy7ZTQFm3RS6iYg4nckrSUzWZctebDYqcUNqtxxBgnLHyeDZiYMxNABAYYt2x
  5fxWE9KQYFQHk2u9ienicDtuaRf9XWBvrmM48CBTxwtmpJXuxHxDzAYSM5atHe77rFTVsezbLCbuzirN1o5XdZTf

Note all three needed signers in final broadcasted message.

Sync metadata to a wrapped Token-2022 mint

This instruction copies metadata from an unwrapped mint to its corresponding wrapped Token-2022 mint. This is useful when you want the wrapped version of your token to have on-chain metadata via the TokenMetadata extension.

This operation supports two main pathways:

  • Syncing from a standard SPL Token with Metaplex metadata to a wrapped Token-2022 mint.
  • Syncing from one Token-2022 mint to another wrapped Token-2022 mint.

Prerequisite: The wrapped Token-2022 mint account must be funded with enough lamports to cover the rent for the additional space required by the TokenMetadata extension.

When syncing from a standard SPL Token, the CLI can automatically derive the source Metaplex metadata PDA using the --metaplex flag.

# The unwrapped mint is a standard SPL Token with Metaplex metadata
$ UNWRAPPED_MINT_ADDRESS=8owJWKMiKfMKYbPmobyZAwXibNFcY7Roj6quktaeqxGL
 
$ spl-token-wrap sync-metadata-to-token2022 $UNWRAPPED_MINT_ADDRESS --metaplex
 
Syncing metadata to Token-2022 mint D7g6P2Yt1gE3n2h6aAC3f2V2b8At3Y8a1b5g2j3k4hL from 8owJWKMiKfMKYbPmobyZAwXibNFcY7Roj6quktaeqxGL
Unwrapped mint: 8owJWKMiKfMKYbPmobyZAwXibNFcY7Roj6quktaeqxGL
Wrapped mint: D7g6P2Yt1gE3n2h6aAC3f2V2b8At3Y8a1b5g2j3k4hL
Wrapped mint authority: C5f9n2h6aAC3f2V2b8At3Y8a1b5g2j3k4hLD7g6P2Yt1
Source metadata: metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s

Sync metadata to a wrapped SPL Token mint

This instruction copies metadata from an unwrapped mint to the Metaplex metadata account of its corresponding wrapped standard SPL Token mint.

This operation supports two main pathways:

  • Syncing from a Token-2022 mint (with a MetadataPointer extension) to a wrapped SPL Token mint.
  • Syncing from one standard SPL Token mint to another wrapped SPL Token mint.

Prerequisite: The wrapped_mint_authority PDA must be funded with enough lamports to pay the rent for creating or updating the Metaplex metadata account via a CPI.

When the unwrapped mint is a Token-2022 mint, the CLI will automatically resolve its metadata pointer.

# The unwrapped mint is a Token-2022 mint with MetadataPointer and TokenMetadata extensions
$ UNWRAPPED_MINT_ADDRESS=5xte8yNSUTrTtfdptekeA4QJyo8zZdanpDJojrRaXP1Y
 
$ spl-token-wrap sync-metadata-to-spl-token $UNWRAPPED_MINT_ADDRESS
 
Syncing metadata to SPL Token mint 9bZg2j3k4hL7g6P2Yt1gE3n2h6aAC3f2V2b8At3Y8a1 from 5xte8yNSUTrTtfdptekeA4QJyo8zZdanpDJojrRaXP1Y
Unwrapped mint: 5xte8yNSUTrTtfdptekeA4QJyo8zZdanpDJojrRaXP1Y
Wrapped mint: 9bZg2j3k4hL7g6P2Yt1gE3n2h6aAC3f2V2b8At3Y8a1
Wrapped mint authority: C5f9n2h6aAC3f2V2b8At3Y8a1b5g2j3k4hLD7g6P2Yt1
Metaplex metadata account: metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s

Close a stuck escrow account

This is an advanced recovery instruction for a specific edge case involving Token-2022 mints with the MintCloseAuthority extension.

If such a mint is closed and then recreated at the same address but with different extensions (e.g., adding TransferFeeConfig), the original escrow Associated Token Account (ATA) becomes "stuck" because its extensions no longer match the new mint's requirements. This instruction allows you to close that incompatible escrow ATA and reclaim its lamports.

Prerequisites:

  • The unwrapped mint must be a Token-2022 mint.
  • The stuck escrow account must have a balance of zero tokens.
  • The extensions on the escrow account must be different from those now required by the new mint.
# Address of the re-created unwrapped mint
$ UNWRAPPED_MINT=MintClosedAndRecreatedWithNewExtensions...
# Account to receive the lamports from the closed escrow
$ DESTINATION_ACCOUNT=RecipientsSolWalletAddress...
# The token program of the wrapped mint
$ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb
 
$ spl-token-wrap close-stuck-escrow $UNWRAPPED_MINT $DESTINATION_ACCOUNT $WRAPPED_TOKEN_PROGRAM
 
Closing stuck escrow account 4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 for unwrapped mint MintClosedAndRecreatedWithNewExtensions...
Unwrapped mint: MintClosedAndRecreatedWithNewExtensions...
Escrow account: 4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3

After closing the stuck escrow, you will need to re-create it using the create-escrow-account command.