今天想要介紹的 NFT further function 是白名單,也是在可以在 NFT 的行銷上經常看到的一種手法。接下來三天我會詳盡的介紹白名單是什麼以及白名單的實作方法有哪些?
事實上,白名單不只隸屬於 web3 或 NFT 的世界。
先翻譯翻譯,什麼叫做「黑名單」。黑名單就是一些你不喜歡的人,以 Facebook 為例,你的封鎖清單就算是你的黑名單;或以店家為例,他們可能會將一些奧客、吃霸王餐的顧客列為黑名單,以防他們再次上門造成諾大的損失。
白名單則與黑名單相反,它更像是一個 VIP 清單,正面表列了所有可以進行一些特權的人物。例如:宴會的邀請名單、貴賓的邀請名單等。
而網路上常會用白名單的功能來達到「防禦惡意攻擊」的效果。例如防毒軟體會正面表列可以使用的應用程式、網頁。原因是因為惡意攻擊的方式千變萬化、百密總有一疏,利用白名單便可以只讓電腦運行確認過無害的程式與網頁來達到防禦效果。
在 NFT 的世界中,white list 常代表的是「擁有先行購買 NFT」、或是「保留具有購買 NFT」的人。
例如今天有一個項目方的地板價為 0.01 ETH
,開賣日期為 2022/09/22 00:00
。在這個日期之前項目方可以透過舉辦活動的方式來抽獎並宣傳他們的 NFT,而白名單就可以是其中一種方式。這時抽到白名單的人可能可以在 2022/09/21 00:00
就開始 mint 他們的 NFT,或是可能可以用更低的價格 0.005 ETH
mint 到一個 NFT。
白名單的實作方法有非常多種,而各種方法相差非常多,因此我會多花點心思在實作方面上。
今天要介紹的 white list 是一個簡單且常見的作法 -- 寫在鏈上。
一般若我們想要儲存一個相同的資料結構,我們會直覺地想到 array。但是使用 array 在鏈上儲存會發生一些問題:存取時耗費大量 gas fee。
使用 array 來當作白名單的著名例子就是前陣子非常紅的嘟嘟房 NFT 事件。
address[] whitelistedAddresses;
//...
function whitelistUsers(address[] calldata _users) public {
require(controllers[msg.sender], "Only controllers can operate this function");
delete whitelistedAddresses;
whitelistedAddresses = _users;
}
function isWhitelisted(address _user) public view returns (bool) {
for (uint i = 0; i < whitelistedAddresses.length; i++) {
if (whitelistedAddresses[i] == _user) {
return true;
}
}
return false;
}
由他們的合約可以看出是使用一個 array 來儲存白名單,並由擁有 controller
的身分的人才能更新白名單。
但是在 isWhitelisted()
這個 function 中便可以看出一些端倪(在 presaleMint()
中會使用)。由於 whitelist 是一個 array,因此只能使用 iteration 的方式來達到遍歷整個 array 來查詢 address _user
是否存在於這個 array 中,而遍歷的複雜度在最差的情況(_user
在第 n-1
個 element)是 O(n)
。
因此,如果 _user
是在整個 array 的後段,因為 for loop 中的迴圈需要執行更多次而導致使用者需要付出的 gas fee 上升。
下述方法不一定較好,只是我的個人意見!請勿使用於何商業用途,不保證其安全性。
因此為了不付出如此高的 gas fee,一般而言會使用 mapping 這個資料結構來做為 white list。
這邊我模仿了 Doodle NFT 的白名單做法為例:
下方的 mapping 中 address 對應的是 每個 address 可以 mint 的數量。
mapping(address => uint8) private _whitelistedAddresses;
就更新白名單而言,嘟嘟房的使用 array 作為白單時可以直接 delete
整個 array(這時 whitelistedAddresses
會 == []
), 但若使用 mapping 就無法直接全部刪除再一起更新。
因此這裡只能透過 iteration 來達到更新全部的 mapping key 與 value。(因此也是各有好壞的)
function setWhitelists(address[] calldata addresses, uint8 numAllowedToMint) external onlyOwner {
for(uint256 i = 0; i < address.length; i++) {
_whitelistedAddresses[ address[i] ] = numAllowedToMint;
}
}
注意其中的 calldata 是一種只有在 external function 才可傳入的資料型態,而它儲存的方式與 memory 較類似,詳細見此。
function isWhitelist(address _user) public returns (bool){
if (_whitelistedAddresses[_user] > 0) {return true;}
return false;
}
function mintWhitelist(uint8 numberOfTokens) external payable {
uint256 ts = totalSupply();
require(isWhitelisted(msg.sender), "Not an address in whitelist"); // 不屬於白單的人
require(ts + numberOfTokens <= MAX_SUPPLY, "Purchase would exceed max tokens"); // 超過 totalSupply
require(PRICE_PER_TOKEN * numberOfTokens <= msg.value, "Ether value sent is not correct"); // 付出的錢低於地板價
_whitelistedAddresses[msg.sender] -= numberOfTokens;
for (uint256 i = 0; i < numberOfTokens; i++) {
_safeMint(msg.sender, ts + i);
}
}
在白單的 mint()
中使用 isWhitelisted() => bool
來確認是否在白名單中。
今天了解了一般實作白名單多是使用 mapping
作為儲存白單的位置,同時也解釋了為何以 array
來儲存白單是不太適合的事情(畢竟有前車之鑑)!其實兩種方式都有明顯的優缺點,我身為一個 reviewer 只能學習在那個情況使用是不好的,但不代表任何情況使用都是不合宜的。
若有文章內有任何錯誤的地方歡迎指點與討論!非常感謝!
歡迎贊助窮困潦倒大學生
0xd8538ea74825080c0c80B9B175f57e91Ff885Cb4