超短秒杀套利!一笔交易刷新你的DeFi认识

📕Uniswap V2 core Uniswap V2

引言

在去中心化金融(DeFi)世界中,闪电贷(Flash Loan)以“不需抵押、借款即还”的特性,让许多聪明的开发者和套利者有了新的工具与玩法。
它充分利用了区块链的原子性:要么交易全部成功,要么全部失败——让复杂的套利、清算、抵押替换等操作都能安全地打包在同一笔交易里,无风险试错,瞬间完成。

本文通过一个具体的示例,结合Uniswap路由器与智能合约,展示了如何从检测套利机会、发起闪电贷,到最终归还贷款并获利的全过程,并帮助读者理解闪电贷的工作机制与核心优势。

一、什么是闪电贷?

闪电贷(Flash Loan)是 DeFi 世界里一种 无需抵押、同一笔交易借还款 的特殊贷款方式。它利用区块链交易的「原子性」属性:交易要么全部成功,要么全部失败。打个比方:

你想做一个三明治,手边却没有面包,但厨房里有一台“万能面包机”(Aave 闪电贷)——它可以免费给你面包,但你必须在2分钟内还上,否则面包机会把这一切当作没发生。

步骤
借面包:你对面包机说“给我10片面包”,面包机给你递来了。
做三明治:你用这10片面包搭三明治(比如再去买肉、蔬菜,甚至帮朋友也做一个)。
还面包:在2分钟里,你把10片面包全还给面包机,加上一点手续费(比如10块钱)。
如果你还不了?面包机就会自动收回所有材料,场景回到借之前的样子——你什么都没拿到也不亏东西。

Aave 是最早广泛支持闪电贷的平台之一,dYdX、MakerDAO 也有类似能力。

二、闪电贷运行原理

1. 借——调用 flashLoan 接口

用户智能合约调用 Aave 的 flashLoan() 或 flashLoanSimple(),指定资产和数量。

2. 用——执行操作逻辑

Aave 会在同一笔交易中回调用户合约 executeOperation() 函数,在这里实现你的逻辑,如套利、清算、换抵押。

3. 还——归还贷款与手续费

executeOperation() 执行结束后,必须将本金 + 手续费归还,否则整个交易失败,借款撤销。

三、主要应用场景

1. 套利

利用不同 DEX 之间的价格差:借代币 → A 上买 → B 上卖 → 还款并获利。

2. 抵押切换

将某个资产作为抵押,想换成另一个资产:借贷后慢动作还债换抵押,再还贷款。

3. 避免清算

借闪电贷偿还即将被清算的债务,赢提回抵押,最后还贷,完美脱险。

4. 跨协议组合

把套利、清算、抵押、拆仓等流程放在一笔交易里完成,节省手续费和时间。

四、代码示例

场景模拟
现在有3个ERC20代币:USDC、USDT、WETH,并且他们两两之间有一个交易对,一共是3个交易对,这3个交易对中代币的数量为:

USDC-USDT:100000:100000

WETH-USDT:100000:100000000

WETH-USDC:100000:100000000

现在有一个uniswap的用户A,想用900000wei的USDC换USDC。正常情况下USDC和USDT的相对价格应为1:1,并且现在交易对的池子中的两种代币的数量也相等,但是用户A输入进去的USDC数量过大,相比之下交易对池子流动性过浅,导致滑点大幅移动,只能取出9000左右的USDT。

由于USDC-USDC交易对滑点的大幅移动,此时两种代币的数量比值达到10:1,相应的价格也变为10:1,存在套利机会。

套利者从WETH-USDT池子中借出90000wei的USDT,再用得到的90000wei的USDT去USDC-USDT池子中去换USDC,因为在这个池子中usdt的价格是usdc的10倍,所以可以换到900000的USDC。这时再用得到的USDC到WETH-USDC的池子换取WETH,能换出大约900wei的WETH,最后再发送100wei的WETH到WETH-USDT的pair合约,还上闪电贷。最后0成本获取约900wei的WETH。

