本文是作者第一次尝试阅读Defi源码,先从V2源码入手,然后分析V3的改进,并不涉及V3源码的分析。
作为最大的DEX,值得看看官方文档学习学习。 本文是作者第一次尝试阅读Defi源码,先从V2源码入手,然后分析V3的改进,并不涉及V3源码的分析。
UniSwap作为DEX,最大的功能就是币币交换了。
x
和y
分别代表交易对中两种代币的储备余额。例如,在一个 ETH - USDT 交易对中,x
可能是 ETH 的储备数量,y
是 USDT 的储备数量。k
是一个常数,这意味着在每一次交易过程中,从这次交易的角度来看,x
和y
的乘积(k
)必须保持不变。
下面是一个UniSwap完成价格定价的例子:
例如,假设初始时,在一个 ETH - USDT 的交易对中,ETH 的储备量x = 20
个,USDT 的储备量y = 500
个,那么根据公式可得`k = 20 500 = 10000。 现在有用户进行了一笔小额交易,用 USDT来换取
1个 ETH 。交易完成后,ETH 的储备量
x变为
20 - 1 = 19个。为了保证
k不变,依旧是
10000,则此时 USDT 的储备量
y就变为
10000 ÷ 19 ≈ 526.32个(这里保留两位小数),也就是说对于这次交易而言,1eth=26.32USDT。可以看到,通过少量 ETH 的兑换,USDT 的储备量相应增加了,而
k始终保持为
10000`,整体交易对的这个 “内在价值” 没有改变,只是两种资产的储备数量进行了调整以适应交易。
可见v2的价格定价是根据constant product formula 来决定的,而v1有专门的定价函数来确认价格,通过constant product formula 来确认价格更加方便安全快捷。
除了依赖公式以外,还依赖预言机**:市场价格是动态变化的。为了防止价格操纵(如抢先交易),Uniswap 的定价也会参考预言机提供的 “公平” 价格。预言机可以是简单的链外市场价格观察,也可以是原生 V2 预言机等其他方式。由于套利机制,同区块内交易对储备的比率往往接近 “真实” 市场价格。如果市场上 ETH - USDT 的外部参考价格发生变化,或者流动性池中的储备量因为其他交易而改变,那么你能换取的 USDT 数量也会相应地根据上述定价机制进行调整。https://github.com/Uniswap/v2-core 源码地址。
v2由两部分合约组成,core合约和periphery合约。 core合约就是主心骨,但它对用户不太友好:用户直接调用core合约做交易并不是推荐的方法,periphery合约就是和主心骨打交道的外围合约,用户用periphery合约作为中介和core合约交互完成交易。
core合约由三个文件构成:
UniswapV2Factory.sol 它的主要工作是为每个唯一的代币对创建且仅创建一个智能合约。
UniswapV2Pair.sol充当自动做市商以及跟踪资金池代币余额。它们还公开可用于构建去中心化价格预言机的数据。
UniswapV2ERC20.sol Pair合约的ERC实现
下面将会从这三个sol文件入手分析uniswap的逻辑。
Pair合约的ERC实现
官方文档https://docs.uniswap.org/contracts/v2/reference/smart-contracts/pair-erc-20
pragma solidity =0.5.16;
import './interfaces/IUniswapV2ERC20.sol';
import './libraries/SafeMath.sol';
contract UniswapV2ERC20 is IUniswapV2ERC20 {
using SafeMath for uint;
string public constant name = 'Uniswap V2';
string public constant symbol = 'UNI-V2';
uint8 public constant decimals = 18;
uint public totalSupply;
mapping(address => uint) public balanceOf;
mapping(address => mapping(address => uint)) public allowance;
bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;
event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);
constructor() public {
uint chainId;
assembly {
chainId := chainid
}
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);
}
function _mint(address to, uint value) internal {
totalSupply = totalSupply.add(value);
balanceOf[to] = balanceOf[to].add(value);
emit Transfer(address(0), to, value);
}
function _burn(address from, uint value) internal {
balanceOf[from] = balanceOf[from].sub(value);
totalSupply = totalSupply.sub(value);
emit Transfer(from, address(0), value);
}
function _approve(address owner, address spender, uint value) private {
allowance[owner][spender] = value;
emit Approval(owner, spender, value);
}
function _transfer(address from, address to, uint value) private {
balanceOf[from] = balanceOf[from].sub(value);
balanceOf[to] = balanceOf[to].add(value);
emit Transfer(from, to, value);
}
function approve(address spender, uint value) external returns (bool) {
_approve(msg.sender, spender, value);
return true;
}
function transfer(address to, uint value) external returns (bool) {
_transfer(msg.sender, to, value);
return true;
}
function transferFrom(address from, address to, uint value) external returns (bool) {
if (allowance[from][msg.sender] != uint(-1)) {
allowance[from][msg.sender] = allowance[from][msg.sender].sub(value);
}
_transfer(from, to, value);
return true;
}
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
//构建签名摘要
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
}
除了permit函数外其它函数都好理解,都是ERC20的标准函数。
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
//构建签名摘要
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
}
在 Uniswap(以及许多去中心化金融应用)中,用户经常需要授权合约对其资产(如 ERC - 20 代币)进行操作。传统的授权方式可能需要用户发送一笔交易来调用授权函数(approve),这可能涉及到一些交易成本(如以太坊网络的燃气费),并且操作相对繁琐。 Uniswap 的permit函数提供了一种更便捷的授权机制。它允许用户通过签名(通常是使用以太坊账户的私钥进行签名)来授权合约对其资产进行特定操作,而无需发送专门的授权交易。这类似于提供了一个 “离线签名授权” 的功能,提高了授权的效率,特别是在一些复杂的交易场景或者批量授权操作中非常有用。
以下是一个更详细的permit
函数使用示例,假设在一个简化的去中心化金融场景中,这个签名校验是ERC-712的杰作:
角色与目标
approve
函数)来授权交易所合约使用她的代币,因为她希望节省燃气费用并且加快交易流程。生成签名(Alice 端)
v = 27
,r = 0x123456789abcdef...
(64 字节的哈希值),s = 0xfedcba987654321...
(64 字节的哈希值)。提交授权(Alice 向 DEXContract 提交)
v
、r
、s
)以及授权的相关信息(自己的地址 Alice、DEXContract 的地址、授权金额 10、截止时间 1609545600)发送给 DEXContract。验证签名(DEXContract 端)
ecrecover
函数,结合接收到的v
、r
、s
以及解码后的授权数据结构中的信息(特别是包含的 Alice 的地址等)来验证签名的有效性。Pair合约主要实现了三个方法:mint(添加流动性)、burn(移除流动性)、swap(兑换)。
当有人提供流动性时,提供LP代币。
function mint(address to) external lock returns (uint liquidity) {
//amount0,amount1为获取用户提供的两种代币的值
(uint112 _reserve0, uint112 _reserve1,) = getReserves();
uint balance0 = IERC20(token0).balanceOf(address(this));
uint balance1 = IERC20(token1).balanceOf(address(this));
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);
//计算交易费,只有当对应factory的feeTO为true时才调用
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
if (_totalSupply == 0) {//总价值为0说明是刚开的流动性池
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {//计算本次提供的流动性能获得的代币
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity); //基于流动性提供者LP代币
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}
移除流动性,烧毁LP代币兑换为原货币
function burn(address to) external lock returns (uint amount0, uint amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)]; //这里我曾诞生出一个疑问:通过address(this)获取LP代币不是意味着获取整个矿池的LP代币吗,后来分析了一下,只有当移除流动性时用户才会把LP代币发到该合约上,LP代币被销毁后再完成移除流动性交易的转账操作后,所以这里获取的LP代币其实就是用户移除流动性时发过来的。 那是不是可以通过钓鱼钓LP代币来盈利?
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution,这样计算能够回收手续费
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
}
关于手续费:总的来说,在burn
函数中,通过计算 LP 在流动性池中的份额,并根据当前包含手续费累积后的余额按比例分配两种代币,从而实现了 LP 在退出流动性提供时回收手续费收益的操作。这种计算方式保证了 LP 能够公平地获取自己应得的包括手续费在内的资产部分。
关于手续费收益:一个池子tvl越少,同一笔钱投进去占得份额越大,取手续费时所分得比例越多,越赚。总体而言进行流动性挖矿时tvl越少,交易量越大的池子手续费收益越高。
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
//这两个 if 语句根据要输出的代币数量是否大于 0,分别调用 _safeTransfer 函数(应该是合约内自定义的用于安全转移代币的函数,可能包含一些异常处理机制确保代币转移的安全性)将 amount0Out 数量 的第一种代币转移到 to 地址,以及将 amount1Out 数量的第二种代币转移到 to 地址。这里采用的是一种 “乐观” 转移的方式,先尝试进行转移操作,后续再根据实际情况进行进一步的验证和调整。
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
//判断用户的代币输入是否符合预期
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors k值判断
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); //当用户进行币币交换时,为了补偿流动性提供者(LP)提供流动性的服务以及覆盖平台的运营成本等,交易通常会收取一定的手续费。这个手续费会导致交换后的两种代币储备量乘积(balance0Adjusted.mul(balance1Adjusted))比没有手续费情况下的理论乘积(uint(_reserve0).mul(_reserve1))稍微大一点。
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
在调用时,用户需要传入必要的参数,包括 amount0Out
(要输出的代币 A 的数量)、amount1Out
(要输出的代币 B 的数量,可能为 0,如果用户只希望输出一种代币来换取另一种代币)、to
(用户自己的钱包地址或者其他指定的接收交换后代币的地址),以及可能的 data
(字节类型的数据,用于一些特殊的回调或者额外的信息传递,通常如果没有特殊需求可以为空)。
它的主要工作是为每个唯一的代币对创建且仅创建一个Pair合约。重点看看createPair函数,下面的代码中我对其进行了注释方便理解。
官方解释文档:https://docs.uniswap.org/contracts/v2/reference/smart-contracts/factory
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';
contract UniswapV2Factory is IUniswapV2Factory {
address public feeTo;
address public feeToSetter;
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
function allPairsLength() external view returns (uint) {
return allPairs.length;
}
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
//这个地方用于创建Pair合约
bytes memory bytecode = type(UniswapV2Pair).creationCode; //获取Pair合约字节码
bytes32 salt = keccak256(abi.encodePacked(token0, token1)); //获取token0和token1作为参数的bytes32作为salt
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt) //生成的地址是根据salt来演算的,我们希望提供A/B地址的时候可以直接算出pair(A,B)的地址
//v=0:向新创建的pair合约中发送的ETH代币数量(单位wei)
//p=add(bytecode, 32):合约字节码的起始位置
//此处为什么要add 32呢?因为bytecode类型为bytes,根据ABI规范,bytes为变长类型,在编码时前32个字节存储bytecode的长度,接着才是bytecode的真正内容,因此合约字节码的起始位置在bytecode+32字节
//n=mload(bytecode):合约字节码总字节长度
//根据上述说明,bytecode前32个字节存储合约字节码的真正长度(以字节为单位),而mload的作用正是读出传入参数的前32个字节的值,因此mload(bytecode)就等于n
//s=salt:s为自定义传入的salt,即token0和token1合并编码
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
function setFeeTo(address _feeTo) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeTo = _feeTo;
}
function setFeeToSetter(address _feeToSetter) external {
require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
feeToSetter = _feeToSetter;
}
}
一堆工具方法,用来方便的获取价格或者各类数据
https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol
由于UniswapV2Router01在处理FeeOnTransferTokens时有bug,目前已不再使用。此处我们仅介绍最新版的UniswapV2Router02合约。
为前端提供交易和流动性管理功能的所有基本要求
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IUniswapV2Pair(pair).mint(to);
}
由于Router02是直接与用户交互的,因此接口设计需要从用户使用场景考虑。addLiquidity提供了8个参数:
用户提交交易后,该交易被矿工打包的时间是不确定的,因此提交时的代币价格与交易打包时的价格可能不同,通过amountMin可以控制价格的浮动范围,防止被矿工或机器人套利;同样,deadline可以确保该交易在超过指定时间后将失效。
在core合约中提到,如果用户提供流动性时的代币价格与实际价格有差距,则只会按照较低的汇率得到流动性代币,多余的代币将贡献给整个池子。_addLiquidity可以帮助计算最佳汇率。如果是首次添加流动性,则会先创建交易对合约;否则根据当前池子余额计算应该注入的最佳代币数量。
最后调用core合约mint方法铸造流动性代币。
首先将流动性代币发送到pair合约,根据收到的流动性代币占全部代币比例,计算该流动性代表的两种代币数量。合约销毁流动性代币后,用户将收到对应比例的代币。如果低于用户设定的最低预期(amountAMin/amountBMin),则回滚交易。
// **** REMOVE LIQUIDITY ****
function removeLiquidity(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, 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');
}
用户正常移除流动性时,需要两个操作:
除非第一次授权了最大限额的代币,否则每次移除流动性都需要两次交互,这意味着用户需要支付两次手续费。而使用removeLiquidityWithPermit方法,用户可以通过签名方式授权Router合约花费自己的代币,无需单独调用approve,只需要调用一次移除流动性方法即可完成操作,节省了gas费用。同时,由于离线签名不需要花费gas,因此可以每次签名仅授权一定额度的代币,提高安全性。
function removeLiquidityWithPermit(
address tokenA,
address tokenB,
uint liquidity,
uint amountAMin,
uint amountBMin,
address to,
uint deadline,
bool approveMax, uint8 v, bytes32 r, bytes32 s
) external virtual override returns (uint amountA, uint amountB) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
uint value = approveMax ? uint(-1) : liquidity;
IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
(amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
}
根据指定的输入代币,获得最多的输出代币
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
该方法实现交易的第二个场景,根据指定的输出代币,使用最少的输入代币完成兑换。
function swapTokensForExactTokens(
uint amountOut,
uint amountInMax,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
concentrated liquidity(集中流动性)
x⋅y = k
这个公式是核心。这里x
和y
分别代表交易对中的两种资产数量,k
是一个常数。例如在 ETH - USDT 交易对中,x
可以是 ETH 的数量,y
是 USDT 的数量。这个公式保证了在任何交易情况下,两种资产数量的乘积保持不变。x
增加,为了保持k
不变,USDT 的数量y
就会相应减少,并且价格是根据交易前后两种资产数量的变化自动计算出来的。x⋅y = k
的机制下,交易对的价格可以在理论上的任何位置出现,所以 LP 需要在整个价格曲线上都有资产储备。Uniswap v3版本的AMM曲线从无限变成局部。通过引入虚拟流动性,允许用户只在一段价格区间内提供流动性。在 x⋅y=k 的函数曲线图中,为了满足让用户可以选择只在[a,b]价格区间内提供流动性。
UniswapV3将连续的价格范围,分割成有限个离散的价格点。每一个价格对应一个 tick,用户在设置流动性的价格区间时,只能选择这些离散的价格点中的某一个作为流动性的边界价格。
如下是一个例子:
假设在 Uniswap v3 的交易世界里,你是一位精明的流动性提供者(LP),你精心选择将自己的资金投入到 ETH - USDT 交易对中,并设置了一个流动性头寸,其价格区间为 [1000 USDT/ETH, 1100 USDT/ETH]。
现在把价格空间想象成一个巨大的尺子,而 Ticks 就是尺子上的刻度。每一个微小的刻度代表着价格 0.01% 的变化,这就像是尺子上极其精细的标记,它们将整个价格范围划分得密密麻麻。对于你设置的这个价格区间 [1000, 1100],就相当于在这把尺子上选定了一段特定的区域。
当市场价格在 900 USDT/ETH 或者 1200 USDT/ETH 时,这就好像市场的价格 “指针” 还在你选定的价格区间 “尺子段” 之外徘徊。此时,你提供的流动性不会加入到交易中。
然而,一旦市场价格的 “指针” 踏入了 1000 USDT/ETH 到 1100 USDT/ETH 这个区间,你提供的流动性就会加入到交易。
每达到一个新的 Tick 刻度,合约都会迅速做出反应,根据新的价格情况和流动性分布,灵活地调整资产交换的策略。就好像一场紧张刺激的接力赛,每跨越一个 Tick,合约就会利用新激活的 Tick 边界头寸内可能存在的休眠流动性(如果有其他 LP 也在类似边界设置了头寸),如同接过接力棒一般,持续为市场提供充足且高效的流动性,确保交易的 “赛道” 畅通无阻
v2在提供流动性中途,不能提取手续费,必须取消流动性才能把手续费提出,v3解决了这个问题随时可以取出
Uniswap v3 引入多种手续费池,目的是为了让每个交易对能够根据自身的特点(如资产的波动性、交易频率等)选择更合适的手续费机制,从而优化交易成本和收益结构,吸引更多的流动性提供者(LP)和交易者,提高整个交易生态系统的效率和稳定性。默认允许创建三个手续费等级:0.05%,0.30%和1%。可以通过UNI治理添加更多手续费等级。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!