天天看點

北大肖臻《區塊鍊技術與應用》學習筆記13

智能合約 solidity

智能合約

智能合約是比特币和以太坊最大的差別。

什麼是智能合約

智能合約是運作在區塊鍊上的一段代碼,代碼的邏輯定義了智能合約的内容。

智能合約的賬戶裡儲存了合約的目前的運作狀态,包含:

Balance 目前餘額

nonce 交易次數

coding 合約代碼

storage 存儲,存儲的資料結構是一棵MPT

智能合約的代碼一般是用solidity語言來編寫的,文法和Javascript接近。

Solidity

address是solidity特有的;mapping是從位址到無符号整數的映射,event 是用來記錄日志的,第一個event的參數是拍賣的位址和金額,第二個是獲勝者的位址和拍賣金額;solidity 不支援周遊,如果想要周遊元素,自己需要想辦法記錄一下哈希表有哪些元素,這裡是用bidders數組記錄的,solidity語言中數組可以是固定長度也可以是動态改變長度的。

構造函數有兩種,第一種是像C++構造函數一樣,定義一個與contract同名的函數,函數可以有參數但是不能有傳回值;新版本更推薦的是用constructor來定義構造函數,這個函數隻有合約在創造的時候才被調用一次,構造函數也隻有一個。

成員函數裡第一個有payable,另外兩個函數沒有,因為以太坊規定,合約賬戶要能接受外部轉賬的話必須标注為payable。

這是一個網上拍賣的合約,這個例子中的bid函數是用來競拍出價的,比如說要參與拍賣,要出100個以太币,那麼就調用合約中的bid函數。是以拍賣的規則是調用bid函數的時候要把拍賣的出價即100個以太币也發送過去存儲在合約裡,鎖定那裡一直到拍賣結束。避免有人憑空出價(實際上沒有那麼多錢,漫天喊價),是以拍賣的時候要把出的價錢發在智能合約裡鎖定起來。是以這個bid函數要有能夠接受外部轉賬的能力,是以才标注了payable。

成員函數的withdraw函數就沒有payable,withdrew函數的用處是在拍賣結束時,出價最高的人赢得拍賣,其他人沒有拍到想要拍到的東西,可以調用withdraw函數,把自己當初的出價,也就是之前bid的時候鎖定在智能合約裡的以太币,再取出來。withdraw的目的不是真的轉賬,不需要把錢轉給智能合約,而僅僅是通過調用withdraw函數把當初鎖定在智能合約的錢取回來,是以沒必要用payable。

bidder.push(bidder) //數組新增元素

bidder.length //計算數組中元素的個數

addr[1024] //數組是固定的長度,可以直接在後面寫入它的長度,這裡長度為1024

北大肖臻《區塊鍊技術與應用》學習筆記13

如何調用智能合約

3.1 外部賬戶調用

調用智能合約和轉賬類似,A-B轉賬,如果B是個普通賬戶,那麼這隻是一個普通的轉賬交易,和BTC的轉賬交易是一樣的,如果B是合約賬戶的話,那麼這個轉賬其實是發起一次對B的合約的調用,具體調用的是合約中的哪個函數是在資料域(data域)中另外說明的。send address是發起調用的賬戶位址,To Contract Address是被調用的合約的位址,調用的函數是TXdata裡面給出的要調用函數,如果這個函數有參數,那麼其參數也在這裡的data域裡說明的,上面的案例的三個成員函數都是沒有參數的,但是有一些成員函數是有參數的。中間一行是調用的參數,Value是發起調用的時候轉賬花的錢數,這裡是0,說明這裡隻是想調用函數并不想真的轉賬,是以這裡的To contract address函數不需要定義payable。Gas used是這個交易所花的汽油費,gas priced 是機關汽油的價格,gas limit是這比交易願意支付的最多汽油。

北大肖臻《區塊鍊技術與應用》學習筆記13

3.2 合約賬戶調用

  1. 直接調用方法,合約A和合約B,A合約就隻是寫成Log(日志),event是定義一個事件,叫LogCallFoo,emit 來調用這個事件,emit的作用就是寫一個log,對于程式運作沒有影響;B合約參數是一個位址,就是A合約的位址,然後這個語句把這個位址轉換成A這個合約的一個執行個體,然後調用foo這個函數。

