DeFi 安全攻略:保障您的數字資產安全!

無論是開發DeFi協議還是其他的智能合約應用,在上線到區塊鏈主網前都需要考慮到許多安全因素。很多團隊在審核代碼時只關注Solidity相關的陷阱,但要確保dApp的安全性足夠支撐上線主網,通常還有很多工作要做。了解大多數流行的DeFi安全漏洞可能會為你和你的用戶節省數十億美元并且免除后續的各種煩惱,如預言機攻擊、暴力攻擊和許多其他威脅等。

考慮到這一點,我們將在下文研究有關DeFi安全的十大最佳實踐,這將有助于防止你的應用程序成為攻擊的受害者、避免與用戶的不愉快對話,并能保護和加強你作為一個超級安全的開發者的聲譽。

1、了解重入攻擊

一種常見的DeFi安全攻擊類型是重入攻擊,這也是臭名昭著DAO攻擊的形式。這種情況就是當一個合約在更新自己的狀態之前調用了一個外部合約。

引用Solidity文檔的內容:

“一個合約(A)與另一個合約(B)的任何交互,以及任何ETH的轉賬都會將控制權移交給該合約(B)。這使得B有可能在這個交互完成前回調到A。”

我們來看看一個例子:

// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.6.2<0.9.0;// THIS CONTRACT CONTAINS A BUG - DO NOT USEcontract Fund { /// @dev Mapping of ether shares of the contract. mapping(address => uint) shares; /// Withdraw your share. function withdraw() public { (bool success,) = msg.sender.call(""); if (success) shares[msg.sender] = 0; }}

在這個函數中,我們用msg.sender.call調用另一個賬戶。我們要記住的是,這可能是另一個智能合約!

在(bool success,) = msg.sender.call(“”); 返回之前,被調用的外部合約可以被編碼為再次調用withdraw(提款)函數。這將允許用戶在狀態更新前提取合約中的所有資金。

合約可以有幾個特殊函數,即receive(接收)和fallback(回退)函數。如果你發送ETH到另一個合約,它將自動被路由到receive函數。如果該receive(接收)函數再指向原來的合約,那麼在你有機會將余額更新為0之前,你就可以不斷提款。

讓我們看看這種合約可能是什麼樣子的:

// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.6.2<0.9.0;// THIS CONTRACT IS EVIL - DO NOT USEcontract Steal { receive() external payable { IFundContract(addressOfFundContract).withdraw(); }}

在這個函數中,當你把ETH發送到steal合約后,它將調用receive函數,該函數指向Fund合約。此時,我們還沒有運行shares[msg.sender] = 0,所以合約仍然認為用戶有可以提取的余額。

解決方案:在轉移ETH/通證或調用不受信任的外部合約之前,更新合約的內部狀態

有幾種方法可以做到這一點,從使用互斥鎖到甚至簡單地排序你的函數調用,你只在狀態被更新后才能接觸到外部合約或函數。一種簡單的修復方法是在調用任何外部未知合約之前更新狀態:

// SPDX-License-Identifier: GPL-3.0pragma solidity >=0.6.0<0.9.0;contract Fund { /// @dev Mapping of ether shares of the contract. mapping(address => uint) shares; /// Withdraw your share. function withdraw() public { uint share = shares[msg.sender]; shares[msg.sender] = 0; (bool success,) = msg.sender.call(""); }}

轉移、調用和發送

長期以來,Solidity安全專家建議不要使用上述方法。他們建議不使用call函數,而是使用transfer,像下面這樣:

payable(msg.sender).transfer(shares[msg.sender]);

我們之所以提到這一點,是因為你可能會看到外面有一些相互矛盾的資料,它們的建議與我們的建議相反。此外,你也會聽到send函數。每一個函數都可以用來發送ETH,但都有輕微的差異。

transfer: 最多需要2300個gas,失敗時會拋出一個錯誤

send: 最多需要2300個gas,失敗時返回false

call: 將所有gas轉移到下一個合約,失敗時返回false

transfer和send在很長一段時間內被認為是 “更好 “的做法,因為2300個gas真的只夠發出一個事件或其他無害的操作;接收合約除了發出事件不能回調或做任何惡意操作,因為如果他們嘗試這樣做的話,他們會耗盡gas。

然而,這只是目前的設置,由于不斷變化的基礎設施生態,gas成本在未來可能會發生變化。我們已經看到有EIP改變了不同操作碼的gas成本。這意味著未來可能有一段時間,你可以以低于2300個gas的價格調用一個函數,或者事件的成本將超過2300個gas,這意味著任何現在要發出事件的接收函數會在未來會失敗。

這意味著最好的做法是在調用項目外的任何合約之前更新狀態。另一個可能的緩解措施是對關鍵函數施加一個互斥鎖,例如ReentrancyGuard中的非重入修改器。采用這樣的互斥鎖將阻止交易合約被重入。這實質上是增加了一個“鎖”,所以在合約執行過程中,任何調用合約的人都不能“重新進入”該合約。

