skip to content

In-kind Funding of Squeeth

By 0xstan & 0xmc

Squeeth funding mechanism

overview

Squeeth (squared ETH) 是由 Opyn 的研究团队(Zubin Koticha、Andrew Leone、Alexis Gauba、Aparna Krishnan)、Dave White 和 Dan Robinson 发明的一种新的金融衍生品, 是第一个 Power Perpetual defi 产品。

在机制方面,Squeeth 的功能类似于永续合约(Perpetual Contracts),跟踪 ETH² 而不是 ETH。它提供类似期权的敞口(pure convexity, pure gamma),无需到期行权,有效地将大部分期权市场流动性整合到单个 ERC20 代币中。

简而言之, Squeeth 实现了永续期权,对于 Uniswap LP、所有 ETH/USD 期权以及任何具有曲线收益的产品来说,都是一种非常有效的对冲工具。

In-kind Funding Flow

不同于大多数永续合约产品,Squeeth 并非使用传统的现金结算模式,而是使用其独有的 funding 结算机制:In-kind funding。得益于简洁的计价模式,该机制不仅提升 gas 使用效率,也极大提升了 Squeeth 产品的可组合性:所有的价值都体现在了 ERC20 币价中,包括需要一直结算的 funding payment。

SqueethNormalizationFactor.png 点击看大图 Squeeth In-kind Funding

上图表达了 Squeeth In-kind Funding 的机制,工程实现中通过一个全局变量 NormalizationFactor 来对 oSQTH 的价格进行折价,变向实现了 funding payment 的结算(从多方转移到空方)。我们来一步一步的梳理流程,拆解上述流程图:

Index Price & Mark Price

IndexPrice=(ETHtwap/10000)2Index Price = (ETHtwap / 10000)^2

  • Oracle price 是从 ETH / USDC Uniswap V3 Pool 中获取的 ETH TWAP 价格
  • Index Price 即为 ETH^2 (缩放后的)
  • ETH^2 价格具有很高的数量级,为了方便计算,Squeeth 先将 ETH twap 价格除以 10000,再进行运算

MarkPrice=oSQTHtwapETHtwap/10000/NormalizationFacotrMark Price = oSQTH twap \cdot ETH twap / 10000 / NormalizationFacotr

  • oSQTH twap 是从 oSQTH / ETH Uniswap V3 Pool 中获取的 oSQTH TWAP 价格
  • 由于 oSQTH 与 ETH 组成交易对,并非 USDC,所以该价格还需要乘以 ETH twap 换算成 USDC 计价
  • 而由于 oSQTH 的价格会不断受到 funding 影响而折价,这里计算 Mark Price 时还需要除以 NormalizationFactor

Normalization Factor

常规的永续合约定义中通常使用 premium 来衡量标的价格与指数价格的偏离程度,即两个价格之差:

Premium=MarkPriceIndexPricePremium = MarkPrice - IndexPrice

而 funding rate 即为:

FundingRate=PremiumIndexPrice=MarkPriceIndexPriceIndexPriceFundingRate = \frac{Premium}{IndexPrice} = \frac{MarkPrice - IndexPrice}{IndexPrice}

但是在 Squeeth 中通过放缩 Normalization Factor 来实现,所以每次都通过 IndexPrice 与 MarkPrice 的比例作为乘数来更新其值;

NormalizationFactor=MultiplierNormalizationFactoroldNormalization Factor = Multiplier \cdot Normalization Factor_{old}

由于是通过 Factor 折价模式来模拟 funding 的结算,每次用户与合约交互都会触发 Factor 更新,而非固定时间间隔来结算 funding,故而还需要考虑时间的权重,这里使用时间间隔与结算周期的比值作为幂来调整:

Multiplier=(IndexPriceMarkPrice)ΔtimeFundingPeriodMultiplier = (\frac{IndexPrice}{MarkPrice})^{\frac{\Delta time}{FundingPeriod}}

FundingPeriod=420hours(17.5days)FundingPeriod = 420 hours (17.5 days)

  • NormalizationFactor 记录 oSQTH 折价的系数,大部分与合约交互的操作都会触发该全局变量的更新,以 Multiplier 进行放缩
  • Multiplier 衡量的是距离上次结算到当下时刻的 Mark 与 Index(经过时间加权处理的)偏离程度
  • delta time 是距离上次结算到现在的时间间隔 ,FundingPeriod 是固定的 420 小时,即 17.5 天

collateralValue

抵押价值,用户存入的 ETH 抵押品价值,以 ETH 计价

