Skip to content

Commit 6c52eb6

Browse files
authored
Contract manager upgrades (#952)
* Reuse xc_governance logic as much as possible in contract manager Some functions in xc_governance were moved and refactored in order to become usable in contract manager * Add getBaseUpdateFee function for contract manager * Add method for executeGovernanceInstructions * Move up SetFee method to base class * Add governance upgrade instruction * Move governance payload generators out of contract classes into chain classes * Switch from json to yaml for storage * Remove test script for ci * Add minimal aptos implementation * Remove global Chains and Contracts variable and put them in DefaultStore * Move aptos getClient function to Chain class * Make denom field in baseUpdateFee optional and remove it from non-cosmwasm chains * More documentation and minor fixes * Add vaults storage Although the set of vaults used in testing/production is just 2 it's a good idea to not set them as predefined constants. So that for development purposes, we can create new vaults and test them on the fly without changing too many places.
1 parent ea8b512 commit 6c52eb6

File tree

67 files changed

+2000
-538
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+2000
-538
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import yargs from "yargs";
2+
import { hideBin } from "yargs/helpers";
3+
import { CosmWasmChain } from "../src/chains";
4+
import { CosmWasmContract } from "../src/cosmwasm";
5+
import { DefaultStore } from "../src/store";
6+
7+
const parser = yargs(hideBin(process.argv))
8+
.scriptName("deploy_cosmwasm.ts")
9+
.usage(
10+
"Usage: $0 --code <path/to/artifact.wasm> --mnemonic <mnemonic> --chain <chain>"
11+
)
12+
.options({
13+
code: {
14+
type: "string",
15+
demandOption: true,
16+
desc: "Path to the artifact .wasm file",
17+
},
18+
mnemonic: {
19+
type: "string",
20+
demandOption: true,
21+
desc: "Mnemonic to use for the deployment",
22+
},
23+
chain: {
24+
type: "string",
25+
demandOption: true,
26+
desc: "Chain to upload the code on. Can be one of the chains available in the store",
27+
},
28+
wormholeContract: {
29+
type: "string",
30+
demandOption: true,
31+
desc: "Wormhole contract address deployed on this chain",
32+
},
33+
});
34+
35+
async function main() {
36+
const argv = await parser.argv;
37+
const { code, wormholeContract } = argv;
38+
console.log(
39+
await CosmWasmContract.deploy(
40+
DefaultStore.chains[argv.chain] as CosmWasmChain,
41+
wormholeContract,
42+
argv.mnemonic,
43+
code
44+
)
45+
);
46+
}
47+
48+
main();

contract_manager/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"private": true,
66
"main": "index.js",
77
"scripts": {
8-
"test": "echo \"Error: no test specified\" && exit 1",
98
"shell": "ts-node ./src/shell.ts"
109
},
1110
"author": "",
@@ -15,10 +14,11 @@
1514
"url": "git+https://github.com/pyth-network/pyth-crosschain.git"
1615
},
1716
"dependencies": {
17+
"@certusone/wormhole-sdk": "^0.9.8",
1818
"@pythnetwork/cosmwasm-deploy-tools": "*",
1919
"@pythnetwork/price-service-client": "*",
2020
"@pythnetwork/xc-governance-sdk": "*",
21-
"@certusone/wormhole-sdk": "^0.9.8",
21+
"bs58": "^5.0.0",
2222
"ts-node": "^10.9.1",
2323
"typescript": "^4.9.3"
2424
},

