2023.07.10

SolanaのToken Programのソースコードを理解する

こんにちは。次世代システム研究室のL.W.です。

研究室のメンバーは「ソラナ(Solana)ブロックチェーン及び ソラナ上のトークンの発行について」を共有したことがあります。

今回はSolanaのトークンのソースコードを理解してみましたので、共有します。

参照になりましたら、ありがたいです。

 

1.基本的なデータストラクチャー

端的に言うと、プログラムはアカウントにCRUD(Create、 Read、Update、Delete)処理を行うオンチェーンコードです。

Solanaのプログラム(program)は、他のブロックチェーンで「スマートコントラクト」と呼ばれるもので、ブロックチェーン上の各トランザクション内に送信された指示を解釈する実行可能コードです。

ネットワークのコアコードに直接デプロイした Native Programs(ネイティブプログラム)もあれば、誰でもプログラムを公開するOn Chain Programs(オンチェーンプログラム)もあります。

Native Programsの例には以下のようなものがあります:
On Chain ProgramsはSolanaクラスタのコアコードに直接組み込まれていない任意のプログラムを指します。誰でも一つプログラムを作成したり公開したりすることができます。オンチェーンプログラムは、各プログラムのアカウント所有者によってブロックチェーン上で直接更新することもできます。(これに対してイーサリアムではコントラクトの更新は容易ではないでしょう。)

Solana Program Library(略してspl)はSolana Labsにより開発、運営しているオンチェーンプログラムの一部に過ぎないです。solanaのToken Programはsplの一部分です。spl-token programとよく言われています。

プログラムがトランザクション間で状態を保存する必要がある場合、アカウントを使用して行います。アカウントは任意のデータを保持することができます。データの存続はlamports(1 lamport = 0.000000001 SOL)の数で表されます。アカウントはバリデータのメモリに保持され、Rent(レンタル料)を支払ってそこに留まります。lamportsがゼロになると、そのアカウントは削除されます。アカウントに十分な数のlamportsが含まれていれば、レンタル免除(rent-exempt)とマークすることもできます。

 

1.1 Pubkey

pub struct Pubkey([u8; 32]);
Solanaクライアントはアドレス(Pubkey)を使用してアカウントを参照します。
このアドレスは256ビットの公開鍵です。通常、32byteで表されるbase58エンコードされたアドレスになります。
プログラムのアドレス、いわゆるProgram Idもこのタイプのアドレスです。
solana cli でPubkeyのアドレスが容易に作れます。
// jsonファイルではプライベートキーを格納されています 
solana-keygen new --outfile 目的dir/ファイル名.json

---
Generating a new keypair

For added security, enter a BIP39 passphrase

NOTE! This passphrase improves security of the recovery seed phrase NOT the
keypair file itself, which is stored as insecure plain text

BIP39 Passphrase (empty for none): 

Wrote new keypair to solana-wallet/keypair.json
================================================================================
pubkey: 4wb4h4XExBm9hF2g3XqAuUE4T8NBErj1QJoFrN6uPNBu
================================================================================
Save this seed phrase and your BIP39 passphrase to recover your new keypair:
sister lucky weasel trouble mosquito bread system winter transfer crew lava page
================================================================================
 

1.2 AccountInfo

Account情報は以下の通りです。
/// Account information
#[derive(Clone)]
pub struct AccountInfo<'a> {
    /// Public key of the account
    pub key: &'a Pubkey,
    /// Was the transaction signed by this account's public key?
    pub is_signer: bool,
    /// Is the account writable?
    pub is_writable: bool,
    /// The lamports in the account.  Modifiable by programs.
    pub lamports: Rc<RefCell<&'a mut u64>>,
    /// The data held in this account.  Modifiable by programs.
    pub data: Rc<RefCell<&'a mut [u8]>>,
    /// Program that owns this account
    pub owner: &'a Pubkey,
    /// This account's data contains a loaded program (and is now read-only)
    pub executable: bool,
    /// The epoch at which this account will next owe rent
    pub rent_epoch: Epoch,
}
solanaでのAccount(アカウント)ですが、大別すると2種類があります。data領域に実行可能なコード(変更不可)が格納される場合、Program Accountと呼ばれます。data領域にユーザの定義したデータ(変更が可能)が格納される場合、Data Accountと呼ばれます。

あるPubkeyがSOLを着金した途端、ownerはSystem Programに初期化され、System Accountも呼ばれます。data領域が空です。

あるPubkeyまたはSystem AccountがSystem Programによりdataをユーザの指定したコンテンツの通りに初期化、ownerもユーザ指定したprogram idに初期化され、Data Accountに変身できます。

あるPubkeyがBPF Loader Programにより実行コードをdataに初期化され、Program Accountに変身できます。


以下のは、各領域を個々に説明します。

・key
アカウントの情報はPubkeyで参照されます。

・is_signer
トランザクションには、一つまたはそれ以上のデジタル署名が含まれ、それぞれがトランザクションによって参照されるアカウントアドレスに対応します。これらのアドレスは全てed25519のキーペアの公開鍵でなければならず、署名は対応する秘密鍵の所有者が署名し、つまり、「承認」したトランザクションを示します。この場合、アカウントは署名者(signer)と呼ばれます。アカウントが署名者であるかどうかは、アカウントのメタデータの一部としてプログラムに伝えられます。プログラムはその情報を使用して権限の決定を行うことができます。

