Skip to main content

πŸͺ™ Migrate ERC20 to LSP7

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

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

Resources

See the contract overview page for the interface differences between ERC20 and LSP7.

Comparisons​

Solidity code​

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

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

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

contract MyERC20Token is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
// Your constructor logic
}
}

To create an LSP7 token, we should instead import LSP7DigitalAsset from the @lukso/lsp7-contracts package, and replace it in the inheritance.

To deploy an LSP7DigitalAsset 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.
  • if the token isNonDivisible.
LSP7 Token
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.15;

import "@lukso/lsp7-contracts/contracts/LSP7DigitalAsset.sol";

contract MyLSP7Token is LSP7DigitalAsset {
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, // 0 if representing a fungible token, 1 if representing an NFT
bool isNonDivisible // false for decimals equal to 18, true for decimals equal to 0
) LSP7DigitalAsset(name, symbol, tokenOwner, lsp4tokenType, isNonDivisible) {
// _mint(to, amount, 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, 200000, true, "");
}
}

Functions & Behaviors​

Below are the function signatures of the transfer functions for ERC20 and LSP7, respectively.

ERC20

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

LSP7

function transfer(
address from,
address to,
uint256 amount,
bool force,
bytes data
) external;

There are 3 main differences for LSP7 to note

For full compatibility with ERC20 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 LSP7 Standard > force mint and transfer section for more details.

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

For full compatibility with ERC20 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 LSP7 Standard > LSP1 Token Hooks section for more details.

ERC20 metadata is limited to name() and symbol(). LSP7 allows to store any data after deployment without limitations.

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 LSP7DigitalAsset contract, different functions should be called. This is a table comparing the different function definitions:

ERC20 FunctionLSP7 Equivalent
name()
const dataKey = keccak256('LSP4TokenName')
getData(bytes32 dataKey)
symbol()
const dataKey = keccak256('LSP4TokenSymbol')
getData(bytes32 dataKey)
decimals()
decimals()
totalSupply()
totalSupply()
balanceOf(address account)
balanceOf(address account)
allowance(address owner, address spender)
authorizedAmountFor(
address spender,
address owner
)
No equivalent
getOperatorsOf(address owner)
approve(address spender, uint256 amount)
authorizeOperator(
address spender,
uint256 amount,
bytes memory data
);
πŸ” Function details
πŸ”€ Example Transaction
No equivalent
revokeOperator(
address spender,
bytes memory data
)
πŸ” Function details
No equivalent
increaseAllowance(
address spender,
uint256 addedAmount,
bytes memory data
)
πŸ” Function details
No equivalent
decreaseAllowance(
address spender,
uint256 subtractedAmount,
bytes memory data
)
πŸ” Function details
transfer(address to, uint256 amount)
transfer(
address from,
address to,
uint256 amount,
bool force
bytes memory data
)

Pass the msg.sender address as from parameter.

πŸ” Function details
transferFrom(
address from,
address to,
uint256 amount
)
transfer(
address from,
address to,
uint256 amount,
bool force
bytes memory data
)

Use msg.sender as an operator and use a different from address to transfer tokens from.

πŸ” Function details
No equivalent
function transferBatch(
address[] from,
address[] to,
uint256[] amount,
bool[] force,
bytes[] data
);

Transfer the same token to multiple recipients in a single transaction. See our examples code snippets.

πŸ” Function details
No equivalent
batchCalls(bytes[] memory data)

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

  1. Transfer tokens to an address.
  2. Authorize an operator.
  3. Update the token contract 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 LSP7 to listen to activities. The table below shows the different event definitions that should be used to track activity on an LSP7-DigitalAsset contract.

ERC20 EventLSP7 Event
Transfer(
address indexed from,
address indexed to,
uint256 value
);
Transfer(
address indexed operator,
address indexed from,
address indexed to,
uint256 amount,
bool force,
bytes data
)
Approval(
address indexed owner,
address indexed spender,
uint256 value
)
OperatorAuthorizationChanged(
address indexed operator,
address indexed tokenOwner,
uint256 indexed amount,
bytes operatorNotificationData
);
No equivalent
OperatorRevoked(
address indexed operator,
address indexed tokenOwner,
bool indexed notified,
bytes operatorNotificationData
)

Metadata Management​

Basic Token Information​

ERC20

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

How to retrieve?

In ERC20, the name and symbol of a token can be retrieved by calling their own function.

LSP7

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 LSP7, the token name and symbol can be retrieved with getData(bytes32). They are stored in the generic metadata key-value store under the data keys LSP4TokenName and LSP4TokenSymbol.

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); // 0x555020417374726f20373235
const symbolValue = await token.getData(symbolKey); // 0x5550373235

const name = ethers.toUtf8String(nameValue); // UP Astro 725
const symbol = ethers.toUtf8String(symbolValue); // UP725

Extended Token Metadata​

Tutorial πŸŽ₯

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

LSP7 allows for more flexible and extensible metadata. 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 (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: 'The first digital golden pig.',
links: [
{ title: 'Twitter', url: 'https://twitter.com/goldenpig123' },
{ title: 'goldenpig.org', url: 'https://goldenpig.org' }
],
icon: [
{
width: 256,
height: 256,
url: 'ifps://QmW5cF4r9yWeY1gUCtt7c6v3ve7Fzdg8CKvTS96NU9Uiwr',
verification: {
method: 'keccak256(bytes)',
data: '0x01299df007997de92a820c6c2ec1cb2d3f5aa5fc1adf294157de563eba39bb6f',
}
}
],
images: [ // COULD be used for LSP8 NFT art
[
{
width: 1024,
height: 974,
url: 'ifps://QmW4wM4r9yWeY1gUCtt7c6v3ve7Fzdg8CKvTS96NU9Uiwr',
verification: {
method: 'keccak256(bytes)',
data: '0xa9399df007997de92a820c6c2ec1cb2d3f5aa5fc1adf294157de563eba39bb6e',
}
},
... // more image sizes
],
... // more images
],
assets: [{
verification: {
method: 'keccak256(bytes)',
data: '0x98fe032f81c43426fbcfb21c780c879667a08e2a65e8ae38027d4d61cdfe6f55',
},
url: 'ifps://QmPJESHbVkPtSaHntNVY5F6JDLW8v69M2d6khXEYGUMn7N',
fileType: 'fbx'
}],
attributes: [
{
key: 'Standard type',
value: 'LSP',
type: "string"
},
{
key: 'Standard number',
value: 4,
type: "number"
},
{
key: 'πŸ†™',
value: true,
type: "boolean"
}
]
}
}