[區塊鏈] Solidity – 實測簡單撰寫 NFT 發行用的寫智能合約

在大班哥開始接觸到 NFT 後,進而認識到了以太鏈上的智能合約,又很好奇這樣的東西內部機制是怎麼用程式運作,

這篇文章會帶著大家完成一個簡單的智能合約,透過合約互動鑄造 (Mint) 出 NFT ,之後能到 Opensea 平台上看到。





以下主要分成幾部分介紹:

  1. 撰寫智能合約
  2. 設置開發環境,測試/部署合約

1 – 撰寫智能合約

智能合約是什麼,為什麼要它?

要能在以太坊(世界上其中一種區塊鏈)上有特定規則的存儲、發送代幣,就需要編寫智能合約來制定

而撰寫智能合約需要透過程式語言,寫完後,編譯它成 Bytecode 給機器讀,

拿以太坊來說就會被放到 EVM (Ethereum Virtual Machine) 上在各個節點中運行,

大班哥這篇文章會以 Solidity 這個程式語言來實測撰寫智能合約。

1-1 Solidity 語法介紹

Solidity 官方開發文件 https://docs.soliditylang.org/en/develop/

簡體中文翻譯文件 https://www.tryblockchain.org/

pragma:

一份智能合約的開頭,一定要先宣告編譯器的版本,pragma 就是在做這件事

pragma solidity >= 7.0.0

function 的變數儲存: memory vs storage

memory 是儲存在記憶體,而 storage 表示儲存在整個區塊鏈當中

function setBaseURI(string memory newBaseURI) public onlyOwner {
    baseURI = newBaseURI;
}

function 的可見度: Pure vs View :

可以理解成 function 的能見度,

view: 當 function 要「讀取」區塊鏈的 storage 時使用

Pure:連 「讀取」都不會時可用

function showInfo() public pure { 
    return false;
}

function senderAddressHashed() public view returns (bytes32){
    return keccak256(abi.encodePacked(msg.sender));
}

1-2. 先來認識 NFT (非同值化代幣) 的標準協定 ERC-721

要撰寫發行 NFT 的智能合約,就一定要認識 ERC-721,

ERC-721 代表著 NFT 的標準,換句話說,這種類型的 Token 是獨一無二的,

並且可能存在來自同一智能合約但不同的價值 Token ,

▼ 可以參考這邊 EIP-721: Non-Fungible Token Standard

IRC721 標準 Protocol:

interface ERC721 /* is ERC165 */ {
    // Event
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
    event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
    event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
    // Function
    function balanceOf(address _owner) external view returns (uint256);
    function ownerOf(uint256 _tokenId) external view returns (address);
    function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data)
    function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
    function approve(address _approved, uint256 _tokenId) external payable;
    function setApprovalForAll(address _operator, bool _approved) external;
    function getApproved(uint256 _tokenId) external view returns (address);
    function isApprovedForAll(address _owner, address _operator) external view returns (bool);

有分 Event 跟 Function

Event

  • Transfer — 當 tokenId 的 token 發生轉移時會觸發
  • Approval — owner 允許 tokenId 的 token 轉移時觸發
  • ApprovalForAll — owner 啟用會禁用 operator 以管理其所有資產時觸發

Function

  • balanceOf — 該 Owner 擁有的 NFT 數量,可能為零
  • ownerOf — 返回擁有該 tokenId 的 owner address

▼ MetaData 標準/解釋

interface ERC721Metadata /* is ERC721 */ {
    function name() external view returns (string _name);
    function symbol() external view returns (string _symbol);
    function tokenURI(uint256 _tokenId) external view returns (string);
}
  • tokenURI — ERC721 標準函數,返回該 tokenId 對應的 URI
  • symbol — token 的簡寫名稱,例如像是比特幣 BTC$
  • name — 合約名稱

▼ Enumerable 標準

interface ERC721Enumerable /* is ERC721 */ {
    function totalSupply() external view returns (uint256);
    function tokenByIndex(uint256 _index) external view returns (uint256);   
    function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}
  • totalSupply — ERC721 標準函數,返回一個正整數,表示存儲在合約中的代幣總數,亦即目前被鑄造的 token 數
  • tokensOfOwner — 返回提供的錢包地址擁有的令牌 ID 數組。




