What is SNRC-20?

SNRC-20 is an inscription standard based on Starknet, developed by developers from the Starknet community worldwide as a public good, capable of off-chain computation and verification.

SNRC-20 not only includes standards for the inscription, but also provides integration interfaces for SNRC-20-compatible smart contracts and suggestions for implementing indexers.

SNRC-20 strives to ensure fairness, transparency, and efficiency in all processes.

This standard is inspired by the following projects/standards:

You can find us here:

SNRC-20 Design Philosophy

Due to the presence of many unnecessary parts in the traditional BRC-20 inscriptions, SNRC-20 adopts the approach of Off-Chain calculation and transmission of hashes for inscriptions.

For example, a standard BRC-20 inscription requires the following three JSON objects to implement all its operations:

Deploy

{ 
  "p": "brc-20",
  "op": "deploy",
  "tick": "ordi",
  "max": "21000000",
  "lim": "1000"
}

Mint

{ 
  "p": "brc-20",
  "op": "mint",
  "tick": "ordi",
  "amt": "1000"
}

Transfer

{ 
  "p": "brc-20",
  "op": "transfer",
  "tick": "ordi",
  "amt": "100"
}

We can see that there is a lot of repeated content in these JSON objects.

For example, if we want to deploy a BRC-20 based inscription that is different from the ordi inscription, we actually only need to change the following three fields:

  "tick": new_tick,
  "max": max_supply,
  "lim": limit

The remaining parts are the same for all inscriptions of brc-20 protocol, and we can easily restore the complete Deploy inscription json from the above three fields.

The same applies to Mint and Transfer.

Since we can infer the Mint or Transfer inscription json from the Deploy inscription json. We only need the tick and amt fields, along with the Deploy inscription json, to restore the complete Mint or Transfer inscription json.

Therefore, as mentioned above, only three fields are needed to restore the complete Deploy inscription. As long as we can query the complete Deploy inscription, we can restore the Mint and Transfer inscriptions from a few parameters.

Now we can say that an inscription based on BRC-20 only requires the following JSON data:

 {
  "tick": new_tick,
  "max": max_supply,
  "lim": limit
  }

This is the initial design concept of SNRC-20.

However, this is still not concise enough. Do we really need to transmit inscription's data in JSON format and inscribe it on the blockchain?

As is well known, the Cairo language is not very friendly to string due to its type system. Can we design an inscription system that is more suitable for Starknet?

We thought of hash. Hash can be considered the lifeblood of the blockchain. Its uniqueness and verifiability provide great convenience for the blockchain. For unchanging content, using its hash is the best method for transmission, comparison, and indexing. Coincidentally, the inscription system has a lot of unchanging content.

Also, the default type Felt252 of the Cairo language is designed to be suitable for storing hashes. Consider this approach:

Convert the unchanging content into a hash, combine it with the changing content, and use as few Felt252 as possible to transmit and inscribe data.

This is the way. This is the design philosophy of SNRC-20.

This is the way

Technologies

This chapter will provide an overview of the technologies required to implement the design philosophy of SNRC-20.

Poseidon HASH

SNRC-20 uses the Poseidon HASH algorithm, which is friendly to Zero-Knowledge Proofs, for Off-Chain hash computation.

Poseidon HASH is a family of hash functions designed to be very efficient as algebraic circuits. As a ZK-friendly hashing, they can be very useful in ZK-proving systems such as STARKs.

Poseidon is a sponge construction based on the Hades permutation. Starknet’s version of Poseidon is based on a three-element state permutation.

A Poseidon hash of up to 2 elements is defined as follows.

\[ poseidon_1(x) := \left[\text{hades_permutation}(x,0,1)\right]_0 \]

\[ poseidon_2(x,y) := \left[\text{hades_permutation}(x,y,2)\right]_0 \]

Where \( [\cdot]_j \) denotes taking the j’th coordinate of a tuple.

Poseidon Array hashing

