《精通以太坊》-智能合約

原文:Smart contracts

正如我們在[intro]中看到的那樣此叠,以太坊中有兩種不同類型的帳戶:外部擁有帳戶(EOA)和合約帳戶毅待。EOA由以太坊外部的軟件控制饱溢,例如錢包應(yīng)用程序庭猩。合約帳戶由在以太坊虛擬機(jī)(EVM)中運(yùn)行的軟件控制。兩種類型的帳戶都由以太坊地址標(biāo)識纸泡。在本節(jié)中漂问,我們將討論第二種類型,合約賬戶以及控制它們的軟件:智能合約。

什么是智能合約级解?

智能合約這個術(shù)語被用來描述各種不同的東西冒黑。在20世紀(jì)90年代田绑,密碼學(xué)家Nick Szabo創(chuàng)造了這個術(shù)語勤哗,并將其定義為“一套以數(shù)字形式指定的承諾,包括各方履行其他承諾的協(xié)議掩驱∶⒒”從那時起,智能合約的概念已經(jīng)發(fā)生變化欧穴,尤其是在2009年通過比特幣的發(fā)明引入去中心化的區(qū)塊鏈之后民逼。在本書中,我們使用術(shù)語“智能合約”來指代在以太坊虛擬機(jī)(作為分散的世界計(jì)算機(jī)運(yùn)行)的上下文中確定性運(yùn)行的不可變計(jì)算機(jī)程序涮帘。

讓我們解開這個定義:

計(jì)算機(jī)程序:智能合約只是計(jì)算機(jī)程序拼苍。合約一詞在這方面沒有法律意義。不可變:一旦部署调缨,智能合約的代碼就無法改變疮鲫。與傳統(tǒng)軟件不同,修改智能合約的唯一方法是部署新實(shí)例弦叶。確定性:智能合約的結(jié)果對于運(yùn)行它的每個人來說都是相同的俊犯,在調(diào)用它的交易的上下文中,以及在執(zhí)行時的以太坊區(qū)塊鏈的狀態(tài)伤哺。EVM上下文:智能合約以非常有限的執(zhí)行上下文運(yùn)行燕侠。他們可以訪問自己的狀態(tài),調(diào)用它們的交易的上下文以及有關(guān)最新區(qū)塊的一些信息立莉。分散的世界計(jì)算機(jī):EVM作為每個以太坊節(jié)點(diǎn)上的本地實(shí)例運(yùn)行绢彤。

智能合約生命周期

智能合約通常用高級語言編寫,例如Solidity蜓耻。但是為了運(yùn)行杖虾,必須將它們編譯為在EVM中運(yùn)行的低級字節(jié)碼(參見[evm])。編譯完成后媒熊,它們將部署在以太坊區(qū)塊鏈上奇适,并與特殊合約創(chuàng)建地址進(jìn)行交易。每個合約都由以太坊地址標(biāo)識芦鳍,該地址是作為始發(fā)帳戶和隨機(jī)數(shù)的函數(shù)從合約創(chuàng)建交易中獲得的嚷往。合約的以太坊地址可以作為收件人在交易中使用,將資金發(fā)送給合約或調(diào)用合約的一個功能柠衅。

重要的是皮仁,合約只有在交易調(diào)用時才會運(yùn)行。以太坊中的所有智能合約均由外部擁有賬戶發(fā)起的交易執(zhí)行。合約可以調(diào)用另一個可以調(diào)用另一個合約的合約贷祈,但是這樣的執(zhí)行鏈中的第一個合約必須始終由EOA的交易調(diào)用趋急。合約從不“自行”運(yùn)行,或“在后臺運(yùn)行”势誊。合約實(shí)際上在區(qū)塊鏈上處于“休眠”狀態(tài)呜达,直到交易觸發(fā)執(zhí)行,直接或間接作為合約調(diào)用鏈的一部分粟耻。

無論他們調(diào)用多少合約或這些合約在調(diào)用時做什么查近,交易都是原子的。交易完全執(zhí)行挤忙,只有在交易成功終止時才會記錄全局狀態(tài)(合約霜威,帳戶等)中的任何更改。成功終止意味著程序在沒有錯誤的情況下執(zhí)行并且到達(dá)執(zhí)行結(jié)束册烈。如果交易由于錯誤而失敗戈泼,則其所有影響(狀態(tài)更改)都“回滾”,就像交易從未運(yùn)行一樣赏僧。失敗的交易仍存儲在區(qū)塊鏈中大猛,并從原始賬戶中扣除燃?xì)赓M(fèi)用,但對合約或賬戶狀態(tài)沒有其他影響次哈。

合約的代碼無法更改胎署。但是,可以“刪除”合約窑滞,從區(qū)塊鏈中刪除代碼及其內(nèi)部狀態(tài)(變量)琼牧。要刪除合約,請執(zhí)行名為SELFDESTRUCT(以前稱為SUICIDE)的EVM操作碼哀卫,該操作碼將從區(qū)塊鏈中刪除合約巨坊。該操作花費(fèi)“negative gas”,從而激勵儲存狀態(tài)的釋放此改。以這種方式刪除合約不會刪除合約的交易歷史(過去)趾撵,因?yàn)閰^(qū)塊鏈本身是不可變的。但它確實(shí)從所有未來的區(qū)塊中刪除了合約狀態(tài)共啃。

以太坊高級語言簡介

EVM是一個仿真計(jì)算機(jī)占调,它運(yùn)行一種稱為EVM字節(jié)碼的特殊形式的機(jī)器代碼,就像計(jì)算機(jī)的CPU一樣移剪,它運(yùn)行機(jī)器代碼究珊,如x86_64。我們將在[evm]中更詳細(xì)地研究EVM的操作和語言纵苛。在本節(jié)中剿涮,我們將了解如何編寫智能合約以在EVM上運(yùn)行言津。

雖然可以直接在字節(jié)碼中編寫智能合約。EVM字節(jié)碼難以處理取试,程序員很難閱讀和理解悬槽。相反,大多數(shù)以太坊開發(fā)人員使用高級語言編寫程序瞬浓,并使用編譯器將其轉(zhuǎn)換為字節(jié)碼初婆。

雖然任何高級語言都可以用來編寫智能合約,但這是一項(xiàng)非常繁瑣的工作瑟蜈。智能合約在高度受限和簡約的執(zhí)行環(huán)境(EVM)中運(yùn)行烟逊,幾乎所有常用的用戶界面渣窜、操作系統(tǒng)接口和硬件接口都缺失铺根。從頭開始構(gòu)建簡約智能合約語言比限制通用語言并使其適合編寫智能合約更容易。結(jié)果乔宿,出現(xiàn)了許多用于編寫智能合約的特殊用途語言位迂。以太坊有幾種這樣的語言,以及生成EVM可執(zhí)行字節(jié)碼所需的編譯器详瑞。

通常掂林,編程語言可以分為兩種廣泛的編程范例:聲明式和命令式,也分別稱為“函數(shù)式”和“過程式”。在聲明式編程中,我們編寫表達(dá)程序邏輯的函數(shù)炎辨,而不是它的流程彭谁。聲明式編程用于創(chuàng)建沒有副作用的程序,意味著函數(shù)外部的狀態(tài)沒有變化未玻。聲明式編程語言包括例如Haskell,SQL和HTML。相比之下元莫,命令式編程是程序員編寫一組程序邏輯和流程的程序。命令式編程語言包括例如BASIC蝶押,C踱蠢,C ++和Java。有些語言是“混合”的棋电,這意味著它們鼓勵聲明式編程茎截,但也可以用來表達(dá)命令式編程范例。這種混合包括Lisp赶盔,Erlang企锌,Prolog,JavaScript和Python招刨。通常霎俩,任何命令式語言都可以用于在聲明性范例中編寫哀军,但它通常會導(dǎo)致代碼不雅。相比之下打却,純粹的聲明式語言不能用于編寫命令式范例杉适。在純粹的聲明式語言中,沒有“變量”柳击。

雖然命令式編程更容易編寫和讀取猿推,并且更常用于程序員,但編寫完全按預(yù)期執(zhí)行的程序可能非常困難捌肴。程序的任何部分都可以更改狀態(tài)蹬叭,這使得很難對程序的執(zhí)行進(jìn)行推理,并為意外的副作用和錯誤帶來許多機(jī)會状知。通過比較進(jìn)行聲明式編程更難以編寫秽五,但避免了副作用,從而更容易理解程序的行為方式饥悴。

智能合約給程序員帶來了很大的負(fù)擔(dān):bug需要花錢坦喘。因此,在不產(chǎn)生意外影響的情況下編寫智能合約至關(guān)重要西设。為此瓣铣,您必須能夠清楚地了解程序的預(yù)期行為。因此贷揽,聲明式語言在智能合約中的作用要大于通用軟件中的作用棠笑。然而,正如您將在下面看到的禽绪,智能合約(Solidity)最多產(chǎn)的語言勢在必行蓖救。

智能合約的高級編程語言包括(按年代排序):

  • LLL:一種函數(shù)式(聲明式)編程語言,具有類Lisp語法丐一。這是Ethereum采用的第一個高級語言藻糖,但在今天很少使用。
  • Serpent:一種過程式(命令式)編程語言库车,語法類似于Python巨柒。也可以用來編寫函數(shù)式(聲明式)代碼,盡管它并不是完全沒有副作用柠衍。使用較少洋满。首先由Vitalik Buterin創(chuàng)建。
  • Solidity:過程式(命令式)編程語言珍坊,其語法類似于JavaScript牺勾、c++或Java。Ethereum智能合約最流行阵漏、最常用的語言驻民。作者是Gavin Wood(這本書的合著者)翻具。
  • Vyper:一種最近開發(fā)的語言,類似于Serpent回还,具有類似于python的語法裆泳。想要比Serpent更接近一種純粹的類python語言,但不是為了取代Serpent柠硕。首先由Vitalik Buterin創(chuàng)建工禾。
  • Bamboo:一種新開發(fā)的語言,受Erlang的影響蝗柔,具有顯式的狀態(tài)轉(zhuǎn)換和沒有迭代流(循環(huán))闻葵。旨在減少副作用,增加可審核性癣丧。非常新穎槽畔,很少使用。

正如您所看到的坎缭,有許多語言可供選擇竟痰。然而签钩,到目前為止掏呼,Solidity是最受歡迎的,因?yàn)樗且蕴簧踔潦瞧渌馿vm一樣的區(qū)塊鏈上真正意義的高級語言铅檩。我們將花費(fèi)大部分時間使用solid憎夷,但也將探索其他高級語言中的一些示例,以了解它們的不同哲學(xué)昧旨。

構(gòu)建一個可靠的智能合約

引自維基百科:

Solidity是一種“面向合約的”編程語言拾给,用于編寫智能合約。它用于在各種區(qū)塊鏈平臺上實(shí)現(xiàn)智能合約兔沃。它是由Gavin Wood, Christian Reitwiessner, Alex Beregszaszi, Liana Husikyan, Yoichi Hirai和幾個以前的Ethereum的核心貢獻(xiàn)者在區(qū)塊鏈平臺上(如Ethereum)開發(fā)的蒋得。

--維基百科的Solidity條目

Solidity是由一個開發(fā)團(tuán)隊(duì)開發(fā)并維護(hù)的,GitHub上的Solidity項(xiàng)目:

https://github.com/ethereum/solidity

Solidity項(xiàng)目的主要“產(chǎn)品”是Solidity Compiler (solc)乒疏,它將用Solidity語言編寫的程序轉(zhuǎn)換為EVM字節(jié)碼额衙,并生成其他的artefacts,如應(yīng)用程序二進(jìn)制接口(ABI)怕吴。Solidity編譯器的每個版本對應(yīng)并編譯Solidity語言的特定版本窍侧。

首先,我們將下載Solidity編譯器的二進(jìn)制可執(zhí)行文件转绷。然后我們將編寫以及編譯一個簡單的合約伟件。

選擇一個Solidity版本

