Implement PythGovernanceAction (#906)
* blah * rename workflow * grrr * ok progress * ok progress * ok progress * grrr * ok * fix * fix layout
This commit is contained in:
parent
52ae0b853a
commit
78917f6d65
|
@ -1,4 +1,4 @@
|
|||
name: Build and Push Cross Chain Admin Frontend
|
||||
name: xc_admin_frontend Docker Image
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue