此文已由做者蘇州受權網易雲社區發佈。java
歡迎訪問網易雲社區,瞭解更多網易技術產品運營經驗android
7.智能合約經驗分享web
1)智能合約開發的工具的問題redis
古人云「工欲善其事必先利其器」,贊成良好的智能合約的開發工具對智能合約的開發效率有極大的提高。如下是一些比較好的智能合約的開發組合:編程
Remix+Ganache/testrpc:Remix爲合約編輯工具,還能自動生成可視化調用入口;Ganache合約的模擬環境,能可視化查看事物、調用的命令等;testrpc功能和ganache同樣,可是不是可視化界面;數組
Truffle+Ganache/testrpc:Truffle是一款集合約部署、調用、單元測試、調試與一身的智能合約開發工具,功能很強大。安全
注:Remix有在線網頁版和本地網頁版,若是嫌在線網頁版滿能夠安裝本地網頁版;另外本人在安裝使用Ganache出現過Remix沒法獲取其自建立的帳戶,能夠試着從新下載安裝。服務器
2)針對web3j開發基於區塊鏈的外部應用時,自定義腳本的使用app
以web3j爲例(固然不侷限於web3j),通常狀況下針對sol源文件須要通過solcd的編譯合約和生成Java文件兩部命令操做。在實際項目中,爲了「一鍵編譯生成Java文件」首先編寫了自動化腳步,而後經過命令自動編譯Java文件到指定目錄下,這點對外部系統的智能合約的開發效率上也起到了很大的提高,例如「一鍵編譯生成Java文件」腳步以下:負載均衡
#!/bin/sh
sol_directory="../src/main/resources/solidity" abi_bin_directory="../src/main/resources/solidity/build" java_directory="../src/main/java" java_package="com.netease.blockchainsdk.contracts.generated"
for file in ${sol_directory}/* do if test -f $file then if [ "${file##*.}"x = "sol"x ];then tmp=${file##*/} filename=${tmp%.*} echo ${filename} echo "Compiling Solidity file ${filename}.sol" solc --bin --abi --optimize --overwrite \ --allow-paths "$(pwd)" \ soldirectory/{filename}.sol -o ${abi_bin_directory}/ echo "Complete"
echo "Generating contract bindings" web3j-3.2.0/bin/web3j solidity generate \ abibindirectory/{filename}.bin \ abibindirectory/{filename}.abi \ -p ${java_package} \ -o ${java_directory} > /dev/null echo "Complete" fi fi if test -d $file then echo $file 是目錄 fi done |
注「腳本」、「工具」對於項目來講都是能提高很高的效率,若是有繁瑣重複的工做,就須要考慮可否經過腳本或者工具去解決。
3)針對web3j的Java應用開發的注意點
經過腳本,生成智能合約對應的java類(該java類對智能合約作了封裝),java類中提供deploy發佈智能合約、load加載智能合約、EventObservable事件監聽通知、buyproduct等業務函數。業務邏輯調用生成的智能合約的java類(proxy智能合約類),實現業務邏輯。使用者使用開發JAR包調用合約須要注意的點:
一次監聽調用者用封裝的ContractProxy.observeEventOnce方法,及時釋放監聽事件;長期性的事件監聽者(如數據服務提供者)則用長期監聽事件方法ContractProxy.observeEvent;
事件監聽observe接口參數須要指定監聽事件的起始塊number,建議把最後處理塊number存入redis等,每次加載重啓時從redis獲取;若是一個block中有多個事件須要處理,還得引入訂單號等業務邏輯去重;
監聽事件在分佈式環境中會被屢次處理,須要增強協調機制;
監聽的回調函數繼承CallBackFun類,注意實現onTimeout、onError等邏輯;
RemoteCall可使用異步發送+超時機制,見ContractProxy的asynCall方法;
調用參數的隱私性經過公鑰加密私鑰解密手段實現,加解密參考CryptoUtils類,訂單號生成參考UIDGenenrator類。
4)在合約中使用自定義互斥鎖
使用互斥鎖。即讓你「鎖定」某些狀態,後期只能由鎖的全部者對這些狀態進行更改,以下所示,這是一個簡單的例子:
若是用戶在第一次調用結束前嘗試再次調用withdraw() 函數,那麼這個鎖定會阻止這個操做,從而使運行結果不受影響。這多是一種有效的解決方案,可是當你要同時運行多個合約時,這種方案也會變得很棘手,如下是一個不安全的例子:
這種狀況下攻擊者能夠調用函數getLock()鎖定合約,而後再也不調用函數releaseLock()解鎖合約。若是他們這樣作,那麼合約將被永久鎖定,而且永遠不能作出進一步的更改。若是你使用互斥鎖來防止競態條件,你須要確保不會出現這種聲明瞭鎖定但永遠沒有解鎖的狀況。在編寫智能合約時使用互斥鎖還有不少其餘的潛在風險,例如死鎖或活鎖。若是你決定採用這種方式,必定要大量閱讀關於互斥鎖的文獻,避免「踩雷」。
5)合約中的錯誤處理
Solidity提供了兩個函數assert和require來進行條件檢查,若是條件不知足則拋出異常。assert函數一般用來檢查(測試)內部錯誤,而require函數來檢查輸入變量或合同狀態變量是否知足條件以及驗證調用外部合約返回值。另外,若是咱們正確使用assert,有一個Solidity分析工具就能夠幫咱們分析出智能合約中的錯誤,幫助咱們發現合約中有邏輯錯誤的bug。
除了能夠兩個函數assert和require來進行條件檢查,另外還有兩種方式來觸發異常:
revert函數能夠用來標記錯誤並回退當前調用;
使用throw關鍵字拋出異常(從0.4.13版本,throw關鍵字已被棄用,未來會被淘汰。)
當子調用中發生異常時,異常會自動向上「冒泡」。 不過也有一些例外:send,和底層的函數調用call, delegatecall,callcode,當發生異常時,這些函數返回false。
注意:在一個不存在的地址上調用底層的函數call,delegatecall,callcode 也會返回成功,因此咱們在進行調用時,應該老是優先進行函數存在性檢查。在下面經過一個示例來 說明如何使用require來檢查輸入條件,以及assert用於內部錯誤檢查:
*assert類型異常, 在下述場景中自動產生assert類型的異常:
1.若是越界,或負的序號值訪問數組,如i >= x.length 或 i < 0時訪問x[i]
2.若是序號越界,或負的序號值時訪問一個定長的bytesN。
3.被除數爲0,如5/0或 23 % 0。
4.對一個二進制移動一個負的值。如:5<<i; i爲-1時。
5.整數進行能夠顯式轉換爲枚舉時,若是將過大值,負值轉爲枚舉類型則拋出異常
6.若是調用未初始化內部函數類型的變量。
7.若是調用assert的參數爲false
當發生assert類型的異常時,Solidity會執行一個無效操做(指令0xfe)。
*require類型異常, 在下述場景中自動產生require類型的異常:
1.調用throw
2.若是調用require的參數爲false
3.若是你經過消息調用一個函數,但在調用的過程當中,並無正確結束(gas不足,沒有匹配到對應的函數,或被調用的函數出現異常)。底層操做如call,send,delegatecall或callcode除外,它們不會拋出異常,但它們會經過返回false來表示失敗。
4.若是在使用new建立一個新合約時出現第3條的緣由沒有正常完成。
5.若是調用外部函數調用時,被調用的對象不包含代碼。
6.若是合約沒有payable修飾符的public的函數在接收以太幣時(包括構造函數,和回退函數)。
7.若是合約經過一個public的getter函數(public getter funciton)接收以太幣。
8.若是.transfer()執行失敗
當發生require類型的異常時,Solidity會執行一個回退操做(指令0xfd)。
在上述的兩種狀況下,EVM都會撤回全部的狀態改變。是由於指望的結果沒有發生,就無法繼續安全執行。必須保證交易的原子性(一致性,要麼所有執行,要麼一點改變都沒有,不能只改變一部分),因此須要撤銷全部操做,讓整個交易沒有任何影響。
注意assert類型的異常會消耗掉全部的gas, 而require從大都會版本(Metropolis, 即目前主網所在的版本)起不會消耗gas。
8.智能合約的漏洞事件分析
1)數值溢出問題
在編寫智能合約時,特別是涉及資金轉帳的狀況下,須要校驗轉帳的數值是否發生溢出。建議能夠經過增長safeMath(以太訪也提供了)方法,將涉及計算的地方都替換爲安全計算方法,規避該問題:
Library SafeMath{ function mul(uint256 a,uint256 b) internal constant returns(uint256){ uint256 c=a *b; assert(a==0||c/a==b); return c; } function div(uint256 a,uint256 b) internal constant returns(uint256){ uint256 c=a/b; return c; } function sub(uint256 a,uint256 b) internal constant returns(uint256){ uint256 c=a/b; return c; } function add(uint256 a,uint256 b) internal constant returns(uint256){ uint256 c=a+b; assert(c>=a); return c; } } |
2)Call Deep Attack(棧深度限制)攻擊
爲了防止在執行智能合約時出現無限遞歸調用,EVM規定了調用棧深度不能超過1024,一旦超過1024,那麼EVM將再也不執行該操做。例如如下代碼:
contract auction{ mapping(address=>uint) refunds; //.... function withDrawReFund(address receipt){ uint refund=refunds[receipt]; refunds[receipt]=0; receipt.send(refund); } } |
假如當咱們運行send函數時,事實上調用了receipt的回調函數,本質上是調用一個函數,所以黑客利用在執行的send方法時,經過大量的合約調用構造了一個調用深度爲1023的棧,那麼會致使withdraw方法執行不成功,可是其餘方法能夠執行。可是這類攻擊在EIP155中已經修復。
3)Reentrancy(可重入)攻擊
The DAO事件即是由此類攻擊引發的,該攻擊是黑客重複調用某個函數,以下:
mapping(address=>uint) private userBalances; //.... function withDrawBalance() public { uint amountToWithDraw=userBalances[msg.sender]; If(!(msg.sender.call.value(amountToWithDraw))){throw;} userBalances[msg.sender]=0; } |
因爲用戶在執行msg.sneder.call.value方法時,實際調用的是該sender帳戶的fallback函數,而該sender帳戶多是一個合約帳戶,同時該帳戶中fallback函數又調用withdrawbalance方法,那麼就實現了黑客能夠重複調用withdrawBalance。
解決方式:使用函數send()而不是函數call.value()(),這將阻止任何外部代碼的執行;可是若是沒法避免要調用外部函數時,防止這種攻擊的下一個簡便方法就是確保在你調用外部函數時已完成全部要執行的內部操做。
4)Cross-function Race Conditions(跨函數的競態條件)攻擊
可重入攻擊是經過對同一個函數的不斷調用,而cross function race condition則是經過對不一樣函數的組合調用來實現攻擊的一種方法。具體以下:
mapping(address=>uint) private userBalances; //.... function transfer(address to,uint amount){ If(userBalances[msg.sender]>=amount){ userBalances[to]+=amount; userBalances[msg.sender]-=amount; } } function withDrawBalance() public { uint amountToWithDraw=userBalances[msg.sender]; If(!(msg.sender.call.value(amountToWithDraw))){throw;} userBalances[msg.sender]=0; } |
加入咱們在調用withdrawBalance的時候,調用者在userBalance還有100ether,那麼咱們在執行msg.sender.call.value時,假如這個sender是一個智能合約B,而且B在fallback函數中會調用這個合約的transfer方法,而這個時候因爲userBalance中的調用者還有100Ether,所以他仍是能夠進行轉帳給to,當執行完後,調用者的合約帳戶才爲0,對於調用者來講多轉了1倍的錢。
解決方案,這兒有兩種解決方案,一是咱們建議先完成全部的內部工做,而後再調用外部函數;二是使用互斥鎖(即讓你「鎖定」某些狀態,後期只能由鎖的全部者對這些狀態進行更改)。
5)DOS with(Unexpected) Throw
通常狀況是遇到異常狀況下,選址throw關鍵字進行異常拋出,可是某些場景下須要注意對throw的使用。
Contract Auction{ Address currentLeader; Uint highestBid; Function bid(){ If(msg.value<=highestBid){throw;} If(!currentLeader.send(highestBid)){throw;} currentLeader=msg.sender; highestBid=msg.value; } } |
假設當前最高價是100,若是有惡意的競標者A,競標價爲101,且該帳戶是一個智能合約帳戶,而且他的fallback函數是一個無限循環,那麼另外一個出價102後會出現異常,致使沒法執行,不管其餘人出多少價,A都是贏家。
6)Dos with Block Gas Limit攻擊
因爲每一個交易和Block都有gaslimit的限制,所以編寫合約的時候須要考慮gaslimit的限制,以及超過gaslimit限制時出現的異常問題。
若一個合約中數據量一直增大,致使處理合約數據時一直會超過gaslimit限制,則會致使合約的功能異常。
7)交易順序依賴與非法預先交易漏洞
交易順序依賴(Transaction-Ordering Dependence,TOD);
非法預先交易(Front Running)非法預先交易是經紀人從客戶交易中獲利的一種不道德作法。在手中持有客戶交易委託的狀況下搶先爲本身的帳戶進行交易。如下是區塊鏈固有的不一樣類型的競態條件:在區塊內部,交易自己的順序很容易受到人爲操控。
因爲在礦工挖礦時,每筆交易都會在內存池中待一段時間,所以能夠想象到交易被打包進區塊前會發生什麼。對於去中心化的市場,可更改的交易順序會帶來不少的麻煩。好比市場上常見的買入某些代幣的交易。而防範這一點十分地困難,由於它會涉及到合約中具體的實現細節。例如,在去中心化市場中,因爲能夠防止高頻交易,故批量拍賣的效果更好。另外一種解決方法就是採用預先提交方案的機制,彆着急,後面我會詳細介紹這個機制的細節。
8)DNS挾持攻擊
解決方式:必定要確保域名以及 HTTPS 證書是正確的;在聯網狀況下,不要輸入私鑰,選擇選擇合適的錢包。
9)強行給智能合約中加入以太幣
原則上,咱們能夠將以太幣強制發送到智能合約中而不觸發回退函數。當給回退函數加入重要功能或計算智能合約的收支平衡時,這是一個重要的考慮因素。請看下面這個例子:
10)已廢棄協議攻擊
這些攻擊因爲以太坊協議的改變或以太坊編程語言solidity的改進而不能使用。
11)Timestamp depedence(時間戳依賴)攻擊
合約中的時間都是根據Block的時間戳得到的,所以若是礦工改變了塊的時間戳將引起程序執行邏輯不同。
Uint startTime=x; If(now>startTime +1 week){//do...} |
免費領取驗證碼、內容安全、短信發送、直播點播體驗包及雲服務器等套餐
更多網易技術、產品、運營經驗分享請點擊。
相關文章:
【推薦】 Android事件分發機制淺析(1)
【推薦】 3分鐘帶你瞭解負載均衡服務