Solidity遵循一個稱為語義版本控制的版本控制模型(https://semver.org/),它指定版本號結(jié)構(gòu)為三個由點(diǎn)分隔的數(shù)字:MAJOR.MINOR.PATCH议经。主要的和向后的不兼容的變更會增加“major”數(shù)字斧账,“minor”數(shù)字會隨著在主要版本之間添加向后兼容的特性而增加谴返,“patch”數(shù)字會隨著錯誤修復(fù)和與安全相關(guān)的變更而增加。

目前咧织,Solidity在0.4.21版本亏镰,其中0.4是主要版本,21是次要版本拯爽,之后指定的任何內(nèi)容都是補(bǔ)丁發(fā)布索抓。即將發(fā)布0.5主要版本的Solidity。

正如我們在[intro]中看到的毯炮,您的Solidity程序可以包含一個pragma指令逼肯,該指令指定與之兼容的最小和最大的Solidity版本,并且可以用來編譯您的合約桃煎。

由于Solidity正在快速發(fā)展篮幢,所以最好總是使用最新的版本。

下載/安裝

有許多方法可以用來下載和安裝Solidity为迈,或者作為二進(jìn)制版本三椿,或者從源代碼編譯。你可在Solidity文件中找到詳細(xì)的說明:

https://solidity.readthedocs.io/en/latest/installing-solidity.html

在[使用apt包管理器在Ubuntu/Debian上安裝solc]時葫辐,我們將使用apt包管理器在Ubuntu/Debian操作系統(tǒng)上安裝最新的Solidity二進(jìn)制版本:

$ sudo add-apt-repository ppa:ethereum/ethereum
$ sudo apt update
$ sudo apt install solc

一旦安裝了solc搜锰,請運(yùn)行以下命令檢查版本:

$ solc --version
solc, the solidity compiler commandline interface
Version: 0.4.21+commit.dfe3193c.Linux.g++

根據(jù)您的操作系統(tǒng)和需求,有許多其他方法來安裝Solidity耿战,包括直接從源代碼編譯蛋叼。更多信息見:

https://github.com/ethereum/solidity

開發(fā)環(huán)境

要在Solidity中開發(fā),您可以在命令行上使用任何文本編輯器和solc剂陡。但是狈涮,您可能會發(fā)現(xiàn)一些為開發(fā)而設(shè)計(jì)的文本編輯器,比如Atom鸭栖,提供了一些額外的特性歌馍,比如語法高亮顯示和宏,這些特性使solid開發(fā)更加容易晕鹊。

還有基于web的開發(fā)環(huán)境松却,比如Remix IDE (https://remix.ethereum.org/)和EthFiddle (https://ethfiddle.com/)。

使用能讓你高效工作的工具捏题。最后玻褪,Solidity程序只是純文本文件。雖然花哨的編輯器和開發(fā)環(huán)境可以使事情變得更簡單公荧,但是您只需要一個簡單的文本編輯器带射,例如vim (Linux/Unix)、TextEdit (MacOS)甚至NotePad (Windows)循狰。只需使用.sol擴(kuò)展名保存程序源代碼窟社,它將被Solidity編譯器識別為一個Solidity程序券勺。

編寫一個簡單的Solidity程序

在[介紹]中,我們編寫了第一個Solidity程序灿里,叫做水龍頭(Faucet)关炼。當(dāng)我們第一次構(gòu)建水龍頭時,我們使用Remix IDE編譯和部署合約匣吊。在本節(jié)中儒拂,我們將重新討論、改進(jìn)和完善Faucet色鸳。

我們的第一次嘗試是這樣的:

Faucet.sol: 實(shí)現(xiàn)水龍頭的Solidity合約

link:code/Solidity/Faucet.sol[]

我們將以第一個例子為基礎(chǔ)社痛,從[make_it_better]開始。

使用Solidity編譯器(solc)進(jìn)行編譯

現(xiàn)在命雀,我們將使用命令行上的Solidity編譯器直接編譯我們的合約蒜哀。Solidity編譯器solc提供了各種選項(xiàng),您可以通過傳遞-help參數(shù)看到這些選項(xiàng)吏砂。

我們使用solc的--bin和--optimize參數(shù)來生成示例合約的優(yōu)化二進(jìn)制文件:

使用solc編譯Faucet.sol

$ solc --optimize --bin Faucet.sol
======= Faucet.sol:Faucet =======
Binary:
6060604052341561000f57600080fd5b60cf8061001d6000396000f300606060405260043610603e5763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416632e1a7d4d81146040575b005b3415604a57600080fd5b603e60043567016345785d8a0000811115606357600080fd5b73ffffffffffffffffffffffffffffffffffffffff331681156108fc0282604051600060405180830381858888f19350505050151560a057600080fd5b505600a165627a7a723058203556d79355f2da19e773a9551e95f1ca7457f2b5fbbf4eacf7748ab59d2532130029

solc產(chǎn)生的結(jié)果是一個十六進(jìn)制串行化的二進(jìn)制文件撵儿,可以提交到Ethereum區(qū)塊鏈。

Ethereum應(yīng)用程序二進(jìn)制接口(ABI)

在計(jì)算機(jī)軟件中狐血,應(yīng)用程序二進(jìn)制接口(ABI)是兩個程序模塊之間的接口;通常淀歇,一個在機(jī)器代碼的級別,另一個在一個由用戶運(yùn)行的程序級別氛雪。ABI定義了如何在機(jī)器代碼中訪問數(shù)據(jù)結(jié)構(gòu)和函數(shù);不要把它與API混淆房匆,API將這種訪問定義為高級的类少、通常是人類可讀的格式作為源代碼呻纹。因此缀踪,ABI是編碼和解碼數(shù)據(jù)進(jìn)出機(jī)器代碼的主要方式。

在Ethereum中弦追,ABI用于對EVM的合約調(diào)用進(jìn)行編碼,并從交易中讀取數(shù)據(jù)花竞。ABI的目的是定義可以調(diào)用合約中的哪些函數(shù)劲件,并描述函數(shù)如何接受參數(shù)和返回數(shù)據(jù)。

合約ABI的JSON格式由函數(shù)描述(請參見[solidity_function])和事件(請參見[solidity_events])描述的數(shù)組給出约急。函數(shù)描述是一個JSON對象零远,包含typename厌蔽、inputs牵辣、outputsconstant和payable的字段奴饮。事件描述對象具有type纬向、name择浊、inputsanonymous字段。

我們使用solc命令行solidity編譯器為我們的Faucet.sol示例合約生成ABI:

solc --abi Faucet.sol
======= Faucet.sol:Faucet =======
Contract JSON ABI
[{"constant":false,"inputs":[{"name":"withdraw_amount","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"}]

如您所見逾条,編譯器生成一個JSON對象琢岩,該對象描述了Faucet.sol定義的兩個函數(shù)。這個JSON對象可以被任何應(yīng)用程序使用师脂,一旦它被部署担孔,它就會訪問這個Faucet合約。使用ABI(一個應(yīng)用程序吃警,如錢包或DApp瀏覽器)攒磨,可以構(gòu)造調(diào)用水龍頭中的函數(shù)的交易,使用正確的參數(shù)和參數(shù)類型汤徽。例如娩缰,一個錢包會知道要調(diào)用函數(shù)withdraw,它必須提供一個名為abstr_amount的uint256參數(shù)谒府。錢包可以提示用戶提供該值拼坎,然后創(chuàng)建一個對其進(jìn)行編碼并執(zhí)行withdraw函數(shù)的交易。

應(yīng)用程序與合約交互所需要的只是一個ABI和部署合約的地址完疫。

選擇Solidity編譯器和語言版本

正如我們在編輯水龍頭時看到的泰鸡。使用solc我們成功地編譯了Solidity 0.4.21版本的水龍頭合約。但是如果我們使用的是另一個版本的Solidity編譯器呢?語言仍在不斷變化壳鹤,事物可能以意想不到的方式發(fā)生變化盛龄。我們的合約相當(dāng)簡單,但是如果我們的程序使用的特性僅僅是在Solidity版本0.4.19中添加的芳誓,并且我們試圖用0.4.18編譯它余舶,會怎么樣呢?

為了解決這些問題,Solidity提供了一個編譯器指令锹淌,稱為版本pragma匿值,它指示編譯器程序需要一個特定的編譯器(和語言)版本。讓我們看一個例子:

pragma solidity ^0.4.19;

如果編譯器版本與版本編譯指示不兼容赂摆,那么Solidity編譯器將讀取版本編譯指示并產(chǎn)生錯誤挟憔。在這種情況下,我們的版本編譯指示說這個程序可以被一個0.4.19版本以上的Solidity編譯器編譯烟号。符號^states,然而,我們允許編譯0.4.19上面任何輕微的修改,例如,0.4.20,但不是0.5.0(這是一個主要的版本,不是一個小修改)绊谭。Pragma指令沒有編譯成EVM字節(jié)碼。編譯器只使用它們檢查兼容性汪拥。

讓我們在我們的水龍頭合約中加入一個實(shí)用指令达传。我們將命名新的文件Faucet2.sol,在我們進(jìn)行這些例子時,注意我們的變化:

link:code/Solidity/Faucet2.sol[]

添加版本編譯指示是一種最佳實(shí)踐趟大,因?yàn)樗苊饬司幾g器和語言版本不匹配的問題鹤树。我們將探索其他最佳實(shí)踐,并在本章中繼續(xù)改進(jìn)水龍頭合約逊朽。

使用Solidity編程

在這一節(jié)中罕伯,我們將討論Solidity語言的一些功能。正如我們在[介紹]中提到的叽讳,我們的第一個合約示例非常簡單追他,并且在許多方面存在缺陷。我們將逐步改進(jìn)這個示例岛蚤,同時學(xué)習(xí)如何使用solid邑狸。不過,這不是一個全面的Solidity教程涤妒,因?yàn)镾olidity是相當(dāng)復(fù)雜和快速發(fā)展的单雾。我們將介紹基本知識,并為您提供足夠的基礎(chǔ)她紫,以便您能夠自己探索其他知識硅堆。完整的Solidity文檔載于:

https://solidity.readthedocs.io/en/latest/

數(shù)據(jù)類型

首先,讓我們看看一些Solidity基本數(shù)據(jù)類型:

  • boolean (bool):布爾值贿讹,真或假渐逃,帶有邏輯運(yùn)算符! (not), && (and), || (or), == (equal), != (not equal)。
  • integer (int/uint):有符號(int)和無符號(uint)整數(shù)民褂,從u/int8到u/int256的增量為8位茄菊。沒有大小后綴,它們被設(shè)置為256位赊堪。
  • fixed point (fixed/ufixed):定點(diǎn)數(shù)面殖,定義為u/fixedMxN,其中M是位的大小(增量為8)雹食,N是小數(shù)點(diǎn)后的小數(shù)位畜普。
  • address:一個20字節(jié)Ethereum地址。地址對象具有成員余額(返回帳戶余額)和轉(zhuǎn)賬(將ether轉(zhuǎn)移到帳戶)群叶。
  • byte array (fixed):固定大小的字節(jié)數(shù)組,定義為bytes1到bytes32
  • byte array (dynamic):動態(tài)大小的字節(jié)數(shù)組钝荡,定義為字節(jié)或字符串
  • enum:用于枚舉離散值的用戶定義類型街立。
  • struct:用戶定義用于分組變量的數(shù)據(jù)容器。
  • mapping:key =>value對的哈希查找表埠通。

除了上面的數(shù)據(jù)類型赎离,Solidity還提供了各種各樣的值類型,可以用來計(jì)算不同的單位:

  • time units:單位秒端辱、分鐘梁剔、小時和天可以用作后綴虽画,轉(zhuǎn)換為基數(shù)單位秒的倍數(shù)。
  • ether units:單位wei荣病、finney码撰、szabo和ether可以作為后綴,轉(zhuǎn)換成單位wei的倍數(shù)个盆。

到目前為止脖岛,在我們的Faucet合約示例中,我們使用了uint(它是uint256的別名)颊亮,用于提取變量柴梆。我們還間接地使用了一個地址變量msg.sender。在本章中终惑,我們將在示例中更多地使用這些數(shù)據(jù)類型绍在。

讓我們使用一個單位乘數(shù),以提高我們的示例合約水龍頭的可讀性雹有。在提取函數(shù)中偿渡,我們對最大提取量進(jìn)行了限制,用ether的基元wei表示:

require(withdraw_amount <= 100000000000000000);

這不是很容易閱讀件舵,所以我們可以通過使用單位乘數(shù)ether來改進(jìn)代碼卸察,用ether代替wei來表達(dá)這個值:

require(withdraw_amount <= 0.1 ether);

預(yù)定義全局變量和函數(shù)

當(dāng)在EVM中執(zhí)行合約時,它可以訪問一組有限的全局對象铅祸。其中包括block坑质、msg和tx對象。此外临梗,Solidity將許多EVM操作碼作為預(yù)定義的Solidity函數(shù)公開涡扼。在本節(jié)中,我們將檢查在一個智能合約中您可以從一個Solidity合約中訪問的變量和函數(shù)盟庞。

調(diào)用交易/消息上下文

msg對象是發(fā)起合約執(zhí)行的交易(EOA發(fā)起)或消息(合約發(fā)起)吃沪。它包含許多有用的屬性:

  • msg.sender:我們已經(jīng)用過這個了。它表示發(fā)起消息的地址什猖。如果我們的合約被一個EOA交易調(diào)用票彪,那么這就是簽署該交易的地址。
  • msg.value:發(fā)送消息用的以太值不狮。
  • msg.gas:在我們的合約中所剩的gas量降铸。它已經(jīng)被棄用,并將在solid v0.4.21中用gasleft()函數(shù)替換摇零。
  • msg.data:調(diào)用我們合約的消息的數(shù)據(jù)有效負(fù)載推掸。
  • msg.sig:數(shù)據(jù)負(fù)載的前四個字節(jié),即函數(shù)選擇器。
Note 每當(dāng)合約調(diào)用另一個合約時谅畅,msg的所有屬性的值都會改變登渣,以反映新的調(diào)用者的信息。唯一的例外是delegatecall函數(shù)毡泻,它在原始的msg上下文中運(yùn)行另一個合約/庫的代碼胜茧。

交易上下文:

  • tx.gasprice:調(diào)用交易中的gas價格。
  • tx.origin:來自發(fā)起(EOA)交易的完整調(diào)用堆棧牙捉。

區(qū)塊上下文:

  • block:區(qū)塊對象包含有關(guān)當(dāng)前區(qū)塊的信息竹揍。
  • block.blockhash(blockNumber):指定區(qū)塊號的區(qū)塊哈希,直到最后256區(qū)塊為止邪铲。Solidity v.0.4.22已棄用芬位,替換為blockhash()函數(shù)。
  • block.coinbase:當(dāng)前區(qū)塊的礦工的地址带到。
  • block.difficulty:當(dāng)前區(qū)塊的困難(工作證明)昧碉。
  • block.gaslimit:當(dāng)前區(qū)塊的總gas限制。
  • block.number:當(dāng)前區(qū)塊號(高度)揽惹。
  • block.timestamp:從Unix紀(jì)元(秒)開始被饿,由礦工放在當(dāng)前區(qū)塊中的時間戳。

地址對象

任何作為輸入傳遞的地址或從合約對象轉(zhuǎn)換的地址都具有許多屬性和方法:

  • address.balance:地址的余額(單位wei)搪搏。例如狭握,當(dāng)前合約余額是address(this).balance。
  • address.transfer(amount):將金額(wei)轉(zhuǎn)移到此地址疯溺,對任何錯誤拋出異常论颅。我們在水龍頭例子中使用了msg.sender.transfer()這個函數(shù)作為msg.sender地址的一個方法,。
  • address.send(amount):與上面的transfer類似囱嫩,它只會在錯誤時返回false而不會拋出異常恃疯。
  • address.call():不常用的調(diào)用函數(shù),可以構(gòu)造任意的帶有value墨闲、數(shù)據(jù)有效負(fù)載的消息今妄。錯誤返回false。
  • address.delegatecall():不常用的調(diào)用函數(shù)鸳碧,保持調(diào)用合約的msg上下文盾鳞,錯誤時返回false。

內(nèi)置函數(shù)

  • addmod, mulmod:取模的加法和乘法瞻离。例如雁仲,addmod(x,y,k)計(jì)算為 (x+y)%k。
  • keccak256, sha256, sha3, ripemd160:使用各種標(biāo)準(zhǔn)哈希算法計(jì)算哈希的函數(shù)。
  • ecrecover:從簽名中恢復(fù)用于簽名消息的地址蓝谨。

合約的定義

Solidity的主要數(shù)據(jù)類型是contract對象恨溜,它在我們的水龍頭示例的頂部定義子刮。與面向?qū)ο笳Z言中的任何對象相似弦疮,合約是包含數(shù)據(jù)和方法的容器诱建。

Solidity還提供了另外兩個與合約相似的對象:

  • interface:接口定義的結(jié)構(gòu)與合約完全相同钮蛛,除了沒有定義任何函數(shù)之外昂拂,它們只被聲明受神。這種類型的函數(shù)聲明通常稱為stub,因?yàn)樗嬖V您參數(shù)格侯,并返回所有類型的函數(shù)鼻听,而不使用任何實(shí)現(xiàn)。它用于指定合約接口联四,如果繼承撑碴,則必須在子函數(shù)中指定每個函數(shù)。
  • library:庫合約是一種僅被部署一次并被其他合約使用的合約朝墩,使用delegatecall 方法(參見Address對象)醉拓。