以太坊中規定一個交易隻有外部賬戶才能發起,合約賬戶不能自己主動發起一個交易。這個例子當中實際上是需要一個外部賬戶調用了合約B當中的函數CallAFooDirectly,然後這個函數再調用A合約中的foo函數。

北大肖臻《區塊鍊技術與應用》學習筆記13
  1. 位址類型調用,addr.call()

第一個參數是函數的signature,後面跟的是調用參數。這種方法和上面的方法的差別:一是對錯誤處理的不同,上面的方法調用的時候,如果調用的合約在執行過程中出現錯誤,會導緻發起這個調用的合約跟着一起復原,上面的例子當中如果合約A出現異常,B也會跟着出現異常;addr.call()這種方法,如果在調用過程中被調用的合約産生異常,call函數會傳回False,表明這個調用時失敗的,但是發起調用的這個函數并不會抛出異常,而是可以繼續執行。

北大肖臻《區塊鍊技術與應用》學習筆記13
  1. delegatecall()

主要差別是delegatecall()不需要切換到被調用的合約的環境中去執行,而是在目前的合約中執行就可以了,比如就用目前的賬戶餘額存儲之類的。

以太坊中凡是要接受外部轉賬的函數都需要标志為payable,否則的話你給這個函數轉錢就引發錯誤處理抛出異常,如果你不需要接受外部轉賬,函數就不用寫payable。

北大肖臻《區塊鍊技術與應用》學習筆記13

fallback函數

無參數無傳回值,無函數名,fallback關鍵字并沒有出現在函數名裡面。調用合約的時候,A調用B合約,要在轉賬交易的data域說明調用的是合約B中的哪個函數,如果A給B轉了一筆錢,沒有說明調用的是哪個函數,也就是data域是空的,這個時候預設的就是調用這個fallback函數,這也是為什麼叫fallback函數,因為沒有别的函數可以調用了,就隻能調用他。還有一種情況是你要調用的函數不存在,在你的data域裡你說你要調用這個函數,實際合約當中沒有這個函數,也是調用fallback函數,這也是為什麼這個函數沒有參數也沒有傳回值。fallback函數也可能需要标注payable關鍵詞,就如果fallback函數需要有接受轉賬的能力的話是需要寫payable,一般情況都是寫成payable,如果合約賬戶沒有任何函數标志為payable,包括fallback函數也沒有,那麼這個合約沒有任何能力可以接受外部的轉賬。如果有人往合約裡轉錢就會引發異常。

北大肖臻《區塊鍊技術與應用》學習筆記13

轉賬金額可以為0,是給收款人的,但是汽油費是要給礦工的,不給的話礦工不會把交易打包到區塊鍊上的。

注釋:隻有合約賬戶才有這些函數以及代碼,外部賬戶沒有代碼,能觸發交易

智能合約的建立

智能合約的建立是由某一個外部的賬戶發起一筆轉賬交易,轉給0X0位址,然後把要釋出的合約代碼放到data域裡面。Java Virtual Machine是為了增強可一緻性,EVM也是類似的思想,通過加一層虛拟機,對智能合約的運作提供一緻性的平台,是以EVM又叫world wide compute,EVM的尋址空間是非常大的,是256位的,像如之前講的uint和signed int就是256位的,普通計算機是64位的。

北大肖臻《區塊鍊技術與應用》學習筆記13

汽油費(gas)

比特币和以太坊兩種區塊鍊模型的設計理念是有很大差别的,比特币的設計理念是簡單,腳本語言的功能很有限,不支援循環。而以太坊是要提供一個圖靈完備的程式設計模型。很多功能在比特币系統上實作不了或者比較困難,在以太坊中實作起來卻是非常容易。當然這樣也會帶來一些問題,比如說出現死循環怎麼辦,當一個全節點收到一個對智能合約的調用,怎麼知道這個調用執行起來會不會導緻死循環,有什麼解法嗎?

沒有,這是一個 halting problem(停機問題)

which is the problem of determining, from a description of an arbitrary computer program and an input, whether the program will finish running, or continue to run forever.

(這個問題是,從一個任意計算機程式的描述和一個輸入來确定,這個程式是會結束運作,還是永遠繼續運作。)

