[Solidity Note] - 在區塊鏈上最好的白名單實作方式?試試看 Merkle Tree 吧

Introduction & 前言

Mint Fail

相信玩 NFT 一段時間的人一定都聽過嘟嘟房事件,這篇文章將會記錄該怎麼處理 NFT 白名單的方法,且可以避開高額的氣費。

如果不知道這是什麼,可以參考 Rex 前輩寫的文章 嘟嘟房NFT出包事件懶人包,如果簡單說就是智能合約的白名單寫法沒有寫好,導致持有白名單的人 Pre-Mint 發生了需要支付高額的氣費,而且還 Mint 失敗。


Summary & 摘要

這篇文章不會檢討或探討這個事件的問題,只會記錄透過這個事件筆者開始學習關於紀錄白名單的方法心得。

本篇文章預設學習前的基本條件需求:

本篇文章將會學到:

  1. 花費少許 ETH 即可達到對智能合約的白名單列表的驗證及儲存
  2. 每次對智能合約更新白名單不需要再花費高額的 ETH

白名單?

NFT 開始火紅之後越來越多人從 Web2 進入到 Web3 世界,相信大部分的人都沒有什麼區塊鏈的知識,例如筆者…

在台灣越來越多團隊開始發行 NFT 之後,幾乎常常都有可以學習的地方,筆者相信不是壞事,畢竟越來越多人願意接觸甚至開始有公司願意出資在這上面,代表越多的人來研究這塊,一開始的 Web2 肯定也是有很多問題是後來慢慢被克服的。

在發行 NFT 之後,大部分的團隊都會碰到一個問題,我需要保留一些名額給特定的人,讓他們保證可以買到,或者保留一部分的名額,給其他的 KOL 抽獎,這些一定可以 MintNFT 的名額簡稱白名單。

KOL中文是什麼意思?如何分辨「網紅」與「KOL」?

這些白名單通常不是小數目,可能幾十位,甚至幾百位,但這些名單我應該怎麼讓合約知道,而且在合約程式碼判斷的時候可以做檢查呢?在 Web2 的做法就是去打 API 跟後端拿白名單,在 Web3 肯定很多人一開始的想法都是把這些名單直接寫在合約內。

雖然 Web3 也可以透過 ChainLink 去打外部 API,但這邊先不討論。

預言機 Chain Link

玩了一陣子以太鏈的人一定都知道,不管呼叫合約或者做任何交易都需要支付一定的氣費,而這些費用會依造你交易的複雜程度決定。

【新手必讀|加密小知識】關於礦工費(Gas Fee),你不可不知的 6 件事

這件事情就從這裡拉開序幕,由於嘟嘟房的合約犯了智能合約很嚴重的錯誤,把白名單存在陣列裡面儲存,在持有白名單的人 Mint 的時候,就會去跑迴圈檢查陣列,由於陣列長度可能很長,這時候氣費計算出來就需要花費較高的以太才可能交易成功。

可以觀察一下嘟嘟房的合約,合約的白名單存在一個叫 whitelistedAddresses 的陣列,這就是這篇文章出現的原因。

陣列存放白名單

在嘟嘟房當天白名單可以先 Mint 發生了 Out Of Gas 的事情之後,許多群組都開始討論這個,筆者決定著手研究大部分處理白名單的方式都如何處理。


解決方案一

大部分寫智能合約會避開使用 Array 去存資訊,也會避免使用迴圈去跑陣列,主要都是為了避免花費大量的氣費,在白名單使用陣列的替代方案最快速的方式就是使用 mapping

mapping 寫起來會感覺很像是陣列,但其實他是類似 hash tables,詳細可以參考 開發智能合約 - mapping 型別 (Day16),這邊就不仔細探討。

要解決前面提到可能花費高昂氣費的白名單問題,需要解決兩件事情:

  1. 把大量的名單存在智能合約內上鏈,需要花費不少的費用去存這些資訊
  2. 跑陣列一筆一筆檢查你是否在白名單內,會造成礦工難以預估你的氣費,造成容易 Out Of Gas

