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