ERC-3525 Starter Kit: Developer Edition

By Ethan Tsai, Alvis Du, Mike Meng

ERC-3525, proposed by Solv Protocol, is a standard for the Semi-Fungible Token (or SFT) approved by the Ethereum community.

It defines a new type of digital asset characterized by the following key features:

  • Unique ID and expressivity of ERC-721 non-fungible tokens. Compatibility with the ERC-721 token standard.
  • It is fractionalizable, combinable, and computable.
  • It can work like an account and nest other digital assets, including ERC-20 fungible tokens and NFTs, with support for token-to-token transfer.
  • Programmable appearance, functionality, lockup, transfer, etc. Metadata is optimized to support dynamic inputs and more complex financial logic.

These features empower ERC-3525 tokens to become vehicles for advanced digital assets such as popular financial instruments, digital certificates or contracts, Web3 virtual assets, art and collectibles, gaming assets, and tokenized RWA (Real-World Assets).

The ERC-3525 Reference Implementation provides — in the form of an open-source code library and an NPM module package — a great way to learn and innovate around the ERC-3525 technology. By rewriting and extending the reference implementation code, developers can use the reference implementation as a starting point to build new ERC-3525 applications.

This article provides a starter kit for developers who would like to install, configure, and deploy the ERC-3525 Reference Implementation. We also added a demonstration for writing a smart contract using ERC-3525. Though the smart contract itself has no real utility, we do believe the demonstration could inspire developers like you to build more interesting smart contracts using our model.

(The content of this article is based on the ERC-3525 Implementation Reference version 1.1.0, released in December 2022.)

Prerequisites

The ERC-3525 Reference Implementation is based on the Hardhat framework and was developed mainly in Solidity. Therefore, we recommend familiarizing yourself with the following prerequisites before developing on ERC-3525:

  • Basic knowledge of Solidity and EVM (Ethereum Virtual Machine) development;
  • Basic knowledge about Hardhat smart contract development environment.

Of course, the skill of using Hardhat presumes a foundational understanding of JavaScript and TypeScript languages. All demonstrations within this document use TypeScript language, all of which, for experienced developers, could be easily done in JavaScript.

