終於來到最後一個大章節了,我把 Security & Extensions 定調為一個可以更熟悉語法與開發的一個環節,因為注重安全性這件事情除了對語法以及其底層架構的了解之外,還得有一定開發過程的邏輯與經驗。本篇文章主要是提出一些容易出現的流程錯誤或者漏洞,雖然程式碼能夠正常且有效率地運作,但不代表是安全的喔!
看完這些章節之後也推薦大家去學習使用 Hardhat 或 Foundry 撰寫測試,我之前也有在 TEM 發過一篇文寫 Foundry 測試:Quick Look DeFi Contract Testing With Foundry,大家可以當作延伸閱讀。
我自己認為要提升開發的安全性跟熟悉度有幾個點:
我自己覺得要熟悉智能合約語法以及促進安全性,多玩 CTF 的幫助是很大的哈哈哈哈。
此外本文最後還有多出來的 debug list,其實一直以來都有不少的 Debug 需求出現在私訊,所以就大概整理了一些 DeBug List 來幫助初學者基本檢查一下是不是犯了哪些錯誤。此外還分享了一些開發小工具,算是這整個系列文的 Appendix!
在合約中最常發生的錯誤有幾個:
那除了以上的錯誤還有許多各種容易發生的漏洞,大家可以多多鑽研!
第一個也是最該注意的攻擊就是重送攻擊,重送攻擊和重放(Replay)不同,Replay 會藉由複製交易資訊並重新發送達到一些目的,而重送則是在一筆交易(一個函式呼叫)中再塞一個函式呼叫,讓在「這筆交易」中可以執行兩次以上的 operations。
例如以下 Solidity by Example - Re-Entrancy 的例子:
在目標合約中我們可以藉由以下這個 withdraw()
來從合約之中提領資金,需要注意的是兩段程式碼的先後順序:(bool sent, ) = msg.sender.call{value: bal}("");
與 balances[msg.sender] = 0;
function withdraw() public {
uint bal = balances[msg.sender];
require(bal > 0);
(bool sent, ) = msg.sender.call{value: bal}("");
require(sent, "Failed to send Ether");
balances[msg.sender] = 0;
}
如果我們使用一個 Attack 合約來呼叫這個 withdraw()
時,在碰到目標合約的 (bool sent, ) = msg.sender.call{value: bal}("");
時我們的 Attack 合約會接收到 ether,此時觸發 fallback()
,藉此又能夠再呼叫一次目標合約的 withdraw()
。
然而這時候第一次呼叫 withdraw()
還沒有碰到 balances[msg.sender] = 0;
,於是乎又可以再領一次了。
fallback() external payable {
if (address(etherStore).balance >= 1 ether) {
etherStore.withdraw();
}
}
function attack() external payable {
require(msg.value >= 1 ether);
etherStore.deposit{value: 1 ether}();
etherStore.withdraw();
}
當我們在製作線上 Judging System 的時候,遇到一個最大的問題其實就是其他的同學可以藉由「重放其他人的交易資訊」來完成題目,所以無論是在函式中要避免別人使用他人的 tx data 或避免 transaction approve
被重放,我們都可以考慮加上 nonce
或者是「用戶的地址」當作部分元素。nonce
的部分我們前面的文章內容已經提到不少了,使用用戶地址的原因是,在限制 msg.sender
的情況下,除非用戶把私鑰進行轉移或者販賣給他人,否則他人是沒有辦法做出同一筆交易的。
e.g. 某個資優生利用一把新的私鑰完成作業後把這個私鑰跟答案賣給其他同學。
selfdestruct()
可以讓一個合約被從區塊鏈刪除(實際上是停止更新),然後可以把這個合約中的剩餘資產轉給想轉的人,主要是用於把舊版合約停掉以避免用戶仍在使用舊版的合約。selfdestruct()
的存在其實還蠻危險的,例如有心人士可以刻意地毀掉一個合約或者是利用 selfdestruct()
強迫執行某些交易。
最容易發生問題的就是誤把 selfdestruct()
設為 public
或者 external
又沒有掛上 onlyOwner
等 modifier。或者是「利用 address(this).balance
來作為 condition statement 或 require()
的敘述」時可能會發生問題。簡單來說 attacker 可以透過幾種方式讓一個合約強迫接收 ether,而 selfdestruct()
就是一個。如果今天 address(this).balance
因為接收了某個合約的 selfdestruct()
剩餘資金,可能就會脫離 owner 當初寫這些 condition statement 的掌控。
交易被送出之後並不會直接被執行和打包進區塊中,在交易真正被礦工寫到區塊鏈上之前,合約中的狀態都是不會改變的。因此我們在觀察 tx pool 之後嘗試使用更高的 gas fee 搶先在其他人之前被打包到區塊裡面。這邊有兩個技術點:一是要查看 tx pool 中正確答案的交易,二是要選擇一個適合的 gas fee 獲得優先權。
目前最知名的靜態分析器就是 Slither,會用來檢查合約裡面的漏洞,在許多審計公司做審計的時候也會對合約用各種靜態分析器進行分析,然後給出各種分數。不過靜態分析器終究是個工具,用「眼睛」以及「測試」檢查合約才是最重要的!
Slither 除了本身的分析功能,還可以透過插件檢查各種合約的設計與狀況,例如 slither-check-upgradeability
能夠檢查可升級合約(有沒有 storage collision 之類的),而 slither-check-erc
可以檢查代幣合約的狀況(例如繼承合約沒有破壞一致性等等的)。
另外一個分析器是 Mythril,同樣也是一個智能合約的安全分析工具。由於是直接分析 bytecode,Mythril 也能直接對其他 EVM-Compatible 的鏈上合約進行分析!這邊主要是使用 MythX 來分析 Solidity,能夠在 Remix 或者上面我們提到的開發框架中加上 MythX 的 Extensions 作分析用。
還有一個看起來超酷的分析工具是 DeFiSafety 但我沒使用過,大家可以自己看看!
根據不同項目還有公司的需求,可能會需要做 Gnosis / The Graph 或 Sanpshot / Tally 的整合。
那除了以上我提到的合約漏洞,明天在審計的部分我也會把一些審計公司或者審計工作室常常會特別去看的部分整理出來,大家在完成自己的專案之後也可以針對這些細項去做研究!
這是我給帶的學生們的「問問題前的詳閱須知」:
console.log()
,不是先找人幫 de...mint()
web3.eth.Contract.methods
還是什麼的好好查完官方文件再寫,名子可以自己令但 member function 的關係是不變的"ether"
」,傳「ether
」只有幫你 de 的助教看得懂而已,React 會說你沒宣告過不是因為它的英文比助教爛...await
,還那麼年輕這麼快就 render 了真的不行.env
加上 SKIP(看報錯)"/"
baseURL = ""
和 setBaseURI()
最後歡迎大家拍打餵食大學生
0x2b83c71A59b926137D3E1f37EF20394d0495d72d