重入攻擊的另一個版本是跨函數重入。下面是一個跨函數重入攻擊的例子,為了便于閱讀,使用了transfer函數:

mapping (address => uint) private userBalances;function transfer(address _recipient, uint _amount) { require(userBalances[msg.sender] >= _amount); userBalances[_recipient] += _amount; userBalances[msg.sender] -= _amount;}function withdrawBalance() public { uint amountToWithdraw = userBalances[msg.sender]; msg.sender.transfer(amountToWithdraw); userBalances[msg.sender] = 0;}

有可能在另一個函數完成之前調用一個函數。這應該是一個明確的提醒,在你發送ETH之前一定要先更新狀態。一些協議甚至在他們的函數上添加了互斥鎖,這樣如果另一個函數還沒有返回,這些函數就不能被調用。

除了常見的重入漏洞外,還有一些重入攻擊可以由特定的EIP機制觸發,如ERC777。ERC-777(EIP-777)是建立在ERC-20(EIP-20)之上的以太坊代幣標準。它向后兼容ERC-20并增加了一個功能,使“運營商”能夠代表通證所有者發送通證。關鍵是該協議還允許為通證所有者添加“send/receive鉤子”,以便在發送/接收交易時自動采取進一步行動。

從Uniswap imBTC黑客事件中可以看出,該漏洞實際上是由Uniswap交易所在余額變化之前發送ETH造成的。在那次攻擊中,Uniswap功能的實現沒有遵循已被廣泛采用的“Check-Effect-Interact”模式,該模式是為了保護智能合約免受重入攻擊而發明的,按照該模式,通證轉移應該在任何ETH轉移之前進行。

2、使用DEX或AMM儲備作為價格預言機將導致漏洞攻擊

這既是用于攻擊協議的最常見方法之一,也是最容易防止的DeFi安全攻擊面之一。如果你使用getReserves()作為量化價格的方法,這應該是一個警示信號。當用戶操縱訂單簿或基于自動做市商的去中心化交易所(DEX)的現貨價格時,這種集中式價格預言機攻擊就會發生,通常是使用閃電貸。然后使用DEX報告價格作為他們的價格預言機的協議,會導致智能合約的執行出現偏差,其形式包括觸發虛假清算、發放過多的貸款或觸發不公平交易。由于這個漏洞的存在,即使是流行的DEX,如Uniswap,也不建議單獨使用他們的儲備池作為價格預言機。

預言機可以是任何外部實體,它獲取外部數據并將其傳遞到區塊鏈上,或進行某種外部計算并將結果傳遞給智能合約。在基于DEX或AMM的預言機機制的情況下,預言機提取的數據源是由DEX上一次成功交易調整的儲備金價格,它可能會與資產的更廣泛的市場價格不同步,例如,在流動性不足的情況下進行大額交易。這將導致價格與所有交易所的成交量加權平均價格相比,要麼升得很高(大額買單),要麼降得很低(大額賣單)。

閃電貸加劇了這個問題,因為它允許任何用戶在沒有任何抵押的情況下獲得大量的臨時資金,以執行大額交易。用戶經常把問題歸咎于閃電貸,并稱其為“閃電貸攻擊”。然而,根本問題是,DEX本身就是不安全的價格預言機,因為現貨價格很容易被操縱,會導致依賴該預言機的協議參考了不準確的價格。這些攻擊更準確的描述是 “預言機操縱攻擊”,在DeFi生態系統中有大量的此類漏洞。所有的開發者都應該在他們的智能合約中刪除預言機操縱攻擊面。

讓我們看看最近一次攻擊的代碼,這次攻擊造成了3000萬美元的損失,隨后該協議的獎勵通證的價格下跌:

為了便于理解,該函數被稍作修改,但實際上效果是相同的。

function valueOfAsset(address asset, uint amount) public view override returns (uint valueInBNB, uint valueInDAI) { if (keccak256(abi.encodePacked(IProtocolPair(asset).symbol())) == keccak256("Protocol-LP")) { (uint reserve0, uint reserve1, ) = IPancakePair(asset).getReserves(); valueInWETH = amount.mul(reserve0).mul(2).div(IProtocolPair(asset).totalSupply()); valueInDAI = valueInWETH.mul(priceOfETH()).div(1e18); }}

該協議有一個從DEX中獲取現貨價格的預言機設置。在DEX中,用戶可以將一對代幣存入流動性池合約(如通證A+通證B),允許用戶根據匯率在這些通證之間進行交換,匯率由池中每一方的流動性數量計算。假設該協議是安全的,因為其大部分代碼是Uniswap的協議的分叉。然而,在上面添加了一個獎勵通證項目,這樣當用戶將流動性存入特定的資金池時,他們不僅獲得一個收據通證(LP 通證),代表他們對自己流動性的取回憑證和礦池費用的百分比,而且還能獲得流動性挖礦獎勵。黑客能夠操縱這個獎勵的鑄造函數,通過閃電貸,并將這些額外的資金存入流動池。這使他們能夠以錯誤的匯率鑄造獎勵通證。

在這個函數中,我們可以看到,攻擊者做的第一件事就是根據流動池中兩種資產的儲備量,獲得流動性池中資產之間的匯率。下面這行代碼被調用以獲得流動性池中的儲備:

(uint reserve0, uint reserve1, ) = IProtocolPair(asset).getReserves();

你可以想象一個有5個WETH和10個DAI的流動性池子會使reserve0為5,reserve1為10。WETH代表“封裝的ETH”,它是ETH的ERC20版本,ETH和WETH之間的匯率為1比1。

一旦你有了協議中的儲備量,獲取兩種資產的價格的簡單方法就是將兩種儲備量相除,得到一個匯率。例如,如果我們的流動資金池中有5個WETH和10個DAI,那麼兌換率是1個WETH兌換2個DAI,因為我們只是用10除以5。

雖然使用去中心化的交易所可以很好地交換具有即時流動性的資產,但它們并不是很好的現貨價格預言機,因為它們的價格很容易被操縱,特別是通過閃電貸,而且DEX只占任何特定資產的總交易量的一小部分。當用于鑄造獎勵通證時,智能合約的執行很容易變得不準確(為便于理解稍作修改)。

// ProtocolMinterV2.sol 0x819eea71d3f93bb604816f1797d4828c90219b5dfunction mintReward(address asset /* LP token */, uint _withdrawalFee /* 0 */, uint _performanceFee /* 0.00015... */, address to /* attacker */, uint) external payable override onlyMinter { uint feeSum = _performanceFee.add(_withdrawalFee); _transferAsset(asset, feeSum); // transfers LP tokens from VaultFlipToFlip to this uint protocolETHAmount = _zapAssetsToProtoclETH(asset, feeSum, true); if (protocolETHAmount == 0) return; IEIP20(PROTOCOL_ETH).safeTransfer(PROTOCOL_POOL, protocolETHAmount); IStakingRewards(PROTOCOL_POOL).notifyRewardAmount(protocolETHAmount); (uint valueInETH,) = priceCalculator.valueOfAsset(PROTOCOL_ETH, protocolETHAmount); // returns inflated value uint contribution = valueInETH.mul(_performanceFee).div(feeSum); uint mintReward = amountRewardToMint(contribution);  _mint(mintReward, to); // mints the reward to the liquidity providers and attacks}

在這個例子中,向用戶付款的主要函數是_mint(mintReward, to); 這行。我們可以看到,該函數是根據用戶在流動池中鎖定的價值多少來鑄造的。因此,如果一個用戶突然在流動池中擁有大量的資產(由于閃電貸的攻擊),那麼該用戶可以很容易地給自己鑄造大量的獎勵通證,這是從該通證的用戶那里偷取獎勵。

然而,這仍然不會給他們帶來他們想要的利潤。而當通證價格預言機被操縱時,他們能得到的通證數量會大大增加。比如說,該協議認為它將給用戶5美元的獎勵–但實際它將發出5000美元的獎勵。這正是這個特定漏洞所發生的情況。

在這種設置下,用戶可以很容易地進行閃電貸,將該臨時資金存入流動池的一方,鑄造大量的獎勵,然后償還閃電貸,犧牲其他流動性提供者以獲利。

為了避免閃電貸市場操縱問題,一種常被提起的解決方案是采取DEX市場的時間加權平均價格(TWAP)(例如,一個資產在一小時內的平均價格)。雖然這可以防止閃電貸歪曲預言機價格,因為閃電貸只存在于一個交易/區塊中,而TWAP是多個區塊的平均值,但這并不是一個完整的解決方案,因為TWAP有其自身的權衡。在波動時期,TWAP預言機會變得不準確,這可能會導致下游事件,如無法在足夠的時間內清償抵押不足的貸款。此外,TWAP預言機不能提供足夠的市場覆蓋,因為只有一個DEX被跟蹤,使其容易受到不同交易所的流動性/交易量變化的影響,使TWAP預言機給出的價格出現偏差。

解決方案:使用一個去中心化的預言機網絡

DeFi安全最佳實踐不是使用一個中心化的預言機(如一個單一的鏈上交易所)來確定匯率,而是使用一個去中心化的預言機網絡來尋找反映廣泛市場覆蓋的匯率的真實數值。DEX作為交易所是去中心化的,但作為價格參考信息它是中心化的。