函數(shù)

在合約中,我們定義可由EOA交易或其他合約調(diào)用的函數(shù)收苏。在我們的水龍頭示例中亿卤,我們有兩個函數(shù):withdraw函數(shù)和(未命名的)fallback函數(shù)。

函數(shù)的定義有以下語法:

function FunctionName([parameters]) {public|private|internal|external} [pure|constant|view|payable] [modifiers] [returns (<return types>)]

讓我們來看看這些組成部分:

  • FunctionName:定義函數(shù)的名稱鹿霸,該函數(shù)用于從交易(EOA)排吴、其他合約或同一合約中調(diào)用函數(shù)。每個合約中的一個函數(shù)可以在沒有名稱的情況下定義懦鼠,在這種情況下钻哩,它是回退函數(shù),當(dāng)沒有其他函數(shù)被命名時葛闷,調(diào)用回退函數(shù)憋槐。回退函數(shù)不能有任何參數(shù)或返回任何內(nèi)容淑趾。
  • parameters:在名稱之后阳仔,我們指定必須傳遞給函數(shù)的參數(shù)及其名稱和類型。在我們的水龍頭示例中扣泊,我們將uint withdraw_amount定義為withdraw函數(shù)的唯一參數(shù)近范。

下一組關(guān)鍵字(public, private, internal, external)指定函數(shù)的可見性:

  • public:public是默認(rèn)值,此類函數(shù)可以由其他合約延蟹、EOA交易或合約內(nèi)部調(diào)用评矩。在我們的水龍頭示例中,這兩個函數(shù)都被定義為public阱飘。
  • external:外部函數(shù)類似于public斥杜,除了它們不能從合約中調(diào)用虱颗,除了它們以this作為前綴。
  • private:內(nèi)部函數(shù)僅在合約中“可見”蔗喂,不能被其他合約或EOA交易調(diào)用忘渔。它們可以通過派生合約(繼承它們的合約)調(diào)用。
  • internal:私有函數(shù)類似于內(nèi)部函數(shù)缰儿,但不能通過派生合約(繼承它們的合約)調(diào)用它們畦粮。

記住,內(nèi)部和私有的術(shù)語都有一定的誤導(dǎo)性乖阵。在公共區(qū)塊鏈中宣赔,合約中的任何函數(shù)或數(shù)據(jù)總是可見的,這意味著任何人都可以看到代碼或數(shù)據(jù)瞪浸。上面的關(guān)鍵字只影響函數(shù)的調(diào)用方式和調(diào)用時間儒将。

下一組關(guān)鍵字(pure、constant默终、view椅棺、payable)會影響函數(shù)的行為:

  • constant/view:標(biāo)記為view的函數(shù),承諾不修改任何狀態(tài)齐蔽。術(shù)語constant是將被棄用的view的別名两疚。此時,編譯器不會強(qiáng)制執(zhí)行view修飾符含滴,只會產(chǎn)生一個警告诱渤,但這將成為solid的v0.5中的一個強(qiáng)制關(guān)鍵字。
  • pure:pure函數(shù)是既不讀也不寫變量的函數(shù)谈况。它只能對參數(shù)進(jìn)行操作并返回數(shù)據(jù)勺美,而不引用任何存儲的數(shù)據(jù)。pure函數(shù)旨在鼓勵聲明式編程碑韵,而不產(chǎn)生副作用或狀態(tài)赡茸。
  • payable:可支付的函數(shù)是可以接受輸入的支付。沒有應(yīng)付款項(xiàng)的功能將拒絕收到的款項(xiàng)祝闻,除非它們源自一個共同基數(shù)(采礦收入)或作為自毀(合約終止)的目的地占卧。在這些情況下,由于EVM中的設(shè)計(jì)決策联喘,合約不能阻止支付华蜒。

正如您在我們的水龍頭示例中所看到的,我們有一個可支付函數(shù)(fallback函數(shù))豁遭,它是唯一可以接收到支付的函數(shù)叭喜。

合約構(gòu)造函數(shù)和selfdestruct

這是一個只使用一次的特殊函數(shù)。當(dāng)創(chuàng)建一個合約時蓖谢,它還運(yùn)行構(gòu)造函數(shù)(如果存在的話)來初始化合約的狀態(tài)捂蕴。構(gòu)造函數(shù)在與創(chuàng)建合約相同的交易中運(yùn)行譬涡。構(gòu)造函數(shù)是可選的。實(shí)際上启绰,我們的水龍頭示例沒有構(gòu)造函數(shù)昂儒。

構(gòu)造函數(shù)可以通過兩種方式指定。Solidity v.0.4.21 以下委可,構(gòu)造函數(shù)是一個函數(shù),其名稱與合約的名稱相匹配:

contract MEContract {
    function MEContract() {
        // This is the constructor
    }
}

這種格式的困難在于腊嗡,如果更改了合約名稱着倾,且構(gòu)造函數(shù)的名稱沒有更改,則不再是構(gòu)造函數(shù)燕少。這可能導(dǎo)致一些非常討厭的卡者、意想不到的和難以注意到的bug。例如客们,假設(shè)構(gòu)造函數(shù)正在為控制目的設(shè)置合約的“所有者”崇决。它不僅不會在合約成立時設(shè)置所有者,還可以像正常功能一樣“callable”底挫,允許任何第三方劫持合約恒傻,在合約成立后成為“所有者”。

為了解決基于相同名稱作為合約的構(gòu)造函數(shù)的潛在問題建邓,Solidity v0.4.22引入了一個構(gòu)造函數(shù)關(guān)鍵字盈厘,它的操作方式類似于構(gòu)造函數(shù),但沒有名稱官边。重命名合約并不影響構(gòu)造函數(shù)沸手。而且,更容易識別哪個函數(shù)是構(gòu)造函數(shù)注簿。它看起來像這樣:

pragma ^0.4.22
contract MEContract {
    constructor () {
        // This is the constructor
    }
}

因此契吉,總而言之,合約的生命周期始于EOA或其他合約的創(chuàng)建交易诡渴。如果有構(gòu)造函數(shù)捐晶,則從相同的創(chuàng)建交易中調(diào)用它,并可以在創(chuàng)建合約時初始化合約的狀態(tài)玩徊。

合約生命周期的另一端是合約銷毀租悄。合約被一種叫做SELFDESTRUCT的特殊EVM操作碼銷毀。它曾經(jīng)是名字自殺恩袱,但由于這個詞的負(fù)面聯(lián)想泣棋,這個名字被棄用了。在Solidity中畔塔,這個opcode被公開為一個名為selfdestruct的高級內(nèi)建函數(shù)潭辈,它接受一個參數(shù):在合約帳戶中獲得任何余額的地址鸯屿。它看起來像這樣:

selfdestruct(address recipient);

在我們的水龍頭示例中添加構(gòu)造函數(shù)和selfdestruct

