πΌοΈ 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.
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.
// 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_
.
// 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 asbytes32
instead ofuint256
in ERC721. This allows for more flexible token identification schemes. -
Additional
force
parameter: for themint(...)
andtransfer(...)
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 themint(...)
,transfer(...)
, andburn(...)
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.
- LSP8 metadata is generic: via a flexible data key / value store. It can be set and retrieved via
setData(...)
/setDataBatch(...)
andgetData(...)
/getDataBatch(...)
.
Interact with the Token Contractβ
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 Function | LSP8 Equivalent |
---|---|
name() | const dataKey = keccak256('LSP4TokenName') |
symbol() | const dataKey = keccak256('LSP4TokenSymbol') |
balanceOf(address owner) | balanceOf(address tokenOwner) |
ownerOf(uint256 tokenId) | tokenOwnerOf(bytes32 tokenId) |
approve(address to, uint256 tokenId) | authorizeOperator(π 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(π Function details |
transferFrom( | transfer(π Function details |
safeTransferFrom( | transfer( Set |
No equivalent | revokeOperator(π Function details |
No equivalent | batchCalls(bytes[] memory data) Allows to pass multiple calls into a single transaction. For instance:
|
Eventsβ
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 Event | LSP8 Event |
---|---|
Transfer( | Transfer( |
Approval( | OperatorAuthorizationChanged( |
ApprovalForAll( | No direct equivalent |
No equivalent | OperatorRevoked( |
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:
- ethers
- erc725.js
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
You can also obtain the full schema definition of the LSP4 Metadata from @erc725/erc725.js
library. This library will also help you to encode easily data key value pairs.
import { ERC725 } from '@erc725/erc725.js';
import LSP4Schema from '@erc725/erc725.js/schemas/LSP4DigitalAssetMetadata.json';
const nameKey = LSP4Schema.find((schema) => schema.name == 'LSP4TokenName').key;
const symbolKey = LSP4Schema.find(
(schema) => schema.name == 'LSP4TokenSymbol',
).key;
const nameValue = await token.getData(nameKey); // 0x4c5350382050726f66696c65
const symbolValue = await token.getData(symbolKey); // 0x4c5350382050726f66696c65
const [name, symbol] = ERC725.decodeData([
{
keyName: 'LSP4TokenName',
value: nameValue,
},
{
keyName: 'LSP4TokenSymbol',
value: symbolValue,
},
]);
/**
[
{
name: 'LSP4TokenName',
key: '0xdeba1e292f8ba88238e10ab3c7f88bd4be4fac56cad5194b6ecceaf653468af1',
value: "Cool NFT",
},
{
name: 'LSP4TokenSymbol',
key: '0x2f0a68ab07768e01943a599e73362a0e17a63a72e94dd2e384d2c1d4db932756',
value: "COOL",
},
]
*/
Extended Collection Metadataβ
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.