diff --git a/apps/web/.gitignore b/apps/web/.gitignore
deleted file mode 100644
index ade4b1731e..0000000000
--- a/apps/web/.gitignore
+++ /dev/null
@@ -1,61 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# types
-/src/types/contracts/
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-.idea
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-.pnpm-debug.log*
-
-# local env files
-.env*.local
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-
-# yalc
-.yalc
-yalc.lock
-.env
-
-/cypress/videos
-/cypress/screenshots
-/cypress/downloads
-
-/public/sw.js
-/public/sw.js.map
-/public/worker-*.js
-/public/workbox-*.js
-/public/workbox-*.js.map
-/public/fallback*
-/public/*.js.LICENSE.txt
-certificates
-*storybook.log
-
-# Yarn v4
-.yarn/*
diff --git a/apps/web/src/components/transactions/TxSummary/styles.module.css b/apps/web/src/components/transactions/TxSummary/styles.module.css
index fc8ed69fac..bba12e11e3 100644
--- a/apps/web/src/components/transactions/TxSummary/styles.module.css
+++ b/apps/web/src/components/transactions/TxSummary/styles.module.css
@@ -29,9 +29,9 @@
}
.gridContainer.conflictGroup {
- grid-template-columns: var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-confirmations) var(--grid-status) var(
- --grid-actions
- );
+ grid-template-columns:
+ var(--grid-type) var(--grid-info) var(--grid-date) var(--grid-confirmations) var(--grid-status)
+ var(--grid-actions);
grid-template-areas: 'type info date confirmations status actions';
}
diff --git a/apps/web/src/components/tx/confirmation-views/UpdateSafe/index.test.tsx b/apps/web/src/components/tx/confirmation-views/UpdateSafe/index.test.tsx
new file mode 100644
index 0000000000..b3989d8570
--- /dev/null
+++ b/apps/web/src/components/tx/confirmation-views/UpdateSafe/index.test.tsx
@@ -0,0 +1,26 @@
+import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
+import { _UpdateSafe as UpdateSafe } from './index'
+import { render } from '@/tests/test-utils'
+
+const chain = {
+ recommendedMasterCopyVersion: '1.4.1',
+} as ChainInfo
+
+const warningText = 'This upgrade will invalidate all queued transactions!'
+
+describe('Container', () => {
+ it('renders correctly with a queue warning', async () => {
+ const container = render()
+ await expect(container.findByText(warningText)).resolves.not.toBeNull()
+ })
+
+ it('renders correctly without a queue warning because no queue', async () => {
+ const container = render()
+ await expect(container.findByText(warningText)).rejects.toThrowError(Error)
+ })
+
+ it('renders correctly without a queue warning because of compatible Safe version', async () => {
+ const container = render()
+ await expect(container.findByText(warningText)).rejects.toThrowError(Error)
+ })
+})
diff --git a/apps/web/src/components/tx/confirmation-views/UpdateSafe/index.tsx b/apps/web/src/components/tx/confirmation-views/UpdateSafe/index.tsx
index 6dec5f3949..520b6be0fd 100644
--- a/apps/web/src/components/tx/confirmation-views/UpdateSafe/index.tsx
+++ b/apps/web/src/components/tx/confirmation-views/UpdateSafe/index.tsx
@@ -1,11 +1,15 @@
import type { ReactNode } from 'react'
import { Alert, AlertTitle, Box, Divider, Stack, Typography } from '@mui/material'
+import semverSatisfies from 'semver/functions/satisfies'
import { LATEST_SAFE_VERSION } from '@/config/constants'
import { useCurrentChain } from '@/hooks/useChains'
import useSafeInfo from '@/hooks/useSafeInfo'
import { useQueuedTxsLength } from '@/hooks/useTxQueue'
import ExternalLink from '@/components/common/ExternalLink'
import { maybePlural } from '@/utils/formatters'
+import madProps from '@/utils/mad-props'
+
+const QUEUE_WARNING_VERSION = '<1.3.0'
function BgBox({ children, light }: { children: ReactNode; light?: boolean }) {
return (
@@ -23,28 +27,34 @@ function BgBox({ children, light }: { children: ReactNode; light?: boolean }) {
)
}
-function UpdateSafe() {
- const { safe } = useSafeInfo()
- const chain = useCurrentChain()
- const queueSize = useQueuedTxsLength()
+export function _UpdateSafe({
+ safeVersion,
+ queueSize,
+ chain,
+}: {
+ safeVersion: string
+ queueSize: string
+ chain: ReturnType
+}) {
+ const showQueueWarning = queueSize && semverSatisfies(safeVersion, QUEUE_WARNING_VERSION)
const latestSafeVersion = chain?.recommendedMasterCopyVersion || LATEST_SAFE_VERSION
return (
<>
- Current version: {safe.version}
+ Current version: {safeVersion}
→
New version: {latestSafeVersion}
-
+
Read about the updates in the new Safe contracts version in the{' '}
version {latestSafeVersion} changelog
- {queueSize && (
+ {showQueueWarning && (
This upgrade will invalidate all queued transactions!
You have {queueSize} unexecuted transaction{maybePlural(parseInt(queueSize))}. Please make sure to execute or
@@ -52,9 +62,20 @@ function UpdateSafe() {
)}
-
+
>
)
}
+function useSafeVersion() {
+ const { safe } = useSafeInfo()
+ return safe?.version || ''
+}
+
+const UpdateSafe = madProps(_UpdateSafe, {
+ chain: useCurrentChain,
+ safeVersion: useSafeVersion,
+ queueSize: useQueuedTxsLength,
+})
+
export default UpdateSafe
diff --git a/apps/web/src/services/tx/safeUpdateParams.ts b/apps/web/src/services/tx/safeUpdateParams.ts
index 36fc3b7ef1..d90c71b0d3 100644
--- a/apps/web/src/services/tx/safeUpdateParams.ts
+++ b/apps/web/src/services/tx/safeUpdateParams.ts
@@ -37,7 +37,7 @@ export const createUpdateSafeTxs = async (safe: SafeInfo, chain: ChainInfo): Pro
// 1.3.0 Safes are updated using a delegate call to a migration contract
if (semverSatisfies(safe.version, '1.3.0')) {
- return [createUpdateMigration(chain)]
+ return [createUpdateMigration(chain, safe.version, safe.fallbackHandler?.value)]
}
// For older Safes, we need to create two transactions
diff --git a/apps/web/src/utils/__tests__/safe-migrations.test.ts b/apps/web/src/utils/__tests__/safe-migrations.test.ts
index 73a447d5aa..478b346ee3 100644
--- a/apps/web/src/utils/__tests__/safe-migrations.test.ts
+++ b/apps/web/src/utils/__tests__/safe-migrations.test.ts
@@ -325,7 +325,7 @@ describe('extractMigrationL2MasterCopyAddress', () => {
} as unknown as ChainInfo
it('should create a migration transaction for L1 chain', () => {
- const result = createUpdateMigration(mockChain)
+ const result = createUpdateMigration(mockChain, '1.3.0')
expect(result).toEqual({
operation: OperationType.DelegateCall,
@@ -336,8 +336,8 @@ describe('extractMigrationL2MasterCopyAddress', () => {
})
it('should create a migration transaction for L2 chain', () => {
- const l2Chain = { ...mockChain, l2: true }
- const result = createUpdateMigration(l2Chain)
+ const l2Chain = { ...mockChain, chainId: '137', l2: true }
+ const result = createUpdateMigration(l2Chain, '1.3.0+L2')
expect(result).toEqual({
operation: OperationType.DelegateCall,
@@ -348,7 +348,41 @@ describe('extractMigrationL2MasterCopyAddress', () => {
})
it('should throw an error if deployment is not found', () => {
- expect(() => createUpdateMigration(mockChainOld)).toThrow('Migration deployment not found')
+ expect(() => createUpdateMigration(mockChainOld, '1.1.1')).toThrow('Migration deployment not found')
+ })
+
+ it('should overwrite fallback handler if it is the default one', () => {
+ const result = createUpdateMigration(mockChain, '1.3.0', '0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4') // 1.3.0 compatibility fallback handler
+
+ expect(result).toEqual({
+ operation: OperationType.DelegateCall,
+ data: '0xed007fc6',
+ to: '0x526643F69b81B008F46d95CD5ced5eC0edFFDaC6',
+ value: '0',
+ })
+ })
+
+ it('should overwrite L2 fallback handler if it is the default one', () => {
+ const l2Chain = { ...mockChain, chainId: '137', l2: true }
+ const result = createUpdateMigration(l2Chain, '1.3.0+L2', '0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4') // 1.3.0 compatibility fallback handler
+
+ expect(result).toEqual({
+ operation: OperationType.DelegateCall,
+ data: '0x68cb3d94',
+ to: '0x526643F69b81B008F46d95CD5ced5eC0edFFDaC6',
+ value: '0',
+ })
+ })
+
+ it('should NOT overwrite a custom fallback handler', () => {
+ const result = createUpdateMigration(mockChain, '1.3.0', '0x526643F69b81B008F46d95CD5ced5eC0edFFDaC6')
+
+ expect(result).toEqual({
+ operation: OperationType.DelegateCall,
+ data: '0xf6682ab0',
+ to: '0x526643F69b81B008F46d95CD5ced5eC0edFFDaC6',
+ value: '0',
+ })
})
})
})
diff --git a/apps/web/src/utils/safe-migrations.ts b/apps/web/src/utils/safe-migrations.ts
index beffec8694..f9c1b6c7d6 100644
--- a/apps/web/src/utils/safe-migrations.ts
+++ b/apps/web/src/utils/safe-migrations.ts
@@ -1,9 +1,15 @@
import { Safe_to_l2_migration__factory, Safe_migration__factory } from '@/types/contracts'
+import { getCompatibilityFallbackHandlerDeployments } from '@safe-global/safe-deployments'
import { type ExtendedSafeInfo } from '@/store/safeInfoSlice'
-import { getSafeContractDeployment } from '@/services/contracts/deployments'
+import { getSafeContractDeployment, hasMatchingDeployment } from '@/services/contracts/deployments'
import { sameAddress } from './addresses'
import { getSafeToL2MigrationDeployment, getSafeMigrationDeployment } from '@safe-global/safe-deployments'
-import { type MetaTransactionData, OperationType, type SafeTransaction } from '@safe-global/safe-core-sdk-types'
+import {
+ type MetaTransactionData,
+ OperationType,
+ type SafeTransaction,
+ type SafeVersion,
+} from '@safe-global/safe-core-sdk-types'
import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { isValidMasterCopy } from '@/services/contracts/safeContracts'
import { isMultiSendCalldata } from './transaction-calldata'
@@ -95,9 +101,11 @@ export const prependSafeToL2Migration = (
return __unsafe_createMultiSendTx(newTxs)
}
-export const createUpdateMigration = (chain: ChainInfo): MetaTransactionData => {
- const interfce = Safe_migration__factory.createInterface()
-
+export const createUpdateMigration = (
+ chain: ChainInfo,
+ safeVersion: string,
+ fallbackHandler?: string,
+): MetaTransactionData => {
const deployment = getSafeMigrationDeployment({
version: chain.recommendedMasterCopyVersion || LATEST_SAFE_VERSION,
released: true,
@@ -108,11 +116,28 @@ export const createUpdateMigration = (chain: ChainInfo): MetaTransactionData =>
throw new Error('Migration deployment not found')
}
+ // Keep fallback handler if it's not a default one
+ const keepFallbackHandler =
+ !!fallbackHandler &&
+ !hasMatchingDeployment(getCompatibilityFallbackHandlerDeployments, fallbackHandler, chain.chainId, [
+ safeVersion as SafeVersion,
+ ])
+
+ const method = (
+ keepFallbackHandler
+ ? chain.l2
+ ? 'migrateL2Singleton'
+ : 'migrateSingleton'
+ : chain.l2
+ ? 'migrateL2WithFallbackHandler'
+ : 'migrateWithFallbackHandler'
+ ) as 'migrateSingleton' // apease typescript
+
+ const interfce = Safe_migration__factory.createInterface()
+
const tx: MetaTransactionData = {
- operation: OperationType.DelegateCall, // DELEGATE CALL REQUIRED
- data: chain.l2
- ? interfce.encodeFunctionData('migrateL2WithFallbackHandler')
- : interfce.encodeFunctionData('migrateWithFallbackHandler'),
+ operation: OperationType.DelegateCall, // delegate call required
+ data: interfce.encodeFunctionData(method),
to: deployment.defaultAddress,
value: '0',
}