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 && ( +
+

{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 && ( -
-

{details}

-
- ) +
+

{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 && ( -
-

{details}

-
- ) - )} -
- + {isOpen && details && ( +
+ {loading ? ( +

Loading...

+ ) : ( +
+

{details}

+
+ )} +
)}
); 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} +

+ ))}
-
- ) + )} + )}
)}