・is_writable
トランザクションは、それが参照するアカウントの一部を読み取り専用アカウントとして扱うよう指示することができます。これにより、トランザクション間での並列アカウント処理が可能となります。ランタイムは、複数のプログラムが同時に読み取り専用アカウントを読むことを許可します。プログラムが読み取り専用アカウントを変更しようとすると、トランザクションはランタイムによって拒否されます。

・lamports

所有するネイティブトークンです。レンタル免除(rent-exempt)するための十分な数のlamportsであれば、dataを存続させる。でないと、dataがクリアされます。

・data

任意のステートのデータです。

アカウントにはメモリの制限があり、アカウントの最大サイズは10メガバイトとなります。

・owner

アカウントにはOwner(所有者)はプログラムidです(プログラムで初期化される)。ランタイム(solanaのruntime)は、そのidが所有者と一致する場合、プログラムにアカウントへの書き込みアクセスを許可します。アカウントの所有権(つまり、所有者を別のプログラムidに変更する)を割り当てることを許可します。アカウントがプログラムに所有されていない場合、プログラムはそのデータを読み取り、アカウントにSOL送金のみが許可されます。

・executable

アカウントで「executable」にマークされている場合、そのアカウントはプログラムとみなされ、インストラクションのプログラムIDにアカウントの公開鍵を含めることで実行できます。dataにはソースコードを保持されます。

・rent_epoch

2年分のレンタル料以上の最小LAMPORT残高を維持しているアカウントは「レンタル免除」アカウントと見なされ、レンタル料の請求は発生しません。

新しいアカウントやプログラムは、レンタル免除になるために十分なLAMPORTSで初期化する必要があります。RPCエンドポイントはこの推定レンタル免除バランスを計算する能力を持っており、使用が推奨されています。

アカウントの残高が減少するたびに、アカウントがまだレンタル免除であるかどうかのチェックが行われます。アカウントの残高がレンタル免除の閾値を下回るようなトランザクションは失敗します。

solana cliでrent_epochの詳しいことが取得できます。
// solana rent <data size>
solana rent 165

-----
Rent per byte-year: 0.00000348 SOL
Rent per epoch: 0.000005583 SOL
Rent-exempt minimum: 0.00203928 SOL
 

1.3 AccountMeta

#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct AccountMeta {
    /// An account's public key.
    pub pubkey: Pubkey,
    /// True if an `Instruction` requires a `Transaction` signature matching `pubkey`.
    pub is_signer: bool,
    /// True if the account data or metadata may be mutated during program execution.
    pub is_writable: bool,
}
AccountMetaは主にInstructionの定義に使用され、Instructionにアカウント情報を伝達するのに役立ちます。これには、アカウントのアドレス、そのアカウントが署名アカウントであるかどうか、そしてそのアカウントに対応する内容dataが変更可能かどうかが含まれます。

クライアント側でmint accountを初期化させることを例にする。以下のkeysの埋め方に参照してください。
/**
 * Construct an InitializeMint instruction
 *
 * @param mint            Token mint account
 * @param decimals        Number of decimals in token account amounts
 * @param mintAuthority   Minting authority
 * @param freezeAuthority Optional authority that can freeze token accounts
 * @param programId       SPL Token program account
 *
 * @return Instruction to add to a transaction
 */
export function createInitializeMintInstruction(
    mint: PublicKey,
    decimals: number,
    mintAuthority: PublicKey,
    freezeAuthority: PublicKey | null,
    programId = TOKEN_PROGRAM_ID
): TransactionInstruction {
    const keys = [
        { pubkey: mint, isSigner: false, isWritable: true },
        { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
    ];

    const data = Buffer.alloc(initializeMintInstructionData.span);
    initializeMintInstructionData.encode(
        {
            instruction: TokenInstruction.InitializeMint,
            decimals,
            mintAuthority,
            freezeAuthorityOption: freezeAuthority ? 1 : 0,
            freezeAuthority: freezeAuthority || new PublicKey(0),
        },
        data
    );

    return new TransactionInstruction({ keys, programId, data });
}
 

2.Token Program

このプログラムは、代替可能トークン(Fungible tokens)と代替不可能トークン(Non Fungible tokens)の共通の実装を定義しています。token/programのファイル構成は以下の通りです。
.
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── entrypoint.rs
│   ├── error.rs
│   ├── instruction.rs
│   ├── lib.rs
│   ├── processsor.rs
│   ├── native_mint.rs
│   └── state.rs
├── inc
├── tests
└── Xargo.toml
 

・entrypoint.rs

entrypoint.rsファイルでは、入り口のprocess_instruction関数が定義されています。
entrypoint!(process_instruction);
fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    if let Err(error) = Processor::process(program_id, accounts, instruction_data) {
        // catch the error so we can print it
        error.print::<TokenError>();
        return Err(error);
    }
    Ok(())
}
パラメータは三つしかありません。

program_idは呼び出し先のprogramのidとなります。

accountsはAccountInfo型の配列で、読み取り専用の対象アカウント、または記入しようとする対象アカウントを全て渡されます。プログラムはこのらのアカウントにCRUD(Create、 Read、Update、Delete)処理を行います。

instruction_dataは、アカウントのdata領域に記入する序列化されたコンテンツです。

 

・processsor.rs

instructionを処理するロジックはここで記載されています。

