編寫智慧合約時通常應遵循的安全模式-part1

買賣虛擬貨幣
本次教程主要展示在編寫智慧合約時通常應遵循的安全模式。方案建議以下建議適用於以太坊上任何智慧合約系統的開發。外部呼叫使用外部呼叫時需要格外注意呼叫不受信任的智慧合約可能會帶來一些意外的風險或Bug。外部呼叫可能在該合約或它依賴的任何其他合約中執行惡意程式碼。因此,每個外部呼叫都應視為潛在的安全風險。 如果無法或不希望刪除外部呼叫,請使用本節教程的建議將危險降至最低。
標記不受信任的合約當與外部合約進行互動時,請以清楚表明與它們進行互動不安全的方式命名變數,方法和合約介面,適用於您自己的呼叫外部合約的函式。// badBank.withdraw(100); // Unclear whether trusted or untrustedfunction makeWithdrawal(uint amount) { // Isn't clear that this function is potentially unsafe    Bank.withdraw(amount);
}// goodUntrustedBank.withdraw(100); // untrusted external callTrustedBank.withdraw(100); // external but trusted bank contract maintained by XYZ Corpfunction makeUntrustedWithdrawal(uint amount) {    UntrustedBank.withdraw(amount);
}避免外部呼叫後的狀態更改無論使用原始呼叫(形式為someAddress.call())還是合約呼叫(形式為ExternalContract.someMethod()),都可能存在執行惡意程式碼的風險。 即使ExternalContract不是惡意的,惡意程式碼也可以透過其呼叫的任何合約執行。一種特別的危險是惡意程式碼可能會劫持控制流,從而導致由於可重入而產生的漏洞。如果要呼叫不受信任的外部合約,請避免在呼叫後更改狀態。這種模式有時也被稱為檢查效果互動模式。 避免使用transfer()和send()
.transfer()和.send()都會將2300gas轉發給收件人。這一硬編碼gas津貼的目的是防止重入漏洞,但這隻有在gas成本不變的假設下才有意義。最近的EIP 1283(在最後一刻退出了君士坦丁堡硬叉)和EIP 1884(預計將在伊斯坦布林硬叉中到達)表明此假設無效。為了避免將來gas成本發生變化時會產生問題,最好改用.call.value(amount)(“”)。請注意,這無助於減輕重入攻擊,因此必須採取其他預防措施。處理外部呼叫中的BugSolidity提供了適用於原始地址的低階呼叫方法:address.call(),address.callcode(),address.delegatecall()和address.send()。 這些低階方法從不丟擲異常,但是如果呼叫遇到異常,則將返回false。 另一方面,合同呼叫(例如,ExternalContract.doSomething())將自動傳播一個引發(例如,如果doSomething()引發,則ExternalContract.doSomething()也將引發)。如果選擇使用低階呼叫方法,請確保透過檢查返回值來處理呼叫失敗的可能性。// bad
someAddress.send(55);someAddress.call.value(55)(""); // this is doubly dangerous, as it will forward all remaining gas and doesn't check for resultsomeAddress.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外部呼叫可能發生意外或者惡意BUG。為了最大限度地減少此類故障造成的損害,通常最好將每個外部呼叫隔離到自己的事務中,該事務可以由呼叫的接收者發起。這與支付尤其相關,在支付中,最好讓使用者提取資金,而不是自動向他們推送資金。(這也降低了GAS限制出現問題的可能性)避免在一個事務中合併多個以太坊轉移。// 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;    }
}// goodcontract 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函式用於從其他合約呼叫函式,就好像它們屬於呼叫方合約一樣。因此呼叫方可以改變呼叫地址的狀態,這是存在風險。下面的示例演示了使用delegatecall如何導致合約的破壞和資金損失。contract Destructor
{    function doWork() external    {        selfdestruct(0);    }}
contract Worker{    function doWork(address _internalWorker) public    {        // unsafe        _internalWorker.delegatecall(bytes4(keccak256("doWork()")));
    }}如果使用已部署的Destructor合約的地址作為引數呼叫Worker.doWork(),則Worker合約將自毀。 僅將執行委託給受信任的合約,而不委託給使用者提供的地址。不要假設合約是用零餘額建立的,攻擊者可以在建立合約之前將以太坊傳送到該合約的地址。請記住,可以強制將以太坊傳送到一個帳戶小心編寫嚴格檢查智慧合約的餘額的不變數。
攻擊者可以強行將以太坊傳送到任何帳戶,並且這是無法避免的(即使使用執行revert()的回退函式也無法阻止)。攻擊者可以透過建立合約,用1 wei資助該合約並呼叫selfdestruct(victimAddress)來實現此目的。在victimaddress中沒有呼叫任何程式碼,因此無法阻止它。傳送到礦工的地址的區塊獎勵也是如此,該地址可以是任意地址。此外,由於可以預先計算合約地址,因此可以在部署合約之前將以太坊傳送到某個地址。請記住,鏈上資料是公開的許多應用程式要求提交的資料在某個時間點之前都是隱匿的。遊戲(如鏈上剪刀石頭布)和拍賣機制(如競價拍賣)兩大類例子。如果您在構建隱私問題的應用程式,請確保避免使用者過早公佈資訊。最好的策略是使用具有不同階段的承諾方案:首先使用值的雜湊值進行提交,然後在後續階段中顯示值。例子:
在剪刀石頭布上,要求兩個玩家先提交其預期動作的雜湊值,然後要求兩個玩家均提交其動作;如果提交的動作與雜湊不匹配,則將其丟棄。在拍賣中,要求玩家在初始階段提交其出價值的雜湊值(以及大於其出價值的保證金),然後在第二階段提交其拍賣出價。開發依賴於隨機數生成器的應用程式時,順序應始終為(1)玩家提交動作,(2)生成隨機數,(3)玩家支付。產生隨機數的方法本身就是積極研究的領域。當前同類最佳的解決方案包括比特幣區塊頭(透過http://btcrelay.org驗證),雜湊提交顯示方案(即,一方生成數字,釋出其雜湊值以“提交”給該值,以及然後顯示價值)和RANDAO。由於以太坊是確定性協議,因此協議中的任何變數都不能用作不可預測的隨機數。還應注意,礦工在某種程度上控制著block.blockhash()值*。注意某些參與者可能“下線”而不上線的可能性不要依賴於由特定方執行特定操作的退款或索賠程式,而沒有其他方法將資金取出。例如在石頭剪刀布遊戲中,一個常見的錯誤是在兩個玩家都提交動作之前不進行支付。 但是惡意的玩者可以透過根本不提交自己的舉動來“困擾”對方-實際上,如果一個玩者看到了對方顯示的舉動並確定自己輸了,則根本沒有理由提出自己的舉動。(1)提供一種規避未參與參與者的方法,可能會在一定時限內進行;
(2)考慮為參與者在其所處的所有情況下提交資訊提供額外的經濟激勵。注意負整數取反solidity提供了幾種處理有符號整數的型別。與大多數程式語言一樣,在solidity中,帶n位的有符號整數可以表示從-2^(n-1)到2^(n-1)-1的值。這意味著MIN_INT沒有正等價物。求反是透過找到一個數字的兩個補數實現的,因此,最負數的求反將得出相同的值。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}處理此問題的一種方法是,在求反之前檢查變數的值,如果該值等於最小整數,則丟擲。另一種選擇是確保使用容量更大的型別(例如int32而不是int16)永遠不會達到最大負數。當min_int乘以或除以-1時,int型別也會出現類似的問題。

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;