Skip to content

Commit ba70974

Browse files
committed
Merge branch 'main' of github.com:morpho-labs/morpho-blue-oracles into fix/512-bit-multiplication
2 parents a73e817 + e6f8bb0 commit ba70974

File tree

6 files changed

+48
-29
lines changed

6 files changed

+48
-29
lines changed

.github/workflows/foundry.yml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,9 @@ jobs:
1414
name: forge-test
1515
runs-on: ubuntu-latest
1616
steps:
17-
- name: Generate a token
18-
id: generate-token
19-
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
20-
with:
21-
app_id: ${{ secrets.APP_ID }}
22-
private_key: ${{ secrets.APP_PRIVATE_KEY }}
23-
2417
- name: Checkout
2518
uses: actions/checkout@v3
2619
with:
27-
token: ${{ steps.generate-token.outputs.token }}
2820
submodules: recursive
2921

3022
- name: Install Foundry

src/ChainlinkOracle.sol

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ contract ChainlinkOracle is IOracle {
3737

3838
/* CONSTRUCTOR */
3939

40+
/// @dev Here is the list of assumptions on the inputs that guarantees the oracle behaves as expected:
41+
/// - Feeds are either Chainlink-compliant or the address zero.
42+
/// - Feeds have the same behavioral assumptions as Chainlink's.
43+
/// - Feeds are set in the correct order.
44+
/// - Decimals passed as argument are correct.
45+
/// - The vault conversion sample is low enough to avoid overflows.
46+
/// - The vault, if set, is ERC4626-compliant.
4047
/// @param vault Vault. Pass address zero to omit this parameter.
4148
/// @param baseFeed1 First base feed. Pass address zero if the price = 1.
4249
/// @param baseFeed2 Second base feed. Pass address zero if the price = 1.
@@ -55,41 +62,51 @@ contract ChainlinkOracle is IOracle {
5562
uint256 baseTokenDecimals,
5663
uint256 quoteTokenDecimals
5764
) {
58-
// The vault parameter is used for ERC4626 tokens, to price its shares.
59-
// It is used to price `VAULT_CONVERSION_SAMPLE` of the vault shares, so it requires dividing by that number,
60-
// hence the division by `VAULT_CONVERSION_SAMPLE` in the `SCALE_FACTOR` definition.
65+
// The ERC4626 vault parameter is used to price `VAULT_CONVERSION_SAMPLE` of its shares, so it requires dividing
66+
// by that number, hence the division by `VAULT_CONVERSION_SAMPLE` in the `SCALE_FACTOR` definition.
6167
// Verify that vault = address(0) => vaultConversionSample = 1.
6268
require(
6369
address(vault) != address(0) || vaultConversionSample == 1, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE
6470
);
71+
require(vaultConversionSample != 0, ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO);
72+
6573
VAULT = vault;
6674
VAULT_CONVERSION_SAMPLE = vaultConversionSample;
6775
BASE_FEED_1 = baseFeed1;
6876
BASE_FEED_2 = baseFeed2;
6977
QUOTE_FEED_1 = quoteFeed1;
7078
QUOTE_FEED_2 = quoteFeed2;
71-
// Let pB1 and pB2 be the base prices, and pQ1 and pQ2 the quote prices (price taking into account the
72-
// decimals of both tokens), in a common currency.
73-
// We tackle the most general case in the remainder of this comment, where we assume that no feed is the address
74-
// zero. Similar explanations would hold in the case where some of the feeds are the address zero.
75-
// Let dB1, dB2, dB3, and dQ1, dQ2, dQ3 be the decimals of the tokens involved.
76-
// For example, pB1 is the number of 1e(dB2) of the second base asset that can be obtained from 1e(dB1) of
77-
// the first base asset.
78-
// We notably have dB3 = dQ3, because those two quantities are the decimals of the same common currency.
79-
// Let fpB1, fpB2, fpQ1 and fpQ2 be the feed precision of the corresponding prices.
80-
// Chainlink feeds return pB1*1e(fpB1), pB2*1e(fpB2), pQ1*1e(fpQ1) and pQ2*1e(fpQ2).
81-
// Because the Blue oracle does not take into account decimals, `price()` should return
82-
// 1e36 * (pB1*1e(dB2-dB1) * pB2*1e(dB3-dB2)) / (pQ1*1e(dQ2-dQ1) * pQ2*1e(dQ3-dQ2))
83-
// Yet `price()` returns (pB1*1e(fpB1) * pB2*1e(fpB2) * SCALE_FACTOR) / (pQ1*1e(fpQ1) * pQ2*1e(fpQ2))
84-
// So 1e36 * pB1 * pB2 * 1e(-dB1) / (pQ1 * pQ2 * 1e(-dQ1)) =
85-
// (pB1*1e(fpB1) * pB2*1e(fpB2) * SCALE_FACTOR) / (pQ1*1e(fpQ1) * pQ2*1e(fpQ2))
79+
80+
// In the following comment, we explain the general case (where we assume that no feed is the address zero)
81+
// how to scale the output price as Morpho Blue expects, given the input feed prices.
82+
// Similar explanations would hold in the case where some of the feeds are the address zero.
83+
84+
// Let B1, B2, Q1, Q2, C be 5 assets, each respectively having dB1, dB2, dQ1, dQ2, dC decimals.
85+
// Let pB1 and pB2 be the base prices, and pQ1 and pQ2 the quote prices, so that:
86+
// - pB1 is the quantity of 1e(dB2) assets B2 that can be exchanged for 1e(dB1) assets B1.
87+
// - pB2 is the quantity of 1e(dC) assets C that can be exchanged for 1e(dB2) assets B2.
88+
// - pQ1 is the quantity of 1e(dQ2) assets Q2 that can be exchanged for 1e(dQ1) assets Q1.
89+
// - pQ2 is the quantity of 1e(dC) assets C that can be exchanged for 1e(dQ2) assets B2.
90+
91+
// Morpho Blue expects `price()` to be the quantity of 1 asset Q1 that can be exchanged for 1 asset B1,
92+
// scaled by 1e36:
93+
// 1e36 * (pB1 * 1e(dB2 - dB1)) * (pB2 * 1e(dC - dB2)) / ((pQ1 * 1e(dQ2 - dQ1)) * (pQ2 * 1e(dC - dQ2)))
94+
// = 1e36 * (pB1 * 1e(-dB1) * pB2) / (pQ1 * 1e(-dQ1) * pQ2)
95+
96+
// Let fpB1, fpB2, fpQ1, fpQ2 be the feed precision of the respective prices pB1, pB2, pQ1, pQ2.
97+
// Chainlink feeds return pB1 * 1e(fpB1), pB2 * 1e(fpB2), pQ1 * 1e(fpQ1) and pQ2 * 1e(fpQ2).
98+
99+
// Based on the implementation of `price()` below, the value of `SCALE_FACTOR` should thus satisfy:
100+
// (pB1 * 1e(fpB1)) * (pB2 * 1e(fpB2)) * SCALE_FACTOR / ((pQ1 * 1e(fpQ1)) * (pQ2 * 1e(fpQ2)))
101+
// = 1e36 * (pB1 * 1e(-dB1) * pB2) / (pQ1 * 1e(-dQ1) * pQ2)
102+
86103
// So SCALE_FACTOR = 1e36 * 1e(-dB1) * 1e(dQ1) * 1e(-fpB1) * 1e(-fpB2) * 1e(fpQ1) * 1e(fpQ2)
87104
// = 1e(36 + dQ1 + fpQ1 + fpQ2 - dB1 - fpB1 - fpB2)
88105
SCALE_FACTOR = 10
89106
** (
90107
36 + quoteTokenDecimals + quoteFeed1.getDecimals() + quoteFeed2.getDecimals() - baseTokenDecimals
91108
- baseFeed1.getDecimals() - baseFeed2.getDecimals()
92-
) / VAULT_CONVERSION_SAMPLE;
109+
) / vaultConversionSample;
93110
}
94111

