Skip to content

Commit

Permalink
fix: Advanced safe creation address computation (#4564)
Browse files Browse the repository at this point in the history
  • Loading branch information
usame-algan authored Nov 27, 2024
1 parent 5098280 commit ed631fc
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 53 deletions.
2 changes: 2 additions & 0 deletions src/components/new-safe/create/AdvancedCreateSafe.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ECOSYSTEM_ID_ADDRESS } from '@/config/constants'
import { Container, Typography, Grid } from '@mui/material'
import { useRouter } from 'next/router'

Expand Down Expand Up @@ -98,6 +99,7 @@ const AdvancedCreateSafe = () => {
threshold: 1,
saltNonce: 0,
safeVersion: getLatestSafeVersion(chain),
paymentReceiver: ECOSYSTEM_ID_ADDRESS,
}

const onClose = () => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/new-safe/create/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ export type NewSafeFormData = {
networks: ChainInfo[]
threshold: number
owners: NamedAddress[]
saltNonce: number
saltNonce?: number
safeVersion: SafeVersion
safeAddress?: string
willRelay?: boolean
paymentReceiver?: string
}

const staticHints: Record<
Expand Down Expand Up @@ -173,7 +174,6 @@ const CreateSafe = () => {
networks: [],
owners: [],
threshold: 1,
saltNonce: 0,
safeVersion: getLatestSafeVersion(chain) as SafeVersion,
}

Expand Down
6 changes: 4 additions & 2 deletions src/components/new-safe/create/logic/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ export type UndeployedSafeWithoutSalt = Omit<ReplayedSafeProps, 'saltNonce'>
*/
export const createNewUndeployedSafeWithoutSalt = (
safeVersion: SafeVersion,
safeAccountConfig: Pick<ReplayedSafeProps['safeAccountConfig'], 'owners' | 'threshold'>,
safeAccountConfig: Pick<ReplayedSafeProps['safeAccountConfig'], 'owners' | 'threshold'> & {
paymentReceiver?: string
},
chain: ChainInfo,
): UndeployedSafeWithoutSalt => {
// Create universal deployment Data across chains:
Expand Down Expand Up @@ -243,7 +245,7 @@ export const createNewUndeployedSafeWithoutSalt = (
fallbackHandler: fallbackHandlerAddress,
to: includeMigration && safeToL2SetupAddress ? safeToL2SetupAddress : ZERO_ADDRESS,
data: includeMigration ? safeToL2SetupInterface.encodeFunctionData('setupToL2', [safeL2Address]) : EMPTY_DATA,
paymentReceiver: ECOSYSTEM_ID_ADDRESS,
paymentReceiver: safeAccountConfig.paymentReceiver ?? ECOSYSTEM_ID_ADDRESS,
},
safeVersion,
}
Expand Down
121 changes: 79 additions & 42 deletions src/components/new-safe/create/steps/AdvancedOptionsStep/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { predictAddressBasedOnReplayData } from '@/features/multichain/utils/utils'
import { useWeb3ReadOnly } from '@/hooks/wallets/web3'
import { Button, MenuItem, Divider, Box, TextField, Stack, Skeleton, SvgIcon, Tooltip, Typography } from '@mui/material'
import { Controller, FormProvider, useForm } from 'react-hook-form'
import type { ReactElement } from 'react'
import { type ReactElement, useMemo } from 'react'

import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper'
import type { NewSafeFormData } from '@/components/new-safe/create'
Expand All @@ -11,29 +13,29 @@ import { type SafeVersion } from '@safe-global/safe-core-sdk-types'
import NumberField from '@/components/common/NumberField'
import { useCurrentChain } from '@/hooks/useChains'
import useAsync from '@/hooks/useAsync'
import { computeNewSafeAddress } from '../../logic'
import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts'
import { createNewUndeployedSafeWithoutSalt } from '../../logic'
import EthHashInfo from '@/components/common/EthHashInfo'
import InfoIcon from '@/public/images/notifications/info.svg'
import useWallet from '@/hooks/wallets/useWallet'
import { isSmartContract } from '@/utils/wallets'

enum AdvancedOptionsFields {
safeVersion = 'safeVersion',
saltNonce = 'saltNonce',
paymentReceiver = 'paymentReceiver',
}

export type AdvancedOptionsStepForm = {
[AdvancedOptionsFields.safeVersion]: SafeVersion
[AdvancedOptionsFields.saltNonce]: number
[AdvancedOptionsFields.paymentReceiver]: string
}

const ADVANCED_OPTIONS_STEP_FORM_ID = 'create-safe-advanced-options-step-form'