我們在[intro]中介紹的水龍頭示例合約沒有任何構(gòu)造函數(shù)或自毀函數(shù)。這是一個永遠(yuǎn)的合約把敢,不能從區(qū)塊鏈中刪除寄摆。我們通過添加構(gòu)造函數(shù)和selfdestruction函數(shù)來改變這一點(diǎn)。我們可能希望銷毀只能由最初創(chuàng)建合約的EOA調(diào)用修赞。按照慣例婶恼,這通常存儲在地址變量中,稱為owner柏副。我們的構(gòu)造函數(shù)設(shè)置所有者變量勾邦,而selfdestruction函數(shù)將首先檢查所有者是否調(diào)用了它。

// Version of Solidity compiler this program was written for
pragma solidity ^0.4.22;

// Our first contract is a faucet!
contract Faucet {

    address owner;

    // Initialize Faucet contract: set owner
    constructor() {
        owner = msg.sender;
    }

[...]

我們修改了pragma指令割择,將v0.4.22指定為本例的最小版本眷篇,因?yàn)槲覀兪褂玫氖侵挥性赟olidity的v0.4.22中才存在的新構(gòu)造函數(shù)關(guān)鍵字。我們的合約現(xiàn)在有一個名為owner的地址類型變量荔泳〗侗“owner”這個名字在任何方面都不特殊。我們可以把這個地址變量叫做“potato”玛歌,仍然用同樣的方法昧港。名稱所有者只是明確了意圖和目的。

然后沾鳄,我們的構(gòu)造函數(shù)慨飘,作為合約創(chuàng)建交易的一部分運(yùn)行,即從msg.sender分配地址到所有者變量译荞。我們使用msg.sender在提取函數(shù)中識別提取請求的來源瓤的。然而,在構(gòu)造函數(shù)中吞歼,msg.sender是簽署合約創(chuàng)建交易的EOA或合約地址圈膏。我們知道這是一種情況,因?yàn)檫@是一個構(gòu)造函數(shù):它只運(yùn)行一次篙骡,而且只作為創(chuàng)建合約交易的結(jié)果稽坤。

好的,現(xiàn)在我們可以添加一個函數(shù)來毀壞合約糯俗。我們需要確保只有所有者可以運(yùn)行這個函數(shù)尿褪,因此我們將使用require語句來控制訪問。下面是它的樣子:

// Contract destructor
function destroy() public {
    require(msg.sender == owner);
    selfdestruct(owner);
}

如果其他任何人從所有者以外的地址調(diào)用此銷毀函數(shù)得湘,它將失敗杖玲。但是如果構(gòu)造函數(shù)調(diào)用的相同地址被構(gòu)造函數(shù)調(diào)用,則該合約將自毀并將剩余的余額發(fā)送給所有者地址淘正。

函數(shù)修飾符

Solidity提供了一種特殊類型的函數(shù)摆马,稱為函數(shù)修飾符臼闻。通過在函數(shù)聲明中添加修飾符名,可以對函數(shù)應(yīng)用修飾符囤采。修改函數(shù)通常用于創(chuàng)建適用于合約中的許多函數(shù)的條件述呐。我們已經(jīng)有了一個訪問控制語句,在我們的銷毀函數(shù)中蕉毯。讓我們創(chuàng)建一個函數(shù)修飾符來表達(dá)這個條件:

onlyOwner函數(shù)修飾符

modifier onlyOwner {
    require(msg.sender == owner);
    _;
}

在onlyOwner函數(shù)修飾符中乓搬,我們看到函數(shù)修飾符的聲明,名為onlyOwner恕刘。該函數(shù)修飾符在它修改的任何函數(shù)上設(shè)置一個條件缤谎,要求作為合約所有者存儲的地址與交易的msg.sender的地址相同。這是訪問控制的基本設(shè)計(jì)模式褐着,允許只有合約的所有者執(zhí)行任何具有唯一所有者修飾符的函數(shù)。

您可能已經(jīng)注意到托呕,我們的函數(shù)修飾符中有一個特殊的語法“占位符”,下劃線后面跟著分號(_;)。這個占位符被正在修改的函數(shù)的代碼所取代司顿∶潭疲基本上,修飾符被“包裝”在修改后的函數(shù)中着降,將其代碼放在由下劃線字符標(biāo)識的位置差油。

要應(yīng)用修飾符,請將其名稱添加到函數(shù)聲明中任洞⌒罾可以將多個修飾符應(yīng)用于函數(shù)(作為逗號分隔的列表),并在聲明它們的序列中應(yīng)用交掏。

讓我們重寫我們的銷毀函數(shù)來使用唯一的所有者修飾符:

function destroy() public onlyOwner {
    selfdestruct(owner);
}

函數(shù)修飾符的名稱(onlyOwner)位于關(guān)鍵字public之后妆偏,并告訴我們銷毀函數(shù)是由onlyOwner修飾符修改的。從本質(zhì)上來說盅弛,你可以把它理解為“只有所有者才能銷毀這份合約”钱骂。在實(shí)踐中,產(chǎn)生的代碼相當(dāng)于“包裝”來自僅有的所有者的代碼挪鹏。

函數(shù)修飾符是一種非常有用的工具见秽,因?yàn)樗鼈冊试S我們?yōu)楹瘮?shù)編寫前置條件并一致地應(yīng)用它們,使代碼更易于閱讀讨盒,從而更容易對安全性問題進(jìn)行審計(jì)解取。它們通常用于訪問控制,如示例[onlyOwner函數(shù)修飾符]催植,但是它們非常通用肮蛹,可以用于各種其他目的勺择。

在修飾符中,您可以訪問修改函數(shù)可見的所有符號(變量和參數(shù))伦忠。在這種情況下省核,我們可以訪問owner變量,它在合約中聲明昆码。但是气忠,反過來并不是正確的:您不能訪問修飾符在修改函數(shù)中的任何變量。

合約繼承

Solidity的合約對象支持繼承赋咽,這是一種使用附加功能擴(kuò)展基本合約的機(jī)制旧噪。若要使用繼承,請指定具有關(guān)鍵字的父合約為:

contract Child is Parent {
}

通過這個構(gòu)造脓匿,子合約繼承了父合約的所有方法淘钟、功能和變量。Solidity還支持多重繼承陪毡,可以通過關(guān)鍵字后面的逗號分隔的合約名指定:

contract Child is Parent1, Parent2 {
}

合約繼承允許我們以這樣的方式編寫合約米母,以實(shí)現(xiàn)模塊化、可擴(kuò)展性和重用毡琉。我們從簡單的合約開始铁瞒,并實(shí)現(xiàn)最通用的功能,然后通過在更專門化的合約中繼承這些功能來擴(kuò)展它們桅滋。

在我們的水龍頭合約中慧耍,我們介紹了構(gòu)造函數(shù)和析構(gòu)函數(shù),以及分配給所有者的訪問控制丐谋。這些功能是非常通用的:許多合約都有它們芍碧。我們可以將它們定義為泛型合約,然后使用繼承將它們擴(kuò)展到水龍頭合約笋鄙。

我們首先定義一個所有者的基本合約师枣,它有所有者變量,并將其設(shè)置在合約的構(gòu)造函數(shù)中:

contract owned {
    address owner;

    // Contract constructor: set owner
    constructor() {
        owner = msg.sender;
    }

    // Access control modifier
    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }
}

接下來萧落,我們定義一個基本合約mortal践美,繼承其owned:

contract mortal is owned {
    // Contract destructor
    function destroy() public onlyOwner {
        selfdestruct(owner);
    }
}

如您所見,mortal合約可以使用唯一的所有者函數(shù)修飾符找岖,定義在owner中陨倡。它還間接使用所有者地址變量和所有者定義的構(gòu)造函數(shù)。繼承使每個合約都更簡單许布,并且關(guān)注類的特定功能兴革,允許我們以模塊化的方式管理細(xì)節(jié)。

現(xiàn)在我們可以進(jìn)一步擴(kuò)展owned合同,繼承其在Faucet上的功能:

contract Faucet is mortal {
    // Give out ether to anyone who asks
    function withdraw(uint withdraw_amount) public {
        // Limit withdrawal amount
        require(withdraw_amount <= 100000000000000000);
        // Send the amount to the address that requested it
        msg.sender.transfer(withdraw_amount);
    }
    // Accept any incoming amount
    function () public payable {}
}

通過繼承mortal杂曲,而mortal又繼承了自己的owned庶艾,水龍頭合約現(xiàn)在擁有構(gòu)造函數(shù)和銷毀函數(shù),以及一個定義的所有者擎勘。功能與水龍頭內(nèi)的功能相同咱揍,但現(xiàn)在我們可以在其他合約中重用這些功能,而不必再編寫它們棚饵。代碼重用和模塊化使我們的代碼更清晰煤裙、更容易閱讀和更容易審計(jì)。

錯誤處理(assert, require, revert)

合約調(diào)用可以終止并返回錯誤噪漾。在Solidity中硼砰,錯誤處理由4個函數(shù)來處理:assert、require欣硼、restore和throw(現(xiàn)在已棄用)题翰。

當(dāng)一個合約以錯誤結(jié)束時,如果調(diào)用了多個合約诈胜,則所有的狀態(tài)變化(對變量遍愿、余額等的更改)都將被恢復(fù),并一直向上循環(huán)到合約調(diào)用的鏈中耘斩。這確保交易是原子的,這意味著它們要么成功完成桅咆,要么對狀態(tài)沒有影響括授,然后完全恢復(fù)。

assert和require函數(shù)以相同的方式操作岩饼,評估條件荚虚,如果條件為假,則停止執(zhí)行籍茧。根據(jù)約定版述,assert在預(yù)期結(jié)果為真時使用,這意味著我們使用assert來測試內(nèi)部條件寞冯。相比之下渴析,在測試輸入(如函數(shù)參數(shù)或交易字段)時,我們使用require吮龄,以設(shè)置我們對這些條件的期望俭茧。

我們已經(jīng)在我們的函數(shù)修飾符中使用了require,以測試消息發(fā)送方是該合約的所有者:

require(msg.sender == owner);

require函數(shù)作為一個gate條件漓帚,阻止函數(shù)其余部分的執(zhí)行母债,如果不滿足就會產(chǎn)生錯誤。

從Solidity v.0.4.22開始,require還可以包含一個有用的文本消息毡们,可以用來顯示錯誤的原因迅皇。錯誤消息被記錄在交易日志中。因此衙熔,我們可以通過在require函數(shù)中添加錯誤消息來改進(jìn)代碼:

require(msg.sender == owner, "Only the contract owner can call this function");

revert和throw函數(shù)登颓,停止執(zhí)行合約并恢復(fù)任何狀態(tài)更改。throw函數(shù)已經(jīng)過時青责,將在以后的Solidity版本中刪除——您應(yīng)該使用“revert”挺据。還原函數(shù)還可以將錯誤消息作為唯一的參數(shù),該參數(shù)記錄在交易日志中脖隶。

合約中的某些條件會產(chǎn)生錯誤扁耐,不管我們是否明確地檢查它們。例如产阱,在我們的水龍頭合約中婉称,我們不檢查是否有足夠的ether來滿足提取要求。這是因?yàn)楣沟牛绻麤]有足夠的余額進(jìn)行轉(zhuǎn)賬王暗,則傳輸函數(shù)將失敗并出現(xiàn)錯誤,并恢復(fù)交易:

如果余額不足庄敛,transfer函數(shù)將失敗

msg.sender.transfer(withdraw_amount);

但是俗壹,最好是顯式地檢查并在失敗時提供明確的錯誤消息。我們可以通過在轉(zhuǎn)賬前添加一個要求聲明來做到這一點(diǎn):

require(this.balance >= withdraw_amount,
    "Insufficient balance in faucet for withdrawal request");
msg.sender.transfer(withdraw_amount);

像這樣額外的錯誤檢查代碼將會稍微增加gas消耗藻烤,但是它提供了比忽略更好的錯誤報告绷雏。在gas消耗和詳細(xì)的錯誤檢查之間找到正確的平衡是你需要根據(jù)你的合約的預(yù)期使用來決定的。對于一個用于測試網(wǎng)絡(luò)的水龍頭怖亭,我們可能會在額外的報告上犯錯誤涎显,即使它需要更多的gas。也許對于主網(wǎng)絡(luò)合約兴猩,我們會選擇節(jié)約使用gas期吓。

