当前位置: 首页 > news >正文

剖析DeFi交易产品之UniswapV4:创建池子

本文首发于公众号:Keegan小钢


创建池子的底层函数是 PoolManager 合约的 initialize 函数,其代码实现并不复杂,如下所示:

function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData)externaloverrideonlyByLockerreturns (int24 tick)
{if (key.fee.isStaticFeeTooLarge()) revert FeeTooLarge();// see TickBitmap.sol for overflow conditions that can arise from tick spacing being too largeif (key.tickSpacing > MAX_TICK_SPACING) revert TickSpacingTooLarge();if (key.tickSpacing < MIN_TICK_SPACING) revert TickSpacingTooSmall();if (key.currency0 >= key.currency1) revert CurrenciesOutOfOrderOrEqual();if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks));if (key.hooks.shouldCallBeforeInitialize()) {if (key.hooks.beforeInitialize(msg.sender, key, sqrtPriceX96, hookData) != IHooks.beforeInitialize.selector){revert Hooks.InvalidHookResponse();}}PoolId id = key.toId();uint24 swapFee = key.fee.isDynamicFee() ? _fetchDynamicSwapFee(key) : key.fee.getStaticFee();tick = pools[id].initialize(sqrtPriceX96, _fetchProtocolFees(key), _fetchHookFees(key), swapFee);if (key.hooks.shouldCallAfterInitialize()) {if (key.hooks.afterInitialize(msg.sender, key, sqrtPriceX96, tick, hookData)!= IHooks.afterInitialize.selector) {revert Hooks.InvalidHookResponse();}}// On intitalize we emit the key's fee, which tells us all fee settings a pool can have: either a static swap fee or dynamic swap fee and if the hook has enabled swap or withdraw fees.emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks);
}

不过,里面有很多信息,我们需要一一拆解才能理解。

先来看入参,有三个:keysqrtPriceX96hookDatakey 指定了一个池子的唯一组成,sqrtPriceX96 是要初始化的根号价格,hookData 是需要传给 hooks 合约的初始化数据。

关于池子的唯一组成,前文我们已经讲过,PoolKey 包含了五个字段:

  • currency0:token0
  • currency1:token1
  • fee:费率
  • tickSpacing:tick 间隔
  • hooks:hooks 地址

currency0currency1 和以前版本的 token0token1 一样,是经过排序的,currency0 为数值较小的代币,currency1 则为数值较大的代币。tickSpacing 和 UniswapV3 的一样,就不再解释了。hooks 是自定义的地址,具体如何实现后面再细说。

fee 则和之前的版本不一样了。UniswapV3 的 fee 只指定了固定的交易费率,但 UniswapV4 的 fee 其实还包含了动态费用、hook 交易费用、hook 提现费用等标志。fee 总共 24 位(bit),前 4 位用来作为不同的标志位,具体解析在 FeeLibrary 里实现,以下是其代码实现:

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.20;library FeeLibrary {// 静态费率掩码uint24 public constant STATIC_FEE_MASK = 0x0FFFFF;// 支持动态费用的标志位uint24 public constant DYNAMIC_FEE_FLAG = 0x800000; // 1000// 支持hook交易费用的标志位uint24 public constant HOOK_SWAP_FEE_FLAG = 0x400000; // 0100// 支持hook提现费用的标志位uint24 public constant HOOK_WITHDRAW_FEE_FLAG = 0x200000; // 0010// 是否支持动态费用function isDynamicFee(uint24 self) internal pure returns (bool) {return self & DYNAMIC_FEE_FLAG != 0;}// 是否支持hook交易费用function hasHookSwapFee(uint24 self) internal pure returns (bool) {return self & HOOK_SWAP_FEE_FLAG != 0;}// 是否支持hook提现费用function hasHookWithdrawFee(uint24 self) internal pure returns (bool) {return self & HOOK_WITHDRAW_FEE_FLAG != 0;}// 静态费率是否超过最大值function isStaticFeeTooLarge(uint24 self) internal pure returns (bool) {return self & STATIC_FEE_MASK >= 1000000;}// 获取出静态手续费率function getStaticFee(uint24 self) internal pure returns (uint24) {return self & STATIC_FEE_MASK;}
}

静态费率最大值为 1000000,表示 100% 费用。那么要设置 0.3% 的费率的话那就是 3000,这个精度和 UniswapV3 是一致的。

那如果是要支持静态费率,就假设静态费率为 0.3%,同时又要支持 hook 交易费和提现费,则需要同时设置这两个标志位,那 fee 字段用 16 进制表示的值为 0xC01778。其二进制表示为:11000000000101110111000,前面两个 1 就是两个标志位,后面的 101110111000 其实就是十进制数 3000 的二进制数。

