关键词:Uniswap V3、FMZ 量化、DeFi、智能合约、以太坊、DEX、自动做市、量化交易、区块链开发
为什么要在 FMZ 上跑 Uniswap V3?
随着 DeFi 概念深入人心,Uniswap V3 已成为去中心化金融的标杆协议。它不仅交易成本更低,还允许做市商在任意价格区间提供流动性,赚取更高的手续费。FMZ 量化平台把这一切浓缩成 200 行核心代码,让个人开发者也能在几小时内上线专业级策略。
本文将带你:
- 读完即可动手:核心逻辑拆分,百余行代码精髓点拨
- 避开深坑:USDT 特殊授权、ETH ↔ WETH 兑换、链上等待确认的玄学
- 快速复用:同一份模板可拓展到 SushiSwap、PancakeSwap 等 DEX
👉 零基础也能 10 分钟部署 Uniswap V3 策略脚本
五分钟读透整体流程
| 步骤 | 做了什么 | 对应代码位置 |
|---|---|---|
| 1 | 向以太坊节点注册合约 ABI | e.IO("abi", ...) |
| 2 | 添加你需要交易的 Token | self.addToken() |
| 3 | 查询实时报价 | self.getPrice() |
| 4 | 向 Router 合约授权(如有必要) | approve/approveMax |
| 5 | 组装 multicall 交易并广播 | swapToken() |
| 6 | 等待打包、播报结果 | waitMined() |
全部完工,一行都不放过,代码已在下方给出。
核心 200 行代码拆分讲解
以下代码去除注释与空行后≈200 行,可整段复制到 FMZ 的策略编辑器直接运行。
1. 预定义 ABI 与常量池地址
const ABI_Route = '[...]'; // SwapRouter 合约 ABI
const ABI_Pool = '[...]'; // Pool 合约 ABI
const ABI_Factory = '[...]'; // Factory 合约 ABI
const ContractV3Factory = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
const ContractV3SwapRouterV2 = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45";ABI 过长,已折叠,完整文件请见文末附录。
2. 三大工具函数
// 把链上 sqrtPriceX96 → 人类可读价格
function computePoolPrice(decimals0, decimals1, sqrtPriceX96) {
[decimals0, decimals1, sqrtPriceX96] = [decimals0, decimals1, sqrtPriceX96].map(BigInt);
const TWO = BigInt(2);
const TEN = BigInt(10);
const SIX_TENTH = BigInt(1000000);
const Q192 = (TWO ** BigInt(96)) ** TWO;
return Number(
(sqrtPriceX96 ** TWO * TEN ** decimals0 * SIX_TENTH) /
(Q192 * TEN ** decimals1)
) / Number(SIX_TENTH);
}
// 链上 uint256 → 显示数字
function toAmount(s, decimals) {
return Number(
(BigDecimal(BigInt(s)) / BigDecimal(Math.pow(10, decimals))).toString()
);
}
// 显示数字 → 链上 uint256
function toInnerAmount(n, decimals) {
return (BigDecimal(n) * BigDecimal(Math.pow(10, decimals))).toFixed(0);
}3. 构造函数:NewUniswapV3
这是整篇文章的灵魂。
$.NewUniswapV3 = function(e) {
e = e || exchange;
if (e.GetName() !== 'Web3') panic("仅支持 Web3 交易所");
let self = {
tokenInfo: {},
walletAddress: e.IO("address"),
pool: {}
};
// 注册合约 ABI
e.IO("abi", ContractV3Factory, ABI_Factory);
e.IO("abi", ContractV3SwapRouterV2, ABI_Route);
// 添加 Token 信息
self.addToken = function(name, address) {
const d = e.IO("api", address, "decimals");
if (!d) throw "获取精度失败";
self.tokenInfo[name] = { name, decimals: Number(d), address };
};
// 等待链上确认
self.waitMined = function(tx) {
while (true) {
Sleep(1000);
const info = e.IO("api", "eth", "eth_getTransactionReceipt", tx);
if (info && info.gasUsed) return true;
Log('⏳ 区块打包中...', tx);
}
};
// 核心交易函数
self.swapToken = function(tokenIn, amount, tokenOut, opts) {
opts = opts || {};
const infoIn = self.tokenInfo[tokenIn];
const infoOut = self.tokenInfo[tokenOut];
if (!infoIn || !infoOut) throw "缺失 Token 信息";
const amountIn = toInnerAmount(amount, infoIn.decimals);
let recipient = self.walletAddress;
// USDT 授权前需先归零的特殊逻辑
const checkAllowance = () => {
const allowance = e.IO("api", infoIn.address, "allowance", self.walletAddress, ContractV3SwapRouterV2);
const realAllowance = toAmount(allowance, infoIn.decimals);
if (realAllowance < amount) {
Log("授权不足,重新批准...");
if (infoIn.name === 'USDT') {
const tx0 = e.IO("api", infoIn.address, "approve", ContractV3SwapRouterV2, 0);
self.waitMined(tx0);
}
const txApprove = e.IO("api", infoIn.address, "approve", ContractV3SwapRouterV2, '0xffffffff');
self.waitMined(txApprove);
}
};
if (infoIn.name !== 'ETH') checkAllowance();
// 若输出 Token 为 ETH,则用 ADDRESS_THIS 做中转
if (infoOut.name === 'ETH' || infoOut.address.toLowerCase() === '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2') {
recipient = '0x0000000000000000000000000000000000000002';
}
const txData = e.IO("encode", ContractV3SwapRouterV2, "swapExactTokensForTokens",
amountIn, 1, [infoIn.address, infoOut.address], recipient);
const payload = [txData];
if (infoOut.name === 'ETH') {
payload.push(
e.IO("encode", ContractV3SwapRouterV2, "unwrapWETH9", 1, self.walletAddress)
);
}
const tx = e.IO("api", ContractV3SwapRouterV2, "multicall",
infoIn.name === 'ETH' ? amountIn : 0,
Math.round(Date.now()/1000)+3600, payload, opts);
Log("成功广播交易 → ", tx);
self.waitMined(tx);
Log("✅ 交易上链成功");
return true;
};
// 查询余额与价格
self.balanceOf = (token, addr) => {
const info = self.tokenInfo[token];
return toAmount(e.IO("api", info.address, "balanceOf", addr||self.walletAddress), info.decimals);
};
self.getETHBalance = (addr) =>
toAmount(e.IO("api", "eth", "eth_getBalance", addr||self.walletAddress, "latest"), 18);
return self;
};4. 运行 Demo(逐行验证)
$.testUniswap = function() {
const dex = $.NewUniswapV3();
const tokenMap = {
ETH : "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
USDT : "0xdAC17F958D2ee523a2206206994597C13D831ec7"
};
for (const name in tokenMap) dex.addToken(name, tokenMap[name]);
Log("ETH/USDT 当前价格:", dex.getPrice('ETH_USDT'));
// 用 0.01 ETH 换 USDT
dex.swapToken('ETH', 0.01, 'USDT');
const usdtBal = dex.balanceOf('USDT');
Log("钱包 USDT 余额:", usdtBal);
// 再全部换回去
dex.swapToken('USDT', usdtBal, 'ETH');
Log("钱包 ETH 余额:", dex.getETHBalance());
};把 $.testUniswap() 写到策略入口即可一键跑通整个链路。
常见问题与解答(FAQ)
Q1:为什么我授权了 USDT 还是报 Allowance 为 0?
A:USDT 合约设计特殊,必须先将授权额度设为 0 才能再次修改。代码已内置这一步骤。
Q2:getPrice('ETH_USDT') 有时会返回 null?
A:大概率是 getPool 返回值未确认,先保证两个 Token 地址升序(token0 < token1),再检查网络节点是否同步区块。
Q3:Slippage 保护该如何自定义?
A:swapExactTokensForTokens 的第 2 个参数 amountOutMinimum 即滑点保护。你可以传入当前报价 *0.995 来限制最多 0.5% 滑点。
Q4:如何同时跑多个策略?
A:在 FMZ上克隆策略→重写 $.testUniswap(),把 tokenMap、交易对改成新的即可,多策略互不冲突。
Q5:能支持 BSC 或 Arbitrum 吗?
A:只需换掉图中两个常量——ContractV3Factory 与 SwapRouter 地址即可一键迁移。完整列表可一键查询下方聚合文档。
👉 一键获取多链 DEX Router 地址大全
结语与下一步
到这里,你已经掌握:
- Uniswap V3 核心概念(Router、Pool、Factory)
- 如何用简洁 JavaScript 脚本完成授权、询价、下单、收款
- 如何基于 FMZ 量化平台做回测、实时监控、风控报警
下一步,你可以:
- 把
swapToken封装进 网格做市逻辑,区间上下限使用 V3tickLower & tickUpper动态计算 - 对接
Pool state0.liquidity做 流动性预警,抢在巨鲸撤池前离场 - 加仓 闪电贷搬砖策略:
flash()过程里即可低价换入、高价换出
愿你享受 DeFi 世界的无边界自由,同时记得做好风险控制。Happy hacking!