Skip to main content

πŸ–ΌοΈ Migrate ERC721 to LSP8

πŸ‘‡πŸ» Hands on πŸ“½οΈ Solidity Workshop Video for the Oxford Blockchain Society from March 2024.

LSP8IdentifiableDigitalAsset is a new token standard that offers a wider range of functionality compared to ERC721, as described in the standard section. For migrating from ERC721 to LSP8, developers need to be aware of several key differences.

Resources

See the contract overview page for the interface differences between ERC721 and LSP8.

Comparisons​

Solidity code​

Usually, to create an ERC721 token, we import and inherit the ERC721 contract from the @openzeppelin/contracts package.

ERC721 Token
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract MyERC721Token is ERC721 {
constructor(string memory name, string memory symbol) ERC721(name, symbol) {
// Your constructor logic
}
}

To create an LSP8 token, we should instead import LSP8IdentifiableDigitalAsset from the @lukso/lsp8-contracts package, and replace it in the inheritance.

To deploy an LSP8IdentifiableDigitalAsset we can use the same constructor parameters, but we also need to specify 2 extra parameters (explanations of params provided in the code comments below).

  • the lsp4TokenType_.
  • the lsp8TokenIdFormat_.
LSP8 Token
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.15;

import "@lukso/lsp8-contracts/contracts/LSP8IdentifiableDigitalAsset.sol";

contract MyLSP8Token is LSP8IdentifiableDigitalAsset {
constructor(
string memory name, // Name of the token
string memory symbol, // Symbol of the token
address tokenOwner, // Owner able to add extensions and change metadata
uint256 lsp4TokenType_, // 1 if NFT, 2 if an advanced collection of multiple NFTs
uint256 lsp8TokenIdFormat_ // 0 for compatibility with ERC721, check LSP8 specs for other values
) LSP8IdentifiableDigitalAsset(name, symbol, tokenOwner, lsp4TokenType_, lsp8TokenIdFormat_) {
// _mint(to, tokenId, force, data)
// force: should be set to true to allow EOA to receive tokens
// data: only relevant if the `to` is a smart contract supporting LSP1.
_mint(tokenOwner, bytes32(uint256(1)), true, "");
}
}

Functions & Behaviors​

Below are the function signatures of the transfer functions for ERC721 and LSP8, respectively.

ERC721

function transferFrom(
address from,
address to,
uint256 tokenId
) external;

LSP8

function transfer(
address from,
address to,
bytes32 tokenId,
bool force,
bytes data
) external;

There are 4 main differences for LSP8 to note:

  • TokenId representation: In LSP8, the tokenId is represented as bytes32 instead of uint256 in ERC721. This allows for more flexible token identification schemes.

  • Additional force parameter: for the mint(...) and transfer(...) functions.

For full compatibility with ERC721 behavior (where the recipient can be any address), set this to true. Setting it to false will only allow the transfer to smart contract addresses supporting the LSP1UniversalReceiver interfaceId.

See the LSP8 Standard > force mint and transfer section for more details.

  • Additional data field: for the mint(...), transfer(...), and burn(...) functions.

For full compatibility with ERC721 behavior, set this to empty bytes "". This data is only relevant when the recipient is a smart contract that supports the LSP1 interfaceId, where the data will be sent and the recipient can act on it (e.g., reject the transfer, forward the tokens to a vault, etc...).

See the LSP8 Standard > LSP1 Token Hooks section for more details.

Interact with the Token Contract​

info

To check function definitions and explanations of behavior and each parameter, check API Reference section.

To interact with the LSP8IdentifiableDigitalAsset contract, different functions should be called. This is a table comparing the different function definitions:

