本文主旨在拆解 morpho-blue 的預言機實作,支援 ERC4626 share token 的報價並整合 chainlink oracle,實作只有一個 constructor 和一個 view function,看似簡單但是其有些複雜的地方。
price()
首先來看 price 的實作,程式碼如下:
function price() external view returns (uint256) {
return SCALE_FACTOR.mulDiv(
BASE_VAULT.getAssets(BASE_VAULT_CONVERSION_SAMPLE)
* BASE_FEED_1.getPrice()
* BASE_FEED_2.getPrice(),
QUOTE_VAULT.getAssets(QUOTE_VAULT_CONVERSION_SAMPLE)
* QUOTE_FEED_1.getPrice()
* QUOTE_FEED_2.getPrice()
);
}
可以從中知道 price() 回傳的就只是一個價格,可以簡化成:
透過 ERC4626.convertToAssets()
支援 shares 的定價,再來以 chainlink oracle 做進一步轉換。
舉例來說,給定有一個接受 native ETH 的 ERC4626 vault,可以以上的方式為其 shares 以美金做定價:
price_share/usd = ERC4626.convertToAssets(amount)
* chainlink_eth/usdc_oracle
* chainlink_usdc/usd_oracle
更近一步,如果要建立 erc4626_wbtc / erc4626_weth
的預言機,可以以下的設定架構出來:
// erc4626_wbtc_share -> wbtc -> usdt -> usd
price_wbtc_share/usd = wbtc_vault.convertToAssets(amount)
* chainlink_wbtc/usdt_oracle
* chainlink_usdt/usd_oracle;
// erc4626_weth_share -> weth -> usdc -> usd
price_weth_share/usd = weth_vault.convertToAssets(amount)
* chainlink_weth/usdc_oracle
* chainlink_usdc/usd_oracle;
return = (price_wbtc/usd) / (price_weth/usd)
SCALE_FACTOR
SCALE_FACTOR
主要在處理 feed decimals、不同 token decimal 和 chainlink feed decimal 之間的轉換,拆成三個部分來理解 scale factor 是怎麼定義出來的:
chainlink feed decimals
chainlink oracle 會揭露出 feed decimals,直接取得即可
interface AggregatorV3Interface {
function decimals() external view returns (uint8);
}
前面知道 price()
是以兩個 erc4626 和四個 chainlink oracle 的 price 計算得出,這邊將計算簡化成只有兩個 chainlink oracle。如果要架構一個 WBTC/WETH
的預言機,可以以 WBTC/USDT
和 WETH/USDT
兩個 chainlink oracle 組出來,將其相除即可得 WBTC/WETH
的預言機:
假定這個 WBTC/WETH
預言機的 feed decimal 為 18,WBTC/USDT
oracle feed decimals 為 13,WETH/USDT
oracle feed decimals 為 7。如果直接相除,最終會得到 decimals 為 6 的價格:
price_wbtc_usdt = p_1 // scaled by 1e13
price_weth_usdt = p_2 // scaled by 1e7
price_wbtc_weth
= price_wbtc_usdt / price_weth_usdt
= p_1 / p_2 // scaled by 1e6 (13 - 7)
大部分預言機實作會額外乘以一個 scale factor,將 feed decimal 滿足,這個範例 scale factor 的 decimal 應該為 12:
price_wbtc_weth
= scale * p_1 / p_2
= return_value // scaled by 1e18 (scale_decimal + 13 - 7)
scale = 18 - 13 + 7 = 12
從以上可以歸納出,定義 scale 的公式如下:
ERC4626 price per shares
前面 price()
的定義中,需要的是 ERC4626 的 share 的「價格」而不是「數量」,所以 morpho 的實作中額外定義了兩個變數 BASE_VAULT_CONVERSION_SAMPLE
和 BASE_VAULT_CONVERSION_SAMPLE
,以固定的數量的 share 轉換並除以固定的數量的 underlying asset amount,為 share 定價:
// pseudocode
uint256 sample = 1000;
function pricePerShare() external view returns (uint256) {
return baseVault.convertToAsset(sample) / sample;
}
推廣成兩個 ERC4626 vault 並設計回傳 base/quote 的 price 函式 ,範例如下:
// pseudocode
uint256 baseSample = 1000;
uint256 quoteSample = 1;
// price of BASE/QUOTE
// Assume that baseVault and quoteVault have the same underlying asset.
function price() external view returns (uint256) {
// uint256 basePrice = baseVault.convertToAssets(baseSample) / baseSample;
// uint256 quotePrice = quoteVault.convertToAssets(quoteSample) / quoteSample;
// return basePrice / quotePrice;
return baseVault.convertToAssets(baseSample)
/ baseSample
/ quoteVault.convertToAssets(quoteSample)
* quoteSample;
}
morpho 的實作中將需要「多乘的 quoteSample」和「多除的 baseSample」拆出來反映在 scale factor,定義 scale factor 的公式如下:
token decimals
convertToAssets(shares)
會將 share 的數量轉換成 underlying asset 的數量並回傳。回傳的是「數量」不是「價格」,只需要考量 underlying token 的 token decimals,不需要考量 share token 的 decimals。
不同的代幣可能有不同的 token decimals 需要考量,同樣需要額外乘以一個 scale 來處理,假設 base/quote
的 decimals 為 18, base underlying asset 的 decimal 為 11,quote underlying asset 的 decimal 為 7,則 scale decimal 應為 14:
price_base_quote(1e18) = scale(?) * baseVault_underlyingAmount(1e11) / quoteVault_underlyingAmount (1e7)
scaleDecimal = 18 - 11 + 7 = 14
從以上可以歸納出,定義 scale 的公式如下:
Final
整合以上三個定義 scale 的公式,可以得到:
SCALE_FACTOR
在 constructor 中計算,可以知道 morpho oracle 是一個 feed decimals 為 36 的預言機:
uint256 SCALE_FACTOR = 10 ** (
36 + quoteTokenDecimals + quoteFeed1.getDecimals() + quoteFeed2.getDecimals()
- baseTokenDecimals - baseFeed1.getDecimals() - baseFeed2.getDecimals()
) * quoteVaultConversionSample / baseVaultConversionSample;
Thought
morpho oracle 雖然設計精良,尤其處理精度的部分,但是還是其缺陷的地方。在和 chainlink 整合的地方上,全盤相信 chainlink 不會失誤,所以忽略對 chainlink 報價的所有檢查:
/// @dev https://github.com/morpho-org/morpho-blue-oracles/blob/630ca9a24065d577fb0d24717bd98f310722f729/src/morpho-chainlink/libraries/ChainlinkDataFeedLib.sol#L15-L19
/// @dev Notes on safety checks:
/// - L2s are not supported.
/// - Staleness is not checked because it's assumed that the Chainlink feed keeps its promises on this.
/// - The price is not checked to be in the min/max bounds because it's assumed that the Chainlink feed keeps its promises on this.
另外註解上寫 L2 不支援,不檢查 down L2 sequencer,卻仍有部署在 base 上。