Solidity 103

1. ERC20
2. Token Faucet
3. Token Faucet
4. ERC721
5. Dutch Auction
6. Merkle Tree
7. Digital Signature
8. NFT Exchange
9. Chainlink Randomness
10. ERC1155
11. WETH
12. Payment Splitting
13. Linear Release
14. Token Lock
15. Time Lock
16. Proxy Contract
17. Upgradeable Contract
18. Transparent Proxy
19. UUPS
20. Multisignature Wallet
21. ERC4626 Tokenization of Vault Standard
22. EIP712 Typed Data Signature
23. ERC-2612 ERC20Permit
24. 跨链桥
25. 多重调用
26. 去中心化交易所
27. 闪电贷
.
闪电贷

我最近在重新学 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()函数支持闪电贷。与闪电贷业务相关的代码如下:

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()函数中:

  1. 先将池子中的代币乐观的转移给了to地址。
  2. 如果传入的data长度大于0,就会调用to地址的回调函数uniswapV2Call,执行闪电贷逻辑。
  3. 最后通过k=x*y检查闪电贷是否归还成功,如果不成功,则回滚交易。

下面,我们完成闪电贷合约UniswapV2Flashloan.sol。我们让它继承IUniswapV2Callee,并将闪电贷的核心逻辑写在回调函数uniswapV2Call中。

整体逻辑很简单,在闪电贷函数flashloan()中,我们从Uniswap V2的WETH-DAI池子借WETH。触发闪电贷之后,回调函数uniswapV2Call会被Pair合约调用,我们不进行套利,仅在计算利息后归还闪电贷。Uniswap V2闪电贷的利息为每笔0.3%。

注意:回调函数一定要做好权限控制,确保只有Uniswap的Pair合约可以调用,否则的话合约中的资金会被黑客盗光。

// 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:

// 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):

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()函数直接支持闪电贷,核心代码如下:

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合约可以调用,否则的话合约中的资金会被黑客盗光。

// 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:

// 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):

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合约可以调用,并且发起者是本合约,否则的话合约中的资金会被黑客盗光。

// 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:

// 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):

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的闪电贷合约。

通过闪电贷,我们能够无抵押的撬动海量资金进行无风险套利或漏洞攻击。你准备用闪电贷做些什么呢?

PreviousNext