原文作者:
透過這篇文章帶大家學習了一種新的 mint 方式「荷蘭拍」以及其原理,另外帶大家認識了一個 2個億的單字 totalBids。
上篇寫 NBA的文章 寫的太累了大傷元氣,想休息一段時間再寫的,結果web3的世界實在是太精彩,每天發生的大新聞太多,大周末的又被迫營業。

今天一個叫 Akutar 的 NFT 項目因為合約 bug,導致 11539 個 ETH ,價值 3400 萬美金 2 個億人民幣的錢永久取不出來被鎖死了,2個億啊!
我們打開合約地址看看這 2 個億來眼饞眼饞,想像一下 Akutar 團隊望著這一串數字的抱頭痛哭的心情。
首先介紹一下 Akutar,從官網的描述和他們 twitter 可以看出,這不是一個土狗項目,相反是一個很用心的高質量項目,不論是從精細的畫風還是 roadmap 描述質量都很高。

它的發起人是一位知名的 棒球運動員 Micha Johnson,起源於他無意間聽到一位黑人小男孩與母親的對話,小男孩問母親宇航員能否是黑人,所以 Micha Johnson 決定發行一系列夢想成為宇航員的戴著頭盔的黑人小男孩,一個還算美妙的故事。

那麼看著這麼暖心的故事背後的 NFT 這麼就砸了呢?從某種程度上還是項目方對於賺錢的渴望大過於所謂的暖心公益,從而搬起石頭砸了自己的腳,因為它使用了一種比較獨特的荷蘭拍方式。
傳統的拍賣方式是設置一個低價,然後大家向上叫價,最終出價最高者可購買,這是英式拍賣;荷蘭式拍賣則是先設置一個最高價,然後逐漸的降價,最終有人在某個價格點出手將其買下來,荷蘭拍更考驗人性,因為每個人都想等最低價,但是都怕別人先於自己購買。
Azuki 就是用的是荷蘭拍,但是 Akutar 相比於 Azuki 的拍賣方式又做了改變, Azuki 的價格是動態下降的,從而買的越晚價格越低,買的越早可能就吃了虧價格會高。 Akutar 則加了一個“退款”規則,該規則看起來好像對用戶更友好但是我認為實際上是想割更多的錢。
這如下圖所示,拍賣起始價格是 3.5ETH,每過 6 分鐘降低 1 ETH,最終最低價格購買的人將成為標準價格,其他高於該價格購買的用戶將獲得退款,比如最後最低出售價格是 1.5ETH,那所有高於 1.5ETH 出價的人均會獲得差價的退款,這種實際上是想讓用戶放心大膽的去買,不要蹲守最低價,即使買高了也能退款。

所以 Akutar 會有一個巨大的資金池用於存儲所有用戶交的錢,這部分錢包括項目方自己應得的,也包括需要退給用戶的。這裡先科普一個知識,之前的文章中也提到過,智能合約的性質和你自己個人的錢包地址是一樣的,都是一個區塊鏈地址,可以接收、發送虛擬貨幣,當你在mint某個項目時,實際上是你先將錢打到項目合約地址,然後合約給你轉一個 NFT,即所有 NFT 的一級市場發售,錢都是先到了合約地址,再由項目方去進行提款操作,將合約裡面的錢提到自己的錢包中。
這次 2 個億被鎖死就是因為在提款這個步驟出了 bug ,因為區塊鏈智能合約不可篡改的特性使得出現了 bug 是沒法修的,傳統互聯網如果有個 bug 導致錢取不出來,修復迭代就可以,但是在 web3 中意味著這輩子你只能與這 2 個億隔空相望。
我們來看一下一些關鍵的代碼都做了什麼幫助大家理解原理,再分析到底是哪裡出了問題。
我們先學習一下荷蘭拍的原理,首先是獲取當前價格,這裡先獲取了最新區塊的時間 block.timestamp ,然後用當前時間減去開始時間 startAt 並除以 6(因為每 6 分鐘降價一次),從而獲取應該降價幾次 timeElapsed ,再用降價次數乘以降價金額計算出降價的總數 discount ,最終用起始價格 startingPrice 減去降價金額得到當前應該要支付的費用。在代碼中剛才提到的這些涉及到金額的參數其實都不是預先寫在合約中的,而是可以修改的變量,說明項目方給自己留了後門可以視情況隨時修改金額從而更好的揮舞鐮刀。