collateralvalue=ETHamountETHtwapcollateralvalue = ETH amount \cdot ETH twap

  • 直接 ETH 存入 controller 合约
  • 抵押 oSQTH-ETH LP token,即注入到 oSQTH-ETH Uniswap V3 pool 中的流动性作为抵押品,用户将 LP 凭证转让给 controller 合约,每次计算抵押价值时,会查询该 LP token 当前时刻内部资产总价值 (以 ETH 计价,包括 ETH 和 oSQTH 两种资产) 计到抵押价值内 (由于 LP 做市受到交易影响,其内部 ETH 资产数量可能发生变化)

debtValue

债务价值,即用户从合约中 mint 出的 oSQTH token (同一个 Vault 账户内)总价值

debtValue=oSQTHamountNormalizationFactorETHtwap/10000debtValue = oSQTH amount \cdot NormalizationFactor \cdot ETH twap / 10000

collaterRatio=collateralvaluedebtValuecollaterRatio = \frac{collateralvalue}{debtValue}

需要注意的是,计算债务价值需要用 NormalizationFactor 进行折价,且抵押率不得低于 150%,如果低于该阈值可以被触发清算

long oSQTH

做多 oSQTH 实际上是直接在 oSQTH/WETH uniswap V3 pool 交易买入 oSQTH。

short oSQTH

做空 oSQTH 需要先存入 ETH 或 oSQTH/ETH LP 作为抵押资产,mint 出 oSQTH,随后马上在 oSQTH-ETH Uniswap V3 pool 中卖出。

空方用户可以 burn oSQTH 赎回抵押资产,赎回之后若仍有债务,依旧需要满足抵押率不低于 150%的限制。

Effect of In-kind Funding on oSQTH's price

MarkPrice 受到市场中 oSQTH 的价格影响,而做多和做空都是在 Uniswap oSQTH/ETH pool 中交易,影响 oSQTH 价格,进而对 MarkPrice 产生影响。

做多时,用户直接在 Uniswap 买入 oSQTH ,使 MarkPrice 上升,Mulptilier 减小,Factor 下降速度更快,即多方(持有 oSQTH 的一方)资产折价更快,相当于多方向空方支付 funding payment;

而多方 funding 支出变高,会倾向于减小手里头寸规模,卖出 oSQTH,从而让 MarkPrice 回落;

做空时,空方抵押 ETH,mint 出 oSQTH 随即在 pool 中卖掉,使 MarkPrice 下降,Mulptilier 增大,Factor 下降速度减慢,即多方的 funding 支出减小,多方会倾向于增大头寸规模,买入 oSQTH,从而让 MarkPrice 回升。

LifeCycle of a Squeeth

为了更好理解 Squeeth 的 In-kind funding 机制,这里以做空 oSQTH token 的过程举例,帮助读者理解。

Sate0

statevalue
eth/usd twap3000
oSQTH/eth twap0.315
NormalizationFactor1
collateralValue0.6
debtValue0.3
collateralRatio200%

初始状态,假设 ETH 价格 3000, oSQTH 以 ETH 计价 0.315, NormalizationFactor 初始值为 1.

做空 oSQTH token,对于合约的操作就是抵押 ETH, mint oSQTH,然后立刻在 Uniswap 市场卖掉。

此时我们希望 mint 1 枚 oSQTH token,然后立刻卖掉,首先需要添加 ETH 作为抵押物,保证抵押率大于 150% 即可(当然,如果刚好 150%很容易被清算),这里我们添加 0.6 ETH 作为抵押资产。接着卖出 1 oSQTH 获得 0.315 ETH

那么抵押资产价值和债务价值分别为(以 ETH 计价):

collateralValue=ETHamount=0.6collateralValue = ETH amount = 0.6

debtValue=oSQTHamountNormalizationFactorETHtwap/10000=113000/10000=0.3debtValue = oSQTH amount \cdot NormalizationFactor \cdot ETH twap / 10000 = 1 \cdot 1 \cdot 3000 / 10000 = 0.3

collateralRatio=collateralValuedebtValue=0.60.3=200%collateralRatio = \frac{collateralValue}{debtValue} = \frac{0.6}{0.3} = 200\%

State1

statevalue
eth/usd twap4000
oSQTH/eth twap0.378
NormalizationFactor0.9
collateralValue0.6
debtValue0.36
collateralRatio166.66%

一段时间后 ETH 价格上涨至 4000,NormalizationFactor 变成 0.9。

collateralValue=ETHamount=0.6collateralValue = ETH amount = 0.6

debtValue=10.94000/10000=0.36debtValue = 1 \cdot 0.9 \cdot 4000 / 10000 = 0.36

collateralRatio=collateralValuedebtValue=0.60.36=166.66%collateralRatio = \frac{collateralValue}{debtValue} = \frac{0.6}{0.36} = 166.66\%

此时如果我们平仓做空头寸,则需要从 Uniswap 市场中买入 1 oSQTH,进行 burn 操作,消除债务。

开仓和平仓的收益情况是最终亏损 0.036 ETH:1 * 0.315 - 1 * 0.378 = -0.036 ETH

在这段时间中,看起来并没有发生显性的 funding 结算(以现金方式从抵押资产中直接结算 funding 费用),但 Squeeth 的 In-kind funding 机制,其实是将结算结果直接反应到了价格中,在上述例子中, 多方已经向空方支付了 funding payment。

你应该已经注意到了,State1 时 NormalizationFactor 从 1 变成 0.9,由于 Factor 减少,debtValue 实际上会减小。

假如此时 Factor 仍然是 1,那么:

debtValue=114000/10000=0.4debtValue = 1 \cdot 1 \cdot 4000 / 10000 = 0.4

collateralRatio=collateralValuedebtValue=0.60.4=150%collateralRatio = \frac{collateralValue}{debtValue} = \frac{0.6}{0.4} = 150\%

抵押率已经触及清算线,ETH 的价格哪怕再波动 0.01 都有可能被外部清算者清算掉我们的头寸,这显然是我们不愿见到的。如果我们想保持 166.66% 的抵押率(面临清算的风险没有那么极端),那么就需要补充抵押资产。这两个抵押率之间的差距实际上就是因为这段时间其他多头头寸向我们这个空头头寸支付的 funding payment。


我们不妨来按照常规永续合约的现金结算模式来模拟一遍结算过程。

State0

IndexPrice=(ETHtwap/10000)2=(3000/10000)2=0.09IndexPrice = (ETHtwap / 10000)^2 = (3000 / 10000)^2 = 0.09

MarkPrice=oSQTHtwapETHtwap/10000/NormalizationFacotr=0.3153000/10000/1=0.0945MarkPrice = oSQTH twap \cdot ETH twap / 10000 / NormalizationFacotr = 0.315 \cdot 3000 / 10000 / 1 = 0.0945

fundingrate=MarkPriceIndexPriceIndexPrice=0.09450.090.09=5%funding rate = \frac{MarkPrice - IndexPrice}{IndexPrice} = \frac{0.0945 - 0.09}{0.09} = 5\%

此时 funding rate 是 5%, 大于 0, 多方支付 funding payment 给空方 (longs to pay shorts)

State1

IndexPrice=(ETHtwap/10000)2=(4000/10000)2=0.16IndexPrice = (ETHtwap / 10000)^2 = (4000 / 10000)^2 = 0.16

MarkPrice=oSQTHtwapETHtwap/10000/NormalizationFacotr=0.3784000/10000/0.9=0.168MarkPrice = oSQTH twap \cdot ETH twap / 10000 / NormalizationFacotr = 0.378 \cdot 4000 / 10000 / 0.9 = 0.168

fundingrate=MarkPriceIndexPriceIndexPrice=0.1680.160.16=5%funding rate = \frac{MarkPrice - IndexPrice}{IndexPrice} = \frac{0.168 - 0.16}{0.16} = 5\%

虽然 oSQTH twap 价格要小于 ETH twap / 10000,但由于 NormalizationFacotr 从 1 变成了 0.9,所以计算出的 MarkPrice 仍然大于 IndexPrice,funding rate 仍是 5%。

Dive Into the Contracts

Squeeth contract architecture

上图是合约结构的示意图,用户直接交互的合约是蓝色的 Controller,白色方块自上而下分别是:

  • ShortPowerPerp 管理用户做空头寸,每一个做空头寸都是一枚 NFT,即 sSQTH,其中抵押资产价值和债务价值是独立计算的
  • WPowerPerp 是 PowerPerp 的 ERC20 token,即 oSQTH
  • Oracle 是提供 TWAP 价格的预言机模块,从 Uniswap 相关 pool 中提取 TWAP 价格

粉色部分则是 Uniswap 的交易路由、两个 pool 以及流动性头寸管理合约。

这便是 Squeeth 合约的主要结构。接下来我们将深入代码,探寻 In-kind Funding 机制的实现细节。

mintPowerPerpAmount

存入抵押资产(ETH)同时 mint PowerPerp token,即 oSQTH。入参分别是:

  • _vaultId 空头头寸的 id,如果是新开头寸直接传 0 值,会自动新建一个空头头寸,并将 id 返回给用户
  • _powerPerpAmount 希望 mint 出的 oSQTH 数量
  • _uniTokenId 传入 Uniswap ETH/USDC pool LP token, 将代表该 pool 流动性凭证的 NFT 质押给 Squeeth 作为抵押物,如果不使用 LP 作为抵押资产,此处直接传 0

Controller 提供了两种调用接口, mintPowerPerpAmountmintWPowerPerpAmount ,他们的区别是前者的 mint 数量会被处以 NormalizationFactor (由于其值小于 1,所以 mint 数量会变大),后者则直接以传入数量 mint。

关于 Uniswap LP 作为抵押物:用户在 ETH/USDC pool 中注入流动性,然后将 NFT 凭证质押作为 Squeeth 的抵押资产,Squeeth 每次检查抵押价值的时候都会去计算该 LP 中还剩下多少 ETH 和 oSQTH 计入两者的总价值;每一个空头头寸 (vault) 最多只能有一个 Uniswap LP 质押资产,不能添加一个以上。

uniswap V3 LP 中的资产会随着交易价格而变化,超出做市的价格范围,LP 中甚至会变成单一资产;所以 Squeeth 对于 UniLP 质押资产都需要查询 Uniswap pool 价格,然后根据 LP 的价格上下限分别计算当前 LP 内 ETH 数量和 oSQTH 数量,ETH 可以直接计入抵押资产,而 oSQTH 则需要换算成 ETH 价值;

// hardhat/contracts/core/Controller.sol

/**
 * @notice deposit collateral and mint wPowerPerp (non-rebasing)
 * for specified powerPerp (rebasing) amount
 * @param _vaultId vault to mint wPowerPerp in
 * @param _powerPerpAmount amount of powerPerp to mint
 * @param _uniTokenId uniswap v3 position token id (additional collateral)
 * @return vaultId
 * @return amount of wPowerPerp minted
 */
function mintPowerPerpAmount(
    uint256 _vaultId,
    uint256 _powerPerpAmount,
    uint256 _uniTokenId
) external payable notPaused nonReentrant returns (uint256, uint256) {
    return _openDepositMint(
        msg.sender,
        _vaultId,
        _powerPerpAmount,
        msg.value,
        _uniTokenId,
        false
    );
}

/**
 * @notice deposit collateral and mint wPowerPerp
 * @param _vaultId vault to mint wPowerPerp in
 * @param _wPowerPerpAmount amount of wPowerPerp to mint
 * @param _uniTokenId uniswap v3 position token id (additional collateral)
 * @return vaultId
 */
function mintWPowerPerpAmount(
    uint256 _vaultId,
    uint256 _wPowerPerpAmount,
    uint256 _uniTokenId
) external payable notPaused nonReentrant returns (uint256) {
    (uint256 vaultId, ) = _openDepositMint(
        msg.sender,
        _vaultId,
        _wPowerPerpAmount,
        msg.value,
        _uniTokenId,
        true
    );
    return vaultId;
}

Deposit and Mint

_openDepositMint 是具体操作质押和 mint 的函数,其中入参 _isWAmount 控制 mint amount 是否需要处以 NormalizationFactor 做调整。函数内部的流程为:

  1. 计算最新的 NormalizationFactor,具体算法参见后文 #getnewnormalizationfactor
  2. 是否需要为用户新开做空头寸的仓位 Vault;每一个 Vault 都会单独计算抵押资产价值和负债资产价值,并且清算时也将单独清算;
  3. mint oSQTH ,会从 mint 数量中扣除一部分手续费,目前线上合约将费率设置为 0
  4. 存入 ETH 抵押资产
  5. 存入 UNI LP 抵押资产
  6. 检查抵押率是否大于 150%
  7. 更新 Vault 中的 抵押资产数量(ETH) 和 负债资产数量(oSQTH)
  8. 将手续费发送给 feeRecipient ,目前合约中该地址填写的是 零地址,即将这部分手续费直接销毁
// hardhat/contracts/core/Controller.sol

/**
  * wrapper function which opens a vault, adds collateral and mints wPowerPerp
  */
function _openDepositMint(
    address _account,
    uint256 _vaultId,
    uint256 _mintAmount,
    uint256 _depositAmount,
    uint256 _uniTokenId,
    bool _isWAmount
) internal returns (uint256, uint256) {
    // get lastest Normalization Factor
    uint256 cachedNormFactor = _applyFunding();
    uint256 depositAmountWithFee = _depositAmount;
    // _isWAmount false: mintPowerPerpAmount()
    // _isWAmount true: mintWPowerPerpAmount()
    uint256 wPowerPerpAmount = _isWAmount ?
                                _mintAmount :
                                _mintAmount.mul(ONE).div(cachedNormFactor);
    uint256 feeAmount;
    VaultLib.Vault memory cachedVault;

    // load vault or create new a new one
    if (_vaultId == 0) {
        (_vaultId, cachedVault) = _openVault(_account);
    } else {
        // make sure we're not accessing an unexistent vault.
        _checkCanModifyVault(_vaultId, msg.sender);
        cachedVault = vaults[_vaultId];
    }

    if (wPowerPerpAmount > 0) {
        (feeAmount, depositAmountWithFee) = _getFee(
            cachedVault,
            wPowerPerpAmount,
            _depositAmount
        );
        _mintWPowerPerp(cachedVault, _account, _vaultId, wPowerPerpAmount);
    }
    if (_depositAmount > 0) {
        _addEthCollateral(cachedVault, _vaultId, depositAmountWithFee);
    }
    if (_uniTokenId != 0) {
        _depositUniPositionToken(cachedVault, _account, _vaultId, _uniTokenId);
    }

    _checkVault(cachedVault, cachedNormFactor);
    _writeVault(_vaultId, cachedVault);

    // pay insurance fee
    if (feeAmount > 0) payable(feeRecipient).sendValue(feeAmount);

    return (_vaultId, wPowerPerpAmount);
}

