Technical Update on LSP7 Digital Asset and LSP8 Identifiable Digital Asset

Jean Cvllr
LUKSO
Published in
13 min readDec 20, 2023

--

Today’s article focuses on the main changes in the LUKSO token standards, commonly known as LSP7 Digital Asset and LSP8 Identifiable Digital Asset.

With these new updates, the LSP7 and LSP8 standards have now moved to the “review” stage.

We will cover the latest changes introduced in the LSP7 and LSP8 token standards by breaking down changes that apply to both and changes specific to each one.

Table of Content

Changes applicable to both LSP7 + LSP8

  • 🧬 Interface IDs changes
  • 🧩 LSP7 + LSP8 now support LSP17 Extendable by default
  • 🗄️ New LSP4 Token Type data key
  • 🔔 Operators get notified via LSP1 Universal Receiver
  • 📣 Events related to operators renamed
  • 🔁 New batchCalls(bytes[]) function added to LSP7 + LSP8
  • 📊 Full data value emitted in DataChanged event
  • 🔗 LSP4Metadata valueContent: change from JSONURL to VerifiableURI
  • 📋 Data sent when notifying via LSP1 is now ABI-encoded

Changes specific to LSP7

  • increaseAllowance(…) and ➖decreaseAllowance(…) functions added
  • 🛃 getOperatorsOf(address) function added in LSP7

Changes specific to LSP8

  • ✏️ New argument lsp8TokenIdFormat set on deployment
  • 🔠 New function get/setDataForTokenId(...) to set metadata per tokenId (including batch function)
  • 📢 New event TokenIdDataChanged
  • 🗑️ Deprecated data keys: LSP8MetadataTokenURI

Conclusion, Guides and Useful Links

Changes applicable to both LSP7 + LSP8

Interface IDs changes

With most breaking changes come interface ID changes. The introduction of new functions in the LSP7 + LSP8 interfaces, the ERC165 interface ID of both standards now changed to the following bytes4 identifiers:

  • LSP7 new interface ID: 0xb3c4928f
  • LSP8 new interface ID: 0x3a271706

Developers should now use these 2 identifiers when calling the supportsInterface(bytes4) function of a contract to identify if it is a LSP7 or LSP8 token contract.

The interface ID change is due to the following functions that were added:

You can also use the following guide on docs.lukso.tech to learn how to detect the new interface ID of LSP7 and LSP8.

LSP7 + LSP8 now support LSP17 Extendable by default

One of the major additions to both standards and their implementations is that they now fully support the LSP17 Extendable standard by default.

LSP17 allows a smart contract to add any new functions to the contract after it has been deployed on a network.

By “any new functions” we mean any publicly callable function that is not supported natively in its ABI. Furthermore, these functions can be added and removed during the contract’s lifetime.

Example of adding a new non-native function flipCoin(…) on a contract using LSP17

This works by setting the extension contract for this non-native function selector as a value for the LSP17Extension:<selector> data key. The code logic in these extension contracts will act as the behaviour for this non-native function. See the following schema below for this data key:

{
"name": "LSP17Extension:<bytes4>",
"key": "0xcee78b4094da860110960000<bytes4>",
"keyType": "Mapping",
"valueType": "address",
"valueContent": "Address"
}

See the LSP17 Extendable Standard on docs.lukso.tech for more details.

By adding LSP17 by default in LSP7 and LSP8, this means new functionalities can be added to the token contracts. Such functionalities could include:

  • making the NFTs directly buyable by interacting with the LSP8 contract.
  • implement royalty distributions mechanisms, to allow tokens or NFT holders to claim their royalty payments.

Check out the “Adding Functionalities to Universal Profiles using LSP17 — Contract Extension” video to learn how to build using the LSP17 standard.

New LSP4 Token Type data key

When deploying an LSP7 or LSP8 contract, the LSP4TokenType data key must be set to define the type of digital asset being created. Whether it is:

  • a Token (value 0)
  • a NFT (value 1)
  • or a Collection (value 2)

