Skip to content

Commit

Permalink
Feat: upgrade to 1.4.1
Browse files Browse the repository at this point in the history
  • Loading branch information
katspaugh committed Dec 12, 2024
1 parent 9421c23 commit a88b9e8
Show file tree
Hide file tree
Showing 9 changed files with 487 additions and 450 deletions.
1 change: 1 addition & 0 deletions src/components/common/ExternalLink/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const ExternalLink = ({
display: 'inline-flex',
alignItems: 'center',
gap: 0.2,
cursor: 'pointer',
}}
>
{children}
Expand Down
2 changes: 1 addition & 1 deletion src/components/tx-flow/SafeTxProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
5 changes: 3 additions & 2 deletions src/components/tx-flow/flows/UpdateSafe/UpdateSafeReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion src/components/tx/SignOrExecuteForm/SignOrExecuteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions src/services/tx/safeUpdateParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,6 +34,10 @@ const getChangeFallbackHandlerCallData = async (
export const createUpdateSafeTxs = async (safe: SafeInfo, chain: ChainInfo): Promise<MetaTransactionData[]> => {
assertValidSafeVersion(safe.version)

if (semverSatisfies(safe.version, '>=1.3.0')) {
return [createUpdateMigration(chain)]
}

const latestMasterCopyAddress = await (
await getReadOnlyGnosisSafeContract(chain, getLatestSafeVersion(chain))
).getAddress()
Expand Down
321 changes: 321 additions & 0 deletions src/utils/__tests__/safe-migrations.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof getAndValidateSafeSDK>

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)
})
})
Loading

0 comments on commit a88b9e8

Please sign in to comment.