解決方案一只能解決第二項,就是使用 mapping,解決方法如下:

1
2
3
4
5
6
7
// 改用 mapping 存放白名單
mapping(address => bool) public whitelistedAddresses;

// 檢查是否在白名單內(修改原本嘟嘟房的函示)
function isWhitelisted(address _user) public view returns (bool) {
return whitelistedAddresses[_user];
}

這邊甚至可以優化一下原本的合約檢查方式,改用 modifier

1
2
3
4
5
6
7
8
9
10
// 優化檢查使用 modifier
modifier checkWhiteList() {
require(whitelistedAddresses[msg.sender], "user is not whitelisted");
_;
}

// 任何一個想檢查白名單的 Function
function preSaleMint(uint256 _mintAmount) public payable checkWhiteList {
//...略
}

解決方式二

這個就是本篇的重點了,原本的解決方式一只能解決 Out Of Gas,但在剛開始存白名單還是不能避免花掉一些 ETH,如果你的白名單很少,那就無所謂,但如果你像嘟嘟房的白名單有 898 筆那就很可觀,尤其在以太坊壅塞的時候。

898筆白名單存放在陣列內

陣列從 0 開始,所以是 0-897,總共 898 筆,交易紀錄可以看這邊 0x8c259c8b199826c9820c72c608fa61e52b687877e416073f9daa97138bfb2301

嘟嘟房 Mint NFT 的時候剛好是 NFT 比較冷清的時候,那時候 Gas Fee 還算低,就已經需要花費 0.689 ETH 去上傳白名單,以當時的價格約莫在四萬八接近五萬台幣。

想像一下今天小幫手上傳白名單之後,啊,不小心漏掉一筆,怎麼辦?只好再傳一次,也許十萬就這樣沒了~

這時候就該本篇紀錄主角出現了,Merkle Tree


什麼樹?

Merkle Tree

Merkle Tree 又被稱為雜湊樹,是一個存儲 Hash 值的一棵樹,其實 Merkle Tree 不止在白名單才被利用,早在密碼學就很常出現,然後像是 比特幣 的交易資訊。

*詳細可參考 Merkle tree–默克爾樹 *******文章的 merkle tree的數據結構

要說 Merkle Tree 提供了什麼好處?簡單說就是可以把好幾筆資料透過 SHA-256 的計算後,最後產出一個 Merkle Root,也就是上圖的 Top Hash,在區塊鏈礦工可以不用一次下載全部的交易紀錄,只需要透過某些節點即可驗證這筆交易是不是真的或者假的,後面我們會再說說怎麼驗證的。

首先通常這棵樹都是顛倒顯示的,根在頂部,葉子在底部;要了解 Merkle Tree 要先知知道這棵樹的基本概念,這棵樹有三種類型節點:

  1. 葉節點(Leaf Nodes) - 位於整棵樹的最底部,通常指資料經過 SHA-256 的計算後產出的結果層,像是上圖 Hash 0-0、0-1、1-0、1-1 那層。
  2. 父節點(Parent Nodes) - 可位於樹的不同層級,但一定都是在 葉節點(Leaf Nodes) 之上,從 葉節點(Leaf Nodes) 出來之後會兩兩一組組成一個 **父節點(Parent Nodes)**,直到最後剩下最後一個節點,父節點只會有最多兩個子節點,最少一個。

    如果存在基數的筆數資料,將會複製自己的節點,然後在計算 SHA-256 產出的結果,如下圖

    基數的筆數

  3. 根節點(Root Node) - 位於整棵樹的最頂端,也就是最後算出來的那個 SHA-256 總和,任何一棵 Merkle Tree 都只會有一個 **根節點(Root Node)**,而我們需要的就是這個 **根節點(Root Node)**。

怎麼計算?

在說怎麼使用之前要先說說這棵樹怎麼來的,最簡單就是看圖解釋,先看看下圖。

