闪电贷原理

说明

现实生活中,如果我们需要借款一般都是需要担保或者是抵押,而且这些贷款需要有很复杂的流程,包括对你的资质的审查,你的抵押物的价值的评估,你的个人的还款能力,你个人的的资产等等,如果现实生活中大家如果有买房或者是买车的经验的话,对这些都应该有深刻的体会。

除此之外,目前很多银行APP中也提供了闪电贷的服务。这些银行因为对于个人的征信记录非常的了解,所以很容易确定每个人的资质,省去了前面很多的评估工作,很方便的每个人的资质决定贷款金额。只需要在这些APP上面点击几个申请按钮,银行就会立马将钱打入导账上。

然而在Web3中却不是这样的。在Web3中的闪电贷有完全一套自己的逻辑:

  • 不需要任何的抵押
  • 借款和还款操作需要在一个交易内完成
  • 只能通过合约的方式借款和和还款
  • 还款时,需要还本金和利息。如果最终检测到还款金额不足本金和利息,整笔交易就会取消

下面以Uniswap V2版本(以下所有的Uniswap均代指Uniswap V2版本)为例来讲解和展示闪电贷的原理

闪电贷的实现原理

Uniswap中的闪电贷的逻辑是在Pair合约中的swap()函数中实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function swap(
uint256 amount0Out,
uint256 amount1Out,
address to,
bytes calldata data
) external lock {
...
if (data.length > 0)
IUniswapV2Callee(to).uniswapV2Call(
msg.sender,
amount0Out,
amount1Out,
data
);
....
}

uniswap是通过swap函数来实现闪电贷的。如果data参数不为空,说明当前是进行闪电贷的操作,需要调用目标合约的uniswapV2Call方法。也就说如果我们需要实现闪电贷,必须通过合约的方式来实现,同时需要实现IUniswapV2Callee接口中的uniswapV2Call方法。

简单理解,其实IUniswapV2Callee(to)就是触发了借款合约的回调函数(回调函数必须是uniswapV2Call()方法)。

可以看到,因为所有的配对合约全部都实现了swap()函数,所以所有的pair合约都可以实现闪电贷。

uniswapV2Call是目标合约获取到闪电贷借款之后需要进行的操作,包括套利和最终的借款操作;

借贷合约和Pair合约之间的交互逻辑如下所示:

因为在pair合约中的代码在实现闪电贷的逻辑时,一定要回调借贷目标合约的uniswapV2Call()方法,所以如果要实现闪电贷,就必须通过合约与Pair合约交互的方式来实现。

闪电贷演示

下面以一个实际的例子来展示闪电贷具体的借贷合约和代码的编写。

例子是来源于DeFiLabs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;

import "forge-std/Test.sol";
import "./interfaces/IWETH9.sol";
import "./interfaces/IUni_Pair_V2.sol";

contract ContractTest is Test {
WETH9 weth = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
Uni_Pair_V2 UniswapV2Pair = Uni_Pair_V2(0xd3d2E2692501A5c9Ca623199D38826e513033a17); // UNI-WETH的对子

function setUp() public {
vm.createSelectFork("mainnet", 15012670); //fork mainnet at block 15012670
}

function testUniswapv2_flashswap() public {
weth.deposit{ value: 2 ether }();
Uni_Pair_V2(UniswapV2Pair).swap(0, 100 * 1e18, address(this), "0x00"); // 借出100个weth
}

function uniswapV2Call(
address sender,
uint256 amount0,
uint256 amount1,
bytes calldata data
) external {
emit log_named_uint(
"Before flashswap, WETH balance of user:",
weth.balanceOf(address(this))
);
// 0.3% fees
uint256 fee = ((amount1 * 3) / 997) + 1;
uint256 amountToRepay = amount1 + fee; // 计算还款时,需要额外还的手续费, 0.3%的手续费
emit log_named_uint("Amount to repay:", amountToRepay);
weth.transfer(address(UniswapV2Pair), amountToRepay); // 还款
emit log_named_uint(
"After flashswap, WETH balance of user:",
weth.balanceOf(address(this)) // 还款之后计算余额
);
}

receive() external payable {}
}

运行查看最终的结果:forge test --contracts ./src/test/Uniswapv2_flashswap.sol -vvv

1
2
3
4
5
6
Running 1 test for src/test/Uniswapv2_flashswap.sol:ContractTest
[PASS] testUniswapv2_flashswap() (gas: 98895)
Logs:
Before flashswap, WETH balance of user:: 102000000000000000000 #借款的100e+原来合约中的2e=102e
Amount to repay:: 100300902708124373120 # 手续费
After flashswap, WETH balance of user:: 1699097291875626880

测试用例正常通过,说明我们测试的借贷合约的借款,计算手续费,还款的逻辑全部没有问题。

这就是一个简单的闪电贷的例子,当然根据实际的场景,可以在uniswapV2Call()方法中可以实现更为复杂的逻辑,包括但不限于各种套利等等。

实例分析

查看一个实际的闪电贷的调用栈理解具体的闪电贷的调用过程。0x04ebf4d6fefed12efb581f3d46db739fbe1832ae4d8a9ac746ed258beafd604a

上面就是一个简单的闪电贷的调用栈的分析。通过调用栈结合上面的原理分析,相信对闪电贷有很深入的理解。

总结

目前一般DEX都会有闪电贷的功能,功能的实现也基本大同小异。因为在Web3中的闪电贷完全不同于真实世界的特殊实现方式,也是DeFi项目繁荣的原因之一。然而闪电贷在繁荣DeFi项目的同时,也有很多的黑客利用闪电贷的功能对各种项目发动攻击,被攻击的DeFi项目不计其数,损失的金额也十分巨大。

作为一个Web3的从业人员,闪电贷是必须要了解和掌握的,但是我们也必须要对闪电贷有一个清晰的认识,闪电贷就是一把双刃剑。

参考

https://github.com/Fankouzu/flashloan

https://www.youtube.com/watch?v=NeVfZYBydDQ

https://www.bccnav.com/2022/08/05/7294/

https://www.bccnav.com/2022/08/05/7292/

https://www.bccnav.com/2022/08/05/7289/

https://www.youtube.com/watch?v=kDkehUi5MoU

https://www.youtube.com/watch?v=EItibs4sy8g

https://www.moonpay.com/blog/defi-flash-loans-explained

https://www.youtube.com/watch?v=HKx89FhZNls

文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2022/10/09/flashloan-intro/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow