Solana SPLトークンのスマートコントラクトについて解説
SolanaのSPLトークンのスマートコントラクトを勉強したので軽く中身を説明します。
こちらが公式のドキュメントです。
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.rsのcommand_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の内容は以下のようにまとめられます。
- Mintアカウント
7LztX8GNgVpF7cuaFj9N7Eb8T1Mw6vU7WH3vXJ1s3w43
を作成 - 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.rsのprocess_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マッピングの詳しい説明があります。
Solscanで確認すると以下のようになっています
(https://solscan.io/tx/49SSBDvHYwUV1ejtH6oBd6KC9adnHg5skjw355uyFR1mUEp2QrzBKLgTrWLvEuDwAPeLn7t1p8U5xoqqDUQwpins?cluster=devnet)
処理の内容は以下のようになっています。
- PDAアカウント
2T8WtgAgVnNJhwccQbjxrfPSjgbR81mGNTQw2H1u4WQo
を作成 - 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.rsのprocess_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
の処理をざっくり説明すると
- source_account(送る人のトークンアカウント)が送りたいamountを持っているか確認
- source_accountのowner(associated accountのowner)がtransferしようとしている人と一致しているか確認
- 上記の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