SRC20 is heavily inspired by ERC20 token standard. Even though SRC20 is have a similar API as ERC20, there are some differences between them due to different programming model between Solana and Ethereum. Let's examine
Description | Solana | Ethereum |
Token name | No | name |
Token symbol | symbol | symbol |
Decimals | decimals | decimals |
Total supply | total_supply | totalSupply |
Balance of token | Yes. But in client side. | balanceOf |
Transfer token | Transfer | transfer |
Transfer token by delegation | TransferFrom | transferFrom |
Approve a delegation | Approve | approve |
Allowance of token | Yes. But in client side. | allowance |
Increase the approval โจ | IncreaseApproval | increaseApproval |
Decrease the approval โจ | DecreaseApproval | decreaseApproval |
โจ These functions are to prevent the front running attackโ
Recall that Solana program didn't own storage by itself. We need to use other accounts to store information. By giving access permission to the program, we can build a complete computing model.
This account is used to store token information such as decimals, total supply, symbol.
pub struct Token {pub symbol: [char;3]pub total_supply: u64,pub decimals: u8,pub initialized: bool,}
To initialize a new token, you need to call AppInstruction::TokenConstructor
with symbol
, total_supply
and decimals
params. In client side you must call the function with both payer account signature, token account signature, and receiver account signature which is very important.
// Build transaction layoutconst schema = [{ key: 'code', type: 'u8' },{ key: 'symbol', type: '[char;3]' },{ key: 'totalSupply', type: 'u64' },{ key: 'demicals', type: 'u8' },];const layout = new soproxABI.struct(schema, {code: 0,symbol: ['S', 'L', 'N'],totalSupply: 500000000000000n,decimals: 8});const instruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false },{ pubkey: token.publicKey, isSigner: true, isWritable: true },{ pubkey: receiver.publicKey, isSigner: true, isWritable: true },],programId,data: layout.toBuffer()});โ// Send transaction with payer, token, receiver signatureconst transaction = new Transaction();transaction.add(instruction);await sendAndConfirmTransaction(connection, transaction,[payer,new Account(Buffer.from(token.secretKey, 'hex')),new Account(Buffer.from(receiver.secretKey, 'hex'))],{skipPreflight: true,commitment: 'recent',});
After successfully deploy the token account, the public key of the token will be the identity will be used in all token communication.
To store information like holder, balance, and token public key, the "account" account will be created to do such things.
pub struct Account {pub owner: Pubkey,pub token: Pubkey,pub amount: u64,pub initialized: bool,}
At this point, you see that each "on-chain" account will be owned by a "off-chain" account specified in owner
field. Instead of using the pair of secret and public key of "on-chain" account to do a thing like transferring token, now manager one "off-chain" account is enough to show the permission of multiple accounts in multiple types of token.
Just the same as Token account, we need to initialize Account account by calling AppInstruction::AccountContructor
to the program.
To mimic the set of functions related to delegation namely approve
, transferFrom
in ERC20, we need Delegation account to realize these. Every time, a holder would like to delegate, or approve, an amount of token to another guy, the holder must create a new Delegation account.
pub struct Delegation {pub owner: Pubkey,pub token: Pubkey,pub source: Pubkey,pub delegate: Pubkey,pub amount: u64,pub initialized: bool,}
Here, owner should be the owner of source account; and also the payer who create this delegation. Obviously, token is the token public key identifying the type of token. The delegate
field is quite confused. You should assign to the value of "off-chain" account public key instead of "on-chain" one. The reason for that is for identical API and code style. We want to use "off-chain" accounts to manager all token communication, then delegate being a "off-chain" account public key is reasonable. Finally, amount
is the amount of token that the owner allows the delegate to legally spent.
However, different from Token
and Account
when we used to call Constructor
to initialize these account types, we want to maintain the identical API as ERC20. Therefore, we will use approve
function to represent the meaning of DelegationConstructor
.
The deployment transaction need signatures of owner and delegation account.
Recall that a transaction in Solana used to bring 3 main information
List of signatures
programId
Data
A transaction typically looks like
const instruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false },{ pubkey: account.publicKey, isSigner: true, isWritable: true },],programId,data: layout.toBuffer()});
TokenConstructor interface
AppInstruction::TokenConstructor {total_supply,decimals,} => {let accounts_iter = &mut accounts.iter();let deployer = next_account_info(accounts_iter)?;let token_acc = next_account_info(accounts_iter)?;let dst_acc = next_account_info(accounts_iter)?;...}
To call the function, caller must provide signatures of payer (or deployer), token account, and receiver. Additionally, total_supply
and decimals
is indispensable in data field.
Client call
const schema = [{ key: 'code', type: 'u8' },{ key: 'totalSupply', type: 'u64' },{ key: 'demicals', type: 'u8' },];const layout = new soproxABI.struct(schema, {code: 0,totalSupply,decimals,});const instruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false },{ pubkey: token.publicKey, isSigner: true, isWritable: true },{ pubkey: receiver.publicKey, isSigner: true, isWritable: true },],programId,data: layout.toBuffer()});
As ERC20, after the deployment, all token equal to the total_supply
will be transferred to the receiver.
Initialize an account to store token.
AccountConstructor interface
AppInstruction::AccountConstructor {} => {let accounts_iter = &mut accounts.iter();let caller = next_account_info(accounts_iter)?;let token_acc = next_account_info(accounts_iter)?;let target_acc = next_account_info(accounts_iter)?;...}
Client call
const schema = [{ key: 'code', type: 'u8' }];const layout = new soproxABI.struct(schema, {code: 1});const instruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false },{ pubkey: token.publicKey, isSigner: false, isWritable: false },{ pubkey: account.publicKey, isSigner: true, isWritable: true },],programId,data: layout.toBuffer()});
We still declare this function for an identical interface although we do nothing inside.
Transfer token by owner account from source to destination.
Transfer interface
AppInstruction::Transfer { amount } => {let accounts_iter = &mut accounts.iter();let owner = next_account_info(accounts_iter)?;let token_acc = next_account_info(accounts_iter)?;let src_acc = next_account_info(accounts_iter)?;let dst_acc = next_account_info(accounts_iter)?;...}
Client call
const schema = [{ key: 'code', type: 'u8' },{ key: 'amount', type: 'u64' }];const layout = new soproxABI.struct(schema, {code: 3,amount,});const instruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false },{ pubkey: token.publicKey, isSigner: false, isWritable: false },{ pubkey: source.publicKey, isSigner: false, isWritable: true },{ pubkey: destination.publicKey, isSigner: false, isWritable: true },],programId,data: layout.toBuffer()});
Create a Delegation account that means owner approve a number of token from source account to delegate account to use it in the future.
Approve interface
AppInstruction::Approve { amount } => {let accounts_iter = &mut accounts.iter();let owner = next_account_info(accounts_iter)?;let token_acc = next_account_info(accounts_iter)?;let delegation_acc = next_account_info(accounts_iter)?;let src_acc = next_account_info(accounts_iter)?;let dlg_acc = next_account_info(accounts_iter)?;...}
Client call
const schema = [{ key: 'code', type: 'u8' },{ key: 'amount', type: 'u64' }];const layout = new soproxABI.struct(schema, {code: 4,amount,});const instruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false },{ pubkey: token.publicKey, isSigner: false, isWritable: false },{ pubkey: delegation.publicKey, isSigner: true, isWritable: true },{ pubkey: source.publicKey, isSigner: false, isWritable: false },{ pubkey: delegate.publicKey, isSigner: false, isWritable: false },],programId,data: layout.toBuffer()});
Transfer token from source to destination by a delegation that owner approved delegate in the delegation account.
TransferFrom interface
AppInstruction::TransferFrom { amount } => {let accounts_iter = &mut accounts.iter();let delegate = next_account_info(accounts_iter)?;let token_acc = next_account_info(accounts_iter)?;let delegation_acc = next_account_info(accounts_iter)?;let src_acc = next_account_info(accounts_iter)?;let dst_acc = next_account_info(accounts_iter)?;...}
Client call
const schema = [{ key: 'code', type: 'u8' },{ key: 'amount', type: 'u64' }];const layout = new soproxABI.struct(schema, {code: 5,amount,});const instruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false },{ pubkey: token.publicKey, isSigner: false, isWritable: false },{ pubkey: delegation.publicKey, isSigner: false, isWritable: true },{ pubkey: source.publicKey, isSigner: false, isWritable: true },{ pubkey: destination.publicKey, isSigner: false, isWritable: true },],programId,data: layout.toBuffer()});
Increase the number of delegated token by an additional amount.
IncreaseApproval interface
AppInstruction::IncreaseApproval { amount } => {let accounts_iter = &mut accounts.iter();let owner = next_account_info(accounts_iter)?;let token_acc = next_account_info(accounts_iter)?;let delegation_acc = next_account_info(accounts_iter)?;...}
Client call
const schema = [{ key: 'code', type: 'u8' },{ key: 'amount', type: 'u64' }];const layout = new soproxABI.struct(schema, {code: 6,amount,});const instruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false },{ pubkey: token.publicKey, isSigner: false, isWritable: false },{ pubkey: delegation.publicKey, isSigner: false, isWritable: true },],programId,data: layout.toBuffer()});
Decrease the number of delegated token by an additional amount.
DecreaseApproval interface
AppInstruction::DecreaseApproval { amount } => {let accounts_iter = &mut accounts.iter();let owner = next_account_info(accounts_iter)?;let token_acc = next_account_info(accounts_iter)?;let delegation_acc = next_account_info(accounts_iter)?;...}
Client call
const schema = [{ key: 'code', type: 'u8' },{ key: 'amount', type: 'u64' }];const layout = new soproxABI.struct(schema, {code: 7,amount,});const instruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false },{ pubkey: token.publicKey, isSigner: false, isWritable: false },{ pubkey: delegation.publicKey, isSigner: false, isWritable: true },],programId,data: layout.toBuffer()});
Revoke a delegation account. All lamports in this account will return to the delegation owner.
Revoke interface
AppInstruction::Revoke {} => {let accounts_iter = &mut accounts.iter();let owner = next_account_info(accounts_iter)?;let token_acc = next_account_info(accounts_iter)?;let delegation_acc = next_account_info(accounts_iter)?;...}
Client call
const schema = [{ key: 'code', type: 'u8' },];const layout = new soproxABI.struct(schema, {code: 8,});const instruction = new TransactionInstruction({keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false },{ pubkey: token.publicKey, isSigner: false, isWritable: false },{ pubkey: delegation.publicKey, isSigner: false, isWritable: true },],programId,data: layout.toBuffer()});
SRC20 Wrapper (for SPL token standard)
Enhance security