停機問題是不可解的,需要注意一下這個問題不是NPC的(Non-deterministic Polynomial的問題,即多項式複雜程度的非确定性問題),NPC的問題是可解的,隻不過沒有多項式時間的解法,很多NPC問題有很多自然的指數時間的解法,比如哈密爾頓回路問題,把所有可能性枚舉一遍,n個頂點的排列是n!,把每個組合檢查一下是不是構成一個合法的回路,就知道它有沒有哈密爾頓回路,哈密爾頓回路是可解的,隻不過解的複雜度是指數級的。停機問題已經從理論上證明不存在這樣的算法能夠對任意給定的輸入程式判斷出這個程式是否會停機,這是不可解的。

以太坊中如何解決的呢?

把這個問題推給發起交易的賬戶,以太坊引入了汽油費機制,你發起一個對智能合約的調用需要支付相應的汽油費。

交易的資料結構:

AccountNonce是交易的序号,用于防止前面說到的replay attack(重播攻擊)

price是機關汽油的價格

Gaslimit是這個交易願意支付的最大汽油量,相乘之後就是這個交易可能消耗的最大汽油費

recipient是收款人的位址

amount的轉賬金額

payload就是之前說的data域,用于存放調用的是合約中哪一個函數以及函數的參數取值是什麼。當一個全節點收到一個對智能合約的調用的時候,先按照這個調用給出的gas limit算出可能花掉的最大汽油費,然後一次性把汽油費從發起調用的賬戶中扣掉,然後再根據實際執行情況算出實際花了多少汽油費,汽油費不夠會引起復原。

簡單指令例如加減法消耗的汽油費比較少,複雜的指令消耗的比較多,比如說取哈希,這個運算雖然一條指令就可以完成,但是汽油費就比較貴。除了計算量之外,需要存儲狀态的指令消耗的汽油費也是比較大的。相比之下,如果隻是為了讀取公共資料,那些指令是可以免費的。

北大肖臻《區塊鍊技術與應用》學習筆記13

以太坊中的錯誤處理

以太坊中的交易執行起來具有原子性,一個交易要麼全部執行要麼完全不執行,不會隻執行一部分。這個交易既包含普通的轉賬交易也包含對智能合約的調用,是以如果在執行智能合約過程中出現任何錯誤,會導緻整個交易的執行復原,退回到開始執行之前的狀态,就好像這個交易完全沒有執行過。

7.1 那麼什麼情況下會出現錯誤呢?

  1. 錯誤處理一:之前所說的汽油費,如果這個交易執行完之後沒有達到當初的gaslimit,那麼多餘的汽油費會被退回到這個賬戶裡;相反的,如果執行到一半,gaslimit用完了,合約的執行要退回到開始執行之前的狀态,而且這個時候已經消耗的汽油費是不退的。為什麼這麼設計呢?防止一些惡意的節點發動denial service attack,發動一個計算量很大的合約然後不停地調用這個合約,每次調用的時候給的汽油費都不夠,反正最後汽油費都會退回來,對惡意節點來說沒什麼損失,但是對礦工來說白白浪費了很多資源。
  2. 錯誤處理二:assert語句和require語句,這裡兩個語句都是用來判斷判斷某種條件,如果條件不滿足的話就會導緻抛出異常。assert語句一般來說是用來判斷某種内部條件,和c語言中的類似;reuire語句判斷某種外部條件,比如說判斷函數的輸入是否符合要求,下圖所給的例子是bid函數裡,判斷目前時間now是否小于等于拍賣結束時間,如果符合條件,繼續執行,不符合,即拍賣時間已經結束了,這個時候就會抛出異常。
  3. revert語句無條件抛出異常,如果執行到revert語句,那麼他自動的就會導緻復原,早期版本用的是throw語句,新版本的solidity建議改為revert語句。

一些語言像java使用者可以自己定義出現錯誤怎麼辦,solidity沒有try-catch結構,不可以。

北大肖臻《區塊鍊技術與應用》學習筆記13

7.2 嵌套調用

Q1: 前面說智能合約調用出現錯誤會導緻復原,那麼如果是嵌套調用,一個智能合約調用另外一個智能合約,被調用的智能合約出現錯誤是不是會導緻發起調用的智能合約也跟着一起復原呢?叫做連鎖式復原。

不一定,這個取決于調用智能合約的方式,如果這個智能合約是直接調用的,那麼它會觸發連鎖式的復原,整個交易都會復原。如果是用call()這種方式調用,他就不會引起連鎖式復原,隻會使目前的調用失敗,傳回一個False的傳回值。

