You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
所以说,虽然 quoteExactInputSingle 是 public 修饰的,但是其本质是一个 view 。
或许你还会问,虽然 revert() 函数的确能取消调用,并且退还 Gas 费,那也只是退还剩余部分,已被计算花销了的 Gas 并不会退还,那客户端不是还是要为了抓取一个汇率而付费吗?其实,在客户端(如 ethers)中,会使用 contract.callStatic.quoteExactInputSingle(…) 的方式,让节点以“假装”不会有状态变化的方式来尝试调用一个 public 函数,来达到没有 Gas 花费而又抓取了汇率的效果。
// Rather than executing the state-change of a transaction, it is possible to ask a node to// pretend that a call is not state-changing and return the result.// This does not actually change any state, but is free. This in some cases can be used// to determine if a transaction will fail or succeed.contract.callStatic.METHOD_NAME( ...args[,overrides])⇒Promise<any>
The text was updated successfully, but these errors were encountered:
笔者前两日学习了 Uniswap 的白皮书以及源码,具体学习笔记可看这篇博客。在阅读其源码的过程中,学习到了一些 Solidity 编程技巧,在此文记录分享。
MLOAD vs SLOAD
在源码更新头寸(position)函数中,在从 storage 载入合约状态时,有如下额外注释(
// SLOAD for gas optimization
):从代码中可见,由于函数内有 10 处需要引用
slot0
storage 变量内的字段。而访问 storage 变量(SLOAD)的成本是比访问 memory 变量(MLOAD)昂贵不少的。所以源码在函数最上方先使用一次 SLOAD 将变量slot0
载入到内存中,以节省 Gas 开销。我们可以使用如下代码试验一下:测试结果为:
可见函数如果单纯同样次数读取 storage 内的变量,先存入 memory 的话,开销大约只有 1/4 左右。
可预知的合约地址
在源码创建代币交易对池的地方,我们可以看到:
代码在创建合约时,先将可预知的交易对池的元信息,通过
abi.encode
编码后,然后作为slat
参数,在合约的创建中使用。在官方文档的解释中,若合约的创建中,指定了salt
参数,则会使用create2
机制创建合约,即若指定的salt
不变,则创建的合约地址不变,只需使用bytes1(0xff)
,工厂合约的地址,salt
,合约代码的哈希以及初始化参数,就可还原。官方的例子如下:
Uniswap 源码中,还原地址的逻辑也是类似的:
如此一来,合约的地址就不需再上链进行抓取,一方面方便了部署,另一方也节省了存储与抓取的 Gas 开销。
不一致的 ERC20 接口定义
由于一些争论,目前 ERC20 接口,对
transfer
等发送代币的函数的返回值定义有两种,一种为若发送失败,则会返回false
,如 openzeppelin 就是这么定义的,还有一种为若发送失败,则直接在函数中进行revert()
,函数没有返回值。面对这种同一函数,同样参数,不同返回值的情况,Uniswap V3 源码是这么处理的:首先,由于 Solidity 的 Function Selector 定义中可知,Function Selector 是函数原型(prototype)哈希的前 4 字节,而函数原型是由函数名和它的所有参数类型决定的。所以
abi.encodeWithSelector
可以允许代码在未知函数返回类型的情况下,调用函数。在调用之后,代码通过第二个返回值data
来判断返回布尔版本的transfer
是否调用成功,第一个返回值bool success
来判断revert()
版本的调用成功(若调用成功就无返回值即data.length == 0
)与否。“预计算”
由于在 Uniswap V3 中,同一种代币对的交换,可能同时存在许许多多不同价格区间的流动性池,所以,在查询当前给定的 A 代币能交换多少 B 代币(即代币的价格)时,并不能像 V2 那样,抓取一下两边代币在流动性池中的数量(仅需要调用一下
view
级别的函数),客户端利用核心公式x * y = (x + Δx) * (y − Δy) = k
一推导,就能够知道。所以在 V3 中,只能像实际交换代币那样,从当前价格开始,一个个头寸池流过去,才能计算出最终可以得到的 B 代币数量。而在 V3 源码种,交换代币函数是一个接近 200 行的大函数,且会改变合约的状态,若用户刚想知道一下代币间的汇率,就要支付 Gas 费,也是不合理的。在这种情况下, Uniswap V3 利用了 solidity 中,revert(string reason)
函数可以终止当前函数调用,并向调用者退还剩余 Gas 费的机制,做了一个实现,首先我们看一下交换代币函数的具体代码:我们可以看到,
swap
函数会要求请求它的合约,自身实现一个IUniswapV3SwapCallback
的接口。函数在计算出可交换出的另一种代币的数量后,在返回之前,会先调用一下请求合约自身实现的IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback
作为回调,并且最终可交换的 B 代币数会作为参数。而在抓取代币价格的合约Quoter
中的具体实现为:从代码中可见,首先使用了
try-catch
捕获revert
,然后使用assembly
读取 free memory pointer ,将汇率信息读出来。具体assembly
的运用,可以参阅这篇文档。所以说,虽然
quoteExactInputSingle
是public
修饰的,但是其本质是一个view
。或许你还会问,虽然
revert()
函数的确能取消调用,并且退还 Gas 费,那也只是退还剩余部分,已被计算花销了的 Gas 并不会退还,那客户端不是还是要为了抓取一个汇率而付费吗?其实,在客户端(如 ethers)中,会使用contract.callStatic.quoteExactInputSingle(…)
的方式,让节点以“假装”不会有状态变化的方式来尝试调用一个public
函数,来达到没有 Gas 花费而又抓取了汇率的效果。正如 ehters 的 callStatic 函数文档 中所描述的:
The text was updated successfully, but these errors were encountered: