CIS-2: Concordium Token Standard 2¶
Created |
May 16, 2021 |
---|---|
Final |
Aug 3, 2022 |
Supported versions |
Smart contract version 1 or newer
(Protocol version 4 or newer)
|
Standard identifier |
|
Requires |
|
Deprecates |
Abstract¶
A standard interface for both fungible and non-fungible tokens implemented in a smart contract. The interface provides functions for transferring token ownership, allowing other addresses to transfer tokens and for querying token balances, operators and token metadata. It allows for off-chain applications to track token balances and the location of token metadata using logged events.
This standard is a modification of CIS-1 that uses features introduced in smart contract version 1.
Specification¶
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
General types and serialization¶
Note
Integers are encoded in little-endian unless stated otherwise.
TokenID
¶
Token Identifier, which combined with the address of the smart contract instance implementing CIS2, forms the globally unique identifier of a token type.
A token ID for a token type SHALL NOT change after a token type has been minted.
A token ID for a token type SHALL NOT be reused for another token type within the same smart contract.
A token ID is serialized as 1 byte for the size (n
) of the identifier, followed by this number of bytes for the token id (id
):
TokenID ::= (n: Byte) (id: Byteⁿ)
Note
Token IDs can be as small as a single byte (by setting the first byte to the value 0) or as big as 256 bytes leaving more than 10^614 possible token IDs. The token ID could be an encoding of a small text string or some checksum hash, but to save energy it is still recommended to use small token IDs if possible.
TokenAmount
¶
An amount of a token type is an unsigned integer up to 2^256 - 1. It is serialized using the LEB128 variable-length unsigned integer encoding, with the additional constraint of the total number of bytes of the encoding MUST not exceed 37 bytes:
TokenAmount ::= (x: Byte) => x if x < 2^7
| (x: Byte) (m: TokenAmount) => (x - 2^7) + 2^7 * m if x >= 2^7
ReceiveHookName
¶
A smart contract entrypoint name. An entrypoint name MUST consist only of ASCII alphanumeric or punctuation characters.
It is serialized as: the function name byte length (n
) is represented by the first 2 bytes, followed by this many bytes for the function name (name
).
The entrypoint name MUST be 100 bytes or less:
ReceiveHookName ::= (n: Byte²) (name: Byteⁿ)
AccountAddress
¶
An address of an account.
It is serialized as 32 bytes:
AccountAddress ::= (address: Byte³²)
ContractAddress
¶
An address of a contract instance. It consists of an index and a subindex both unsigned 64 bit integers.
It is serialized as: first 8 bytes for the index (index
) followed by 8 bytes for the subindex (subindex
) both little endian:
ContractAddress ::= (index: Byte⁸) (subindex: Byte⁸)
Address
¶
Is either an account address or a contract address.
It is serialized as: First byte indicates whether it is an account address or a contract address.
In case the first byte is 0 then an AccountAddress
(address
) follows.
In case the first byte is 1 then a ContractAddress
(address
) follows:
Address ::= (0: Byte) (address: AccountAddress)
| (1: Byte) (address: ContractAddress)
Receiver
¶
The receiving address of a transfer, which is either an account address or a contract address. In the case of a contract address: a name of the hook receive function to invoke is also needed.
It is serialized as: First byte indicates whether it is an account address or a contract address.
In case the first byte is 0 then an AccountAddress
(address
) follows.
In case the first byte is 1 then a ContractAddress
(address
) and bytes for ReceiveHookName (hook
) follows:
Receiver ::= (0: Byte) (address: AccountAddress)
| (1: Byte) (address: ContractAddress) (hook: ReceiveHookName)
AdditionalData
¶
Additional bytes to include in a transfer, which can be used to add additional parameters for the transfer function call.
It is serialized as: the first 2 bytes encode the length (n
) of the data, followed by this many bytes for the data (data
):
AdditionalData ::= (n: Byte²) (data: Byteⁿ)
Note
This type is passed as part of a parameter for smart contract function calls.
MetadataUrl
¶
A URL and optional checksum for metadata stored outside of this contract.
It is serialized as: 2 bytes for the length of the metadata URL (n
) and then this many bytes for the URL to the metadata (url
) (UTF-8 encoded) followed by an optional checksum.
The checksum is serialized by 1 byte to indicate whether a hash of the metadata is included, if its value is 0, then no content hash, if the value is 1 then 32 bytes for a SHA256 hash (hash
) follows:
MetadataChecksum ::= (0: Byte)
| (1: Byte) (hash: Byte³²)
MetadataUrl ::= (n: Byte²) (url: Byteⁿ) (checksum: MetadataChecksum)
Contract functions¶
A smart contract implementing CIS2 MUST export the following functions transfer, updateOperator, balanceOf, operatorOf and tokenMetadata according to the following description:
transfer
¶
Executes a list of token transfers.
A transfer is a token ID, an amount of tokens to be transferred, and the from
address and to
address.
When transferring tokens to a contract address, additional information for a receive function hook to trigger is required.
Parameter¶
The parameter is a list of transfers.
It is serialized as: 2 bytes representing the number of transfers (n
) followed by the bytes for this number of transfers (transfers
).
Each transfer is serialized as: a TokenID (id
), a TokenAmount (amount
), the token owner address Address (from
), the receiving address Receiver (to
) and some additional data (data
):
Transfer ::= (id: TokenID) (amount: TokenAmount) (from: Address) (to: Receiver) (data: AdditionalData)
TransferParameter ::= (n: Byte²) (transfers: Transferⁿ)
Note
Since the byte size of a single transfer can vary in size, this will limit the number of transfers that can be included in the same function call since a maximum size of a parameter is 65535 bytes.
Receive hook parameter¶
The parameter for the receive function hook contains information about the transfer and some additional data bytes.
It is serialized as: a TokenID (id
), a TokenAmount (amount
), the token owner address Address (from
) and AdditionalData (data
):
ReceiveHookParameter ::= (id: TokenID) (amount: TokenAmount) (from: Address) (data: AdditionalData)
Requirements¶
The list of transfers MUST be executed in order.
The contract function MUST reject if any of the transfers fail to be executed.
A transfer MUST fail if:
The token balance of the
from
address is insufficient to do the transfer.The TokenID is not known by the contract.
A transfer MUST non-strictly decrease the balance of the
from
address and non-strictly increase the balance of theto
address or fail.A transfer with the same address as
from
andto
MUST be executed as a normal transfer.A transfer of a token amount zero MUST be executed as a normal transfer.
A transfer of some amount of a token type MUST only transfer the exact amount of the given token type between balances.
A transfer of any amount of a token type to a contract address MUST call receive hook function on the receiving smart contract with a receive hook parameter.
Let
operator
be an operator of the addressowner
. A transfer of any amount of a token type from an addressowner
sent by an addressoperator
MUST be executed as if the transfer was sent byowner
.The contract function MUST reject if a receive hook function called on the contract receiving tokens rejects.
The balance of an address not owning any amount of a token type SHOULD be treated as having a balance of zero.
Warning
Be aware of transferring tokens to a non-existing account address. This specification by itself does not include a mechanism to recover these tokens. Checking the existence of an account address would ideally be done off-chain before the message is even sent to the token smart contract.
updateOperator
¶
Add or remove a number of addresses as operators of the address sending this message.
Parameter¶
The parameter contains a list of operator updates. An operator update includes information on whether to add or remove an operator and the address to add/remove as operator. It does not contain the address which is adding/removing the operator as this will be the sender of the message invoking this function.
The parameter is serialized as: first 2 bytes (n
) for the number of updates followed by this number of operator updates (updates
).
An operator update is serialized as: 1 byte (update
) indicating whether to remove or add an operator, where if the byte value is 0 the sender is removing an operator, if the byte value is 1 the sender is adding an operator.
The update is followed by the operator address (operator
) Address to add or remove as operator for the sender:
OperatorUpdate ::= (0: Byte) // Remove operator
| (1: Byte) // Add operator
UpdateOperator ::= (update: OperatorUpdate) (operator: Address)
UpdateOperatorParameter ::= (n: Byte²) (updates: UpdateOperatorⁿ)
Requirements¶
The list of updates MUST be executed in order.
The contract function MUST NOT increase or decrease the balance of any address for any token type.
The balance of an address not owning any amount of a token type SHOULD be treated as having a balance of zero.
The contract function MUST reject if any of the updates fails to be executed.
balanceOf
¶
Query balances of a list of addresses and token IDs.
Parameter¶
The parameter consists of a list of token ID and address pairs.
It is serialized as: 2 bytes for the number of queries (n
) and then this number of queries (queries
).
A query is serialized as TokenID (id
) followed by Address (address
):
BalanceOfQuery ::= (id: TokenID) (address: Address)
BalanceOfParameter ::= (n: Byte²) (queries: BalanceOfQueryⁿ)
Response¶
The function output response is a list of token amounts.
It is serialized as: 2 bytes for the number of token amounts (n
) and then this number of TokenAmount (results
):
BalanceOfResponse ::= (n: Byte²) (results: TokenAmountⁿ)
Requirements¶
The balance of an address not owning any amount of a token type SHOULD be treated as having a balance of zero.
The number of results in the response MUST correspond to the number of the queries in the parameter.
The order of results in the response MUST correspond to the order of queries in the parameter.
The contract function MUST NOT increase or decrease the balance of any address for any token type.
The contract function MUST NOT add or remove any operator for any address.
The contract function MUST reject if any of the queries fail:
A query MUST fail if the token ID is unknown.
operatorOf
¶
Query operators with a list of pairs, an owner address and a potential operator address, to check whether the potential operator address is an operator for the owner address.
Parameter¶
The parameter consists of a list of address pairs.
It is serialized as: 2 bytes for the number of queries (n
) and then this number of queries (queries
).
A query is serialized as Address (owner
) followed by Address (address
):
OperatorOfQuery ::= (owner: Address) (address: Address)
OperatorOfParameter ::= (n: Byte²) (queries: OperatorOfQueryⁿ)
Response¶
The function output is a list of booleans, where a value is True
if and only if the address
is an operator of the owner
address from the corresponding query.
It is serialized as: 2 bytes for the number of results (n
) and then this number of results (results
).
A boolean is serialized as a byte with value 0 for false and 1 for true (isOperator
):
Bool ::= (0: Byte) // False
| (1: Byte) // True
OperatorOfQueryResult ::= (isOperator: Bool)
OperatorOfResultParameter ::= (n: Byte²) (results: OperatorOfQueryResultⁿ)
Requirements¶
The number of results in the response MUST correspond to the number of the queries in the parameter.
The order of results in the response MUST correspond to the order of queries in the parameter.
The contract function MUST NOT increase or decrease the balance of any address for any token type.
The contract function MUST NOT add or remove any operator for any address.
The contract function MUST reject if any of the queries fail.
tokenMetadata
¶
Query the current token metadata URLs for a list of token IDs.
Parameter¶
The parameter consists of a list of token IDs.
It is serialized as: 2 bytes for the number of queries (n
) and then this number of TokenID (ids
):
TokenMetadataParameter ::= (n: Byte²) (ids: TokenIDⁿ)
Response¶
The function output is a list of token metadata URLs.
It is serialized as: 2 bytes for the number of queries (n
) and then this number of MetadataUrl (results
):
TokenMetadataResultParameter ::= (n: Byte²) (results: MetadataUrlⁿ)
Requirements¶
The number of results in the response MUST correspond to the number of the queries in the parameter.
The order of results in the response MUST correspond to the order of queries in the parameter.
The contract function MUST NOT increase or decrease the balance of any address for any token type.
The contract function MUST NOT add or remove any operator for any address.
The contract function MUST reject if any of the queries fail:
A query MUST fail if the token ID is unknown.
Logged events¶
The idea of the logged events for this specification is for off-chain applications to be able to track balances and operators without knowledge of the contract-specific implementation details. For this reason, it is important for the token contract to log the appropriate event, any time modifications of balances or operators are made.
It MUST be possible to derive the balance of an address for a token type from the logged TransferEvent, MintEvent and BurnEvent events.
It MUST be safe to assume that with no events logged, every address has zero tokens and no operators enabled.
The events defined by this specification are serialized using one byte to discriminate the different events. A custom event SHOULD NOT have a first byte colliding with any of the events defined by this specification.
TransferEvent
¶
A TransferEvent
event MUST be logged for every amount of a token type changing ownership from one address to another.
The TransferEvent
event is serialized as: first a byte with the value of 255, followed by the token ID TokenID (id
), an amount of tokens TokenAmount (amount
), from address Address (from
) and to address Address (to
):
TransferEvent ::= (255: Byte) (id: TokenID) (amount: TokenAmount) (from: Address) (to: Address)
MintEvent
¶
A MintEvent
event MUST be logged every time a new token is minted. This also applies when introducing new token types and the initial token types and amounts in a contract.
Minting a token with a zero amount can be used to indicate the existence of a token type without minting any amount of tokens.
The MintEvent
event is serialized as: first a byte with the value of 254, followed by the token ID TokenID (id
), an amount of tokens being minted TokenAmount (amount
) and the owner address of the tokens Address (to
):
MintEvent ::= (254: Byte) (id: TokenID) (amount: TokenAmount) (to: Address)
Note
Be aware of the limit on the number of logs. A token smart contract function which needs to mint a large number of token types with token metadata might hit this limit.
BurnEvent
¶
A BurnEvent
event MUST be logged every time an amount of a token type is burned.
Summing all of the minted amounts from MintEvent
events and subtracting all of the burned amounts from BurnEvent
events for a token type MUST sum up to the total supply for the token type.
The total supply of a token type MUST be in the inclusive range of [0, 2^256 - 1].
The BurnEvent
event is serialized as: first a byte with the value of 253, followed by the token ID TokenID (id
), an amount of tokens being burned TokenAmount (amount
), and the owner address of the tokens Address (from
):
BurnEvent ::= (253: Byte) (id: TokenID) (amount: TokenAmount) (from: Address)
UpdateOperatorEvent
¶
The event to log when updating an operator of some address.
The UpdateOperatorEvent
event is serialized as: first a byte with the value of 252, followed by a OperatorUpdate
(update
), then the owner address updating an operator Address (owner
), and an operator address Address (operator
) being added or removed:
UpdateOperatorEvent ::= (252: Byte) (update: OperatorUpdate) (owner: Address) (operator: Address)
TokenMetadataEvent
¶
The event to log when setting the metadata url for a token type.
It consists of a token ID and a URL (RFC 3986) for the location of the metadata for this token type with an optional SHA256 checksum of the content.
Logging the TokenMetadataEvent
event again with the same token ID, is used to update the metadata location and only the most recently logged token metadata event for a certain token id should be used to get the token metadata.
The TokenMetadataEvent
event is serialized as: first a byte with the value of 251, followed by the token ID TokenID (id
), and then a MetadataUrl (metadata
):
TokenMetadataEvent ::= (251: Byte) (id: TokenID) (metadata: MetadataUrl)
Note
Be aware of the limit on the number of logs, and also the byte size limit on each logged event. This will limit the length of the metadata URL depending on the size of the token ID and whether a content hash is included.
Rejection errors¶
A smart contract following this specification MAY reject using the following error codes:
Name |
Error code |
Description |
---|---|---|
INVALID_TOKEN_ID |
-42000001 |
A provided token ID it not part of this token contract. |
INSUFFICIENT_FUNDS |
-42000002 |
An address balance contains insufficient amount of tokens to complete some transfer of a token. |
UNAUTHORIZED |
-42000003 |
Sender is unauthorized to call this function. Note authorization is not mandated anywhere in this specification, but can still be introduced on top of the standard. |
Rejecting using an error code from the table above MUST only occur in a situation as described in the corresponding error description.
The smart contract implementing this specification MAY introduce custom error codes other than the ones specified in the table above.
Token metadata JSON¶
The token metadata is stored off-chain and MUST be a JSON (RFC 8259) file.
All of the fields in the JSON file are optional, and this specification reserves a number of field names, shown in the table below.
Property |
JSON value type [JSON-Schema] |
Description |
---|---|---|
|
string |
The name to display for the token type. |
|
string |
Short text to display for the token type. |
|
boolean |
Describes whether a token should be treated as unique. If unique, a wallet should treat the balance as a boolean. If this field is not present, the token should not be treated as unique. |
|
number [ |
The number of decimals, when displaying an amount of this token type in a user interface.
If the decimal is set to |
|
string |
A description for this token type. |
|
URL JSON object |
An image URL to a small image for displaying the asset. |
|
URL JSON object |
An image URL to a large image for displaying the asset. |
|
URL JSON object |
A URL to the token asset. |
|
JSON array of Token metadata JSON objects |
Collection of assets. |
|
JSON array of Attribute JSON objects |
Assign a number of attributes to the token type. Attributes can be used to include extra information about the token type. |
|
JSON object with locales as field names (RFC 5646) and field values are URL JSON objects linking to JSON files. |
URLs to JSON files with localized token metadata. |
Optionally a SHA256 hash of the JSON file can be logged with the TokenMetadata event for checking integrity. Since the metadata JSON file could contain URLs, a SHA256 hash can optionally be associated with the URL. To associate a hash with a URL the JSON value is an object:
Property |
JSON value type [JSON-Schema] |
Description |
---|---|---|
|
string (RFC 3986) [ |
A URL. |
|
string |
A SHA256 hash of the URL content encoded as a hex string. |
Attributes are objects with the following fields:
Property |
JSON value type [JSON-Schema] |
Description |
---|---|---|
|
string |
Type for the value field of the attribute. |
|
string |
Name of the attribute. |
|
string |
Value of the attrbute. |
Example token metadata: Fungible¶
An example of token metadata for a CIS2 implementation wrapping the CCD could be:
{
"name": "Wrapped CCD Token",
"symbol": "wCCD",
"decimals": 6,
"description": "A CIS2 token wrapping the Concordium native token (CCD)",
"thumbnail": { "url": "https://location.of/the/thumbnail.png" },
"display": { "url": "https://location.of/the/display.png" },
"artifact": { "url": "https://location.of/the/artifact.png" },
"localization": {
"da-DK": {
"url": "https://location.of/the/danish/metadata.json",
"hash": "624a1a7e51f7a87effbf8261426cb7d436cf597be327ebbf113e62cb7814a34b"
}
}
}
The danish localization JSON file could be:
{
"description": "CIS2 indpakket CCD"
}
Example token metadata: Non-fungible¶
An example of token metadata for a NFT could be:
{
"name": "Bibi - The Ryan Cat",
"unique": true,
"description": "Ryan cats are lonely creatures travelling the galaxy in search of their ancestors and true inheritance",
"thumbnail": { "url": "https://location.of/the/thumbnail.png" },
"display": { "url": "https://location.of/the/display.png" },
"attributes": [{
"type": "date",
"name": "Birthday",
"value": "1629792199610"
}, {
"type": "string",
"name": "Body",
"value": "Strong"
}, {
"type": "string",
"name": "Head",
"value": "Round"
}, {
"type": "string",
"name": "Tail",
"value": "Short"
}],
"localization": {
"da-DK": {
"url": "https://location.of/the/danish/metadata.json",
"hash": "588d7c14883231cfee522479cc66565fd9a50024603a7b8c99bd7869ca2f0ea3"
}
}
}
The danish localization JSON file could be:
{
"name": "Bibi - Ryan katten",
"description": "Ryan katte er ensomme væsner, som rejser rundt i galaxen søgende efter deres forfædre og sande arv"
}
Token address¶
A token address is the globally unique identifier for a CIS-2 token type on the Concordium blockchain. It consists of a contract address paired with a CIS-2 Token ID.
The textual representation is defined as follows: the index and subindex of the contract address are byte-encoded using unsigned LEB128 followed by the bytes of the token ID, this bytestring is then encoded using Base58Check with version byte 2 and the Bitcoin symbol chart.
Smart contract limitations¶
A number of limitations are important to be aware of:
The byte size of smart contract function parameters are limited to at most 65535 B.
Each logged event is limited to 0.5 KiB.
The total size of the smart contract module is limited to 512 KiB.
Decisions and rationale¶
In this section we point out some of the differences from other popular token standards found on other blockchains, and provide reasons for deviating from them in CIS2.
Token ID bytes instead of integers¶
Token standards such as ERC721 and ERC1155 both use a 256-bit unsigned integer (32 bytes) for the token ID, to support using something like a SHA256 hash for the token ID. But in the case where the token ID have no significance other than a simple identifier, smaller sized token IDs can reduce energy costs. This is why we chose to let the first byte indicate the size of the token ID, meaning a token ID can vary between 1 byte and 256 bytes. The latter allows more than 10^614 possible token IDs.
Variable-length encoding of token amount¶
Similar to ERC721 and ERC1155, the token amount is limited to a 256-bit unsigned integer. However, using 32 bytes for encoding the token amount is wasteful for token contracts with a total supply fitting into fewer bytes. This is especially the case for non-fungible tokens. Additionally 256-bit integers are not natively supported by WebAssembly meaning arithmetics are expensive compared to a 32-bit or 64-bit integer. This specification uses a variable-length encoding of the token amount, allowing a token smart contract to restrict the token amount and internally represent the token amount using fewer bytes.
Only batched transfers¶
The specification only has a transfer smart contract function that takes a list of transfers and no function for a single transfer. This will result in lower energy cost compared to multiple contract calls and only introduces a small overhead for single transfers. The reason for not also including a single transfer function is to have smaller smart contract modules, which in turn leads to saving cost on every function call.
Note
Notice that transfer is more general than both safeTransferFrom
and safeBatchTransferFrom
found in ERC721 and ERC1155 as these standards only take a single sender and receiver for a batch of transfers.
No token-level approval/allowance like in ERC20 and ERC721¶
This standard only specifies address-level operators and not token-level operators. The main argument is simplicity and to save energy cost on common cases, but other reasons are:
Token-level operators require the token smart contract to track more state, which increases the overall energy cost.
For token smart contracts with a lot of token types, such as a smart contract with a large collection of NFTs, token-level operators could become very expensive.
For fungible tokens; approval/allowance introduces an attack vector.
Note
The specification does not prevent adding more fine-grained authorization, such as token-level operators.
Receive hook function¶
The specification requires a token receive hook to be invoked on a smart contract receiving tokens, this will in some cases prevent mistakes such as sending tokens to smart contracts which do not define behavior for receiving tokens. These token could then be lost forever.
The reason for this not being optional is to allow other smart contracts, which integrate with a token smart contract, to rely on this for functionality.
Warning
The smart contract receive hook function can be called by any account or smart contract. It is up to the integrating contract whether it should trust the caller or not.
Receive hook function callback argument¶
The name of the receive hook function called on a smart contract receiving tokens is supplied as part of the parameter. This allows for a smart contract integrating with a token smart contract to have multiple hooks and leave it to the caller to know which hook they want to trigger.
No sender hook function¶
The FA2 token standard found on Tezos allows for a hook function to be called on a smart contract sending tokens, such that the contract can reject the transfer on some criteria. This seems to only make sense if some operator is transferring tokens from a contract, in which case the sender smart contract might as well contain the logic to transfer the tokens and trigger this directly.
Explicit events for mint and burn¶
ERC20, ERC721 and ERC1155 use a transfer event from or to the zero address to indicate mint and burn respectively, but since there are no such thing as the zero address on the Concordium blockchain these events are separate. Making it more explicit instead of special case transfer events.
No error code for receive hook rejecting¶
The specification could include an error code for the receive hook function to return if rejecting the token transferred (as seen in the FA2 standard on Tezos). But we chose to leave this error code up to the receiving smart contract, which allows for more informative error codes.
Adding SHA256 checksum for token metadata event¶
A token can optionally include a SHA256 checksum when logging the token metadata event, this is to ensure the integrity of the token metadata. This checksum can be updated by logging a new event.
Differences from CIS1¶
The query functions balanceOf, operatorOf, and tokenMetadata differ from CIS1. The query functions in CIS1 use a callback pattern to output the result of a query. However, starting from Concordium smart contract version 1, a smart contract receive function can return values back to the invoker. CIS2 uses this output instead of a callback pattern to return the query result. Using output instead of callbacks requires less energy and will reduce the contract code needed for querying.
In CIS1 the callback result includes the corresponding query to ease the use of the callback pattern. The query information is not needed in the output result of CIS2 query functions. Instead, the results are required to be the same length and order as the queries.
In CIS2 smart contract functions are not required to fail with a specific error code as in CIS1. This is to allow receive functions to fail early for reason specific to the implementation such as authorization or serialization.
Prior to smart contract version 1 invoking another smart contract required knowing the contract name as well as the contract address and endpoint. Smart contract version 1 removes the need for the contract name, which is why Receive hook parameter does not included the token contract name as seen in CIS1.
In CIS1 the token amount is fixed to u64 which was deemed sufficient for most token smart contracts. However, to improve the interoperability with decentralized applications with support for other blockchains using 256-bit integers, the variable-length encoding was introduced making CIS2 token amount more flexible.
Additions after finalization¶
October 13, 2022: Added the optional unique
field to the metadata specification. This field helps specify how wallets should display a token and its balance.
April 24, 2023: Added description of a Token Address and how to construct its textual representation.