怎麼獲取價格清楚了,我們再看用戶出價的過程都發生了什麼,這部分代碼太長了我就不都貼了,挑重要的講。
先獲取了上面提到的當前價格,然後乘以用戶購買的數量 amount ,得到應該支付的總價totalPrice,再判斷用戶實際支付的價格value是否大於總價,如果大於說明錢給夠了接著向下執行。

這裡先定義了一個報價 bid 都包含了什麼,分別是 bidder 報價者地址, price 具體報價, bidsPlaced 總共購買數量,和 finalProcess 退款狀態, 0 是退款, 1 是已退款, 2 是取消退款。

接下來到了第一個埋坑的地方: totalBids 表示當前所拍賣出去的 NFT 數量,默認是 0,每次有用戶報價則加上用戶要購買的數量 amount,記住這裡,等會會用到。

然後埋了第二個坑:使用了一個叫 bidIndex 的參數用於存儲產生報價的用戶有多少人。記住這兩個參數, totalBids 存儲了總共賣出多少個 NFT , bidIndex 存儲了總共有多少人買了 NFT 。

再講一下項目方為用戶退款的過程,項目方要先點擊一個叫 processRefunds 的按鈕開啟退款,這個按鈕背後的邏輯是把所有出價的用戶全部循環處理一次,循環的次數就是剛才說的存儲出價人數的 bidIndex。處理的內容是先判斷該用戶 finalProcess 退款狀態是否為 0,0 表示尚未退款,如果為 0 的話繼續向下執行,將用戶當時的報價減去最低成交價,再乘以購買數量,則等於要退給用戶的差價 refund。
然後將該 finalProcess 用戶退款從 0 設置為 1,表示已經完成退款,從而該用戶不能再去退了。
參數 refundProgress 是記錄完成退款人數的,每退完一個用戶就會加 1,因為是按照出價人數bidIndex循環的,所以 refundProgress 和 bidIndex 是一致的,這裡其實沒有毛病,本來出價的人和退款的人就應該是一樣的,但是!接著向下看!

項目方提款的邏輯是怎樣的,又有什麼漏洞導致其無法提款?
下列為項目方進行提款的函數,即當項目方點擊 claimProjectFunds 按鈕後可以將錢提到自己錢包裡,這裡有三層校驗:第一層是先驗證當前是否已經結束了拍賣,如果結束進入第二層校驗退款人數是否大於報價人數,其實這裡項目方是好意,因為要確保每個人都退完了錢,項目方再提款,但就是這一層校驗出了問題,不知道你還記不記得 totalBids 是什麼意思?是售出 NFT 數量呀,不是報價人數!

你會問那這又怎麼了呢?一個人在報價的時候是可以購買多個 NFT 的呀,退款人數實際上是購買人數,你要求購買人數超過賣出 NFT 數量,但是每人又可以買多個,那隻要有 1 個人買了 2 個,就意味著購買人數永遠不可能大於賣出數量, 10 個人賣出了 11 個,你怎麼要求 10 大於 11 呢?
我們上 etherscan 看一下, refundProgess 的數量是 3699 ,說明共有3699人報價,但是totalBids 的數量是 5495 ,即共賣出了 5495 個,遠遠超過 3699 ,這輩子 refundProgess 都不可能大於 totalBids ,這2個億就永遠被鎖死在了合約中供後人觀摩。


所以是項目方寫錯單字了,本來應該是想寫 bidIndex 購買人數,結果寫成了 totalBids 賣出數量,一個單字價值 2 個億,這應該是全世界最貴的一個單字了,大家給我狠狠的記住這個單字totalBids,就是它值 2 個億!
通過這篇文章帶著大家學習了一種新的 mint 方式荷蘭拍以及其原理,另外帶大家認識了一個 2 個億的單字 totalBids。