本記事は Ethereum Advent Calendar 2022 の24日目の記事です。
ERC-3525標準とは
ERC3525 は、Solv Protocolによって提案された、半代替性トークン (Semi Fungible Token、SFT と呼ばれる) 規格です。
ERC3525はERC721の拡張で、NFTの分割やマージが可能です。ですので所有権を分割したりマージしたり(足したり)できる株や金融商品などに適しています。
ERC3525についての概要は省略してさっさと実装していきましょう!詳細の説明はこちらに委ねます。
本ドキュメントについて
このドキュメントはERC-3525をの生みの親である Solv Protocol が公開している記事を参考に作成しています。
(日本語記事がない&実際のドキュメントではエラーが出る箇所があったりしたのでそれらを解消しています。)
このドキュメントは、ERC-3525 の公式リファレンス実装をインストールし、ERC-3525トークンをmintする簡単なコントラクトを開発していきます。このコントラクトに特別な機能はありませんが、開発、テスト、デプロイまで一気通貫して行います。
使用技術
- solidity
- hardhat
- TypeScript
- (Sepolia テスト ネットにデプロイします)
- svg
なおドキュメントの内容は、ERC-3525 リファレンス実装バージョン 1.1.0 (2022 年 12 月リリース) に基づいています。
開発スタート!
開発環境
macOS または Linux のコマンドライン環境での開発を想定しています。Windows を使用している場合は、最初に Windows Subsystem for Linux (WSL) をインストールしてから、WSL 環境で開発することをお勧めします。
またエディタにはVisual Studio Codeを使用しています。
Hardhat TypeScript プロジェクトを作成する
まず、terminalを開いて下記のコマンドを実行します。下記のコマンドではプロジェクト(プロジェクト名:erc3525tutorial)のためのディレクトリを準備し、hardhat開発環境を構成しています。
mkdir erc3525tutorial
npm install hardhat
npx hardhat
そうすると次のような画面が開きますので、Create a TypeScript project
を選択し、後に出てくる質問にはデフォルト設定でよいので enter を押し続けてください!
すべての選択が完了した後、Visual Studio Code でディレクトリを開くと次のようなプロジェクト構造になっているかと思います。
ERC-3525 リファレンス実装 パッケージをインストールする
次に、npm コマンドを使用して、ERC-3525 リファレンス実装を現在のディレクトリにインストールします。
npm install @solvprotocol/erc-3525@latest
OpenZeppelin の String ライブラリを使用するため、次のコマンドで OpenZeppelin をインストールしておきましょう。
npm install @openzeppelin/contracts@latest
インストールが完了したら、package.json ファイルを開いて、dependencies
に@solvprotocol/erc-3525
が表示されていれば正常にインストールされています!
スマートコントラクトの作成
ERC-3525 のコード開発プロセスを体験することを主軸に置いているため、ERC-3525 トークンを作成するとても単純なコントラクトになっています。後程 SVG を使用してトークンの作成していくのでお楽しみに!
Hardhat プロジェクトの作成中に、サンプル コードとしてLock.sol
が自動的に追加されます。今回このファイルは必要ありません。まず、contracts/Lock.sol を削除してから、contracts 配下に新しいファイル ERC3525Tutorial.sol を作成しましょう。コードは次のとおりです。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/utils/Strings.sol";
import "@solvprotocol/erc-3525/ERC3525.sol";
contract ERC3525Tutorial is ERC3525 {
using Strings for uint256;
address public owner;
constructor() ERC3525("ERC3525Tutorial", "ERC3525T", 18) {
owner = msg.sender;
}
function mint(
address to_,
uint256 slot_,
uint256 amount_
) external {
require(msg.sender == owner, "ERC3525Tutorial: only owner can mint");
_mint(to_, slot_, amount_);
}
}
コンストラクタ
上記のコードでは、新しいコントラクトERC3525Tutorial.solを作成します。このコントラクトは ERC3525リファレンス実装コントラクトを継承しています。コンストラタはERC3525コントラクトのコンストラクタを直接呼び出し、コントラクトの名前、シンボル、および小数点以下の桁数を渡し、所有者に msg.sender を割り当てます。
constructor(string memory name_, string memory symbol_, uint8 decimals_) {
_name = name_;
_symbol = symbol_;
_decimals = decimals_;
}
mint関数
ERC3525Tutorialでは、mint() 関数を追加して、所有者のみがこの ERC-3525 トークンを作成できるようにしています。実際のmintのプロセスは、ERC3525コントラクトで _mint() を呼び出すことによって実現されています。
ERC-3525のリファレンス実装によって、対応する関数を呼び出すことで多くの基本機能を直接実装できるので、開発者は自身のプロジェクトのロジックと機能にのみ集中できますね!
ERC-3525リファレンスの_mint関数実装について
とは言っても_mint()の中身も気になると思いますので簡単に説明します。
Visual Studio Codeをお使いでしたら Command + クリック
で定義に飛ぶと_mint関数は2つあります。
_mint(address to_, uint256 slot_, uint256 value_)
_mint(address to_, uint256 tokenId_, uint256 slot_, uint256 value_)
今回の実装では引数を3つ(to_, slot_, amount_)渡しているので前者の方が呼ばれています。以下コメントにて簡単に説明します。
// 初めに呼ばれるmint関数
function _mint(address to_, uint256 slot_, uint256 value_) internal virtual returns (uint256) {
// _createOriginalTokenId()でインクリメントしたtokenIdを代入
// 1 -> 2 -> 3...
uint256 tokenId = _createOriginalTokenId();
// 新しいtokenIdを引数に渡し、実際のmintの挙動をするmint関数を実行
_mint(to_, tokenId, slot_, value_);
// 戻り値としてtokenIdを返す
return tokenId;
}
// 引数3つの_mint関数からtokenIdを引数に受け取る
function _mint(address to_, uint256 tokenId_, uint256 slot_, uint256 value_) internal virtual {
// address(0)にはmintしない
require(to_ != address(0), "ERC3525: mint to the zero address");
// tokenIdは0以外
require(tokenId_ != 0, "ERC3525: cannot mint zero tokenId");
// tokenIdは新しいものでなければならない
require(!_exists(tokenId_), "ERC3525: token already minted");
// _beforeValueTransfer関数をオーバーライドすればmint前に処理の追加が可能
_beforeValueTransfer(address(0), to_, 0, tokenId_, slot_, value_);
// マッピングや構造体にtokenIdやowner情報などデータを追加していく
__mintToken(to_, tokenId_, slot_);
// _allTokensの構造体配列の"balance"に量を追加
__mintValue(tokenId_, value_);
// _afterValueTransfer関数をオーバーライドすればmint後に処理の追加が可能
_afterValueTransfer(address(0), to_, 0, tokenId_, slot_, value_);
}
コンパイル
コードを記述したら、コマンド ラインで次のコマンドを実行してコンパイルします。
npx hardhat compile
コンパイルが成功した結果は次のとおりです。
コンパイルがうまくいかない場合は下記を実行して再度コンパイルを試してください。
npm install @nomicfoundation/hardhat-toolbox
テストケースを書く
Hardhat フレームワークを使用してスマート コントラクトを開発する主な利点の 1 つは、自動テストを可能にしてくれることです。以下に、Hardhat のテスト フレームワークを使用して ERC3525Tutorial コントラクトのテストを自動化する方法を紹介します。
テストコードはtestディレクトリに配置します。スマートコントラクト作成した時と同様に、test/Lock.ts を削除してから、test ディレクトリ配下に ERC3525Tutorial.ts を作成します。コードは次のとおりです。
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";
// 全テストで共通する初期処理を実行する
describe("ERC3525Tutorial", function () {
// Fixtureを使用すると関数実行後の状態を保存しておいてすぐに再利用可能。
//そのため、コントラクトのデプロイやアドレスの用意はFixtureで行うことで後続するテストの実行時間を短縮できる
async function deployERC3525TutorialFixture() {
const [owner, otherAccount] = await ethers.getSigners();
const ERC3525Tutorial = await ethers.getContractFactory(
"ERC3525Tutorial");
const erc3525tutorial = await ERC3525Tutorial.deploy();
return { erc3525tutorial, owner, otherAccount };
}
// deployテスト
describe("Deployment", function () {
it("Should set the right owner", async function () {
// loadFixtureでdeployERC3525TutorialFixture()後の状態を取得する。
const { erc3525tutorial, owner } = await loadFixture(
deployERC3525TutorialFixture);
expect(await erc3525tutorial.owner()).to.equal(owner.address);
});
});
// mintテスト① コントラクトのownerのみmint可能になっているか
describe("Mintable", function () {
describe("Validations", function () {
it("Should revert with not owner", async function () {
const { erc3525tutorial, owner, otherAccount } =
await loadFixture(deployERC3525TutorialFixture);
const slot = 3525
const value = ethers.utils.parseEther("9.5");
await expect(
erc3525tutorial.connect(otherAccount)
.mint(owner.address, slot, value))
.to.be.revertedWith(
"ERC3525Tutorial: only owner can mint"
);
});
});
// mintテスト② 想定しているmintの挙動になっているか
describe("Mint", function () {
it("Should mint to other account", async function () {
const { erc3525tutorial, owner, otherAccount } =
await loadFixture(deployERC3525TutorialFixture);
const slot = 3525
const value = await ethers.utils.parseEther("9.5");
await erc3525tutorial.mint(otherAccount.address, slot, value);
expect(await erc3525tutorial["balanceOf(uint256)"](1)).to.eq(value);
expect(await erc3525tutorial.slotOf(1)).to.eq(slot);
expect(await erc3525tutorial.ownerOf(1))
.to.eq(otherAccount.address);
});
});
});
});
上記のテストコードでは、Fixtureと3 つのテストケースを記述しています。
テストFixture
Fixtureを使用すると関数実行後の状態を保存しておいてすぐに再利用することができます。
そのため、コントラクトのデプロイやアドレスの用意はFixtureで行うことでテスト時間を短縮できます。下記のようにすることでdeployERC3525TutorialFixture()後の状態を取得できます。
const { erc3525tutorial, owner } = await loadFixture(deployERC3525TutorialFixture);
テストケース
- デプロイ時のownerの正確性
- mintの実行権限
- mintの実行機能
テストを実行する
では実際にテストを実行してみましょう。プロジェクトのメインディレクトリで次のコマンドを実行してください!
npx hardhat test
実行結果は次のとおりです。
3 つのテストケースすべてにパスしたことが分かりますね!
SVG 画像の追加
ERC-3525 はもともと、複雑な金融資産、特にデジタル商品を表現するために設計されました。デジタル資産であるため、分割とマージをサポートし、ERC-20トークンのようなさまざまな数学的計算を実行できる必要があります。一方、ERC-3525がERC-20の拡張ではない重要な点は、視覚的なイメージを持っていることであり、それによって豊富な情報をユーザーに伝え、複雑なデジタル資産も表現することができます。これが、ERC-3525 が ERC-721 の拡張であり、互換性がある大きな理由です。
したがって、ERC-3525 はメタデータをサポートし、IERC721Metadata インターフェイスから継承された tokenURI 関数を介してリソースの URL を返すか、画像のコンテンツデータを直接返します。NFT では、画像をオフチェーンに置き、tokenURI 関数がその URL を返すようにするのが一般的です。この設計には、実際には大きなリスクもあります。NFT が販売された後、ストレージを管理する人物が URL の画像を変更できるため、購入者の手にある NFT が改ざんされる可能性があります。この問題を解決するために、IPFS ストレージを使用して、ハッシュ値によって画像リソースの一意性を保証します。それでも、IPFS上に保存された画像リソースを削除するなど、一部の被害を防ぐことは困難です。
ERC-3525 の本来の意図は、金融資産を表現することであり、金融資産の情報は非常に機密性が高く重要であり、置き換えたり削除したりということがあってはなりません。そのため、Solv Protocol では SVGで直接表現し、オンチェーンに直接配置することを推奨しています。つまり、tokenURI 関数がリソースへのリンクではなく、SVG コードを直接返すようにします。
次の関数を ERC3525Tutorialコントラクトに追加します。
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="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_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_4" y="160" x="150" stroke-width="0" stroke="#000" fill="#ffffff">ERC3525 Tutorial</text>',
" </g> </svg>"
)
);
}
これにより、次のような黒い背景の SVG 画像が生成されます。
Slot、TokenId、および Balance の値は、ERC-3525 トークンの現在の状態から直接抽出されます。画像については移行のSepoliaテストネットでmintした後SVGを表示させるためのツールを紹介します。
ローカル ノードにデプロイする
Hardhat を使用し、ローカルノードにデプロイしテストデバックをしていきます。
scripts ディレクトリの deploy.ts を次のように変更します。
import { ethers } from "hardhat";
async function main() {
const ERC3525Tutorial = await ethers.getContractFactory("ERC3525Tutorial");
const erc3525tutorial = await ERC3525Tutorial.deploy();
erc3525tutorial.deployed();
console.log(`ERC3525Tutorial deployed to ${erc3525tutorial.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
新しいターミナルを開き、hardhat でノードを実行します
npx hardhat node
実行結果は次のとおりです (実際はAccount #19まであります)。
上記ノードは繋いだまま、新しいターミナルを開き次のコマンドを実行します。
npx hardhat run --network localhost scripts/deploy.ts
実行が成功すると、次の結果が表示されます。赤いボックスのaddress部分に注意してください。これは、以降の対話で使用されます。
スマートコントラクトがデプロイされた後は、Hardhatコンソール コマンドを介してやり取りできます。これはHardhatノードの利点であり、テストおよびデバッグフェーズでの作業を大幅に簡素化できます。次のコマンドを入力します。
npx hardhat console --network localhost
インタラクティブなコマンドと結果は次のとおりです。
mameta erc3525tutorial %npx hardhat console --network localhost
Welcome to Node.js v16.17.0.
Type ".help" for more information.
> const ERC3525Tutorial = await ethers.getContractFactory("ERC3525Tutorial")
undefined
> const erc3525tutorial = await ERC3525Tutorial.attach('0x5FbDB2315678afecb367f032d93F642f64180aa3')
undefined
> const [owner, otherAccount] = await ethers.getSigners()
undefined
> await erc3525tutorial.mint(otherAccount.address, 3525, 10000)
{
hash: '0x6632323d9a2a3776464935fe292fcd5e2779e29fd36c19e969eb2c93142283fe',
type: 0,
accessList: null,
blockHash: '0x6cbd209c9c3714a0036c52d76c76c87f482d9e466f9c4f10c0b7ebde1bbd63d4',
blockNumber: 2,
transactionIndex: 0,
confirmations: 1,
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
gasPrice: BigNumber { value: "1801910689" },
gasLimit: BigNumber { value: "220177" },
to: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
value: BigNumber { value: "0" },
nonce: 1,
data: '0x156e29f600000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c80000000000000000000000000000000000000000000000000000000000000dc50000000000000000000000000000000000000000000000000000000000002710',
r: '0x5285341f8eefdf3c2e3d4e150996be1a6e3d33af181fd9316b027745299bcf4b',
s: '0x7c83994b6b5b5519e0cdbe59a241f496da7de21e58cfd9d762b5b82bc5eeb0c8',
v: 62710,
creates: null,
chainId: 31337,
wait: [Function (anonymous)]
}
スマートコントラクトとのやり取りをができるのって楽しいですね!
Sepolia テストネットワークにデプロイする
開発環境でのテストとデバッグが終わったので、次はテストネットにデプロイしていきましょう!テストネットは、メインネットと基本的に同等の動作環境ですが、テストのためにガス代を支払う必要がありません。
一方、開発用のHardhatなどの仮想ノードではサポートされていない、Oracle とのやり取りなど、一部のスマートコントラクト機能はテストネットで実行する必要があります。今回のケースではとてもシンプルな開発のため Oracle を使用していませんが、原則として、スマートコントラクトをメインネットにアップロードする前に、テストネットで正しくテストしておきましょう。
Ethereum は 2022 年 9 月 15 日に PoS にアップグレードされたため、Ropsten、Rinkeby、Kovan などのテストネットは使えなくなりました。現在、2 つの主要なテストネットは Goerli と Sepolia です。Goerli は長い歴史があり、完全にオープンであり、複雑なスマートコントラクトのテストに適しています。Sepolia は比較的新しく、自由に参加できない一連の決定された検証ノードで構成されています。この例では、Sepolia テストネットを使用していきいます。
Sepolia テストネットにデプロイするには、https://www.infura.io/からinfura API KEY を申請する必要があります。この作業を行っていることを想定して勧めます。infura API KEYの取得方法についてはこちらを参考になさってください。
hardhat.config.ts を次のように変更します。
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
require('dotenv').config();
/** @type import('hardhat/config').HardhatUserConfig */
const {
SEPOLIA_URL,
PRIVATE_KEY,
INFURA_KEY,
} = process.env;
const config: HardhatUserConfig = {
solidity: "0.8.17",
networks: {
sepolia: {
url: SEPOLIA_URL || `https://sepolia.infura.io/v3/${INFURA_KEY}`,
accounts:
PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
},
}
};
export default config;
次に、プロジェクトディレクトリに .env ファイルを作成し、下記のように記入します。黄色い部分で隠している箇所はご自身で取得した値を記載してください。
- SEPOLIA_URL -> 上記 infuraで取得したAPI KEY含めた”https://~”から始まるURL
- INFURA_KEY -> 上記 infuraで取得したAPI KEY
- PRIVATE_KEY -> ご自身の開発用の秘密鍵
Sepolia テスト ネットワークでのテストのため、Sepolia Faucetでテストネット用のEtherを取得しましょう。こちらから取得できます。
これらの準備が完了したら、デプロイのために書きのスクリプトを実行します。
npx hardhat run --network sepolia scripts/deploy.ts
正常に実行された後、結果は次のようになります。黄色いボックス内にこのコントラクトのアドレスが表示されています。こちら次のステップで使用していきます。
ERC3525Tutorial トークンの発行
次に、ERC3525Tutorial トークンを作成しましょう。私たちが採用した方法は、TypeScript を使用してトークン キャストのコントラクト関数を呼び出すことです。これは、Web3 DApp 開発のパターンと一致しています。
最初にスクリプト ディレクトリに新しいファイル mint.ts を作成します。コードは次のとおりです。
import { ethers } from "hardhat";
async function main() {
const [owner] = await ethers.getSigners();
const ERC3525Tutorial = await ethers.getContractFactory("ERC3525Tutorial");
const erc3525tutorial = await ERC3525Tutorial.attach('<デプロイしたコントラクトアドレス>');
const tx = await erc3525tutorial.mint(owner.address, 3525, 1000);
await tx.wait();
const uri = await erc3525tutorial.tokenURI(1);
console.log(uri);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
コード内の <デプロイしたコントラクトアドレス>
を、前のセクションの黄色いボックス内のアドレスに置き換えてください。
最後に、次のコマンドを実行します。
npx hardhat run --network sepolia src/mint.ts
このようにして、ERC3525Tutorialトークンの作成に成功しました。
Sepolia Etherscan ( https://sepolia.etherscan.io/ ) にアクセスして、コントラクトアドレスで検索することで作成されたトークンを確認できます!
まとめ
すべてがうまくいけば、最初のERC-3525 トークンの開発に成功し、分割やマージなど、さまざまな新しい操作を実行できます。ぜひ試してみてください!(こちら私もおいおい公開できたらと思います!)
この記事の完全なサンプル コードは、GitHub ( https://github.com/Mameta29/erc3525Tutorial ) にあります。
おまけ① ~mintしたトークンのsvgを表示してみよう~
せっかくmintできたトークン、見たいですよね!SVGの表示の仕方(Mac用)について簡単に説明します。
mintする際にコンソールでターミナルにSVGが出力されます。(または”very”することで Sepolia Etherscan ( https://sepolia.etherscan.io/ ) にアクセスし、tokenURIから確認することも可能です。)
- SVGを任意の名前でファイルとして保存(sample.svg など)
- GapplinというSVGビューアを使用することでどのような画像になっているかを確認することができます。
手順など詳しくはこちらを参考になさってください!
https://pikawaka.com/tips/gapplin
おまけ②~ERC-3525開発のための次のステップ~
このチュートリアルでは、ERC-3525 半代替性トークン (SFT) のアプリケーション開発の始めのプロセスを簡単に説明しました。より実用的なERC-3525の開発の一助になれば嬉しいです。ERC-3525 の知識と開発技術についてさらに学びたい場合は、以下のサイトを参考にしてみてください。
- ERC-3525 ホワイト ペーパーを読む ( https://whitepaper.sftlabs.io/SFT%20Whitepaper.pdf )
- ERC-3525 リファレンス実装の研究 ( https://github.com/solv-finance/erc-3525 )
- SFTLabs が公式に提供する Showroom ケース ( https://showroom.sftlabs.io/showroom/ )
- ERC-3525 の技術専門家によって開発されたCrypto Notes ( https://cryptonotes.fun/ )
また、ERC-3525 の開発とその手法についての情報についてのアウトプットを引き続き発行します。