ERC721 FunctionLSP8 Equivalent
name()
const dataKey = keccak256('LSP4TokenName')
getData(bytes32 dataKey)
symbol()
const dataKey = keccak256('LSP4TokenSymbol')
getData(bytes32 dataKey)
balanceOf(address owner)
balanceOf(address tokenOwner)
ownerOf(uint256 tokenId)
tokenOwnerOf(bytes32 tokenId)
approve(address to, uint256 tokenId)
authorizeOperator(
address operator,
bytes32 tokenId,
bytes memory data
)
πŸ” Function details
getApproved(uint256 tokenId)
getOperatorsOf(bytes32 tokenId)
πŸ” Function details
setApprovalForAll(address operator, bool approved)
No direct equivalent, use authorizeOperator for each token
isApprovedForAll(address owner, address operator)
isOperatorFor(
address operator,
bytes32 tokenId
)
πŸ” Function details
transferFrom(
address from,
address to,
uint256 tokenId
)
transfer(
address from,
address to,
bytes32 tokenId,
bool force,
bytes memory data
)
πŸ” Function details
safeTransferFrom(
address from,
address to,
uint256 tokenId
)
transfer(
address from,
address to,
bytes32 tokenId,
bool force,
bytes memory data
)

Set force = false for safe transfer behavior

πŸ” Function details
No equivalent
revokeOperator(
address operator,
bytes32 tokenId,
bool notify,
bytes memory data
)
πŸ” Function details
No equivalent
batchCalls(bytes[] memory data)

Allows to pass multiple calls into a single transaction. For instance:

  1. Transfer an NFT to an address.
  2. Authorize an operator for a specific tokenId.
  3. Update the NFT metadata.
  4. etc...

πŸ” Function details

Events​

info

To check event definitions and explanations of behavior and each parameter, check API Reference section.

Services like dApps and Indexers can use different events from LSP8 to listen to activities. The table below shows the different event definitions that should be used to track activity on an LSP8-IdentifiableDigitalAsset contract.

ERC721 EventLSP8 Event
Transfer(
address indexed from,
address indexed to,
uint256 indexed tokenId
);
Transfer(
address operator,
address indexed from,
address indexed to,
bytes32 indexed tokenId,
bool force,
bytes data
)
Approval(
address indexed owner,
address indexed approved,
uint256 indexed tokenId
)
OperatorAuthorizationChanged(
address indexed operator,
address indexed tokenOwner,
bytes32 indexed tokenId,
bytes operatorNotificationData
);
ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
)
No direct equivalent
No equivalent
OperatorRevoked(
address indexed operator,
address indexed tokenOwner,
bytes32 indexed tokenId,
bool notified,
bytes operatorNotificationData
)

Metadata Management​

Basic Token Information​

ERC721

const name = await token.name();
const symbol = await token.symbol();
const tokenURI = await token.tokenURI(tokenId);

How to retrieve?

In ERC721, the name, symbol, and tokenURI of a token can be retrieved by calling their own functions.

LSP8

import { keccak256, toUtf8Bytes } from 'ethers';

const nameKey = keccak256(toUtf8Bytes('LSP4TokenName'));
const symbolKey = keccak256(toUtf8Bytes('LSP4TokenSymbol'));

const nameValue = await token.getData(nameKey);
const symbolValue = await token.getData(symbolKey);

const name = ethers.toUtf8String(nameValue);
const symbol = ethers.toUtf8String(symbolValue);

How to retrieve?

In LSP8, the token name, symbol and base URI can be retrieved with getData(bytes32). They are stored in the generic metadata key-value store under the data keys LSP4TokenName, LSP4TokenSymbol and LSP8TokenMetadataBaseURI.

Once you have fetched the raw hex encoded value, you will need to decode it into a human readable string.

You can import the list of data keys related to each individual LSP standard from one of our library. There are 2 options:

For dApp developers, you can import the data keys from the @lukso/lsp-smart-contracts and use them directly in your scripts via ethers.js or web3.js.

import { ERC725YDataKeys } from '@lukso/lsp-smart-contracts';

const nameKey = ERC725YDataKeys.LSP4.LSP4TokenName;
const symbolKey = ERC725YDataKeys.LSP4.LSP4TokenSymbol;

const nameValue = await token.getData(nameKey); // 0x4c5350382050726f66696c65
const symbolValue = await token.getData(symbolKey); // 0x4c5350382050726f66696c65

