WTF Solidity极简入门: 57. 闪电贷
我最近在重新学 Solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。
推特:@0xAA_Science |@WTFAcademy_
社区:Discord |微信群 |官网 wtf.academy
所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity
“闪电贷攻击”这个词大家一定听说过,但是什么是闪电贷?如何编写闪电贷合约?这一讲,我们将介绍区块链中的闪电贷,实现基于Uniswap V2,Uniswap V3,和AAVE V3的闪电贷合约,并使用Foundry进行测试。
闪电贷
你第一次听说"闪电贷"一定是在Web3,因为Web2没有这个东西。闪电贷(Flashloan)是DeFi的一种创新,它允许用户在一个交易中借出并迅速归还资金,而无需提供任何抵押。
想象一下,你突然在市场中发现了一个套利机会,但是需要准备100万u的资金才能完成套利。在Web2,你去银行申请贷款,需要审批,很可能错过套利的机会。另外,如果套利失败,你不光要支付利息,还需要归还损失的本金。
而在Web3,你可以在DeFI平台(Uniswap,AAVE,Dodo)中进行闪电贷获取资金,就可以在无担保的情况下借100万u的代币,执行链上套利,最后再归还贷款和利息。
闪电贷利用了以太坊交易的原子性:一个交易(包括其中的所有操作)要么完全执行,要么完全不执行。如果一个用户尝试使用闪电贷并在同一个交易中没有归还资金,那么整个交易都会失败并被回滚,就像它从未发生过一样。因此,DeFi平台不需要担心借款人还不上款,因为还不上的话就意味着钱没借出去;同时,借款人也不用担心套利不成功,因为套利不成功的话就还不上款,也就意味着借钱没成功。
闪电贷实战
下面,我们分别介绍如何在Uniswap V2,Uniswap V3,和AAVE V3的实现闪电贷合约。
1. Uniswap V2闪电贷
Uniswap V2 Pair 合约的swap()
函数支持闪电贷。与闪电贷业务相关的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { // 其他逻辑... // 乐观的发送代币到to地址 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // 调用to地址的回调函数uniswapV2Call if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); // 其他逻辑... // 通过k=x*y公式,检查闪电贷是否归还成功 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); }
在swap()
函数中:
先将池子中的代币乐观的转移给了to
地址。
如果传入的data
长度大于0
,就会调用to
地址的回调函数uniswapV2Call
,执行闪电贷逻辑。
最后通过k=x*y
检查闪电贷是否归还成功,如果不成功,则回滚交易。
下面,我们完成闪电贷合约UniswapV2Flashloan.sol
。我们让它继承IUniswapV2Callee
,并将闪电贷的核心逻辑写在回调函数uniswapV2Call
中。
整体逻辑很简单,在闪电贷函数flashloan()
中,我们从Uniswap V2的WETH-DAI
池子借WETH
。触发闪电贷之后,回调函数uniswapV2Call
会被Pair合约调用,我们不进行套利,仅在计算利息后归还闪电贷。Uniswap V2闪电贷的利息为每笔0.3%
。
注意 :回调函数一定要做好权限控制,确保只有Uniswap的Pair合约可以调用,否则的话合约中的资金会被黑客盗光。
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "./Lib.sol"; // UniswapV2闪电贷回调接口 interface IUniswapV2Callee { function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; } // UniswapV2闪电贷合约 contract UniswapV2Flashloan is IUniswapV2Callee { address private constant UNISWAP_V2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; IUniswapV2Factory private constant factory = IUniswapV2Factory(UNISWAP_V2_FACTORY); IERC20 private constant weth = IERC20(WETH); IUniswapV2Pair private immutable pair; constructor() { pair = IUniswapV2Pair(factory.getPair(DAI, WETH)); } // 闪电贷函数 function flashloan(uint wethAmount) external { // calldata长度大于1才能触发闪电贷回调函数 bytes memory data = abi.encode(WETH, wethAmount); // amount0Out是要借的DAI, amount1Out是要借的WETH pair.swap(0, wethAmount, address(this), data); } // 闪电贷回调函数,只能被 DAI/WETH pair 合约调用 function uniswapV2Call( address sender, uint amount0, uint amount1, bytes calldata data ) external { // 确认调用的是 DAI/WETH pair 合约 address token0 = IUniswapV2Pair(msg.sender).token0(); // 获取token0地址 address token1 = IUniswapV2Pair(msg.sender).token1(); // 获取token1地址 assert(msg.sender == factory.getPair(token0, token1)); // ensure that msg.sender is a V2 pair // 解码calldata (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); // flashloan 逻辑,这里省略 require(tokenBorrow == WETH, "token borrow != WETH"); // 计算flashloan费用 // fee / (amount + fee) = 3/1000 // 向上取整 uint fee = (amount1 * 3) / 997 + 1; uint amountToRepay = amount1 + fee; // 归还闪电贷 weth.transfer(address(pair), amountToRepay); } }
Foundry测试合约UniswapV2Flashloan.t.sol
:
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/UniswapV2Flashloan.sol"; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; contract UniswapV2FlashloanTest is Test { IWETH private weth = IWETH(WETH); UniswapV2Flashloan private flashloan; function setUp() public { flashloan = new UniswapV2Flashloan(); } function testFlashloan() public { // 换weth,并转入flashloan合约,用做手续费 weth.deposit{value: 1e18}(); weth.transfer(address(flashloan), 1e18); // 闪电贷借贷金额 uint amountToBorrow = 100 * 1e18; flashloan.flashloan(amountToBorrow); } // 手续费不足,会revert function testFlashloanFail() public { // 换weth,并转入flashloan合约,用做手续费 weth.deposit{value: 1e18}(); weth.transfer(address(flashloan), 3e17); // 闪电贷借贷金额 uint amountToBorrow = 100 * 1e18; // 手续费不足 vm.expectRevert(); flashloan.flashloan(amountToBorrow); } }
在测试合约中,我们分别测试了手续费充足和不足的情况,你可以在安装Foundry后使用下面的命令行进行测试(你可以将RPC换成其他以太坊RPC):
1 2 FORK_URL=https://singapore.rpc.blxrbdn.com forge test --fork-url $FORK_URL --match-path test/UniswapV2Flashloan.t.sol -vv
2. Uniswap V3闪电贷
与Uniswap V2在swap()
交换函数中间接支持闪电贷不同,Uniswap V3在Pool池合约 中加入了flash()
函数直接支持闪电贷,核心代码如下:
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 function flash( address recipient, uint256 amount0, uint256 amount1, bytes calldata data ) external override lock noDelegateCall { // 其他逻辑... // 乐观的发送代币到to地址 if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0); if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1); // 调用to地址的回调函数uniswapV3FlashCallback IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data); // 检查闪电贷是否归还成功 uint256 balance0After = balance0(); uint256 balance1After = balance1(); require(balance0Before.add(fee0) <= balance0After, 'F0'); require(balance1Before.add(fee1) <= balance1After, 'F1'); // sub is safe because we know balanceAfter is gt balanceBefore by at least fee uint256 paid0 = balance0After - balance0Before; uint256 paid1 = balance1After - balance1Before; // 其他逻辑... }
下面,我们完成闪电贷合约UniswapV3Flashloan.sol
。我们让它继承IUniswapV3FlashCallback
,并将闪电贷的核心逻辑写在回调函数uniswapV3FlashCallback
中。
整体逻辑与V2的类似,在闪电贷函数flashloan()
中,我们从Uniswap V3的WETH-DAI
池子借WETH
。触发闪电贷之后,回调函数uniswapV3FlashCallback
会被Pool合约调用,我们不进行套利,仅在计算利息后归还闪电贷。Uniswap V3每笔闪电贷的手续费与交易手续费一致。
注意 :回调函数一定要做好权限控制,确保只有Uniswap的Pair合约可以调用,否则的话合约中的资金会被黑客盗光。
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "./Lib.sol"; // UniswapV3闪电贷回调接口 // 需要实现并重写uniswapV3FlashCallback()函数 interface IUniswapV3FlashCallback { /// 在实现中,你必须偿还池中由 flash 发送的代币及计算出的费用金额。 /// 调用此方法的合约必须经由官方 UniswapV3Factory 部署的 UniswapV3Pool 检查。 /// @param fee0 闪电贷结束时,应支付给池的 token0 的费用金额 /// @param fee1 闪电贷结束时,应支付给池的 token1 的费用金额 /// @param data 通过 IUniswapV3PoolActions#flash 调用由调用者传递的任何数据 function uniswapV3FlashCallback( uint256 fee0, uint256 fee1, bytes calldata data ) external; } // UniswapV3闪电贷合约 contract UniswapV3Flashloan is IUniswapV3FlashCallback { address private constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; uint24 private constant poolFee = 3000; IERC20 private constant weth = IERC20(WETH); IUniswapV3Pool private immutable pool; constructor() { pool = IUniswapV3Pool(getPool(DAI, WETH, poolFee)); } function getPool( address _token0, address _token1, uint24 _fee ) public pure returns (address) { PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey( _token0, _token1, _fee ); return PoolAddress.computeAddress(UNISWAP_V3_FACTORY, poolKey); } // 闪电贷函数 function flashloan(uint wethAmount) external { bytes memory data = abi.encode(WETH, wethAmount); IUniswapV3Pool(pool).flash(address(this), 0, wethAmount, data); } // 闪电贷回调函数,只能被 DAI/WETH pair 合约调用 function uniswapV3FlashCallback( uint fee0, uint fee1, bytes calldata data ) external { // 确认调用的是 DAI/WETH pair 合约 require(msg.sender == address(pool), "not authorized"); // 解码calldata (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); // flashloan 逻辑,这里省略 require(tokenBorrow == WETH, "token borrow != WETH"); // 归还闪电贷 weth.transfer(address(pool), wethAmount + fee1); } }
Foundry测试合约UniswapV3Flashloan.t.sol
:
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {Test, console2} from "forge-std/Test.sol"; import "../src/UniswapV3Flashloan.sol"; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; contract UniswapV2FlashloanTest is Test { IWETH private weth = IWETH(WETH); UniswapV3Flashloan private flashloan; function setUp() public { flashloan = new UniswapV3Flashloan(); } function testFlashloan() public { // 换weth,并转入flashloan合约,用做手续费 weth.deposit{value: 1e18}(); weth.transfer(address(flashloan), 1e18); uint balBefore = weth.balanceOf(address(flashloan)); console2.logUint(balBefore); // 闪电贷借贷金额 uint amountToBorrow = 1 * 1e18; flashloan.flashloan(amountToBorrow); } // 手续费不足,会revert function testFlashloanFail() public { // 换weth,并转入flashloan合约,用做手续费 weth.deposit{value: 1e18}(); weth.transfer(address(flashloan), 1e17); // 闪电贷借贷金额 uint amountToBorrow = 100 * 1e18; // 手续费不足 vm.expectRevert(); flashloan.flashloan(amountToBorrow); } }
在测试合约中,我们分别测试了手续费充足和不足的情况,你可以在安装Foundry后使用下面的命令行进行测试(你可以将RPC换成其他以太坊RPC):
1 2 FORK_URL=https://singapore.rpc.blxrbdn.com forge test --fork-url $FORK_URL --match-path test/UniswapV3Flashloan.t.sol -vv
3. AAVE V3闪电贷
AAVE是去中心的借贷平台,它的Pool合约 通过flashLoan()
和flashLoanSimple()
两个函数支持单资产和多资产的闪电贷。这里,我们仅利用flashLoan()
实现单个资产(WETH
)的闪电贷。
下面,我们完成闪电贷合约AaveV3Flashloan.sol
。我们让它继承IFlashLoanSimpleReceiver
,并将闪电贷的核心逻辑写在回调函数executeOperation
中。
整体逻辑与V2的类似,在闪电贷函数flashloan()
中,我们从AAVE V3的WETH
池子借WETH
。触发闪电贷之后,回调函数executeOperation
会被Pool合约调用,我们不进行套利,仅在计算利息后归还闪电贷。AAVE V3闪电贷的手续费默认为每笔0.05%
,比Uniswap的要低。
注意 :回调函数一定要做好权限控制,确保只有AAVE的Pool合约可以调用,并且发起者是本合约,否则的话合约中的资金会被黑客盗光。
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "./Lib.sol"; interface IFlashLoanSimpleReceiver { /** * @notice 在接收闪电借款资产后执行操作 * @dev 确保合约能够归还债务 + 额外费用,例如,具有 * 足够的资金来偿还,并已批准 Pool 提取总金额 * @param asset 闪电借款资产的地址 * @param amount 闪电借款资产的数量 * @param premium 闪电借款资产的费用 * @param initiator 发起闪电贷款的地址 * @param params 初始化闪电贷款时传递的字节编码参数 * @return 如果操作的执行成功则返回 True,否则返回 False */ function executeOperation( address asset, uint256 amount, uint256 premium, address initiator, bytes calldata params ) external returns (bool); } // AAVE V3闪电贷合约 contract AaveV3Flashloan { address private constant AAVE_V3_POOL = 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; ILendingPool public aave; constructor() { aave = ILendingPool(AAVE_V3_POOL); } // 闪电贷函数 function flashloan(uint256 wethAmount) external { aave.flashLoanSimple(address(this), WETH, wethAmount, "", 0); } // 闪电贷回调函数,只能被 pool 合约调用 function executeOperation(address asset, uint256 amount, uint256 premium, address initiator, bytes calldata) external returns (bool) { // 确认调用的是 DAI/WETH pair 合约 require(msg.sender == AAVE_V3_POOL, "not authorized"); // 确认闪电贷发起者是本合约 require(initiator == address(this), "invalid initiator"); // flashloan 逻辑,这里省略 // 计算flashloan费用 // fee = 5/1000 * amount uint fee = (amount * 5) / 10000 + 1; uint amountToRepay = amount + fee; // 归还闪电贷 IERC20(WETH).approve(AAVE_V3_POOL, amountToRepay); return true; } }
Foundry测试合约AaveV3Flashloan.t.sol
:
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-std/Test.sol"; import "../src/AaveV3Flashloan.sol"; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; contract UniswapV2FlashloanTest is Test { IWETH private weth = IWETH(WETH); AaveV3Flashloan private flashloan; function setUp() public { flashloan = new AaveV3Flashloan(); } function testFlashloan() public { // 换weth,并转入flashloan合约,用做手续费 weth.deposit{value: 1e18}(); weth.transfer(address(flashloan), 1e18); // 闪电贷借贷金额 uint amountToBorrow = 100 * 1e18; flashloan.flashloan(amountToBorrow); } // 手续费不足,会revert function testFlashloanFail() public { // 换weth,并转入flashloan合约,用做手续费 weth.deposit{value: 1e18}(); weth.transfer(address(flashloan), 4e16); // 闪电贷借贷金额 uint amountToBorrow = 100 * 1e18; // 手续费不足 vm.expectRevert(); flashloan.flashloan(amountToBorrow); } }
在测试合约中,我们分别测试了手续费充足和不足的情况,你可以在安装Foundry后使用下面的命令行进行测试(你可以将RPC换成其他以太坊RPC):
1 2 FORK_URL=https://singapore.rpc.blxrbdn.com forge test --fork-url $FORK_URL --match-path test/AaveV3Flashloan.t.sol -vv
总结
这一讲,我们介绍了闪电贷,它允许用户在一个交易中借出并迅速归还资金,而无需提供任何抵押。并且,我们分别实现了Uniswap V2,Uniswap V3,和AAVE的闪电贷合约。
通过闪电贷,我们能够无抵押的撬动海量资金进行无风险套利或漏洞攻击。你准备用闪电贷做些什么呢?