Additionally, more custom token types could be defined in the future.

This value is then publicly readable under the LSP4TokenType data key after the contract has been deployed. For instance, our erc725.js library can be used to retrieve this information and decode it as follows:

import { ERC725 } from '@erc725/erc725.js';
import LSP4Schema from '@erc725/erc725.js/schemas/LSP4DigitalAssetMetadata.json';
const tokenContractAddress = '0x0Dc07C77985fE31996Ed612F568eb441afe5768D';
const RPC_URL = 'https://rpc.testnet.lukso.network';
const config = {
ipfsGateway: 'https://YOUR-IPFS-GATEWAY/ipfs/',
};
const erc725 = new ERC725(LSP4Schema, tokenContractAddress, RPC_URL, config);await erc725.fetchData('LSP4Metadata');/**
{
key: '0x9afb95cacc9f95858ec44aa8c3b685511002e30ae54415823f406128b85b238e',
name: 'LSP4Metadata',
value: {
LSP4Metadata: {
name: 'Digital Handbags collection',
description: 'The first digital golden handbags collection.',
links: [
{ title: 'Twitter', url: 'https://twitter.com/goldenhandbags123' },
{ title: 'goldenhandbags.org', url: 'https://goldenhandbags.org' }
],
icon: [ ... ],
images: [ ... ], // list of images thatCOULD be used for LSP8 NFT art
assets: [{
verification: {
method: 'keccak256(bytes)',
data: '0x98fe032f81c43426fbcfb21c780c879667a08e2a65e8ae38027d4d61cdfe6f55',
},
url: 'ifps://QmPJESHbVkPtSaHntNVY5F6JDLW8v69M2d6khXEYGUMn7N',
fileType: 'fbx'
}],
attributes: [
...
]
}
}
*/

The Solidity implementation LSP7 and LSP8 implementation on the @lukso/lsp-smart-contracts package set this data key on deployment.

Deployment parameters for the `LSP7DigitalAsset.sol` implementation in Solidity.
Deployment parameters for the `LSP8IdentifiableDigitalAsset.sol` implementation in Solidity.

You can check our guides in docs.lukso.tech to see how these parameters are passed to your deployment scripts when deploying an LSP7 Token or deploying an LSP8 NFT Collection.

The addition of the LSP4TokenType data key solves the following problem:

Currently, dApps, users and interfaces cannot differentiate if a LSP7 contract is a token or an NFT, and if a LSP8 contract is a NFT or a collection.

In fact, the LSP7 and LSP8 standards can both be used to create complex NFT collections as described below.

  • LSP7 can also be used to create NFTs. where the LSP4Metadata represents the information of a single NFT that has multiple ownable amounts or IDs, and NFTs can be minted in large quantities in a single transaction.
  • Additionally, LSP8 is more useful for physical or NFTs with unique properties per item, where these properties can also evolve.
  • LSP8 can also represent a singular item, with many ownable IDs (e.g. a physical handbag), but the same metadata for each tokenIds.
  • Finally, LSP8 can also be used to create a collection of items, where each item has its own ID, or even smart contract address

The new LSP4TokenType data key helps in defining in which of these categories the LSP7 or LSP8 contract fits, which was less clear before.

You can learn more thanks to our Youtube video guide below!

See also the PR and the LSP4 specs for more details about LSP4TokenType.

Operators get notified via LSP1 Universal Receiver

Similarly to other token standards like ERC20 and ERC721, LSP7 and LSP8 offer functionalities to allow other addresses to spend some of your balance (for LSP7) or transfer some of your NFTs (LSP8) on your behalf. These approved addresses are commonly referred to as operators.

When authorising an operator via the authorizeOperator(…), the operator is always notified. This means that if the operator is a contract that supports the LSP1 interface, its universalReceiver(...) function will be called with a specific LSP1 typeId being the keccak225 hash of:

