SoproX
Search…
SRC20
A ERC20-like token standard
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​

Core accounts

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.

Token

This account is used to store token information such as decimals, total supply, symbol.
1
pub struct Token {
2
pub symbol: [char;3]
3
pub total_supply: u64,
4
pub decimals: u8,
5
pub initialized: bool,
6
}
Copied!
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.
1
// Build transaction layout
2
const schema = [
3
{ key: 'code', type: 'u8' },
4
{ key: 'symbol', type: '[char;3]' },
5
{ key: 'totalSupply', type: 'u64' },
6
{ key: 'demicals', type: 'u8' },
7
];
8
const layout = new soproxABI.struct(schema, {
9
code: 0,
10
symbol: ['S', 'L', 'N'],
11
totalSupply: 500000000000000n,
12
decimals: 8});
13
const instruction = new TransactionInstruction({
14
keys: [
15
{ pubkey: payer.publicKey, isSigner: true, isWritable: false },
16
{ pubkey: token.publicKey, isSigner: true, isWritable: true },
17
{ pubkey: receiver.publicKey, isSigner: true, isWritable: true },
18
],
19
programId,
20
data: layout.toBuffer()
21
});
22
​
23
// Send transaction with payer, token, receiver signature
24
const transaction = new Transaction();
25
transaction.add(instruction);
26
await sendAndConfirmTransaction(
27
connection, transaction,
28
[
29
payer,
30
new Account(Buffer.from(token.secretKey, 'hex')),
31
new Account(Buffer.from(receiver.secretKey, 'hex'))
32
],
33
{
34
skipPreflight: true,
35
commitment: 'recent',
36
});
Copied!
After successfully deploy the token account, the public key of the token will be the identity will be used in all token communication.

Account

To store information like holder, balance, and token public key, the "account" account will be created to do such things.
1
pub struct Account {
2
pub owner: Pubkey,
3
pub token: Pubkey,
4
pub amount: u64,
5
pub initialized: bool,
6
}
Copied!
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.

Delegation

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.
1
pub struct Delegation {
2
pub owner: Pubkey,
3
pub token: Pubkey,
4
pub source: Pubkey,
5
pub delegate: Pubkey,
6
pub amount: u64,
7
pub initialized: bool,
8
}
Copied!
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.

Token functions

Recall that a transaction in Solana used to bring 3 main information
    1.
    List of signatures
    2.
    programId
    3.
    Data
A transaction typically looks like
1
const instruction = new TransactionInstruction({
2
keys: [
3
{ pubkey: payer.publicKey, isSigner: true, isWritable: false },
4
{ pubkey: account.publicKey, isSigner: true, isWritable: true },
5
],
6
programId,
7
data: layout.toBuffer()
8
});
Copied!

TokenConstructor - Code 0

TokenConstructor interface
1
AppInstruction::TokenConstructor {
2
total_supply,
3
decimals,
4
} => {
5
let accounts_iter = &mut accounts.iter();
6
let deployer = next_account_info(accounts_iter)?;
7
let token_acc = next_account_info(accounts_iter)?;
8
let dst_acc = next_account_info(accounts_iter)?;
9
...
10
}
Copied!
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
1
const schema = [
2
{ key: 'code', type: 'u8' },
3
{ key: 'totalSupply', type: 'u64' },
4
{ key: 'demicals', type: 'u8' },
5
];
6
const layout = new soproxABI.struct(schema, {
7
code: 0,
8
totalSupply,
9
decimals,
10
});
11
const instruction = new TransactionInstruction({
12
keys: [
13
{ pubkey: payer.publicKey, isSigner: true, isWritable: false },
14
{ pubkey: token.publicKey, isSigner: true, isWritable: true },
15
{ pubkey: receiver.publicKey, isSigner: true, isWritable: true },
16
],
17
programId,
18
data: layout.toBuffer()
19
});
Copied!
As ERC20, after the deployment, all token equal to the total_supply will be transferred to the receiver.

AccountConstructor - Code 1

Initialize an account to store token.
AccountConstructor interface
1
AppInstruction::AccountConstructor {} => {
2
let accounts_iter = &mut accounts.iter();
3
let caller = next_account_info(accounts_iter)?;
4
let token_acc = next_account_info(accounts_iter)?;
5
let target_acc = next_account_info(accounts_iter)?;
6
...
7
}
Copied!
Client call
1
const schema = [
2
{ key: 'code', type: 'u8' }
3
];
4
const layout = new soproxABI.struct(schema, {
5
code: 1
6
});
7
const instruction = new TransactionInstruction({
8
keys: [
9
{ pubkey: payer.publicKey, isSigner: true, isWritable: false },
10
{ pubkey: token.publicKey, isSigner: false, isWritable: false },
11
{ pubkey: account.publicKey, isSigner: true, isWritable: true },
12
],
13
programId,
14
data: layout.toBuffer()
15
});
Copied!

DelegationConstructor - Code 2

We still declare this function for an identical interface although we do nothing inside.

Transfer - Code 3

Transfer token by owner account from source to destination.
Transfer interface
1
AppInstruction::Transfer { amount } => {
2
let accounts_iter = &mut accounts.iter();
3
let owner = next_account_info(accounts_iter)?;
4
let token_acc = next_account_info(accounts_iter)?;
5
let src_acc = next_account_info(accounts_iter)?;
6
let dst_acc = next_account_info(accounts_iter)?;
7
...
8
}
Copied!
Client call
1
const schema = [
2
{ key: 'code', type: 'u8' },
3
{ key: 'amount', type: 'u64' }
4
];
5
const layout = new soproxABI.struct(schema, {
6
code: 3,
7
amount,
8
});
9
const instruction = new TransactionInstruction({
10
keys: [
11
{ pubkey: payer.publicKey, isSigner: true, isWritable: false },
12
{ pubkey: token.publicKey, isSigner: false, isWritable: false },
13
{ pubkey: source.publicKey, isSigner: false, isWritable: true },
14
{ pubkey: destination.publicKey, isSigner: false, isWritable: true },
15
],
16
programId,
17
data: layout.toBuffer()
18
});
Copied!

Approve - Code 4

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
1
AppInstruction::Approve { amount } => {
2
let accounts_iter = &mut accounts.iter();
3
let owner = next_account_info(accounts_iter)?;
4
let token_acc = next_account_info(accounts_iter)?;
5
let delegation_acc = next_account_info(accounts_iter)?;
6
let src_acc = next_account_info(accounts_iter)?;
7
let dlg_acc = next_account_info(accounts_iter)?;
8
...
9
}
Copied!
Client call
1
const schema = [
2
{ key: 'code', type: 'u8' },
3
{ key: 'amount', type: 'u64' }
4
];
5
const layout = new soproxABI.struct(schema, {
6
code: 4,
7
amount,
8
});
9
const instruction = new TransactionInstruction({
10
keys: [
11
{ pubkey: payer.publicKey, isSigner: true, isWritable: false },
12
{ pubkey: token.publicKey, isSigner: false, isWritable: false },
13
{ pubkey: delegation.publicKey, isSigner: true, isWritable: true },
14
{ pubkey: source.publicKey, isSigner: false, isWritable: false },
15
{ pubkey: delegate.publicKey, isSigner: false, isWritable: false },
16
],
17
programId,
18
data: layout.toBuffer()
19
});
Copied!

TransferFrom - Code 5

