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