diff --git a/src/components/common/ExternalLink/index.tsx b/src/components/common/ExternalLink/index.tsx index b70a61b347..c32b9f8004 100644 --- a/src/components/common/ExternalLink/index.tsx +++ b/src/components/common/ExternalLink/index.tsx @@ -22,6 +22,7 @@ const ExternalLink = ({ display: 'inline-flex', alignItems: 'center', gap: 0.2, + cursor: 'pointer', }} > {children} diff --git a/src/components/tx-flow/SafeTxProvider.tsx b/src/components/tx-flow/SafeTxProvider.tsx index e46ec0a846..63dd9d9a9d 100644 --- a/src/components/tx-flow/SafeTxProvider.tsx +++ b/src/components/tx-flow/SafeTxProvider.tsx @@ -7,7 +7,7 @@ import { Errors, logError } from '@/services/exceptions' import type { EIP712TypedData } from '@safe-global/safe-gateway-typescript-sdk' import useSafeInfo from '@/hooks/useSafeInfo' import { useCurrentChain } from '@/hooks/useChains' -import { prependSafeToL2Migration } from '@/utils/transactions' +import { prependSafeToL2Migration } from '@/utils/safe-migrations' import { useSelectAvailableSigner } from '@/hooks/wallets/useSelectAvailableSigner' export type SafeTxContextParams = { diff --git a/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx b/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx index 4fe31a6fda..a9eea0d76e 100644 --- a/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx +++ b/src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx @@ -5,7 +5,7 @@ import ExternalLink from '@/components/common/ExternalLink' import { useCurrentChain } from '@/hooks/useChains' import useSafeInfo from '@/hooks/useSafeInfo' import { createUpdateSafeTxs } from '@/services/tx/safeUpdateParams' -import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender' +import { createMultiSendCallOnlyTx, createTx } from '@/services/tx/tx-sender' import { SafeTxContext } from '../../SafeTxProvider' import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import useAsync from '@/hooks/useAsync' @@ -24,7 +24,8 @@ export const UpdateSafeReview = () => { } const txs = await createUpdateSafeTxs(safe, chain) - createMultiSendCallOnlyTx(txs).then(setSafeTx).catch(setSafeTxError) + const safeTxPromise = txs.length > 1 ? createMultiSendCallOnlyTx(txs) : createTx(txs[0]) + safeTxPromise.then(setSafeTx).catch(setSafeTxError) }, [safe, safeLoaded, chain, setSafeTx, setSafeTxError]) return ( diff --git a/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx b/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx index f5944f1a52..13552752eb 100644 --- a/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx +++ b/src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx @@ -30,7 +30,7 @@ import { BlockaidBalanceChanges } from '../security/blockaid/BlockaidBalanceChan import { Blockaid } from '../security/blockaid' import { MigrateToL2Information } from './MigrateToL2Information' -import { extractMigrationL2MasterCopyAddress } from '@/utils/transactions' +import { extractMigrationL2MasterCopyAddress } from '@/utils/safe-migrations' import { useLazyGetTransactionDetailsQuery } from '@/store/api/gateway' import { useApprovalInfos } from '../ApprovalEditor/hooks/useApprovalInfos' diff --git a/src/services/tx/safeUpdateParams.ts b/src/services/tx/safeUpdateParams.ts index 9b0fc5f320..5975ec6a22 100644 --- a/src/services/tx/safeUpdateParams.ts +++ b/src/services/tx/safeUpdateParams.ts @@ -2,11 +2,13 @@ import type { SafeContractImplementationType } from '@safe-global/protocol-kit/d import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types' import { OperationType } from '@safe-global/safe-core-sdk-types' import type { ChainInfo, SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' +import semverSatisfies from 'semver/functions/satisfies' import { getReadOnlyFallbackHandlerContract, getReadOnlyGnosisSafeContract } from '@/services/contracts/safeContracts' import { assertValidSafeVersion } from '@/hooks/coreSDK/safeCoreSDK' import { SAFE_FEATURES } from '@safe-global/protocol-kit/dist/src/utils/safeVersions' import { hasSafeFeature } from '@/utils/safe-versions' import { getLatestSafeVersion } from '@/utils/chains' +import { createUpdateMigration } from '@/utils/safe-migrations' const getChangeFallbackHandlerCallData = async ( safeContractInstance: SafeContractImplementationType, @@ -32,6 +34,10 @@ const getChangeFallbackHandlerCallData = async ( export const createUpdateSafeTxs = async (safe: SafeInfo, chain: ChainInfo): Promise => { assertValidSafeVersion(safe.version) + if (semverSatisfies(safe.version, '>=1.3.0')) { + return [createUpdateMigration(chain)] + } + const latestMasterCopyAddress = await ( await getReadOnlyGnosisSafeContract(chain, getLatestSafeVersion(chain)) ).getAddress() diff --git a/src/utils/__tests__/safe-migrations.test.ts b/src/utils/__tests__/safe-migrations.test.ts new file mode 100644 index 0000000000..59286f081b --- /dev/null +++ b/src/utils/__tests__/safe-migrations.test.ts @@ -0,0 +1,321 @@ +import { ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' +import { extractMigrationL2MasterCopyAddress, prependSafeToL2Migration } from '../safe-migrations' +import { extendedSafeInfoBuilder } from '@/tests/builders/safe' +import { chainBuilder } from '@/tests/builders/chains' +import { safeSignatureBuilder, safeTxBuilder, safeTxDataBuilder } from '@/tests/builders/safeTx' +import { + getMultiSendCallOnlyDeployment, + getMultiSendDeployment, + getSafeL2SingletonDeployment, + getSafeSingletonDeployment, + getSafeToL2MigrationDeployment, +} from '@safe-global/safe-deployments' +import type Safe from '@safe-global/protocol-kit' +import { encodeMultiSendData } from '@safe-global/protocol-kit' +import { Multi_send__factory, Safe_to_l2_migration__factory } from '@/types/contracts' +import { faker } from '@faker-js/faker' +import { getAndValidateSafeSDK } from '@/services/tx/tx-sender/sdk' +import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils' +import { checksumAddress } from '../addresses' + +jest.mock('@/services/tx/tx-sender/sdk') + +const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment() +const safeToL2MigrationAddress = safeToL2MigrationDeployment?.defaultAddress +const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface() +const multisendInterface = Multi_send__factory.createInterface() + +describe('prependSafeToL2Migration', () => { + const mockGetAndValidateSdk = getAndValidateSafeSDK as jest.MockedFunction + + beforeEach(() => { + // Mock create Tx + mockGetAndValidateSdk.mockReturnValue({ + createTransaction: ({ transactions, onlyCalls }) => { + return Promise.resolve( + safeTxBuilder() + .with({ + data: safeTxDataBuilder() + .with({ + to: onlyCalls + ? (getMultiSendCallOnlyDeployment()?.defaultAddress ?? faker.finance.ethereumAddress()) + : (getMultiSendDeployment()?.defaultAddress ?? faker.finance.ethereumAddress()), + value: '0', + data: Multi_send__factory.createInterface().encodeFunctionData('multiSend', [ + encodeMultiSendData(transactions), + ]), + nonce: 0, + operation: 1, + }) + .build(), + }) + .build(), + ) + }, + } as Safe) + }) + + it('should return undefined for undefined safeTx', () => { + expect( + prependSafeToL2Migration(undefined, extendedSafeInfoBuilder().build(), chainBuilder().build()), + ).resolves.toBeUndefined() + }) + + it('should throw if chain is undefined', () => { + expect(() => prependSafeToL2Migration(undefined, extendedSafeInfoBuilder().build(), undefined)).toThrowError() + }) + + it('should not modify tx if the chain is L1', () => { + const safeTx = safeTxBuilder() + .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() }) + .build() + + const safeInfo = extendedSafeInfoBuilder() + .with({ implementationVersionState: ImplementationVersionState.UNKNOWN }) + .build() + + expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: false }).build())).resolves.toEqual( + safeTx, + ) + }) + + it('should not modify tx if the nonce is > 0', () => { + const safeTx = safeTxBuilder() + .with({ data: safeTxDataBuilder().with({ nonce: 1 }).build() }) + .build() + + const safeInfo = extendedSafeInfoBuilder() + .with({ implementationVersionState: ImplementationVersionState.UNKNOWN }) + .build() + + expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual( + safeTx, + ) + }) + + it('should not modify tx if implementationState is correct', () => { + const safeTx = safeTxBuilder() + .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() }) + .build() + + const safeInfo = extendedSafeInfoBuilder() + .with({ implementationVersionState: ImplementationVersionState.UP_TO_DATE }) + .build() + expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual( + safeTx, + ) + }) + + it('should not modify tx if the tx is already signed', () => { + const safeTx = safeTxBuilder() + .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() }) + .build() + + safeTx.addSignature(safeSignatureBuilder().build()) + + const safeInfo = extendedSafeInfoBuilder() + .with({ implementationVersionState: ImplementationVersionState.UNKNOWN }) + .build() + + expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual( + safeTx, + ) + }) + + it('should not modify tx if the chain has no migration lib deployed', () => { + const safeTx = safeTxBuilder() + .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() }) + .build() + + const safeInfo = extendedSafeInfoBuilder() + .with({ implementationVersionState: ImplementationVersionState.UNKNOWN }) + .build() + + expect( + prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '69420' }).build()), + ).resolves.toEqual(safeTx) + }) + + it('should not modify tx if the tx already migrates', () => { + const safeL2SingletonDeployment = getSafeL2SingletonDeployment()?.defaultAddress + + const safeTx = safeTxBuilder() + .with({ + data: safeTxDataBuilder() + .with({ + nonce: 0, + to: safeToL2MigrationAddress, + data: + safeL2SingletonDeployment && + safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]), + }) + .build(), + }) + .build() + const safeInfo = extendedSafeInfoBuilder() + .with({ + implementationVersionState: ImplementationVersionState.UNKNOWN, + implementation: { + name: '1.3.0', + value: getSafeSingletonDeployment()?.defaultAddress ?? faker.finance.ethereumAddress(), + }, + }) + .build() + expect( + prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '10' }).build()), + ).resolves.toEqual(safeTx) + const multiSendSafeTx = safeTxBuilder() + .with({ + data: safeTxDataBuilder() + .with({ + nonce: 0, + to: getMultiSendDeployment()?.defaultAddress, + data: + safeToL2MigrationAddress && + safeL2SingletonDeployment && + Multi_send__factory.createInterface().encodeFunctionData('multiSend', [ + encodeMultiSendData([ + { + value: '0', + operation: 1, + to: safeToL2MigrationAddress, + data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]), + }, + ]), + ]), + }) + .build(), + }) + .build() + expect( + prependSafeToL2Migration(multiSendSafeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '10' }).build()), + ).resolves.toEqual(multiSendSafeTx) + }) + + it('should modify single txs if applicable', async () => { + const safeTx = safeTxBuilder() + .with({ + data: safeTxDataBuilder() + .with({ + nonce: 0, + to: faker.finance.ethereumAddress(), + data: faker.string.hexadecimal({ length: 10 }), + value: '0', + }) + .build(), + }) + .build() + + const safeInfo = extendedSafeInfoBuilder() + .with({ + implementationVersionState: ImplementationVersionState.UNKNOWN, + implementation: { + name: '1.3.0', + value: getSafeSingletonDeployment()?.defaultAddress ?? faker.finance.ethereumAddress(), + }, + }) + .build() + + const modifiedTx = await prependSafeToL2Migration( + safeTx, + safeInfo, + chainBuilder().with({ l2: true, chainId: '10' }).build(), + ) + + expect(modifiedTx).not.toEqual(safeTx) + expect(modifiedTx?.data.to).toEqual(getMultiSendDeployment()?.defaultAddress) + const decodedMultiSend = decodeMultiSendData(modifiedTx!.data.data) + expect(decodedMultiSend).toHaveLength(2) + const safeL2SingletonDeployment = getSafeL2SingletonDeployment()?.defaultAddress + + expect(decodedMultiSend).toEqual([ + { + to: safeToL2MigrationAddress, + value: '0', + operation: 1, + data: + safeL2SingletonDeployment && + safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]), + }, + { + to: checksumAddress(safeTx.data.to), + value: safeTx.data.value, + operation: safeTx.data.operation, + data: safeTx.data.data.toLowerCase(), + }, + ]) + }) +}) + +describe('extractMigrationL2MasterCopyAddress', () => { + it('should return undefined for undefined safeTx', () => { + expect(extractMigrationL2MasterCopyAddress(undefined)).toBeUndefined() + }) + + it('should return undefined for non multisend safeTx', () => { + expect(extractMigrationL2MasterCopyAddress(safeTxBuilder().build())).toBeUndefined() + }) + + it('should return undefined for multisend without migration', () => { + expect( + extractMigrationL2MasterCopyAddress( + safeTxBuilder() + .with({ + data: safeTxDataBuilder() + .with({ + data: multisendInterface.encodeFunctionData('multiSend', [ + encodeMultiSendData([ + { + to: faker.finance.ethereumAddress(), + data: faker.string.hexadecimal({ length: 64 }), + value: '0', + operation: 0, + }, + { + to: faker.finance.ethereumAddress(), + data: faker.string.hexadecimal({ length: 64 }), + value: '0', + operation: 0, + }, + ]), + ]), + }) + .build(), + }) + .build(), + ), + ).toBeUndefined() + }) + + it('should return migration address for multisend with migration as first tx', () => { + const l2SingletonAddress = getSafeL2SingletonDeployment()?.defaultAddress! + expect( + extractMigrationL2MasterCopyAddress( + safeTxBuilder() + .with({ + data: safeTxDataBuilder() + .with({ + data: multisendInterface.encodeFunctionData('multiSend', [ + encodeMultiSendData([ + { + to: safeToL2MigrationAddress!, + data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [l2SingletonAddress]), + value: '0', + operation: 1, + }, + { + to: faker.finance.ethereumAddress(), + data: faker.string.hexadecimal({ length: 64 }), + value: '0', + operation: 0, + }, + ]), + ]), + }) + .build(), + }) + .build(), + ), + ).toEqual(l2SingletonAddress) + }) +}) diff --git a/src/utils/__tests__/transactions.test.ts b/src/utils/__tests__/transactions.test.ts index fb96d9c00a..b2c47a7083 100644 --- a/src/utils/__tests__/transactions.test.ts +++ b/src/utils/__tests__/transactions.test.ts @@ -5,43 +5,13 @@ import type { SafeAppData, Transaction, } from '@safe-global/safe-gateway-typescript-sdk' -import { TransactionInfoType, ImplementationVersionState } from '@safe-global/safe-gateway-typescript-sdk' +import { TransactionInfoType } from '@safe-global/safe-gateway-typescript-sdk' import { isMultiSendTxInfo } from '../transaction-guards' -import { - extractMigrationL2MasterCopyAddress, - getQueuedTransactionCount, - getTxOrigin, - prependSafeToL2Migration, -} from '../transactions' -import { extendedSafeInfoBuilder } from '@/tests/builders/safe' -import { chainBuilder } from '@/tests/builders/chains' -import { safeSignatureBuilder, safeTxBuilder, safeTxDataBuilder } from '@/tests/builders/safeTx' -import { - getMultiSendCallOnlyDeployment, - getMultiSendDeployment, - getSafeL2SingletonDeployment, - getSafeSingletonDeployment, - getSafeToL2MigrationDeployment, -} from '@safe-global/safe-deployments' -import type Safe from '@safe-global/protocol-kit' -import { encodeMultiSendData } from '@safe-global/protocol-kit' -import { Multi_send__factory, Safe_to_l2_migration__factory } from '@/types/contracts' -import { faker } from '@faker-js/faker' -import { getAndValidateSafeSDK } from '@/services/tx/tx-sender/sdk' -import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils' -import { checksumAddress } from '../addresses' +import { getQueuedTransactionCount, getTxOrigin } from '../transactions' jest.mock('@/services/tx/tx-sender/sdk') -const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment() -const safeToL2MigrationAddress = safeToL2MigrationDeployment?.defaultAddress -const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface() - -const multisendInterface = Multi_send__factory.createInterface() - describe('transactions', () => { - const mockGetAndValidateSdk = getAndValidateSafeSDK as jest.MockedFunction - describe('getQueuedTransactionCount', () => { it('should return 0 if no txPage is provided', () => { expect(getQueuedTransactionCount()).toBe('0') @@ -226,297 +196,4 @@ describe('transactions', () => { ).toBe(false) }) }) - - describe('prependSafeToL2Migration', () => { - beforeEach(() => { - // Mock create Tx - mockGetAndValidateSdk.mockReturnValue({ - createTransaction: ({ transactions, onlyCalls }) => { - return Promise.resolve( - safeTxBuilder() - .with({ - data: safeTxDataBuilder() - .with({ - to: onlyCalls - ? (getMultiSendCallOnlyDeployment()?.defaultAddress ?? faker.finance.ethereumAddress()) - : (getMultiSendDeployment()?.defaultAddress ?? faker.finance.ethereumAddress()), - value: '0', - data: Multi_send__factory.createInterface().encodeFunctionData('multiSend', [ - encodeMultiSendData(transactions), - ]), - nonce: 0, - operation: 1, - }) - .build(), - }) - .build(), - ) - }, - } as Safe) - }) - - it('should return undefined for undefined safeTx', () => { - expect( - prependSafeToL2Migration(undefined, extendedSafeInfoBuilder().build(), chainBuilder().build()), - ).resolves.toBeUndefined() - }) - - it('should throw if chain is undefined', () => { - expect(() => prependSafeToL2Migration(undefined, extendedSafeInfoBuilder().build(), undefined)).toThrowError() - }) - - it('should not modify tx if the chain is L1', () => { - const safeTx = safeTxBuilder() - .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() }) - .build() - - const safeInfo = extendedSafeInfoBuilder() - .with({ implementationVersionState: ImplementationVersionState.UNKNOWN }) - .build() - - expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: false }).build())).resolves.toEqual( - safeTx, - ) - }) - - it('should not modify tx if the nonce is > 0', () => { - const safeTx = safeTxBuilder() - .with({ data: safeTxDataBuilder().with({ nonce: 1 }).build() }) - .build() - - const safeInfo = extendedSafeInfoBuilder() - .with({ implementationVersionState: ImplementationVersionState.UNKNOWN }) - .build() - - expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual( - safeTx, - ) - }) - - it('should not modify tx if implementationState is correct', () => { - const safeTx = safeTxBuilder() - .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() }) - .build() - - const safeInfo = extendedSafeInfoBuilder() - .with({ implementationVersionState: ImplementationVersionState.UP_TO_DATE }) - .build() - expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual( - safeTx, - ) - }) - - it('should not modify tx if the tx is already signed', () => { - const safeTx = safeTxBuilder() - .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() }) - .build() - - safeTx.addSignature(safeSignatureBuilder().build()) - - const safeInfo = extendedSafeInfoBuilder() - .with({ implementationVersionState: ImplementationVersionState.UNKNOWN }) - .build() - - expect(prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true }).build())).resolves.toEqual( - safeTx, - ) - }) - - it('should not modify tx if the chain has no migration lib deployed', () => { - const safeTx = safeTxBuilder() - .with({ data: safeTxDataBuilder().with({ nonce: 0 }).build() }) - .build() - - const safeInfo = extendedSafeInfoBuilder() - .with({ implementationVersionState: ImplementationVersionState.UNKNOWN }) - .build() - - expect( - prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '69420' }).build()), - ).resolves.toEqual(safeTx) - }) - - it('should not modify tx if the tx already migrates', () => { - const safeL2SingletonDeployment = getSafeL2SingletonDeployment()?.defaultAddress - - const safeTx = safeTxBuilder() - .with({ - data: safeTxDataBuilder() - .with({ - nonce: 0, - to: safeToL2MigrationAddress, - data: - safeL2SingletonDeployment && - safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]), - }) - .build(), - }) - .build() - const safeInfo = extendedSafeInfoBuilder() - .with({ - implementationVersionState: ImplementationVersionState.UNKNOWN, - implementation: { - name: '1.3.0', - value: getSafeSingletonDeployment()?.defaultAddress ?? faker.finance.ethereumAddress(), - }, - }) - .build() - expect( - prependSafeToL2Migration(safeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '10' }).build()), - ).resolves.toEqual(safeTx) - const multiSendSafeTx = safeTxBuilder() - .with({ - data: safeTxDataBuilder() - .with({ - nonce: 0, - to: getMultiSendDeployment()?.defaultAddress, - data: - safeToL2MigrationAddress && - safeL2SingletonDeployment && - Multi_send__factory.createInterface().encodeFunctionData('multiSend', [ - encodeMultiSendData([ - { - value: '0', - operation: 1, - to: safeToL2MigrationAddress, - data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]), - }, - ]), - ]), - }) - .build(), - }) - .build() - expect( - prependSafeToL2Migration(multiSendSafeTx, safeInfo, chainBuilder().with({ l2: true, chainId: '10' }).build()), - ).resolves.toEqual(multiSendSafeTx) - }) - - it('should modify single txs if applicable', async () => { - const safeTx = safeTxBuilder() - .with({ - data: safeTxDataBuilder() - .with({ - nonce: 0, - to: faker.finance.ethereumAddress(), - data: faker.string.hexadecimal({ length: 10 }), - value: '0', - }) - .build(), - }) - .build() - - const safeInfo = extendedSafeInfoBuilder() - .with({ - implementationVersionState: ImplementationVersionState.UNKNOWN, - implementation: { - name: '1.3.0', - value: getSafeSingletonDeployment()?.defaultAddress ?? faker.finance.ethereumAddress(), - }, - }) - .build() - - const modifiedTx = await prependSafeToL2Migration( - safeTx, - safeInfo, - chainBuilder().with({ l2: true, chainId: '10' }).build(), - ) - - expect(modifiedTx).not.toEqual(safeTx) - expect(modifiedTx?.data.to).toEqual(getMultiSendDeployment()?.defaultAddress) - const decodedMultiSend = decodeMultiSendData(modifiedTx!.data.data) - expect(decodedMultiSend).toHaveLength(2) - const safeL2SingletonDeployment = getSafeL2SingletonDeployment()?.defaultAddress - - expect(decodedMultiSend).toEqual([ - { - to: safeToL2MigrationAddress, - value: '0', - operation: 1, - data: - safeL2SingletonDeployment && - safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2SingletonDeployment]), - }, - { - to: checksumAddress(safeTx.data.to), - value: safeTx.data.value, - operation: safeTx.data.operation, - data: safeTx.data.data.toLowerCase(), - }, - ]) - }) - }) - - describe('extractMigrationL2MasterCopyAddress', () => { - it('should return undefined for undefined safeTx', () => { - expect(extractMigrationL2MasterCopyAddress(undefined)).toBeUndefined() - }) - - it('should return undefined for non multisend safeTx', () => { - expect(extractMigrationL2MasterCopyAddress(safeTxBuilder().build())).toBeUndefined() - }) - - it('should return undefined for multisend without migration', () => { - expect( - extractMigrationL2MasterCopyAddress( - safeTxBuilder() - .with({ - data: safeTxDataBuilder() - .with({ - data: multisendInterface.encodeFunctionData('multiSend', [ - encodeMultiSendData([ - { - to: faker.finance.ethereumAddress(), - data: faker.string.hexadecimal({ length: 64 }), - value: '0', - operation: 0, - }, - { - to: faker.finance.ethereumAddress(), - data: faker.string.hexadecimal({ length: 64 }), - value: '0', - operation: 0, - }, - ]), - ]), - }) - .build(), - }) - .build(), - ), - ).toBeUndefined() - }) - - it('should return migration address for multisend with migration as first tx', () => { - const l2SingletonAddress = getSafeL2SingletonDeployment()?.defaultAddress! - expect( - extractMigrationL2MasterCopyAddress( - safeTxBuilder() - .with({ - data: safeTxDataBuilder() - .with({ - data: multisendInterface.encodeFunctionData('multiSend', [ - encodeMultiSendData([ - { - to: safeToL2MigrationAddress!, - data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [l2SingletonAddress]), - value: '0', - operation: 1, - }, - { - to: faker.finance.ethereumAddress(), - data: faker.string.hexadecimal({ length: 64 }), - value: '0', - operation: 0, - }, - ]), - ]), - }) - .build(), - }) - .build(), - ), - ).toEqual(l2SingletonAddress) - }) - }) }) diff --git a/src/utils/safe-migrations.ts b/src/utils/safe-migrations.ts new file mode 100644 index 0000000000..8932e0b130 --- /dev/null +++ b/src/utils/safe-migrations.ts @@ -0,0 +1,151 @@ +import { Safe_to_l2_migration__factory, Safe_migration__factory } from '@/types/contracts' +import { type ExtendedSafeInfo } from '@/store/safeInfoSlice' +import { getSafeContractDeployment } from '@/services/contracts/deployments' +import { sameAddress } from './addresses' +import { getSafeToL2MigrationDeployment, getSafeMigrationDeployment } from '@safe-global/safe-deployments' +import { MetaTransactionData, OperationType, SafeTransaction } from '@safe-global/safe-core-sdk-types' +import { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { isValidMasterCopy } from '@/services/contracts/safeContracts' +import { isMultiSendCalldata } from './transaction-calldata' +import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils' +import { __unsafe_createMultiSendTx } from '@/services/tx/tx-sender' +import { LATEST_SAFE_VERSION } from '@/config/constants' + +/** + * + * If the Safe is using a invalid masterCopy this function will modify the passed in `safeTx` by making it a MultiSend that migrates the Safe to L2 as the first action. + * + * This only happens under the conditions that + * - The Safe's nonce is 0 + * - The SafeTx's nonce is 0 + * - The Safe is using an invalid masterCopy + * - The SafeTx is not already including a Migration + * + * @param safeTx original SafeTx + * @param safe + * @param chain + * @returns + */ +export const prependSafeToL2Migration = ( + safeTx: SafeTransaction | undefined, + safe: ExtendedSafeInfo, + chain: ChainInfo | undefined, +): Promise => { + if (!chain) { + throw new Error('No Network information available') + } + + const safeL2Deployment = getSafeContractDeployment(chain, safe.version) + const safeL2DeploymentAddress = safeL2Deployment?.networkAddresses[chain.chainId] + const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment({ network: chain.chainId }) + const safeToL2MigrationAddress = safeToL2MigrationDeployment?.networkAddresses[chain.chainId] + + if ( + !safeTx || + safeTx.signatures.size > 0 || + !chain.l2 || + safeTx.data.nonce > 0 || + isValidMasterCopy(safe.implementationVersionState) || + !safeToL2MigrationAddress || + !safeL2DeploymentAddress + ) { + // We do not migrate on L1s + // We cannot migrate if the nonce is > 0 + // We do not modify already signed txs + // We do not modify supported masterCopies + // We cannot migrate if no migration contract or L2 contract exists + return Promise.resolve(safeTx) + } + + const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface() + + if (sameAddress(safe.implementation.value, safeL2DeploymentAddress)) { + // Safe already has the correct L2 masterCopy + // This should in theory never happen if the implementationState is valid + return Promise.resolve(safeTx) + } + + // If the Safe is a L1 masterCopy on a L2 network and still has nonce 0, we prepend a call to the migration contract to the safeTx. + const txData = safeTx.data.data + + let internalTxs: MetaTransactionData[] + if (isMultiSendCalldata(txData)) { + // Check if the first tx is already a call to the migration contract + internalTxs = decodeMultiSendData(txData) + } else { + internalTxs = [{ to: safeTx.data.to, operation: safeTx.data.operation, value: safeTx.data.value, data: txData }] + } + + if (sameAddress(internalTxs[0]?.to, safeToL2MigrationAddress)) { + // We already migrate. Nothing to do. + return Promise.resolve(safeTx) + } + + // Prepend the migration tx + const newTxs: MetaTransactionData[] = [ + { + operation: OperationType.DelegateCall, // DELEGATE CALL REQUIRED + data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2DeploymentAddress]), + to: safeToL2MigrationAddress, + value: '0', + }, + ...internalTxs, + ] + + return __unsafe_createMultiSendTx(newTxs) +} + +export const extractMigrationL2MasterCopyAddress = (safeTx: SafeTransaction | undefined): string | undefined => { + if (!safeTx) { + return undefined + } + + if (!isMultiSendCalldata(safeTx.data.data)) { + return undefined + } + + const innerTxs = decodeMultiSendData(safeTx.data.data) + const firstInnerTx = innerTxs[0] + if (!firstInnerTx) { + return undefined + } + + const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment() + const safeToL2MigrationAddress = safeToL2MigrationDeployment?.defaultAddress + const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface() + + if ( + firstInnerTx.data.startsWith(safeToL2MigrationInterface.getFunction('migrateToL2').selector) && + sameAddress(firstInnerTx.to, safeToL2MigrationAddress) + ) { + const callParams = safeToL2MigrationInterface.decodeFunctionData('migrateToL2', firstInnerTx.data) + return callParams[0] + } + + return undefined +} + +export const createUpdateMigration = (chain: ChainInfo): MetaTransactionData => { + const interfce = Safe_migration__factory.createInterface() + + const deployment = getSafeMigrationDeployment({ + version: chain.recommendedMasterCopyVersion || LATEST_SAFE_VERSION, + released: true, + network: chain.chainId, + }) + + if (!deployment) { + throw new Error('Migration deployment not found') + } + + const tx: MetaTransactionData = { + operation: OperationType.DelegateCall, // DELEGATE CALL REQUIRED + data: chain.l2 + ? interfce.encodeFunctionData('migrateL2WithFallbackHandler') + : interfce.encodeFunctionData('migrateWithFallbackHandler'), + to: deployment.defaultAddress, + value: '0', + } + + return tx +} diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts index 4348270044..fda843b7d3 100644 --- a/src/utils/transactions.ts +++ b/src/utils/transactions.ts @@ -21,24 +21,18 @@ import { } from './transaction-guards' import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types/dist/src/types' import { OperationType } from '@safe-global/safe-core-sdk-types/dist/src/types' -import { getReadOnlyGnosisSafeContract, isValidMasterCopy } from '@/services/contracts/safeContracts' +import { getReadOnlyGnosisSafeContract } from '@/services/contracts/safeContracts' import extractTxInfo from '@/services/tx/extractTxInfo' import type { AdvancedParameters } from '@/components/tx/AdvancedParams' import type { SafeTransaction, TransactionOptions } from '@safe-global/safe-core-sdk-types' import { FEATURES, hasFeature } from '@/utils/chains' import uniqBy from 'lodash/uniqBy' import { Errors, logError } from '@/services/exceptions' -import { Safe_to_l2_migration__factory } from '@/types/contracts' import { type BaseTransaction } from '@safe-global/safe-apps-sdk' import { isEmptyHexData } from '@/utils/hex' -import { type ExtendedSafeInfo } from '@/store/safeInfoSlice' -import { getSafeContractDeployment } from '@/services/contracts/deployments' -import { sameAddress } from './addresses' import { isMultiSendCalldata } from './transaction-calldata' import { decodeMultiSendData } from '@safe-global/protocol-kit/dist/src/utils' -import { __unsafe_createMultiSendTx } from '@/services/tx/tx-sender' import { getOriginPath } from './url' -import { getSafeToL2MigrationDeployment } from '@safe-global/safe-deployments' export const makeTxFromDetails = (txDetails: TransactionDetails): Transaction => { const getMissingSigners = ({ @@ -237,120 +231,6 @@ export const isImitation = ({ txInfo }: TransactionSummary): boolean => { return isTransferTxInfo(txInfo) && isERC20Transfer(txInfo.transferInfo) && Boolean(txInfo.transferInfo.imitation) } -/** - * - * If the Safe is using a invalid masterCopy this function will modify the passed in `safeTx` by making it a MultiSend that migrates the Safe to L2 as the first action. - * - * This only happens under the conditions that - * - The Safe's nonce is 0 - * - The SafeTx's nonce is 0 - * - The Safe is using an invalid masterCopy - * - The SafeTx is not already including a Migration - * - * @param safeTx original SafeTx - * @param safe - * @param chain - * @returns - */ -export const prependSafeToL2Migration = ( - safeTx: SafeTransaction | undefined, - safe: ExtendedSafeInfo, - chain: ChainInfo | undefined, -): Promise => { - if (!chain) { - throw new Error('No Network information available') - } - - const safeL2Deployment = getSafeContractDeployment(chain, safe.version) - const safeL2DeploymentAddress = safeL2Deployment?.networkAddresses[chain.chainId] - const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment({ network: chain.chainId }) - const safeToL2MigrationAddress = safeToL2MigrationDeployment?.networkAddresses[chain.chainId] - - if ( - !safeTx || - safeTx.signatures.size > 0 || - !chain.l2 || - safeTx.data.nonce > 0 || - isValidMasterCopy(safe.implementationVersionState) || - !safeToL2MigrationAddress || - !safeL2DeploymentAddress - ) { - // We do not migrate on L1s - // We cannot migrate if the nonce is > 0 - // We do not modify already signed txs - // We do not modify supported masterCopies - // We cannot migrate if no migration contract or L2 contract exists - return Promise.resolve(safeTx) - } - - const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface() - - if (sameAddress(safe.implementation.value, safeL2DeploymentAddress)) { - // Safe already has the correct L2 masterCopy - // This should in theory never happen if the implementationState is valid - return Promise.resolve(safeTx) - } - - // If the Safe is a L1 masterCopy on a L2 network and still has nonce 0, we prepend a call to the migration contract to the safeTx. - const txData = safeTx.data.data - - let internalTxs: MetaTransactionData[] - if (isMultiSendCalldata(txData)) { - // Check if the first tx is already a call to the migration contract - internalTxs = decodeMultiSendData(txData) - } else { - internalTxs = [{ to: safeTx.data.to, operation: safeTx.data.operation, value: safeTx.data.value, data: txData }] - } - - if (sameAddress(internalTxs[0]?.to, safeToL2MigrationAddress)) { - // We already migrate. Nothing to do. - return Promise.resolve(safeTx) - } - - // Prepend the migration tx - const newTxs: MetaTransactionData[] = [ - { - operation: 1, // DELEGATE CALL REQUIRED - data: safeToL2MigrationInterface.encodeFunctionData('migrateToL2', [safeL2DeploymentAddress]), - to: safeToL2MigrationAddress, - value: '0', - }, - ...internalTxs, - ] - - return __unsafe_createMultiSendTx(newTxs) -} - -export const extractMigrationL2MasterCopyAddress = (safeTx: SafeTransaction | undefined): string | undefined => { - if (!safeTx) { - return undefined - } - - if (!isMultiSendCalldata(safeTx.data.data)) { - return undefined - } - - const innerTxs = decodeMultiSendData(safeTx.data.data) - const firstInnerTx = innerTxs[0] - if (!firstInnerTx) { - return undefined - } - - const safeToL2MigrationDeployment = getSafeToL2MigrationDeployment() - const safeToL2MigrationAddress = safeToL2MigrationDeployment?.defaultAddress - const safeToL2MigrationInterface = Safe_to_l2_migration__factory.createInterface() - - if ( - firstInnerTx.data.startsWith(safeToL2MigrationInterface.getFunction('migrateToL2').selector) && - sameAddress(firstInnerTx.to, safeToL2MigrationAddress) - ) { - const callParams = safeToL2MigrationInterface.decodeFunctionData('migrateToL2', firstInnerTx.data) - return callParams[0] - } - - return undefined -} - export const getSafeTransaction = async (safeTxHash: string, chainId: string, safeAddress: string) => { const txId = `multisig_${safeAddress}_${safeTxHash}`