Let \( \text{hades}:\mathbb{F}_P^3\rightarrow\mathbb{F}_P^3 \) denote the Hades permutation, with Starknet’s parameters, then given an array a_1,...,a_n of 𝑛 field elements we define poseidon(a_1,...,a_n) to be the first coordinate of H(a_1,...,a_n;0,0,0), where: \[ H(a_1,...,a_n;s_1,s_2,s_3)=\begin{cases} H\big(a_3,...,a_n;\text{hades}(s_1+a_1, s_2+a_2, s_3)\big), & \text{if } n\ge 2 \ \text{hades}(s_1+a_1,s_2+1,s_3), & \text{if } n=1 \ \text{hades}(s_1+1,s_2,s_3), & \text{if } n=0 \ \end{cases} \]

Implementation

You can find an implementation of Poseidon Hash in Cairo 1 here.

You can also try it in this code block, it is editable and runnable:

use core::poseidon::PoseidonTrait;
use core::hash::{HashStateTrait, HashStateExTrait};

#[derive(Drop, Hash)]
struct StructForHash {
    first: felt252,
    second: felt252,
    third: (u32, u32),
    last: bool,
}

fn main() -> felt252 {
    let struct_to_hash = StructForHash { first: 0, second: 1, third: (1, 2), last: false };

    let hash = PoseidonTrait::new().update_with(struct_to_hash).finalize();
    hash
}

L2 -> L1 Messaging

One of the key characteristics of a Layer 2 solution is its capacity to communicate with Layer 1.

Starknet employs a unique L1-L2 Messaging system, distinct from its consensus protocol and the process of submitting state updates on L1. This messaging system enables smart contracts on L1 to interface with those on L2 (and vice versa), facilitating "cross-chain" transactions.

mechanism

In the SNRC-20 standard, we use the Messaging System to send the content of the inscriptions to Ethereum L1. This allows the inscriptions to be verified on Ethereum as well, providing new possibilities for cross-chain operations of inscriptions.

However, when sending a message from Starknet to Ethereum, only the hash of the message is sent on L1 by the Starknet sequencer. You must write your own Ethereum contract to consume the message manually.

You can learn how to do it here.

What is the next?

In the following chapters, we will explain in detail the specific format and calculation methods for each inscription operation.

SNRC-20 Operations Overview

The SNRC-20 standard defines the following three operations, along with their inputs and outputs.

In the most ideal and simple case, the output and input of an operation should be equivalent.

Operations

Deploy SNRC-20

Off-Chain Calculation

Consider the following inscription format, similar to BRC-20:

{ 
  "p": "snrc-20",
  "op": "deploy",
  "tick": "nwhp",
  "max": "19770525",
  "lim": "19770525" 
}

It's important to note that, since the default type of the Cairo virtual machine is Felt252, all numerical parameters should be of the u128 type, which ranges from 0 to 2^128-1 and can fit within 252 bits. The type requirements are as follows:

KeytypeDescription
pFelt252(ShortString)Protocol: Helps other systems identify and process snrc-20 events
opFelt252(ShortString)Operation: Type of event (Deploy, Mint, Transfer)
tickFelt252(ShortString)Ticker: A string identifier of the snrc-20
maxFelt252(u128)Max supply: set max supply of the snrc-20
limFelt252(u128)Mint limit: Limit per tx for users

At the same time, to be compatible with the ETHS standard, we also need to satisfy the validation of contentURI.

Therefore, the final Deploy inscription format involved in the calculation is as follows:

data:,{ 
  "p": "snrc-20",
  "op": "deploy",
  "tick": "nwhp",
  "max": "19770525",
  "lim": "19770525" 
}

Next, we need to calculate the Poseidon Hash of the fixed part of the Mint inscription. The complete Cairo program code is as follows(The following code block is editable and runnable) :

use core::clone::Clone;
use core::serde::Serde;
use core::poseidon::PoseidonTrait;
use core::byte_array::ByteArrayTrait;
use core::hash::{HashStateTrait, HashStateExTrait};
use core::array::{ArrayTrait, SpanTrait};
use core::traits::{Into, TryInto};

