diff --git a/packages/protocol-kit/src/contracts/utils.ts b/packages/protocol-kit/src/contracts/utils.ts index 163e75d16..df0168c26 100644 --- a/packages/protocol-kit/src/contracts/utils.ts +++ b/packages/protocol-kit/src/contracts/utils.ts @@ -1,4 +1,4 @@ -import { isAddress } from 'ethers' +import { isAddress, zeroPadValue } from 'ethers' import { DEFAULT_SAFE_VERSION } from '@safe-global/protocol-kit/contracts/config' import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/utils/constants' import { createMemoizedFunction } from '@safe-global/protocol-kit/utils/memoized' @@ -10,7 +10,6 @@ import { } from '@safe-global/safe-core-sdk-types' import { generateAddress2, keccak256, toBuffer } from 'ethereumjs-util' import semverSatisfies from 'semver/functions/satisfies' -// import { utils as zkSyncUtils } from 'zksync-web3' import { getCompatibilityFallbackHandlerContract, @@ -41,6 +40,9 @@ const ZKSYNC_SAFE_PROXY_DEPLOYED_BYTECODE: { } } +// keccak256(toUtf8Bytes('zksyncCreate2')) +const ZKSYNC_CREATE2_PREFIX = '0x2020dba91b30cc0006188af794c2fb30dd8520db7e2c088b7fc7c103c00ca494' + export interface PredictSafeAddressProps { ethAdapter: EthAdapter safeAccountConfig: SafeAccountConfig @@ -202,29 +204,22 @@ export async function predictSafeAddress({ const input = ethAdapter.encodeParameters(['address'], [await safeContract.getAddress()]) const chainId = await ethAdapter.getChainId() - // zkSync Era counterfactual deployment is calculated differently - // https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html#create-create2 - // if ([ZKSYNC_MAINNET, ZKSYNC_TESTNET].includes(chainId)) { - // const bytecodeHash = ZKSYNC_SAFE_PROXY_DEPLOYED_BYTECODE[safeVersion].deployedBytecodeHash - // return zkSyncUtils.create2Address( - // await safeProxyFactoryContract.getAddress(), - // bytecodeHash, - // salt, - // input - // ) - // } + const from = await safeProxyFactoryContract.getAddress() + + // On the zkSync Era chain, the counterfactual deployment address is calculated differently + const isZkSyncEraChain = [ZKSYNC_MAINNET, ZKSYNC_TESTNET].includes(chainId) + if (isZkSyncEraChain) { + const proxyAddress = zkSyncEraCreate2Address(from, safeVersion, salt, input) + + return ethAdapter.getChecksummedAddress(proxyAddress) + } const constructorData = toBuffer(input).toString('hex') const initCode = proxyCreationCode + constructorData const proxyAddress = - '0x' + - generateAddress2( - toBuffer(await safeProxyFactoryContract.getAddress()), - toBuffer(salt), - toBuffer(initCode) - ).toString('hex') + '0x' + generateAddress2(toBuffer(from), toBuffer(salt), toBuffer(initCode)).toString('hex') return ethAdapter.getChecksummedAddress(proxyAddress) } @@ -240,3 +235,38 @@ export const validateSafeDeploymentConfig = ({ saltNonce }: SafeDeploymentConfig if (saltNonce && BigInt(saltNonce) < 0) throw new Error('saltNonce must be greater than or equal to 0') } + +/** + * Generates a zkSync Era address. zkSync Era uses a distinct address derivation method compared to Ethereum + * see: https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html#address-derivation + * + * @param {string} from - The sender's address. + * @param {SafeVersion} safeVersion - The version of the safe. + * @param {Buffer} salt - The salt used for address derivation. + * @param {string} input - Additional input data for the derivation. + * + * @returns {string} The derived zkSync Era address. + */ +export function zkSyncEraCreate2Address( + from: string, + safeVersion: SafeVersion, + salt: Buffer, + input: string +): string { + const bytecodeHash = ZKSYNC_SAFE_PROXY_DEPLOYED_BYTECODE[safeVersion].deployedBytecodeHash + const inputHash = keccak256(toBuffer(input)) + + const addressBytes = keccak256( + toBuffer( + ZKSYNC_CREATE2_PREFIX + + zeroPadValue(from, 32).slice(2) + + salt.toString('hex') + + bytecodeHash.slice(2) + + inputHash.toString('hex') + ) + ) + .toString('hex') + .slice(24) + + return addressBytes +} diff --git a/packages/protocol-kit/tests/e2e/contractManager.test.ts b/packages/protocol-kit/tests/e2e/contractManager.test.ts index 4d1130dc9..6d07da639 100644 --- a/packages/protocol-kit/tests/e2e/contractManager.test.ts +++ b/packages/protocol-kit/tests/e2e/contractManager.test.ts @@ -3,7 +3,7 @@ import Safe, { ContractNetworksConfig, PredictedSafeProps } from '@safe-global/p import { ZERO_ADDRESS } from '@safe-global/protocol-kit/utils/constants' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import { deployments, waffle } from 'hardhat' +import { deployments } from 'hardhat' import { getContractNetworks } from './utils/setupContractNetworks' import { getCompatibilityFallbackHandler, @@ -13,7 +13,8 @@ import { getMultiSendCallOnly, getSafeSingleton, getSafeWithOwners, - getSignMessageLib + getSignMessageLib, + getSimulateTxAccessor } from './utils/setupContracts' import { getEthAdapter } from './utils/setupEthAdapter' import { getAccounts } from './utils/setupTestNetwork' @@ -108,7 +109,9 @@ describe('Safe contracts manager', () => { signMessageLibAddress: ZERO_ADDRESS, signMessageLibAbi: (await getSignMessageLib()).abi, createCallAddress: ZERO_ADDRESS, - createCallAbi: (await getCreateCall()).abi + createCallAbi: (await getCreateCall()).abi, + simulateTxAccessorAddress: ZERO_ADDRESS, + simulateTxAccessorAbi: (await getSimulateTxAccessor()).abi } } const [account1] = accounts diff --git a/packages/protocol-kit/tests/e2e/utilsContracts.test.ts b/packages/protocol-kit/tests/e2e/utilsContracts.test.ts index 8eaf69859..be83c9e84 100644 --- a/packages/protocol-kit/tests/e2e/utilsContracts.test.ts +++ b/packages/protocol-kit/tests/e2e/utilsContracts.test.ts @@ -1,5 +1,5 @@ import chai from 'chai' -import { deployments, waffle } from 'hardhat' +import { deployments } from 'hardhat' import { getAccounts } from './utils/setupTestNetwork' import { getContractNetworks } from './utils/setupContractNetworks' @@ -491,84 +491,85 @@ describe('Contract utils', () => { } ) - // FIXME: This test will be fixed in the issue safe-core-sdk #546 .See: https://github.com/safe-global/safe-core-sdk/issues/546 - // itif(true && safeVersionDeployed === '1.3.0')( - it.skip('returns the predicted address for Safes deployed on zkSync Era', async () => { - const { contractNetworks } = await setupTests() + itif(safeVersionDeployed === '1.3.0')( + 'returns the predicted address for Safes deployed on zkSync Era', + async () => { + const { contractNetworks } = await setupTests() - const safeVersion = safeVersionDeployed - // Create EthAdapter instance - const ethAdapter = await getEthAdapter(getNetworkProvider('zksync')) - const chainId = await ethAdapter.getChainId() - const customContracts = contractNetworks[chainId] + const safeVersion = safeVersionDeployed + // Create EthAdapter instance + const ethAdapter = await getEthAdapter(getNetworkProvider('zksync')) + const chainId = await ethAdapter.getChainId() + const customContracts = contractNetworks[chainId] - // We check real deployments from zksync return the expected address. + // We check real deployments from zksync return the expected address. - // 1/1 Safe - const safeAccountConfig1: SafeAccountConfig = { - owners: ['0xc6b82bA149CFA113f8f48d5E3b1F78e933e16DfD'], - threshold: 1 - } - const safeDeploymentConfig1: SafeDeploymentConfig = { - safeVersion, - saltNonce: '1691490995332' - } - const expectedSafeAddress1 = '0x4e19dA81a54eFbaBeb9AD50646f7643076475D65' + // 1/1 Safe + const safeAccountConfig1: SafeAccountConfig = { + owners: ['0xc6b82bA149CFA113f8f48d5E3b1F78e933e16DfD'], + threshold: 1 + } + const safeDeploymentConfig1: SafeDeploymentConfig = { + safeVersion, + saltNonce: '1691490995332' + } + const expectedSafeAddress1 = '0x4e19dA81a54eFbaBeb9AD50646f7643076475D65' - const firstPredictedSafeAddress = await predictSafeAddress({ - ethAdapter, - safeAccountConfig: safeAccountConfig1, - safeDeploymentConfig: safeDeploymentConfig1, - customContracts - }) + const firstPredictedSafeAddress = await predictSafeAddress({ + ethAdapter, + safeAccountConfig: safeAccountConfig1, + safeDeploymentConfig: safeDeploymentConfig1, + customContracts + }) - // 1/2 Safe - const safeAccountConfig2: SafeAccountConfig = { - owners: [ - '0x7E5E1C1FC6d625C1e60d78fDAB1CCE91e32261e4', - '0x6994Dc2544C1137b355488A9fc7b4F6EC2Bfeb5D' - ], - threshold: 1 - } - const safeDeploymentConfig2: SafeDeploymentConfig = { - safeVersion, - saltNonce: '1690771277826' - } - const expectedSafeAddress2 = '0x60c7F13dE7C8Fb88b3845e58859658bdc44243F8' + // 1/2 Safe + const safeAccountConfig2: SafeAccountConfig = { + owners: [ + '0x7E5E1C1FC6d625C1e60d78fDAB1CCE91e32261e4', + '0x6994Dc2544C1137b355488A9fc7b4F6EC2Bfeb5D' + ], + threshold: 1 + } + const safeDeploymentConfig2: SafeDeploymentConfig = { + safeVersion, + saltNonce: '1690771277826' + } + const expectedSafeAddress2 = '0x60c7F13dE7C8Fb88b3845e58859658bdc44243F8' - const secondPredictedSafeAddress = await predictSafeAddress({ - ethAdapter, - safeAccountConfig: safeAccountConfig2, - safeDeploymentConfig: safeDeploymentConfig2, - customContracts - }) + const secondPredictedSafeAddress = await predictSafeAddress({ + ethAdapter, + safeAccountConfig: safeAccountConfig2, + safeDeploymentConfig: safeDeploymentConfig2, + customContracts + }) - // 2/3 Safe - const safeAccountConfig3: SafeAccountConfig = { - owners: [ - '0x99999A3C4cB8427c44294Ad36895b6a3A047060d', - '0x1234561fEd41DD2D867a038bBdB857f291864225', - '0xe2c1F5DDcc99B0D70584fB4aD9D52b49cD4Cab03' - ], - threshold: 2 - } - const safeDeploymentConfig3: SafeDeploymentConfig = { - safeVersion, - saltNonce: '1690944491662' - } - const expectedSafeAddress3 = '0xD971FAA20db3ad4d51D453047ca03Ce4ec164CE2' + // 2/3 Safe + const safeAccountConfig3: SafeAccountConfig = { + owners: [ + '0x99999A3C4cB8427c44294Ad36895b6a3A047060d', + '0x1234561fEd41DD2D867a038bBdB857f291864225', + '0xe2c1F5DDcc99B0D70584fB4aD9D52b49cD4Cab03' + ], + threshold: 2 + } + const safeDeploymentConfig3: SafeDeploymentConfig = { + safeVersion, + saltNonce: '1690944491662' + } + const expectedSafeAddress3 = '0xD971FAA20db3ad4d51D453047ca03Ce4ec164CE2' - const thirdPredictedSafeAddress = await predictSafeAddress({ - ethAdapter, - safeAccountConfig: safeAccountConfig3, - safeDeploymentConfig: safeDeploymentConfig3, - customContracts - }) + const thirdPredictedSafeAddress = await predictSafeAddress({ + ethAdapter, + safeAccountConfig: safeAccountConfig3, + safeDeploymentConfig: safeDeploymentConfig3, + customContracts + }) - // returns the same predicted address each call - chai.expect(firstPredictedSafeAddress).to.be.equal(expectedSafeAddress1) - chai.expect(secondPredictedSafeAddress).to.be.equal(expectedSafeAddress2) - chai.expect(thirdPredictedSafeAddress).to.be.equal(expectedSafeAddress3) - }) + // returns the same predicted address each call + chai.expect(firstPredictedSafeAddress).to.be.equal(expectedSafeAddress1) + chai.expect(secondPredictedSafeAddress).to.be.equal(expectedSafeAddress2) + chai.expect(thirdPredictedSafeAddress).to.be.equal(expectedSafeAddress3) + } + ) }) })