iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
自我挑戰組

Solidity 初學之路系列 第 27

DAY 27 - 鏈上隨機數、EIP1155

  • 分享至 

  • xImage
  •  

鏈上隨機數

許多以太坊上的應用都需要用到隨機數,例如 NFT 隨機抽取 tokenId、抽盲盒、gamefi 戰鬥中隨機分勝負等等。但由於以太坊上所有資料都是公開透明(public)且確定性(deterministic)的,它沒法像其他程式語言一樣提供開發者產生隨機數的方法。今天將介紹鏈上(雜湊函數)和鏈下(chainlink 預言機)隨機數產生的兩種方法,並利用它們做一款 tokenId 隨機鑄造的NFT。

鏈上隨機數生成

我們可以將一些鏈上的全域變數當作種子,利用 keccak256() 雜湊函數來取得偽隨機數。這是因為雜湊函數具有靈敏度和均一性,可以得到「看似」隨機的結果。下面的 getRandomOnchain() 函數利用全域變數 block.timestamp、msg.sender 和 blockhash(block.number-1) 作為種子來取得隨機數:

    /** 
    * 鏈上偽隨機數生成 
    * 利用keccak256()打包一些鏈上的全域變數/自訂變數
    * 返回時轉換成uint256類型
    */
    function getRandomOnchain() public view returns(uint256){
        // remix運行blockhash會報錯
        bytes32 randomBytes = keccak256(abi.encodePacked(block.timestamp, msg.sender, blockhash(block.number-1)));
        
        return uint256(randomBytes);
    }

注意:這個方法不安全,因為 block.timestamp、msg.sender 和 blockhash(block.number-1) 這些變數都是公開的,使用者可以預測出用這些種子產生的隨機數,並挑出他們想要的隨機數執行合約,礦工可以操縱 blockhash 和 block.timestamp,使得產生的隨機數符合他的利益。

鏈下隨機數生成

我們可以在鏈下產生隨機數,然後透過**預言機(Chainlink)**把隨機數上傳到鏈上。Chainlink 提供 VRF(可驗證隨機函數)服務,鏈上開發者可以支付 LINK 代幣來取得隨機數。Chainlink VRF有兩個版本,第二個版本需要官網註冊並預付費,比第一個版本多許多操作,需要花費更多的 gas,但取消訂閱後可以拿回剩餘的 Link,這裡介紹第二個版本 Chainlink VRF V2。

Chainlink VRF 使用步驟

  1. 智能合約應用發送隨機數請求
  2. Chainlink 產生隨機數並將證明發送到 VRF 合約
  3. VRF 合約驗證隨機數
  4. 智能合約應用接收隨機數

