Skip to content

Commit a447e01

Browse files
authored
feat: Add deploy Safe script (#1120)
Required to have safe deployed when using the UniversalSpokePool which is an ownable contract. Note that this script will deploy the safe at a new address than the existing safe's because the salt nonce is set differently from whatever the Safe UI uses.
1 parent 56064c8 commit a447e01

File tree

6 files changed

+279
-20
lines changed

6 files changed

+279
-20
lines changed

hardhat.config.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as dotenv from "dotenv";
22
dotenv.config();
33
import { HardhatUserConfig } from "hardhat/config";
4-
import { CHAIN_IDs, PUBLIC_NETWORKS } from "./utils/constants";
4+
import { CHAIN_IDs } from "./utils/constants";
5+
import { getNodeUrl } from "./utils";
56

67
import "@nomicfoundation/hardhat-verify"; // Must be above hardhat-upgrades
78
import "@nomiclabs/hardhat-waffle";
@@ -14,17 +15,6 @@ import "solidity-coverage";
1415
import "hardhat-deploy";
1516
import "@openzeppelin/hardhat-upgrades";
1617

17-
const getNodeUrl = (chainId: number): string => {
18-
let url = process.env[`NODE_URL_${chainId}`] ?? process.env.CUSTOM_NODE_URL;
19-
if (url === undefined) {
20-
// eslint-disable-next-line no-console
21-
console.log(`No configured RPC provider for chain ${chainId}, reverting to public RPC.`);
22-
url = PUBLIC_NETWORKS[chainId].publicRPC;
23-
}
24-
25-
return url;
26-
};
27-
2818
const getMnemonic = () => {
2919
// Publicly-disclosed mnemonic. This is required for hre deployments in test.
3020
const PUBLIC_MNEMONIC = "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat";

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"@openzeppelin/contracts": "4.9.6",
6363
"@openzeppelin/contracts-upgradeable": "4.9.6",
6464
"@openzeppelin/foundry-upgrades": "^0.4.0",
65+
"@safe-global/protocol-kit": "^6.1.1",
6566
"@scroll-tech/contracts": "^0.1.0",
6667
"@solana-developers/helpers": "^2.4.0",
6768
"@solana-program/address-lookup-table": "^0.7.0",

scripts/deployMultisig.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { getNodeUrl } from "../utils";
2+
import { ethers } from "../utils/utils";
3+
import { hre } from "../utils/utils.hre";
4+
import Safe, { SafeAccountConfig, PredictedSafeProps } from "@safe-global/protocol-kit";
5+
6+
const safeAccountConfig: SafeAccountConfig = {
7+
owners: [
8+
"0x868CF19464e17F76D6419ACC802B122c22D2FD34",
9+
"0xcc400c09ecBAC3e0033e4587BdFAABB26223e37d",
10+
"0x837219D7a9C666F5542c4559Bf17D7B804E5c5fe",
11+
"0x1d933Fd71FF07E69f066d50B39a7C34EB3b69F05",
12+
"0x996267d7d1B7f5046543feDe2c2Db473Ed4f65e9",
13+
],
14+
threshold: 2,
15+
};
16+
const EXPECTED_SAFE_ADDRESS = "0x0Fc8E2BB9bEd4FDb51a0d36f2415c4C7F9e75F6e";
17+
const predictedSafe: PredictedSafeProps = {
18+
safeAccountConfig,
19+
safeDeploymentConfig: {
20+
// Safe addresses are deterministic based on owners and salt nonce.
21+
saltNonce: "0x1234",
22+
},
23+
};
24+
25+
/**
26+
* Script to deploy a new Safe Multisig contract via the Safe SDK. Run via:
27+
* ```
28+
* yarn hardhat run ./scripts/deployMultisig.ts \
29+
* --network hyperevm \
30+
* ```
31+
*/
32+
async function main() {
33+
const chainId = parseInt(await hre.getChainId());
34+
const nodeUrl = getNodeUrl(chainId);
35+
const wallet = ethers.Wallet.fromMnemonic((hre.network.config.accounts as any).mnemonic);
36+
const privateKey = wallet._signingKey().privateKey;
37+
console.log(`Connected to node ${nodeUrl} for chain ${chainId}`);
38+
const signer = wallet.connect(new ethers.providers.JsonRpcProvider(nodeUrl));
39+
40+
const protocolKit = await Safe.init({
41+
provider: nodeUrl,
42+
signer: privateKey,
43+
predictedSafe,
44+
});
45+
46+
// Check if the safe already exists:
47+
const existingProtocolKit = await protocolKit.connect({
48+
safeAddress: EXPECTED_SAFE_ADDRESS,
49+
});
50+
const isDeployed = await existingProtocolKit.isSafeDeployed();
51+
if (isDeployed) {
52+
const safeAddress = await existingProtocolKit.getAddress();
53+
const safeOwners = await existingProtocolKit.getOwners();
54+
const safeThreshold = await existingProtocolKit.getThreshold();
55+
console.log(`Safe already exists at ${EXPECTED_SAFE_ADDRESS}:`, {
56+
safeAddress,
57+
safeOwners,
58+
safeThreshold,
59+
});
60+
return;
61+
}
62+
63+
// Deploy a new safe:
64+
const safeAddress = await protocolKit.getAddress();
65+
if (safeAddress !== EXPECTED_SAFE_ADDRESS) {
66+
throw new Error(`Safe address ${safeAddress} does not match expected ${EXPECTED_SAFE_ADDRESS}`);
67+
}
68+
console.log(`Deploying a new safe with determinstic address: ${safeAddress}`);
69+
const deploymentTransaction = await protocolKit.createSafeDeploymentTransaction();
70+
console.log(`Deployment txn data`, deploymentTransaction);
71+
const client = await protocolKit.getSafeProvider().getExternalSigner();
72+
if (!client) {
73+
throw new Error("Unable to get external signer from safe provider");
74+
}
75+
const deployerAccount = client.account.address;
76+
console.log(`Deployer account: ${deployerAccount}`);
77+
const clientConnectedChain = client.chain;
78+
if (client.chain?.id !== chainId) {
79+
throw new Error(`Client connected to chain ${clientConnectedChain?.id}, but expected ${chainId}`);
80+
}
81+
if (deploymentTransaction.value.toString() !== "0") {
82+
throw new Error(`Deployment transaction value should be 0, but is ${deploymentTransaction.value}`);
83+
}
84+
console.log(`Sending deployment transaction...`);
85+
const txnHash = await signer.sendTransaction({
86+
to: deploymentTransaction.to,
87+
value: 0,
88+
data: deploymentTransaction.data,
89+
});
90+
const txnReceipt = await txnHash.wait();
91+
console.log(`Success! Deployment transaction receipt:`, txnReceipt);
92+
}
93+
94+
main().catch((error) => {
95+
console.error(error);
96+
process.exitCode = 1;
97+
});

utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./MerkleTree";
22
export * from "./constants";
3+
export * from "./network";

utils/network.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { PUBLIC_NETWORKS } from "./constants";
2+
3+
export function getNodeUrl(chainId: number): string {
4+
let url = process.env[`NODE_URL_${chainId}`] ?? process.env.CUSTOM_NODE_URL;
5+
if (url === undefined) {
6+
// eslint-disable-next-line no-console
7+
console.log(`No configured RPC provider for chain ${chainId}, reverting to public RPC.`);
8+
url = PUBLIC_NETWORKS[chainId].publicRPC;
9+
}
10+
11+
return url;
12+
}

0 commit comments

Comments
 (0)