Implement PythGovernanceAction (#906)

* blah

* rename workflow

* grrr

* ok progress

* ok progress

* ok progress

* grrr

* ok

* fix

* fix layout
This commit is contained in:
Jayant Krishnamurthy 2023-07-05 08:35:47 -07:00 committed by GitHub
parent 52ae0b853a
commit 78917f6d65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 832 additions and 181 deletions

View File

@ -1,4 +1,4 @@
name: Build and Push Cross Chain Admin Frontend
name: xc_admin_frontend Docker Image
on:
pull_request:
push:

View File

@ -38,6 +38,7 @@
"@types/lodash": "^4.14.191",
"jest": "^29.3.1",
"prettier": "^2.8.1",
"ts-jest": "^29.0.3"
"ts-jest": "^29.0.3",
"fast-check": "^3.10.0"
}
}

View File

@ -1,5 +1,39 @@
import { PublicKey, SystemProgram } from "@solana/web3.js";
import { PythGovernanceHeader, ExecutePostedVaa } from "..";
import {
PythGovernanceHeader,
ExecutePostedVaa,
MODULES,
MODULE_EXECUTOR,
TargetAction,
ExecutorAction,
ActionName,
PythGovernanceAction,
decodeGovernancePayload,
} from "..";
import * as fc from "fast-check";
import {
ChainId,
ChainName,
CHAINS,
toChainId,
toChainName,
} from "@certusone/wormhole-sdk";
import { Arbitrary, IntArrayConstraints } from "fast-check";
import {
AptosAuthorizeUpgradeContract,
CosmosUpgradeContract,
EvmUpgradeContract,
} from "../governance_payload/UpgradeContract";
import {
AuthorizeGovernanceDataSourceTransfer,
RequestGovernanceDataSourceTransfer,
} from "../governance_payload/GovernanceDataSourceTransfer";
import { SetFee } from "../governance_payload/SetFee";
import { SetValidPeriod } from "../governance_payload/SetValidPeriod";
import {
DataSource,
SetDataSources,
} from "../governance_payload/SetDataSources";
test("GovernancePayload ser/de", (done) => {
jest.setTimeout(60000);
@ -121,3 +155,142 @@ test("GovernancePayload ser/de", (done) => {
done();
});
/** Fastcheck generator for arbitrary PythGovernanceHeaders */
function governanceHeaderArb(): Arbitrary<PythGovernanceHeader> {
const actions = [
...Object.keys(ExecutorAction),
...Object.keys(TargetAction),
] as ActionName[];
const actionArb = fc.constantFrom(...actions);
const targetChainIdArb = fc.constantFrom(
...(Object.keys(CHAINS) as ChainName[])
);
return actionArb.chain((action) => {
return targetChainIdArb.chain((chainId) => {
return fc.constant(new PythGovernanceHeader(chainId, action));
});
});
}
/** Fastcheck generator for arbitrary Buffers */
function bufferArb(constraints?: IntArrayConstraints): Arbitrary<Buffer> {
return fc.uint8Array(constraints).map((a) => Buffer.from(a));
}
/** Fastcheck generator for a uint of numBits bits. Warning: don't pass numBits > float precision */
function uintArb(numBits: number): Arbitrary<number> {
return fc.bigUintN(numBits).map((x) => Number.parseInt(x.toString()));
}
/** Fastcheck generator for a byte array encoded as a hex string. */
function hexBytesArb(constraints?: IntArrayConstraints): Arbitrary<string> {
return fc.uint8Array(constraints).map((a) => Buffer.from(a).toString("hex"));
}
function dataSourceArb(): Arbitrary<DataSource> {
return fc.record({
emitterChain: uintArb(16),
emitterAddress: hexBytesArb({ minLength: 32, maxLength: 32 }),
});
}
/**
* Fastcheck generator for arbitrary PythGovernanceActions.
*
* Note that this generator doesn't generate ExecutePostedVaa instruction payloads because they're hard to generate.
*/
function governanceActionArb(): Arbitrary<PythGovernanceAction> {
return governanceHeaderArb().chain<PythGovernanceAction>((header) => {
if (header.action === "ExecutePostedVaa") {
// NOTE: the instructions field is hard to generatively test, so we're using the hardcoded
// tests above instead.
return fc.constant(new ExecutePostedVaa(header.targetChainId, []));
} else if (header.action === "UpgradeContract") {
const cosmosArb = fc.bigUintN(64).map((codeId) => {
return new CosmosUpgradeContract(header.targetChainId, codeId);
});
const aptosArb = hexBytesArb({ minLength: 32, maxLength: 32 }).map(
(buffer) => {
return new AptosAuthorizeUpgradeContract(
header.targetChainId,
buffer
);
}
);
const evmArb = hexBytesArb({ minLength: 20, maxLength: 20 }).map(
(address) => {
return new EvmUpgradeContract(header.targetChainId, address);
}
);
return fc.oneof(cosmosArb, aptosArb, evmArb);
} else if (header.action === "AuthorizeGovernanceDataSourceTransfer") {
return bufferArb().map((claimVaa) => {
return new AuthorizeGovernanceDataSourceTransfer(
header.targetChainId,
claimVaa
);
});
} else if (header.action === "SetDataSources") {
return fc.array(dataSourceArb()).map((dataSources) => {
return new SetDataSources(header.targetChainId, dataSources);
});
} else if (header.action === "SetFee") {
return fc
.record({ v: fc.bigUintN(64), e: fc.bigUintN(64) })
.map(({ v, e }) => {
return new SetFee(header.targetChainId, v, e);
});
} else if (header.action === "SetValidPeriod") {
return fc.bigUintN(64).map((period) => {
return new SetValidPeriod(header.targetChainId, period);
});
} else if (header.action === "RequestGovernanceDataSourceTransfer") {
return fc.bigUintN(32).map((index) => {
return new RequestGovernanceDataSourceTransfer(
header.targetChainId,
parseInt(index.toString())
);
});
} else {
throw new Error("Unsupported action type");
}
});
}
test("Header serialization round-trip test", (done) => {
fc.assert(
fc.property(governanceHeaderArb(), (original) => {
const decoded = PythGovernanceHeader.decode(original.encode());
if (decoded === undefined) {
return false;
}
return (
decoded.action === original.action &&
decoded.targetChainId === original.targetChainId
);
})
);
done();
});
test("Governance action serialization round-trip test", (done) => {
fc.assert(
fc.property(governanceActionArb(), (original) => {
const encoded = original.encode();
const decoded = decodeGovernancePayload(encoded);
if (decoded === undefined) {
return false;
}
// TODO: not sure if i love this test.
return decoded.encode().equals(original.encode());
})
);
done();
});

View File

@ -0,0 +1,51 @@
import { Layout } from "@solana/buffer-layout";
export class UInt64BE extends Layout<bigint> {
constructor(span: number, property?: string) {
super(span, property);
}
override decode(b: Uint8Array, offset?: number): bigint {
let o = offset ?? 0;
return Buffer.from(b.slice(o, o + this.span)).readBigUInt64BE();
}
override encode(src: bigint, b: Uint8Array, offset?: number): number {
const buffer = Buffer.alloc(this.span);
buffer.writeBigUint64BE(src);
b.set(buffer, offset);
return this.span;
}
}
export class HexBytes extends Layout<string> {
// span is the number of bytes to read
constructor(span: number, property?: string) {
super(span, property);
}
override decode(b: Uint8Array, offset?: number): string {
let o = offset ?? 0;
return Buffer.from(b.slice(o, o + this.span)).toString("hex");
}
override encode(src: string, b: Uint8Array, offset?: number): number {
const buffer = Buffer.alloc(this.span);
buffer.write(src, "hex");
b.set(buffer, offset);
return this.span;
}
}
/** A big-endian u64, returned as a bigint. */
export function u64be(property?: string | undefined): UInt64BE {
return new UInt64BE(8, property);
}
/** An array of numBytes bytes, returned as a hexadecimal string. */
export function hexBytes(
numBytes: number,
property?: string | undefined
): HexBytes {
return new HexBytes(numBytes, property);
}

View File

@ -0,0 +1,84 @@
import {
ActionName,
PythGovernanceAction,
PythGovernanceActionImpl,
PythGovernanceHeader,
} from "./PythGovernanceAction";
import * as BufferLayout from "@solana/buffer-layout";
import { ChainName } from "@certusone/wormhole-sdk";
/**
* Authorize transferring the governance data source from the sender's emitter address to another emitter.
* The receiving emitter address is the sender of claimVaa, which must be a RequestGovernanceDataSourceTransfer message.
*/
export class AuthorizeGovernanceDataSourceTransfer
implements PythGovernanceAction
{
readonly actionName: ActionName;
readonly claimVaa: Buffer;
constructor(readonly targetChainId: ChainName, vaa: Buffer) {
this.actionName = "AuthorizeGovernanceDataSourceTransfer";
this.claimVaa = new Buffer(vaa);
}
static decode(
data: Buffer
): AuthorizeGovernanceDataSourceTransfer | undefined {
const header = PythGovernanceHeader.decode(data);
if (!header || header.action !== "AuthorizeGovernanceDataSourceTransfer") {
return undefined;
}
const payload = data.subarray(PythGovernanceHeader.span, data.length);
return new AuthorizeGovernanceDataSourceTransfer(
header.targetChainId,
payload
);
}
encode(): Buffer {
const headerBuffer = new PythGovernanceHeader(
this.targetChainId,
this.actionName
).encode();
return Buffer.concat([headerBuffer, this.claimVaa]);
}
}
/**
* Request a transfer of the governance data source to the emitter of this message.
*/
export class RequestGovernanceDataSourceTransfer extends PythGovernanceActionImpl {
static layout: BufferLayout.Structure<
Readonly<{ governanceDataSourceIndex: number }>
> = BufferLayout.struct([BufferLayout.u32be()]);
constructor(
targetChainId: ChainName,
readonly governanceDataSourceIndex: number
) {
super(targetChainId, "RequestGovernanceDataSourceTransfer");
}
static decode(data: Buffer): RequestGovernanceDataSourceTransfer | undefined {
const decoded = PythGovernanceActionImpl.decodeWithPayload(
data,
"RequestGovernanceDataSourceTransfer",
RequestGovernanceDataSourceTransfer.layout
);
if (!decoded) return undefined;
return new RequestGovernanceDataSourceTransfer(
decoded[0].targetChainId,
decoded[1].governanceDataSourceIndex
);
}
encode(): Buffer {
return super.encodeWithPayload(RequestGovernanceDataSourceTransfer.layout, {
governanceDataSourceIndex: this.governanceDataSourceIndex,
});
}
}

View File

@ -0,0 +1,198 @@
import {
ChainId,
ChainName,
toChainId,
toChainName,
} from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout";
import { PACKET_DATA_SIZE } from "@solana/web3.js";
/** Each of the actions that can be directed to the Executor Module */
export const ExecutorAction = {
ExecutePostedVaa: 0,
} as const;
export const TargetAction = {
UpgradeContract: 0,
AuthorizeGovernanceDataSourceTransfer: 1,
SetDataSources: 2,
SetFee: 3,
SetValidPeriod: 4,
RequestGovernanceDataSourceTransfer: 5,
} as const;
/** Helper to get the ActionName from a (moduleId, actionId) tuple*/
export function toActionName(
deserialized: Readonly<{ moduleId: number; actionId: number }>
): ActionName | undefined {
if (deserialized.moduleId == MODULE_EXECUTOR && deserialized.actionId == 0) {
return "ExecutePostedVaa";
} else if (deserialized.moduleId == MODULE_TARGET) {
switch (deserialized.actionId) {
case 0:
return "UpgradeContract";
case 1:
return "AuthorizeGovernanceDataSourceTransfer";
case 2:
return "SetDataSources";
case 3:
return "SetFee";
case 4:
return "SetValidPeriod";
case 5:
return "RequestGovernanceDataSourceTransfer";
}
}
return undefined;
}
export declare type ActionName =
| keyof typeof ExecutorAction
| keyof typeof TargetAction;
/** Governance header that should be in every Pyth crosschain governance message*/
export class PythGovernanceHeader {
readonly targetChainId: ChainName;
readonly action: ActionName;
static layout: BufferLayout.Structure<
Readonly<{
magicNumber: number;
module: number;
action: number;
chain: ChainId;
}>
> = BufferLayout.struct(
[
BufferLayout.u32("magicNumber"),
BufferLayout.u8("module"),
BufferLayout.u8("action"),
BufferLayout.u16be("chain"),
],
"header"
);
/** Span of the serialized governance header */
static span = 8;
constructor(targetChainId: ChainName, action: ActionName) {
this.targetChainId = targetChainId;
this.action = action;
}
/** Decode Pyth Governance Header */
static decode(data: Buffer): PythGovernanceHeader | undefined {
const deserialized = safeLayoutDecode(this.layout, data);
if (!deserialized) return undefined;
if (deserialized.magicNumber !== MAGIC_NUMBER) return undefined;
if (!toChainName(deserialized.chain)) return undefined;
const actionName = toActionName({
actionId: deserialized.action,
moduleId: deserialized.module,
});
if (actionName) {
return new PythGovernanceHeader(
toChainName(deserialized.chain),
actionName
);
} else {
return undefined;
}
}
/** Encode Pyth Governance Header */
encode(): Buffer {
// The code will crash if the payload is actually bigger than PACKET_DATA_SIZE. But PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload should never be bigger than this anyway
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
let module: number;
let action: number;
if (this.action in ExecutorAction) {
module = MODULE_EXECUTOR;
action = ExecutorAction[this.action as keyof typeof ExecutorAction];
} else {
module = MODULE_TARGET;
action = TargetAction[this.action as keyof typeof TargetAction];
}
const span = PythGovernanceHeader.layout.encode(
{
magicNumber: MAGIC_NUMBER,
module,
action,
chain: toChainId(this.targetChainId),
},
buffer
);
return buffer.subarray(0, span);
}
}
export const MAGIC_NUMBER = 0x4d475450;
export const MODULE_EXECUTOR = 0;
export const MODULE_TARGET = 1;
export const MODULES = [MODULE_EXECUTOR, MODULE_TARGET];
export interface PythGovernanceAction {
readonly targetChainId: ChainName;
encode(): Buffer;
}
/** Helper class for implementing PythGovernanceAction using a BufferLayout.Layout for the payload. */
export abstract class PythGovernanceActionImpl implements PythGovernanceAction {
readonly targetChainId: ChainName;
readonly action: ActionName;
protected constructor(targetChainId: ChainName, action: ActionName) {
this.targetChainId = targetChainId;
this.action = action;
}
abstract encode(): Buffer;
protected header(): PythGovernanceHeader {
return new PythGovernanceHeader(this.targetChainId, this.action);
}
/** Encode this action as a buffer with the given payload (encoded using the given layout). */
protected encodeWithPayload<T>(
payloadLayout: BufferLayout.Layout<T>,
payload: T
): Buffer {
const headerBuffer = this.header().encode();
const payloadBuffer = Buffer.alloc(payloadLayout.span);
payloadLayout.encode(payload, payloadBuffer);
return Buffer.concat([headerBuffer, payloadBuffer]);
}
/** Decode this action from a buffer using the given layout for the payload. */
protected static decodeWithPayload<T>(
buffer: Buffer,
requiredAction: ActionName,
payloadLayout: BufferLayout.Layout<T>
): [PythGovernanceHeader, T] | undefined {
const header = PythGovernanceHeader.decode(buffer);
if (!header || header.action !== requiredAction) return undefined;
const payload = safeLayoutDecode(
payloadLayout,
buffer.subarray(PythGovernanceHeader.span, buffer.length)
);
if (!payload) return undefined;
return [header, payload];
}
}
export function safeLayoutDecode<T>(
layout: BufferLayout.Layout<T>,
data: Buffer
): T | undefined {
try {
return layout.decode(data);
} catch {
return undefined;
}
}

View File

@ -0,0 +1,68 @@
import {
ActionName,
PythGovernanceAction,
PythGovernanceActionImpl,
PythGovernanceHeader,
} from "./PythGovernanceAction";
import { ChainName } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout";
import * as BufferLayoutExt from "./BufferLayoutExt";
/** A data source is a wormhole emitter, i.e., a specific contract on a specific chain. */
export interface DataSource {
emitterChain: number;
emitterAddress: string;
}
const DataSourceLayout: BufferLayout.Structure<DataSource> =
BufferLayout.struct([
BufferLayout.u16be("emitterChain"),
BufferLayoutExt.hexBytes(32, "emitterAddress"),
]);
/** Set the data sources (where price updates must come from) on targetChainId to the provided values. */
export class SetDataSources implements PythGovernanceAction {
readonly actionName: ActionName;
constructor(
readonly targetChainId: ChainName,
readonly dataSources: DataSource[]
) {
this.actionName = "SetDataSources";
}
static decode(data: Buffer): SetDataSources | undefined {
const header = PythGovernanceHeader.decode(data);
if (!header || header.action !== "SetDataSources") {
return undefined;
}
let index = PythGovernanceHeader.span;
const numSources = BufferLayout.u8().decode(data, index);
index += 1;
const dataSources = [];
for (let i = 0; i < numSources; i++) {
dataSources.push(DataSourceLayout.decode(data, index));
index += DataSourceLayout.span;
}
return new SetDataSources(header.targetChainId, dataSources);
}
encode(): Buffer {
const headerBuffer = new PythGovernanceHeader(
this.targetChainId,
"SetDataSources"
).encode();
const numSourcesBuf = Buffer.alloc(1);
BufferLayout.u8().encode(this.dataSources.length, numSourcesBuf);
const dataSourceBufs = this.dataSources.map((source) => {
const buf = Buffer.alloc(DataSourceLayout.span);
DataSourceLayout.encode(source, buf);
return buf;
});
return Buffer.concat([headerBuffer, numSourcesBuf, ...dataSourceBufs]);
}
}

View File

@ -0,0 +1,44 @@
import { PythGovernanceActionImpl } from "./PythGovernanceAction";
import * as BufferLayout from "@solana/buffer-layout";
import * as BufferLayoutExt from "./BufferLayoutExt";
import { ChainName } from "@certusone/wormhole-sdk";
/** Set the fee on the target chain to newFeeValue * 10^newFeeExpo */
export class SetFee extends PythGovernanceActionImpl {
static layout: BufferLayout.Structure<
Readonly<{ newFeeValue: bigint; newFeeExpo: bigint }>
> = BufferLayout.struct([
BufferLayoutExt.u64be("newFeeValue"),
BufferLayoutExt.u64be("newFeeExpo"),
]);
constructor(
targetChainId: ChainName,
readonly newFeeValue: bigint,
readonly newFeeExpo: bigint
) {
super(targetChainId, "SetFee");
}
static decode(data: Buffer): SetFee | undefined {
const decoded = PythGovernanceActionImpl.decodeWithPayload(
data,
"SetFee",
SetFee.layout
);
if (!decoded) return undefined;
return new SetFee(
decoded[0].targetChainId,
decoded[1].newFeeValue,
decoded[1].newFeeExpo
);
}
encode(): Buffer {
return super.encodeWithPayload(SetFee.layout, {
newFeeValue: this.newFeeValue,
newFeeExpo: this.newFeeExpo,
});
}
}

View File

@ -0,0 +1,37 @@
import {
PythGovernanceActionImpl,
PythGovernanceHeader,
} from "./PythGovernanceAction";
import * as BufferLayout from "@solana/buffer-layout";
import * as BufferLayoutExt from "./BufferLayoutExt";
import { ChainName } from "@certusone/wormhole-sdk";
/** Set the valid period (the default amount of time in which prices are considered fresh) to the provided value */
export class SetValidPeriod extends PythGovernanceActionImpl {
static layout: BufferLayout.Structure<Readonly<{ newValidPeriod: bigint }>> =
BufferLayout.struct([BufferLayoutExt.u64be("newValidPeriod")]);
constructor(targetChainId: ChainName, readonly newValidPeriod: bigint) {
super(targetChainId, "SetValidPeriod");
}
static decode(data: Buffer): SetValidPeriod | undefined {
const decoded = PythGovernanceActionImpl.decodeWithPayload(
data,
"SetValidPeriod",
SetValidPeriod.layout
);
if (!decoded) return undefined;
return new SetValidPeriod(
decoded[0].targetChainId,
decoded[1].newValidPeriod
);
}
encode(): Buffer {
return super.encodeWithPayload(SetValidPeriod.layout, {
newValidPeriod: this.newValidPeriod,
});
}
}

View File

@ -1,38 +1,90 @@
import { ChainName } from "@certusone/wormhole-sdk";
import { PythGovernanceAction, PythGovernanceHeader } from ".";
import { PythGovernanceActionImpl } from "./PythGovernanceAction";
import * as BufferLayout from "@solana/buffer-layout";
import * as BufferLayoutExt from "./BufferLayoutExt";
export class CosmosUpgradeContract implements PythGovernanceAction {
readonly targetChainId: ChainName;
readonly codeId: bigint;
/** Upgrade a cosmos contract to the implementation at codeId. (Note that this requires someone to upload the new
* contract code first to obtain a codeId.) */
export class CosmosUpgradeContract extends PythGovernanceActionImpl {
static layout: BufferLayout.Structure<Readonly<{ codeId: bigint }>> =
BufferLayout.struct([BufferLayoutExt.u64be("codeId")]);
constructor(targetChainId: ChainName, codeId: bigint) {
this.targetChainId = targetChainId;
this.codeId = codeId;
constructor(targetChainId: ChainName, readonly codeId: bigint) {
super(targetChainId, "UpgradeContract");
}
static span: number = 8;
static decode(data: Buffer): CosmosUpgradeContract | undefined {
const header = PythGovernanceHeader.decode(data);
if (!header) return undefined;
const decoded = PythGovernanceActionImpl.decodeWithPayload(
data,
"UpgradeContract",
CosmosUpgradeContract.layout
);
if (!decoded) return undefined;
const codeId = data.subarray(PythGovernanceHeader.span).readBigUInt64BE();
if (!codeId) return undefined;
return new CosmosUpgradeContract(header.targetChainId, codeId);
return new CosmosUpgradeContract(
decoded[0].targetChainId,
decoded[1].codeId
);
}
/** Encode CosmosUpgradeContract */
encode(): Buffer {
const headerBuffer = new PythGovernanceHeader(
this.targetChainId,
"UpgradeContract"
).encode();
const buffer = Buffer.alloc(
PythGovernanceHeader.span + CosmosUpgradeContract.span
);
const span = buffer.writeBigUInt64BE(this.codeId);
return Buffer.concat([headerBuffer, buffer.subarray(0, span)]);
return super.encodeWithPayload(CosmosUpgradeContract.layout, {
codeId: this.codeId,
});
}
}
export class AptosAuthorizeUpgradeContract extends PythGovernanceActionImpl {
static layout: BufferLayout.Structure<Readonly<{ hash: string }>> =
BufferLayout.struct([BufferLayoutExt.hexBytes(32, "hash")]);
constructor(targetChainId: ChainName, readonly hash: string) {
super(targetChainId, "UpgradeContract");
}
static decode(data: Buffer): AptosAuthorizeUpgradeContract | undefined {
const decoded = PythGovernanceActionImpl.decodeWithPayload(
data,
"UpgradeContract",
this.layout
);
if (!decoded) return undefined;
return new AptosAuthorizeUpgradeContract(
decoded[0].targetChainId,
decoded[1].hash
);
}
encode(): Buffer {
return super.encodeWithPayload(AptosAuthorizeUpgradeContract.layout, {
hash: this.hash,
});
}
}
export class EvmUpgradeContract extends PythGovernanceActionImpl {
static layout: BufferLayout.Structure<Readonly<{ address: string }>> =
BufferLayout.struct([BufferLayoutExt.hexBytes(20, "address")]);
constructor(targetChainId: ChainName, readonly address: string) {
super(targetChainId, "UpgradeContract");
}
static decode(data: Buffer): EvmUpgradeContract | undefined {
const decoded = PythGovernanceActionImpl.decodeWithPayload(
data,
"UpgradeContract",
this.layout
);
if (!decoded) return undefined;
return new EvmUpgradeContract(decoded[0].targetChainId, decoded[1].address);
}
encode(): Buffer {
return super.encodeWithPayload(EvmUpgradeContract.layout, {
address: this.address,
});
}
}

View File

@ -1,143 +1,20 @@
import {
ChainId,
ChainName,
toChainId,
toChainName,
} from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout";
import { PACKET_DATA_SIZE } from "@solana/web3.js";
import { ExecutePostedVaa } from "./ExecutePostedVaa";
import { CosmosUpgradeContract } from "./UpgradeContract";
export interface PythGovernanceAction {
readonly targetChainId: ChainName;
encode(): Buffer;
}
/** Each of the actions that can be directed to the Executor Module */
export const ExecutorAction = {
ExecutePostedVaa: 0,
} as const;
export const TargetAction = {
UpgradeContract: 0,
AuthorizeGovernanceDataSourceTransfer: 1,
SetDataSources: 2,
SetFee: 3,
SetValidPeriod: 4,
RequestGovernanceDataSourceTransfer: 5,
} as const;
/** Helper to get the ActionName from a (moduleId, actionId) tuple*/
export function toActionName(
deserialized: Readonly<{ moduleId: number; actionId: number }>
): ActionName | undefined {
if (deserialized.moduleId == MODULE_EXECUTOR && deserialized.actionId == 0) {
return "ExecutePostedVaa";
} else if (deserialized.moduleId == MODULE_TARGET) {
switch (deserialized.actionId) {
case 0:
return "UpgradeContract";
case 1:
return "AuthorizeGovernanceDataSourceTransfer";
case 2:
return "SetDataSources";
case 3:
return "SetFee";
case 4:
return "SetValidPeriod";
case 5:
return "RequestGovernanceDataSourceTransfer";
}
}
return undefined;
}
export declare type ActionName =
| keyof typeof ExecutorAction
| keyof typeof TargetAction;
/** Governance header that should be in every Pyth crosschain governance message*/
export class PythGovernanceHeader {
readonly targetChainId: ChainName;
readonly action: ActionName;
static layout: BufferLayout.Structure<
Readonly<{
magicNumber: number;
module: number;
action: number;
chain: ChainId;
}>
> = BufferLayout.struct(
[
BufferLayout.u32("magicNumber"),
BufferLayout.u8("module"),
BufferLayout.u8("action"),
BufferLayout.u16be("chain"),
],
"header"
);
/** Span of the serialized governance header */
static span = 8;
constructor(targetChainId: ChainName, action: ActionName) {
this.targetChainId = targetChainId;
this.action = action;
}
/** Decode Pyth Governance Header */
static decode(data: Buffer): PythGovernanceHeader | undefined {
const deserialized = safeLayoutDecode(this.layout, data);
if (!deserialized) return undefined;
if (deserialized.magicNumber !== MAGIC_NUMBER) return undefined;
if (!toChainName(deserialized.chain)) return undefined;
const actionName = toActionName({
actionId: deserialized.action,
moduleId: deserialized.module,
});
if (actionName) {
return new PythGovernanceHeader(
toChainName(deserialized.chain),
actionName
);
} else {
return undefined;
}
}
/** Encode Pyth Governance Header */
encode(): Buffer {
// The code will crash if the payload is actually bigger than PACKET_DATA_SIZE. But PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload should never be bigger than this anyway
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
let module: number;
let action: number;
if (this.action in ExecutorAction) {
module = MODULE_EXECUTOR;
action = ExecutorAction[this.action as keyof typeof ExecutorAction];
} else {
module = MODULE_TARGET;
action = TargetAction[this.action as keyof typeof TargetAction];
}
const span = PythGovernanceHeader.layout.encode(
{
magicNumber: MAGIC_NUMBER,
module,
action,
chain: toChainId(this.targetChainId),
},
buffer
);
return buffer.subarray(0, span);
}
}
export const MAGIC_NUMBER = 0x4d475450;
export const MODULE_EXECUTOR = 0;
export const MODULE_TARGET = 1;
import {
AptosAuthorizeUpgradeContract,
CosmosUpgradeContract,
EvmUpgradeContract,
} from "./UpgradeContract";
import {
PythGovernanceAction,
PythGovernanceHeader,
} from "./PythGovernanceAction";
import {
AuthorizeGovernanceDataSourceTransfer,
RequestGovernanceDataSourceTransfer,
} from "./GovernanceDataSourceTransfer";
import { SetDataSources } from "./SetDataSources";
import { SetValidPeriod } from "./SetValidPeriod";
import { SetFee } from "./SetFee";
/** Decode a governance payload */
export function decodeGovernancePayload(
@ -150,22 +27,33 @@ export function decodeGovernancePayload(
case "ExecutePostedVaa":
return ExecutePostedVaa.decode(data);
case "UpgradeContract":
//TO DO : Support non-cosmos upgrades
return CosmosUpgradeContract.decode(data);
// NOTE: the only way to distinguish the different types of upgrade contract instructions
// is their payload lengths. We're getting a little lucky here that all of these upgrade instructions
// have different-length payloads.
const payloadLength = data.length - PythGovernanceHeader.span;
if (payloadLength == CosmosUpgradeContract.layout.span) {
return CosmosUpgradeContract.decode(data);
} else if (payloadLength == AptosAuthorizeUpgradeContract.layout.span) {
return AptosAuthorizeUpgradeContract.decode(data);
} else if (payloadLength == EvmUpgradeContract.layout.span) {
return EvmUpgradeContract.decode(data);
} else {
return undefined;
}
case "AuthorizeGovernanceDataSourceTransfer":
return AuthorizeGovernanceDataSourceTransfer.decode(data);
case "SetDataSources":
return SetDataSources.decode(data);
case "SetFee":
return SetFee.decode(data);
case "SetValidPeriod":
return SetValidPeriod.decode(data);
case "RequestGovernanceDataSourceTransfer":
return RequestGovernanceDataSourceTransfer.decode(data);
default:
return undefined;
}
}
export function safeLayoutDecode<T>(
layout: BufferLayout.Layout<T>,
data: Buffer
): T | undefined {
try {
return layout.decode(data);
} catch {
return undefined;
}
}
export { ExecutePostedVaa } from "./ExecutePostedVaa";
export * from "./PythGovernanceAction";

55
package-lock.json generated
View File

@ -1349,6 +1349,7 @@
"@types/bn.js": "^5.1.1",
"@types/jest": "^29.2.5",
"@types/lodash": "^4.14.191",
"fast-check": "^3.10.0",
"jest": "^29.3.1",
"prettier": "^2.8.1",
"ts-jest": "^29.0.3"
@ -1389,6 +1390,28 @@
"follow-redirects": "^1.14.4"
}
},
"governance/xc_admin/packages/xc_admin_common/node_modules/fast-check": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.10.0.tgz",
"integrity": "sha512-I2FldZwnCbcY6iL+H0rp9m4D+O3PotuFu9FasWjMCzUedYHMP89/37JbSt6/n7Yq/IZmJDW0B2h30sPYdzrfzw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"dependencies": {
"pure-rand": "^6.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"governance/xc_admin/packages/xc_admin_common/node_modules/prettier": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz",
@ -1404,6 +1427,22 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"governance/xc_admin/packages/xc_admin_common/node_modules/pure-rand": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz",
"integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
]
},
"governance/xc_admin/packages/xc_admin_frontend": {
"version": "0.1.0",
"dependencies": {
@ -106805,6 +106844,7 @@
"@types/jest": "^29.2.5",
"@types/lodash": "^4.14.191",
"ethers": "^5.7.2",
"fast-check": "^3.10.0",
"jest": "^29.3.1",
"lodash": "^4.17.21",
"prettier": "^2.8.1",
@ -106847,11 +106887,26 @@
"follow-redirects": "^1.14.4"
}
},
"fast-check": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.10.0.tgz",
"integrity": "sha512-I2FldZwnCbcY6iL+H0rp9m4D+O3PotuFu9FasWjMCzUedYHMP89/37JbSt6/n7Yq/IZmJDW0B2h30sPYdzrfzw==",
"dev": true,
"requires": {
"pure-rand": "^6.0.0"
}
},
"prettier": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz",
"integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==",
"dev": true
},
"pure-rand": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz",
"integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==",
"dev": true
}
}
},