Soildity是一種風格類似JavaScript的強型別高階語言,專門用來進行智能合約的開發及編程。使用前需要被編譯成EVM字節碼,然後在EVM或其他區塊鏈虛擬機上實行。
仔細想想因為智能合約多涉及金流交易而且上鏈之後無法修改。此外,合約運行過程需要花費一定的Gas費用,因此做為智能合約的編程語言之一,Solidity會很重視安全性問題以及運算的複雜度。這導致Solidity的語法和寫法上有許多跟其他程式語言不同的地方。歡迎大家跟我一起來一邊學習Solidity、一邊看看Solidity有什麼有趣的獨特之處吧。
程式碼開頭
當使用 Solidity 撰寫代碼時,為了避免編譯器出現警告,有兩行特定的註解需要添加。
第一行註解用於指定該文件的使用許可(license)。這是一個可選的步驟,但在一些情況下,編譯器可能會提示你添加該註解。你可以在第一行註解中提供有關該文件的使用許可的相關信息。
第二行註解用於指定編譯器的版本需求條件。這將確保你的代碼可以與指定版本的 Solidity 編譯器相容。你應該在第二行註解中明確指定所需的 Solidity 編譯器版本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
上述程式碼說明合約的授權許可證,這裡使用的是 MIT 授權。這是一種常見的做法,用於確定合約的授權許可證,以便其他開發者了解並遵守該許可證的規定。`pragma `關鍵字,它用於告訴編譯器使用的 Solidity 版本。在上面的例子中,`^0.8.4 `表示編譯器應使用 0.8.4 或更高版本的 Solidity。這是為了確保合約在特定的 Solidity 版本下進行編譯。
安全性
由於 Solidity 用於智能合約開發,被大量用作金流或價值有關的交易,安全性是非常重要的考慮因素。為大家統整 Solidity 具有的一些安全性的語法機制和設計模式,旨在防止常見的漏洞和攻擊,例如重入攻擊(reentrancy attack)、整數溢出等( overflow 、underflow)。
1.報錯:
Solidity中有require、revert、assert 三種函式用於報錯並終止交易。require 跟 revert 被建議用來檢查合約狀態或 input,例如最常使用的 require 其實就有點像if 語法,當判定錯誤時整個交易會失敗,並且跳出預設的錯誤訊息。撰寫者可以用這方法來確保合約接收到的資料符合預期,且交易中止時可以拿回沒有用到的 Gas fee。而 revert 類似於 require 但是語法本身不帶有判斷功能,失敗訊息則需要用error 加以定義。最後 assert 則更適合用來檢查重要安全問題,因為他會吃掉所有Gas fee 所以通常將它擺在函式的最後面,以檢查最終的條件有沒有符合需求,不過此函式通常較少被使用。
2.限制權限:
雖然別的程式語言也有類似的功能,但solidity 更大量使用修飾器(modifier)來限制函式的執行權限,例如只讓特定帳戶或合約才能執行某些操作。這有助於防止未授權的帳戶執行具有風險的操作,另外也減少重複的程式碼以節省Gas。
以下列舉內建的modifier,都是很常用的功能:
- public:指定函式或狀態變量為公開可訪問的,可以在合約內部和外部被調用或訪問。
- private:指定函式或狀態變量為僅合約內部可訪問的,無法從合約外部進行調用或訪問。
- internal:指定函式或狀態變量為合約內部和合約繼承者可訪問的,無法從合約外部進行訪問。
- external:指定函式為僅能從合約外部調用的,無法在合約內部被直接調用。
- payable:指定函式接受以太幣的支付,可以接收價值。
另外撰寫人也可以自定義modifier來應付自己的需求,例如寫一個限定只有合約的擁有者才可以觸發的modifier。
3.interface(接口):
interface是智能合約的骨架,interface乍看下像是一個合約,不過具有一些約束條件:
- 不能包含狀態變量
- 不能包含建構子(constructor)
- 不能繼承除 interface外的其他合约
- 所有函數都必需是 external且不能有函数體
- 繼承 interface 的合约必需實現包含但不限於interface定義的所有功能
通過定義一個 interface,可以在多個合約中實現相同的介面,從而達到代碼的共享和重用。例如常見ERC20或ERC721的interface,可以提供合約之間的標準化介面,促進代碼重用和測試,同時提高代碼的可讀性和可維護性。
4.數學運算安全 :
使用 OpenZeppelin 的
SafeMath 函式庫 可以有效的避免數學運算時可能造成的漏洞,例如整數溢位、加減乘除順序造成的整數與浮點數格式問題。使用SafeMath函式庫可以確保數值大小適用於指定的資料型態。另外,在remix IDE的0.8.0編譯版本以後,也有檢查整數溢位的功能。
5.Checks-Effects-Interactions Pattern(檢查-影響-交互模式):
Checks-Effects-Interactions 模式是一種在撰寫智能合約時的設計模式,旨在提高合約的安全性和可預測性。該模式強調合約應該按照特定的順序執行操作,先進行檢查,再執行內部操作,最後進行交互操作 :
- Checks(檢查):在合約執行任何操作之前,應該先進行必要的檢查和驗證。這些檢查可以包括驗證調用者的權限、檢查數據的有效性、驗證合約狀態的一致性等。通過進行檢查,可以避免不必要的操作和防止潛在的漏洞。
- Effects(影響):在確定檢查通過後,合約應該執行相應的操作並修改合約的狀態。這些操作可以包括更新變量的值、修改數據結構、觸發事件等。在此階段,合約應該只對自身的狀態進行修改,而不應該與其他合約或外部世界進行交互。
- Interactions(交互):在完成所有內部操作後,合約可以與其他合約或外部世界進行交互。這包括與其他合約進行調用、發送資金、觸發事件等。在進行交互操作時,合約應該謹慎處理,確保適當的權限檢查和數據驗證,以防止意外情況和安全漏洞。
Checks-Effects-Interactions 模式的目的是確保合約操作的順序和一致性,從而減少潛在的漏洞和不可預測的行為,提高合約的可靠性、可讀性和維護性,使合約的行為更易於理解和追蹤。例如 token 額度的改變應該在 token 所有權的轉移之前,被用來避免重入攻擊。
節省Gas
在以太坊區塊鏈上執行智能合約或執行運算需要消耗 Gas,也就說明的執行合約需要消耗一定的以太幣。撰寫程式的工程師需要再考慮實現合約功能和預期的使用情況下,最有效的程式設計,來減少 Gas 費用並達到適當的成本效益。因此撰寫 solidity 時有一些可靠的方法可以降低 Gas 的使用:
1.使用 mapping (映射):
solidity 中的 mapping 資料結構常用作表示「使用者ID-錢包地址」、「錢包地址-餘額」之間的關係,該結構由於在設計上具有包括儲存空間較小和高效查找的優點而被大量使用:
- 儲存空間效率:mapping 使用哈希表(Hash Table)的數據結構,將鍵(Key)映射到值(Value)。相較於陣列或其他結構,mapping 具有較小的儲存空間需求。在 Solidity 中,只有被使用的鍵值對才會佔用實際儲存空間。
- 高效查找時間:由於 mapping 使用哈希表,查找特定鍵的值的操作時間複雜度是常數級別(O(1)),這意味著在mapping中查找特定鍵的操作速度與映射的大小無關。
- 適合關聯數據:mapping 非常適合用於關聯數據,例如將地址映射到餘額、ID映射到物品等。這樣的關聯數據結構常見於智能合約中的代幣餘額、所有權追蹤等場景。
雖然mapping在 Solidity 中有這些優點,但在撰寫時還是需要小心使用。因為mapping可能存在一些坑,例如需要處理未初始化鍵的情況,以及不支援迭代等。
2.減少迴圈的使用或避免複雜的迴圈:
由於迴圈需要重複執行相同的操作,並且每次迭代都需要消耗一定的計算資源,因此 solidity 裡面使用迴圈會大量的耗費 Gas fee,特別是跌代次數過多、多重迴圈結構、在迴圈中呼叫外部資源進行讀取,都會大大地加劇運算量。有些智能合約就是不當的使用迴圈使得部署之後無法使用,因為該合約在交易過程耗費的Gas fee大於區塊開採Gas的上限。
3.考慮使用event (事件)還是 新變量 :
event 會在合約中設定好的條件下被觸發,並且將 event 的數據儲存在區塊鏈上永久保存並供檢視,因此常被用作監視或處理合約異常狀態的手段之一。 因此如果不是需要追蹤合約內部狀態的改變而是向外表達狀態時,使用 event 就會比創建一個新變量更適用,而且 event 所消耗的 Gas 也比較少。
4.選擇適當的儲存位置:
solidity數據儲存位置有三類:storage、memory 和 calldata。默認情況下數據都會存成 storage,存成 storage 的資料會放在區塊鏈上,所需的 gas 費用也比 memory 和 calldata 還要高。所以如果只是在合約運行中暫時需要用到的變量,可以存成 memory 或 calldata 就好。而 calldata 相較於 memory 是唯讀的,因此從外部給值之後就不能修改值,用意在保護數據。
5.使用繼承( Inheritance ):
solidity具有物件導向的性質,其中合約、建構子(constructor)、修飾器(modifier)可以被繼承。選擇合適的父合約來繼承相對應的功能可以避免重複成冗的程式碼,並可以節省部署時 Gas 的使用量。繼承被大量使用者驗證過的合約看來也是一個較安全的選擇。
交易
智能合約跟虛擬貨幣的交易實在密不可分,以太坊上的交易內容經常牽扯到ETH的接收與發送。Solidity也有提供專用的接收、發送ETH的函式,給撰寫智能合約的工程師使用。
1. 接收ETH:
包括receive跟fallback兩種函式。receive函式在一個合約中只能有一個,而且只用於接收ETH。fallback函式可以用於接收ETH或ERC20等,它可以在調用合約不存在的函式時被觸發,所以也被用於代理合約。fallback跟receive需要`external` 與`payable`來修飾以接收ETH。
2.發送ETH:
可以使用transfer()、send()和call(),其中call()沒有Gas的限制,所以使用call函式時接收方的接收函式就可以寫的比較複雜。而transfer跟send函式就有2300 Gas的限制,撰寫人需要考慮接收方的接收函式不能太複雜,不然交易會失敗。其中transfer在交易失敗時會自帶revert功能。
在合約安全性方面Alice提供了
一篇文章介紹常用的合約掃描工具,讀者可以利用這些工具掃描合約。如果未來還有發現其他Solidity與其他程式語言的差異,我會再與大家分享,如果各位讀者還有其他想知道或是想分享的,歡迎各位留言給我知道。也期待solidity語言在未來能有更有趣的發展~
___________________________________________________________________________
作者阿原目前從事區塊鏈資料分析工作,對區塊鏈的經濟架構、事件發展有很大的興趣,並希望能將相關的區塊鏈知識分析並且統整給大家。如果喜歡我的文章,或是想獲得更多區塊鏈大小事,歡迎關注
我的vocus帳號!
另外,我已經加入由
趨勢科技防詐達人所成立的方格子專題-《區塊鏈生存守則》,在那裡我會跟其他優質的創作者一起帶大家深入瞭解區塊鏈,並隨時向大家更新區塊鏈資安事件。