另外,UniswapV3 的费率只能在指定支持的几个费率中选择一个,而 UniswapV4 取消了这个限制,费率完全放开了,由池子的创建者自己去决定要设置多少费率。

回到 initialize 函数,函数声明里还有一个函数修饰器 onlyByLocker,这也是需要展开说明的一个地方。我们先来看这个函数修饰器的代码:

modifier onlyByLocker() {address locker = Lockers.getCurrentLocker();if (msg.sender != locker) revert LockedBy(locker);_;
}

它要求调用者需是当前的 locker。要成为 locker,需要调用 PoolManager 合约的 lock() 函数。以下是 lock() 函数的实现:

function lock(bytes calldata data) external override returns (bytes memory result) {//把调用者添加到locker队列里Lockers.push(msg.sender);//需在这个回调函数里完成所有事情,包括支付等操作result = ILockCallback(msg.sender).lockAcquired(data);if (Lockers.length() == 1) {//只有一个locker的情况下,做清理操作if (Lockers.nonzeroDeltaCount() != 0) revert CurrencyNotSettled();Lockers.clear();} else {//不止一个locker的情况下,移出顶部的lockerLockers.pop();}
}

其中,Lockers 是封装了锁定操作的库合约,push() 函数会把当前调用者添加到锁定者队列里,具体实现用到了 EIP-1153 所引入的 tstore 瞬态存储操作码。具体原理不在这里展开。

而下一步是调用了 msg.sender 的回调函数 lockAcquired(),这一步非常关键,透露出很多信息。首先,这说明了,调用者需是一个合约才行,而不能是一个 EOA 账户。然后,调用者需实现 ILockCallback 接口,该接口只定义了一个函数,就是 lockAcquired() 函数。最后,调用者合约需在 lockAcquired() 函数里实现所有事情,包括完成支付和各种不同的交易场景,其实也包括了调用 initialize 函数。

我的理解,lock() 函数调用者应该是一个路由合约,或不同功能模块用不同的合约实现,比如可以加一个工厂合约用于完成创建池子的操作,但目前 UniswapV4 还没看到关于路由合约或工厂合约的实现,所以具体逻辑不得而知。

总而言之,到了这里,我们就已经知道了,创建池子的调用者需是一个实现了 ILockCallback 接口的合约,先调用 lock() 函数成为 locker,再通过 lockAcquired() 回调函数调其 initialize 函数来完成初始化池子。

回到 initialize 函数的具体实现。前面是一些基本的校验,我们摘出来看一下:

// 静态费率不能超过最大值
if (key.fee.isStaticFeeTooLarge()) revert FeeTooLarge();
// tickSpacing需在限定的有效范围内
if (key.tickSpacing > MAX_TICK_SPACING) revert TickSpacingTooLarge();
if (key.tickSpacing < MIN_TICK_SPACING) revert TickSpacingTooSmall();
// currency0需小于currency1
if (key.currency0 >= key.currency1) revert CurrenciesOutOfOrderOrEqual();
// hooks地址需是符合条件的有效地址
if (!key.hooks.isValidHookAddress(key.fee)) revert Hooks.HookAddressNotValid(address(key.hooks));

接着,判断是否需要调用 beforeInitialize 的钩子函数,如下:

if (key.hooks.shouldCallBeforeInitialize()) {if (key.hooks.beforeInitialize(msg.sender, key, sqrtPriceX96, hookData) != IHooks.beforeInitialize.selector){revert Hooks.InvalidHookResponse();}
}

钩子函数需返回该函数的 selector

之后的三行代码实现初始化逻辑,代码如下:

// 把key转为id
PoolId id = key.toId();
// 读取出交易费率
uint24 swapFee = key.fee.isDynamicFee() ? _fetchDynamicSwapFee(key) : key.fee.getStaticFee();
// 执行实际的初始化操作
tick = pools[id].initialize(sqrtPriceX96, _fetchProtocolFees(key), _fetchHookFees(key), swapFee);

这里面有好几个跟费用相关的函数,有必要说明一下。

isDynamicFee() 就是前面所说的 FeeLibrary 库合约的函数,判断是否设置了支持动态费用的标志位。如果不支持,则通过 getStaticFee() 读取出静态费率;如果支持动态费用,则通过 _fetchDynamicSwapFee() 获取费率。 _fetchDynamicSwapFee() 函数是在抽象合约 Fees 里实现的,其实现非常简单,就两行代码,如下所示:

function _fetchDynamicSwapFee(PoolKey memory key) internal view returns (uint24 dynamicSwapFee) {dynamicSwapFee = IDynamicFeeManager(address(key.hooks)).getFee(msg.sender, key);if (dynamicSwapFee >= MAX_SWAP_FEE) revert FeeTooLarge();
}

可见,其实是调用了 hooks 合约的 getFee() 函数。即是说,要支持动态费用,则 hooks 合约需要实现 IDynamicFeeManager 接口的 getFee() 函数。

_fetchHookFees() 函数也类似,需要 hooks 合约实现 IHookFeeManager 接口的 getHookFees() 函数。不过 getHookFees() 的返回值里其实是由两个费用组合而成的,一个是交易费,一个是提现费。返回值是 24 位,前 12 位是交易费,后 12 位是提现费。

_fetchProtocolFees() 函数则是用于获取协议费,这就和 hooks 合约没有关系了,是由一个实现了 IProtocolFeeController 接口的合约进行管理的。只有合约 owner 可以设置这个合约地址。目前 UniswapV4 还没有提供关于该合约的实现,短期内应该也不会开启收取协议费。

最后,通过调用 pools[id].initialize() 函数完成内部的初始化工作。这里的关键就是 pools 状态变量,新建的池子状态最终其实也是存储在了 pools 里。它是一个 mapping 类型的变量,如下:

mapping(PoolId id => Pool.State) public pools;

其 value 存的是一个 Pool.State 对象,这是一个定义在 Pool 库合约里的结构体,具体包含了如下数据:

struct State {Slot0 slot0;uint256 feeGrowthGlobal0X128;uint256 feeGrowthGlobal1X128;uint128 liquidity;mapping(int24 => TickInfo) ticks;mapping(int16 => uint256) tickBitmap;mapping(bytes32 => Position.Info) positions;
}

如果和 UniswapV3 对比就会发现,其实就是将 UniswapV3Pool 里的大部分状态变量移到了 State 里。另外,slot0 的字段与 UniswapV3Pool 的有所不同,以下是其具体字段:

struct Slot0 {// the current priceuint160 sqrtPriceX96;// the current tickint24 tick;uint24 protocolFees;uint24 hookFees;// used for the swap fee, either static at initialize or dynamic via hookuint24 swapFee;
}

可看到,与 UniswapV3Pool 的 Slot0 相比,没有了预言机相关的状态数据。另外,关于费用的字段总共有三个:protocolFeeshookFeesswapFee

pools[id].initialize() 函数的实现是在 Pool 库合约里,其代码逻辑很简单,就是初始化了 slot0,代码如下:

function initialize(State storage self, uint160 sqrtPriceX96, uint24 protocolFees, uint24 hookFees, uint24 swapFee)internalreturns (int24 tick)
{//当前状态下的根号价格不为0,说明已经初始化过了if (self.slot0.sqrtPriceX96 != 0) revert PoolAlreadyInitialized();//根据根号价格算出ticktick = TickMath.getTickAtSqrtRatio(sqrtPriceX96);//初始化slot0self.slot0 = Slot0({sqrtPriceX96: sqrtPriceX96,tick: tick,protocolFees: protocolFees,hookFees: hookFees,swapFee: swapFee});
}

再回到 PoolManager 合约自身的 initialize() 函数,还剩下最后一段代码如下:

if (key.hooks.shouldCallAfterInitialize()) {if (key.hooks.afterInitialize(msg.sender, key, sqrtPriceX96, tick, hookData)!= IHooks.afterInitialize.selector) {revert Hooks.InvalidHookResponse();}
}
//发送事件
emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks);

完成了 PoolManager 自身的初始化逻辑之后,就是判断是否需要再调用 hooks 合约的 afterInitialize 钩子函数了。最后发送事件,整个创建池子的流程就完成了。

相关文章:

剖析DeFi交易产品之UniswapV4:创建池子

本文首发于公众号&#xff1a;Keegan小钢 创建池子的底层函数是 PoolManager 合约的 initialize 函数&#xff0c;其代码实现并不复杂&#xff0c;如下所示&#xff1a; function initialize(PoolKey memory key, uint160 sqrtPriceX96, bytes calldata hookData)externalover…...

速盾:cdn内容分发服务有哪些优势?

CDN&#xff08;Content Delivery Network&#xff09;是指内容分发网络&#xff0c;是一种将网络内容分发到全球各个地点的技术和架构。在现代互联网架构中&#xff0c;CDN已经变得非常重要。CDN通过将内容分发到靠近用户的服务器上&#xff0c;提供高速、高效的服务。下面是C…...

如何利用React和Python构建强大的网络爬虫应用