Q:有些情況下,從表面上看,你并沒有調用任何函數,比如說單純的賬戶轉賬,但是如果這是以個合約賬戶的話,轉賬的本身就有可能觸發對函數的調用,為什麼呢?

因為有fallback函數,這就是一種嵌套調用,一個合約往另外一個合約裡轉賬,就可能調用這個合約裡的fallback函數。

(給合約轉賬,合約裡沒有fallback函數,也沒有說明調用哪個函數,call本身就會傳回false,但是不會引起連鎖式復原。)

北大肖臻《區塊鍊技術與應用》學習筆記13

資料結構

8.1 blockheader資料結構回顧

比特币中規定每個區塊不能超過1M,是寫在協定裡不能更改的,比特币的交易是比較簡單的,基本上可以用交易的位元組數來衡量出這個交易消耗的資源有多少,但是以太坊這麼規定是不行的,因為智能合約的邏輯很複雜,有的交易從位元組上看可能很小,但是它消耗的資源很大,比如它可能調用别的合約之類的,是以怎麼辦呢?要根據交易的具體操作來收費,這就是汽油費。

GasUsed是這個區塊裡,所有交易所消耗的汽油費加在一起,GasLimit是這個區塊裡所有交易能夠消耗汽油的一個上限,這裡和每個交易的gaslimit(自己設定的)是不一樣的。以太坊的上限GasLimit,和比特币不太一樣,每個礦工在釋出區塊的時候可以對這個GasLimit進行微調,它可以在上一個區塊的GasLimit上調或者下調1/1024,這種機制實際求出的系統GasLimit是所有礦工認為比較合理的GasLimit的一個平均值。

北大肖臻《區塊鍊技術與應用》學習筆記13

8.2 Receipt資料結構

Q1: 假設某個全節點要打包一些交易到一個區塊裡面,這些交易裡有一些是對智能合約的調用,那麼這個全節點是應該先把智能合約都執行完之後再去挖礦呢?還是先挖礦獲得記賬權再去執行智能合約?

先執行智能合約再挖礦,以太坊挖礦需要嘗試各種不同的nonce值,找到一個符合要求的,計算哈希的時候要用到blockheader的内容,包含三棵樹的根哈希值,隻有執行完區塊中的所有交易包括智能合約交易,這樣才能更新這三棵樹,知道三個根哈希值,blockheader的内容才能确定,然後才能嘗試各個nonce挖礦。

Q2:全節點在收到一個對智能合約的調用的時候,要一次性先把這個調用可能花掉的最大汽油費從發起這個調用的賬戶扣掉,這個具體是怎麼操作的?

三棵樹,狀态樹,交易樹和收據樹都是全節點在本地維護的資料結構,狀态樹記錄了每個賬戶的狀态,包括賬戶餘額,汽油費是全節點收到調用的時候從本地維護的資料結構裡把他賬戶的餘額減掉就行了,隻有區塊釋出之後本地修改才會變成外部可見的,區塊鍊共識。

Q3:礦工在挖礦執行智能合約的過程中消耗了很多本地資源,但是并沒有獲得記賬權,沒有出塊獎勵,也不會得到汽油費獎勵,怎麼辦?

沒有辦法,以太坊中就是沒有補償,還需要把别人釋出的區塊裡的交易在本地執行一遍,以太坊規定要驗證釋出區塊的正确性,每個全節點要獨立驗證,把别人釋出的交易區塊在本地執行一遍,更新三棵樹的内容算出根哈希值,再和釋出的新區塊的根哈希值比較是否一緻。這種機制下挖礦慢的礦工就特别吃虧,汽油費的設定本來是對礦工執行智能合約所消耗的資源的一種補償,但是這種補償隻有挖到礦的礦工才能得到,其他礦工得不到。

Q4:上述問題會造成什麼影響?如何改進?

