diff --git a/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx b/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx index 37f8735a4d..50bdbfc53f 100644 --- a/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx +++ b/src/components/transactions/TxDetails/TxData/DecodedData/index.tsx @@ -12,6 +12,7 @@ import MethodCall from './MethodCall' import useSafeAddress from '@/hooks/useSafeAddress' import { sameAddress } from '@/utils/addresses' import { DelegateCallWarning } from '@/components/transactions/Warning' +import { isMigrateToL2TxData } from '@/utils/transaction-guards' interface Props { txData: TransactionDetails['txData'] @@ -58,9 +59,11 @@ export const DecodedData = ({ txData, toInfo }: Props): ReactElement | null => { decodedData = } + const isL2Migration = isMigrateToL2TxData(txData, chainInfo?.chainId) + return ( - {isDelegateCall && } + {isDelegateCall && } {method ? ( diff --git a/src/components/transactions/TxDetails/TxData/index.tsx b/src/components/transactions/TxDetails/TxData/index.tsx index 9eb018ee9d..2c92a9493c 100644 --- a/src/components/transactions/TxDetails/TxData/index.tsx +++ b/src/components/transactions/TxDetails/TxData/index.tsx @@ -73,7 +73,7 @@ const TxData = ({ return } - if (isMigrateToL2TxData(txDetails.txData)) { + if (isMigrateToL2TxData(txDetails.txData, chainId)) { return } return diff --git a/src/utils/transaction-guards.ts b/src/utils/transaction-guards.ts index 0d5e1e55ea..bf9227901c 100644 --- a/src/utils/transaction-guards.ts +++ b/src/utils/transaction-guards.ts @@ -57,8 +57,12 @@ import { sameAddress } from '@/utils/addresses' import type { NamedAddress } from '@/components/new-safe/create/types' import type { RecoveryQueueItem } from '@/features/recovery/services/recovery-state' import { ethers } from 'ethers' -import { getSafeToL2MigrationDeployment } from '@safe-global/safe-deployments' +import { getSafeToL2MigrationDeployment, getMultiSendDeployments } from '@safe-global/safe-deployments' import { Safe_to_l2_migration__factory } from '@/types/contracts' +import { hasMatchingDeployment } from '@/services/contracts/deployments' +import { isMultiSendCalldata } from './transaction-calldata' +import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils' +import { OperationType } from '@safe-global/safe-core-sdk-types' export const isTxQueued = (value: TransactionStatus): boolean => { return [TransactionStatus.AWAITING_CONFIRMATIONS, TransactionStatus.AWAITING_EXECUTION].includes(value) @@ -87,18 +91,50 @@ export const isModuleDetailedExecutionInfo = (value?: DetailedExecutionInfo): va return value?.type === DetailedExecutionInfoType.MODULE } -export const isMigrateToL2TxData = (value: TransactionData | undefined): boolean => { +const isMigrateToL2CallData = (value: { + to: string + data: string | undefined + operation?: OperationType | undefined +}) => { const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment() const safeToL2MigrationAddress = safeToL2MigrationDeployment?.defaultAddress const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface() - if (sameAddress(value?.to.value, safeToL2MigrationAddress)) { + if (value.operation === OperationType.DelegateCall && sameAddress(value.to, safeToL2MigrationAddress)) { const migrateToL2Selector = safeToL2MigrationInterface?.getFunction('migrateToL2')?.selector - return migrateToL2Selector && value?.hexData ? value.hexData?.startsWith(migrateToL2Selector) : false + return migrateToL2Selector && value.data ? value.data.startsWith(migrateToL2Selector) : false } return false } +export const isMigrateToL2TxData = (value: TransactionData | undefined, chainId: string | undefined): boolean => { + if (!value) { + return false + } + + if ( + chainId && + value?.hexData && + isMultiSendCalldata(value?.hexData) && + hasMatchingDeployment(getMultiSendDeployments, value.to.value, chainId, ['1.3.0', '1.4.1']) + ) { + // Its a multiSend to the MultiSend contract (not CallOnly) + const decodedMultiSend = decodeMultiSendData(value.hexData) + const firstTx = decodedMultiSend[0] + + // We only trust the tx if the first tx is the only delegateCall + const hasMoreDelegateCalls = decodedMultiSend + .slice(1) + .some((value) => value.operation === OperationType.DelegateCall) + + if (!hasMoreDelegateCalls && firstTx && isMigrateToL2CallData(firstTx)) { + return true + } + } + + return isMigrateToL2CallData({ to: value.to.value, data: value.hexData, operation: value.operation as 0 | 1 }) +} + // TransactionInfo type guards export const isTransferTxInfo = (value: TransactionInfo): value is Transfer => { return value.type === TransactionInfoType.TRANSFER || isSwapTransferOrderTxInfo(value)