在本文中,我將分享一些示例,說明消除特殊情況如何減少程式碼複雜性並提高可維護性。
特殊最大值
常見的特殊情況是使用0表示“無最大值”。這種特殊情況通常很容易消除。
Special Expirations
看下面的程式碼👇
uint256 expiration;
// Use 0 to mean "no expiration".
functionsetExpiration(uint256 newExpiration) external{
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 newExpiration) external{
expiration = newExpiration;
}
functiondoSomething() external{
require(now <= expiration, "Error: expired");
...
}
這裡,我使用的是uint256允許的最大值的expiration,而不是0,當涉及到時間戳時,expiration實際上是無限的。
特殊最大以太幣數量
這是一個非常相似的示例,但這次涉及以太幣:
uint256 maxWithdrawal;
// Use 0 to mean "no maximum".
functionsetMaxWithdrawal(uint256 newMax) external{
maxWithdrawal = newMax;
}
functionwithdraw(uint256 amount) external{
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 newMax) external{
maxWithdrawal = newMax;
}
functionwithdraw(uint256 amount) external{
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。
特殊地址
關於地址,我經常看到兩種特殊情況:
地址0通常是不允許的。
不允許使用特定地址(通常是特權角色)。
特別地址0
這是一些熟悉的程式碼,其中使用0作為特殊情況:
functiontransfer(address to, uint256 amount) external{
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攻擊,因為系統中的多個地址由同一個人控制。
在這個特定的例子中,所有者可以簡單地接收具有不同地址的令牌。如果這違反了合同的安全性假設,那就有問題了。
像這樣的特殊情況是一種程式碼氣息,但這並不意味著它們總是應該被消除。要做的重要事情是記錄為什麼需要這種特殊情況,並考慮替代方案。
總結
特殊情況會導致程式碼複雜性,從而導致錯誤。
在可能的情況下,完全消除特殊情況。
2256-1是最大值的良好替代品。
地址0的特殊情況通常可以。
其他特定地址的特殊情況是程式碼氣味。
如果決定在程式碼中使用具有特殊意義的值,請嘗試隔離用於處理這些值的程式碼。