會直接威脅到區塊鍊的安全,區塊鍊的安全保證是來自所有全節點獨立驗證釋出的區塊的合法性,這樣少數有惡意的節點才沒有辦法篡改這些内容,如果一些礦工想不通,不給錢就不驗證了,這樣就會危及到區塊鍊的安全。這樣是不可行的,因為如果跳過驗證步驟,以後就沒法挖礦了,因為驗證的時候需要把區塊的交易都執行一遍,更新本地的三棵樹,擷取最新的根哈希值,如果不驗證的話,本地三棵樹的内容沒有辦法更新,以後就沒辦法釋出新的區塊了。因為釋出的區塊沒有三棵樹的内容,隻是塊頭裡有個根哈希值,是以沒有辦法不驗證的。在礦池裡,礦工本身就不驗證了,有一個全節點pool manager負責統一驗證,礦工相信全節點驗證的正确性,全節點配置設定給礦工看到的是puzzle的内容,puzzle是全節點跟着區塊鍊更新得來的。

Q5:釋出到區塊鍊上的交易是不是都是成功執行的?如果智能合約在執行中出現錯誤,要不要也釋出在區塊鍊上?

執行發生錯誤的交易也要釋出到區塊鍊上,否則沒有辦法扣掉汽油費。

Q6:怎麼知道這個交易是執行成功了呢?

三棵樹裡面,每個交易執行完之後形成一個收據,下圖是收據的内容,其中status域會告訴你這個交易的執行情況。

北大肖臻《區塊鍊技術與應用》學習筆記13

Q7:智能合約支不支援多線程?多核并行處理。

以太坊是一個交易驅動的狀态機,這個狀态機必須是完全确認性的,即給定一個智能合約,面對同一種輸入,産生的輸出或者是轉移到下一個地方的狀态必須是完全确定的,因為所有的全節點都得執行同一組操作,到達同一個狀态,要驗證。如果狀态不确定的話那三棵樹的根哈希值根本對不上,是以必須完全确定才行。多線程的問題在于,多個核對記憶體通路順序不同,執行結果有可能是不确定的,是以solidity是不支援多線程的。除了多線程,其他所有可能造成結果不确定的操作也都不支援,比如産生随機數。是以以太坊中沒有辦法真正産生随機數,隻能産生僞随機數,否則的話又會出現前面的問題,每個全節點執行完一遍得到的結果都不一樣。

智能合約的執行必須是确定性的,這也就導緻了智能合約不能像通用的程式設計語言那樣通過系統調用得到一些system call的一些環境資訊,因為每個全節點的執行環境 不是完全一樣的,是以他隻有通過一些固定的變量的值能夠得到一些狀态資訊,這個表格就是智能合約能夠得到的區塊鍊的一些資訊。

北大肖臻《區塊鍊技術與應用》學習筆記13

智能合約的其他資訊

9.1 可獲得的調用資訊

msg.sender發起這個調用的使用者,和tx.origin交易發起者是不一樣的。

北大肖臻《區塊鍊技術與應用》學習筆記13
北大肖臻《區塊鍊技術與應用》學習筆記13

比如說外部賬戶A,調用合約C1,合約中有一個函數f1,f1又調用另外一個合約C2,裡面有一個函數f2,那麼對這個f2函數,msg.sender是C1合約,tx.origin是賬戶A。msg.gas是目前這個調用還剩下多少汽油費,這個決定了我還能做哪些操作。包括你要想再調用别的合約,前提是還有足夠的汽油費剩下來,msg.data就是資料域,裡面寫了調用的函數和這個函數的參數取值,msg.signature是msg.data的前四個位元組,就是函數辨別符,調用的是哪個函數。now和timestamp是一個意思,智能合約裡沒有辦法獲得很精确的時間,隻能獲得跟這個目前區塊的一些資訊時間。

9.2 位址類型

北大肖臻《區塊鍊技術與應用》學習筆記13

第一個是成員變量:就是成員賬戶的餘額balance, uint256是成員變量的類型,不是函數調用/參數,機關是比較小的。addr.balance()這個位址上賬戶的餘額。

剩下的都是成員函數,成員函數的語義和直覺了解不太相同,addr.transfer(12345)是目前這個合約C向這個位址轉入多少錢。addr.call是指目前這個合約發起一個調用,調用的是addr這個合約。

北大肖臻《區塊鍊技術與應用》學習筆記13

Q8: 向一個函數轉賬,這個函數沒有定義fallback函數,引起錯誤會不會連鎖復原?

