From 3af548eefe2cc2604cfec2aba2d380f5af4d775b Mon Sep 17 00:00:00 2001 From: Mark Grothe Date: Fri, 28 Aug 2026 14:27:17 -0500 Subject: [PATCH 1/4] feat: check if pool supports multicall --- .../CollateralChangeActions.tsx | 73 +++++++++++++------ .../transactions/Supply/SupplyActions.tsx | 36 ++++++--- src/hooks/pool/usePoolSupportsMulticall.ts | 40 ++++++++++ 3 files changed, 116 insertions(+), 33 deletions(-) create mode 100644 src/hooks/pool/usePoolSupportsMulticall.ts diff --git a/src/components/transactions/CollateralChange/CollateralChangeActions.tsx b/src/components/transactions/CollateralChange/CollateralChangeActions.tsx index 31e020998a..badc770548 100644 --- a/src/components/transactions/CollateralChange/CollateralChangeActions.tsx +++ b/src/components/transactions/CollateralChange/CollateralChangeActions.tsx @@ -2,10 +2,11 @@ import { ProtocolAction } from '@aave/contract-helpers'; import { TransactionResponse } from '@ethersproject/providers'; import { Trans } from '@lingui/macro'; import { useQueryClient } from '@tanstack/react-query'; -import { Contract } from 'ethers'; +import { Contract, PopulatedTransaction } from 'ethers'; import React from 'react'; import { useTransactionHandler } from 'src/helpers/useTransactionHandler'; import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider'; +import { usePoolSupportsMulticall } from 'src/hooks/pool/usePoolSupportsMulticall'; import { useModalContext } from 'src/hooks/useModal'; import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; import { useRootStore } from 'src/store/root'; @@ -39,9 +40,10 @@ export const CollateralChangeActions = ({ symbol, selectedEmodeId, }: CollateralChangeActionsProps) => { - const [setUsageAsCollateral, estimateGasLimit, currentMarketData] = useRootStore( + const [setUsageAsCollateral, setUserEMode, estimateGasLimit, currentMarketData] = useRootStore( useShallow((state) => [ state.setUsageAsCollateral, + state.setUserEMode, state.estimateGasLimit, state.currentMarketData, ]) @@ -49,6 +51,8 @@ export const CollateralChangeActions = ({ const { sendTx } = useWeb3Context(); const queryClient = useQueryClient(); const { mainTxState, setMainTxState, setTxError } = useModalContext(); + // Resolved on modal open so it is cached well before the user picks an e-mode. + const { data: poolSupportsMulticall } = usePoolSupportsMulticall(currentMarketData); const needsEmodeSwitch = selectedEmodeId !== undefined; @@ -76,29 +80,52 @@ export const CollateralChangeActions = ({ skip: blocked || needsEmodeSwitch, }); - // Multicall action: setUserEMode + setUserUseReserveAsCollateral - const multicallAction = async () => { + // setUserEMode + setUserUseReserveAsCollateral, bundled via Pool.multicall + // where the pool supports it and sent sequentially where it does not. + const emodeAction = async () => { try { setMainTxState({ ...mainTxState, loading: true }); - const poolContractAddress = currentMarketData.addresses.LENDING_POOL; - const poolInterface = new Contract(poolContractAddress, POOL_MULTICALL_ABI).interface; - const currentAccount = useRootStore.getState().account; - - const setEModeCalldata = poolInterface.encodeFunctionData('setUserEMode', [selectedEmodeId]); - - const setCollateralCalldata = poolInterface.encodeFunctionData( - 'setUserUseReserveAsCollateral', - [poolReserve.underlyingAsset, usageAsCollateral] - ); - - let multicallTxData = await new Contract( - poolContractAddress, - POOL_MULTICALL_ABI - ).populateTransaction.multicall([setEModeCalldata, setCollateralCalldata]); - multicallTxData = { ...multicallTxData, from: currentAccount }; - multicallTxData = await estimateGasLimit(multicallTxData); - const response: TransactionResponse = await sendTx(multicallTxData); + let response: TransactionResponse; + + if (poolSupportsMulticall) { + const poolContractAddress = currentMarketData.addresses.LENDING_POOL; + const poolInterface = new Contract(poolContractAddress, POOL_MULTICALL_ABI).interface; + const currentAccount = useRootStore.getState().account; + + const setEModeCalldata = poolInterface.encodeFunctionData('setUserEMode', [ + selectedEmodeId, + ]); + + const setCollateralCalldata = poolInterface.encodeFunctionData( + 'setUserUseReserveAsCollateral', + [poolReserve.underlyingAsset, usageAsCollateral] + ); + + let multicallTxData = await new Contract( + poolContractAddress, + POOL_MULTICALL_ABI + ).populateTransaction.multicall([setEModeCalldata, setCollateralCalldata]); + multicallTxData = { ...multicallTxData, from: currentAccount }; + multicallTxData = await estimateGasLimit(multicallTxData); + response = await sendTx(multicallTxData); + } else { + // Pools below POOL_REVISION 8 have no multicall — send the two calls + // one after the other instead. + const eModeTxs = await setUserEMode(selectedEmodeId as number); + const eModeTx = await eModeTxs[0].tx(); + const eModeTxWithGas = await estimateGasLimit(eModeTx as PopulatedTransaction); + const eModeResponse = await sendTx(eModeTxWithGas); + await eModeResponse.wait(1); + + const collateralTxs = await setUsageAsCollateral({ + reserve: poolReserve.underlyingAsset, + usageAsCollateral, + }); + const collateralTx = await collateralTxs[0].tx(); + const collateralTxWithGas = await estimateGasLimit(collateralTx as PopulatedTransaction); + response = await sendTx(collateralTxWithGas); + } await response.wait(1); @@ -136,7 +163,7 @@ export const CollateralChangeActions = ({ ) } actionInProgressText={Pending...} - handleAction={needsEmodeSwitch ? multicallAction : action} + handleAction={needsEmodeSwitch ? emodeAction : action} /> ); }; diff --git a/src/components/transactions/Supply/SupplyActions.tsx b/src/components/transactions/Supply/SupplyActions.tsx index f522141c1e..fdeccee6fb 100644 --- a/src/components/transactions/Supply/SupplyActions.tsx +++ b/src/components/transactions/Supply/SupplyActions.tsx @@ -12,6 +12,7 @@ import { Contract, PopulatedTransaction } from 'ethers'; import { parseUnits, splitSignature } from 'ethers/lib/utils'; import React, { useEffect, useState } from 'react'; import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider'; +import { usePoolSupportsMulticall } from 'src/hooks/pool/usePoolSupportsMulticall'; import { SignedParams, useApprovalTx } from 'src/hooks/useApprovalTx'; import { usePoolApprovedAmount } from 'src/hooks/useApprovedAmount'; import { useModalContext } from 'src/hooks/useModal'; @@ -96,6 +97,8 @@ export const SupplyActions = React.memo( } = useModalContext(); const permitAvailable = tryPermit({ reserveAddress: poolAddress, isWrappedBaseAsset }); const { sendTx } = useWeb3Context(); + // Resolved on modal open so it is cached well before the user picks an e-mode. + const { data: poolSupportsMulticall } = usePoolSupportsMulticall(currentMarketData); const queryClient = useQueryClient(); const [signatureParams, setSignatureParams] = useState(); @@ -176,22 +179,35 @@ export const SupplyActions = React.memo( const isNativeAsset = poolAddress.toLowerCase() === API_ETH_MOCK_ADDRESS.toLowerCase(); if (needsEmodeSwitch) { - if (isNativeAsset) { - // Native ETH goes through WETH Gateway which is a separate contract — - // can't bundle with Pool.multicall. Send setUserEMode first, then supply. + if (isNativeAsset || !poolSupportsMulticall) { + // Native ETH goes through WETH Gateway which is a separate contract, + // and pools below POOL_REVISION 8 have no multicall at all — neither + // can be bundled. Send setUserEMode first, then supply. const eModeTxs = await setUserEMode(selectedEmodeId); const eModeTx = await eModeTxs[0].tx(); const eModeTxWithGas = await estimateGasLimit(eModeTx as PopulatedTransaction); const eModeResponse = await sendTx(eModeTxWithGas); await eModeResponse.wait(1); - action = ProtocolAction.supply; - let supplyTxData = supply({ - amount: parseUnits(amountToSupply, decimals).toString(), - reserve: poolAddress, - }); - supplyTxData = await estimateGasLimit(supplyTxData); - response = await sendTx(supplyTxData); + if (usePermit && signatureParams) { + action = ProtocolAction.supplyWithPermit; + let signedSupplyWithPermitTxData = supplyWithPermit({ + signature: signatureParams.signature, + amount: parseUnits(amountToSupply, decimals).toString(), + reserve: poolAddress, + deadline: signatureParams.deadline, + }); + signedSupplyWithPermitTxData = await estimateGasLimit(signedSupplyWithPermitTxData); + response = await sendTx(signedSupplyWithPermitTxData); + } else { + action = ProtocolAction.supply; + let supplyTxData = supply({ + amount: parseUnits(amountToSupply, decimals).toString(), + reserve: poolAddress, + }); + supplyTxData = await estimateGasLimit(supplyTxData); + response = await sendTx(supplyTxData); + } await response.wait(1); } else { // ERC20: Bundle setUserEMode + supply via Pool multicall diff --git a/src/hooks/pool/usePoolSupportsMulticall.ts b/src/hooks/pool/usePoolSupportsMulticall.ts new file mode 100644 index 0000000000..e538d4123d --- /dev/null +++ b/src/hooks/pool/usePoolSupportsMulticall.ts @@ -0,0 +1,40 @@ +import { useQuery } from '@tanstack/react-query'; +import { Contract } from 'ethers'; +import { MarketDataType } from 'src/ui-config/marketsConfig'; +import { getProvider } from 'src/utils/marketsAndNetworksConfig'; + +/** + * Pool.multicall was introduced in Aave v3.4, which bumped POOL_REVISION to 8. + * Pools below that revision revert on the call, so anything that bundles pool + * actions has to fall back to sending them sequentially. + */ +export const MULTICALL_POOL_REVISION = 8; + +const POOL_REVISION_ABI = ['function POOL_REVISION() view returns (uint256)']; + +export const usePoolSupportsMulticall = (marketData: MarketDataType) => { + const poolAddress = marketData.addresses.LENDING_POOL; + const chainId = marketData.chainId; + + return useQuery({ + queryFn: async () => { + try { + const provider = getProvider(chainId); + const revision = await new Contract( + poolAddress, + POOL_REVISION_ABI, + provider + ).POOL_REVISION(); + return revision.gte(MULTICALL_POOL_REVISION); + } catch (error) { + // Pools that predate the getter, and any RPC failure, fall back to + // sequential transactions — those work on every revision. + console.error('Error reading POOL_REVISION:', error); + return false; + } + }, + queryKey: ['poolSupportsMulticall', poolAddress, chainId], + enabled: !!poolAddress, + staleTime: Infinity, + }); +}; From 9eb2b15d5be42c3c4cc5eade5e6c32d4b0a5aef5 Mon Sep 17 00:00:00 2001 From: Mark Grothe Date: Mon, 31 Aug 2026 09:12:15 -0500 Subject: [PATCH 2/4] fix: default to non-multicall flows --- .../CollateralChangeActions.tsx | 73 ++++++------------- .../CollateralChangeModalContent.tsx | 8 +- .../transactions/Supply/SupplyActions.tsx | 36 +++------ .../Supply/SupplyModalContent.tsx | 5 ++ .../app-data-provider/useAppDataProvider.tsx | 6 ++ src/hooks/pool/usePoolSupportsMulticall.ts | 9 ++- 6 files changed, 55 insertions(+), 82 deletions(-) diff --git a/src/components/transactions/CollateralChange/CollateralChangeActions.tsx b/src/components/transactions/CollateralChange/CollateralChangeActions.tsx index badc770548..31e020998a 100644 --- a/src/components/transactions/CollateralChange/CollateralChangeActions.tsx +++ b/src/components/transactions/CollateralChange/CollateralChangeActions.tsx @@ -2,11 +2,10 @@ import { ProtocolAction } from '@aave/contract-helpers'; import { TransactionResponse } from '@ethersproject/providers'; import { Trans } from '@lingui/macro'; import { useQueryClient } from '@tanstack/react-query'; -import { Contract, PopulatedTransaction } from 'ethers'; +import { Contract } from 'ethers'; import React from 'react'; import { useTransactionHandler } from 'src/helpers/useTransactionHandler'; import { ComputedReserveData } from 'src/hooks/app-data-provider/useAppDataProvider'; -import { usePoolSupportsMulticall } from 'src/hooks/pool/usePoolSupportsMulticall'; import { useModalContext } from 'src/hooks/useModal'; import { useWeb3Context } from 'src/libs/hooks/useWeb3Context'; import { useRootStore } from 'src/store/root'; @@ -40,10 +39,9 @@ export const CollateralChangeActions = ({ symbol, selectedEmodeId, }: CollateralChangeActionsProps) => { - const [setUsageAsCollateral, setUserEMode, estimateGasLimit, currentMarketData] = useRootStore( + const [setUsageAsCollateral, estimateGasLimit, currentMarketData] = useRootStore( useShallow((state) => [ state.setUsageAsCollateral, - state.setUserEMode, state.estimateGasLimit, state.currentMarketData, ]) @@ -51,8 +49,6 @@ export const CollateralChangeActions = ({ const { sendTx } = useWeb3Context(); const queryClient = useQueryClient(); const { mainTxState, setMainTxState, setTxError } = useModalContext(); - // Resolved on modal open so it is cached well before the user picks an e-mode. - const { data: poolSupportsMulticall } = usePoolSupportsMulticall(currentMarketData); const needsEmodeSwitch = selectedEmodeId !== undefined; @@ -80,52 +76,29 @@ export const CollateralChangeActions = ({ skip: blocked || needsEmodeSwitch, }); - // setUserEMode + setUserUseReserveAsCollateral, bundled via Pool.multicall - // where the pool supports it and sent sequentially where it does not. - const emodeAction = async () => { + // Multicall action: setUserEMode + setUserUseReserveAsCollateral + const multicallAction = async () => { try { setMainTxState({ ...mainTxState, loading: true }); - let response: TransactionResponse; - - if (poolSupportsMulticall) { - const poolContractAddress = currentMarketData.addresses.LENDING_POOL; - const poolInterface = new Contract(poolContractAddress, POOL_MULTICALL_ABI).interface; - const currentAccount = useRootStore.getState().account; - - const setEModeCalldata = poolInterface.encodeFunctionData('setUserEMode', [ - selectedEmodeId, - ]); - - const setCollateralCalldata = poolInterface.encodeFunctionData( - 'setUserUseReserveAsCollateral', - [poolReserve.underlyingAsset, usageAsCollateral] - ); - - let multicallTxData = await new Contract( - poolContractAddress, - POOL_MULTICALL_ABI - ).populateTransaction.multicall([setEModeCalldata, setCollateralCalldata]); - multicallTxData = { ...multicallTxData, from: currentAccount }; - multicallTxData = await estimateGasLimit(multicallTxData); - response = await sendTx(multicallTxData); - } else { - // Pools below POOL_REVISION 8 have no multicall — send the two calls - // one after the other instead. - const eModeTxs = await setUserEMode(selectedEmodeId as number); - const eModeTx = await eModeTxs[0].tx(); - const eModeTxWithGas = await estimateGasLimit(eModeTx as PopulatedTransaction); - const eModeResponse = await sendTx(eModeTxWithGas); - await eModeResponse.wait(1); - - const collateralTxs = await setUsageAsCollateral({ - reserve: poolReserve.underlyingAsset, - usageAsCollateral, - }); - const collateralTx = await collateralTxs[0].tx(); - const collateralTxWithGas = await estimateGasLimit(collateralTx as PopulatedTransaction); - response = await sendTx(collateralTxWithGas); - } + const poolContractAddress = currentMarketData.addresses.LENDING_POOL; + const poolInterface = new Contract(poolContractAddress, POOL_MULTICALL_ABI).interface; + const currentAccount = useRootStore.getState().account; + + const setEModeCalldata = poolInterface.encodeFunctionData('setUserEMode', [selectedEmodeId]); + + const setCollateralCalldata = poolInterface.encodeFunctionData( + 'setUserUseReserveAsCollateral', + [poolReserve.underlyingAsset, usageAsCollateral] + ); + + let multicallTxData = await new Contract( + poolContractAddress, + POOL_MULTICALL_ABI + ).populateTransaction.multicall([setEModeCalldata, setCollateralCalldata]); + multicallTxData = { ...multicallTxData, from: currentAccount }; + multicallTxData = await estimateGasLimit(multicallTxData); + const response: TransactionResponse = await sendTx(multicallTxData); await response.wait(1); @@ -163,7 +136,7 @@ export const CollateralChangeActions = ({ ) } actionInProgressText={Pending...} - handleAction={needsEmodeSwitch ? emodeAction : action} + handleAction={needsEmodeSwitch ? multicallAction : action} /> ); }; diff --git a/src/components/transactions/CollateralChange/CollateralChangeModalContent.tsx b/src/components/transactions/CollateralChange/CollateralChangeModalContent.tsx index 73c56f385f..882e1798ab 100644 --- a/src/components/transactions/CollateralChange/CollateralChangeModalContent.tsx +++ b/src/components/transactions/CollateralChange/CollateralChangeModalContent.tsx @@ -41,7 +41,7 @@ export const CollateralChangeModalContent = ({ }: ModalWrapperProps & { user: ExtendedFormattedUser }) => { const { gasLimit, mainTxState: collateralChangeTxState, txError } = useModalContext(); const { debtCeiling } = useAssetCaps(); - const { reserves, eModes } = useAppDataContext(); + const { reserves, eModes, poolSupportsMulticall } = useAppDataContext(); const [collateralEnabled, setCollateralEnabled] = useState( userReserve.usageAsCollateralEnabledOnUser @@ -94,7 +94,11 @@ export const CollateralChangeModalContent = ({ } else if ( !userReserve.usageAsCollateralEnabledOnUser && !hasNonZeroLtv && - collateralEmodeCategories.length > 0 + collateralEmodeCategories.length > 0 && + // Enabling collateral on a 0 LTV asset only works if the e-mode switch + // lands first, which needs Pool.multicall. Without it, fall through to the + // plain "can not use as collateral" block, as before bundling existed. + poolSupportsMulticall ) { blockingError = ErrorType.ZERO_LTV_ENABLE_EMODE_FIRST; } else if (!userReserve.usageAsCollateralEnabledOnUser && !hasNonZeroLtv) { diff --git a/src/components/transactions/Supply/SupplyActions.tsx b/src/components/transactions/Supply/SupplyActions.tsx index fdeccee6fb..f522141c1e 100644 --- a/src/components/transactions/Supply/SupplyActions.tsx +++ b/src/components/transactions/Supply/SupplyActions.tsx @@ -12,7 +12,6 @@ import { Contract, PopulatedTransaction } from 'ethers'; import { parseUnits, splitSignature } from 'ethers/lib/utils'; import React, { useEffect, useState } from 'react'; import { useAppDataContext } from 'src/hooks/app-data-provider/useAppDataProvider'; -import { usePoolSupportsMulticall } from 'src/hooks/pool/usePoolSupportsMulticall'; import { SignedParams, useApprovalTx } from 'src/hooks/useApprovalTx'; import { usePoolApprovedAmount } from 'src/hooks/useApprovedAmount'; import { useModalContext } from 'src/hooks/useModal'; @@ -97,8 +96,6 @@ export const SupplyActions = React.memo( } = useModalContext(); const permitAvailable = tryPermit({ reserveAddress: poolAddress, isWrappedBaseAsset }); const { sendTx } = useWeb3Context(); - // Resolved on modal open so it is cached well before the user picks an e-mode. - const { data: poolSupportsMulticall } = usePoolSupportsMulticall(currentMarketData); const queryClient = useQueryClient(); const [signatureParams, setSignatureParams] = useState(); @@ -179,35 +176,22 @@ export const SupplyActions = React.memo( const isNativeAsset = poolAddress.toLowerCase() === API_ETH_MOCK_ADDRESS.toLowerCase(); if (needsEmodeSwitch) { - if (isNativeAsset || !poolSupportsMulticall) { - // Native ETH goes through WETH Gateway which is a separate contract, - // and pools below POOL_REVISION 8 have no multicall at all — neither - // can be bundled. Send setUserEMode first, then supply. + if (isNativeAsset) { + // Native ETH goes through WETH Gateway which is a separate contract — + // can't bundle with Pool.multicall. Send setUserEMode first, then supply. const eModeTxs = await setUserEMode(selectedEmodeId); const eModeTx = await eModeTxs[0].tx(); const eModeTxWithGas = await estimateGasLimit(eModeTx as PopulatedTransaction); const eModeResponse = await sendTx(eModeTxWithGas); await eModeResponse.wait(1); - if (usePermit && signatureParams) { - action = ProtocolAction.supplyWithPermit; - let signedSupplyWithPermitTxData = supplyWithPermit({ - signature: signatureParams.signature, - amount: parseUnits(amountToSupply, decimals).toString(), - reserve: poolAddress, - deadline: signatureParams.deadline, - }); - signedSupplyWithPermitTxData = await estimateGasLimit(signedSupplyWithPermitTxData); - response = await sendTx(signedSupplyWithPermitTxData); - } else { - action = ProtocolAction.supply; - let supplyTxData = supply({ - amount: parseUnits(amountToSupply, decimals).toString(), - reserve: poolAddress, - }); - supplyTxData = await estimateGasLimit(supplyTxData); - response = await sendTx(supplyTxData); - } + action = ProtocolAction.supply; + let supplyTxData = supply({ + amount: parseUnits(amountToSupply, decimals).toString(), + reserve: poolAddress, + }); + supplyTxData = await estimateGasLimit(supplyTxData); + response = await sendTx(supplyTxData); await response.wait(1); } else { // ERC20: Bundle setUserEMode + supply via Pool multicall diff --git a/src/components/transactions/Supply/SupplyModalContent.tsx b/src/components/transactions/Supply/SupplyModalContent.tsx index d049b37372..2b95985a30 100644 --- a/src/components/transactions/Supply/SupplyModalContent.tsx +++ b/src/components/transactions/Supply/SupplyModalContent.tsx @@ -155,6 +155,7 @@ export const SupplyModalContent = React.memo( eModes, reserves, userReserves, + poolSupportsMulticall, } = useAppDataContext(); const currentTimestamp = useCurrentTimestamp(1); const { mainTxState: supplyTxState, gasLimit, txError } = useModalContext(); @@ -172,6 +173,10 @@ export const SupplyModalContent = React.memo( const [showUSDTResetWarning, setShowUSDTResetWarning] = useState(false); const [selectedEmodeId, setSelectedEmodeId] = useState(user.userEmodeCategoryId); const hasEmodeOptions = + // Switching e-mode as part of the supply needs Pool.multicall to bundle + // the two calls atomically. Pools without it keep the pre-bundling + // behaviour: supply only, e-mode set separately from the dashboard. + poolSupportsMulticall && !poolReserve.isIsolated && !user.isInIsolationMode && poolReserve.eModes.filter((e) => e.id !== 0 && e.collateralEnabled).length > 0; diff --git a/src/hooks/app-data-provider/useAppDataProvider.tsx b/src/hooks/app-data-provider/useAppDataProvider.tsx index 266dbdff16..307f2a335b 100644 --- a/src/hooks/app-data-provider/useAppDataProvider.tsx +++ b/src/hooks/app-data-provider/useAppDataProvider.tsx @@ -17,6 +17,7 @@ import { usePoolFormattedReserves, } from '../pool/usePoolFormattedReserves'; import { usePoolReservesHumanized } from '../pool/usePoolReserves'; +import { usePoolSupportsMulticall } from '../pool/usePoolSupportsMulticall'; import { useUserPoolReservesHumanized } from '../pool/useUserPoolReserves'; import { FormattedUserReserves } from '../pool/useUserSummaryAndIncentives'; import { useMarketsData } from './useMarketsData'; @@ -57,6 +58,8 @@ export interface AppDataContextType { /** Legacy fields (deprecated) kept temporarily for incremental migration */ reserves: ComputedReserveData[]; eModes: Record; + /** Pool.multicall exists (POOL_REVISION >= 8) — gates the bundled e-mode flows. */ + poolSupportsMulticall: boolean; user?: ExtendedFormattedUser; marketReferencePriceInUsd: string; marketReferenceCurrencyDecimals: number; @@ -126,6 +129,8 @@ export const AppDataProvider: React.FC = ({ children }) => { const eModes = formattedPoolReserves ? formatEmodes(formattedPoolReserves) : {}; + const { data: poolSupportsMulticall } = usePoolSupportsMulticall(currentMarketData); + const { data: userReservesData, isPending: userReservesDataLoading } = useUserPoolReservesHumanized(currentMarketData); const { data: userSummary, isPending: userSummaryLoading } = @@ -151,6 +156,7 @@ export const AppDataProvider: React.FC = ({ children }) => { // Legacy fields (to be removed once consumers migrate) reserves: formattedPoolReserves || [], eModes, + poolSupportsMulticall: poolSupportsMulticall ?? false, user: userSummary, userReserves: userReserves || [], marketReferencePriceInUsd: baseCurrencyData?.marketReferenceCurrencyPriceInUsd || '0', diff --git a/src/hooks/pool/usePoolSupportsMulticall.ts b/src/hooks/pool/usePoolSupportsMulticall.ts index e538d4123d..684e6ff005 100644 --- a/src/hooks/pool/usePoolSupportsMulticall.ts +++ b/src/hooks/pool/usePoolSupportsMulticall.ts @@ -5,8 +5,8 @@ import { getProvider } from 'src/utils/marketsAndNetworksConfig'; /** * Pool.multicall was introduced in Aave v3.4, which bumped POOL_REVISION to 8. - * Pools below that revision revert on the call, so anything that bundles pool - * actions has to fall back to sending them sequentially. + * Pools below that revision revert on the call, so the flows that bundle pool + * actions are hidden entirely on those markets. */ export const MULTICALL_POOL_REVISION = 8; @@ -27,8 +27,9 @@ export const usePoolSupportsMulticall = (marketData: MarketDataType) => { ).POOL_REVISION(); return revision.gte(MULTICALL_POOL_REVISION); } catch (error) { - // Pools that predate the getter, and any RPC failure, fall back to - // sequential transactions — those work on every revision. + // Pools that predate the getter, and any RPC failure, report no + // support — the bundled flows stay hidden rather than sending a tx + // that would revert. console.error('Error reading POOL_REVISION:', error); return false; } From 5fe4c7ed894d31a07588e28e5f026b5c8fab34d1 Mon Sep 17 00:00:00 2001 From: Mark Grothe Date: Mon, 31 Aug 2026 09:14:50 -0500 Subject: [PATCH 3/4] fix: comment --- src/hooks/app-data-provider/useAppDataProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/app-data-provider/useAppDataProvider.tsx b/src/hooks/app-data-provider/useAppDataProvider.tsx index 307f2a335b..0d6cee8e14 100644 --- a/src/hooks/app-data-provider/useAppDataProvider.tsx +++ b/src/hooks/app-data-provider/useAppDataProvider.tsx @@ -58,7 +58,7 @@ export interface AppDataContextType { /** Legacy fields (deprecated) kept temporarily for incremental migration */ reserves: ComputedReserveData[]; eModes: Record; - /** Pool.multicall exists (POOL_REVISION >= 8) — gates the bundled e-mode flows. */ + /** Pool.multicall exists (POOL_REVISION >= 8) */ poolSupportsMulticall: boolean; user?: ExtendedFormattedUser; marketReferencePriceInUsd: string; From 9a966a6e179fc38ee577e95d632b74cda3f7a9d9 Mon Sep 17 00:00:00 2001 From: Mark Grothe Date: Mon, 31 Aug 2026 09:27:45 -0500 Subject: [PATCH 4/4] fix: error handling --- src/hooks/pool/usePoolSupportsMulticall.ts | 32 ++++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/hooks/pool/usePoolSupportsMulticall.ts b/src/hooks/pool/usePoolSupportsMulticall.ts index 684e6ff005..2ec67154e1 100644 --- a/src/hooks/pool/usePoolSupportsMulticall.ts +++ b/src/hooks/pool/usePoolSupportsMulticall.ts @@ -12,30 +12,32 @@ export const MULTICALL_POOL_REVISION = 8; const POOL_REVISION_ABI = ['function POOL_REVISION() view returns (uint256)']; +/** + * Only applies if there was an rpc error. A successful answer cached and never refetched. + */ +const ERROR_RETRY_INTERVAL = 30_000; + export const usePoolSupportsMulticall = (marketData: MarketDataType) => { const poolAddress = marketData.addresses.LENDING_POOL; const chainId = marketData.chainId; return useQuery({ queryFn: async () => { - try { - const provider = getProvider(chainId); - const revision = await new Contract( - poolAddress, - POOL_REVISION_ABI, - provider - ).POOL_REVISION(); - return revision.gte(MULTICALL_POOL_REVISION); - } catch (error) { - // Pools that predate the getter, and any RPC failure, report no - // support — the bundled flows stay hidden rather than sending a tx - // that would revert. - console.error('Error reading POOL_REVISION:', error); - return false; - } + // Errors are deliberately not swallowed. ethers reports a reverted call + // and an unreachable RPC with the same code, so there is no way to tell a + // pool that lacks the getter from a transient failure — and catching both + // would cache a false negative for the rest of the session. Letting the + // error through lets react-query retry; callers read the absent value as + // "no multicall", which is the safe default either way. + const provider = getProvider(chainId); + const revision = await new Contract(poolAddress, POOL_REVISION_ABI, provider).POOL_REVISION(); + return revision.gte(MULTICALL_POOL_REVISION); }, queryKey: ['poolSupportsMulticall', poolAddress, chainId], enabled: !!poolAddress, staleTime: Infinity, + // The default retries cover a blip; this keeps trying through a longer + // outage so the bundled flows come back without a page reload. + refetchInterval: (query) => (query.state.status === 'error' ? ERROR_RETRY_INTERVAL : false), }); };