价格预言机的使用总结(二):UniswapV2篇_虚拟币交易所平台,数字货币,NFT

admin 阅读:31 2024-04-01 12:40:40 评论:0
美化布局示例

欧易(OKX)最新版本

【遇到注册下载问题请加文章最下面的客服微信】永久享受返佣20%手续费!

APP下载   全球官网 大陆官网

币安(Binance)最新版本

币安交易所app【遇到注册下载问题请加文章最下面的客服微信】永久享受返佣20%手续费!

APP下载   官网地址

火币HTX最新版本

火币老牌交易所【遇到注册下载问题请加文章最下面的客服微信】永久享受返佣20%手续费!

APP下载   官网地址

前言


foreword 比特币今日价格行情网_okx交易所app_永续合约_比特币怎么买卖交易_虚拟币交易所平台

该系列的前一篇文章介绍了Chainlink价格预言机的使用,其目前也被大部分 DeFi 应用所使用,但依然存在局限性。首先是所支持的 Token 的覆盖率还不全,尤其是长尾资产,大多还未支持,比如SHIB,目前只在 BSC 主网有SHIB/USD的Price Feed,而其它网络的都还没有,连Ethereum的都还没支持。其次,有些资产的偏差阈值较大,价格更新也比较慢,可能长达十几二十个小时才会更新价格,比如BNT。

这时候就需要考虑其它价格预言机了,而 UniswapV2 和 UniswapV3 都是不错的选择。

本篇先来聊聊如何使用 UniswapV2 作为价格预言机。

UniswapV2 价格预言机

UniswapV2 使用的价格预言机称为TWAP(Time-Weighted Average Price),即时间加权平均价格。不同于链下聚合的 Chainlink 取自多个不同交易所的数据作为数据源,TWAP 的数据源来自于 Uniswap 自身的交易数据,价格的计算也都是在链上执行的,因此,TWAP 属于链上预言机。

TWAP 的原理比较简单,首先,在UniswapV2Pair合约中,会存储两个变量price0CumulativeLast和price1CumulativeLast,在_update()函数中会更新这两个变量,其相关代码如下:

