董 東
(河北師范大學 計算機與網(wǎng)絡空間安全學院,河北 石家莊 050024)
從Solidity語言的角度,智能合約(Smart Contract)是與以太坊區(qū)塊鏈上特定地址綁定的代碼(function)和數(shù)據(jù)(stateVariable)的集合[1]。智能合約的EBNF定義如下[2]:
<合約> := abstract? ( contract | interface | library ) <修飾符>
(is<繼承修飾語> (,<繼承修飾語> )* )?
{ <合約部件>* } ;
其中,
<合約部件> := <狀態(tài)變量> | <用于> | <結(jié)構(gòu)> | <修飾語> | <函數(shù)> |<事件> | <枚舉>;
模式是某種事物的標準形式或使人可以照著做的標準形式。設計模式(Design Pattern)是在軟件開發(fā)中針對普遍發(fā)生的問題而總結(jié)的被學術(shù)界、企業(yè)界和教育界認同的、反復使用的、經(jīng)過分類編目的代碼設計經(jīng)驗。設計模式是在特定環(huán)境下為解決某一通用軟件設計問題提供的一套有效的解決方案。設計模式的應用提高了代碼復用、可理解和可靠的程度。
面向合約設計模式包括四個要素:
名稱(Pattern Name):每一個模式都有自己的名字,起到對該模式的標識作用,方便進行引用。
問題(Problem):在面向合約的系統(tǒng)設計過程中頻繁出現(xiàn)的特定場景,比如“針對所有輸入都是不可信的”,在函數(shù)中如何處理。面向合約的設計模式所解決的問題通常是與合約思維、安全、隱私等相關的問題。
解決方案(Solution):針對問題所設計的函數(shù)、修飾語、狀態(tài)變量及其之間的關系,識別參與者和協(xié)作方式。
效果(Consequence):采用該模式對解決軟件系統(tǒng)復雜性的影響。
應用設計模式為面向合約的軟件開發(fā)能夠提供如下幫助:(1)提高源代碼的可讀性和可理解性。由于設計模式提煉了針對一類場景的解決方案,具有普適性,所以源代碼的閱讀和理解變得更加容易;(2)降低對程序設計語言的學習成本。語言學習者在不同應用場景下和解決方案下理解語言中的保留字會更加有效率;(3)為代碼審計(code audit)自動化提供途徑[3]。代碼審計是分析判斷源代碼是否符合正確性、安全性規(guī)范要求的過程。符合安全規(guī)范的設計模式為人工代碼審計或自動化審計工具的研究提供了基礎。
通過對github (www.github.com)2019年更新的Solidity語言489 項目觀察,初步識別了6個合約設計模式。
由于區(qū)塊鏈的公開性,無法完全保證合約的隱私。雖然在區(qū)塊鏈中合約狀態(tài)是公開訪問的,但是可以通過另外的合約限制對某合約狀態(tài)的讀訪問,比如約定某些情形下才可以調(diào)用合約函數(shù)或者只是允許授權(quán)訪問等等。
在這個模式中有兩個參與者:函數(shù)所屬的合約和函數(shù)的調(diào)用者。函數(shù)的調(diào)用者可能是一個用戶(user)或者是另外一個合約。被調(diào)用的函數(shù)以及相應的訪問控制部件是模式的執(zhí)行者。訪問控制部件就是修飾語modifier。通過在修飾語的開始部分設置對約束條件的檢查就可以實現(xiàn)對合約中函數(shù)的受限訪問。下面是示例代碼:
contract R {
//狀態(tài)變量a在合約實例化時初始化為合約的創(chuàng)建者
address public a = msg.sender;
modifier onlyChangedBy(address _account) {
require(msg.sender == _account);
}
//通過把修飾語onlyChangedBy附加到函數(shù)setA上,使得只有該函數(shù)的調(diào)用者是合約的創(chuàng)建者時才能修改狀態(tài)變量a
function setA(address _newA) public onlyChangedBy(a) {
a=_newA;
}
}
一個合約的生命周期是從其被創(chuàng)建到其被撤銷的整個過程。這個過程如果可以劃分為幾個不同的階段,并且階段之間存在轉(zhuǎn)移條件,那么自然地形成幾個狀態(tài)和若干狀態(tài)轉(zhuǎn)移,一般稱為初始狀態(tài)、結(jié)束狀態(tài)和中間狀態(tài)。在不同的狀態(tài)下,合約的參與者能夠執(zhí)行不同的功能。重要的是,狀態(tài)之間的轉(zhuǎn)移是明確定義的、而且至少有一個參與者可以觸發(fā)。
狀態(tài)機模式的的參與者有兩個:合約的創(chuàng)建者(owner)和與合約交互的訪問者(user)。參與者能夠直接或間接地引起合約狀態(tài)轉(zhuǎn)移。
實現(xiàn)狀態(tài)機設計模式需要三個部件:狀態(tài)的表示、函數(shù)的交互控制和狀態(tài)的轉(zhuǎn)移。枚舉(Enum)是源代碼中定義的類型,可以用來建模不同的狀態(tài)。因為枚舉可以顯式地與整數(shù)類型進行轉(zhuǎn)換,所以通過加1就能夠轉(zhuǎn)移到下一個狀態(tài),如果狀態(tài)是線性的。通過訪問約束模式應用修飾語實現(xiàn)第二個部件:限制某些函數(shù)只能在某狀態(tài)下使用。只需設計函數(shù)修飾語,并附加在特定函數(shù)上,那么在執(zhí)行被調(diào)用函數(shù)之前就會檢查是否在制定狀態(tài)下執(zhí)行。如果某個函數(shù)執(zhí)行完畢就應引起狀態(tài)轉(zhuǎn)移,則在該函數(shù)體末尾設計修飾語或者直接把新狀態(tài)賦值給狀態(tài)變量。下面的合約使用枚舉定義了四個狀態(tài),使用修飾符定義了對狀態(tài)檢查,通過參數(shù)Stages.Running和修飾語at限定函數(shù)run只能在Stages.Running階段執(zhí)行。下面是示例代碼:
contract S{
//定義四個狀態(tài)
enum Stages { Ready, Running, Finalized, Finished }
//狀態(tài)變量,指示當前狀態(tài)
Stages public stage = Stages.Ready;
//判斷當前狀態(tài)是否是期望的狀態(tài)
modifier at(Stages _stage) {
require(stage == _stage);
}
function run() public at(Stages.Running) {
//業(yè)務邏輯實現(xiàn)代碼
}
}
訪問約束和狀態(tài)機設計模式在Solidity語言的官方文檔中已經(jīng)介紹。
函數(shù)執(zhí)行所要滿足的顯式的或者隱式的條件稱為函數(shù)的前置條件。可把require()作為函數(shù)體的初始語句,用于對本函數(shù)所處理的參數(shù)以及其它執(zhí)行條件進行有效性檢查,這種情況稱為前置條件模式。下面是示例代碼:
contract P {
function t(address addr) payable public {
//檢查事務中用于轉(zhuǎn)賬的金額不能為0
require(msg.value != 0);
addr.transfer(msg.value);
}
}
函數(shù)執(zhí)行后應當出現(xiàn)或者維持的一些條件稱為函數(shù)執(zhí)行的后置條件。當合約提出的需求被滿足,合約自然生效,需求的提出方應自動按照合約規(guī)則進行滿足后的運作。例如在一個關于某物品的拍賣合約中,一旦確定了最后的買家,則買家的錢將被自動轉(zhuǎn)給拍賣者。在區(qū)塊鏈中,應有某種機制保證合約邏輯按照預期進行。assert()通常作為函數(shù)體的結(jié)束語句,用于檢查不變量和本函數(shù)的后置條件。下面是示例代碼:
contract G {
function t(address addr) payable public {
uint balanceBeforeTransfer = this.balance;
uint transferAmount;
addr.transfer(transferAmount);
//確保轉(zhuǎn)出后的余額等于轉(zhuǎn)出前的余額減去轉(zhuǎn)出金額
assert(this.balance == balanceBeforeTransfer-transferAmount);
}
}
通常情況下,assert()參數(shù)中的謂詞總是為真。一旦不為真,整個轉(zhuǎn)賬事務回退,賬戶回到執(zhí)行函數(shù)t之前的狀態(tài)。這件事情由EVM來完成。
即便是經(jīng)過仔細的測試和嚴格的審計,智能合約中依然有存在缺陷和漏洞可能性。由于區(qū)塊鏈的不變性(immutability)準則,一旦發(fā)現(xiàn)缺陷,但很難迅速修復代碼。在完成修復之前,將暫停合約中至關重要的功能,就形成了緊急制動設計模式。在這個模式中有一個參與者:有權(quán)緊急制動的實體。通過一個指示合約是否處于制動狀態(tài)的狀態(tài)變量、一個具有授權(quán)修飾語的制動狀態(tài)激活函數(shù)和一組由制動狀態(tài)下禁止執(zhí)行修飾語修飾的重要函數(shù)。下面是示例代碼:
contract E {
//指示是否處于制動狀態(tài)
bool isBraked = false;
modifier brakedInEmergency {
require(!isBraked);
}
modifier onlyWhenBraked {
require(isBraked);
}
modifier onlyAuthorized {
//檢查msg.sender是否具有授權(quán)
//如require(msg.sender == owner)
}
//只有授權(quán)者才能調(diào)用
function brake() public onlyAuthorized {
isBraked = true;
}
//只有授權(quán)者才能調(diào)用
function resume() public onlyAuthorized {
isBraked = false;
}
//一旦制動,不可訪問
function criticalOperation() public payable brakedInEmergency {
//關鍵函數(shù)的業(yè)務邏輯
}
function emergencyOperation() public onlyWhenBraked {
//緊急情況下的處理邏輯
}
}
代理模式就是在合約訪問者和合約提供者之間設置一個代理合約,訪問者通過訪問代理合約完成合約函數(shù)的調(diào)用。比如,由于合約的不變性,使得對合約的版本升級成為問題。在維持不變性的前提下,通過變通的方法可以實現(xiàn)版本升級。但是,新版本的合約地址將替換舊版本的合約地址。為了能夠讓合約的引用者使用新的地址,增加一個代理,讓合約的訪問者委托代理實現(xiàn)對合約的訪問。當代理變更了新的合約地址后,就讓所有對合約函數(shù)的訪問變更到新地址上。實現(xiàn)時由讓delegatecall函數(shù)把于在msg.data的前4個字節(jié)(包含了被調(diào)用函數(shù)的標識符)與新的地址綁定即可。為了讓合約所有函數(shù)調(diào)用都能與新地址綁定,把delegatedcall函數(shù)放在合約的應變(fallback)函數(shù)中就能實現(xiàn)。應變函數(shù)是在合約收到不認識的函數(shù)調(diào)用時執(zhí)行的函數(shù)。下面是示例代碼:
contract P{
address delegate;
address owner = msg.sender;
//授權(quán)實體變更合約新地址
function upgrade(address newAddress) public {
require(msg.sender == owner);
delegate = newAddress;
}
//應變函數(shù),處理upgrade以外的其它函數(shù)調(diào)用都觸發(fā)應變函數(shù)執(zhí)行
function() external payable {
assembly {
//目標地址
let _target := sload(0)
//復制函數(shù)簽名和參數(shù)
calldatacopy(0x0, 0x0, calldatasize)
//在目標地址上執(zhí)行函數(shù)調(diào)用
let result := delegatecall(gas, _target, 0x0, calldatasize, 0x0, 0)
//其它處理
}
}
}
Donald Knuth教授說,設計程序是解釋給人類讓計算機做了什么的過程而不是告訴計算機做什么的過程[4]。程序員編寫代碼不僅僅是為了讓計算機執(zhí)行,也是為了將代碼操作的精確細節(jié)傳達給以后將對代碼進行調(diào)整、更新、測試和維護的開發(fā)人員。學習和使用大多數(shù)程序員們?yōu)榱私鉀Q某一問題而使用的代碼片段對于提高程序質(zhì)量和可理解性顯得更加重要。
所以,設計模式一直是軟件工程領域的熱點話題。本文抽取了2019年以來的樣本進行觀察,識別了六種設計模式。隨著Solidity語言版本迅速變化,個別模式的的實現(xiàn)設施可能會發(fā)生變化,但抽象結(jié)構(gòu)不會改變。未來還需要做兩方面工作:一是繼續(xù)識別更多的設計模式,并使用適當?shù)目梢暬椒ㄟM行表示。雖然智能合約也是對象,但有自己的特點,需要對UML類模型和記號進行擴展;二是設計Soidity語法分析器,形成大量源代碼的中間表示,再使用是適當?shù)臋C器學習收到進行模式識別。