instructionの種類によって、処理が異なります。

 

・instruction.rs

runtimeからのinstructionを解析し、異なる種類に作成します。クライアントから受け入れたinstructionの初めてのバイトは種類に当たります。以下の種類があります。
export enum TokenInstruction {
    InitializeMint = 0,
    InitializeAccount = 1,
    InitializeMultisig = 2,
    Transfer = 3,
    Approve = 4,
    Revoke = 5,
    SetAuthority = 6,
    MintTo = 7,
    Burn = 8,
    CloseAccount = 9,
    FreezeAccount = 10,
    ThawAccount = 11,
    TransferChecked = 12,
    ApproveChecked = 13,
    MintToChecked = 14,
    BurnChecked = 15,
    InitializeAccount2 = 16,
    SyncNative = 17,
    InitializeAccount3 = 18,
    InitializeMultisig2 = 19,
    InitializeMint2 = 20,
}
instruction_dataの序列化(pack)と逆序列化(unpack)も定義されています。

そして、他のプログラムに呼び出せるinstructionのインターフェースもここで定義されています。

 

・lib.rs

utils方法がここで定義されています。一番重要なのは、
solana_program::declare_id!(“TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA”)です。
「solana_program::declare_id!」マクロを使用することで、この文字列をSolana上のユニークなIDとして宣言することができます。このIDは、Solanaスマートコントラクトの開発時に特定のコントラクトやプログラムを識別および参照するために使用されます。

IDの宣言により、Solanaブロックチェーン上のスマートコントラクトは、コントラクトのアドレスや他の情報をハードコーディングすることなく相互参照や相互作用が可能となります。これにより、コントラクトの開発とメンテナンスがより便利になり、スケーラビリティと相互運用性も向上します。

 

・state.rs

記入可能なAccountInfo型のアカウントのdataに記入するコンテンツのデータストラクチャーが定義されています。同時にデータストラクチャーに付随する方法も実現します。詳しくは、後でも触れます。

 

・error.rs
enum型のエラーの定義です。
プログラムの呼び出しで失敗となった場合、エラー番号が返却されます。該当エラー番号をenum型のエラーの定義に照らし合わせて、エラー内容を知られます。

 

2.1Token Programでのデータストラクチャー

2.1.1mint account

/// Mint data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
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<Pubkey>,
    /// Total supply of tokens.
    pub supply: u64,
    /// Number of base 10 digits to the right of the decimal place.
    pub decimals: u8,
    /// Is `true` if this structure has been initialized
    pub is_initialized: bool,
    /// Optional authority to freeze token accounts.
    pub freeze_authority: COption<Pubkey>,
}
Token programでアカウントのデータにmint dataに初期化された場合、該当アカウントはmint accountと呼ばれます。
mint accountは代替可能トークン(Fungible tokens)と代替不可能トークン(Non Fungible tokens)の属性を定義されています。
NFT(非代替可能トークン)は、単一のトークンのみが発行されているトークンタイプです。
通常はdecimalsを0に指定します。
mint_authorityとfreeze_authorityの二つの管理ロールが設けられています。
mint_authorityのアカウントしかミントできない、freeze_authorityのアカウントしかアカウントを凍結しかできないです。

2.1.2 token account

/// Account data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
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.
    pub amount: u64,
    /// If `delegate` is `Some` then `delegated_amount` represents
    /// the amount authorized by the delegate
    pub delegate: COption<Pubkey>,
    /// The account's state
    pub state: AccountState,
    /// 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.
    pub is_native: COption<u64>,
    /// The amount delegated
    pub delegated_amount: u64,
    /// Optional authority to close the account.
    pub close_authority: COption<Pubkey>,
}
Token programでアカウントのデータにaccount dataに初期化された場合、該当アカウントはtoken accountと呼ばれます。

token accountはmint accountと紐づいています。属性でのamountとは、該当mint account の発行トークン総量(supply)のの所持分となります。

ownerは通常ed25519のキーペアの公開鍵であり、ownerのトランザクションでの署名で該当token accountでのamountを送金できます。または他のアカウント(delegate)にdelegated_amountの金額をデリゲートします。

close_authorityを指定した場合、close_authorityのアカウントで該当token accountをクローズできます。

close_authorityを指定しない場合、ownerのアカウントで該当token accountをクローズできます。

 

2.1.3 MultiSig account

/// Multisignature data.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
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
    pub is_initialized: bool,
    /// Signer public keys
    pub signers: [Pubkey; MAX_SIGNERS],
}
Token programでアカウントのデータにMultisignature dataに初期化された場合、該当アカウントはmultisig accountと呼ばれます。

つまり、トークンの送金する場合、m/n署名が可能です。ここでのnは最大は11と定めています。

 

2.2 提供した機能

2.2.1 create mint

