智能合約
智能合約是比特幣和以太坊最大的區別。
什么是智能合約
智能合約是運行在區塊鏈上的一段代碼,代碼的邏輯定義了智能合約的內容。
智能合約的賬戶里保存了合約的當前的運行狀態,包含:
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
如何調用智能合約
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是這比交易願意支付的最多汽油。
3.2 合約賬戶調用
- 直接調用方法,合約A和合約B,A合約就只是寫成Log(日志),event是定義一個事件,叫LogCallFoo,emit 來調用這個事件,emit的作用就是寫一個log,對於程序運行沒有影響;B合約參數是一個地址,就是A合約的地址,然后這個語句把這個地址轉換成A這個合約的一個實例,然后調用foo這個函數。
以太坊中規定一個交易只有外部賬戶才能發起,合約賬戶不能自己主動發起一個交易。這個例子當中實際上是需要一個外部賬戶調用了合約B當中的函數CallAFooDirectly,然后這個函數再調用A合約中的foo函數。
- 地址類型調用,addr.call()
第一個參數是函數的signature,后面跟的是調用參數。這種方法和上面的方法的區別:一是對錯誤處理的不同,上面的方法調用的時候,如果調用的合約在執行過程中出現錯誤,會導致發起這個調用的合約跟着一起回滾,上面的例子當中如果合約A出現異常,B也會跟着出現異常;addr.call()這種方法,如果在調用過程中被調用的合約產生異常,call函數會返回False,表明這個調用時失敗的,但是發起調用的這個函數並不會拋出異常,而是可以繼續執行。
- delegatecall()
主要區別是delegatecall()不需要切換到被調用的合約的環境中去執行,而是在當前的合約中執行就可以了,比如就用當前的賬戶余額存儲之類的。
以太坊中凡是要接受外部轉賬的函數都需要標志為payable,否則的話你給這個函數轉錢就引發錯誤處理拋出異常,如果你不需要接受外部轉賬,函數就不用寫payable。
fallback函數
無參數無返回值,無函數名,fallback關鍵字並沒有出現在函數名里面。調用合約的時候,A調用B合約,要在轉賬交易的data域說明調用的是合約B中的哪個函數,如果A給B轉了一筆錢,沒有說明調用的是哪個函數,也就是data域是空的,這個時候缺省的就是調用這個fallback函數,這也是為什么叫fallback函數,因為沒有別的函數可以調用了,就只能調用他。還有一種情況是你要調用的函數不存在,在你的data域里你說你要調用這個函數,實際合約當中沒有這個函數,也是調用fallback函數,這也是為什么這個函數沒有參數也沒有返回值。fallback函數也可能需要標注payable關鍵詞,就如果fallback函數需要有接受轉賬的能力的話是需要寫payable,一般情況都是寫成payable,如果合約賬戶沒有任何函數標志為payable,包括fallback函數也沒有,那么這個合約沒有任何能力可以接受外部的轉賬。如果有人往合約里轉錢就會引發異常。
轉賬金額可以為0,是給收款人的,但是汽油費是要給礦工的,不給的話礦工不會把交易打包到區塊鏈上的。
注釋:只有合約賬戶才有這些函數以及代碼,外部賬戶沒有代碼,能觸發交易
智能合約的創建
智能合約的創建是由某一個外部的賬戶發起一筆轉賬交易,轉給0X0地址,然后把要發布的合約代碼放到data域里面。Java Virtual Machine是為了增強可一致性,EVM也是類似的思想,通過加一層虛擬機,對智能合約的運行提供一致性的平台,所以EVM又叫world wide compute,EVM的尋址空間是非常大的,是256位的,像如之前講的uint和signed int就是256位的,普通計算機是64位的。
汽油費(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算出可能花掉的最大汽油費,然后一次性把汽油費從發起調用的賬戶中扣掉,然后再根據實際執行情況算出實際花了多少汽油費,汽油費不夠會引起回滾。
簡單指令例如加減法消耗的汽油費比較少,復雜的指令消耗的比較多,比如說取哈希,這個運算雖然一條指令就可以完成,但是汽油費就比較貴。除了計算量之外,需要存儲狀態的指令消耗的汽油費也是比較大的。相比之下,如果只是為了讀取公共數據,那些指令是可以免費的。
以太坊中的錯誤處理
以太坊中的交易執行起來具有原子性,一個交易要么全部執行要么完全不執行,不會只執行一部分。這個交易既包含普通的轉賬交易也包含對智能合約的調用,所以如果在執行智能合約過程中出現任何錯誤,會導致整個交易的執行回滾,退回到開始執行之前的狀態,就好像這個交易完全沒有執行過。
7.1 那么什么情況下會出現錯誤呢?
-
錯誤處理一:之前所說的汽油費,如果這個交易執行完之后沒有達到當初的gaslimit,那么多余的汽油費會被退回到這個賬戶里;相反的,如果執行到一半,gaslimit用完了,合約的執行要退回到開始執行之前的狀態,而且這個時候已經消耗的汽油費是不退的。為什么這么設計呢?防止一些惡意的節點發動denial service attack,發動一個計算量很大的合約然后不停地調用這個合約,每次調用的時候給的汽油費都不夠,反正最后汽油費都會退回來,對惡意節點來說沒什么損失,但是對礦工來說白白浪費了很多資源。
-
錯誤處理二:assert語句和require語句,這里兩個語句都是用來判斷判斷某種條件,如果條件不滿足的話就會導致拋出異常。assert語句一般來說是用來判斷某種內部條件,和c語言中的類似;reuire語句判斷某種外部條件,比如說判斷函數的輸入是否符合要求,下圖所給的例子是bid函數里,判斷當前時間now是否小於等於拍賣結束時間,如果符合條件,繼續執行,不符合,即拍賣時間已經結束了,這個時候就會拋出異常。
-
revert語句無條件拋出異常,如果執行到revert語句,那么他自動的就會導致回滾,早期版本用的是throw語句,新版本的solidity建議改為revert語句。
一些語言像java用戶可以自己定義出現錯誤怎么辦,solidity沒有try-catch結構,不可以。
7.2 嵌套調用
Q1: 前面說智能合約調用出現錯誤會導致回滾,那么如果是嵌套調用,一個智能合約調用另外一個智能合約,被調用的智能合約出現錯誤是不是會導致發起調用的智能合約也跟着一起回滾呢?叫做連鎖式回滾。
不一定,這個取決於調用智能合約的方式,如果這個智能合約是直接調用的,那么它會觸發連鎖式的回滾,整個交易都會回滾。如果是用call()這種方式調用,他就不會引起連鎖式回滾,只會使當前的調用失敗,返回一個False的返回值。
Q:有些情況下,從表面上看,你並沒有調用任何函數,比如說單純的賬戶轉賬,但是如果這是以個合約賬戶的話,轉賬的本身就有可能觸發對函數的調用,為什么呢?
因為有fallback函數,這就是一種嵌套調用,一個合約往另外一個合約里轉賬,就可能調用這個合約里的fallback函數。
(給合約轉賬,合約里沒有fallback函數,也沒有說明調用哪個函數,call本身就會返回false,但是不會引起連鎖式回滾。)
數據結構
8.1 blockheader數據結構回顧
比特幣中規定每個區塊不能超過1M,是寫在協議里不能更改的,比特幣的交易是比較簡單的,基本上可以用交易的字節數來衡量出這個交易消耗的資源有多少,但是以太坊這么規定是不行的,因為智能合約的邏輯很復雜,有的交易從字節上看可能很小,但是它消耗的資源很大,比如它可能調用別的合約之類的,所以怎么辦呢?要根據交易的具體操作來收費,這就是汽油費。
GasUsed是這個區塊里,所有交易所消耗的汽油費加在一起,GasLimit是這個區塊里所有交易能夠消耗汽油的一個上限,這里和每個交易的gaslimit(自己設定的)是不一樣的。以太坊的上限GasLimit,和比特幣不太一樣,每個礦工在發布區塊的時候可以對這個GasLimit進行微調,它可以在上一個區塊的GasLimit上調或者下調1/1024,這種機制實際求出的系統GasLimit是所有礦工認為比較合理的GasLimit的一個平均值。
8.2 Receipt數據結構
Q1: 假設某個全節點要打包一些交易到一個區塊里面,這些交易里有一些是對智能合約的調用,那么這個全節點是應該先把智能合約都執行完之后再去挖礦呢?還是先挖礦獲得記賬權再去執行智能合約?
先執行智能合約再挖礦,以太坊挖礦需要嘗試各種不同的nonce值,找到一個符合要求的,計算哈希的時候要用到blockheader的內容,包含三棵樹的根哈希值,只有執行完區塊中的所有交易包括智能合約交易,這樣才能更新這三棵樹,知道三個根哈希值,blockheader的內容才能確定,然后才能嘗試各個nonce挖礦。
Q2:全節點在收到一個對智能合約的調用的時候,要一次性先把這個調用可能花掉的最大汽油費從發起這個調用的賬戶扣掉,這個具體是怎么操作的?
三棵樹,狀態樹,交易樹和收據樹都是全節點在本地維護的數據結構,狀態樹記錄了每個賬戶的狀態,包括賬戶余額,汽油費是全節點收到調用的時候從本地維護的數據結構里把他賬戶的余額減掉就行了,只有區塊發布之后本地修改才會變成外部可見的,區塊鏈共識。
Q3:礦工在挖礦執行智能合約的過程中消耗了很多本地資源,但是並沒有獲得記賬權,沒有出塊獎勵,也不會得到汽油費獎勵,怎么辦?
沒有辦法,以太坊中就是沒有補償,還需要把別人發布的區塊里的交易在本地執行一遍,以太坊規定要驗證發布區塊的正確性,每個全節點要獨立驗證,把別人發布的交易區塊在本地執行一遍,更新三棵樹的內容算出根哈希值,再和發布的新區塊的根哈希值比較是否一致。這種機制下挖礦慢的礦工就特別吃虧,汽油費的設置本來是對礦工執行智能合約所消耗的資源的一種補償,但是這種補償只有挖到礦的礦工才能得到,其他礦工得不到。
Q4:上述問題會造成什么影響?如何改進?
會直接威脅到區塊鏈的安全,區塊鏈的安全保證是來自所有全節點獨立驗證發布的區塊的合法性,這樣少數有惡意的節點才沒有辦法篡改這些內容,如果一些礦工想不通,不給錢就不驗證了,這樣就會危及到區塊鏈的安全。這樣是不可行的,因為如果跳過驗證步驟,以后就沒法挖礦了,因為驗證的時候需要把區塊的交易都執行一遍,更新本地的三棵樹,獲取最新的根哈希值,如果不驗證的話,本地三棵樹的內容沒有辦法更新,以后就沒辦法發布新的區塊了。因為發布的區塊沒有三棵樹的內容,只是塊頭里有個根哈希值,所以沒有辦法不驗證的。在礦池里,礦工本身就不驗證了,有一個全節點pool manager負責統一驗證,礦工相信全節點驗證的正確性,全節點分配給礦工看到的是puzzle的內容,puzzle是全節點跟着區塊鏈更新得來的。
Q5:發布到區塊鏈上的交易是不是都是成功執行的?如果智能合約在執行中出現錯誤,要不要也發布在區塊鏈上?
執行發生錯誤的交易也要發布到區塊鏈上,否則沒有辦法扣掉汽油費。
Q6:怎么知道這個交易是執行成功了呢?
三棵樹里面,每個交易執行完之后形成一個收據,下圖是收據的內容,其中status域會告訴你這個交易的執行情況。
Q7:智能合約支不支持多線程?多核並行處理。
以太坊是一個交易驅動的狀態機,這個狀態機必須是完全確認性的,即給定一個智能合約,面對同一種輸入,產生的輸出或者是轉移到下一個地方的狀態必須是完全確定的,因為所有的全節點都得執行同一組操作,到達同一個狀態,要驗證。如果狀態不確定的話那三棵樹的根哈希值根本對不上,所以必須完全確定才行。多線程的問題在於,多個核對內存訪問順序不同,執行結果有可能是不確定的,所以solidity是不支持多線程的。除了多線程,其他所有可能造成結果不確定的操作也都不支持,比如產生隨機數。所以以太坊中沒有辦法真正產生隨機數,只能產生偽隨機數,否則的話又會出現前面的問題,每個全節點執行完一遍得到的結果都不一樣。
智能合約的執行必須是確定性的,這也就導致了智能合約不能像通用的編程語言那樣通過系統調用得到一些system call的一些環境信息,因為每個全節點的執行環境 不是完全一樣的,所以他只有通過一些固定的變量的值能夠得到一些狀態信息,這個表格就是智能合約能夠得到的區塊鏈的一些信息。
智能合約的其他信息
9.1 可獲得的調用信息
msg.sender發起這個調用的用戶,和tx.origin交易發起者是不一樣的。
比如說外部賬戶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 地址類型
第一個是成員變量:就是成員賬戶的余額balance, uint256是成員變量的類型,不是函數調用/參數,單位是比較小的。addr.balance()這個地址上賬戶的余額。
剩下的都是成員函數,成員函數的語義和直觀理解不太相同,addr.transfer(12345)是當前這個合約C向這個地址轉入多少錢。addr.call是指當前這個合約發起一個調用,調用的是addr這個合約。
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這種方法轉賬就把剩下的汽油都發過去了。
拍賣例子
拍賣受益人就是拍賣前物品的所有者。
拍賣規則:拍賣結束之前每個人都可以去出價去競拍,競拍的時候為了保證誠信,需要把競拍的價格相應的以太幣發過去,比如出一百個以太幣,你用bid函數競拍的時候,要把100個以太幣發送到智能合約,並鎖在里面直到拍賣結束,不允許中途退出,可以加價,拍賣結束之后,highestBidder的出價的錢數會給受益人beneficiary,受益人應該把拍賣物也給最高出價人。其他沒有競拍成功的人可以把錢再取出來。競拍可以多次出價,補差價發到智能合約里就可以,出價有效的話必須保證加價之后的出價高於之前的最高出價,否則就是無效(非法)的。constructor構造函數在合約創建的時候會記錄受益人是誰,結束時間是什么時候。
下面兩個是拍賣用的兩個函數,左邊是競拍bid函數,競拍的時候發起一個交易調用拍賣合約中的bid函數,bid雖然沒有參數,但是在msg.value發起這個調用的時候轉賬轉過去的以太幣數目,就是出的競拍價格。
-
先查詢一下拍賣是否結束,如果已經結束還參與拍賣則拋出異常。
-
查一下上一次的出價加上當前調用所發過去的以太幣大於最高出價,如果是以前沒有出價過,第一部分就是0。bids是個哈希表,solidity里的特點是如果要查詢的鍵值不存在,則返回默認值為0。所以如果是以前沒有出價過,第一部分就是0。第一次拍賣的時候把拍賣者的信息放在bidder數組里,因為solidity不支持遍歷,要遍歷哈希表必須保存一下包含哪些元素,然后記錄一下新的最高出價人是誰,寫一些日志之類的。
右邊是拍賣結束的函數,首先查一下拍賣是否已經結束了,如果拍賣還沒有結束,有人調用這個函數就是非法的,就拋出異常。第二行判斷這個函數是不是被調用過,如果調用過就不用再調一遍了。第三行把最高出價人的錢轉給受益人,對於拍賣沒有成功的人,最后循環把金額退回給bidder。然后標注一下這個函數已經執行完了,寫一個log。
智能合約的代碼是儲存在data域里面的,礦工把智能合約發布到區塊鏈上之后返回給你一個合約的地址,然后這個合約就在區塊鏈上了,所有人都可以調用。任何人出價競拍調用Bid函數的操作都需要礦工發布在區塊鏈上。
但是這里有個問題就是AuctionEnded函數必須有人調用才會執行,執行之后才會結束,solidity語言沒有辦法把他設置成為拍賣結束之后自動執行end。
假設有一個人通過上圖的合約賬戶參與競拍,會有什么結果?
這個合約只有一個函數,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。
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,再轉一次錢。注意黑客合約賬戶的清零的操作(左下角),只有在轉賬交易完成之后才進行,但是前面的轉賬交易已經陷入到和黑客合約的遞歸調用當中,根本執行不到清零后面,導致黑客按照自己的出價價格不停地從拍賣合約中取錢,只有第一次是自己的出價,其他都是合約里面的。
這個遞歸重復到1)拍賣合約上的余額不夠了,不支持這樣的轉賬語句,2)汽油費不夠了,每次遞歸調用還是消耗汽油費的,3)調用棧溢出了,在右下角黑客合約的fallback函數判斷一下拍賣合約的余額還足以支持轉賬,當前調用的剩余汽油msg.gas還有6000個單位以上,調用棧的深度不超過500,那么就再發起一輪攻擊。
如何解決?
可以先清0再轉賬,和第二版的右邊寫法一致,轉賬不成功再回復余額。
也可以不用call.value來轉賬,換成send或者transfer。先清0再轉賬,send和transfer有一個特點就是轉賬的時候發送過去的汽油費只有2300個單位,不足以讓接收的合約再發起新的調用,只夠寫一個log而已。