95112
/* PRICE */

src/libraries/ChainlinkDataFeedLib.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ library ChainlinkDataFeedLib {
1313
/// @dev Performs safety checks and returns the latest price of a `feed`.
1414
/// @dev When `feed` is the address zero, returns 1.
1515
/// @dev Notes on safety checks:
16+
/// - L2s are not supported.
1617
/// - Staleness is not checked because it's assumed that the Chainlink feed keeps its promises on this.
1718
/// - The price is not checked to be in the min/max bounds because it's assumed that the Chainlink feed keeps its
1819
/// promises on this.

src/libraries/ErrorsLib.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ library ErrorsLib {
99
/// @notice Thrown when the answer returned by a Chainlink feed is negative.
1010
string constant NEGATIVE_ANSWER = "negative answer";
1111

12+
/// @notice Thrown when the vault conversion sample is 0.
13+
string constant VAULT_CONVERSION_SAMPLE_IS_ZERO = "vault conversion sample is zero";
14+
1215
/// @notice Thrown when the vault conversion sample is not 1 while vault = address(0).
1316
string constant VAULT_CONVERSION_SAMPLE_IS_NOT_ONE = "vault conversion sample is not one";
1417
}

src/libraries/VaultLib.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ pragma solidity ^0.8.0;
33

44
import {IERC4626} from "../interfaces/IERC4626.sol";
55

6-
/// @title ChainlinkDataFeedLib
6+
/// @title VaultLib
77
/// @author Morpho Labs
88
/// @custom:contact [email protected]
99
/// @notice Library exposing functions to price shares of an ERC4626 vault.

test/ChainlinkOracleTest.sol

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,14 @@ contract ChainlinkOracleTest is Test {
132132
assertApproxEqRel(oracle.price(), expectedPrice, deviation);
133133
}
134134

135+
function testConstructorZeroVaultConversionSample() public {
136+
vm.expectRevert(bytes(ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_ZERO));
137+
new ChainlinkOracle(sDaiVault, daiEthFeed, feedZero, usdcEthFeed, feedZero, 0, 18, 6);
138+
}
139+
135140
function testConstructorVaultZeroNonOneSample(uint256 vaultConversionSample) public {
136-
vm.assume(vaultConversionSample != 1);
141+
vaultConversionSample = bound(vaultConversionSample, 2, type(uint256).max);
142+
137143
vm.expectRevert(bytes(ErrorsLib.VAULT_CONVERSION_SAMPLE_IS_NOT_ONE));
138144
new ChainlinkOracle(vaultZero, daiEthFeed, feedZero, usdcEthFeed, feedZero, vaultConversionSample, 18, 6);
139145
}

0 commit comments

Comments
 (0)