樹的長出方式

每一筆資料都會先經過 SHA-256 去計算,運算結果,是一個 64 bytesHEX(十六進位)字串。

1
2
HA = SHA256( SHA256(TxA) )
HB = SHA256( SHA256(TxB) )

然後在兩兩一組,先把二個字串連接(concat)在一起,變成一個 64*2=128 bytes 的字串,再透過 double SHA-256 計算,以次類推,多筆資料就這樣一直 SHA256() 上去,直到最後拿到根節點。

1
HAB = SHA256( SHA256(HA + HB) )

正因為我們都是透過 SHA256() 去計算拿到字串結果,只要資料稍微有點不一樣,就會影響整棵樹最後的 **根節點(Root Node)**,這時候我們便可以拿著 根節點(Root Node) 去驗證我們起始的資料正不正確。

改變資料後計算結果會錯誤

只要中間被動過,最後結果就會不一樣,像是 TxG 被改為 TxP 最後結果就會從 Habcdefgh 變成 Habcdefph


如何驗證?

聰明的你一定想到了最底層的資料就是我們的白名單列表,每一個地址我們都會透過 SHA256(),最後組出 Merkle Tree 並且拿到 根節點(Root Node) 又稱 Merkle Root

但是 Merkle Tree 會怎麼驗證呢?我們可以簡單透過下圖了解驗證方式。

驗證所需要提供的值

假如我們有一個地址 TxG 想要驗證是不是在白名單內,我們必須要先持有 Hh Hef Habcd,這三個用 SHA256() 產出的結果(藍色框框),然後依然按造上面我們說的產生方式,把 TxGSHA256() 產出 Hg 再透過產出的結果跟 Hh 產出下一個字串,最後看看我們拿到的 Merkle Root 是否符合我們原本預期的結果。

你可能會想,為何不把所有 葉節點(Leaf Nodes) 一次透過 SHA256() 去計算出 Merkle Root 呢?大部分的文章都會告訴你因為這樣我們只需要持有部分節點即可,就像 比特幣 如果自創節點,我可以使用 輕節點(註1) 去建立,不需要創建完整的整份節點,這種解釋對新手(如筆者)可能會比較難以理解,但如果使用白名單的概念來告訴你,你並不需要持有整份白名單直接去算出 Merkle Root,只需要拿某一個地址去驗證即可,就方便理解許多。

註1:簡易支付驗證節點,SPV(Simplified Payment Verification) node,不需運行完全節點也可以驗證支付,使用者只需要保存所有的 block header,詳細可參考 《詳解比特幣白皮書》-Simplified Payment Verification(簡化的交易驗證(SPV))

在這方便的年代我們不需要自己去造輪子,不需要我們自己把 Hh Hef Habcd 手動挑出來送去驗證,這個工作只要透過套件即可創建並且驗證 Merkle Tree,接下來就是進入我們實作的部分。


實際上陣

在開始前我們先懶人包知道一下我們需要做什麼:

  1. 透過 Merkle Tree 去對白名單列表產出 Merkle Root
  2. Merkle Root 儲存在智能合約上
  3. Mint Function 執行前透過驗證檢查該地址是否在白名單中
  4. 不需花費高額 ETH 去更新白名單列表

透過 Merkle Tree 去對白名單列表產出 Merkle Root

首先我們為了方便先打開 CodePen 的畫面,這也是一個線上 IDE,方便的是如果你有登入,儲存後下次進來程式碼還會在,並且可以把完成的程式碼分享給其他人看,如果修改程式碼並不會影響到你的存檔,他如果儲存的話在自動另外自己存一份他的版本

我們會需要用到 merkletreejs 去幫我們產生樹,另外還需要透過 keccak256 去幫我們做 散列(註2),目前 比特幣 使用 SHA256以太坊 使用 Keccak256

註2:散列又稱雜湊,意指透過雜湊函示(Hash Function),詳細可參考 維基百科 - 雜湊函式