Check Vault Status

从 openDepositMint 的流程中可以看出,用户可 mint 出 oSQTH 最大数量的关键在于 checkVault 函数。

// hardhat/contracts/core/Controller.sol

/**
 * @notice check if vault has enough collateral and is not a dust vault
 * @dev revert if vault has insufficient collateral or is a dust vault
 * @param _vault the Vault memory to update
 * @param _normalizationFactor normalization factor
 */
function _checkVault(VaultLib.Vault memory _vault, uint256 _normalizationFactor)
    internal
    view
{
    (bool isSafe, bool isDust) = _getVaultStatus(_vault, _normalizationFactor);
    require(isSafe, "C24");
    require(!isDust, "C22");
}

其中 _getVaultStatus() 返回变量 isSafe 表示该空头头寸抵押率是否大于等于 150%。

// hardhat/contracts/core/Controller.sol

/**
 * @notice return if the vault is properly collateralized and if it is a dust vault
 * @param _vault the Vault memory to update
 * @param _normalizationFactor normalization factor
 * @return true if the vault is safe
 * @return true if the vault is a dust vault
 */
function _getVaultStatus(VaultLib.Vault memory _vault, uint256 _normalizationFactor)
    internal
    view
    returns (bool, bool)
{
    // scaledEthPrice = ETH twap / 10000
    uint256 scaledEthPrice = Power2Base._getScaledTwap(
        oracle,
        ethQuoteCurrencyPool,
        weth,
        quoteCurrency,
        TWAP_PERIOD,
        true // do not call more than maximum period so it does not revert
    );

    // get short position collateral ratio
    return
        VaultLib.getVaultStatus(
            _vault,
            uniswapPositionManager,
            _normalizationFactor,
            scaledEthPrice,
            MIN_COLLATERAL,
            IOracle(oracle).getTimeWeightedAverageTickSafe(
                wPowerPerpPool,
                TWAP_PERIOD
            ),
            isWethToken0
        );
}

getVaultStatus() 获取空头头寸的资产状态,返回其抵押率是否安全(>= 150%) ,是否抵押资产已经小于 _minCollateral (小于该值可以认为该空头头寸已经完全平仓,由于 solidity 的运算精度,头寸平仓之后难免有一些极小数量的抵押资产没有被扣除干净)。

collateralRatio=collateralValuedebtValue150%collateralRatio = \frac{collateralValue}{debtValue} \geq 150 \%

代码中对于抵押率是否在安全线以上做了优化,并非直接计算其比例,上述不等式等价于:

collateralValue2debtValue3collateralValue \cdot 2 \geq debtValue \cdot 3

如此即可避免 solidity 中的除法运算。

// hardhat/contracts/core/Controller.sol

/// @dev vault data storage
mapping(uint256 => VaultLib.Vault) public vaults;

// hardhat/contracts/core/VaultLib.sol

// the collateralization ratio (CR) is checked with the numerator
// and denominator separately a user is safe if
// - collateral value >= (COLLAT_RATIO_NUMER/COLLAT_RATIO_DENOM)* debt value
uint256 public constant CR_NUMERATOR = 3;
uint256 public constant CR_DENOMINATOR = 2;

struct Vault {
    // the address that can update the vault
    address operator;
    // uniswap position token id deposited into the vault as collateral
    // 2^32 is 4,294,967,296, which means the vault structure will work
    // with up to 4 billion positions
    uint32 NftCollateralId;
    // amount of eth (wei) used in the vault as collateral
    // 2^96 / 1e18 = 79,228,162,514, which means a vault can store
    // up to 79 billion eth when we need to do calculations,
    // we always cast this number to uint256 to avoid overflow
    uint96 collateralAmount;
    // amount of wPowerPerp minted from the vault
    uint128 shortAmount;
}

/**
 * @notice check if a vault is properly collateralized
 * @param _vault the vault we want to check
 * @param _positionManager address of the uniswap position manager
 * @param _normalizationFactor current _normalizationFactor
 * @param _ethQuoteCurrencyPrice current eth price scaled by 1e18
 * @param _minCollateral minimum collateral that needs to be in a vault
 * @param _wsqueethPoolTick current price tick for wsqueeth pool
 * @param _isWethToken0 whether weth is token0 in the wsqueeth pool
 * @return true if the vault is sufficiently collateralized
 * @return true if the vault is considered as a dust vault
 */