fn main() -> felt252 {
    /// Replace this with your own data. Be careful you should stringify it first.
    let tick = 'nwhp';
    let max: u128 = 19770525;
    let lim: u128 = 19770525;
    let payload_pref: ByteArray = "data:,{\"p\":\"snrc-20\",\"op\":\"deploy\",\"tick\":\"";
    let payload_max: ByteArray = "\",\"max\":\"";
    let payload_lim: ByteArray = "\",\"lim\":\"";
    let payload_remain: ByteArray = "\"}";

    let mut output_array = ArrayTrait::<felt252>::new();
    let mut i: usize = 0;
    /// This is the same as `let serialized = payload.serialize(ref output_array);`
    /// , but it still not supported by the online compiler
    ///
    loop {
        if i >= payload_pref.len() {
            break;
        }
        let char = payload_pref.at(i).unwrap();
        char.serialize(ref output_array);
        i += 1;
    };
    tick.serialize(ref output_array);
    i=0;
    loop {
        if i >= payload_max.len() {
            break;
        }
        let char = payload_max.at(i).unwrap();
        char.serialize(ref output_array);
        i += 1;
    };
    i=0;
    max.serialize(ref output_array);
    loop {
        if i >= payload_lim.len() {
            break;
        }
        let char = payload_lim.at(i).unwrap();
        char.serialize(ref output_array);
        i += 1;
    };
    i=0;
    lim.serialize(ref output_array);
    loop {
        if i >= payload_remain.len() {
            break;
        }
        let char = payload_remain.at(i).unwrap();
        char.serialize(ref output_array);
        i += 1;
    };
    core::poseidon::poseidon_hash_span(output_array.span())
}

The return value of this Cairo program is

916838225186069478585876038986673186814268706728240273539841908638806157666

, represented in hex as

0x206E97BD728106AAE642A8847107EF91922321FF00A4FEB7A99880D4D8BA962.

This is the hash value corresponding to the Deploy operation.

Through this hash value, we will be able to identify what operations the user has performed on chain in the indexer, and restore the complete inscription by searching the hash table.

Organize parameters for Deploy operartion

However, for the complete Deploy parameters, we also need the hash values corresponding to the inscriptions of Mint and Transfer operations.

Please refer to the next two chapters for this content. Here, for this tick :

  "tick": "nwhp",
  "max": "19770525",
  "lim": "19770525" 

We will first assume that the hash corresponding to Mint is

0x33FF744581AA76AFA81006908ADC9A41B68FACB15ECF5E980EF56F9910380DE

, and the hash corresponding to Transfer is

0x70B420BAF038B3D467D80FD313B0D2AEDBEB46B7157CED682649D4507292E6A.

A Deploy operation requires the following six parameters:

ParamtertypeDescription
Deploy_hashFelt252Hash of Deploy operation
Mint_hashFelt252Hash of Mint operation
Transfer_hashFelt252Hash of Transfer operation
TickFelt252(ShortString)Ticker: A string identifier of the snrc-20
MaxFelt252(u128)Max supply: Max supply of the snrc-20
LimFelt252(u128)Mint limit: Limit per tx for users

Where deploy_hash, mint_hash, transfer_hash are the Poseidon Hash corresponding to the Deploy, Mint, and Transfer inscriptions respectively.

Deploy an inscription by a SNRC-20 Contract

When users deploy an inscription in a contract that meets the SNRC-20 standard, they need to organize their input in a format like previous section said.

The input value in this example should be:

"Deploy_hash": "0x206E97BD728106AAE642A8847107EF91922321FF00A4FEB7A99880D4D8BA962"
"Mint_hash": "0x33FF744581AA76AFA81006908ADC9A41B68FACB15ECF5E980EF56F9910380DE"
"Transfer_hash": "0x70B420BAF038B3D467D80FD313B0D2AEDBEB46B7157CED682649D4507292E6A",
"Tick": "nwhp",
"Max": 19770525,
"Lim": 19770525