fn _process_initialize_mint(
    accounts: &[AccountInfo],
    decimals: u8,
    mint_authority: Pubkey,
    freeze_authority: COption<Pubkey>,
    rent_sysvar_account: bool,
) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let mint_info = next_account_info(account_info_iter)?;
    let mint_data_len = mint_info.data_len();
    let rent = if rent_sysvar_account {
        Rent::from_account_info(next_account_info(account_info_iter)?)?
    } else {
        Rent::get()?
    };

    let mut mint = Mint::unpack_unchecked(&mint_info.data.borrow())?;
    if mint.is_initialized {
        return Err(TokenError::AlreadyInUse.into());
    }

    if !rent.is_exempt(mint_info.lamports(), mint_data_len) {
        return Err(TokenError::NotRentExempt.into());
    }

    mint.mint_authority = COption::Some(mint_authority);
    mint.decimals = decimals;
    mint.is_initialized = true;
    mint.freeze_authority = freeze_authority;

    Mint::pack(mint, &mut mint_info.data.borrow_mut())?;

    Ok(())
}
クライアント側指定したアカウントmintのdataをmint dataで初期化する。
初期化済みの場合、再度初期化しようとする際には、TokenError::AlreadyInUseのエラーとなります。
アカウントmintでのlamportsがレンタル免除に足りない場合、TokenError::NotRentExemptのエラーとなります。

2.2.2 create token account

fn _process_initialize_account(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        owner: Option<&Pubkey>,
        rent_sysvar_account: bool,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let new_account_info = next_account_info(account_info_iter)?;
        let mint_info = next_account_info(account_info_iter)?;
        let owner = if let Some(owner) = owner {
            owner
        } else {
            next_account_info(account_info_iter)?.key
        };
        let new_account_info_data_len = new_account_info.data_len();
        let rent = if rent_sysvar_account {
            Rent::from_account_info(next_account_info(account_info_iter)?)?
        } else {
            Rent::get()?
        };

        let mut account = Account::unpack_unchecked(&new_account_info.data.borrow())?;
        if account.is_initialized() {
            return Err(TokenError::AlreadyInUse.into());
        }

        if !rent.is_exempt(new_account_info.lamports(), new_account_info_data_len) {
            return Err(TokenError::NotRentExempt.into());
        }

        let is_native_mint = Self::cmp_pubkeys(mint_info.key, &crate::native_mint::id());
        if !is_native_mint {
            Self::check_account_owner(program_id, mint_info)?;
            let _ = Mint::unpack(&mint_info.data.borrow_mut())
                .map_err(|_| Into::<ProgramError>::into(TokenError::InvalidMint))?;
        }

        account.mint = *mint_info.key;
        account.owner = *owner;
        account.close_authority = COption::None;
        account.delegate = COption::None;
        account.delegated_amount = 0;
        account.state = AccountState::Initialized;
        if is_native_mint {
            let rent_exempt_reserve = rent.minimum_balance(new_account_info_data_len);
            account.is_native = COption::Some(rent_exempt_reserve);
            account.amount = new_account_info
                .lamports()
                .checked_sub(rent_exempt_reserve)
                .ok_or(TokenError::Overflow)?;
        } else {
            account.is_native = COption::None;
            account.amount = 0;
        };

        Account::pack(account, &mut new_account_info.data.borrow_mut())?;

        Ok(())
    }
クライアント側指定したアカウントのdataをtoken dataで初期化する。

初期化済みの場合、再度初期化しようとする際には、TokenError::AlreadyInUseのエラーとなります。
アカウントでのlamportsがレンタル免除(rent-exempt)に足りない場合、TokenError::NotRentExemptのエラーとなります。

Self::check_account_owner(program_id, mint_info)方法で、指定したアカウントのmint_infoのownerは該当プログラムのprogram_idか否かをチェックします。一致ではない場合、ProgramError::IncorrectProgramIdのエラーとなります。

チェックを通らせるためには、事前に対象アカウントのmint_infoのアカウントの初期化で該当program_idでownerを初期化すべきですね。

 

2.2.3 create multisig account

fn _process_initialize_multisig(
        accounts: &[AccountInfo],
        m: u8,
        rent_sysvar_account: bool,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let multisig_info = next_account_info(account_info_iter)?;
        let multisig_info_data_len = multisig_info.data_len();
        let rent = if rent_sysvar_account {
            Rent::from_account_info(next_account_info(account_info_iter)?)?
        } else {
            Rent::get()?
        };

        let mut multisig = Multisig::unpack_unchecked(&multisig_info.data.borrow())?;
        if multisig.is_initialized {
            return Err(TokenError::AlreadyInUse.into());
        }

        if !rent.is_exempt(multisig_info.lamports(), multisig_info_data_len) {
            return Err(TokenError::NotRentExempt.into());
        }

        let signer_infos = account_info_iter.as_slice();
        multisig.m = m;
        multisig.n = signer_infos.len() as u8;
        if !is_valid_signer_index(multisig.n as usize) {
            return Err(TokenError::InvalidNumberOfProvidedSigners.into());
        }
        if !is_valid_signer_index(multisig.m as usize) {
            return Err(TokenError::InvalidNumberOfRequiredSigners.into());
        }
        for (i, signer_info) in signer_infos.iter().enumerate() {
            multisig.signers[i] = *signer_info.key;
        }
        multisig.is_initialized = true;

        Multisig::pack(multisig, &mut multisig_info.data.borrow_mut())?;

        Ok(())
    }
クライアント側指定したアカウントのdataをmultisignature dataで初期化する。
初期化済みの場合、再度初期化しようとする際には、TokenError::AlreadyInUseのエラーとなります。
アカウントでのlamportsがレンタル免除に足りない場合、TokenError::NotRentExemptのエラーとなります。
必要な署名の数が足りない、または、署名の総数のnが11より大きない場合、TokenError::InvalidNumberOfRequiredSignersのエラーとなります。