function getVaultStatus(
    Vault memory _vault,
    address _positionManager,
    uint256 _normalizationFactor,
    uint256 _ethQuoteCurrencyPrice,
    uint256 _minCollateral,
    int24 _wsqueethPoolTick,
    bool _isWethToken0
) internal view returns (bool, bool) {
    if (_vault.shortAmount == 0) return (true, false);

    // debtValue = shortAmount * nomalizationFactor * ETH twap / 10000
    uint256 debtValueInETH = uint256(_vault.shortAmount)
                                .mul(_normalizationFactor)
                                .mul(_ethQuoteCurrencyPrice)
                                .div(ONE_ONE);

    // get effective collateral ETH value, include Uniswap LP assets
    uint256 totalCollateral = _getEffectiveCollateral(
        _vault,
        _positionManager,
        _normalizationFactor,
        _ethQuoteCurrencyPrice,
        _wsqueethPoolTick,
        _isWethToken0
    );

    bool isDust = totalCollateral < _minCollateral;
    // collateralValue / debtValue >= 150% = 3 / 2
    // collateralValue * 2 >= debtValue * 3
    bool isAboveWater = totalCollateral.mul(
                            CR_DENOMINATOR) >= debtValueInETH.mul(CR_NUMERATOR);
    return (isAboveWater, isDust);
}

Effective Collateral

Squeeth 接受两种抵押资产,一种是直接转入 ETH,另一种是 Uniswap oSQTH/ETH pool LP token,即质押该交易池子的流动性凭证给 Squeeth,将流动性中的两种资产作为抵押。

_getEffectiveCollateral() 计算指定空头头寸内的抵押资产价值,包括 ETH 抵押资产 和 Uniswap oSQTH/ETH pool LP 内的资产(ETH 和 oSQTH),其中 oSQTH 需要换算成 ETH 价值。oSQTH 价值换算 ETH 价值的公式如下:

oSQTHvalue=oSQTHamountNormalizationFactorETHtwap/10000oSQTHvalue = oSQTHamount \cdot NormalizationFactor \cdot ETHtwap / 10000

// hardhat/contracts/core/VaultLib.sol

/**
 * @notice get the total effective collateral of a vault, which is:
 *         collateral amount + uniswap position token equivelent amount in eth
 * @param _vault the vault we want to check
 * @param _positionManager address of the uniswap position manager
 * @param _normalizationFactor current _normalizationFactor
 * @param _ethQuoteCurrencyPrice current eth price scaled by 1e18
 * @param _wsqueethPoolTick current price tick for wsqueeth pool
 * @param _isWethToken0 whether weth is token0 in the wsqueeth pool
 * @return the total worth of collateral in the vault
 */
function _getEffectiveCollateral(
    Vault memory _vault,
    address _positionManager,
    uint256 _normalizationFactor,
    uint256 _ethQuoteCurrencyPrice,
    int24 _wsqueethPoolTick,
    bool _isWethToken0
) internal view returns (uint256) {
    if (_vault.NftCollateralId == 0) return _vault.collateralAmount;

    // the user has deposited uniswap position token as collateral,
    // see how much eth / wSqueeth the uniswap position token has
    (uint256 nftEthAmount, uint256 nftWsqueethAmount) = _getUniPositionBalances(
        _positionManager,
        _vault.NftCollateralId,
        _wsqueethPoolTick,
        _isWethToken0
    );
    // convert squeeth amount from uniswap position token as
    // equivalent amount of collateral
    uint256 wSqueethIndexValueInEth = nftWsqueethAmount
                                        .mul(_normalizationFactor)
                                        .mul(_ethQuoteCurrencyPrice)
                                        .div(ONE_ONE);
    // add eth value from uniswap position token as collateral
    return nftEthAmount.add(wSqueethIndexValueInEth).add(_vault.collateralAmount);
}

_getUniPositionBalances() 查询 LP 内资产,由两部分组成,第一部分是 LP 参与做市的资产,由 Uniswap V3 相关计算公式计算得出,另一部分则是 LP 当前已收取的手续费 (tokensOwed0, tokensOwed1),这部分也是抵押资产的一部分。其中 LP 内部根据价格计算资产数量的公式为:

amount0 = liquidity / sqrt(lower) - liquidity / sqrt(upper)

amount1 = liquidity * (sqrt(upper) - sqrt(lower))

关于 Uniswap LP 资产数量的计算原理已有许多优秀文章介绍,这里推荐 0xpaco 的系列文章 Uniswap v3 详解(二):创建交易对/提供流动性, 此处不再赘述其原理。