Then, the contract that complies with the SNRC-20 standard will inscribe the above input into the L2->L1 Message and emit an event.

For more information about the SNRC-20 Contract, please refer to this chapter.

Mint SNRC-20

Off-Chain Calculation

Firstly, for any mint operation, we need to prepare the following standard inscription format similar to BRC-20, but without amt:

{ 
  "p": "snrc-20",
  "op": "mint",
  "tick": "tick"
}

At the same time, to be compatible with the ETHS standard, we also need to satisfy the validation of contentURI. Therefore, the final Mint inscription format involved in the calculation is as follows:

data:,{ 
  "p": "snrc-20",
  "op": "mint",
  "tick": "tick"
}

Next, we need to calculate the Poseidon Hash of the fixed part of the Mint inscription. The complete Cairo program code is as follows(The following code block is editable and runnable) :

use core::serde::Serde;
use core::poseidon::PoseidonTrait;
use core::byte_array::ByteArrayTrait;
use core::hash::{HashStateTrait, HashStateExTrait};
use core::array::{ArrayTrait, SpanTrait};
use core::traits::{Into, TryInto};

fn main() -> felt252 {
    /// Replace this with your own data. Be careful you should stringify it first.
    let tick = 'nwhp';
    let payload_pref: ByteArray = "data:,{\"p\":\"snrc-20\",\"op\":\"mint\",\"tick\":\"";
    let payload_remain: ByteArray = "\"}";
    let mut output_array = ArrayTrait::<felt252>::new();
    let mut i: usize = 0;
    /// This is the same as `let serialized = payload.serialize(ref output_array);`
    /// , but it still not supported by the online compiler
    ///
    loop {
        if i >= payload_pref.len() {
            break;
        }
        let char = payload_pref.at(i).unwrap();
        char.serialize(ref output_array);
        i += 1;
    };
    tick.serialize(ref output_array);
    let mut i: usize = 0;
    loop {
        if i >= payload_remain.len() {
            break;
        }
        let char = payload_remain.at(i).unwrap();
        char.serialize(ref output_array);
        i += 1;
    };
    core::poseidon::poseidon_hash_span(output_array.span())
}

The return value of this Cairo program is

1469956484733314490006856178496349941983860913245365919661958978345090908382,

represented in hex as

0x33FF744581AA76AFA81006908ADC9A41B68FACB15ECF5E980EF56F9910380DE.

This is the hash value corresponding to the Mint operation.

Through this hash value, we will be able to identify what operations the user has performed on chain in the indexer, and restore the complete inscription by searching the hash table.

Mint an inscription by a SNRC-20 Contract

When users mint in a contract that meets the SNRC-20 standard, they need to organize their input in the following format:

ParamtertypeDescription
Mint_hashFelt252Hash of Mint operation
AmountFelt252(u128)Amount of mint

Referring to the Lim field in the previous Deploy operation example, we can set Amount to 19770525.

Therefore, the input value in this example should be:

"Mint_hash": "0x33FF744581AA76AFA81006908ADC9A41B68FACB15ECF5E980EF56F9910380DE"
"Amount": 19770525

Then, the contract that complies with the SNRC-20 standard will inscribe the above input into the L2->L1 Message and emit an event.

Transfer SNRC-20

Off-Chain Calculation

Firstly, for any transfer operation, we need to prepare the following standard inscription format similar to BRC-20, but without amt:

{ 
  "p": "snrc-20",
  "op": "transfer",
  "tick": "tick"
}

At the same time, to be compatible with the ETHS standard, we also need to satisfy the validation of contentURI. Therefore, the final Transfer inscription format involved in the calculation is as follows:

data:,{ 
  "p": "snrc-20",
  "op": "transfer",
  "tick": "tick"
}

Next, we need to calculate the Poseidon Hash of the fixed part of the Transfer inscription. The complete Cairo program code is as follows(The following code block is editable and runnable) :

