이자 발생 민트란 무엇인가요?
Token Extensions Program의 InterestBearingConfig 민트 확장 기능을 사용하면
민트가 연간 이자율을 온체인에 직접 저장할 수 있습니다.
이자 발생 토큰은 시간이 지남에 따라 토큰 계정에 더 많은 토큰을 추가하지 않습니다. 각 토큰 계정에 저장된 토큰 수량은 민팅, 전송 또는 소각과 같은 토큰 프로그램 명령어가 변경할 때까지 동일하게 유지됩니다.
시간이 지남에 따라 토큰 수량과 토큰 공급량은 동일하게 유지되더라도 이자가 계산된 UI 수량은 증가할 수 있습니다.
과거 이자율 변경은 어떻게 계산되나요?
InterestBearingConfig는initialization_timestamp,pre_update_average_rate,last_update_timestamp,current_rate를 저장합니다.amount_to_ui_amount는 두 단계로 UI 수량을 계산합니다. 먼저pre_update_average_rate를 사용하여 초기화부터last_update_timestamp까지의 이자를 계산한 다음,current_rate를 사용하여last_update_timestamp부터 현재 타임스탬프까지의 이자를 계산합니다.UpdateRate가 실행되면,process_update_rate가time_weighted_average_rate를 호출하고pre_update_average_rate를 새로운 시간 가중 과거 평균으로 업데이트한 다음, 새로운last_update_timestamp와current_rate를 기록합니다.- 시간이 지남에 따라 이자율이 변경되면 초기 변경 사항은
pre_update_average_rate로 표현되며,last_update_timestamp부터 현재 시점까지의 기간은current_rate로 계산됩니다.
이자 발생 토큰 생성 및 사용 방법
이자 발생 토큰을 생성하고 사용하려면:
- 민트와
InterestBearingConfig확장에 필요한 민트 계정 크기와 rent를 계산합니다. - *rs
CreateAccount*로 민트 계정을 생성하고, *rsInterestBearingConfig*를 초기화한 다음, *rsInitializeMint*로 민트를 초기화합니다. - 일반적인 방법으로 토큰을 민팅합니다.
- 시간이 지남에 따라 민트의 현재 이자율을 변경하려면 *rs
UpdateRate*를 사용합니다. AmountToUiAmount또는 민트 계정과 clock sysvar를 가져온 다음 트랜잭션을 전송하지 않고 UI 수량을 계산하는 헬퍼 메서드를 사용하여 토큰 수량을 이자가 포함된 UI 수량으로 변환합니다.
오프체인 및 온체인 UI 금액 변환
오프체인 헬퍼 경로는 mint account와 클록 시스템 변수를 가져온 다음 트랜잭션을
전송하지 않고 로컬에서 UI 금액을 계산합니다. 온체인 경로는 Token Program에서
실행되고 트랜잭션 반환 데이터로 UI 금액을 반환하는 _rsAmountToUiAmount_를
사용합니다. _rsAmountToUiAmount_는 트랜잭션으로 전송하거나 시뮬레이션할 수
있습니다.
계정 크기 계산
기본 민트에 대한 mint account 크기와 InterestBearingConfig 확장을
계산합니다. 이는 *rsCreateAccount*에서 사용되는 크기입니다.
rent 계산
민트에 필요한 크기와 InterestBearingConfig 확장을 사용하여 rent를
계산합니다.
mint account 생성
계산된 공간과 lamport로 mint account를 생성합니다.
InterestBearingConfig 초기화
민트에서 InterestBearingConfig 확장을 초기화합니다.
민트 초기화
동일한 트랜잭션에서 *rsInitializeMint*로 민트를 초기화합니다.
token account 생성 및 토큰 발행
민트에 대한 token account를 생성한 다음 해당 token account에 토큰을 발행합니다.
오프체인 헬퍼로 UI 금액 계산하기
mint account와 clock sysvar를 가져온 다음, 트랜잭션을 전송하지 않고 UI 금액을 계산합니다.
명령어 순서
_rsInterestBearingMintInstruction::Initialize_는 _rsInitializeMint_보다
먼저 실행되어야 합니다. CreateAccount,
InterestBearingMintInstruction::Initialize, 그리고
_rsInitializeMint_는 동일한 트랜잭션에 포함되어야 합니다.
소스 참조
| 항목 | 설명 | 소스 |
|---|---|---|
InterestBearingConfig | 이자율 권한, 타임스탬프, 현재 및 과거 이자율을 저장하는 민트 확장입니다. | 소스 |
InterestBearingMintInstruction::Initialize | InitializeMint 전에 이자 부과 설정을 초기화하는 명령어입니다. | 소스 |
InterestBearingMintInstruction::UpdateRate | 민트의 현재 이자율을 변경하는 명령어입니다. | 소스 |
AmountToUiAmount | 민트의 활성 설정을 사용하여 토큰 금액에 대한 현재 UI 금액 문자열을 반환하는 명령어입니다. | 소스 |
InterestBearingConfig::amount_to_ui_amount | 타임스탬프에 대한 이자를 포함한 토큰 금액을 UI 금액으로 변환하는 헬퍼입니다. | 소스 |
process_initialize | *rsInterestBearingConfig*를 초기화하고 초기 타임스탬프와 이자율을 기록하는 프로세서 로직입니다. | 소스 |
process_update_rate | 현재 이자율을 업데이트하고 시간 가중 평균 이자율을 재계산하며 업데이트 타임스탬프를 기록하는 프로세서 로직입니다. | 소스 |
process_amount_to_ui_amount | 민트의 활성 이자 부과 설정을 사용하여 UI 금액 문자열을 반환하는 프로세서 로직입니다. | 소스 |
타입스크립트
아래 Kit 예제는 생성된 명령어를 직접 사용합니다. @solana/web3.js 및
@solana/spl-token를 사용하는 레거시 예제는 참조용으로 포함되어 있습니다.
Kit
import {lamports,createClient,appendTransactionMessageInstructions,createTransactionMessage,generateKeyPairSigner,getBase64EncodedWireTransaction,pipe,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,signTransactionMessageWithSigners,unwrapOption} from "@solana/kit";import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";import { getCreateAccountInstruction } from "@solana-program/system";import {amountToUiAmountForMintWithoutSimulation,extension,fetchMint,findAssociatedTokenPda,getAmountToUiAmountInstruction,getCreateAssociatedTokenInstructionAsync,getInitializeInterestBearingMintInstruction,getInitializeMintInstruction,getMintSize,getMintToCheckedInstruction,getUpdateRateInterestBearingMintInstruction,isExtension,TOKEN_2022_PROGRAM_ADDRESS} from "@solana-program/token-2022";const client = await createClient().use(generatedPayer()).use(solanaRpc({rpcUrl: "http://localhost:8899",rpcSubscriptionsUrl: "ws://localhost:8900"})).use(rpcAirdrop()).use(airdropPayer(lamports(1_000_000_000n)));const mint = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const tokenAmount = 1_000_000_000_000n;const interestBearingExtension = extension("InterestBearingConfig", {rateAuthority: client.payer.address,initializationTimestamp: BigInt(Math.floor(Date.now() / 1000)),lastUpdateTimestamp: BigInt(Math.floor(Date.now() / 1000)),preUpdateAverageRate: 30000,currentRate: 30000});const mintSpace = BigInt(getMintSize([interestBearingExtension]));const mintRent = await client.rpc.getMinimumBalanceForRentExemption(mintSpace).send();const [tokenAccount] = await findAssociatedTokenPda({mint: mint.address,owner: recipient.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});await client.sendTransaction([getCreateAccountInstruction({payer: client.payer, // Account funding the new mint account.newAccount: mint, // New mint account to create.lamports: mintRent, // Lamports funding the mint account rent.space: mintSpace, // Account size in bytes for the mint plus InterestBearingConfig.programAddress: TOKEN_2022_PROGRAM_ADDRESS // Program that owns the mint account.}),getInitializeInterestBearingMintInstruction({mint: mint.address, // Mint account that stores the InterestBearingConfig extension.rateAuthority: client.payer.address, // Authority allowed to update the interest rate later.rate: 30000 // Interest rate in basis points.}),getInitializeMintInstruction({mint: mint.address, // Mint account to initialize.decimals: 0, // Number of decimals for the token.mintAuthority: client.payer.address, // Authority allowed to mint new tokens.freezeAuthority: client.payer.address // Authority allowed to freeze token accounts.})]);await client.sendTransaction([await getCreateAssociatedTokenInstructionAsync({payer: client.payer, // Account funding the associated token account creation.mint: mint.address, // Mint for the associated token account.owner: recipient.address // Owner of the token account.}),getMintToCheckedInstruction({mint: mint.address, // Mint account that issues the tokens.token: tokenAccount, // Token account receiving the newly minted tokens.mintAuthority: client.payer, // Signer authorized to mint new tokens.amount: tokenAmount, // Token amount in base units.decimals: 0 // Decimals defined on the mint.})]);await new Promise((resolve) => setTimeout(resolve, 2_000));const calculatedUiAmount = await amountToUiAmountForMintWithoutSimulation(client.rpc,mint.address,tokenAmount);const amountToUiInstruction = getAmountToUiAmountInstruction({mint: mint.address, // Mint whose UI amount conversion is being simulated.amount: tokenAmount // Token amount in base units.});const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();const amountToUiMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(client.payer, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),(tx) => appendTransactionMessageInstructions([amountToUiInstruction], tx));const signedAmountToUiMessage =await signTransactionMessageWithSigners(amountToUiMessage);const simulation = await client.rpc.simulateTransaction(getBase64EncodedWireTransaction(signedAmountToUiMessage),{encoding: "base64"}).send();const simulatedUiAmount = Buffer.from(simulation.value.returnData?.data?.[0] ?? "","base64").toString("utf8");const updateRateInstruction = getUpdateRateInterestBearingMintInstruction({mint: mint.address, // Mint account that stores the InterestBearingConfig extension.rateAuthority: client.payer, // Signer authorized to update the interest rate.rate: 15000 // New interest rate in basis points.});await client.sendTransaction([updateRateInstruction]);const mintAccount = await fetchMint(client.rpc, mint.address);const interestBearingConfig = (unwrapOption(mintAccount.data.extensions) ?? []).find((item) => isExtension("InterestBearingConfig", item));console.log("Mint Address:", mint.address);console.log("Token Account:", tokenAccount);console.log("Calculated UI Amount:", calculatedUiAmount);console.log("Simulated UI Amount:", simulatedUiAmount);console.log("InterestBearingConfig:", interestBearingConfig);
Web3.js
import {Connection,Keypair,sendAndConfirmTransaction,SystemProgram,Transaction,LAMPORTS_PER_SOL} from "@solana/web3.js";import {ASSOCIATED_TOKEN_PROGRAM_ID,amountToUiAmountForMintWithoutSimulation,createAmountToUiAmountInstruction,createAssociatedTokenAccountInstruction,createInitializeMintInstruction,createInitializeInterestBearingMintInstruction,createMintToCheckedInstruction,createUpdateRateInterestBearingMintInstruction,ExtensionType,getAssociatedTokenAddressSync,getInterestBearingMintConfigState,getMint,getMintLen,TOKEN_2022_PROGRAM_ID} from "@solana/spl-token";const connection = new Connection("http://localhost:8899", "confirmed");const latestBlockhash = await connection.getLatestBlockhash();const feePayer = Keypair.generate();const recipient = Keypair.generate();const tokenAmount = 1_000_000_000_000n;const airdropSignature = await connection.requestAirdrop(feePayer.publicKey,5 * LAMPORTS_PER_SOL);await connection.confirmTransaction({blockhash: latestBlockhash.blockhash,lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,signature: airdropSignature});const extensions = [ExtensionType.InterestBearingConfig];const mint = Keypair.generate();const mintLength = getMintLen(extensions);const mintRent = await connection.getMinimumBalanceForRentExemption(mintLength);const tokenAccount = getAssociatedTokenAddressSync(mint.publicKey,recipient.publicKey,false,TOKEN_2022_PROGRAM_ID,ASSOCIATED_TOKEN_PROGRAM_ID);const createMintAccountInstruction = SystemProgram.createAccount({fromPubkey: feePayer.publicKey, // Account funding the new mint account.newAccountPubkey: mint.publicKey, // New mint account to create.space: mintLength, // Account size in bytes for the mint plus InterestBearingConfig.lamports: mintRent, // Lamports funding the mint account rent.programId: TOKEN_2022_PROGRAM_ID // Program that owns the mint account.});const initializeInterestBearingInstruction =createInitializeInterestBearingMintInstruction(mint.publicKey, // Mint account that stores the InterestBearingConfig extension.feePayer.publicKey, // Authority allowed to update the interest rate later.30000, // Interest rate in basis points.TOKEN_2022_PROGRAM_ID // Token program that owns the mint.);const initializeMintInstruction = createInitializeMintInstruction(mint.publicKey, // Mint account to initialize.0, // Number of decimals for the token.feePayer.publicKey, // Authority allowed to mint new tokens.feePayer.publicKey, // Authority allowed to freeze token accounts.TOKEN_2022_PROGRAM_ID // Program that owns the mint account.);const createTokenAccountInstruction = createAssociatedTokenAccountInstruction(feePayer.publicKey, // Account funding the associated token account creation.tokenAccount, // Associated token account address to create.recipient.publicKey, // Owner of the token account.mint.publicKey, // Mint for the associated token account.TOKEN_2022_PROGRAM_ID, // Token program that owns the token account.ASSOCIATED_TOKEN_PROGRAM_ID // Associated Token Program that creates the account.);const mintToTokenAccountInstruction = createMintToCheckedInstruction(mint.publicKey, // Mint account that issues the tokens.tokenAccount, // Token account receiving the newly minted tokens.feePayer.publicKey, // Signer authorized to mint new tokens.tokenAmount, // Token amount in base units.0, // Decimals defined on the mint.[], // Additional multisig signers.TOKEN_2022_PROGRAM_ID // Token program that owns the mint and token account.);await sendAndConfirmTransaction(connection,new Transaction({feePayer: feePayer.publicKey,blockhash: latestBlockhash.blockhash,lastValidBlockHeight: latestBlockhash.lastValidBlockHeight}).add(createMintAccountInstruction,initializeInterestBearingInstruction,initializeMintInstruction),[feePayer, mint]);await sendAndConfirmTransaction(connection,new Transaction().add(createTokenAccountInstruction,mintToTokenAccountInstruction),[feePayer]);await new Promise((resolve) => setTimeout(resolve, 2_000));const calculatedUiAmount = await amountToUiAmountForMintWithoutSimulation(connection,mint.publicKey,tokenAmount);const amountToUiInstruction = createAmountToUiAmountInstruction(mint.publicKey, // Mint whose UI amount conversion is being simulated.tokenAmount, // Token amount in base units.TOKEN_2022_PROGRAM_ID // Token program that owns the mint.);const amountToUiSimulation = await connection.simulateTransaction(new Transaction().add(amountToUiInstruction),[feePayer],false);const simulatedUiAmount = Buffer.from(amountToUiSimulation.value.returnData?.data?.[0] ?? "","base64").toString("utf8");const updateRateInstruction = createUpdateRateInterestBearingMintInstruction(mint.publicKey, // Mint account that stores the InterestBearingConfig extension.feePayer.publicKey, // Signer authorized to update the interest rate.15000, // New interest rate in basis points.[], // Additional multisig signers.TOKEN_2022_PROGRAM_ID // Token program that owns the mint.);await sendAndConfirmTransaction(connection,new Transaction().add(updateRateInstruction),[feePayer]);const mintAccount = await getMint(connection,mint.publicKey,"confirmed",TOKEN_2022_PROGRAM_ID);const interestBearingConfig = getInterestBearingMintConfigState(mintAccount);console.log("Mint Address:", mint.publicKey.toBase58());console.log("Token Account:", tokenAccount.toBase58());console.log("Calculated UI Amount:", calculatedUiAmount);console.log("Simulated UI Amount:", simulatedUiAmount);console.log("InterestBearingConfig:", interestBearingConfig);
Rust
use anyhow::{anyhow, Result};use base64::prelude::{Engine as _, BASE64_STANDARD};use solana_client::nonblocking::rpc_client::RpcClient;use solana_commitment_config::CommitmentConfig;use solana_sdk::{sysvar::clock::{self, Clock},signature::{Keypair, Signer},transaction::Transaction,};use solana_system_interface::instruction::create_account;use spl_associated_token_account_interface::{address::get_associated_token_address_with_program_id,instruction::create_associated_token_account,};use spl_token_2022_interface::{extension::{interest_bearing_mint::{instruction::{initialize as initialize_interest_bearing_instruction, update_rate,},InterestBearingConfig,},BaseStateWithExtensions, ExtensionType, StateWithExtensions,},instruction::{amount_to_ui_amount, initialize_mint, mint_to_checked},state::Mint,ID as TOKEN_2022_PROGRAM_ID,};#[tokio::main]async fn main() -> Result<()> {let client = RpcClient::new_with_commitment(String::from("http://localhost:8899"),CommitmentConfig::confirmed(),);let fee_payer = Keypair::new();let recipient = Keypair::new();let token_amount = 1_000_000_000_000u64;let airdrop_signature = client.request_airdrop(&fee_payer.pubkey(), 5_000_000_000).await?;loop {let confirmed = client.confirm_transaction(&airdrop_signature).await?;if confirmed {break;}}let mint = Keypair::new();let mint_space =ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::InterestBearingConfig])?;let mint_rent = client.get_minimum_balance_for_rent_exemption(mint_space).await?;let create_mint_account_instruction = create_account(&fee_payer.pubkey(), // Account funding the new mint account.&mint.pubkey(), // New mint account to create.mint_rent, // Lamports funding the mint account rent.mint_space as u64, // Account size in bytes for the mint plus InterestBearingConfig.&TOKEN_2022_PROGRAM_ID, // Program that owns the mint account.);let initialize_interest_bearing_instruction = initialize_interest_bearing_instruction(&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint.&mint.pubkey(), // Mint account that stores the InterestBearingConfig extension.Some(fee_payer.pubkey()), // Authority allowed to update the interest rate later.30000, // Interest rate in basis points.)?;let initialize_mint_instruction = initialize_mint(&TOKEN_2022_PROGRAM_ID, // Program that owns the mint account.&mint.pubkey(), // Mint account to initialize.&fee_payer.pubkey(), // Authority allowed to mint new tokens.Some(&fee_payer.pubkey()), // Authority allowed to freeze token accounts.0, // Number of decimals for the token.)?;let create_mint_transaction = Transaction::new_signed_with_payer(&[create_mint_account_instruction,initialize_interest_bearing_instruction,initialize_mint_instruction,],Some(&fee_payer.pubkey()),&[&fee_payer, &mint],client.get_latest_blockhash().await?,);client.send_and_confirm_transaction(&create_mint_transaction).await?;let token_account = get_associated_token_address_with_program_id(&recipient.pubkey(),&mint.pubkey(),&TOKEN_2022_PROGRAM_ID,);let create_token_account_instruction = create_associated_token_account(&fee_payer.pubkey(), // Account funding the associated token account creation.&recipient.pubkey(), // Owner of the token account.&mint.pubkey(), // Mint for the associated token account.&TOKEN_2022_PROGRAM_ID, // Token program that owns the token account.);let mint_to_token_account_instruction = mint_to_checked(&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint and token account.&mint.pubkey(), // Mint account that issues the tokens.&token_account, // Token account receiving the newly minted tokens.&fee_payer.pubkey(), // Signer authorized to mint new tokens.&[], // Additional multisig signers.token_amount, // Token amount in base units.0, // Decimals defined on the mint.)?;let create_token_account_transaction = Transaction::new_signed_with_payer(&[create_token_account_instruction,mint_to_token_account_instruction,],Some(&fee_payer.pubkey()),&[&fee_payer],client.get_latest_blockhash().await?,);client.send_and_confirm_transaction(&create_token_account_transaction).await?;tokio::time::sleep(std::time::Duration::from_secs(2)).await;let mint_account = client.get_account(&mint.pubkey()).await?;let mint_state = StateWithExtensions::<Mint>::unpack(&mint_account.data)?;let interest_bearing_config = mint_state.get_extension::<InterestBearingConfig>()?;let clock_account = client.get_account(&clock::ID).await?;let clock: Clock = clock_account.deserialize_data()?;let calculated_ui_amount = interest_bearing_config.amount_to_ui_amount(token_amount, mint_state.base.decimals, clock.unix_timestamp).ok_or_else(|| anyhow!("Failed to calculate UI amount"))?;let amount_to_ui_instruction = amount_to_ui_amount(&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint.&mint.pubkey(), // Mint whose UI amount conversion is being simulated.token_amount, // Token amount in base units.)?;let amount_to_ui_blockhash = client.get_latest_blockhash().await?;let amount_to_ui_transaction = Transaction::new_signed_with_payer(&[amount_to_ui_instruction],Some(&fee_payer.pubkey()),&[&fee_payer],amount_to_ui_blockhash,);let amount_to_ui_result = client.simulate_transaction(&amount_to_ui_transaction).await?;let simulated_ui_amount = String::from_utf8(BASE64_STANDARD.decode(amount_to_ui_result.value.return_data.ok_or_else(|| anyhow!("Expected AmountToUiAmount return data"))?.data.0,)?,)?;let update_rate_instruction = update_rate(&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint.&mint.pubkey(), // Mint account that stores the InterestBearingConfig extension.&fee_payer.pubkey(), // Signer authorized to update the interest rate.&[], // Additional multisig signers.15000, // New interest rate in basis points.)?;let update_rate_transaction = Transaction::new_signed_with_payer(&[update_rate_instruction],Some(&fee_payer.pubkey()),&[&fee_payer],client.get_latest_blockhash().await?,);client.send_and_confirm_transaction(&update_rate_transaction).await?;let updated_mint_account = client.get_account(&mint.pubkey()).await?;let updated_mint_state = StateWithExtensions::<Mint>::unpack(&updated_mint_account.data)?;let interest_bearing_config = updated_mint_state.get_extension::<InterestBearingConfig>()?;println!("Mint Address: {}", mint.pubkey());println!("Token Account: {}", token_account);println!("Calculated UI Amount: {}", calculated_ui_amount);println!("Simulated UI Amount: {}", simulated_ui_amount);println!("InterestBearingConfig: {:#?}", interest_bearing_config);Ok(())}
Is this page helpful?