function _getUniPositionBalances(
    address _positionManager,
    uint256 _tokenId,
    int24 _wPowerPerpPoolTick,
    bool _isWethToken0
)
    internal
    view
    returns (uint256 ethAmount, uint256 wPowerPerpAmount)
{
    // tokensOwed0 and tokensOwed1 can both be collected as fee
    (
        int24 tickLower,
        int24 tickUpper,
        uint128 liquidity,
        uint128 tokensOwed0,
        uint128 tokensOwed1
    ) = _getUniswapPositionInfo(_positionManager, _tokenId);

    // calc balance0 and balance1 in LP
    (uint256 amount0, uint256 amount1) = _getToken0Token1Balances(
        tickLower,
        tickUpper,
        _wPowerPerpPoolTick,
        liquidity
    );

    return
        _isWethToken0
            ? (amount0 + tokensOwed0, amount1 + tokensOwed1)
            : (amount1 + tokensOwed1, amount0 + tokensOwed0);
}

getNewNormalizationFactor

最后我们来看看如何更新 NormalizationFactor

_applyFunding() 首先会判断距离上次更新的时间间隔,如果上一次更新的时间戳与当前区块的时间戳 block.timestamp 相同,则不需要更新 NormalizationFactor;即,同一区块内只在第一笔交易计算更新其值,因为同一区块内交易的时间戳都相同,所以后续交易不用触发 NormalizationFactor 更新。

// hardhat/contracts/core/Controller.sol

/**
 * @notice update the normalization factor as a way to pay in-kind funding
 * @return new normalization factor
 **/
function _applyFunding() internal returns (uint256) {
    // only update the norm factor once per block
    if (lastFundingUpdateTimestamp == block.timestamp) return normalizationFactor;

    uint256 newNormalizationFactor = _getNewNormalizationFactor();

    ...

    return newNormalizationFactor;
}

_getNewNormalizationFactor() 是实际计算 NormalizationFactor 最新值的函数,其运行逻辑为:

  1. 计算距离上一次更新的时间间隔 period,若为 0,直接返回当前 NormalizationFactor 值,不需要更新
  2. period 不能大于最大计算周期 max_period, max_period 是从 ETH/USDC 和 oSQTH/ETH 两个 pool 设置的最大计算周期中取较小的一个
    • max_period = min(ETH/USDC pool max period, oSQTH/ETH pool max period)
    • period = period > max_period ? max_period : period
  3. 获取 MarkPrice 和 IndexPrice,具体逻辑见后文
  4. 计算复利时间指数 rFunding,即 periodFundingPeriod 的比值,将此作为复利运算的幂
    • FundingPeriod = 420 hours (17.5 天)
    • rFunding = period / FundingPeriod
  5. IndexPrice 与 MarkPrice 的比值需要在规定的范围内,即 NormalizationFactor 的变化程度被认为限制,funding rate 的波动不能超出一定范围
    • 80%<IndexPriceMarkPrice<140%80\% < \frac{IndexPrice}{MarkPrice} < 140\%
    • 代码中 LOWER_MARK_RATIO = 0.8, UPPER_MARK_RATIO = 1.4 即为比值的上下限
  6. 根据 Index 和 Mark 比值,以及时间间隔比值计算上次结算至今的 NormalizationFactor 变化乘数 Multiplier
    • multiplier = 2^( log2(index/mark) * rFunding )
  7. 最后使用乘数 multiplier 去更新 NormalizationFactor

代码中将复利指数运算部分先转换成了以 2 为底数的对数运算,再计算 2 的指数运算,之所以不直接计算价格比例的指数,是因为转换成以 2 为底的对数和 2 为底的指数运算在 EVM 中更省 gas。具体代码参见 ABKMath64x64

Multiplier=(IndexPriceMarkPrice)rFunding=2(log2(IndexPrice/MarkPrice)rFunding)Multiplier = (\frac{IndexPrice}{MarkPrice})^{rFunding} = 2^{(log_{2}(IndexPrice / MarkPrice) \cdot rFunding)}

NormalizationFactor=MultiplierNormalizationFactoroldNormalization Factor = Multiplier \cdot Normalization Factor_{old}

// hardhat/contracts/core/Controller.sol

//80% of index
uint256 internal constant LOWER_MARK_RATIO = 8e17;
//140% of index
uint256 internal constant UPPER_MARK_RATIO = 140e16;

/**
 * @dev calculate new normalization factor base on the current timestamp
 * @return new normalization factor if funding happens in the current block
 */
