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)