Events

事件是促進(jìn)交易日志生成的可靠構(gòu)造。當(dāng)一個交易完成(成功與否)時倾芝,它將產(chǎn)生一個交易收據(jù)讨勤,我們將在[evm]中看到。交易收據(jù)包含日志條目蛀醉,這些條目提供關(guān)于在執(zhí)行交易期間發(fā)生的操作的信息悬襟。事件是用于構(gòu)造這些日志的可靠的高級對象。

事件在輕量級客戶端和DApps中尤其有用拯刁,它們可以“監(jiān)視”特定事件并將它們報告給用戶界面脊岳,或者更改應(yīng)用程序的狀態(tài)以反映底層合約中的事件。

事件對象接受序列化并記錄在交易日志(在區(qū)塊鏈中)中的參數(shù)「钔保可以在參數(shù)之前提供關(guān)鍵字索引奶躯,使索引表(哈希表)的值部分能夠被應(yīng)用程序搜索或過濾。

到目前為止亿驾,我們還沒有在我們的水龍頭示例中添加任何事件嘹黔,所以讓我們這樣做。我們將添加兩個事件莫瞬,一個用于記錄取款儡蔓,一個用于記錄存款。我們將這些事件分別稱為Withdrawal和Deposit疼邀。首先喂江,我們定義水龍頭合約中的事件:

contract Faucet is mortal {
    event Withdrawal(address indexed to, uint amount);
    event Deposit(address indexed from, uint amount);

    [...]
}

我們選擇將地址編入索引,以便在任何用于訪問水龍頭的用戶界面中進(jìn)行搜索和過濾旁振。

接下來获询,我們使用emit關(guān)鍵字將事件數(shù)據(jù)合并到交易日志中:

// Give out ether to anyone who asks
function withdraw(uint withdraw_amount) public {
    [...]
    msg.sender.transfer(withdraw_amount);
    emit Withdrawal(msg.sender, withdraw_amount);
}
// Accept any incoming amount
function () public payable {
    emit Deposit(msg.sender, msg.value);
}

由此產(chǎn)生的Faucet.sol合約是這樣的:

Faucet8.sol:修改后的水龍頭合約,包括事件

link:code/Solidity/Faucet8.sol[]

捕獲事件

好的拐袜,我們已經(jīng)建立了合約去emit事件吉嚣。我們?nèi)绾慰吹浇灰椎慕Y(jié)果并“捕獲”事件?web3.js庫提供一個數(shù)據(jù)結(jié)構(gòu),作為包含交易日志的交易的結(jié)果蹬铺。在其中尝哆,我們可以看到交易生成的事件。

讓我們使用truffle對修改后的水龍頭合約進(jìn)行測試甜攀。按照[truffle]中的說明設(shè)置一個項(xiàng)目目錄并編譯Faucet代碼较解。源代碼可以在本書的GitHub資源庫中找到:

code/truffle/FaucetEvents
$ truffle develop
truffle(develop)> compile
truffle(develop)> migrate
Using network 'develop'.

Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0xb77ceae7c3f5afb7fbe3a6c5974d352aa844f53f955ee7d707ef6f3f8e6b4e61
  Migrations: 0x8cdaf0cd259887258bc13a92c0a6da92698644c0
Saving successful migration to network...
  ... 0xd7bc86d31bee32fa3988f1c1eabce403a1b5d570340a3a9cdba53a472ee8c956
Saving artifacts...
Running migration: 2_deploy_contracts.js
  Deploying Faucet...
  ... 0xfa850d754314c3fb83f43ca1fa6ee20bc9652d891c00a2f63fd43ab5bfb0d781
  Faucet: 0x345ca3e014aaf5dca488057592ee47305d9b3e10
Saving successful migration to network...
  ... 0xf36163615f41ef7ed8f4a8f192149a0bf633fe1a2398ce001bf44c43dc7bdda0
Saving artifacts...

truffle(develop)> Faucet.deployed().then(i => {FaucetDeployed = i})
truffle(develop)> FaucetDeployed.send(web3.toWei(1, "ether")).then(res => { console.log(res.logs[0].event, res.logs[0].args) })
Deposit { from: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  amount: BigNumber { s: 1, e: 18, c: [ 10000 ] } }
truffle(develop)> FaucetDeployed.withdraw(web3.toWei(0.1, "ether")).then(res => { console.log(res.logs[0].event, res.logs[0].args) })
Withdrawal { to: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  amount: BigNumber { s: 1, e: 17, c: [ 1000 ] } }

在使用deploy()函數(shù)獲得部署的合約之后,我們執(zhí)行兩個交易赴邻。第一個交易是存款(使用send),它在交易日志中發(fā)出一個存款事件:

Deposit { from: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  amount: BigNumber { s: 1, e: 18, c: [ 10000 ] } }

接下來啡捶,我們使用withdraw函數(shù)進(jìn)行提取姥敛。這就產(chǎn)生了提取事件:

Withdrawal { to: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  amount: BigNumber { s: 1, e: 17, c: [ 1000 ] } }

為了獲得這些事件,我們查看了作為交易結(jié)果(res)返回的日志數(shù)組瞎暑。第一個日志條目(log[0])在log[0]中包含一個事件名彤敛。事件和logs[0].args中的事件參數(shù)。通過在控制臺中顯示這些了赌,我們可以看到發(fā)出的事件名稱和事件參數(shù)墨榄。

事件是一種非常有用的機(jī)制,不僅用于合約內(nèi)通信勿她,還用于開發(fā)期間的調(diào)試袄秩。

調(diào)用其他合約 (call, send, delegatecall, callcode)

從您的合約中調(diào)用其他合約是一個非常有用但潛在危險的操作。我們將研究實(shí)現(xiàn)這一目標(biāo)的各種方法,并評估每種方法的風(fēng)險之剧。

創(chuàng)建一個新的實(shí)例

調(diào)用另一個合約最安全的方法是您自己創(chuàng)建另一個合約郭卫。這樣,您就確定了它的接口和行為背稼。要做到這一點(diǎn)贰军,您可以簡單地實(shí)例化它,使用關(guān)鍵字new赴精,就像任何面向?qū)ο笳Z言一樣冒窍。在solid中蝌衔,關(guān)鍵字new將在區(qū)塊鏈上創(chuàng)建合約,并返回一個您可以用來引用它的對象贰盗。假設(shè)你想要從另一個叫做Token的合約中創(chuàng)建并調(diào)用一個水龍頭合約:

contract Token is mortal {
    Faucet _faucet;

    constructor() {
        _faucet = new Faucet();
    }
}

這種合約構(gòu)建機(jī)制確保您知道合約的確切類型及其接口。合約水龍頭必須在Token范圍內(nèi)定義竹椒,如果定義在另一個文件中童太,您可以使用導(dǎo)入語句:

import "Faucet.sol"

contract Token is mortal {
    Faucet _faucet;

    constructor() {
        _faucet = new Faucet();
    }
}

新的關(guān)鍵字還可以接受可選參數(shù),以指定在創(chuàng)建時的ether傳輸值胸完,以及傳遞給新合約的構(gòu)造函數(shù)的參數(shù)书释,如果有的話:

import "Faucet.sol"

contract Token is mortal {
    Faucet _faucet;

    constructor() {
        _faucet = (new Faucet).value(0.5 ether)();
    }
}

如果我們賦予創(chuàng)建的水龍頭一些ether,我們也可以調(diào)用水龍頭函數(shù)赊窥,它的操作就像一個方法調(diào)用爆惧。在本例中,我們從Token的銷毀函數(shù)中調(diào)用了Faucet的銷毀函數(shù):

import "Faucet.sol"

contract Token is mortal {
    Faucet _faucet;

    constructor() {
        _faucet = (new Faucet).value(0.5 ether)();
    }

    function destroy() ownerOnly {
        _faucet.destroy();
    }
}

尋址現(xiàn)有的實(shí)例

我們可以使用另一種方式來調(diào)用合約锨能,即對合約的一個現(xiàn)有實(shí)例的地址進(jìn)行強(qiáng)制轉(zhuǎn)換扯再。使用這個方法,我們將一個已知的接口應(yīng)用到一個現(xiàn)有的實(shí)例址遇。因此熄阻,至關(guān)重要的一點(diǎn)是,我們肯定地知道倔约,我們正在處理的實(shí)例實(shí)際上與我們假設(shè)的類型相同秃殉。讓我們看一個例子:

import "Faucet.sol"

contract Token is mortal {

    Faucet _faucet;

    constructor(address _f) {
        _faucet = Faucet(_f);
        _faucet.withdraw(0.1 ether)
    }
}

在這里,我們將提供的地址作為構(gòu)造函數(shù)的參數(shù)浸剩,并將其轉(zhuǎn)換為水龍頭對象钾军。這比之前的機(jī)制要危險得多,因?yàn)槲覀儗?shí)際上不知道那個地址是否真的是一個水龍頭對象绢要。當(dāng)我們調(diào)用withdraw時吏恭,我們假設(shè)它接受相同的參數(shù)并執(zhí)行與我們的水龍頭聲明相同的代碼,但是我們不能確定重罪。據(jù)我們所知樱哼,這個地址的withdraw函數(shù)可以執(zhí)行與我們期望的完全不同的內(nèi)容哀九,即使它的名稱是相同的。因此唇礁,使用作為輸入傳遞的地址并將其轉(zhuǎn)換為特定對象比自己創(chuàng)建合約要危險得多勾栗。

Raw call, delegatecall

Solidity為調(diào)用其他合約提供了一些更“低級”的功能。它們直接對應(yīng)于同名的EVM操作碼盏筐,并允許我們手工構(gòu)造從合約到合約的調(diào)用围俘。因此,它們代表了調(diào)用其他合約的最靈活和最危險的機(jī)制琢融。

這里是同樣的例子界牡,使用call方法:

contract Token is mortal {
    constructor(address _faucet) {
        _faucet.call("withdraw", 0.1 ether);
    }
}

正如您所看到的,這種類型的調(diào)用漾抬,是對函數(shù)的盲調(diào)用宿亡,非常類似于構(gòu)造原始交易,僅從合約的上下文中進(jìn)行纳令。它可以使我們的合約暴露在許多安全風(fēng)險中挽荠,最重要的是可重入性,我們將更詳細(xì)地討論[reentrancy]平绩。如果出現(xiàn)問題圈匆,調(diào)用函數(shù)將返回false,因此我們可以計(jì)算返回值捏雌,進(jìn)行錯誤處理:

contract Token is mortal {
    constructor(address _faucet) {
        if !(_faucet.call("withdraw", 0.1 ether)) {
            revert("Withdrawal from faucet failed");
        }
    }
}

調(diào)用的另一種變體是delegatecall跃赚,它取代了非常危險的callcode。callcode方法將很快被棄用性湿,因此不應(yīng)該使用它纬傲。

正如Address對象中所提到的,delegatecall不同于調(diào)用肤频,因?yàn)閙sg上下文不會改變叹括。例如,一個調(diào)用改變了msg的值宵荒。發(fā)送方作為調(diào)用合約领猾,委托方保留相同的msg。發(fā)送者就像在呼叫合約中一樣骇扇。從本質(zhì)上說,delegatecall在當(dāng)前合約的上下文中運(yùn)行另一個合約的代碼面粮。它通常用于從庫調(diào)用代碼少孝。

應(yīng)該非常小心地使用delegatecall。它可能會有一些意想不到的效果熬苍,特別是如果您調(diào)用的合約不是作為一個庫設(shè)計(jì)的稍走。

讓我們使用一個示例合約袁翁,演示call和delegatecall用于調(diào)用庫和合約的各種調(diào)用語義。我們使用一個事件來記錄每個調(diào)用的起源婿脸,并查看調(diào)用上下文如何根據(jù)調(diào)用類型變化:

CallExamples.sol: 不同調(diào)用語義的一個例子粱胜。

link:code/truffle/CallExamples/contracts/CallExamples.sol[]

