# ERC3525 半同質化代幣 概念與技術實作 # 前言 ERC3525 是一個介於 ERC721 跟 ERC20 之間的新代幣模式,既有著同種 token 可紀錄 value 的同質性,同時也有著每種 token 都有一個 tokenId 的非同質性,主要目前被認為用來做債券、支票等等之類的金融商品,ERC-3525 可以用 tokenId 來保證每個 NFT 資產都有獨特的編號,而 slot 和 value 的引入,則讓擁有相同業務規則的資產具備了互相轉賬和計算的可能 ERC3525 簡稱為**SFT(Semi-Fungible Token)**,而 SFT 主要由三種屬性構成 - **TokenId** 用來表示該 tokenId 的唯一性,可以簡單理解為 ERC721 的 tokenId,每個 token 都會有自己的 ID,可以藉由這個 ID 找出相關的資訊,例如擁有者是誰之類的,跟 NFT 相似 - **Slot** 用來表示 token 的**種類**,這也是 SFT 可以有同質性的地方所在,因為擁有相同 Slot 的 token 之間可以轉移 value,也就可以做到轉帳、結算等等功能。 - **Value** 用來表示 token 的數量,跟 ERC20`balanceOf()`的效果一樣,用來記錄你擁有多少個 #### 這邊有一個 SFT 的應用例子 ![](https://www.rayskyinvest.com/wp-content/uploads/2022/09/13_0Pz32LUljEI66O1rk.png) > Ref: [深入 ERC-3525:新協議的契機、現狀與未來](https://www.rayskyinvest.com/76531/erc3525-sft) #### 擁有的關係如下 ![](https://lh4.googleusercontent.com/duoPrtRFcFbG_kxZKFNLN-pO9ryMRIAw2j3WzWPjcKdGO4f-3VLXv_R9kf1BsIHAroK7OX8IMqNH6OKCZdJYOGximT1pV2UfPLzzncwVg-VZLEs8_MKNK3mrqtlPhz57CGzX-ZqRaKckm1Ol-vIvlHu45t4DSeruLup1xe7JeVO4GMKBAt1xCZ5dJA) > Ref: [ID-SLOT-VALUE - Solv Documentation](https://docs.solv.finance/solv-documentation/ERC-3525-SFT/core-mechanisms/id-slot-units) 從這張圖可以看到,SFT 是**address 直接擁有 tokenId**,這部分跟 ERC721 一樣,但不同的是這個 tokenId 本身自己還會自帶 value,因此,在選擇轉移的時候我們就會有兩種選擇 1. 轉移整個 tokenId 給他人,這種方法也會連帶 value 的轉移權力給他人,等於 token 擁有權(擁有者可調用權利)全部移交給他人 2. 轉移 tokenId 上的 value 部分給其他 tokenId,這種轉移是有條件限制的,條件限制就是互相轉移的 tokenId,他們的 slot 必須相同,且相較於第一種,只有 value 的部分會做更動,擁有者的部分並不會有所更動 # Source Code 這邊只解釋部分程式碼,後續若有空會持續補充 ```solidity //IERC3525 function transferFrom(uint256 _fromTokenId, uint256 _toTokenId, uint256 _value) external payable; function transferFrom(uint256 _fromTokenId, address _to, uint256 _value) external payable returns (uint256 toTokenId_); ``` 前面提到 ERC3525 有兩種 transfer 方式,因此這邊在 interface 有定義兩種`transferFrom` 這兩種 transfer 的方法其實**都對應上述提到兩種傳送方法的第二種,也就是進行 token 之間的 value 轉移。** 第一個 function 相對簡單明瞭指定`_from`跟`_to` 的`tokenId`,以及需要的 value 第二個 function 做的事情一樣是 value 的轉移,但是`_to`的部分是 address,因此需要多一些步驟,先幫`_to` mint 一個新 token,這個新 token 的 slot 跟`_fromTokenId`一樣,但是`_to`有這個新 token 的擁有權,然後再將`_fromTokenId`的`_value`轉移過去給新 token 由於[solv-finance/erc-3525](https://github.com/solv-finance/erc-3525)的實作版本 IERC3525 有繼承 IERC721,因此在 ERC3525 概念當中提到的兩種轉移方式的第一種(擁有權的直接轉移),就是直接採用 IERC721 的實作方式 > 所以實際上的 ERC3525 有三個`transferFrom` ```solidity= // From IERC721 function transferFrom(address _from, address _to, uint256 _tokenId) external payable; ``` # 實作 ## 前置準備 需要有 hardhat 開發智能合約的知識 ## 開始實作 1. 開啟一個 hardhat 專案 2. 安裝套件 ```bash= npm install @solvprotocol/erc-3525@latest ``` 3. 撰寫一個 SFT 的合約 ```solidity= //SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {ERC3525} from "@solvprotocol/erc-3525/ERC3525.sol"; contract SFT is ERC3525 { constructor() ERC3525("SFT", "MySFT", 18) {} function mint(address _to, uint256 _slot, uint256 _value) public { _mint(_to, _slot, _value); } } ``` 4. 編譯,確認有無問題 ```bash= npx hardhat compile ``` 5. 開始寫測試(前置部分) ```javascript= const { ethers, tracer } = require("hardhat"); const { expect } = require("chai"); let SFT, sft; let deployer, user1, user2; const tokenId = 1; const slot = 1; const value = 10000; describe("SFT", function () { before(async function () { [deployer, user1, user2] = await ethers.getSigners(); SFT = await ethers.getContractFactory("SFT"); }); beforeEach(async function () { sft = await SFT.deploy(); await sft.deployed(); }); }) ``` ## 測試案例 1. 確認 mint 有沒有成功 ```javascript= it("Mint", async function () { const ownedToken = 1; await expect(sft.mint(user1.address, tokenId, slot, value)).to.not .reverted; expect(await sft.slotOf(tokenId)).to.equal(slot); //檢查token的slot expect(await sft["balanceOf(uint256)"] (tokenId)).to.equal(value); // 檢查token的value expect(await sft["balanceOf(address)"](user1.address)).to.equal( ownedToken ); //檢查address擁有的token量 expect(await sft.ownerOf(tokenId)).to.equal(user1.address); //檢查token的擁有權 }); ``` > 注意! 這邊 balanceOf 有兩種不同的參數(重載 overloading),因此在調用 function 需採用上面的寫法 2. Approve,分為兩種 approve,一種是 approve value(類似 ERC20),一種是 approve ownership(ERC721) ```javascript= it("Approve value", async function () { await expect(sft.mint(user1.address, tokenId, slot, value)).to.not .reverted; await expect( sft .connect(user1) ["approve(uint256,address,uint256)"](tokenId, user2.address, value) ).to.not.reverted; expect(await sft.allowance(tokenId, user2.address)).to.equal(value); // 檢查allowance }); it("Approve token ownership", async function () { await expect(sft.mint(user1.address, tokenId, slot, value)).to.not .reverted; await expect( sft.connect(user1)["approve(address,uint256)"](user2.address, tokenId) ).to.not.reverted; expect(await sft.getApproved(tokenId)).to.equal(user2.address); //檢查擁有權是否approve }); ``` 3. 測試`transferFrom` 轉移 token 的 value 給其他 token ```javascript= it("Transfer token value to user2", async function () { await expect(sft.mint(user1.address, slot, value)).to.not.reverted; await expect( sft .connect(user1) ["approve(uint256,address,uint256)"](tokenId, user2.address, value) ).to.not.reverted; const newTokenId = tokenId + 1; //還沒有transfer之前,user2沒有任何token expect(await sft["balanceOf(address)"](user2.address)).to.equal(0); await expect(sft["balanceOf(uint256)"](newTokenId)).to.revertedWith( "ERC3525: invalid token ID" ); await expect( sft .connect(user2) ["transferFrom(uint256,address,uint256)"]( tokenId, user2.address, value ) ).to.not.reverted; //User2 得到一個新的tokenId(原tokenId + 1) expect(await sft["balanceOf(address)"](user2.address)).to.equal(1); expect(await sft["balanceOf(uint256)"](newTokenId)).to.equal(value); }); ``` 這邊有個有趣的點在於,我前面的測試 transfer 跟 approve 都是對應的 也就是說如果我要 transfer value,我就 approve 相對應的 value,我要 transfer ownership,我就 approve ownership 那麼若是不對應的話呢? 4. 當 approve ownership 時,執行 transfer value ```javascript= it("Transfer token value when approve ownership", async function () { await expect(sft.mint(user1.address, slot, value)).to.not.reverted; await expect( sft.connect(user1)["approve(address,uint256)"](user2.address, tokenId) ).to.not.reverted; const newTokenid = tokenId + 1; await expect( sft .connect(user2) ["transferFrom(uint256,address,uint256)"]( tokenId, user2.address, value ) ).to.not.reverted; expect(await sft.ownerOf(tokenId)).to.equal(user1.address); expect(await sft["balanceOf(uint256)"](newTokenid)).to.equal(value); expect(await sft["balanceOf(address)"](user2.address)).to.equal(1); }); ``` 我們可以看到仍然會成功,user2 仍然會多出一個 token 5. 那麼反過來呢?當 approve value 時,執行 transfer ownership ```javascript= it("Transfer token ownership to user2 by user1", async function () { await expect(sft.mint(user1.address, slot, value)).to.not.reverted; await expect( sft .connect(user1) ["approve(uint256,address,uint256)"](tokenId, user2.address, value) ).to.not.reverted; await expect( sft .connect(user2) ["transferFrom(address,address,uint256)"]( user1.address, user2.address, tokenId ) ).to.revertedWith("ERC3525: transfer caller is not owner nor approved"); }); ``` 這邊可以看到 transfer 會失敗,因為 ERC3525 的擁有關係是有上下層級的(可參考上方關係圖),下層級的 approve(approve value)是不可以更動上層級的資料(ownership),但反過來上層級的 approve(approve ownership)可以更動下層級的資料(value) ## 備註 這邊有我實作的版本[LI-YONG-QI/ERC3525-demo](https://github.com/LI-YONG-QI/ERC3525-demo),裡面有多一個Foundry的版本可供使用,測試內容是差不多的 ## Reference [深入 ERC-3525:新協議的契機、現狀與未來](https://www.rayskyinvest.com/76531/erc3525-sft) [ID-SLOT-VALUE - Solv Documentation](https://docs.solv.finance/solv-documentation/ERC-3525-SFT/core-mechanisms/id-slot-units) [ERC3525](https://eips.ethereum.org/EIPS/eip-3525)