開發者需要考慮使用safe 函數族的風險,並考慮外部調用會怎樣跟自己寫下的代碼交互。
如果你也是軟件工程師,你應該聽過一些軟件工程的格言。雖然我並不主張嚴格遵守每一句格言,但有一些值得你放在心上。
今天我想講的是“ 最少驚嚇原理”。名字挺彆扭,但意思很簡單:在面對一份聲稱可以做某事的代碼時,大多數用戶都會對它的工作方式作一些假設;因此,你作為開發者,要做的事情就是確保自己的代碼與這些假設相匹配,這樣你的用戶就不會驚嚇連連。
這是個很棒的原則,因為開發者自己也要對一些東西作假設。如果你放出一個叫做calculateScore(GameState) 的函數(字面意思為“根據遊戲狀態計算分數”),那麼大部人都會正確地預期,這個函數只會從“遊戲狀態” 中讀取數據。如果你這個函數同時也修改了遊戲的狀態,你就給了大部分用戶一個驚嚇,他們會很困惑,想搞清楚為什麼遊戲的狀態會隨機地變掉。即使你在解釋文檔中說了,也沒法保證大家都看過文檔,所以,最好從一開始就保證你的代碼不會給人驚嚇。
– “調試代碼6 個鐘,就能省下5 分鐘閱讀文檔的時間喲” –
更安全就更好,對嗎?
回到2018 年頭,ERC-721 還在草案階段,有一個建議是實現“轉賬安全性”,以確保token 不會被困在(沒有設計好處理這種token)接收方合約裡。為此,提案的作者修改了transfer 函數的行動,檢查類接收方是否有能力支持這種token 的轉賬。這也引入了一個unsafeTransfer 函數,它可以跳過這個檢查,如果發送者自己要求的話。
但是,因為擔心向後兼容的問題,這個函數在後續的修改中重新命名了。這使得transfer 函數的表現在ERC-20 和ERC-721 上是完全相同的。但是,現在接收方的能力檢查就要挪到別的地方去了。因此, safe 函數被發明出來:safeTransfer 和safeTransferFrom。
這個問題是合理的,在此之前,已經有很多ERC-20 代幣被意外轉入根本沒有預期會接收代幣的合約,導致這些token 被鎖死的案例(一個很常見的錯誤是把代幣轉給它所屬的代幣合約,導致這些代幣完全鎖死)。毫不意外,當ERC-1155 標准在草案階段時,他們吸收了ERC-721 標準的啟發,不僅在轉賬中引入了接收方檢查,在鑄造中也加入了檢查。
這些標准在接下來幾年中大部分都處於無人問津的狀態,而ERC-20 標准在隔壁獨自精彩,直到最近,NFT 引發的gas 價格暴漲表明,ERC-721 和ERC-1155 標準迎來了開發者使用量上的暴漲。開發者的興趣捲土重來,而這些標准在設計時都考慮到了安全性,這當然是一件幸事,對吧?
再問一次,更安全,一定更好嗎?
OK,你們考慮是考慮到了,但這些函數如何能讓轉賬或者鑄造變得安全呢?不同的團隊對“安全” 的理解各有不同。對於一個開發者來說,安全的函數意味著這個函數里面沒有bug、不會引入額外的安全擔憂。對於用戶來說,安全性可能意味著程序做了充分的措施,可以保護他們不會不小心搬起石頭砸自己的腳。
事實證明,要按這樣來區分的話,這些函數更多是後者(保護用戶不受錯誤操作的困擾),而不是前者。因為,它給了開發者兩個選擇: transfer 和safeTransfer ,為什麼你不用“安全” 的那個?名字裡面都寫好了嘛。
嗯,一個理由是我們的老朋友,可重入漏洞(我一直在盡最大努力希望它能重命名為“不安全的外部調用”)。回想一下,任何外部調用都可能是不安全的,只要接受方賬戶是由攻擊者控制的;因為攻擊者也許可以讓你的合約轉變成一種沒有得到定義的狀態。從設計上來說,這些“safe” 函數扮演著對代幣接收方的一個外部調用,這個調用通常是由鑄造代幣或轉移代幣的發送者控制的。換句話說,這就是不安全調用的一個典型案例。
你可能會問,就是允許一個接收方拒絕一筆自己沒法處理的轉賬而已,能有多大事呢?我們用兩個簡單的案例來回答這個問題。
哈希掩碼
Hashmasks 是一種供給量有限的NFT。用戶在單筆交易中最多可以購買20 個NFT,雖然這些NFT 口罩在幾個月前就賣光了。這裡我們看看買口罩的函數:
-
函數 mintNFT(uint256 numberOfNfts) 公共應付 {
-
require(totalSupply() < MAX_NFT_SUPPLY, "銷售已經結束");
-
require(numberOfNfts > 0, “numberOfNfts 不能為 0”);
-
require(numberOfNfts <= 20, "你不能一次購買超過 20 個 NFT");
-
require(totalSupply().add(numberOfNfts) <= MAX_NFT_SUPPLY, "超過 MAX_NFT_SUPPLY");
-
require(getNFTPrice().mul(numberOfNfts) == msg.value, “發送的以太值不正確”);
-
for (uint i = 0; i < numberOfNfts; i++) {
-
uint mintIndex = totalSupply();
-
if (block.timestamp < REVEAL_TIMESTAMP) {
-
_mintedBeforeReveal[mintIndex] = 真;
-
}
-
_safeMint(msg.sender, mintIndex);
-
}
-
/**
-
* 隨機性的來源。 理論上的礦工拒絕操縱可能,但在實用意義上應該足夠了
-
*/
-
if (startingIndexBlock == 0 && (totalSupply() == MAX_NFT_SUPPLY || block.timestamp >= REVEAL_TIMESTAMP)) {
-
起始索引塊 = 塊數;
-
}
-
}
如果你沒有預設,那這個函數看起來非常完美,有理有據。但是,如果你是個有心人,那你就能看出_safeMint 調用裡面藏著一些可怕的東西。
-
函數_safeMint(地址到,uint256 tokenId,字節內存_data)內部虛擬{
-
_mint(to, tokenId);
-
要求(_checkOnERC721Received(地址(0),to,tokenId,_data),“ERC721:轉移到非ERC721Receiver實現者”);
-
}
從安全性出發,這個函數對代幣的接收方調用了一個callback 函數,來確認接受方願不願意接收這個代幣。但是,如果你是代幣的接受方,那麼收到callback 調用的時候,要採取什麼行動完全是隨你的便,這其中就包括再次調用mintNFT。若是這麼做了,我們只需鑄造一個NFT 之後,就可以重入這個函數,意味著我們可以請求鑄造另外19 個口罩NFT。結果是我可以鑄造出39 個口罩,即使本來單次可以鑄造的最大數量是20 個。
ENS 域名封裝器
最近,ENS 團隊的Nick Johnson 聯繫了我們,希望我們看看他們正在開發的一個ENS 域名封裝器。這個封裝器允許用戶將手上的域名代幣化為一個新的ERC-1155 代幣,以此支持更細粒度的權限和更一致的API。
抽像地說,為了封裝任意的ENS 域名(準確來說是任意並非二級域名的.eth 域名),你必須先允許域名封裝器訪問你的ENS 域名。然後你調用wrap(bytes,address,uint96,address) 函數,一邊鑄造一個ERC-1155 代幣,另一邊託管了底層的ENS 域名。
這裡是封裝函數,可以說非常直接。首先,調用_wrap 函數做一些邏輯計算,返回哈希化的域名名稱。然後保證交易的發送者就是這個ENS 域名的擁有者,然後託管這個域名。注意,如果發送者並不擁有這個底層的ENS 域名,那麼整個交易應該回滾,取消_wrap 函數造成的所有變更。
-
功能包裝(
-
字節調用數據名稱,
-
地址包裹的所有者,
-
uint96 _保險絲,
-
地址解析器
-
) 公共覆蓋 {
-
bytes32 節點 = _wrap(name,wrappedOwner,_fuses);
-
地址所有者 = ens.owner(node);
-
要求(
-
所有者 == msg.sender ||
-
isApprovedForAll(owner, msg.sender) ||
-
ens.isApprovedForAll(所有者,msg.sender),
-
“NameWrapper:域不歸發件人所有”
-
);
-
ens.setOwner(node, address(this));
-
如果(解析器!=地址(0)){
-
ens.setResolver(節點,解析);
-
}
-
}
下面是_wrap 函數,看起來沒有任何特別的。
-
函數 _wrap(
-
字節內存名稱,
-
地址包裹的所有者,
-
uint96 _fuses
-
) 私人回報(bytes32 節點){
-
(bytes32 labelhash, uint256 offset) = name.readLabel(0);
-
bytes32 parentNode = name.namehash(offset);
-
要求(
-
parentNode! = ETH_NODE,
-
“NameWrapper:.eth 域需要使用 wrapETH2LD()”
-
);
-
node = _makeNode(parentNode, labelhash);
-
_mint(節點,名稱,wrappedOwner,_fuses);
-
發出 NameWrapped(節點,名稱,wrappedOwner,_fuses);
-
}
不幸的是,_mint 本身可能會給不知情的開發者一個驚嚇。 ERC-1155 的規範裡面聲明,在鑄造代幣時,應該諮詢接收方是否願意接收這個代幣。在深入研究庫代碼(從OpenZeppelin 的基礎上稍微修改而來),我們可以看到確實是這樣的。
-
功能_薄荷(
-
bytes32 節點,
-
字節內存名稱,
-
地址包裹的所有者,
-
uint96 _fuses
-
) 內部的 {
-
名字[node] = 姓名;
-
地址 oldWrappedOwner = ownerOf(uint256(node));
-
如果(oldWrappedOwner != 地址(0)){
-
// 銷毀並解開舊所有者的舊令牌
-
_burn(uint256(節點));
-
發出 NameUnwrapped(node, address(0));
-
}
-
_mint(節點,wrappedOwner,_fuses);
-
}
-
功能_薄荷(
-
bytes32 節點,
-
地址 newOwner,
-
uint96 _fuses
-
) 內部虛擬 {
-
uint256 tokenId = uint256(節點);
-
地址所有者 = ownerOf(tokenId);
-
要求(所有者 == 地址(0),“ERC1155:現有令牌的鑄造”);
-
require(newOwner != address(0), “ERC1155: mint to the zero address”);
-
要求(
-
newOwner != address(this),
-
“ERC1155:newOwner 不能是 NameWrapper 合約”
-
);
-
_setData(tokenId, newOwner, _fuses);
-
發出 TransferSingle(msg.sender, address(0x0), newOwner, tokenId, 1);
-
_doSafeTransferAcceptanceCheck(
-
msg.sender,
-
地址(0),
-
新業主,
-
令牌標識,
-
1、
-
“”
-
);
-
}
但是這對我們來說到底有什麼用呢?這又是一個不安全的外部調用,我們可以用來激發重入的漏洞。具體來說,請注意,在callback 執行期間,我們還持有表示這個ENS 域名的ERC-1155 代幣,但域名封裝器還沒驗證完成我們是這個ENS 域名的所有者。這時候我們可以直接操作這個ENS 域名而無需是其所有者。舉個例子,我們可以要求域名封裝器解封這個域名,燒掉這個我們鑄造出來的代幣,然後獲得它所代表的ENS 域名。
-
函數解包(
-
bytes32 父節點,
-
bytes32 標籤,
-
地址新控制器
-
) 公共覆蓋 onlyTokenOwner(_makeNode(parentNode, label)) {
-
要求(
-
parentNode! = ETH_NODE,
-
“NameWrapper:.eth 名稱必須用 unwrapETH2LD() 解包”
-
);
-
_unwrap(_makeNode(parentNode, label), newController);
-
}
-
函數 _unwrap(bytes32 節點,地址 newOwner) 私有 {
-
要求(
-
newOwner != 地址(0x0),
-
“NameWrapper:目標所有者不能是 0x0”
-
);
-
要求(
-
newOwner != address(this),
-
“NameWrapper:目標所有者不能是 NameWrapper 合約”
-
);
-
要求(
-
!allFusesBurned(節點,CANNOT_UNWRAP),
-
“NameWrapper:域不可解開”
-
);
-
// 銷毀令牌並融合數據
-
_burn(uint256(節點));
-
ens.setOwner (node, newOwner);
-
發出 NameUnwrapped(node, newOwner);
-
}
現在我們拿到了目標ENS 域名了,可以為所欲為了,比如我可以註冊一個子域名,或者重設解析器。完成之後再退出call back 函數。域名封裝器這時候會獲取這個ENS 域名的所有者,也就是我們,發現匹配之後驗證完成,交易成功。就像這樣,我們可以暫時獲取向域名封裝器授權的任何ENS 域名的所有權並執行任意的修改。
結論
給人驚嚇的代碼可能造成惡劣的後果,在兩個案例中,開發者都理性地假設了safe 函數族(至少)會跟普通的函數一樣安全而不會增加攻擊面。隨著ERC-721 和ERC-1155 標準變得更加流行,這種攻擊可能會越來越頻繁。開發者需要考慮使用safe 函數族的風險,並考慮外部調用會怎樣跟自己寫下的代碼交互。
展開全文打開碳鏈價值APP 查看更多精彩資訊