DEFI - 如何在Uniswap V3上交换代币 - Quicknode

  • QuickNode
  • 发布于 2024-11-15 19:20
  • 阅读 28

这篇文章介绍了如何在Uniswap V3上执行代币交换的步骤,重点在于通过Ethers.js与Uniswap的智能合约进行交互,提供了从设置节点到执行交换的详细指导,包括代码示例和图示,适合开发者快速上手,并了解Uniswap V3架构的核心概念。

info

Uniswap v4 已正式发布!查看我们的技术指南 - 如何在 Uniswap V4 上创建 Hooks,了解新功能以及如何使用它。

概述

随着 Uniswap 不断改善用户体验,无论是前端还是通过智能合约,开发者需要跟上最新版本的更新。在2021年3月,Uniswap 发布了其新的 v3 架构,包括对智能合约和前端的更新。本指南将帮助开发者克服学习新 v3 架构的挑战,并向你展示如何使用 Ethers.js 程序化地交换代币。

让我们开始吧!

你需要的条件

  • 对 Ethereum 和智能合约的基本理解
  • 带有私钥访问权限的 Web3 钱包
  • 一个 QuickNode 账户
  • 具有 JavaScript 经验并已安装 Node.js
  • 一个终端窗口
  • 一个代码编辑器(例如:VSCode)

你将要做的事情

  • 学习 Uniswap V3 架构
  • 深入了解 Uniswap 智能合约及其工作原理
  • 创建一个 QuickNode 节点端点(你可以在 这里 创建一个)
  • 通过 QuickNode Faucet 为你的钱包充值
  • 在 Uniswap V3 上使用 exactInputSingle 函数在 Sepolia 测试网执行一次池子交换 (ETH -> USDC)
依赖项 版本
node ^18.18
dotenv ^16.4.5
ethers ^6.13.0

Uniswap V3 架构

如果你已经对 Uniswap 熟悉,那么你可能会理解 Uniswap V2 的工作原理。快速回顾一下;Uniswap v2 使用常数乘积公式 (xy=k) 计算一对的交换价格。这些对中的流动性来自 LP(流动性提供者)的存款,涵盖了一对的整个价格范围。这使得流动性管理变得更简单,但资本效率较低,大部分流动性很少被使用。

而在 v3 中,引入了集中流动性,流动性提供者可以选择他们希望提供流动性的价格范围,从而实现更高的资本效率,更好地赚钱。这在资本投资和费用/收入回报上具有显著的效率提升;然而,这可能导致对流动性提供者的头寸进行更积极的管理,研究显示,暂时性损失可能更大 (来源)。

现在,让我们看看 Uniswap V3 协议的两个主要代码库:

核心

Core 合约是 Uniswap V3 的低级操作。它们负责管理流动性池、执行交换并包括:

  • UniswapV3Factory:部署新池并跟踪所有现有池。
  • UniswapV3Pool:处理特定池内交换、流动性提供和费用收集的核心功能。

外围

Periphery 合约是与核心合约交互的更高级合约,提供辅助功能和更简单的交互。这组合约包括:

  • SwapRouter:提供执行交换的界面,具有多次调用支持等高级功能。
  • NonfungiblePositionManager:管理流动性头寸的创建和管理,以 NFT 形式表示。
  • Quoter:帮助用户获取交换报价,而无需执行交换。

接下来,让我们深入代码。在开始之前,我们需要处理一些前提条件,比如获取对 RPC 的访问和在 Sepolia 测试网为测试钱包充值 ETH。如果你已经拥有这两项内容,请 跳过 这些前提条件部分。

项目前提:创建一个以太坊节点端点

要与以太坊区块链通信,你需要获取对节点的访问。虽然我们可以运行自己的节点,但在 QuickNode,我们使启动区块链节点变得快捷而简单。你可以在 这里 注册一个账户。一旦启动节点,获取 HTTP URL。它应如下所示:

