diff --git a/interface/Cargo.toml b/interface/Cargo.toml new file mode 100644 index 00000000..ab63d2ea --- /dev/null +++ b/interface/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "token-interface" +version = "0.0.0" +edition = { workspace = true } +readme = "./README.md" +license = { workspace = true } +repository = { workspace = true } +publish = false + +[lib] +crate-type = ["rlib"] + +[dependencies] +pinocchio = { workspace = true } +pinocchio-pubkey = { workspace = true } diff --git a/interface/src/error.rs b/interface/src/error.rs new file mode 100644 index 00000000..68b669ef --- /dev/null +++ b/interface/src/error.rs @@ -0,0 +1,61 @@ +//! Error types + +use pinocchio::program_error::ProgramError; + +/// Errors that may be returned by the Token program. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TokenError { + // 0 + /// Lamport balance below rent-exempt threshold. + NotRentExempt, + /// Insufficient funds for the operation requested. + InsufficientFunds, + /// Invalid Mint. + InvalidMint, + /// Account not associated with this Mint. + MintMismatch, + /// Owner does not match. + OwnerMismatch, + + // 5 + /// This token's supply is fixed and new tokens cannot be minted. + FixedSupply, + /// The account cannot be initialized because it is already being used. + AlreadyInUse, + /// Invalid number of provided signers. + InvalidNumberOfProvidedSigners, + /// Invalid number of required signers. + InvalidNumberOfRequiredSigners, + /// State is uninitialized. + UninitializedState, + + // 10 + /// Instruction does not support native tokens + NativeNotSupported, + /// Non-native account can only be closed if its balance is zero + NonNativeHasBalance, + /// Invalid instruction + InvalidInstruction, + /// State is invalid for requested operation. + InvalidState, + /// Operation overflowed + Overflow, + + // 15 + /// Account does not support specified authority type. + AuthorityTypeNotSupported, + /// This token mint cannot freeze accounts. + MintCannotFreeze, + /// Account is frozen; all account operations will fail + AccountFrozen, + /// Mint decimals mismatch between the client and mint + MintDecimalsMismatch, + /// Instruction does not support non-native tokens + NonNativeNotSupported, +} + +impl From for ProgramError { + fn from(e: TokenError) -> Self { + ProgramError::Custom(e as u32) + } +} diff --git a/interface/src/instruction.rs b/interface/src/instruction.rs new file mode 100644 index 00000000..a829e9a8 --- /dev/null +++ b/interface/src/instruction.rs @@ -0,0 +1,518 @@ +//! Instruction types. + +use pinocchio::{program_error::ProgramError, pubkey::Pubkey}; + +use crate::error::TokenError; + +/// Instructions supported by the token program. +#[repr(C)] +#[derive(Clone, Debug, PartialEq)] +pub enum TokenInstruction<'a> { + /// Initializes a new mint and optionally deposits all the newly minted + /// tokens in an account. + /// + /// The `InitializeMint` instruction requires no signers and MUST be + /// included within the same Transaction as the system program's + /// `CreateAccount` instruction that creates the account being initialized. + /// Otherwise another party can acquire ownership of the uninitialized + /// account. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint to initialize. + /// 1. `[]` Rent sysvar + InitializeMint { + /// Number of base 10 digits to the right of the decimal place. + decimals: u8, + /// The authority/multisignature to mint tokens. + mint_authority: Pubkey, + /// The freeze authority/multisignature of the mint. + freeze_authority: Option, + }, + + /// Initializes a new account to hold tokens. If this account is associated + /// with the native mint then the token balance of the initialized account + /// will be equal to the amount of SOL in the account. If this account is + /// associated with another mint, that mint must be initialized before this + /// command can succeed. + /// + /// The `InitializeAccount` instruction requires no signers and MUST be + /// included within the same Transaction as the system program's + /// `CreateAccount` instruction that creates the account being initialized. + /// Otherwise another party can acquire ownership of the uninitialized + /// account. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The account to initialize. + /// 1. `[]` The mint this account will be associated with. + /// 2. `[]` The new account's owner/multisignature. + /// 3. `[]` Rent sysvar + InitializeAccount, + + /// Initializes a multisignature account with N provided signers. + /// + /// Multisignature accounts can used in place of any single owner/delegate + /// accounts in any token instruction that require an owner/delegate to be + /// present. The variant field represents the number of signers (M) + /// required to validate this multisignature account. + /// + /// The `InitializeMultisig` instruction requires no signers and MUST be + /// included within the same Transaction as the system program's + /// `CreateAccount` instruction that creates the account being initialized. + /// Otherwise another party can acquire ownership of the uninitialized + /// account. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The multisignature account to initialize. + /// 1. `[]` Rent sysvar + /// 2. ..2+N. `[]` The signer accounts, must equal to N where 1 <= N <= + /// 11. + InitializeMultisig { + /// The number of signers (M) required to validate this multisignature + /// account. + m: u8, + }, + + /// Transfers tokens from one account to another either directly or via a + /// delegate. If this account is associated with the native mint then equal + /// amounts of SOL and Tokens will be transferred to the destination + /// account. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[writable]` The destination account. + /// 2. `[signer]` The source account's owner/delegate. + /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[writable]` The destination account. + /// 2. `[]` The source account's multisignature owner/delegate. + /// 3. ..3+M `[signer]` M signer accounts. + Transfer { + /// The amount of tokens to transfer. + amount: u64, + }, + + /// Approves a delegate. A delegate is given the authority over tokens on + /// behalf of the source account's owner. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The source account. + /// 1. `[]` The delegate. + /// 2. `[signer]` The source account owner. + /// + /// * Multisignature owner + /// 0. `[writable]` The source account. + /// 1. `[]` The delegate. + /// 2. `[]` The source account's multisignature owner. + /// 3. ..3+M `[signer]` M signer accounts + Approve { + /// The amount of tokens the delegate is approved for. + amount: u64, + }, + + /// Revokes the delegate's authority. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The source account. + /// 1. `[signer]` The source account owner. + /// + /// * Multisignature owner + /// 0. `[writable]` The source account. + /// 1. `[]` The source account's multisignature owner. + /// 2. ..2+M `[signer]` M signer accounts + Revoke, + + /// Sets a new authority of a mint or account. + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The mint or account to change the authority of. + /// 1. `[signer]` The current authority of the mint or account. + /// + /// * Multisignature authority + /// 0. `[writable]` The mint or account to change the authority of. + /// 1. `[]` The mint's or account's current multisignature authority. + /// 2. ..2+M `[signer]` M signer accounts + SetAuthority { + /// The type of authority to update. + authority_type: AuthorityType, + /// The new authority + new_authority: Option, + }, + + /// Mints new tokens to an account. The native mint does not support + /// minting. + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The mint. + /// 1. `[writable]` The account to mint tokens to. + /// 2. `[signer]` The mint's minting authority. + /// + /// * Multisignature authority + /// 0. `[writable]` The mint. + /// 1. `[writable]` The account to mint tokens to. + /// 2. `[]` The mint's multisignature mint-tokens authority. + /// 3. ..3+M `[signer]` M signer accounts. + MintTo { + /// The amount of new tokens to mint. + amount: u64, + }, + + /// Burns tokens by removing them from an account. `Burn` does not support + /// accounts associated with the native mint, use `CloseAccount` instead. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner/delegate + /// 0. `[writable]` The account to burn from. + /// 1. `[writable]` The token mint. + /// 2. `[signer]` The account's owner/delegate. + /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The account to burn from. + /// 1. `[writable]` The token mint. + /// 2. `[]` The account's multisignature owner/delegate. + /// 3. ..3+M `[signer]` M signer accounts. + Burn { + /// The amount of tokens to burn. + amount: u64, + }, + + /// Close an account by transferring all its SOL to the destination account. + /// Non-native accounts may only be closed if its token amount is zero. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The account to close. + /// 1. `[writable]` The destination account. + /// 2. `[signer]` The account's owner. + /// + /// * Multisignature owner + /// 0. `[writable]` The account to close. + /// 1. `[writable]` The destination account. + /// 2. `[]` The account's multisignature owner. + /// 3. ..3+M `[signer]` M signer accounts. + CloseAccount, + + /// Freeze an Initialized account using the Mint's freeze_authority (if + /// set). + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The account to freeze. + /// 1. `[]` The token mint. + /// 2. `[signer]` The mint freeze authority. + /// + /// * Multisignature owner + /// 0. `[writable]` The account to freeze. + /// 1. `[]` The token mint. + /// 2. `[]` The mint's multisignature freeze authority. + /// 3. ..3+M `[signer]` M signer accounts. + FreezeAccount, + + /// Thaw a Frozen account using the Mint's freeze_authority (if set). + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The account to freeze. + /// 1. `[]` The token mint. + /// 2. `[signer]` The mint freeze authority. + /// + /// * Multisignature owner + /// 0. `[writable]` The account to freeze. + /// 1. `[]` The token mint. + /// 2. `[]` The mint's multisignature freeze authority. + /// 3. ..3+M `[signer]` M signer accounts. + ThawAccount, + + /// Transfers tokens from one account to another either directly or via a + /// delegate. If this account is associated with the native mint then equal + /// amounts of SOL and Tokens will be transferred to the destination + /// account. + /// + /// This instruction differs from Transfer in that the token mint and + /// decimals value is checked by the caller. This may be useful when + /// creating transactions offline or within a hardware wallet. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[]` The token mint. + /// 2. `[writable]` The destination account. + /// 3. `[signer]` The source account's owner/delegate. + /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The source account. + /// 1. `[]` The token mint. + /// 2. `[writable]` The destination account. + /// 3. `[]` The source account's multisignature owner/delegate. + /// 4. ..4+M `[signer]` M signer accounts. + TransferChecked { + /// The amount of tokens to transfer. + amount: u64, + /// Expected number of base 10 digits to the right of the decimal place. + decimals: u8, + }, + + /// Approves a delegate. A delegate is given the authority over tokens on + /// behalf of the source account's owner. + /// + /// This instruction differs from Approve in that the token mint and + /// decimals value is checked by the caller. This may be useful when + /// creating transactions offline or within a hardware wallet. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner + /// 0. `[writable]` The source account. + /// 1. `[]` The token mint. + /// 2. `[]` The delegate. + /// 3. `[signer]` The source account owner. + /// + /// * Multisignature owner + /// 0. `[writable]` The source account. + /// 1. `[]` The token mint. + /// 2. `[]` The delegate. + /// 3. `[]` The source account's multisignature owner. + /// 4. ..4+M `[signer]` M signer accounts + ApproveChecked { + /// The amount of tokens the delegate is approved for. + amount: u64, + /// Expected number of base 10 digits to the right of the decimal place. + decimals: u8, + }, + + /// Mints new tokens to an account. The native mint does not support + /// minting. + /// + /// This instruction differs from MintTo in that the decimals value is + /// checked by the caller. This may be useful when creating transactions + /// offline or within a hardware wallet. + /// + /// Accounts expected by this instruction: + /// + /// * Single authority + /// 0. `[writable]` The mint. + /// 1. `[writable]` The account to mint tokens to. + /// 2. `[signer]` The mint's minting authority. + /// + /// * Multisignature authority + /// 0. `[writable]` The mint. + /// 1. `[writable]` The account to mint tokens to. + /// 2. `[]` The mint's multisignature mint-tokens authority. + /// 3. ..3+M `[signer]` M signer accounts. + MintToChecked { + /// The amount of new tokens to mint. + amount: u64, + /// Expected number of base 10 digits to the right of the decimal place. + decimals: u8, + }, + + /// Burns tokens by removing them from an account. `BurnChecked` does not + /// support accounts associated with the native mint, use `CloseAccount` + /// instead. + /// + /// This instruction differs from Burn in that the decimals value is checked + /// by the caller. This may be useful when creating transactions offline or + /// within a hardware wallet. + /// + /// Accounts expected by this instruction: + /// + /// * Single owner/delegate + /// 0. `[writable]` The account to burn from. + /// 1. `[writable]` The token mint. + /// 2. `[signer]` The account's owner/delegate. + /// + /// * Multisignature owner/delegate + /// 0. `[writable]` The account to burn from. + /// 1. `[writable]` The token mint. + /// 2. `[]` The account's multisignature owner/delegate. + /// 3. ..3+M `[signer]` M signer accounts. + BurnChecked { + /// The amount of tokens to burn. + amount: u64, + /// Expected number of base 10 digits to the right of the decimal place. + decimals: u8, + }, + + /// Like InitializeAccount, but the owner pubkey is passed via instruction + /// data rather than the accounts list. This variant may be preferable + /// when using Cross Program Invocation from an instruction that does + /// not need the owner's `AccountInfo` otherwise. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The account to initialize. + /// 1. `[]` The mint this account will be associated with. + /// 3. `[]` Rent sysvar + InitializeAccount2 { + /// The new account's owner/multisignature. + owner: Pubkey, + }, + + /// Given a wrapped / native token account (a token account containing SOL) + /// updates its amount field based on the account's underlying `lamports`. + /// This is useful if a non-wrapped SOL account uses + /// `system_instruction::transfer` to move lamports to a wrapped token + /// account, and needs to have its token `amount` field updated. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The native token account to sync with its underlying + /// lamports. + SyncNative, + + /// Like InitializeAccount2, but does not require the Rent sysvar to be + /// provided + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The account to initialize. + /// 1. `[]` The mint this account will be associated with. + InitializeAccount3 { + /// The new account's owner/multisignature. + owner: Pubkey, + }, + + /// Like InitializeMultisig, but does not require the Rent sysvar to be + /// provided + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The multisignature account to initialize. + /// 1. ..1+N. `[]` The signer accounts, must equal to N where 1 <= N <= + /// 11. + InitializeMultisig2 { + /// The number of signers (M) required to validate this multisignature + /// account. + m: u8, + }, + + /// Like [`InitializeMint`], but does not require the Rent sysvar to be + /// provided + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The mint to initialize. + InitializeMint2 { + /// Number of base 10 digits to the right of the decimal place. + decimals: u8, + /// The authority/multisignature to mint tokens. + mint_authority: Pubkey, + /// The freeze authority/multisignature of the mint. + freeze_authority: Option, + }, + + /// Gets the required size of an account for the given mint as a + /// little-endian `u64`. + /// + /// Return data can be fetched using `sol_get_return_data` and deserializing + /// the return data as a little-endian `u64`. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[]` The mint to calculate for + GetAccountDataSize, // typically, there's also data, but this program ignores it + + /// Initialize the Immutable Owner extension for the given token account + /// + /// Fails if the account has already been initialized, so must be called + /// before `InitializeAccount`. + /// + /// No-ops in this version of the program, but is included for compatibility + /// with the Associated Token Account program. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` The account to initialize. + /// + /// Data expected by this instruction: + /// None + InitializeImmutableOwner, + + /// Convert an Amount of tokens to a UiAmount `string`, using the given + /// mint. In this version of the program, the mint can only specify the + /// number of decimals. + /// + /// Fails on an invalid mint. + /// + /// Return data can be fetched using `sol_get_return_data` and deserialized + /// with `String::from_utf8`. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[]` The mint to calculate for + AmountToUiAmount { + /// The amount of tokens to reformat. + amount: u64, + }, + + /// Convert a UiAmount of tokens to a little-endian `u64` raw Amount, using + /// the given mint. In this version of the program, the mint can only + /// specify the number of decimals. + /// + /// Return data can be fetched using `sol_get_return_data` and deserializing + /// the return data as a little-endian `u64`. + /// + /// Accounts expected by this instruction: + /// + /// 0. `[]` The mint to calculate for + UiAmountToAmount { + /// The ui_amount of tokens to reformat. + ui_amount: &'a str, + }, + // Any new variants also need to be added to program-2022 `TokenInstruction`, so that the + // latter remains a superset of this instruction set. New variants also need to be added to + // token/js/src/instructions/types.ts to maintain @solana/spl-token compatibility +} + +/// Specifies the authority type for `SetAuthority` instructions +#[repr(u8)] +#[derive(Clone, Debug, PartialEq)] +pub enum AuthorityType { + /// Authority to mint new tokens + MintTokens, + /// Authority to freeze any account associated with the Mint + FreezeAccount, + /// Owner of a given token account + AccountOwner, + /// Authority to close a token account + CloseAccount, +} + +impl AuthorityType { + pub fn into(&self) -> u8 { + match self { + AuthorityType::MintTokens => 0, + AuthorityType::FreezeAccount => 1, + AuthorityType::AccountOwner => 2, + AuthorityType::CloseAccount => 3, + } + } + + pub fn from(index: u8) -> Result { + match index { + 0 => Ok(AuthorityType::MintTokens), + 1 => Ok(AuthorityType::FreezeAccount), + 2 => Ok(AuthorityType::AccountOwner), + 3 => Ok(AuthorityType::CloseAccount), + _ => Err(TokenError::InvalidInstruction.into()), + } + } +} diff --git a/interface/src/lib.rs b/interface/src/lib.rs new file mode 100644 index 00000000..9622a25a --- /dev/null +++ b/interface/src/lib.rs @@ -0,0 +1,8 @@ +pub mod error; +pub mod instruction; +pub mod native_mint; +pub mod state; + +pub mod program { + pinocchio_pubkey::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); +} diff --git a/interface/src/native_mint.rs b/interface/src/native_mint.rs new file mode 100644 index 00000000..a99a5bc6 --- /dev/null +++ b/interface/src/native_mint.rs @@ -0,0 +1,14 @@ +//! The Mint that represents the native token. + +use pinocchio::pubkey::Pubkey; + +/// There are 10^9 lamports in one SOL +pub const DECIMALS: u8 = 9; + +// The Mint for native SOL Token accounts +pub const ID: Pubkey = pinocchio_pubkey::pubkey!("So11111111111111111111111111111111111111112"); + +#[inline(always)] +pub fn is_native_mint(mint: &Pubkey) -> bool { + mint == &ID +} diff --git a/interface/src/state/account.rs b/interface/src/state/account.rs new file mode 100644 index 00000000..0f0ba646 --- /dev/null +++ b/interface/src/state/account.rs @@ -0,0 +1,152 @@ +use pinocchio::pubkey::Pubkey; + +use super::{account_state::AccountState, COption, Initializable, RawType}; + +/// Incinerator address. +const INCINERATOR_ID: Pubkey = + pinocchio_pubkey::pubkey!("1nc1nerator11111111111111111111111111111111"); + +/// System program id. +const SYSTEM_PROGRAM_ID: Pubkey = pinocchio_pubkey::pubkey!("11111111111111111111111111111111"); + +/// Internal representation of a token account data. +#[repr(C)] +pub struct Account { + /// The mint associated with this account + pub mint: Pubkey, + + /// The owner of this account. + pub owner: Pubkey, + + /// The amount of tokens this account holds. + amount: [u8; 8], + + /// If `delegate` is `Some` then `delegated_amount` represents + /// the amount authorized by the delegate. + delegate: COption, + + /// The account's state. + pub state: AccountState, + + /// Indicates whether this account represents a native token or not. + is_native: [u8; 4], + + /// If is_native.is_some, this is a native token, and the value logs the + /// rent-exempt reserve. An Account is required to be rent-exempt, so + /// the value is used by the Processor to ensure that wrapped SOL + /// accounts do not drop below this threshold. + native_amount: [u8; 8], + + /// The amount delegated. + delegated_amount: [u8; 8], + + /// Optional authority to close the account. + close_authority: COption, +} + +impl Account { + #[inline(always)] + pub fn set_amount(&mut self, amount: u64) { + self.amount = amount.to_le_bytes(); + } + + #[inline(always)] + pub fn amount(&self) -> u64 { + u64::from_le_bytes(self.amount) + } + + #[inline(always)] + pub fn clear_delegate(&mut self) { + self.delegate.0[0] = 0; + } + + #[inline(always)] + pub fn set_delegate(&mut self, delegate: &Pubkey) { + self.delegate.0[0] = 1; + self.delegate.1 = *delegate; + } + + #[inline(always)] + pub fn delegate(&self) -> Option<&Pubkey> { + if self.delegate.0[0] == 1 { + Some(&self.delegate.1) + } else { + None + } + } + + #[inline(always)] + pub fn set_native(&mut self, value: bool) { + self.is_native[0] = value as u8; + } + + #[inline(always)] + pub fn is_native(&self) -> bool { + self.is_native[0] == 1 + } + + #[inline(always)] + pub fn set_native_amount(&mut self, amount: u64) { + self.native_amount = amount.to_le_bytes(); + } + + #[inline(always)] + pub fn native_amount(&self) -> Option { + if self.is_native() { + Some(u64::from_le_bytes(self.native_amount)) + } else { + None + } + } + + #[inline(always)] + pub fn set_delegated_amount(&mut self, amount: u64) { + self.delegated_amount = amount.to_le_bytes(); + } + + #[inline(always)] + pub fn delegated_amount(&self) -> u64 { + u64::from_le_bytes(self.delegated_amount) + } + + #[inline(always)] + pub fn clear_close_authority(&mut self) { + self.close_authority.0[0] = 0; + } + + #[inline(always)] + pub fn set_close_authority(&mut self, value: &Pubkey) { + self.close_authority.0[0] = 1; + self.close_authority.1 = *value; + } + + #[inline(always)] + pub fn close_authority(&self) -> Option<&Pubkey> { + if self.close_authority.0[0] == 1 { + Some(&self.close_authority.1) + } else { + None + } + } + + #[inline(always)] + pub fn is_frozen(&self) -> bool { + self.state == AccountState::Frozen + } + + #[inline(always)] + pub fn is_owned_by_system_program_or_incinerator(&self) -> bool { + SYSTEM_PROGRAM_ID == self.owner || INCINERATOR_ID == self.owner + } +} + +impl RawType for Account { + const LEN: usize = core::mem::size_of::(); +} + +impl Initializable for Account { + #[inline(always)] + fn is_initialized(&self) -> bool { + self.state != AccountState::Uninitialized + } +} diff --git a/interface/src/state/account_state.rs b/interface/src/state/account_state.rs new file mode 100644 index 00000000..67477577 --- /dev/null +++ b/interface/src/state/account_state.rs @@ -0,0 +1,15 @@ +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AccountState { + /// Account is not yet initialized + Uninitialized, + + /// Account is initialized; the account owner and/or delegate may perform + /// permitted operations on this account + Initialized, + + /// Account has been frozen by the mint freeze authority. Neither the + /// account owner nor the delegate are able to perform operations on + /// this account. + Frozen, +} diff --git a/interface/src/state/mint.rs b/interface/src/state/mint.rs new file mode 100644 index 00000000..3b4d41b0 --- /dev/null +++ b/interface/src/state/mint.rs @@ -0,0 +1,96 @@ +use pinocchio::pubkey::Pubkey; + +use super::{COption, Initializable, RawType}; + +/// Internal representation of a mint data. +#[repr(C)] +pub struct Mint { + /// Optional authority used to mint new tokens. The mint authority may only + /// be provided during mint creation. If no mint authority is present + /// then the mint has a fixed supply and no further tokens may be + /// minted. + pub mint_authority: COption, + + /// Total supply of tokens. + supply: [u8; 8], + + /// Number of base 10 digits to the right of the decimal place. + pub decimals: u8, + + /// Is `true` if this structure has been initialized. + is_initialized: u8, + + // Indicates whether the freeze authority is present or not. + //freeze_authority_option: [u8; 4], + /// Optional authority to freeze token accounts. + pub freeze_authority: COption, +} + +impl Mint { + #[inline(always)] + pub fn set_supply(&mut self, supply: u64) { + self.supply = supply.to_le_bytes(); + } + + #[inline(always)] + pub fn supply(&self) -> u64 { + u64::from_le_bytes(self.supply) + } + + #[inline(always)] + pub fn set_initialized(&mut self, value: bool) { + self.is_initialized = value as u8; + } + + #[inline(always)] + pub fn clear_mint_authority(&mut self) { + self.mint_authority.0[0] = 0; + } + + #[inline(always)] + pub fn set_mint_authority(&mut self, mint_authority: &Pubkey) { + self.mint_authority.0[0] = 1; + self.mint_authority.1 = *mint_authority; + } + + #[inline(always)] + pub fn mint_authority(&self) -> Option<&Pubkey> { + if self.mint_authority.0[0] == 1 { + Some(&self.mint_authority.1) + } else { + None + } + } + + #[inline(always)] + pub fn clear_freeze_authority(&mut self) { + self.freeze_authority.0[0] = 0; + } + + #[inline(always)] + pub fn set_freeze_authority(&mut self, freeze_authority: &Pubkey) { + self.freeze_authority.0[0] = 1; + self.freeze_authority.1 = *freeze_authority; + } + + #[inline(always)] + pub fn freeze_authority(&self) -> Option<&Pubkey> { + if self.freeze_authority.0[0] == 1 { + Some(&self.freeze_authority.1) + } else { + None + } + } +} + +impl RawType for Mint { + /// The length of the `Mint` account data. + const LEN: usize = core::mem::size_of::(); +} + +impl Initializable for Mint { + #[inline(always)] + fn is_initialized(&self) -> bool { + self.is_initialized == 1 + } +} diff --git a/interface/src/state/mod.rs b/interface/src/state/mod.rs new file mode 100644 index 00000000..b7c35df6 --- /dev/null +++ b/interface/src/state/mod.rs @@ -0,0 +1,92 @@ +use pinocchio::program_error::ProgramError; + +pub mod account; +pub mod account_state; +pub mod mint; +pub mod multisig; + +/// Type alias for fields represented as `COption`. +pub type COption = ([u8; 4], T); + +/// Marker trait for types that can cast from a raw pointer. +/// +/// It is up to the type implementing this trait to guarantee that the cast is safe, +/// i.e., that the fields of the type are well aligned and there are no padding bytes. +pub trait RawType { + /// The length of the type. + /// + /// This must be equal to the size of each individual field in the type. + const LEN: usize; +} + +/// Trait to represent a type that can be initialized. +pub trait Initializable { + /// Return `true` if the object is initialized. + fn is_initialized(&self) -> bool; +} + +/// Return a reference for an initialized `T` from the given bytes. +/// +/// # Safety +/// +/// The caller must ensure that `bytes` contains a valid representation of `T`. +#[inline(always)] +pub unsafe fn load(bytes: &[u8]) -> Result<&T, ProgramError> { + load_unchecked(bytes).and_then(|t: &T| { + // checks if the data is initialized + if t.is_initialized() { + Ok(t) + } else { + Err(ProgramError::UninitializedAccount) + } + }) +} + +/// Return a `T` reference from the given bytes. +/// +/// This function does not check if the data is initialized. +/// +/// # Safety +/// +/// The caller must ensure that `bytes` contains a valid representation of `T`. +#[inline(always)] +pub unsafe fn load_unchecked(bytes: &[u8]) -> Result<&T, ProgramError> { + if bytes.len() != T::LEN { + return Err(ProgramError::InvalidAccountData); + } + Ok(&*(bytes.as_ptr() as *const T)) +} + +/// Return a mutable reference for an initialized `T` from the given bytes. +/// +/// # Safety +/// +/// The caller must ensure that `bytes` contains a valid representation of `T`. +#[inline(always)] +pub unsafe fn load_mut( + bytes: &mut [u8], +) -> Result<&mut T, ProgramError> { + load_mut_unchecked(bytes).and_then(|t: &mut T| { + // checks if the data is initialized + if t.is_initialized() { + Ok(t) + } else { + Err(ProgramError::UninitializedAccount) + } + }) +} + +/// Return a mutable `T` reference from the given bytes. +/// +/// This function does not check if the data is initialized. +/// +/// # Safety +/// +/// The caller must ensure that `bytes` contains a valid representation of `T`. +#[inline(always)] +pub unsafe fn load_mut_unchecked(bytes: &mut [u8]) -> Result<&mut T, ProgramError> { + if bytes.len() != T::LEN { + return Err(ProgramError::InvalidAccountData); + } + Ok(&mut *(bytes.as_mut_ptr() as *mut T)) +} diff --git a/interface/src/state/multisig.rs b/interface/src/state/multisig.rs new file mode 100644 index 00000000..920da97a --- /dev/null +++ b/interface/src/state/multisig.rs @@ -0,0 +1,49 @@ +use pinocchio::pubkey::Pubkey; + +use super::{Initializable, RawType}; + +/// Minimum number of multisignature signers (min N) +pub const MIN_SIGNERS: usize = 1; + +/// Maximum number of multisignature signers (max N) +pub const MAX_SIGNERS: usize = 11; + +/// Multisignature data. +#[repr(C)] +pub struct Multisig { + /// Number of signers required. + pub m: u8, + + /// Number of valid signers. + pub n: u8, + + /// Is `true` if this structure has been initialized + is_initialized: u8, + + /// Signer public keys + pub signers: [Pubkey; MAX_SIGNERS], +} + +impl Multisig { + /// Utility function that checks index is between [`MIN_SIGNERS`] and [`MAX_SIGNERS`]. + pub fn is_valid_signer_index(index: usize) -> bool { + (MIN_SIGNERS..=MAX_SIGNERS).contains(&index) + } + + #[inline] + pub fn set_initialized(&mut self, value: bool) { + self.is_initialized = value as u8; + } +} + +impl RawType for Multisig { + /// The length of the `Mint` account data. + const LEN: usize = core::mem::size_of::(); +} + +impl Initializable for Multisig { + #[inline(always)] + fn is_initialized(&self) -> bool { + self.is_initialized == 1 + } +} diff --git a/p-token/Cargo.toml b/p-token/Cargo.toml new file mode 100644 index 00000000..d527968e --- /dev/null +++ b/p-token/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "token-program" +version = "0.0.0" +edition = { workspace = true } +readme = "./README.md" +license = { workspace = true } +repository = { workspace = true } +publish = false + +[package.metadata.solana] +program-id = "PToken1111111111111111111111111111111111111" + +[lib] +crate-type = ["cdylib"] + +[features] +logging = [] +test-sbf = [] + +[dependencies] +pinocchio = { workspace = true } +pinocchio-log = { workspace = true } +token-interface = { version = "^0", path = "../interface" } + +[dev-dependencies] +assert_matches = "1.5.0" +solana-program-test = "~1.18" +solana-sdk = "~1.18" +spl-token = { version="^4", features=["no-entrypoint"] } +test-case = "3.3.1" diff --git a/p-token/README.md b/p-token/README.md new file mode 100644 index 00000000..4fa737cd --- /dev/null +++ b/p-token/README.md @@ -0,0 +1,3 @@ +# `p-token` + +A `pinocchio`-based Token program. diff --git a/p-token/keypair.json b/p-token/keypair.json new file mode 100644 index 00000000..30925386 --- /dev/null +++ b/p-token/keypair.json @@ -0,0 +1 @@ +[178,215,114,55,146,0,60,153,90,63,112,26,148,148,111,230,196,181,5,124,14,237,142,43,207,114,102,60,145,103,53,23,249,192,123,198,160,247,138,44,243,38,29,240,233,86,143,132,170,26,154,207,174,195,147,223,12,231,253,195,118,55,207,100] \ No newline at end of file diff --git a/p-token/src/entrypoint.rs b/p-token/src/entrypoint.rs new file mode 100644 index 00000000..07808293 --- /dev/null +++ b/p-token/src/entrypoint.rs @@ -0,0 +1,234 @@ +use pinocchio::{ + account_info::AccountInfo, default_panic_handler, no_allocator, program_entrypoint, + program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +use crate::processor::*; + +program_entrypoint!(process_instruction); +// Do not allocate memory. +no_allocator!(); +// Use the default panic handler. +default_panic_handler!(); + +/// Process an instruction. +/// +/// The processor of the token program is divided into two parts to reduce the overhead +/// of having a large `match` statement. The first part of the processor handles the +/// most common instructions, while the second part handles the remaining instructions. +/// The rationale is to reduce the overhead of making multiple comparisons for popular +/// instructions. +/// +/// Instructions on the first part of the processor: +/// +/// - `0`: `InitializeMint` +/// - `3`: `Transfer` +/// - `7`: `MintTo` +/// - `9`: `CloseAccount` +/// - `18`: `InitializeAccount3` +/// - `20`: `InitializeMint2` +#[inline(always)] +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let (discriminator, instruction_data) = instruction_data + .split_first() + .ok_or(ProgramError::InvalidInstructionData)?; + + match *discriminator { + // 0 - InitializeMint + 0 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeMint"); + + process_initialize_mint(accounts, instruction_data, true) + } + + // 3 - Transfer + 3 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: Transfer"); + + process_transfer(accounts, instruction_data) + } + // 7 - MintTo + 7 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: MintTo"); + + process_mint_to(accounts, instruction_data) + } + // 9 - CloseAccount + 9 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: CloseAccount"); + + process_close_account(accounts) + } + // 18 - InitializeAccount3 + 18 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeAccount3"); + + process_initialize_account3(accounts, instruction_data) + } + // 20 - InitializeMint2 + 20 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeMint2"); + + process_initialize_mint2(accounts, instruction_data) + } + _ => process_remaining_instruction(accounts, instruction_data, *discriminator), + } +} + +/// Process the remaining instructions. +/// +/// This function is called by the `process_instruction` function if the discriminator +/// does not match any of the common instructions. This function is used to reduce the +/// overhead of having a large `match` statement in the `process_instruction` function. +fn process_remaining_instruction( + accounts: &[AccountInfo], + instruction_data: &[u8], + discriminator: u8, +) -> ProgramResult { + match discriminator { + // 1 - InitializeAccount + 1 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeAccount"); + + process_initialize_account(accounts) + } + // 2 - InitializeMultisig + 2 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeMultisig"); + + process_initialize_multisig(accounts, instruction_data) + } + // 4 - Approve + 4 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: Approve"); + + process_approve(accounts, instruction_data) + } + // 5 - Revoke + 5 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: Revoke"); + + process_revoke(accounts, instruction_data) + } + // 6 - SetAuthority + 6 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: SetAuthority"); + + process_set_authority(accounts, instruction_data) + } + // 8 - Burn + 8 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: Burn"); + + process_burn(accounts, instruction_data) + } + // 10 - FreezeAccount + 10 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: FreezeAccount"); + + process_freeze_account(accounts) + } + // 11 - ThawAccount + 11 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: ThawAccount"); + + process_thaw_account(accounts) + } + // 12 - TransferChecked + 12 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: TransferChecked"); + + process_transfer_checked(accounts, instruction_data) + } + // 13 - ApproveChecked + 13 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: ApproveChecked"); + + process_approve_checked(accounts, instruction_data) + } + // 14 - MintToChecked + 14 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: MintToChecked"); + + process_mint_to_checked(accounts, instruction_data) + } + // 15 - BurnChecked + 15 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: BurnChecked"); + + process_burn_checked(accounts, instruction_data) + } + // 16 - InitializeAccount2 + 16 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeAccount2"); + + process_initialize_account2(accounts, instruction_data) + } + // 17 - SyncNative + 17 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: SyncNative"); + + process_sync_native(accounts) + } + // 19 - InitializeMultisig2 + 19 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeMultisig2"); + + process_initialize_multisig2(accounts, instruction_data) + } + // 21 - GetAccountDataSize + 21 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: GetAccountDataSize"); + + process_get_account_data_size(accounts) + } + // 22 - InitializeImmutableOwner + 22 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: InitializeImmutableOwner"); + + process_initialize_immutable_owner(accounts) + } + // 23 - AmountToUiAmount + 23 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: AmountToUiAmount"); + + process_amount_to_ui_amount(accounts, instruction_data) + } + // 24 - UiAmountToAmount + 24 => { + #[cfg(feature = "logging")] + pinocchio::msg!("Instruction: UiAmountToAmount"); + + process_ui_amount_to_amount(accounts, instruction_data) + } + _ => Err(ProgramError::InvalidInstructionData), + } +} diff --git a/p-token/src/lib.rs b/p-token/src/lib.rs new file mode 100644 index 00000000..0bd44399 --- /dev/null +++ b/p-token/src/lib.rs @@ -0,0 +1,6 @@ +//! Another ERC20-like Token program for the Solana blockchain. + +#![no_std] + +mod entrypoint; +mod processor; diff --git a/p-token/src/processor/amount_to_ui_amount.rs b/p-token/src/processor/amount_to_ui_amount.rs new file mode 100644 index 00000000..6c139938 --- /dev/null +++ b/p-token/src/processor/amount_to_ui_amount.rs @@ -0,0 +1,47 @@ +use core::str::from_utf8_unchecked; +use pinocchio::{ + account_info::AccountInfo, program::set_return_data, program_error::ProgramError, ProgramResult, +}; +use pinocchio_log::logger::{Argument, Logger}; +use token_interface::{ + error::TokenError, + state::{load, mint::Mint}, +}; + +use super::{check_account_owner, MAX_FORMATTED_DIGITS}; + +#[inline(always)] +pub fn process_amount_to_ui_amount( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let amount = u64::from_le_bytes( + instruction_data + .try_into() + .map_err(|_error| ProgramError::InvalidInstructionData)?, + ); + + let mint_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; + check_account_owner(mint_info)?; + // SAFETY: single immutable borrow to `mint_info` account data and + // `load` validates that the mint is initialized. + let mint = unsafe { + load::(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)? + }; + + let mut logger = Logger::::default(); + logger.append_with_args(amount, &[Argument::Precision(mint.decimals)]); + // "Extract" the formatted string from the logger. + // + // SAFETY: the logger is guaranteed to be a valid UTF-8 string. + let mut s = unsafe { from_utf8_unchecked(&logger) }; + + if mint.decimals > 0 && s.contains('.') { + let zeros_trimmed = s.trim_end_matches('0'); + s = zeros_trimmed.trim_end_matches('.'); + } + + set_return_data(s.as_bytes()); + + Ok(()) +} diff --git a/p-token/src/processor/approve.rs b/p-token/src/processor/approve.rs new file mode 100644 index 00000000..10c61ede --- /dev/null +++ b/p-token/src/processor/approve.rs @@ -0,0 +1,14 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_approve(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + let amount = u64::from_le_bytes( + instruction_data + .try_into() + .map_err(|_error| ProgramError::InvalidInstructionData)?, + ); + + shared::approve::process_approve(accounts, amount, None) +} diff --git a/p-token/src/processor/approve_checked.rs b/p-token/src/processor/approve_checked.rs new file mode 100644 index 00000000..580a93d4 --- /dev/null +++ b/p-token/src/processor/approve_checked.rs @@ -0,0 +1,19 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_approve_checked(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + let (amount, decimals) = instruction_data.split_at(core::mem::size_of::()); + let amount = u64::from_le_bytes( + amount + .try_into() + .map_err(|_error| ProgramError::InvalidInstructionData)?, + ); + + shared::approve::process_approve( + accounts, + amount, + Some(*decimals.first().ok_or(ProgramError::InvalidAccountData)?), + ) +} diff --git a/p-token/src/processor/burn.rs b/p-token/src/processor/burn.rs new file mode 100644 index 00000000..6ceed84a --- /dev/null +++ b/p-token/src/processor/burn.rs @@ -0,0 +1,14 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_burn(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + let amount = u64::from_le_bytes( + instruction_data + .try_into() + .map_err(|_error| ProgramError::InvalidInstructionData)?, + ); + + shared::burn::process_burn(accounts, amount, None) +} diff --git a/p-token/src/processor/burn_checked.rs b/p-token/src/processor/burn_checked.rs new file mode 100644 index 00000000..88a745d3 --- /dev/null +++ b/p-token/src/processor/burn_checked.rs @@ -0,0 +1,23 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_burn_checked(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + // expected u64 (8) + u8 (1) + let (amount, decimals) = if instruction_data.len() == 9 { + let (amount, decimals) = instruction_data.split_at(core::mem::size_of::()); + ( + u64::from_le_bytes( + amount + .try_into() + .map_err(|_error| ProgramError::InvalidInstructionData)?, + ), + decimals.first(), + ) + } else { + return Err(ProgramError::InvalidInstructionData); + }; + + shared::burn::process_burn(accounts, amount, decimals.copied()) +} diff --git a/p-token/src/processor/close_account.rs b/p-token/src/processor/close_account.rs new file mode 100644 index 00000000..3f90dea0 --- /dev/null +++ b/p-token/src/processor/close_account.rs @@ -0,0 +1,63 @@ +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; +use token_interface::{ + error::TokenError, + state::{account::Account, load}, +}; + +use super::validate_owner; + +/// Incinerator (`1nc1nerator11111111111111111111111111111111`) address. +const INCINERATOR_ID: Pubkey = [ + 0, 51, 144, 114, 141, 52, 17, 96, 121, 189, 201, 17, 191, 255, 0, 219, 212, 77, 46, 205, 204, + 247, 156, 166, 225, 0, 56, 225, 0, 0, 0, 0, +]; + +#[inline(always)] +pub fn process_close_account(accounts: &[AccountInfo]) -> ProgramResult { + let [source_account_info, destination_account_info, authority_info, remaining @ ..] = accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Comparing whether the AccountInfo's "point" to the same account or + // not - this is a faster comparison since it just checks the internal + // raw pointer. + if source_account_info == destination_account_info { + return Err(ProgramError::InvalidAccountData); + } else { + // SAFETY: scoped immutable borrow to `source_account_info` account data and + // `load` validates that the account is initialized. + let source_account = + unsafe { load::(source_account_info.borrow_data_unchecked())? }; + + if !source_account.is_native() && source_account.amount() != 0 { + return Err(TokenError::NonNativeHasBalance.into()); + } + + let authority = source_account + .close_authority() + .unwrap_or(&source_account.owner); + + if !source_account.is_owned_by_system_program_or_incinerator() { + validate_owner(authority, authority_info, remaining)?; + } else if destination_account_info.key() != &INCINERATOR_ID { + return Err(ProgramError::InvalidAccountData); + } + } + + let destination_starting_lamports = destination_account_info.lamports(); + // SAFETY: single mutable borrow to `destination_account_info` lamports and + // there are no "active" borrows of `source_account_info` account data. + unsafe { + // Moves the lamports to the destination account. + *destination_account_info.borrow_mut_lamports_unchecked() = destination_starting_lamports + .checked_add(source_account_info.lamports()) + .ok_or(TokenError::Overflow)?; + // Closes the source account. + source_account_info.close_unchecked(); + } + + Ok(()) +} diff --git a/p-token/src/processor/freeze_account.rs b/p-token/src/processor/freeze_account.rs new file mode 100644 index 00000000..56376110 --- /dev/null +++ b/p-token/src/processor/freeze_account.rs @@ -0,0 +1,8 @@ +use pinocchio::{account_info::AccountInfo, ProgramResult}; + +use super::shared::toggle_account_state::process_toggle_account_state; + +#[inline(always)] +pub fn process_freeze_account(accounts: &[AccountInfo]) -> ProgramResult { + process_toggle_account_state(accounts, true) +} diff --git a/p-token/src/processor/get_account_data_size.rs b/p-token/src/processor/get_account_data_size.rs new file mode 100644 index 00000000..7693b641 --- /dev/null +++ b/p-token/src/processor/get_account_data_size.rs @@ -0,0 +1,29 @@ +use pinocchio::{ + account_info::AccountInfo, program::set_return_data, program_error::ProgramError, ProgramResult, +}; +use token_interface::{ + error::TokenError, + state::{account::Account, load, mint::Mint, RawType}, +}; + +use super::check_account_owner; + +#[inline(always)] +pub fn process_get_account_data_size(accounts: &[AccountInfo]) -> ProgramResult { + let [mint_info, _remaning @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Make sure the mint is valid. + check_account_owner(mint_info)?; + + // SAFETY: single immutable borrow to `mint_info` account data and + // `load` validates that the mint is initialized. + let _ = unsafe { + load::(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)? + }; + + set_return_data(&Account::LEN.to_le_bytes()); + + Ok(()) +} diff --git a/p-token/src/processor/initialize_account.rs b/p-token/src/processor/initialize_account.rs new file mode 100644 index 00000000..2c125096 --- /dev/null +++ b/p-token/src/processor/initialize_account.rs @@ -0,0 +1,8 @@ +use pinocchio::{account_info::AccountInfo, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_initialize_account(accounts: &[AccountInfo]) -> ProgramResult { + shared::initialize_account::process_initialize_account(accounts, None, true) +} diff --git a/p-token/src/processor/initialize_account2.rs b/p-token/src/processor/initialize_account2.rs new file mode 100644 index 00000000..185c291b --- /dev/null +++ b/p-token/src/processor/initialize_account2.rs @@ -0,0 +1,24 @@ +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + pubkey::{Pubkey, PUBKEY_BYTES}, + ProgramResult, +}; + +use super::shared; + +#[inline(always)] +pub fn process_initialize_account2( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // SAFETY: validate `instruction_data` length. + let owner = unsafe { + if instruction_data.len() != PUBKEY_BYTES { + return Err(ProgramError::InvalidInstructionData); + } else { + &*(instruction_data.as_ptr() as *const Pubkey) + } + }; + shared::initialize_account::process_initialize_account(accounts, Some(owner), true) +} diff --git a/p-token/src/processor/initialize_account3.rs b/p-token/src/processor/initialize_account3.rs new file mode 100644 index 00000000..54e55014 --- /dev/null +++ b/p-token/src/processor/initialize_account3.rs @@ -0,0 +1,24 @@ +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + pubkey::{Pubkey, PUBKEY_BYTES}, + ProgramResult, +}; + +use super::shared; + +#[inline(always)] +pub fn process_initialize_account3( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // SAFETY: validate `instruction_data` length. + let owner = unsafe { + if instruction_data.len() != PUBKEY_BYTES { + return Err(ProgramError::InvalidInstructionData); + } else { + &*(instruction_data.as_ptr() as *const Pubkey) + } + }; + shared::initialize_account::process_initialize_account(accounts, Some(owner), false) +} diff --git a/p-token/src/processor/initialize_immutable_owner.rs b/p-token/src/processor/initialize_immutable_owner.rs new file mode 100644 index 00000000..ba98c284 --- /dev/null +++ b/p-token/src/processor/initialize_immutable_owner.rs @@ -0,0 +1,19 @@ +use pinocchio::{account_info::AccountInfo, msg, program_error::ProgramError, ProgramResult}; +use token_interface::{ + error::TokenError, + state::{account::Account, load_unchecked, Initializable}, +}; + +#[inline(always)] +pub fn process_initialize_immutable_owner(accounts: &[AccountInfo]) -> ProgramResult { + let token_account_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; + + // SAFETY: single immutable borrow to `token_account_info` account data. + let account = unsafe { load_unchecked::(token_account_info.borrow_data_unchecked())? }; + + if account.is_initialized() { + return Err(TokenError::AlreadyInUse.into()); + } + msg!("Please upgrade to SPL Token 2022 for immutable owner support"); + Ok(()) +} diff --git a/p-token/src/processor/initialize_mint.rs b/p-token/src/processor/initialize_mint.rs new file mode 100644 index 00000000..ee52eb41 --- /dev/null +++ b/p-token/src/processor/initialize_mint.rs @@ -0,0 +1,120 @@ +use core::{marker::PhantomData, mem::size_of}; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + pubkey::Pubkey, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; +use token_interface::{ + error::TokenError, + state::{load_mut_unchecked, mint::Mint, Initializable}, +}; + +#[inline(always)] +pub fn process_initialize_mint( + accounts: &[AccountInfo], + instruction_data: &[u8], + rent_sysvar_account: bool, +) -> ProgramResult { + // Validates the instruction data. + + let args = InitializeMint::try_from_bytes(instruction_data)?; + + // Validates the accounts. + + let (mint_info, rent_sysvar_info) = if rent_sysvar_account { + let [mint_info, rent_sysvar_info, _remaining @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + (mint_info, Some(rent_sysvar_info)) + } else { + let [mint_info, _remaining @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + (mint_info, None) + }; + + // SAFETY: single mutable borrow to `mint_info` account data. + let mint = unsafe { load_mut_unchecked::(mint_info.borrow_mut_data_unchecked())? }; + + if mint.is_initialized() { + return Err(TokenError::AlreadyInUse.into()); + } + + // Check rent-exempt status of the mint account. + + let is_exempt = if let Some(rent_sysvar_info) = rent_sysvar_info { + // SAFETY: single immutable borrow to `rent_sysvar_info`; account ID and length are + // checked by `from_account_info_unchecked`. + let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar_info)? }; + rent.is_exempt(mint_info.lamports(), size_of::()) + } else { + Rent::get()?.is_exempt(mint_info.lamports(), size_of::()) + }; + + if !is_exempt { + return Err(TokenError::NotRentExempt.into()); + } + + // Initialize the mint. + + mint.set_initialized(true); + mint.set_mint_authority(args.mint_authority()); + mint.decimals = args.decimals(); + + if let Some(freeze_authority) = args.freeze_authority() { + mint.set_freeze_authority(freeze_authority); + } + + Ok(()) +} + +/// Instruction data for the `InitializeMint` instruction. +pub struct InitializeMint<'a> { + raw: *const u8, + + _data: PhantomData<&'a [u8]>, +} + +impl InitializeMint<'_> { + #[inline] + pub fn try_from_bytes(bytes: &[u8]) -> Result { + // The minimum expected size of the instruction data. + // - decimals (1 byte) + // - mint_authority (32 bytes) + // - option + freeze_authority (1 byte + 32 bytes) + if bytes.len() < 34 || (bytes[33] == 1 && bytes.len() < 66) { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(InitializeMint { + raw: bytes.as_ptr(), + _data: PhantomData, + }) + } + + #[inline] + pub fn decimals(&self) -> u8 { + // SAFETY: the `bytes` length was validated in `try_from_bytes`. + unsafe { *self.raw } + } + + #[inline] + pub fn mint_authority(&self) -> &Pubkey { + // SAFETY: the `bytes` length was validated in `try_from_bytes`. + unsafe { &*(self.raw.add(1) as *const Pubkey) } + } + + #[inline] + pub fn freeze_authority(&self) -> Option<&Pubkey> { + // SAFETY: the `bytes` length was validated in `try_from_bytes`. + unsafe { + if *self.raw.add(33) == 0 { + Option::None + } else { + Option::Some(&*(self.raw.add(34) as *const Pubkey)) + } + } + } +} diff --git a/p-token/src/processor/initialize_mint2.rs b/p-token/src/processor/initialize_mint2.rs new file mode 100644 index 00000000..0f1f07d2 --- /dev/null +++ b/p-token/src/processor/initialize_mint2.rs @@ -0,0 +1,11 @@ +use pinocchio::{account_info::AccountInfo, ProgramResult}; + +use super::initialize_mint::process_initialize_mint; + +#[inline(always)] +pub fn process_initialize_mint2( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + process_initialize_mint(accounts, instruction_data, false) +} diff --git a/p-token/src/processor/initialize_multisig.rs b/p-token/src/processor/initialize_multisig.rs new file mode 100644 index 00000000..a5f888bc --- /dev/null +++ b/p-token/src/processor/initialize_multisig.rs @@ -0,0 +1,15 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_initialize_multisig( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let m = instruction_data + .first() + .ok_or(ProgramError::InvalidInstructionData)?; + + shared::initialize_multisig::process_initialize_multisig(accounts, *m, true) +} diff --git a/p-token/src/processor/initialize_multisig2.rs b/p-token/src/processor/initialize_multisig2.rs new file mode 100644 index 00000000..138a91b9 --- /dev/null +++ b/p-token/src/processor/initialize_multisig2.rs @@ -0,0 +1,14 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_initialize_multisig2( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let m = instruction_data + .first() + .ok_or(ProgramError::InvalidInstructionData)?; + shared::initialize_multisig::process_initialize_multisig(accounts, *m, false) +} diff --git a/p-token/src/processor/mint_to.rs b/p-token/src/processor/mint_to.rs new file mode 100644 index 00000000..59db5335 --- /dev/null +++ b/p-token/src/processor/mint_to.rs @@ -0,0 +1,14 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_mint_to(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + let amount = u64::from_le_bytes( + instruction_data + .try_into() + .map_err(|_error| ProgramError::InvalidInstructionData)?, + ); + + shared::mint_to::process_mint_to(accounts, amount, None) +} diff --git a/p-token/src/processor/mint_to_checked.rs b/p-token/src/processor/mint_to_checked.rs new file mode 100644 index 00000000..ebfcdea4 --- /dev/null +++ b/p-token/src/processor/mint_to_checked.rs @@ -0,0 +1,23 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_mint_to_checked(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + // expected u64 (8) + u8 (1) + let (amount, decimals) = if instruction_data.len() == 9 { + let (amount, decimals) = instruction_data.split_at(core::mem::size_of::()); + ( + u64::from_le_bytes( + amount + .try_into() + .map_err(|_error| ProgramError::InvalidInstructionData)?, + ), + decimals.first(), + ) + } else { + return Err(ProgramError::InvalidInstructionData); + }; + + shared::mint_to::process_mint_to(accounts, amount, decimals.copied()) +} diff --git a/p-token/src/processor/mod.rs b/p-token/src/processor/mod.rs new file mode 100644 index 00000000..0f0592f5 --- /dev/null +++ b/p-token/src/processor/mod.rs @@ -0,0 +1,206 @@ +use core::{ + cmp::max, + mem::MaybeUninit, + slice::{from_raw_parts, from_raw_parts_mut}, + str::from_utf8_unchecked, +}; +use pinocchio::{ + account_info::AccountInfo, memory::sol_memcpy, program_error::ProgramError, pubkey::Pubkey, + ProgramResult, +}; +use token_interface::{ + error::TokenError, + program::ID as TOKEN_PROGRAM_ID, + state::{ + load, + multisig::{Multisig, MAX_SIGNERS}, + RawType, + }, +}; + +pub mod amount_to_ui_amount; +pub mod approve; +pub mod approve_checked; +pub mod burn; +pub mod burn_checked; +pub mod close_account; +pub mod freeze_account; +pub mod get_account_data_size; +pub mod initialize_account; +pub mod initialize_account2; +pub mod initialize_account3; +pub mod initialize_immutable_owner; +pub mod initialize_mint; +pub mod initialize_mint2; +pub mod initialize_multisig; +pub mod initialize_multisig2; +pub mod mint_to; +pub mod mint_to_checked; +pub mod revoke; +pub mod set_authority; +pub mod sync_native; +pub mod thaw_account; +pub mod transfer; +pub mod transfer_checked; +pub mod ui_amount_to_amount; +// Shared processors. +pub mod shared; + +pub use amount_to_ui_amount::process_amount_to_ui_amount; +pub use approve::process_approve; +pub use approve_checked::process_approve_checked; +pub use burn::process_burn; +pub use burn_checked::process_burn_checked; +pub use close_account::process_close_account; +pub use freeze_account::process_freeze_account; +pub use get_account_data_size::process_get_account_data_size; +pub use initialize_account::process_initialize_account; +pub use initialize_account2::process_initialize_account2; +pub use initialize_account3::process_initialize_account3; +pub use initialize_immutable_owner::process_initialize_immutable_owner; +pub use initialize_mint::process_initialize_mint; +pub use initialize_mint2::process_initialize_mint2; +pub use initialize_multisig::process_initialize_multisig; +pub use initialize_multisig2::process_initialize_multisig2; +pub use mint_to::process_mint_to; +pub use mint_to_checked::process_mint_to_checked; +pub use revoke::process_revoke; +pub use set_authority::process_set_authority; +pub use sync_native::process_sync_native; +pub use thaw_account::process_thaw_account; +pub use transfer::process_transfer; +pub use transfer_checked::process_transfer_checked; +pub use ui_amount_to_amount::process_ui_amount_to_amount; + +/// An uninitialized byte. +const UNINIT_BYTE: MaybeUninit = MaybeUninit::uninit(); + +/// Maximum number of digits in a formatted `u64`. +/// +/// The maximum number of digits is equal to the maximum number +/// of decimals (`u8::MAX`) plus the length of the decimal point +/// and the leading zero. +const MAX_FORMATTED_DIGITS: usize = u8::MAX as usize + 2; + +/// Checks that the account is owned by the expected program. +#[inline(always)] +fn check_account_owner(account_info: &AccountInfo) -> ProgramResult { + if &TOKEN_PROGRAM_ID != account_info.owner() { + Err(ProgramError::IncorrectProgramId) + } else { + Ok(()) + } +} + +/// Validates owner(s) are present. +/// +/// Note that `owner_account_info` will be immutable borrowed when it represents +/// a multisig account. +#[inline(always)] +fn validate_owner( + expected_owner: &Pubkey, + owner_account_info: &AccountInfo, + signers: &[AccountInfo], +) -> ProgramResult { + if expected_owner != owner_account_info.key() { + return Err(TokenError::OwnerMismatch.into()); + } + + if owner_account_info.data_len() == Multisig::LEN + && owner_account_info.owner() == &TOKEN_PROGRAM_ID + { + // SAFETY: the caller guarantees that there are no mutable borrows of `owner_account_info` + // account data and the `load` validates that the account is initialized. + let multisig = unsafe { load::(owner_account_info.borrow_data_unchecked())? }; + + let mut num_signers = 0; + let mut matched = [false; MAX_SIGNERS]; + + for signer in signers.iter() { + for (position, key) in multisig.signers[0..multisig.n as usize].iter().enumerate() { + if key == signer.key() && !matched[position] { + if !signer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + matched[position] = true; + num_signers += 1; + } + } + } + if num_signers < multisig.m { + return Err(ProgramError::MissingRequiredSignature); + } + } else if !owner_account_info.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + Ok(()) +} + +/// Try to convert a UI representation of a token amount to its raw amount using +/// the given decimals field +fn try_ui_amount_into_amount(ui_amount: &str, decimals: u8) -> Result { + let decimals = decimals as usize; + let mut parts = ui_amount.split('.'); + + // Splitting a string, even an empty one, will always yield an iterator of at + // least length == 1. + let amount_str = parts.next().unwrap(); + let after_decimal = parts.next().unwrap_or(""); + // Clean up trailing zeros. + let after_decimal = after_decimal.trim_end_matches('0'); + + // Validates the input. + + let mut length = amount_str.len(); + let expected_after_decimal_length = max(after_decimal.len(), decimals); + + if (amount_str.is_empty() && after_decimal.is_empty()) + || parts.next().is_some() + || after_decimal.len() > decimals + || (length + expected_after_decimal_length) > MAX_FORMATTED_DIGITS + { + return Err(ProgramError::InvalidArgument); + } + + let mut digits = [UNINIT_BYTE; MAX_FORMATTED_DIGITS]; + // SAFETY: `digits` is an array of `MaybeUninit`, which has the same + // memory layout as `u8`. + let slice: &mut [u8] = + unsafe { from_raw_parts_mut(digits.as_mut_ptr() as *mut _, MAX_FORMATTED_DIGITS) }; + + // SAFETY: the total length of `amount_str` and `after_decimal` is less than + // `MAX_DIGITS_U64`. + unsafe { + sol_memcpy(slice, amount_str.as_bytes(), length); + + sol_memcpy( + &mut slice[length..], + after_decimal.as_bytes(), + after_decimal.len(), + ); + } + + length += after_decimal.len(); + let remaining = decimals.saturating_sub(after_decimal.len()); + + // SAFETY: `digits` is an array of `MaybeUninit`, which has the same memory + // layout as `u8`. + let ptr = unsafe { digits.as_mut_ptr().add(length) }; + + for offset in 0..remaining { + // SAFETY: `ptr` is within the bounds of `digits`. + unsafe { + (ptr.add(offset) as *mut u8).write(b'0'); + } + } + + length += remaining; + + // SAFETY: `digits` only contains valid UTF-8 bytes. + unsafe { + from_utf8_unchecked(from_raw_parts(digits.as_ptr() as _, length)) + .parse::() + .map_err(|_| ProgramError::InvalidArgument) + } +} diff --git a/p-token/src/processor/revoke.rs b/p-token/src/processor/revoke.rs new file mode 100644 index 00000000..6361d99a --- /dev/null +++ b/p-token/src/processor/revoke.rs @@ -0,0 +1,30 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; +use token_interface::{ + error::TokenError, + state::{account::Account, load_mut}, +}; + +use super::validate_owner; + +#[inline(always)] +pub fn process_revoke(accounts: &[AccountInfo], _instruction_data: &[u8]) -> ProgramResult { + let [source_account_info, owner_info, remaning @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // SAFETY: single mutable borrow to `source_account_info` account data and + // `load_mut` validates that the account is initialized. + let source_account = + unsafe { load_mut::(source_account_info.borrow_mut_data_unchecked())? }; + + if source_account.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + + validate_owner(&source_account.owner, owner_info, remaning)?; + + source_account.clear_delegate(); + source_account.set_delegated_amount(0); + + Ok(()) +} diff --git a/p-token/src/processor/set_authority.rs b/p-token/src/processor/set_authority.rs new file mode 100644 index 00000000..3ad4d128 --- /dev/null +++ b/p-token/src/processor/set_authority.rs @@ -0,0 +1,153 @@ +use core::marker::PhantomData; + +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; +use token_interface::{ + error::TokenError, + instruction::AuthorityType, + state::{account::Account, load_mut, mint::Mint, RawType}, +}; + +use super::validate_owner; + +#[inline(always)] +pub fn process_set_authority(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + // Validates the instruction data. + + let args = SetAuthority::try_from_bytes(instruction_data)?; + + let authority_type = args.authority_type()?; + let new_authority = args.new_authority(); + + // Validates the accounts. + + let [account_info, authority_info, remaning @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + if account_info.data_len() == Account::LEN { + // SAFETY: single mutable borrow to `account_info` account data and + // `load_mut` validates that the account is initialized. + let account = unsafe { load_mut::(account_info.borrow_mut_data_unchecked())? }; + + if account.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + + match authority_type { + AuthorityType::AccountOwner => { + validate_owner(&account.owner, authority_info, remaning)?; + + if let Some(authority) = new_authority { + account.owner = *authority; + } else { + return Err(TokenError::InvalidInstruction.into()); + } + + account.clear_delegate(); + account.set_delegated_amount(0); + + if account.is_native() { + account.clear_close_authority(); + } + } + AuthorityType::CloseAccount => { + let authority = account.close_authority().unwrap_or(&account.owner); + validate_owner(authority, authority_info, remaning)?; + + if let Some(authority) = new_authority { + account.set_close_authority(authority); + } else { + account.clear_close_authority(); + } + } + _ => { + return Err(TokenError::AuthorityTypeNotSupported.into()); + } + } + } else if account_info.data_len() == Mint::LEN { + // SAFETY: single mutable borrow to `account_info` account data and + // `load_mut` validates that the mint is initialized. + let mint = unsafe { load_mut::(account_info.borrow_mut_data_unchecked())? }; + + match authority_type { + AuthorityType::MintTokens => { + // Once a mint's supply is fixed, it cannot be undone by setting a new + // mint_authority. + let mint_authority = mint.mint_authority().ok_or(TokenError::FixedSupply)?; + + validate_owner(mint_authority, authority_info, remaning)?; + + if let Some(authority) = new_authority { + mint.set_mint_authority(authority); + } else { + mint.clear_mint_authority(); + } + } + AuthorityType::FreezeAccount => { + // Once a mint's freeze authority is disabled, it cannot be re-enabled by + // setting a new freeze_authority. + let freeze_authority = mint + .freeze_authority() + .ok_or(TokenError::MintCannotFreeze)?; + + validate_owner(freeze_authority, authority_info, remaning)?; + + if let Some(authority) = new_authority { + mint.set_freeze_authority(authority); + } else { + mint.clear_freeze_authority(); + } + } + _ => { + return Err(TokenError::AuthorityTypeNotSupported.into()); + } + } + } else { + return Err(ProgramError::InvalidArgument); + } + + Ok(()) +} + +struct SetAuthority<'a> { + raw: *const u8, + + _data: PhantomData<&'a [u8]>, +} + +impl SetAuthority<'_> { + #[inline(always)] + pub fn try_from_bytes(bytes: &[u8]) -> Result { + // The minimum expected size of the instruction data. + // - authority_type (1 byte) + // - option + new_authority (1 byte + 32 bytes) + if bytes.len() < 2 || (bytes[1] == 1 && bytes.len() < 34) { + return Err(ProgramError::InvalidInstructionData); + } + + Ok(SetAuthority { + raw: bytes.as_ptr(), + _data: PhantomData, + }) + } + + #[inline(always)] + pub fn authority_type(&self) -> Result { + // SAFETY: `bytes` length is validated in `try_from_bytes`. + unsafe { AuthorityType::from(*self.raw) } + } + + #[inline(always)] + pub fn new_authority(&self) -> Option<&Pubkey> { + // SAFETY: `bytes` length is validated in `try_from_bytes`. + unsafe { + if *self.raw.add(1) == 0 { + Option::None + } else { + Option::Some(&*(self.raw.add(2) as *const Pubkey)) + } + } + } +} diff --git a/p-token/src/processor/shared/approve.rs b/p-token/src/processor/shared/approve.rs new file mode 100644 index 00000000..a9d2812a --- /dev/null +++ b/p-token/src/processor/shared/approve.rs @@ -0,0 +1,79 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; +use token_interface::{ + error::TokenError, + state::{account::Account, load, load_mut, mint::Mint}, +}; + +use crate::processor::validate_owner; + +#[inline(always)] +pub fn process_approve( + accounts: &[AccountInfo], + amount: u64, + expected_decimals: Option, +) -> ProgramResult { + // Accounts expected depend on whether we have the mint `decimals` or not; when we have the + // mint `decimals`, we expect the mint account to be present. + + let (source_account_info, expected_mint_info, delegate_info, owner_info, remaining) = + if let Some(expected_decimals) = expected_decimals { + let [source_account_info, expected_mint_info, delegate_info, owner_info, remaning @ ..] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + ( + source_account_info, + Some((expected_mint_info, expected_decimals)), + delegate_info, + owner_info, + remaning, + ) + } else { + let [source_account_info, delegate_info, owner_info, remaning @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + ( + source_account_info, + None, + delegate_info, + owner_info, + remaning, + ) + }; + + // Validates source account. + + // SAFETY: single mutable borrow to `source_account_info` account data and + // `load_mut` validates that the account is initialized. + let source_account = + unsafe { load_mut::(source_account_info.borrow_mut_data_unchecked())? }; + + if source_account.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + + if let Some((mint_info, expected_decimals)) = expected_mint_info { + if mint_info.key() != &source_account.mint { + return Err(TokenError::MintMismatch.into()); + } + + // SAFETY: single immutable borrow of `mint_info` account data and + // `load` validates that the mint is initialized. + let mint = unsafe { load::(mint_info.borrow_data_unchecked())? }; + + if expected_decimals != mint.decimals { + return Err(TokenError::MintDecimalsMismatch.into()); + } + } + + validate_owner(&source_account.owner, owner_info, remaining)?; + + // Sets the delegate and delegated amount. + + source_account.set_delegate(delegate_info.key()); + source_account.set_delegated_amount(amount); + + Ok(()) +} diff --git a/p-token/src/processor/shared/burn.rs b/p-token/src/processor/shared/burn.rs new file mode 100644 index 00000000..06f741a9 --- /dev/null +++ b/p-token/src/processor/shared/burn.rs @@ -0,0 +1,89 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; +use token_interface::{ + error::TokenError, + state::{account::Account, load_mut, mint::Mint}, +}; + +use crate::processor::{check_account_owner, validate_owner}; + +#[inline(always)] +pub fn process_burn( + accounts: &[AccountInfo], + amount: u64, + expected_decimals: Option, +) -> ProgramResult { + let [source_account_info, mint_info, authority_info, remaining @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // SAFETY: single mutable borrow to `source_account_info` account data and + // `load_mut` validates that the account is initialized. + let source_account = + unsafe { load_mut::(source_account_info.borrow_mut_data_unchecked())? }; + + if source_account.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + if source_account.is_native() { + return Err(TokenError::NativeNotSupported.into()); + } + + // Ensure the source account has the sufficient amount. This is done before + // the value is updated on the account. + let updated_source_amount = source_account + .amount() + .checked_sub(amount) + .ok_or(TokenError::InsufficientFunds)?; + + // SAFETY: single mutable borrow to `mint_info` account data and + // `load_mut` validates that the mint is initialized. + let mint = unsafe { load_mut::(mint_info.borrow_mut_data_unchecked())? }; + + if mint_info.key() != &source_account.mint { + return Err(TokenError::MintMismatch.into()); + } + + if let Some(expected_decimals) = expected_decimals { + if expected_decimals != mint.decimals { + return Err(TokenError::MintDecimalsMismatch.into()); + } + } + + if !source_account.is_owned_by_system_program_or_incinerator() { + match source_account.delegate() { + Some(delegate) if authority_info.key() == delegate => { + validate_owner(delegate, authority_info, remaining)?; + + let delegated_amount = source_account + .delegated_amount() + .checked_sub(amount) + .ok_or(TokenError::InsufficientFunds)?; + source_account.set_delegated_amount(delegated_amount); + + if delegated_amount == 0 { + source_account.clear_delegate(); + } + } + _ => { + validate_owner(&source_account.owner, authority_info, remaining)?; + } + } + } + + // Updates the source account and mint supply. + + if amount == 0 { + check_account_owner(source_account_info)?; + check_account_owner(mint_info)?; + } else { + source_account.set_amount(updated_source_amount); + + let mint_supply = mint + .supply() + .checked_sub(amount) + .ok_or(TokenError::Overflow)?; + mint.set_supply(mint_supply); + } + + Ok(()) +} diff --git a/p-token/src/processor/shared/initialize_account.rs b/p-token/src/processor/shared/initialize_account.rs new file mode 100644 index 00000000..952dd1f1 --- /dev/null +++ b/p-token/src/processor/shared/initialize_account.rs @@ -0,0 +1,98 @@ +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + pubkey::Pubkey, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; +use token_interface::{ + error::TokenError, + native_mint::is_native_mint, + state::{ + account::Account, account_state::AccountState, load, load_mut_unchecked, mint::Mint, + Initializable, + }, +}; + +use crate::processor::check_account_owner; + +#[inline(always)] +pub fn process_initialize_account( + accounts: &[AccountInfo], + owner: Option<&Pubkey>, + rent_sysvar_account: bool, +) -> ProgramResult { + // Accounts expected depend on whether we have the `rent_sysvar` account or not. + + let (new_account_info, mint_info, owner, remaning) = if let Some(owner) = owner { + let [new_account_info, mint_info, remaning @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + (new_account_info, mint_info, owner, remaning) + } else { + let [new_account_info, mint_info, owner_info, remaning @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + (new_account_info, mint_info, owner_info.key(), remaning) + }; + + // Check rent-exempt status of the token account. + + let new_account_info_data_len = new_account_info.data_len(); + + let minimum_balance = if rent_sysvar_account { + let rent_sysvar_info = remaning.first().ok_or(ProgramError::NotEnoughAccountKeys)?; + // SAFETY: single immutable borrow to `rent_sysvar_info`; account ID and length are + // checked by `from_account_info_unchecked`. + let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar_info)? }; + rent.minimum_balance(new_account_info_data_len) + } else { + Rent::get()?.minimum_balance(new_account_info_data_len) + }; + + let is_native_mint = is_native_mint(mint_info.key()); + + // Initialize the account. + + // SAFETY: single mutable borrow of the 'new_account_info' account data. + let account = + unsafe { load_mut_unchecked::(new_account_info.borrow_mut_data_unchecked())? }; + + if account.is_initialized() { + return Err(TokenError::AlreadyInUse.into()); + } + + if new_account_info.lamports() < minimum_balance { + return Err(TokenError::NotRentExempt.into()); + } + + if !is_native_mint { + check_account_owner(mint_info)?; + + // SAFETY: single immutable borrow of `mint_info` account data and + // `load` validates that the mint is initialized. + let _ = unsafe { + load::(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)? + }; + } + + account.state = AccountState::Initialized; + account.mint = *mint_info.key(); + account.owner = *owner; + + if is_native_mint { + account.set_native(true); + account.set_native_amount(minimum_balance); + // SAFETY: single mutable borrow to `new_account_info` lamports. + unsafe { + account.set_amount( + new_account_info + .borrow_lamports_unchecked() + .checked_sub(minimum_balance) + .ok_or(TokenError::Overflow)?, + ); + } + } + + Ok(()) +} diff --git a/p-token/src/processor/shared/initialize_multisig.rs b/p-token/src/processor/shared/initialize_multisig.rs new file mode 100644 index 00000000..0d45fa8c --- /dev/null +++ b/p-token/src/processor/shared/initialize_multisig.rs @@ -0,0 +1,74 @@ +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{rent::Rent, Sysvar}, + ProgramResult, +}; +use token_interface::{ + error::TokenError, + state::{load_mut_unchecked, multisig::Multisig, Initializable}, +}; + +#[inline(always)] +pub fn process_initialize_multisig( + accounts: &[AccountInfo], + m: u8, + rent_sysvar_account: bool, +) -> ProgramResult { + // Accounts expected depend on whether we have the `rent_sysvar` account or not. + + let (multisig_info, rent_sysvar_info, remaining) = if rent_sysvar_account { + let [multisig_info, rent_sysvar_info, remaining @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + (multisig_info, Some(rent_sysvar_info), remaining) + } else { + let [multisig_info, remaining @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + (multisig_info, None, remaining) + }; + + let multisig_info_data_len = multisig_info.data_len(); + + let is_exempt = if let Some(rent_sysvar_info) = rent_sysvar_info { + // SAFETY: single immutable borrow to `rent_sysvar_info`; account ID and length are + // checked by `from_account_info_unchecked`. + let rent = unsafe { Rent::from_account_info_unchecked(rent_sysvar_info)? }; + rent.is_exempt(multisig_info.lamports(), multisig_info_data_len) + } else { + Rent::get()?.is_exempt(multisig_info.lamports(), multisig_info_data_len) + }; + + // SAFETY: single mutable borrow to `multisig_info` account data. + let multisig = + unsafe { load_mut_unchecked::(multisig_info.borrow_mut_data_unchecked())? }; + + if multisig.is_initialized() { + return Err(TokenError::AlreadyInUse.into()); + } + + if !is_exempt { + return Err(TokenError::NotRentExempt.into()); + } + + // Initialize the multisig account. + + multisig.m = m; + multisig.n = remaining.len() as u8; + + if !Multisig::is_valid_signer_index(multisig.n as usize) { + return Err(TokenError::InvalidNumberOfProvidedSigners.into()); + } + if !Multisig::is_valid_signer_index(multisig.m as usize) { + return Err(TokenError::InvalidNumberOfRequiredSigners.into()); + } + + for (i, signer_info) in remaining.iter().enumerate() { + multisig.signers[i] = *signer_info.key(); + } + + multisig.set_initialized(true); + + Ok(()) +} diff --git a/p-token/src/processor/shared/mint_to.rs b/p-token/src/processor/shared/mint_to.rs new file mode 100644 index 00000000..773039ff --- /dev/null +++ b/p-token/src/processor/shared/mint_to.rs @@ -0,0 +1,71 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; +use token_interface::{ + error::TokenError, + state::{account::Account, load_mut, mint::Mint}, +}; + +use crate::processor::{check_account_owner, validate_owner}; + +#[inline(always)] +pub fn process_mint_to( + accounts: &[AccountInfo], + amount: u64, + expected_decimals: Option, +) -> ProgramResult { + let [mint_info, destination_account_info, owner_info, remaining @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Validates the destination account. + + // SAFETY: single mutable borrow to `destination_account_info` account data and + // `load_mut` validates that the account is initialized. + let destination_account = + unsafe { load_mut::(destination_account_info.borrow_mut_data_unchecked())? }; + + if destination_account.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + + if destination_account.is_native() { + return Err(TokenError::NativeNotSupported.into()); + } + + if mint_info.key() != &destination_account.mint { + return Err(TokenError::MintMismatch.into()); + } + + // SAFETY: single mutable borrow to `mint_info` account data and + // `load_mut` validates that the mint is initialized. + let mint = unsafe { load_mut::(mint_info.borrow_mut_data_unchecked())? }; + + if let Some(expected_decimals) = expected_decimals { + if expected_decimals != mint.decimals { + return Err(TokenError::MintDecimalsMismatch.into()); + } + } + + match mint.mint_authority() { + Some(mint_authority) => validate_owner(mint_authority, owner_info, remaining)?, + None => return Err(TokenError::FixedSupply.into()), + } + + if amount == 0 { + check_account_owner(mint_info)?; + check_account_owner(destination_account_info)?; + } else { + let destination_amount = destination_account + .amount() + .checked_add(amount) + .ok_or(TokenError::Overflow)?; + destination_account.set_amount(destination_amount); + + let mint_supply = mint + .supply() + .checked_add(amount) + .ok_or(TokenError::Overflow)?; + mint.set_supply(mint_supply); + } + + Ok(()) +} diff --git a/p-token/src/processor/shared/mod.rs b/p-token/src/processor/shared/mod.rs new file mode 100644 index 00000000..e55f1298 --- /dev/null +++ b/p-token/src/processor/shared/mod.rs @@ -0,0 +1,12 @@ +//! Shared processor functions. +//! +//! This module contains the shared processor functions that are used by +//! the multiple instruction processors. + +pub mod approve; +pub mod burn; +pub mod initialize_account; +pub mod initialize_multisig; +pub mod mint_to; +pub mod toggle_account_state; +pub mod transfer; diff --git a/p-token/src/processor/shared/toggle_account_state.rs b/p-token/src/processor/shared/toggle_account_state.rs new file mode 100644 index 00000000..c9ff11ab --- /dev/null +++ b/p-token/src/processor/shared/toggle_account_state.rs @@ -0,0 +1,46 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; +use token_interface::{ + error::TokenError, + state::{account::Account, account_state::AccountState, load, load_mut, mint::Mint}, +}; + +use crate::processor::validate_owner; + +#[inline(always)] +pub fn process_toggle_account_state(accounts: &[AccountInfo], freeze: bool) -> ProgramResult { + let [source_account_info, mint_info, authority_info, remaining @ ..] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // SAFETY: single mutable borrow to `source_account_info` account data and + // `load_mut` validates that the account is initialized. + let source_account = + unsafe { load_mut::(source_account_info.borrow_mut_data_unchecked())? }; + + if freeze == source_account.is_frozen() { + return Err(TokenError::InvalidState.into()); + } + if source_account.is_native() { + return Err(TokenError::NativeNotSupported.into()); + } + if mint_info.key() != &source_account.mint { + return Err(TokenError::MintMismatch.into()); + } + + // SAFETY: single immutable borrow of `mint_info` account data and + // `load` validates that the mint is initialized. + let mint = unsafe { load::(mint_info.borrow_data_unchecked())? }; + + match mint.freeze_authority() { + Some(authority) => validate_owner(authority, authority_info, remaining), + None => Err(TokenError::MintCannotFreeze.into()), + }?; + + source_account.state = if freeze { + AccountState::Frozen + } else { + AccountState::Initialized + }; + + Ok(()) +} diff --git a/p-token/src/processor/shared/transfer.rs b/p-token/src/processor/shared/transfer.rs new file mode 100644 index 00000000..aafd0a1f --- /dev/null +++ b/p-token/src/processor/shared/transfer.rs @@ -0,0 +1,173 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; +use token_interface::{ + error::TokenError, + state::{account::Account, load, load_mut, load_mut_unchecked, mint::Mint}, +}; + +use crate::processor::{check_account_owner, validate_owner}; + +#[inline(always)] +pub fn process_transfer( + accounts: &[AccountInfo], + amount: u64, + expected_decimals: Option, +) -> ProgramResult { + // Accounts expected depend on whether we have the mint `decimals` or not; when we have the + // mint `decimals`, we expect the mint account to be present. + + let ( + source_account_info, + expected_mint_info, + destination_account_info, + authority_info, + remaning, + ) = if let Some(decimals) = expected_decimals { + let [source_account_info, mint_info, destination_account_info, authority_info, remaning @ ..] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + ( + source_account_info, + Some((mint_info, decimals)), + destination_account_info, + authority_info, + remaning, + ) + } else { + let [source_account_info, destination_account_info, authority_info, remaning @ ..] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + ( + source_account_info, + None, + destination_account_info, + authority_info, + remaning, + ) + }; + + // Validates source and destination accounts. + + // SAFETY: single mutable borrow to `source_account_info` account data and + // `load_mut` validates that the account is initialized. + let source_account = + unsafe { load_mut::(source_account_info.borrow_mut_data_unchecked())? }; + + // Comparing whether the AccountInfo's "point" to the same account or + // not - this is a faster comparison since it just checks the internal + // raw pointer. + let self_transfer = source_account_info == destination_account_info; + + // Implicitly validates that the account has enough tokens by calculating the + // remaining amount - the amount is only updated on the account if the transfer + // is successful. + let remaining_amount = if self_transfer { + if source_account.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + + source_account + .amount() + .checked_sub(amount) + .ok_or(TokenError::InsufficientFunds)? + } else { + // SAFETY: scoped immutable borrow to `destination_account_info` account data and + // `load` validates that the account is initialized. + let destination_account = + unsafe { load::(destination_account_info.borrow_data_unchecked())? }; + + if source_account.is_frozen() || destination_account.is_frozen() { + return Err(TokenError::AccountFrozen.into()); + } + + let remaining_amount = source_account + .amount() + .checked_sub(amount) + .ok_or(TokenError::InsufficientFunds)?; + + if source_account.mint != destination_account.mint { + return Err(TokenError::MintMismatch.into()); + } + + remaining_amount + }; + + // Validates the mint information. + + if let Some((mint_info, decimals)) = expected_mint_info { + if mint_info.key() != &source_account.mint { + return Err(TokenError::MintMismatch.into()); + } + + // SAFETY: single immutable borrow of `mint_info` account data and + // `load` validates that the mint is initialized. + let mint = unsafe { load::(mint_info.borrow_data_unchecked())? }; + + if decimals != mint.decimals { + return Err(TokenError::MintDecimalsMismatch.into()); + } + } + + // Validates the authority (delegate or owner). + + if source_account.delegate() == Some(authority_info.key()) { + validate_owner(authority_info.key(), authority_info, remaning)?; + + let delegated_amount = source_account + .delegated_amount() + .checked_sub(amount) + .ok_or(TokenError::InsufficientFunds)?; + + if !self_transfer { + source_account.set_delegated_amount(delegated_amount); + + if delegated_amount == 0 { + source_account.clear_delegate(); + } + } + } else { + validate_owner(&source_account.owner, authority_info, remaning)?; + } + + if self_transfer || amount == 0 { + // Validates the token accounts owner since we are not writing + // to these account. + check_account_owner(source_account_info)?; + check_account_owner(destination_account_info)?; + } else { + // Moves the tokens. + + source_account.set_amount(remaining_amount); + + // SAFETY: single mutable borrow to `destination_account_info` account data; the account + // is guaranteed to be initialized and different than `source_account_info`. + let destination_account = unsafe { + load_mut_unchecked::(destination_account_info.borrow_mut_data_unchecked())? + }; + let destination_amount = destination_account + .amount() + .checked_add(amount) + .ok_or(TokenError::Overflow)?; + destination_account.set_amount(destination_amount); + + if source_account.is_native() { + // SAFETY: single mutable borrow to `source_account_info` lamports. + let source_lamports = unsafe { source_account_info.borrow_mut_lamports_unchecked() }; + *source_lamports = source_lamports + .checked_sub(amount) + .ok_or(TokenError::Overflow)?; + + // SAFETY: single mutable borrow to `destination_account_info` lamports. + let destination_lamports = + unsafe { destination_account_info.borrow_mut_lamports_unchecked() }; + *destination_lamports = destination_lamports + .checked_add(amount) + .ok_or(TokenError::Overflow)?; + } + } + + Ok(()) +} diff --git a/p-token/src/processor/sync_native.rs b/p-token/src/processor/sync_native.rs new file mode 100644 index 00000000..4ccc276f --- /dev/null +++ b/p-token/src/processor/sync_native.rs @@ -0,0 +1,35 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; +use token_interface::{ + error::TokenError, + state::{account::Account, load_mut}, +}; + +use super::check_account_owner; + +#[inline(always)] +pub fn process_sync_native(accounts: &[AccountInfo]) -> ProgramResult { + let native_account_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; + + check_account_owner(native_account_info)?; + + // SAFETY: single mutable borrow to `native_account_info` account data and + // `load_mut` validates that the account is initialized. + let native_account = + unsafe { load_mut::(native_account_info.borrow_mut_data_unchecked())? }; + + if let Option::Some(rent_exempt_reserve) = native_account.native_amount() { + let new_amount = native_account_info + .lamports() + .checked_sub(rent_exempt_reserve) + .ok_or(TokenError::Overflow)?; + + if new_amount < native_account.amount() { + return Err(TokenError::InvalidState.into()); + } + native_account.set_amount(new_amount); + } else { + return Err(TokenError::NonNativeNotSupported.into()); + } + + Ok(()) +} diff --git a/p-token/src/processor/thaw_account.rs b/p-token/src/processor/thaw_account.rs new file mode 100644 index 00000000..924d3b64 --- /dev/null +++ b/p-token/src/processor/thaw_account.rs @@ -0,0 +1,8 @@ +use pinocchio::{account_info::AccountInfo, ProgramResult}; + +use super::shared::toggle_account_state::process_toggle_account_state; + +#[inline(always)] +pub fn process_thaw_account(accounts: &[AccountInfo]) -> ProgramResult { + process_toggle_account_state(accounts, false) +} diff --git a/p-token/src/processor/transfer.rs b/p-token/src/processor/transfer.rs new file mode 100644 index 00000000..52af3302 --- /dev/null +++ b/p-token/src/processor/transfer.rs @@ -0,0 +1,14 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_transfer(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult { + let amount = u64::from_le_bytes( + instruction_data + .try_into() + .map_err(|_error| ProgramError::InvalidInstructionData)?, + ); + + shared::transfer::process_transfer(accounts, amount, None) +} diff --git a/p-token/src/processor/transfer_checked.rs b/p-token/src/processor/transfer_checked.rs new file mode 100644 index 00000000..ea75a289 --- /dev/null +++ b/p-token/src/processor/transfer_checked.rs @@ -0,0 +1,26 @@ +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, ProgramResult}; + +use super::shared; + +#[inline(always)] +pub fn process_transfer_checked( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + // expected u64 (8) + u8 (1) + let (amount, decimals) = if instruction_data.len() == 9 { + let (amount, decimals) = instruction_data.split_at(core::mem::size_of::()); + ( + u64::from_le_bytes( + amount + .try_into() + .map_err(|_error| ProgramError::InvalidInstructionData)?, + ), + decimals.first(), + ) + } else { + return Err(ProgramError::InvalidInstructionData); + }; + + shared::transfer::process_transfer(accounts, amount, decimals.copied()) +} diff --git a/p-token/src/processor/ui_amount_to_amount.rs b/p-token/src/processor/ui_amount_to_amount.rs new file mode 100644 index 00000000..823ffbc7 --- /dev/null +++ b/p-token/src/processor/ui_amount_to_amount.rs @@ -0,0 +1,32 @@ +use core::str::from_utf8; +use pinocchio::{ + account_info::AccountInfo, program::set_return_data, program_error::ProgramError, ProgramResult, +}; +use token_interface::{ + error::TokenError, + state::{load, mint::Mint}, +}; + +use super::{check_account_owner, try_ui_amount_into_amount}; + +#[inline(always)] +pub fn process_ui_amount_to_amount( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + let ui_amount = + from_utf8(instruction_data).map_err(|_error| ProgramError::InvalidInstructionData)?; + + let mint_info = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; + check_account_owner(mint_info)?; + // SAFETY: single immutable borrow to `mint_info` account data and + // `load` validates that the mint is initialized. + let mint = unsafe { + load::(mint_info.borrow_data_unchecked()).map_err(|_| TokenError::InvalidMint)? + }; + + let amount = try_ui_amount_into_amount(ui_amount, mint.decimals)?; + set_return_data(&amount.to_le_bytes()); + + Ok(()) +} diff --git a/p-token/tests/amount_to_ui_amount.rs b/p-token/tests/amount_to_ui_amount.rs new file mode 100644 index 00000000..402be081 --- /dev/null +++ b/p-token/tests/amount_to_ui_amount.rs @@ -0,0 +1,48 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn amount_to_ui_amount(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Pubkey::new_unique(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority, + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + let mut amount_to_ui_amount_ix = + spl_token::instruction::amount_to_ui_amount(&spl_token::ID, &mint, 1000).unwrap(); + // Switches the program id to the token program. + amount_to_ui_amount_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[amount_to_ui_amount_ix], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the transaction should succeed. + + let account = context.banks_client.get_account(mint).await.unwrap(); + + assert!(account.is_some()); +} diff --git a/p-token/tests/approve.rs b/p-token/tests/approve.rs new file mode 100644 index 00000000..f3b28e2a --- /dev/null +++ b/p-token/tests/approve.rs @@ -0,0 +1,87 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn approve(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // And a token account with 100 tokens. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + mint::mint( + &mut context, + &mint, + &account, + &mint_authority, + 100, + &token_program, + ) + .await + .unwrap(); + + // When we approve a delegate. + + let delegate = Pubkey::new_unique(); + + let mut approve_ix = spl_token::instruction::approve( + &spl_token::ID, + &account, + &delegate, + &owner.pubkey(), + &[], + 50, + ) + .unwrap(); + approve_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[approve_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &owner], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the account should have the delegate and delegated amount. + + let account = context.banks_client.get_account(account).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(account.delegate.is_some()); + assert!(account.delegate.unwrap() == delegate); + assert!(account.delegated_amount == 50); +} diff --git a/p-token/tests/approve_checked.rs b/p-token/tests/approve_checked.rs new file mode 100644 index 00000000..e34fa7a1 --- /dev/null +++ b/p-token/tests/approve_checked.rs @@ -0,0 +1,89 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn approve_checked(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // And a token account with 100 tokens. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + mint::mint( + &mut context, + &mint, + &account, + &mint_authority, + 100, + &token_program, + ) + .await + .unwrap(); + + // When we approve a delegate. + + let delegate = Pubkey::new_unique(); + + let mut approve_ix = spl_token::instruction::approve_checked( + &spl_token::ID, + &account, + &mint, + &delegate, + &owner.pubkey(), + &[], + 50, + 4, + ) + .unwrap(); + approve_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[approve_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &owner], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the account should have the delegate and delegated amount. + + let account = context.banks_client.get_account(account).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(account.delegate.is_some()); + assert!(account.delegate.unwrap() == delegate); + assert!(account.delegated_amount == 50); +} diff --git a/p-token/tests/burn.rs b/p-token/tests/burn.rs new file mode 100644 index 00000000..6e5e2e5c --- /dev/null +++ b/p-token/tests/burn.rs @@ -0,0 +1,77 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn burn(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // And a token account with 100 tokens. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + mint::mint( + &mut context, + &mint, + &account, + &mint_authority, + 100, + &token_program, + ) + .await + .unwrap(); + + // When we burn 50 tokens. + + let mut burn_ix = + spl_token::instruction::burn(&spl_token::ID, &account, &mint, &owner.pubkey(), &[], 50) + .unwrap(); + burn_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[burn_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &owner], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the account should have 50 tokens remaining. + + let account = context.banks_client.get_account(account).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(account.amount == 50); +} diff --git a/p-token/tests/burn_checked.rs b/p-token/tests/burn_checked.rs new file mode 100644 index 00000000..a0082621 --- /dev/null +++ b/p-token/tests/burn_checked.rs @@ -0,0 +1,84 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn burn_checked(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // And a token account with 100 tokens. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + mint::mint( + &mut context, + &mint, + &account, + &mint_authority, + 100, + &token_program, + ) + .await + .unwrap(); + + // When we burn 50 tokens. + + let mut burn_ix = spl_token::instruction::burn_checked( + &spl_token::ID, + &account, + &mint, + &owner.pubkey(), + &[], + 50, + 4, + ) + .unwrap(); + burn_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[burn_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &owner], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the account should have 50 tokens remaining. + + let account = context.banks_client.get_account(account).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(account.amount == 50); +} diff --git a/p-token/tests/close_account.rs b/p-token/tests/close_account.rs new file mode 100644 index 00000000..8a734324 --- /dev/null +++ b/p-token/tests/close_account.rs @@ -0,0 +1,67 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn close_account(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // And a token account. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + let token_account = context.banks_client.get_account(account).await.unwrap(); + assert!(token_account.is_some()); + + // When we close the account. + + let mut close_account_ix = spl_token::instruction::close_account( + &spl_token::ID, + &account, + &owner.pubkey(), + &owner.pubkey(), + &[], + ) + .unwrap(); + close_account_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[close_account_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &owner], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then an account must not exist. + + let token_account = context.banks_client.get_account(account).await.unwrap(); + assert!(token_account.is_none()); +} diff --git a/p-token/tests/freeze_account.rs b/p-token/tests/freeze_account.rs new file mode 100644 index 00000000..d5fb3afc --- /dev/null +++ b/p-token/tests/freeze_account.rs @@ -0,0 +1,74 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token::state::AccountState; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn freeze_account(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Keypair::new(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority.pubkey()), + &token_program, + ) + .await + .unwrap(); + + // And a token account. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + let token_account = context.banks_client.get_account(account).await.unwrap(); + assert!(token_account.is_some()); + + // When we freeze the account. + + let mut freeze_account_ix = spl_token::instruction::freeze_account( + &spl_token::ID, + &account, + &mint, + &freeze_authority.pubkey(), + &[], + ) + .unwrap(); + freeze_account_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[freeze_account_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &freeze_authority], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the account is frozen. + + let token_account = context.banks_client.get_account(account).await.unwrap(); + assert!(token_account.is_some()); + + let token_account = token_account.unwrap(); + let token_account = spl_token::state::Account::unpack(&token_account.data).unwrap(); + + assert_eq!(token_account.state, AccountState::Frozen); +} diff --git a/p-token/tests/initialize_account.rs b/p-token/tests/initialize_account.rs new file mode 100644 index 00000000..4c491bb5 --- /dev/null +++ b/p-token/tests/initialize_account.rs @@ -0,0 +1,91 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn initialize_account(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Pubkey::new_unique(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority, + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // Given a mint authority, freeze authority and an account keypair. + + let owner = Pubkey::new_unique(); + let account = Keypair::new(); + + let account_size = 165; + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_account( + &spl_token::ID, + &account.pubkey(), + &mint, + &owner, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // When a new mint account is created and initialized. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size), + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then an account has the correct data. + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(!account.is_frozen()); + assert!(account.owner == owner); + assert!(account.mint == mint); +} diff --git a/p-token/tests/initialize_account2.rs b/p-token/tests/initialize_account2.rs new file mode 100644 index 00000000..9f250e4e --- /dev/null +++ b/p-token/tests/initialize_account2.rs @@ -0,0 +1,91 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn initialize_account2(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Pubkey::new_unique(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority, + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // Given a mint authority, freeze authority and an account keypair. + + let owner = Pubkey::new_unique(); + let account = Keypair::new(); + + let account_size = 165; + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_account2( + &spl_token::ID, + &account.pubkey(), + &mint, + &owner, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // When a new mint account is created and initialized. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size), + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then an account has the correct data. + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(!account.is_frozen()); + assert!(account.owner == owner); + assert!(account.mint == mint); +} diff --git a/p-token/tests/initialize_account3.rs b/p-token/tests/initialize_account3.rs new file mode 100644 index 00000000..7fee802b --- /dev/null +++ b/p-token/tests/initialize_account3.rs @@ -0,0 +1,91 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn initialize_account3(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Pubkey::new_unique(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority, + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // Given a mint authority, freeze authority and an account keypair. + + let owner = Pubkey::new_unique(); + let account = Keypair::new(); + + let account_size = 165; + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_account3( + &spl_token::ID, + &account.pubkey(), + &mint, + &owner, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // When a new mint account is created and initialized. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size), + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then an account has the correct data. + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(!account.is_frozen()); + assert!(account.owner == owner); + assert!(account.mint == mint); +} diff --git a/p-token/tests/initialize_mint.rs b/p-token/tests/initialize_mint.rs new file mode 100644 index 00000000..175e805e --- /dev/null +++ b/p-token/tests/initialize_mint.rs @@ -0,0 +1,84 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use std::mem::size_of; + +use setup::TOKEN_PROGRAM_ID; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_option::COption, + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, +}; +use token_interface::state::mint::Mint; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn initialize_mint(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint authority, freeze authority and an account keypair. + + let mint_authority = Pubkey::new_unique(); + let freeze_authority = Pubkey::new_unique(); + let account = Keypair::new(); + + let account_size = size_of::(); + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_mint( + &spl_token::ID, + &account.pubkey(), + &mint_authority, + Some(&freeze_authority), + 0, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // When a new mint account is created and initialized. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size), + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then an account has the correct data. + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let mint = spl_token::state::Mint::unpack(&account.data).unwrap(); + + assert!(mint.is_initialized); + assert!(mint.mint_authority == COption::Some(mint_authority)); + assert!(mint.freeze_authority == COption::Some(freeze_authority)); + assert!(mint.decimals == 0) +} diff --git a/p-token/tests/initialize_mint2.rs b/p-token/tests/initialize_mint2.rs new file mode 100644 index 00000000..683e1e3b --- /dev/null +++ b/p-token/tests/initialize_mint2.rs @@ -0,0 +1,84 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use std::mem::size_of; + +use setup::TOKEN_PROGRAM_ID; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_option::COption, + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, +}; +use token_interface::state::mint::Mint; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn initialize_mint2(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint authority, freeze authority and an account keypair. + + let mint_authority = Pubkey::new_unique(); + let freeze_authority = Pubkey::new_unique(); + let account = Keypair::new(); + + let account_size = size_of::(); + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_mint2( + &spl_token::ID, + &account.pubkey(), + &mint_authority, + Some(&freeze_authority), + 0, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // When a new mint account is created and initialized. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size), + account_size as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then an account has the correct data. + + let account = context + .banks_client + .get_account(account.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let mint = spl_token::state::Mint::unpack(&account.data).unwrap(); + + assert!(mint.is_initialized); + assert!(mint.mint_authority == COption::Some(mint_authority)); + assert!(mint.freeze_authority == COption::Some(freeze_authority)); + assert!(mint.decimals == 0) +} diff --git a/p-token/tests/initialize_multisig.rs b/p-token/tests/initialize_multisig.rs new file mode 100644 index 00000000..5cf9e344 --- /dev/null +++ b/p-token/tests/initialize_multisig.rs @@ -0,0 +1,80 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::TOKEN_PROGRAM_ID; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, +}; +use spl_token::state::Multisig; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn initialize_multisig(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given an account + + let multisig = Keypair::new(); + let signer1 = Pubkey::new_unique(); + let signer2 = Pubkey::new_unique(); + let signer3 = Pubkey::new_unique(); + let signers = vec![&signer1, &signer2, &signer3]; + + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_multisig( + &spl_token::ID, + &multisig.pubkey(), + &signers, + 2, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // When a new multisig account is created and initialized. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &multisig.pubkey(), + rent.minimum_balance(Multisig::LEN), + Multisig::LEN as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &multisig], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the multisig has the correct data. + + let account = context + .banks_client + .get_account(multisig.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let multisig = spl_token::state::Multisig::unpack(&account.data).unwrap(); + + assert!(multisig.is_initialized); + assert_eq!(multisig.n, 3); + assert_eq!(multisig.m, 2); +} diff --git a/p-token/tests/initialize_multisig2.rs b/p-token/tests/initialize_multisig2.rs new file mode 100644 index 00000000..443a988f --- /dev/null +++ b/p-token/tests/initialize_multisig2.rs @@ -0,0 +1,80 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::TOKEN_PROGRAM_ID; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_instruction, + transaction::Transaction, +}; +use spl_token::state::Multisig; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn initialize_multisig2(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given an account + + let multisig = Keypair::new(); + let signer1 = Pubkey::new_unique(); + let signer2 = Pubkey::new_unique(); + let signer3 = Pubkey::new_unique(); + let signers = vec![&signer1, &signer2, &signer3]; + + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_multisig2( + &spl_token::ID, + &multisig.pubkey(), + &signers, + 2, + ) + .unwrap(); + // Switches the program id to the token program. + initialize_ix.program_id = token_program; + + // When a new multisig account is created and initialized. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &multisig.pubkey(), + rent.minimum_balance(Multisig::LEN), + Multisig::LEN as u64, + &token_program, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &multisig], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the multisig has the correct data. + + let account = context + .banks_client + .get_account(multisig.pubkey()) + .await + .unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let multisig = spl_token::state::Multisig::unpack(&account.data).unwrap(); + + assert!(multisig.is_initialized); + assert_eq!(multisig.n, 3); + assert_eq!(multisig.m, 2); +} diff --git a/p-token/tests/mint_to.rs b/p-token/tests/mint_to.rs new file mode 100644 index 00000000..9cafae37 --- /dev/null +++ b/p-token/tests/mint_to.rs @@ -0,0 +1,73 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn mint_to(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // And a token account. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + // When we mint tokens to it. + + let mut mint_ix = spl_token::instruction::mint_to( + &spl_token::ID, + &mint, + &account, + &mint_authority.pubkey(), + &[], + 100, + ) + .unwrap(); + // Switches the program id to the token program. + mint_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[mint_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then an account has the correct data. + + let account = context.banks_client.get_account(account).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(account.amount == 100); +} diff --git a/p-token/tests/mint_to_checked.rs b/p-token/tests/mint_to_checked.rs new file mode 100644 index 00000000..1d0cb72d --- /dev/null +++ b/p-token/tests/mint_to_checked.rs @@ -0,0 +1,74 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn mint_to_checked(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // And a token account. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + // When we mint tokens to it. + + let mut mint_ix = spl_token::instruction::mint_to_checked( + &spl_token::ID, + &mint, + &account, + &mint_authority.pubkey(), + &[], + 100, + 4, + ) + .unwrap(); + // Switches the program id to the token program. + mint_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[mint_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then an account has the correct data. + + let account = context.banks_client.get_account(account).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(account.amount == 100); +} diff --git a/p-token/tests/revoke.rs b/p-token/tests/revoke.rs new file mode 100644 index 00000000..689c48cc --- /dev/null +++ b/p-token/tests/revoke.rs @@ -0,0 +1,91 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn revoke(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // And a token account with 100 tokens. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + mint::mint( + &mut context, + &mint, + &account, + &mint_authority, + 100, + &token_program, + ) + .await + .unwrap(); + + // And 50 tokens delegated. + + let delegate = Pubkey::new_unique(); + + account::approve( + &mut context, + &account, + &delegate, + &owner, + 50, + &token_program, + ) + .await; + + // When we revoke the delegation. + + let mut revoke_ix = + spl_token::instruction::revoke(&spl_token::ID, &account, &owner.pubkey(), &[]).unwrap(); + revoke_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[revoke_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &owner], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the account should not have a delegate nor delegated amount. + + let account = context.banks_client.get_account(account).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(account.delegate.is_none()); + assert!(account.delegated_amount == 0); +} diff --git a/p-token/tests/set_authority.rs b/p-token/tests/set_authority.rs new file mode 100644 index 00000000..977d22e6 --- /dev/null +++ b/p-token/tests/set_authority.rs @@ -0,0 +1,70 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_option::COption, + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token::instruction::AuthorityType; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn set_authority(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Keypair::new(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority.pubkey()), + &token_program, + ) + .await + .unwrap(); + + // When we set a new freeze authority. + + let new_authority = Pubkey::new_unique(); + + let mut set_authority_ix = spl_token::instruction::set_authority( + &spl_token::ID, + &mint, + Some(&new_authority), + AuthorityType::FreezeAccount, + &freeze_authority.pubkey(), + &[], + ) + .unwrap(); + set_authority_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[set_authority_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &freeze_authority], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the account should have the delegate and delegated amount. + + let account = context.banks_client.get_account(mint).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let mint = spl_token::state::Mint::unpack(&account.data).unwrap(); + + assert!(mint.freeze_authority == COption::Some(new_authority)); +} diff --git a/p-token/tests/setup/account.rs b/p-token/tests/setup/account.rs new file mode 100644 index 00000000..cb2a0971 --- /dev/null +++ b/p-token/tests/setup/account.rs @@ -0,0 +1,97 @@ +use solana_program_test::ProgramTestContext; +use solana_sdk::{ + pubkey::Pubkey, signature::Keypair, signer::Signer, system_instruction, + transaction::Transaction, +}; + +pub async fn initialize( + context: &mut ProgramTestContext, + mint: &Pubkey, + owner: &Pubkey, + program_id: &Pubkey, +) -> Pubkey { + let account = Keypair::new(); + + let account_size = 165; + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = + spl_token::instruction::initialize_account(&spl_token::ID, &account.pubkey(), mint, owner) + .unwrap(); + initialize_ix.program_id = *program_id; + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size), + account_size as u64, + program_id, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + account.pubkey() +} + +pub async fn approve( + context: &mut ProgramTestContext, + account: &Pubkey, + delegate: &Pubkey, + owner: &Keypair, + amount: u64, + program_id: &Pubkey, +) { + let mut approve_ix = spl_token::instruction::approve( + &spl_token::ID, + account, + delegate, + &owner.pubkey(), + &[], + amount, + ) + .unwrap(); + approve_ix.program_id = *program_id; + + let tx = Transaction::new_signed_with_payer( + &[approve_ix], + Some(&context.payer.pubkey()), + &[&context.payer, owner], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); +} + +pub async fn freeze( + context: &mut ProgramTestContext, + account: &Pubkey, + mint: &Pubkey, + freeze_authority: &Keypair, + program_id: &Pubkey, +) { + let mut freeze_account_ix = spl_token::instruction::freeze_account( + &spl_token::ID, + account, + mint, + &freeze_authority.pubkey(), + &[], + ) + .unwrap(); + freeze_account_ix.program_id = *program_id; + + let tx = Transaction::new_signed_with_payer( + &[freeze_account_ix], + Some(&context.payer.pubkey()), + &[&context.payer, freeze_authority], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); +} diff --git a/p-token/tests/setup/mint.rs b/p-token/tests/setup/mint.rs new file mode 100644 index 00000000..2714757b --- /dev/null +++ b/p-token/tests/setup/mint.rs @@ -0,0 +1,84 @@ +use std::mem::size_of; + +use solana_program_test::{BanksClientError, ProgramTestContext}; +use solana_sdk::{ + program_error::ProgramError, pubkey::Pubkey, signature::Keypair, signer::Signer, + system_instruction, transaction::Transaction, +}; +use token_interface::state::mint::Mint; + +pub async fn initialize( + context: &mut ProgramTestContext, + mint_authority: Pubkey, + freeze_authority: Option, + program_id: &Pubkey, +) -> Result { + // Mint account keypair. + let account = Keypair::new(); + + let account_size = size_of::(); + let rent = context.banks_client.get_rent().await.unwrap(); + + let mut initialize_ix = spl_token::instruction::initialize_mint( + &spl_token::ID, + &account.pubkey(), + &mint_authority, + freeze_authority.as_ref(), + 4, + ) + .unwrap(); + // Switches the program id in case we are using a "custom" one. + initialize_ix.program_id = *program_id; + + // Create a new account and initialize as a mint. + + let instructions = vec![ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + rent.minimum_balance(account_size), + account_size as u64, + program_id, + ), + initialize_ix, + ]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &[&context.payer, &account], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + Ok(account.pubkey()) +} + +pub async fn mint( + context: &mut ProgramTestContext, + mint: &Pubkey, + account: &Pubkey, + mint_authority: &Keypair, + amount: u64, + program_id: &Pubkey, +) -> Result<(), BanksClientError> { + let mut mint_ix = spl_token::instruction::mint_to( + &spl_token::ID, + mint, + account, + &mint_authority.pubkey(), + &[], + amount, + ) + .unwrap(); + // Switches the program id to the token program. + mint_ix.program_id = *program_id; + + let tx = Transaction::new_signed_with_payer( + &[mint_ix], + Some(&context.payer.pubkey()), + &[&context.payer, mint_authority], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await +} diff --git a/p-token/tests/setup/mod.rs b/p-token/tests/setup/mod.rs new file mode 100644 index 00000000..e79b8ce3 --- /dev/null +++ b/p-token/tests/setup/mod.rs @@ -0,0 +1,8 @@ +use solana_sdk::pubkey::Pubkey; + +#[allow(dead_code)] +pub mod account; +#[allow(dead_code)] +pub mod mint; + +pub const TOKEN_PROGRAM_ID: Pubkey = Pubkey::new_from_array(token_interface::program::ID); diff --git a/p-token/tests/thaw_account.rs b/p-token/tests/thaw_account.rs new file mode 100644 index 00000000..6fe57623 --- /dev/null +++ b/p-token/tests/thaw_account.rs @@ -0,0 +1,83 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; +use spl_token::state::AccountState; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn thaw_account(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Keypair::new(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority.pubkey()), + &token_program, + ) + .await + .unwrap(); + + // And a frozen token account. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + let token_account = context.banks_client.get_account(account).await.unwrap(); + assert!(token_account.is_some()); + + account::freeze( + &mut context, + &account, + &mint, + &freeze_authority, + &token_program, + ) + .await; + + // When we thaw the account. + + let mut thaw_account_ix = spl_token::instruction::thaw_account( + &spl_token::ID, + &account, + &mint, + &freeze_authority.pubkey(), + &[], + ) + .unwrap(); + thaw_account_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[thaw_account_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &freeze_authority], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the account is frozen. + + let token_account = context.banks_client.get_account(account).await.unwrap(); + assert!(token_account.is_some()); + + let token_account = token_account.unwrap(); + let token_account = spl_token::state::Account::unpack(&token_account.data).unwrap(); + + assert_eq!(token_account.state, AccountState::Initialized); +} diff --git a/p-token/tests/transfer.rs b/p-token/tests/transfer.rs new file mode 100644 index 00000000..16491c24 --- /dev/null +++ b/p-token/tests/transfer.rs @@ -0,0 +1,88 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn transfer(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // And a token account with 100 tokens. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + mint::mint( + &mut context, + &mint, + &account, + &mint_authority, + 100, + &token_program, + ) + .await + .unwrap(); + + // When we transfer the tokens. + + let destination = Pubkey::new_unique(); + + let destination_account = + account::initialize(&mut context, &mint, &destination, &token_program).await; + + let mut transfer_ix = spl_token::instruction::transfer( + &spl_token::ID, + &account, + &destination_account, + &owner.pubkey(), + &[], + 100, + ) + .unwrap(); + transfer_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[transfer_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &owner], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then an account has the correct data. + + let account = context.banks_client.get_account(account).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(account.amount == 0); +} diff --git a/p-token/tests/transfer_checked.rs b/p-token/tests/transfer_checked.rs new file mode 100644 index 00000000..d03c9f56 --- /dev/null +++ b/p-token/tests/transfer_checked.rs @@ -0,0 +1,90 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{account, mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + program_pack::Pack, + pubkey::Pubkey, + signature::{Keypair, Signer}, + transaction::Transaction, +}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn transfer_checked(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + // And a token account with 100 tokens. + + let owner = Keypair::new(); + + let account = account::initialize(&mut context, &mint, &owner.pubkey(), &token_program).await; + + mint::mint( + &mut context, + &mint, + &account, + &mint_authority, + 100, + &token_program, + ) + .await + .unwrap(); + + // When we transfer the tokens. + + let destination = Pubkey::new_unique(); + + let destination_account = + account::initialize(&mut context, &mint, &destination, &token_program).await; + + let mut transfer_ix = spl_token::instruction::transfer_checked( + &spl_token::ID, + &account, + &mint, + &destination_account, + &owner.pubkey(), + &[], + 100, + 4, + ) + .unwrap(); + transfer_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[transfer_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &owner], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then an account has the correct data. + + let account = context.banks_client.get_account(account).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token::state::Account::unpack(&account.data).unwrap(); + + assert!(account.amount == 0); +} diff --git a/p-token/tests/ui_amount_to_amount.rs b/p-token/tests/ui_amount_to_amount.rs new file mode 100644 index 00000000..3b1a6db2 --- /dev/null +++ b/p-token/tests/ui_amount_to_amount.rs @@ -0,0 +1,48 @@ +#![cfg(feature = "test-sbf")] + +mod setup; + +use setup::{mint, TOKEN_PROGRAM_ID}; +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{pubkey::Pubkey, signature::Signer, transaction::Transaction}; + +#[test_case::test_case(TOKEN_PROGRAM_ID ; "p-token")] +#[tokio::test] +async fn ui_amount_to_amount(token_program: Pubkey) { + let mut context = ProgramTest::new("token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Pubkey::new_unique(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority, + Some(freeze_authority), + &token_program, + ) + .await + .unwrap(); + + let mut ui_amount_to_amount_ix = + spl_token::instruction::ui_amount_to_amount(&spl_token::ID, &mint, "1000.00").unwrap(); + // Switches the program id to the token program. + ui_amount_to_amount_ix.program_id = token_program; + + let tx = Transaction::new_signed_with_payer( + &[ui_amount_to_amount_ix], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the transaction should succeed. + + let account = context.banks_client.get_account(mint).await.unwrap(); + + assert!(account.is_some()); +}