diff --git a/package.json b/package.json index 59382a2985..9608c5c087 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@safe-global/api-kit": "^2.4.6", "@safe-global/protocol-kit": "^4.1.1", "@safe-global/safe-apps-sdk": "^9.1.0", - "@safe-global/safe-deployments": "^1.37.10", + "@safe-global/safe-deployments": "1.37.12", "@safe-global/safe-client-gateway-sdk": "1.58.0-next-25bba61", "@safe-global/safe-gateway-typescript-sdk": "3.22.3-beta.15", "@safe-global/safe-modules-deployments": "^2.2.1", diff --git a/src/components/common/NetworkSelector/NetworkMultiSelector.tsx b/src/components/common/NetworkSelector/NetworkMultiSelector.tsx index 5a4fbdcc68..3e57812247 100644 --- a/src/components/common/NetworkSelector/NetworkMultiSelector.tsx +++ b/src/components/common/NetworkSelector/NetworkMultiSelector.tsx @@ -9,7 +9,7 @@ import { Controller, useFormContext, useWatch } from 'react-hook-form' import { useRouter } from 'next/router' import { getNetworkLink } from '.' import { SetNameStepFields } from '@/components/new-safe/create/steps/SetNameStep' -import { getSafeSingletonDeployments } from '@safe-global/safe-deployments' +import { getSafeSingletonDeployments, getSafeToL2SetupDeployments } from '@safe-global/safe-deployments' import { getLatestSafeVersion } from '@/utils/chains' import { hasCanonicalDeployment } from '@/services/contracts/deployments' import { hasMultiChainCreationFeatures } from '@/features/multichain/utils/utils' @@ -74,22 +74,33 @@ const NetworkMultiSelector = ({ } // Check if required deployments are available - const optionHasCanonicalSingletonDeployment = hasCanonicalDeployment( - getSafeSingletonDeployments({ - network: optionNetwork.chainId, - version: getLatestSafeVersion(firstSelectedNetwork), - }), - optionNetwork.chainId, - ) - const selectedHasCanonicalSingletonDeployment = hasCanonicalDeployment( - getSafeSingletonDeployments({ - network: firstSelectedNetwork.chainId, - version: getLatestSafeVersion(firstSelectedNetwork), - }), - firstSelectedNetwork.chainId, - ) + const optionHasCanonicalSingletonDeployment = + hasCanonicalDeployment( + getSafeSingletonDeployments({ + network: optionNetwork.chainId, + version: getLatestSafeVersion(firstSelectedNetwork), + }), + optionNetwork.chainId, + ) && + hasCanonicalDeployment( + getSafeToL2SetupDeployments({ network: optionNetwork.chainId, version: '1.4.1' }), + optionNetwork.chainId, + ) - // Only 1.4.1 safes with canonical deployment addresses can be deployed as part of a multichain group + const selectedHasCanonicalSingletonDeployment = + hasCanonicalDeployment( + getSafeSingletonDeployments({ + network: firstSelectedNetwork.chainId, + version: getLatestSafeVersion(firstSelectedNetwork), + }), + firstSelectedNetwork.chainId, + ) && + hasCanonicalDeployment( + getSafeToL2SetupDeployments({ network: firstSelectedNetwork.chainId, version: '1.4.1' }), + firstSelectedNetwork.chainId, + ) + + // Only 1.4.1 safes with canonical deployment addresses and SafeToL2Setup can be deployed as part of a multichain group if (!selectedHasCanonicalSingletonDeployment) return !optionIsSelectedNetwork return !optionHasCanonicalSingletonDeployment }, @@ -128,7 +139,7 @@ const NetworkMultiSelector = ({ )) } renderOption={(props, chain, { selected }) => ( -
  • +
  • diff --git a/src/components/new-safe/create/logic/index.ts b/src/components/new-safe/create/logic/index.ts index d7d629eb17..af85829355 100644 --- a/src/components/new-safe/create/logic/index.ts +++ b/src/components/new-safe/create/logic/index.ts @@ -228,8 +228,9 @@ export const createNewUndeployedSafeWithoutSalt = ( const safeToL2SetupAddress = safeToL2SetupDeployment?.networkAddresses[chain.chainId] const safeToL2SetupInterface = Safe_to_l2_setup__factory.createInterface() - // Only do migration if the chain supports multiChain deployments. - const includeMigration = hasMultiChainCreationFeatures(chain) && semverSatisfies(safeVersion, '>=1.4.1') + // Only do migration if the chain supports multiChain deployments and has a SafeToL2Setup deployment + const includeMigration = + hasMultiChainCreationFeatures(chain) && semverSatisfies(safeVersion, '>=1.4.1') && Boolean(safeToL2SetupAddress) const masterCopy = includeMigration ? safeL1Address : chain.l2 ? safeL2Address : safeL1Address diff --git a/src/components/transactions/TxDetails/TxData/MigrationToL2TxData/index.tsx b/src/components/transactions/TxDetails/TxData/MigrationToL2TxData/index.tsx index 636769abc7..2d4a106297 100644 --- a/src/components/transactions/TxDetails/TxData/MigrationToL2TxData/index.tsx +++ b/src/components/transactions/TxDetails/TxData/MigrationToL2TxData/index.tsx @@ -14,6 +14,7 @@ import ErrorMessage from '@/components/tx/ErrorMessage' import { useSafeSDK } from '@/hooks/coreSDK/safeCoreSDK' import { MigrateToL2Information } from '@/components/tx/SignOrExecuteForm/MigrateToL2Information' import { Box } from '@mui/material' +import { isMultisigDetailedExecutionInfo } from '@/utils/transaction-guards' export const MigrationToL2TxData = ({ txDetails }: { txDetails: TransactionDetails }) => { const readOnlyProvider = useWeb3ReadOnly() @@ -47,19 +48,24 @@ export const MigrationToL2TxData = ({ txDetails }: { txDetails: TransactionDetai if (!execTxArgs || execTxArgs.length < 10) { return undefined } - return createTx({ - to: execTxArgs[0], - value: execTxArgs[1].toString(), - data: execTxArgs[2], - operation: Number(execTxArgs[3]), - safeTxGas: execTxArgs[4].toString(), - baseGas: execTxArgs[5].toString(), - gasPrice: execTxArgs[6].toString(), - gasToken: execTxArgs[7].toString(), - refundReceiver: execTxArgs[8], - }) + return createTx( + { + to: execTxArgs[0], + value: execTxArgs[1].toString(), + data: execTxArgs[2], + operation: Number(execTxArgs[3]), + safeTxGas: execTxArgs[4].toString(), + baseGas: execTxArgs[5].toString(), + gasPrice: execTxArgs[6].toString(), + gasToken: execTxArgs[7].toString(), + refundReceiver: execTxArgs[8], + }, + isMultisigDetailedExecutionInfo(txDetails.detailedExecutionInfo) + ? txDetails.detailedExecutionInfo.nonce + : undefined, + ) } - }, [readOnlyProvider, txDetails.txHash, chain, safe.version, sdk]) + }, [txDetails.txHash, txDetails.detailedExecutionInfo, chain, sdk, readOnlyProvider, safe.version]) const [decodedRealTx, decodedRealTxError] = useDecodeTx(realSafeTx) diff --git a/src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts b/src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts index 9aa0f7a208..532623a4e6 100644 --- a/src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts +++ b/src/features/multichain/hooks/__tests__/useCompatibleNetworks.test.ts @@ -31,11 +31,12 @@ describe('useCompatibleNetworks', () => { beforeAll(() => { jest.spyOn(useChains, 'default').mockReturnValue({ configs: [ - chainBuilder().with({ chainId: '1' }).build(), - chainBuilder().with({ chainId: '10' }).build(), // This has the eip155 and then the canonical addresses - chainBuilder().with({ chainId: '100' }).build(), // This has the canonical and then the eip155 addresses - chainBuilder().with({ chainId: '324' }).build(), // ZkSync has different addresses for all versions - chainBuilder().with({ chainId: '480' }).build(), // Worldchain has 1.4.1 but not 1.1.1 + chainBuilder().with({ chainId: '1', l2: false }).build(), + chainBuilder().with({ chainId: '10', l2: true }).build(), // This has the eip155 and then the canonical addresses + chainBuilder().with({ chainId: '100', l2: true }).build(), // This has the canonical and then the eip155 addresses + chainBuilder().with({ chainId: '324', l2: true }).build(), // ZkSync has different addresses for all versions + chainBuilder().with({ chainId: '480', l2: true }).build(), // Worldchain has 1.4.1 but not 1.1.1 + chainBuilder().with({ chainId: '10200', l2: true }).build(), // Gnosis Chiado has no migration contracts ], }) }) @@ -68,7 +69,7 @@ describe('useCompatibleNetworks', () => { expect(result.current.every((config) => config.available)).toEqual(false) }) - it('should set everything to available except zkSync for 1.4.1 Safes', () => { + it('should set everything to available except zkSync and GnosisChain Chiado for 1.4.1 Safes', () => { const callData = { owners: [faker.finance.ethereumAddress()], threshold: 1, @@ -88,9 +89,9 @@ describe('useCompatibleNetworks', () => { safeVersion: '1.4.1', } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(5) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) - expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true]) + expect(result.current).toHaveLength(6) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480', '10200']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true, false]) } { @@ -102,9 +103,9 @@ describe('useCompatibleNetworks', () => { safeVersion: '1.4.1', } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(5) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) - expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true]) + expect(result.current).toHaveLength(6) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480', '10200']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true, false]) } }) @@ -120,7 +121,7 @@ describe('useCompatibleNetworks', () => { paymentReceiver: ECOSYSTEM_ID_ADDRESS, } - // 1.3.0, L1 and canonical + // 1.3.0, L1 and canonical, not available on Chiado as no migration exists { const creationData: ReplayedSafeProps = { factoryAddress: PROXY_FACTORY_130_DEPLOYMENTS?.canonical?.address!, @@ -130,9 +131,9 @@ describe('useCompatibleNetworks', () => { safeVersion: '1.3.0', } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(5) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) - expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true]) + expect(result.current).toHaveLength(6) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480', '10200']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true, false]) } // 1.3.0, L2 and canonical @@ -145,12 +146,12 @@ describe('useCompatibleNetworks', () => { safeVersion: '1.3.0', } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(5) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) - expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true]) + expect(result.current).toHaveLength(6) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480', '10200']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, true, true]) } - // 1.3.0, L1 and EIP155 is not available on Worldchain + // 1.3.0, L1 and EIP155 is not available on Worldchain and Chiado { const creationData: ReplayedSafeProps = { factoryAddress: PROXY_FACTORY_130_DEPLOYMENTS?.eip155?.address!, @@ -160,9 +161,9 @@ describe('useCompatibleNetworks', () => { safeVersion: '1.3.0', } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(5) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) - expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, false]) + expect(result.current).toHaveLength(6) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480', '10200']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, false, false]) } // 1.3.0, L2 and EIP155 @@ -175,9 +176,9 @@ describe('useCompatibleNetworks', () => { safeVersion: '1.3.0', } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(5) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) - expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, false]) + expect(result.current).toHaveLength(6) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480', '10200']) + expect(result.current.map((chain) => chain.available)).toEqual([true, true, true, false, false, false]) } }) @@ -201,8 +202,8 @@ describe('useCompatibleNetworks', () => { safeVersion: '1.1.1', } const { result } = renderHook(() => useCompatibleNetworks(creationData)) - expect(result.current).toHaveLength(5) - expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480']) - expect(result.current.map((chain) => chain.available)).toEqual([false, false, false, false, false]) + expect(result.current).toHaveLength(6) + expect(result.current.map((chain) => chain.chainId)).toEqual(['1', '10', '100', '324', '480', '10200']) + expect(result.current.map((chain) => chain.available)).toEqual([false, false, false, false, false, false]) }) }) diff --git a/src/features/multichain/hooks/useCompatibleNetworks.ts b/src/features/multichain/hooks/useCompatibleNetworks.ts index ded3c949c4..881076dd32 100644 --- a/src/features/multichain/hooks/useCompatibleNetworks.ts +++ b/src/features/multichain/hooks/useCompatibleNetworks.ts @@ -1,12 +1,15 @@ import { type ReplayedSafeProps } from '@/features/counterfactual/store/undeployedSafesSlice' import useChains from '@/hooks/useChains' -import { hasMatchingDeployment } from '@/services/contracts/deployments' +import { hasCanonicalDeployment, hasMatchingDeployment } from '@/services/contracts/deployments' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' import { type SafeVersion } from '@safe-global/safe-core-sdk-types' import { getCompatibilityFallbackHandlerDeployments, getProxyFactoryDeployments, getSafeL2SingletonDeployments, getSafeSingletonDeployments, + getSafeToL2MigrationDeployments, + getSafeToL2SetupDeployments, } from '@safe-global/safe-deployments' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' @@ -27,21 +30,60 @@ export const useCompatibleNetworks = ( const { masterCopy, factoryAddress, safeAccountConfig } = creation - const { fallbackHandler } = safeAccountConfig + const { fallbackHandler, to } = safeAccountConfig return configs.map((config) => { + const isL1MasterCopy = hasMatchingDeployment( + getSafeSingletonDeployments, + masterCopy, + config.chainId, + SUPPORTED_VERSIONS, + ) + const isL2MasterCopy = hasMatchingDeployment( + getSafeL2SingletonDeployments, + masterCopy, + config.chainId, + SUPPORTED_VERSIONS, + ) + const masterCopyExists = isL1MasterCopy || isL2MasterCopy + + const proxyFactoryExists = hasMatchingDeployment( + getProxyFactoryDeployments, + factoryAddress, + config.chainId, + SUPPORTED_VERSIONS, + ) + const fallbackHandlerExists = hasMatchingDeployment( + getCompatibilityFallbackHandlerDeployments, + fallbackHandler, + config.chainId, + SUPPORTED_VERSIONS, + ) + + // We only need to check that it is nonzero as useSafeCreationData already validates that it is the setupToL2 call otherwise + const includesSetupToL2 = to !== ZERO_ADDRESS + + // If the creation includes the setupToL2 call, the contract needs to be deployed on the chain + const areSetupToL2ConditionsMet = + !includesSetupToL2 || + hasCanonicalDeployment(getSafeToL2SetupDeployments({ network: config.chainId, version: '1.4.1' }), config.chainId) + + // If the masterCopy is L1 on a L2 chain, includes the setupToL2 Call or the Migration contract exists + const isMigrationRequired = isL1MasterCopy && !includesSetupToL2 && config.l2 + const isMigrationPossible = hasCanonicalDeployment( + getSafeToL2MigrationDeployments({ network: config.chainId, version: '1.4.1' }), + config.chainId, + ) + const areMigrationConditionsMet = !isMigrationRequired || isMigrationPossible + return { ...config, available: - (hasMatchingDeployment(getSafeSingletonDeployments, masterCopy, config.chainId, SUPPORTED_VERSIONS) || - hasMatchingDeployment(getSafeL2SingletonDeployments, masterCopy, config.chainId, SUPPORTED_VERSIONS)) && - hasMatchingDeployment(getProxyFactoryDeployments, factoryAddress, config.chainId, SUPPORTED_VERSIONS) && - hasMatchingDeployment( - getCompatibilityFallbackHandlerDeployments, - fallbackHandler, - config.chainId, - SUPPORTED_VERSIONS, - ), + masterCopyExists && + proxyFactoryExists && + fallbackHandlerExists && + areSetupToL2ConditionsMet && + areMigrationConditionsMet, } }) } diff --git a/yarn.lock b/yarn.lock index 4dcef175cd..81802ca913 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4216,7 +4216,14 @@ dependencies: abitype "^1.0.2" -"@safe-global/safe-deployments@^1.37.10", "@safe-global/safe-deployments@^1.37.9": +"@safe-global/safe-deployments@1.37.12": + version "1.37.12" + resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.37.12.tgz#c01b0a272991ae0e63a37d506024a809de53c370" + integrity sha512-6UM5wS6b0h4Uu2BCK6sWLNC5UYFBd1Xu4YsuZpoSjlo/6UDbbK6lzYVEgtqbsy8p1z2ZO+Z73WgRo3mlh7awig== + dependencies: + semver "^7.6.2" + +"@safe-global/safe-deployments@^1.37.9": version "1.37.10" resolved "https://registry.yarnpkg.com/@safe-global/safe-deployments/-/safe-deployments-1.37.10.tgz#2f61a25bd479332821ba2e91a575237d77406ec3" integrity sha512-lcxX9CV+xdcLs4dF6Cx18zDww5JyqaX6RdcvU0o/34IgJ4Wjo3J/RNzJAoMhurCAfTGr+0vyJ9V13Qo50AR6JA== @@ -17238,16 +17245,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -17343,14 +17341,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -19106,7 +19097,7 @@ workbox-window@7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19124,15 +19115,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"