SNIP-20, is a specification for private fungible tokens based on CosmWasm on the Secret Network. The name and design is loosely based on Ethereum's ERC-20 & ERC-777 standards, and a superset of CosmWasm's CW-20. The additions to this spec over the CW20 specification are mostly for privacy focused features, and as such will strive to maintain compatibility.
The specification is split into multiple sections, a contract may only implement some of this functionality, but must implement the base functionality.
Contracts that implement this interface SHOULD support only a single token/symbol, and MUST conform to the standards set in this memo.
-
-
Messages:
Queries:
-
Messages:
Queries:
-
Messages:
Queries:
-
Messages:
Queries:
-
This document aims to set standard interfaces that SNIP-20 contract implementors will create, and that both wallet implementors & dependant contract creators will consume. For this reason, the focus of this document is to merely give SNIP-20 contract implementors the tools needed to create contracts that fully maintain privacy but not to specify implementation details.
Notation in this document conforms to the standards set by RFC-2119
- Message - This is an on-chain interface. It is triggered by sending a transaction, and receiving an on-chain response which is read by the client. Messages are authenticated both by the blockchain, and by the secret enclave.
- Query - This is an off-chain interface. Queries are done by returning data that a node has locally, and are not public. Query responses are returned immediately, and do not have to wait for blocks. In addition, queries cannot be authenticated using the standard interfaces. Any contract that wishes to strongly enforce query permissions must implement it themselves. (TBD - should this be standardized?)
- Cosmos Message Sender - The account that is found under the
sender
field in a standard Cosmos SDK message. This is also the signer of the message. - Native Asset - A coin which is defined and managed by the blockchain infrastructure, not a secret contract.
Users may want to enforce constant length messages to avoid leaking data. To support this functionality, SNIP-20 tokens MUST support the option to include a padding field in every message. This optional padding field may be sent with ANY of the messages in this spec. Contracts and Clients MUST ignore this field if sent, either in the request or response fields
Requests SHOULD be sent as base64 encoded JSON. Future versions of Secret Network may add support for other formats as well, but at this time we recommend usage of JSON only. For this reason the parameter descriptions specify the JSON type which must be used. In addition, request parameters will include in parentheses a CosmWasm (or other) underlying type that this value must conform to. E.g. a recipient address is sent as a string, but must also be parsed to a bech32 address.
Queries are off-chain requests, that are not cryptographically validated. This
means that contracts that wish to validate the caller of a query MUST implement
some sort of authentication. SNIP-20 uses an "API key" scheme, which validates
a (viewing key, account)
pair.
Authentication MUST happen on each query that reveals private account-specific information. Authentication MUST be a resource intensive operation, that takes a significant amount of time to compute. This is because such queries are open to offline brute-force attacks, which can be parallelized to scale linearly with the resources of a motivated attacker. Authentication MUST perform the same computation even if the user does not have a viewing key set. Authentication response MUST be indistinguishable for both the case of a wrong viewing key and the case of a non-existent viewing key.
Unless specified otherwise, all message & query responses will be JSON encoded
in the data
field of the Cosmos response, rather than in the logs
.
This is meant to reduce the potential for data-leakage through side-channel
attacks. In addition, since all keys will be encrypted, it is not possible to
use the log
events for event triggering.
Some of the messages detailed in this document contain a "status"
field.
This field MUST hold one of two values: "success"
or "failure"
.
While errors during execution of contract functions should usually result in a
proper and detailed error response, The "failure"
status is reserved for cases
where a contract might choose to obfuscate the exact cause of failure, or
otherwise indicate that while nothing failed to happen, the operation itself
could not be completed for some valid reason.
Note that all amounts are represented as numerical strings (The Uint128 type). Handling decimals is left to the UI.
Moves tokens from the account that appears in the Cosmos message sender field to the account in the recipient field.
- Variation from CW-20: It is NOT required to validate that the recipient is an address and not a contract. This command will work when trying to send funds to contract accounts as well.
Name | Type | Description | optional |
---|---|---|---|
recipient | string | Accounts SHOULD be a valid bech32 address, but contracts may use a different naming scheme as well | no |
amount | string (Uint128) | The amount of tokens to transfer | no |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"transfer": {
"status": "success"
}
}
Moves amount from the Cosmos message sender account to the recipient account. The receiver account MAY be a contract that has registered itself using a RegisterReceive message. If such a registration has been performed, a message MUST be sent to the contract's address as a callback, after completing the transfer. The format of this message is described under Receiver interface.
If the callback fails due to an error in the Receiver contract, the entire transaction will be reverted.
Name | Type | Description | optional |
---|---|---|---|
recipient | string | Accounts SHOULD be a valid bech32 address, but contracts may use a different naming scheme as well | no |
amount | string (Uint128) | The amount of tokens to send | no |
msg | string (base64) | Base64 encoded message, which the recipient will receive | yes |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"send": {
"status": "success"
}
}
This message is used to tell the SNIP-20 contract to call the Receive
function
of the Cosmos message sender after a successful Send
.
In Secret Network this is used to pair a code hash with the contract address
that must be called. This means that the SNIP-20 MUST store the sent code_hash
and use it when calling the Receive
function.
Name | Type | Description | optional |
---|---|---|---|
code_hash | string | A 32-byte hex encoded string, with the code hash of the receiver contract | no |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"register_receive": {
"status": "success"
}
}
This function generates a new viewing key for the Cosmos message sender, which is used in ALL account specific queries. This key is used to validate the identity of the caller, since in queries in Cosmos there is no way to cryptographically authenticate the querier's identity.
The Viewing Key MUST be stored in such a way that lookup takes a significant amount to time to perform, in order to be resistant to brute-force attacks. The viewing key MUST NOT control any functions which actively affect the balance of the user.
The entropy
field of the request should be a client supplied string used for
entropy for generation of the viewing key. Secure implementation is left to the
client, but it is recommended to use base-64 encoded random bytes and not
predictable inputs.
Name | Type | Description | optional |
---|---|---|---|
entropy | string | A source of random information | no |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"create_viewing_key": {
"key": "<string>"
}
}
Set a viewing key with a predefined value for Cosmos message sender, without creating it. This is useful to manage multiple SNIP-20 tokens using the same viewing key.
If a viewing key is already set, the contract MUST replace the current key. If a viewing key is not set, the contract MUST set the provided key as the viewing key.
It is NOT RECOMMENDED to use this function to create easy to remember passwords for users, but this is left up to implementors to enforce.
Name | Type | Description | optional |
---|---|---|---|
key | string | A user supplied string that will be used to authenticate the sender | no |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"set_viewing_key": {
"status": "success"
}
}
This query MUST be authenticated.
Returns the balance of the given address. Returns "0" if the address is unknown to the contract.
Name | Type | Description | optional |
---|---|---|---|
key | string | The viewing key | no |
address | string | Addresses SHOULD be a valid bech32 address, but contracts may use a different naming scheme as well | no |
{
"balance": {
"amount": "123"
}
}
This query need not be authenticated.
Returns the token info of the contract. The response MUST contain: token name, token symbol, and the number of decimals the token uses. The response MAY additionally contain the total-supply of tokens. This is to enable Layer-2 tokens which want to hide the amounts converted as well.
No parameters required.
{
"name": "coin name",
"symbol": "coin symbol",
"decimals": 6,
"total_supply": "Uint128"
}
This query MUST be authenticated.
This query SHOULD return a list of json objects describing the transactions made by the querying address, in newest-first order. The user may optionally specify a limit on the amount of information returned by paging the available items.
The response should look as described below. The id
field is optional, and may
be used by contracts that choose to populate it. The from
and sender
fields
may be different if the TX was generated by a TransferFrom
or SendFrom operation.
Name | Type | Description | optional |
---|---|---|---|
key | string | The viewing key | no |
address | string | Addresses SHOULD be a valid bech32 address, but contracts may use a different naming scheme as well | no |
page_size | number | Number of transactions to return, starting from the latest. i.e. n=1 will return only the latest transaction | no |
page | number | This defaults to 0. Specifying a positive number will skip page * page_size txs from the start. |
yes |
{
"transfer_history": {
"txs": [
{
"id": "optional ID",
"from": "address of the owner of the funds",
"sender": "address of the sender",
"receiver": "address of the receiver",
"coins": {
"denom": "coin denomination/name",
"amount": "Uint128"
}
},
{ "...": "..." }
]
}
}
A SNIP-20 contract may allow accounts to delegate some of their balance to other accounts. This is very similar to the allowance feature of ERC-20 contracts. It can be used by accounts to allow other contracts to manage a portion of their balance.
This is also useful for maintaining a higher degree of transactional privacy. A user may choose to operate two accounts: One would deposit/withdraw funds to the SNIP-20 contract, and then give allowance to a second account. That second account will seem unrelated to the depositing account to outside observers, and will be able to make transactions in its name. The recipients of the transfer will still be able to see the connection, but the sender can change the account that gets allowance as often as they need to. This will allow you to minimize the interaction of the depositing account with the contract, and dissociate the (public) calls to the contract from the funds they operate on.
There was an issue with race conditions in the original ERC20 approval spec. If you had an approval of 50 and I then want to reduce it to 20, I submit a Tx to set the allowance to 20. If you see that and immediately submit a tx using the entire 50, you then get access to the other 20. Not only did you quickly spend the 50 before I could reduce it, you get another 20 for free.
The solution discussed in the Ethereum community was an IncreaseAllowance and DecreaseAllowance operator (instead of Approve). To originally set an approval, use IncreaseAllowance, which works fine with no previous allowance. DecreaseAllowance is meant to be robust, that is if you decrease by more than the current allowance (eg. the user spent some in the middle), it will just round down to 0 and not make any underflow error.
Set or increase the allowance such that spender may access up to
current_allowance + amount
tokens from the Cosmos message sender account.
This may optionally come with an expiration time, which if set limits when the
approval can be used (by time).
Name | Type | Description | optional |
---|---|---|---|
spender | string | The address of the account getting access to the funds | no |
amount | string(Uint128) | The number of tokens to increase allowance by | no |
expiration | number | Time at which the allowance expires. Counts the number of seconds from epoch, 1.1.1970 encoded as uint64 |
yes |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"increase_allowance": {
"spender": "<address>",
"owner": "<address>",
"allowance": "<current allowance>"
}
}
Decrease or clear the allowance by a sent amount. This may optionally come with
an expiration time, which if set limits when the approval can be used. If amount
is equal or greater than the current allowance, this action MUST set the
allowance to zero, and return a "success"
response.
Name | Type | Description | optional |
---|---|---|---|
spender | string | The address of the account getting access to the funds | no |
amount | string(Uint128) | The number of tokens to decrease allowance by | no |
expiration | number | Time at which the allowance expires. Counts the number of seconds from epoch, 1.1.1970 encoded as uint64 |
yes |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"decrease_allowance": {
"spender": "<address>",
"owner": "<address>",
"allowance": "<current allowance>"
}
}
Transfer an amount of tokens from a specified account, to another specified account. This action MUST fail if the Cosmos message sender does not have an allowance limit that is equal or greater than the amount of tokens sent for the owner account
Name | Type | Description | optional |
---|---|---|---|
owner | string | Account to take tokens from | no |
recipient | string | Account to send tokens to | no |
amount | string(Uint128) | Amount of tokens to transfer | no |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"transfer_from": {
"status": "success"
}
}
SendFrom is to Send, what TransferFrom is to Transfer. This allows a
pre-approved account to not just transfer the tokens,
but to send them to another address to trigger a given action. Note SendFrom
will set the Receive{sender}
to be the env.message.sender
(the account that triggered the transfer) rather than the owner account (the
account the money is coming from).
Name | Type | Description | optional |
---|---|---|---|
owner | string | Account to take tokens from | no |
recipient | string | Account to send tokens to | no |
amount | string(Uint128) | Amount of tokens to transfer | no |
msg | string(base64) | Base64 encoded message, which the recipient will receive | yes |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"send_from": {
"status": "success"
}
}
This query MUST be authenticated.
This returns the available allowance that spender can access from the owner's account, along with the expiration info.
Every account's viewing key MUST be given permissions to query the allowance of any pair of owner and spender, as long as that account is either the owner or the spender in the query. In other words, every account's viewing key can be used to find out how much allowance the account has given other accounts, and how much it has been given by other accounts.
The expiration
field of the response may be either null
or unset if no
expiration has been set.
Name | Type | Description | optional |
---|---|---|---|
owner | string | Account from which tokens are allowed to be taken | no |
spender | string | Account which is allowed to spend tokens on behalf of the owner | no |
key | string | The viewing key | no |
{
"allowance": {
"spender": "<address>",
"owner": "<address>",
"allowance": "<current allowance>",
"expiration": 1234
}
}
This allows another contract to mint new tokens, possibly with a cap. There is only one minter specified here, if you want more complex access management. please use a multisig or other contract as the minter address and handle updating the ACL there.
The functions in this section allow managing a list of addresses that have permissions to mint new coins and increase the total supply. You may choose to only give this permission to only one address, or a multisig address, but we leave the option open for some contracts to define more than one address, as either a backup, or because they choose to trust more than one party.
Access to these functions should normally be restricted to a very small set of known addresses (e.g. an predetermined admin address, or members of the minters list).
This function MUST be allowed only for accounts on the minters list.
If the Cosmos message sender is an allowed minter, this will create amount
new
tokens and add them to the balance of recipient
.
Name | Type | Description | optional |
---|---|---|---|
recipient | string | Account to mint tokens to | no |
amount | string(Uint128) | Amount of tokens to mint | no |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"mint": {
"status": "success"
}
}
This function MUST only be allowed for authorized accounts.
The list of addresses in the message will be set to the list of minters in the contract. This completely overrides the previously saved list.
Name | Type | Description | optional |
---|---|---|---|
minters | list of strings | List of addresses to set to the list of minters | no |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"set_minters": {
"status": "success"
}
}
MUST remove amount
tokens from the balance of the Cosmos message sender and
MUST reduce the total supply by the same amount.
MUST NOT transfer the funds to another account.
Name | Type | Description | optional |
---|---|---|---|
amount | string (Uint128) | The amount of tokens to burn | no |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"burn": {
"status": "success"
}
}
This works like TransferFrom, but burns the tokens instead of transferring them. This will reduce the owner's balance, total_supply and the caller's allowance.
This function should be available when a contract supports both the Mintable and Allowances interfaces.
Name | Type | Description | optional |
---|---|---|---|
owner | string | Account to take tokens from | no |
amount | string(Uint128) | Amount of tokens to burn | no |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"burn_from": {
"status": "success"
}
}
Returns the list of minters that have been configured in the contract.
None
{
"minters": {
"minters": ["<address>", "<address>", "<address>"]
}
}
This section describes functions that can be useful for coins that are backed by some other native asset. Using these, you can create privacy tokens which wrap assets, such as SCRT, ATOM, etc.
Deposits a native coin into the contract, which will mint an equivalent amount
of tokens to be created. The amount MUST be sent in the sent_funds
field of
the transaction itself, as coins must really be sent to the contract's native
address.
The minted amounts MUST match the exchange rate specified by the
ExchangeRate query.
The deposit MUST return an error if any coins that do not match expected
denominations are sent.
Name | Type | Description | optional |
---|---|---|---|
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"deposit": {
"status": "success"
}
}
Redeems tokens in exchange for native coins. The redeemed tokens SHOULD be burned, and taken out of the pool.
Name | Type | Description | optional |
---|---|---|---|
amount | string(Uint128) | The amount of tokens to redeem to | no |
denom | string | Denom of tokens to mint. Only used if the contract supports multiple denoms | yes |
padding | string | Ignored string used to maintain constant-length messages | yes |
{
"redeem": {
"status": "success"
}
}
This query need not be authenticated.
Gets information about the token exchange rate functionality that the contract provides. This query MUST return.
- exchange rate, as an integer string. The amount of native coins that equal one token.
- Denomination of native tokens which are acceptable, as a string OR a comma separated value.
None
{
"exchange_rate": {
"rate": "<U128>",
"denom": "<denom>"
}
}
The Send and SendFrom functions optionally send a callback to a registered contract address, as described above. The callback in both cases will include a message with the following format:
{
"receive": {
"sender": "address of the sender",
"from": "address of the owner of the funds",
"amount": "funds that were sent as Uint128",
"msg": "custom message, optional"
}
}
The sender
and from
fields may be different if the receive
message is
sent using the SendFrom message. They will be the same when sent
by a Send
call.
If an optional msg
field is included in the message to the SNIP-20
contract, then it will be included in the receive
message sent to the
Receiver contract.