contract_manager/src/aptos.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { Contract } from "./base";
2+
import { AptosChain, Chain } from "./chains";
3+
import { DataSource, HexString32Bytes } from "@pythnetwork/xc-governance-sdk";
4+
import { AptosClient } from "aptos";
5+
6+
export class AptosContract extends Contract {
7+
static type: string = "AptosContract";
8+
9+
/**
10+
* Given the ids of the pyth state and wormhole state, create a new AptosContract
11+
* The package ids are derived based on the state ids
12+
*
13+
* @param chain the chain which this contract is deployed on
14+
* @param stateId id of the pyth state for the deployed contract
15+
* @param wormholeStateId id of the wormhole state for the wormhole contract that pyth binds to
16+
*/
17+
constructor(
18+
public chain: AptosChain,
19+
public stateId: string,
20+
public wormholeStateId: string
21+
) {
22+
super();
23+
}
24+
25+
static fromJson(chain: Chain, parsed: any): AptosContract {
26+
if (parsed.type !== AptosContract.type) throw new Error("Invalid type");
27+
if (!(chain instanceof AptosChain))
28+
throw new Error(`Wrong chain type ${chain}`);
29+
return new AptosContract(chain, parsed.stateId, parsed.wormholeStateId);
30+
}
31+
32+
executeGovernanceInstruction(sender: any, vaa: Buffer): Promise<any> {
33+
throw new Error("Method not implemented.");
34+
}
35+
36+
getStateResources() {
37+
const client = this.chain.getClient();
38+
return client.getAccountResources(this.stateId);
39+
}
40+
41+
/**
42+
* Returns the first occurrence of a resource with the given type in the pyth package state
43+
* @param type
44+
*/
45+
async findResource(type: string) {
46+
const resources = await this.getStateResources();
47+
for (const resource of resources) {
48+
if (resource.type === `${this.stateId}::state::${type}`) {
49+
return resource.data;
50+
}
51+
}
52+
throw new Error(`${type} resource not found in state ${this.stateId}`);
53+
}
54+
55+
async getBaseUpdateFee() {
56+
const data = (await this.findResource("BaseUpdateFee")) as any;
57+
return { amount: data.fee };
58+
}
59+
60+
getChain(): AptosChain {
61+
return this.chain;
62+
}
63+
64+
async getDataSources(): Promise<DataSource[]> {
65+
const data = (await this.findResource("DataSources")) as any;
66+
return data.sources.keys.map((source: any) => {
67+
return new DataSource(
68+
Number(source.emitter_chain),
69+
new HexString32Bytes(source.emitter_address.external_address)
70+
);
71+
});
72+
}
73+
74+
async getGovernanceDataSource(): Promise<DataSource> {
75+
const data = (await this.findResource("GovernanceDataSource")) as any;
76+
return new DataSource(
77+
Number(data.source.emitter_chain),
78+
new HexString32Bytes(data.source.emitter_address.external_address)
79+
);
80+
}
81+
82+
getId(): string {
83+
return `${this.chain.getId()}_${this.stateId}`;
84+
}
85+
86+
getType(): string {
87+
return AptosContract.type;
88+
}
89+
90+
async getValidTimePeriod() {
91+
const data = (await this.findResource("StalePriceThreshold")) as any;
92+
return Number(data.threshold_secs);
93+
}
94+
95+
toJson() {
96+
return {
97+
chain: this.chain.id,
98+
stateId: this.stateId,
99+
wormholeStateId: this.wormholeStateId,
100+
type: AptosContract.type,
101+
};
102+
}
103+
}

contract_manager/src/base.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { DataSource, HexString32Bytes } from "@pythnetwork/xc-governance-sdk";
1+
import {
2+
CHAINS,
3+
DataSource,
4+
HexString32Bytes,
5+
SetFeeInstruction,
6+
} from "@pythnetwork/xc-governance-sdk";
7+
import { Chain } from "./chains";
28