If you aren’t already well-versed with the Hardhat environment, read its official documentation (http://hardhat.org/docs) to get started.

Getting Started

1. Development Environment.

Developing ERC-3525 in the command line environment in macOS or Linux is advisable. If you are a user of Windows, we strongly recommend installing Windows Subsystem for Linux (WSL) before performing the operations in the WSL environment below.

Technically, you may choose to use any code editing tool when you write. We recommend using Visual Studio Code, simply because it has a Solidity add-on created by Nomic Foundation, a team of developers of Hardhat, to improve the efficiency of writing in Hardhat or Solidity.

Note that JavaScript or TypeScript is widely used to write test cases in Hardhat development, and Visual Studio Code itself provides good support for JavaScript and TypeScript.

2. Create a Hardhat TypeScript project.

To create a Hardhat Typescript project, start by preparing the project directory through the following command in the command line environment. The sample project name is erc3525-getting-started:

mkdir erc3525-getting-startedcd erc3525-getting-startednpm init -ynpm install --save-dev hardhat

Enter npx hardhat on the command line (in Mac OS X), and you should see the following:

After selecting “Create a TypeScript project”, Hardhat will prompt several questions, and the reader can select the default option by pressing the Enter key.

After all the selections are completed, the system will automatically start installations and other preparatory tasks. After these tasks are complete, open the directory with Visual Studio Code, and you will see the following project structure:

3. Import and install the ERC-3525 Reference Implementation module package.

Next, install the ERC-3525 reference implementation in the current directory through the npm command:

npm install @solvprotocol/erc-3525@latest

Since we will be using OpenZeppelin’s String library, here we need to install OpenZeppelin, using the following command:

npm install @openzeppelin/contracts@latest

Once the installation is complete, you can open the package.json file to view the relevant information of @solvprotocol/erc-3525. This indicates that the ERC-3525 Reference Implementation module package has been successfully installed.

4. Write a smart contract.

For simplicity, we’ll avoid complex business logic and instead use the simplest application case to explain the code development of ERC-3525. Here, we create a simple token, which has only the basic functionality of ERC-3525. We’ll also create an “exterior,” which displays the token’s internal state through SVG.

Note that a code file Lock.sol was added when creating the Hardhat project. Proceed to delete contract/Lock.sol, as it is not needed for our smart contract. Then, create a new file ERC3525GettingStarted.sol under the directory:

// SPDX-License-Identifier: MITpragma solidity ^0.8.9;
import "@openzeppelin/contracts/utils/Strings.sol";
import "@solvprotocol/erc-3525/ERC3525.sol";
contract ERC3525GettingStarted is ERC3525 {
    using Strings for uint256;
    address public owner;

constructor(address owner_)
    ERC3525("ERC3525GettingStarted", "ERC3525GS", 18) {
        owner = owner_;
    }
   function mint(address to_, uint256 slot_, uint256 amount_) external {
   require(msg.sender == owner,
     "ERC3525GettingStarted: only owner can mint");
   _mint(to_, slot_, amount_);
    }
}

In this code, we’ve created a new smart contract named ERC3525GettingStarted. It is derived from the ERC-3525 Reference Implementation contract, whose constructor directly calls the constructor of the ERC-3525 contract, passing in the contract’s full name, symbol, and decimal places, and assigning a value to the owner. We also added a mint() function to ensure that only the owner can mint this token. The specific casting process is realized by calling _mint() in the ERC-3525 contract, so we’ve reused the Reference Implementation to generate a simple ERC-3525 token contract.

Many basic functions could be easily implemented by calling corresponding functions from the ERC-3525 Reference Implementation so that developers can spend their energy building business logic and more interesting functions.

After the code is completed, execute the following command on the command line to Compile:

npx hardhat compile

5. Write a test case.

One of the benefits of developing smart contracts using the Hardhat environment is the possibility of automated testing. In this section, we’ll demonstrate how to use Hardhat’s testing framework to automate the testing of the ERC3525GettingStarted contract.

The test code is under the test directory, so we’ll first delete test/Lock.ts, and then create a new ERC3525GettingStarted.ts under the directory:

import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";

describe("ERC3525GettingStarted", function () {
// We define a fixture to reuse the same setup in every test.
// We use loadFixture to run this setup once, snapshot that state,
// and reset Hardhat Network to that snapshot in every test.
async function deployGettingStartedFixture() {

// Contracts are deployed using the first signer/account by default
const [owner, otherAccount] = await ethers.getSigners();

const GettingStarted = await ethers.getContractFactory(
"ERC3525GettingStarted");
const gettingStarted = await GettingStarted.deploy(owner.address);

return { gettingStarted, owner, otherAccount };
}

describe("Deployment", function () {
it("Should set the right owner", async function () {
const { gettingStarted, owner } = await loadFixture(
deployGettingStartedFixture);
expect(await gettingStarted.owner()).to.equal(owner.address);
});
});

describe("Mintable", function () {
describe("Validations", function () {
it("Should revert with not owner", async function () {
const { gettingStarted, owner, otherAccount } =
await loadFixture(deployGettingStartedFixture);
const slot = 3525
const value = ethers.utils.parseEther("9.5");
await expect(
gettingStarted.connect(otherAccount)
.mint(owner.address, slot, value))
.to.be.revertedWith(
"ERC3525GettingStarted: only owner can mint"
);
});
});

describe("Mint", function () {
it("Should mint to other account", async function () {
const { gettingStarted, owner, otherAccount } =
await loadFixture(deployGettingStartedFixture);
const slot = 3525
const value = await ethers.utils.parseEther("9.5");

await gettingStarted.mint(otherAccount.address, slot, value);
expect(await gettingStarted["balanceOf(uint256)"](1)).to.eq(value);
expect(await gettingStarted.slotOf(1)).to.eq(slot);
expect(await gettingStarted.ownerOf(1))
.to.eq(otherAccount.address);
});
});
});
});

In the test code above, we’ve written a test fixture and three test cases to verify the owner, authorization to mint, and the mint operation. These use cases follow the standard of writing the test code in Hardhat. Further details which can be found in the Hardhat official documentation are omitted here.

6. Run the test.

To run the test, execute the following command in the project main directory:

npx hardhat test

The execution results are as follows:

This shows that our smart contract successfully passed all three test cases.

7. Adding SVG images.

As a token standard, ERC-3525 was designed to express sophisticated digital financial instruments, and these assets must be computable (i.e. quantitatively fractionalizable) like the ERC-20 tokens. One of the important things that separate ERC-3525s from ERC-20s is visualization. In this sense, ERC-3525 inherits the legacy of highly visualized ERC-721 and is compatible with the latter.

With support for metadata, ERC-3525 can return the resource’s URL through tokenURI, which is inherited from the IERC721Metadata interface or the content data of an image.

The image file of an NFT is usually stored off-chain, whose URL can be accessed through tokenURI. This poses a risk to the NFT owner, in that it’s possible to modify the image associated with the URL without changing the URL itself. To solve this problem, most NFTs use IPFS storage in order to ensure the authenticity and immutability of the image through a hash. Even so, the NFT is still vulnerable to vicious operations such as deleting the image resources stored on IPFS.

As mentioned, the motivation for ERC-3525 is to store, express, and protect the sensitive information associated with a digital financial instrument. Therefore, we recommend using on-chain SVG for the entire presentation layer, by having tokenURI return the SVG snippet instead of a link to the image resource.

Add the following function to the ERC3525GettingStarted contract:

     function tokenURI(uint256 tokenId_) public view virtual override returns (string memory) {
return string(
abi.encodePacked(
'<svg width="600" height="600" xmlns="http://www.w3.org/2000/svg">',
' <g> <title>Layer 1</title>',
' <rect id="svg_1" height="600" width="600" y="0" x="0" stroke="#000" fill="#000000"/>',
' <text xml:space="preserve" text-anchor="start" font-family="Noto Sans JP" font-size="24" id="svg_2" y="340" x="200" stroke-width="0" stroke="#000" fill="#ffffff">TokenId: ',
tokenId_.toString(),
'</text>',
' <text xml:space="preserve" text-anchor="start" font-family="Noto Sans JP" font-size="24" id="svg_3" y="410" x="200" stroke-width="0" stroke="#000" fill="#ffffff">Balance: ',
balanceOf(tokenId_).toString(),
'</text>',
' <text xml:space="preserve" text-anchor="start" font-family="Noto Sans JP" font-size="24" id="svg_3" y="270" x="200" stroke-width="0" stroke="#000" fill="#ffffff">Slot: ',
slotOf(tokenId_).toString(),
'</text>',
' <text xml:space="preserve" text-anchor="start" font-family="Noto Sans JP" font-size="24" id="svg_4" y="160" x="150" stroke-width="0" stroke="#000" fill="#ffffff">ERC3525 GETTING STARTED</text>',
' </g> </svg>'
)
);
}

This will generate an SVG image on a black background that looks like this:

Note that the values of Slot, TokenId, and Balance are directly extracted from the current state of the ERC-3525 token.

8. Deploy to a local node.

To start, modify deploy.ts in the scripts directory as follows:

The Hardhat framework comes with an implementation of the Ethereum local node, which has been optimized for various needs that emerge in the development. We recommend deploying the smart contract to the following node during development and debugging.

import { ethers } from "hardhat";
async function main() {
const [owner] = await ethers.getSigners();
const GettingStarted = await ethers.getContractFactory("ERC3525GettingStarted");
const gettingStarted = await GettingStarted.deploy(owner.address);
gettingStarted.deployed();
console.log(`GettingStarted deployed to ${gettingStarted.address}`);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Then, open a new Terminal and run the hardhat built-in node:

npx hardhat node

The running results are as follows (for simplicity, other accounts are omitted here):

Now, execute the following command in the project main directory:

npx hardhat run --network localhost scripts/deploy.ts

After successful execution, you will see the following results. Take note of the boxed address, which will be used in subsequent interactions.

After the smart contract is deployed, you can interact with it through the Hardhat console. This is an important advantage of the Hardhat node, which greatly simplifies work during the testing and debugging phases. Enter the following command:

npx hardhat console --network localhost

The interactive commands and results are as follows:

~/Sources/erc3525-getting-started$ npx hardhat console --network localhost
Welcome to Node.js v16.18.1.
Type ".help" for more information.
> const GettingStarted=await ethers.getContractFactory("ERC3525GettingStarted")
undefined
> const gettingStarted=await GettingStarted.attach('<Insert the boxed address here>')
undefined
> const [owner, otherAccount] = await ethers.getSigners()
undefined
> await gettingStarted.mint(otherAccount.address, 3525, 10000)
{
hash: '0x94d428b32da7e66e8f0e2d48a37ddb9072dca54013130d95779495e1e443df2c',
...
}

You can enter your own TypeScript code to interact with the smart contract.

9. Deploy the smart contract to the Sepolia testnet.

Once the testing and debugging in the development environment are done, the next step is deploying the smart contract to the testnet. In general, a testnet provides an operating environment equivalent to that of a mainnet, and activities like testing and debugging that take place on a testnet cost no gas fee. Some smart contract functions must be run on the testnet, such as the interaction with Oracle, which is not supported on the virtual node for development. The smart contract in this demonstration is very simple and does not use Oracle. However, as a rule of thumb, always test a smart contract on the testnet before deploying it to the mainnet.

Since Ethereum’s POS upgrade on September 15, 2022, popular testnets such as Ropsten, Rinkeby, Kovan, etc., have been abandoned. Currently, the two primary testnets in use are Goerli and Sepolia. Of these, Goerli is open and suitable for testing complex smart contracts, and Sepolia, newer than Goerli, consists of a set of specified verifier nodes which cannot be joined at will. For this reason, Sepolia is a preferred testnet for decentralized app development and testing, and the one we’ll choose for our smart contract.

To deploy on the Sepolia testnet, you must first apply for an Infura API key through https://www.infura.io/. Once you’re done applying for the API key, read on for the deployment process.

Modify hardhat.config.ts as follows:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: "0.8.17",
networks: {
sepolia: {
url: process.env.SEPOLIA_URL || `https://sepolia.infura.io/v3/${process.env.INFURA_KEY}`,
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
}
};
export default config;

Execute the following command in the Terminal command line to set the Infura API key and the private key:

export INFURA_KEY=<YOUR_INFURA_KEY>; export PRIVATE_KEY=<YOUR_PRIVATE_KEY>;

REMEMBER to replace <YOUR_INFURA_KEY> with the Infura API key you have obtained and replace <YOUR_PRIVATE_KEY> with your private key. For security purposes, we recommend using different private keys for the testnet and for the mainnet.

Testing in Sepolia requires an amount of Sepolia FaucETH, the test tokens. To apply for FaucETH, go to https://faucet.sepolia.dev/.

Next, execute the script for deployment:

npx hardhat run --network sepolia scripts/deploy.ts

Once the execution is successful, you should see the result as follows. The boxed address will be used in the next step.

10. Mint an ERC3525GettingStarted token

Now, we mint an ERC3525GettingStarted token. To do so, we use TypeScript to call the contract function for token casting, a widely used method for Web3 dApp development.

Create a new file mint.ts in the scripts directory in the code as follows:

import { ethers } from "hardhat";
async function main() {
const [owner] = await ethers.getSigners();
const GettingStarted = await ethers.getContractFactory("ERC3525GettingStarted");
const gettingStarted = await GettingStarted.attach('<deploy contract address>');
const tx = await gettingStarted.mint(owner.address, 3525, 20220905);
await tx.wait();
const uri = await gettingStarted.tokenURI(1);
console.log(uri);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Note: Replace <deploy contract address> in the code with the boxed address from the previous step.

Finally, execute the following command:

npx hardhat run --network sepolia scripts/mint.ts

We now have successfully minted an ERC3525GettingStarted token. To confirm, view the token on Sepolia Etherscan (https://sepolia.etherscan.io/). To do so, type the following in the search bar of your browser:

https://sepolia.etherscan.io/address/ <deployment contract address>

Note: Replace <deploy contract address> in the code with the boxed address from the previous step.

Congratulations, you have successfully developed and deployed your first ERC-3525 token, and you may perform various operations on it, like fractionalizing, combining, token-to-token transfer, etc. Give it a shot!

For a complete sample code demonstrated in this article, go to: https://github.com/solv-finance/erc3525-getting-started

What’s Next?

We hope this tutorial has provided you with a basic framework for developing your own ERC-3525 Semi-Fungible Token to serve your unique use. This is just the beginning. From here, you can take on a more advanced journey for ERC-3525 development by delving into the docs below. Our team will continue to publish content that helps developers like you to create with ERC-3525. Stay tuned!

Developers

© 2024 SFT Labs. All Rights Reserved.

了解 SFT Labs 的更多信息

立即订阅以继续阅读并访问完整档案。

Continue reading