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