Quicknode 主网端点的截图

项目前提:从 QuickNode 多链水龙头获取 ETH

为了在链上交换代币,你需要 ETH 来支付 gas 费。由于我们使用的是 Sepolia 测试网,我们可以从 多链 QuickNode 水龙头 获取一些测试 ETH。

导航到 多链 QuickNode 水龙头 并连接你的钱包(例如:MetaMask、Coinbase Wallet)或粘贴你的钱包地址以获取测试 ETH。请注意,使用 EVM 水龙头时,以太坊主网要求的主网余额是 0.001 ETH。你还可以通过推特获取或使用你的 QuickNode 账户登录以获得额外奖励!

多链水龙头的截图

项目设置

导航到 qn-guides-examples 仓库并克隆它:

git clone git@github.com:quiknode-labs/qn-guide-examples.git

接下来,导航到 defi/uniswap-v3-swaps 仓库:

cd qn-guide-examples/defi/uniswap-v3-swaps

然后,安装所需的依赖:

npm i

.env.example 重命名为 .env.local 并相应地填写环境变量。RPC_URL 的值应为你在上一步通过你的 QuickNode 控制面板创建的 QuickNode 节点端点,而 PRIVATE_KEY 的值应通过你的钱包安全设置获取(请查看这篇 文章 来了解如何操作)。确保保存文件。

在我们执行代码之前,让我们分析一下它的设计。

创建 Uniswap V3 交换脚本

我们将按部分介绍代码。你可以在 index.js 中找到完整版本 这里

首先,导入像 ethers、dotenv 和在创建合约实例时需要的 ABI 文件等依赖项。

导入依赖

import { ethers } from 'ethers'
import FACTORY_ABI from './abis/factory.json' assert { type: 'json' };
import QUOTER_ABI from './abis/quoter.json' assert { type: 'json' };
import SWAP_ROUTER_ABI from './abis/swaprouter.json' assert { type: 'json' };
import POOL_ABI from './abis/pool.json' assert { type: 'json' };
import TOKEN_IN_ABI from './abis/weth.json' assert { type: 'json' };
import 'dotenv/config'

设置常量

虽然本指南涵盖了 Sepolia,你可以根据需要将 部署地址 更改为其他兼容 EVM 的链。

以下代码设置常量变量,这些变量将定义在 Sepolia 上开发的合约地址,并创建 ethers.Contract 实例。

// 部署地址
const POOL_FACTORY_CONTRACT_ADDRESS = '0x0227628f3F023bb0B980b67D528571c95c6DaC1c'
const QUOTER_CONTRACT_ADDRESS = '0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3'
const SWAP_ROUTER_CONTRACT_ADDRESS = '0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E'

// 提供者、合约与签名器实例
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL)
const factoryContract = new ethers.Contract(POOL_FACTORY_CONTRACT_ADDRESS, FACTORY_ABI, provider);
const quoterContract = new ethers.Contract(QUOTER_CONTRACT_ADDRESS, QUOTER_ABI, provider)
const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider)

// 代币配置
const WETH = {
    chainId: 11155111,
    address: '0xfff9976782d46cc05630d1f6ebab18b2324d6b14',
    decimals: 18,
    symbol: 'WETH',
    name: 'Wrapped Ether',
    isToken: true,
    isNative: true,
    wrapped: true
  }

const USDC = {
    chainId: 11155111,
    address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
    decimals: 6,
    symbol: 'USDC',
    name: 'USD//C',
    isToken: true,
    isNative: true,
    wrapped: false
}

设置代币批准

无论我们交换哪个代币,我们都需要授予 SwapRouter 合约批准,以便为我们花费代币。

下面的函数需要一个 token addressABIamount(批准的数量)和 signer 对象。它使用 Contract.method.populateTransaction 函数在 ethers 中创建一个批准交易负载,然后使用 Signer.sendTransaction 发送交易并输出相关交易详情。