merkletreejs

以太坊使用 keccak256 而 比特幣 使用 SHA256

點擊 JS 那欄的齒輪,把這兩個套件透過 Github 文章上的 CDN 網址引入使用

CodePen

接著點擊 Save & Close

儲存並關閉

對準備好的白名單的列表,進行散列且產出 Merkle Tree,這邊產出樹的第三個參數大部分教學都有寫需要加上去,筆者有加沒加似乎都可以,如果你沒有加上去卻發生了問題,可以試看看加 { sortPairs: true } 上去會不會解決

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 白名單列表,可寫死,可從後端拿
const whitelistedAddresses = [
"0xE09Eb3a29358dBDE39FE5f77F6597e1Dee0ceb97",
"0x7b399B9c5bb519D4258e61100cC5ef2F4Bd9c873",
"0x54Ac1F0366724cf30a6E91f32769D95540a18203",
"0xdC1Ca18bEeB3DFAEfCEe4aa91491B365A6F5aE12",
"0xe5A7B3B752d93514a97a23A9e134aBF6CF01DC6e",
"0x59734A5500faDCd7c64B3212538442Cfd26416Cb"
];

// 對所有名單進行雜湊 - SHA256
const leafOfNodes = whitelistedAddresses.map(address => keccak256(address));

// 產生出 Merkle Tree
const merkleTree = new MerkleTree(leafOfNodes, keccak256, { sortPairs: true });

console.log(merkleTree.toString());

MerkleTree toString

那串 3263cb94bd59231d2efe63bbad8915f59119fff6a872fa0fa7513f37fb19f141 就是我們需要拿去放在智能合約裡的 Merkle Root

使用 getRoot() 也可以拿到 merkleTreeRoot,將上面的程式碼改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 白名單列表,可寫死,可從後端拿
const whitelistedAddresses = [
"0xE09Eb3a29358dBDE39FE5f77F6597e1Dee0ceb97",
"0x7b399B9c5bb519D4258e61100cC5ef2F4Bd9c873",
"0x54Ac1F0366724cf30a6E91f32769D95540a18203",
"0xdC1Ca18bEeB3DFAEfCEe4aa91491B365A6F5aE12",
"0xe5A7B3B752d93514a97a23A9e134aBF6CF01DC6e",
"0x59734A5500faDCd7c64B3212538442Cfd26416Cb"
];

// 對所有名單進行雜湊 - SHA256
const leafOfNodes = whitelistedAddresses.map(address => keccak256(address));

// 產生出 Merkle Tree
const merkleTree = new MerkleTree(leafOfNodes, keccak256, { sortPairs: true }); // 拿整棵樹
const merkleRoot = merkleTree.getRoot().toString('hex'); // 直接拿 Root

console.log(merkleTree.toString());
console.log(merkleRoot);

取得 Merkle Root

Merkle Root 儲存在智能合約上

拿到 Root 後我們需要著手下去寫合約,透過修改之前我們的合約,把 Mint Function 多加上一個 modifier,如果不曉得怎麼創建合約並且推上鏈,可以先參考之前的文章 [Solidity Note] - 透過工程師的方式發布 基於 ERC721 的 NFT

在合約內我們需要引入 MerkleProof.sol,接著我們會寫幾項功能:

  1. Merkle Root 存在合約上的變數,以及可以替換這個變數的 Function
  2. 透過 modifier 加上 require 去驗證我們的地址是否在白名單內

請複製以下的程式碼,這是部分程式碼,但還是可以 Work,如果需要完整的合約程式碼可以到 0x31BF20772514551CBC7B897914321D98559a9FF9 去複製,完整的合約包含 設定 ERC721 合約及代幣名稱、限制一定金額才能 Mint、盲盒階段、新增刪除控制人員、領出合約餘額、刪除合約…等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract WhiteListByMerkeleTree is ERC721Enumerable, Ownable {
string public baseURI;
string public baseExtension = ".json";

bytes32 public merkleRoot;

uint256 public cost = 0.0001 ether; // Mint 價格
uint256 public maxSupply = 5;
uint256 public maxMintAmount = 1; // 一次最多只能 Mint 一個

constructor(
string memory _name,
string memory _symbol,
string memory _initBaseURI,
) ERC721(_name, _symbol)
baseURI = _initBaseURI;
}

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

