diff --git a/program/Cargo.toml b/program/Cargo.toml new file mode 100644 index 00000000..9fd8596c --- /dev/null +++ b/program/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "spl-token-wrap" +version = "0.1.0" +description = "Solana Program Library Token Wrap" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2018" + +[features] +no-entrypoint = [] +test-sbf = [] + +[dependencies] +bytemuck = { version = "1.21.0", features = ["derive"] } +num_enum = "0.7" +solana-program = "2.1.0" +spl-associated-token-account = { version = "6.0.0", features = ["no-entrypoint"] } +spl-token = { version = "7.0", features = ["no-entrypoint"] } +spl-token-2022 = { version = "6.0.0", features = ["no-entrypoint"] } +thiserror = "2.0" + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lints] +workspace = true diff --git a/program/src/entrypoint.rs b/program/src/entrypoint.rs new file mode 100644 index 00000000..62a02f2a --- /dev/null +++ b/program/src/entrypoint.rs @@ -0,0 +1,14 @@ +//! Program entrypoint + +#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] + +use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; + +solana_program::entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + crate::processor::process_instruction(program_id, accounts, instruction_data) +} diff --git a/program/src/instruction.rs b/program/src/instruction.rs new file mode 100644 index 00000000..bca604a9 --- /dev/null +++ b/program/src/instruction.rs @@ -0,0 +1,81 @@ +//! Program instructions + +use num_enum::{IntoPrimitive, TryFromPrimitive}; + +/// Instructions supported by the Token Wrap program +#[derive(Clone, Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] +#[repr(u8)] +pub enum TokenWrapInstruction { + /// Create a wrapped token mint + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writeable,signer]` Funding account for mint and backpointer (must + /// be a system account) + /// 1. `[writeable]` Unallocated wrapped mint account to create, address + /// must be: `get_wrapped_mint_address(unwrapped_mint_address, + /// wrapped_token_program_id)` + /// 2. `[writeable]` Unallocated wrapped backpointer account to create + /// `get_wrapped_mint_backpointer_address(wrapped_mint_address)` + /// 3. `[]` Existing unwrapped mint + /// 4. `[]` System program + /// 5. `[]` SPL Token program for wrapped mint + /// + /// Data expected by this instruction: + /// * bool: true = idempotent creation, false = non-idempotent creation + CreateMint, + + /// Wrap tokens + /// + /// Move a user's unwrapped tokens into an escrow account and mint the same + /// number of wrapped tokens into the provided account. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writeable]` Unwrapped token account to wrap + /// 1. `[writeable]` Escrow of unwrapped tokens, must be owned by: + /// `get_wrapped_mint_authority(wrapped_mint_address)` + /// 2. `[]` Unwrapped token mint + /// 3. `[writeable]` Wrapped mint, must be initialized, address must be: + /// `get_wrapped_mint_address(unwrapped_mint_address, + /// wrapped_token_program_id)` + /// 4. `[writeable]` Recipient wrapped token account + /// 5. `[]` Escrow mint authority, address must be: + /// `get_wrapped_mint_authority(wrapped_mint)` + /// 6. `[]` SPL Token program for unwrapped mint + /// 7. `[]` SPL Token program for wrapped mint + /// 8. `[signer]` Transfer authority on unwrapped token account + /// 9. ..8+M. `[signer]` (Optional) M multisig signers on unwrapped token + /// account + /// + /// Data expected by this instruction: + /// * little-endian u64 representing the amount to wrap + Wrap, + + /// Unwrap tokens + /// + /// Burn user wrapped tokens and transfer the same amount of unwrapped + /// tokens from the escrow account to the provided account. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writeable]` Wrapped token account to unwrap + /// 1. `[writeable]` Wrapped mint, address must be: + /// `get_wrapped_mint_address(unwrapped_mint_address, + /// wrapped_token_program_id)` + /// 2. `[writeable]` Escrow of unwrapped tokens, must be owned by: + /// `get_wrapped_mint_authority(wrapped_mint_address)` + /// 3. `[writeable]` Recipient unwrapped tokens + /// 4. `[]` Unwrapped token mint + /// 5. `[]` Escrow unwrapped token authority + /// `get_wrapped_mint_authority(wrapped_mint)` + /// 6. `[]` SPL Token program for wrapped mint + /// 7. `[]` SPL Token program for unwrapped mint + /// 8. `[signer]` Transfer authority on wrapped token account + /// 9. ..8+M. `[signer]` (Optional) M multisig signers on wrapped token + /// account + /// + /// Data expected by this instruction: + /// * little-endian u64 representing the amount to unwrap + Unwrap, +} diff --git a/program/src/lib.rs b/program/src/lib.rs new file mode 100644 index 00000000..79fda083 --- /dev/null +++ b/program/src/lib.rs @@ -0,0 +1,116 @@ +//! Token Wrap program +#![deny(missing_docs)] +#![forbid(unsafe_code)] + +mod entrypoint; +pub mod instruction; +pub mod processor; +pub mod state; + +// Export current SDK types for downstream users building with a different SDK +// version +pub use solana_program; +use solana_program::pubkey::Pubkey; + +solana_program::declare_id!("TwRapQCDhWkZRrDaHfZGuHxkZ91gHDRkyuzNqeU5MgR"); + +const WRAPPED_MINT_SEED: &[u8] = br"mint"; + +pub(crate) fn get_wrapped_mint_address_with_seed( + unwrapped_mint: &Pubkey, + wrapped_token_program_id: &Pubkey, +) -> (Pubkey, u8) { + Pubkey::find_program_address( + &get_wrapped_mint_seeds(unwrapped_mint, wrapped_token_program_id), + &id(), + ) +} + +pub(crate) fn get_wrapped_mint_seeds<'a>( + unwrapped_mint: &'a Pubkey, + wrapped_token_program_id: &'a Pubkey, +) -> [&'a [u8]; 3] { + [ + WRAPPED_MINT_SEED, + unwrapped_mint.as_ref(), + wrapped_token_program_id.as_ref(), + ] +} + +pub(crate) fn _get_wrapped_mint_signer_seeds<'a>( + unwrapped_mint: &'a Pubkey, + wrapped_token_program_id: &'a Pubkey, + bump_seed: &'a [u8], +) -> [&'a [u8]; 4] { + [ + WRAPPED_MINT_SEED, + unwrapped_mint.as_ref(), + wrapped_token_program_id.as_ref(), + bump_seed, + ] +} + +/// Derive the SPL Token wrapped mint address associated with an unwrapped mint +pub fn get_wrapped_mint_address( + unwrapped_mint: &Pubkey, + wrapped_token_program_id: &Pubkey, +) -> Pubkey { + get_wrapped_mint_address_with_seed(unwrapped_mint, wrapped_token_program_id).0 +} + +const WRAPPED_MINT_AUTHORITY_SEED: &[u8] = br"authority"; + +pub(crate) fn get_wrapped_mint_authority_seeds(wrapped_mint: &Pubkey) -> [&[u8]; 2] { + [WRAPPED_MINT_AUTHORITY_SEED, wrapped_mint.as_ref()] +} + +pub(crate) fn _get_wrapped_mint_authority_signer_seeds<'a>( + wrapped_mint: &'a Pubkey, + bump_seed: &'a [u8], +) -> [&'a [u8]; 3] { + [ + WRAPPED_MINT_AUTHORITY_SEED, + wrapped_mint.as_ref(), + bump_seed, + ] +} + +pub(crate) fn get_wrapped_mint_authority_with_seed(wrapped_mint: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&get_wrapped_mint_authority_seeds(wrapped_mint), &id()) +} + +/// Derive the SPL Token wrapped mint authority address +pub fn get_wrapped_mint_authority(wrapped_mint: &Pubkey) -> Pubkey { + get_wrapped_mint_authority_with_seed(wrapped_mint).0 +} + +const WRAPPED_MINT_BACKPOINTER_SEED: &[u8] = br"backpointer"; + +pub(crate) fn get_wrapped_mint_backpointer_address_seeds(wrapped_mint: &Pubkey) -> [&[u8]; 2] { + [WRAPPED_MINT_BACKPOINTER_SEED, wrapped_mint.as_ref()] +} + +pub(crate) fn _get_wrapped_mint_backpointer_address_signer_seeds<'a>( + wrapped_mint: &'a Pubkey, + bump_seed: &'a [u8], +) -> [&'a [u8]; 3] { + [ + WRAPPED_MINT_BACKPOINTER_SEED, + wrapped_mint.as_ref(), + bump_seed, + ] +} + +pub(crate) fn get_wrapped_mint_backpointer_address_with_seed( + wrapped_mint: &Pubkey, +) -> (Pubkey, u8) { + Pubkey::find_program_address( + &get_wrapped_mint_backpointer_address_seeds(wrapped_mint), + &id(), + ) +} + +/// Derive the SPL Token wrapped mint backpointer address +pub fn get_wrapped_mint_backpointer_address(wrapped_mint: &Pubkey) -> Pubkey { + get_wrapped_mint_backpointer_address_with_seed(wrapped_mint).0 +} diff --git a/program/src/processor.rs b/program/src/processor.rs new file mode 100644 index 00000000..bb0cf1a7 --- /dev/null +++ b/program/src/processor.rs @@ -0,0 +1,26 @@ +//! Program state processor + +use { + crate::instruction::TokenWrapInstruction, + solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}, + spl_token_2022::instruction::decode_instruction_type, +}; + +/// Instruction processor +pub fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + match decode_instruction_type(input)? { + TokenWrapInstruction::CreateMint => { + unimplemented!(); + } + TokenWrapInstruction::Wrap => { + unimplemented!(); + } + TokenWrapInstruction::Unwrap => { + unimplemented!(); + } + } +} diff --git a/program/src/state.rs b/program/src/state.rs new file mode 100644 index 00000000..977c34d6 --- /dev/null +++ b/program/src/state.rs @@ -0,0 +1,24 @@ +//! Program state + +use { + bytemuck::{Pod, Zeroable}, + solana_program::pubkey::Pubkey, +}; + +/// Backpointer +/// +/// Since the backpointer account address is derived from the wrapped mint, it +/// allows clients to easily work with wrapped tokens. +/// +/// Try to fetch the account at `get_wrapped_mint_backpointer_address`. +/// * if it doesn't exist, then the token is not wrapped +/// * if it exists, read the data in the account as the unwrapped mint address +/// +/// With this info, clients can easily unwrap tokens, even if they don't know +/// the origin. +#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)] +#[repr(transparent)] +pub struct Backpointer { + /// Address that the wrapped mint is wrapping + pub unwrapped_mint: Pubkey, +}