本次教程主要展示在編寫智能合約時通常應遵循的安全模式。
方案建議
以下建議適用于以太坊上任何智能合約系統(tǒng)的開發(fā)。
外部調用
使用外部調用時需要格外注意
調用不受信任的智能合約可能會帶來一些意外的風險或Bug。外部調用可能在該合約或它依賴的任何其他合約中執(zhí)行惡意代碼。因此,每個外部調用都應視為潛在的安全風險。 如果無法或不希望刪除外部調用,請使用本節(jié)教程的建議將危險降至最低。
標記不受信任的合約
當與外部合約進行交互時,請以清楚表明與它們進行交互不安全的方式命名變量,方法和合約接口,適用于您自己的調用外部合約的函數(shù)。
// bad
Bank.withdraw(100); // Unclear whether trusted or untrusted
function makeWithdrawal(uint amount) { // Isn‘t clear that this funcTIon is potenTIally unsafe
Bank.withdraw(amount);
}
// good
UntrustedBank.withdraw(100); // untrusted external call
TrustedBank.withdraw(100); // external but trusted bank contract maintained by XYZ Corp
funcTIon makeUntrustedWithdrawal(uint amount) {
UntrustedBank.withdraw(amount);
}
避免外部調用后的狀態(tài)更改
無論使用原始調用(形式為someAddress.call())還是合約調用(形式為ExternalContract.someMethod()),都可能存在執(zhí)行惡意代碼的風險。 即使ExternalContract不是惡意的,惡意代碼也可以通過其調用的任何合約執(zhí)行。
一種特別的危險是惡意代碼可能會劫持控制流,從而導致由于可重入而產生的漏洞。
如果要調用不受信任的外部合約,請避免在調用后更改狀態(tài)。這種模式有時也被稱為檢查效果交互模式。
避免使用transfer()和send()
.transfer()和.send()都會將2300gas轉發(fā)給收件人。這一硬編碼gas津貼的目的是防止重入漏洞,但這只有在gas成本不變的假設下才有意義。最近的EIP 1283(在最后一刻退出了君士坦丁堡硬叉)和EIP 1884(預計將在伊斯坦布爾硬叉中到達)表明此假設無效。
為了避免將來gas成本發(fā)生變化時會產生問題,最好改用.call.value(amount)(“”)。請注意,這無助于減輕重入攻擊,因此必須采取其他預防措施。
處理外部調用中的Bug
Solidity提供了適用于原始地址的低級調用方法:address.call(),address.callcode(),address.delegatecall()和address.send()。 這些低級方法從不拋出異常,但是如果調用遇到異常,則將返回false。 另一方面,合同調用(例如,ExternalContract.doSomething())將自動傳播一個引發(fā)(例如,如果doSomething()引發(fā),則ExternalContract.doSomething()也將引發(fā))。
如果選擇使用低級調用方法,請確保通過檢查返回值來處理調用失敗的可能性。
// bad
someAddress.send(55);
someAddress.call.value(55)(“”); // this is doubly dangerous, as it will forward all remaining gas and doesn’t check for result
someAddress.call.value(100)(bytes4(sha3(“deposit()”))); // if deposit throws an excepTIon, the raw call() will only return false and transaction will NOT be reverted
// good
(bool success, ) = someAddress.call.value(55)(“”);
if(!success) {
// handle failure code
}
ExternalContract(someAddress).deposit.value(100)();
支持外部調用push
外部調用可能發(fā)生意外或者惡意BUG。為了最大限度地減少此類故障造成的損害,通常最好將每個外部調用隔離到自己的事務中,該事務可以由調用的接收者發(fā)起。這與支付尤其相關,在支付中,最好讓用戶提取資金,而不是自動向他們推送資金。(這也降低了GAS限制出現(xiàn)問題的可能性)避免在一個事務中合并多個以太坊轉移。
// bad
contract auction {
address highestBidder;
uint highestBid;
function bid() payable {
require(msg.value 》= highestBid);
if (highestBidder != address(0)) {
(bool success, ) = highestBidder.call.value(highestBid)(“”);
require(success); // if this call consistently fails, no one else can bid
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
// good
contract auction {
address highestBidder;
uint highestBid;
mapping(address =》 uint) refunds;
function bid() payable external {
require(msg.value 》= highestBid);
if (highestBidder != address(0)) {
refunds[highestBidder] += highestBid; // record the refund that this user can claim
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(refund)(“”);
require(success);
}
}
不要將調用委托給不受信任的代碼
delegateCall函數(shù)用于從其他合約調用函數(shù),就好像它們屬于調用方合約一樣。因此調用方可以改變調用地址的狀態(tài),這是存在風險。下面的示例演示了使用delegatecall如何導致合約的破壞和資金損失。
contract Destructor
{
function doWork() external
{
selfdestruct(0);
}
}
contract Worker
{
function doWork(address _internalWorker) public
{
// unsafe
_internalWorker.delegatecall(bytes4(keccak256(“doWork()”)));
}
}
如果使用已部署的Destructor合約的地址作為參數(shù)調用Worker.doWork(),則Worker合約將自毀。 僅將執(zhí)行委托給受信任的合約,而不委托給用戶提供的地址。
不要假設合約是用零余額創(chuàng)建的,攻擊者可以在創(chuàng)建合約之前將以太坊發(fā)送到該合約的地址。
請記住,可以強制將以太坊發(fā)送到一個帳戶
小心編寫嚴格檢查智能合約的余額的不變量。
攻擊者可以強行將以太坊發(fā)送到任何帳戶,并且這是無法避免的(即使使用執(zhí)行revert()的回退函數(shù)也無法阻止)。
攻擊者可以通過創(chuàng)建合約,用1 wei資助該合約并調用selfdestruct(victimAddress)來實現(xiàn)此目的。在victimaddress中沒有調用任何代碼,因此無法阻止它。發(fā)送到礦工的地址的區(qū)塊獎勵也是如此,該地址可以是任意地址。
此外,由于可以預先計算合約地址,因此可以在部署合約之前將以太坊發(fā)送到某個地址。
請記住,鏈上數(shù)據是公開的
許多應用程序要求提交的數(shù)據在某個時間點之前都是隱匿的。游戲(如鏈上剪刀石頭布)和拍賣機制(如競價拍賣)兩大類例子。如果您在構建隱私問題的應用程序,請確保避免用戶過早公布信息。最好的策略是使用具有不同階段的承諾方案:首先使用值的哈希值進行提交,然后在后續(xù)階段中顯示值。
例子:
在剪刀石頭布上,要求兩個玩家先提交其預期動作的哈希值,然后要求兩個玩家均提交其動作;如果提交的動作與散列不匹配,則將其丟棄。
在拍賣中,要求玩家在初始階段提交其出價值的哈希值(以及大于其出價值的保證金),然后在第二階段提交其拍賣出價。
開發(fā)依賴于隨機數(shù)生成器的應用程序時,順序應始終為(1)玩家提交動作,(2)生成隨機數(shù),(3)玩家支付。產生隨機數(shù)的方法本身就是積極研究的領域。當前同類最佳的解決方案包括比特幣區(qū)塊頭(通過http://btcrelay.org驗證),哈希提交顯示方案(即,一方生成數(shù)字,發(fā)布其哈希值以“提交”給該值,以及然后顯示價值)和RANDAO。由于以太坊是確定性協(xié)議,因此協(xié)議中的任何變量都不能用作不可預測的隨機數(shù)。還應注意,礦工在某種程度上控制著block.blockhash()值*。
注意某些參與者可能“下線”而不上線的可能性
不要依賴于由特定方執(zhí)行特定操作的退款或索賠程序,而沒有其他方法將資金取出。例如在石頭剪刀布游戲中,一個常見的錯誤是在兩個玩家都提交動作之前不進行支付。 但是惡意的玩者可以通過根本不提交自己的舉動來“困擾”對方-實際上,如果一個玩者看到了對方顯示的舉動并確定自己輸了,則根本沒有理由提出自己的舉動。
(1)提供一種規(guī)避未參與參與者的方法,可能會在一定時限內進行;
(2)考慮為參與者在其所處的所有情況下提交信息提供額外的經濟激勵。
注意負整數(shù)取反
solidity提供了幾種處理有符號整數(shù)的類型。與大多數(shù)編程語言一樣,在solidity中,帶n位的有符號整數(shù)可以表示從-2^(n-1)到2^(n-1)-1的值。這意味著MIN_INT沒有正等價物。求反是通過找到一個數(shù)字的兩個補數(shù)實現(xiàn)的,因此,最負數(shù)的求反將得出相同的值。
contract Negation {
function negate8(int8 _i) public pure returns(int8) {
return -_i;
}
function negate16(int16 _i) public pure returns(int16) {
return -_i;
}
int8 public a = negate8(-128); // -128
int16 public b = negate16(-128); // 128
int16 public c = negate16(-32768); // -32768
}
處理此問題的一種方法是,在求反之前檢查變量的值,如果該值等于最小整數(shù),則拋出。另一種選擇是確保使用容量更大的類型(例如int32而不是int16)永遠不會達到最大負數(shù)。
當min_int乘以或除以-1時,int類型也會出現(xiàn)類似的問題。
來源: 區(qū)塊鏈研究實驗室?