智能合約教學(9) NFT去中心化交易所
依照預告,這次要來實作NFT的去中心化交易所
首先實際操作一遍NFT去中心化交易所看看:
https://vcckvv.github.io/SmartContract/Poolnft/
其中用到的合約程式碼為:
https://mumbai.polygonscan.com/address/0xFFC56B04A7D67fB0e565d9e938aba14c496C6060#code
1.首先還是一樣,要先有安裝MetaMask的瀏覽器外掛並且開啟起來,然後打開網頁後會要求連結MetaMask錢包、切換到Mumbai測試鏈
2.如果沒有Mumbai的MATIC幣就點選水龍頭網站連結去取得,也就是https://mumbaifaucet.com/
3.主網頁的最下方會顯示這個帳號擁有的NFT,不過一開始沒鑄造還沒有NFT,這個網頁也只會顯示這個合約的NFT,不會像OpenSea一樣另外顯示其他合約的NFT,所以接下來我們直接去鑄造新的NFT
4.點選鑄造新的NFT的連結:https://vcckvv.github.io/SmartContract/Poolnft/mint.html ,介面如圖
5.這裡上傳圖的方式和一般NFT使用IPFS網址的方式不同,這裡允許圖上傳到任何的網頁空間或圖床網站,甚至之後圖片被刪掉也可以上傳到其他網站並修改網址,而為了保證圖片的唯一性,這裡會讀取圖片的資料並計算其sha256的hash,然後資料上鏈後就無法再改變hash,之後當我們檢視NFT時,就能透過這個hash來檢驗圖片是不是相同的資料
這裡推薦的上傳圖的網站是github給的網頁空間,不過如果有其他用的順手的上傳圖的網站也可以拿來使用,要注意有些網站不允許跨域讀取資料,所以可能可以顯示預覽圖,但是卻無法讀取資料計算出hash,這個叫CORS問題,遇到這個問題就直接換一個網站就好
上傳圖完取得圖片網址然後輸入表單欄位,確定預覽圖和hash都有顯示出來,然後輸入標題和描述,按下「鑄造」的按鈕後MetaMask會跳出視窗要簽署交易,簽署完後等待一段時間上鏈消息出來就算鑄造完成,網頁底下會產生新的NFT連結可用來檢視
值得一提的是,Mumbai測試鏈好像沒goerli鏈那麼穩定,有時會有塞車的現象,所以不一定會在30秒內完成上鏈,這時就要點選MetaMask的加速交易的功能,來快點完成上鏈
6.檢視個別的NFT網頁的介面如下圖,如果擁有創造者或擁有者的身份,則底下還會有其他的介面可以使用
7.首先是擁有者具有的拍賣功能,可以設定拍賣結束時間和直購價、起標價,如果只想要讓人直購,那麼結束時間就設成現在或過去,這樣的話一開始拍賣時間就結束,只能用直購價購買
一旦開始拍賣,在NFT列表中就會在這個NFT上額外顯示「拍賣中!」的訊息,並且點進去會有投標的介面
如果沒有人投標或是投標者就是擁有者,則擁有者可以隨時停止拍賣,但如果有別的投標者,則不能停止拍賣
一旦拍賣時間結束,這時投標者或擁有者都可以進行「完成交易」的動作,這個動作會把錢和NFT轉交過去,此時投標者才正式擁有NFT,不過要注意其他人也可以在這個時候用直購價直接買下
8.擁有者還具有傳送NFT給別人的介面功能,這個功能在拍賣中的狀態時會無法使用
9.最後是創造者和擁有者都有的「重設JSON資料」介面功能,這裡可以改掉JSON標題、描述、以及圖片網址的資料,這個功能主要是為了避免上傳的圖片網址出問題,所以可以隨時進行修改
10.和之前聊天室的功能一樣,每個帳號都可以設定自己獨有的名字,可以到這個網頁去設定:https://vcckvv.github.io/SmartContract/Poolnft/name.html
11.接下來是介紹各種NFT列表的網頁:
全部的NFT列表 - https://vcckvv.github.io/SmartContract/Poolnft/all.html
某人擁有的NFT列表 - https://vcckvv.github.io/SmartContract/Poolnft/own.html
某人鑄造的NFT列表 - https://vcckvv.github.io/SmartContract/Poolnft/creator.html
某人拍賣的NFT列表 - https://vcckvv.github.io/SmartContract/Poolnft/auction.html
某人投標的NFT列表 - https://vcckvv.github.io/SmartContract/Poolnft/bid.html
基本功能很簡單,除了全部NFT列表不用查詢外,其他網頁只要輸入帳號位址或是帳號的名字然後點選查詢的按鈕,之後就可以看到查詢的結果
而某人投標的NFT列表還要另外輸入搜尋區塊的範圍,之所以做成這樣,是因為這部份的資料沒有存在合約之中,而是使用event的方式去記錄起來,然後透過之前提到的queryFilter函數來讀取出來(需要指定搜尋區塊的範圍)
值得注意的是,Mumbai測試鏈的RPC Server在這部份運作的效能似乎比較低,在搜尋較大的區塊範圍時會拖很多時間才會回應,所以網頁有時會有卡住的樣子,因為是免費使用的RPC Server,所以這裡也不好要求太多
12.最後是Log記錄網頁:https://vcckvv.github.io/SmartContract/Poolnft/log.html
一樣是用event的方式去記錄起來,所以要輸入搜尋區塊的範圍
因為ethers.js沒有提供「時間->對應的區塊數」的函數功能,所以這裡輸入的是區塊數,然後自動顯示對應的時間
一開始預設的範圍是「24小時之前的區塊~現在的區塊」,目前的Mumbai測試鏈的出塊速度大約是6秒,所以實際在javascript中是這樣設定的:
let nowBlockNum = await provider.getBlockNumber();
startBlock.value=nowBlockNum - Math.floor(24*60*60/6);
endBlock.value=nowBlockNum;
雖然不算精準的設定方法但是能用就好
13.這次的做法是把NFT和交易所功能寫在同一個合約中,所以沒有之前ERC20代幣的approve問題,拍賣系統可以直接把NFT轉換到得標者的帳號而不需要再另外去做approve()的動作,因為同一個合約的內部操作都可以使用不需要權限的內部函數,比如_transfer(),一般OpenZeppelin範例的內部函數名稱都會加底線
另外一提的是,一般NFT交易所都會允許加入不同的外部NFT合約,不過那樣一來的話就需要進行嚴格的安全性檢查,這裡因為嫌麻煩就沒另外加這個功能,所以這裡所有可以拍賣交易的NFT都是這個合約本身鑄造出來的
----------------------------------------------
Poolnft的合約主要程式碼:
https://github.com/vcckvv/SmartContract/blob/gh-pages/Poolnft/Poolnft.sol
這次的合約程式碼其實沒有什麼太多好說的,用到的都是之前已經提過的技術,儲存ID集合也是使用之前用過的紅黑樹資料結構,不過還是有幾點值得提一下:
在ERC721中提供了「_mint()和_safeMint()函數」以及「_transfer()和_safeTransfer()函數」,Poolnft裡面一律使用_mint()、_transfer(),主要原因是因為_safeMint()和_safeTransfer()有可能會呼叫外部合約的函數,也就是IERC721Receiver介面的onERC721Received
這個可以在https://github.com/vcckvv/SmartContract/blob/gh-pages/Poolnft/ERC721.sol 的931行上看到,然後_checkOnERC721Received()函數會被_safeMint()和_safeTransfer()所呼叫
雖然名稱加上safe,不過在網上查了一下資料,發現有些合約就是因為這個函數呼叫而產生漏洞,所以這裡就不使用以免產生問題
----------------------------------------------
這次繼承的是IERC721Enumerable,而不是上次的ERC721Enumerable,所以裡面的函數都需要自己實作,包含totalSupply()、tokenOfOwnerByIndex()、tokenByIndex(),ERC721Enumerable的主要功能在於提供可以依序列出所有NFT ID的函數
另外supportsInterface()則是用來顯示這個合約有哪些ERC721的實作功能,這裡直接照抄ERC721Enumerable的函數內容就好:
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721) returns (bool) {
return interfaceId == type(IERC721Enumerable).interfaceId || super.supportsInterface(interfaceId);
}
----------------------------------------------
function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721) {
require(!auctionMap[tokenId].isActive);//拍賣時不能轉移
super._beforeTokenTransfer(from, to, tokenId);
if(from!=address(0) ){
ownMap[from].remove(tokenId);
}
ownMap[to].insert(tokenId);
}
_beforeTokenTransfer這個函數之前就只是照抄內容,不過這次要檢查不能在拍賣時進行轉移,同時如果進行轉移,就要把使用者擁有NFT ID集合進行更新的動作
----------------------------------------------
這次的合約程式碼就講到這裡,另外之前測試發布合約的時候遇到了一件特別的事,MetaMask直接把我發布的Poolnft合約顯示成「Rug Pull All」
我猜想可能因為這個ERC721合約有涉及到交易和提領功能,和常見的ERC721合約不同,所以誤判成詐騙合約,不過之後MetaMask似乎有做過更新,所以後來發布的合約就沒有出現相同的誤判情況
----------------------------------------------
這次的網頁很多,程式碼都放在這個github目錄底下:https://github.com/vcckvv/SmartContract/tree/gh-pages/Poolnft ,接下來簡單講解一些值得說的東西
----------------------------------------------
這次網頁程式有簡單額外判斷是否有安裝MetaMask:
if(typeof window.ethereum === "undefined"){
alert("error : 尚未安裝MetaMask,此網頁需要安裝MetaMask才能使用");
throw new Error();
}
----------------------------------------------
NFT的資料需要用到base64和JSON的技術處理,好在大部份用內建的函數就可以搞定:
base64解碼可以使用atob(),編碼則是使用btoa(),但要注意沒辦法直接把UTF8資料塞入btoa()中,所以另外找了個Utf8Encode()函數來轉換資料:
btoa(Utf8Encode(jsonStr) )
而要讀取JSON資料可以使用:
let jsonObj=JSON.parse(decodeURIComponent(escape(uri) ) );
這裡為了正確處理UTF8文字,所以必須加上decodeURIComponent(escape())
的處理
----------------------------------------------
因為有些使用者輸入資料會顯示在網頁上,所以要做一些處理來避免「代碼注入」:
function toSafeUrl(url){
return url.replace(/</g, "%3C").replace(/>/g, "%3E").replace(/'/g, "%27").replace(/"/g, "%22");
}
function toSafeText(str){
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
基本上就是把一些可能會被錯誤解讀的字元改成URL編碼或HTML entity碼,以避免任何可能的問題
另外因為JSON.parse()
函數允許"\n"之類的字串自動轉換成換行字元,所以要對反斜線字元、雙引號字元、換行字元做特別的處理,這樣讀取起來才不會出差錯:
function toJSONText(str){
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
}
----------------------------------------------
這次的講解就到這裡,目前沒有預定接下來要講解什麼,雖然還有很多題材可以說,像是如何更新合約內容,或是常見的合約漏洞,不過因為內容更為深入,講起來也沒有像聊天室或去中心化交易所那樣有趣,所以沒有特別想說,大概之後心血來潮才會再來寫些東西
最後來簡單總結下這次學習的心得:
其實在學寫智能合約之前就覺得EVM是還不成熟的技術,實際寫了之後就更加確信了這點,寫起來限制很多,不只要注意程式不能寫太大,而且要擔心gas費不能用太多,更重要的是如果沒有花時間研究常見的漏洞,會很容易一不小心就寫出漏洞來,我寫的二個去中心化交易所合約都有遇到隔了好幾天才發現有毀滅性的漏洞
不過這還只是小事,對外行人來說智能合約可能就像是「Code is law」的具體呈現,是很去中心化的東西,不過實際上看了一些項目的合約會發現,程式碼可以設置所謂的「管理員功能」,在極端的情況下,管理員甚至有能力把合約內容換掉(比如使用代理合約的技術),也就是說管理員想做惡的話隨時可以換成惡意合約的內容,然後捲款跑人,這種東西一點也不去中心化,不過卻可以騙到那些不會看合約的人,讓人覺得把錢放在裡面就不會被搞,講難聽點,在去中心化的系統裡面構造一個可以中心化的系統,這東西就是幣圈的毒蘋果
依照我個人的觀點來看的話,EVM系統應該遲早會大改,如果不改的話大概遲早會被其他競爭者取代掉,雖然那可能是好幾年以後的事情