use core::serde::Serde;
use core::poseidon::PoseidonTrait;
use core::byte_array::ByteArrayTrait;
use core::hash::{HashStateTrait, HashStateExTrait};
use core::array::{ArrayTrait, SpanTrait};
use core::traits::{Into, TryInto};

fn main() -> felt252 {
    /// Replace this with your own data. Be careful you should stringify it first.
    let tick = 'nwhp';
    let payload_pref: ByteArray = "data:,{\"p\":\"snrc-20\",\"op\":\"transfer\",\"tick\":\"";
    let payload_remain: ByteArray = "\"}";
    let mut output_array = ArrayTrait::<felt252>::new();
    let mut i: usize = 0;
    /// This is the same as `let serialized = payload.serialize(ref output_array);`
    /// , but it still not supported by the online compiler
    ///
    loop {
        if i >= payload_pref.len() {
            break;
        }
        let char = payload_pref.at(i).unwrap();
        char.serialize(ref output_array);
        i += 1;
    };
    tick.serialize(ref output_array);
    let mut i: usize = 0;
    loop {
        if i >= payload_remain.len() {
            break;
        }
        let char = payload_remain.at(i).unwrap();
        char.serialize(ref output_array);
        i += 1;
    };
    core::poseidon::poseidon_hash_span(output_array.span())
}

The return value of this Cairo program is

3186081088044837381540966151676090343793197013422856004776743671156846440042,

represented in hex as

0x70B420BAF038B3D467D80FD313B0D2AEDBEB46B7157CED682649D4507292E6A.

This is the hash value corresponding to the Mint operation. Through this hash value, we will be able to identify what operations the user has performed on chain in the indexer, and restore the complete inscription by searching the hash table.

Transfer an inscription by a SNRC-20 Contract

When users transfer in a contract that meets the SNRC-20 standard, they need to organize their input in the following format:

ParamtertypeDescription
Transfer_hashFelt252Hash of Transfer operation
SenderFelt252(address)Address of the sender
RecipientFelt252(address)Address of the recipient
AmountFelt252(u128)Amount of transfer

For the Transfer operation, there is no Lim limit -- of course, you cannot transfer more than the amount of inscription balance you own.

Therefore, the input value in this example coulb be:

"Transfer_hash": "0x70B420BAF038B3D467D80FD313B0D2AEDBEB46B7157CED682649D4507292E6A"
"Sender": "Sender's address"
"Recipient": "Recipient's address"
"Amount": 19770525

Then, the contract that complies with the SNRC-20 standard will inscribe the above input into the L2->L1 Message and emit an event.

SNRC-20 Contract

An SNRC-20 contract refers to a contract that can issue SNRC-20 operations. As is well known, since there are no EOA addresses in Starknet, accounts themselves are contracts, and you can consider SNRC-20 as an extension of abstract accounts. Anyone can deploy an independent SNRC-20 contract, or integrate the interface defined below into a contract to become an SNRC-20 compatible contract.

Interfaces

Deploy function Interface

The Deploy Interface defines a function interface that can execute a Deploy operation.

Input

There are no strict requirements for the input, but it is recommended to use the input format defined in the Deploy operation.

You can also write a function that only takes the tick, max, lim parameters, then calculate the hash of all operations in your contract and inscribe it into the message, although this will consume more gas.

Output

The output of this function should be a message payload, which conform to the following format:

ParamtertypeDescription
Deploy_hashFelt252Hash of Deploy operation
Mint_hashFelt252Hash of Mint operation
Transfer_hashFelt252Hash of Transfer operation
TickFelt252(ShortString)Ticker: A string identifier of the snrc-20
MaxFelt252(u128)Max supply: Max supply of the snrc-20
LimFelt252(u128)Mint limit: Limit per tx for users

These data contain the key elements to restore the complete Deploy inscription, and provide the Hash for Mint and Transfer, which will facilitate indexing.

Here is an example of an message payload(in Hex):

IndexValue
00x206E97BD728106AAE642A8847107EF91922321FF00A4FEB7A99880D4D8BA962
10x33FF744581AA76AFA81006908ADC9A41B68FACB15ECF5E980EF56F9910380DE
20x70B420BAF038B3D467D80FD313B0D2AEDBEB46B7157CED682649D4507292E6A
30x434f4f4c
40x22658
50x22658

Event

After a Deploy function is successfully executed, it should emit an event. This will facilitate the indexer to include this transaction.

The format of the events is as follows:

event Deploy(sender, hash, tick, max, lim)

The parameters are:

InputTypeDescription
senderContractAddressSender's address
hashArray::< Felt252 >[Deploy_hash, Mint_hash, Transfer_hash]
tickFelt252(ShortString)A string identifier of the snrc-20
maxFelt252(u128)Max supply: Max supply of the snrc-20
limFelt252(u128)Mint limit: Limit per tx for users

Mint function Interface

The Mint Interface defines a function interface that can execute a Mint operation.

Input

There are no strict requirements for the input, but it is recommended to use the input format defined in the Mint operation.

You can also write a function that only takes the tick, amt parameters, then calculate the hash of all operations in your contract and inscribe it into the message, although this will consume more gas.

Output

The output of this function should be a message payload, which conform to the following format:

ParamtertypeDescription
Mint_hashFelt252Hash of Mint operation
AmountFelt252(u128)Amount of this mint operation

These data contain the key elements to restore the complete Mint inscription.

Here is an example of an message payload(in Hex):

IndexValue
00x33FF744581AA76AFA81006908ADC9A41B68FACB15ECF5E980EF56F9910380DE
10x22658

Event

After a Mint function is successfully executed, it should emit an event. This will facilitate the indexer to include this transaction.

The format of the events is as follows:

event Mint(sender, hash, amount)

The parameters are:

InputTypeDescription
senderContractAddressSender's address
hashFelt252Mint_hash
amountFelt252(u128)Amount of this mint operation

Transfer function Interface

The Transfer Interface defines a function interface that can execute a Transfer operation.

Input

There are no strict requirements for the input, but it is recommended to use the input format defined in the Transfer operation.

You can also write a function that only takes the tick, amt and recipient parameters, then calculate the hash of all operations in your contract and inscribe it into the message, although this will consume more gas.

Output

The output of this function should be a message payload, which conform to the following format:

ParamtertypeDescription
Transfer_hashFelt252Hash of Transfer operation
SenderContractAddressSender's address
RecipientContractAddressRecipient's address
AmountFelt252(u128)Amount of this transfer operation

These data contain the key elements to restore the complete Transfer inscription.

Here is an example of an message payload(in Hex):

IndexValue
00x70B420BAF038B3D467D80FD313B0D2AEDBEB46B7157CED682649D4507292E6A
10x111111111111111111111111111111111111111111111111111111111111111
20x0
30x22658

Event

After a Transfer function is successfully executed, it should emit an event. This will facilitate the indexer to include this transaction.

The format of the events is as follows:

event Trasfer(sender, hash, recipient, amount)

The parameters are:

InputTypeDescription
senderContractAddressSender's address
hashFelt252Mint_hash
recipientContractAddressRecipient's address
amountFelt252(u128)Amount of this mint operation

Indexer

Since inscriptions do not adopt the account balance mechanism by default (reminder: you can still combine it with the account balance mechanism, this is not mandatory), you need an indexer to record user balances.

For a contract that complies with the SNRC-20 standard, there are two ways to index:

Complete indexing and Quick indexing.

How to index?

Complete Indexing

Complete indexing means that for an inscription transaction, you need to obtain its Events, Transaction, TransactionReceipt on the Starknet chain, and index them.

Quick Indexing

Quick indexing allows you to only index Events, to establish an index that only contains basic inscription information. This is suitable for scenarios that do not need to read L2->L1 Message.

Use Full-node or RPC service

You can obtain the necessary information through any Full-node or RPC service with an endpoint version greater than 0.5. Here is a list of available services.

You can refer to the Voyager's documentation to learn how to use Starknet's RPC service and find the APIs you need.