# 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 的應用例子

> Ref: [深入 ERC-3525:新協議的契機、現狀與未來](https://www.rayskyinvest.com/76531/erc3525-sft)
#### 擁有的關係如下

> 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)