(一)安装依赖

npm install --save-dev hardhat
npx hardhat init
yarn add @openzeppelin/contracts@2.5.1
yarn install

(二)把 uniswap-v2-core 代码复制到根目录的contarcts文件下

(三)将 UniswapV2Library.sol 文件复制到uniswap-v2-core

uniswapV2Library.sol

(四)在uniswap-v2-core文件下创建UniswapRouter.sol

//SPDX-License-Identifier: MIT
pragma solidity =0.5.16;


import "./interfaces/IUniswapRouter.sol";
import "./libraries/SafeMath.sol";
import "./UniswapV2Library.sol";
import "./UniswapV2Pair.sol";
import "hardhat/console.sol";

contract UniswapRouter is IUniswapRouter {
    using SafeMath for uint;

    address public factory;
    address public WETH;
    address public getpair;

    modifier ensure(uint deadline) {
        require(deadline >= block.timestamp, "UniswapV2Router: EXPIRED");
        _;
    }

    constructor(address _factory, address _WETH) public {
        factory = _factory;
        WETH = _WETH;
    }

    function getPair(
        address tokenA,
        address tokenB
    ) public view returns (address) {
        (address token0, address token1) = UniswapV2Library.sortTokens(
            tokenA,
            tokenB
        );
        address pair = IUniswapV2Factory(factory).getPair(token0, token1);
        return pair;
    }

    function getPairTotalsupply(address pair) external view returns (uint) {
        uint256 totalSupply = IUniswapV2Pair(pair).totalSupply();
        return totalSupply;
    }

    // **** ADD LIQUIDITY ****
    function _addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin
    ) internal returns (uint amountA, uint amountB) {
        // create the pair if it doesn't exist yet
        if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
            IUniswapV2Factory(factory).createPair(tokenA, tokenB);
        }
        (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(
            factory,
            tokenA,
            tokenB
        );
        if (reserveA == 0 && reserveB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);
        } else {
            uint amountBOptimal = UniswapV2Library.quote(
                amountADesired,
                reserveA,
                reserveB
            );
            if (amountBOptimal <= amountBDesired) {
                require(
                    amountBOptimal >= amountBMin,
                    "UniswapV2Router: INSUFFICIENT_B_AMOUNT"
                );
                (amountA, amountB) = (amountADesired, amountBOptimal);
            } else {
                uint amountAOptimal = UniswapV2Library.quote(
                    amountBDesired,
                    reserveB,
                    reserveA
                );
                assert(amountAOptimal <= amountADesired);
                require(
                    amountAOptimal >= amountAMin,
                    "UniswapV2Router: INSUFFICIENT_A_AMOUNT"
                );
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }
        }
    }

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    )
        external
        ensure(deadline)
        returns (uint amountA, uint amountB, uint liquidity)
    {
        (amountA, amountB) = _addLiquidity(
            tokenA,
            tokenB,
            amountADesired,
            amountBDesired,
            amountAMin,
            amountBMin
        );
        address pair = IUniswapV2Factory(factory).getPair(tokenA, tokenB);
        IERC20(tokenA).transferFrom(msg.sender, pair, amountA);
        IERC20(tokenB).transferFrom(msg.sender, pair, amountB);
        liquidity = IUniswapV2Pair(pair).mint(to);
    }

    // **** REMOVE LIQUIDITY ****
    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) public ensure(deadline) returns (uint amountA, uint amountB) {
        address pair = IUniswapV2Factory(factory).getPair(tokenA, tokenB);
        IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
        (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
        (address token0, ) = UniswapV2Library.sortTokens(tokenA, tokenB);
        (amountA, amountB) = tokenA == token0
            ? (amount0, amount1)
            : (amount1, amount0);
        require(
            amountA >= amountAMin,
            "UniswapV2Router: INSUFFICIENT_A_AMOUNT"
        );
        require(
            amountB >= amountBMin,
            "UniswapV2Router: INSUFFICIENT_B_AMOUNT"
        );
    }

    function flash_swap(
        address uniswapAddress,
        address token0,
        address token1,
        uint amount0Out,
        uint amount1Out
    ) external {
        IUniswapV2Pair(
            IUniswapV2Factory(uniswapAddress).getPair(token0, token1)
        ).swap(
                amount0Out,
                amount1Out,
                address(this),
                abi.encodeWithSignature("flashLoan()")
            );
    }

    // **** SWAP ****
    // requires the initial amount to have already been sent to the first pair
    function _swap(
        uint[] memory amounts,
        address[] memory path,
        address _to
    ) internal {
        for (uint i; i < path.length - 1; i++) {
            (address input, address output) = (path[i], path[i + 1]);
            (address token0, ) = UniswapV2Library.sortTokens(input, output);
            uint amountOut = amounts[i + 1];
            console.log(
                "Router swapExactTokensForTokens amountsIn:%d,amountsOut:%d",
                amounts[i],
                amounts[i + 1]
            );
            (uint amount0Out, uint amount1Out) = input == token0
                ? (uint(0), amountOut)
                : (amountOut, uint(0));
            address to = i < path.length - 2 // ? UniswapV2Library.pairFor(factory, output, path[i + 2])
                ? IUniswapV2Factory(factory).getPair(output, path[i + 2])
                : _to;

            IUniswapV2Pair(IUniswapV2Factory(factory).getPair(input, output))
                .swap(amount0Out, amount1Out, to, new bytes(0));
        }
    }

    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external ensure(deadline) returns (uint[] memory amounts) {
        amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);

        require(
            amounts[amounts.length - 1] >= amountOutMin,
            "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"
        );
        // console.log("Router swapExactTokensForTokens msg.sender:", msg.sender);
        IERC20(path[0]).transferFrom(
            msg.sender,
            // UniswapV2Library.pairFor(factory, path[0], path[1]),
            IUniswapV2Factory(factory).getPair(path[0], path[1]),
            amounts[0]
        ); //address token, address from, address to, uint value)

        _swap(amounts, path, to);
    }

    function swapTokensForExactTokens(
        uint amountOut,
        uint amountInMax,
        address[] calldata path,
        address to,
        uint deadline
    ) external ensure(deadline) returns (uint[] memory amounts) {
        amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
        require(
            amounts[0] <= amountInMax,
            "UniswapV2Router: EXCESSIVE_INPUT_AMOUNT"
        );
        IERC20(path[0]).transferFrom(
            msg.sender,
            // UniswapV2Library.pairFor(factory, path[0], path[1]),
            IUniswapV2Factory(factory).getPair(path[0], path[1]),
            amounts[0]
        ); //address token, address from, address to, uint value)

        _swap(amounts, path, to);
    }

    // **** LIBRARY FUNCTIONS ****
    function quote(
        uint amountA,
        uint reserveA,
        uint reserveB
    ) public pure returns (uint amountB) {
        return UniswapV2Library.quote(amountA, reserveA, reserveB);
    }

    function getAmountOut(
        uint amountIn,
        uint reserveIn,
        uint reserveOut
    ) public pure returns (uint amountOut) {
        return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
    }

    function getAmountIn(
        uint amountOut,
        uint reserveIn,
        uint reserveOut
    ) public pure returns (uint amountIn) {
        return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
    }

    function getAmountsOut(
        uint amountIn,
        address[] memory path
    ) public view returns (uint[] memory amounts) {
        return UniswapV2Library.getAmountsOut(factory, amountIn, path);
    }

    function getAmountsIn(
        uint amountOut,
        address[] memory path
    ) public view returns (uint[] memory amounts) {
        return UniswapV2Library.getAmountsIn(factory, amountOut, path);
    }
}