我們的主要合約是caller,它調(diào)用一個名calledLibrary的庫和一個名calledContract的合同狐树。被調(diào)用的庫和合約都具有相同的函數(shù)calledFunction焙压,它發(fā)出一個事件calledEvent。事件calledEvent記錄了三段數(shù)據(jù):mmsg.sender抑钟、tx.origin和this涯曲。每次調(diào)用函數(shù)時,都可能有不同的執(zhí)行上下文(例如在塔,在msg.sender中有不同的值)幻件,這取決于它是direclty還是通過delegatecall。

在caller中蛔溃,我們首先直接調(diào)用合約和庫绰沥,通過調(diào)用每個函數(shù)中的calledFunction。然后贺待,我們顯式地使用低級函數(shù)call和delegatecall來調(diào)用calledContract.calledFunction徽曲。通過這種方式,我們可以看到各種調(diào)用機(jī)制的行為狠持。

讓我們在truffle開發(fā)環(huán)境中運(yùn)行它疟位,并捕獲事件,看看它是什么樣子的:

truffle(develop)> migrate
Using network 'develop'.
[...]
Saving artifacts...
truffle(develop)> web3.eth.accounts[0]
'0x627306090abab3a6e1400e9345bc60c78a8bef57'
truffle(develop)> caller.address
'0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f'
truffle(develop)> calledContract.address
'0x345ca3e014aaf5dca488057592ee47305d9b3e10'
truffle(develop)> calledLibrary.address
'0xf25186b5081ff5ce73482ad761db0eb0d25abfbf'
truffle(develop)> caller.deployed().then( i => { callerDeployed = i })

truffle(develop)> callerDeployed.make_calls(calledContract.address).then(res => { res.logs.forEach( log => { console.log(log.args) })})
{ sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
  origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10' }
{ sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' }
{ sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
  origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10' }
{ sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
  from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f' }

讓我們看看這里發(fā)生了什么喘垂。我們調(diào)用了make_call函數(shù)并傳遞了calledContract的地址甜刻,然后捕獲了每個不同調(diào)用發(fā)出的四個事件≌眨看一下make_calls函數(shù)得院,讓我們來遍歷每一步。

第一個調(diào)用是:

_calledContract.calledFunction();

在這里章贞,我們直接調(diào)用calledContract.calledFunction祥绞,使用高級ABI來調(diào)用函數(shù)。發(fā)出的事件是:

sender: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x345ca3e014aaf5dca488057592ee47305d9b3e10'

如你所見鸭限,msg.sender是caller合約的地址蜕径。tx.origin是我們錢包web3.eth.accounts[0]的地址,它將交易發(fā)送給caller败京。事件是由calledContract發(fā)出的兜喻,我們可以從事件的最后一個參數(shù)中看到。

make_calls中的下一個調(diào)用是調(diào)用library:

calledLibrary.calledFunction();

它看起來和我們所謂的合約是一樣的赡麦,但行為卻非常不同朴皆。讓我們看一下發(fā)出的第二個事件:

sender: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
origin: '0x627306090abab3a6e1400e9345bc60c78a8bef57',
from: '0x8f0483125fcb9aaaefa9209d8e9d7b9c8b9fb90f'

這一次帕识,the msg.sender不是caller的地址。相反遂铡,它是我們錢包的地址肮疗,和我們的交易發(fā)起是一樣的。這是因?yàn)楫?dāng)您調(diào)用一個庫時扒接,調(diào)用始終用的是delegatecall伪货,并在caller的上下文中運(yùn)行。因此珠增,當(dāng)calledLibrary代碼運(yùn)行時超歌,它繼承了caller的執(zhí)行上下文,就好像它的代碼在調(diào)用者內(nèi)部運(yùn)行一樣蒂教。這個變量(從發(fā)出的事件中顯示)是caller的地址巍举,即使它是從calledLibrary中訪問的。

接下來的兩個調(diào)用凝垛,使用低級的call和delegatecall懊悯,驗(yàn)證我們的預(yù)期,發(fā)出與上面看到的類似的事件梦皮。

Gas注意事項(xiàng)

Gas在[Gas]一節(jié)中有更詳細(xì)的描述炭分,它是智能合約編程中非常重要的考慮因素。gas是限制Ethereum允許交易使用的最大計(jì)算量的資源剑肯。如果在計(jì)算過程中超過gas limit捧毛,則會發(fā)生以下一系列事件:

  • 一個“out of gas”異常被拋出。
  • 在函數(shù)執(zhí)行之前的合約狀態(tài)被恢復(fù)让网。
  • 所有的gas都作為交易費(fèi)用交給礦工呀忧,不退還。

因?yàn)間as是由創(chuàng)建交易的用戶支付的溃睹,所以不鼓勵用戶調(diào)用具有高開銷的函數(shù)而账。因此,將合約功能的gas成本降到最低對程序員來說是最有利的因篇。為此目的泞辐,在構(gòu)建智能合約時,推薦使用某些實(shí)踐竞滓,以最小化函數(shù)調(diào)用周圍的gas成本咐吼。

避免動態(tài)大小的數(shù)組

在動態(tài)大小的數(shù)組中,函數(shù)對每個元素執(zhí)行操作或搜索特定元素的任何循環(huán)都會帶來使用過多gas的風(fēng)險商佑。在找到期望的結(jié)果之前锯茄,或者在對每個元素采取行動之前,合約可能會耗盡gas莉御。

避免調(diào)用其他合約

調(diào)用其他合約撇吞,特別是在不知道其功能的gas成本的情況下,會引入耗盡gas的風(fēng)險礁叔。避免使用沒有經(jīng)過良好測試和廣泛使用的庫牍颈。一個庫從其他程序員那里得到的審查越少,使用它的風(fēng)險就越大琅关。

估計(jì)gas成本

如果你需要估計(jì)執(zhí)行某一合約的某一方法所需要的氣體煮岁,考慮它的調(diào)用參數(shù),例如涣易,你可以使用以下程序;

var contract = web3.eth.contract(abi).at(address);
var gasEstimate = contract.myAweSomeMethod.estimateGas(arg1, arg2, {from: account});

gasEstimate將告訴我們執(zhí)行它所需要的氣體單位的數(shù)量画机。

從你可以使用的網(wǎng)絡(luò)中獲取gas price;

var gasPrice = web3.eth.getGasPrice();

然后,估計(jì)gas的成本;

ar gasCostInEther = web3.fromWei((gasEstimate * gasPrice), 'ether');

讓我們應(yīng)用我們的gas cost函數(shù)來估計(jì)我們的水龍頭例子的gas成本新症,使用書中的代碼:

code/truffle/FaucetEvents

我們在開發(fā)模式中啟動truffle步氏,并執(zhí)行一個JavaScript文件gas_estimate.js,其中包含:

gas_estimates.js: 使用estimateGas函數(shù)

var FaucetContract = artifacts.require("./Faucet.sol");

FaucetContract.web3.eth.getGasPrice(function(error, result) {
    var gasPrice = Number(result);
    console.log("Gas Price is " + gasPrice + " wei"); // "10000000000000"

    // Get the contract instance
    FaucetContract.deployed().then(function(FaucetContractInstance) {

        // Use the keyword 'estimateGas' after the function name to get the gas estimation for this particular function (aprove)
        FaucetContractInstance.send(web3.toWei(1, "ether"));
        return FaucetContractInstance.withdraw.estimateGas(web3.toWei(0.1, "ether"));

    }).then(function(result) {
        var gas = Number(result);

        console.log("gas estimation = " + gas + " units");
        console.log("gas cost estimation = " + (gas * gasPrice) + " wei");
        console.log("gas cost estimation = " + FaucetContract.web3.fromWei((gas * gasPrice), 'ether') + " ether");
    });
});

以下是truffle開發(fā)控制臺的結(jié)果:

$ truffle develop

truffle(develop)> exec gas_estimates.js
Using network 'develop'.

Gas Price is 20000000000 wei
gas estimation = 31397 units
gas cost estimation = 627940000000000 wei
gas cost estimation = 0.00062794 ether

建議您將估計(jì)gas成本函數(shù)作為開發(fā)工作流程的一部分,以避免在將合約部署到mainnet時出現(xiàn)任何意外徒爹。

安全注意事項(xiàng)

在編寫智能合約時荚醒,安全性是最重要的考慮因素之一。與其他程序一樣隆嗅,一個智能合約將執(zhí)行所寫的內(nèi)容界阁,這并不總是程序員想要的。此外胖喳,所有的智能合約都是公開的泡躯,任何用戶只需創(chuàng)建一個交易就可以與它們交互。任何漏洞都可以被利用丽焊,損失幾乎總是不可能恢復(fù)较剃。

在智能合約編程領(lǐng)域,錯誤代價高昂粹懒,很容易被利用重付。因此,遵循最佳實(shí)踐并使用經(jīng)過良好測試的設(shè)計(jì)模式是至關(guān)重要的凫乖。

防御性編程是一種特別適合編寫智能合約的編程風(fēng)格确垫,具有以下特點(diǎn):

  • 極簡主義/簡潔:復(fù)雜性是安全的敵人。代碼越簡單帽芽,它所做的就越少删掀,出現(xiàn)錯誤或無法預(yù)見的影響的可能性就越低。當(dāng)開發(fā)人員第一次參與智能合約編程時导街,他們會嘗試編寫大量代碼披泪。相反,您應(yīng)該檢查您的智能合約代碼搬瑰,并嘗試尋找方法款票,以更少的代碼行控硼、更少的復(fù)雜性和更少的“特性”來做更少的工作。如果有人告訴您艾少,他們的項(xiàng)目已經(jīng)生成了“數(shù)千行代碼”卡乾,您應(yīng)該質(zhì)疑該項(xiàng)目的安全性。更簡單更安全缚够。

  • 代碼重用:盡量不要“重新發(fā)明輪子”幔妨。如果一個庫或合約已經(jīng)存在,可以滿足您的大部分需求谍椅,那么請重新使用它怠蹂。在你自己的代碼中似炎,遵循DRY的原則:不要重復(fù)你自己。如果您看到任何代碼片段重復(fù)不止一次,請捫心自問它是否可以寫成函數(shù)或庫揩魂,并重新使用界轩。被廣泛使用和測試的代碼可能比您編寫的任何新代碼更安全酝豪。要注意“非我發(fā)明”的態(tài)度敛惊,在這種態(tài)度下,您可能會嘗試從頭構(gòu)建一個特性或組件來“改進(jìn)”它胀莹。安全風(fēng)險通常大于改進(jìn)值基跑。

  • 代碼質(zhì)量:Smart-contract代碼是不可原諒的。每一個錯誤都會導(dǎo)致金錢的損失描焰。您不應(yīng)該將智能合約編程視為通用編程媳否。相反,您應(yīng)該應(yīng)用嚴(yán)格的工程和軟件開發(fā)方法荆秦,類似于航空航天工程或類似的不寬容的工程學(xué)科篱竭。一旦你“啟動”你的代碼,你幾乎沒有什么可以去修復(fù)任何問題步绸。

  • 可讀性和可審查性:您的代碼應(yīng)該易于理解和清理掺逼。讀起來越容易,審計(jì)就越容易瓤介。智能合約是公開的吕喘,因?yàn)槿魏稳硕伎梢苑聪蛟O(shè)計(jì)字節(jié)碼。因此刑桑,您應(yīng)該使用協(xié)作和開源方法在公共場合開發(fā)您的工作氯质。您應(yīng)該按照Ethereum社區(qū)中樣式約定和命名約定編寫文檔良好且易于閱讀的代碼。

  • 測試覆蓋率:測試所有你能測試的東西祠斧。智能合約運(yùn)行在公共執(zhí)行環(huán)境中闻察,任何人都可以用他們想要的任何輸入來執(zhí)行它們。您永遠(yuǎn)不應(yīng)該假設(shè)輸入(如函數(shù)參數(shù))是格式良好的、有適當(dāng)?shù)倪吔绲脑⑶揖哂辛己玫哪康哪卦睢y試所有參數(shù),以確保它們在預(yù)期范圍內(nèi)并正確格式化钉嘹。

常見的安全風(fēng)險

智能合約程序員應(yīng)該熟悉許多最常見的安全風(fēng)險填抬,以便能夠檢測和避免使他們暴露于這些風(fēng)險的編程模式。

Re-entrancy

Re-entrancy是編程中的一種現(xiàn)象隧期,其中一個函數(shù)或程序被中斷,然后在之前的調(diào)用完成之前再次調(diào)用赘娄。在智能合約編程的上下文中仆潮,當(dāng)合約A調(diào)用合約B中的一個函數(shù)時,可以重新進(jìn)入遣臼,而合約B反過來調(diào)用合約A中的相同函數(shù)性置,從而導(dǎo)致遞歸執(zhí)行。在關(guān)鍵調(diào)用結(jié)束后才更新合約狀態(tài)的情況下揍堰,這可能是特別危險的鹏浅。

要理解這一點(diǎn),可以想象一個錢包合約里的withdrawal叫做銀行合約屏歹。合約A調(diào)用合約B中的withdraw功能隐砸,試圖提取金額X。該方案將涉及以下行為:

  1. 合約B檢查A是否有提取X所需的余額
  2. B將X轉(zhuǎn)到A的地址(運(yùn)行一個payable fallback函數(shù))
  3. B更新A的余額以反映提款情況

無論何時向合約發(fā)送付款蝙眶,如本例中所示季希,接收方合約(a)都有機(jī)會執(zhí)行一個payable函數(shù),如默認(rèn)fallback函數(shù)幽纷。然而式塌,惡意攻擊者可以利用這種執(zhí)行。假設(shè)在A的payable fallback函數(shù)中友浸,合約A再次調(diào)用銀行B的withdraw函數(shù)峰尝。B的withdraw函數(shù)現(xiàn)在將重新進(jìn)入,因?yàn)橄嗤某跏冀灰赚F(xiàn)在正在引起循環(huán)調(diào)用收恢。

"(1)A調(diào)用B(2)調(diào)用A的payable函數(shù)(1)再次調(diào)用B "

在B withdrawal函數(shù)的第二次重復(fù)中武学,B將再次檢查A是否有可用的余額。由于第3步(更新A的余額)還沒有執(zhí)行派诬,因此B認(rèn)為A仍然有可用的資金可以提取劳淆,無論這個函數(shù)被重新調(diào)用多少次。這個循環(huán)可以重復(fù)默赂,只要有g(shù)as可以持續(xù)運(yùn)行沛鸵。當(dāng)A檢測到gas正在耗盡時,它可以停止在payable函數(shù)中調(diào)用B。B最終執(zhí)行步驟3曲掰,從A的余額中減去X疾捍。但此時,B可能已經(jīng)執(zhí)行了數(shù)百次轉(zhuǎn)賬栏妖,并且只扣除一次乱豆。A通過這次攻擊有效地從B身上抽走了資金。

這個漏洞因?yàn)榕cDAO攻擊相關(guān)而特別出名吊趾。一名用戶利用了這樣一個事實(shí)宛裕,即合約中的余額在接到轉(zhuǎn)賬調(diào)用并提取了價值數(shù)百萬美元的ether后發(fā)生了變化。

為了防止re-entrancy论泛,最好的做法是程序員使用Checks-Effects-Interactions模式揩尸,其中函數(shù)調(diào)用的效果(例如減少余額)在調(diào)用之前發(fā)生。在我們的示例中屁奏,這意味著轉(zhuǎn)換步驟3和步驟2:在轉(zhuǎn)移之前更新用戶的余額岩榆。

在Ethereum中,這是完全可以接受的坟瓢,因?yàn)榻灰椎乃杏绊懚际窃拥挠卤撸@意味著在用戶沒有支付的情況下,不可能更新余額折联。要么同時發(fā)生粒褒,要么拋出異常,但都沒有發(fā)生诚镰。這可以防止re-entrancy攻擊怀浆,因?yàn)樗泻罄m(xù)調(diào)用都將遇到正確的修改后的余額。通過切換這兩個步驟怕享,可以防止A的提款超過其余額执赡。

Delegatecall

調(diào)用方法,如前所述函筋,從調(diào)用合約的上下文中“調(diào)用”到一個函數(shù)沙合。

設(shè)計(jì)模式

任何編程范式的軟件開發(fā)人員通常都會遇到圍繞行為、結(jié)構(gòu)跌帐、交互和創(chuàng)建主題的反復(fù)設(shè)計(jì)挑戰(zhàn)首懈。通常,這些問題可以被推廣并重新應(yīng)用到類似性質(zhì)的未來問題中谨敛。當(dāng)給定一個正式的結(jié)構(gòu)時究履,這些概括被稱為設(shè)計(jì)模式。智能合約有他們自己的一組反復(fù)的設(shè)計(jì)問題脸狸,可以使用下面描述的一些模式來解決這些問題最仑。

在智能合約的開發(fā)過程中藐俺,存在著無數(shù)的設(shè)計(jì)問題,因此不可能在這里討論所有這些問題泥彤。因此欲芹,本節(jié)將重點(diǎn)討論智能合約設(shè)計(jì)中最常見的三個問題分類:訪問控制狀態(tài)流資金支付吟吝。

在這一節(jié)中菱父,我們將編寫一個最終將所有這三種設(shè)計(jì)模式結(jié)合在一起的合約。該合約將運(yùn)行一個投票系統(tǒng)剑逃,允許用戶對“真相”進(jìn)行投票浙宜。該合約將提出諸如“小熊隊(duì)贏得了世界大賽”或“紐約正在下雨”之類的主張,然后用戶將有機(jī)會投票真假蛹磺。如果大多數(shù)參與者投票為真梆奈,合約將認(rèn)為該命題為真,如果大多數(shù)參與者投票為假称开,合約也將認(rèn)為該命題為假。為了激勵真實(shí)性乓梨,每一次投票都必須向合約投送100ether鳖轰,敗選的少數(shù)人的捐款將被大多數(shù)人瓜分。大多數(shù)人的每一個參與者都將從少數(shù)人那里獲得他們的部分獎金以及他們最初的投資扶镀。

這種“真相投票”系統(tǒng)實(shí)際上是Gnosis的基礎(chǔ)蕴侣,Gnosis是一個建立在Ethereum之上的預(yù)測工具。更多關(guān)于Gnosis的信息可以在這里找到:https://gnosis.pm/臭觉。

訪問控制

訪問控制限制哪些用戶可以調(diào)用合約函數(shù)昆雀。例如,真實(shí)投票合約的所有人可以決定限制那些可以參與投票的人蝠筑。為了實(shí)現(xiàn)這一目標(biāo)狞膘,合約必須實(shí)施兩個訪問限制:

  1. 只有合約的所有者可以將新用戶添加到“允許投票者”列表中
  2. 只有被允許的選民才可以投票

Solidity函數(shù)修飾符提供了一種實(shí)現(xiàn)這些限制的簡明方法。

注意:下面的示例在修飾符主體中使用下劃線分號什乙。這是一個Solidity特性挽封,用于告訴編譯器何時運(yùn)行修改后的函數(shù)體。開發(fā)人員可以將修改后的函數(shù)的主體復(fù)制到下劃線的位置臣镣。

pragma solidity ^0.4.21;

contract TruthVote {

    address public owner = msg.sender;

    address[] true_votes;
    address[] false_votes;
    mapping (address => bool) voters;
    mapping (address => bool) hasVoted;

    uint VOTE_COST = 100;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    modifier onlyVoter() {
        require(voters[msg.sender] != false);
        _;
    }

    modifier hasNotVoted() {
        require(hasVoted[msg.sender] == false);
        _;
    }

    function addVoter(address voter)
        public
        onlyOwner()
    {
        voters[voter] = true;
    }

    function vote(bool val)
        public
        payable
        onlyVoter()
        hasNotVoted()
    {
        if (msg.value >= VOTE_COST) {
            if (val) {
                true_votes.push(msg.sender);
            } else {
                false_votes.push(msg.sender);
            }
            hasVoted[msg.sender] = true;
        }
    }
}

修飾符及功能說明:

  • onlyOwner:這個修飾符可以修飾一個函數(shù)辅愿,這樣該函數(shù)就只能由發(fā)送者調(diào)用,其地址與所有者的地址相匹配忆某。
  • onlyVoter:這個修飾符可以修飾一個函數(shù)点待,這樣該函數(shù)就只能由已注冊投票者調(diào)用。
  • addVoter(voter):此函數(shù)用于將投票者添加到投票者列表中弃舒。此函數(shù)使用onlyOwner修飾符癞埠,因此只有合約的所有者可以調(diào)用它。
  • vote(val):投票人使用這個函數(shù)來對所提出的提案投真或假的票。它使用onlyVoter修飾符燕差,所以只有注冊選民可以調(diào)用它遭笋。

狀態(tài)流

許多合約需要一些操作狀態(tài)的概念。合約的狀態(tài)將決定合約如何運(yùn)行徒探,以及在給定的時間點(diǎn)它將提供什么操作瓦呼。讓我們回到我們的真相投票系統(tǒng),尋找一個更具體的例子测暗。

我們的投票系統(tǒng)的運(yùn)作可分為三種不同的狀態(tài)央串。

  1. Register:服務(wù)已經(jīng)創(chuàng)建,所有者現(xiàn)在可以添加投票者碗啄。
  2. Vote:所有的選民都投了票质和。
  3. Disperse:投票付款分為兩部分,并發(fā)給大多數(shù)參與者稚字。

下面的代碼繼續(xù)構(gòu)建在訪問控制代碼之上饲宿,但是進(jìn)一步將功能限制到特定的狀態(tài)。在Solidity中胆描,通常使用枚舉值來表示狀態(tài)瘫想。

pragma solidity ^0.4.21;

contract TruthVote {
    enum States {
        REGISTER,
        VOTE,
        DISPERSE
    }

    address public owner = msg.sender;

    uint voteCost;

    address[] trueVotes;
    address[] falseVotes;


    mapping (address => bool) voters;
    mapping (address => bool) hasVoted;

    uint VOTE_COST = 100;

    States state;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    modifier onlyVoter() {
        require(voters[msg.sender] != false);
        _;
    }

    modifier isCurrentState(States _stage) {
        require(state == _stage);
        _;
    }

    modifier hasNotVoted() {
        require(hasVoted[msg.sender] == false);
        _;
    }

    function startVote()
        public
        onlyOwner()
        isCurrentState(States.REGISTER)
    {
        goToNextState();
    }

    function goToNextState() internal {
        state = States(uint(state) + 1);
    }

    modifier pretransition() {
        goToNextState();
        _;
    }

    function addVoter(address voter)
        public
        onlyOwner()
        isCurrentState(States.REGISTER)
    {
        voters[voter] = true;
    }

    function vote(bool val)
        public
        payable
        isCurrentState(States.VOTE)
        onlyVoter()
        hasNotVoted()
    {
        if (msg.value >= VOTE_COST) {
            if (val) {
                trueVotes.push(msg.sender);
            } else {
                falseVotes.push(msg.sender);
            }
            hasVoted[msg.sender] = true;
        }
    }

    function disperse(bool val)
        public
        onlyOwner()
        isCurrentState(States.VOTE)
        pretransition()
    {
        address[] memory winningGroup;
        uint winningCompensation;
        if (trueVotes.length > falseVotes.length) {
            winningGroup = trueVotes;
            winningCompensation = VOTE_COST + (VOTE_COST*falseVotes.length) / trueVotes.length;
        } else if (trueVotes.length < falseVotes.length) {
            winningGroup = falseVotes;
            winningCompensation = VOTE_COST + (VOTE_COST*trueVotes.length) / falseVotes.length;
        } else {
            winningGroup = trueVotes;
            winningCompensation = VOTE_COST;
            for (uint i = 0; i < falseVotes.length; i++) {
                falseVotes[i].transfer(winningCompensation);
            }
        }

        for (uint j = 0; j < winningGroup.length; j++) {
            winningGroup[j].transfer(winningCompensation);
        }
    }
}