async function approveToken(tokenAddress, tokenABI, amount, wallet) {
    try {
        const tokenContract = new ethers.Contract(tokenAddress, tokenABI, wallet);

        const approveTransaction = await tokenContract.approve.populateTransaction(
            SWAP_ROUTER_CONTRACT_ADDRESS,
            ethers.parseEther(amount.toString())
        );

        const transactionResponse = await wallet.sendTransaction(approveTransaction);
        console.log(`-------------------------------`)
        console.log(`正在发送批准交易...`)
        console.log(`-------------------------------`)
        console.log(`交易已发送: ${transactionResponse.hash}`)
        console.log(`-------------------------------`)
        const receipt = await transactionResponse.wait();
        console.log(`批准交易已确认! https://sepolia.etherscan.io/txn/${receipt.hash}`);
    } catch (error) {
        console.error("在代币批准过程中发生错误:", error);
        throw new Error("代币批准失败");
    }
}

获取池信息

批准代币后下一步是获取池信息,即我们试图交换的代币对(例如:ETH/USDC)。

首先,我们在 Factory 合约上调用 getPool 函数,该函数将根据 tokenIntokenOutfee 返回一个池地址。获取到池地址后,我们可以创建一个 ethers 合约实例并初始化池元数据变量,以便稍后使用。

变量定义的快速总结:

  • fee:这是在池上执行的交换收取的费用。我们交易的池的费用值是 3000(即 3000/1000000(或 0.30%)),这个费用来自流动性提供者。
  • liquidity:在当前价格下,池可用的流动性数量。
  • sqrtPriceX96:池的当前价格,计算为 token0 和 token1 之间的比例(即 tokenIn/tokenOut)
  • tick:当前池价格的 tick。
async function getPoolInfo(factoryContract, tokenIn, tokenOut) {
    const poolAddress = await factoryContract.getPool(tokenIn.address, tokenOut.address, 3000);
    if (!poolAddress) {
        throw new Error("获取池地址失败");
    }
    const poolContract = new ethers.Contract(poolAddress, POOL_ABI, provider);
    const [token0, token1, fee] = await Promise.all([\
        poolContract.token0(),\
        poolContract.token1(),\
        poolContract.fee(),\
    ]);
    return { poolContract, token0, token1, fee };
}

在交换前获取报价

Uniswap 实现了一个 Quoter 合约来获取交易报价。我们将使用此合约确定我们的交易期望输出量而不实际执行它。quoteExactInputSingle 方法是报价合约中可用的四种方法之一。它为单个池中的交换提供了输出金额的报价,给定你希望交换的输入金额。

我们可以利用 ethers 提供的 callStatic 方法向以太坊节点提交状态更改事务,但要求节点在本地模拟状态更改而不是执行它。该方法的目的是不打算在链上调用。

async function quoteAndLogSwap(quoterContract, fee, signer, amountIn) {
    const quotedAmountOut = await quoterContract.quoteExactInputSingle.staticCall({
        tokenIn: WETH.address,
        tokenOut: USDC.address,
        fee: fee,
        recipient: signer.address,
        deadline: Math.floor(new Date().getTime() / 1000 + 60 * 10),
        amountIn: amountIn,
        sqrtPriceLimitX96: 0,
    });
    console.log(`-------------------------------`)
    console.log(`代币交换将导致: ${ethers.formatUnits(quotedAmountOut[0].toString(), USDC.decimals)} ${USDC.symbol} 交换 ${ethers.formatEther(amountIn)} ${WETH.symbol}`);
    const amountOut = ethers.formatUnits(quotedAmountOut[0], USDC.decimals)
    return amountOut;
}

创建交换交易

现在我们有了报价,我们可以开始创建交换交易。prepareSwapParams 函数旨在设置代币交换所需的参数。该函数接受 pool contract 实例、输入和输出代币、一个 signer、输入代币的数量(例如 amountIn)和预期的最小输出代币数量(例如 amountOut)。