2.2.4 mint to

pub fn process_mint_to(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        amount: u64,
        expected_decimals: Option<u8>,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let mint_info = next_account_info(account_info_iter)?;
        let destination_account_info = next_account_info(account_info_iter)?;
        let owner_info = next_account_info(account_info_iter)?;

        let mut destination_account = Account::unpack(&destination_account_info.data.borrow())?;
        if destination_account.is_frozen() {
            return Err(TokenError::AccountFrozen.into());
        }

        if destination_account.is_native() {
            return Err(TokenError::NativeNotSupported.into());
        }
        if !Self::cmp_pubkeys(mint_info.key, &destination_account.mint) {
            return Err(TokenError::MintMismatch.into());
        }

        let mut mint = Mint::unpack(&mint_info.data.borrow())?;
        if let Some(expected_decimals) = expected_decimals {
            if expected_decimals != mint.decimals {
                return Err(TokenError::MintDecimalsMismatch.into());
            }
        }

        match mint.mint_authority {
            COption::Some(mint_authority) => Self::validate_owner(
                program_id,
                &mint_authority,
                owner_info,
                account_info_iter.as_slice(),
            )?,
            COption::None => return Err(TokenError::FixedSupply.into()),
        }

        if amount == 0 {
            Self::check_account_owner(program_id, mint_info)?;
            Self::check_account_owner(program_id, destination_account_info)?;
        }

        destination_account.amount = destination_account
            .amount
            .checked_add(amount)
            .ok_or(TokenError::Overflow)?;

        mint.supply = mint
            .supply
            .checked_add(amount)
            .ok_or(TokenError::Overflow)?;

        Account::pack(
            destination_account,
            &mut destination_account_info.data.borrow_mut(),
        )?;
        Mint::pack(mint, &mut mint_info.data.borrow_mut())?;

        Ok(())
    }
クライアント側指定したtoken accountにトークンを発行する。supplyもカウントアップする。

ミントの宛先のアカウントは、凍結された場合、TokenError::AccountFrozenのエラーとなります。

ミントの宛先のアカウントのmintの情報はパラメーターでのと不一致となった場合、
TokenError::MintMismatchまたはTokenError::MintDecimalsMismatchのエラーとなります。

ミントの権限を持たない場合、TokenError::OwnerMismatchのエラーとなります。ミントの権限の持つアカウントの署名がない場合、ProgramError::MissingRequiredSignatureのエラーとなります。
Multisigの場合、必要な署名数が足りない場合も、ProgramError::MissingRequiredSignatureのエラーとなります。
mint_infoのアカウントとミントの宛先のアカウントのownerは該当program idではない場合、ProgramError::IncorrectProgramIdのエラーとなります。

2.2.5 burn

pub fn process_burn(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        amount: u64,
        expected_decimals: Option<u8>,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();

        let source_account_info = next_account_info(account_info_iter)?;
        let mint_info = next_account_info(account_info_iter)?;
        let authority_info = next_account_info(account_info_iter)?;

        let mut source_account = Account::unpack(&source_account_info.data.borrow())?;
        let mut mint = Mint::unpack(&mint_info.data.borrow())?;

        if source_account.is_frozen() {
            return Err(TokenError::AccountFrozen.into());
        }
        if source_account.is_native() {
            return Err(TokenError::NativeNotSupported.into());
        }
        if source_account.amount < amount {
            return Err(TokenError::InsufficientFunds.into());
        }
        if !Self::cmp_pubkeys(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 {
                COption::Some(ref delegate) if Self::cmp_pubkeys(authority_info.key, delegate) => {
                    Self::validate_owner(
                        program_id,
                        delegate,
                        authority_info,
                        account_info_iter.as_slice(),
                    )?;

                    if source_account.delegated_amount < amount {
                        return Err(TokenError::InsufficientFunds.into());
                    }
                    source_account.delegated_amount = source_account
                        .delegated_amount
                        .checked_sub(amount)
                        .ok_or(TokenError::Overflow)?;
                    if source_account.delegated_amount == 0 {
                        source_account.delegate = COption::None;
                    }
                }
                _ => Self::validate_owner(
                    program_id,
                    &source_account.owner,
                    authority_info,
                    account_info_iter.as_slice(),
                )?,
            }
        }

        if amount == 0 {
            Self::check_account_owner(program_id, source_account_info)?;
            Self::check_account_owner(program_id, mint_info)?;
        }

        source_account.amount = source_account
            .amount
            .checked_sub(amount)
            .ok_or(TokenError::Overflow)?;
        mint.supply = mint
            .supply
            .checked_sub(amount)
            .ok_or(TokenError::Overflow)?;

        Account::pack(source_account, &mut source_account_info.data.borrow_mut())?;
        Mint::pack(mint, &mut mint_info.data.borrow_mut())?;

        Ok(())
    }
oken accountのownerにより、token accountの所持するトークンを焼却する。supplyもカウントダウンする。
burnしようとするtokenアカウントは、凍結された場合、TokenError::AccountFrozenのエラーとなります。
burnしようとする金額はtokenアカウントの所持する金額より大きい場合、TokenError::InsufficientFundsのエラーとなります。
tokenアカウントのmint情報はパラメーターのと不一致となった場合、TokenError::MintMismatchのエラーとなります。
burnを他のユーザdelegateに肩代わり実施する許可も可能です。
ownerまたはdelegateの権限を持たない場合、TokenError::OwnerMismatchのエラーとなります。ownerの権限の持つアカウントの署名がない場合、ProgramError::MissingRequiredSignatureのエラーとなります。
delegateの場合、burnの許可金額はburnしようとする金額より大きい場合、TokenError::InsufficientFundsのエラーとなります。
burnしようとするtokenアカウントと紐づいているミントのアカウントのownerは該当program idではない場合、ProgramError::IncorrectProgramIdのエラーとなります。