39
export abstract class Storable {
410
/**
@@ -25,11 +31,29 @@ export abstract class Contract extends Storable {
2531
*/
2632
abstract getValidTimePeriod(): Promise<number>;
2733

34+
/**
35+
* Returns the chain that this contract is deployed on
36+
*/
37+
abstract getChain(): Chain;
38+
2839
/**
2940
* Returns an array of data sources that this contract accepts price feed messages from
3041
*/
3142
abstract getDataSources(): Promise<DataSource[]>;
3243

44+
/**
45+
* Returns the base update fee for this contract
46+
* This is the required fee for updating the price feeds in the contract
47+
*/
48+
abstract getBaseUpdateFee(): Promise<{ amount: string; denom?: string }>;
49+
50+
/**
51+
* Executes the governance instruction contained in the VAA using the sender credentials
52+
* @param sender based on the contract type, this can be a private key, a mnemonic, a wallet, etc.
53+
* @param vaa the VAA to execute
54+
*/
55+
abstract executeGovernanceInstruction(sender: any, vaa: Buffer): Promise<any>;
56+
3357
/**
3458
* Returns the single data source that this contract accepts governance messages from
3559
*/

contract_manager/src/chains.ts

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
import { readdirSync, readFileSync, writeFileSync } from "fs";
22
import { Storable } from "./base";
3+
import {
4+
CHAINS,
5+
CosmwasmUpgradeContractInstruction,
6+
EthereumUpgradeContractInstruction,
7+
HexString20Bytes,
8+
HexString32Bytes,
9+
SetFeeInstruction,
10+
SuiAuthorizeUpgradeContractInstruction,
11+
} from "@pythnetwork/xc-governance-sdk";
12+
import { BufferBuilder } from "@pythnetwork/xc-governance-sdk/lib/serialize";
13+
import { AptosClient } from "aptos";
314

415
export abstract class Chain extends Storable {
516
protected constructor(public id: string) {
@@ -9,6 +20,25 @@ export abstract class Chain extends Storable {
920
getId(): string {
1021
return this.id;
1122
}
23+
24+
/**
25+
* Returns the payload for a governance SetFee instruction for contracts deployed on this chain
26+
* @param fee the new fee to set
27+
* @param exponent the new fee exponent to set
28+
*/
29+
generateGovernanceSetFeePayload(fee: number, exponent: number): Buffer {
30+
return new SetFeeInstruction(
31+
CHAINS[this.getId() as keyof typeof CHAINS],
32+
BigInt(fee),
33+
BigInt(exponent)
34+
).serialize();
35+
}
36+
37+
/**
38+
* Returns the payload for a governance contract upgrade instruction for contracts deployed on this chain
39+
* @param upgradeInfo based on the contract type, this can be a contract address, codeId, package digest, etc.
40+
*/
41+
abstract generateGovernanceUpgradePayload(upgradeInfo: any): Buffer;
1242
}
1343

1444
export class CosmWasmChain extends Chain {
@@ -52,6 +82,13 @@ export class CosmWasmChain extends Chain {
5282
getType(): string {
5383
return CosmWasmChain.type;
5484
}
85+
86+
generateGovernanceUpgradePayload(codeId: bigint): Buffer {
87+
return new CosmwasmUpgradeContractInstruction(
88+
CHAINS[this.getId() as keyof typeof CHAINS],
89+
codeId
90+
).serialize();
91+
}
5592
}
5693

5794
export class SuiChain extends Chain {
@@ -77,6 +114,40 @@ export class SuiChain extends Chain {
77114
getType(): string {
78115
return SuiChain.type;
79116
}
117+
118+
private wrapWithWormholeGovernancePayload(
119+
actionVariant: number,
120+
payload: Buffer
121+
): Buffer {
122+
const builder = new BufferBuilder();
123+
builder.addBuffer(
124+
Buffer.from(
125+
"0000000000000000000000000000000000000000000000000000000000000001",
126+
"hex"
127+
)
128+
);
129+
builder.addUint8(actionVariant);
130+
builder.addUint16(CHAINS["sui"]); // should always be sui (21) no matter devnet or testnet
131+
builder.addBuffer(payload);
132+
return builder.build();
133+
}
134+
135+
generateGovernanceUpgradePayload(digest: string): Buffer {
136+
let setFee = new SuiAuthorizeUpgradeContractInstruction(
137+
CHAINS["sui"],
138+
new HexString32Bytes(digest)
139+
).serialize();
140+
return this.wrapWithWormholeGovernancePayload(0, setFee);
141+
}
142+
143+
generateGovernanceSetFeePayload(fee: number, exponent: number): Buffer {
144+
let setFee = new SetFeeInstruction(
145+
CHAINS["sui"],
146+
BigInt(fee),
147+
BigInt(exponent)
148+
).serialize();
149+
return this.wrapWithWormholeGovernancePayload(3, setFee);
150+
}
80151
}
81152

82153
export class EVMChain extends Chain {
@@ -86,11 +157,18 @@ export class EVMChain extends Chain {
86157
super(id);
87158
}
88159

89-
static fromJson(parsed: any): SuiChain {
160+
static fromJson(parsed: any): EVMChain {
90161
if (parsed.type !== EVMChain.type) throw new Error("Invalid type");
91162
return new EVMChain(parsed.id, parsed.rpcUrl);
92163
}
93164

165+
generateGovernanceUpgradePayload(address: HexString20Bytes): Buffer {
166+
return new EthereumUpgradeContractInstruction(
167+
CHAINS[this.getId() as keyof typeof CHAINS],
168+
address
169+
).serialize();
170+
}
171+
94172
toJson(): any {
95173
return {
96174
id: this.id,
@@ -104,4 +182,46 @@ export class EVMChain extends Chain {
104182
}
105183
}
106184

107-
export const Chains: Record<string, Chain> = {};
185+
export class AptosChain extends Chain {
186+
static type = "AptosChain";
187+
188+
constructor(id: string, public rpcUrl: string) {
189+
super(id);
190+
}
191+
192+
getClient(): AptosClient {
193+
return new AptosClient(this.rpcUrl);
194+
}
195+
196+
generateGovernanceUpgradePayload(digest: string): Buffer {
197+
return new SuiAuthorizeUpgradeContractInstruction(
198+
CHAINS["aptos"],
199+
new HexString32Bytes(digest)
200+
).serialize();
201+
}
202+
203+
generateGovernanceSetFeePayload(fee: number, exponent: number): Buffer {
204+
return new SetFeeInstruction(
205+
CHAINS["aptos"], // should always be aptos (22) no matter devnet or testnet or mainnet
206+
BigInt(fee),
207+
BigInt(exponent)
208+
).serialize();
209+
}
210+
211+
getType(): string {
212+
return AptosChain.type;
213+
}
214+
215+
toJson(): any {
216+
return {
217+
id: this.id,
218+
rpcUrl: this.rpcUrl,
219+
type: AptosChain.type,
220+
};
221+
}
222+
223+
static fromJson(parsed: any): AptosChain {
224+
if (parsed.type !== AptosChain.type) throw new Error("Invalid type");
225+
return new AptosChain(parsed.id, parsed.rpcUrl);
226+
}
227+
}

0 commit comments

Comments
 (0)