Transfer token from source to destination by a delegation that owner approved delegate in the delegation account.
TransferFrom interface
1
AppInstruction::TransferFrom { amount } => {
2
let accounts_iter = &mut accounts.iter();
3
let delegate = next_account_info(accounts_iter)?;
4
let token_acc = next_account_info(accounts_iter)?;
5
let delegation_acc = next_account_info(accounts_iter)?;
6
let src_acc = next_account_info(accounts_iter)?;
7
let dst_acc = next_account_info(accounts_iter)?;
8
...
9
}
Copied!
Client call
1
const schema = [
2
{ key: 'code', type: 'u8' },
3
{ key: 'amount', type: 'u64' }
4
];
5
const layout = new soproxABI.struct(schema, {
6
code: 5,
7
amount,
8
});
9
const instruction = new TransactionInstruction({
10
keys: [
11
{ pubkey: payer.publicKey, isSigner: true, isWritable: false },
12
{ pubkey: token.publicKey, isSigner: false, isWritable: false },
13
{ pubkey: delegation.publicKey, isSigner: false, isWritable: true },
14
{ pubkey: source.publicKey, isSigner: false, isWritable: true },
15
{ pubkey: destination.publicKey, isSigner: false, isWritable: true },
16
],
17
programId,
18
data: layout.toBuffer()
19
});
Copied!

IncreaseApproval - Code 6

Increase the number of delegated token by an additional amount.
IncreaseApproval interface
1
AppInstruction::IncreaseApproval { amount } => {
2
let accounts_iter = &mut accounts.iter();
3
let owner = next_account_info(accounts_iter)?;
4
let token_acc = next_account_info(accounts_iter)?;
5
let delegation_acc = next_account_info(accounts_iter)?;
6
...
7
}
Copied!
Client call
1
const schema = [
2
{ key: 'code', type: 'u8' },
3
{ key: 'amount', type: 'u64' }
4
];
5
const layout = new soproxABI.struct(schema, {
6
code: 6,
7
amount,
8
});
9
const instruction = new TransactionInstruction({
10
keys: [
11
{ pubkey: payer.publicKey, isSigner: true, isWritable: false },
12
{ pubkey: token.publicKey, isSigner: false, isWritable: false },
13
{ pubkey: delegation.publicKey, isSigner: false, isWritable: true },
14
],
15
programId,
16
data: layout.toBuffer()
17
});
Copied!

DecreaseApproval - Code 7

Decrease the number of delegated token by an additional amount.
DecreaseApproval interface
1
AppInstruction::DecreaseApproval { amount } => {
2
let accounts_iter = &mut accounts.iter();
3
let owner = next_account_info(accounts_iter)?;
4
let token_acc = next_account_info(accounts_iter)?;
5
let delegation_acc = next_account_info(accounts_iter)?;
6
...
7
}
Copied!
Client call
1
const schema = [
2
{ key: 'code', type: 'u8' },
3
{ key: 'amount', type: 'u64' }
4
];
5
const layout = new soproxABI.struct(schema, {
6
code: 7,
7
amount,
8
});
9
const instruction = new TransactionInstruction({
10
keys: [
11
{ pubkey: payer.publicKey, isSigner: true, isWritable: false },
12
{ pubkey: token.publicKey, isSigner: false, isWritable: false },
13
{ pubkey: delegation.publicKey, isSigner: false, isWritable: true },
14
],
15
programId,
16
data: layout.toBuffer()
17
});
Copied!

Revoke - Code 8

Revoke a delegation account. All lamports in this account will return to the delegation owner.
Revoke interface
1
AppInstruction::Revoke {} => {
2
let accounts_iter = &mut accounts.iter();
3
let owner = next_account_info(accounts_iter)?;
4
let token_acc = next_account_info(accounts_iter)?;
5
let delegation_acc = next_account_info(accounts_iter)?;
6
...
7
}
Copied!
Client call
1
const schema = [
2
{ key: 'code', type: 'u8' },
3
];
4
const layout = new soproxABI.struct(schema, {
5
code: 8,
6
});
7
const instruction = new TransactionInstruction({
8
keys: [
9
{ pubkey: payer.publicKey, isSigner: true, isWritable: false },
10
{ pubkey: token.publicKey, isSigner: false, isWritable: false },
11
{ pubkey: delegation.publicKey, isSigner: false, isWritable: true },
12
],
13
programId,
14
data: layout.toBuffer()
15
});
Copied!

Future works

    SRC20 Wrapper (for SPL token standard)
    Enhance security
Last modified 10mo ago