// public
function mint(address _to, uint256 _mintAmount, bytes32[] calldata _merkleProof)
public
payable
isContractAvailable
checkWhiteList(_to, _merkleProof, merkleRoot)
{
uint256 supply = totalSupply();
require(_mintAmount > 0); // 每次必須鑄造超過 0 個
require(_mintAmount <= maxMintAmount, "You can only adopt 1 BigBenFun at a time"); // 鑄造的數量不可以大於每次最大鑄造數量
require(supply + _mintAmount <= maxSupply); // 鑄造的數量和當前發行量加起來,不可以超過最大總發行量
require(cost * _mintAmount <= msg.value, "Ether value sent is not correct");

for(uint256 i = 0; i < _mintAmount; i++) {
uint256 mintIndex = supply + i;
while(_exists(mintIndex)){
mintIndex++;
}

if (mintIndex <= maxSupply) {
_safeMint(_to, mintIndex);
}
}
}

// 查看使用者持有的 NFT MetaDate
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
require(
_exists(tokenId),
"ERC721Metadata: URI query for nonexistent token"
);

string memory currentBaseURI = _baseURI();
return bytes(baseURI).length > 0
? string(abi.encodePacked(currentBaseURI, Strings.toString(tokenId), baseExtension))
: "";
}

// 檢查是否在白名單內
modifier checkWhiteList (address _to, bytes32[] calldata _merkleProof, bytes32 _merkleRoot) {
require(
MerkleProof.verify(
_merkleProof,
_merkleRoot,
keccak256(abi.encodePacked(_to))
), "Address does not exist in WL");
_;
}

// 賦值給 Merkle Root
function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {
merkleRoot = _merkleRoot;
}
}

這邊快速解釋一下,正常我們可以透過 MerkleProof.sol 去幫我們驗證傳進來的地址是不是在白名單內,會需要 Merkle Root,這個參數我們存在合約內不寫死,透過 setMerkleRoot() 可以讓我們隨時修改,之後如果有新增餐除白名單,都可以在透過這個 Function 去更新,由於更新近來不再是一整段陣列,所以費用會少很多。

在檢查白名單部分這邊使用 modifier 把檢查包起來,方便之後其他 Function 使用,裡面就是 MerkleProof.sol 的使用方式,需要傳入三個參數,第一個就是我們之前上面提到的,如果需要驗證某個資料是不是正確的,就要把當初跟他是一組的對面那個參數拿出來,像上面的 Hh Hef Habcd 這三個值,把他組成陣列丟進來,怎麼拿出來等等會再說。

第二個參數是我們樹的 Merkle Root,如剛剛所提,這個值我們隨時可以透過 setMerkleRoot() 隨時去更改,達到隨時可以更新白名單。

第三個參數是要驗證的白名單地址,需要先透過散列 keccak256() 去把它轉成 bytes32,才能丟進 MerkleProof.verify() 去驗證。

最後只需要在 mint() 的後面加上 modifier checkWhiteList() 即可,不知道 modifier 用法的可以參考 solidity的函數修改器(modifier) 這篇文章。

tokenURI 這個 Function 就是 ERC721 一定會有的 Function,主要是返回你指定 Token IDURL,這個 URL 會指向 一張圖 或 一個 .json,詳細也可以參考之前的文章 [Solidity Note] - 透過工程師的方式發布 基於 ERC721 的 NFT,這邊如果要刪掉也可以,只是最後去 Test OpenSea 查看圖會是空的。

tokenURI & baseURI

