構建安全的 NFT 遊戲體驗。 牧民日記#1
我對 Wolf Game 的持續安全審計的經驗和學習。
source : https://muellerberndt.medium.com/building-a-secure-nft-gaming-experience-a-herdsmans-diary-1-91aab11139dc by Bernhard Mueller
牧羊人從不輸球。在 13,809 隻狼和羊的大營救僅僅幾個小時後,敏捷的牧羊人就已經預見到了遊戲的下一階段,並正在以光速編寫代碼。畢竟,玩家們渴望他們應得的 WOOL!必須做點什麼,而且要快。
Shepherd 的最新配方由兩種成分組成:
- 羊毛袋:發射 ERC20 的 ERC721 代幣。用戶可以從小袋中領取解鎖的 WOOL,並在二級市場上交易小袋 NFT。
- 分發/冒險遊戲:綿羊持有者可以選擇領取一個包含一定數量的 WOOL 的 WOOL 袋和與狼戰鬥以獲得更大的 WOOL 支出。
我的任務是不斷審核 Shepherd 的代碼並就設計決策進行諮詢。這篇日記記錄了我們在這個過程中遇到的一些相關問題和考慮。
作弊隨機性
區塊鏈遊戲通常涉及隨機結果。例如,在冒險遊戲中,羊以 50% 的概率擊敗狼。這聽起來很簡單,但做對卻出奇地難。
用戶可以恢復不需要的結果
如果用戶不喜歡隨機結果,他們可以嘗試恢復他們的操作。想像一個固定價格的鑄幣廠功能,用戶在 95% 的時間內收到普通的 NFT,在 5% 的時間裡收到寶貴的稀有 NFT。邪惡的用戶可以編寫一個調用鑄幣函數的智能合約,檢查結果並在正常 NFT 鑄幣時恢復,因此只接收(並支付)稀有的 NFT。
阻止來自智能合約的調用似乎可以阻止這種攻擊,但事實並非如此(另外,EOA 檢查是不可靠的)。你看,礦工可以被賄賂去做非凡的事情。例如,Flashbots 搜索者可以將鑄幣交易與第二筆交易捆綁在一起,根據隨機遊戲事件的結果有條件地還原較早的交易(查看 revertingTxHashes )。
隨機數可以提前洩露
當使用外部隨機源時,攻擊者可以等待交易出現在內存池中並提前運行以採取最佳行動。
這裡的一個想法是使用私有內存池服務隱藏交易。不幸的是,鑑於以太坊的叔叔率超過 5%,這種方法不是很有效。土匪叔叔可以利用隱藏交易仍然可以在孤立塊中披露的事實(這個問題在 Wolf Game 的一個分支中被利用)。
TL;DR:讓用戶以隨機結果進入遊戲並在同一個區塊內決定結果從根本上說是不安全的。
重組出租
一個謹慎的牧民必須運用縝密的預見性來預測未知的威脅。
區塊鏈環境在未來注定會變得更加敵對。例如,重組即服務可能變得廣泛可用並非不可想像。如果礦工/驗證者要為賄賂提供重組,攻擊者可以提前幾個街區修改決策。
考慮到這一點,最安全的選擇是在隨機遊戲事件發生之前將用戶操作鎖定幾個塊。在 Wolf Game 中,此鎖定期為 10 個區塊(更多內容見下文)。
偽隨機性不是隨機的
另一個挑戰是找到一個好的隨機性來源。例如,假設您想生成一個介於 0 和 1024 之間的隨機值。您可以做的一件事是讓提交用戶進入遊戲,等待一些塊,然後使用 rand = block.blockhash(block.number — 1 ) % 1024 作為隨機數。從表面上看,塊哈希提供了良好的熵。但這裡的問題是,對於礦工來說,製作一個 rand 為特定數字的區塊會相對便宜 — — 他們需要做的就是找到一個區塊哈希 % 1024 產生所需值的區塊。
更好的方法是從多個塊中累積熵。例如:等待 10 個區塊被挖出,然後從間歇性區塊哈希的某種組合的 Keccak 哈希中推導出一個偽隨機數。如果做得正確,這至少會顯著增加攻擊的難度。
另一種選擇是使用像 Chainlink VRF(可驗證隨機函數)這樣的 Oracle,這是為智能合約設計的可證明公平且可驗證的隨機源,這也是 Wolf Game 選擇的選項。這樣,我們可以保持代碼簡單,同時獲得足夠的隨機值。
狼遊戲實現
Wolf Game 結合使用 Chainlink 請求和接收數據周期以及在請求隨機數時禁用用戶選擇加入的狀態變量。在默認配置中,Chainlink 的 VRF 協調器將等待 10 個區塊,直到滿足隨機性請求,這足以說明可能的重組。
關於 WOOL 小袋的安全性
WOOL 小袋能夠通過動態生成的 SVG 展示自己的 WOOL。 從安全角度來看,重要的是要考慮在二級市場上的小袋交易將如何進行。
在早期的迭代中,渲染的小袋將顯示鎖定的 WOOL 以及未鎖定但尚未認領的 WOOL。 想想這裡可能會發生什麼:惡意賣家可以在 OpenSea 上提供包含大量未鎖定 WOOL 的小袋,只是在將 NFT 轉移給買家之前索取 WOOL。
為了防止這種攻擊,我們考慮實施時間鎖定以禁止在索賠後轉移多個區塊。 然而,即使有了這種機制,只要有足夠大的時間窗口,賣家仍然可以搶先買家。 另外,像這樣添加非標準行為可能會在未來產生許多不可預見的問題。
最終,我們決定在渲染圖像中根本不顯示未鎖定的 WOOL — — 因此,設定了在出售 NFT 時只轉移鎖定的 WOOL 的預期。 我們確實添加了基本的搶先保護,如果 NFT 的 WOOL 在同一個區塊中被認領,則不允許轉移 NFT。
整數魔法
牧羊人決心不浪費一丁點寶貴的存儲空間。 因此,Wool Pouch 數據被整齊地打包到 256 位結構中。 但是,通常在對小類型和整數進行計算時必須特別小心。'
在測試過程中,發現 WoolPouch 中的某些功能莫名其妙地恢復了。 為了說明原因,讓我們看一個簡化的例子。
當使用 solc 0.8.0 或更高版本編譯時,以下兩個函數之一會由於整數溢出而恢復。 你能說出是哪一個嗎?
function function1() external view returns (uint256) {
uint8 x = 128;
uint256 y = x * 2;
}function function2() external view returns (uint256) {
uint8 x = 128;
uint256 y = x * 100000;
}
這是第一個恢復的功能。 原因是間歇乘法 x * 2 中的溢出。Solc 使用最小操作數的數據類型創建一個臨時變量來保存 MUL 運算的結果。 在 function1 中,第二個操作數是適合 uint8 的文字 2。 因為兩個操作數的大小都是 8 位,所以分配了一個 uint8 來保存結果。 由於 128 * 2 導致的操作溢出不適合 uint8。 相反, function2 很好,因為 100000 恰好編譯為足夠大的類型。
這裡的主要教訓是,將操作數顯式轉換為 uint256 總是更好。
另一個常見問題是由於舍入誤差導致的精度損失。 考慮以下函數,該函數計算暫停 Barn 和遷移到新智能合約之間的綿羊收益:
uint128 unstaked_earnings = (MIGRATION_TIMESTAMP - BARN_PAUSE_TIMESTAMP) / 1 days * 10000 ether;
字節碼中的操作順序將從左到右,即:
(((MIGRATION_TIMESTAMP — BARN_PAUSE_TIMESTAMP) / 86400) * 10000) * 1e18)
(MIGRATION_TIMESTAMP — BARN_PAUSE_TIMESTAMP) / 86400 首先計算,其結果在乘以 10000 * 1e18 之前向下舍入。 因此,最後一天累積的質押秒數將“丟失”。
作為一般規則,應始終在除法之前執行乘法:
uint128 unstaked_earnings = (MIGRATION_TIMESTAMP — BARN_PAUSE_TIMESTAMP) * 10000 以太幣 / 1 天;
現在,四捨五入發生在最後一次操作中,綿羊可以享受到完整的、精確計算的羊毛量。
最終的智能合約和漏洞賞金
經審計的智能合約在 Github 上發布,並於 12 月 4 日添加到漏洞賞金範圍,隨後於 12 月 6 日啟動主網。 如果您在代碼中發現安全問題,請參與正在進行的錯誤賞金計劃。
Further reading
- Flashbots: Understanding bundles
- Chainlink: VRF security considerations
- Smart contract best practices: Integer division and multiplication
- Solidity documentation: Types