USDT合约

//SPDX-License-Identifier: MIT
pragma solidity =0.5.16;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";

contract USDT is ERC20, ERC20Detailed {
    constructor() public ERC20Detailed("USDT", "USDT", 18) {}

    function mint(address to, uint256 amount) public {
        _mint(to, amount);
    }
}

USDC合约

//SPDX-License-Identifier: MIT
pragma solidity =0.5.16;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20Detailed.sol";

contract USDC is ERC20, ERC20Detailed {
    constructor() public ERC20Detailed("USDC", "USDC", 18) {}

    function mint(address to, uint256 amount) public {
        _mint(to, amount);
    }
}

(六)contracts文件下创建 Bot.sol 套利合约

const { ethers } = require("hardhat");

// 主部署函数。
async function main() {
  //从 ethers 提供的 signers 中获取签名者。
  const [deployer, account01] = await ethers.getSigners();
  const account1 = account01.getAddress();
  const deployerAddress = deployer.address;
  console.log(`使用账户部署合约: ${deployer.address}`);

  //部署uniswapFactory和三种token
  const UniswapFactory = await ethers.getContractFactory("UniswapV2Factory");
  const uniswapFactory = await UniswapFactory.deploy(deployerAddress);
  const uniswapAddress = await uniswapFactory.getAddress();
  console.log(`uniswap Factory 部署在 ${uniswapAddress}`);
  const WETHFactory = await ethers.getContractFactory("WETH");
  const weth = await WETHFactory.deploy();
  const USDTFactory = await ethers.getContractFactory("USDT");
  const usdt = await USDTFactory.deploy();
  const USDCFactory = await ethers.getContractFactory("USDC");
  const usdc = await USDCFactory.deploy();
  const usdcAddress = await usdc.getAddress();
  const usdtAddress = await usdt.getAddress();
  const wethAddress = await weth.getAddress();
  console.log(`usdcAddress : ${usdcAddress}`);
  console.log(`usdtAddress : ${usdtAddress}`);
  console.log(`wethAddress : ${wethAddress}`);

  //创建pair代币对
  await uniswapFactory.createPair(wethAddress, usdcAddress);
  await uniswapFactory.createPair(wethAddress, usdtAddress);
  await uniswapFactory.createPair(usdcAddress, usdtAddress);

  //部署 Router
  const Router = await ethers.getContractFactory("UniswapRouter");
  const router = await Router.deploy(uniswapAddress, wethAddress);
  const routerAddress = await router.getAddress();
  console.log(`router: ${routerAddress}`);

  //铸币
  await usdc.mint(deployerAddress, 100000100000);
  await usdt.mint(deployerAddress, 100000100000);
  await weth.mint(deployerAddress, 20000000);

  console.log("\n---------添加流动性--------------------------");

  // 授权给 Router 合约,允许其操作部署者的代币
  await usdc.approve(routerAddress, 9999999999999);
  await usdt.approve(routerAddress, 9999999999999);
  await weth.approve(routerAddress, 9999999999999);

  // 开始添加流动性
  const deadline = Math.floor(Date.now() / 1000) + 10 * 60; // 当前时间加10分钟作为交易的最后期限

  // USDC-USDT 交易对添加流动性
  await router.addLiquidity(
    usdcAddress,
    usdtAddress,
    100000, // 添加的USDC 数量
    100000, // 添加的USDT 数量
    0,
    0,
    deployer,
    deadline
  );

  // WETH-USDT 交易对添加流动性
  await router.addLiquidity(
    wethAddress,
    usdtAddress,
    100000,
    100000000,
    0,
    0,
    deployerAddress,
    deadline
  );

  // WETH-USDC 交易对添加流动性
  await router.addLiquidity(
    wethAddress,
    usdcAddress,
    100000,
    100000000,
    0,
    0,
    deployerAddress,
    deadline
  );
  console.log(`add liquidity end `);

  console.log(
    "\n---------笨蛋用户 用 USDC swap USDT--------------------------"
  );
  // 假设 account1 拥有 5000000 wei 的 USDC,并且将其授权给 routerAddress
  await usdc.mint(account1, 5000000);
  await usdc.connect(account01).approve(routerAddress, 9999999999999);

  // 用 5000000 wei 的 USDC 兑换 USDT
  const path0 = [usdcAddress, usdtAddress]; // 交易路径 USDC -> USDT
  await router
    .connect(account01)
    .swapExactTokensForTokens(500000, 0, path0, account1, deadline);
  console.log(`笨蛋 完成换币 `); // 打印兑换完成的消息

  // 攻击者开始套利
  console.log("\n-------------攻击--------------------------");
  const Bot = await ethers.getContractFactory("arbitrageBot");
  const bot = await Bot.deploy(uniswapAddress, routerAddress);
  const botAddress = await bot.getAddress();
  console.log(`bot 部署在 : ${botAddress} `);

  // 给 bot 合约账户铸造 WETH 代币
  const bot_weth_balance = await weth.balanceOf(botAddress);
  console.log("攻击前:bot_weth_balance:", bot_weth_balance);

  // 开始套利,套利路径 WETH -> USDT -> USDC
  const path = [wethAddress, usdtAddress, usdcAddress];
  await bot.attack(path, 20000); // 使用 20000 WETH 进行套利

  // 攻击后检查 bot 的 WETH 余额
  const bot_weth_balance2 = await weth.balanceOf(botAddress);
  console.log("攻击后:bot_weth_balance:", bot_weth_balance2);

  // 判断是否套利成功
  if (bot_weth_balance2 > bot_weth_balance) {
    console.log("攻击成功!!~~~~");
  }
}

