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.
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.
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:
Key | type | Description |
---|---|---|
p | Felt252(ShortString) | Protocol: Helps other systems identify and process snrc-20 events |
op | Felt252(ShortString) | Operation: Type of event (Deploy, Mint, Transfer) |
tick | Felt252(ShortString) | Ticker: A string identifier of the snrc-20 |
max | Felt252(u128) | Max supply: set max supply of the snrc-20 |
lim | Felt252(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:
Paramter | type | Description |
---|---|---|
Deploy_hash | Felt252 | Hash of Deploy operation |
Mint_hash | Felt252 | Hash of Mint operation |
Transfer_hash | Felt252 | Hash of Transfer operation |
Tick | Felt252(ShortString) | Ticker: A string identifier of the snrc-20 |
Max | Felt252(u128) | Max supply: Max supply of the snrc-20 |
Lim | Felt252(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:
Paramter | type | Description |
---|---|---|
Mint_hash | Felt252 | Hash of Mint operation |
Amount | Felt252(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:
Paramter | type | Description |
---|---|---|
Transfer_hash | Felt252 | Hash of Transfer operation |
Sender | Felt252(address) | Address of the sender |
Recipient | Felt252(address) | Address of the recipient |
Amount | Felt252(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:
Paramter | type | Description |
---|---|---|
Deploy_hash | Felt252 | Hash of Deploy operation |
Mint_hash | Felt252 | Hash of Mint operation |
Transfer_hash | Felt252 | Hash of Transfer operation |
Tick | Felt252(ShortString) | Ticker: A string identifier of the snrc-20 |
Max | Felt252(u128) | Max supply: Max supply of the snrc-20 |
Lim | Felt252(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):
Index | Value |
---|---|
0 | 0x206E97BD728106AAE642A8847107EF91922321FF00A4FEB7A99880D4D8BA962 |
1 | 0x33FF744581AA76AFA81006908ADC9A41B68FACB15ECF5E980EF56F9910380DE |
2 | 0x70B420BAF038B3D467D80FD313B0D2AEDBEB46B7157CED682649D4507292E6A |
3 | 0x434f4f4c |
4 | 0x22658 |
5 | 0x22658 |
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:
Input | Type | Description |
---|---|---|
sender | ContractAddress | Sender's address |
hash | Array::< Felt252 > | [Deploy_hash, Mint_hash, Transfer_hash] |
tick | Felt252(ShortString) | A string identifier of the snrc-20 |
max | Felt252(u128) | Max supply: Max supply of the snrc-20 |
lim | Felt252(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:
Paramter | type | Description |
---|---|---|
Mint_hash | Felt252 | Hash of Mint operation |
Amount | Felt252(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):
Index | Value |
---|---|
0 | 0x33FF744581AA76AFA81006908ADC9A41B68FACB15ECF5E980EF56F9910380DE |
1 | 0x22658 |
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:
Input | Type | Description |
---|---|---|
sender | ContractAddress | Sender's address |
hash | Felt252 | Mint_hash |
amount | Felt252(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:
Paramter | type | Description |
---|---|---|
Transfer_hash | Felt252 | Hash of Transfer operation |
Sender | ContractAddress | Sender's address |
Recipient | ContractAddress | Recipient's address |
Amount | Felt252(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):
Index | Value |
---|---|
0 | 0x70B420BAF038B3D467D80FD313B0D2AEDBEB46B7157CED682649D4507292E6A |
1 | 0x111111111111111111111111111111111111111111111111111111111111111 |
2 | 0x0 |
3 | 0x22658 |
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:
Input | Type | Description |
---|---|---|
sender | ContractAddress | Sender's address |
hash | Felt252 | Mint_hash |
recipient | ContractAddress | Recipient's address |
amount | Felt252(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.
// TODO: Full example>