Skip to content

Commit

Permalink
[Multichain] Feat: multichain feature toggle (#4209)
Browse files Browse the repository at this point in the history
- add multichain feature toggle
- do not offer multichain creation with toggle off
- do not include migration if feature toggle is off
  • Loading branch information
schmanu authored Sep 19, 2024
1 parent bf6ea57 commit 91f0566
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 112 deletions.
13 changes: 12 additions & 1 deletion src/components/common/NetworkSelector/NetworkMultiSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SetNameStepFields } from '@/components/new-safe/create/steps/SetNameSte
import { getSafeSingletonDeployments } from '@safe-global/safe-deployments'
import { getLatestSafeVersion } from '@/utils/chains'
import { hasCanonicalDeployment } from '@/services/contracts/deployments'
import { hasMultiChainCreationFeatures } from '@/components/welcome/MyAccounts/utils/multiChainSafe'

const NetworkMultiSelector = ({
name,
Expand Down Expand Up @@ -55,12 +56,22 @@ const NetworkMultiSelector = ({

const isOptionDisabled = useCallback(
(optionNetwork: ChainInfo) => {
if (selectedNetworks.length === 0) return false
// Initially all networks are always available
if (selectedNetworks.length === 0) {
return false
}

const firstSelectedNetwork = selectedNetworks[0]

// do not allow multi chain safes for advanced setup flow.
if (isAdvancedFlow) return optionNetwork.chainId != firstSelectedNetwork.chainId

// Check required feature toggles
if (!hasMultiChainCreationFeatures(optionNetwork) || !hasMultiChainCreationFeatures(firstSelectedNetwork)) {
return true
}

// Check if required deployments are available
const optionHasCanonicalSingletonDeployment = hasCanonicalDeployment(
getSafeSingletonDeployments({
network: optionNetwork.chainId,
Expand Down
322 changes: 221 additions & 101 deletions src/components/new-safe/create/logic/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import type { CompatibilityFallbackHandlerContractImplementationType } from '@sa
import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants'
import * as web3 from '@/hooks/wallets/web3'
import * as sdkHelpers from '@/services/tx/tx-sender/sdk'
import { relaySafeCreation, getRedirect } from '@/components/new-safe/create/logic/index'
import {
relaySafeCreation,
getRedirect,
createNewUndeployedSafeWithoutSalt,
SAFE_TO_L2_SETUP_INTERFACE,
} from '@/components/new-safe/create/logic/index'
import { relayTransaction } from '@safe-global/safe-gateway-typescript-sdk'
import { toBeHex } from 'ethers'
import {
Expand All @@ -23,6 +28,13 @@ import { type FEATURES as GatewayFeatures } from '@safe-global/safe-gateway-type
import { chainBuilder } from '@/tests/builders/chains'
import { type ReplayedSafeProps } from '@/store/slices'
import { faker } from '@faker-js/faker'
import { ECOSYSTEM_ID_ADDRESS, SAFE_TO_L2_SETUP_ADDRESS } from '@/config/constants'
import {
getFallbackHandlerDeployment,
getProxyFactoryDeployment,
getSafeL2SingletonDeployment,
getSafeSingletonDeployment,
} from '@safe-global/safe-deployments'

const provider = new JsonRpcProvider(undefined, { name: 'ethereum', chainId: 1 })

Expand All @@ -32,114 +44,115 @@ const latestSafeVersion = getLatestSafeVersion(
.build(),
)

describe('createNewSafeViaRelayer', () => {
const owner1 = toBeHex('0x1', 20)
const owner2 = toBeHex('0x2', 20)
describe('create/logic', () => {
describe('createNewSafeViaRelayer', () => {
const owner1 = toBeHex('0x1', 20)
const owner2 = toBeHex('0x2', 20)

const mockChainInfo = chainBuilder()
.with({
chainId: '1',
l2: false,
features: [FEATURES.SAFE_141 as unknown as GatewayFeatures],
})
.build()

const mockChainInfo = chainBuilder()
.with({
chainId: '1',
l2: false,
features: [FEATURES.SAFE_141 as unknown as GatewayFeatures],
beforeAll(() => {
jest.resetAllMocks()
jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => provider)
})
.build()

beforeAll(() => {
jest.resetAllMocks()
jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => provider)
})
it('returns taskId if create Safe successfully relayed', async () => {
const mockSafeProvider = {
getExternalProvider: jest.fn(),
getExternalSigner: jest.fn(),
getChainId: jest.fn().mockReturnValue(BigInt(1)),
} as unknown as SafeProvider

it('returns taskId if create Safe successfully relayed', async () => {
const mockSafeProvider = {
getExternalProvider: jest.fn(),
getExternalSigner: jest.fn(),
getChainId: jest.fn().mockReturnValue(BigInt(1)),
} as unknown as SafeProvider

jest.spyOn(gateway, 'relayTransaction').mockResolvedValue({ taskId: '0x123' })
jest.spyOn(sdkHelpers, 'getSafeProvider').mockImplementation(() => mockSafeProvider)

jest.spyOn(contracts, 'getReadOnlyFallbackHandlerContract').mockResolvedValue({
getAddress: () => '0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4',
} as unknown as CompatibilityFallbackHandlerContractImplementationType)

const expectedSaltNonce = 69
const expectedThreshold = 1
const proxyFactoryAddress = await (await getReadOnlyProxyFactoryContract(latestSafeVersion)).getAddress()
const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(latestSafeVersion)
const safeContractAddress = await (
await getReadOnlyGnosisSafeContract(mockChainInfo, latestSafeVersion)
).getAddress()

const undeployedSafeProps: ReplayedSafeProps = {
safeAccountConfig: {
owners: [owner1, owner2],
threshold: 1,
data: EMPTY_DATA,
to: ZERO_ADDRESS,
fallbackHandler: await readOnlyFallbackHandlerContract.getAddress(),
paymentReceiver: ZERO_ADDRESS,
payment: 0,
paymentToken: ZERO_ADDRESS,
},
safeVersion: latestSafeVersion,
factoryAddress: proxyFactoryAddress,
masterCopy: safeContractAddress,
saltNonce: '69',
}

const expectedInitializer = Gnosis_safe__factory.createInterface().encodeFunctionData('setup', [
[owner1, owner2],
expectedThreshold,
ZERO_ADDRESS,
EMPTY_DATA,
await readOnlyFallbackHandlerContract.getAddress(),
ZERO_ADDRESS,
0,
ZERO_ADDRESS,
])

const expectedCallData = Proxy_factory__factory.createInterface().encodeFunctionData('createProxyWithNonce', [
safeContractAddress,
expectedInitializer,
expectedSaltNonce,
])

const taskId = await relaySafeCreation(mockChainInfo, undeployedSafeProps)

expect(taskId).toEqual('0x123')
expect(relayTransaction).toHaveBeenCalledTimes(1)
expect(relayTransaction).toHaveBeenCalledWith('1', {
to: proxyFactoryAddress,
data: expectedCallData,
version: latestSafeVersion,
jest.spyOn(gateway, 'relayTransaction').mockResolvedValue({ taskId: '0x123' })
jest.spyOn(sdkHelpers, 'getSafeProvider').mockImplementation(() => mockSafeProvider)

jest.spyOn(contracts, 'getReadOnlyFallbackHandlerContract').mockResolvedValue({
getAddress: () => '0xf48f2B2d2a534e402487b3ee7C18c33Aec0Fe5e4',
} as unknown as CompatibilityFallbackHandlerContractImplementationType)

const expectedSaltNonce = 69
const expectedThreshold = 1
const proxyFactoryAddress = await (await getReadOnlyProxyFactoryContract(latestSafeVersion)).getAddress()
const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract(latestSafeVersion)
const safeContractAddress = await (
await getReadOnlyGnosisSafeContract(mockChainInfo, latestSafeVersion)
).getAddress()

const undeployedSafeProps: ReplayedSafeProps = {
safeAccountConfig: {
owners: [owner1, owner2],
threshold: 1,
data: EMPTY_DATA,
to: ZERO_ADDRESS,
fallbackHandler: await readOnlyFallbackHandlerContract.getAddress(),
paymentReceiver: ZERO_ADDRESS,
payment: 0,
paymentToken: ZERO_ADDRESS,
},
safeVersion: latestSafeVersion,
factoryAddress: proxyFactoryAddress,
masterCopy: safeContractAddress,
saltNonce: '69',
}

const expectedInitializer = Gnosis_safe__factory.createInterface().encodeFunctionData('setup', [
[owner1, owner2],
expectedThreshold,
ZERO_ADDRESS,
EMPTY_DATA,
await readOnlyFallbackHandlerContract.getAddress(),
ZERO_ADDRESS,
0,
ZERO_ADDRESS,
])

const expectedCallData = Proxy_factory__factory.createInterface().encodeFunctionData('createProxyWithNonce', [
safeContractAddress,
expectedInitializer,
expectedSaltNonce,
])

const taskId = await relaySafeCreation(mockChainInfo, undeployedSafeProps)

expect(taskId).toEqual('0x123')
expect(relayTransaction).toHaveBeenCalledTimes(1)
expect(relayTransaction).toHaveBeenCalledWith('1', {
to: proxyFactoryAddress,
data: expectedCallData,
version: latestSafeVersion,
})
})
})

it('should throw an error if relaying fails', () => {
const relayFailedError = new Error('Relay failed')
jest.spyOn(gateway, 'relayTransaction').mockRejectedValue(relayFailedError)
it('should throw an error if relaying fails', () => {
const relayFailedError = new Error('Relay failed')
jest.spyOn(gateway, 'relayTransaction').mockRejectedValue(relayFailedError)

const undeployedSafeProps: ReplayedSafeProps = {
safeAccountConfig: {
owners: [owner1, owner2],
threshold: 1,
data: EMPTY_DATA,
to: ZERO_ADDRESS,
fallbackHandler: faker.finance.ethereumAddress(),
paymentReceiver: ZERO_ADDRESS,
payment: 0,
paymentToken: ZERO_ADDRESS,
},
safeVersion: latestSafeVersion,
factoryAddress: faker.finance.ethereumAddress(),
masterCopy: faker.finance.ethereumAddress(),
saltNonce: '69',
}

expect(relaySafeCreation(mockChainInfo, undeployedSafeProps)).rejects.toEqual(relayFailedError)
})
const undeployedSafeProps: ReplayedSafeProps = {
safeAccountConfig: {
owners: [owner1, owner2],
threshold: 1,
data: EMPTY_DATA,
to: ZERO_ADDRESS,
fallbackHandler: faker.finance.ethereumAddress(),
paymentReceiver: ZERO_ADDRESS,
payment: 0,
paymentToken: ZERO_ADDRESS,
},
safeVersion: latestSafeVersion,
factoryAddress: faker.finance.ethereumAddress(),
masterCopy: faker.finance.ethereumAddress(),
saltNonce: '69',
}

expect(relaySafeCreation(mockChainInfo, undeployedSafeProps)).rejects.toEqual(relayFailedError)
})
})
describe('getRedirect', () => {
it("should redirect to home for any redirect that doesn't start with /apps", () => {
const expected = {
Expand All @@ -162,4 +175,111 @@ describe('createNewSafeViaRelayer', () => {
)
})
})

describe('createNewUndeployedSafeWithoutSalt', () => {
it('should throw errors if no deployments are found', () => {
expect(() =>
createNewUndeployedSafeWithoutSalt(
'1.4.1',
{
owners: [faker.finance.ethereumAddress()],
threshold: 1,
},
chainBuilder().with({ chainId: 'NON_EXISTING' }).build(),
),
).toThrowError(new Error('No Safe deployment found'))
})

it('should use l1 masterCopy and no migration on l1s without multichain feature', () => {
const safeSetup = {
owners: [faker.finance.ethereumAddress()],
threshold: 1,
}
expect(
createNewUndeployedSafeWithoutSalt(
'1.4.1',
safeSetup,
chainBuilder()
.with({ chainId: '1' })
// Multichain creation is toggled off
.with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL] as any })
.with({ l2: false })
.build(),
),
).toEqual({
safeAccountConfig: {
...safeSetup,
fallbackHandler: getFallbackHandlerDeployment({ version: '1.4.1', network: '1' })?.defaultAddress,
to: ZERO_ADDRESS,
data: EMPTY_DATA,
paymentReceiver: ECOSYSTEM_ID_ADDRESS,
},
safeVersion: '1.4.1',
masterCopy: getSafeSingletonDeployment({ version: '1.4.1', network: '1' })?.defaultAddress,
factoryAddress: getProxyFactoryDeployment({ version: '1.4.1', network: '1' })?.defaultAddress,
})
})

it('should use l2 masterCopy and no migration on l2s without multichain feature', () => {
const safeSetup = {
owners: [faker.finance.ethereumAddress()],
threshold: 1,
}
expect(
createNewUndeployedSafeWithoutSalt(
'1.4.1',
safeSetup,
chainBuilder()
.with({ chainId: '137' })
// Multichain creation is toggled off
.with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL] as any })
.with({ l2: true })
.build(),
),
).toEqual({
safeAccountConfig: {
...safeSetup,
fallbackHandler: getFallbackHandlerDeployment({ version: '1.4.1', network: '137' })?.defaultAddress,
to: ZERO_ADDRESS,
data: EMPTY_DATA,
paymentReceiver: ECOSYSTEM_ID_ADDRESS,
},
safeVersion: '1.4.1',
masterCopy: getSafeL2SingletonDeployment({ version: '1.4.1', network: '137' })?.defaultAddress,
factoryAddress: getProxyFactoryDeployment({ version: '1.4.1', network: '137' })?.defaultAddress,
})
})

it('should use l1 masterCopy and migration on l2s with multichain feature', () => {
const safeSetup = {
owners: [faker.finance.ethereumAddress()],
threshold: 1,
}
expect(
createNewUndeployedSafeWithoutSalt(
'1.4.1',
safeSetup,
chainBuilder()
.with({ chainId: '137' })
// Multichain creation is toggled off
.with({ features: [FEATURES.SAFE_141, FEATURES.COUNTERFACTUAL, FEATURES.MULTI_CHAIN_SAFE_CREATION] as any })
.with({ l2: true })
.build(),
),
).toEqual({
safeAccountConfig: {
...safeSetup,
fallbackHandler: getFallbackHandlerDeployment({ version: '1.4.1', network: '137' })?.defaultAddress,
to: SAFE_TO_L2_SETUP_ADDRESS,
data: SAFE_TO_L2_SETUP_INTERFACE.encodeFunctionData('setupToL2', [
getSafeL2SingletonDeployment({ version: '1.4.1', network: '137' })?.defaultAddress,
]),
paymentReceiver: ECOSYSTEM_ID_ADDRESS,
},
safeVersion: '1.4.1',
masterCopy: getSafeSingletonDeployment({ version: '1.4.1', network: '137' })?.defaultAddress,
factoryAddress: getProxyFactoryDeployment({ version: '1.4.1', network: '137' })?.defaultAddress,
})
})
})
})
Loading

0 comments on commit 91f0566

Please sign in to comment.