const AdvancedOptionsStep = ({ onSubmit, onBack, data, setStep }: StepRenderProps<NewSafeFormData>): ReactElement => {
const wallet = useWallet()
useSyncSafeCreationStep(setStep, data.networks)
const chain = useCurrentChain()
const provider = useWeb3ReadOnly()

const formMethods = useForm<AdvancedOptionsStepForm>({
mode: 'onChange',
Expand All @@ -44,39 +46,31 @@ const AdvancedOptionsStep = ({ onSubmit, onBack, data, setStep }: StepRenderProp

const selectedSafeVersion = watch(AdvancedOptionsFields.safeVersion)
const selectedSaltNonce = watch(AdvancedOptionsFields.saltNonce)
const selectedPaymentReceiver = watch(AdvancedOptionsFields.paymentReceiver)

const [readOnlyFallbackHandlerContract] = useAsync(
() => (chain ? getReadOnlyFallbackHandlerContract(selectedSafeVersion) : undefined),
[chain, selectedSafeVersion],
const newSafeProps = useMemo(
() =>
chain
? createNewUndeployedSafeWithoutSalt(
selectedSafeVersion,
{
owners: data.owners.map((owner) => owner.address),
threshold: data.threshold,
paymentReceiver: selectedPaymentReceiver,
},
chain,
)
: undefined,
[chain, data.owners, data.threshold, selectedSafeVersion, selectedPaymentReceiver],
)

const [predictedSafeAddress] = useAsync(async () => {
if (!chain || !readOnlyFallbackHandlerContract || !wallet) {
return undefined
}

return computeNewSafeAddress(
wallet.provider,
{
safeAccountConfig: {
owners: data.owners.map((owner) => owner.address),
threshold: data.threshold,
fallbackHandler: await readOnlyFallbackHandlerContract.getAddress(),
},
saltNonce: selectedSaltNonce.toString(),
},
chain,
selectedSafeVersion,
)
}, [
chain,
data.owners,
data.threshold,
wallet,
readOnlyFallbackHandlerContract,
selectedSafeVersion,
selectedSaltNonce,
])
if (!provider || !newSafeProps) return

const replayedSafeWithNonce = { ...newSafeProps, saltNonce: selectedSaltNonce.toString() }

return predictAddressBasedOnReplayData(replayedSafeWithNonce, provider)
}, [provider, newSafeProps, selectedSaltNonce])

const [isDeployed] = useAsync(
async () => (predictedSafeAddress ? await isSmartContract(predictedSafeAddress) : false),
Expand All @@ -102,7 +96,7 @@ const AdvancedOptionsStep = ({ onSubmit, onBack, data, setStep }: StepRenderProp
<Stack spacing={2}>
<Box className={layoutCss.row}>
<Typography
variant="h4"
variant="h5"
sx={{
fontWeight: 700,
display: 'inline-flex',
Expand Down Expand Up @@ -139,17 +133,16 @@ const AdvancedOptionsStep = ({ onSubmit, onBack, data, setStep }: StepRenderProp
</TextField>
)}
/>
</Box>

<Divider />
<Box className={layoutCss.row}>
<Typography
variant="h4"
variant="h5"
sx={{
fontWeight: 700,
display: 'inline-flex',
alignItems: 'center',
gap: 1,
mt: 4,
width: 1,
}}
>
Salt nonce
Expand Down Expand Up @@ -183,15 +176,57 @@ const AdvancedOptionsStep = ({ onSubmit, onBack, data, setStep }: StepRenderProp
},
required: true,
})}
fullWidth
label="Salt nonce"
error={Boolean(formState.errors[AdvancedOptionsFields.saltNonce]) || Boolean(isDeployed)}
helperText={
(formState.errors[AdvancedOptionsFields.saltNonce]?.message ?? Boolean(isDeployed))
? 'The Safe is already deployed. Use a different salt nonce.'
: undefined
formState.errors[AdvancedOptionsFields.saltNonce]?.message ??
(Boolean(isDeployed) ? 'The Safe is already deployed. Use a different salt nonce.' : undefined)
}
/>

<Typography
variant="h5"
sx={{
fontWeight: 700,
display: 'inline-flex',
alignItems: 'center',
gap: 1,
mt: 4,
width: 1,
}}
>
Payment receiver
<Tooltip title="The payment receiver changes the predicted Safe address." arrow placement="top">
<span style={{ display: 'flex' }}>
<SvgIcon component={InfoIcon} inheritViewBox color="border" fontSize="small" />
</span>
</Tooltip>
</Typography>
<Typography
variant="body2"
sx={{
mb: 2,
}}
>
Impacts the derived Safe address
</Typography>
<TextField
{...register(AdvancedOptionsFields.paymentReceiver, {
required: true,
})}
label="Payment receiver"
error={Boolean(formState.errors[AdvancedOptionsFields.paymentReceiver]) || Boolean(isDeployed)}
helperText={
formState.errors[AdvancedOptionsFields.paymentReceiver]?.message ??
(Boolean(isDeployed) ? 'The Safe is already deployed. Use a different payment receiver.' : undefined)
}
fullWidth
/>
</Box>

<Divider />

<Box className={layoutCss.row}>
<Typography
variant="h4"
Expand All @@ -208,7 +243,9 @@ const AdvancedOptionsStep = ({ onSubmit, onBack, data, setStep }: StepRenderProp
<Skeleton />
)}
</Box>

<Divider />

<Box className={layoutCss.row}>
<Box
sx={{
Expand Down
13 changes: 6 additions & 7 deletions src/components/new-safe/create/steps/ReviewStep/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,12 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
{
owners: data.owners.map((owner) => owner.address),
threshold: data.threshold,
paymentReceiver: data.paymentReceiver,
},
chain,
)
: undefined,
[chain, data.owners, data.safeVersion, data.threshold],
[chain, data.owners, data.safeVersion, data.threshold, data.paymentReceiver],
)

const safePropsForGasEstimation = useMemo(() => {
Expand Down Expand Up @@ -226,12 +227,10 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
setIsCreating(true)

// Figure out the shared available nonce across chains
const nextAvailableNonce = await getAvailableSaltNonce(
customRPCs,
{ ...newSafeProps, saltNonce: '0' },
data.networks,
knownAddresses,
)
const nextAvailableNonce =
data.saltNonce !== undefined
? data.saltNonce.toString()
: await getAvailableSaltNonce(customRPCs, { ...newSafeProps, saltNonce: '0' }, data.networks, knownAddresses)

const replayedSafeWithNonce = { ...newSafeProps, saltNonce: nextAvailableNonce }

Expand Down

0 comments on commit ed631fc

Please sign in to comment.