以太坊上的應(yīng)用程序管理財務(wù)價值,使安全性變得絕對重要。作為一種新興的、實驗性的技術(shù),智能合約當(dāng)然也受到了相當(dāng)多的攻擊。
為了防止進一步的攻擊,我列出了幾乎所有已知的攻擊和漏洞的列表。盡管此列表可能包含已知的攻擊,但新的漏洞仍在定期發(fā)現(xiàn),因此,這應(yīng)該只是您作為工程師研究智能合約安全性的開始。
攻擊
在本節(jié)中,我們將介紹可用于利用智能合約漏洞的已知攻擊。
前端運行又稱為事務(wù)排序依賴性
康科迪亞大學(xué)(University of Concordia)認為,“先行是一種行動,在此過程中,用戶可以從預(yù)先訪問有關(guān)即將發(fā)生的交易和交易的特權(quán)市場信息中受益”,對市場中未來事件的了解會導(dǎo)致剝削。
例如如果知道某個特定代幣將要進行非常大的購買,那么壞的參與者可以提前購買該代幣,并在超大的購買訂單提高價格時出售該代幣以獲取利潤。
前端攻擊在金融市場長期以來一直是一個問題,由于區(qū)塊鏈的透明性,這個問題在加密貨幣市場再次出現(xiàn)。
由于此問題的解決方案因合約而異,因此很難避免??赡艿慕鉀Q方案包括批量交易和使用預(yù)提交方案(即允許用戶稍后提交詳細信息)。
限制區(qū)塊氣體的DoS
在以太坊區(qū)塊鏈中,所有區(qū)塊都有氣體限制。氣體限制的好處之一是,它可以防止攻擊者創(chuàng)建無限的事務(wù)循環(huán),但是如果事務(wù)的氣體使用量超過此限制,則事務(wù)將失敗。 這可能以幾種不同的方式導(dǎo)致DoS攻擊。
無限操作
在這種情況下,區(qū)塊氣限額可能是一個問題,即向一系列地址發(fā)送資金。即使沒有任何惡意,這也很容易出錯。僅僅是因為有太多的用戶需要付費,就可以最大限度地超出氣體的限額,并阻止交易的成功。
這種情況也可能導(dǎo)致攻擊。假設(shè)一個壞的參與者決定創(chuàng)建大量的地址,每個地址從智能合約中支付少量資金。如果有效地執(zhí)行,則可以無限期地阻止事務(wù),甚至可能阻止進一步的事務(wù)處理。
解決此問題的有效方法是在當(dāng)前的推付式支付系統(tǒng)上使用預(yù)付式支付系統(tǒng)。為此,請將每筆付款分成自己的交易,然后讓收款人調(diào)用該功能。
如果出于某種原因,您真的需要遍歷一個未指定長度的數(shù)組,至少希望它可能占用多個區(qū)塊,并允許它在多個事務(wù)中執(zhí)行,如本例所示:
struct Payee {
address addr;
uint256 value;
}
Payee[] payees;
uint256 nextPayeeIndex;
function payOut() {
uint256 i = nextPayeeIndex;
while (i 《 payees.length && msg.gas 》 200000) {
payees[i].addr.send(payees[i].value);
i++;
}
nextPayeeIndex = i;
}
區(qū)塊填充
在某些情況下,即使您未遍歷未指定長度的數(shù)組,您的智能合約也可能受到氣體限制的攻擊。攻擊者可以通過使用足夠高的氣體價格來填充交易之前的幾個區(qū)塊。
這種攻擊是通過以很高的氣體價格發(fā)行幾筆交易來完成的。如果氣體價格足夠高,并且交易消耗了足夠的氣體,它們就可以填滿整個區(qū)塊并阻止其他交易被處理。
以太坊交易要求發(fā)送者支付費以抑制垃圾交易攻擊,但是在某些情況下,可以有足夠的動機來進行此類攻擊。例如在Dapp Fomo3D上使用了區(qū)塊填充攻擊。該應(yīng)用程序具有倒數(shù)計時器,通過最后一次購買密鑰,用戶可以贏得大獎-除非用戶每次購買鑰密鑰,計時器都會延長。攻擊者購買了一把密鑰,然后連續(xù)塞滿了接下來的13個區(qū)塊,這樣他們才能贏得大獎。
為了防止此類攻擊的發(fā)生,必須仔細考慮在應(yīng)用程序中合并基于時間的操作是否安全。
撤回Dos
DoS(拒絕服務(wù))攻擊可能發(fā)生在函數(shù)中,當(dāng)您嘗試向用戶發(fā)送資金時,該函數(shù)依賴于該資金轉(zhuǎn)移是否成功。
如果資金被發(fā)送到一個由壞的參與者創(chuàng)建的智能合約中,這可能會有問題,因為他們可以簡單地創(chuàng)建一個回退函數(shù)來還原所有付款。
例如:
// INSECURE
contract AucTIon {
address currentLeader;
uint highestBid;
funcTIon bid() payable {
require(msg.value 》 highestBid);
require(currentLeader.send(highestBid));
// Refund the old leader, if it fails then revert
currentLeader = msg.sender;
highestBid = msg.value;
}
}
如本例所示,如果攻擊者通過具有回退函數(shù)的智能合約出價來還原所有付款,則它們將永遠無法退款,因此,沒有人可以提出更高的出價。
在沒有攻擊者在場的情況下,這也可能會帶來問題。 例如您可能希望通過遍歷數(shù)組來向用戶支付費用,當(dāng)然,您要確保為每個用戶都支付了適當(dāng)?shù)馁M用。 這里的問題是,如果一次付款失敗,該功能將被還原并且而沒有人得到付款。
address[] private refundAddresses;
mapping (address =》 uint) public refunds;
// bad
funcTIon refundAll() public {
for(uint x; x 《 refundAddresses.length; x++) {
// arbitrary length iteraTIon based on how many addresses participated
require(refundAddresses[x].send(refunds[refundAddresses[x]]))
// doubly bad, now a single failure on send will hold up all funds
}
}
解決此問題的有效方法是在當(dāng)前的推付式支付系統(tǒng)上使用預(yù)付式支付系統(tǒng)。 為此,請將每筆付款分成自己的交易,然后讓收款人調(diào)用該功能。
contract auction {
address highestBidder;
uint highestBid;
mapping(address =》 uint) refunds;
function bid() payable external {
require(msg.value 》= highestBid);
if (highestBidder != address(0)) {
refunds[highestBidder] += highestBid; // record the refund that this user can claim
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(refund)(“”);
require(success);
}
}
強制將以太坊發(fā)送給智能合約
有時候,用戶不需要將以太坊發(fā)送到智能合約。不幸的是,在這種情況下,可以繞過智能合約回退函數(shù)并強行發(fā)送以太坊。
contract Vulnerable {
function () payable {
revert();
}
function somethingBad() {
require(this.balance 》 0);
// Do something bad
}
}
盡管似乎應(yīng)該撤消與Vulnerable智能合約的任何交易,但實際上有兩種方法可以強制發(fā)送Ether。
第一種方法是在以“易受攻擊的合同”地址設(shè)置為受益人的合同上調(diào)用“selfdestruct”方法。 這是可行的,因為selfdestruct不會觸發(fā)回退函數(shù)。
另一種方法是預(yù)先計算智能合約的地址,并在部署智能合約之前將以太坊發(fā)送到該地址。令人驚訝的是,這是可能實現(xiàn)的。
Griefing是一種經(jīng)常在視頻游戲中執(zhí)行的攻擊,惡意用戶以一種意外的方式玩游戲,以打擾其他玩家,也就是trolling。此類攻擊還用于阻止事務(wù)按預(yù)期執(zhí)行。
可以對接受數(shù)據(jù)并在另一個智能合約的子調(diào)用中使用它的智能合約進行此攻擊。此方法通常用于多簽名錢包以及交易中繼器中。如果子調(diào)用失敗,則將還原整個事務(wù)或繼續(xù)執(zhí)行。
讓我們以一個簡單的中繼智能合約為例。 如下所示,中繼智能合約允許某人進行交易并簽署交易,而不必執(zhí)行交易。當(dāng)用戶無法支付與交易相關(guān)的氣體時,通常會使用此函數(shù)。
contract Relayer {
mapping (bytes =》 bool) executed;
function relay(bytes _data) public {
// replay protection; do not call the same transaction twice
require(executed[_data] == 0, “Duplicate call”);
executed[_data] = true;
innerContract.call(bytes4(keccak256(“execute(bytes)”)), _data);
}
}
執(zhí)行事務(wù)的用戶(轉(zhuǎn)發(fā)器)可以通過僅使用足以執(zhí)行事務(wù)的氣體而不是足夠使子調(diào)用成功的氣體來有效地審查事務(wù)。
有兩種方法可以防止這種情況發(fā)生。第一種解決方案是僅允許受信任的用戶中繼事務(wù)。另一種解決方案是要求轉(zhuǎn)運商提供足夠的氣體,如下所示。
// contract called by Relayer
contract Executor {
function execute(bytes _data, uint _gasLimit) {
require(gasleft() 》= _gasLimit);
。..
}
}
#### 可重入攻擊
可重入性是一種攻擊,當(dāng)契約函數(shù)中的錯誤允許函數(shù)在本應(yīng)禁止的情況下多次執(zhí)行時,可能會發(fā)生這種攻擊。如果惡意使用,這可以用來從智能合約中抽走資金。實際上,可重入性是DAO攻擊中使用的攻擊向量。
單函數(shù)可重入(Single-function reentrancy)
當(dāng)易受攻擊的函數(shù)與攻擊者試圖遞歸調(diào)用的函數(shù)相同時,就會發(fā)生單函數(shù)重入攻擊。
// INSECURE
function withdraw() external {
uint256 amount = balances[msg.sender];
require(msg.sender.call.value(amount)());
balances[msg.sender] = 0;
}
在這里,我們可以看到余額只有在資金轉(zhuǎn)移后才被修改。這可以讓黑客在余額設(shè)置為0之前多次調(diào)用該函數(shù),有效地耗盡智能合約。
跨函數(shù)重入攻擊
跨函數(shù)重入攻擊是同一過程的更復(fù)雜版本。當(dāng)易受攻擊的功能與攻擊者可以利用的功能共享狀態(tài)時,就會發(fā)生跨函數(shù)重入攻擊。
// INSECURE
function transfer(address to, uint amount) external {
if (balances[msg.sender] 》= amount) {
balances[to] += amount;
balances[msg.sender] -= amount;
}
}
function withdraw() external {
uint256 amount = balances[msg.sender];
require(msg.sender.call.value(amount)());
balances[msg.sender] = 0;
}
在此示例中,黑客可以通過在fallout()函數(shù)中將余額設(shè)置為0之前具有回退函數(shù)調(diào)用transfer()來轉(zhuǎn)移已用資金來利用此智能合約。
防止可重入攻擊
在智能合約中轉(zhuǎn)移資金時,請使用發(fā)送或轉(zhuǎn)移而不是調(diào)用。使用調(diào)用的問題與其他函數(shù)不同,它沒有2300的限制。這意味著可以在外部函數(shù)調(diào)用中使用該調(diào)用,該函數(shù)可用于執(zhí)行重入攻擊。
另一種可靠的預(yù)防方法是標(biāo)記不受信任的功能。
function untrustedWithdraw() public {
uint256 amount = balances[msg.sender];
require(msg.sender.call.value(amount)());
balances[msg.sender] = 0;
}
此外,為了獲得最佳安全性,請使用“checks-effects-interactions”模式。這是智能合約函數(shù)的簡單經(jīng)驗法則。
該函數(shù)應(yīng)從checks開始-例如require和assert語句。
接下來,應(yīng)執(zhí)行智能合約的效力-例如狀態(tài)修改。
最后,我們可以與其他智能合約進行交互-例如外部函數(shù)調(diào)用。
這種結(jié)構(gòu)可有效防止重入,因為智能合約的修改狀態(tài)將防止不良行為者執(zhí)行惡意交互。
function withdraw() external {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
require(msg.sender.call.value(amount)());
}
由于在執(zhí)行任何交互操作之前將余額設(shè)置為0,因此如果遞歸調(diào)用智能合約,則在第一個事務(wù)之后將沒有任何要發(fā)送的內(nèi)容。
脆弱性
在本節(jié)中,我們將介紹已知的智能合約漏洞以及如何避免這些漏洞。此處列出的幾乎所有漏洞都可以在智能合約弱點分類中找到。
整數(shù)上溢和下溢
實際上,整數(shù)類型具有最大值。 例如:
uint8 =》 255
uint16 =》 65535
uint24 =》 16777215
uint256 =》(2 ^ 256)-1
當(dāng)您超過最大值(溢出)或低于最小值(下溢)時,可能會發(fā)生溢出和下溢錯誤。當(dāng)您超過最大值時,您將返回到零,而當(dāng)您低于最小值時,它將使您返回到最大值。
由于較小的整數(shù)類型(如uint8,uint16等)具有較小的最大值,因此更容易引起溢出; 因此應(yīng)謹(jǐn)慎使用它們。
可能的最佳解決溢出和下溢錯誤的方法是在執(zhí)行數(shù)學(xué)運算時使用OpenZeppelin SafeMath庫。
時間戳依賴性
現(xiàn)在或block.timestamp訪問的塊的時間戳可由礦工操作。 使用時間戳執(zhí)行智能合約函數(shù)時,應(yīng)考慮三個因素。
時間戳操縱
如果使用時間戳來嘗試產(chǎn)生隨機性,則礦工可以在區(qū)塊驗證后的15秒鐘內(nèi)發(fā)布時間戳,從而使他們能夠?qū)r間戳設(shè)置為一個值,從而增加使用該功能的幾率。
例如彩票應(yīng)用可以使用區(qū)塊時間戳來選擇組中的隨機投標(biāo)人。礦工可以進入彩票,然后將時間戳修改為一個值,使他們有更大的幾率贏得彩票。
因此,不應(yīng)將時間戳用于創(chuàng)建隨機性。
5秒規(guī)則
以太坊的參考規(guī)范“ Yellow Paper”(黃皮書)沒有規(guī)定可以改變多少區(qū)塊的時間限制,它必須大于其父級的時間戳。話雖這么說,流行的協(xié)議實現(xiàn)會拒絕將來時間戳大于15秒的區(qū)塊,因此只要您的時間相關(guān)事件可以安全地相差15秒,就可以安全地使用區(qū)塊時間戳。
不要使用block.number作為時間戳
您可以使用block.number和平均區(qū)塊時間來估計事件之間的時間差。 但是阻止時間可能會更改并破壞函數(shù),因此最好避免這種用法。
通過tx.origin授權(quán)
tx.origin是Solidity中的全局變量,它返回發(fā)送事務(wù)的地址。重要的是您切勿使用tx.origin進行授權(quán),因為另一個智能合約可以使用回退函數(shù)來調(diào)用您的智能合約并獲得授權(quán),因為授權(quán)地址存儲在tx.origin中。 考慮以下示例:
pragma solidity 》=0.5.0 《0.7.0;
// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract TxUserWallet {
address owner;
constructor() public {
owner = msg.sender;
}
function transferTo(address payable dest, uint amount) public {
require(tx.origin == owner);
dest.transfer(amount);
}
}
在這里我們可以看到TxUserWallet智能合約使用tx.origin授權(quán)transferTo()函數(shù)。
pragma solidity 》=0.5.0 《0.7.0;
interface TxUserWallet {
function transferTo(address payable dest, uint amount) external;
}
contract TxAttackWallet {
address payable owner;
constructor() public {
owner = msg.sender;
}
function() external {
TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
}
}
現(xiàn)在如果有人誘騙您將以太坊發(fā)送至TxAttackWallet智能合約地址,他們可以通過檢查tx.origin來查找發(fā)送交易的地址來竊取您的資金。
為了防止這種攻擊,請使用msg.sender進行授權(quán)。
浮動編譯器
最好選擇一個編譯器版本并堅持使用它。使用浮動編譯器時,可能會使用過時或有問題的編譯器版本意外地部署智能合約,這可能會導(dǎo)致錯誤,從而使智能合約的安全性受到威脅。對于開源項目,該實用程序還會告訴開發(fā)人員在部署您的智能合約時要使用哪個版本。所選的編譯器版本應(yīng)經(jīng)過全面測試,并考慮是否存在已知錯誤。
對于庫和包,可以使用浮動編譯指示例外。 否則開發(fā)人員將需要手動更新編譯指示以在本地編譯。
函數(shù)默認可見性
可以將功能可見性指定為public,private,internal或external。重要的是要考慮哪種可視性最適合您的智能合約函數(shù)。
許多智能合約攻擊是由開發(fā)人員忘記或放棄使用可見性修飾符引起的。 然后默認情況下將該函數(shù)設(shè)置為public,這可能導(dǎo)致意外狀態(tài)更改。
過時的編譯器版本
開發(fā)人員通常會在現(xiàn)有軟件中發(fā)現(xiàn)錯誤和漏洞并進行修補。 因此盡可能使用最新的編譯器版本很重要。
未檢查的調(diào)用返回值
如果未檢查低級調(diào)用的返回值,則即使函數(shù)調(diào)用拋出錯誤,也可能繼續(xù)執(zhí)行。這可能導(dǎo)致意外行為并破壞程序邏輯。失敗的調(diào)用甚至可能是由攻擊者引起的,攻擊者可以進一步利用應(yīng)用程序進行攻擊。
在Solidity中,您可以使用低級調(diào)用,例如address.call(),address.callcode(),address.delegatecall()和address.send(),也可以使用智能合約調(diào)用,例如ExternalContract.doSomething( )。低級調(diào)用永遠不會引發(fā)異常-相反,如果遇到異常,它們將返回false,而智能合約調(diào)用將自動引發(fā)。
如果您使用低級調(diào)用,請確保檢查返回值以處理可能的失敗調(diào)用。
無保護的以太坊提款
如果沒有足夠的訪問控制,不良行為者可能能夠從智能合約中撤出部分或全部以太坊。這可能是由于錯誤地命名了要用作構(gòu)造函數(shù)的函數(shù),從而使任何人都可以重新初始化智能合約。為了避免此漏洞,請僅允許授權(quán)或按預(yù)期的方式觸發(fā)提款,并適當(dāng)命名您的構(gòu)造函數(shù)。
無保護的自毀指令
在具有自毀方法的智能合約中,如果缺少訪問控制或訪問控制不足,惡意行為者可以自毀智能合約。重要的是要考慮自毀功能是否絕對必要。如有必要,請考慮使用多重簽名授權(quán)來防止攻擊。
在Parity攻擊中使用了此攻擊。一位匿名用戶定位并利用了“庫”智能合約中的漏洞,從而使自己成為智能合約的所有者。 然后攻擊者開始自毀智能合約。這導(dǎo)致資金被凍結(jié)在587個唯一的錢包中,總共持有513,774.16個以太坊。
狀態(tài)變量默認可見性
開發(fā)人員通常會明確聲明函數(shù)可見性,而聲明變量可見性并不常見。狀態(tài)變量可以具有三個可見性標(biāo)識符之一:public,private或internal。幸運的是,變量的默認可見性是內(nèi)部的,而不是public的,但是即使您打算將變量聲明為internal的,也必須明確,因此對于誰可以訪問該變量沒有錯誤的假設(shè)。
未初始化的存儲指針
數(shù)據(jù)作為存儲,內(nèi)存或調(diào)用數(shù)據(jù)存儲在EVM中。 理解并正確初始化這兩者很重要。 錯誤地初始化數(shù)據(jù)存儲指針,或者只是不進行初始化就可能導(dǎo)致智能合約漏洞。
斷言assert
從Solidity 0.5.0開始,未初始化的存儲指針不再是問題,因為與未初始化的存儲指針的協(xié)定將不再編譯。 話雖如此,了解在某些情況下應(yīng)該使用哪些存儲指針仍然很重要。
在Solidity 0.4.10中,創(chuàng)建了以下函數(shù):assert(),require()和revert()。 在這里,我們將討論assert函數(shù)以及如何使用它。
正式地說,assert()函數(shù)用于聲明不變量;非正式地說,assert()是一個過分自信的保鏢,可以保護您的智能合約,但會在過程中竊取您的氣體。正常運行的智能合約永遠不應(yīng)到達失敗的assert聲明。如果到達了失敗的assert語句,則說明您使用了assert()的方式不正確,或者智能合約中存在將其置于無效狀態(tài)的錯誤。
如果在assert()中檢查的條件實際上不是不變的,則建議您將其替換為require()語句。
使用過時的函數(shù)
隨著時間的流逝,Solidity中的函數(shù)已被棄用,并經(jīng)常被更好的函數(shù)所取代。不要使用過時的函數(shù),這很重要,因為它可能導(dǎo)致意外的效果和編譯錯誤。
下面是一個不推薦使用的函數(shù)和替代項的列表。許多替代品都是簡單的別名,如果用作不推薦使用的替代品,則不會破壞當(dāng)前行為。
委托給不受信任的調(diào)用者
Delegatecall是消息調(diào)用的一種特殊變體。它幾乎與常規(guī)消息調(diào)用相同,只是目標(biāo)地址在調(diào)用協(xié)定的上下文中執(zhí)行,msg.sender和msg.value保持不變。實際上,delegatecall委托其他智能合約修改調(diào)用智能合約的存儲。
由于delegatecall提供了對智能合約的如此多的控制權(quán),因此只將其用于可信的智能合約(比如您自己的智能合約)是非常重要的。如果目標(biāo)地址來自用戶輸入,請確保它是受信任的協(xié)定。
簽名延展性
人們通常會假設(shè)在智能合約中使用加密簽名系統(tǒng)會驗證簽名是否唯一; 但是事實并非如此。在以太坊中的簽名可以在沒有私鑰的情況下進行更改并保持有效。 例如橢圓密鑰密碼術(shù)由三個變量v,r和s組成,如果以正確的方式修改了這些值,則可以獲得帶有無效私鑰的有效簽名。
為避免簽名可延展性的問題,切勿在簽名消息哈希中使用簽名來檢查智能合約是否已處理了先前簽名的消息,因為惡意用戶可以找到并重新創(chuàng)建您的簽名。
構(gòu)造函數(shù)名稱不正確
在Solidity 0.4.22之前,定義構(gòu)造函數(shù)的唯一方法是使用智能合約名稱創(chuàng)建函數(shù)。在某些情況下,這是有問題的。 例如如果智能合約以不同的名稱重復(fù)使用,但構(gòu)造函數(shù)也未更改,則它將變成常規(guī)的可調(diào)用函數(shù)。
現(xiàn)在,使用Solidity的現(xiàn)代版本,您可以使用Constructor關(guān)鍵字定義構(gòu)造函數(shù),從而有效棄用此漏洞。 因此,解決此問題的方法只是使用現(xiàn)代的Solidity編譯器版本。
隱藏狀態(tài)變量
在Solidity中可以兩次使用相同的變量,但可能會導(dǎo)致意外的副作用。 對于使用多個智能合約,這尤其困難。 請看以下示例:
contract SuperContract {
uint a = 1;
}
contract SubContract is SuperContract {
uint a = 2;
}
在這里,我們可以看到SubContract繼承了SuperContract,并且變量a被兩次定義為不同的值。 現(xiàn)在,假設(shè)我們使用a在SubContract中執(zhí)行某些功能。 由于已修改a的值,因此從SuperContract繼承的功能將不再起作用。
為避免此漏洞,重要的是我們檢查整個智能合約系統(tǒng)是否存在歧義。檢查編譯器警告也很重要,因為只要它們在智能合約中,它們就可以標(biāo)記這些歧義。
區(qū)塊鏈屬性的隨機性來源較弱
在以太坊中,某些應(yīng)用程序依賴于隨機數(shù)的生成來保持公平。但是在以太坊中,隨機數(shù)的生成非常困難,并且有一些陷阱值得考慮。
使用諸如block.timestamp,blockhash和block.difficulty之類的鏈屬性似乎是個好主意,因為它們通常會產(chǎn)生偽隨機值。然而,問題在于礦工修改這些值的能力。 例如在具有數(shù)百萬美元大獎的賭博應(yīng)用中,礦工有足夠的動力去生成許多替代區(qū)塊,只選擇會導(dǎo)致礦工中獎的區(qū)塊。當(dāng)然,要像這樣控制區(qū)塊鏈會付出巨大的代價,但是如果賭注足夠高,那肯定可以做到。
為了避免在隨機數(shù)生成中操縱礦工,有一些解決方案:
1. 承諾方案,例如RANDAO,DAO,其中隨機數(shù)由DAO中的所有參與者生成。
2. 通過oracle的外部來源-例如Oraclize。
3. 使用比特幣區(qū)塊哈希,因為網(wǎng)絡(luò)更加分散,區(qū)塊的開采成本更高。
缺少針對簽名重放攻擊的保護
有時在智能合約中,有必要執(zhí)行簽名驗證以提高可用性和氣體的成本。但是在實施簽名驗證時需要考慮。
為了防止簽名重放攻擊,智能合約應(yīng)僅允許處理新的哈希。這樣可以防止惡意用戶多次重播另一個用戶的簽名。
為了更加安全地進行簽名驗證,請遵循以下建議:
· 存儲智能合約處理的每個消息哈希,然后在執(zhí)行功能之前對照現(xiàn)有哈希檢查消息哈希。
· 在哈希中包括合同的地址,以確保消息僅在單個合同中使用。
· 切勿生成包含簽名的消息哈希。
違反條例
equire()方法用于驗證條件,例如輸入或智能合約狀態(tài)變量,或驗證來自外部智能合約調(diào)用的返回值。 為了驗證外部調(diào)用,可以由調(diào)用者提供輸入,也可以由被調(diào)用返回輸入。如果被調(diào)用方的返回值發(fā)生輸入沖突,則可能是以下兩種情況之一出了問題:
· 提供輸入的合同中有一個bug。
· 要求條件太強。
要解決此問題,首先要考慮需求條件是否太強。如有必要,請減弱它以允許任何有效的外部輸入。如果問題不是必需條件,則智能合約中必須有提供外部輸入的錯誤。確保此智能合約未提供無效輸入。
寫入任意存儲位置
只有授權(quán)地址才能訪問敏感存儲位置。如果整個智能合約中沒有適當(dāng)?shù)氖跈?quán)檢查,則惡意用戶可能會覆蓋敏感數(shù)據(jù)。但是即使存在用于寫入敏感數(shù)據(jù)的授權(quán)檢查,攻擊者仍可能能夠通過不敏感數(shù)據(jù)覆蓋敏感數(shù)據(jù)。 這可能使攻擊者可以覆蓋重要的變量,例如智能合約所有者。
為了防止這種情況的發(fā)生,我們不僅要保護具有授權(quán)要求的敏感數(shù)據(jù)存儲,而且還要確保對一個數(shù)據(jù)結(jié)構(gòu)的寫入不會無意間覆蓋另一數(shù)據(jù)結(jié)構(gòu)的條目。
繼承順序不正確
在Solidity中,可以從多個來源繼承,如果不能正確理解,則可能會引起歧義。這種歧義被稱為鉆石問題:如果兩個基本智能合約具有相同的函數(shù),那么哪個優(yōu)先? 幸運的是,只要開發(fā)人員了解解決方案,Solidity就可以很好地處理此問題。
Solidity為鉆石問題提供的解決方案是使用反向C3線性化。這意味著它將使繼承從右到左線性化,因此繼承的順序很重要。建議從更一般的智能合約開始,再到更具體的智能合約結(jié)束,以避免出現(xiàn)問題。
具有函數(shù)類型變量的任意跳轉(zhuǎn)
Solidity支持函數(shù)類型。這意味著可以將類型為function的變量分配給具有匹配簽名的函數(shù)。然后可以像其他任何函數(shù)一樣從變量中調(diào)用該函數(shù)。用戶不應(yīng)更改函數(shù)變量,但是在某些情況下,這是可能的。
如果智能合約使用某些匯編指令(例如mstore),則攻擊者可能能夠?qū)⒑瘮?shù)變量指向任何其他函數(shù)。這可能使攻擊者能夠破壞智能合約的函數(shù),甚至可能耗盡智能合約資金。
由于內(nèi)聯(lián)匯編是從底層訪問EVM的一種方式,因此它繞過了許多重要的安全功能。 因此,只有在必要且正確理解的情況下,才使用匯編程序。
存在未使用的變量
盡管允許,但最好的做法是避免使用未使用的變量。 未使用的變量會導(dǎo)致一些不同的問題:
· 計算量增加(不必要的氣體消耗)
· 錯誤或數(shù)據(jù)結(jié)構(gòu)錯誤的指示
· 代碼可讀性降低
強烈建議從代碼庫中刪除所有未使用的變量。
意外的以太坊余額
由于始終可以將以太坊發(fā)送到智能合約中(請參閱“強行將以太幣發(fā)送到智能合約”)-如果智能合約具有特定的余額,則很容易受到攻擊。
假設(shè)我們有一個智能合約,如果智能合約中存儲了任何以太坊,則該智能合約將阻止所有函數(shù)執(zhí)行。如果惡意用戶決定通過強行發(fā)送Ether來利用此漏洞,則將引發(fā)DoS,使智能合約無法使用。 因此請勿對智能合約中的以太坊余額使用嚴(yán)格的平等檢查,這一點很重要。
以太坊智能合約代碼始終可以被讀取。即使您的代碼未在Etherscan上進行驗證,攻擊者仍然可以反編譯甚至檢查與它之間的事務(wù)以進行分析。
這里的一個問題示例是猜謎游戲,用戶必須猜測所存儲的私有變量才能贏得合同中的以太坊。當(dāng)然這是極其瑣碎的利用(要點是您不應(yīng)該嘗試,因為它幾乎可以肯定是蜜罐合約,要復(fù)雜得多)。
這里的另一個常見問題是在Oracle調(diào)用中使用未加密的鏈下機密(例如API密鑰)。如果可以確定您的API密鑰,惡意行為者可以簡單地自己使用它或利用其他媒介,例如用盡您允許的API調(diào)用并強迫Oracle返回錯誤頁面,這可能會或可能不會導(dǎo)致問題,具體取決于智能合約的結(jié)構(gòu)。
檢測智能合約中錯誤
有些智能合約不希望其他智能合約與之交互。防止這種情況的常見方法是檢查主叫帳戶中是否存儲了任何代碼。但是智能合約帳戶在構(gòu)建過程中發(fā)起調(diào)用仍不會顯示它們存儲代碼,從而有效地繞過了智能合約檢測。
非封閉區(qū)塊鏈依賴
許多智能合約依賴于在一定時間內(nèi)發(fā)生的調(diào)用,但以太坊可以在相當(dāng)長的時間內(nèi)以相對便宜的價格通過非常高的Gwei交易進行垃圾郵件發(fā)送。
如Fomo3D(倒數(shù)游戲,最后一位投資者贏得了頭獎,但每項投資都增加了倒計時的時間)是由一個用戶贏得的,該用戶在短時間內(nèi)完全阻塞了區(qū)塊鏈,不允許其他人在定時器運行之前進行投資出局,他贏了得了比賽。
如今有許多經(jīng)紀(jì)人賭博合同依靠過去的哈希來提供RNG。在大多數(shù)情況下,這不是可怕的RNG來源,甚至可以解釋256個區(qū)塊后發(fā)生的哈希刪除。但是到那時,他們中的許多人根本就沒有下注。這將使某人可以對許多這些功能相似的智能合約下注,并以一定的結(jié)果作為所有人的贏家,在主持人仍未決的情況下檢查主持人的提交,并且如果不利,只需阻塞區(qū)塊鏈,直到進行修剪即可,得到他們的賭注。
不遵守標(biāo)準(zhǔn)
在智能合約開發(fā)方面,遵循標(biāo)準(zhǔn)很重要。設(shè)置標(biāo)準(zhǔn)是為了防止漏洞,而忽略這些漏洞可能會導(dǎo)致意想不到的后果。
以Binance的原始BNB令牌為例。它以ERC20代幣的形式銷售,但后來指出它實際上不符合ERC-20的原因