相反,你要收集所有中心化和去中心化交易所的價格,按交易量加權并去除異常值,能夠獲得相關資產的全球匯率的去中心化且準確的視圖,這能確保全面的市場覆蓋。如果你有代表所有交易環境的成交量加權的全球平均值的資產價格,如果閃電貸操縱了單一交易所的資產價格,那也就無所謂了。

此外,由于閃電貸只存在于單個交易中(同步),它們對去中心化的Price Feed沒有影響,這些Price Feed在單獨的事務中生成具有廣泛市場范圍定價的預言機更新(異步更新)。Chainlink預言機網絡的去中心化架構和它們實現的廣泛的市場覆蓋保護了DeFi協議免受閃電貸資助的市場操縱,這就是為什麼越來越多的DeFi項目正在整合Chainlink Price Feed以防止價格預言機的攻擊,并確保在突然的交易量變化中準確定價。

你可以不用getReserves來計算價格,而是從Chainlink Data Feed獲得轉換率,這是去中心化的預言機網絡在鏈上提供反映所有相關CEX和DEX的成交量加權平均價格(VWAP)。

pragma solidity ^0.6.7;import "@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol";contract PriceConsumerV3 { AggregatorV3Interface internal priceFeed; /** * Network: Kovan * Aggregator: ETH/USD * Address: 0x9326BFA02ADD2366b30bacB125260Af641031331 */ constructor() public { priceFeed = AggregatorV3Interface(0x9326BFA02ADD2366b30bacB125260Af641031331); } /** * Returns the latest price */ function getThePrice() public view returns (int) { ( uint80 roundID,  int price, uint startedAt, uint timeStamp, uint80 answeredInRound ) = priceFeed.latestRoundData(); return price; }}

上面的代碼是實現訪問Chainlink價格預言機的全部內容,你可以閱讀文檔,開始在你的應用程序中實現它們。如果你是智能合約或預言機的新手,我們有一個初學者教程幫助你開始使用,并保護你的協議和用戶免受閃電貸和預言機操縱攻擊。

如果你想了解更多并實際體驗,可以玩一下OpenZeppelin的DEX Ethernaut關卡,它顯示了操縱DEX的現貨價格是多麼容易。

3、不要使用Keccak256或Blockhash作為隨機性的來源

使用block.difficulty、block.timestamp、blockhash或任何與block相關的東西來獲得一個隨機數到你的應用程序中,都會使你的代碼被攻擊。智能合約中的隨機性對許多用例都是有用的,比如在無偏見的情況下確定獎勵的贏家,或者公平地將一個罕見的NFT分配給用戶。然而,區塊鏈是確定的系統,不能提供隨機數的防篡改來源,所以試圖從鏈上獲得一個隨機數總是會出現問題,并有可能導致被漏洞利用。隨機數漏洞并不像預言機操縱攻擊或重入攻擊那樣普遍,但它們在Solidity教學資料中出現的頻率令人震驚。很多教育內容會教導區塊鏈開發者用下面這樣的代碼獲得一個隨機數:

uint randomNumber = uint(keccak256(abi.encodePacked(nonce, msg.sender, block.difficulty, block.timestamp))) % totalSize;

這里的想法是使用nonce、區塊難度和時間戳的某種組合來創建一個“隨機”數字。然而這有幾個明顯的缺點。

實際上,你可以用取消交易不斷地“回滾”,直到你得到一個你喜歡的隨機數字。這對任何人來說都非常容易做到。

使用對block.difficity這樣對象的哈希值(或者鏈上的任何其他東西)作為隨機數時,礦工對結果有巨大的影響力。與“回滾”策略類似,如果結果對礦工不利,礦工可以利用他們交易排序的能力,將某些交易從區塊中排除。如果這是用于隨機性的鏈上數據的來源,礦工也可以選擇扣留對他們不利的區塊哈希的區塊。

使用block.timestamp這樣的東西則無隨機性,因為時間戳是任何人都可以預測的。

這種方式的鏈上隨機數生成器,用戶和/或礦工都能對“隨機”數字產生影響和控制。如果你想擁有的是一個公平系統,這種方式的隨機性只會極度有利于惡意行為者。隨著被隨機性功能保障的價值的增加,這個問題會變得更糟,因為攻擊它的動機也在增加。

解決方案:使用Chainlink VRF作為一個可驗證的隨機性預言機

為了防止漏洞,開發者需要一種方法來創建可驗證的隨機性,并防止礦工和回滾用戶的篡改。所需要的是來自于預言機的鏈外隨機性。然而,許多提供隨機性來源的預言機沒有辦法真正證明他們提供的數字確實是隨機產生的(被操縱的隨機性看起來就像正常的隨機性,你無法區分)。開發者需要能夠從鏈外獲取隨機性,同時也要有辦法明確地、并且可通過密碼學證明隨機性沒有被操縱。

Chainlink可驗證隨機函數(VRF)正是實現了這一點。它使用預言機節點在鏈外生成一個隨機數,并提供該數字的完整性的加密證明。然后由VRF協調器在鏈上檢查該加密證明,以驗證VRF的完整性是確定且防篡改的。它的工作流程如下:

一個用戶從Chainlink節點請求一個隨機數,并提供一個種子值(使用最新的VRF不需要用戶提供種子值)。這會發出一個鏈上事件日志。

鏈外的Chainlink預言機讀取該日志并使用可驗證的隨機函數(VRF)創建一個隨機數和密碼學證明,依據的是節點的keyhash、用戶給定的種子和請求時未知的區塊數據。然后,它在第二筆交易中把隨機數返回鏈上,并在鏈上通過VRF協調器合約使用該密碼學證明驗證此隨機數。

Chainlink VRF是如何解決上述問題的呢?

你不能回滾攻擊

由于這個過程需要兩筆交易,第二筆交易是創建隨機數的地方,你無法看到隨機數或取消你的交易。

礦工沒有影響力

由于Chainlink VRF不使用礦工可以控制的值,如block.difficulty或block.timestamp等可預測的值,所以他們無法控制隨機數。

用戶、預言機節點或dApp開發者無法操縱Chainlink VRF提供的隨機性數值,這就使得Chainlink VRF提供的隨機性數值是智能合約應用程序可用的極其安全的鏈上隨機性來源。

你可以按照文檔的要求開始在你的代碼中實現Chainlink VRF,或者按照我們的初學者指南來使用Chainlink VRF,其中包括一個視頻教程。

4、避免常見故障

這一點是對Solidity的一個概括,但要有一個安全的合約,你需要在構建它時將所有的DeFi安全原則銘記于心。要寫出真正可靠的Solidity代碼,你必須知道它在底層是如何工作的。否則,你可能會受到影響:

上溢出/下溢出

在Solidity中,uint256和int256是“封裝”的。這就是說,如果你有一個uint256能表示的最大的數字,然后再對它加1,它將會得到它能表示的最小的數字。請務必檢查這一點。在0.8之前的Solidity版本中,你會使用類似safemath的東西。

在Solidity 0.8.x中,算術運算被默認檢查。這意味著x + y在溢出時將拋出一個異常。所以請確保你知道你使用的是什麼版本!

循環的gas限制

當編寫動態大小的循環時,需要非常小心它們能有多大。一個循環可以很容易地超過區塊的最大gas限制,并在恢復時使得合約無用。

避免使用tx.origin

tx.origin不應該被用于智能合約的授權,因為它可能會導致類似釣魚的攻擊。

代理存儲碰撞

對于一個采用代理實現模式的項目,實現合約可以通過改變代理合約中的實現合約地址來更新。

通常,在代理合約中,有一個特定的變量來存儲實現合約的地址。如果這個變量的存儲位置是固定的,而恰好有另一個變量在實現合約中的存儲位置有相同的索引/偏移,那麼就會出現存儲碰撞。

pragma solidity 0.8.1;contract Implementation { address public myAddress; uint public myUint; function setAddress(address _address) public { myAddress = _address; }}contract Proxy { address public otherContractAddress; constructor(address _otherContract) { otherContractAddress = _otherContract; } function setOtherAddress(address _otherContract) public { otherContractAddress = _otherContract; } fallback() external { address _impl = otherContractAddress; assembly { let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize()) let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0) let size := returndatasize() returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } default { return(ptr, size) } } }}

為了觸發存儲碰撞,可以在Remix中遵循下面這些步驟:

部署實現合約;

部署代理合約,將實現合約的部署地址作為其構造函數參數;

在代理合約的部署地址上運行實現合約;

調用myAddress()函數。它將返回一個非零的地址,這就是存儲在代理合約中的otherContractAddress變量中的部署地址。

那麼,在上面的四個步驟中發生了什麼呢?

首先實施合約被部署,生成了合約地址;

代理合約部署時用到實現合約的部署地址,其中代理合約的構造器被調用,otherContractAddress變量用實現合約的部署地址賦值;

在步驟3中,實現合約與代理存儲進行交互,即在部署的實現合約中的變量可以讀取部署的代理合約中相應的哈希碰撞變量的值。

十大DeFi安全最佳實踐

myAddress可以通過碰撞讀取otherContractAddress的

4.myAddress()函數的返回值只是部署的實現合約中的myAddress變量的值,它與部署的代理合約中的otherContractAddress變量相碰撞,可以在那里獲得otherContractAddress變量的值。

為了避免代理存儲碰撞,我們建議開發者為存儲變量選擇偽隨機槽來實現非結構化的存儲代理。

一種常見的做法是為項目采用一個可靠的代理模式。最廣泛采用的代理模式是通用可升級代理標準(UUPS)和透明代理模式。它們都提供了具體的存儲偏移offset,以避免在代理合約和實現合約中使用相同的存儲槽。

下面是一個使用透明代理模式實現隨機存儲的例子:

bytes32 private constant implementationPosition = bytes32(uint256( keccak256('eip1967.proxy.implementation')) - 1));

通證轉移計算的準確性

通常情況下,對于一個普通的ERC20通證,收到的通證數量應該等于用函數調用的原始數量;例如,下面的函數retrieveTokens()。

function retrieveTokens(address sender, uint256 amount) public { token.transferFrom(sender, address(this), amount); totalTokenTransferred += amount;}

然而如果通證是通縮的,即每次轉讓都有費用,那麼實際收到的通證數量將少于最初要求轉讓的通證數量。

在下面修改后的函數retrieveTokens(address sender, uint256 amount)中,金額是根據轉移操作前后的余額重新計算的。無論通證轉移機制如何,這都能準確地計算出已經轉移到address(this)的通證數量。

function retrieveTokens(address sender, uint256 amount) public { uint256 balanceBefore = deflationaryToken.balanceOf(address(this)); deflationaryToken.transferFrom(sender, address(this), amount); uint256 balanceAfter = deflationaryToken.balanceOf(address(this)); amount = balanceAfter.sub(balanceBefore); totalTokenTransferred += amount;}

正確的數據刪除

有很多情況下需要刪除合約中不再需要的某個對象或值。在像Java這樣的成熟語言中,有一個垃圾回收機制,可以自動和安全地處理這個問題。然而在Solidity中,開發者必須手動處理“垃圾”。因此,不正確地處理垃圾可能給智能合約帶來安全問題。

例如,當用delete刪除數組中的一個元素時,即delete array[member],array[member]仍然存在,但會根據array[member]的類型重置為一個默認值。開發者應該記得跳過這個元素或者重新組織數組并減少其長度。比如說:

array[member] = array[array.length - 1]; array.pop()

這些只是需要注意的一些漏洞,但深入了解Solidity將幫助你避免這些“麻煩”。你可以查看審計工程師Sigma Prime關于常見Solidity漏洞的文章:

https://blog.sigmaprime.io/solidity-security.html

5、函數的可見性和限制

在Solidity語言的設計中,有四種類型的函數可見性:

private:該函數只在當前合約中可見;

internal:該函數在當前合約和派生合約中是可見的;

external:該函數只對外部調用可見;

public:該函數對內部和外部調用都是可見的。

函數可見性是指上述四種可見性中的一種,用于限制某組用戶的訪問。至于限制,它指的是專門為訪問限制目的而編寫的自定義代碼段。

可見性和限制可以結合起來,為特定的功能設置一個適當的訪問授權。例如,在ERC20實現的函數_mint()中:

function _mint(address account, uint256 amount) internal virtual { require(account != address(0), "ERC20: mint to the zero address"); _beforeTokenTransfer(address(0), account, amount); _totalSupply += amount; _balances[account] += amount; emit Transfer(address(0), account, amount); _afterTokenTransfer(address(0), account, amount);}

函數_mint()的可見性被設置為internal,這正確地保護了它不能被外部調用。為了給mint函數設置一個適當的訪問授權,可以使用下面的代碼片段:

function mint(address account, uint256 amount) public onlyOwner { _mint(account, amount); require(MaxTotalSupply >= _totalSupply, "over mint");}

函數mint()只允許合約的所有者進行鑄造,require()語句防止所有者鑄造過多的通證。

正確使用可見性和限制有利于合約管理。也就是說,一方面缺乏這樣的設置可能會讓惡意攻擊者調用管理配置功能來操縱項目,另一方面,過度的限制設置可能會給合約帶來中心化的擔憂,也可能會引起社區的質疑。

6、在部署到主網前做好外部審計

可以將代碼審計視為以安全為中心的同行評審。審計員將逐行檢查你的整個代碼庫,并使用形式化驗證技術來檢查你的智能合約是否存在任何漏洞。在沒有審計的情況下部署代碼或在審計后更改代碼并重新部署是會讓合約暴露于潛在漏洞的威脅。

有多種方法可以幫助你和審計員確保代碼審計盡可能全面:

使用文檔記錄所有內容,以便他們更輕松地跟蹤正在發生的事情

與他們保持溝通渠道暢通,以防他們有任何疑問

在你的代碼中添加注釋,會讓你的團隊和他們的團隊更容易

然而,不要依賴審計人員來捕捉一切問題。你應該首先有一個安全心智模型,因為在未來某一天,如果你的協議被黑客攻擊,你仍然會是那個最終負責的團隊。安全審計不一定能解決所有問題,但它們確實提供了額外的一輪審查,對捕捉你沒有發現的錯誤有一定幫助。

Tincho有一個關于如何最好地與審計工程師合作的很好的推特內容。

如果你想找推薦的審計工程師,請隨時聯系我們的技術專家。

7、進行測試和使用靜態分析工具

