在大班哥開始接觸到 NFT 後,進而認識到了以太鏈上的智能合約,又很好奇這樣的東西內部機制是怎麼用程式運作,
這篇文章會帶著大家完成一個簡單的智能合約,透過合約互動鑄造 (Mint) 出 NFT ,之後能到 Opensea 平台上看到。
以下主要分成幾部分介紹:
- 撰寫智能合約
- 設置開發環境,測試/部署合約
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 這邊會有兩個設定要做:
- 更改網路到測試網路(鏈),改到 Rinkeby
- 把帳號充值測試用 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 的智能合約流程走過一次,分享給大家,希望大家能對智能合約能夠有更近一步理解,以上
大班哥的文章圖文並茂、深入淺出!
對於那些想要創造自己的 NTF 的程式小白是一個很棒的入門磚~
希望以後大班哥還能多多產出這類的高質量文章🤘
感謝支持,會繼續產出相關文章!
太強拉 學習學習!
你好,文章裡我用的是 PINATA 這個 IPFS 服務平台,相關付費規則可以參考 https://www.pinata.cloud/pricing
請教版主, 如果已經手動上傳100個( no.1~100), 剩下的900個想要用合約上傳,是否可行,合約參數要如何修改? 謝謝
不太懂你的意思,猜測你想問的 NFT 顯示圖檔跟 meta data 要上傳的事情,其實跟合約本身沒有關係,合約只是幫你指到一個 URI 位置,真的要改可以透過 set baseURI()
很棒的文章,請問版主,在用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」
感謝大班哥提供教學
我有一個疑問,如果我製作了幾千張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 就是看當下鏈上本身的費用跟礦工費
大班哥你好,如果透過set baseURI()去更改智能合約中的圖片,這樣智能合約要在重新部屬到區塊鏈上嗎?有點不能理解,記得區塊鏈上的資訊不是都不能動嗎?怎麼樣透過這樣的方式完成nft的圖片更改?
Hi 你好,
不用再重新部署合約喔,只要是透過合約跟區塊鏈互動都是允許的,像是轉錢、轉移 NFT owner 等,所以合約要定義清楚很重要,
然後每個 NFT token 都有對應的 uri (uri 裡面才有對應的圖檔位置或屬性),可以想成一個網址,是已經是存在在鏈上了
可以透過 setBaseURI() 只是去改 NFT token 相對應的 uri,可以達到更改指向的圖片的 uri
一般來說這個舉動很危險,所以只能是合約 owner 才有權限呼叫 setBaseURI() function