Skip to content

Commit 05cd9a4

Browse files
committed
add test
1 parent 21baee7 commit 05cd9a4

File tree

9 files changed

+135
-80
lines changed

9 files changed

+135
-80
lines changed

src/morpho-chainlink/interfaces/IMorphoChainlinkOracleV2.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: GPL-2.0-or-later
22
pragma solidity >=0.5.0;
33

4-
import {IERC4626} from "./IERC4626.sol";
4+
import {IERC4626} from "../../interfaces/IERC4626.sol";
55
import {IOracle} from "../../../lib/morpho-blue/src/interfaces/IOracle.sol";
66
import {AggregatorV3Interface} from "./AggregatorV3Interface.sol";
77

src/morpho-chainlink/libraries/VaultLib.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: GPL-2.0-or-later
22
pragma solidity ^0.8.0;
33

4-
import {IERC4626} from "../interfaces/IERC4626.sol";
4+
import {IERC4626} from "../../interfaces/IERC4626.sol";
55

66
/// @title VaultLib
77
/// @author Morpho Labs

src/morpho-pyth/MorphoPythOracle.sol

Lines changed: 11 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// SPDX-License-Identifier: GPL-2.0-or-later
2-
pragma solidity 0.8.21;
2+
pragma solidity ^0.8.0;
33

44
import {IOracle} from "../../lib/morpho-blue/src/interfaces/IOracle.sol";
55
import {Math} from "../../lib/openzeppelin-contracts/contracts/utils/math/Math.sol";
66
import {IERC4626, VaultLib} from "./libraries/VaultLib.sol";
77
import {PythErrorsLib} from "./libraries/PythErrorsLib.sol";
8+
import {PythFeedLib} from "./libraries/PythFeedLib.sol";
89

910
import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
10-
import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
1111

1212
contract MorphoPythOracle is IOracle {
1313
using Math for uint256;
@@ -70,91 +70,32 @@ contract MorphoPythOracle is IOracle {
7070
BASE_FEED_2 = baseFeed2;
7171
QUOTE_FEED_1 = quoteFeed1;
7272
QUOTE_FEED_2 = quoteFeed2;
73+
7374
pyth = IPyth(pyth_);
74-
75-
PythStructs.Price memory baseFeed1Price = pyth.getPriceUnsafe(
76-
BASE_FEED_1
77-
);
78-
PythStructs.Price memory baseFeed2Price = pyth.getPriceUnsafe(
79-
BASE_FEED_2
80-
);
81-
PythStructs.Price memory quoteFeed1Price = pyth.getPriceUnsafe(
82-
QUOTE_FEED_1
83-
);
84-
PythStructs.Price memory quoteFeed2Price = pyth.getPriceUnsafe(
85-
QUOTE_FEED_2
86-
);
87-
8875
SCALE_FACTOR =
8976
(10 **
9077
(36 +
9178
quoteTokenDecimals +
92-
uint256(-1 * int256(quoteFeed1Price.expo)) +
93-
uint256(-1 * int256(quoteFeed2Price.expo)) -
79+
PythFeedLib.getDecimals(pyth, QUOTE_FEED_1) +
80+
PythFeedLib.getDecimals(pyth, QUOTE_FEED_2) -
9481
baseTokenDecimals -
95-
uint256(-1 * int256(baseFeed1Price.expo)) -
96-
uint256(-1 * int256(baseFeed2Price.expo))) *
82+
PythFeedLib.getDecimals(pyth, BASE_FEED_1) -
83+
PythFeedLib.getDecimals(pyth, BASE_FEED_2)) *
9784
quoteVaultConversionSample) /
9885
baseVaultConversionSample;
9986

10087
PRICE_FEED_MAX_AGE = priceFeedMaxAge;
10188
}
10289