*這邊是 ERC721.sol *******的程式碼,不複寫之後呼叫 tokenURI 就會回傳空字串

Mint Function 執行前透過驗證檢查該地址是否在白名單中

前面有提到我們要 Mint 的時候需要丟 MerkleProof 近來,我們先來改寫一下剛剛的 CodePen 程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 白名單列表,可寫死,可從後端拿
const whitelistedAddresses = [
"0xE09Eb3a29358dBDE39FE5f77F6597e1Dee0ceb97",
"0x7b399B9c5bb519D4258e61100cC5ef2F4Bd9c873",
"0x54Ac1F0366724cf30a6E91f32769D95540a18203",
"0xdC1Ca18bEeB3DFAEfCEe4aa91491B365A6F5aE12",
"0xe5A7B3B752d93514a97a23A9e134aBF6CF01DC6e",
"0x59734A5500faDCd7c64B3212538442Cfd26416Cb"
];

// 對所有名單進行雜湊 - SHA256
const leafOfNodes = whitelistedAddresses.map(address => keccak256(address));

// 產生出 Merkle Tree
const merkleTree = new MerkleTree(leafOfNodes, keccak256, { sortPairs: true }); // 拿整棵樹
const merkleRoot = merkleTree.getRoot().toString('hex'); // 直接拿 Root

// 上面已經拿到 MerkleRoot 了,這邊我們需要拿驗證的 MerkleProof
const mintUserLeaf = keccak256("0xE09Eb3a29358dBDE39FE5f77F6597e1Dee0ceb97");
const mintUserProof = merkleTree.getHexProof(mintUserLeaf);

console.log("mintUserProof:", mintUserProof);

大概如下面的概念,我們需要驗證 0xE09Eb3a29358dBDE39FE5f77F6597e1Dee0ceb97 是否在白單內,要先把他散列,然後透過 merkleTreejs 提供的 getHexProof(),去拿到藍色框框部分的值,這些結果會是一串陣列。

驗證地址

拿到這串陣列就是我們需要的 MerkleProof,之後需要丟到合約內讓 MerkleProof.verify() 去驗證。

要拿來驗證的陣列

不需花費高額 ETH 去更新白名單列表

回到合約這邊來,我們已經知道隨時可以產生新的 Merkle Root 去更新白名單,這時候我們先透過 setMerkleRoot() 去把 CodePenMerkle Root 傳進去。

請記得 merkleTreejs 產生出來的值我們需要在前面加上 0x 才可以設定到合約內,這邊將 0x5636657f8352b2652e3a43c07a6e8e7413d37f553633f901fd22c01cf605f9cf 設定進去合約。

隨時可以更改 Merkle Root 達到隨時更新白名單又不用花高額氣費需求

記錄在 0xd4c7bb2fb1cd2f0a08957c2f1ad6b2764304f40dde4a15d226cb86dcb217ff11

設定完記得再去看一下是不是有成功

取得 MerkleRoot

接下來就可以執行 mint() 了,把相關參數帶進去,這邊帶入地址 0xE09Eb3a29358dBDE39FE5f77F6597e1Dee0ceb97 還有 Mint 數量一個,加上剛剛從 CodePen 那邊拿到的 Proof 陣列,["0xff5222b3eecfb5315af7137810edbf828ba4069a2dcb427f4567c271e2d9cc20","0x64024fbe99ce8546990c17ddf281ceb42af726f7ff8aeae82ef56d6eeafa5dac","0x91a14a4390ff7871c7544a0533850a620de989cca0eb04658b885c63e645178b"]

Mint NFT

送出交易後檢查並成功 Mint 紀錄 0x3dc271c7dd217387cab97c3649d71b34212b9c5a621b6cba13907489c4bca59a

成功紀錄

仿造錯誤流程

為了驗證是否不對的地址會失敗,我們這邊傳入不在剛剛 codePenwhitelistedAddresses 陣列內的地址 0xf14cc09860DA951B0310d8192f74Ca1dB7a27C92 ,結果會拿到空陣列

