Skip to content

Commit 084f201

Browse files
authored
[contract-manager] More utility scripts and docs (#1222)
* Remove mainnet condition in check proposal Testnet contracts can also listen to mainnet governance so this check does not make sense anymore * Simple script for executing vaas * Add script for evm upgrades * Script to list evm contracts with their versions * More docs
1 parent cee5da9 commit 084f201

File tree

11 files changed

+515
-11
lines changed

11 files changed

+515
-11
lines changed

contract_manager/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
lib/
2+
.cache*
3+
docs

contract_manager/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,21 @@ It has the following structure:
77
- `store` contains all the necessary information for registered chains and deployed contracts
88
- `scripts` contains utility scripts to interact with the contract manager and accomplish common tasks
99
- `src` contains the contract manager code
10+
11+
# Main Entities
12+
13+
Contract Manager has base classes which you can use to interact with the following entities:
14+
15+
- Chain
16+
- PythContract
17+
- WormholeContract
18+
19+
Each of these entities has a specialized class for each supported chain (EVM/Cosmos/Aptos/Sui).
20+
21+
# Docs
22+
23+
You can generate the docs by running `npx typedoc src/index.ts` from this directory. Open the docs by opening `docs/index.html` in your browser.
24+
25+
# Scripts
26+
27+
You can run the scripts by executing `npx ts-node scripts/<script_name>.ts` from this directory.

contract_manager/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,19 @@
2020
"url": "git+https://github.com/pyth-network/pyth-crosschain.git"
2121
},
2222
"dependencies": {
23-
"@mysten/sui.js": "^0.37.1",
2423
"@certusone/wormhole-sdk": "^0.9.8",
24+
"@injectivelabs/networks": "1.0.68",
25+
"@mysten/sui.js": "^0.37.1",
2526
"@pythnetwork/cosmwasm-deploy-tools": "*",
2627
"@pythnetwork/price-service-client": "*",
2728
"@pythnetwork/pyth-sui-js": "*",
28-
"@injectivelabs/networks": "1.0.68",
2929
"aptos": "^1.5.0",
3030
"bs58": "^5.0.0",
3131
"ts-node": "^10.9.1",
3232
"typescript": "^4.9.3"
3333
},
3434
"devDependencies": {
35-
"prettier": "^2.6.2"
35+
"prettier": "^2.6.2",
36+
"typedoc": "^0.25.7"
3637
}
3738
}