1-3. 認識 OpenZeppelin

OpenZeppelin 是一個開發以太坊智能合約的 Open Source library,把一些常用功能包裝起來,

然後有 community 共同審議程式碼,安全性有基本保障,

OpenZeppelin 的 ERC-721文件:https://docs.openzeppelin.com/contracts/4.x/api/token/erc721

本篇大班哥也會使用 OpenZeppelin 來開發智能合約。

大家如果有興趣,可以去 Github 看 OpenZeppelin 的原始碼 : https://github.com/OpenZeppelin/openzeppelin-contracts/

大班哥之後獨立寫一篇詳細介紹 OpenZeppelin,這次只是先簡單使用來撰寫智能合約。

1-4. 撰寫智能合約 Smart Contract

大班哥會以 OpenZeppelin 套件來實作智能合約

Solidity 程式碼 bigben_nft.sol 如下 :

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract BigBenFun is ERC721, ERC721Enumerable, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    uint256 public maxTokenSupply;

    uint256 public constant MAX_MINTS_PER_TXN = 5;

    uint256 public mintPrice = 80000000 gwei; // 0.08 ETH

    bool public saleIsActive = false;

    string public baseURI;
    
    constructor(string memory name, string memory symbol, uint256 maxBigBenTokenSupply) ERC721(name, symbol) {
        maxTokenSupply = maxBigBenTokenSupply;
    }

    function setMaxTokenSupply(uint256 maxBigBenTokenSupply) public onlyOwner {
        maxTokenSupply = maxBigBenTokenSupply;
    }

    function setMintPrice(uint256 newPrice) public onlyOwner {
        mintPrice = newPrice;
    }

    /*
    * Pause sale if active, make active if paused.
    */
    function flipSaleState() public onlyOwner {
        saleIsActive = !saleIsActive;
    }

    /*
    * Mint BigBenFun NFTs
    */
    function mintBigBenFun(uint256 numberOfTokens) public payable {
        require(saleIsActive, "Sale must be active");
        require(numberOfTokens <= MAX_MINTS_PER_TXN, "You can only adopt 5 BigBenFun at a time");
        require(totalSupply() + numberOfTokens <= maxTokenSupply, "Purchase would exceed max available BigBenFun");
        require(mintPrice * numberOfTokens <= msg.value, "Ether value sent is not correct");

        for(uint256 i = 0; i < numberOfTokens; i++) {
            uint256 mintIndex = _tokenIdCounter.current() + 1;
            if (mintIndex <= maxTokenSupply) {
                _safeMint(msg.sender, mintIndex);
                _tokenIdCounter.increment();
            }
        }
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return baseURI;
    }

    function setBaseURI(string memory newBaseURI) public onlyOwner {
        baseURI = newBaseURI;
    }

    function withdraw(address to) public onlyOwner {
        uint256 balance = address(this).balance;
        payable(to).transfer(balance);
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) {
        return super.supportsInterface(interfaceId);
    }

}




大班哥大概講解其中幾個重要的

Function 解釋:

mintBigBenFun() – 最關鍵的 function ,提供使用者能夠鑄造 (mint) 我們的 NFT ,在執行真正的 _safeMint() 前,需要通過檢查 require(),像是 token 提供數目有沒有超過, 提供費用是否大於要 mint 的個數乘上 0.08 ETH 等等

flipSaleState() – 啟動/關閉開賣時機

Function Modifiers 解釋:

onlyOwner – 表示只能是合約部署者才能有權限呼叫,可以參考原始碼,在合約部署時 Ownable 的 constructor() 會帶入自己

payable – 可以允許 function 在呼叫時收到 ETH ,詳細參考文件

Special Variables and Functions 解釋 (官方文件):

有些變數是存在全域都可以使用,用於 Block 跟 Transaction

msg.value – 在 message 交易裡使用到的 Wei 值,詳細可參考官方文件

msg.sender – 當下呼叫這個 message 的人

tokenId:特別注意 tokenId 這個東西,你可以理解一個 NFT 就是對應到一個 tokenId,