2.2.6 close

pub fn process_close_account(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let source_account_info = next_account_info(account_info_iter)?;
        let destination_account_info = next_account_info(account_info_iter)?;
        let authority_info = next_account_info(account_info_iter)?;

        if Self::cmp_pubkeys(source_account_info.key, destination_account_info.key) {
            return Err(ProgramError::InvalidAccountData);
        }

        let source_account = Account::unpack(&source_account_info.data.borrow())?;
        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() {
            Self::validate_owner(
                program_id,
                &authority,
                authority_info,
                account_info_iter.as_slice(),
            )?;
        } else if !solana_program::incinerator::check_id(destination_account_info.key) {
            return Err(ProgramError::InvalidAccountData);
        }

        let destination_starting_lamports = destination_account_info.lamports();
        **destination_account_info.lamports.borrow_mut() = destination_starting_lamports
            .checked_add(source_account_info.lamports())
            .ok_or(TokenError::Overflow)?;

        **source_account_info.lamports.borrow_mut() = 0;
        delete_account(source_account_info)?;

        Ok(())
    }
token accountの所持するトークンが0の場合、token accountのownerにより、該当token accountのlamportsを指定したdestination account回収します。これで、dataがクリアされます。

クローズしようとするカウントはdestination accountが同一の場合、ProgramError::InvalidAccountDataのエラーとなります。

クローズしようとするカウントのトークン残高は0ではない場合、TokenError::NonNativeHasBalanceのエラーとなります。

ownerの権限を持たない場合、TokenError::OwnerMismatchのエラーとなります。ownerの権限の持つアカウントの署名がない場合、ProgramError::MissingRequiredSignatureのエラーとなります。

 

2.2.7 freeze

pub fn process_toggle_freeze_account(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        freeze: bool,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let source_account_info = next_account_info(account_info_iter)?;
        let mint_info = next_account_info(account_info_iter)?;
        let authority_info = next_account_info(account_info_iter)?;

        let mut source_account = Account::unpack(&source_account_info.data.borrow())?;
        if freeze && source_account.is_frozen() || !freeze && !source_account.is_frozen() {
            return Err(TokenError::InvalidState.into());
        }
        if source_account.is_native() {
            return Err(TokenError::NativeNotSupported.into());
        }
        if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) {
            return Err(TokenError::MintMismatch.into());
        }

        let mint = Mint::unpack(&mint_info.data.borrow_mut())?;
        match mint.freeze_authority {
            COption::Some(authority) => Self::validate_owner(
                program_id,
                &authority,
                authority_info,
                account_info_iter.as_slice(),
            ),
            COption::None => Err(TokenError::MintCannotFreeze.into()),
        }?;

        source_account.state = if freeze {
            AccountState::Frozen
        } else {
            AccountState::Initialized
        };

        Account::pack(source_account, &mut source_account_info.data.borrow_mut())?;

        Ok(())
    }
freeze authorityであるtoken accountの送受金を一時停止させます。
freezeしようとするtokenアカウントのmint情報はパラメーターのと不一致となった場合、TokenError::MintMismatchのエラーとなります。
freezeしようとするtokenアカウントが既に凍結された場合、TokenError::InvalidStateのエラーとなります。
freezeの権限を持たない場合、TokenError::OwnerMismatchのエラーとなります。freezeの権限の持つアカウントの署名がない場合、ProgramError::MissingRequiredSignatureのエラーとなります。

2.2.8 thaw

pub fn process_toggle_freeze_account(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        freeze: bool,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let source_account_info = next_account_info(account_info_iter)?;
        let mint_info = next_account_info(account_info_iter)?;
        let authority_info = next_account_info(account_info_iter)?;

        let mut source_account = Account::unpack(&source_account_info.data.borrow())?;
        if freeze && source_account.is_frozen() || !freeze && !source_account.is_frozen() {
            return Err(TokenError::InvalidState.into());
        }
        if source_account.is_native() {
            return Err(TokenError::NativeNotSupported.into());
        }
        if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) {
            return Err(TokenError::MintMismatch.into());
        }

        let mint = Mint::unpack(&mint_info.data.borrow_mut())?;
        match mint.freeze_authority {
            COption::Some(authority) => Self::validate_owner(
                program_id,
                &authority,
                authority_info,
                account_info_iter.as_slice(),
            ),
            COption::None => Err(TokenError::MintCannotFreeze.into()),
        }?;

        source_account.state = if freeze {
            AccountState::Frozen
        } else {
            AccountState::Initialized
        };

        Account::pack(source_account, &mut source_account_info.data.borrow_mut())?;

        Ok(())
    }
freeze authorityであるtoken accountを解凍し、送受金を再開させます。