如何利用React和Python构建强大的网络爬虫应用 引言&#xff1a; 网络爬虫是一种自动化程序&#xff0c;用于通过互联网抓取网页数据。随着互联网的不断发展和数据的爆炸式增长&#xff0c;网络爬虫越来越受欢迎。本文将介绍如何利用React和Python这两种流行的技术&#xff0c…...

炎黄数智人:招商局集团推出AI数字员工“招小影”

引言 在全球数字化浪潮的推动下&#xff0c;招商局集团开启了一项具有里程碑意义的项目。招商局集团将引入AI数字员工“招小影”&#xff0c;这一举措不仅彰显了招商局集团在智能化转型方面的坚定决心&#xff0c;也为企业管理模式的创新注入了新的活力。 “招小影”是一款集成…...

【开发篇】明明配置跨域声明,为什么却仍可以发送HTTP请求

一、问题 在SpringBoot项目中&#xff0c;明确指定仅允许指定网站跨域访问&#xff1a; 为什么开发人员却仍旧可以通过HTTP工具调用接口&#xff1f; 二、为什么 在回答这个问题之前&#xff0c;我们首先要了解一下什么是CORS&#xff01; 1、什么是CORS CORS的全称为跨域资源…...

单片机中有FLASH为啥还需要EEROM?

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「单片机的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 一是EEPROM操作简单&…...

Qt的源码目录集合(V5.12.12版本)

目录 1.QObject实现源码 2.qml中的ListModel实现源码 3.qml中的JS运行时的环境和数据类型源码 1.QObject实现源码 .\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\kernel\qobject.h .\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\kernel\qobject.cpp .\Qt\Qt5.12.12\5…...

记因hive配置文件参数运用不当导致 sqoop MySQL导入数据到hive 失败的案例

sqoop MySQL导入数据到hive报错 ERROR tool.ImportTool: Encountered IOException running import job: java.io.IOException: Hive exited with status 64 报错解释&#xff1a; 这个错误表明Sqoop在尝试导入数据到Hive时遇到了问题&#xff0c;导致Hive进程异常退出。状态码…...

自动化邮件通知:批处理脚本的通讯增强

自动化邮件通知&#xff1a;批处理脚本的通讯增强 引言 批处理脚本在自动化任务中扮演着重要角色&#xff0c;无论是在系统管理、数据处理还是日常任务调度中。然而&#xff0c;批处理脚本的自动化能力可以通过集成邮件通知功能得到显著增强。当脚本执行完毕或在执行过程中遇…...

236、二叉树的最近公共祖先

前提&#xff1a; 所有 Node.val 互不相同 。p ! qp 和 q 均存在于给定的二叉树中。 代码如下&#xff1a; class Solution { public:TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {if (root q || root p || root NULL) return root;TreeN…...

WebStorm 2024 for Mac JavaScript前端开发工具

Mac分享吧 文章目录 效果一、下载软件二、开始安装1、双击运行软件&#xff08;适合自己的M芯片版或Intel芯片版&#xff09;&#xff0c;将其从左侧拖入右侧文件夹中&#xff0c;等待安装完毕2、应用程序显示软件图标&#xff0c;表示安装成功3、打开访达&#xff0c;点击【文…...

【Redis7】零基础篇

1 课程概述 2 Redis入门概述 2.1 是什么 Redis是基于内存的KV键值对内存数据库 Redis&#xff1a;Remote Dictionary Server(远程字典服务)是完全开源的&#xff0c;使用ANSIC语言编写遵守BSD协议&#xff0c;是一个高性能的Key-Value数据库提供了丰富的数据结构&#xff0c…...

[ROS 系列学习教程] 建模与仿真 - 使用 ros_control 控制差速轮式机器人

ROS 系列学习教程(总目录) 本文目录 一、差速轮式机器人二、差速驱动机器人运动学模型三、对外接口3.1 输入接口3.2 输出接口 四、控制器参数五、配置控制器参数六、编写硬件抽象接口七、控制机器人移动八、源码 ros_control 提供了多种控制器&#xff0c;其中 diff_drive_cont…...

Ubuntu22.04使用Systemd设置ROS 2开机自启动遇到的问题

在查找网上的各种开机自启动资料配置好开机自启动后&#xff0c;使用ros2 topic list不能显示话题。 1、问题解决&#xff1a;用户问题与domenID问题2、ROS2开机自启动服务教程3、多个ROS2开机自启动服务教程 1、问题解决&#xff1a;用户问题与domenID问题 在root用户下能看到…...

AI安全研究滞后?清华专家团来支招

