Skip to content

Commit 25c1ac2

Browse files
authored
[xc-admin] Contract management tool (#885)
* cleanup * blah * gr * stuff * hm * hmm * wtf * ah fix this * ok finally it does something * ok * hrm * hrm * blah * blah
1 parent e1377e5 commit 25c1ac2

File tree

13 files changed

+535
-5
lines changed

13 files changed

+535
-5
lines changed

governance/xc_admin/packages/xc_admin_cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
},
1717
"scripts": {
1818
"build": "tsc",
19-
"format": "prettier --write \"src/**/*.ts\""
19+
"format": "prettier --write \"src/**/*.ts\"",
20+
"cli": "npm run build && node lib/cli.js"
2021
},
2122
"dependencies": {
2223
"@coral-xyz/anchor": "^0.26.0",
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { program } from "commander";
2+
import { loadContractConfig, ContractType, SyncOp } from "xc_admin_common";
3+
import * as fs from "fs";
4+
5+
// TODO: extract this configuration to a file
6+
const contractsConfig = [
7+
{
8+
type: ContractType.EvmPythUpgradable,
9+
networkId: "arbitrum",
10+
address: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
11+
},
12+
{
13+
type: ContractType.EvmWormholeReceiver,
14+
networkId: "canto",
15+
address: "0x87047526937246727E4869C5f76A347160e08672",
16+
},
17+
{
18+
type: ContractType.EvmPythUpgradable,
19+
networkId: "canto",
20+
address: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
21+
},
22+
{
23+
type: ContractType.EvmPythUpgradable,
24+
networkId: "avalanche",
25+
address: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
26+
},
27+
];
28+
29+
const networksConfig = {
30+
evm: {
31+
optimism_goerli: {
32+
url: `https://rpc.ankr.com/optimism_testnet`,
33+
},
34+
arbitrum: {
35+
url: "https://arb1.arbitrum.io/rpc",
36+
},
37+
avalanche: {
38+
url: "https://api.avax.network/ext/bc/C/rpc",
39+
},
40+
canto: {
41+
url: "https://canto.gravitychain.io",
42+
},
43+
},
44+
};
45+
46+
// TODO: we will need configuration of this stuff to decide which multisig to run.
47+
const multisigs = [
48+
{
49+
name: "",
50+
wormholeNetwork: "mainnet",
51+
},
52+
];
53+
54+
program
55+
.name("pyth_governance")
56+
.description("CLI for governing Pyth contracts")
57+
.version("0.1.0");
58+
59+
program
60+
.command("get")
61+
.description("Find Pyth contracts matching the given search criteria")
62+
.option("-n, --network <network-id>", "Find contracts on the given network")
63+
.option("-a, --address <address>", "Find contracts with the given address")
64+
.option("-t, --type <type-id>", "Find contracts of the given type")
65+
.action(async (options: any) => {
66+
const contracts = loadContractConfig(contractsConfig, networksConfig);
67+
68+
console.log(JSON.stringify(options));
69+
70+
const matches = [];
71+
for (const contract of contracts) {
72+
if (
73+
(options.network === undefined ||
74+
contract.networkId == options.network) &&
75+
(options.address === undefined ||
76+
contract.getAddress() == options.address) &&
77+
(options.type === undefined || contract.type == options.type)
78+
) {
79+
matches.push(contract);
80+
}
81+
}
82+
83+
for (const contract of matches) {
84+
const state = await contract.getState();
85+
console.log({
86+
networkId: contract.networkId,
87+
address: contract.getAddress(),
88+
type: contract.type,
89+
state: state,
90+
});
91+
}
92+
});
93+
94+
class Cache {
95+
private path: string;
96+
97+
constructor(path: string) {
98+
this.path = path;
99+
}
100+
101+
private opFilePath(op: SyncOp): string {
102+
return `${this.path}/${op.id()}.json`;
103+
}
104+
105+
public readOpCache(op: SyncOp): Record<string, any> {
106+
const path = this.opFilePath(op);
107+
if (fs.existsSync(path)) {
108+
return JSON.parse(fs.readFileSync(path).toString("utf-8"));
109+
} else {
110+
return {};
111+
}
112+
}
113+
114+
public writeOpCache(op: SyncOp, cache: Record<string, any>) {
115+
fs.writeFileSync(this.opFilePath(op), JSON.stringify(cache));
116+
}
117+
118+
public deleteCache(op: SyncOp) {
119+
fs.rmSync(this.opFilePath(op));
120+
}
121+
}
122+
123+
program
124+
.command("set")
125+
.description("Set a configuration parameter for one or more Pyth contracts")
126+
.option("-n, --network <network-id>", "Find contracts on the given network")
127+
.option("-a, --address <address>", "Find contracts with the given address")
128+
.option("-t, --type <type-id>", "Find contracts of the given type")
129+
.argument("<fields...>", "Fields to set on the given contracts")
130+
.action(async (fields, options: any, command) => {
131+
const contracts = loadContractConfig(contractsConfig, networksConfig);
132+
133+
console.log(JSON.stringify(fields));
134+
console.log(JSON.stringify(options));
135+
136+
const setters = fields.map((value: string) => value.split("="));
137+
138+
const matches = [];
139+
for (const contract of contracts) {
140+
if (
141+
(options.network === undefined ||
142+
contract.networkId == options.network) &&
143+
(options.address === undefined ||
144+
contract.getAddress() == options.address) &&
145+
(options.type === undefined || contract.type == options.type)
146+
) {
147+
matches.push(contract);
148+
}
149+
}
150+
151+
const ops = [];
152+
for (const contract of matches) {
153+
const state = await contract.getState();
154+
// TODO: make a decent format for this
155+
for (const [field, value] of setters) {
156+
state[field] = value;
157+
}
158+
159+
ops.push(...(await contract.sync(state)));
160+
}
161+
162+
// TODO: extract constant
163+
const cacheDir = "cache";
164+
fs.mkdirSync(cacheDir, { recursive: true });
165+
const cache = new Cache(cacheDir);
166+
167+
for (const op of ops) {
168+
const opCache = cache.readOpCache(op);
169+
const isDone = await op.run(opCache);
170+
if (isDone) {
171+
cache.deleteCache(op);
172+
} else {
173+
cache.writeOpCache(op, opCache);
174+
}
175+
}
176+
});
177+
178+
program.parse();

governance/xc_admin/packages/xc_admin_common/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
"@certusone/wormhole-sdk": "^0.9.8",
2424
"@coral-xyz/anchor": "^0.26.0",
2525
"@pythnetwork/client": "^2.17.0",
26+
"@pythnetwork/pyth-sdk-solidity": "*",
27+
"@pythnetwork/xc-governance-sdk": "*",
2628
"@solana/buffer-layout": "^4.0.1",
2729
"@solana/web3.js": "^1.73.0",
2830
"@sqds/mesh": "^1.0.6",
31+
"ethers": "^5.7.2",
2932
"lodash": "^4.17.21",
3033
"typescript": "^4.9.4"
3134
},
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { ChainId, Instruction } from "@pythnetwork/xc-governance-sdk";
2+
import { ethers } from "ethers";
3+
4+
export enum ContractType {
5+
Oracle,
6+
EvmPythUpgradable,
7+
EvmWormholeReceiver,
8+
}
9+
10+
/**
11+
* A unique identifier for a blockchain. Note that we cannot use ChainId for this, as ChainId currently reuses
12+
* some ids across mainnet / testnet chains (e.g., ethereum goerli has the same id as ethereum mainnet).
13+
*/
14+
export type NetworkId = string;
15+
16+
/** A unique identifier for message senders across all wormhole networks. */
17+
export interface WormholeAddress {
18+
emitter: string;
19+
chainId: ChainId;
20+
// which network this sender is on
21+
network: WormholeNetwork;
22+
}
23+
export type WormholeNetwork = "mainnet" | "testnet";
24+
25+
/**
26+
* A Contract is the basic unit of on-chain state that is managed by xc_admin.
27+
* Each contracts lives at a specific address of a specific network, and has a type
28+
* representing which of several known contract types (evm target chain, wormhole receiver, etc)
29+
* that it is.
30+
*
31+
* Contracts further expose a state representing values that can be modified by governance.
32+
* The fields of the state object vary depending on what type of contract this is.
33+
* Finally, contracts expose a sync method that generates the needed operations to bring the on-chain state
34+
* in sync with a provided desired state.
35+
*/
36+
export interface Contract<State> {
37+
type: ContractType;
38+
networkId: NetworkId;
39+
/** The address of the contract. The address may be written in different formats for different networks. */
40+
getAddress(): string;
41+
42+
/** Get the on-chain state of all governance-controllable fields of this contract. */
43+
getState(): Promise<State>;
44+
45+
/** Generate a set of operations that, if executed, will update the on-chain contract state to be `target`. */
46+
sync(target: State): Promise<SyncOp[]>;
47+
}
48+
49+
/**
50+
* An idempotent synchronization operation to update on-chain state. The operation may depend on
51+
* external approvals or actions to complete, in which case the operation will pause and need to
52+
* be resumed later.
53+
*/
54+
export interface SyncOp {
55+
/**
56+
* A unique identifier for this operation. The id represents the content of the operation (e.g., "sets the X
57+
* field to Y on contract Z"), so can be used to identify the "same" operation across multiple runs of this program.
58+
*/
59+
id(): string;
60+
/**
61+
* Run this operation from a previous state (recorded in cache). The operation can modify cache
62+
* to record progress, then returns true if the operation has completed. If this function returns false,
63+
* it is waiting on an external operation to complete (e.g., a multisig transaction to be approved).
64+
* Re-run this function again once that operation is completed to continue making progress.
65+
*
66+
* The caller of this function is responsible for preserving the contents of `cache` between calls to
67+
* this function.
68+
*/
69+
run(cache: Record<string, any>): Promise<boolean>;
70+
}
71+
72+
export class SendGovernanceInstruction implements SyncOp {
73+
private instruction: Instruction;
74+
private sender: WormholeAddress;
75+
// function to submit the signed VAA to the target chain contract
76+
private submitVaa: (vaa: string) => Promise<boolean>;
77+
78+
constructor(
79+
instruction: Instruction,
80+
from: WormholeAddress,
81+
submitVaa: (vaa: string) => Promise<boolean>
82+
) {
83+
this.instruction = instruction;
84+
this.sender = from;
85+
this.submitVaa = submitVaa;
86+
}
87+
88+
public id(): string {
89+
// TODO: use a more understandable identifier (also this may not be unique)
90+
return ethers.utils.sha256(this.instruction.serialize());
91+
}
92+
93+
public async run(cache: Record<string, any>): Promise<boolean> {
94+
// FIXME: this implementation is temporary. replace with something like the commented out code below.
95+
if (cache["multisigTx"] === undefined) {
96+
cache["multisigTx"] = "fooooo";
97+
return false;
98+
}
99+
100+
if (cache["vaa"] === undefined) {
101+
return false;
102+
}
103+
104+
// VAA is guaranteed to be defined here
105+
const vaa = cache["vaa"];
106+
107+
// assertVaaPayloadEquals(vaa, payload);
108+
109+
return await this.submitVaa(vaa);
110+
}
111+
112+
/*
113+
public async run(cache: Record<string,any>): Promise<boolean> {
114+
if (cache["multisigTx"] === undefined) {
115+
// Have not yet submitted this operation to the multisig.
116+
const payload = this.instruction.serialize();
117+
const txKey = vault.sendWormholeInstruction(payload);
118+
cache["multisigTx"] = txKey;
119+
return false;
120+
}
121+
122+
if (cache["vaa"] === undefined) {
123+
const vaa = await executeMultisigTxAndGetVaa(txKey, payloadHex);
124+
if (vaa === undefined) {
125+
return false;
126+
}
127+
cache["vaa"] = vaa;
128+
}
129+
130+
// VAA is guaranteed to be defined here
131+
const vaa = cache["vaa"];
132+
133+
assertVaaPayloadEquals(vaa, payload);
134+
135+
// await proxy.executeGovernanceInstruction("0x" + vaa);
136+
await submitVaa(vaa);
137+
}
138+
*/
139+
}

0 commit comments

Comments
 (0)