🦔

Solana SPLトークンのスマートコントラクトについて解説

2022/08/06に公開

SolanaのSPLトークンのスマートコントラクトを勉強したので軽く中身を説明します。
こちらが公式のドキュメントです。
https://spl.solana.com/token

CLIからトークンを発行する

spl-token-cliをつかうことで、コマンドラインから簡単にトークンを発行することができます。

以下のコマンドで新規トークンを発行できます。
アドレス7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43がcreateされたトークンのMintアドレスです。

$  spl-token create-token
Creating token 7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43
Signature: 5G7G1VW57ZrcT4dBD9J9fvdLqZgqNF56K1b8iVVUAXN9XvNGJcMvCxLrzmS574pdyx9gtYPVQiATkTySF3anCiME

このtxの内容をSolscanで確認してみます。Create AccountとInitializeMintという2つのinstructionが実行されていることがわかります。
(https://solscan.io/tx/5G7G1VW57ZrcT4dBD9J9fvdLqZgqNF56K1b8iVVUAXN9XvNGJcMvCxLrzmS574pdyx9gtYPVQiATkTySF3anCiME?cluster=devnet)

実際にどういう処理が実行されているの確認してみます。solana-program-library/token/cli/src/main.rscommand_create_tokenで実際に実行されたinstructionを確認できます。create_accountでmintアカウントを新規作成し、その後にtoken programのinitialize_mintを実行していることがわかります。

    let mut instructions = vec![
        system_instruction::create_account(
            &config.fee_payer,
            &token,
            minimum_balance_for_rent_exemption,
            Mint::LEN as u64,
            &config.program_id,
        ),
        initialize_mint(
            &config.program_id,
            &token,
            &authority,
            freeze_authority_pubkey.as_ref(),
            decimals,
        )?,
    ];

solana-program-library/token/program/src/processor.rs
initialize_mintの処理内容が書いてあります。重要な部分は以下の部分です。入力したmintアカウントに対して、authoriryとdecimalsの設定を行っています。

        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())?;

つまり先程のtxの内容は以下のようにまとめられます。

  1. Mintアカウント7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43を作成
  2. Mintアカウント7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43に対しinitialize_mintして、MintAuthority変数を自分のウォレットのアドレス9wucoMiaozqBm6qnwtwZhYGfyDHa7PJwth4bYYaq9TBrで上書き

Associated token accountの作成

Mintアカウントを作成しましたが、実際にトークンのやり取りを行うためにはassociated tokenアカウントを作成する必要があります。associated tokenアカウントは個別のユーザーに紐付いたtokenアカウントで、このアカウントで各ユーザーのbalanceを管理します。

先程createしたトークン7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43に対する自分のassociated accountは以下のコマンドで作成できます。

$ spl-token create-account  7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43
Creating account 2T8WtgAgVnNJhwccQbjxrfPSjgbR81mGNTQw2H1u4WQo
Signature: 49SSBDvHYwUV1ejtH6oBd6KC9adnHg5skjw355uyFR1mUEp2QrzBKLgTrWLvEuDwAPeLn7t1p8U5xoqqDUQwpins

create-accountの処理内容はsolana-program-library/associated-token-account/program/src/processor.rsprocess_create_associated_token_accountに書いてあります。
処理の内容をざっくり説明するとmintアカウントとユーザーのアドレスをseedにしてPDAアカウントを新規作成しています。PDAは以下のように作られています。

fn get_associated_token_address_and_bump_seed_internal(
    wallet_address: &Pubkey,
    token_mint_address: &Pubkey,
    program_id: &Pubkey,
    token_program_id: &Pubkey,
) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &[
            &wallet_address.to_bytes(),
            &token_program_id.to_bytes(),
            &token_mint_address.to_bytes(),
        ],
        program_id,
    )
}

PDAとはProgram derived addressesの略で、指定したprogram_idのprogramだけが状態を変更できるアカウントです。PDAはseedによって任意のidをマッピングできるため、ユーザー情報の管理によく使われます。今回はwallet_address, token_program_id, token_mint_addressをseedにすることで、個別のユーザーのトークンアドレスを作成しています。以下にPDAを使ったaccountマッピングの詳しい説明があります。
https://solanacookbook.com/guides/account-maps.html#deriving-pdas