我們將用一個簡單的合約介紹使用 Chainlink VRF的步驟。 RandomNumberConsumer 合約可以向 VRF 請求隨機數,並儲存在狀態變數 randomWords 中。

  1. 申請 Subscription 並轉入 Link 代幣
    Chainlink VRF 網站上創建一個 Subscription,其中信箱和專案名稱都是選填。創建完成後往 Subscription 中轉入一些 Link 代幣。Sepolia 測試網的 LINK 代幣可以從 LINK 水龍頭領取。
  2. 使用者合約繼承 VRFConsumerBaseV2
    為了使用 VRF 取得隨機數,合約需要繼承 VRFConsumerBaseV2 合約,並在建構子中初始化 VRFCoordinatorV2Interface 和 Subscription Id。(不同鏈對應不同的參數
    // SPDX-License-Identifier: MIT
    

pragma solidity ^0.8.21;

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

contract RandomNumberConsumer is VRFConsumerBaseV2{

//請求隨機數需要呼叫VRFCoordinatorV2Interface介面
VRFCoordinatorV2Interface COORDINATOR;

// 申請後的subId
uint64 subId;

//存放得到的 requestId 和 隨機數
uint256 public requestId;
uint256[] public randomWords;

/**
 * 使用chainlink VRF,建構子需要繼承 VRFConsumerBaseV2
 * 不同鏈的參數填的不一樣,可參 https://docs.chain.link/vrf/v2/subscription/supported-networks
 * 網路: Sepolia測試網
 * Chainlink VRF Coordinator 地址: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
 * LINK 代幣地址: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709
 * 30 gwei Key Hash: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c
 * Minimum Confirmations 最小確認塊數 : 3 (數字大安全性高,一般填12)
 * callbackGasLimit gas限制 : 最大 2,500,000
 * Maximum Random Values 一次可以得到的隨機數個數 : 最大 500         
 */
address vrfCoordinator = 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625;
bytes32 keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
uint16 requestConfirmations = 3;
uint32 callbackGasLimit = 200_000;
uint32 numWords = 3;

constructor(uint64 s_subId) VRFConsumerBaseV2(vrfCoordinator){
    COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
    subId = s_subId;
}
```
  1. 使用者合約申請隨機數
    使用者可以呼叫從 VRFCoordinatorV2Interface 介面合約中的 requestRandomWords 函數申請隨機數,並傳回申請識別碼 requestId。這個申請會傳遞給 VRF 合約。合約部署後,需要把合約加入 Subscription 的 Consumers 中,才能發送申請。
     /** 
     * 向VRF合約申請隨機數
     */
    function requestRandomWords() external {
        requestId = COORDINATOR.requestRandomWords(
            keyHash,
            subId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );
    }
    
  2. Chainlink 節點鏈下產生隨機數和數字簽名,並發送給 VRF 合約。
  3. VRF 合約驗證簽名有效性
  4. 使用者合約接收並使用隨機數
    在 VRF 合約驗證簽章有效之後,會自動呼叫使用者合約的回退函數 fulfillRandomness(),將鏈下產生的隨機數傳送過來。使用者要把消耗隨機數的邏輯寫在這裡。使用者申請隨機數時呼叫的 requestRandomness() 和 VRF 合約傳回隨機數時呼叫的回退函數 fulfillRandomness() 是兩筆交易,呼叫者分別是使用者合約和 VRF 合約,後者比前者晚幾分鐘(不同鏈延遲不一樣)。
     /**
     * VRF合約的回傳函數,驗證隨機數有效之後會自動被調用
     * 消耗隨機數的邏輯寫在這裡
     */
    function fulfillRandomWords(uint256 requestId, uint256[] memory s_randomWords) internal override {
        randomWords = s_randomWords;
    }
    

tokenId 隨機鑄造的 NFT

我們將利用鏈上和鏈下隨機數字來做一款tokenId隨機鑄造的NFT。 Random合約繼承ERC721和VRFConsumerBaseV2合約。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/ERC721.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

contract Random is ERC721, VRFConsumerBaseV2{
    // NFT相關
    uint256 public totalSupply = 100; // NFT總供給
    uint256[100] public ids; // 數組,用於計算可供mint的tokenId,請參閱pickRandomUniqueId()函數。
    uint256 public mintCount; // 已經mint的數量

    // Chainlink VRF相關參數
    
    // VRFCoordinatorV2Interface
    VRFCoordinatorV2Interface COORDINATOR; // 呼叫VRFCoordinatorV2Interface介面
    
    /**
     * 使用chainlink VRF,建構子需要繼承 VRFConsumerBaseV2
     * 不同鏈的參數填的不一樣,可參 https://docs.chain.link/vrf/v2/subscription/supported-networks
     * 網路: Sepolia測試網
     * Chainlink VRF Coordinator 地址: 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625
     * LINK 代幣地址: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709
     * 30 gwei Key Hash: 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c
     * Minimum Confirmations 最小確認塊數 : 3 (數字大安全性高,一般填12)
     * callbackGasLimit gas限制 : 最大 2,500,000
     * Maximum Random Values 一次可以得到的隨機數個數 : 最大 500         
     */
    address vrfCoordinator = 0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625; // VRF 合約地址
    bytes32 keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c; // VRF唯一識別符
    uint16 requestConfirmations = 3; // 確認區塊數
    uint32 callbackGasLimit = 1_000_000; // VRF手續費
    uint32 numWords = 1; // 請求的隨機數個數
    uint64 subId; // 申請的Subscription Id
    uint256 public requestId; // 申請標識符
    
    // 記錄申請VRF用於mint的使用者地址
    mapping(uint256 => address) public requestToSender;

建構子

初始化繼承的 VRFConsumerBaseV2 和 ERC721 合約的相關變數。

    constructor(uint64 s_subId) 
        VRFConsumerBaseV2(vrfCoordinator)
        ERC721("WTF Random", "WTF"){
            COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
            subId = s_subId;
    }

函數

    // 輸入uint256數字,回傳一個可以mint的tokenId
    function pickRandomUniqueId(uint256 random) private returns (uint256 tokenId) {
        //先計算減法,再計算++,注意(a++,++a)區別
        uint256 len = totalSupply - mintCount++; // 可mint數量
        require(len > 0, "mint close"); // 所有tokenId被mint完了
        uint256 randomIndex = random % len; // 取得鏈上隨機數

        //隨機數取模,得到tokenId,作為數組下標,同時記錄value為len-1,如果取模得到的值已存在,則tokenId取該數組下標的value
        tokenId = ids[randomIndex] != 0 ? ids[randomIndex] : randomIndex; // 取得tokenId
        ids[randomIndex] = ids[len - 1] == 0 ? len - 1 : ids[len - 1]; // 更新ids 列表
        ids[len - 1] = 0; // 刪除最後一個元素,能返還gas
    }

    /** 
    * 鏈上偽隨機數生成
    * keccak256(abi.encodePacked()中填上一些鏈上的全域變數/自訂變數
    * 返回時轉換成uint256型
    */
    function getRandomOnchain() public view returns(uint256){
        /*
         * 本例鏈上隨機只依賴區塊哈希,呼叫者位址,和區塊時間,
         * 想提高隨機性可以再增加一些屬性例如nonce等,但不能根本解決安全問題
         */
        bytes32 randomBytes = keccak256(abi.encodePacked(blockhash(block.number-1), msg.sender, block.timestamp));
        return uint256(randomBytes);
    }

    // 利用鏈上偽隨機數鑄造NFT
    function mintRandomOnchain() public {
        uint256 _tokenId = pickRandomUniqueId(getRandomOnchain()); 
        _mint(msg.sender, _tokenId);
    }

    /** 
     * 呼叫VRF取得隨機數,並mintNFT
     * 要呼叫requestRandomness()函數獲取,消耗隨機數的邏輯寫在VRF的回呼函數fulfillRandomness()中
     * 在呼叫之前,需要在Subscriptions中轉入足夠的Link
     */
    function mintRandomVRF() public {
        // 呼叫requestRandomness取得隨機數
        requestId = COORDINATOR.requestRandomWords(
            keyHash,
            subId,
            requestConfirmations,
            callbackGasLimit,
            numWords
        );
        requestToSender[requestId] = msg.sender;
    }

    /**
     * VRF的回傳函數,由VRF Coordinator呼叫
     * 消耗隨機數的邏輯寫在本函數
     */
    function fulfillRandomWords(uint256 requestId, uint256[] memory s_randomWords) internal override{
        address sender = requestToSender[requestId]; // 从requestToSender中获取minter用户地址
        uint256 tokenId = pickRandomUniqueId(s_randomWords[0]); // 利用VRF返回的随机数生成tokenId
        _mint(sender, tokenId);
    }
  • pickRandomUniaueId():輸入隨機數,取得可供mint的tokenId
    演算法過程可理解為:totalSupply個空杯子(0初始化的ids)排成一排,每個杯子旁邊放一個球,編號為[0, totalSupply - 1]。 每次從場上隨機拿走一個球(球可能在杯子旁邊,這是初始狀態;也可能是在杯子裡,說明杯子旁邊的球已經被拿走過,則此時新的球從末尾被放到了杯子裡) 再把最後的一個球(還是可能在杯子裡也可能在杯子旁邊)放進被拿走的球的杯子裡,循環totalSupply次。相較於傳統的隨機排列,省去了初始化ids[]的gas。
  • getRandomOnchain():取得鏈上隨機數(不安全)。
  • mintRandomOnchain():利用鏈上隨機數鑄造NFT,呼叫了getRandomOnchain()和pickRandomUniqueId()。
  • mintRandomVRF():申請Chainlink VRF用於鑄造隨機數。由於使用隨機數鑄造的邏輯在回調函數fulfillRandomness(),而回調函數的呼叫者是VRF合約,而非鑄造NFT的用戶,這裡必須利用requestToSender狀態變數記錄VRF申請識別碼對應的用戶位址。
  • fulfillRandomWords():VRF的回調函數,由VRF合約在驗證隨機數真實性後自動調用,用返回的鏈下隨機數鑄造NFT。

使用鏈上隨機數高效,但是不安全;而鏈下隨機數生成依賴於第三方提供的預言機服務,比較安全,但是沒那麼簡單經濟。專案方要根據業務場景來選擇適合自己的方案。

EIP1155

ERC1155標準支援一個合約包含多種代幣。並且我們可以發行一個魔改的無聊猿( BAYC1155):它包含 10000 種代幣,且元資料與 BAYC 一致。

  不論是 ERC20 或 ERC721 標準,每個合約都對應一個獨立的代幣。假設我們要在以太坊上打造一個類似《魔獸世界》的大型遊戲,這需要我們對每個裝備都部署一個合約。上千種裝備就要部署和管理上千個合約,這非常麻煩。因此,以太坊 EIP1155 提出了一個多代幣標準 ERC1155,允許一個合約包含多個同質化和非同質化代幣。ERC1155 在 GameFi 應用最多,Decentraland、Sandbox 等知名鏈遊都使用它。
  簡單來說,ERC1155 與先前介紹的非同質化代幣標準 ERC721 類似:在 ERC721 中,每個代幣都有一個 tokenId 作為唯一標識,每個 tokenId 只對應一個代幣;而在 ERC1155 中,每一種代幣都有一個 id 作為唯一標識,每個 id 對應一種代幣。這樣,代幣種類就可以非同質的在同一個合約裡管理了,而且每種代幣都有一個網址 uri 來儲存它的元數據,類似 ERC721 的 tokenURI。下面是 ERC1155 的元資料介面合約 IERC1155MetadataURI:

/**
 * @dev ERC1155的可選介面,加入了uri()函數查詢元數據
 */
interface IERC1155MetadataURI is IERC1155 {
    /**
     * @dev 回傳第`id`種類代幣的URI
     */
    function uri(uint256 id) external view returns (string memory);

那麼要怎麼區分 ERC1155 中的某類代幣是同質化還是非同質化代幣呢?其實很簡單:如果某一 id 對應的代幣總量為 1,那麼它就是非同質化代幣,類似 ERC721;如果某 id 對應的代幣總量大於 1,那麼他就是同質化代幣,因為這些代幣都分享同一個 id,類似 ERC20。

IERC1155 介面合約

IERC1155 介面合約抽象化了 EIP1155 需要實現的功能,其中包含 4 個事件和 6 個函數。與 ERC721 不同,因為 ERC1155 包含多類代幣,它實現了批量轉帳和批量餘額查詢,一次操作多種代幣:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/IERC165.sol";

/**
 * @dev ERC1155標準的介面合約,實現了EIP1155的功能
 * 詳見:https://eips.ethereum.org/EIPS/eip-1155[EIP]
 */
interface IERC1155 is IERC165 {
    /**
     * @dev 單類代幣轉帳事件
     * 當`value`個`id`種類的代幣被`operator`從`from`轉帳到`to`時釋放
     */
    event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);

    /**
     * @dev 批量代幣轉帳事件
     * ids和values為轉帳的代幣種類和數量陣列
     */
    event TransferBatch(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256[] ids,
        uint256[] values
    );

    /**
     * @dev 批量授權事件
     * 當`account`將所有代幣授權給`operator`時釋放
     */
    event ApprovalForAll(address indexed account, address indexed operator, bool approved);

    /**
     * @dev 當`id`種類的代幣的URI發生變化時釋放,`value`為新的URI
     */
    event URI(string value, uint256 indexed id);

    /**
     * @dev 持倉查詢,回傳`account`擁有的`id`種類的代幣的持倉量
     */
    function balanceOf(address account, uint256 id) external view returns (uint256);

    /**
     * 批量持倉查詢,`accounts`和`ids`陣列的長度要相等
     */
    function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids)
        external
        view
        returns (uint256[] memory);

    /**
     * @dev 批量授權,將呼叫者的代幣授權給`operator`地址。
     * 釋放{ApprovalForAll}事件
     */
    function setApprovalForAll(address operator, bool approved) external;

    /**
     * @dev 批次授權查詢,如果授權位址`operator`被`account`授權,則傳回`true`
     * 見 {setApprovalForAll}函數
     */
    function isApprovedForAll(address account, address operator) external view returns (bool);

    /**
     * @dev 安全轉賬,將`amount`單位`id`種類的代幣從`from`轉帳給`to`
     * 釋放{TransferSingle}事件
     * 要求:
     * - 如果呼叫者不是`from`位址而是授權位址,則需要得到`from`的授權
     * - `from`地址必須有足夠的持倉
     * - 如果接收方是合約,則需要實作`IERC1155Receiver`的`onERC1155Received`方法,並傳回對應的值
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes calldata data
    ) external;

    /**
     * @dev 批量安全轉帳
     * 釋放{TransferBatch}事件
     * 要求:
     * - `ids`和`amounts`長度相等
     * - 如果接收方是合約,則需要實作`IERC1155Receiver`的`onERC1155BatchReceived`方法,並傳回對應的值
     */
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] calldata ids,
        uint256[] calldata amounts,
        bytes calldata data
    ) external;
}

IERC1155 事件

  • TransferSingle事件:單類代幣轉帳事件,在單幣種轉帳時釋放。
  • TransferBatch事件:大量代幣轉帳事件,在多幣種轉帳時釋放。
  • ApprovalForAll事件:批次授權事件,在批次授權時釋放。
  • URI事件:元資料位址變更事件,在uri變化時釋放。

IERC1155 函數

  • balanceOf():單幣種餘額查詢,傳回account擁有的id種類的代幣的持倉量。
  • balanceOfBatch():多幣種餘額查詢,查詢的位址accounts陣列和代幣種類ids陣列的長度要相等。
  • setApprovalForAll():大量授權,將呼叫者的代幣授權給operator位址。
  • isApprovedForAll():查詢批次授權訊息,如果授權地址operator被account授權,則傳回true。
  • safeTransferFrom():安全單幣轉賬,將amount單位id種類的代幣從from地址轉帳給to地址。如果to位址是合約,則會驗證是否實作了onERC1155Received()接收函數。
  • safeBatchTransferFrom():安全多幣轉賬,與單幣轉帳類似,只不過轉帳數量amounts和代幣種類ids變成數組,且長度相等。如果to位址是合約,則會驗證是否實作了onERC1155BatchReceived()接收函數。

ERC1155 接收合約

與 ERC721 標準類似,為了避免代幣被轉入黑洞合約,ERC1155 要求代幣接收合約繼承 IERC1155Receiver 並實現兩個接收函數:

  • onERC1155Received():單幣轉帳接收函數,接受ERC1155安全轉帳safeTransferFrom 需要實作並傳回自己的選擇器0xf23a6e61。
  • onERC1155BatchReceived():多幣轉帳接收函數,接受ERC1155安全多幣轉帳safeBatchTransferFrom 需要實作並傳回自己的選擇器0xbc197c81。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/IERC165.sol";

/**
 * @dev ERC1155接收合約,要接受ERC1155的安全轉賬,就需要實現這個合約
 */
interface IERC1155Receiver is IERC165 {
    /**
     * @dev 接受ERC1155安全轉帳`safeTransferFrom`
     * 需要回傳 0xf23a6e61 或 `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
     */
    function onERC1155Received(
        address operator,
        address from,
        uint256 id,
        uint256 value,
        bytes calldata data
    ) external returns (bytes4);

    /**
     * @dev 接受ERC1155批量安全轉帳`safeBatchTransferFrom`
     * 需要回傳 0xbc197c81 或 `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
     */
    function onERC1155BatchReceived(
        address operator,
        address from,
        uint256[] calldata ids,
        uint256[] calldata values,
        bytes calldata data
    ) external returns (bytes4);
}

ERC1155 主合約

ERC1155主合約實現了IERC1155介面合約規定的函數,以及單幣/多幣的鑄造和銷毀函數。

ERC1155 變數

ERC1155 主合約包含 4 個狀態變數:

  • name:代幣名稱
  • symbol:代幣代號
  • _balances:代幣持倉映射,記錄代幣種類id下某地址account的持倉量balances。
  • _operatorApprovals:批次授權映射,記錄持有位址給另一個位址的授權情況。

ERC1155 函數

  • 建構子:初始化狀態變數name和symbol。
  • supportsInterface():實現ERC165標準,聲明它支援的接口,供其他合約檢查。
  • balanceOf():實作IERC1155的balanceOf(),查詢持倉量。與ERC721標準不同,這裡要輸入查詢的持倉地址account以及幣種id。
  • balanceOfBatch():實作IERC1155的balanceOfBatch(),批次查詢持倉量。
  • setApprovalForAll():實作IERC1155的setApprovalForAll(),批次授權,釋放ApprovalForAll事件。
  • isApprovedForAll():實作IERC1155的isApprovedForAll(),查詢批次授權資訊。
  • safeTransferFrom():實作IERC1155的safeTransferFrom(),單幣種安全轉賬,釋放TransferSingle事件。與ERC721不同,這裡不僅需要填發出方from,接收方to,代幣種類id,還需要填轉帳金額amount。
  • safeBatchTransferFrom():實作IERC1155的safeBatchTransferFrom(),多幣種安全轉賬,釋放TransferBatch事件。
  • _mint():單幣種鑄造函數。
  • _mintBatch():多幣種鑄造函數。
  • _burn():單幣種銷毀函數。
  • _burnBatch():多幣種銷毀函數。
  • _doSafeTransferAcceptanceCheck:單幣種轉帳的安全檢查,被safeTransferFrom()調用,確保接收方為合約的情況下,實作了onERC1155Received()函數。
  • _doSafeBatchTransferAcceptanceCheck:多幣種轉帳的安全檢查,,被safeBatchTransferFrom調用,確保接收方為合約的情況下,實現了onERC1155BatchReceived()函數。
  • uri():傳回ERC1155的第id種代幣儲存元資料的網址,類似ERC721的tokenURI。
  • baseURI():返回baseURI,uri就是把baseURI和id拼接在一起,需要開發重寫。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IERC1155.sol";
import "./IERC1155Receiver.sol";
import "./IERC1155MetadataURI.sol";
import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/Address.sol";
import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/String.sol";
import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/IERC165.sol";

/**
 * @dev ERC1155多代幣標準
 * 見 https://eips.ethereum.org/EIPS/eip-1155
 */
contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI {
    using Address for address; // 使用Address庫,用isContract來判斷地址是否為合約
    using Strings for uint256; // 使用String庫
    // Token名稱
    string public name;
    // Token代號
    string public symbol;
    // 代幣種類id 到帳戶account 到餘額balances 的映射
    mapping(uint256 => mapping(address => uint256)) private _balances;
    // address 到 授權地址 的批次授權映射
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    /**
     * 建構子,初始化`name` 和`symbol`, uri_
     */
    constructor(string memory name_, string memory symbol_) {
        name = name_;
        symbol = symbol_;
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return
            interfaceId == type(IERC1155).interfaceId ||
            interfaceId == type(IERC1155MetadataURI).interfaceId ||
            interfaceId == type(IERC165).interfaceId;
    }

    /**
     * @dev 持倉查詢 實現IERC1155的balanceOf,返回account地址的id種類代幣持倉量。
     */
    function balanceOf(address account, uint256 id) public view virtual override returns (uint256) {
        require(account != address(0), "ERC1155: address zero is not a valid owner");
        return _balances[id][account];
    }

    /**
     * @dev 批量持倉查詢
     * 要求:
     * `accounts` 和 `ids` 陣列長度相等
     */
    function balanceOfBatch(address[] memory accounts, uint256[] memory ids)
        public view virtual override
        returns (uint256[] memory)
    {
        require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch");
        uint256[] memory batchBalances = new uint256[](accounts.length);
        for (uint256 i = 0; i < accounts.length; ++i) {
            batchBalances[i] = balanceOf(accounts[i], ids[i]);
        }
        return batchBalances;
    }

    /**
     * @dev 批量授權,調用者授權operator使用其所有代幣
     * 釋放{ApprovalForAll}事件
     * 條件:msg.sender != operator
     */
    function setApprovalForAll(address operator, bool approved) public virtual override {
        require(msg.sender != operator, "ERC1155: setting approval status for self");
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    /**
     * @dev 查詢批量授權
     */
    function isApprovedForAll(address account, address operator) public view virtual override returns (bool) {
        return _operatorApprovals[account][operator];
    }

    /**
     * @dev 安全轉賬,將`amount`單位的`id`種類代幣從`from`轉賬到`to`
     *  釋放 {TransferSingle} 事件
     * 要求:
     * - to 不能是0位址
     * - from擁有足夠的持倉量,且呼叫者擁有授權
     * - 如果 to 是智能合約,他必須支援 IERC1155Receiver-onERC1155Received
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) public virtual override {
        address operator = msg.sender;
        // 呼叫者是持有者或是被授權
        require(
            from == operator || isApprovedForAll(from, operator),
            "ERC1155: caller is not token owner nor approved"
        );
        require(to != address(0), "ERC1155: transfer to the zero address");
        // from地址有足夠持倉
        uint256 fromBalance = _balances[id][from];
        require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
        // 更新持倉量
        unchecked {
            _balances[id][from] = fromBalance - amount;
        }
        _balances[id][to] += amount;
        // 釋放事件
        emit TransferSingle(operator, from, to, id, amount);
        // 安全檢查
        _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);    
    }

    /**
     * @dev 批量安全轉賬,將`amounts`數組單位的`ids`數組種類代幣從`from`轉賬到`to`
     * 釋放 {TransferSingle} 事件
     * 要求:
     * - to 不能是0位址
     * - from擁有足夠的持倉量,且呼叫者擁有授權
     * - 如果 to 是智能合約, 他必須支援 IERC1155Receiver-onERC1155BatchReceived
     * - ids和amounts陣列長度相等
     */
    function safeBatchTransferFrom(
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) public virtual override {
        address operator = msg.sender;
        // 呼叫者是持有者或是被授權
        require(
            from == operator || isApprovedForAll(from, operator),
            "ERC1155: caller is not token owner nor approved"
        );
        require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
        require(to != address(0), "ERC1155: transfer to the zero address");

        // 透過for循環更新持倉
        for (uint256 i = 0; i < ids.length; ++i) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];

            uint256 fromBalance = _balances[id][from];
            require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
            unchecked {
                _balances[id][from] = fromBalance - amount;
            }
            _balances[id][to] += amount;
        }

        emit TransferBatch(operator, from, to, ids, amounts);
        // 安全檢查
        _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);    
    }

    /**
     * @dev 鑄造
     * 釋放 {TransferSingle} 事件
     */
    function _mint(
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) internal virtual {
        require(to != address(0), "ERC1155: mint to the zero address");

        address operator = msg.sender;

        _balances[id][to] += amount;
        emit TransferSingle(operator, address(0), to, id, amount);

        _doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
    }

    /**
     * @dev 批量鑄造
     * 釋放 {TransferBatch} 事件
     */
    function _mintBatch(
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) internal virtual {
        require(to != address(0), "ERC1155: mint to the zero address");
        require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");

        address operator = msg.sender;

        for (uint256 i = 0; i < ids.length; i++) {
            _balances[ids[i]][to] += amounts[i];
        }

        emit TransferBatch(operator, address(0), to, ids, amounts);

        _doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data);
    }

    /**
     * @dev 銷毀
     */
    function _burn(
        address from,
        uint256 id,
        uint256 amount
    ) internal virtual {
        require(from != address(0), "ERC1155: burn from the zero address");

        address operator = msg.sender;

        uint256 fromBalance = _balances[id][from];
        require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
        unchecked {
            _balances[id][from] = fromBalance - amount;
        }

        emit TransferSingle(operator, from, address(0), id, amount);
    }

    /**
     * @dev 批量銷毀
     */
    function _burnBatch(
        address from,
        uint256[] memory ids,
        uint256[] memory amounts
    ) internal virtual {
        require(from != address(0), "ERC1155: burn from the zero address");
        require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");

        address operator = msg.sender;

        for (uint256 i = 0; i < ids.length; i++) {
            uint256 id = ids[i];
            uint256 amount = amounts[i];

            uint256 fromBalance = _balances[id][from];
            require(fromBalance >= amount, "ERC1155: burn amount exceeds balance");
            unchecked {
                _balances[id][from] = fromBalance - amount;
            }
        }

        emit TransferBatch(operator, from, address(0), ids, amounts);
    }

    // @dev ERC1155的安全轉帳檢查
    function _doSafeTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256 id,
        uint256 amount,
        bytes memory data
    ) private {
        if (to.isContract()) {
            try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) {
                if (response != IERC1155Receiver.onERC1155Received.selector) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non-ERC1155Receiver implementer");
            }
        }
    }

    // @dev ERC1155的批量安全轉帳檢查
    function _doSafeBatchTransferAcceptanceCheck(
        address operator,
        address from,
        address to,
        uint256[] memory ids,
        uint256[] memory amounts,
        bytes memory data
    ) private {
        if (to.isContract()) {
            try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns (
                bytes4 response
            ) {
                if (response != IERC1155Receiver.onERC1155BatchReceived.selector) {
                    revert("ERC1155: ERC1155Receiver rejected tokens");
                }
            } catch Error(string memory reason) {
                revert(reason);
            } catch {
                revert("ERC1155: transfer to non-ERC1155Receiver implementer");
            }
        }
    }

    /**
     * @dev 返回ERC1155的id種類代幣的uri,存放metadata,類似ERC721的tokenURI
     */
    function uri(uint256 id) public view virtual override returns (string memory) {
        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, id.toString())) : "";
    }

    /**
     * 計算{uri}的BaseURI,uri就是把baseURI和tokenId拼接在一起,需要開發重寫
     */
    function _baseURI() internal view virtual returns (string memory) {
        return "";
    }
}