在21世纪的科技浪潮中&#xff0c;人工智能&#xff08;AI&#xff09;无疑是最为耀眼的一抹亮色。随着技术的不断突破&#xff0c;AI正以前所未有的速度融入我们的日常生活&#xff0c;重塑着社会、经济乃至人类文明的面貌。然而&#xff0c;在这股汹涌澎湃的发展洪流中&#…...

12寸FAB 信息部内外工作职责的一些划分构思

FAB的信息部&#xff0c;也常被称为IT部门或信息化部门&#xff0c;承担着确保整个制造工厂的信息技术系统高效、安全运行的职责。以下是 一、FAB信息部的一些关键部门职责&#xff1a; 1. 战略规划&#xff1a;制定和实施信息技术战略&#xff0c;以支持FAB的长期业务目标和增…...

css做旋转星球可举一反三

<!DOCTYPE html> <html lang"en"><head> <meta charset"UTF-8" /> <title>旋转的星球</title> <style type"text/css">.box {/*position: relative;*/position: absolute;width: 139px;height: 139p…...

AcWing 1256:扩展二叉树

【题目来源】https://www.acwing.com/problem/content/1258/【题目描述】 由于先序、中序和后序序列中的任一个都不能唯一确定一棵二叉树&#xff0c;所以对二叉树做如下处理&#xff0c;将二叉树的空结点用 补齐&#xff0c;如图所示。 我们把这样处理后的二叉树称为原二叉树…...

三维家:SaaS的IT规模化降本之道|OceanBase 《DB大咖说》(十一)

OceanBase《DB大咖说》第 11 期&#xff0c;我们邀请到了三维家的技术总监庄建超&#xff0c;来分享他对数据库技术的理解&#xff0c;以及典型 SaaS 场景在数据库如何实现规模化降本的经验与体会。 庄建超&#xff0c;身为三维家的技术总监&#xff0c;独挑大梁&#xff0c;负…...

ai智能语音机器人是如何影响客户体验的?电销机器人部署

随着人工智能技术的进步&#xff0c;越来越多的企业在寻求如何将人工智能技术融合到现有的商业模式上&#xff0c;进而实现自动化、智能化。在通信行业大量使用智能语音机器人、聊天机器人、客服机器人时&#xff0c;它能和“客户体验”并驾齐驱吗&#xff0c;还是可以让客户体…...

web vue 项目 Docker化部署

Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段&#xff1a; 构建阶段&#xff08;Build Stage&#xff09;&#xff1a…...

Cursor实现用excel数据填充word模版的方法

cursor主页&#xff1a;https://www.cursor.com/ 任务目标&#xff1a;把excel格式的数据里的单元格&#xff0c;按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例&#xff0c;…...

椭圆曲线密码学(ECC)

一、ECC算法概述 椭圆曲线密码学&#xff08;Elliptic Curve Cryptography&#xff09;是基于椭圆曲线数学理论的公钥密码系统&#xff0c;由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA&#xff0c;ECC在相同安全强度下密钥更短&#xff08;256位ECC ≈ 3072位RSA…...

逻辑回归:给不确定性划界的分类大师

想象你是一名医生。面对患者的检查报告&#xff08;肿瘤大小、血液指标&#xff09;&#xff0c;你需要做出一个**决定性判断**&#xff1a;恶性还是良性&#xff1f;这种“非黑即白”的抉择&#xff0c;正是**逻辑回归&#xff08;Logistic Regression&#xff09;** 的战场&a…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级

在互联网的快速发展中&#xff0c;高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司&#xff0c;近期做出了一个重大技术决策&#xff1a;弃用长期使用的 Nginx&#xff0c;转而采用其内部开发…...

在WSL2的Ubuntu镜像中安装Docker

Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包&#xff1a; for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台

🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...

重启Eureka集群中的节点,对已经注册的服务有什么影响

先看答案&#xff0c;如果正确地操作&#xff0c;重启Eureka集群中的节点&#xff0c;对已经注册的服务影响非常小&#xff0c;甚至可以做到无感知。 但如果操作不当&#xff0c;可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...

Go 语言并发编程基础:无缓冲与有缓冲通道

在上一章节中&#xff0c;我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道&#xff0c;它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好&#xff0…...

从实验室到产业:IndexTTS 在六大核心场景的落地实践

一、内容创作&#xff1a;重构数字内容生产范式 在短视频创作领域&#xff0c;IndexTTS 的语音克隆技术彻底改变了配音流程。B 站 UP 主通过 5 秒参考音频即可克隆出郭老师音色&#xff0c;生成的 “各位吴彦祖们大家好” 语音相似度达 97%&#xff0c;单条视频播放量突破百万…...