---
# System prepended metadata

title: ERC3525 半同質化代幣 概念與技術實作

---

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