contract?UniswapV2Pair?{ ??... ??uint32?private?blockTimestampLast; ??uint?public?price0CumulativeLast; ??uint?public?price1CumulativeLast; ??... ??//?update?reserves?and,?on?the?first?call?per?block,?price?accumulators ??function?_update(uint?balance0,?uint?balance1,?uint112?_reserve0,?uint112?_reserve1)?private?{ ????... ????uint32?blockTimestamp?=?uint32(block.timestamp?%?2**32); ????uint32?timeElapsed?=?blockTimestamp?-?blockTimestampLast; ????if?(timeElapsed? ?0? ?_reserve0?!=?0? ?_reserve1?!=?0)?{ ??????//?*?never?overflows,?and?+?overflow?is?desired ??????price0CumulativeLast?+=?uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0))?*?timeElapsed; ??????price1CumulativeLast?+=?uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1))?*?timeElapsed; ????} ????blockTimestampLast?=?blockTimestamp; ????... }

price0CumulativeLast 和 price1CumulativeLast 分别记录了 token0 和 token1 的累计价格。所谓累计价格,其代表的是整个合约历史中每一秒的 Uniswap 价格总和。且只会在每个区块第一笔交易时执行累加计算,累加的值不是当前区块的第一笔交易的价格,而是在这之前的最后一笔交易的价格,所以至少也是上个区块的价格。取自之前区块的价格,可以大大提高操控价格的成本,所以自然也提高了安全性。

如上图所示,合约的第一个区块为 Block 122,这时候,价格和时间差都为 0,所以累计价格也为 ?0。到了下一个区块 Block 123,这时候取自上个区块的最后一口价格 10.2,且经过的时间差为 7,因此就可以计算出累计价格 priceCumulative = 10.2 * 7 = 71.4。再到下个区块 Block 124,取自上一口价格 10.3,两个区块间的时间差为 8,那此时的累计价格就变成了 71.4 + (10.3 * 8) = 153.8。Block 125 的时候也同理,上口价格为 10.5,区块时间差为 5,所以最新的累计价格就变成了 153.8 + (10.5 * 5) = 206.3。

有了这个基础之后,就可以计算 TWAP 了。

固定时间窗口 TWAP

计算 TWAP 的原理也是非常简单,如上图所示,这是计算时间间隔为 1 小时的 TWAP,取自开始和结束时的累计价格和两区块当时的时间戳,两者的累计价格相减,再除以两者之间的时间差,就算出这 1 小时内的 TWAP 价格了。

这是 TWAP 最简单的计算方式,也称为固定时间窗口的 TWAP。下面来讲讲具体如何实现。

Uniswap 官方也有提供了一个示例代码来计算固定时间窗口的 TWAP,其代码放在 v2-periphery 项目中:

https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleOracleSimple.sol

该示例代码也比较简单,我们直接贴上代码看看:

pragma?solidity?=0.6.6; import? @uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol ; import? @uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol ; import? @uniswap/lib/contracts/libraries/FixedPoint.sol ; import? ../libraries/UniswapV2OracleLibrary.sol ; import? ../libraries/UniswapV2Library.sol ; //?fixed?window?oracle?that?recomputes?the?average?price?for?the?entire?period?once?every?period //?note?that?the?price?average?is?only?guaranteed?to?be?over?at?least?1?period,?but?may?be?over?a?longer?period contract?ExampleOracleSimple?{ ????using?FixedPoint?for?*; ????uint?public?constant?PERIOD?=?24?hours; ????IUniswapV2Pair?immutable?pair; ????address?public?immutable?token0; ????address?public?immutable?token1; ????uint????public?price0CumulativeLast; ????uint????public?price1CumulativeLast; ????uint32??public?blockTimestampLast; ????FixedPoint.uq112x112?public?price0Average; ????FixedPoint.uq112x112?public?price1Average; ????constructor(address?factory,?address?tokenA,?address?tokenB)?public?{ ????????IUniswapV2Pair?_pair?=?IUniswapV2Pair(UniswapV2Library.pairFor(factory,?tokenA,?tokenB)); ????????pair?=?_pair; ????????token0?=?_pair.token0(); ????????token1?=?_pair.token1(); ????????price0CumulativeLast?=?_pair.price0CumulativeLast();?//?fetch?the?current?accumulated?price?value?(1?/?0) ????????price1CumulativeLast?=?_pair.price1CumulativeLast();?//?fetch?the?current?accumulated?price?value?(0?/?1) ????????uint112?reserve0; ????????uint112?reserve1; ????????(reserve0,?reserve1,?blockTimestampLast)?=?_pair.getReserves(); ????????require(reserve0?!=?0? ?reserve1?!=?0,? ExampleOracleSimple:?NO_RESERVES );?//?ensure?that?there s?liquidity?in?the?pair ????} ????function?update()?external?{ ????????(uint?price0Cumulative,?uint?price1Cumulative,?uint32?blockTimestamp)?= ????????????UniswapV2OracleLibrary.currentCumulativePrices(address(pair)); ????????uint32?timeElapsed?=?blockTimestamp?-?blockTimestampLast;?//?overflow?is?desired ????????//?ensure?that?at?least?one?full?period?has?passed?since?the?last?update ????????require(timeElapsed? =?PERIOD,? ExampleOracleSimple:?PERIOD_NOT_ELAPSED ); ????????//?overflow?is?desired,?casting?never?truncates ????????//?cumulative?price?is?in?(uq112x112?price?*?seconds)?units?so?we?simply?wrap?it?after?division?by?time?elapsed ????????price0Average?=?FixedPoint.uq112x112(uint224((price0Cumulative?-?price0CumulativeLast)?/?timeElapsed)); ????????price1Average?=?FixedPoint.uq112x112(uint224((price1Cumulative?-?price1CumulativeLast)?/?timeElapsed)); ????????price0CumulativeLast?=?price0Cumulative; ????????price1CumulativeLast?=?price1Cumulative; ????????blockTimestampLast?=?blockTimestamp; ????} ????//?note?this?will?always?return?0?before?update?has?been?called?successfully?for?the?first?time. ????function?consult(address?token,?uint?amountIn)?external?view?returns?(uint?amountOut)?{ ????????if?(token?==?token0)?{ ????????????amountOut?=?price0Average.mul(amountIn).decode144(); ????????}?else?{ ????????????require(token?==?token1,? ExampleOracleSimple:?INVALID_TOKEN ); ????????????amountOut?=?price1Average.mul(amountIn).decode144(); ????????} ????} }

PERIOD指定为了 24 小时,说明这个示例计算 TWAP 的固定时间窗口为 24 小时,即每隔 24 小时才更新一次价格。

该示例也只保存一个交易对的价格,即token0-token1的价格。price0Average和price1Average分别就是 token0 和 token1 的 TWAP 价格。比如,token0 为 WETH,token1 为 USDC,那 price0Average 就是 WETH 对 USDC 的价格,而 price1Average 则是 USDC 对 WETH 的价格。

update()函数就是更新 TWAP 价格的函数,这一般需要链下程序的定时任务来触发,按照这个示例的话,就是链下的定时任务需要每隔 24 小时就定时触发调用 update() 函数。

update()函数的实现逻辑也和上面所述的公式一致:

读取出当前最新的累计价格和当前的时间戳;

计算出当前时间和上一次更新价格时的时间差 timeElapsed,要求该时间差需要达 24 小时;

根据公式 TWAP = (priceCumulative - priceCumulativeLast) / timeElapsed 计算得到最新的 TWAP,即priceAverage;

更新 priceCumulativeLast 和 blockTimestampLast 为当前最新的累计价格和时间戳。

不过,有一点需要注意,因为priceCumulative本身计算存储时是做了左移 112 位的操作的,所以计算所得的priceAverage也是左移了 112 位的。

consult()函数则可查询出用 TWAP 价格计算可兑换的数量。比如,token0 为 WETH,token1 为 USDC,假设 WETH 的价格为 3000 USDC,查询 consult() 时,若传入的参数 token 为 token0 的地址,amountIn 为 2,那输出的 amountOut 则为 3000 * 2 = 6000,可理解为若支付 2 WETH,就可根据价格换算成 6000 USDC。

滑动时间窗口 TWAP

固定时间窗口 TWAP 的原理和实现,比较简单,但其最大的不足就是价格变化不够平滑,时间窗口越长,价格变化就可能会越陡峭。因此,在实际应用中,更多其实是用滑动时间窗口的 TWAP。

所谓滑动时间窗口 TWAP,就是说,计算 TWAP 的时间窗口并非固定的,而是滑动的。这种算法的主要原理就是将时间窗口划分为多个时间片段,每过一个时间片段,时间窗口就会往右滑动一格,如下图所示:

上图所示的时间窗口为 1 小时,划分为了 6 个时间片段,每个时间片段则为 10 分钟。那每过 10 分钟,整个时间窗口就会往右滑动一格。而计算 TWAP 时的公式则没有变,依然还是取自时间窗口的起点和终点。如果时间窗口为 24 小时,按照固定时间窗口算法,每隔 24 小时 TWAP 价格才会更新,但使用滑动时间窗口算法后,假设时间片段为 1 小时,则 TWAP 价格是每隔 1 小时就会更新。

Uniswap 官方也同样提供了这种滑动时间窗口 TWAP 实现的示例代码,其 Github 地址为:

https://github.com/Uniswap/v2-periphery/blob/master/contracts/examples/ExampleSlidingWindowOracle.sol

我们也贴上代码看看:

pragma?solidity?=0.6.6; import? @uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol ; import? @uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol ; import? @uniswap/lib/contracts/libraries/FixedPoint.sol ; import? ../libraries/SafeMath.sol ; import? ../libraries/UniswapV2Library.sol ; import? ../libraries/UniswapV2OracleLibrary.sol ; //?sliding?window?oracle?that?uses?observations?collected?over?a?window?to?provide?moving?price?averages?in?the?past //?`windowSize`?with?a?precision?of?`windowSize?/?granularity` //?note?this?is?a?singleton?oracle?and?only?needs?to?be?deployed?once?per?desired?parameters,?which //?differs?from?the?simple?oracle?which?must?be?deployed?once?per?pair. contract?ExampleSlidingWindowOracle?{ ????using?FixedPoint?for?*; ????using?SafeMath?for?uint; ????struct?Observation?{ ????????uint?timestamp; ????????uint?price0Cumulative; ????????uint?price1Cumulative; ????} ????address?public?immutable?factory; ????//?the?desired?amount?of?time?over?which?the?moving?average?should?be?computed,?e.g.?24?hours ????uint?public?immutable?windowSize; ????//?the?number?of?observations?stored?for?each?pair,?i.e.?how?many?price?observations?are?stored?for?the?window. ????//?as?granularity?increases?from?1,?more?frequent?updates?are?needed,?but?moving?averages?become?more?precise. ????//?averages?are?computed?over?intervals?with?sizes?in?the?range: ????//???[windowSize?-?(windowSize?/?granularity)?*?2,?windowSize] ????//?e.g.?if?the?window?size?is?24?hours,?and?the?granularity?is?24,?the?oracle?will?return?the?average?price?for ????//???the?period: ????//???[now?-?[22?hours,?24?hours],?now] ????uint8?public?immutable?granularity; ????//?this?is?redundant?with?granularity?and?windowSize,?but?stored?for?gas?savings? ?informational?purposes. ????uint?public?immutable?periodSize; ????//?mapping?from?pair?address?to?a?list?of?price?observations?of?that?pair ????mapping(address?= ?Observation[])?public?pairObservations; ????constructor(address?factory_,?uint?windowSize_,?uint8?granularity_)?public?{ ????????require(granularity_? ?1,? SlidingWindowOracle:?GRANULARITY ); ????????require( ????????????(periodSize?=?windowSize_?/?granularity_)?*?granularity_?==?windowSize_, ???????????? SlidingWindowOracle:?WINDOW_NOT_EVENLY_DIVISIBLE ????????); ????????factory?=?factory_; ????????windowSize?=?windowSize_; ????????granularity?=?granularity_; ????} ????//?returns?the?index?of?the?observation?corresponding?to?the?given?timestamp ????function?observationIndexOf(uint?timestamp)?public?view?returns?(uint8?index)?{ ????????uint?epochPeriod?=?timestamp?/?periodSize; ????????return?uint8(epochPeriod?%?granularity); ????} ????//?returns?the?observation?from?the?oldest?epoch?(at?the?beginning?of?the?window)?relative?to?the?current?time ????function?getFirstObservationInWindow(address?pair)?private?view?returns?(Observation?storage?firstObservation)?{ ????????uint8?observationIndex?=?observationIndexOf(block.timestamp); ????????//?no?overflow?issue.?if?observationIndex?+?1?overflows,?result?is?still?zero. ????????uint8?firstObservationIndex?=?(observationIndex?+?1)?%?granularity; ????????firstObservation?=?pairObservations[pair][firstObservationIndex]; ????} ????//?update?the?cumulative?price?for?the?observation?at?the?current?timestamp.?each?observation?is?updated?at?most ????//?once?per?epoch?period. ????function?update(address?tokenA,?address?tokenB)?external?{ ????????address?pair?=?UniswapV2Library.pairFor(factory,?tokenA,?tokenB); ????????//?populate?the?array?with?empty?observations?(first?call?only) ????????for?(uint?i?=?pairObservations[pair].length;?i? ?granularity;?i++)?{ ????????????pairObservations[pair].push(); ????????} ????????//?get?the?observation?for?the?current?period ????????uint8?observationIndex?=?observationIndexOf(block.timestamp); ????????Observation?storage?observation?=?pairObservations[pair][observationIndex]; ????????//?we?only?want?to?commit?updates?once?per?period?(i.e.?windowSize?/?granularity) ????????uint?timeElapsed?=?block.timestamp?-?observation.timestamp; ????????if?(timeElapsed? ?periodSize)?{ ????????????(uint?price0Cumulative,?uint?price1Cumulative,)?=?UniswapV2OracleLibrary.currentCumulativePrices(pair); ????????????observation.timestamp?=?block.timestamp; ????????????observation.price0Cumulative?=?price0Cumulative; ????????????observation.price1Cumulative?=?price1Cumulative; ????????} ????} ????//?given?the?cumulative?prices?of?the?start?and?end?of?a?period,?and?the?length?of?the?period,?compute?the?average ????//?price?in?terms?of?how?much?amount?out?is?received?for?the?amount?in ????function?computeAmountOut( ????????uint?priceCumulativeStart,?uint?priceCumulativeEnd, ????????uint?timeElapsed,?uint?amountIn ????)?private?pure?returns?(uint?amountOut)?{ ????????//?overflow?is?desired. ????????FixedPoint.uq112x112?memory?priceAverage?=?FixedPoint.uq112x112( ????????????uint224((priceCumulativeEnd?-?priceCumulativeStart)?/?timeElapsed) ????????); ????????amountOut?=?priceAverage.mul(amountIn).decode144(); ????} ????//?returns?the?amount?out?corresponding?to?the?amount?in?for?a?given?token?using?the?moving?average?over?the?time ????//?range?[now?-?[windowSize,?windowSize?-?periodSize?*?2],?now] ????//?update?must?have?been?called?for?the?bucket?corresponding?to?timestamp?`now?-?windowSize` ????function?consult(address?tokenIn,?uint?amountIn,?address?tokenOut)?external?view?returns?(uint?amountOut)?{ ????????address?pair?=?UniswapV2Library.pairFor(factory,?tokenIn,?tokenOut); ????????Observation?storage?firstObservation?=?getFirstObservationInWindow(pair); ????????uint?timeElapsed?=?block.timestamp?-?firstObservation.timestamp; ????????require(timeElapsed? =?windowSize,? SlidingWindowOracle:?MISSING_HISTORICAL_OBSERVATION ); ????????//?should?never?happen. ????????require(timeElapsed? =?windowSize?-?periodSize?*?2,? SlidingWindowOracle:?UNEXPECTED_TIME_ELAPSED ); ????????(uint?price0Cumulative,?uint?price1Cumulative,)?=?UniswapV2OracleLibrary.currentCumulativePrices(pair); ????????(address?token0,)?=?UniswapV2Library.sortTokens(tokenIn,?tokenOut); ????????if?(token0?==?tokenIn)?{ ????????????return?computeAmountOut(firstObservation.price0Cumulative,?price0Cumulative,?timeElapsed,?amountIn); ????????}?else?{ ????????????return?computeAmountOut(firstObservation.price1Cumulative,?price1Cumulative,?timeElapsed,?amountIn); ????????} ????} }

要实现滑动时间窗口算法,就需要将时间分段,还需要保存每个时间段的priceCumulative。在这实现的示例代码中,定义了结构体Observation,用来保存每个时间片段的数据,包括两个 token 的priceCumulative和记录的时间点timestamp。还定义了pairObservations用来存储每个 pair 的Observation数组,而数组实际的长度取决于将整个时间窗口划分为多少个时间片段。

windowSize表示时间窗口大小,比如 24 小时,granularity是划分的时间片段数量,比如 24 段,periodSize则是每时间片段的大小,比如 1 小时,是由 windowSize / granularity 计算所得。这几个值都在构造函数中进行了初始化。

触发update()函数则更新存储最新时间片段的 observation,如时间片段大小为 1 小时,即每隔 1 小时就要触发 update() 函数一次。因为这个示例中是支持多个 pair 的,所以 update() 时需要指定所要更新的两个 token。

而查询当前 TWAP 价格的计算就在consult()函数里实现了。首先,先获取到当前时间窗口里的第一个时间片段的 observation,也算出当前时间与第一个 observation 时间的时间差,且读取出当前最新的 priceCumulative,之后就在 computeAmountOut() 函数里计算得到最新的 TWAP 价格 priceAverage,且根据 amountIn 算出了 amountOut 并返回。

总结

本文我们主要介绍了被广泛使用的一种链上预言机TWAP(时间加权平均价格),且介绍了固定时间窗口和滑点时间窗口两种算法的 TWAP。虽然,TWAP 是由 Uniswap 推出的,但因为很多其他 DEX 也采用了和 Uniswap 一样的底层实现,如 SushiSwap、PancakeSwap 等,所以这些 DEX 也可以用同样的算法计算出对应的 TWAP。

但使用 UniswapV2 的 TWAP,其主要缺陷就是需要链下程序定时触发 update() 函数,存在维护成本。UniswapV3的 TWAP 则解决了这个问题,下一篇会来聊聊其具体是如何实现的。

文章首发于「Keegan小钢」公众号:

https://mp.weixin.qq.com/s?__biz=MzA5OTI1NDE0Mw==&mid=2652494441&idx=1&sn=57a97690390b93770c5a906dce4157c8&chksm=8b685079bc1fd96f9ab60cc1b41b8642abf807a13a37c12f05a280be2e03f3a9288a047b5739&token=1584634265&lang=zh_CN#rd

查看更多

文字格式和图片示例

注册有任何问题请添加 微信:MVIP619 拉你进入群

弹窗与图片大小一致 文章转载注明 网址:https://netpsp.com/?id=39520

美化布局示例

欧易(OKX)最新版本

【遇到注册下载问题请加文章最下面的客服微信】永久享受返佣20%手续费!

APP下载   全球官网 大陆官网

币安(Binance)最新版本

币安交易所app【遇到注册下载问题请加文章最下面的客服微信】永久享受返佣20%手续费!

APP下载   官网地址

火币HTX最新版本

火币老牌交易所【遇到注册下载问题请加文章最下面的客服微信】永久享受返佣20%手续费!

APP下载   官网地址
可以去百度分享获取分享代码输入这里。
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

发表评论
平台列表
美化布局示例

欧易(OKX)

  全球官网 大陆官网

币安(Binance)

  官网

火币(HTX)

  官网

Gate.io

  官网

Bitget

  官网

deepcoin

  官网
关注我们

若遇到问题,加微信客服---清歌

搜索
排行榜
扫一扫,加我为微信好友加我为微信好友