Skip to content

Commit

Permalink
fix(Multichain): add more unsupported Safe cases (#4233)
Browse files Browse the repository at this point in the history
- do not support adding networks to Safes with unknown fallback handlers and Safe creations with reimbursements
  • Loading branch information
schmanu authored Sep 23, 2024
1 parent a4ee3d6 commit 357fc85
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@ import { renderHook } from '@/tests/test-utils'
import { useCompatibleNetworks } from '../useCompatibleNetworks'
import { type ReplayedSafeProps } from '@/store/slices'
import { faker } from '@faker-js/faker'
import { Safe__factory } from '@/types/contracts'
import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants'
import { ECOSYSTEM_ID_ADDRESS } from '@/config/constants'
import { chainBuilder } from '@/tests/builders/chains'
import {
getSafeSingletonDeployments,
getSafeL2SingletonDeployments,
getProxyFactoryDeployments,
getCompatibilityFallbackHandlerDeployments,
} from '@safe-global/safe-deployments'
import * as useChains from '@/hooks/useChains'

const safeInterface = Safe__factory.createInterface()

const L1_111_MASTERCOPY_DEPLOYMENTS = getSafeSingletonDeployments({ version: '1.1.1' })?.deployments
const L1_130_MASTERCOPY_DEPLOYMENTS = getSafeSingletonDeployments({ version: '1.3.0' })?.deployments
const L1_141_MASTERCOPY_DEPLOYMENTS = getSafeSingletonDeployments({ version: '1.4.1' })?.deployments
Expand All @@ -26,6 +24,9 @@ const PROXY_FACTORY_111_DEPLOYMENTS = getProxyFactoryDeployments({ version: '1.1
const PROXY_FACTORY_130_DEPLOYMENTS = getProxyFactoryDeployments({ version: '1.3.0' })?.deployments
const PROXY_FACTORY_141_DEPLOYMENTS = getProxyFactoryDeployments({ version: '1.4.1' })?.deployments

const FALLBACK_HANDLER_130_DEPLOYMENTS = getCompatibilityFallbackHandlerDeployments({ version: '1.3.0' })?.deployments
const FALLBACK_HANDLER_141_DEPLOYMENTS = getCompatibilityFallbackHandlerDeployments({ version: '1.4.1' })?.deployments

describe('useCompatibleNetworks', () => {
beforeAll(() => {
jest.spyOn(useChains, 'default').mockReturnValue({
Expand All @@ -44,7 +45,7 @@ describe('useCompatibleNetworks', () => {
expect(result.current).toHaveLength(0)
})

it('should set available to false for unknown masterCopies', () => {
it('should set available to false for unknown contracts', () => {
const callData = {
owners: [faker.finance.ethereumAddress()],
threshold: 1,
Expand Down Expand Up @@ -73,7 +74,7 @@ describe('useCompatibleNetworks', () => {
threshold: 1,
to: ZERO_ADDRESS,
data: EMPTY_DATA,
fallbackHandler: faker.finance.ethereumAddress(),
fallbackHandler: FALLBACK_HANDLER_141_DEPLOYMENTS?.canonical?.address!,
paymentToken: ZERO_ADDRESS,
payment: 0,
paymentReceiver: ECOSYSTEM_ID_ADDRESS,
Expand Down Expand Up @@ -107,13 +108,13 @@ describe('useCompatibleNetworks', () => {
}
})

it('should mark already deployed chains as not available', () => {
it('should mark compatible chains as available', () => {
const callData = {
owners: [faker.finance.ethereumAddress()],
threshold: 1,
to: ZERO_ADDRESS,
data: EMPTY_DATA,
fallbackHandler: faker.finance.ethereumAddress(),
fallbackHandler: ZERO_ADDRESS,
paymentToken: ZERO_ADDRESS,
payment: 0,
paymentReceiver: ECOSYSTEM_ID_ADDRESS,
Expand All @@ -125,7 +126,7 @@ describe('useCompatibleNetworks', () => {
factoryAddress: PROXY_FACTORY_130_DEPLOYMENTS?.canonical?.address!,
masterCopy: L1_130_MASTERCOPY_DEPLOYMENTS?.canonical?.address!,
saltNonce: '0',
safeAccountConfig: callData,
safeAccountConfig: { ...callData, fallbackHandler: FALLBACK_HANDLER_130_DEPLOYMENTS?.canonical?.address! },
safeVersion: '1.3.0',
}
const { result } = renderHook(() => useCompatibleNetworks(creationData))
Expand All @@ -140,7 +141,7 @@ describe('useCompatibleNetworks', () => {
factoryAddress: PROXY_FACTORY_130_DEPLOYMENTS?.canonical?.address!,
masterCopy: L2_130_MASTERCOPY_DEPLOYMENTS?.canonical?.address!,
saltNonce: '0',
safeAccountConfig: callData,
safeAccountConfig: { ...callData, fallbackHandler: FALLBACK_HANDLER_130_DEPLOYMENTS?.canonical?.address! },
safeVersion: '1.3.0',
}
const { result } = renderHook(() => useCompatibleNetworks(creationData))
Expand All @@ -155,7 +156,7 @@ describe('useCompatibleNetworks', () => {
factoryAddress: PROXY_FACTORY_130_DEPLOYMENTS?.eip155?.address!,
masterCopy: L1_130_MASTERCOPY_DEPLOYMENTS?.eip155?.address!,
saltNonce: '0',
safeAccountConfig: callData,
safeAccountConfig: { ...callData, fallbackHandler: FALLBACK_HANDLER_130_DEPLOYMENTS?.eip155?.address! },
safeVersion: '1.3.0',
}
const { result } = renderHook(() => useCompatibleNetworks(creationData))
Expand All @@ -170,7 +171,7 @@ describe('useCompatibleNetworks', () => {
factoryAddress: PROXY_FACTORY_130_DEPLOYMENTS?.eip155?.address!,
masterCopy: L2_130_MASTERCOPY_DEPLOYMENTS?.eip155?.address!,
saltNonce: '0',
safeAccountConfig: callData,
safeAccountConfig: { ...callData, fallbackHandler: FALLBACK_HANDLER_130_DEPLOYMENTS?.eip155?.address! },
safeVersion: '1.3.0',
}
const { result } = renderHook(() => useCompatibleNetworks(creationData))
Expand Down
132 changes: 123 additions & 9 deletions src/features/multichain/hooks/__tests__/useSafeCreationData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,46 @@ describe('useSafeCreationData', () => {
})
})

it('should throw error if RPC could not be created', async () => {
it('should throw error if outdated masterCopy is being used', async () => {
const setupData = Safe__factory.createInterface().encodeFunctionData('setup', [
[faker.finance.ethereumAddress(), faker.finance.ethereumAddress()],
1,
faker.finance.ethereumAddress(),
faker.string.hexadecimal({ length: 64 }),
faker.finance.ethereumAddress(),
faker.finance.ethereumAddress(),
0,
faker.finance.ethereumAddress(),
])

jest.spyOn(cgwSdk, 'getCreationTransaction').mockResolvedValue({
data: {
created: new Date(Date.now()).toISOString(),
creator: faker.finance.ethereumAddress(),
factoryAddress: faker.finance.ethereumAddress(),
transactionHash: faker.string.hexadecimal({ length: 64 }),
masterCopy: getSafeSingletonDeployment({ version: '1.1.1' })?.defaultAddress,
setupData,
},
response: new Response(),
})

const safeAddress = faker.finance.ethereumAddress()
const chainInfos = [chainBuilder().with({ chainId: '1', l2: false }).build()]

// Run hook
const { result } = renderHook(() => useSafeCreationData(safeAddress, chainInfos))

await waitFor(() => {
expect(result.current).toEqual([
undefined,
new Error(SAFE_CREATION_DATA_ERRORS.UNSUPPORTED_IMPLEMENTATION),
false,
])
})
})

it('should throw error if unknown masterCopy is being used', async () => {
const setupData = Safe__factory.createInterface().encodeFunctionData('setup', [
[faker.finance.ethereumAddress(), faker.finance.ethereumAddress()],
1,
Expand All @@ -175,6 +214,80 @@ describe('useSafeCreationData', () => {
response: new Response(),
})

const safeAddress = faker.finance.ethereumAddress()
const chainInfos = [chainBuilder().with({ chainId: '1', l2: false }).build()]

// Run hook
const { result } = renderHook(() => useSafeCreationData(safeAddress, chainInfos))

await waitFor(() => {
expect(result.current).toEqual([
undefined,
new Error(SAFE_CREATION_DATA_ERRORS.UNSUPPORTED_IMPLEMENTATION),
false,
])
})
})

it('should throw error if the Safe creation uses reimbursement', async () => {
const setupData = Safe__factory.createInterface().encodeFunctionData('setup', [
[faker.finance.ethereumAddress(), faker.finance.ethereumAddress()],
1,
faker.finance.ethereumAddress(),
faker.string.hexadecimal({ length: 64 }),
faker.finance.ethereumAddress(),
faker.finance.ethereumAddress(),
420,
faker.finance.ethereumAddress(),
])

jest.spyOn(cgwSdk, 'getCreationTransaction').mockResolvedValue({
data: {
created: new Date(Date.now()).toISOString(),
creator: faker.finance.ethereumAddress(),
factoryAddress: faker.finance.ethereumAddress(),
transactionHash: faker.string.hexadecimal({ length: 64 }),
masterCopy: getSafeSingletonDeployment({ version: '1.3.0' })?.defaultAddress,
setupData,
},
response: new Response(),
})

const safeAddress = faker.finance.ethereumAddress()
const chainInfos = [chainBuilder().with({ chainId: '1', l2: false }).build()]

// Run hook
const { result } = renderHook(() => useSafeCreationData(safeAddress, chainInfos))

await waitFor(() => {
expect(result.current).toEqual([undefined, new Error(SAFE_CREATION_DATA_ERRORS.PAYMENT_SAFE), false])
})
})

it('should throw error if RPC could not be created', async () => {
const setupData = Safe__factory.createInterface().encodeFunctionData('setup', [
[faker.finance.ethereumAddress(), faker.finance.ethereumAddress()],
1,
faker.finance.ethereumAddress(),
faker.string.hexadecimal({ length: 64 }),
faker.finance.ethereumAddress(),
ZERO_ADDRESS,
0,
faker.finance.ethereumAddress(),
])

jest.spyOn(cgwSdk, 'getCreationTransaction').mockResolvedValue({
data: {
created: new Date(Date.now()).toISOString(),
creator: faker.finance.ethereumAddress(),
factoryAddress: faker.finance.ethereumAddress(),
transactionHash: faker.string.hexadecimal({ length: 64 }),
masterCopy: getSafeSingletonDeployment({ version: '1.3.0' })?.defaultAddress,
setupData,
},
response: new Response(),
})

jest.spyOn(web3, 'createWeb3ReadOnly').mockReturnValue(undefined)

const safeAddress = faker.finance.ethereumAddress()
Expand All @@ -195,14 +308,15 @@ describe('useSafeCreationData', () => {
faker.finance.ethereumAddress(),
faker.string.hexadecimal({ length: 64 }),
faker.finance.ethereumAddress(),
faker.finance.ethereumAddress(),
ZERO_ADDRESS,
0,
faker.finance.ethereumAddress(),
])

const mockTxHash = faker.string.hexadecimal({ length: 64 })
const mockFactoryAddress = faker.finance.ethereumAddress()
const mockMasterCopyAddress = faker.finance.ethereumAddress()
const mockMasterCopyAddress = getSafeSingletonDeployment({ version: '1.3.0' })?.defaultAddress

jest.spyOn(cgwSdk, 'getCreationTransaction').mockResolvedValue({
data: {
created: new Date(Date.now()).toISOString(),
Expand Down Expand Up @@ -237,14 +351,14 @@ describe('useSafeCreationData', () => {
faker.finance.ethereumAddress(),
faker.string.hexadecimal({ length: 64 }),
faker.finance.ethereumAddress(),
faker.finance.ethereumAddress(),
ZERO_ADDRESS,
0,
faker.finance.ethereumAddress(),
])

const mockTxHash = faker.string.hexadecimal({ length: 64 })
const mockFactoryAddress = faker.finance.ethereumAddress()
const mockMasterCopyAddress = faker.finance.ethereumAddress()
const mockMasterCopyAddress = getSafeSingletonDeployment({ version: '1.3.0' })?.defaultAddress!
jest.spyOn(cgwSdk, 'getCreationTransaction').mockResolvedValue({
data: {
created: new Date(Date.now()).toISOString(),
Expand Down Expand Up @@ -291,7 +405,7 @@ describe('useSafeCreationData', () => {
faker.finance.ethereumAddress(),
faker.string.hexadecimal({ length: 64 }),
faker.finance.ethereumAddress(),
faker.finance.ethereumAddress(),
ZERO_ADDRESS,
0,
faker.finance.ethereumAddress(),
])
Expand All @@ -309,7 +423,7 @@ describe('useSafeCreationData', () => {

const mockTxHash = faker.string.hexadecimal({ length: 64 })
const mockFactoryAddress = faker.finance.ethereumAddress()
const mockMasterCopyAddress = faker.finance.ethereumAddress()
const mockMasterCopyAddress = getSafeSingletonDeployment({ version: '1.3.0' })?.defaultAddress!
jest.spyOn(cgwSdk, 'getCreationTransaction').mockResolvedValue({
data: {
created: new Date(Date.now()).toISOString(),
Expand Down Expand Up @@ -356,14 +470,14 @@ describe('useSafeCreationData', () => {
faker.finance.ethereumAddress(),
faker.string.hexadecimal({ length: 64, casing: 'lower' }),
faker.finance.ethereumAddress(),
faker.finance.ethereumAddress(),
ZERO_ADDRESS,
0,
faker.finance.ethereumAddress(),
])

const mockTxHash = faker.string.hexadecimal({ length: 64 })
const mockFactoryAddress = faker.finance.ethereumAddress()
const mockMasterCopyAddress = faker.finance.ethereumAddress()
const mockMasterCopyAddress = getSafeSingletonDeployment({ version: '1.3.0' })?.defaultAddress!
jest.spyOn(cgwSdk, 'getCreationTransaction').mockResolvedValue({
data: {
created: new Date(Date.now()).toISOString(),
Expand Down
40 changes: 13 additions & 27 deletions src/features/multichain/hooks/useCompatibleNetworks.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { type ReplayedSafeProps } from '@/features/counterfactual/store/undeployedSafesSlice'
import useChains from '@/hooks/useChains'
import { sameAddress } from '@/utils/addresses'
import { hasMatchingDeployment } from '@/services/contracts/deployments'
import { type SafeVersion } from '@safe-global/safe-core-sdk-types'
import {
type SingletonDeploymentV2,
getCompatibilityFallbackHandlerDeployments,
getProxyFactoryDeployments,
getSafeL2SingletonDeployments,
getSafeSingletonDeployments,
Expand All @@ -12,16 +12,6 @@ import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'

const SUPPORTED_VERSIONS: SafeVersion[] = ['1.4.1', '1.3.0']

const hasDeployment = (chainId: string, contractAddress: string, deployments: SingletonDeploymentV2[]) => {
return deployments.some((deployment) => {
// Check that deployment contains the contract Address on given chain
const networkDeployments = deployment.networkAddresses[chainId]
return Array.isArray(networkDeployments)
? networkDeployments.some((networkDeployment) => sameAddress(networkDeployment, contractAddress))
: sameAddress(networkDeployments, contractAddress)
})
}

/**
* Returns all chains where the creations's masterCopy and factory are deployed.
* @param creation
Expand All @@ -35,27 +25,23 @@ export const useCompatibleNetworks = (
return []
}

const { masterCopy, factoryAddress } = creation

const allL1SingletonDeployments = SUPPORTED_VERSIONS.map((version) =>
getSafeSingletonDeployments({ version }),
).filter(Boolean) as SingletonDeploymentV2[]

const allL2SingletonDeployments = SUPPORTED_VERSIONS.map((version) =>
getSafeL2SingletonDeployments({ version }),
).filter(Boolean) as SingletonDeploymentV2[]
const { masterCopy, factoryAddress, safeAccountConfig } = creation

const allProxyFactoryDeployments = SUPPORTED_VERSIONS.map((version) =>
getProxyFactoryDeployments({ version }),
).filter(Boolean) as SingletonDeploymentV2[]
const { fallbackHandler } = safeAccountConfig

return configs.map((config) => {
return {
...config,
available:
(hasDeployment(config.chainId, masterCopy, allL1SingletonDeployments) ||
hasDeployment(config.chainId, masterCopy, allL2SingletonDeployments)) &&
hasDeployment(config.chainId, factoryAddress, allProxyFactoryDeployments),
(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,
),
}
})
}
Loading

0 comments on commit 357fc85

Please sign in to comment.