因為對於任何 ERC-721 合約,contract address 對應到的 tokenId 必須是唯一的。

對於個別的 NFT 都有一個 type 為 uint256 的 tokenId

所以可以透過 tokenURI() 的 function 來查詢該 NFT 的資產對應位置

2 – 設置開發環境,測試/部署合約

2-1 設定 Metamask 的環境改為 Rinkeby 測試網路

Metamask 這邊會有兩個設定要做:

  1. 更改網路到測試網路(鏈),改到 Rinkeby
  2. 把帳號充值測試用 ETH 幣

1.更改網路到測試網路(鏈),改到 Rinkeby

▼ 點選 Metamask 的設定 -> 進階 -> Show test networks -> 按上方網路按鈕,選取 Rinkeby 測試網路

2.把帳號充值測試用 ETH

到 Chainlink faucets -> https://faucets.chain.link/

申請測試用的 ETH





2-2 了解 Remix 並設置好開發測試環境

Remix 是一個可以用 Solidity 開發智能合約的 web 平台,能夠使用 編譯/測試/部署等。

首先安裝 remix-project 可以讓 local 端的程式碼直接 sync 到 Remix web IDE,

安裝好後,參考 Remix 的 Command 文件說明,用意是啟動一個 server 去聽 web remix IDE 的事件,開啟 port 65520

remixd -s  --remix-ide 

-s 表示 –shared-folder 意指連接到 Remixd Web IDE 的檔案路徑 https://remix.ethereum.org

執行以下指令,

remixd -s . --remix-ide https://remix.ethereum.org

啟動後,在終端機如果看到 remixd is listening on 127.0.01:65520 就表示本地端正在等待 web 端啟動,

接下來就到 https://remix.ethereum.org 網站連結 localhost,

▼ 到 Remixd IDE 左側 Workspaces 選擇 connect to localhost,點擊 Connect,

Connect 連結完後,當看到 local 的 command line 出現 setup notification for .

就表示成功連結 local file 跟 remixd web IDE 囉。

以上詳細流程也可以參考官方教學





2-3 存放 NFT 文件檔案至 IPFS

將圖檔上傳至 IPFS,InterPlanetary File System (星際檔案系統),

它是實現檔案的分散式儲存、共享和持久化的網路傳輸協定,

因為區塊鏈技術一直以來的核心目標是去中心化,在 NFT 藝術品的世界也希望能把資產透過去中心方式儲存,

而 IPFS 就是能達到存儲跟傳播都去中心化。

大班哥選用一個 IPFS 服務平台 – Pinata

▼ 假設大班哥要製作兩張的 NFT 為下圖:

▼ 需要上傳兩份檔案,分別是:

1. 智能合約裡的 BaseURI 需要用到的 IPFS 的 URI :

一個 IPFS 位置資料夾底下放了 2 份 JSON 檔案,分別是對應到 NFT #1, NFT #2,

可以注意到上圖中 bigbenfun_content 的 CID,即是 IPFS 的網址, ipfs://{CID}

2. 圖片檔案: 兩張圖片的 IPFS 位置

▼ 其中 NFT #1 對應的 JSON file 內容為,裡面還可以設定屬性,像是大班哥就定義了 Color 屬性

{
  "image": "ipfs://QmSxPN8U6yjdv5DTn8NPrD_______USGeY3rC7RqW/1.png",
  "name": "BigBenFun #1",
  "description": "For fun",
  "attributes": [
    {
      "trait_type": "Color",
      "value": "Green"
    }
  ]
}




2-4 測試合約/部署合約/鑄造 (Mint) NFT

Compile 編譯合約:

▼ 回到剛剛設定好的 Remix 那邊,選取剛剛寫好的智能合約 bigben_nft.sol (內容就是上面方分享的 Solidity 程式碼)e

▼ 按下 Compile 開始編譯

Deploy 部署合約:

▼ 開始部署合約到測試網,輸入合約裡 constractor 定義的三種參數

合約名稱: BigBenFun、token 名稱: BBF、最大提供數量: 15

如下圖看到做邊出現合約地址後,表示部署成功,也可以從 console 訊息知道

鑄造 (Mint) NFT

開始可以透過合約來 Mint NFT