BAYC,但ERC1155

我們魔改下 ERC721 標準的無聊猿 BAYC,創造一個免費鑄造的 BAYC1155。我們修改_baseURI()函數,讓 BAYC1155 的 uri 和 BAYC 的 tokenURI 一樣。這樣,BAYC1155元資料會與無聊猿的相同:

// SPDX-License-Identifier: MIT
// by 0xAA
pragma solidity ^0.8.21;

import "./ERC1155.sol";

contract BAYC1155 is ERC1155{
    uint256 constant MAX_ID = 10000; 
    // 建構子
    constructor() ERC1155("BAYC1155", "BAYC1155"){
    }

    //BAYC的baseURI為ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/
    function _baseURI() internal pure override returns (string memory) {
        return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/";
    }
    
    // 鑄造函數
    function mint(address to, uint256 id, uint256 amount) external {
        // id 不能超过10,000
        require(id < MAX_ID, "id overflow");
        _mint(to, id, amount, "");
    }

    // 批量鑄造函數
    function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) external {
        // id 不能超過10000
        for (uint256 i = 0; i < ids.length; i++) {
            require(ids[i] < MAX_ID, "id overflow");
        }
        _mintBatch(to, ids, amounts, "");
    }
}

上一篇
DAY 26 - Signature、NFT 交易所
下一篇
DAY 28 - WETH、分帳
系列文
Solidity 初學之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言