修飾符及功能說明:

  • isCurrentState:在繼續(xù)執(zhí)行修飾函數(shù)之前,該修飾符將要求合約處于指定的狀態(tài)昌讲。
  • pretransition:在執(zhí)行修飾函數(shù)的其余部分之前国夜,該修飾符將轉(zhuǎn)換到下一個狀態(tài)。
  • goToNextState:函數(shù)短绸,將合約轉(zhuǎn)換為下一個狀態(tài)
  • disperse:相應(yīng)計(jì)算出多數(shù)和分發(fā)的函數(shù)车吹。只有所有者可以調(diào)用這個函數(shù)來正式結(jié)束投票。
  • startVote:所有者可以用來開始投票的功能醋闭。

需要注意的是窄驹,允許所有者隨意關(guān)閉投票程序會導(dǎo)致本合約被濫用。在一個更真實(shí)的執(zhí)行中证逻,投票期間應(yīng)在公眾了解的一段時間后結(jié)束馒吴。對于這個例子,這是可以的瑟曲。

現(xiàn)在增加的內(nèi)容確保只有當(dāng)所有者決定開始投票時才允許投票饮戳,用戶只能在投票發(fā)生前由所有者注冊,資金只能在投票結(jié)束后才會分發(fā)洞拨。

提款

許多合約將為用戶提供一些從合約中獲取金錢的方法扯罐。在我們的工作示例中,當(dāng)合約開始分發(fā)資金時烦衣,大多數(shù)用戶直接收到錢歹河。盡管這似乎是可行的掩浙,但這是一個考慮不足的解決方案。分發(fā)中的addr.send()調(diào)用的接收地址可以是一個具有fallback功能的合約秸歧,該fallback功可能失敗厨姚,從而破壞分發(fā)。這有效地阻止了所有更多的參與者接受他們的收入键菱。一個更好的解決方案是提供一個用戶可以調(diào)用的提取功能來收集他們的收入谬墙。

...

enum States {
    REGISTER,
    VOTE,
    DETERMINE,
    WITHDRAW
}

mapping (address => bool) votes;
uint trueCount;
uint falseCount;

bool winner;
uint winningCompensation;

modifier posttransition() {
    _;
    goToNextState();
}

function vote(bool val)
    public
    onlyVoter()
    isCurrentStage(State.VOTE)
{
    if (votes[msg.sender] == address(0) && msg.value >= VOTE_COST) {
        votes[msg.sender] = val;
        if (val) {
            trueCount++;
        } else {
            falseCount++;
        }
    }
}

function determine(bool val)
    public
    onlyOwner()
    isCurrentState(State.VOTE)
    pretransition()
    posttransition()
{
    if (trueCount > falseCount) {
        winner = true;
        winningCompensation = VOTE_COST + (VOTE_COST*false_votes.length) / true_votes.length;
    } else if (falseCount > trueCount) {
        winner = false;
        winningCompensation = VOTE_COST + (VOTE_COST*true_votes.length) / false_votes.length;
    } else {
        winningCompensation = VOTE_COST;
    }
}

function withdraw()
    public
    onlyVoter()
    isCurrentState(State.WITHDRAW)
{
    if (votes[msg.sender] != address(0)) {
        if (votes[msg.sender] == winner) {
            msg.sender.transfer(winningCompensation);
        }
    }
}

...

修飾符及功能說明:

  • posttransition:在函數(shù)調(diào)用之后轉(zhuǎn)換到下一個狀態(tài)
  • determine:這個函數(shù)與之前的disperse函數(shù)非常相似,只是計(jì)算贏者和贏者的獎金经备,而不實(shí)際發(fā)送任何資金拭抬。
  • vote:現(xiàn)在,選票被添加到選票的映射中侵蒙,真實(shí)/虛假的計(jì)數(shù)器增加了造虎。
  • withdraw:允許選民收集獎金(如果有的話)。

這樣纷闺,如果發(fā)送失敗算凿,它只會在特定調(diào)用者的情況下失敗,并且不會妨礙其他所有用戶收集他們的獎金的能力犁功。

合約庫

安全最佳實(shí)踐

也許最基本的軟件安全原則是最大限度地重用受信任的代碼氓轰。在區(qū)塊鏈技術(shù)中,這甚至可以濃縮成一句格言:“Do not roll your own crypto”波桩。在智能合約的情況下,這意味著從社區(qū)徹底審查過的免費(fèi)可用庫中獲得盡可能多的好處请敦。

在Ethereum镐躲,應(yīng)用最廣泛的解決方案是OpenZeppelin,這是一個豐富的合約庫侍筛,從ERC20和ERC721 tokens的實(shí)現(xiàn)萤皂,到crowdsale模型的許多風(fēng)格侨颈,再到在合約中常見的簡單行為人芽,如Ownable、Pausable或LimitBalance偎快。這個存儲庫中的合約經(jīng)過了廣泛的測試禽笑,在某些情況下甚至可以作為事實(shí)上的標(biāo)準(zhǔn)實(shí)現(xiàn)入录。它們是免費(fèi)使用的,由Zeppelin和越來越多的外部貢獻(xiàn)者一起建造和管理佳镜。

同樣源自Zeppelin的Zeppelin_os僚稿,這是一個開源的服務(wù)和工具平臺,用于安全地開發(fā)和管理智能合約應(yīng)用程序蟀伸。zeppelin_os在EVM之上提供了一層蚀同,使開發(fā)人員可以輕松地啟動可升級的DApps缅刽,這些DApps鏈接到一個鏈上庫,它是經(jīng)過良好測試的蠢络、本身可以升級的合約衰猛。這些庫的不同版本可以在區(qū)塊鏈中共存,而一個擔(dān)保系統(tǒng)允許用戶在不同的方向上提出或推動改進(jìn)刹孔。平臺還提供了一組用于調(diào)試啡省、測試、部署和監(jiān)控去中心化應(yīng)用程序的離線工具芦疏。

進(jìn)一步的閱讀

應(yīng)用程序二進(jìn)制接口(ABI)是強(qiáng)類型的冕杠,在編譯時是已知的,并且是靜態(tài)的酸茴。所有的合約都有它們打算在編譯時調(diào)用的任何合約的接口定義分预。

對Ethereum ABI有更嚴(yán)格和深入的解釋可以在網(wǎng)站上找到:https://solidity.readthedocs.io/en/develop/abi-spec.html。該鏈接包含有關(guān)編碼的正式規(guī)范的詳細(xì)信息和各種有用的示例薪捍。

測試智能合約

測試框架

有幾個常用的測試框架(沒有特定的順序):

Truffle Test

作為Truffle框架的一部分笼痹,Truffle允許用JavaScript(基于Mocha的)或Solidity編寫單元測試。這些測試是針對TestRPC/Ganache運(yùn)行的酪穿。有關(guān)編寫這些測試的詳細(xì)信息請參見[truffle]

Embark Framework Testing

Embark與Mocha集成凳干,運(yùn)行用JavaScript編寫的單元測試。這些測試反過來是針對部署在TestRPC/Ganache上的合約運(yùn)行的被济。Embark框架自動部署智能合約救赐,并在合約被更改時自動重新部署它們。它還跟蹤已部署的合約只磷,并在真正需要時部署合約经磅。containsobject包括一個測試庫,它可以在EVM中快速運(yùn)行和測試您的合約钮追,并使用assert.equal()這樣的函數(shù)预厌。embark test將在目錄test/下運(yùn)行任何測試文件。

DApp

DApp使用原生的Solidity代碼(一個名為ds-test的庫)和一個名為Ethrun的Parity庫來執(zhí)行Ethereum字節(jié)碼元媚,然后斷言正確性轧叽。ds-test庫提供用于驗(yàn)證正確性的斷言函數(shù),以及用于在控制臺中記錄數(shù)據(jù)的事件刊棕。

斷言函數(shù)包括

assert(bool condition)
assertEq(address a, address b)
assertEq(bytes32 a, bytes32 b)
assertEq(int a, int b)
assertEq(uint a, uint b)
assertEq0(bytes a, bytes b)
expectEventsExact(address target)

日志記錄事件將日志信息記錄到控制臺炭晒,使它們對調(diào)試非常有用。

logs(bytes)
log_bytes32(bytes32)
log_named_bytes32(bytes32 key, bytes32 val)
log_named_address(bytes32 key, address val)
log_named_int(bytes32 key, int val)
log_named_uint(bytes32 key, uint val)
log_named_decimal_int(bytes32 key, int val, uint decimals)
log_named_decimal_uint(bytes32 key, uint val, uint decimals)

Populus

Populus使用python及其自己的鏈模擬器來運(yùn)行Solidity編寫的合約甥角。單元測試是用Python和pytest庫編寫的腰埂。Populus支持編寫專門用于測試的合約。這些合約文件名應(yīng)該與glob模式Test*.sol匹配蜈膨,并位于項(xiàng)目測試目錄./tests/下的任何位置屿笼。

框架 測試語言 測試框架 鏈模擬器 網(wǎng)站
Truffle Javascript/Solidity Mocha TestRPC/Ganache truffleframework.com
Embark Javascript Mocha TestRPC/Ganache embark.readthedocs.io
DApp Solidity ds-test (custom) Ethrun (Parity) dapp.readthedocs.io
Populus Python Pytes Python chain emulator populus.readthedocs.io

如果這是您第一次使用geth牺荠,可能需要一段時間才能與網(wǎng)絡(luò)同步。然后將變量設(shè)置為:

> var foo = eth(<CONTENTS_OF_ABI_FILE>)
> var byteCode = '0x<CONTENTS_OF_BIN_FILE>)

用上面更多命令的輸出填充參數(shù)驴一。然后最終部署您的合約:

> var deploy = {from eth.coinbase, data:byteCode, gas:2000000}
> var fooInstance = foo(bar, baz)

On-Blockchain測試

雖然大多數(shù)測試不應(yīng)該在部署的合約上進(jìn)行休雌,但是合約的行為可以通過Ethereum客戶端進(jìn)行檢查。下面的命令可以用來評估一個智能合約的狀態(tài)肝断。這些命令應(yīng)該在“geth”終端上鍵入杈曲,盡管任何web3調(diào)用也將支持這些命令。

eth.getTransactionReceipt(txhash);

可用于在txhash獲取合同地址胸懈。

eth.getCode(contractaddress)

獲取在contractaddress中部署的合約的代碼担扑。這可以用來驗(yàn)證正確的部署。

eth.getPastLogs(options)

獲取位于選項(xiàng)中指定的地址的合約的完整日志趣钱。這有助于查看合約調(diào)用的歷史涌献。

eth.getStorageAt(address, position)

獲取位于地址的存儲,位置偏移量顯示該合約中存儲的數(shù)據(jù)首有。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末燕垃,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子井联,更是在濱河造成了極大的恐慌卜壕,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烙常,死亡現(xiàn)場離奇詭異轴捎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蚕脏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門侦副,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蝗锥,你說我怎么就攤上這事跃洛÷蚀ィ” “怎么了终议?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長葱蝗。 經(jīng)常有香客問我穴张,道長,這世上最難降的妖魔是什么两曼? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任皂甘,我火速辦了婚禮,結(jié)果婚禮上悼凑,老公的妹妹穿的比我還像新娘偿枕。我一直安慰自己璧瞬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布渐夸。 她就那樣靜靜地躺著嗤锉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪墓塌。 梳的紋絲不亂的頭發(fā)上瘟忱,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天,我揣著相機(jī)與錄音苫幢,去河邊找鬼访诱。 笑死,一個胖子當(dāng)著我的面吹牛韩肝,可吹牛的內(nèi)容都是我干的触菜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼伞梯,長吁一口氣:“原來是場噩夢啊……” “哼玫氢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起谜诫,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤漾峡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后喻旷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體生逸,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年且预,在試婚紗的時候發(fā)現(xiàn)自己被綠了槽袄。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡锋谐,死狀恐怖遍尺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情涮拗,我是刑警寧澤乾戏,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站三热,受9級特大地震影響鼓择,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜就漾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一呐能、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧抑堡,春花似錦摆出、人聲如沸朗徊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽荣倾。三九已至,卻和暖如春骑丸,著一層夾襖步出監(jiān)牢的瞬間舌仍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工通危, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铸豁,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓菊碟,卻偏偏與公主長得像节芥,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子逆害,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容