這個取決于怎麼轉賬。共有三種轉賬方法,transfer, send, and call.value都可以發送ETH,但是transfer 和 send這兩個是專門用來轉賬的函數,差別在于transfer會導緻連鎖性復原,類似直接調用的方法,失敗的時候抛出異常;send傳回一個False,不會導緻連鎖式復原;call也是可以轉賬的,call.value(轉賬金額)(調用的函數,可為空),不過call的本意是發動函數調用的,但是也可以用來轉賬,這個也是不會引起連鎖式復原,傳回False。另外一個差別是transfer和send這兩個在發起調用的時候隻給了一點汽油,汽油是2300個機關,非常少的,收到轉賬的合約基本上幹不了别的事,也就寫一個log,而call呢是把目前這個調用剩下的所有汽油都發過去了,比如call所在的這個合約它本身被外面調用的時候可能還剩8000個汽油,然後他去調别的合約如果是用call這種方法轉賬就把剩下的汽油都發過去了。

北大肖臻《區塊鍊技術與應用》學習筆記13

拍賣例子

拍賣受益人就是拍賣前物品的所有者。

拍賣規則:拍賣結束之前每個人都可以去出價去競拍,競拍的時候為了保證誠信,需要把競拍的價格相應的以太币發過去,比如出一百個以太币,你用bid函數競拍的時候,要把100個以太币發送到智能合約,并鎖在裡面直到拍賣結束,不允許中途退出,可以加價,拍賣結束之後,highestBidder的出價的錢數會給受益人beneficiary,受益人應該把拍賣物也給最高出價人。其他沒有競拍成功的人可以把錢再取出來。競拍可以多次出價,補差價發到智能合約裡就可以,出價有效的話必須保證加價之後的出價高于之前的最高出價,否則就是無效(非法)的。constructor構造函數在合約建立的時候會記錄受益人是誰,結束時間是什麼時候。

北大肖臻《區塊鍊技術與應用》學習筆記13

下面兩個是拍賣用的兩個函數,左邊是競拍bid函數,競拍的時候發起一個交易調用拍賣合約中的bid函數,bid雖然沒有參數,但是在msg.value發起這個調用的時候轉賬轉過去的以太币數目,就是出的競拍價格。

  1. 先查詢一下拍賣是否結束,如果已經結束還參與拍賣則抛出異常。
  2. 查一下上一次的出價加上目前調用所發過去的以太币大于最高出價,如果是以前沒有出價過,第一部分就是0。bids是個哈希表,solidity裡的特點是如果要查詢的鍵值不存在,則傳回預設值為0。是以如果是以前沒有出價過,第一部分就是0。第一次拍賣的時候把拍賣者的資訊放在bidder數組裡,因為solidity不支援周遊,要周遊哈希表必須儲存一下包含哪些元素,然後記錄一下新的最高出價人是誰,寫一些日志之類的。
北大肖臻《區塊鍊技術與應用》學習筆記13

右邊是拍賣結束的函數,首先查一下拍賣是否已經結束了,如果拍賣還沒有結束,有人調用這個函數就是非法的,就抛出異常。第二行判斷這個函數是不是被調用過,如果調用過就不用再調一遍了。第三行把最高出價人的錢轉給受益人,對于拍賣沒有成功的人,最後循環把金額退回給bidder。然後标注一下這個函數已經執行完了,寫一個log。

智能合約的代碼是儲存在data域裡面的,礦工把智能合約釋出到區塊鍊上之後傳回給你一個合約的位址,然後這個合約就在區塊鍊上了,所有人都可以調用。任何人出價競拍調用Bid函數的操作都需要礦工釋出在區塊鍊上。

但是這裡有個問題就是AuctionEnded函數必須有人調用才會執行,執行之後才會結束,solidity語言沒有辦法把他設定成為拍賣結束之後自動執行end。

北大肖臻《區塊鍊技術與應用》學習筆記13

假設有一個人通過上圖的合約賬戶參與競拍,會有什麼結果?

這個合約隻有一個函數,hack_bid,參數是拍賣合約的位址,把它轉成拍賣合約的執行個體,然後調用拍賣合約的bid函數,把錢發送過去。這是一個合約賬戶,合約賬戶不能自己發起交易,得有一個黑客從他自己的外部賬戶發起一個交易,調用這個合約的hack_bid函數,這個函數再調用拍賣合約的bid函數,把他自己收到的轉過來的錢,黑客外部轉過來的錢,再轉給拍賣合約中的bid函數就參與拍賣了。

參與拍賣沒有問題,但是退款會有問題,轉給這個合約賬戶的錢會有什麼情況?