Solscanで確認すると以下のようになっています
(https://solscan.io/tx/49SSBDvHYwUV1ejtH6oBd6KC9adnHg5skjw355uyFR1mUEp2QrzBKLgTrWLvEuDwAPeLn7t1p8U5xoqqDUQwpins?cluster=devnet)

処理の内容は以下のようになっています。

  1. PDAアカウント2T8WtgAgVnNJhwccQbjxrfPSjgbR81mGNTQw2H1u4WQoを作成
  2. PDAアカウントのOwner変数を自分のアドレス(9wucoMiaozqBm6qnwtwZhYGfyDHa7PJwth4bYYaq9TBr)で書き換え

Owner変数を書き換えることで、自分のトークンアカウントを作成することができました。

Associated tokenアカウントにmintする

以下のコマンドで、自分のトークンアカウントに100トークンをミントできます。

$ spl-token mint  7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43 100
Minting 100 tokens
  Token: 7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43
  Recipient: 2T8WtgAgVnNJhwccQbjxrfPSjgbR81mGNTQw2H1u4WQo
Signature: 2S3x3SUs2qMcdNQ2EgBSoUjNmDag8HiKQ7kgrjPwzFdYMXiWiZCNNNz1k2s8zc6JGxY3rENxEUgBxsY6EvRi2uQE

処理内容はhttps://github.com/solana-labs/solana-program-library/blob/master/token/program/src/processor.rsprocess_mint_toに書いてあります。ざっくり説明するとmintアカウントのmint_authoriryがmintの実行者と一致しているか確認して、OKだったらdestinationにamountがaddされるようになっています。今回の場合、mint_authoriryはmint_initializeで設定した自分のウォレットアドレス(9wucoMiaozqBm6qnwtwZhYGfyDHa7PJwth4bYYaq9TBr)で、destinationは先程作成したPDA(2T8WtgAgVnNJhwccQbjxrfPSjgbR81mGNTQw2H1u4WQo)になります。これにより自分のトークンアカウントに100トークンミントすることができました。

        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()),
        }

// ...

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

Transfer実行

transferするためもう一つ別のassociated accountを作ります。solana config set --keypair <keypair-path>でデフォルトユーザーを変更して、もう一度create-accountします。
solana config set --keypair <original-path>でまたもとのユーザーに切り替え、以下のコマンドを実行すると50トークン送ることができます(8yqpWiVumiAhcD2HH4SMZ5u5bFPk2MgFDwjqbutRUGbFは2回めに作成した別ユーザーのトークンアカウントです)。

$ spl-token transfer  7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43 50 8yqpWiVumiAhcD2HH4SMZ5u5bFPk2MgFDwjqbutRUGbF
Transfer 50 tokens
  Sender: 2T8WtgAgVnNJhwccQbjxrfPSjgbR81mGNTQw2H1u4WQo
  Recipient: 8yqpWiVumiAhcD2HH4SMZ5u5bFPk2MgFDwjqbutRUGbF
Signature: 2swknNyB2AHWwArc5Uh4WxqUy6AfCSXh95p7q57B1vXXoAgEgwCzajuGkHGHs6HPBbyrC27BkLjb5f9oaT2ruvyE

balanceを確認すると100 - 50 = 50になっています。

$ spl-token balance  7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43
50

process_transferの処理をざっくり説明すると

  1. source_account(送る人のトークンアカウント)が送りたいamountを持っているか確認
  2. source_accountのowner(associated accountのowner)がtransferしようとしている人と一致しているか確認
  3. 上記のvalidationがOKだったらsourceの残高をマイナスamountして、destinationを+=amountする
//...
        if source_account.amount < amount {
            return Err(TokenError::InsufficientFunds.into());
        }
//...
            _ => Self::validate_owner(
                program_id,
                &source_account.owner,
                authority_info,
                account_info_iter.as_slice(),
            )?,
//...
        source_account.amount = source_account
            .amount
            .checked_sub(amount)
            .ok_or(TokenError::Overflow)?;
        destination_account.amount = destination_account
            .amount
            .checked_add(amount)
            .ok_or(TokenError::Overflow)?;

まとめ

SPLトークンCLIを実行することで処理内容を確認できた。SPLトークンでは、Mintアカウント、Associated tokenアカウントでステートを管理している。

Discussion