解凍しようとするtokenアカウントのmint情報はパラメーターのと不一致となった場合、TokenError::MintMismatchのエラーとなります。
解凍しようとするtokenアカウントが凍結されていない場合、TokenError::InvalidStateのエラーとなります。
freezeの権限を持たない場合、TokenError::OwnerMismatchのエラーとなります。権限の持つアカウントの署名がない場合、ProgramError::MissingRequiredSignatureのエラーとなります。

2.2.9 set authority

pub fn process_set_authority(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        authority_type: AuthorityType,
        new_authority: COption<Pubkey>,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let account_info = next_account_info(account_info_iter)?;
        let authority_info = next_account_info(account_info_iter)?;

        if account_info.data_len() == Account::get_packed_len() {
            let mut account = Account::unpack(&account_info.data.borrow())?;

            if account.is_frozen() {
                return Err(TokenError::AccountFrozen.into());
            }

            match authority_type {
                AuthorityType::AccountOwner => {
                    Self::validate_owner(
                        program_id,
                        &account.owner,
                        authority_info,
                        account_info_iter.as_slice(),
                    )?;

                    if let COption::Some(authority) = new_authority {
                        account.owner = authority;
                    } else {
                        return Err(TokenError::InvalidInstruction.into());
                    }

                    account.delegate = COption::None;
                    account.delegated_amount = 0;

                    if account.is_native() {
                        account.close_authority = COption::None;
                    }
                }
                AuthorityType::CloseAccount => {
                    let authority = account.close_authority.unwrap_or(account.owner);
                    Self::validate_owner(
                        program_id,
                        &authority,
                        authority_info,
                        account_info_iter.as_slice(),
                    )?;
                    account.close_authority = new_authority;
                }
                _ => {
                    return Err(TokenError::AuthorityTypeNotSupported.into());
                }
            }
            Account::pack(account, &mut account_info.data.borrow_mut())?;
        } else if account_info.data_len() == Mint::get_packed_len() {
            let mut mint = Mint::unpack(&account_info.data.borrow())?;
            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(Into::<ProgramError>::into(TokenError::FixedSupply))?;
                    Self::validate_owner(
                        program_id,
                        &mint_authority,
                        authority_info,
                        account_info_iter.as_slice(),
                    )?;
                    mint.mint_authority = new_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(Into::<ProgramError>::into(TokenError::MintCannotFreeze))?;
                    Self::validate_owner(
                        program_id,
                        &freeze_authority,
                        authority_info,
                        account_info_iter.as_slice(),
                    )?;
                    mint.freeze_authority = new_authority;
                }
                _ => {
                    return Err(TokenError::AuthorityTypeNotSupported.into());
                }
            }
            Mint::pack(mint, &mut account_info.data.borrow_mut())?;
        } else {
            return Err(ProgramError::InvalidArgument);
        }

        Ok(())
    }
close authorityがnullの場合、今のownerでtoken accountでのownerまたはclose authorityのアカウントを変更します。ownerをnullへ変更が不可、TokenError::InvalidInstructionのエラーとなります。

close authorityがnullへ変更可能です。

close authorityがnullではない場合、今のclose authorityでtoken accountでのclose authorityのアカウントを変更します。

今のmint authorityでmint accountでのmint authorityのアカウントを変更します。mint authorityをnullにする場合、mint機能が無効となり、復旧することができません。復旧しようとする場合、TokenError::FixedSupplyのエラーとなります。
今のfreeze authorityでmint accountでのfreeze authorityのアカウントを変更します。freeze authorityをnullにする場合、freeze機能が無効となり、復旧することができません。復旧しようとする場合、TokenError::MintCannotFreezeのエラーとなります。
対応する権限を持たない場合、TokenError::OwnerMismatchのエラーとなります。権限の持つアカウントの署名がない場合、ProgramError::MissingRequiredSignatureのエラーとなります。
変更先のアカウントは凍結された場合、TokenError::AccountFrozenのエラーとなります。
ownerを変更しようとする場合、該当アカウントのdelegate情報も初期化されます。

 

2.2.10 delegate

pub fn process_approve(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        amount: u64,
        expected_decimals: Option<u8>,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();

        let source_account_info = next_account_info(account_info_iter)?;

        let expected_mint_info = if let Some(expected_decimals) = expected_decimals {
            Some((next_account_info(account_info_iter)?, expected_decimals))
        } else {
            None
        };
        let delegate_info = next_account_info(account_info_iter)?;
        let owner_info = next_account_info(account_info_iter)?;

        let mut source_account = Account::unpack(&source_account_info.data.borrow())?;

        if source_account.is_frozen() {
            return Err(TokenError::AccountFrozen.into());
        }

        if let Some((mint_info, expected_decimals)) = expected_mint_info {
            if !Self::cmp_pubkeys(mint_info.key, &source_account.mint) {
                return Err(TokenError::MintMismatch.into());
            }

            let mint = Mint::unpack(&mint_info.data.borrow_mut())?;
            if expected_decimals != mint.decimals {
                return Err(TokenError::MintDecimalsMismatch.into());
            }
        }

        Self::validate_owner(
            program_id,
            &source_account.owner,
            owner_info,
            account_info_iter.as_slice(),
        )?;

        source_account.delegate = COption::Some(*delegate_info.key);
        source_account.delegated_amount = amount;

        Account::pack(source_account, &mut source_account_info.data.borrow_mut())?;

        Ok(())
    }
ERC20でのapproveと似ています。他のアカウントに自分の所持するトークンの送金を許可する。