你需要對你的應用程序進行測試。人類是偉大的,但他們永遠無法提供自動化測試套件所能提供的代碼覆蓋率。Chainlink的入門套件倉庫有一些測試套件的樣本供你參考作為起點。像Aave和Synthetix這樣的協議也有很好的測試套件,查看他們的代碼了解一些測試的最佳實踐(也包括更通用的編碼實踐)可能是一個好的思路。

靜態分析工具也能幫助你更早地發現錯誤。它們被設計成自動運行你的合約并尋找潛在的漏洞。目前最流行的靜態分析工具之一是Slither。CertiK目前也在根據其在審計、驗證和監控智能合約方面的豐富經驗,建立下一代靜態分析、語法分析、漏洞分析和形式化驗證工具。

8、將安全視為整個生命周期的工作

雖然毫無疑問你應該盡力在產品部署前創建一個安全可靠的智能合約,但現實是區塊鏈和DeFi協議快速發展以及新攻擊的不斷發明意味著你不能止步于此。相反,你應該獲取并跟蹤最新的監測和警報情報。如果可能的話,嘗試在智能合約中引入面向未來的功能,以獲取快速增長的動態安全情報并從中受益。

同時也可以引入一些額外的幫助。CertiK Skynet作為一個24/7的安全智能引擎,為智能合約的鏈上部署提供多維度和實時的透明安全監控。它包括社會情緒、治理、市場波動、安全評估等,為區塊鏈客戶、社區和通證投資者提供一般的安全理解。CertiK安全排行榜提供透明的、易于理解的安全洞察和最新的項目狀態,并提供獎勵改進的社區問責制。

9、制定一個災難恢復計劃

根據你的協議,如果你被黑客攻擊,有一個救助計劃是很好的。一些流行的方法是:

購買保險

添加一個緊急“暫停”功能

有一個升級計劃

保險協議越來越受歡迎,這是是最去中心化的災難恢復方式之一。它們在不影響去中心化的情況下增加了一定程度的財務安全。即使你也有其他災難恢復計劃,你也應該持有保險。一種解決方案是CertiK的ShentuShield,這是一個增加了去中心化和透明度的保險產品。

設置緊急“暫停”功能是一個有利有弊的策略。在發現漏洞的情況下,這種功能會停止與你的智能合約的所有交互。如果你設置了這個功能,你需要確保你的用戶知道誰能夠操作它。如果只有一個用戶擁有權限,這就不是一個去中心化的協議,并且精明的用戶可以通過你的代碼找到答案。要小心你的實現方式,因為你實際上可能最終在一個去中心化的平臺上得到一個中心化的協議。

升級計劃也有同樣的問題。轉移到一個沒有錯誤的智能合約可能很好,但你需要以一種深思熟慮的方式升級你的合約,以免犧牲去中心化。一些安全公司甚至強烈建議不要采用可升級的智能合約模式。你可以在這篇《智能合約升級現狀》的演講中或者Patrick Collins關于這個話題的YouTube視頻中了解更多關于可升級智能合約的話題。

如果你正在尋找一些保險建議,可隨時加入Chainlink Discord。

10、防范搶跑交易

在區塊鏈中,所有的交易在mempool中都是可見的,這意味著每個人都有機會看到你的交易,并有可能在你的交易進行之前進行交易,以便從你的交易中獲利。

例如,假設你使用DEX以當前的市場價格將5個ETH兌換成DAI。一旦你將交易發送到mempool進行處理,一個搶跑者可以在你之前進行交易,購買大量的ETH,導致價格上漲。然后他們可以以更高的價格向你出售他們購買的ETH,并以你為代價獲利。目前,搶跑機器人在區塊鏈世界中橫行,并以犧牲普通用戶的利益為代價獲利。這個術語來自于傳統的金融世界,其中交易員試圖做完全相同的事情,只是涉及的是股票、商品、衍生品和其他金融資產和相應的工具。

另一個例子,下面列出的函數有很高的被搶跑的風險。根據修改器initializer,該函數只能被調用一次。如果調用initialize()函數的交易被攻擊者在mempool中監控,那麼攻擊者就可以用一組定制的通證(token)、分銷商(distributor)和工廠(factory)的參數來復制該交易,并最終控制整個合約。由于函數initialize()只能被調用一次,合約所有者沒有辦法防御或減輕這種攻擊。

function initialize(IERC20 _token, IDistributor _distributor, IFactory _factory) public initializer { Ownable.initialize(); token = _token; distributor = _distributor; factory = _factory;}

這通常也與所謂的礦工可提取價值,即MEV有關。MEV是指礦工或機器人對交易進行重新排序,以便他們能以某種方式從排序中獲利。就像搶跑者支付更多的gas以使他們的交易領先于你的交易一樣,礦工可以直接重新排序交易,使他們的交易領先于你的交易。在整個區塊鏈生態系統中,MEV每天從普通用戶那里竊取數百萬美元。

幸運的是,包括Chainlink實驗室首席科學家Ari Juels在內的一群世界級智能合約和密碼學研究人員正在努力解決這個確切的問題,其解決方案名為“公平排序服務”(FSS)。

開發中的解決方案:Chainlink公平排序服務(FSS)

Chainlink 2.0白皮書概述了公平排序服務的主要特點,這是一項由Chainlink去中心化預言機網絡(DON)提供的安全的鏈外服務,將用于根據dApp陳述的公平性的時間概念對交易進行排序(例如首次在mempool中看到)。FSS旨在極大地緩解搶跑交易和MEV的影響,并為整個區塊鏈生態系統的用戶減少交易費用。你可以在這篇介紹性的文章中閱讀更多關于FSS的內容,并在Chainlink 2.0白皮書的第五節中查看擴展內容。

除了FSS之外,緩解搶跑問題的最好方法之一是盡可能降低交易排序的重要性,從而抑制交易重新排序和MEV在你的協議中的作用。

總結和下一步工作

在保護你的智能合約時,有許多關鍵的DeFi安全因素需要考慮,我們已經看到了太多的漏洞和攻擊使用戶損失了數千萬美元。掌握上面的提示將幫助你在構建智能合約時避免這些漏洞。然而,永遠不會有一個列表涵蓋每一個獨特的漏洞。我們會繼續看到圍繞中心化機制的新的和復雜形式的經濟漏洞,以及圍繞脆弱的抵押品的閃電貸資助的市場操縱。DeFi社區必須共同努力,在整個生態系統中發現并減輕這些新出現的風險。

發文者:鏈站長,轉載請註明出處:https://www.jmb-bio.com/4158.html

讚! (0)
Donate 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
Previous 2023 年 2 月 28 日 下午 3:12
Next 2023 年 2 月 28 日 下午 3:20

相關文章

  • PolyYeld Finance 被攻擊事件全解析:代幣 YELD 價格跳水歸零!

    北京時間7月28日,安全公司Rugdoc在推特表示,收益耕作協議PolyYeld Finance遭到攻擊,所有者已宣布合約已被利用并鑄造了大量YELD代幣。CoinGeckko行情顯示,YELD代幣價格直線跳水歸零,狂跌100%。 事件概覽 攻擊如何發生 Event overview PolyYeld Finance 是 Polygon 網絡上的下一代產量農…

    2023 年 2 月 28 日
  • Vitalik Buterin:乙太坊的設計理念(一)

    盡管以太坊的許多理念在早先的密碼學貨幣(如比特幣)上已經運用并測試了5年之久,但從某些協議功能的處理方法上來說,以太坊與常見方式仍有許多不同。而且,以太坊可用于開發全新的經濟工具,因為它具有其他系統不具備的許多功能。本文會詳細描述以太坊所有的潛在優點,以及在構建以太坊協議過程中某些有爭議的地方。另外,也會指出我們的方案及替代方案的潛在風險。 原則 以太坊協議…

    2023 年 2 月 28 日
  • NFT 資產安全問題:白帽駭客 samczsun 警告攻擊頻繁!

    注:原文作者是擁有“審計上帝”之稱的白帽黑客samczsun,同時他也是Paradigm的研究合伙人,其最近出手拯救了BitDAO MISO荷蘭拍賣資金池中的3.5億美元資產,而在這篇文章中,他提醒了關于NFT代幣標準的潛在安全風險,他還預測稱,隨著ERC-721和ERC-1155代幣標準變得越來越流行,針對NFT的攻擊很可能會越來越頻繁。 如果你從事軟件工…

    2023 年 2 月 28 日
  • 閃電網路應用開發速成指南,掌握 Lightning Network!

    如果你想要入門閃電網絡編程,又不在乎所謂正確、最佳的做法,這篇文很適合你。換言之,這篇指南具有很強的 主觀性。如果你偏好 O’Reilly 式的權威文本,這篇文章可能不對你的胃口。但是,如果你想要了解閃電網絡的最小開發環境及其基本內容,不妨讀一讀。我寫本文的目的是提煉出創建閃電網絡應用的基礎知識點,讓新手能夠快速構建和實驗。 我相信,只…

    2023 年 2 月 28 日
  • 乙太坊的設計理念詳解:探索乙太坊的發展歷程和設計理念!

    叔塊(uncle blocks)獎勵 GHOST 協議是一項不起的創新,由 Yonatan Sompolinsky 和 Aviv Zohar 在 2013 年 10 月首次提出的。它是解決快速出塊伴生問題的第一個認真嘗試。 GHOST 的用意是解決這樣一個難題:更短的出塊時間(因此確認速度會更快)會導致有更多區塊 “過時” 因而安全性會下降 —— 因為區塊在…

    2023 年 2 月 28 日
每日鏈頭條給你最新幣圈相關資訊!