async function prepareSwapParams(poolContract, signer, amountIn, amountOut) {
    return {
        tokenIn: WETH.address,
        tokenOut: USDC.address,
        fee: await poolContract.fee(),
        recipient: signer.address,
        amountIn: amountIn,
        amountOutMinimum: amountOut,
        sqrtPriceLimitX96: 0,
    };
}

async function executeSwap(swapRouter, params, signer) {
    const transaction = await swapRouter.exactInputSingle.populateTransaction(params);
    const receipt = await signer.sendTransaction(transaction);
    console.log(`-------------------------------`)
    console.log(`收据: https://sepolia.etherscan.io/tx/${receipt.hash}`);
    console.log(`-------------------------------`)
}

executeSwap 函数使用 exactInputSingle 方法填充包含给定参数的交易。它使用签名者发送交易,负责交易的签名和发送。在交易发送后,它在 Etherscan 上记录收据 URL。

执行交换

为了将所有函数串联在一起,我们创建一个 main 函数,该函数接受输入金额(以 Ether 格式),例如 .001。

然后,它按顺序调用我们覆盖的所有步骤:

  1. 收集池信息
  2. 获取报价
  3. 创建交换负载
  4. 发送交换交易
async function main(swapAmount) {
    const inputAmount = swapAmount
    const amountIn = ethers.parseUnits(inputAmount.toString(), 18);

    try {
        await approveToken(WETH.address, TOKEN_IN_ABI, amountIn, signer)
        const { poolContract, token0, token1, fee } = await getPoolInfo(factoryContract, WETH, USDC);
        console.log(`-------------------------------`)
        console.log(`正在获取报价: ${WETH.symbol} 到 ${USDC.symbol}`);
        console.log(`-------------------------------`)
        console.log(`交换金额: ${ethers.formatEther(amountIn)}`);

        const quotedAmountOut = await quoteAndLogSwap(quoterContract, fee, signer, amountIn);

        const params = await prepareSwapParams(poolContract, signer, amountIn, quotedAmountOut[0].toString());
        const swapRouter = new ethers.Contract(SWAP_ROUTER_CONTRACT_ADDRESS, SWAP_ROUTER_ABI, signer);
        await executeSwap(swapRouter, params, signer);
    } catch (error) {
        console.error("发生错误:", error.message);
    }
}

main(0.0001) // 根据需要更改金额

在运行执行命令之前,请确保你的提供者详情(例如:RPC_URL)和私钥已定义。接下来,运行命令:

node index.js

你将看到类似这样的输出:

-------------------------------
正在发送批准交易...
-------------------------------
交易已发送: 0x797429192800abec107ebfec73040bf88fd3c3ddabc2c0a66607393e14315ecd
-------------------------------
批准交易已确认! https://sepolia.etherscan.io/txn/0x797429192800abec107ebfec73040bf88fd3c3ddabc2c0a66607393e14315ecd
-------------------------------
正在获取报价: WETH 到 USDC
-------------------------------
交换金额: 0.0001
-------------------------------
代币交换将导致: 0.070107 USDC 交换 0.0001 WETH
-------------------------------
收据: https://sepolia.etherscan.io/tx/0xa508a0956e18ad23688be2b746c2a74bad4b04aad963c052ddcdc1202b3d9e8d
-------------------------------

通过查看区块浏览器验证交换。

就这样;你现在知道如何使用 exactInputSingle 函数在 Uniswap V3 上执行单一交换。

额外的想法与资源

最后的想法

订阅我们的 时事通讯,获取更多关于以太坊的文章和指南。如果你有任何反馈,随时通过 Twitter 联系我们。你也可以在我们充满才华的开发者的 Discord 社区服务器上与我们交谈 :)

我们 ❤️ 反馈!

告诉我们 如果你有任何反馈或对新主题的请求。我们很乐意听到你的意见。

  • 原文链接: quicknode.com/guides/def...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。