錯誤的 Address 拿到錯誤的 Merkle Proof

但如果發起交易者聰明一點,直接使用你的合約 FunctionMint,自己造假白名單,這邊我們也可以來嘗試一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 造假的白名單 只有一開始兩筆一樣 其餘都不同
const fake_whitelistedAddresses = [
"0xE09Eb3a29358dBDE39FE5f77F6597e1Dee0ceb97",
"0x7b399B9c5bb519D4258e61100cC5ef2F4Bd9c873",
"0xf14cc09860DA951B0310d8192f74Ca1dB7a27C92",
"0xC87aC2a4Cec03e1913f13ce6fED4Dd4d880362AB",
"0x09a8Ba0EEaaA0D5c81ebf0E1C15C771FB3149f80",
"0x052EA8Ec718Ac9Bca7E37A9c91717ED219155B1f"
];

// 對所有假名單進行雜湊 - SHA256
const fake_leafOfNodes = fake_whitelistedAddresses.map(address => keccak256(address));

// 產生出假的 Merkle Tree
const fake_merkleTree = new MerkleTree(fake_leafOfNodes, keccak256, { sortPairs: true });

// 拿假的 Proof
const mintUserLeaf = keccak256("0xE09Eb3a29358dBDE39FE5f77F6597e1Dee0ceb97");
const fake_mintUserProof = fake_merkleTree.getHexProof(mintUserLeaf);

console.log("fake_mintUserProof:", fake_mintUserProof);

仿造錯誤的白名單拿 Proof

這時候會拿到一串陣列, ["0xff5222b3eecfb5315af7137810edbf828ba4069a2dcb427f4567c271e2d9cc20","0x76c410014698a9fed99b4053e2389e27c1ea14a0e5cbf4e3a0bdfbc4a7c8daa1","0xb536b4e32056b0e9000f4c0b2af31adceb92f637390dfdcaf606ca65a5902f8a"] 與原本的相比明顯就不同了,這時候我們繼續把他送進去 mint()

Mint 失敗

這時候就會跳出錯誤警告,這筆失敗交易記錄在 0xac16bec0714adf0c9077428a3d7d42e290487cc7b43dd73dd62dbc149b7ffa61


總結

整體流程

了解完整個過程其實過程是算很簡單的,開發者先行設定 Merkle Root 到合約上,隨時也都可以做更改,Merkle Root 就像是一個寶箱。

使用者在前端操作 Mint 的時候,前端網站從後端拿白名單,透過 merkleTreejs 提供的 getHexProof() 拿到 msg.sender(交易發起人) 的 Merkle Proof

最後把這個 msg.senderProof 傳進合約內去,透過 MerkleProof.sol 去幫我們驗證是否可以打開寶箱,這個過程也就猶如拿了你給他的材料去鑄造一把鑰匙。


Conclusion & 結論

每一次的錯誤都在使我們成長,不過有些教訓是真的滿貴的,嘟嘟房後續看起來是滿有心處理,但也花費了許多錢去補償,透過這個教訓這次也讓筆者學到了怎麼去實作白名單。

這次的完整合約 0x31BF20772514551CBC7B897914321D98559a9FF9 修正了一些之前第一次發布的問題,像是 totalSupply() 筆者一直認為是總發行數,結果是已經發行出去的數量,筆者也還在努力學習中,如果這篇文章中有任何錯誤,還請不吝嗇指出,感謝各位前輩,也希望和之前在學習前端時一樣,能跟一群志同道合的前端夥伴一起前進。

另外在 MerkleTree 的網站也有提供方便的生成器,如果不習慣用 CodePen 的人,也可以使用 MerleTree.js example 去直接產出 Proof

合約裡面有一個判斷寫錯了,關於盲盒的部分,判斷應該是 if(blindBoStep == true) 才對,但我寫反了,後來有透過 SetBlindBoxStep() 把變數改成 false…如果有看到的前輩請手下留情


參考網站