From d2ab540bd381d0392978e597f493530c3faf2c0d Mon Sep 17 00:00:00 2001
From: Reppelin Tom
Date: Tue, 30 Jul 2024 18:17:18 +0200
Subject: [PATCH 1/3] feat(oracle deployed): adding deployment check part 1
---
src/component/OracleDecoder.tsx | 1 +
src/component/OracleTestor.tsx | 29 ++++++
src/component/common/CheckItemDeployment.tsx | 101 +++++++++++++++++++
src/component/common/CheckItemFeeds.tsx | 20 +++-
src/hooks/testor/useOracleDeploymentCheck.ts | 52 ++++++++++
src/hooks/testor/useOraclePriceCheck.ts | 3 +-
src/hooks/testor/useRouteMatch.ts | 19 +++-
src/services/errorTypes.ts | 2 +
src/services/fetchers/fetchAPI.ts | 81 +++++++++++++++
src/services/fetchers/oracleFetcher.ts | 34 +++++++
10 files changed, 335 insertions(+), 7 deletions(-)
create mode 100644 src/component/common/CheckItemDeployment.tsx
create mode 100644 src/hooks/testor/useOracleDeploymentCheck.ts
diff --git a/src/component/OracleDecoder.tsx b/src/component/OracleDecoder.tsx
index 3a9054d..5f7e15f 100644
--- a/src/component/OracleDecoder.tsx
+++ b/src/component/OracleDecoder.tsx
@@ -405,6 +405,7 @@ Provided: ${decimalResult.quoteTokenDecimalsProvided}, Expected: ${decimalResult
{
assets
);
+ const {
+ result: deploymentResult,
+ loading: deploymentLoading,
+ errors: deploymentError,
+ checkDeployment,
+ } = useOracleDeploymentCheck();
+
useEffect(() => {
setIsSubmitEnabled(
collateralAssetTouched &&
@@ -187,6 +196,7 @@ const OracleTestor = () => {
loanAssetSymbol
);
await priceCheck();
+ await checkDeployment(selectedNetwork.value, oracleInputs);
setIsSubmitting(false);
};
@@ -570,9 +580,28 @@ Provided: ${decimalResult.quoteTokenDecimalsProvided}, Expected: ${decimalResult
loading={formSubmitted ? decimalLoading : false}
/>
+
+
= ({
+ title,
+ isVerified,
+ details,
+ description,
+ loading,
+}) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const backgroundColor =
+ isVerified === null ? "#e2e3e5" : isVerified ? "#d4edda" : "#ffeeba";
+ const textColor =
+ isVerified === null ? "#6c757d" : isVerified ? "#155724" : "#6c757d";
+ const handleToggle = () => {
+ setIsOpen(!isOpen);
+ };
+ return (
+
+
+ {isOpen ? : }
+
+ {title}{" "}
+ {isVerified === null ? (
+ ""
+ ) : isVerified ? (
+
+ ) : (
+
+ )}
+
+
+ {description && (
+
+ {description}
+
+ )}
+ {isOpen && (
+ <>
+
+ {loading ? (
+
Loading...
+ ) : (
+ details && (
+
+ )
+ )}
+
+ >
+ )}
+
+ );
+};
+
+export default CheckItem;
diff --git a/src/component/common/CheckItemFeeds.tsx b/src/component/common/CheckItemFeeds.tsx
index 0f6d18e..1d79747 100644
--- a/src/component/common/CheckItemFeeds.tsx
+++ b/src/component/common/CheckItemFeeds.tsx
@@ -14,6 +14,7 @@ interface FeedMetadata {
interface CheckItemFeedsProps {
title: string;
isVerified: boolean | null;
+ isHardcoded: boolean | null;
details?: string;
description?: string;
loading?: boolean;
@@ -23,6 +24,7 @@ interface CheckItemFeedsProps {
const CheckItemFeeds: React.FC = ({
title,
isVerified,
+ isHardcoded,
details,
description,
loading,
@@ -31,11 +33,27 @@ const CheckItemFeeds: React.FC = ({
}) => {
const [isOpen, setIsOpen] = useState(false);
const backgroundColor =
- isVerified === null ? "#e2e3e5" : isVerified ? "#d4edda" : "#f8d7da";
+ isVerified === null
+ ? "#e2e3e5"
+ : isHardcoded
+ ? "#ffeeba" // Light orange color for hardcoded case
+ : isVerified
+ ? "#d4edda"
+ : "#f8d7da";
+
const textColor =
isVerified === null ? "#6c757d" : isVerified ? "#155724" : "#721c24";
const formatDescription = (feeds: FeedMetadata[]) => {
+ if (isHardcoded) {
+ return (
+
+ All feeds are set to zero. You might want to check if this is done on
+ purpose.
+
+ );
+ }
+
return feeds
.filter(
(feed) => feed.address !== "0x0000000000000000000000000000000000000000"
diff --git a/src/hooks/testor/useOracleDeploymentCheck.ts b/src/hooks/testor/useOracleDeploymentCheck.ts
new file mode 100644
index 0000000..e21beae
--- /dev/null
+++ b/src/hooks/testor/useOracleDeploymentCheck.ts
@@ -0,0 +1,52 @@
+import { useState } from "react";
+import { OracleInputs } from "../types";
+import { ErrorTypes } from "../../services/errorTypes";
+import { checkOracleDeployment } from "../../services/fetchers/oracleFetcher";
+
+const useOracleDeploymentCheck = () => {
+ const [result, setResult] = useState<{
+ isDeployed: boolean;
+ address: string | null;
+ } | null>(null);
+ const [loading, setLoading] = useState(false);
+ const [errors, setErrors] = useState([]);
+
+ const checkDeployment = async (
+ chainId: number,
+ oracleInputs: OracleInputs
+ ) => {
+ try {
+ setLoading(true);
+ setErrors([]);
+ const deploymentStatus = await checkOracleDeployment(
+ chainId,
+ oracleInputs
+ );
+
+ if (deploymentStatus.isDeployed) {
+ setResult({
+ isDeployed: true,
+ address: deploymentStatus.address || null,
+ });
+ } else {
+ setResult({
+ isDeployed: false,
+ address: null,
+ });
+ }
+ } catch (err) {
+ console.error("Error checking oracle deployment:", err);
+ setErrors((prevErrors) => [
+ ...prevErrors,
+ ErrorTypes.ORACLE_API_FETCH_ERROR,
+ ]);
+ setResult(null);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return { result, loading, errors, checkDeployment };
+};
+
+export default useOracleDeploymentCheck;
diff --git a/src/hooks/testor/useOraclePriceCheck.ts b/src/hooks/testor/useOraclePriceCheck.ts
index 580d17a..574fb82 100644
--- a/src/hooks/testor/useOraclePriceCheck.ts
+++ b/src/hooks/testor/useOraclePriceCheck.ts
@@ -225,7 +225,8 @@ const useOraclePriceCheck = (
const collateralDecimals = BigInt(collateral?.decimals ?? 18);
const loanDecimals = BigInt(loan?.decimals ?? 18);
- const ratioUsdPrice = collateralPriceUsd.wadDiv(loanPriceUsd);
+ // allowing us to not suffer of a div by zero error.
+ const ratioUsdPrice = collateralPriceUsd.wadDiv(loanPriceUsd) + BigInt(1);
// Calculate oracle price equivalent with high precision
const oraclePriceEquivalent =
(price * PRECISION) /
diff --git a/src/hooks/testor/useRouteMatch.ts b/src/hooks/testor/useRouteMatch.ts
index f9ee7ec..d8169c4 100644
--- a/src/hooks/testor/useRouteMatch.ts
+++ b/src/hooks/testor/useRouteMatch.ts
@@ -216,10 +216,6 @@ const useRouteMatch = () => {
edges: OracleFeedGraphEdge[],
allowHardcoded?: boolean
) => {
- if (edges.length === 0) {
- throw new Error("Empty oracle feed graph.");
- }
-
if (edges.length === 1) {
return true;
}
@@ -340,6 +336,19 @@ const useRouteMatch = () => {
}
}
}
+ const allAddressesZero = [
+ oracleInputs.baseVault,
+ oracleInputs.baseFeed1,
+ oracleInputs.baseFeed2,
+ oracleInputs.quoteFeed1,
+ oracleInputs.quoteFeed2,
+ oracleInputs.quoteVault,
+ ].every((address) => address === ZERO_ADDRESS);
+
+ if (allAddressesZero) {
+ setResult({ isValid: true, feedsMetadata: [], isHardcoded: true });
+ return;
+ }
const resolvableOracleFeedGraphs = getResolvableOracleFeedGraphs(
oracleInputs,
@@ -426,7 +435,7 @@ const useRouteMatch = () => {
}
});
- setResult({ isValid, feedsMetadata });
+ setResult({ isValid, feedsMetadata, isHardcoded: false });
} catch (error) {
console.error("Error validating route:", error);
setErrors((prevErrors) => [...prevErrors, ErrorTypes.FETCH_ERROR]);
diff --git a/src/services/errorTypes.ts b/src/services/errorTypes.ts
index 9bc5b9d..b8bb2eb 100644
--- a/src/services/errorTypes.ts
+++ b/src/services/errorTypes.ts
@@ -25,6 +25,7 @@ export enum ErrorTypes {
BASE_MATCH_ERROR = "BASE_MATCH_ERROR",
QUOTE_MATCH_ERROR = "QUOTE_MATCH_ERROR",
LOAN_ASSET_ZERO_PRICE = "LOAN_ASSET_ZERO_PRICE",
+ ORACLE_API_FETCH_ERROR = "ORACLE_API_FETCH_ERROR",
}
export const ErrorMessages: { [key in ErrorTypes]: string } = {
@@ -66,6 +67,7 @@ export const ErrorMessages: { [key in ErrorTypes]: string } = {
"Quote token does not match. Is there any harcoded oracle price?",
[ErrorTypes.LOAN_ASSET_ZERO_PRICE]:
"Can't fetch the USD value of the loan asset. The Morpho-Blue API seems to not be pricing it.",
+ [ErrorTypes.ORACLE_API_FETCH_ERROR]: "Error fetching oracle data on the api.",
};
export enum LoadingStates {
diff --git a/src/services/fetchers/fetchAPI.ts b/src/services/fetchers/fetchAPI.ts
index d16ac67..38a76d9 100644
--- a/src/services/fetchers/fetchAPI.ts
+++ b/src/services/fetchers/fetchAPI.ts
@@ -198,3 +198,84 @@ export const queryAsset = async (chainId: number) => {
throw error;
}
};
+
+export const queryOracles = async (chainId: number) => {
+ let allOracles: any[] = [];
+ let hasNextPage = true;
+ let skip = 0;
+ const zeroAddress = "0x0000000000000000000000000000000000000000";
+
+ while (hasNextPage) {
+ const query = `query {
+ oracles(first: 100, skip: ${skip}, where: { chainId_in: [${chainId}] }) {
+ items {
+ address
+ data {
+ ... on MorphoChainlinkOracleV2Data {
+ baseVault
+ baseFeedOne {
+ address
+ }
+ baseFeedTwo {
+ address
+ }
+ quoteVault
+ quoteFeedOne {
+ address
+ }
+ quoteFeedTwo {
+ address
+ }
+ baseVaultConversionSample
+ quoteVaultConversionSample
+ }
+ }
+ chain {
+ network
+ }
+ }
+ }
+ }`;
+
+ try {
+ const response = await fetch(API_URL, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ query }),
+ });
+
+ const result: any = await response.json();
+ const oraclesData = result.data.oracles;
+
+ // Replace null values with zero address and filter out items with null data
+ const sanitizedItems = oraclesData.items
+ .filter((item: any) => item.data !== null)
+ .map((item: any) => {
+ const data = item.data;
+ if (data.baseFeedOne === null)
+ data.baseFeedOne = { address: zeroAddress };
+ if (data.baseFeedTwo === null)
+ data.baseFeedTwo = { address: zeroAddress };
+ if (data.quoteFeedOne === null)
+ data.quoteFeedOne = { address: zeroAddress };
+ if (data.quoteFeedTwo === null)
+ data.quoteFeedTwo = { address: zeroAddress };
+ return item;
+ });
+
+ allOracles = [...allOracles, ...sanitizedItems];
+
+ // Check if we received exactly 100 items
+ if (oraclesData.items.length < 100) {
+ hasNextPage = false;
+ } else {
+ skip += 100;
+ }
+ } catch (error) {
+ console.error("Error fetching oracles:", error);
+ throw error;
+ }
+ }
+
+ return allOracles;
+};
diff --git a/src/services/fetchers/oracleFetcher.ts b/src/services/fetchers/oracleFetcher.ts
index 923c3b3..67e7e74 100644
--- a/src/services/fetchers/oracleFetcher.ts
+++ b/src/services/fetchers/oracleFetcher.ts
@@ -1,4 +1,6 @@
import { ethers, Provider } from "ethers";
+import { OracleInputs } from "../../hooks/types";
+import { queryOracles } from "./fetchAPI";
interface OracleReadData {
priceUnscaled: number;
@@ -78,3 +80,35 @@ export const fetchOracleDataFromtx = async (
return null;
}
};
+
+export const checkOracleDeployment = async (
+ chainId: number,
+ oracleInputs: OracleInputs
+) => {
+ const oracles = await queryOracles(chainId);
+
+ for (const oracle of oracles) {
+ if (
+ oracle.data.baseVault === oracleInputs.baseVault &&
+ oracle.data.quoteVault === oracleInputs.quoteVault &&
+ oracle.data.baseFeedOne.address === oracleInputs.baseFeed1 &&
+ oracle.data.baseFeedTwo.address === oracleInputs.baseFeed2 &&
+ oracle.data.quoteFeedOne.address === oracleInputs.quoteFeed1 &&
+ oracle.data.quoteFeedTwo.address === oracleInputs.quoteFeed2 &&
+ BigInt(oracle.data.baseVaultConversionSample) ===
+ BigInt(oracleInputs.baseVaultConversionSample) &&
+ BigInt(oracle.data.quoteVaultConversionSample) ===
+ BigInt(oracleInputs.quoteVaultConversionSample)
+ ) {
+ return {
+ isDeployed: true,
+ address: oracle.address,
+ };
+ }
+ }
+
+ return {
+ isDeployed: false,
+ address: null,
+ };
+};
From 6fae2b189edc243fd5f6f2241bd4d909f873ed83 Mon Sep 17 00:00:00 2001
From: Reppelin Tom
Date: Wed, 31 Jul 2024 17:18:15 +0200
Subject: [PATCH 2/3] feat(oracle): add oracle deployent checker
---
src/component/OracleTestor.tsx | 34 +++++++++++++++++---
src/component/common/CheckItemDeployment.tsx | 2 +-
2 files changed, 30 insertions(+), 6 deletions(-)
diff --git a/src/component/OracleTestor.tsx b/src/component/OracleTestor.tsx
index ccf7c09..6d605d8 100644
--- a/src/component/OracleTestor.tsx
+++ b/src/component/OracleTestor.tsx
@@ -106,6 +106,7 @@ const OracleTestor = () => {
const {
result: deploymentResult,
loading: deploymentLoading,
+ //eslint-disable-next-line
errors: deploymentError,
checkDeployment,
} = useOracleDeploymentCheck();
@@ -257,6 +258,14 @@ const OracleTestor = () => {
},
];
+ const getExplorerUrl = (address: string) => {
+ const baseUrl =
+ selectedNetwork.value === 1
+ ? "https://etherscan.io/address/"
+ : "https://basescan.org/address/";
+ return `${baseUrl}${address}`;
+ };
+
return (
@@ -586,11 +595,26 @@ Provided: ${decimalResult.quoteTokenDecimalsProvided}, Expected: ${decimalResult
deploymentResult ? !deploymentResult.isDeployed : null
}
details={
- deploymentResult
- ? deploymentResult.isDeployed
- ? `An oracle with these inputs is already deployed at address: ${deploymentResult.address}`
- : "No oracle with these inputs has been deployed yet, feel free to proceed."
- : ""
+ deploymentResult ? (
+ deploymentResult.isDeployed ? (
+ <>
+ An oracle with these inputs is already deployed at
+ address:{" "}
+
+ {deploymentResult.address}
+
+ >
+ ) : (
+ "No oracle with these inputs has been deployed yet, feel free to proceed."
+ )
+ ) : (
+ ""
+ )
}
description="Check if an oracle with the same inputs has already been deployed."
loading={deploymentLoading}
diff --git a/src/component/common/CheckItemDeployment.tsx b/src/component/common/CheckItemDeployment.tsx
index f9568c0..3d1e174 100644
--- a/src/component/common/CheckItemDeployment.tsx
+++ b/src/component/common/CheckItemDeployment.tsx
@@ -4,7 +4,7 @@ import { BiCheckDouble, BiError, BiCaretDown, BiCaretUp } from "react-icons/bi";
interface CheckItemProps {
title: string;
isVerified: boolean | null;
- details?: string;
+ details?: React.ReactNode;
description?: string;
loading?: boolean;
}
From 149e1d93e0c56b87ca37596eaa9a74e7a184d82c Mon Sep 17 00:00:00 2001
From: Reppelin Tom
Date: Wed, 31 Jul 2024 17:31:43 +0200
Subject: [PATCH 3/3] feat(UI): improving the UI of the oracle testor
---
src/component/common/CheckItem.tsx | 22 ++--
src/component/common/CheckItemDeployment.tsx | 50 ++++-----
src/component/common/CheckItemFeeds.tsx | 112 +++++++++----------
src/component/common/CheckItemPrice.tsx | 107 +++++++++---------
4 files changed, 140 insertions(+), 151 deletions(-)
diff --git a/src/component/common/CheckItem.tsx b/src/component/common/CheckItem.tsx
index aae9f48..4c750dd 100644
--- a/src/component/common/CheckItem.tsx
+++ b/src/component/common/CheckItem.tsx
@@ -66,7 +66,7 @@ const CheckItem: React.FC = ({
{description}
)}
- {isOpen && (
+ {isOpen && details && (
<>
= ({
{loading ? (
Loading...
) : (
- details && (
-
- )
+
)}
>
diff --git a/src/component/common/CheckItemDeployment.tsx b/src/component/common/CheckItemDeployment.tsx
index 3d1e174..dedf855 100644
--- a/src/component/common/CheckItemDeployment.tsx
+++ b/src/component/common/CheckItemDeployment.tsx
@@ -66,33 +66,29 @@ const CheckItem: React.FC = ({
{description}
)}
- {isOpen && (
- <>
-
- {loading ? (
-
Loading...
- ) : (
- details && (
-
- )
- )}
-
- >
+ {isOpen && details && (
+
+ {loading ? (
+
Loading...
+ ) : (
+
+ )}
+
)}
);
diff --git a/src/component/common/CheckItemFeeds.tsx b/src/component/common/CheckItemFeeds.tsx
index 1d79747..e1f7213 100644
--- a/src/component/common/CheckItemFeeds.tsx
+++ b/src/component/common/CheckItemFeeds.tsx
@@ -21,6 +21,7 @@ interface CheckItemFeedsProps {
feedsMetadata?: FeedMetadata[];
errors?: ErrorTypes[];
}
+
const CheckItemFeeds: React.FC
= ({
title,
isVerified,
@@ -36,7 +37,7 @@ const CheckItemFeeds: React.FC = ({
isVerified === null
? "#e2e3e5"
: isHardcoded
- ? "#ffeeba" // Light orange color for hardcoded case
+ ? "#ffeeba"
: isVerified
? "#d4edda"
: "#f8d7da";
@@ -114,70 +115,65 @@ const CheckItemFeeds: React.FC = ({
{details}
)}
- {isOpen && (
- <>
+ {isOpen && (feedsMetadata || (errors && errors.length > 0)) && (
+
{loading ? (
Loading...
) : (
<>
-
- {feedsMetadata && (
-
-
- {formatDescription(feedsMetadata)}
- {"\n "}
-
- {feedsMetadata.map((feed, index) => (
-
-
- Address: {feed.address}
-
-
- Vendor: {feed.vendor}
-
-
- Description: {feed.description}
-
-
- Pair:{" "}
- {feed.pair ? feed.pair.join(" / ") : "N/A"}
-
-
- Chain ID: {feed.chainId}
-
-
- ))}
-
- )}
- {errors && errors.length > 0 && (
-
- {errors.map((error, index) => (
-
- {ErrorMessages[error] || error}
+ {feedsMetadata && (
+
+
+ {formatDescription(feedsMetadata)}
+ {"\n "}
+
+ {feedsMetadata.map((feed, index) => (
+
+
+ Address: {feed.address}
- ))}
-
- )}
-
+
+ Vendor: {feed.vendor}
+
+
+ Description: {feed.description}
+
+
+ Pair:{" "}
+ {feed.pair ? feed.pair.join(" / ") : "N/A"}
+
+
+ Chain ID: {feed.chainId}
+
+
+ ))}
+
+ )}
+ {errors && errors.length > 0 && (
+
+ {errors.map((error, index) => (
+
+ {ErrorMessages[error] || error}
+
+ ))}
+
+ )}
>
)}
- >
+
)}
);
diff --git a/src/component/common/CheckItemPrice.tsx b/src/component/common/CheckItemPrice.tsx
index b01467a..9c45a38 100644
--- a/src/component/common/CheckItemPrice.tsx
+++ b/src/component/common/CheckItemPrice.tsx
@@ -138,7 +138,7 @@ const CheckItemPrice: React.FC = ({
{formatNumber(details.percentageDifference, 2)}%
)}
- {isOpen && (
+ {isOpen && (details || (errors && errors.length > 0)) && (
= ({
{loading ? (
Loading...
) : (
- details && (
-
- {renderDetailItem(
- "Scale Factor",
- details.scaleFactor,
- formatNumber(details.scaleFactor)
- )}
- {renderDetailItem(
- "Oracle Price",
- details.oraclePrice,
- formatNumber(details.oraclePrice, 0)
- )}
- {renderDetailItem(
- "Price in Collateral Token Decimals",
- formatNumber(
- details.priceUnscaledInCollateralTokenDecimals,
- 4
- )
- )}
- {renderDetailItem(
- "Collateral USD Price",
- `$${formatNumber(details.collateralPriceUsd)}`
- )}
- {renderDetailItem(
- "Loan USD Price",
- `$${formatNumber(details.loanPriceUsd)}`
- )}
-
+ <>
+ {details && (
+
{renderDetailItem(
- "Ratio USD Price",
- formatNumber(details.ratioUsdPrice, 4)
+ "Scale Factor",
+ details.scaleFactor,
+ formatNumber(details.scaleFactor)
)}
{renderDetailItem(
- "Oracle price equivalent",
- formatNumber(details.oraclePriceEquivalent, 4)
+ "Oracle Price",
+ details.oraclePrice,
+ formatNumber(details.oraclePrice, 0)
)}
{renderDetailItem(
- "Deviation",
- `${formatNumber(details.percentageDifference, 2)}%`
+ "Price in Collateral Token Decimals",
+ formatNumber(
+ details.priceUnscaledInCollateralTokenDecimals,
+ 4
+ )
)}
- {errors && errors.length > 0 && (
-
- {errors.map((error, index) => (
-
- {ErrorMessages[error] || error}
-
- ))}
-
+ {renderDetailItem(
+ "Collateral USD Price",
+ `$${formatNumber(details.collateralPriceUsd)}`
+ )}
+ {renderDetailItem(
+ "Loan USD Price",
+ `$${formatNumber(details.loanPriceUsd)}`
)}
+
+ {renderDetailItem(
+ "Ratio USD Price",
+ formatNumber(details.ratioUsdPrice, 4)
+ )}
+ {renderDetailItem(
+ "Oracle price equivalent",
+ formatNumber(details.oraclePriceEquivalent, 4)
+ )}
+ {renderDetailItem(
+ "Deviation",
+ `${formatNumber(details.percentageDifference, 2)}%`
+ )}
+
+
+ )}
+ {errors && errors.length > 0 && (
+
+ {errors.map((error, index) => (
+
+ {ErrorMessages[error] || error}
+
+ ))}
-
- )
+ )}
+ >
)}
)}