10390
function price() external view returns (uint256) {
104-
uint256 baseFeed1Price;
105-
uint256 baseFeed2Price;
106-
uint256 quoteFeed1Price;
107-
uint256 quoteFeed2Price;
108-
PythStructs.Price memory latestPrice;
109-
110-
if (BASE_FEED_1 == bytes32(0)) {
111-
baseFeed1Price = 1;
112-
} else {
113-
latestPrice = pyth.getPriceNoOlderThan(
114-
BASE_FEED_1,
115-
PRICE_FEED_MAX_AGE
116-
);
117-
baseFeed1Price = uint256(int256(latestPrice.price));
118-
}
119-
120-
if (BASE_FEED_2 == bytes32(0)) {
121-
baseFeed2Price = 1;
122-
} else {
123-
latestPrice = pyth.getPriceNoOlderThan(
124-
BASE_FEED_2,
125-
PRICE_FEED_MAX_AGE
126-
);
127-
baseFeed2Price = uint256(int256(latestPrice.price));
128-
}
129-
130-
if (QUOTE_FEED_1 == bytes32(0)) {
131-
quoteFeed1Price = 1;
132-
} else {
133-
latestPrice = pyth.getPriceNoOlderThan(
134-
QUOTE_FEED_1,
135-
PRICE_FEED_MAX_AGE
136-
);
137-
138-
quoteFeed1Price = uint256(int256(latestPrice.price));
139-
}
140-
141-
if (QUOTE_FEED_2 == bytes32(0)) {
142-
quoteFeed2Price = 1;
143-
} else {
144-
latestPrice = pyth.getPriceNoOlderThan(
145-
QUOTE_FEED_2,
146-
PRICE_FEED_MAX_AGE
147-
);
148-
quoteFeed2Price = uint256(int256(latestPrice.price));
149-
}
15091
return
15192
SCALE_FACTOR.mulDiv(
15293
BASE_VAULT.getAssets(BASE_VAULT_CONVERSION_SAMPLE) *
153-
baseFeed1Price *
154-
baseFeed2Price,
94+
PythFeedLib.getPrice(pyth, BASE_FEED_1, PRICE_FEED_MAX_AGE) *
95+
PythFeedLib.getPrice(pyth, BASE_FEED_2, PRICE_FEED_MAX_AGE),
15596
QUOTE_VAULT.getAssets(QUOTE_VAULT_CONVERSION_SAMPLE) *
156-
quoteFeed1Price *
157-
quoteFeed2Price
97+
PythFeedLib.getPrice(pyth, QUOTE_FEED_1, PRICE_FEED_MAX_AGE) *
98+
PythFeedLib.getPrice(pyth, QUOTE_FEED_2, PRICE_FEED_MAX_AGE)
15899
);
159100
}
160101
}

src/morpho-pyth/interfaces/IERC4626.sol

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
pragma solidity ^0.8.0;
3+
4+
import {IPyth} from "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
5+
import {PythStructs} from "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
6+
7+
import {PythErrorsLib} from "./PythErrorsLib.sol";
8+
9+
/// @title PythFeedLib
10+
/// @author Morpho Labs
11+
/// @custom:contact security@morpho.org
12+
/// @notice Library exposing functions to interact with a Pyth feed.
13+
library PythFeedLib {
14+
15+
/// @dev Returns the price of a `priceId`.
16+
/// @dev When `priceId` is the address zero, returns 1.
17+
function getPrice(IPyth pyth, bytes32 priceId, uint256 maxAge) internal view returns (uint256) {
18+
if (priceId == bytes32(0)) return 1;
19+
20+
PythStructs.Price memory price = pyth.getPriceNoOlderThan(priceId, maxAge);
21+
return uint256(int256(price.price));
22+
}
23+
/// @dev Returns the number of decimals of a `priceId`.
24+
/// @dev When `priceId` is the address zero, returns 0.
25+
function getDecimals(IPyth pyth, bytes32 priceId) internal view returns (uint256) {
26+
if (priceId == bytes32(0)) return 0;
27+
28+
PythStructs.Price memory price = pyth.getPriceUnsafe(priceId);
29+
return uint256(-1 * int256(price.expo));
30+
}
31+
}

src/morpho-pyth/libraries/VaultLib.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: GPL-2.0-or-later
22
pragma solidity ^0.8.0;
33

4-
import {IERC4626} from "../interfaces/IERC4626.sol";
4+
import {IERC4626} from "../../interfaces/IERC4626.sol";
55

66
/// @title VaultLib
77
/// @author Morpho Labs