contract_manager/scripts/check_proposal.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ async function main() {
6363
for (const chain of Object.values(DefaultStore.chains)) {
6464
if (
6565
chain instanceof EvmChain &&
66-
chain.isMainnet() === (cluster === "mainnet-beta") &&
6766
chain.wormholeChainName ===
6867
instruction.governanceAction.targetChainId
6968
) {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import yargs from "yargs";
2+
import { hideBin } from "yargs/helpers";
3+
import { DefaultStore } from "../src/store";
4+
import { SubmittedWormholeMessage, Vault } from "../src/governance";
5+
import { parseVaa } from "@certusone/wormhole-sdk";
6+
import { decodeGovernancePayload } from "xc_admin_common";
7+
import { executeVaa } from "../src/executor";
8+
import { toPrivateKey } from "../src";
9+
10+
const parser = yargs(hideBin(process.argv))
11+
.usage(
12+
"Tries to execute all vaas on a vault.\n" +
13+
"Useful for batch upgrades.\n" +
14+
"Usage: $0 --vault <mainnet|devnet> --private-key <private-key> --offset <offset> [--dryrun]"
15+
)
16+
.options({
17+
vault: {
18+
type: "string",
19+
default: "mainnet",
20+
choices: ["mainnet", "devnet"],
21+
desc: "Which vault to use for fetching VAAs",
22+
},
23+
"private-key": {
24+
type: "string",
25+
demandOption: true,
26+
desc: "Private key to sign the transactions executing the governance VAAs. Hex format, without 0x prefix.",
27+
},
28+
offset: {
29+
type: "number",
30+
demandOption: true,
31+
desc: "Offset to use from the last executed sequence number",
32+
},
33+
dryrun: {
34+
type: "boolean",
35+
default: false,
36+
desc: "Whether to execute the VAAs or just print them",
37+
},
38+
});
39+
40+
async function main() {
41+
const argv = await parser.argv;
42+
let vault: Vault;
43+
if (argv.vault === "mainnet") {
44+
vault =
45+
DefaultStore.vaults[
46+
"mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj"
47+
];
48+
} else {
49+
vault =
50+
DefaultStore.vaults[
51+
"devnet_6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3"
52+
];
53+
}
54+
console.log("Executing VAAs for vault", vault.getId());
55+
console.log(
56+
"Executing VAAs for emitter",
57+
(await vault.getEmitter()).toBase58()
58+
);
59+
const lastSequenceNumber = await vault.getLastSequenceNumber();
60+
const startSequenceNumber = lastSequenceNumber - argv.offset;
61+
console.log(
62+
`Going from sequence number ${startSequenceNumber} to ${lastSequenceNumber}`
63+
);
64+
for (
65+
let seqNumber = startSequenceNumber;
66+
seqNumber <= lastSequenceNumber;
67+
seqNumber++
68+
) {
69+
const submittedWormholeMessage = new SubmittedWormholeMessage(
70+
await vault.getEmitter(),
71+
seqNumber,
72+
vault.cluster
73+
);
74+
const vaa = await submittedWormholeMessage.fetchVaa();
75+
const decodedAction = decodeGovernancePayload(parseVaa(vaa).payload);
76+
if (!decodedAction) {
77+
console.log("Skipping unknown action for vaa ", seqNumber);
78+
continue;
79+
}
80+
console.log("Executing vaa", seqNumber);
81+
console.log(decodedAction);
82+
if (!argv.dryrun) {
83+
await executeVaa(toPrivateKey(argv["private-key"]), vaa);
84+
}
85+
}
86+
}
87+
88+
main();
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import yargs from "yargs";
2+
import { hideBin } from "yargs/helpers";
3+
import {
4+
AptosContract,
5+
CosmWasmContract,
6+
DefaultStore,
7+
EvmContract,
8+
} from "../src";
9+
10+
const parser = yargs(hideBin(process.argv))
11+
.usage("Usage: $0")
12+
.options({
13+
testnet: {
14+
type: "boolean",
15+
default: false,
16+
desc: "Fetch testnet contract fees instead of mainnet",
17+
},
18+
});
19+
20+
async function main() {
21+
const argv = await parser.argv;
22+
const entries = [];
23+
for (const contract of Object.values(DefaultStore.contracts)) {
24+
if (contract.getChain().isMainnet() === argv.testnet) continue;
25+
if (contract instanceof EvmContract) {
26+
try {
27+
const version = await contract.getVersion();
28+
entries.push({
29+
chain: contract.getChain().getId(),
30+
contract: contract.address,
31+
version: version,
32+
});
33+
console.log(`Fetched version for ${contract.getId()}`);
34+
} catch (e) {
35+
console.error(`Error fetching version for ${contract.getId()}`, e);
36+
}
37+
}
38+
}
39+
console.table(entries);
40+
}
41+
42+
main();
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import yargs from "yargs";
2+
import { hideBin } from "yargs/helpers";
3+
import { DefaultStore, EvmChain, loadHotWallet, toPrivateKey } from "../src";
4+
import { existsSync, readFileSync, writeFileSync } from "fs";
5+
6+
const CACHE_FILE = ".cache-upgrade-evm";
7+
8+
const parser = yargs(hideBin(process.argv))
9+
.usage(
10+
"Deploys a new PythUpgradable contract to a set of chains and creates a governance proposal for it.\n" +
11+
`Uses a cache file (${CACHE_FILE}) to avoid deploying contracts twice\n` +
12+
"Usage: $0 --chain <chain_1> --chain <chain_2> --private-key <private_key> --ops-key-path <ops_key_path> --std-output <std_output>"
13+
)
14+
.options({
15+
testnet: {
16+
type: "boolean",
17+
default: false,
18+
desc: "Upgrade testnet contracts instead of mainnet",
19+
},
20+
"all-chains": {
21+
type: "boolean",
22+
default: false,
23+
desc: "Upgrade the contract on all chains. Use with --testnet flag to upgrade all testnet contracts",
24+
},
25+
chain: {
26+
type: "array",
27+
string: true,
28+
desc: "Chains to upgrade the contract on",
29+
},
30+
"private-key": {
31+
type: "string",
32+
demandOption: true,
33+
desc: "Private key to use for the deployment",
34+
},
35+
"ops-key-path": {
36+
type: "string",
37+
demandOption: true,
38+
desc: "Path to the private key of the proposer to use for the operations multisig governance proposal",
39+
},
40+
"std-output": {
41+
type: "string",
42+
demandOption: true,
43+
desc: "Path to the standard JSON output of the pyth contract (build artifact)",
44+
},
45+
});
46+
47+
async function run_if_not_cached(
48+
cache_key: string,
49+
fn: () => Promise<string>
50+
): Promise<string> {
51+
const cache = existsSync(CACHE_FILE)
52+
? JSON.parse(readFileSync(CACHE_FILE, "utf8"))
53+
: {};
54+
if (cache[cache_key]) {
55+
return cache[cache_key];
56+
}
57+
const result = await fn();
58+
cache[cache_key] = result;
59+
writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
60+
return result;
61+
}
62+
63+
async function main() {
64+
const argv = await parser.argv;
65+
const selectedChains: EvmChain[] = [];
66+
67+
if (argv.allChains && argv.chain)
68+
throw new Error("Cannot use both --all-chains and --chain");
69+
if (!argv.allChains && !argv.chain)
70+
throw new Error("Must use either --all-chains or --chain");
71+
for (const chain of Object.values(DefaultStore.chains)) {
72+
if (!(chain instanceof EvmChain)) continue;
73+
if (
74+
(argv.allChains && chain.isMainnet() !== argv.testnet) ||
75+
argv.chain?.includes(chain.getId())
76+
)
77+
selectedChains.push(chain);
78+
}
79+
if (argv.chain && selectedChains.length !== argv.chain.length)
80+
throw new Error(
81+
`Some chains were not found ${selectedChains
82+
.map((chain) => chain.getId())
83+
.toString()}`
84+
);
85+
for (const chain of selectedChains) {
86+
if (chain.isMainnet() != selectedChains[0].isMainnet())
87+
throw new Error("All chains must be either mainnet or testnet");
88+
}
89+
90+
const vault =
91+
DefaultStore.vaults[
92+
"mainnet-beta_FVQyHcooAtThJ83XFrNnv74BcinbRH3bRmfFamAHBfuj"
93+
];
94+
95+
console.log("Using cache file", CACHE_FILE);
96+
console.log(
97+
"Upgrading on chains",
98+
selectedChains.map((c) => c.getId())
99+
);
100+
101+
const payloads: Buffer[] = [];
102+
for (const chain of selectedChains) {
103+
const artifact = JSON.parse(readFileSync(argv["std-output"], "utf8"));
104+
console.log("Deploying contract to", chain.getId());
105+
const address = await run_if_not_cached(`deploy-${chain.getId()}`, () => {
106+
return chain.deploy(
107+
toPrivateKey(argv["private-key"]),
108+
artifact["abi"],
109+
artifact["bytecode"],
110+
[]
111+
);
112+
});
113+
console.log(`Deployed contract at ${address} on ${chain.getId()}`);
114+
payloads.push(
115+
chain.generateGovernanceUpgradePayload(address.replace("0x", ""))
116+
);
117+
}
118+
119+
console.log("Using vault at for proposal", vault.getId());
120+
const wallet = await loadHotWallet(argv["ops-key-path"]);
121+
console.log("Using wallet ", wallet.publicKey.toBase58());
122+
await vault.connect(wallet);
123+
const proposal = await vault.proposeWormholeMessage(payloads);
124+
console.log("Proposal address", proposal.address.toBase58());
125+
}
126+
127+
main();

contract_manager/src/executor.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,16 @@ export async function executeVaa(senderPrivateKey: PrivateKey, vaa: Buffer) {
2323
parsedVaa.emitterAddress.toString("hex") &&
2424
governanceSource.emitterChain === parsedVaa.emitterChain
2525
) {
26-
// TODO: check governance sequence number as well
26+
const lastExecutedSequence =
27+
await contract.getLastExecutedGovernanceSequence();
28+
if (lastExecutedSequence >= parsedVaa.sequence) {
29+
console.log(
30+
`Skipping on contract ${contract.getId()} as it was already executed`
31+
);
32+
continue;
33+
}
2734
await contract.executeGovernanceInstruction(senderPrivateKey, vaa);
35+
console.log(`Executed on contract ${contract.getId()}`);
2836
}
2937
}
3038
}

0 commit comments

Comments
 (0)