Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(contract_manager): script for EVM feeds contract deployment #1254

Merged
merged 2 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contract_manager/scripts/deploy_evm_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { toPrivateKey } from "../src";
const parser = yargs(hideBin(process.argv))
.scriptName("deploy_evm_contract.ts")
.usage(
"Usage: $0 --code <path/to/std-output.json> --private-key <private-key> --chain <chain> [--deploy-args <arg> [... <args>]]"
"Usage: $0 --std-output <path/to/std-output.json> --private-key <private-key> --chain <chain> [--deploy-args <arg> [... <args>]]"
)
.options({
"std-output": {
Expand Down
299 changes: 299 additions & 0 deletions contract_manager/scripts/deploy_evm_pricefeed_contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { EvmChain } from "../src/chains";
import { DefaultStore } from "../src/store";
import { existsSync, readFileSync, writeFileSync } from "fs";
import {
DeploymentType,
EvmPriceFeedContract,
getDefaultDeploymentConfig,
PrivateKey,
toDeploymentType,
toPrivateKey,
WormholeEvmContract,
} from "../src";
import { join } from "path";
import Web3 from "web3";
import { Contract } from "web3-eth-contract";

type DeploymentConfig = {
type: DeploymentType;
validTimePeriodSeconds: number;
singleUpdateFeeInWei: number;
gasMultiplier: number;
gasPriceMultiplier: number;
privateKey: PrivateKey;
jsonOutputDir: string;
saveContract: boolean;
};

const CACHE_FILE = ".cache-deploy-evm";

const parser = yargs(hideBin(process.argv))
.scriptName("deploy_evm_pricefeed_contracts.ts")
.usage(
"Usage: $0 --std-output-dir <path/to/std-output-dir/> --private-key <private-key> --chain <chain0> --chain <chain1>"
)
.options({
"std-output-dir": {
type: "string",
demandOption: true,
desc: "Path to the standard JSON output of the contracts (build artifact) directory",
},
"private-key": {
type: "string",
demandOption: true,
desc: "Private key to use for the deployment",
},
chain: {
type: "array",
demandOption: true,
desc: "Chain to upload the contract on. Can be one of the evm chains available in the store",
},
"deployment-type": {
type: "string",
demandOption: false,
default: "stable",
desc: "Deployment type to use. Can be 'stable' or 'beta'",
},
"valid-time-period-seconds": {
type: "number",
demandOption: false,
default: 60,
desc: "Valid time period in seconds for the price feed staleness",
},
"single-update-fee-in-wei": {
type: "number",
demandOption: false,
default: 1,
desc: "Single update fee in wei for the price feed",
},
"gas-multiplier": {
type: "number",
demandOption: false,
// Pyth Proxy (ERC1967) gas estimate is insufficient in many networks and thus we use 2 by default to make it work.
default: 2,
desc: "Gas multiplier to use for the deployment. This is useful when gas estimates are not accurate",
},
"gas-price-multiplier": {
type: "number",
demandOption: false,
default: 1,
desc: "Gas price multiplier to use for the deployment. This is useful when gas price estimates are not accurate",
},
"save-contract": {
type: "boolean",
demandOption: false,
default: true,
desc: "Save the contract to the store",
},
});

async function deployIfNotCached(
chain: EvmChain,
config: DeploymentConfig,
artifactName: string,
deployArgs: any[] // eslint-disable-line @typescript-eslint/no-explicit-any
): Promise<string> {
const cache = existsSync(CACHE_FILE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use runIfNotCached here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's a good idea. Let me do it in a separate PR (because this one is already very big)

? JSON.parse(readFileSync(CACHE_FILE, "utf8"))
: {};

const cacheKey = `${chain.getId()}-${artifactName}`;
if (cache[cacheKey]) {
const address = cache[cacheKey];
console.log(
`Using cached deployment of ${artifactName} on ${chain.getId()} at ${address}`
);
return address;
}

const artifact = JSON.parse(
readFileSync(join(config.jsonOutputDir, `${artifactName}.json`), "utf8")
);

console.log(`Deploying ${artifactName} on ${chain.getId()}...`);

const addr = await chain.deploy(
config.privateKey,
artifact["abi"],
artifact["bytecode"],
deployArgs,
config.gasMultiplier,
config.gasPriceMultiplier
);

console.log(`✅ Deployed ${artifactName} on ${chain.getId()} at ${addr}`);

cache[cacheKey] = addr;
writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
return addr;
}

function getWeb3Contract(
config: DeploymentConfig,
artifactName: string,
address: string
): Contract {
const artifact = JSON.parse(
readFileSync(join(config.jsonOutputDir, `${artifactName}.json`), "utf8")
);
const web3 = new Web3();
return new web3.eth.Contract(artifact["abi"], address);
}

async function deployWormholeReceiverContracts(
chain: EvmChain,
config: DeploymentConfig
): Promise<string> {
const receiverSetupAddr = await deployIfNotCached(
chain,
config,
"ReceiverSetup",
[]
);

const receiverImplAddr = await deployIfNotCached(
chain,
config,
"ReceiverImplementation",
[]
);

// Craft the init data for the proxy contract
const setupContract = getWeb3Contract(
config,
"ReceiverSetup",
receiverSetupAddr
);

const { wormholeConfig } = getDefaultDeploymentConfig(config.type);

const initData = setupContract.methods
.setup(
receiverImplAddr,
wormholeConfig.initialGuardianSet.map((addr: string) => "0x" + addr),
chain.getWormholeChainId(),
wormholeConfig.governanceChainId,
"0x" + wormholeConfig.governanceContract
)
.encodeABI();

const wormholeReceiverAddr = await deployIfNotCached(
chain,
config,
"WormholeReceiver",
[receiverSetupAddr, initData]
);

const wormholeEvmContract = new WormholeEvmContract(
chain,
wormholeReceiverAddr
);

if (config.type === "stable") {
console.log(`Syncing mainnet guardian sets for ${chain.getId()}...`);
// TODO: Add a way to pass gas configs to this
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a generic method I can't easily add the gas options to it. Something to refactor later in the future.

await wormholeEvmContract.syncMainnetGuardianSets(config.privateKey);
console.log(`✅ Synced mainnet guardian sets for ${chain.getId()}`);
}

return wormholeReceiverAddr;
}

async function deployPriceFeedContracts(
chain: EvmChain,
config: DeploymentConfig,
wormholeAddr: string
): Promise<string> {
const pythImplAddr = await deployIfNotCached(
chain,
config,
"PythUpgradable",
[]
);

// Craft the init data for the proxy contract
const { dataSources, governanceDataSource } = getDefaultDeploymentConfig(
config.type
);

const pythImplContract = getWeb3Contract(
config,
"PythUpgradable",
pythImplAddr
);

const pythInitData = pythImplContract.methods
.initialize(
wormholeAddr,
dataSources.map((ds) => ds.emitterChain),
dataSources.map((ds) => "0x" + ds.emitterAddress),
governanceDataSource.emitterChain,
"0x" + governanceDataSource.emitterAddress,
0, // governanceInitialSequence
config.validTimePeriodSeconds,
config.singleUpdateFeeInWei
)
.encodeABI();

return await deployIfNotCached(chain, config, "ERC1967Proxy", [
pythImplAddr,
pythInitData,
]);
}

async function main() {
const argv = await parser.argv;

const deploymentConfig: DeploymentConfig = {
type: toDeploymentType(argv.deploymentType),
validTimePeriodSeconds: argv.validTimePeriodSeconds,
singleUpdateFeeInWei: argv.singleUpdateFeeInWei,
gasMultiplier: argv.gasMultiplier,
gasPriceMultiplier: argv.gasPriceMultiplier,
privateKey: toPrivateKey(argv.privateKey),
jsonOutputDir: argv.stdOutputDir,
saveContract: argv.saveContract,
};

console.log(
`Deployment config: ${JSON.stringify(deploymentConfig, null, 2)}\n`
);

const chainNames = argv.chain;

for (const chainName of chainNames) {
const chain = DefaultStore.chains[chainName];
if (!chain) {
throw new Error(`Chain ${chain} not found`);
} else if (!(chain instanceof EvmChain)) {
throw new Error(`Chain ${chain} is not an EVM chain`);
}

console.log(`Deploying price feed contracts on ${chain.getId()}...`);

const wormholeAddr = await deployWormholeReceiverContracts(
chain,
deploymentConfig
);
const priceFeedAddr = await deployPriceFeedContracts(
chain,
deploymentConfig,
wormholeAddr
);

if (deploymentConfig.saveContract) {
console.log("Saving the contract in the store...");
const contract = new EvmPriceFeedContract(chain, priceFeedAddr);
DefaultStore.contracts[contract.getId()] = contract;
DefaultStore.saveAllContracts();
}

console.log(
`✅ Deployed price feed contracts on ${chain.getId()} at ${priceFeedAddr}\n\n`
);
}
}

main();
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ const parser = yargs(hideBin(process.argv))
},
});

async function run_if_not_cached(
cache_key: string,
async function runIfNotCached(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

format the naming style

cacheKey: string,
fn: () => Promise<string>
): Promise<string> {
const cache = existsSync(CACHE_FILE)
? JSON.parse(readFileSync(CACHE_FILE, "utf8"))
: {};
if (cache[cache_key]) {
return cache[cache_key];
if (cache[cacheKey]) {
return cache[cacheKey];
}
const result = await fn();
cache[cache_key] = result;
cache[cacheKey] = result;
writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
return result;
}
Expand Down Expand Up @@ -102,7 +102,7 @@ async function main() {
for (const chain of selectedChains) {
const artifact = JSON.parse(readFileSync(argv["std-output"], "utf8"));
console.log("Deploying contract to", chain.getId());
const address = await run_if_not_cached(`deploy-${chain.getId()}`, () => {
const address = await runIfNotCached(`deploy-${chain.getId()}`, () => {
return chain.deploy(
toPrivateKey(argv["private-key"]),
artifact["abi"],
Expand Down
7 changes: 7 additions & 0 deletions contract_manager/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ export interface TxResult {

export type DeploymentType = "stable" | "beta";
export type PrivateKey = string & { __type: "PrivateKey" };

function checkIsPrivateKey(key: string): asserts key is PrivateKey {
if (Buffer.from(key, "hex").length !== 32)
throw new Error("Invalid private key, must be 64 hex chars");
}

export function toPrivateKey(key: string): PrivateKey {
checkIsPrivateKey(key);
return key;
}

export function toDeploymentType(type: string): DeploymentType {
if (type === "stable" || type === "beta") return type;
throw new Error(`Invalid deployment type ${type}`);
}

export type KeyValueConfig = Record<string, string | number | boolean>;

export abstract class Storable {
Expand Down
10 changes: 6 additions & 4 deletions contract_manager/src/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,15 +403,17 @@ export class EvmChain extends Chain {
privateKey: PrivateKey,
abi: any, // eslint-disable-line @typescript-eslint/no-explicit-any
bytecode: string,
deployArgs: any[] // eslint-disable-line @typescript-eslint/no-explicit-any
deployArgs: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
gasMultiplier = 1,
gasPriceMultiplier = 1
): Promise<string> {
const web3 = new Web3(this.getRpcUrl());
const signer = web3.eth.accounts.privateKeyToAccount(privateKey);
web3.eth.accounts.wallet.add(signer);
const contract = new web3.eth.Contract(abi);
const deployTx = contract.deploy({ data: bytecode, arguments: deployArgs });
const gas = await deployTx.estimateGas();
const gasPrice = await this.getGasPrice();
const gas = (await deployTx.estimateGas()) * gasMultiplier;
const gasPrice = Number(await this.getGasPrice()) * gasPriceMultiplier;
const deployerBalance = await web3.eth.getBalance(signer.address);
const gasDiff = BigInt(gas) * BigInt(gasPrice) - BigInt(deployerBalance);
if (gasDiff > 0n) {
Expand All @@ -427,7 +429,7 @@ export class EvmChain extends Chain {
const deployedContract = await deployTx.send({
from: signer.address,
gas,
gasPrice,
gasPrice: gasPrice.toString(),
});
return deployedContract.options.address;
}
Expand Down
Loading
Loading