test/MorphoPythOracleTest.sol

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
pragma solidity ^0.8.0;
2+
import "../lib/forge-std/src/Test.sol";
3+
import "@pythnetwork/pyth-sdk-solidity/MockPyth.sol";
4+
import "../src/morpho-pyth/MorphoPythOracle.sol";
5+
import "./helpers/Constants.sol";
6+
import {console} from "forge-std/console.sol";
7+
8+
contract MorphoPythOracleTest is Test {
9+
MockPyth public mockPyth;
10+
MorphoPythOracle public oracle;
11+
12+
function setUp() public {
13+
mockPyth = new MockPyth(60, 1);
14+
15+
// Create price feed update data for WBTC/USD
16+
bytes[] memory updateData = new bytes[](2);
17+
updateData[0] = mockPyth.createPriceFeedUpdateData(
18+
pythWbtcUsdFeed,
19+
30000 * 1e8, // Price of 30,000 USD
20+
0, // Confidence interval
21+
-8, // Expo (-8 means price is multiplied by 10^8)
22+
30000 * 1e8, // EMA price
23+
0, // EMA Confidence interval
24+
uint64(block.timestamp),
25+
uint64(block.timestamp)
26+
);
27+
28+
updateData[1] = mockPyth.createPriceFeedUpdateData(
29+
pythUsdtUsdFeed,
30+
1 * 1e8, // Price of 1 USD
31+
0, // Confidence interval
32+
-6, // Expo (-6 means price is multiplied by 10^6)
33+
1 * 1e8, // EMA price
34+
0, // EMA Confidence interval
35+
uint64(block.timestamp),
36+
uint64(block.timestamp)
37+
);
38+
// Update the price feed
39+
mockPyth.updatePriceFeeds{value: 2}(updateData);
40+
assertEq(mockPyth.getPriceUnsafe(pythWbtcUsdFeed).price, 30000 * 1e8);
41+
assertEq(mockPyth.getPriceUnsafe(pythUsdtUsdFeed).price, 1 * 1e8);
42+
43+
oracle = new MorphoPythOracle(
44+
address(mockPyth),
45+
vaultZero,
46+
1,
47+
pythWbtcUsdFeed,
48+
pythFeedZero,
49+
pythWbtcUsdTokenDecimals,
50+
vaultZero,
51+
1,
52+
pythUsdtUsdFeed,
53+
pythFeedZero,
54+
pythUsdtUsdTokenDecimals,
55+
oneHour
56+
);
57+
}
58+
59+
function testInitialSetup() public {
60+
assertTrue(address(oracle) != address(0), "Oracle not deployed");
61+
}
62+
63+
function testPythOracleWbtcUsdt() public {
64+
assertEq(
65+
oracle.price(),
66+
((uint256(int256(mockPyth.getPriceUnsafe(pythWbtcUsdFeed).price))) *
67+
10 **
68+
(36 +
69+
pythUsdtUsdTokenDecimals +
70+
uint256(-1 * int256(-6)) -
71+
pythWbtcUsdTokenDecimals -
72+
uint256(-1 * int256(-8)))) /
73+
uint256(int256(mockPyth.getPriceUnsafe(pythUsdtUsdFeed).price))
74+
);
75+
}
76+
}

test/helpers/Constants.sol

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-License-Identifier: GPL-2.0-or-later
22
pragma solidity ^0.8.0;
33

4-
import {IERC4626} from "../../src/morpho-chainlink/interfaces/IERC4626.sol";
4+
import {IERC4626} from "../../src/interfaces/IERC4626.sol";
55
import {AggregatorV3Interface} from "../../src/morpho-chainlink/interfaces/AggregatorV3Interface.sol";
66

77
AggregatorV3Interface constant feedZero = AggregatorV3Interface(address(0));
@@ -25,3 +25,16 @@ AggregatorV3Interface constant daiEthFeed = AggregatorV3Interface(0x773616E4d11A
2525
IERC4626 constant vaultZero = IERC4626(address(0));
2626
IERC4626 constant sDaiVault = IERC4626(0x83F20F44975D03b1b09e64809B757c47f942BEeA);
2727
IERC4626 constant sfrxEthVault = IERC4626(0xac3E018457B222d93114458476f3E3416Abbe38F);
28+
29+
address constant pythBaseContractAddress = 0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a;
30+
address constant pythEthereumContractAddress = 0x4305FB66699C3B2702D4d05CF36551390A4c69C6;
31+
32+
bytes32 constant pythFeedZero = 0x0000000000000000000000000000000000000000000000000000000000000000;
33+
bytes32 constant pythWbtcUsdFeed = 0xc9d8b075a5c69303365ae23633d4e085199bf5c520a3b90fed1322a0342ffc33;
34+
uint256 constant pythWbtcUsdTokenDecimals = 8;
35+
bytes32 constant pythEthUsdFeed = 0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace;
36+
bytes32 constant pythUsdtUsdFeed = 0x2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b;
37+
uint256 constant pythUsdtUsdTokenDecimals = 6;
38+
bytes32 constant pythCbethUsdFeed = 0x15ecddd26d49e1a8f1de9376ebebc03916ede873447c1255d2d5891b92ce5717;
39+
uint256 constant oneHour = 3600;
40+
uint256 constant oneMinute = 60;

0 commit comments

Comments
 (0)