source: Github, LSP7DigitalAssetCore.sol

When revoking an operator via the revokeOperator(…) function, the operator can be notified via its LSP1 Universal Receiver optionally. This can be done by setting the notify parameter to true.

source: Github, LSP7DigitalAssetCore.sol

The same change applies to LSP8, despite the code snippet showing LSP7.

Events related to operators renamed

For the following events emitted when authorising or revoking operators:

  • From AuthorizedOperator → to OperatorAuthorizationChanged
  • From RevokedOperator → to OperatorRevoked

LSP7 Operators Events before update:

event AuthorizedOperator(
address indexed operator,
address indexed tokenOwner,
uint256 indexed amount,
bytes operatorNotificationData
);
event RevokedOperator( 
address indexed operator,
address indexed tokenOwner,
bool notified,
bytes operatorNotificationData
);

LSP8 Operators Events before udpate:

event AuthorizedOperator( 
address indexed operator,
address indexed tokenOwner,
bytes32 indexed tokenId,
bytes operatorNotificationData
);
event RevokedOperator(
address indexed operator,
address indexed tokenOwner,
bytes32 indexed tokenId,
bool notified,
bytes operatorNotificationData
);

LSP7 Operators Events Now:

event OperatorAuthorizationChanged( 
address indexed operator,
address indexed tokenOwner,
uint256 indexed amount,
bytes operatorNotificationData
);
event OperatorRevoked(
address indexed operator,
address indexed tokenOwner,
bool indexed notified,
bytes operatorNotificationData
);

LSP8 Operators Events now:

event OperatorAuthorizationChanged( 
address indexed operator,
address indexed tokenOwner,
bytes32 indexed tokenId,
bytes operatorNotificationData
);
event OperatorRevoked(
address indexed operator,
address indexed tokenOwner,
bytes32 indexed tokenId,
bool notified,
bytes operatorNotificationData
);

New batchCalls(…) function added to LSP7 + LSP8

Similarly to Universal Profiles, the function batchCalls(bytes[]) was added to the LSP7 and LSP8 standards. This function enables batching calls to multiple functions from the interface into a single transaction.

For instance, it is possible to do things such as; minting a tokenId for an address or authorising an operator for this newly minted tokenId, all in one single transaction.

This can be done by abi-encoding the function calls to these two functions, and passing this array of bytes[] ABI-encoded function calls to the batchCalls(bytes[]) function and calling it.

JSONURL deprecated, LSP4 Metadata now encoded as VerifiableURI

The metadata of LSP7 and LSP8 digital assets — stored under the LSP4Metadata data key — is now encoded and decoded as a VerifiableURI

{
"name": "LSP4Metadata",
"key": "0x9afb95cacc9f95858ec44aa8c3b685511002e30ae54415823f406128b85b238e",
"keyType": "Singleton",
"valueType": "bytes",
"valueContent": "VerifiableURI"
}

This new encoding scheme was introduced over JSONURL (which is now deprecated) as it allows more features over JSONURL. Unlike a JSONURL, a VerifiableURI can contain verification data that can be used to verify the authenticity and integrity of the content linked. For instance, the content can be verified using various verification methods such as content hashing with keccak256 or using ECDSA signature recovery.

For more details, see the LSP2 specification for VerifiableURI.

DataChanged event now emits the full bytes data value parameter

The data value of the DataChanged event was capped to emit the first 256 bytes of the data value; now, the data is not capped and will be fully emitted.

This detail was initially intended to minimise the gas cost when setting metadata in the ERC725Y storage of digital assets when the data to be set was large with many bytes.

However, capping to 256 bytes being emitted in the dataValue parameter of the DataChanged event is very limiting for users who are already storing big amount of data in their ERC725Y storage. Since storing large data size is already costly, the additional gas cost of emitting the full data value is minimal in proportion to the total gas cost of the transaction.