function _getNewNormalizationFactor() internal view returns (uint256) {
    // period is delta time
    uint32 period = block.timestamp.sub(lastFundingUpdateTimestamp).toUint32();

    // only update the norm factor once per block
    if (period == 0) {
        return normalizationFactor;
    }

    // make sure we use the same period for mark and index
    // max period = min(ETH/USDC pool max period, oSQTH/ETH pool max period)
    // if the period is greater than, periodForOracle = max period
    uint32 periodForOracle = _getConsistentPeriodForOracle(period);

    // avoid reading normalizationFactor from storage multiple times
    uint256 cacheNormFactor = normalizationFactor;

    uint256 mark = Power2Base._getDenormalizedMark(
        periodForOracle,
        oracle,
        wPowerPerpPool,
        ethQuoteCurrencyPool,
        weth,
        quoteCurrency,
        wPowerPerp,
        cacheNormFactor
    );
    uint256 index = Power2Base._getIndex(
        periodForOracle,
        oracle,
        ethQuoteCurrencyPool,
        weth,
        quoteCurrency
    );

    //the fraction of the funding period. used to compound the funding rate
    int128 rFunding = ABDKMath64x64.divu(period, FUNDING_PERIOD);

    // floor mark to be at least LOWER_MARK_RATIO of index
    uint256 lowerBound = index.mul(LOWER_MARK_RATIO).div(ONE);
    if (mark < lowerBound) {
        mark = lowerBound;
    } else {
        // cap mark to be at most UPPER_MARK_RATIO of index
        uint256 upperBound = index.mul(UPPER_MARK_RATIO).div(ONE);
        if (mark > upperBound) mark = upperBound;
    }

    // normFactor(new) = multiplier * normFactor(old)
    // multiplier = (index/mark)^rFunding
    // x^r = n^(log_n(x) * r)
    // multiplier = 2^( log2(index/mark) * rFunding )

    int128 base = ABDKMath64x64.divu(index, mark);
    int128 logTerm = ABDKMath64x64.log_2(base).mul(rFunding);
    int128 multiplier = logTerm.exp_2();
    return multiplier.mulu(cacheNormFactor);
}

_getDenormalizedMark() 返回 MarkPrice,其结果精度为 18:

  1. 获取缩放后的 ETH twap, 以 USDC 计价
    • ethQuoteCurrencyPrice = (ETH twap / 10000)
    • 缩放是为了方便计算,和 oSQTH 价格统一数量级
  2. 获取 oSQTH twap 价格(注意 oSQTH 价格并非 MarkPrice)
    • oSQTH twap 是 oSQTH/ETH 的价格
    • 那么以 USDC 计价则是 oSQTH twap * ethQuoteCurrencyPrice
  3. 计算 MarkPrice , 以 USDC 计价

MarkPrice(inUSDC)=oSQTHtwap(ETHtwap/10000)/NormalizationFacotrMarkPrice (in USDC) = oSQTH twap \cdot (ETH twap / 10000) / NormalizationFacotr

/**
 * @notice return the mark price of power perp in quoteCurrency,
 * scaled by 18 decimals
 * @return for squeeth, return ethPrice * squeethPriceInEth
 */
function _getDenormalizedMark(
    uint32 _period,
    address _oracle,
    address _wSqueethEthPool,
    address _ethQuoteCurrencyPool,
    address _weth,
    address _quoteCurrency,
    address _wSqueeth,
    uint256 _normalizationFactor
) internal view returns (uint256) {
    uint256 ethQuoteCurrencyPrice = _getScaledTwap(
        _oracle,
        _ethQuoteCurrencyPool,
        _weth,
        _quoteCurrency,
        _period,
        false
    );
    uint256 wsqueethEthPrice = _getTwap(
                                    _oracle,
                                    _wSqueethEthPool,
                                    _wSqueeth,
                                    _weth,
                                    _period,
                                    false
                                );

    return wsqueethEthPrice.mul(ethQuoteCurrencyPrice).div(_normalizationFactor);
}

Conclusion

Squeeth 的 In-kind funding 机制区别于传统现金结算模式,使用一个全局变量 NormalizationFactor 来使 oSQTH 折价,从而变向实现了 funding payment 的转移(通常是多方支付给空方)。

  • IndexPrice 是 ETH 价格的平方再除以调整系数 10000 (由于价格的平方数量级过大,不适用合约中计算,所以代码实现中的 ETH twap 价格都缩小了 10000 倍)
  • MarkPrice 是 oSQTH 价格除以 NormalizationFactor(由于 oSQTH 的价格已经被折价过,所以需要还原到折价之前的状态)
  • NormalizationFactor 每次都会以 IndexPrice/MarkPrice 的比例更新,这便导致了 oSQTH 的价格已经反应出了累计的 funding ,由于通常是 longs to pay shorts, 所以 oSQTH 的价格总是会比 MarkPrice 小,且这个趋势会一直增大
  • 做多 Squeeth 不需要抵押,直接在 Uniswap pool 交易即可
  • 做空 Squeeth 需要超额抵押,直接抵押 ETH,也可以将 Uniswap LP 凭证作为抵押,抵押率不能低于 150%
  • 正因为做空的资金成本更大(需要超额抵押),做多则只需要直接购买,所以通常是多方支付空方funding (longs to pay shorts)