SNIP-20 Spec: Private, Fungible Tokens
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.
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.
- 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.
Transfer
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.
Request
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 |
Response
{
"transfer": {
"status": "success"
}
}
Send
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.
Request
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 |
Response
{
"send": {
"status": "success"
}
}
RegisterReceive
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.Request
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 |
Response
{
"register_receive": {
"status": "success"
}
}
CreateViewingKey
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.Request
Name | Type | Description | optional |
---|---|---|---|
entropy | string | A source of random information | no |
padding | string | Ignored string used to maintain constant-length messages | yes |
Response
{
"create_viewing_key": {
"key": "<string>"
}
}
SetViewingKey
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.
Request
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 |
Response
{
"set_viewing_key": {
"status": "success"
}
}
Balance
This query MUST be authenticated.
Returns the balance of the given address. Returns "0" if the address is unknown to the contract.
Request
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 |
Response
{
"balance": {
"amount": "123"
}
}
TokenInfo
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.
Request
No parameters required.
Response
{
"name": "coin name",
"symbol": "coin symbol",
"decimals": 6,
"total_supply": "Uint128"
}
TransferHistory
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.Request
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 |
Response
{
"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.
IncreaseAllowance
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).Request
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 |
Response
{
"increase_allowance": {
"spender": "<address>",
"owner": "<address>",
"allowance": "<current allowance>"
}
}
DecreaseAllowance
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.Request
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 |
Response
{
"decrease_allowance": {
"spender": "<address>",
"owner": "<address>",
"allowance": "<current allowance>"
}
}
TransferFrom
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
Request
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 |
Response
{
"transfer_from": {
"status": "success"
}
}
SendFrom
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).Request
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 |
Response
{
"send_from": {
"status": "success"
}
}
Allowance
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.Request
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 |
Response
{
"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).
Mint
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
.Request
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 |
Response
{
"mint": {
"status": "success"
}
}
SetMinters
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.
Request
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 |
Response
{
"set_minters": {
"status": "success"
}
}
Burn
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.Request
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 |
Response
{
"burn": {
"status": "success"
}
}
BurnFrom
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.
Request
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 |
Response
{
"burn_from": {
"status": "success"
}
}
Minters
Returns the list of minters that have been configured in the contract.
Request
None
Response
{
"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.
Deposit
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.Request
Name | Type | Description | optional |
---|---|---|---|
padding | string | Ignored string used to maintain constant-length messages | yes |
Response
{
"deposit": {
"status": "success"
}
}
Redeem
Redeems tokens in exchange for native coins. The redeemed tokens SHOULD be burned, and taken out of the pool.
Request
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 |
Response
{
"redeem": {
"status": "success"
}
}
ExchangeRate
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.
Request
None
Response
{
"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.Last modified 1mo ago