In addition, emitting the full bytes of the data value can be useful for interfaces to detect on the fly the previous data that was set, for instance to track the values that were set under a data key for historical reasons.

Data sent when notifying via LSP1 is now ABI-encoded

Among the new changes in LSP7 and LSP8 is how the notification data (related to LSP1) is encoded when notifying a sender, recipient or operator.

Previously, the data was packed encoded, so the number of bytes sent would be as small as possible. This was changed to be abi-encoded.

The following information is sent when notifying:

  • on token transfers, minting and burning: the operator, sender, recipient, amount (for LSP7) or tokenId (for LSP8) and the additional data parameter sent.
  • when authorising operators: the tokenOwner, the amount (for LSP7) or tokenId (for LSP8), true when authorising, false when revoking (or if the operator balance reached 0 for LSP7), and the bytes operatorNotificationData .
Solidity code of LSP8 that shows how the informations sent to the LSP1 `universalReceiver(…)` function of the sender and recipient (source: Github.com, LSP8IdentifiableDigitalAssetCore.sol)

The main benefit of this change is that it makes it easier for any smart contract implementing LSP1 acting as a sender, recipient or operator, to decode the LSP1 data received.

Solidity code showing how to decode the LSP1 notification data of an LSP7 token transfer.

Changes specific to LSP7

Functions increaseAllowance and decreaseAllowance added to LSP7

These functions were added so that they can be used to mitigate the risk of double spending allowance related to fungible tokens.

Function getOperatorsOf added to LSP7 interface

The function getOperatorsOf(address) was added to the LSP7 interface, so that it enables any token holder to retrieve the list of operators it has allowed to spend allowance on its balance.

Changes specific to LSP8

Setting the tokenId format on deployment

Note: the data key LSP8TokenIdFormat was formerly known as LSP8TokenIdType. It was renamed to avoid confusion with LSP4TokenType.

LSP8TokenIdFormat is a data key defined in LSP8-IdentifiableDigitalAsset to highlight what is the format used for each tokenId.

{
"name": "LSP8TokenIdFormat",
"key": "0xf675e9361af1c1664c1868cfa3eb97672d6b1a513aa5b81dec34c9ee330e818d",
"keyType": "Singleton",
"valueType": "uint256",
"valueContent": "Number"
}

There can be:

  • one format per tokenId (0 to 4)
List of single format per tokenIds (source: LSP8 specs on Github)
  • or mixed formats with tokenIds, meaning a default one, but some tokenId can have a different one (100 to 104)
List of mixed formats per tokenIds (source: LSP8 specs on Github)

These constants can be imported in Javascript from the package as follows:

import { LSP8_TOKEN_ID_FORMAT } from "@lukso/lsp-smart-contracts";

With the following values available, based on the table above:

export const LSP8_TOKEN_ID_FORMAT = {
NUMBER: 0,
STRING: 1,
ADDRESS: 2,
UNIQUE_ID: 3,
HASH: 4,
MIXED_DEFAULT_NUMBER: 100,
MIXED_DEFAULT_STRING: 101,
MIXED_DEFAULT_ADDRESS: 102,
MIXED_DEFAULT_UNIQUE_ID: 103,
MIXED_DEFAULT_HASH: 104,
};

When deploying an LSP8IdentifiableDigitalAsset, the tokenId format must be provided when deploying the contract.

Functions to get/set metadata per tokenId

The new functions get/setDataForTokenId(…) have been added in the interface of the LSP8 standard.

function getDataForTokenId(
bytes32 tokenId,
bytes32 dataKey
) external returns (bytes memory dataValue);
function getDataBatchForTokenIds(
bytes32[] memory tokenIds,
bytes32[] memory dataKeys
) external returns (bytes[] memory dataValues);
function setDataForTokenId(
bytes32 tokenId,
bytes32 dataKey,
bytes memory dataValue
) external;
function setDataBatchForTokenIds(
bytes32[] memory tokenIds,
bytes32[] memory dataKeys,
bytes[] memory dataValues
) external;

