揭开闪电贷的神秘面纱
摘要: “空手套白狼”?闪电贷,到底是什么?
闪电贷最初为开发者设计,所以闪电贷业务只能由智能合约实现,虽然现在有些如 Furucombo 等平台已经省去了写代码的复杂流程,但是想要清楚理解闪电贷原理我们需要从底层代码开始。
本文从技术角度带你了解到什么是闪电贷和闪电贷流程。
什么是闪电贷
闪电贷最早是由 Marble 协议引入的概念,当时是为了让用户可以在以太坊借用 Ether 和 ERC-20 代币,后来由 Aave、dYdX 等进行了普及。
简单来说,闪电贷就是在一个区块交易中,同时完成借款和还款操作这两个操作,无需抵押任何资产,只需支付手续费即可。
用户在借到款之后,可以利用借到的资产进行其他操作,然后在交易结束的时候,用户只要把借到的款项及手续费及时归还就可以,否则该笔交易就会回滚,就像什么也没有发生过一样。
当用户使用闪电贷借款后可以使用其资金进行套利、抵押物互换、自我清算等操作。
但是由于闪电贷能以低风险,低投入的方式快速借到大量资金,很多黑客便利用智能合约中的漏洞,用极低的成本,从闪电贷协议借得大量资金,用于攻击操作。
近段时间闪电贷攻击事件频繁发生,这也不得不让开发者在合约开发的安全性上引起更多的注意。
常见的闪电贷方式
目前市面上主流的几种提供闪电贷业务的 DeFi 项目,主要有 Aave、Uniswap、dYdX 等。
-
Aave闪电贷
Aave是最早将闪电贷进行实际应用的DeFi 协议,主要原理是通过调用 Aave 中 lendingPool 合约中的 flashLoan() 函数实现借贷,通过executeOperation() 函数实现具体的业务逻辑,下文将进行具体讲解。该闪电贷在实际借贷操作中有详细的官方文档,较为方便,对新手学习较友好。
特点
-手续费 0.09%
-代币种类多(ETH,USDC,DAI,MAKER,REP,BAT,TUSD,USDT... ),简短代码,详细文档支持
-
Uniswap 闪电贷
Uniswap 作为 DeFi 中最受欢迎的去中心化交易所之一,从V2版本开始支持闪电贷(Flash Swaps)功能,该功能通过调用UniswapV2pair 合约实例的 swap 方法带入额外 data 实现,和 Aave 一样也有着较为详细的官方文档供借贷者参考。
特点
-手续费 0.3%
-代币种类多,使用闪电贷可直接在 Uniswap 上交易,可以直接借 ETH,详细文档支持
-
dYdX 闪电贷
dYdX 是一个针对专业交易者的去中心化交易所,本身并没有闪电贷功能,但是可以通过对 SoloMargin 合约执行一系列操作来实现类似闪电贷功能。其主要原理是通过继承 DydxFlashloanBase 合约编写initiateFlashLoan() 回调 callFunction() 实现借贷、套利、还款等操作。但没有提供官方文档去参考,只能通过第三方网站找到较为详细的参考资料,对新手借贷难度较大。
特点
-手续费 2 Wei
-手续费较低,代币种类较少,不能直接借 ETH,操作较复杂
AAVE闪电贷原理
这里我们将通过 Aave 闪电贷为例分析闪电贷原理。
借贷方法
在分析aave
闪电贷源码前我们首先来看一个调用闪电贷的方法。
function flashloan(address _asset) public {
bytes memory data = "";
uint amount = 1 ether;
ILendingPool lendingPool = ILendingPool(addressesProvider.getLendingPool());
lendingPool.flashLoan(address(this), _asset, amount, data);
}
-
该函数首先接受地址传参
_asset
; -
将
data
信息赋值为空 ; -
将
amount
赋值为 1 ether ; -
获取
lendingPool
地址 ; -
调用
lendingPool
地址的flashLoan
函数并将依次 该合约地址、传参地址、amount
和data
作为调用信息。
可以看到该闪电贷调用方法只需要用户传入一个地址信息就能启动闪电贷,其他信息已经配置好了。
flashLoan 函数
那么这些参数有什么具体含义,我们接着 flashLoan
函数具体分析。
function flashLoan(address _receiver, address _reserve, uint256 _amount, bytes memory _params)
public
nonReentrant
onlyActiveReserve(_reserve)
onlyAmountGreaterThanZero(_amount)
{
//check that the reserve has enough available liquidity
//we avoid using the getAvailableLiquidity() function in LendingPoolCore to save gas
uint256 availableLiquidityBefore = _reserve == EthAddressLib.ethAddress()
? address(core).balance
: IERC20(_reserve).balanceOf(address(core));
require(
availableLiquidityBefore >= _amount,
"There is not enough liquidity available to borrow"
);
(uint256 totalFeeBips, uint256 protocolFeeBips) = parametersProvider
.getFlashLoanFeesInBips();
//calculate amount fee
uint256 amountFee = _amount.mul(totalFeeBips).div(10000);
//protocol fee is the part of the amountFee reserved for the protocol - the rest goes to depositors
uint256 protocolFee = amountFee.mul(protocolFeeBips).div(10000);
require(
amountFee > 0 && protocolFee > 0,
"The requested amount is too small for a flashLoan."
);
//get the FlashLoanReceiver instance
IFlashLoanReceiver receiver = IFlashLoanReceiver(_receiver);
address payable userPayable = address(uint160(_receiver));
//transfer funds to the receiver
core.transferToUser(_reserve, userPayable, _amount);
//execute action of the receiver
receiver.executeOperation(_reserve, _amount, amountFee, _params);
//check that the actual balance of the core contract includes the returned amount
uint256 availableLiquidityAfter = _reserve == EthAddressLib.ethAddress()
? address(core).balance
: IERC20(_reserve).balanceOf(address(core));
require(
availableLiquidityAfter == availableLiquidityBefore.add(amountFee),
"The actual balance of the protocol is inconsistent"
);
core.updateStateOnFlashLoan(
_reserve,
availableLiquidityBefore,
amountFee.sub(protocolFee),
protocolFee
);
//solium-disable-next-line
emit FlashLoan(_receiver, _reserve, _amount, amountFee, protocolFee, block.timestamp);
}
由于代码过长为方便理解我们就具体分析该函数实现了哪些功能:
1.验证地址 _reserve
余额是否小于 _amount
, 如果是就回滚初始状态,不是继续后续操作;
uint256 availableLiquidityBefore = _reserve == EthAddressLib.ethAddress()
? address(core).balance
: IERC20(_reserve).balanceOf(address(core));
require(
availableLiquidityBefore >= _amount,
"There is not enough liquidity available to borrow"
);
2.计算手续费(gas 费+协议费),验证 gas 费和协议费是否大于 0,是则继续后续操作,否则回滚初始状态;
(uint256 totalFeeBips, uint256 protocolFeeBips) = parametersProvider
.getFlashLoanFeesInBips();
//calculate amount fee
uint256 amountFee = _amount.mul(totalFeeBips).div(10000);
//protocol fee is the part of the amountFee reserved for the protocol - the rest goes to depositors
uint256 protocolFee = amountFee.mul(protocolFeeBips).div(10000);
require(
amountFee > 0 && protocolFee > 0,
"The requested amount is too small for a flashLoan."
);
3.将地址_receiver
定义为可接受转账地址,并从 _reserve
地址向 _receiver
转账 _amount
值。
//get the FlashLoanReceiver instance
IFlashLoanReceiver receiver = IFlashLoanReceiver(_receiver);
address payable userPayable = address(uint160(_receiver));
//transfer funds to the receiver
core.transferToUser(_reserve, userPayable, _amount);
4.触发 executeOperation
函数,该函数主要用于还款操作后续会讲解。
//execute action of the receiver
receiver.executeOperation(_reserve, _amount, amountFee, _params);
5.还款完成后,进行转账前后余额比较,如果转账前余额与还款后余额加手续费不相等回滚初始状态,相等继续后续操作。
//check that the actual balance of the core contract includes the returned amount
uint256 availableLiquidityAfter = _reserve == EthAddressLib.ethAddress()
? address(core).balance
: IERC20(_reserve).balanceOf(address(core));
require(
availableLiquidityAfter == availableLiquidityBefore.add(amountFee),
"The actual balance of the protocol is inconsistent"
);
6.更新闪电贷进行情况
core.updateStateOnFlashLoan(
_reserve,
availableLiquidityBefore,
amountFee.sub(protocolFee),
protocolFee
);
7.最后触发闪电贷交易完成事件
//solium-disable-next-line
emit FlashLoan(_receiver, _reserve, _amount, amountFee, protocolFee, block.timestamp);
以上就是 flashLoan
函数实现的所有功能,由于 executeOperation
函数实现了还款功能十分重要,以下接着分析 executeOperation
函数如何实现还款功能。
executeOperation函数
function executeOperation(
address _reserve, uint256 _amount,
uint256 _fee, bytes calldata _params
)
external override
{
require(_amount <= getBalanceInternal(address(this), _reserve), "Invalid balance, was the flashLoan successful?");
//
// Your logic goes here. 业务逻辑实现
// !! Ensure that *this contract* has enough of `_reserve` funds to payback the `_fee` !!
//
uint totalDebt = _amount.add(_fee);
transferFundsBackToPoolInternal(_reserve, totalDebt);
}
1.在 flashLoan
函数进行有效转账后,executeOperation
函数将被触发并接受来自flashLoan函数的传参。
2.接着验证接收到的金额是否正确,不正确则回滚初始状态。
require(_amount <= getBalanceInternal(address(this), _reserve), "Invalid balance, was the flashLoan successful?");
3.业务逻辑实现,如实现套利操作等。本文仅对借贷原理分析不进行套利逻辑分析。
4.偿还资金,还款金额= 借款金额 + 借款金额的 0.09%。
uint totalDebt = _amount.add(_fee);
5.最后一步就是调用 transferFundsBackToPoolInternal
来偿还闪电贷。
闪电贷具体流程
现在我们可以逆推 address(this), _asset, amount, data 在 flashLoan 函数的调用信息作用了
address(this):闪电贷用户借款地址
_asset:闪电贷借款币种地址
amount:闪电贷用户借款金额
data:附带调用信息
再结合 flashLoan 函数实现的功能我们可以充分了解到闪电贷的具体流程:
第一步
用户调用flashLoan 函数传入参数:用户借款地址、借款币种地址、借款金额、附带调用信息
第二步
flashLoan 函数进行借款金额判断
第三步
flashLoan 函数进行手续费判断
第四步
flashLoan 函数向用户借款地址转账
第五步
executeOperation 函数执行附带调用信息,并进行还款操作
第六步
flashLoan 函数进行转账前与还款后余额比较
第七步
还款完成,更新闪电贷进程
总结
闪电贷作为 DeFi 领域的新兴事物,实现了不同于传统金融的无抵押贷款。攻击事件频发也将闪电贷推上了风口浪尖,智能合约的安全作为一种技术手段,可以成为攻击者的作案工具,也可成为操作者的交易利器。
一边是口诛笔伐的对象,一边是 DeFi 革命的狂热,面对如此创新的技术未来如何发展我们不得而知。
作者:创宇区块链安全实验室;来自链得得内容开放平台“得得号”,本文仅代表作者观点,不代表链得得官方立场凡“得得号”文章,原创性和内容的真实性由投稿人保证,如果稿件因抄袭、作假等行为导致的法律后果,由投稿人本人负责得得号平台发布文章,如有侵权、违规及其他不当言论内容,请广大读者监督,一经证实,平台会立即下线。如遇文章内容问题,请联系微信:chaindd123。
评论(0)
Oh! no
您是否确认要删除该条评论吗?