// 执行主函数并处理可能的结果。
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

(七)根目录下创建 scripts文件夹,并创建 flashLoan_attack.js 文件

const { ethers } = require("hardhat");

// 主部署函数。
async function main() {
  //从 ethers 提供的 signers 中获取签名者。
  const [deployer, account01] = await ethers.getSigners();
  const account1 = account01.getAddress();
  const deployerAddress = deployer.address;
  console.log(`使用账户部署合约: ${deployer.address}`);

  //部署uniswapFactory和三种token
  const UniswapFactory = await ethers.getContractFactory("UniswapV2Factory");
  const uniswapFactory = await UniswapFactory.deploy(deployerAddress);
  const uniswapAddress = await uniswapFactory.getAddress();
  console.log(`uniswap Factory 部署在 ${uniswapAddress}`);
  const WETHFactory = await ethers.getContractFactory("WETH");
  const weth = await WETHFactory.deploy();
  const USDTFactory = await ethers.getContractFactory("USDT");
  const usdt = await USDTFactory.deploy();
  const USDCFactory = await ethers.getContractFactory("USDC");
  const usdc = await USDCFactory.deploy();
  const usdcAddress = await usdc.getAddress();
  const usdtAddress = await usdt.getAddress();
  const wethAddress = await weth.getAddress();
  console.log(`usdcAddress : ${usdcAddress}`);
  console.log(`usdtAddress : ${usdtAddress}`);
  console.log(`wethAddress : ${wethAddress}`);

  //创建pair代币对
  await uniswapFactory.createPair(wethAddress, usdcAddress);
  await uniswapFactory.createPair(wethAddress, usdtAddress);
  await uniswapFactory.createPair(usdcAddress, usdtAddress);

  //部署 Router
  const Router = await ethers.getContractFactory("UniswapRouter");
  const router = await Router.deploy(uniswapAddress, wethAddress);
  const routerAddress = await router.getAddress();
  console.log(`router: ${routerAddress}`);

  //铸币
  await usdc.mint(deployerAddress, 100000100000);
  await usdt.mint(deployerAddress, 100000100000);
  await weth.mint(deployerAddress, 20000000);

  console.log("\n---------添加流动性--------------------------");

  // 授权给 Router 合约,允许其操作部署者的代币
  await usdc.approve(routerAddress, 9999999999999);
  await usdt.approve(routerAddress, 9999999999999);
  await weth.approve(routerAddress, 9999999999999);

  // 开始添加流动性
  const deadline = Math.floor(Date.now() / 1000) + 10 * 60; // 当前时间加10分钟作为交易的最后期限

  // USDC-USDT 交易对添加流动性
  await router.addLiquidity(
    usdcAddress,
    usdtAddress,
    100000, // 添加的USDC 数量
    100000, // 添加的USDT 数量
    0,
    0,
    deployer,
    deadline
  );

  // WETH-USDT 交易对添加流动性
  await router.addLiquidity(
    wethAddress,
    usdtAddress,
    100000,
    100000000,
    0,
    0,
    deployerAddress,
    deadline
  );

  // WETH-USDC 交易对添加流动性
  await router.addLiquidity(
    wethAddress,
    usdcAddress,
    100000,
    100000000,
    0,
    0,
    deployerAddress,
    deadline
  );
  console.log(`add liquidity end `);

  console.log(
    "\n---------笨蛋用户 用 USDC swap USDT--------------------------"
  );
  // 假设 account1 拥有 5000000 wei 的 USDC,并且将其授权给 routerAddress
  await usdc.mint(account1, 5000000);
  await usdc.connect(account01).approve(routerAddress, 9999999999999);

  // 用 5000000 wei 的 USDC 兑换 USDT
  const path0 = [usdcAddress, usdtAddress]; // 交易路径 USDC -> USDT
  await router
    .connect(account01)
    .swapExactTokensForTokens(500000, 0, path0, account1, deadline);
  console.log(`笨蛋 完成换币 `); // 打印兑换完成的消息

  // 攻击者开始套利
  console.log("\n-------------攻击--------------------------");
  const Bot = await ethers.getContractFactory("arbitrageBot");
  const bot = await Bot.deploy(uniswapAddress, routerAddress);
  const botAddress = await bot.getAddress();
  console.log(`bot 部署在 : ${botAddress} `);

  // 给 bot 合约账户铸造 WETH 代币
  const bot_weth_balance = await weth.balanceOf(botAddress);
  console.log("攻击前:bot_weth_balance:", bot_weth_balance);

  // 开始套利,套利路径 WETH -> USDT -> USDC
  const path = [wethAddress, usdtAddress, usdcAddress];
  await bot.attack(path, 20000); // 使用 20000 WETH 进行套利

  // 攻击后检查 bot 的 WETH 余额
  const bot_weth_balance2 = await weth.balanceOf(botAddress);
  console.log("攻击后:bot_weth_balance:", bot_weth_balance2);

  // 判断是否套利成功
  if (bot_weth_balance2 > bot_weth_balance) {
    console.log("攻击成功!!~~~~");
  }
}

// 执行主函数并处理可能的结果。
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

五、闪电贷流程概述

用户通过调用 UniswapV2Pair 合约的 swap 方法,将21 WETH 换成 20000 USDT, 其中 swap会先将 20000 USDT 转给用户
然后用户可以用这得到的 20000 USDT 换成 326448 USDC,再用这 326448 USDC换成 324 的WETH
得到 324 WETH后,用户将 21 的 WETH 支付给UniswapV2Pair合约
最后,swap方法检查是否收到了 21 WETH,收到则完成整个swap操作

总结

闪电贷的本质是:
:瞬间从池子中无抵押借出大量资金;
:利用这笔资金在不同交易对间套利、清算或进行复杂组合操作;
:在同一笔交易结束前归还本金加手续费,否则整个操作回滚。

在实战中,套利者利用市场中的价格波动,比如大额用户交易带来的滑点,构造出一个从USDT到USDC再到WETH的套利路径,成功将零成本资金转化为实实在在的收益。

闪电贷不仅是技术创新,更是DeFi世界开放性和可组合性的体现,让每个人都能用聪明的合约逻辑撬动庞大的流动性池,获得高效、灵活的金融工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yoona1020

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值