-
Notifications
You must be signed in to change notification settings - Fork 308
[xc-admin] Contract management tool #885
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
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Ignored Deployment
|
| externalDir: true, | ||
| }, | ||
| webpack(config) { | ||
| config.experiments = { asyncWebAssembly: true } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here and below is fixing some weird build issues from pulling the xc-governance-sdk into xc-admin & the frontend components.
| @@ -0,0 +1,139 @@ | |||
| import { ChainId, Instruction } from "@pythnetwork/xc-governance-sdk"; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you see any problem in using PythGovernanceAction instead? Instruction from @pythnetwork/xc-governance-sdk. Goal is enumerating all the available governance actions in one place and using the same class for deserializing them in the proposal page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall it makes sense to me. It seems an amazing improvement for setting up the config of many contracts on many chains at once.
I guess my question is whether an evm relayer for pyth governance messages is out of the picture, but I guess we can always build the relayer later and make it compatible with this code.
| }; | ||
|
|
||
| // TODO: we will need configuration of this stuff to decide which multisig to run. | ||
| const multisigs = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * A unique identifier for a blockchain. Note that we cannot use ChainId for this, as ChainId currently reuses | ||
| * some ids across mainnet / testnet chains (e.g., ethereum goerli has the same id as ethereum mainnet). | ||
| */ | ||
| export type NetworkId = string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cartesian product of wormhole network + chainid?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could use that tuple, but it's going to get confusing, especially as we start rolling out stable/edge configurations to each network. In that case ethereum goerli stable will be [ethereum_goerli, mainnet] and ethereum goerli edge will be [ethereum, testnet]. I think it's better to make a clean break here and define the right concept.
| "build": "tsc", | ||
| "format": "prettier --write \"src/**/*.ts\"" | ||
| "format": "prettier --write \"src/**/*.ts\"", | ||
| "cli": "npm run build && node lib/cli.js" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This command is a bit brittle as the build is not stanalone in the monorepo. Can you either run build using npx lerna run build --scope="xc_admin_cli" --include-dependencies && ..
| } | ||
|
|
||
| public async submitGovernanceVaa(vaa: string): Promise<boolean> { | ||
| // FIXME: also needs the full PythUpgradable ABI |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You will just need to single abi to executeGovernanceInstruction. You can manually write that down and it's fine. I think full abi is not needed at all.
| const ethersContract = new ethers.Contract( | ||
| contractConfig.address, | ||
| PythAbi, | ||
| getEvmProvider(contractConfig.networkId, networksConfig) | ||
| ); | ||
|
|
||
| return new EvmPythUpgradable( | ||
| contractConfig.networkId, | ||
| contractConfig.address, | ||
| ethersContract | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's easier to understand (and less coupled) if this be a factory method in the contract file. And in general we automatically call the factory methods without having switch case at all.
| export class SendGovernanceInstruction implements SyncOp { | ||
| private instruction: Instruction; | ||
| private sender: WormholeAddress; | ||
| // function to submit the signed VAA to the target chain contract | ||
| private submitVaa: (vaa: string) => Promise<boolean>; | ||
|
|
||
| constructor( | ||
| instruction: Instruction, | ||
| from: WormholeAddress, | ||
| submitVaa: (vaa: string) => Promise<boolean> | ||
| ) { | ||
| this.instruction = instruction; | ||
| this.sender = from; | ||
| this.submitVaa = submitVaa; | ||
| } | ||
|
|
||
| public id(): string { | ||
| // TODO: use a more understandable identifier (also this may not be unique) | ||
| return ethers.utils.sha256(this.instruction.serialize()); | ||
| } | ||
|
|
||
| public async run(cache: Record<string, any>): Promise<boolean> { | ||
| // FIXME: this implementation is temporary. replace with something like the commented out code below. | ||
| if (cache["multisigTx"] === undefined) { | ||
| cache["multisigTx"] = "fooooo"; | ||
| return false; | ||
| } | ||
|
|
||
| if (cache["vaa"] === undefined) { | ||
| return false; | ||
| } | ||
|
|
||
| // VAA is guaranteed to be defined here | ||
| const vaa = cache["vaa"]; | ||
|
|
||
| // assertVaaPayloadEquals(vaa, payload); | ||
|
|
||
| return await this.submitVaa(vaa); | ||
| } | ||
|
|
||
| /* | ||
| public async run(cache: Record<string,any>): Promise<boolean> { | ||
| if (cache["multisigTx"] === undefined) { | ||
| // Have not yet submitted this operation to the multisig. | ||
| const payload = this.instruction.serialize(); | ||
| const txKey = vault.sendWormholeInstruction(payload); | ||
| cache["multisigTx"] = txKey; | ||
| return false; | ||
| } | ||
| if (cache["vaa"] === undefined) { | ||
| const vaa = await executeMultisigTxAndGetVaa(txKey, payloadHex); | ||
| if (vaa === undefined) { | ||
| return false; | ||
| } | ||
| cache["vaa"] = vaa; | ||
| } | ||
| // VAA is guaranteed to be defined here | ||
| const vaa = cache["vaa"]; | ||
| assertVaaPayloadEquals(vaa, payload); | ||
| // await proxy.executeGovernanceInstruction("0x" + vaa); | ||
| await submitVaa(vaa); | ||
| } | ||
| */ | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think they should not be here and should live in their respective contract files.
| class Cache { | ||
| private path: string; | ||
|
|
||
| constructor(path: string) { | ||
| this.path = path; | ||
| } | ||
|
|
||
| private opFilePath(op: SyncOp): string { | ||
| return `${this.path}/${op.id()}.json`; | ||
| } | ||
|
|
||
| public readOpCache(op: SyncOp): Record<string, any> { | ||
| const path = this.opFilePath(op); | ||
| if (fs.existsSync(path)) { | ||
| return JSON.parse(fs.readFileSync(path).toString("utf-8")); | ||
| } else { | ||
| return {}; | ||
| } | ||
| } | ||
|
|
||
| public writeOpCache(op: SyncOp, cache: Record<string, any>) { | ||
| fs.writeFileSync(this.opFilePath(op), JSON.stringify(cache)); | ||
| } | ||
|
|
||
| public deleteCache(op: SyncOp) { | ||
| fs.rmSync(this.opFilePath(op)); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to move the cache to the common package. Ideally the CLI doesn't have any business logic and storage is kind of a business logic. Moving it there allows us to use it in other places if needed too.
| (options.network === undefined || | ||
| contract.networkId == options.network) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not contract.networkId === options.network (tripple eq + no undefined check)?
| .option("-n, --network <network-id>", "Find contracts on the given network") | ||
| .option("-a, --address <address>", "Find contracts with the given address") | ||
| .option("-t, --type <type-id>", "Find contracts of the given type") | ||
| .argument("<fields...>", "Fields to set on the given contracts") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think having the contract state written in a file is a better idea and easier to understand. Instead of set and get we can have a sync method that checks the written file is a match. The current way is prone to mistakes and requires looking at the source.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the design! Thanks!
It seems that this code is no utilizing what xc-admin is offering. I think as Guillermo said we can get use of xc-admin structures in the code and it will help with parsing the proposals and displaying them.
My general thinking is that the logics here should be moved to a separate package and only the wire format live in the xc-admin-common to have a better boundry.
I left some inline comments which are mostly my opinions about the structure of the code. I might be wrong but would be good if you address them in later PRs.
Lastly, another cool feature that is good if the CL offer is checking the balance of the our deployer key in different networks.
I've been experimenting with different ways to read / write the state of contracts, and I have an approach that seems reasonable. The idea is to make a collection of Contract objects, each of which represents a single deployed on-chain contract. There can be separate types of Contracts to represent different types of programs (target chain vs. oracle program vs. ...). Each contract exposes its state as a json object which can be both read & written. You can then update contract state by manipulating the JSON object and calling the write method. The JSON interface makes this approach very flexible, as we can change its schema to fit whatever kind of contract we have.
Writing the state back to the chain is a little complicated because we need to run operations that pause and wait for external input (e.g., multisig approvals). I've implemented this using a filesystem dependency, which is not perfect, but is how we've solved this problem in all of our previous implementations. I think we can circle back and fix that dependency later by exposing sufficient APIs on all of our various components to let this code directly query the real world state.
There's also a CLI that lets you query and update the state. The upshot of all of this is that you'll be able to make a change to a set of contracts by running a command like:
npm run cli -- set --network avalanche --type evmTargetChain validTimePeriod=30then approving the multisig tx and rerunning the above command from the same computer. You can rerun the command whenever and it'll do something sane (e.g., if you approve some proposals but not others, it'll submit the approved changes and wait for the remaining pending changes).
There's still a lot of work to do to finish this up, but I wanted to share to get feedback on the general idea. I'll fix things up in smaller changes as we go.