📕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
(四)在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世界开放性和可组合性的体现,让每个人都能用聪明的合约逻辑撬动庞大的流动性池,获得高效、灵活的金融工具。