ただ、ここでの「他のアカウント」は一つのアカウントにするしかありません。ERC20では複数のアカウントに任意の金額を送金許可できますね。
対応する権限を持たない場合、TokenError::OwnerMismatchのエラーとなります。権限の持つアカウントの署名がない場合、ProgramError::MissingRequiredSignatureのエラーとなります。
指定したdelegateのアカウントは凍結された場合、TokenError::AccountFrozenのエラーとなります。
許可しようとするtokenアカウントのmint情報はパラメーターのと不一致となった場合、TokenError::MintMismatchのエラーとなります。

2.2.11 revoke

pub fn process_revoke(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let source_account_info = next_account_info(account_info_iter)?;

        let mut source_account = Account::unpack(&source_account_info.data.borrow())?;

        let owner_info = next_account_info(account_info_iter)?;

        if source_account.is_frozen() {
            return Err(TokenError::AccountFrozen.into());
        }

        Self::validate_owner(
            program_id,
            &source_account.owner,
            owner_info,
            account_info_iter.as_slice(),
        )?;

        source_account.delegate = COption::None;
        source_account.delegated_amount = 0;

        Account::pack(source_account, &mut source_account_info.data.borrow_mut())?;

        Ok(())
    }

    /// Processes a [SetAuthority](enum.TokenInstruction.html) instruction.
    pub fn process_set_authority(
        program_id: &Pubkey,
        accounts: &[AccountInfo],
        authority_type: AuthorityType,
        new_authority: COption<Pubkey>,
    ) -> ProgramResult {
        let account_info_iter = &mut accounts.iter();
        let account_info = next_account_info(account_info_iter)?;
        let authority_info = next_account_info(account_info_iter)?;

        if account_info.data_len() == Account::get_packed_len() {
            let mut account = Account::unpack(&account_info.data.borrow())?;

            if account.is_frozen() {
                return Err(TokenError::AccountFrozen.into());
            }

            match authority_type {
                AuthorityType::AccountOwner => {
                    Self::validate_owner(
                        program_id,
                        &account.owner,
                        authority_info,
                        account_info_iter.as_slice(),
                    )?;

                    if let COption::Some(authority) = new_authority {
                        account.owner = authority;
                    } else {
                        return Err(TokenError::InvalidInstruction.into());
                    }

                    account.delegate = COption::None;
                    account.delegated_amount = 0;

                    if account.is_native() {
                        account.close_authority = COption::None;
                    }
                }
                AuthorityType::CloseAccount => {
                    let authority = account.close_authority.unwrap_or(account.owner);
                    Self::validate_owner(
                        program_id,
                        &authority,
                        authority_info,
                        account_info_iter.as_slice(),
                    )?;
                    account.close_authority = new_authority;
                }
                _ => {
                    return Err(TokenError::AuthorityTypeNotSupported.into());
                }
            }
            Account::pack(account, &mut account_info.data.borrow_mut())?;
        } else if account_info.data_len() == Mint::get_packed_len() {
            let mut mint = Mint::unpack(&account_info.data.borrow())?;
            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(Into::<ProgramError>::into(TokenError::FixedSupply))?;
                    Self::validate_owner(
                        program_id,
                        &mint_authority,
                        authority_info,
                        account_info_iter.as_slice(),
                    )?;
                    mint.mint_authority = new_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(Into::<ProgramError>::into(TokenError::MintCannotFreeze))?;
                    Self::validate_owner(
                        program_id,
                        &freeze_authority,
                        authority_info,
                        account_info_iter.as_slice(),
                    )?;
                    mint.freeze_authority = new_authority;
                }
                _ => {
                    return Err(TokenError::AuthorityTypeNotSupported.into());
                }
            }
            Mint::pack(mint, &mut account_info.data.borrow_mut())?;
        } else {
            return Err(ProgramError::InvalidArgument);
        }

        Ok(())
    }
他のアカウントに自分の所持するトークンの送金の許可を取り消します。
対応する権限を持たない場合、TokenError::OwnerMismatchのエラーとなります。権限の持つアカウントの署名がない場合、ProgramError::MissingRequiredSignatureのエラーとなります。
取り消ししようとするdelegateのアカウントは凍結された場合、TokenError::AccountFrozenのエラーとなります。
 

3.まとめ

Token Programはデプロイ済み、TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DAのprogram idとなります。etherscanと異なり、ソースコードとの紐付けが見られないです。

solscanでのTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DAソースコードは改竄されていないか疑問されている方がいますね。テストを通して、改竄しないことを証明することができるはずですね。

誰でもTokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DAでmint accountが作れ、トークンが発行できます。ただ、新規な機能を拡張することができないですね。

Token Programが提供する機能のスーパーセットとして、Token-2022プログラムは作られました。新規機能が追加されつつあります。

現在プログラムはまだ監査中であり、完全なプロダクション使用を意図したものではないとオフィシャルドキュメントに書いてありますが、既にsolscanでデプロイされました。TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEbです。

このToken-2022プログラムで既に多くのトークンが発行されていますね。危ないじゃないかと思っていますね。

これからの進展を見守って行きます。

 

4.最後に

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。

皆さんのご応募をお待ちしています。

  • Twitter
  • Facebook
  • はてなブックマークに追加

グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。

 
  • AI研究開発室
  • 大阪研究開発グループ

関連記事