const name = ethers.toUtf8String(nameValue); // Cool NFT
const symbol = ethers.toUtf8String(symbolValue); // COOL

Extended Collection Metadata​

Tutorial πŸŽ₯

See the Metadata Management guide + video for how to create and set the JSON metadata for your LSP8 Token.

LSP8 allows for more flexible and extensible metadata. You can store a JSON object containing information about:

  • the whole NFT Collection contract
  • and for each individual NFT tokenId.

The LSP4Metadata is a JSON object that can contain many information about the token, including:

  • 🌐 official link to websites (e.g: project website, social medias, community channels, etc...).
  • πŸ–ΌοΈ images (token icon and backgrounds) to display the token in dApps, explorers, or decentralised exchanges.
  • 🏷️ custom attributes (for each specific NFTs for instance, can be displayed as badges on UIs).
const metadataKey = ethers.keccak256(ethers.toUtf8Bytes('LSP4Metadata'));
const storedMetadata = await token.getData(metadataKey);
const retrievedJsonMetadata = JSON.parse(ethers.toUtf8String(storedMetadata));

// JSON Stored:

{
LSP4Metadata: {
description: 'A unique digital artwork collection.',
links: [
{ title: 'Website', url: 'https://myawesomenft.com' },
{ title: 'Twitter', url: 'https://twitter.com/myawesomenft' }
],
icon: [
{
width: 256,
height: 256,
url: 'ipfs://QmW5cF4r9yWeY1gUCtt7c6v3ve7Fzdg8CKvTS96NU9Uiwr',
verification: {
method: 'keccak256(bytes)',
data: '0x01299df007997de92a820c6c2ec1cb2d3f5aa5fc1adf294157de563eba39bb6f',
}
}
],
images: [
[
{
width: 1024,
height: 974,
url: 'ipfs://QmW4wM4r9yWeY1gUCtt7c6v3ve7Fzdg8CKvTS96NU9Uiwr',
verification: {
method: 'keccak256(bytes)',
data: '0xa9399df007997de92a820c6c2ec1cb2d3f5aa5fc1adf294157de563eba39bb6e',
}
},
// ... more image sizes
],
// ... more images
],
assets: [{
verification: {
method: 'keccak256(bytes)',
data: '0x98fe032f81c43426fbcfb21c780c879667a08e2a65e8ae38027d4d61cdfe6f55',
},
url: 'ipfs://QmPJESHbVkPtSaHntNVY5F6JDLW8v69M2d6khXEYGUMn7N',
fileType: 'json'
}],
attributes: [
{
key: 'Artist',
value: 'Jane Doe',
type: "string"
},
{
key: 'Edition',
value: 1,
type: "number"
},
{
key: 'Original',
value: true,
type: "boolean"
}
]
}
}

NFT-specific Metadata​

LSP8 allows you to set and retrieve metadata for individual tokens using the setDataForTokenId(...) and getDataForTokenId(...) functions. This is particularly useful for NFTs where each token might have unique properties.

// Setting token-specific metadata
const tokenId = '0x1234...'; // your token ID in bytes32 format
const metadataKey = ethers.keccak256(ethers.toUtf8Bytes('LSP4Metadata'));
const metadataValue = ethers.toUtf8Bytes(JSON.stringify({
// Your token-specific metadata here
}));

await token.setDataForTokenId(tokenId, metadataKey, metadataValue);

// Retrieving token-specific metadata
const storedMetadata = await token.getDataForTokenId(tokenId, metadataKey);
const retrievedJsonMetadata = JSON.parse(ethers.toUtf8String(storedMetadata));

// Example of token-specific metadata
{
LSP4Metadata: {
description: 'Unique NFT #1234',
image: 'ipfs://QmYourImageCID',
attributes: [
{
trait_type: 'Rarity',
value: 'Legendary'
},
{
trait_type: 'Power Level',
value: 9000
}
]
}
}

This feature allows for much more flexible and dynamic NFTs compared to the static tokenURI approach in ERC721.