黑客外部賬戶對于拍賣合約是不可見的,拍賣合約能看到的隻是黑客合約,這裡的退款轉賬函數沒有調用任何函數,當一個合約賬戶收到轉賬沒有調用任何賬戶的時候,應該調用fallback函數,但是這個函數沒有定義fallback函數,會調用失敗并抛出異常,transfer函數會引起連鎖式復原,導緻轉賬操作失敗收不到錢。轉賬的過程是全節點執行到beneficiarytransfer的時候把相應賬戶的餘額進行了調整,所有的solidity語句即智能合約執行過程中的任何語句對狀态的修改該的都是本地的狀态和本地的資料結構。是以這個循環當中不論是排在黑客合約順序前面還是後面都是在改本地資料結構,隻不過排在後面的bidder根本沒有機會來得及執行,然後整個都復原了,就好像這個智能合約從來沒有執行,是以所有人都收不到錢。出現這種情況怎麼辦?

沒有辦法,code is law,智能合約的規則是由代碼邏輯決定的,代碼一旦釋出到區塊鍊上就改不了了,這樣的好處是沒有人能夠篡改規則,壞處是出現漏洞也無法修改。智能合約如果設計的不好的話有可能把以太币永久的鎖起來誰也取不了。有點像irrevocable trust不可撤銷的信托。

能不能給智能合約留個後門給開發者用來修複bug?構造函數加一個域owner,記錄一下owner是誰,然後對這個owner的位址允許他做一些類似系統管理者的操作,比如可以任意轉賬。出現Bug之後超級管理者就可以把鎖定的錢轉出來。

這樣做的前提是所有人應該信任這個人,否則他有可能攜款逃走。那有什麼其他改進方法嗎?

把前面的auctionend函數拆成兩個函數,左邊是withdraw右邊是beneficiary。

北大肖臻《區塊鍊技術與應用》學習筆記13

withdraw是說不用循環了,每個競拍失敗的人自己調用withdraw函數把錢取出來。

左邊:判斷這個人是不是最高出價者,是的話不能退錢。判斷賬戶餘額是不是正的,amount就是賬戶餘額,if 把賬戶餘額轉給msg.sender,就發起調用的人,然後把賬戶餘額清0,免得下次再取錢。

右邊:把最高出價給受益人。

這樣可以了嗎?

右邊是黑客合約,hack_bid就和前面的合約hack_bid是一樣的,通過調用拍賣合約的bid函數參與競拍,hack_withdraw就在拍賣結束的時候調用withdraw函數,把錢取回來。

問題在于右邊最後一個函數fallback函數又把錢取了一遍,hack_withdraw調用拍賣合約的withdraw函數的時候,左邊執行到If(msg.sender)會向黑客合約轉賬,msg.sender就是黑客的合約,把他當初出價的金額轉給他,右邊合約最下面又調用了拍賣函數的withdraw函數去取錢,這裡的msg.sender就是拍賣合約,因為是拍賣合約把錢轉給這個合約的,左邊的拍賣合約又開始執行到if,再轉一次錢。注意黑客合約賬戶的清零的操作(左下角),隻有在轉賬交易完成之後才進行,但是前面的轉賬交易已經陷入到和黑客合約的遞歸調用當中,根本執行不到清零後面,導緻黑客按照自己的出價價格不停地從拍賣合約中取錢,隻有第一次是自己的出價,其他都是合約裡面的。

北大肖臻《區塊鍊技術與應用》學習筆記13

這個遞歸重複到1)拍賣合約上的餘額不夠了,不支援這樣的轉賬語句,2)汽油費不夠了,每次遞歸調用還是消耗汽油費的,3)調用棧溢出了,在右下角黑客合約的fallback函數判斷一下拍賣合約的餘額還足以支援轉賬,目前調用的剩餘汽油msg.gas還有6000個機關以上,調用棧的深度不超過500,那麼就再發起一輪攻擊。

如何解決?

可以先清0再轉賬,和第二版的右邊寫法一緻,轉賬不成功再回複餘額。

也可以不用call.value來轉賬,換成send或者transfer。先清0再轉賬,send和transfer有一個特點就是轉賬的時候發送過去的汽油費隻有2300個機關,不足以讓接收的合約再發起新的調用,隻夠寫一個log而已。

北大肖臻《區塊鍊技術與應用》學習筆記13