區塊鏈研究實驗室|減少智慧合約程式碼複雜性並提高可維護性

買賣虛擬貨幣

在本文中,我將分享一些示例,說明消除特殊情況如何減少程式碼複雜性並提高可維護性。

特殊最大值

常見的特殊情況是使用0表示“無最大值”。這種特殊情況通常很容易消除。

Special Expirations

看下面的程式碼👇

uint256 expiration;

// Use 0 to mean "no expiration".
functionsetExpiration(uint256 newExpirationexternal{
    expiration = newExpiration;
}

functiondoSomething() external{
require(expiration == 0 || now < expiration, "Error: expired");
    ...
}

在這段程式碼中,0是一種特殊情況,表示“沒有過期”。這種特殊情況是不直觀的,它增加了require語句的複雜性。

然而,真正的危險是團隊中的一個新開發人員忽略了這個微妙之處,無法處理expiration==0的特殊情況。這很容易導致資金損失或其他嚴重問題。

這樣程式碼就更簡單更明顯了:

// Default to 2**256-1 instead.
uint256 expiration = 2**256-1;

// Use 2**256-1 to mean "no expiration".
functionsetExpiration(uint256 newExpirationexternal{
    expiration = newExpiration;
}

functiondoSomething() external{
require(now <= expiration, "Error: expired");
    ...
}

這裡,我使用的是uint256允許的最大值的expiration,而不是0,當涉及到時間戳時,expiration實際上是無限的。

特殊最大以太幣數量

這是一個非常相似的示例,但這次涉及以太幣:

uint256 maxWithdrawal;

// Use 0 to mean "no maximum".
functionsetMaxWithdrawal(uint256 newMaxexternal{
    maxWithdrawal = newMax;
}

functionwithdraw(uint256 amountexternal{
require(maxWithdrawal == 0 || amount <= maxWithdrawal, "Error: too much");
    ...
}

同樣,我們有一個非直觀的特例,我們可以透過使用一個有效的無限值來解決這個問題:

// Default to 2**256-1 instead.
uint256 maxWithdrawal = 2**256-1;

// Use 2**256-1 to mean "no maximum".
functionsetMaxWithdrawal(uint256 newMaxexternal{
    maxWithdrawal = newMax;
}

functionwithdraw(uint256 amountexternal{
require(amount <= maxWithdrawal, "Error: too much");
    ...
}

2256-1是最大值

注意,同樣的技巧可以概括為令牌數量或任何值。由於Solidity不能表示大於2256-1的值,因此它始終可以與uint256進行比較,成為“有效無限”值

解決gas成本問題

通常,在gas成本方面需要進行權衡。人們最終將預設值設為0的一個典型原因是儲存非零值會耗費大量gas。

如果儲存成本對於您的用例而言是很高的,請考慮以下技巧:

uint256 _expiration; // 0 still means "no expiration"

...

// Properly handle the special cases in one place.
functionexpiration() internalviewreturns (uint256{
return _expiration > 0 ? _expiration : 2**256-1;
}

functiondoSomething() external{
require(now < expiration(), "Error: expired");
}

在此程式碼中,寫入儲存的_expiration值預設情況下為0,與以前的特殊含義相同。但是,我介紹了一個輔助函式expiration(),它將0轉換為不太特殊的值2256-1。這意味著我的其餘程式碼無需處理這種特殊情況。

考慮將此技術與自定義的linter規則配對使用,以確保您不會在expiration()函式之外的任何地方直接讀取_expiration。

特殊地址

關於地址,我經常看到兩種特殊情況:

  1. 地址0通常是不允許的。

  2. 不允許使用特定地址(通常是特權角色)。

特別地址0

這是一些熟悉的程式碼,其中使用0作為特殊情況:

functiontransfer(address to, uint256 amountexternal{
require(to != address(0), "Error: can't send to 0x0");
    ...
}

禁止使用地址0通常是為了保護使用者不受錯誤的影響。將令牌傳送到地址0通常不會比將它們傳送到地址1更糟糕,但0是預設值,因此更可能由於有錯誤的工具或庫而意外傳入。

我個人不喜歡這種地址0的支票,但這很少有問題。與前面的示例不同,如果開發人員在維護程式碼時忘記了這種特殊情況,那麼一切都不會中斷。

特殊角色地址

這段程式碼比上一段要麻煩得多:

address owner;

constructor()public{
    owner = msg.sender;
}


functiontransfer(address to, uint256 amount)external{
    require(to != owner, "Error: can't send to owner.");
    ...
}

當我看到這樣的程式碼時,我的直接問題是為什麼所有者地址無法接收令牌。這樣的檢查通常是為了將安全控制措施放在適當的位置,但通常無法解決Sybil攻擊,因為系統中的多個地址由同一個人控制。

在這個特定的例子中,所有者可以簡單地接收具有不同地址的令牌。如果這違反了合同的安全性假設,那就有問題了。

像這樣的特殊情況是一種程式碼氣息,但這並不意味著它們總是應該被消除。要做的重要事情是記錄為什麼需要這種特殊情況,並考慮替代方案。

總結

  1. 特殊情況會導致程式碼複雜性,從而導致錯誤。

  2. 在可能的情況下,完全消除特殊情況。

  3. 2256-1是最大值的良好替代品。

  4. 地址0的特殊情況通常可以。

  5. 其他特定地址的特殊情況是程式碼氣味。

  6. 如果決定在程式碼中使用具有特殊意義的值,請嘗試隔離用於處理這些值的程式碼。


免責聲明:

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

推荐阅读

;