200 行代码优雅连接 Uniswap V3:FMZ 量化平台实战

·

关键词:Uniswap V3、FMZ 量化、DeFi、智能合约、以太坊、DEX、自动做市、量化交易、区块链开发

为什么要在 FMZ 上跑 Uniswap V3?

随着 DeFi 概念深入人心,Uniswap V3 已成为去中心化金融的标杆协议。它不仅交易成本更低,还允许做市商在任意价格区间提供流动性,赚取更高的手续费。FMZ 量化平台把这一切浓缩成 200 行核心代码,让个人开发者也能在几小时内上线专业级策略。

本文将带你:

👉 零基础也能 10 分钟部署 Uniswap V3 策略脚本


五分钟读透整体流程

步骤做了什么对应代码位置
1向以太坊节点注册合约 ABIe.IO("abi", ...)
2添加你需要交易的 Tokenself.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:只需换掉图中两个常量——ContractV3FactorySwapRouter 地址即可一键迁移。完整列表可一键查询下方聚合文档。
👉 一键获取多链 DEX Router 地址大全


结语与下一步

到这里,你已经掌握:

  1. Uniswap V3 核心概念(Router、Pool、Factory)
  2. 如何用简洁 JavaScript 脚本完成授权、询价、下单、收款
  3. 如何基于 FMZ 量化平台做回测、实时监控、风控报警

下一步,你可以:

愿你享受 DeFi 世界的无边界自由,同时记得做好风险控制。Happy hacking!