From ad7ac2b0fbf89b78fcd67826505904b84f7a2c31 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Wed, 24 Sep 2025 09:55:39 -0400 Subject: [PATCH 1/3] feat: Transfer Ownership on Universal SpokePool Deployment Signed-off-by: Faisal Usmani --- deploy/111_deploy_universal_spokepool.ts | 27 ++++++++++++++++++++++-- deploy/consts.ts | 3 +++ hardhat.config.ts | 8 +++++++ utils/utils.hre.ts | 17 +++++++++------ 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/deploy/111_deploy_universal_spokepool.ts b/deploy/111_deploy_universal_spokepool.ts index f309106f5..07a0af4bc 100644 --- a/deploy/111_deploy_universal_spokepool.ts +++ b/deploy/111_deploy_universal_spokepool.ts @@ -1,10 +1,18 @@ import { DeployFunction } from "hardhat-deploy/types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { deployNewProxy, getSpokePoolDeploymentInfo } from "../utils/utils.hre"; -import { FILL_DEADLINE_BUFFER, L2_ADDRESS_MAP, QUOTE_TIME_BUFFER, USDC, ZERO_ADDRESS } from "./consts"; +import { + EXPECTED_SAFE_ADDRESS, + FILL_DEADLINE_BUFFER, + L2_ADDRESS_MAP, + QUOTE_TIME_BUFFER, + USDC, + ZERO_ADDRESS, +} from "./consts"; import { CHAIN_IDs, PRODUCTION_NETWORKS, TOKEN_SYMBOLS_MAP } from "../utils/constants"; import { getOftEid, toWei } from "../utils/utils"; import { getDeployedAddress } from "../src/DeploymentUtils"; +import "@nomiclabs/hardhat-ethers"; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { hubPool, hubChainId, spokeChainId } = await getSpokePoolDeploymentInfo(hre); @@ -53,7 +61,22 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { // target address of the spoke pool. This is because the HubPool does not pass in the chainId when calling // relayMessage() on the Adapter. Therefore, if Universal SpokePools share the same address, // then a message designed to be sent to one chain could be sent to another's SpokePool. - await deployNewProxy("Universal_SpokePool", constructorArgs, initArgs); + const { proxyAddress } = await deployNewProxy("Universal_SpokePool", constructorArgs, initArgs); + + const provider = hre.ethers.provider; + const safeCode = await provider.getCode(EXPECTED_SAFE_ADDRESS); + if (safeCode !== "0x" && proxyAddress) { + const factory = await hre.ethers.getContractFactory("Universal_SpokePool"); + const contract = factory.attach(proxyAddress); + + const owner = await contract.owner(); + if (owner !== EXPECTED_SAFE_ADDRESS) { + await contract.transferOwnership(EXPECTED_SAFE_ADDRESS); + console.log("Transferred ownership to Expected Safe address:", EXPECTED_SAFE_ADDRESS); + } else { + console.log("Expected Safe address is already the owner of the Universal SpokePool"); + } + } }; module.exports = func; func.tags = ["UniversalSpokePool"]; diff --git a/deploy/consts.ts b/deploy/consts.ts index 3eee06198..92b50545d 100644 --- a/deploy/consts.ts +++ b/deploy/consts.ts @@ -22,6 +22,9 @@ export const ZK_L1_GAS_TO_L2_GAS_PER_PUBDATA_LIMIT = 800; export const ZK_L2_GAS_LIMIT = 2000000; export const ZK_MAX_GASPRICE = "10000000000000"; // 10k gwei +// Expected Safe address for Universal SpokePool +export const EXPECTED_SAFE_ADDRESS = "0xB524735356985D2f267FA010D681f061DfF03715"; + export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string } } = { [CHAIN_IDs.MAINNET]: { finder: "0x40f941E48A552bF496B154Af6bf55725f18D77c3", diff --git a/hardhat.config.ts b/hardhat.config.ts index 0c4f4df43..d141f0f2c 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -324,6 +324,14 @@ const config: HardhatUserConfig = { browserURL: "https://explorer.redstone.xyz", }, }, + { + network: "plasma", + chainId: 9745, + urls: { + apiURL: "https://api.routescan.io/v2/network/mainnet/evm/9745/etherscan", + browserURL: "https://plasmascan.to", + }, + }, { network: "soneium", chainId: CHAIN_IDs.SONEIUM, diff --git a/utils/utils.hre.ts b/utils/utils.hre.ts index 7e7fc3325..4f2f090ff 100644 --- a/utils/utils.hre.ts +++ b/utils/utils.hre.ts @@ -43,7 +43,7 @@ export async function deployNewProxy( constructorArgs: FnArgs[], initArgs: FnArgs[], implementationOnly?: boolean -): Promise { +): Promise<{ proxyAddress?: string; implementationAddress: string }> { const { deployments, run, upgrades, getChainId } = hre; let chainId = Number(await getChainId()); @@ -54,11 +54,12 @@ export async function deployNewProxy( } // If a SpokePool can be found in deployments/deployments.json, then only deploy an implementation contract. - const proxy = getDeployedAddress("SpokePool", chainId, false); - implementationOnly ??= proxy !== undefined; + let proxyAddress = getDeployedAddress("SpokePool", chainId, false); + implementationOnly ??= proxyAddress !== undefined; + let implementationAddress: string; if (implementationOnly) { - console.log(`${name} deployment already detected @ ${proxy}, deploying new implementation.`); - instance = (await upgrades.deployImplementation(await getContractFactory(name, {}), { + console.log(`${name} deployment already detected @ ${proxyAddress}, deploying new implementation.`); + implementationAddress = instance = (await upgrades.deployImplementation(await getContractFactory(name, {}), { constructorArgs, kind: "uups", unsafeAllow: unsafeAllowArgs as unsafeAllowTypes, @@ -71,9 +72,9 @@ export async function deployNewProxy( constructorArgs, initializer: "initialize", }); - instance = (await proxy.deployed()).address; + proxyAddress = instance = (await proxy.deployed()).address; console.log(`New ${name} proxy deployed @ ${instance}`); - const implementationAddress = await upgrades.erc1967.getImplementationAddress(instance); + implementationAddress = await upgrades.erc1967.getImplementationAddress(instance); console.log(`${name} implementation deployed @ ${implementationAddress}`); } @@ -93,6 +94,8 @@ export async function deployNewProxy( // https://docs.openzeppelin.com/upgrades-plugins/1.x/api-hardhat-upgrades#verify const contract = `contracts/${name}.sol:${name}`; await verifyContract(instance, constructorArgs, contract); + + return { proxyAddress, implementationAddress }; } export async function verifyContract(address: string, constructorArguments: any[], contract?: string) { From 8c03b88657efb2ce280483bc20b3b18d8e2de02d Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Thu, 25 Sep 2025 09:48:16 -0400 Subject: [PATCH 2/3] using safe sdk to check for owners Signed-off-by: Faisal Usmani --- deploy/111_deploy_universal_spokepool.ts | 37 +++++++++++++++++------- deploy/consts.ts | 9 +++++- scripts/deployMultisig.ts | 24 ++------------- utils/utils.ts | 16 +++++++++- 4 files changed, 52 insertions(+), 34 deletions(-) diff --git a/deploy/111_deploy_universal_spokepool.ts b/deploy/111_deploy_universal_spokepool.ts index 07a0af4bc..4d4c55358 100644 --- a/deploy/111_deploy_universal_spokepool.ts +++ b/deploy/111_deploy_universal_spokepool.ts @@ -10,9 +10,11 @@ import { ZERO_ADDRESS, } from "./consts"; import { CHAIN_IDs, PRODUCTION_NETWORKS, TOKEN_SYMBOLS_MAP } from "../utils/constants"; -import { getOftEid, toWei } from "../utils/utils"; +import { getOftEid, toWei, predictedSafe } from "../utils/utils"; +import { getNodeUrl } from "../utils"; import { getDeployedAddress } from "../src/DeploymentUtils"; import "@nomiclabs/hardhat-ethers"; +import Safe from "@safe-global/protocol-kit"; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { hubPool, hubChainId, spokeChainId } = await getSpokePoolDeploymentInfo(hre); @@ -63,18 +65,31 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { // then a message designed to be sent to one chain could be sent to another's SpokePool. const { proxyAddress } = await deployNewProxy("Universal_SpokePool", constructorArgs, initArgs); - const provider = hre.ethers.provider; - const safeCode = await provider.getCode(EXPECTED_SAFE_ADDRESS); - if (safeCode !== "0x" && proxyAddress) { - const factory = await hre.ethers.getContractFactory("Universal_SpokePool"); - const contract = factory.attach(proxyAddress); + const nodeUrl = getNodeUrl(spokeChainId); - const owner = await contract.owner(); - if (owner !== EXPECTED_SAFE_ADDRESS) { - await contract.transferOwnership(EXPECTED_SAFE_ADDRESS); - console.log("Transferred ownership to Expected Safe address:", EXPECTED_SAFE_ADDRESS); + const protocolKit = await Safe.init({ + provider: nodeUrl, + predictedSafe, + }); + + const existingProtocolKit = await protocolKit.connect({ + safeAddress: EXPECTED_SAFE_ADDRESS, + }); + const isDeployed = await existingProtocolKit.isSafeDeployed(); + if (proxyAddress) { + if (isDeployed) { + const factory = await hre.ethers.getContractFactory("Universal_SpokePool"); + const contract = factory.attach(proxyAddress); + + const owner = await contract.owner(); + if (owner !== EXPECTED_SAFE_ADDRESS) { + await (await contract.transferOwnership(EXPECTED_SAFE_ADDRESS)).wait(); + console.log("Transferred ownership to Expected Safe address:", await contract.owner()); + } else { + console.log("Expected Safe address is already the owner of the Universal SpokePool"); + } } else { - console.log("Expected Safe address is already the owner of the Universal SpokePool"); + console.log("Safe is not deployed, skipping ownership transfer"); } } }; diff --git a/deploy/consts.ts b/deploy/consts.ts index 92b50545d..36db826fe 100644 --- a/deploy/consts.ts +++ b/deploy/consts.ts @@ -23,7 +23,14 @@ export const ZK_L2_GAS_LIMIT = 2000000; export const ZK_MAX_GASPRICE = "10000000000000"; // 10k gwei // Expected Safe address for Universal SpokePool -export const EXPECTED_SAFE_ADDRESS = "0xB524735356985D2f267FA010D681f061DfF03715"; +export const EXPECTED_SAFE_ADDRESS = "0x0Fc8E2BB9bEd4FDb51a0d36f2415c4C7F9e75F6e"; +export const EXPECTED_SAFE_OWNERS = [ + "0x868CF19464e17F76D6419ACC802B122c22D2FD34", + "0xcc400c09ecBAC3e0033e4587BdFAABB26223e37d", + "0x837219D7a9C666F5542c4559Bf17D7B804E5c5fe", + "0x1d933Fd71FF07E69f066d50B39a7C34EB3b69F05", + "0x996267d7d1B7f5046543feDe2c2Db473Ed4f65e9", +]; export const L1_ADDRESS_MAP: { [key: number]: { [contractName: string]: string } } = { [CHAIN_IDs.MAINNET]: { diff --git a/scripts/deployMultisig.ts b/scripts/deployMultisig.ts index f1271eb04..af86981f6 100644 --- a/scripts/deployMultisig.ts +++ b/scripts/deployMultisig.ts @@ -1,26 +1,8 @@ import { getNodeUrl } from "../utils"; -import { ethers } from "../utils/utils"; +import { ethers, predictedSafe } from "../utils/utils"; import { hre } from "../utils/utils.hre"; -import Safe, { SafeAccountConfig, PredictedSafeProps } from "@safe-global/protocol-kit"; - -const safeAccountConfig: SafeAccountConfig = { - owners: [ - "0x868CF19464e17F76D6419ACC802B122c22D2FD34", - "0xcc400c09ecBAC3e0033e4587BdFAABB26223e37d", - "0x837219D7a9C666F5542c4559Bf17D7B804E5c5fe", - "0x1d933Fd71FF07E69f066d50B39a7C34EB3b69F05", - "0x996267d7d1B7f5046543feDe2c2Db473Ed4f65e9", - ], - threshold: 2, -}; -const EXPECTED_SAFE_ADDRESS = "0x0Fc8E2BB9bEd4FDb51a0d36f2415c4C7F9e75F6e"; -const predictedSafe: PredictedSafeProps = { - safeAccountConfig, - safeDeploymentConfig: { - // Safe addresses are deterministic based on owners and salt nonce. - saltNonce: "0x1234", - }, -}; +import { EXPECTED_SAFE_ADDRESS } from "../deploy/consts"; +import Safe from "@safe-global/protocol-kit"; /** * Script to deploy a new Safe Multisig contract via the Safe SDK. Run via: diff --git a/utils/utils.ts b/utils/utils.ts index 3a67e13f9..10e192409 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -8,8 +8,9 @@ import { smock, FakeContract } from "@defi-wonderland/smock"; import { FactoryOptions } from "hardhat/types"; import { ethers } from "hardhat"; import { BigNumber, Signer, Contract, ContractFactory, BaseContract } from "ethers"; -import { OFT_EIDs } from "../deploy/consts"; +import { EXPECTED_SAFE_OWNERS, OFT_EIDs } from "../deploy/consts"; export { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { SafeAccountConfig, PredictedSafeProps } from "@safe-global/protocol-kit"; chai.use(smock.matchers); @@ -218,3 +219,16 @@ export function getOftEid(chainId: number): number { } return value; } + +export const safeAccountConfig: SafeAccountConfig = { + owners: EXPECTED_SAFE_OWNERS, + threshold: 2, +}; + +export const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + // Safe addresses are deterministic based on owners and salt nonce. + saltNonce: "0x1234", + }, +}; From 9bd543b25e829cdd62c6b84acdb648cce2258742 Mon Sep 17 00:00:00 2001 From: Faisal Usmani Date: Wed, 8 Oct 2025 09:44:53 -0400 Subject: [PATCH 3/3] Refactor & error on no safe wallet Signed-off-by: Faisal Usmani --- deploy/111_deploy_universal_spokepool.ts | 31 +++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/deploy/111_deploy_universal_spokepool.ts b/deploy/111_deploy_universal_spokepool.ts index 4d4c55358..4f228e5f0 100644 --- a/deploy/111_deploy_universal_spokepool.ts +++ b/deploy/111_deploy_universal_spokepool.ts @@ -65,6 +65,10 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { // then a message designed to be sent to one chain could be sent to another's SpokePool. const { proxyAddress } = await deployNewProxy("Universal_SpokePool", constructorArgs, initArgs); + if (!proxyAddress) { + return; + } + const nodeUrl = getNodeUrl(spokeChainId); const protocolKit = await Safe.init({ @@ -76,21 +80,20 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { safeAddress: EXPECTED_SAFE_ADDRESS, }); const isDeployed = await existingProtocolKit.isSafeDeployed(); - if (proxyAddress) { - if (isDeployed) { - const factory = await hre.ethers.getContractFactory("Universal_SpokePool"); - const contract = factory.attach(proxyAddress); - const owner = await contract.owner(); - if (owner !== EXPECTED_SAFE_ADDRESS) { - await (await contract.transferOwnership(EXPECTED_SAFE_ADDRESS)).wait(); - console.log("Transferred ownership to Expected Safe address:", await contract.owner()); - } else { - console.log("Expected Safe address is already the owner of the Universal SpokePool"); - } - } else { - console.log("Safe is not deployed, skipping ownership transfer"); - } + if (!isDeployed) { + throw new Error("Expected Safe address is not deployed, please deploy it first"); + } + + const factory = await hre.ethers.getContractFactory("Universal_SpokePool"); + const contract = factory.attach(proxyAddress); + + const owner = await contract.owner(); + if (owner !== EXPECTED_SAFE_ADDRESS) { + await (await contract.transferOwnership(EXPECTED_SAFE_ADDRESS)).wait(); + console.log("Transferred ownership to Expected Safe address:", await contract.owner()); + } else { + console.log("Expected Safe address is already the owner of the Universal SpokePool"); } }; module.exports = func;