Previously in the LSP8 standard, metadata for each NFT (= defined below by their tokenId) could be set using the following data key:

{
"name": "LSP8MetadataTokenURI:<address|uint256|bytes32|string>",
"key": "0x1339e76a390b7b9ec9010000<address|uint256|bytes32|string>",
"keyType": "Mapping",
"valueType": "(bytes4,string)",
"valueContent": "(Bytes4,URI)"
}

This Mapping data key (now deprecated, see further below in the article) presented the problem of the tokenId truncation. When constructing the actual 32 bytes long "key”, only the first 20 bytes of the tokenId would be kept to be concatenated with the left part 0x1339e76a390b7b9ec9010000.

Since tokenId are represented as bytes32, truncating to only the first 20 bytes in the Mapping data key presented the risk of generating the same data key for multiple token IDs, and therefore to have multiple same LSP8MetadataTokenURI that would collide.

This was especially risky if the tokenId were represented as strings (LSP8TokenIdFormat = string, value = 2), and the first 20 characters of the strings were common to each token ID, while the last 12 characters were the unique part of each token ID.

Moreover, the different parts of the bytes32 tokenId could represent different things depending on the NFT collections being created. For instance, the first 10 bytes could signal the overall rarity, the next 10 bytes a sub-category, etc… Setting the metadata for these token IDs using this former method (keeping only the first 20 bytes of the tokenId) would not have been possible.

These new functions setDataForTokenId(...) and setDataBatchForTokenIds(...) solve this problem above. They enable to define a subset of metadata per specific tokenId, while also offering flexible functionalities, such as in one transaction:

  • set multiple dataKeys for a single tokenId.
  • set the same data key (e.g: the LSP4Metadata) for multiple different tokenIds.
  • a combination of the two previous cases.

For more details and code examples, see the section “Setting metadata for one or multiple token IDs” on docs.luks.tech.

New event TokenIdDataChanged

With the new functions listed above, a new event was introduced in LSP8:

source: Github.com, ILSP8IdentifiableDigitalAsset.sol

The TokenIdDataChanged event offers a flexible way to store metadata specific to a particular NFT on-chain while also making this metadata discoverable off-chain.

The TokenIdDataChanged event enables indexers and services listening for events to keep track of which metadata and data keys have been updated for specific token IDs. Additionally, both parameters tokenId and dataKey are indexed , so they can be filtered.

For instance, a service could use these indexed parameters to keep track of the metadata of NFTs being updated. To illustrate:

  • for a single NFT: which data keys have been updated for a specific tokenId, by filtering the tokenId parameter.
  • for multiple NFTs: all the tokenIds that have had a specific data key updated (e.g: LSP4Metadata), by filtering the dataKey parameter.

See “Check if the metadata of a tokenId changed” in docs.lukso.tech for details.

Deprecated data keys: LSP8MetadataTokenId

Finally, the last update specific to LSP8 is the deprecation of the data key, LSP8MetadataTokenId.

To set the metadata specific to a tokenId, the data key LSP4Metadata
can be set for a specific tokenId by passing this tokenId and this data key to the setDataForTokenIf function.

Conclusion, Guides and Useful Links

The full specifications of the LSP7 Digital Asset and LSP8 Identifiable Digital Asset standards can be found under the links below:

You can also check the Learn section on docs.lukso.tech to familiarise yourself with building your LSP7 token or LSP8 NFT contract on LUKSO.

We hope this article will help you build and deploy Tokens and NFT 2.0 collections on the LUKSO blockchain using the LSP7 and LSP8 standards!

If you have any questions or comments on this update, we welcome you to join our Discord and write to us in our Developer channels or browse our Technical Documentation.

--

--

Jean Cvllr
LUKSO

Smart Contract engineer at @LUKSO. Full Stack Developer. https://github.com/CJ42