▼ 鑄造 (Mint) NFT,透過 Metamask 彈窗確認, 共付款 0.08 E + gas fee

失敗原因 1:沒有打開合約 Sale

▼ 如果發現 Mint 失敗看到「Sale must be active」,表示合約需要手動開啟

點選合約 Function 的 flipSaleState() 即可

失敗原因 2:Value 不夠

需要把 value 調整到 Mint 的價錢,因為 1E = 109 Gwei,

所以像是大班哥合約是設定 Mint 一個是 0.08 E, Value 需填入 80000000 單位 Gwei

轉換 E 可以用這個工具 -> https://etherscan.io/unitconverter





2-5. 到 Opensea 上查看 Mint 出來的 NFT

▼ 可以到 Opensea 測試網 https://testnets.opensea.io/,查看剛剛 Mint 出來的 NFT #1 跟 NFT #2

▼ 可以看到上面提到的 JSON 檔案裡面有設定屬性,也可以在 opensea 上面看到 Color 是 Green

小結

目前這篇是大班哥目前部落格資訊量最大的一篇,從語言介紹、合約撰寫、測試部署等,只算是簡單帶過,每個主題都可以單獨寫成一篇文章,之後有時間會在發佈文章,目前算是把整個 NFT 的智能合約流程走過一次,分享給大家,希望大家能對智能合約能夠有更近一步理解,以上



11 則留言

  1. HsinHan

    大班哥的文章圖文並茂、深入淺出!
    對於那些想要創造自己的 NTF 的程式小白是一個很棒的入門磚~
    希望以後大班哥還能多多產出這類的高質量文章🤘

    • 大班哥

      感謝支持,會繼續產出相關文章!

  2. 匿名訪客

    想請問大班哥~如果上船一萬張 是需要付多少費用呢

  3. Wayne

    請教版主, 如果已經手動上傳100個( no.1~100), 剩下的900個想要用合約上傳,是否可行,合約參數要如何修改? 謝謝

    • 大班哥

      不太懂你的意思,猜測你想問的 NFT 顯示圖檔跟 meta data 要上傳的事情,其實跟合約本身沒有關係,合約只是幫你指到一個 URI 位置,真的要改可以透過 set baseURI()

  4. Realbob

    很棒的文章,請問版主,在用pinata 上傳大量NFT後(假設200張),用智能合約連通前端的網站給人購買(也就是Mint),這樣一個過程賣方會需要支付200次上架費的Gas fee嗎?感謝回答🙏

    • 大班哥

      不會喔,其實你可以理解成,要付出 Gas Fee 就是請以太坊上的礦工幫你做事(驗證區塊鏈),例如合約部署、合約互動 call function 等,
      而你上架到 pinata 的 200 張圖跟合約無關,你只是在合約裡指定圖檔位置, 當初寫合約時,就制定好規則 mint 完當下就自動產生,
      例如第一個 mint 的人 NFT 圖檔位置是指到 {baseUri}/0,第二個人是 {baseUri}/1… {baseUri}/200,
      所以是不用支付你說的「200次上架費的Gas fee」

  5. Dean

    感謝大班哥提供教學

    我有一個疑問,如果我製作了幾千張NFT,一口氣把幾千張都mint到ETH的網絡上,能夠比較省gas fee嗎?

    如果已經mint了所有NFT,我同時把幾個NFT轉移到另一個錢包,能夠只付一次gas fee嗎?

    謝謝

    • 大班哥

      Q: 「如果我製作了幾千張NFT,一口氣把幾千張都mint到ETH的網絡上,能夠比較省gas fee嗎?」
      A: 這取決於 NFT 合約 Mint 時要再鏈上消耗掉的指令資源,像是知名項目 Azuki 就自己開發 ERC721a 標準,當 Mint 多張時能剩下之前好幾倍的 gas fee

      Q: 「已經mint了所有NFT,我同時把幾個NFT轉移到另一個錢包,能夠只付一次gas fee嗎?」
      A: 這我不確定,已經 Mint 出來的 NFT 就像是一個貨幣 Token,要一次打包轉移到其他錢包所收的 gas fee 就是看當下鏈上本身的費用跟礦工費

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。